第一章 Java程序设计概述
JAVA语言的关键术语:简单性、面向对象、分布式、健壮性、安全性、体系结构中立、可移植性、解释性、高性能、多线程和动态性。
程序设计语言的成功更多地取决于其支持系统的能力,而不是语法的精巧性。
第二章 Java编程环境
类库源代码在JDK中以压缩文件lib/src.zip
的形式发布,解压缩这个文件来得到源代码。
Java9引入了另一种使用Java的方法。JShell程序提供了一个“读取-评估-打印循环”。键入一个Java表达式,JShell会评估输入,打印结果,并等待下一个输入。要启动JShell,只需要在终端窗口键入jshell。
第三章 Java的基本程序设计结构
类是所有Java应用的构建模块。Java程序的所有内容都必须放在类中。
运行一个已编译的程序时,Java虚拟机总是从指定类的main方法的代码开始执行,因此为了能够执行代码,类的源代码中必须包含一个main方法。
在C和C++中,int和long等类型的大小与目标平台相关。在Java中,所有数值类型的大小都与平台无关。
注意,Java没有无符号(unsigned)形式的int、long、short或byte类型,但提供了方法使其互相转换。
Unicode转义字符会在解析代码之前处理.,因此要小心注释中的\u
。
强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元。最好将字符串作为抽象数据类型来处理。
从Java10开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不需要声明类型。只需要使用关键字var而无需指定类型:var num = 12
。
值得注意,整数除以0将产生一个异常,而浮点数被0除将会得到一个无穷大或者NaN类型。
如果得到一个可预测的结果比运行速度更重要的话,就应该使用StrictMath类,它实现了”可自由分发数学库“的算法,确保在所有平台上得到相同的结果。
Java14中引入Switch表达式,写法如下;
int num = switch(seasonName){
case "Spring","Summer","Winter" -> 6;
case "Fall" ->4;
default -> -1;
}
由于不能修改Java字符串中的单个字符,所有在Java文档中将String类对象称为是不可变的。缺点是降低了效率,优点是编译器可以让字符串共享。
如果虚拟机总是共享相等的字符串,则可以通过==
运算符检测字符串是否相等。但实际上只有字符串字面量会共享,而+
或substring等操作得到的字符串并不共享。因此千万不要使用==
运算符测试字符串的相等性。
如果想要频繁的拼接字符串,可以使用StringBuilder或StringBuffer类。
Java不允许在嵌套的两个块中声明同名的变量。
在循环中,检测两个浮点数是否相等需要格外小心。
在Java中,允许将一个数组变量拷贝到另一个数值变量。这时,两个变量将引用同一个数组。如果确实希望将一个数组的所有值拷贝到一个新的数组中,就要使用Arrays类中的copyOf
方法:int[] copied = Arrays.copyOf(old,olld.length)
。
第四章 对象与类
在Java中,任何对象变量的值都是一个引用,指向存储在另外一个地方的某个对象。new操作符的返回值也是一个引用。
注意不要编写返回可变对象引用的访问器方法,否则外部可能通过该引用修改值。如果需要返回一个可变对象的引用,首先应该对它进行克隆。
final关键字只是表示存储在该变量中的对象引用不会再指示另一个不同的对象,但对象本身可以更改。
原生访问可以绕过Java语言的访问控制机制。
Java程序设计语言总是采用按值调用。方法会得到所有参数值的一个副本。具体来说,方法不能修改传递给它的任何参数变量的内容。例子如下:
public static void swap(Employee x,Employee y){
Employee temp = x;
x = y;
y = temp;
}//并不能实现两者交换,改变的只是x和y的指向的引用
记录(record)是一种特殊形式的类,其状态不可变,而且公共可读。
静态导入:import语句允许导入静态方法和静态字段,而不只是类,例如import static java.lang.System.out;
。
JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。
对象的三个主要特性
-
对象的行为:可以对这个对象做哪些操作,或者可以对这个对象应用哪些方法?
-
对象的状态:调用方法时,对象会如何响应。
-
对象的标识:如何区分可能有相同行为和状态的不同对象。
Java处理方法参数
- 方法不能修改基本数据类型的参数。
- 方法可以改变对象参数的状态,因为副本和原数据所指向的引用相同,因此可以利用副本修改原数据的状态。
- 方法不能让一个对象参数引用一个新对象。
类设计技巧
- 一定要保证数据私有。这时最重要的,绝对不要破坏封装性。
- 一定要初始化数据。最好不要依赖于系统的初始值,而是应该显式地初始化所有变量。
- 不要在类中使用过多的数据类型。其想法是要用其他的类,而不是使用多个相关的基本类型。这样会使类更易于理解,也更易于修改。
- 不是所有的字段都需要单独的字段访问器和更改器。
- 分解有过多职责的类。
- 类名和方法名要能体现出他们的职责。
- 优先使用不可变的类。更改对象的问题在于,如果多个线程同时更新一个对象,就会发生并发更改,其结果是不可预料的。
第五章 继承
继承的基本思想是,可以基于已有的类创建新的类。
反射是指程序运行期间更多地了解类及其属性的能力。
“is-a"是继承的一个明显特征。
Java语言规范指出:“声明为私有的类成员不会被这个类的子类继承”。规范中狭义地使用了“继承”一词。它认为私有字段不会继承,因为子类不能自己访问这些私有字段。所有,每个子类对象有超类的字段,但是子类并没有“继承”这些字段。
super关键字不是一个对象的引用,super只是一个指示编译器调用超类方法的特殊关键字。
在Java中,子类引用数组可以转换成超类引用数组,而不需要强制类型转换。但是会导致子类的元素可以被赋值为超类的值,这显然不合适。因此所有数组都要牢记创建时的元素类型,并负责监督仅将类型兼容的引用存储到数组中。
返回类型不是签名的一部分。不过在覆盖一个方法时,需要保证返回类型的兼容性。允许子类将覆盖方法的返回类型改为原返回类型的子类型。
如果将一个类声明为final,只有其中的方法自动地成为final,而不包括字段。
只能在继承层次结构内使用强制类型转换。
Java要求equals()方法具有下列性质:自反性、对称性、传递性、一致性和若x非空,则x.equals(null)
为false。
打印数组可使用静态方法Arrays.toString(arr)
,想要正确的打印多维数组则需要调用Array.deepToString()
方法。
不要使用包装器类构造器,它们已被弃用,并将被完全删除。例如可以使用Integer.valueOf(100)
,而绝不要使用new Integer(100)
。或者可以依赖自动装箱:Integer a=100
。
抽象方法相当于子类中实现的具体方法的占位符。
在Java中,密封类会控制哪些类可以继承它。例如public abstract sealed class JSONValue permits JSONArray,JSONNumber,JSONString,JSONBoolean,JSONObject,JSONNull{}
。
non-sealed
关键字是第一个带连字符的Java关键字。
鉴于历史原因,getName方法对数组类型会返回有些奇怪的名字。
反射机制的默认行为受限于Java的访问控制。不过可以调用Field、Method或Constructor对象的setAccessible方法覆盖Java的访问控制。
invoke的参数和返回值必须是Object类型。这就意味着必须来回进行多次强制类型转换。不仅如此,使用反射获得方法指针的代码要比直接调用方法的代码慢得多。更好的做饭是使用接口已经Java8引入的lambda表达式。
继承的设计技巧
- 将公共操作和字段放在超类中。
- 不要使用受保护的字段。
- 使用继承实现”is-a“关系。
- 除非所有继承的方法都有意义,否则不要使用继承。
- 覆盖方法时,不要改变预期的行为。
- 使用多态,而不要使用类型信息。
- 不要滥用反射。
第六章 接口、lambda表达式与内部类
接口
接口用来描述类应该做什么,而不指定它们具体应该如何做。
接口中的所有方法都自动是public方法。因此,在接口声明方法时,不必提供关键字public。
虽然接口中不能包含实例字段,但是可以包含常量,接口中的字段总是public static final
。
如果你的类实现了Cloneable接口,Object类中的clone方法就可以创建你的类对象的一个完全副本。
接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
可以为任何接口方法提供一个默认实现,既可以减少重复的实现,还可以保证源代码兼容性问题,因为如果接口中添加了方法而没有提供默认实现,使用了该接口的类可能会出现兼容性问题。
绝对不能创建一个默认方法重新定义Object类中的某个方法。因为类优先原则,这样的方法实际上会被Object类所覆盖。
回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。
子类只能调用受保护的clone方法来克隆它自己的对象。
Cloneable接口是Java提供的少数标记接口之一。标记接口不包含任何方法,它唯一的作用就是允许在类型查询中使用instanceof。
要当心子类的克隆。因为子类中可能有需要深拷贝的字段或者不可克隆的字段。
lambda表达式
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
表达式System.out::println
是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。
lambda表达式可以捕获外围作用域中变量的值,但lambda表达式中捕获的变量必须是事实最终变量。事实最终变量是指,这个变量初始化之后就不会再为它赋新值。而且lambda表达式中也不能更改变量。
lambda表达式的体和嵌套块有相同的作用域。
可以把比较器与thenComparing
方法串起来,来处理比较结果相同的情况。
public static void wrong(String text int count,int start){
for(int i=1;i<=count;i++){
ActionListener listener = event ->{
System.out.println(i+":"+text);//引用的外部变量值不能在外部改变
start++;//不能修改外部变量的值
}
}
}
内部类
内部类是定义在另一个类中的类。使用内部类的两个主要原因是:一是内部类可以对同一个包内的其他类隐藏。二是内部类方法可以访问定义这些方法的作用域中的数据,包括原本私有的数据。
一个内部类方法可以访问自身的实例字段,也可以访问创建它的外部类对象的实例字段。为此,内部类的对象总有一个隐式引用,指向创建它的外部类对象。
内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。
内部类不能有static方法。
与其他内部类相比较,局部类还有另外一个优点。它们不仅能够访问外部类的字段,还可以访问局部变量。不过,那些局部变量必须是事实最终变量。
第七章 异常、断言和日志
异常
异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的一个错误处理器。
常见错误有:用户输入错误、设备错误、物理限制和代码错误。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽问题。
继承自RuntimeException的异常包括以下问题:错误的强制类型转换、越界的数组访问和访问null指针。
不继承RuntimeException的异常包括:试图越过文件末尾继续读取数据、试图打开一个不存在的文件和试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常,所有其他异常称为检查型异常。
一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在你的控制之外(Error),要么是从一开始就应该避免的情况导致的(RuntimeException)。
如果在子类中覆盖了一个超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更加通用。
要捕获那些你知道如何处理的异常,而继续传播那些你不知道怎么处理的异常。
假设由return语句从try语句块中间退出。在方法返回前,会执行finally语句块。如果finally语句块也有一个return语句,这个返回值将会遮蔽原来的返回值。因此finally子句的体要用于清理资源。不要把改变控制流的语句(return,throw,break,continue)放在finally子句中。
try-with-resources
需要这个资源属于一个实现了AutoCloseable接口的类。当try块退出时,会自动执行res.close()。
只要需要关闭资源,就要尽可能使用try-with-resources语句。
try(Resource res=...){
work with res
}
//实例
try(var in = new Scanner(Path.of("in.txt"),StandardCharsets.UTF_8)){
while(in.hasNext())
System.out.println(in.next());
}
异常使用技巧
- 异常处理不能代替简单的处理。
- 不要过分地细化异常。
- 合理利用异常层次结构。
- 不要压制异常。
- 在检测错误时,“苛刻”要比放任好。
- 不要羞于传递异常。
- 使用标准方法报告null指针和越界异常。
- 不要向最终用户显式栈轨迹。
断言
断言机制允许你在测试期间在代码内插入一些检查,而在生产环境中自动删除这些检查。
在默认情况下,断言是禁用的。可以在运行程序时用-enableassertions
或-ea
选项启用断言:java -enableassertions MyApp
。
不应该使用断言向程序的其他部分通知发生了可恢复的错误,或者,不应该利用断言与客户沟通问题,断言只应该使用在测试阶段确定程序内部错误的位置。
日志
日志记录器会把记录发送到父处理器,而最终的祖先处理器(名为“”)有一个ConsoleHandler。
与日志记录器一样,处理器也有日志级别。对于一个要记录的日志记录,它的日志级别必须高于日志记录器和处理器二者的阈值。
日志使用技巧
- 对一个简单的应用,选择一个日志记录器。、
- 默认的日志配置会将级别等于或高于INFO的所有信息记录到控制台。
- 对于程序员想要的日志信息,FINE级别是一个很好的选择。
调试技巧
- 打印
System.out.println("x="+x)
,如果x是一个值,会转换成等价的字符串。如果x是一个对象,那么Java会调用这个对象的toString方法。 - 可以在每一个类中放置一个单独的main方法。这个就可以提供一个单元测试桩(stub),允许你独立地测试类。
- JUnit是一个非常流行的单元测试框架,利用它可以很容易地组装测试用例套件。
- 利用Throwable类的printStackTrace方法,可以从任意的异常对象获得栈轨迹。
- 一般来说,栈轨迹显示在System.err上。如果想要记录或显示栈轨迹,可以将它捕获到一个字符串内,或者记录到文件中。
- 要想观察类的加载过程,启动Java虚拟机时可以使用
-verbose
标志。 -Xlint
选项告诉编译器找出常见的代码问题:javac -Xlint sourceFiles
。- Java虚拟提供了对Java应用的监控和管理支持,允许在虚拟机中安装代理来跟踪内存消耗、线程使用、类加载等情况。
第八章 泛型程序设计
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
在Java中增加泛型类之前,泛型程序设计是用继承实现的。
这正是类型参数的魅力所在:它们会让你的程序更加易读,也更安全。
限制T只能是实现了Comparable接口的一个类:public static <T extends Comparable> T min(T[] a)
。
无论S和T是什么关系,Pair<S>
和Pair<T>
都没有任何关系。
在通配符类型中,允许类型参数变化。例如,通配符类型Pair<? extends Employee>
。
通配符不是类型变量,因此,不能编写使用?作为一种类型的代码。例如? t=p.getFirst()
。
反射允许你在运行时分析任意对象。如果对象是泛型类的实例,关于泛型类型参数,你可能得不到多少信息,因为它们已经被擦除了。
擦除的类仍然保留原先泛型的一些微弱记忆。例如,原始Pair类知道它源于泛型类Pair<T>
,尽管一个Pair类型的对象无法区分它构造为Pair<String>
还是Pair<Employee>
。
类型擦除
虚拟机没有泛型类型对象——所有对象都属于普通类。无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型。
类型擦除会导致父类继承时使用泛型是被擦除后的类,而不是父类指定的泛型,解决方法时使用桥方法编写方法。
为保持类型安全性,必要时会插入强制类型转换。
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。例如下面代码的输出值为true。详细介绍
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
}
}
限制和局限性
- 不能使用基本类型实例化类型参数。
- 运行时类型查询只适用于原始类型。
- 不能创建参数化的数组,例如:
var table = new Pair<String>[10]
。 - 不能实例化类型变量。
- 不能构造泛型数组。
- 泛型类的静态上下文中类型变量无效。
- 不能抛出或捕获泛型类的实例,事实上,甚至泛型类拓展Throwable都是不合法的。不过在异常规范中使用类型变量是允许的。
- 可以取消对检查型异常的检查。
- 注意擦除后的冲突。倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。
第九章 集合
与现代的数据结构类库的常见做法一样,Java集合类库也将接口与实现分离。
对于并发修改的检测有一个奇怪的例外。链表只跟踪对列表的结构性修改,例如,添加和删除链接。set方法不被视为结构性修改。
LinkedList类提供了一个get方法,用于访问某个特定数据。绝对不要使用这个“虚假”的随机访问方法来遍历链表。
在Java8中,桶满时会从链表变成平衡二叉树。
优先队列使用了一个精巧且高效的数据结构,称为堆。
WeakHashMap:当键的唯一引用来自散列表条目时,这个数据结构将于垃圾回收器合作删除键/值对。
有些Java程序员在编写程序时,其程序的正确性依赖于一个假设,即实现细节永远不会改变。
视图就是集合或者映射中某一部分或者某一类数据的再映射得到的结果集。
类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类。
在集合和迭代器接口的API文档中,许多方法描述为“可选操作”。这看起来与接口的概念有冲突。
toArray方法返回的数组创建一个Object[]数组,不能改变它的类型。实际上,要向toArray方法传入一个数组构造器表达式。这样一来,返回的数组就会有正确的数组类型:String[] values = staff.toArray(String[]::new);
。
BitSet类提供了一个用于读取、设置或重置各个位的很方便的接口。
第十二章 并发
每当线程调度器有机会选择新线程时,它首先选择有较高优先级的线程。但是,线程优先级高度依赖于系统。
重入锁:线程可以反复获得已拥有的锁。
如果声明一个字段为volatile,那么编译器和虚拟机就会考虑到该字段可能被另一个线程并发更新。
volatile变量不能提供原子性。
遗憾的是,Java程序设计语言中没有提供任何特性可以避免或打破这些死锁。你必须仔细设计程序,确保不会死锁。
stop方法被废弃的原因是一个线程被终止时,它会立即释放被它锁定的所有对象的锁。这会导致对象处于不一致的状态。
如果调用suspend方法的线程尝试获得同一个锁,程序就会死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
线程局部变量有时用于向协助完成某个任务的所有方法提供对象,而不必在调用者之间传递这个对象。
线程安全的数据结构会允许非线程安全的操作。
.
同步代码编写
- 最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下,可以使用Java.util.concurrent包中的某些机制,它会为你处理所有的锁定。
- 如果synchronized关键字适合你的程序,那么尽量使用这种做法,这样可以减少编写的代码量,还能减少出错。
- 如果特别需要Lock/Condition结构提供的额外能力,则使用Lock/Condition。
监视器
- 监视器是只包含私有字段的类。
- 监视器的每个对象有一个关联的锁。
- 所有方法由这个锁锁定。
- 锁可以有任意多个关联的条件。