Java面试宝典:从基础到架构2

第2章 Java集合框架深度解析

面试频率:★★★★★

集合框架是Java面试的"绝对主力"。HashMap源码、ConcurrentHashMap演进、ArrayList扩容------这些问题几乎出现在每一场Java面试中。本章不满足于"知道",而是带你"看透":从源码级剖析到面试连环追问,让你在面试中真正做到胸有成竹。

学习目标

  1. 掌握HashMap从put到扩容的完整源码流程,理解红黑树化的触发条件
  2. 理解HashMap线程不安全的具体表现及ConcurrentHashMap的解决方案
  3. 熟悉ArrayList扩容机制与序列化优化
  4. 能手写LRU缓存,理解LinkedHashMap的访问序实现
  5. 掌握集合选型决策树,能根据场景选择合适的集合

2.1 集合体系总览------List/Set/Map/Queue四大家族

面试官怎么问

"说说Java集合框架的整体结构。"

核心体系图

复制代码
Iterable
├── Collection
│   ├── List(有序可重复)
│   │   ├── ArrayList    → 数组,随机访问O(1),插入删除O(n)
│   │   ├── LinkedList   → 双向链表,随机访问O(n),头尾操作O(1)
│   │   └── Vector       → 线程安全的ArrayList(已过时)
│   ├── Set(无序不可重复)
│   │   ├── HashSet      → HashMap包装器
│   │   ├── LinkedHashSet → LinkedHashMap包装器,保持插入序
│   │   └── TreeSet      → TreeMap包装器,排序
│   └── Queue(队列)
│       ├── PriorityQueue → 优先队列,堆实现
│       ├── ArrayDeque    → 循环数组双端队列
│       └── LinkedList    → 同时实现Deque接口
└── Map(键值对)
    ├── HashMap          → 数组+链表+红黑树
    ├── LinkedHashMap    → HashMap+双向链表(插入序/访问序)
    ├── TreeMap          → 红黑树排序
    ├── ConcurrentHashMap → 线程安全HashMap
    └── Hashtable        → 全表锁线程安全(已过时)

关键接口对比

接口 核心契约 允许null 线程安全
List 有序可重复 否(Vector除外)
Set 唯一性 HashSet允许一个null
Queue FIFO/LIFO/优先级 否(PriorityQueue)
Map key唯一 HashMap允许null key/value 否(CHM/Hashtable除外)

2.2 ArrayList源码分析

面试官怎么问

"ArrayList的扩容机制是怎样的?和LinkedList怎么选?"

底层数据结构

java 复制代码
public class ArrayList<E> {
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空数组(无参构造时共享)
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默认空数组(区分首次添加时扩容到10)
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 底层数组------transient:自定义序列化
    transient Object[] elementData;
    // 元素数量
    private int size;
}

扩容机制

java 复制代码
// 扩容核心方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量的1.5倍(右移1位 = 除以2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果1.5倍仍不够,直接用minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 如果超过最大值,用Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 复制到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容时间线

复制代码
new ArrayList()         → elementData = {}(空数组,size=0)
第1次add                → 扩容到10
第11次add               → 扩容到15(10 * 1.5)
第16次add               → 扩容到22(15 * 1.5)
...

序列化优化

ArrayList重写了writeObjectreadObject,只序列化实际元素而非整个数组:

java 复制代码
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
    // 只写入size个元素,而非elementData.length个
    s.writeInt(size);
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }
}

面试追问:为什么用transient修饰elementData?

elementData的长度通常远大于size,如果直接序列化会浪费大量空间。自定义writeObject只序列化有效元素,节省I/O和存储。

RandomAccess标记

java 复制代码
public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable { }

RandomAccess是一个标记接口,表示该集合支持高效随机访问。遍历时可根据是否实现此接口选择不同策略:

java 复制代码
if (list instanceof RandomAccess) {
    // 用for循环+get(i),O(1)随机访问
    for (int i = 0; i < list.size(); i++) { list.get(i); }
} else {
    // 用Iterator,避免每次get都是O(n)
    for (E e : list) { ... }
}

