diff --git a/notes/Java 基础.md b/notes/Java 基础.md
index 8f006ed4..57d23ea4 100644
--- a/notes/Java 基础.md
+++ b/notes/Java 基础.md
@@ -20,7 +20,7 @@
* [final](#final)
* [static](#static)
* [五、Object 通用方法](#五object-通用方法)
- * [概览](#概览)
+ * [概览](#概览-1)
* [equals()](#equals)
* [hashCode()](#hashcode)
* [toString()](#tostring)
@@ -38,6 +38,12 @@
* [Java 各版本的新特性](#java-各版本的新特性)
* [Java 与 C++ 的区别](#java-与-c-的区别)
* [JRE or JDK](#jre-or-jdk)
+ * [附录:实测](#附录实测)
+ * [A1 包装类型的缓存池](#a1-包装类型的缓存池)
+ * [A2 字符串的存储](#a2-字符串的存储)
+ * [A3 泛型的类型擦除](#a3-泛型的类型擦除)
+ * [A4 反射与注解](#a4-反射与注解)
+ * [A5 Lambda 与 Stream](#a5-lambda-与-stream)
* [参考资料](#参考资料)
@@ -55,10 +61,7 @@
- double/64
- boolean/\~
-boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
-
-- [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)
-- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf)
+boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。编译器会把 boolean 类型的数据编译为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
### 包装类型
@@ -66,11 +69,9 @@ boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是
```java
Integer x = 2; // 装箱 调用了 Integer.valueOf(2)
-int y = x; // 拆箱 调用了 X.intValue()
+int y = x; // 拆箱 调用了 x.intValue()
```
-- [Autoboxing and Unboxing](https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html)
-
### 缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
@@ -87,6 +88,8 @@ Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
```
+需要注意的是,new Integer(123) 从 JDK 9 起已经废弃,JDK 16 起标记为将要删除,编译时会给出警告,应该使用 Integer.valueOf(123)。
+
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
```java
@@ -97,40 +100,6 @@ public static Integer valueOf(int i) {
}
```
-在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。
-
-```java
-static final int low = -128;
-static final int high;
-static final Integer cache[];
-
-static {
- // high value may be configured by property
- int h = 127;
- String integerCacheHighPropValue =
- sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
- if (integerCacheHighPropValue != null) {
- try {
- int i = parseInt(integerCacheHighPropValue);
- i = Math.max(i, 127);
- // Maximum array size is Integer.MAX_VALUE
- h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
- } catch( NumberFormatException nfe) {
- // If the property cannot be parsed into an int, ignore it.
- }
- }
- high = h;
-
- cache = new Integer[(high - low) + 1];
- int j = low;
- for(int k = 0; k < cache.length; k++)
- cache[k] = new Integer(j++);
-
- // range [-128, 127] must be interned (JLS7 5.1.7)
- assert IntegerCache.high >= 127;
-}
-```
-
编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
```java
@@ -139,26 +108,21 @@ Integer n = 123;
System.out.println(m == n); // true
```
-基本类型对应的缓冲池如下:
+各包装类型的缓存池如下(JDK 21 实测,见[附录 A1](#a1-包装类型的缓存池)):
-- boolean values true and false
-- all byte values
-- short values between -128 and 127
-- int values between -128 and 127
-- char in the range \u0000 to \u007F
+- Boolean:true 和 false;
+- Byte:全部 256 个值;
+- Short、Integer、Long:-128\~127;
+- Character:0\~127,即 \u0000\~\u007F;
+- Float、Double 没有缓存池。
-在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
-
-在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
-
-[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
-](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
+Integer 缓存池的上界可以调整:启动时加上 -XX:AutoBoxCacheMax=1000,IntegerCache 初始化时会读取这个值,之后 Integer.valueOf(128) 也会返回同一个对象。其它包装类型的缓存范围是固定的。
## 二、String
### 概览
-String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
+String 被声明为 final,因此它不可被继承(Integer 等包装类也不能被继承)。
在 Java 8 中,String 内部使用 char 数组存储数据。
@@ -170,7 +134,7 @@ public final class String
}
```
-在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。
+从 Java 9 开始,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码(紧凑字符串,JEP 254):只包含 Latin-1 字符的字符串每个字符占 1 个字节,coder 为 0;含有中文等其它字符时按 UTF-16 存储,每个字符占 2 个字节,coder 为 1。英文为主的程序中,字符串占用的内存因此减少近一半。
```java
public final class String
@@ -187,40 +151,30 @@ value 数组被声明为 final,这意味着 value 数组初始化之后就不
### 不可变的好处
-**1. 可以缓存 hash 值**
+#### 1. 可以缓存哈希值
-因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
+因为 String 的哈希值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得哈希值也不可变,因此只需要进行一次计算。
-**2. String Pool 的需要**
+#### 2. String Pool 的需要
-如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
+如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool:下图中 s1 和 s2 引用的是同一个对象,如果可以通过 s1 修改它,s2 的值也会跟着改变。
-
+
-**3. 安全性**
+#### 3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
-**4. 线程安全**
+#### 4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
-[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
-
### String, StringBuffer and StringBuilder
-**1. 可变性**
+- 可变性:String 不可变,StringBuffer 和 StringBuilder 可变;
+- 线程安全:String 不可变,因此是线程安全的;StringBuffer 的方法使用 synchronized 进行同步,是线程安全的;StringBuilder 不是线程安全的,但没有加锁的开销,单线程中拼接字符串时使用它。
-- String 不可变
-- StringBuffer 和 StringBuilder 可变
-
-**2. 线程安全**
-
-- String 不可变,因此是线程安全的
-- StringBuilder 不是线程安全的
-- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
-
-[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
+需要注意的是,在循环中用 + 拼接字符串,每次循环都会创建一个新的 String 对象并复制之前的内容,应该在循环外创建一个 StringBuilder,在循环中调用 append()。
### String Pool
@@ -249,9 +203,6 @@ System.out.println(s5 == s6); // true
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
-- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
-- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
-
### new String("abc")
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。
@@ -297,12 +248,14 @@ Constant pool:
在 class 文件的常量池中,#19 存储字符串字面量 "abc",#3 是 String 类型的常量,它指向 #19;运行时解析 #3 得到的就是 String Pool 中的字符串对象。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
-以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
+以下是 JDK 21 中 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组,即上图中的 s3。
```java
public String(String original) {
this.value = original.value;
+ this.coder = original.coder;
this.hash = original.hash;
+ this.hashIsZero = original.hashIsZero;
}
```
@@ -374,8 +327,6 @@ public class PassByValueExample {
}
```
-[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value)
-
### float 与 double
Java 不能隐式执行向下转型,因为这会使得精度降低。
@@ -414,8 +365,6 @@ s1++;
s1 = (short) (s1 + 1);
```
-[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting)
-
### switch
从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
@@ -432,11 +381,11 @@ switch (s) {
}
```
-switch 不支持 long、float、double,是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
+switch 不支持 long、float、double,这是因为 switch 编译后的 tableswitch 和 lookupswitch 指令只能按 int 值跳转。对 String 使用 switch 时,编译器先按 hashCode() 跳转,再用 equals() 比较(JDK 21 javap 实测,见[附录 A2](#a2-字符串的存储))。
```java
// long x = 111;
-// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
+// switch (x) { // error: selector type long is not allowed
// case 111:
// System.out.println(111);
// break;
@@ -446,14 +395,24 @@ switch 不支持 long、float、double,是因为 switch 的设计初衷是对
// }
```
-[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
+从 Java 14 开始,switch 还可以作为表达式使用。case 后面用 -> 时不会贯穿到下一个分支,不需要 break:
+
+```java
+int days = switch (month) {
+ case 2 -> 28;
+ case 4, 6, 9, 11 -> 30;
+ default -> 31;
+};
+```
+
+Java 21 起 switch 还可以按类型匹配,见 [Java 各版本的新特性](#java-各版本的新特性)。
## 四、关键字
### final
-**1. 数据**
+#### 1. 数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
@@ -467,21 +426,21 @@ final A y = new A();
y.a = 1;
```
-**2. 方法**
+#### 2. 方法
声明方法不能被子类重写。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
-**3. 类**
+#### 3. 类
声明类不允许被继承。
### static
-**1. 静态变量**
+#### 1. 静态变量
-- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
+- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它,静态变量在内存中只存在一份;
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
```java
@@ -499,7 +458,7 @@ public class A {
}
```
-**2. 静态方法**
+#### 2. 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
@@ -527,7 +486,7 @@ public class A {
}
```
-**3. 静态语句块**
+#### 3. 静态语句块
静态语句块在类初始化时运行一次。
@@ -544,11 +503,11 @@ public class A {
}
```
-```html
+```text
123
```
-**4. 静态内部类**
+#### 4. 静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
@@ -572,15 +531,15 @@ public class OuterClass {
静态内部类不能访问外部类的非静态的变量和方法。
-**5. 静态导包**
+#### 5. 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
```java
-import static com.xxx.ClassName.*
+import static com.xxx.ClassName.*;
```
-**6. 初始化顺序**
+#### 6. 初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
@@ -626,7 +585,6 @@ public InitialOrderTest() {
### 概览
```java
-
public native int hashCode()
public boolean equals(Object obj)
@@ -637,7 +595,7 @@ public String toString()
public final native Class> getClass()
-protected void finalize() throws Throwable {}
+protected void finalize() throws Throwable {} // JDK 9 起废弃,JDK 18 起标记为将要删除
public final native void notify()
@@ -652,48 +610,19 @@ public final void wait() throws InterruptedException
### equals()
-**1. 等价关系**
+#### 1. 等价关系
两个对象具有等价关系,需要满足以下五个条件:
-Ⅰ 自反性
+- 自反性:x.equals(x) 为 true;
+- 对称性:x.equals(y) 与 y.equals(x) 的结果相同;
+- 传递性:x.equals(y) 和 y.equals(z) 都为 true 时,x.equals(z) 也为 true;
+- 一致性:对象没有被修改时,多次调用 x.equals(y) 的结果不变;
+- 与 null 的比较:对任何不是 null 的对象 x,x.equals(null) 为 false。
-```java
-x.equals(x); // true
-```
+#### 2. 等价与相等
-Ⅱ 对称性
-
-```java
-x.equals(y) == y.equals(x); // true
-```
-
-Ⅲ 传递性
-
-```java
-if (x.equals(y) && y.equals(z))
- x.equals(z); // true;
-```
-
-Ⅳ 一致性
-
-多次调用 equals() 方法结果不变
-
-```java
-x.equals(y) == x.equals(y); // true
-```
-
-Ⅴ 与 null 的比较
-
-对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
-
-```java
-x.equals(null); // false;
-```
-
-**2. 等价与相等**
-
-- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
+- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法;
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
```java
@@ -703,7 +632,7 @@ System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
```
-**3. 实现**
+#### 3. 实现
- 检查是否为同一个对象的引用,如果是直接返回 true;
- 检查是否是同一个类型,如果不是,直接返回 false;
@@ -739,13 +668,13 @@ public class EqualExample {
### hashCode()
-hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。
+hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。Object 规定等价的两个对象哈希值必须相同,但是哈希值相同的两个对象不一定等价,这是因为哈希值只有 32 位,不同的对象可能计算出相同的哈希值,例如 "Aa" 和 "BB" 的哈希值都是 2112(见[附录 A2](#a2-字符串的存储))。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。
-HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。
+HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。
-下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象。但是 EqualExample 没有实现 hashCode() 方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。
+下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。期望的结果是把这两个对象当成一样的,只在集合中添加一个对象。但是 EqualExample 没有实现 hashCode() 方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。
```java
EqualExample e1 = new EqualExample(1, 1, 1);
@@ -759,7 +688,7 @@ System.out.println(set.size()); // 2
理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。
-R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。
+R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,虚拟机可以自动进行这个优化。
```java
@Override
@@ -772,9 +701,11 @@ public int hashCode() {
}
```
+实际编写时可以直接调用 Objects.hash(x, y, z),它按同样的方式计算,只是初始值为 1。
+
### toString()
-默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
+默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为哈希码的无符号十六进制表示。
```java
public class ToStringExample {
@@ -792,13 +723,13 @@ ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
```
-```html
+```text
ToStringExample@4554617c
```
### clone()
-**1. cloneable**
+#### 1. Cloneable
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
@@ -837,7 +768,7 @@ try {
}
```
-```html
+```text
java.lang.CloneNotSupportedException: CloneExample
```
@@ -857,7 +788,7 @@ public class CloneExample implements Cloneable {
}
```
-**2. 浅拷贝**
+#### 2. 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
@@ -900,7 +831,7 @@ e1.set(2, 222);
System.out.println(e2.get(2)); // 222
```
-**3. 深拷贝**
+#### 3. 深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
@@ -948,9 +879,9 @@ e1.set(2, 222);
System.out.println(e2.get(2)); // 2
```
-**4. clone() 的替代方案**
+#### 4. clone() 的替代方案
-使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
+使用 clone() 方法来拷贝一个对象既复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
```java
public class CloneConstructorExample {
@@ -996,8 +927,8 @@ Java 中有三个访问权限修饰符:private、protected 以及 public,如
可以对类或类中的成员(字段和方法)加上访问修饰符。
-- 类可见表示其它类可以用这个类创建实例对象。
-- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
+- 类可见表示其它类可以用这个类创建实例对象;
+- 成员可见表示其它类可以用这个类的实例对象访问到该成员。
protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
@@ -1005,7 +936,7 @@ protected 用于修饰成员,表示在继承体系中成员对于子类可见
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替,也就是确保满足里氏替换原则。
-字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。
+字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻想要改为使用 int 存储 id 字段,那么就需要修改所有的客户端代码。
```java
public class AccessExample {
@@ -1053,7 +984,7 @@ public class AccessWithInnerClassExample {
### 抽象类与接口
-**1. 抽象类**
+#### 1. 抽象类
抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
@@ -1088,7 +1019,7 @@ AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();
```
-**2. 接口**
+#### 2. 接口
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
@@ -1112,7 +1043,7 @@ public interface InterfaceExample {
public int z = 0; // Modifier 'public' is redundant for interface fields
// private int k = 0; // Modifier 'private' not allowed here
// protected int l = 0; // Modifier 'protected' not allowed here
- // private void fun3(); // Modifier 'private' not allowed here
+ // private void fun3(); // missing method body:Java 9 起可以定义私有方法,但必须有方法体
}
```
@@ -1132,14 +1063,14 @@ ie2.func1();
System.out.println(InterfaceExample.x);
```
-**3. 比较**
+#### 3. 比较
-- 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里氏替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
-- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
-- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
-- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
+- 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里氏替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系;
+- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类;
+- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制;
+- 接口的成员默认是 public 的,Java 9 起方法可以是 private 的,但不能是 protected 的;抽象类的成员可以有多种访问权限。
-**4. 使用选择**
+#### 4. 使用选择
使用接口:
@@ -1148,21 +1079,16 @@ System.out.println(InterfaceExample.x);
使用抽象类:
-- 需要在几个相关的类中共享代码。
-- 需要能控制继承来的成员的访问权限,而不是都为 public。
+- 需要在几个相关的类中共享代码;
+- 需要能控制继承来的成员的访问权限,而不是都为 public;
- 需要继承非静态和非常量字段。
-在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
-
-- [Abstract Methods and Classes](https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html)
-- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
-- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
-- [Java 9 Private Methods in Interfaces](https://www.journaldev.com/12850/java-9-private-methods-interfaces)
+在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变得很低。
### super
-- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数。
+- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数;
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
```java
@@ -1205,33 +1131,31 @@ SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
```
-```html
+```text
SuperExample.func()
SuperExtendExample.func()
```
-[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
-
### 重写与重载
-**1. 重写(Override)**
+#### 1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里氏替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法;
-- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
-- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
+- 子类方法的返回类型必须是父类方法返回类型或为其子类型;
+- 子类方法抛出的受检异常必须是父类方法抛出的异常类型或为其子类型。
-使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
+上面三个限制无论是否加注解,编译器都会检查。@Override 注解让编译器检查这个方法确实重写了父类的方法,例如方法名或参数类型写错时,没有注解就会悄悄地变成一个新方法。
下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:
-- 子类方法访问权限为 public,大于父类的 protected。
-- 子类的返回类型为 ArrayList\,是父类返回类型 List\ 的子类。
-- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
-- 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。
+- 子类方法访问权限为 public,大于父类的 protected;
+- 子类的返回类型为 ArrayList\,是父类返回类型 List\ 的子类;
+- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类;
+- 子类重写方法使用 @Override 注解,从而让编译器检查它确实重写了父类的方法。
```java
class SuperClass {
@@ -1248,12 +1172,12 @@ class SubClass extends SuperClass {
}
```
-在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:
+调用一个方法分为两步:
-- this.func(this)
-- super.func(this)
-- this.func(super)
-- super.func(super)
+- 编译时,按变量声明的类型(静态类型)找到可以调用的方法,包括从父类继承来的方法,在其中选择参数类型最匹配的一个,参数没有完全匹配的方法时,把参数转型为父类再找;
+- 运行时,按对象的实际类型,如果子类重写了编译时选定的方法,就调用子类的版本。
+
+下面的代码中,ba 的静态类型是 A,ba.show(d) 在编译时选定 show(C obj),运行时的对象是 B,而 B 没有重写 show(C obj),所以调用的是 A.show(C)。
```java
@@ -1318,7 +1242,7 @@ public static void main(String[] args) {
}
```
-**2. 重载(Overload)**
+#### 2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
@@ -1346,52 +1270,56 @@ public static void main(String[] args) {
## 七、反射
-每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件保存着类的字节码,类加载时 JVM 根据它创建对应的 Class 对象。
+每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件保存着类的字节码,类加载时 JVM 根据它创建对应的 Class 对象。
-类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。
+类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.example.Order") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
-- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
-- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法;
-- **Constructor** :可以用 Constructor 的 newInstance() 创建新的对象。
+- Field:可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
+- Method:可以使用 invoke() 方法调用与 Method 对象关联的方法;
+- Constructor:可以用 Constructor 的 newInstance() 创建新的对象。
-**反射的优点:**
+例如只知道类名 Order 时,创建对象、调用方法并读取私有字段:
-- **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
-- **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
-- **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
+```java
+Class> clazz = Class.forName("Order");
+Object order = clazz.getConstructor(long.class).newInstance(1024L);
+Method pay = clazz.getMethod("pay", int.class);
+System.out.println(pay.invoke(order, 99)); // order 1024 paid 99
-**反射的缺点:**
+Field status = clazz.getDeclaredField("status");
+status.setAccessible(true); // 私有字段要先关闭访问检查
+System.out.println(status.get(order)); // PAID
+```
-尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
+反射的用途:
-- **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
+- 框架:Spring 按配置或注解中的类名创建 Bean 并注入字段,MyBatis 把查询结果写入对象的字段,JUnit 找出带 @Test 注解的方法逐个调用,这些类在编写框架时都还不存在;
+- 动态代理:JDK 动态代理在运行时生成实现了指定接口的类,调用通过 Method 对象转发,见 [动态代理](Spring.md#动态代理);
+- 开发工具:IDE 列出类的成员,调试器查看对象的私有字段。
-- **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
+反射的缺点:
-- **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
-
-- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
-- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
+- 性能开销:反射调用要检查访问权限、把参数装进 Object 数组,查找 Method 对象的开销更大。JDK 21 上把 Method 对象缓存起来时,每次调用只比直接调用多 2 纳秒左右;每次都用 getMethod() 查找再调用则约 28 纳秒(见[附录 A4](#a4-反射与注解)),所以框架都会缓存 Method 对象;
+- 访问限制:JDK 9 引入模块系统后,JDK 内部类的私有成员默认不能通过反射访问,例如对 String 的 value 字段调用 setAccessible(true) 会抛出 InaccessibleObjectException,需要启动时加上 --add-opens;
+- 破坏封装:反射可以访问私有的字段和方法,依赖这些内部细节的代码在类的实现改变后可能失效。
## 八、异常
-Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
+Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,例如 OutOfMemoryError、StackOverflowError,程序一般不捕获。Exception 分为两种:
-- **受检异常** :需要用 try...catch... 语句捕获并进行处理,或者用 throws 声明抛出,并且可以从异常中恢复;
-- **非受检异常** :是程序运行时错误,例如除 0 会引发 ArithmeticException。编译器不强制处理这类异常,通常应该修改代码来避免,而不是捕获之后恢复。
+- 受检异常:例如 IOException、SQLException,需要用 try...catch... 语句捕获并进行处理,或者用 throws 声明抛出,否则编译不通过,这类异常通常可以恢复;
+- 非受检异常:RuntimeException 及其子类,是程序运行时错误,例如除 0 会引发 ArithmeticException,访问 null 的成员会引发 NullPointerException。编译器不强制处理这类异常,通常应该修改代码来避免,而不是捕获之后恢复。
-- [Java Exception Interview Questions and Answers](https://www.journaldev.com/2167/java-exception-interview-questions-and-answers)
-
-- [Java提高篇——Java 异常处理](https://www.cnblogs.com/Qian123/p/5715402.html)
-
## 九、泛型
+泛型把类型作为参数,让编译器在编译时检查类型。例如 List\ 中只能放入字符串,取出时也不需要强制类型转换:
+
```java
public class Box {
// T stands for "Type"
@@ -1401,14 +1329,59 @@ public class Box {
}
```
-- [Java 泛型详解](https://www.cnblogs.com/Blue-Keroro/p/8875898.html)
-- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
+Java 的泛型通过类型擦除实现:类型参数只在编译时检查,编译后都替换为它的上界(没有声明上界时为 Object),在取出元素的地方由编译器插入强制类型转换。所以 List\ 和 List\ 在运行时是同一个类:
+
+```java
+List a = new ArrayList<>();
+List b = new ArrayList<>();
+System.out.println(a.getClass() == b.getClass()); // true
+
+a.add("redis");
+String s = a.get(0); // 编译为 List.get,返回 Object,再执行 checkcast String
+```
+
+类型擦除带来以下限制:
+
+- 类型参数不能是基本类型,只能写 List\,不能写 List\;
+- 不能 new T(),也不能用 instanceof List\ 判断,因为运行时已经没有 T 和 String 这些信息;
+- 类型检查只发生在编译时:把 List\ 赋值给原始类型 List 后,可以放入一个 Integer,编译器只给出警告,等到取出赋给 String 时才抛出 ClassCastException;通过反射调用 add() 也能绕过检查(见[附录 A3](#a3-泛型的类型擦除))。
+
+通配符用于方法参数,让方法接受一族泛型类型:List\ extends Number\> 可以接收 List\、List\,只能从中读出 Number,不能放入元素;List\ super Integer\> 可以接收 List\、List\