# 7.3 - 使用异常的技巧
- 异常处理不能代替简单的测试。
- 捕获异常所花费的时间远远超过了简单的测试。
- 不要过分的细化异常
- 尽量将多条语句包含在一个 try-catch 里,然后使用多个 catch 子句,而不是每一条语句都增加一个 try-catch。
- 充分利用异常层次结构
- 不要压制异常
- 在检测错误时,” 苛刻 “要比放任更好
- 不要羞于传递异常
- 可以归纳为:早抛出,晚捕获。类似于 Web 项目中 DAO 层的异常需要抛到 Service 层进行处理
# 7.4.1 - assert 之断言
assert conditionassert condition : expression
这两个语句都会计算条件,如果结果为 false ,则抛出一个 AssertionError 异常。在第二个语句中,表达式将传入 AssertError 对象的构造器,并转换为一个消息字符串。
# 7.4.2 - 断言的开启和禁用
- 断言默认是关闭的,可以通过以下方式来开启断言:(启用或禁用断言是类加载器的功能,所以不需要重新编译)
- 运行时用
-enableassertions或-ea选项启用断言。如:java -enableassertions MyApp - 也可以在某个类中或整个包中启用断言。例如:
java -ea:MyClass -ea:com.mycompany.mylib MyApp,这条命令将为 MyClass 类以及 com.mycompany.mylib 包和他的子包中的所有类打开断言。选项 -ea 将打开无名包中所有类的断言。 - 也可以使用
-diableassertions或-da在某个特定类和包中禁用断言。
- 运行时用
- 通过类加载器的方法来开启或禁用断言:
**public void setDefaultAssertionStatus(boolean enabled)**- 设置此类加载器的默认断言状态。此设置确定由此类加载器加载并在将来初始化的类是否默认启用或禁用断言。通过调用
setPackageAssertionStatus(String, boolean)或setClassAssertionStatus(String, boolean)可以在每个包或每个类的基础上覆盖此设置。 - 参形:enabled - 如果此类加载器加载的类今后将默认启用断言,则为 true ;如果默认情况下禁用断言,则为 false。
- 设置此类加载器的默认断言状态。此设置确定由此类加载器加载并在将来初始化的类是否默认启用或禁用断言。通过调用
**public void setClassAssertionStatus(String className, boolean enabled)**- 为此类加载器中的命名顶级类和其中包含的任何嵌套类设置所需的断言状态。此设置优先于类加载器的默认断言状态,以及任何适用的每个包的默认值。如果命名类已经初始化,则此方法无效。 (一旦一个类被初始化,它的断言状态就不能改变。)如果命名类不是顶级类,则此调用不会影响任何类的实际断言状态。
- 参形:className – 要设置其断言状态的顶级类的完全限定类名。enabled - 如果命名类在初始化时(并且如果)启用断言,则为 true ,如果类要禁用断言,则为 false。
**public void setPackageAssertionStatus(String packageName, boolean enabled) {**- 设置命名包的包默认断言状态。包默认断言状态决定了将来初始化的属于指定包或其任何 “子包” 的类的断言状态。名为 p 的包的子包是名称以 “ p. ” 开头的任何包。例如 javax.swing.text 是 javax.swing 的子包, java.util 和 java.lang.reflect 都是 java 的子包。如果多个包默认值适用于给定类,则属于最特定包的包默认值优先于其他包。例如,如果 javax.lang 和 javax.lang.reflect 都有与之关联的包默认值,则后一个包默认值适用于 javax.lang.reflect 中的类。包默认值优先于类加载器的默认断言状态,并且可以通过调用 setClassAssertionStatus (String, boolean) 在每个类的基础上被覆盖。
- 参形:packageName – 要设置其包默认断言状态的包的名称。空值表示 “当前” 的未命名包(请参阅 The Java™ Language Specification 的第 7.4.2 节。)enabled –如果类加载器加载并属于命名包或其任何子包的类默认启用断言,则为 true,如果默认禁用断言,则为 false 。
# 注:
- 断言关闭了也会被编译进 class 文件,只是不起作用。不知道为啥。开启断言和未开启断言的程序在编译后的字节码文件也看不出区别。

# 7.4.3 - 使用断言完成参数检查
在 Java 中,给出了 3 种处理系统错误的机制:
- 抛出一个异常
- 日志
- 使用断言
什么时候应该使用断言呢?
- 断言失败是致命的、不可恢复的错误。
- 断言检查只是在开发和测试阶段打开(这种做法有时候被戏称为 “在靠进海岸时穿上救生衣,但在海里就把救生衣抛掉)。
- 断言只应该用于在测试阶段确定程序内部错误的位置。
# 8.4 - 类型变量的限定
<T extends Comparable>:表示类型变量 T 必须是 Comparable 的实现类。 如果有多个,用 & 连接。
# 8.5.1 - 类型擦除
虚拟机没有泛型类型对象 —— 所有对象都属于普通类。
- 所以类型变量会被擦除,并替换为其 限定类型,对于无限定类型的变量则替换为 Object。
- 假设限定类型有多个,例如:
public class Interval<T extends Comparable & Serializable> implement Serializable {//...}。- 这种情况下会用 Comparable 替换掉 T
- 所以为了提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾。
# 8.5.2 - 类型擦除的额外处理
编译器会自动在字节码文件种为 类型擦除之后的代码中适当的插入强制类型转换。
# 8.5.3 - 转换泛型方法
# 考虑一个场景:
当父类中有一个泛型方法(参数为泛型),而子类重写了这个方法。那么编译后,由于类型擦除,父类中的泛型参数会被修改为 Object,所以此时子类中会有两个同名的方法。
# 代码如下:
package fanxing; | |
import java.lang.reflect.Method; | |
import java.util.Arrays; | |
/** | |
* @author: Ding | |
* @date: 2022/5/19 | |
* @description: | |
* @modify: | |
*/ | |
class FanXing01<T> { | |
public void test(T t) { | |
System.out.println("01 : " + t.toString()); | |
} | |
} | |
class FanXing02 extends FanXing01<String> { | |
@Override | |
public void test(String s) { | |
System.out.println("02 : " + s.toString()); | |
} | |
} | |
class Father { | |
public void test(String s) { | |
System.out.println("Father"); | |
} | |
} | |
class Son extends Father { | |
@Override | |
public void test(String s) { | |
System.out.println("Son"); | |
} | |
} | |
public class Test01 { | |
public static void main(String[] args) { | |
Method[] methods1 = FanXing02.class.getMethods(); | |
for (Method method : methods1) { | |
System.out.println(method.getName() + " : " + Arrays.toString(method.getGenericParameterTypes())); | |
} | |
System.out.println("_____________________"); | |
Method[] methods2 = Son.class.getMethods(); | |
for (Method method : methods2) { | |
System.out.println(method.getName() + " : " + Arrays.toString(method.getGenericParameterTypes())); | |
} | |
} | |
} |
# 运行结果:
test : [class java.lang.String]
test : [class java.lang.Object]
wait : []
wait : [long, int]
wait : [long]
equals : [class java.lang.Object]
toString : []
hashCode : []
getClass : []
notify : []
notifyAll : []
_____________________
test : [class java.lang.String]
wait : []
wait : [long, int]
wait : [long]
equals : [class java.lang.Object]
toString : []
hashCode : []
getClass : []
notify : []
notifyAll : []
# 代码截图:

# 说明:
可以看到,泛型类的子类重写父类的反方法之后,子类会有两个 test 方法,这感觉更像是重载并非重写。这就是类型擦除与多态发生了冲突。为了解决这个问题,编译器会在 FanXIng002 中生成一个桥方法。


此方法就是编译器生成的桥方法。
# 那么再考虑一个场景:
还是类似的,父类中的方法里面,返回值是泛型,那么子类覆盖这个方法会发生什么?父类的泛型擦除了变成 Object,子类覆盖的方法名和方法参数将和父类一模一样,只是返回值不同。
# 代码如下:
较上个代码片段增加了两个 get ()
package fanxing; | |
import java.lang.reflect.Method; | |
import java.util.Arrays; | |
/** | |
* @author: Ding | |
* @date: 2022/5/19 | |
* @description: | |
* @modify: | |
*/ | |
class FanXing01<T> { | |
private T t; | |
public void test(T t) { | |
this.t = t; | |
System.out.println("01 : " + t.toString()); | |
} | |
public T get() { | |
return t; | |
} | |
} | |
class FanXing02 extends FanXing01<String> { | |
@Override | |
public void test(String s) { | |
System.out.println("02 : " + s.toString()); | |
} | |
@Override | |
public String get() { | |
return "Hello"; | |
} | |
} | |
class Father { | |
public void test(String s) { | |
System.out.println("Father"); | |
} | |
} | |
class Son extends Father { | |
@Override | |
public void test(String s) { | |
System.out.println("Son"); | |
} | |
} | |
public class Test01 { | |
public static void main(String[] args) { | |
Method[] methods1 = FanXing02.class.getMethods(); | |
for (Method method : methods1) { | |
System.out.println(method.getName() + " : return " + method.getReturnType() + " " + Arrays.toString(method.getGenericParameterTypes())); | |
} | |
System.out.println("_____________________"); | |
Method[] methods2 = Son.class.getMethods(); | |
for (Method method : methods2) { | |
System.out.println(method.getName() + " : " + Arrays.toString(method.getGenericParameterTypes())); | |
} | |
} | |
} |
# 运行结果:
get : return class java.lang.Object []
get : return class java.lang.String []
test : return void [class java.lang.String]
test : return void [class java.lang.Object]
wait : return void []
wait : return void [long, int]
wait : return void [long]
equals : return boolean [class java.lang.Object]
toString : return class java.lang.String []
hashCode : return int []
getClass : return class java.lang.Class []
notify : return void []
notifyAll : return void []
_____________________
test : [class java.lang.String]
wait : []
wait : [long, int]
wait : [long]
equals : [class java.lang.Object]
toString : []
hashCode : []
getClass : []
notify : []
notifyAll : []
# 说明:
可以看到,此时 子类中有两个方法签名相同的方法,只是返回值不同。
这样的方法是不合法的,但在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够争取恶的处理这种情况。
# 注:
桥方法不仅用于泛型类型。一个方法覆盖另一个方法时可以指定一个更严格的返回类型,这是合法的。
看来这就是桥方法的作用了?
Object.clone () 和 Employee.clone () 方法呗成为有协变的返回类型。实际上,Employee 方法有两个克隆方法:
Employee clone () // 自定义的 clone 方法
Object clone () // 编译器生成的桥方法,继承自 Object.clone ()
# 总结:
- Java 中重写父类的方法分两种情况:
- 方法返回值类型比父类更严格:编译器自动生成一个桥方法去真正重写父类的方法,并调用被 @Override 标注的 ” 重写 “ 的方法。
- 方法返回值和父类相同:直接重写,没有需要强调的。
# 8.6 - 限制与局限性
# 8.6.1 - 不能使用基本类型实例化类型参数
- 因为泛型擦除后是 Object,而 Object 不能存储八种基本数据类型。
# 8.6.2 - 运行时类型查询只适用于原始类型
if (a instanceof Pair<String>) // ERROR
if (a instanceof Pair<T>) // ERROR
# 8.6.3 - 不能创建参数化类型的数组
Pair<String>[] table = new Pair<>[]; // ERROR
- 但是可以另辟蹊径:
FanXing01<String>[] fanXing01s = (FanXing01<String>[]) new FanXing01<?>[10];但结果将是不安全的,例如:var table = (Pair<String>[]) new Pair<?>[]{new Pair<Integer>(1, 2), new Pair<String>("1", "2")};将对象显示声明在数组中只是为了方便,实际情况中很有可能接收到类型参数不一样的 Pair 对象,那时就将收获一个类型转换异常。
# 8.6.5 - 不能直接实例化类型变量
T t = new T();// ERROR- 间接方法:
- 让调用者提供一个构造器表达式,例如:
Pair<String> p = Pair.makePair(String::new);,其中 makePair 方法接收一个 Suppliers,这是一个函数式接口。
- 让调用者提供一个构造器表达式,例如:
public static <T> Pair<T> makePair(Supplier<T> constr) { | |
return new Pair<>(constr.get(), constr.get()); | |
} |
- 反射创建对象。
public static <T> Pair<T> makePair(Class<T> cl) throw Exception { | |
return new Par<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance()); | |
} |
# 8.6.6 - 不能直接构造泛型数组
T[] t = new T[2] // ERROR
- 构造泛型数组的间接方法:
- 让用户提供一个数组构造器表达式:
String[] names2 = ArrayAlg.minmax(String[]::new, "ABC", "BCD");
- 让用户提供一个数组构造器表达式:
package pair2; | |
import java.time.*; | |
import java.util.function.IntFunction; | |
/** | |
* @author Cay Horstmann | |
* @version 1.02 2015-06-21 | |
*/ | |
public class PairTest2 { | |
public static void main(String[] args) { | |
String[] names1 = ArrayAlg.minmax(new IntFunction<String[]>() { | |
@Override | |
public String[] apply(int value) { | |
return new String[value]; | |
} | |
}, "ABC", "BCD"); | |
String[] names2 = ArrayAlg.minmax(String[]::new, "ABC", "BCD"); | |
System.out.println(names1[0]); | |
System.out.println(names2[0]); | |
} | |
} | |
class ArrayAlg { | |
/** | |
* 获取类型为 T 的对象数组的最小值和最大值。 | |
* | |
* @param a T 类型的对象数组 | |
* @return 具有最小值和最大值的对,如果 a 为 null 或为空,则为 null | |
*/ | |
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) { | |
var result = constr.apply(2); | |
result = a; | |
return (T[]) result; | |
} | |
} |
- 使用传统的反射创建对象:
Array.newInstance(a.getClass().getComponentType(), 2);
# 8.6.7 - 泛型类的静态上下文中类型变量无效
- 原因:在 java 中泛型只是一个占位符,必须在传递类型后才能使用。就泛型类而言,类实例化时才能传递真正的类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数时,静态方法就已经加载完成。显然,静态方法不能使用 / 访问泛型类中的泛型。这和静态方法不能调用普通方法 / 访问普通变量类似,都是因为静态申明与非静态申明的生命周期不同。
# 8.6.8 - 不能抛出或捕获泛型类的实例
- 假设当前我们有两个类 —— SomeException
类和 SomeException 类,它们都是继承自 Throwable 类的。代码中的 doSomeStuff () 方法可能是抛出 SomeException 异常或 SomeException 异常。我们针对不同的异常做出不同的逻辑操作。这样看似完全没有问题。但是熟悉泛型的小伙伴都知道,还有一种叫做类型擦除机制的存在,何为类型擦除?此处不扩展了。通俗点说:java 中不存在泛型代码,泛型代码是写给我们看的,编译器会将泛型代码转换成普通类代码。所以无论是 SomeException 或者是 SomeException 经过编译器的类型擦除后都将会变成 SomeException。故上述代码是不可以运行的,因为当代码抛出异常时编译器是无法判断走哪个 catch 分支的,所以 java 为了避免这样的问题出现,故泛型类是无法继承自 Throwable 类的。 - 作者:陈皮的柚子链接:https://juejin.cn/post/6844904157988519944 来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
# 8.6.10 - 注意泛型擦除后的冲突
先来看一个示例:
public class Pair<T> { | |
public boolean equals (T value) {//... | |
} | |
} |
考虑一个 Pair<String> ,从概念上来讲,它有两个 equals 方法: boolean equals(String) 和 boolean equals(Object)
但其实类型擦除后会变成: boolean equals(Object) ,所以会和 Object 的 equals 冲突。
- 为什么冲突?
- 因为两个方法的方法签名不同,不是重写关系。而 T 擦除后变成 Object 就和父类中的发生了冲突。
- 如何补救?
- 重新命名引发冲突的方法。
除此之外。
泛型规范说明还引用了另一个原则:” 为了支持擦除转换,我们要施加一个限制:倘若两个接口类型是同意接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。“例如,下述代码是非法的:
Comparable<Employee>和Comparable<Manager>就是同一接口的不同参数化。
class Employee implements Comparable<Employee> {} | |
class Manager extends Employe implements Comparable<Manager> {} |
- 这可能会与合成的桥方法产生冲突,桥方法中会进行强制类型转换,所以不能在一个桥方法中使其转换为两个类型。
# 9.1.3 - 迭代器
可以认为 Java 迭代器位于两个元素之间。当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
注:
- InputStream 的 read 方法也是类似的。
- 调用 remove 之前没有调用 next 将是不合法的。
# 9.3 - Java 库中的具体集合
| 集合类型(数据结构) | 描述 | 参见 |
|---|---|---|
| ArrayList | 可以动态增长和缩减的一个索引序列 | 9.3.2 节 |
| LinkedList | 可以在任何位置高效插入和删除的一个有序序列 | 9.3.1 节 |
| ArrayDeque | 实现为循环数组的一个双端队列 | 9.3.5 节 |
| HashSet | 没有重复元素的一个无序集合 | 9.3.2 节 |
| TreeSet | 一个有序集 | 9.3.4 节 |
| EnumSet | 一个包含枚举类型值的集 | 9.4.6 节 |
| LinkedHashSet | 一个可以记住元素插入次序的集 | 9.4.5 节 |
| PriorityQueue | 允许高效删除最小元素的一个集合 | 9.3.6 节 |
| HashMap | 存储 键值对 的一个数据结构 | 9.4.4 节 |
| TreeMap | 键有序的一个映射 | 9.4 节 |
| EnumMap | 键属于枚举的一个映射 | 9.4.6 节 |
| LinkedHashMap | 可以记住 键值对 添加次序的一个集合 | 9.4.5 节 |
| WeakHashMap | 值不会在别处使用时就可以被垃圾回收的一个映射 | 9.4.4 节 |
| IdentityHashMap | 用 == 而不是用 equals 比较键的一个映射 | 9.4.7 节 |
值得一提的是:
在 Java 程序设计语言中,所有的链表实际上都是双向链接的 —— 即每个链接还存放着其前驱的引用。
# 9.3.1 - Iterator 的好兄弟 ListIterator
ListIterator 有两个方法可以用来反向遍历链表。
E previous()
boolean hasPrevious()与 next 一样,previous 方法返回越过的对象。
注:
- ListIterator 的 void add (E e)
- 将指定元素插入列表(可选操作)。该元素被插入到 next 将返回的元素(如果有)之前,以及在 previous 将返回的元素(如果有)之后。 (如果列表不包含任何元素,则新元素将成为列表中的唯一元素。)新元素插入到隐式光标之前:对 next 的后续调用将不受影响,对 previous 的后续调用将返回新元素。
-
# 在使用光标类比时要格外小心。不能连续调用两次 remove 方法,add 方法依赖于迭代器的位置,remove 方法依赖于迭代器的状态。以下是来自源码注释
IllegalStateException – 如果在最后一次调用 next 或 previous 之后既没有调用 next 也没有调用 previous ,或者 remove 或 add 没有被调用。
- 链表只跟踪对列表的结构性修改,例如添加和删除。**set 方法不被视为结构性修改。** 也就是不会增加 modCount 值
- get 方法做了个小优化,如果索引大于 size () / 2,就从列表尾端开始搜索元素。
# 9.3.3 - 散列集
直接复制粘贴韩顺平老师的一个源码解读视频笔记。
感谢韩顺平老师!
下面是视频地址
【零基础 快速学 Java】韩顺平 零基础 30 天学会 Java_哔哩哔哩_bilibili
干就完了!
以防忘记,写下来了一些笔记(其实大都是抄韩老师写的)
# 总结:
- HashSet 底层是 HashMap 实现
- 添加机制
- 添加一个元素时,先得到 Hash 值,Hash 值会转成 -> 索引值
- 找到存储数据表 table ,看这个位置是否已经存放元素
- 如果没有,则直接加入
- 如果有,调用 equals(该方法可重写,具体比较逻辑可由开发者定制) 比较,如果相同,就放弃添加,如果不同,则添加到最后
- 在 Java8 中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD (默认是 8) ,并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认 64),就会进行树化(红黑树)
- 如果当链表长度 > 8,但是 table 的大小还未 >= 64,那么会将 table 扩容(双倍扩容)
- 扩容机制
- 第一次添加元素直接扩容到 16 , 阈值为 16 * 0.75 == 12
- 当 table 长度到达 12 时就会准备扩容,第 13 个元素成功添加后就会扩容(双倍扩容),即扩容到 32。此时的阈值为 24
- 第 13 个元素是指:数组加链表的总元素个数,而不是单指在数组上的元素个数或者在链表上的元素个数
- 以此类推…
- Java 设计者买菜是不是也用补码算钱的,太强了啊!!!
# 源码解读:
# 示例代码:
public static void main(String[] args) { | |
HashSet<String> set = new HashSet<>(); | |
set.add("java"); | |
set.add("PHP"); | |
set.add("java"); | |
System.out.println("set = " + set); | |
} |
# line 3 :
HashSet 的无参构造创建 HashMap 对象,默认长度是 16,这里需要注意的是:长度并不会立即分配,而是在第一次添加元素时进行分配。