2.3 LinkedList源码分析

底层数据结构

java 复制代码
public class LinkedList<E> implements List<E>, Deque<E> {
    // 双向链表节点
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
    }
    
    transient int size = 0;
    transient Node<E> first; // 头节点
    transient Node<E> last;  // 尾节点
}

ArrayList vs LinkedList性能对比

操作 ArrayList LinkedList
随机访问get(i) O(1) O(n)
头部插入add(0, e) O(n)(数组移动) O(1)
尾部插入add(e) O(1)均摊 O(1)
中间插入add(i, e) O(n) O(n)(查找O(n)+插入O(1))
内存占用 紧凑(连续数组) 每个节点额外2个指针

面试追问:实际开发中LinkedList几乎不被使用,为什么?

①CPU缓存友好性:ArrayList连续内存,缓存命中率高;LinkedList节点分散,频繁cache miss。②即使头部插入,ArrayList在数据量不大时数组的native copy比链表指针操作还快。③LinkedList每个节点额外消耗16字节(prev+next指针)。


2.4 HashMap源码深度分析

面试官怎么问

"说说HashMap的底层原理。put一个键值对的完整流程是什么?"

这是集合面试的"必考题",必须能完整讲述put流程。

JDK7 vs JDK8核心差异

维度 JDK7 JDK8
底层结构 数组+单向链表 数组+链表+红黑树
插入方式 头插法 尾插法
扩容链表顺序 反转 保持原顺序
扰动函数 9次扰动 1次异或
并发问题 死循环+数据丢失 数据覆盖+丢失
查询最坏 O(n) O(log n)

核心成员变量

java 复制代码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;        // 负载因子
static final int TREEIFY_THRESHOLD = 8;                // 树化阈值
static final int UNTREEIFY_THRESHOLD = 6;              // 反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;            // 最小树化容量

transient Node<K,V>[] table;    // 哈希桶数组
transient int size;              // 键值对总数
int threshold;                   // 扩容阈值 = capacity * loadFactor
final float loadFactor;          // 负载因子
transient int modCount;          // 修改计数(fail-fast)

hash方法------为什么异或高位

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

数组下标计算:index = (n - 1) & hash

当n=16时,n-1=15(二进制0000 1111),只取hash低4位。如果两个key的hashCode高位不同但低位相同,就会产生冲突。(h >>> 16) ^ h让高16位也参与低位计算,减少冲突概率。

put完整流程(源码级)

复制代码
map.put(key, value) 的完整执行路径:

1. 计算hash:hash(key)
2. 定位桶:i = (n - 1) & hash
3. 桶为空?→ 直接插入newNode
4. 桶不为空:
   4a. 头节点key相同?→ 覆盖value
   4b. 头节点是TreeNode?→ 红黑树插入putTreeVal
   4c. 遍历链表:
       - 找到相同key?→ 覆盖value
       - 到达尾部?→ 尾插新节点
       - 链表长度≥8?→ 调用treeifyBin尝试树化
5. modCount++
6. size++ > threshold?→ resize()扩容
java 复制代码
// 核心源码(简化版)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. 初始化table
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 桶为空,直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 3. 头节点key匹配
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 5. 链表遍历
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); // 尾插法
                    if (binCount >= TREEIFY_THRESHOLD - 1)    // 链表长度≥8
                        treeifyBin(tab, hash);                // 尝试树化
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // key已存在,覆盖value
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    return null;
}

树化的两个条件

java 复制代码
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 关键判断:如果数组长度<64,只扩容不树化!
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 扩容比树化更划算
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 链表→红黑树转换...
    }
}

为什么需要MIN_TREEIFY_CAPACITY=64?

  • 数组小时,哈希冲突概率高,树化后很快又退化,白费功夫
  • 扩容能把元素分散到更多桶中,从根本上减少冲突
  • 经验值:数组≥64后,如果某桶仍有8+元素,说明确实需要树化

