完整教程:JDK源码阅读篇——持续更新

完整教程: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、CopyOnWriteArrayListJVM 关联: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.

*

* A pool of strings, initially empty, is maintained privately by the

* class {@code String}.

*

* 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.

*

* 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}.

*

* All literal strings and string-valued constant expressions are

* interned. String literals are defined in section 3.10.5 of the

* The Java™ Language Specification.

*

* @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 与 StringBuffer1. 核心区别:线程安全性与性能最核心的区别在于线程安全性,这直接导致性能差异,具体对比如下:

特性StringBuilderStringBuffer线程安全非线程安全线程安全(方法加 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 f = first; // 保存当前头节点(旧头节点)

// 创建新节点:前驱为 null(头部无前驱),后继为旧头节点 f

final Node newNode = new Node<>(null, e, f);

first = newNode; // 更新头指针为新节点

if (f == null) {

// 原链表为空,新节点同时作为尾节点

last = newNode;

} else {

// 原链表非空,旧头节点的前驱指向新节点(完成双向关联)

f.prev = newNode;

}

size++; // 链表长度+1

modCount++; // 修改次数+1(用于迭代器快速失败)

}

图示:原链表 first -> A <-> B <-> C <- last → 调用后 first -> D <-> A <-> B <-> C <- last(3)linkLast(E e):尾部添加元素作用:将新元素 e 插入链表尾部,成为新尾节点。

void linkLast(E e) {

final Node l = last; // 保存当前尾节点(旧尾节点)

// 创建新节点:前驱为旧尾节点 l,后继为 null(尾部无后继)

final Node newNode = new Node<>(l, e, null);

last = newNode; // 更新尾指针为新节点

if (l == null) {

// 原链表为空,新节点同时作为头节点

first = newNode;

} else {

// 原链表非空,旧尾节点的后继指向新节点(完成双向关联)

l.next = newNode;

}

size++; // 链表长度+1

modCount++; // 修改次数+1

}

图示:原链表 first -> A <-> B <-> C <- last → 调用后 first -> A <-> B <-> C <-> D <- last2. “增删快、查询慢”的原因(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))。

相关推荐