并将 DEFAULT_LOAD_FACTOR (默认负载系数)赋值给 loadFactor

# line 5 :
调用 HashMap 对象 map 的 public V put (K key, V value) 方法;PRESENT 只是填补 value 这个位置,传入参数 key 可变化,但 value 一直是 PRESENT (官方文档:要与后备映射中的对象关联的虚拟值)

再次调用 static final int hash (Object key) 方法,得到 key 的 hash 值
在 hash () 里判断传入参数 key 是否为空,若为空则返回 0,若不为空则根据 (h = key.hashCode()) ^ (h >>> 16) 算法(为避免碰撞)计算其 hash 值(不完全等价于 HashCode)并返回,作为 putVal () 方法的第一个参数

# putVal(…)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, | |
boolean evict) { | |
// 定义辅助变量 | |
Node<K,V>[] tab; Node<K,V> p; int n, i; | |
//table 是放 Node 结点的数组,类型是 Node [] | |
//if 语句表示 如果当前的 table 是 null,或者大小 == 0,则进行第一次扩容 | |
if ((tab = table) == null || (n = tab.length) == 0) | |
//resize ():第一次扩容 table 数组 | |
n = (tab = resize()).length; | |
// 根据 key 得到的 Hash 值去计算 key 应该存放到 table 表的哪个索引位置 | |
// 并把这个位置的对象,赋给 p | |
// 再判断 p 是否为 null | |
// 如果 p 为空:表示还未存放元素,就创建一个 Node(hash 用于比较是否相等,key 是传入参数,value 是 PREENT ,null 类似于 尾结点 | |
// 如果 p 不为空:line 18 :即 key 元素本应存放的位置已经存放了元素,被占用了,所以 table [...] 不为空 | |
if ((p = tab[i = (n - 1) & hash]) == null) | |
tab[i] = newNode(hash, key, value, null); | |
else { | |
// 开发技巧提示:局部变量在需要时再创建 | |
// 定义辅助变量 | |
Node<K,V> e; K k; | |
// 将准备添加的 key 的 hash 值 与 p (是当前索引位置的链表的第一个元素) 的 hash 值比较 | |
if (p.hash == hash && | |
// 并且满足 准备加入的 key 与 p 指向的 Node 结点的 key 是同一个对象 | |
// 或者 两者不是同一个对象,但两者 通过 p 指向的 Node 结点的 key 的 equals () 比较后 相同 | |
// 此 equals () 程序员可以定制 | |
((k = p.key) == key || (key != null && key.equals(k)))) | |
e = p; | |
// 判断 p 是不是一颗红黑树 | |
// 如果是 红黑树 ,就调用 putTreeVal () 方法添加 | |
else if (p instanceof TreeNode) | |
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); | |
// | |
else { | |
// 依次和该链表的每一个元素比较后,都不相同,则添加到该链表的最后 | |
for (int binCount = 0; ; ++binCount) { | |
// 由于前面已经比较了一次,这里不在比较 | |
// 判断是否到了链表的最后(链表最后一个结点的 next 是 null) | |
// 并且将结点 p 的尾结点 p.next 赋值给 e | |
// 由于这里比较的是尾结点是否为空,故当链表长度为 9 时才能使 binCount 为 7 (binCount 是从 0 开始,为 7 时循环 8 次) | |
if ((e = p.next) == null) { | |
// 已经到了链表末尾,用传入参数 key 创建一个 Node 结点添加到最后一个结点 p 的末尾,即添加到 p.next | |
p.next = newNode(hash, key, value, null); | |
// 添加元素到链表后,立即判断该链表是否已经达到 8 个结点 | |
// 如果达到,就调用 treeifyBin () 对当前链表进行树化(转成红黑树) | |
// !!!注意:在转成 红黑树 时,要进行判断,详见下方 treeifyBin (tab, hash) | |
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st | |
treeifyBin(tab, hash); | |
break; | |
} | |
// 没有到末尾,则比较 传入参数 key 与 当前索引位置的第一个元素的下一个元素(因为前面 e = p.next) 比较是否相等 | |
// 比较逻辑同前文一样 | |
if (e.hash == hash && | |
((k = e.key) == key || (key != null && key.equals(k)))) | |
break; | |
p = e; | |
} | |
} | |
if (e != null) { // existing mapping for key | |
V oldValue = e.value; | |
if (!onlyIfAbsent || oldValue == null) | |
e.value = value; | |
afterNodeAccess(e); | |
return oldValue; | |
} | |
} | |
++modCount; | |
// threshold == 12 | |
// 判断长度是否大于 12 ,是否进行扩容 | |
if (++size > threshold) | |
resize(); | |
//hashMap 留给其子类的方法,此方法在 HashMap 中为空 | |
afterNodeInsertion(evict); | |
return null; | |
} |
# line 9 :resize()
final Node<K,V>[] resize() { | |
// table == 0 | |
Node<K,V>[] oldTab = table; | |
int oldCap = (oldTab == null) ? 0 : oldTab.length; | |
int oldThr = threshold; | |
int newCap, newThr = 0; | |
if (oldCap > 0) { | |
... | |
// initial capacity was placed in threshold | |
else if (oldThr > 0) | |
newCap = oldThr; | |
// zero initial threshold signifies using defaults | |
else { | |
// 扩容长度 : 16(DEFAULT_INITIAL_CAPACITY == 1 << 4 == 16 | |
newCap = DEFAULT_INITIAL_CAPACITY; | |
// 确定阈值:当 table 长度到达 16 * DEFAULT_LOAD_FACTOR(0.75) == 12 的时候就准备扩容,防止当操作量比较大时发生阻塞 | |
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); | |
} | |
if (newThr == 0) { | |
float ft = (float)newCap * loadFactor; | |
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? | |
(int)ft : Integer.MAX_VALUE); | |
} | |
threshold = newThr; | |
@SuppressWarnings({"rawtypes","unchecked"}) | |
// 创建一个长度为 newCap(16) 的 Node [] | |
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; | |
// 并赋值给 table ,所以 table 的长度也是 16 | |
table = newTab; | |
if (oldTab != null) { | |
... | |
} | |
return newTab; | |
} |
# line : 29 :treeifyBin(tab, hash):
// 当链表的长度大于等于 8 时,进入此方法 | |
final void treeifyBin(Node<K,V>[] tab, int hash) { | |
int n, index; Node<K,V> e; | |
// 若 tab 的长度还未到达 MIN_TREEIFY_CAPACITY (64) ,则先进行扩容,暂时不树化 | |
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) | |
resize(); | |
else if ((e = tab[index = (n - 1) & hash]) != null) { | |
TreeNode<K,V> hd = null, tl = null; | |
do { | |
TreeNode<K,V> p = replacementTreeNode(e, null); | |
if (tl == null) | |
hd = p; | |
else { | |
p.prev = tl; | |
tl.next = p; | |
} | |
tl = p; | |
} while ((e = e.next) != null); | |
if ((tab[index] = hd) != null) | |
hd.treeify(tab); | |
} | |
} |
# 课后疑问:
为什么在链表长 >= 8 时,原本在 table [4] 位置的链表被移到了 table [36]?
# 问题分析:
当链表长度 >= 8 且 table 长度 < 64 时,会在 resize () 方法 即本文 **line : 29 :treeifyBin (tab, hash):** 代码块中的 line :6 处进入 resize () 方法并进行扩容,此处将 resize () 方法 的代码补全,并加以分析
# resize () 方法源码分析:
假设只在 table [4] 位置存在一个长度为 10 的链表
链表长度为 9 时第一次扩容 table.length 16 --> 32
链表长度为 10 时第二次扩容 table.length 32 --> 64
final Node<K,V>[] resize() { | |
// 将 table 备份 | |
Node<K,V>[] oldTab = table; | |
// 判断是不是第一次扩容 | |
// 第一次扩容:上一个 table 的长度为 0 | |
// 否则:得到上一个 table 的实际长度(包括 null) | |
int oldCap = (oldTab == null) ? 0 : oldTab.length; | |
// 备份 threshold | |
int oldThr = threshold; | |
// 定义辅助变量 | |
int newCap, newThr = 0; | |
if (oldCap > 0) { | |
// 判断 table 长度是否超过 2 的 30 次方,即数组的最大长度 | |
if (oldCap >= MAXIMUM_CAPACITY) { | |
// 修改阈值为 int 的最大值 (2^31-1),这样以后就不会扩容了 | |
threshold = Integer.MAX_VALUE; | |
return oldTab; | |
} | |
// 将旧的 table 长度 左移一位:即乘以 2 ,再赋值给 新的 table 长度即 newCap | |
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && | |
oldCap >= DEFAULT_INITIAL_CAPACITY) | |
// 将 旧的阈值也 乘以 2 | |
newThr = oldThr << 1; // double threshold | |
} | |
/* | |
// 这一段源码在本次操作中并不会涉及,故注释一下 | |
//initial capacity was placed in threshold | |
else if (oldThr> 0) | |
newCap = oldThr; | |
//zero initial threshold signifies using defaults | |
else { | |
newCap = DEFAULT_INITIAL_CAPACITY; | |
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); | |
} | |
if (newThr == 0) { | |
float ft = (float) newCap * loadFactor; | |
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? | |
(int) ft : Integer.MAX_VALUE); | |
} | |
*/ | |
// 将 翻倍后的 阈值 赋值给此对象的 threshold | |
threshold = newThr; | |
@SuppressWarnings({"rawtypes","unchecked"}) | |
// 定义一个 长度为 newCap(即翻倍后的 oldCap)的 Node 数组 | |
// 来储存原 Node 数组内的元素 | |
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; | |
// 将原 Node [] 即 table 覆盖,或者说 使 table 指向新的 Node [] | |
table = newTab; | |
if (oldTab != null) { | |
// 循环遍历 原 Node [] 中的每一个元素 | |
for (int j = 0; j < oldCap; ++j) { | |
// 定义辅助变量,储存从原 Node [] 取出的 Node 对象 | |
Node<K,V> e; | |
// 将原 Node [] 中的第 j 个元素赋值给 e,相当于备份 | |
// 并判断其是否为空,此时:第四个元素不为空,其余都为空 | |
if ((e = oldTab[j]) != null) { | |
// 将原来存有元素的位置用 null 替换 | |
oldTab[j] = null; | |
// 判断该位置是否形成链表 | |
if (e.next == null) | |
// 没有形成链表,则直接将备份的 e 以相同的方式计算出其在 Node [] 中的位置后赋值到该位置 | |
newTab[e.hash & (newCap - 1)] = e; | |
// 已经形成链表,判断是否为红黑树,此时未树化 | |
// 暂时未学习数据结构,暂不探究 | |
else if (e instanceof TreeNode) | |
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); | |
// 进入到这 | |
else { // preserve order | |
// 定义辅助变量 | |
Node<K,V> loHead = null, loTail = null; | |
Node<K,V> hiHead = null, hiTail = null; | |
Node<K,V> next; | |
// 将此链表上的元素通过 do...while (...) 循环放到一个新的链表上 | |
do { | |
next = e.next; | |
// 关于这里的按位与,会在下面放一段链接,看完就懂 | |
if ((e.hash & oldCap) == 0) { | |
if (loTail == null) | |
loHead = e; | |
else | |
loTail.next = e; | |
loTail = e; | |
} | |
else { | |
if (hiTail == null) | |
hiHead = e; | |
else | |
hiTail.next = e; | |
hiTail = e; | |
} | |
} while ((e = next) != null); | |
// 如果前面按位与的结果是 0,则将复制的链表放回原位置 | |
if (loTail != null) { | |
loTail.next = null; | |
newTab[j] = loHead; | |
} | |
// 如果前面按位与的结果不为 0,则将复制的链表放到 j + oldCap 处 | |
if (hiTail != null) { | |
hiTail.next = null; | |
newTab[j + oldCap] = hiHead; | |
} | |
} | |
} | |
} | |
} | |
return newTab; | |
} |
line 77: Java HashMap 中在 resize () 时候的 rehash, 即再哈希法的理解_请叫我大师兄_的博客 - CSDN 博客_rehash 和 resize
# 9.5.1 - Java 9 中集合的新特性
Java 9 引入了一些静态方法,可以生成给定元素的集或列表,以及给定键值对的映射。List 接口、 Set 接口、Map 接口有 11 个方法,分别有 0 到 10 个参数,其中 List 接口、Set 接口还有一个额外的 参数个数可变的 of 方法。提供这种特定性是为了提高效率。-> 即减少使用可变参数所带来的创建数组以及回收数组所带来的消耗。