扩容机制

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    if (oldCap > 0) {
        // 已有数据,容量翻倍
        newCap = oldCap << 1;  // 2倍扩容
        newThr = oldThr << 1; // 阈值也翻倍
    } else if (oldThr > 0) {
        // 指定初始容量的构造器
        newCap = oldThr;
    } else {
        // 无参构造,使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY; // 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
    }
    
    // 迁移数据------JDK8优化
    // 关键优化:e.hash & oldCap 判断节点在新数组中的位置
    // 如果结果为0,留在原位置(index)
    // 如果不为0,移动到新位置(index + oldCap)
}

JDK8扩容的巧妙优化

复制代码
扩容前:容量16,hash=21
index = 21 & 15 = 5(二进制0101)

扩容后:容量32
index = 21 & 31 = 21(二进制10101)或 5(二进制00101)

判断方法:21 & 16 = 16 ≠ 0 → 移动到 index + oldCap = 5 + 16 = 21
如果 hash & 16 == 0 → 留在原位index=5

这避免了重新计算hash,一次位运算即可确定位置!

JDK7并发扩容死循环

java 复制代码
// JDK7 头插法 + 并发扩容 = 环形链表
// 线程A和线程B同时扩容,假设链表顺序为 A→B→null

// 线程A执行到:newTable[i] = e(指向A),e = e.next(指向B),被挂起
// 线程B完成扩容:头插法导致链表反转为 B→A→null
// 线程A恢复执行:
// 1. e=B, next=A(从原链表取的)
// 2. B插入新表头:B→A(头插法)
// 3. e=A, next=B(B已在步骤2中指向A)
// 4. A插入新表头:A→B
// 5. e=B, next=null → B.next=A(步骤4)
// 结果:A→B→A→B→... 环形链表!get时死循环!

面试追问:JDK8为什么改用尾插法?

尾插法保持链表原有顺序,扩容迁移时不会反转,因此不会产生环形链表。但JDK8的HashMap仍然是线程不安全的------并发put可能导致数据覆盖(两个线程同时判断桶为空,后写入的覆盖先写入的)。

负载因子为什么是0.75

这是空间利用率哈希冲突率的平衡点:

负载因子 空间利用率 哈希冲突率 适用场景
0.5 低(频繁扩容) 查询频繁
0.75 适中 适中 通用场景
1.0 内存紧张

泊松分布计算:在负载因子0.75下,每个桶出现8个以上元素的概率约为0.00000006,这也是树化阈值选择8的统计学依据。


2.5 ConcurrentHashMap演进

面试官怎么问

"ConcurrentHashMap怎么保证线程安全?JDK7和JDK8有什么区别?"

JDK7:分段锁(Segment)

java 复制代码
// JDK7 核心结构
public class ConcurrentHashMap<K, V> {
    final Segment<K,V>[] segments; // 分段数组
    
    static final class Segment<K,V> extends ReentrantLock {
        transient HashEntry<K,V>[] table; // 每段独立的哈希表
    }
}

// 默认16个Segment,并发度=16
// put时:先定位Segment(段级hash),再lock Segment,再操作HashEntry
// get时:HashEntry的value是volatile的,无需加锁

JDK7的缺点

  1. 并发度固定为16,无法扩展
  2. Segment内仍然是全局锁,冲突严重时性能退化
  3. 每个Segment独立扩容,内存碎片化
  4. 代码复杂,维护成本高

JDK8:CAS + synchronized

java 复制代码
// JDK8 核心结构
public class ConcurrentHashMap<K,V> {
    transient volatile Node<K,V>[] table; // 单一桶数组
    
    static class Node<K,V> {
        final int hash;
        final K key;
        volatile V val;           // volatile保证可见性
        volatile Node<K,V> next;  // volatile保证可见性
    }
}

put流程(JDK8)

