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

以下是按“基础→进阶→深入”顺序整理的 JDK 核心类阅读路线图及核心问题解析

JDK 核心类阅读路线图(基础→进阶→深入)

一、阅读路线总览(3 阶段)

按“基础入门→能力提升→深入拓展”循序渐进,每个阶段明确目标、前置知识和核心类,帮助建立系统化的源码阅读体系。

阶段 1:基础入门(1-2 天)

  • 目标:熟悉最常用类的实现逻辑,建立源码阅读习惯。
  • 前置知识:Java 基础语法、面向对象概念、简单数据结构(数组、链表)。
  • 核心类StringStringBuilderStringBufferArrayListLinkedList

阶段 2:能力提升(2-3 天)

  • 目标:理解经典设计思想和复杂数据结构,接触并发基础。
  • 前置知识:阶段 1 内容、哈希表原理、简单并发概念(线程、锁)。
  • 核心类HashMapHashSetConcurrentHashMap(基础部分)、Iterator 接口

阶段 3:深入拓展(按需选择)

  • 目标:根据职业方向(如并发、JVM)深入专项领域,提升技术深度。
  • 前置知识:阶段 1-2 内容、JVM 基础、并发编程原理。
  • 核心类/模块
    • 并发方向:ConcurrentHashMap(深入)、ReentrantLockThreadPoolExecutorCountDownLatch
    • 集合进阶:TreeMap(红黑树)、LinkedHashMapCopyOnWriteArrayList
    • JVM 关联:Integer(缓存机制)、ClassLoader(类加载)

二、阅读小贴士

  1. 建议使用 IDEA 阅读源码,它能快速跳转方法、查看继承关系,大幅提升效率。
  2. 遇到复杂逻辑(如红黑树旋转),可先画流程图,再对照源码理解,不要死记硬背。
  3. 每天固定 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&trade; 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. 核心区别:线程安全性与性能

最核心的区别在于线程安全性,这直接导致性能差异,具体对比如下:

特性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 个字符):

  1. 计算 minimumCapacity = 10 + 8 = 18
  2. 进入 ensureCapacityInternal(18)18 - 16 = 2 > 0 → 需要扩容。
  3. 调用 newCapacity(18):默认新容量 16*2+2=34,且 34>18 → 新容量为 34。
  4. 通过 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个元素)

  1. 计算 minCapacity = 10 + 5 = 15
  2. 旧容量 10,默认新容量 = 10 + 5 = 15。
  3. 15 >= 15 → 新容量保持 15。
  4. 15 < MAX_ARRAY_SIZE → 无需特殊处理。
  5. 拷贝旧数组到新数组(容量 15),elementData 指向新数组,扩容完成。

4. 扩容逻辑总结

  1. 默认扩容为旧容量的 1.5 倍;
  2. 若 1.5 倍仍不够,直接用所需最小容量;
  3. 若超过最大限制,用 Integer.MAX_VALUE 或抛出 OOM;
  4. 最后通过数组拷贝完成扩容。

四、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(头部无前驱),后继为旧头节点 f
  final Node<E> 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<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++; // 链表长度+1
    modCount++; // 修改次数+1
    }
  • 图示:原链表 first -> A <-> B <-> C <- last → 调用后 first -> A <-> B <-> C <-> D <- last

2. “增删快、查询慢”的原因

(1)增删快(头尾或已知节点附近操作)
  • 增删本质:只需修改相邻节点的 prevnext 指针,无需移动其他元素。
    • 头部/尾部插入/删除:仅修改头尾指针和相邻节点关联,时间复杂度 O(1)。
    • 中间增删:若已知目标节点(如迭代器定位),仅修改前后节点指针,时间复杂度 O(1)。
  • 对比 ArrayList:增删需移动大量后续元素,最坏时间复杂度 O(n)。
(2)查询慢(随机访问效率低)
  • 查询本质:双向链表无下标,无法像数组那样通过“首地址+偏移量”直接定位(数组随机访问 O(1))。
  • 示例:获取第 i 个元素(get(i)),需从 firstlast 开始遍历,最多遍历 i 次或 size-i 次,时间复杂度 O(n)。
  • 原因:链表节点在内存中分散存储(非连续空间),只能依赖指针逐个访问。

3. 总结

  • LinkedList 通过 Nodeprev/next 指针实现双向链表,通过 first/last 指针快速定位头尾,实现高效增删。
  • 增删快:仅修改指针,无需移动元素(已知节点时 O(1))。
  • 查询慢:无下标,需遍历节点(O(n))。
posted on 2025-11-24 15:28  ljbguanli  阅读(0)  评论(0)    收藏  举报