需要注意的是:
- 其中的元素、键、值不能为 null。
- 这些集合对象是不可更改的,如果试图改变他们的内容,会得到一个 UnsupportedOperationException 异常。
![]()
- 若要得到一个可更改的,可以把这个集合传递到集合的构造器中中。
另外,Java 9 中的 Map 引入了另一个方法: static <K, V> Entry<K, V> entry(K k, V v) ,这个静态方法可以得到一个 Map.Entry 的对象,可以作为对组,存储一对元素。
在 Java 9 之前这会很麻烦,你必须使用
new AbstractNap.SimpleImmutableEntry<>(first, second)构造对象。
我什么不使用 Object[] twoObj = new Object[2]; ??
# 9.5.3 - 不可修改的视图
Collections 的 一系列方法将帮助我们得到一个不可修改的视图:

需要注意的是:
- 不可修改的视图并不是集合本身不可更改。仍然可以通过集合的原始引用对集合进行修改,并且仍然可以对集合的元素调用更改器方法。
- 不可修改的集合中的 equals 方法以及 hashCode 方法将直接使用两个集合的对象地址,不在比较其元素内容。
- AbstractList 中的 equals 和 hashCode:
![]()
![]()
- UnmodifiableList 中的 equals 和 hashCode:
![]()
- AbstractList 中的 equals 和 hashCode:
# 9.6 - 集合中的算法
先欠着,学完排序再来。
# 12.4.10 - 原子性
原子性是指一个操作不可被中断,要么全部执行成功要么全部执行失败;
java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而没有使用锁)来保证其他操作的原子性。
# 12.4.12 - 线程局部变量
使用 ThreadLocal 辅助类为各个线程提供各自的实例。例如:将 SqlSession 对象存入 ThreadLocal 中,以保证 service 层的操作具有原子性。
# 12.5.1 - 阻塞队列
很多线程问题可以使用一个或多个队列以优雅而安全的方式来描述。
所以 java.util.concurrent 包提供了阻塞队列的几个变体。
默认情况下:
- LinkedBlockingQueue 的容量没有上届,但是也可以选择指定一个最大容量。
- LinkedBlockingDeque 是一个双端队列。
- ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。
- PriorityBlockingQueue 是一个优先队列,而不是先进先出队列。
# 12.5.2 - 高效的映射、集、队列
java.util.concurrent 包提供了映射,有序集、队列的高效实现:ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet, ConcurrentLinkedQueue。