复制代码
1. 计算hash,定位桶
2. 桶为空?→ CAS插入新节点(无锁!)
3. 桶正在扩容?→ 协助扩容
4. 桶不为空?→ synchronized锁定头节点
   4a. 链表→遍历插入
   4b. 红黑树→树插入
5. 判断是否需要扩容
java 复制代码
// JDK8 put核心逻辑(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许null key/value
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();                    // CAS初始化
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;                            // CAS插入成功
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);           // 协助扩容
        else {
            synchronized (f) {                    // 锁定头节点
                // 链表/红黑树插入...
            }
        }
    }
    addCount(1L, binCount);                       // 更新计数
    return null;
}

size()方法的实现

java 复制代码
// JDK8:LongAdder思想
// baseCount:基础计数器
// CounterCell[]:分散计数器(高并发时使用)

private transient volatile CounterCell[] counterCells;
private transient volatile long baseCount;

// size() = baseCount + 所有CounterCell的值
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 : (n > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells;
    long sum = baseCount;
    if (as != null) {
        for (CounterCell a : as) {
            if (a != null)
                sum += a.value;
        }
    }
    return sum;
}

为什么用LongAdder而不是AtomicLong?

  • 高并发下AtomicLong的CAS竞争激烈,性能下降
  • LongAdder将竞争分散到多个Cell,每个线程更新不同的Cell,最后求和
  • 代价:size()不是强一致的(但并发场景下通常不需要强一致)

ConcurrentHashMap为什么不允许null

java 复制代码
// 如果允许null value,会有歧义:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key", null);
map.get("key"); // 返回null

// 这里的null有两个含义:
// 1. key存在,value是null
// 2. key不存在(在并发环境下,get和containsKey之间可能被其他线程修改)

// HashMap单线程下可以用containsKey区分,但CHM多线程下无法保证
// 所以直接禁止null,消歧义

2.6 TreeMap与红黑树

面试官怎么问

"TreeMap的底层是什么?什么场景用TreeMap?"

TreeMap基于红黑树实现,所有操作O(log n),key必须实现Comparable或传入Comparator。

java 复制代码
// TreeMap核心操作
TreeMap<String, Integer> map = new TreeMap<>();
map.put("banana", 2);
map.put("apple", 1);
map.put("cherry", 3);

// 自动排序
map.firstKey();   // "apple"
map.lastKey();    // "cherry"
map.subMap("apple", "cherry"); // {"apple"=1, "banana"=2}

// 导航方法(面试亮点)
map.lowerKey("banana");    // "apple"(严格小于)
map.floorKey("banana");    // "banana"(小于等于)
map.higherKey("banana");   // "cherry"(严格大于)
map.ceilingKey("banana");  // "banana"(大于等于)

HashMap vs TreeMap选型

场景 推荐 原因
通用键值存储 HashMap O(1)查找,性能最优
需要排序遍历 TreeMap 自动排序
范围查询 TreeMap subMap/headMap/tailMap
需要排名/TopK TreeMap 导航方法

2.7 LinkedHashMap------插入序/访问序/LRU缓存

面试官怎么问

"LinkedHashMap怎么实现LRU缓存?"

双向链表维护顺序

java 复制代码
public class LinkedHashMap<K,V> extends HashMap<K,V> {
    // 双向链表头尾
    transient LinkedHashMap.Entry<K,V> head;
    transient LinkedHashMap.Entry<K,V> tail;
    // 排序模式:true=访问序,false=插入序
    final boolean accessOrder;
    
    // 关键钩子方法:afterNodeAccess(访问后移到链表尾部)
    // 关键钩子方法:afterNodeInsertion(插入后判断是否删除最老元素)
}

访问序模式

java 复制代码
// accessOrder=true时,get/put会将访问的节点移到链表尾部
LinkedHashMap<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("A", 1);
lru.put("B", 2);
lru.put("C", 3);
// 链表顺序:A→B→C

