完整教程:JDK源码阅读篇——持续更新
以下是按“基础→进阶→深入”顺序整理的 JDK 核心类阅读路线图及核心问题解析
JDK 核心类阅读路线图(基础→进阶→深入)
一、阅读路线总览(3 阶段)
按“基础入门→能力提升→深入拓展”循序渐进,每个阶段明确目标、前置知识和核心类,帮助建立系统化的源码阅读体系。
阶段 1:基础入门(1-2 天)
- 目标:熟悉最常用类的实现逻辑,建立源码阅读习惯。
- 前置知识:Java 基础语法、面向对象概念、简单数据结构(数组、链表)。
- 核心类:
String、StringBuilder、StringBuffer、ArrayList、LinkedList
阶段 2:能力提升(2-3 天)
- 目标:理解经典设计思想和复杂数据结构,接触并发基础。
- 前置知识:阶段 1 内容、哈希表原理、简单并发概念(线程、锁)。
- 核心类:
HashMap、HashSet、ConcurrentHashMap(基础部分)、Iterator接口
阶段 3:深入拓展(按需选择)
- 目标:根据职业方向(如并发、JVM)深入专项领域,提升技术深度。
- 前置知识:阶段 1-2 内容、JVM 基础、并发编程原理。
- 核心类/模块:
- 并发方向:
ConcurrentHashMap(深入)、ReentrantLock、ThreadPoolExecutor、CountDownLatch - 集合进阶:
TreeMap(红黑树)、LinkedHashMap、CopyOnWriteArrayList - JVM 关联:
Integer(缓存机制)、ClassLoader(类加载)
- 并发方向:
二、阅读小贴士
- 建议使用 IDEA 阅读源码,它能快速跳转方法、查看继承关系,大幅提升效率。
- 遇到复杂逻辑(如红黑树旋转),可先画流程图,再对照源码理解,不要死记硬背。
- 每天固定 1-2 小时阅读,重点在于“理解”而非“记住”,读完后可写笔记总结核心逻辑。
核心类问题解析
一、String 相关
1. 为什么 String 是不可变的?
String 的不可变性(Immutable)是 Java 设计中的经典决策,核心原因体现在底层实现和设计目标两方面。
(1)底层实现:value 数组的不可修改性
String 类的核心存储是 private final char value[](JDK9 后改为 byte[],原理一致):
private:外部无法直接访问该数组,避免外部修改。final:数组引用本身不可变(即value不能指向新数组),且 String 类无修改数组元素的方法(如setCharAt())。- 注:通过反射可强制修改
value数组元素,但属于非常规操作,会破坏不可变性。
(2)设计目标:安全性、高性能与可靠性
- 线程安全:不可变对象天然线程安全,多线程环境下无需额外同步,可直接共享。
- 缓存优化:String 常量池(如
String.intern())依赖不可变性。若 String 可变,常量池中的值被修改后,所有引用它的变量都会受影响,导致逻辑混乱。 - 哈希表友好:String 常作为
HashMap的 key,其hashCode()计算依赖value数组。不可变性保证hashCode()一旦计算就不会改变,避免哈希表存储位置失效。 - 安全性:网络通信、文件操作等场景中,String 常作为参数(如 URL、文件名),不可变性可防止传递过程中被篡改,保证信息安全。
2. intern() 方法的作用是什么?
intern() 是 String 类的 native 方法,核心作用是将字符串加入常量池并返回常量池中的引用,实现字符串复用,节省内存。
(1)具体逻辑
当调用 s.intern() 时,JVM 会检查字符串常量池:
- 若已存在与
s内容相同的字符串,直接返回常量池中该字符串的引用。 - 若不存在,将当前字符串
s加入常量池(JDK7+ 后把堆中字符串的引用放入常量池,而非复制字符数组),并返回该引用。
(2)示例
String s1 = new String("abc"); // 堆中创建对象,常量池已有 "abc"
String s2 = s1.intern(); // 返回常量池中的 "abc" 引用
String s3 = "abc"; // 直接指向常量池中的 "abc"
System.out.println(s1 == s2); // false(s1 是堆对象,s2 是常量池引用)
System.out.println(s2 == s3); // true(两者都指向常量池)
(3)应用场景
- 频繁使用相同字符串(如数据库字段名、固定配置)时,
intern()可减少重复对象创建,降低内存消耗。 - 注意:JDK7+ 后常量池移至堆中,
intern()性能更优,但过度使用可能导致常量池膨胀,需谨慎。
(4)源码注释
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
二、StringBuilder 与 StringBuffer
1. 核心区别:线程安全性与性能
最核心的区别在于线程安全性,这直接导致性能差异,具体对比如下:
| 特性 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(方法加 synchronized) |
| 性能 | 较高(无同步开销) | 较低(有同步开销) |
| 适用场景 | 单线程环境(如普通字符串拼接) | 多线程环境(如并发字符串操作) |
2. 源码差异示例(以 append(String str) 为例)
(1)StringBuilder.append()(非同步)
@Override
public StringBuilder append(String str) {
super.append(str); // 直接调用父类方法,无同步
return this;
}
(2)StringBuffer.append()(同步)
@Override
public synchronized StringBuffer append(String str) {
super.append(str); // 加了 synchronized 修饰,保证线程安全
return this;
}
3. 扩容机制(两者逻辑一致)
(1)ensureCapacityInternal:扩容入口检查
该方法是扩容的“开关”,作用是判断当前数组容量是否足够,不够则触发扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// 1. 计算“所需最小容量”与“当前数组长度”的差值
// 差值 > 0 说明当前容量不够,需要扩容
if (minimumCapacity - value.length > 0) {
// 2. 调用 newCapacity 计算新容量,通过 Arrays.copyOf 完成扩容
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
- 参数
minimumCapacity:容纳新内容所需的最小容量(由append/insert等方法根据新增内容计算)。 - 触发条件:仅当
minimumCapacity大于当前数组长度(value.length)时,才会扩容。
(2)newCapacity:核心扩容逻辑
负责计算“新容量”的具体值(JDK 8 源码):
private int newCapacity(int minCapacity) {
// 1. 默认新容量:旧容量 * 2 + 2(“2倍+2”规则)
int newCapacity = (value.length << 1) + 2; // 等价于 value.length * 2 + 2
// 2. 若默认新容量仍小于最小容量,直接用最小容量作为新容量
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 3. 检查是否溢出(超过 Integer.MAX_VALUE 则抛出 OOM)
return (newCapacity <= 0 || Integer.MAX_VALUE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
// 处理超大容量(超出 Integer.MAX_VALUE 时)
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) {
// 所需容量超过 Integer.MAX_VALUE,直接抛出 OOM
throw new OutOfMemoryError();
}
// 否则,用 Integer.MAX_VALUE 作为最大容量
return (minCapacity > Integer.MAX_VALUE - 8) ? Integer.MAX_VALUE : Integer.MAX_VALUE - 8;
}
(3)扩容完整流程(举例)
假设当前 StringBuilder 状态:value.length = 16(初始容量),已使用长度 count = 10,调用 append("abcdefgh")(新增 8 个字符):
- 计算
minimumCapacity = 10 + 8 = 18。 - 进入
ensureCapacityInternal(18):18 - 16 = 2 > 0→ 需要扩容。 - 调用
newCapacity(18):默认新容量16*2+2=34,且34>18→ 新容量为 34。 - 通过
Arrays.copyOf(value, 34)创建新数组,拷贝旧数据,value指向新数组。
(4)关键细节
- 为什么是“2倍+2”?早期 JDK 设计为小容量时预留额外空间,减少频繁扩容(如 16→34,而非 32),大容量下+2 影响可忽略。
- 新增内容过大时:若
minimumCapacity远大于“2倍+2”,直接用minimumCapacity作为新容量,避免多次扩容。 - 容量上限:最大容量为
Integer.MAX_VALUE(约 20 亿),超过则抛出OutOfMemoryError。
三、ArrayList 扩容机制
1. 核心参数说明
minCapacity:当前所需最小容量(“当前元素个数 + 新增元素个数”,确保容纳新元素)。elementData:ArrayList 底层存储元素的数组(真正存放数据的容器)。
2. 扩容步骤详解
(1)获取旧容量
int oldCapacity = elementData.length;
oldCapacity是当前数组长度(即当前容量,非元素个数size)。
(2)计算默认新容量(核心规则)
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity >> 1等价于oldCapacity / 2(整数除法)。- 新容量 = 旧容量 * 1.5 倍(如旧容量 10→15,16→24),平衡内存占用和扩容频率。
(3)确保新容量不小于最小所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
- 若 1.5 倍旧容量仍小于
minCapacity,直接用minCapacity作为新容量(如旧容量 10,minCapacity=20→ 新容量改为 20)。
(4)处理超大容量(超过最大限制)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
MAX_ARRAY_SIZE是数组最大容量限制(Integer.MAX_VALUE - 8,预留 8 字节给数组头信息)。hugeCapacity处理逻辑:private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 整数溢出导致 minCapacity 为负数 throw new OutOfMemoryError(); // 若最小容量超过 Integer.MAX_VALUE 则 OOM,否则用 MAX_ARRAY_SIZE return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
(5)拷贝数组(完成扩容)
elementData = Arrays.copyOf(elementData, newCapacity);
- 通过
Arrays.copyOf创建新数组(容量为newCapacity),拷贝旧数据,更新elementData指向新数组。
3. 举例说明扩容流程
假设 ArrayList 状态:elementData.length=10(旧容量 10),size=10(数组已满),调用 add(5个元素):
- 计算
minCapacity = 10 + 5 = 15。 - 旧容量 10,默认新容量 = 10 + 5 = 15。
- 15 >= 15 → 新容量保持 15。
- 15 < MAX_ARRAY_SIZE → 无需特殊处理。
- 拷贝旧数组到新数组(容量 15),
elementData指向新数组,扩容完成。
4. 扩容逻辑总结
- 默认扩容为旧容量的 1.5 倍;
- 若 1.5 倍仍不够,直接用所需最小容量;
- 若超过最大限制,用
Integer.MAX_VALUE或抛出 OOM; - 最后通过数组拷贝完成扩容。
四、LinkedList 双向链表实现与特性
1. 双向链表的实现原理(结合 linkFirst() 和 linkLast())
LinkedList 底层是双向链表,核心是 Node 节点类串联数据,同时维护头尾指针。
(1)核心结构
- Node 节点:每个节点包含三个部分
prev:指向当前节点的前一个节点(前驱)item:当前节点存储的元素next:指向当前节点的后一个节点(后继)
- LinkedList 指针
first:指向链表的第一个节点(头节点)last:指向链表的最后一个节点(尾节点)
(2)linkFirst(E e):头部添加元素
作用:将新元素 e 插入链表头部,成为新头节点。
private void linkFirst(E e) {
final Node<E> f = first; // 保存当前头节点(旧头节点)// 创建新节点:前驱为 null(头部无前驱),后继为旧头节点 ffinal Node<E> newNode = new Node<>(null, e, f);first = newNode; // 更新头指针为新节点if (f == null) {// 原链表为空,新节点同时作为尾节点last = newNode;} else {// 原链表非空,旧头节点的前驱指向新节点(完成双向关联)f.prev = newNode;}size++; // 链表长度+1modCount++; // 修改次数+1(用于迭代器快速失败)}
- 图示:原链表
first -> A <-> B <-> C <- last→ 调用后first -> D <-> A <-> B <-> C <- last
(3)linkLast(E e):尾部添加元素
作用:将新元素 e 插入链表尾部,成为新尾节点。
void linkLast(E e) {
final Node<E> l = last; // 保存当前尾节点(旧尾节点)// 创建新节点:前驱为旧尾节点 l,后继为 null(尾部无后继)final Node<E> newNode = new Node<>(l, e, null);last = newNode; // 更新尾指针为新节点if (l == null) {// 原链表为空,新节点同时作为头节点first = newNode;} else {// 原链表非空,旧尾节点的后继指向新节点(完成双向关联)l.next = newNode;}size++; // 链表长度+1modCount++; // 修改次数+1}
- 图示:原链表
first -> A <-> B <-> C <- last→ 调用后first -> A <-> B <-> C <-> D <- last
2. “增删快、查询慢”的原因
(1)增删快(头尾或已知节点附近操作)
- 增删本质:只需修改相邻节点的
prev和next指针,无需移动其他元素。- 头部/尾部插入/删除:仅修改头尾指针和相邻节点关联,时间复杂度 O(1)。
- 中间增删:若已知目标节点(如迭代器定位),仅修改前后节点指针,时间复杂度 O(1)。
- 对比 ArrayList:增删需移动大量后续元素,最坏时间复杂度 O(n)。
(2)查询慢(随机访问效率低)
- 查询本质:双向链表无下标,无法像数组那样通过“首地址+偏移量”直接定位(数组随机访问 O(1))。
- 示例:获取第
i个元素(get(i)),需从first或last开始遍历,最多遍历i次或size-i次,时间复杂度 O(n)。 - 原因:链表节点在内存中分散存储(非连续空间),只能依赖指针逐个访问。
3. 总结
- LinkedList 通过
Node的prev/next指针实现双向链表,通过first/last指针快速定位头尾,实现高效增删。 - 增删快:仅修改指针,无需移动元素(已知节点时 O(1))。
- 查询慢:无下标,需遍历节点(O(n))。