注:
- 获取集合长度应该使用
**mappingCount()**而不是**size()**,因为 ConcurrentHashMap 可能包含比 int 表示的更多映射。返回的值是估计值;如果存在并发插入或删除,实际计数可能会有所不同。 - 集合返回弱一致性的迭代器:即迭代器不一定能反映出他们构造之后的所有更改,但不会抛出 ConcurrentModificationException 异常。
- 默认情况下并发散列映射可以有至多 16 个同时运行的书写器线程。如果有更多的,那么多余的将阻塞。
- ConcurrentHashMap 不允许有 null 值,因为许多方法都使用 null 值来指示某个映射不存在。(HashMap 允许存在一个 null 键)
# 12.6.2 - 执行器
执行器 (
Executors) 有许多静态工厂方法,用来构造线程池,如下表
| 方法 | 描述 |
|---|---|
| newCachedThreadPool | 必要时创建新线程,空闲线程会保留 60 s |
| newFixedThreadPool | 池中包含固定数目的线程,空闲线程会一直保留 |
| newWorkStealingPool | 一种适合 “fork-join” 任务的线程池,其中复杂的任务会分解为简单的任务,空闲线程会 “密取” 较简单的任务 |
| newSingleThreadExector | 只有一个线程的 “池”,会顺序地执行所提交地任务 |
| newScheduledThreadPool | 用于调度执行地固定线程池 |
| newSingleThreadScheduledExecutor | 用于调度执行地单线程 “池” |
注:
- 为了得到最优的运行速度,并发线程数等于处理器地内核数。
- 也可以使用如下方法自定义一个线程池:
ExecutorService executorService = new ThreadPoolExecutor(//...);
public ThreadPoolExecutor(int corePoolSize, | |
int maximumPoolSize, | |
long keepAliveTime, | |
TimeUnit unit, | |
BlockingQueue<Runnable> workQueue, | |
ThreadFactory threadFactory, | |
RejectedExecutionHandler handler) | |
/* | |
使用给定的初始参数创建一个新的 ThreadPoolExecutor 。 | |
参形: | |
corePoolSize - 保留在池中的线程数,即使它们是空闲的,除非设置 allowCoreThreadTimeOut | |
maximumPoolSize – 池中允许的最大线程数 | |
keepAliveTime – 当线程数大于核心时,这是多余的空闲线程在终止前等待新任务的最长时间。 | |
unit – keepAliveTime 参数的时间单位 | |
workQueue – 用于在执行任务之前保存任务的队列。此队列将仅保存由 execute 方法提交的 Runnable 任务。 | |
threadFactory – 执行器创建新线程时使用的工厂 | |
handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。 | |
CallerRunsPolicy:提交任务的线程自己去执行该任务。 | |
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。 | |
DiscardPolicy:直接丢弃任务,没有任何异常抛出。 | |
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。 | |
抛出: | |
IllegalArgumentException – 如果以下条件之一成立: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize | |
NullPointerException – 如果 workQueue 或 threadFactory 或 handler 为 null | |
*/ |
# 12.6.4 - fork-join 框架
先画个饼,学完分治算法再回来