lru.get("A");  // A被访问,移到尾部
// 链表顺序:B→C→A

lru.put("D", 4); // D插入到尾部
// 链表顺序:B→C→A→D

手写LRU缓存

java 复制代码
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // accessOrder=true
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当size超过capacity时,自动删除最老的entry(链表头部)
        return size() > capacity;
    }
}

// 使用
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("A", "valueA");
cache.put("B", "valueB");
cache.put("C", "valueC");
cache.get("A");  // A移到尾部:B→C→A
cache.put("D", "valueD"); // 超过容量3,删除B:C→A→D
cache.get("B");  // 返回null

面试追问:为什么LinkedHashMap能做LRU?

核心是accessOrder=true模式。每次get/put都把访问的节点移到链表尾部,而链表头部就是最久未访问的。重写removeEldestEntry在size超限时自动淘汰头部。这比手动维护双向链表+HashMap简洁得多。


2.8 HashSet/LinkedHashSet------HashMap的包装器

java 复制代码
// HashSet源码:本质是HashMap的keySet
public class HashSet<E> extends AbstractSet<E> {
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object(); // 占位value
    
    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }
    
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    
    public boolean remove(Object o) {
        return map.remove(o) == PRESENT;
    }
}

所以HashSet的所有特性(扩容、树化、线程不安全)都来自HashMap。


2.9 集合选型决策树

复制代码
需要键值对?──是──→ Map
│                    ├── 需要线程安全?→ ConcurrentHashMap
│                    ├── 需要排序?→ TreeMap
│                    ├── 需要插入序/访问序?→ LinkedHashMap
│                    └── 通用→ HashMap
│
否
↓
需要唯一性?──是──→ Set
│                    ├── 需要排序?→ TreeSet
│                    ├── 需要插入序?→ LinkedHashSet
│                    └── 通用→ HashSet
│
否
↓
需要队列语义?──是──→ Queue/Deque
│                    ├── 需要优先级?→ PriorityQueue
│                    └── 双端队列→ ArrayDeque
│
否
↓
需要随机访问?──是──→ ArrayList
否
↓
频繁头部插入?──是──→ LinkedList
否
↓
ArrayList(默认选择)

面试连环追问实战

追问链1:HashMap

Q1:HashMap的hash方法为什么先异或高位?

定位桶只用hash低位((n-1)&hash),如果两个key高位不同但低位相同就会冲突。异或高位让高位信息参与低位计算,减少冲突。

Q2:为什么HashMap线程不安全?具体场景?

三个场景:①并发put数据覆盖(两个线程同时判断桶为空)②并发put+get数据不一致(一个线程put另一个get可能看到半完成状态)③JDK7并发扩容死循环。注意JDK8改为尾插法解决了死循环,但数据覆盖问题仍然存在。

Q3:HashMap的key可以是null吗?

可以。HashMap专门处理null key,hash值固定为0,存放在table0桶中。但ConcurrentHashMap不允许null key/value。

追问链2:ConcurrentHashMap

Q1:ConcurrentHashMap的size()方法是精确的吗?

不保证强一致。size()基于baseCount + CounterCell求和,在高并发下可能略有误差。但这在绝大多数场景下是可接受的------你通常只需要"大概多少条数据"而非精确值。

Q2:为什么JDK8用synchronized而不用ReentrantLock?

三个原因:①JDK6+对synchronized做了锁升级优化(偏向锁→轻量锁→重量锁),在细粒度短持有场景下性能不比ReentrantLock差 ②synchronized是JVM内置锁,不需要额外的对象头和AQS节点,内存更紧凑 ③synchronized在锁升级失败时自动膨胀,不需要手动控制。


常见坑点与最佳实践

坑点1:遍历时修改集合

java 复制代码
// ConcurrentModificationException!
for (String key : map.keySet()) {
    if (key.equals("remove_me")) {
        map.remove(key); // fail-fast机制抛出异常
    }
}

