Java 基础核心
Java 基础核心
涵盖集合源码、String 原理、Java 8-21 新特性、虚拟线程——字节/阿里/腾讯/美团基础必考模块。
核心原理
HashMap 底层实现与演进
JDK 7 vs JDK 8 核心差异:
| 对比项 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 树化阈值 | 无 | 链表长度 >= 8 且数组容量 >= 64 |
| 多线程问题 | resize 头插法造成环形链表 → 死循环 | 并发 put 数据丢失(覆盖写) |
JDK 8 put 流程源码关键逻辑:
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. 首次 put 时 resize 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算桶下标:(n-1) & hash,桶为空直接放
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 3. 桶不为空:找到已存在的 key 或追加到链尾/红黑树
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表尾插
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度达到 8 → 树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 4. key 已存在则更新值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) e.value = value;
return oldValue;
}
}
// 5. 超过阈值 (capacity * 0.75) 扩容为 2 倍
if (++size > threshold) resize();
return null;
}扩容为何是 2 的幂次? (n-1) & hash 等价于 hash % n,但位运算更快。扩容后元素要么在原位置,要么在原位置 + oldCapacity,只需检查 hash 的高位 bit 是否为 1。
ConcurrentHashMap JDK 8 并发原理
JDK 8 抛弃了 JDK 7 的 Segment 分段锁,改为 CAS + synchronized 锁单个桶头节点:
// 关键:put 时只锁冲突的桶头节点,并发度大幅提升
synchronized (f) { // f = 桶头节点
if (tabAt(tab, i) == f) {
// 链表或红黑树操作
}
}
// 扩容:多线程协同 transfer,每个线程认领一段桶范围
// sizeCtl 状态机:-1 表示初始化中,-(1+n) 表示 n 个线程在扩容
// size 统计:LongAdder 思想,baseCount + CounterCell[] 分散竞争读操作完全无锁: 节点的 val 和 next 均用 volatile 修饰,读时不加锁,保证可见性。
String、StringBuilder、StringBuffer
JDK 9 起 String 底层从 char[] 改为 byte[]:
// JDK 9+ String 内部结构
public final class String implements Serializable, Comparable<String>, CharSequence {
private final byte[] value; // 使用 LATIN1 编码时1字节/字符,节省约一半内存
private final byte coder; // 0=LATIN1, 1=UTF16
private int hash; // hashCode 缓存,延迟计算
}字符串常量池(String Pool):
- JDK 7 之前在永久代(PermGen),JDK 7 起移至堆中
String.intern()将字符串放入常量池,返回常量池中的引用"a" + "b"编译期优化为"ab";变量拼接编译为StringBuilder.append()
为什么 String 不可变?
- 安全性:URL、文件路径不被篡改
- 线程安全:天然不可变,无需同步
- HashCode 缓存:
hash字段只计算一次,保证 HashMap key 一致性
高频面试题
Q: HashMap 的 key 为什么必须重写 hashCode 和 equals?(阿里高频)
HashMap 查找元素的流程:先用
hashCode定位桶,再用equals在桶内链表/红黑树中找到精确 key。若只重写equals而不重写hashCode,两个内容相同的对象会散落在不同桶中,get时永远找不到已存入的put值。equals 和 hashCode 的约定:
a.equals(b) == true则a.hashCode() == b.hashCode()必须成立(反之不要求)。
Q: ConcurrentHashMap 的 size() 是精确的吗?(字节高频)
JDK 8 的
size()是近似值,不保证精确:
- 内部通过
baseCount + CounterCell[]累加,类似LongAdder- 高并发下
CounterCell写入和size()读取存在时间差- 若需精确统计,业务层自行维护计数器(如 AtomicLong)
面试加分点:说明这是 CAP 中 Availability 优先的设计取舍,牺牲精确 size 换取极高的并发 put 吞吐量。
Q: Java 8 Stream 的 parallel() 底层原理?(腾讯)
parallelStream()底层基于 Fork/Join 框架,使用公共的ForkJoinPool.commonPool()(默认线程数 = CPU 核数 - 1)。工作窃取(Work Stealing):空闲线程从其他线程的任务队列尾部窃取任务,提高 CPU 利用率。
注意事项:
- 不适合 IO 密集型任务(线程阻塞浪费公共池)
- 需要无状态、无副作用的操作(避免线程安全问题)
- 数据量小时并行开销 > 收益,反而更慢
// 正确使用:CPU密集型、大数据量
List<Integer> result = IntStream.range(0, 1_000_000)
.parallel()
.filter(n -> n % 2 == 0)
.boxed()
.collect(Collectors.toList());
// 自定义线程池(避免污染公共池)
ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() -> list.parallelStream().forEach(this::process)).get();Q: Java 21 虚拟线程(Virtual Threads)是什么?和平台线程有何区别?(2024/2025 字节、美团必问)
虚拟线程(Project Loom,JDK 21 正式发布) 是 JVM 管理的轻量级线程,不与 OS 线程一一对应:
对比项 平台线程 虚拟线程 创建成本 MB 级栈内存 KB 级,可创建数百万 调度 OS 内核调度 JVM 调度(挂载到 Carrier 线程) 阻塞行为 阻塞时占用 OS 线程 阻塞时卸载 Carrier 线程,不占用 适用场景 CPU 密集型 IO 密集型(数据库、HTTP 调用)
// JDK 21 创建虚拟线程
// 方式1:Thread.ofVirtual()
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread: " + Thread.currentThread().isVirtual());
});
// 方式2:ExecutorService(推荐,Spring Boot 3.2+ 已内置支持)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // 阻塞时自动卸载载体线程
return i;
})
);
}
// Spring Boot 3.2+ 开启虚拟线程
// application.yml: spring.threads.virtual.enabled=true虚拟线程注意事项:
synchronized块内若发生阻塞,会 pin(钉住) Carrier 线程,降级为平台线程行为。JDK 21 建议改用ReentrantLock替代synchronized,JDK 24 已修复大部分 pin 问题。- ThreadLocal 在虚拟线程中仍可使用,但大量创建时内存占用需关注,推荐改用
ScopedValue(JDK 21 预览)。
Q: Java 16 Record 类和传统 POJO 的区别?(阿里)
record是不可变数据载体,编译器自动生成equals()、hashCode()、toString()和所有字段的getter:
// Record(JDK 16 正式)
public record Point(int x, int y) {}
// 等价于(约50行样板代码):
// - final 字段
// - 全参构造器
// - equals/hashCode/toString
// 使用
Point p = new Point(1, 2);
System.out.println(p.x()); // getter
System.out.println(p); // Point[x=1, y=2]
// 可添加自定义方法和 compact constructor
public record Range(int min, int max) {
Range { // compact constructor,自动赋值前验证
if (min > max) throw new IllegalArgumentException("min > max");
}
public int length() { return max - min; }
}Q: Java 17 Sealed Classes 解决什么问题?(腾讯)
sealed类限制哪些类可以继承它,配合pattern matching switch(JDK 21 正式)实现类型安全的代数数据类型:
// 定义密封层次(JDK 17)
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// JDK 21 pattern matching switch(exhaustive,编译器检查完整性)
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// 无需 default,编译器确认已覆盖所有子类型
};知识星球深度内容
完整大厂面经实录(字节/阿里/腾讯/美团)、简历 1v1 修改、每周高频题精讲,扫码加入「AI 工程师加速社区」知识星球 👉 立即加入