// 正确方式1:使用Iterator
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    if (it.next().equals("remove_me")) {
        it.remove(); // Iterator的remove是安全的
    }
}

// 正确方式2:使用removeIf(Java8+)
map.keySet().removeIf(key -> key.equals("remove_me"));

// 正确方式3:使用ConcurrentHashMap(弱一致迭代器)

坑点2:HashMap初始容量设置

java 复制代码
// 如果你知道要放1000个元素,怎么设置初始容量?
// 错误:new HashMap<>(1000)
// 因为1000 * 0.75 = 750,在放入第751个元素时就会触发扩容
// 正确:计算 threshold > 1000 的最小2的幂
new HashMap<>(1000 / 0.75 + 1); // → 1334 → 取最近的2的幂2048
// 或者使用Guava:Maps.newHashMapWithExpectedSize(1000)

坑点3:subList的坑

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> sub = list.subList(1, 3); // [2, 3]

// 坑1:subList是原List的视图,修改sub会影响原List
sub.set(0, 99);
// list: [1, 99, 3, 4, 5]

// 坑2:修改原List结构后使用subList会抛ConcurrentModificationException
list.add(6);
sub.get(0); // ConcurrentModificationException!

总结与复习题

核心知识图谱

复制代码
Java集合框架
├── List
│   ├── ArrayList → 数组+1.5倍扩容+transient序列化优化
│   └── LinkedList → 双向链表+Deque接口
├── Set → HashMap包装器
├── Map
│   ├── HashMap → 数组+链表+红黑树
│   │   ├── hash扰动:h^(h>>>16)
│   │   ├── 树化条件:链表≥8 且 数组≥64
│   │   ├── 扩容:2倍 + e.hash&oldCap优化
│   │   └── JDK7头插法死循环 → JDK8尾插法
│   ├── LinkedHashMap → HashMap+双向链表(LRU)
│   └── ConcurrentHashMap → CAS+synchronized
│       ├── JDK7:Segment分段锁
│       ├── JDK8:CAS初始化+synchronized桶锁
│       └── size():LongAdder思想
└── Queue → ArrayDeque/PriorityQueue

复习题

  1. HashMap的put完整流程是什么?从计算hash到判断扩容,每一步都要说清楚。
  2. HashMap为什么线程不安全?JDK7和JDK8的具体问题分别是什么?
  3. ConcurrentHashMap JDK8的put流程是什么?CAS和synchronized分别用在哪些步骤?
  4. ArrayList的扩容机制是怎样的?为什么elementData用transient修饰?
  5. 如何用LinkedHashMap实现LRU缓存?为什么accessOrder=true能做到?
  6. HashMap的容量为什么必须是2的幂次方?扩容时如何高效确定节点新位置?

下一章预告:第3章将深入泛型、反射与动态代理------从类型擦除的底层机制到JDK Proxy和CGLIB的源码对比,再到Spring AOP如何利用动态代理,帮你彻底理解Java元编程的核心原理。

相关推荐
会周易的程序员1 小时前
microLog 的 log_reader 架构解析:嵌入式日志检索引擎的设计之道
c++·物联网·架构·日志·iot·aiot
万亿少女的梦1681 小时前
基于Spring Boot与Vue的繁星技术论坛系统设计与实现
java·spring boot·mysql·vue·系统设计
阿pin1 小时前
Android随笔-神经系统Handler
android·java·开发语言·handler
Geek-Chow1 小时前
微服务认证与授权:08 — OPA(PDP)
微服务·云原生·架构
阿标在干嘛1 小时前
100+微服务的统一出入口:政策快报平台的API网关设计
微服务·云原生·架构
ch.ju2 小时前
Java程序设计(第3版)第四章——final修饰的方法和类
java·开发语言
摇滚侠2 小时前
云原生 Java 架构师的第一课 K8s+Docker+KubeSphere+DevOps 26-40
java·云原生·kubernetes
认真的酒窝11 小时前
自己动手开发编译器(十一)语义分析
java·开发语言