Java 基础面试题

覆盖 HashMap、集合框架、字符串、异常体系、泛型擦除、反射、动态代理、新特性、equals/hashCode、接口 vs 抽象类等高频考点。
每道题包含中英双语答案、代码示例、常见误区和风控关联。
相关页面: 并发编程 | JVM | Spring

Q1. HashMap 底层原理与 JDK 1.7 vs 1.8 的核心区别

EN: Explain how HashMap works internally. Compare JDK 1.7 vs 1.8.

难度: ★★★★ | 出现频率: 极高(阿里、美团、字节、腾讯、京东、小红书)

Key Terms: HashMap (哈希表), 红黑树 (Red-Black Tree), 扩容机制 (Resize Mechanism), 扰动函数 (Hash Perturbation Function), ConcurrentHashMap (并发哈希表)

答案要点:

  1. 底层数据结构
  2. - JDK 1.7:数组 + 链表(Entry<K,V>[] table) - JDK 1.8:数组 + 链表 + 红黑树(Node<K,V>[] table),链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树;红黑树节点 ≤ 6 时退化为链表

  1. 哈希计算
  2. - 先调用 key.hashCode(),再做扰动处理 - JDK 1.7:9 次扰动(4 次位运算 + 5 次异或) - JDK 1.8:1 次扰动——(h = key.hashCode()) ^ (h >>> 16),高位低位混合,减少碰撞

  1. put 流程(1.8)
  2. - 计算 hash → 定位桶下标 (n - 1) & hash - 桶为空 → 直接插入 - 桶非空 → key 相同则覆盖 value;否则遍历链表/红黑树插入 - 插入后链表长度 ≥ 8 → treeifyBin()(数组长度 < 64 先扩容不转树)

  1. 扩容机制
  2. - 触发条件:元素数量 > capacity * loadFactor(默认 16 * 0.75 = 12) - 新容量:旧容量 × 2(newCap = oldCap << 1) - JDK 1.7:重新计算每个元素的 hash 和下标,头插法导致并发时可能形成环形链表(死循环) - JDK 1.8:尾插法,利用 hash & oldCap 判断元素在新数组中的位置——结果为 0 则原位不变,非 0 则移动到 原下标 + oldCap,避免了环形链表

  1. 线程安全
  2. - HashMap 本身非线程安全 - HashTable:全方法 synchronized,性能差,已淘汰 - Collections.synchronizedMap():包装层加锁,粒度粗 - ConcurrentHashMap:推荐方案。1.7 用分段锁(Segment),1.8 改为 CAS + synchronized(锁桶的头节点)

代码示例:


// JDK 1.8 HashMap 的 hash 扰动
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 桶下标计算,等价于取模但更快(要求 capacity 为 2 的幂)
int index = (n - 1) & hash;

// 扩容时的位置判断
// 如果 (e.hash & oldCap) == 0,留在原位
// 否则移动到 原下标 + oldCap

常见误区:

  • ❌ 误以为链表长度到 8 就一定会转红黑树 → ✅ 必须同时满足数组长度 ≥ 64
  • ❌ 误以为 HashMap 的 key 有序 → ✅ HashMap 无序,需用 LinkedHashMap 保持插入/访问顺序
  • ❌ 误以为 JDK 1.8 的 HashMap 线程安全 → ✅ 只是修复了环形链表问题,并发 put 仍会丢数据
  • ❌ Assuming a linked list of 8 always becomes a red-black tree → ✅ The array must also be at least 64 in length
  • ❌ Assuming HashMap keys are ordered → ✅ HashMap is unordered; use LinkedHashMap for insertion/access order
  • ❌ Assuming JDK 1.8 HashMap is thread-safe → ✅ It only fixes the circular linked list bug; concurrent puts can still lose data

延伸追问:

  • 为什么容量必须是 2 的幂?(n - 1 的二进制全为 1,与 hash 做 & 运算等价于取模,效率更高)
  • HashMap 的 key 能否用可变对象?(不建议,key 的 hash 变化后无法正确 get)
  • ConcurrentHashMap 1.8 为什么放弃分段锁?(分段锁粒度固定,并发度受限;CAS + synchronized 锁粒度更细,只在冲突时加锁)
  • Why must capacity always be a power of 2? (Because n - 1 in binary is all 1s, making hash & (n-1) equivalent to modulo but faster)
  • Can a mutable object be used as a HashMap key? (Not recommended — if the key's hashCode changes, the entry becomes unreachable)
  • Why did ConcurrentHashMap 1.8 drop segmented locking? (Fixed granularity limits concurrency; CAS + per-bucket synchronized offers finer-grained locking)

风控关联:

  • 实时风控引擎中常用 HashMap 存储规则匹配结果、特征缓存。高并发场景下必须使用 ConcurrentHashMap,否则并发 put 导致数据丢失会影响风控决策准确性
  • HashMap 扩容时的性能抖动在低延迟风控链路中需要关注——可预估容量在构造时指定 initialCapacity 避免运行中扩容
  • 关联 风控技术架构题
  • In real-time risk engines, HashMap is commonly used for rule match results and feature caches. Under high concurrency, ConcurrentHashMap is mandatory — otherwise concurrent puts can silently lose data and compromise decision accuracy
  • HashMap resize-induced latency spikes are critical in low-latency risk pipelines — pre-size with initialCapacity to avoid runtime resizing

English Answer:

HashMap is implemented as a hash table. In JDK 1.7, its core structure is an array plus linked lists. In JDK 1.8, it becomes an array plus linked lists plus red-black trees. A bucket is treeified only when the linked list length reaches at least 8 and the table capacity is at least 64; if the tree becomes small again, it can be converted back to a linked list.

The hash calculation first calls key.hashCode() and then applies a perturbation function. JDK 1.7 used multiple rounds of bit operations and XOR. JDK 1.8 simplified this to (h = key.hashCode()) ^ (h >>> 16), mixing high bits into low bits to reduce collisions when the index is computed by (n - 1) & hash.

The put flow in JDK 1.8 is: compute the hash, locate the bucket, insert directly if the bucket is empty, overwrite the value if the same key already exists, or insert into the linked list / red-black tree otherwise. After insertion, if the linked list is too long, treeifyBin() is triggered, but if the array length is still below 64, HashMap chooses resize before treeification.

Resize happens when size exceeds capacity * loadFactor; the default threshold is 16 * 0.75 = 12. Capacity doubles each time. JDK 1.7 recalculates positions and uses head insertion during transfer, which can create circular linked lists under unsafe concurrent modification. JDK 1.8 uses tail insertion and the hash & oldCap rule: if the result is 0, the node stays at the old index; otherwise it moves to oldIndex + oldCap.

HashMap is not thread-safe. Hashtable synchronizes every method and is usually obsolete. Collections.synchronizedMap() uses coarse-grained locking. In modern high-concurrency code, ConcurrentHashMap is preferred; JDK 1.7 used segmented locks, while JDK 1.8 uses CAS plus synchronized locking on bucket heads.


Q2. ArrayList vs LinkedList 核心区别与选型

EN: Compare ArrayList and LinkedList.

难度: ★★★ | 出现频率: 高(阿里、美团、字节、拼多多)

Key Terms: ArrayList (动态数组), LinkedList (双向链表), Dynamic Array (动态数组), Doubly-Linked List (双向链表), CPU Cache Line (CPU 缓存行)

答案要点:

维度 ArrayList LinkedList
底层结构 Object[] 动态数组 双向链表(Node<E> 有 prev/next)
随机访问 O(1)——直接下标寻址 O(n)——从头/尾遍历
头部插入/删除 O(n)——需要移动后续元素 O(1)——修改指针
中间插入/删除 O(n) O(n)(定位 O(n) + 操作 O(1))
内存占用 紧凑,可能有预分配浪费 每个节点额外 2 个指针(16 字节)
缓存友好性 高(连续内存,CPU 缓存命中率高) 低(链表节点分散)
扩容机制 默认容量 10,扩容为 1.5 倍 无需扩容

选型结论:绝大多数场景选 ArrayList。CPU 缓存行的预读效应使得即使需要移动元素,ArrayList 的实际性能往往也优于 LinkedList。仅在频繁头部插入且几乎不随机访问时才考虑 LinkedList。

代码示例:


// ArrayList 扩容源码(简化)
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
    if (newCapacity < minCapacity)
        newCapacity = minCapacity;
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 批量插入时预估容量,避免多次扩容
List<String> list = new ArrayList<>(expectedSize);

常见误区:

  • ❌ 误以为 LinkedList 的"中间插入"比 ArrayList 快 → ✅ 忽略了遍历定位的 O(n) 开销
  • ❌ 忘记 LinkedList 实现了 Deque 接口 → ✅ LinkedList 可当双端队列使用
  • ❌ Assuming LinkedList is faster for middle insertion than ArrayList → ✅ Overlooks the O(n) traversal cost to locate the insertion point
  • ❌ Forgetting that LinkedList implements Deque → ✅ LinkedList can be used as a double-ended queue

延伸追问:

  • ArrayListsubList() 返回的是视图还是副本?(视图,修改子 list 会影响原 list)
  • ArrayList 是线程安全的吗?如何获得线程安全的 List?(CopyOnWriteArrayListCollections.synchronizedList()
  • Does ArrayList.subList() return a view or a copy? (A view — modifications to the sublist affect the original list)
  • Is ArrayList thread-safe? How do you get a thread-safe List? (Use CopyOnWriteArrayList or Collections.synchronizedList())

风控关联:

  • 风控规则引擎中用 ArrayList 存储有序规则列表,规则命中后按优先级执行
  • 高并发读取场景用 CopyOnWriteArrayList 存储风控策略配置——写少读多,读不加锁
  • 关联 风控技术架构题
  • Risk rule engines use ArrayList to store ordered rule lists, executing matched rules by priority
  • CopyOnWriteArrayList is ideal for risk control policy configs under high-concurrency reads — write-rare, read-many, no lock on reads

English Answer:

ArrayList is backed by a dynamic Object[] array. It supports O(1) random access because elements are stored contiguously and can be accessed by index. Insertions or deletions at the beginning or in the middle require shifting later elements, so the complexity is O(n). Its memory layout is compact, and it benefits from CPU cache locality. The default initial capacity is 10, and expansion grows the array by about 1.5 times.

LinkedList is backed by a doubly linked list. Each node stores the element plus prev and next references. If you already have the target node, insertion or deletion can be O(1), but locating a middle position still costs O(n). Random access is also O(n) because it must traverse from the head or tail. Its memory overhead is higher because every node carries extra references, and its nodes are not cache-friendly.

In practice, ArrayList should be the default choice for most scenarios, even when occasional element shifting is required. Its contiguous memory and cache behavior usually make it faster than LinkedList. LinkedList is only worth considering when there are frequent head/tail operations, almost no random access, and its Deque behavior is actually needed. For batch insertion, pre-size ArrayList with new ArrayList<>(expectedSize) to avoid repeated resizing.


Q3. String、StringBuilder、StringBuffer 三者的区别

EN: String vs StringBuilder vs StringBuffer?

难度: ★★★ | 出现频率: 极高(阿里、美团、字节、京东、百度)

Key Terms: String Immutability (String 不可变性), StringBuilder, String Constant Pool (字符串常量池), Compact Strings (紧凑字符串), intern (字符串驻留)

答案要点:

维度 String StringBuilder StringBuffer
可变性 不可变(final char[],JDK 9 后 final byte[] 可变 可变
线程安全 安全(不可变天然安全) 不安全 安全(方法加 synchronized
性能 拼接产生大量中间对象 最快 略慢(锁开销)
适用场景 少量拼接、常量 单线程大量拼接 多线程大量拼接

关键原理

  1. String 不可变性:底层 private final byte[] value(JDK 9+ Compact Strings),一旦创建不可修改。所有"修改"操作都返回新对象。不可变性带来:字符串常量池优化、hashCode 缓存、天然线程安全、安全性(如数据库连接字符串不会被篡改)
  2. 字符串常量池String s = "abc" 先检查常量池,存在则直接引用;new String("abc") 会在堆上创建新对象
  3. JDK 9 Compact Strings:String 内部从 char[](UTF-16,每字符 2 字节)改为 byte[] + coder 字段,Latin-1 字符串每字符只占 1 字节,节省内存
  4. 编译期优化javac 对字符串常量的连续 + 拼接自动优化为 StringBuilder.append()(单语句内)

代码示例:


// 编译器优化:单语句拼接自动转 StringBuilder
String s = "a" + "b" + "c"; // 编译后等价于 String s = "abc";

// 循环中拼接必须手动用 StringBuilder
StringBuilder sb = new StringBuilder(256); // 预估容量
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(",");
}
String result = sb.toString();

// intern() 方法——将字符串放入常量池
String a = new String("hello");
String b = a.intern(); // 常量池引用
String c = "hello";
System.out.println(b == c); // true

常见误区:

  • ❌ 误以为循环中用 + 拼接 String 会自动优化 → ✅ 多语句拼接每次循环都创建新的 StringBuilder 和 String 对象,O(n²) 级别内存开销
  • ❌ 混淆 equals()== → ✅ == 比较引用,equals() 比较内容
  • ❌ Assuming + concatenation in loops is auto-optimized → ✅ Multi-statement concatenation creates a new StringBuilder and String object per iteration — O(n²) memory overhead
  • ❌ Confusing equals() with == → ✅ == compares references, equals() compares content

延伸追问:

  • String s = new String("abc") 创建了几个对象?(最多 2 个:堆上的 String 对象 + 常量池中的 "abc",如果常量池已有则为 1 个)
  • JDK 9 的 Compact Strings 对中文有什么影响?(中文字符仍用 UTF-16 编码,coder=1,不节省空间)
  • String 为什么设计为不可变?(安全、hashCode 缓存、字符串常量池、线程安全、防止被继承篡改)
  • How many objects does String s = new String("abc") create? (Up to 2: a heap String object + the constant pool "abc"; 1 if "abc" already exists in the pool)
  • Why is String designed to be immutable? (Security, hashCode caching, string constant pool optimization, thread safety, preventing subclass tampering)

风控关联:

  • 风控日志脱敏处理中大量字符串拼接,应使用 StringBuilder 避免频繁 GC
  • 规则表达式解析时 String 不可变性保证了表达式在解析过程中不会被并发修改
  • 关联 风控技术架构题
  • Risk control log masking involves heavy string concatenation — use StringBuilder to avoid frequent GC pauses
  • String immutability ensures rule expressions cannot be concurrently modified during parsing

English Answer:

String is immutable. In modern JDKs, its internal storage is a final byte[] plus a coder flag, introduced by Compact Strings in JDK 9. Any operation that appears to modify a String actually creates a new object. This immutability enables string constant pool optimization, cached hashCode, natural thread safety, and security for values such as class names, URLs, and database connection strings.

StringBuilder is mutable and not synchronized. It is the preferred tool for heavy string concatenation in a single-threaded context, especially inside loops. The compiler can optimize simple compile-time constants such as "a" + "b" + "c", but repeated concatenation across loop iterations should still use StringBuilder explicitly, preferably with an estimated initial capacity.

StringBuffer is also mutable, but its methods are synchronized. It is thread-safe at the method level, but the lock overhead makes it slower. In modern backend code, it is rarely the best choice unless multiple threads must mutate the same string buffer, which is itself usually a questionable design.

The string constant pool means String s = "abc" reuses an existing pooled literal if available, while new String("abc") creates a heap object in addition to the pooled literal if that literal is not already present. intern() returns the canonical pooled reference. In risk systems, large-scale log masking or rule-expression construction should avoid repeated String concatenation and use StringBuilder to reduce GC pressure.


Q4. Java 异常体系:checked vs unchecked

EN: Checked vs unchecked exceptions?

难度: ★★★ | 出现频率: 高(阿里、美团、字节、京东)

Key Terms: Throwable (可抛出), Checked Exception (受检异常), RuntimeException (运行时异常), try-with-resources (自动资源管理), suppressed exception (被抑制的异常)

答案要点:


Throwable
├── Error(不应被应用捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception
    ├── 受检异常(Checked Exception)
    │   ├── IOException
    │   ├── SQLException
    │   └── ...
    └── 非受检异常(RuntimeException,Unchecked)
        ├── NullPointerException
        ├── ArrayIndexOutOfBoundsException
        ├── ClassCastException
        ├── IllegalArgumentException
        └── ...

核心区别

维度 Checked Exception Unchecked Exception
编译检查 编译器强制要求 try-catch 或 throws 声明 编译器不强制处理
继承关系 Exception 的非 RuntimeException 子类 RuntimeException 及其子类
设计理念 可预期的异常(如 IO 失败),调用方应处理 编程错误(如空指针),应修复代码而非捕获
使用场景 外部资源操作、业务校验 参数校验、前置条件检查

try-with-resources(JDK 7+)


// 自动关闭实现了 AutoCloseable 的资源
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    // 业务逻辑
} catch (SQLException e) {
    log.error("数据库查询失败", e);
    throw new RuntimeException("查询异常", e); // 异常转换
}
// 无需手写 finally 关闭资源

// JDK 9+ 增强版:可以直接使用 effectively final 的资源变量
Connection conn = dataSource.getConnection();
try (conn) { // 直接引用已有变量
    // ...
}

常见误区:

  • ❌ 捕获 Exception 来处理所有异常 → ✅ 掩盖了编程错误,违反异常分类设计,应捕获具体异常类型
  • ❌ 在 catch 块中吞掉异常不做任何处理 → ✅ catch (Exception e) {} 是反模式,至少要记录日志或抛出
  • ❌ 混淆 ErrorException → ✅ Error 是 JVM 级别的问题,不应被业务代码捕获
  • ❌ Catching generic Exception to handle everything → ✅ This masks programming errors and violates exception hierarchy design — always catch specific exception types
  • ❌ Silently swallowing exceptions in catch blocks → ✅ catch (Exception e) {} is an anti-pattern — at minimum, log or rethrow
  • ❌ Confusing Error with Exception → ✅ Error indicates JVM-level problems that should not be caught by business code

延伸追问:

  • 为什么 RuntimeException 是 unchecked 的?(这些异常表示编程错误,到处声明 throws 会让代码变得臃肿且没有实际意义)
  • Spring 事务默认回滚哪些异常?(默认只回滚 RuntimeExceptionError,checked exception 不回滚——可通过 rollbackFor 指定)
  • suppressed exception 是什么?(try-with-resources 中主异常和资源关闭异常的关系,通过 Throwable.addSuppressed() 收集)
  • Why is RuntimeException unchecked? (These represent programming errors — declaring throws everywhere would bloat code with no real value)
  • What is a suppressed exception? (In try-with-resources, it captures resource-close exceptions alongside the primary exception via Throwable.addSuppressed())

风控关联:

  • 风控系统对外部服务调用(征信、名单)的异常应转为 unchecked 并统一由全局异常处理器兜底,避免风控链路因外部服务异常而中断
  • 实时风控决策的异常处理策略:上游超时/异常 → 降级放行 or 拒绝,需在 catch 中明确降级逻辑
  • 关联 风控技术架构题
  • External service call failures (credit checks, blacklists) in risk systems should be wrapped as unchecked exceptions and handled by a global exception handler — preventing risk pipeline interruption
  • Real-time risk decision exception strategy: upstream timeout/error → fallback to pass or reject, with explicit degradation logic in each catch block

English Answer:

Java's exception hierarchy starts with Throwable. It has two major branches: Error and Exception. Error represents serious JVM-level problems such as OutOfMemoryError and StackOverflowError; business code should normally not catch it. Exception represents conditions that application code may handle.

Checked exceptions are subclasses of Exception that are not subclasses of RuntimeException, such as IOException and SQLException. The compiler forces callers to either catch them or declare them with throws. They are intended for predictable, recoverable external failures, such as file I/O, database access, or network resources.

Unchecked exceptions are RuntimeException and its subclasses, such as NullPointerException, IllegalArgumentException, ClassCastException, and ArrayIndexOutOfBoundsException. The compiler does not force handling. They usually represent programming errors or violated preconditions, so the right fix is often to correct the code or validate input earlier rather than catching them everywhere.

try-with-resources automatically closes resources that implement AutoCloseable, and it preserves close-time failures as suppressed exceptions. In real backend systems, checked exceptions from infrastructure are often translated into unchecked exceptions and handled by a global exception handler. In risk-control services, exception handling must explicitly define degradation behavior, for example whether an upstream timeout should lead to pass, reject, review, or fallback scoring.


Q5. Java 泛型擦除机制

EN: What is type erasure? Limitations?

难度: ★★★★ | 出现频率: 高(阿里、美团、字节、京东)

Key Terms: Type Erasure (类型擦除), Bridge Method (桥方法), PECS Principle (PECS 原则), Signature Attribute (Signature 属性), Generic Array (泛型数组)

答案要点:

  1. 什么是类型擦除:Java 泛型是编译期特性。编译后泛型信息被擦除,List<String>List<Integer> 在字节码层面都是 List(原始类型 Raw Type),泛型参数替换为上限类型(默认 Object 或指定的 bound)
  1. 擦除规则
  2. - 无限界泛型 <T> → 擦除为 Object - 有上限 <T extends Comparable<T>> → 擦除为 Comparable - 多上限 <T extends Comparable<T> & Serializable> → 擦除为第一个上限 Comparable

  1. 保留的泛型信息
  2. - 类定义上的泛型签名(Signature 属性)保留,反射可获取 - 方法参数/返回值的泛型信息保留 - 运行时容器内部存储的元素类型信息丢失

  1. 桥方法(Bridge Method):编译器自动生成,保证多态正确性

代码示例:


// 类型擦除证明
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true

// 反射可以绕过泛型检查
List<String> list = new ArrayList<>();
list.add("hello");
Method addMethod = list.getClass().getMethod("add", Object.class);
addMethod.invoke(list, 123); // 编译不报错,运行时 List 里混入了 Integer
// 此时 list.get(1) 取出时抛 ClassCastException

// 桥方法示例
class MyComparator implements Comparable<MyComparator> {
    @Override
    public int compareTo(MyComparator o) { return 0; }
    // 编译器自动生成桥方法:
    // public int compareTo(Object o) { return compareTo((MyComparator) o); }
}

常见误区:

  • ❌ 误以为 new T() 可以编译 → ✅ 类型擦除后 T 变成 Object,无法 new
  • ❌ 误以为 T[] 可以直接创建 → ✅ 只能通过 (T[]) new Object[size]Array.newInstance()
  • ❌ 误以为泛型数组 List<String>[] 可以创建 → ✅ Java 禁止创建泛型数组(类型不安全)
  • ❌ Assuming new T() compiles → ✅ After erasure T becomes Object, so you cannot instantiate it
  • ❌ Assuming generic arrays like List<String>[] can be created → ✅ Java prohibits generic array creation due to type unsafety

延伸追问:

  • 为什么 Java 不像 C# 那样实现真泛型(reified generics)?(向后兼容 JDK 1.4 及之前代码,泛型擦除保证与非泛型代码互操作)
  • <T> T<?> 的区别?(T 是具体类型占位符,可在方法体内使用;? 是未知类型通配符,不能直接使用)
  • PECS 原则是什么?(Producer Extends, Consumer Super——<? extends T> 用于读,<? super T> 用于写)
  • Why did Java choose type erasure over reified generics like C#? (Backward compatibility with pre-JDK 1.4 code — erasure ensures interoperability with non-generic code)
  • What is the PECS principle? (Producer Extends, Consumer Super — <? extends T> for reading, <? super T> for writing)

风控关联:

  • 风控规则引擎的泛型设计:Rule<T extends RiskContext> 定义规则接口,类型擦除后运行时需要通过 Signature 属性反射获取具体 RiskContext 子类
  • 风控特征平台中 Feature<T> 泛型容器,擦除后需额外存储 Class<T> 以做运行时类型校验
  • 关联 风控技术架构题
  • Risk rule engine generic design: Rule<T extends RiskContext> defines the rule interface; after erasure, runtime reflection via the Signature attribute is needed to resolve concrete RiskContext subclasses
  • Risk feature platform Feature<T> containers require an additional Class<T> token after erasure for runtime type validation

English Answer:

Java generics are mainly a compile-time type-safety mechanism. After compilation, most type parameters are erased. For example, List<String> and List<Integer> both become the raw type List at runtime, so their getClass() results are the same.

The erasure rules are straightforward. An unbounded type parameter such as <T> is erased to Object. A bounded type parameter such as <T extends Comparable<T>> is erased to its first bound, Comparable. With multiple bounds, the first bound determines the erased type. Because of this, Java cannot directly instantiate new T(), cannot safely create generic arrays like new T[], and cannot check instanceof List<String> at runtime.

Some generic metadata is still stored in the class file as the Signature attribute. Reflection can read generic signatures from class declarations, fields, method parameters, and method return types, but the actual element type stored inside a runtime collection is not preserved by the collection object itself.

Bridge methods are compiler-generated methods used to preserve polymorphism after erasure. For example, a class implementing Comparable<MyComparator> may compile to both compareTo(MyComparator) and a bridge compareTo(Object) that casts and delegates.

Java chose erasure for backward compatibility with pre-generics code. In frameworks and risk feature platforms, a common workaround is to carry an explicit Class<T> token or type descriptor so runtime validation can still know the intended type.


Q6. Java 反射机制

EN: What is reflection? Downsides?

难度: ★★★ | 出现频率: 高(阿里、美团、字节、京东、百度)

Key Terms: Class Object (Class 对象), MethodHandle (方法句柄), setAccessible (访问控制), java.lang.reflect (反射包), Module System (模块化系统)

答案要点:

  1. 核心类ClassFieldMethodConstructor,都在 java.lang.reflect 包下
  2. 获取 Class 对象的方式
  3. - 类名.class(最安全,编译期检查) - 对象.getClass() - Class.forName("全限定名")(运行时动态加载,可能抛 ClassNotFoundException

  4. 反射能力:运行时获取类结构、调用方法、访问/修改字段(包括 private,需 setAccessible(true)
  5. 性能考量:反射比直接调用慢(JIT 难以内联、需要安全检查),高频调用场景应缓存 Method 对象或使用 MethodHandle
  6. 安全限制:模块化系统(JDK 9+)中,setAccessible(true) 可能因模块封装而失败

代码示例:


// 通过反射调用私有方法
Class<?> clazz = Class.forName("com.example.RiskEngine");
Method method = clazz.getDeclaredMethod("evaluate", RiskContext.class);
method.setAccessible(true);
Object result = method.invoke(riskEngineInstance, context);

// 反射获取泛型信息(Signature 属性)
Field field = clazz.getDeclaredField("features");
ParameterizedType genericType = (ParameterizedType) field.getGenericType();
Type[] typeArgs = genericType.getActualTypeArguments(); // 获取 List<Feature> 中的 Feature

// MethodHandle(JDK 7+,比反射更底层,性能更好)
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, RiskContext.class);
MethodHandle mh = lookup.findVirtual(RiskEngine.class, "evaluate", mt);
String result = (String) mh.invokeExact(engine, context);

常见误区:

  • ❌ 在循环中反复调用 getDeclaredMethod() → ✅ 应缓存 Method 对象
  • ❌ 误以为 setAccessible(true) 在所有环境都能生效 → ✅ SecurityManager 或 JDK 9+ 模块系统可能阻止
  • ❌ 过度使用反射导致代码可读性和可维护性下降 → ✅ 优先使用多态和接口,反射作为最后手段
  • ❌ Calling getDeclaredMethod() repeatedly inside a loop → ✅ Cache the Method object outside the loop
  • ❌ Assuming setAccessible(true) always works → ✅ SecurityManager or JDK 9+ module encapsulation may block it
  • ❌ Overusing reflection, degrading readability and maintainability → ✅ Prefer polymorphism and interfaces; use reflection as a last resort

延伸追问:

  • 反射为什么慢?(运行时类型检查、JIT 无法内联、方法查找开销、参数装箱拆箱)
  • MethodHandleMethod 的区别?(MethodHandle 更底层,支持编译期类型检查,JIT 友好)
  • Spring 中反射用在哪里?(IoC 容器创建 Bean、@Autowired 注入、AOP、@RequestMapping 映射)
  • Why is reflection slow? (Runtime type checks, JIT cannot inline, method lookup overhead, boxing/unboxing)
  • What's the difference between MethodHandle and Method? (MethodHandle is lower-level, supports compile-time type checking, and is more JIT-friendly)

风控关联:

  • 风控规则引擎通过反射动态加载规则类,实现规则热更新(新增规则无需重启服务)
  • 反射性能问题在实时风控链路中尤为敏感——应使用 MethodHandle 或字节码生成(如 ASM)替代反射
  • 关联 风控技术架构题
  • Risk rule engines use reflection to dynamically load rule classes, enabling hot-reload without service restart
  • Reflection overhead is especially critical in real-time risk pipelines — prefer MethodHandle or bytecode generation (e.g., ASM) over raw reflection

English Answer:

Reflection allows Java code to inspect and manipulate classes at runtime. The core reflection types are Class, Field, Method, and Constructor from java.lang.reflect. A Class object can be obtained by SomeClass.class, object.getClass(), or Class.forName("fully.qualified.Name"). The first approach is compile-time safe, while Class.forName is useful for dynamic loading but may throw ClassNotFoundException.

Reflection can read class structure, call methods, create instances, and access fields, including private members if setAccessible(true) is allowed. However, it has downsides: reflective calls are slower than direct calls, harder for the JIT to inline, require runtime checks, and reduce compile-time safety. In hot paths, you should cache Method / Field objects, or consider MethodHandle, which is lower-level and more JIT-friendly.

Since JDK 9, the module system can restrict deep reflection. setAccessible(true) is no longer guaranteed to work if a package is strongly encapsulated and not opened.

Frameworks such as Spring, Hibernate, and JUnit use reflection for dependency injection, annotation scanning, entity mapping, and test execution. In risk-control systems, reflection can load rule classes dynamically for hot updates, but it should not be used repeatedly inside the real-time decision path unless cached or replaced with MethodHandle / generated bytecode.


Q7. JDK 动态代理 vs CGLIB 动态代理

EN: JDK dynamic proxy vs CGLIB?

难度: ★★★★ | 出现频率: 极高(阿里、美团、字节、腾讯、京东)

Key Terms: InvocationHandler (调用处理器), MethodInterceptor (方法拦截器), ASM Bytecode (ASM 字节码), LambdaForm (Lambda 表单), proxy-target-class (代理目标类)

答案要点:

维度 JDK 动态代理 CGLIB 动态代理
原理 运行时生成实现接口的代理类 运行时生成目标类的子类
限制 目标类必须实现接口 不能代理 final 类和 final 方法
核心 API java.lang.reflect.Proxy + InvocationHandler net.sf.cglib.proxy.Enhancer + MethodInterceptor
性能 JDK 8+ 优化后性能优于 CGLIB 生成类略慢,调用性能在 JDK 8 之前更好
Spring 默认 有接口用 JDK 代理,无接口用 CGLIB Spring Boot 2.x 默认全部用 CGLIB(spring.aop.proxy-target-class=true

底层原理

  1. JDK 动态代理Proxy.newProxyInstance() 在运行时动态生成一个实现了指定接口的类,将方法调用转发给 InvocationHandler.invoke()
  2. CGLIB 动态代理:使用 ASM 字节码框架在运行时生成目标类的子类,重写非 final 方法,通过方法拦截器 MethodInterceptor.intercept() 实现 AOP 增强
  3. JDK 8+ 性能优化:JDK 引入了 LambdaFormMethodHandle 优化动态调用,使得 JDK 代理在高并发场景下的性能反超 CGLIB

代码示例:


// JDK 动态代理
public class JdkProxyDemo implements InvocationHandler {
    private final Object target;

    public JdkProxyDemo(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置增强
        long start = System.nanoTime();
        Object result = method.invoke(target, args); // 委托原始对象
        long cost = System.nanoTime() - start;
        log.info("方法 {} 耗时 {} ns", method.getName(), cost);
        return result;
    }

    public static <T> T createProxy(T target) {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new JdkProxyDemo(target)
        );
    }
}

// CGLIB 动态代理
public class CglibProxyDemo implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        long start = System.nanoTime();
        Object result = proxy.invokeSuper(obj, args); // 调用父类方法
        log.info("方法 {} 耗时 {} ns", method.getName(), System.nanoTime() - start);
        return result;
    }

    public static <T> T createProxy(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new CglibProxyDemo());
        return (T) enhancer.create();
    }
}

常见误区:

  • ❌ 误以为 JDK 动态代理可以代理类 → ✅ 只能代理接口,目标类必须实现至少一个接口
  • ❌ 误以为 CGLIB 代理后调用的是原始方法 → ✅ 实际调用的是生成的子类方法,this 引用指向子类
  • ❌ 混淆静态代理和动态代理 → ✅ 静态代理需要手写代理类,动态代理运行时自动生成
  • ❌ Assuming JDK dynamic proxy can proxy classes → ✅ It can only proxy interfaces; the target class must implement at least one interface
  • ❌ Assuming CGLIB calls the original method after proxying → ✅ It calls the generated subclass method — this refers to the subclass
  • ❌ Confusing static proxy with dynamic proxy → ✅ Static proxies require hand-written proxy classes; dynamic proxies are generated at runtime

延伸追问:

  • Spring AOP 的 proxy-target-class 参数有什么影响?(true 强制使用 CGLIB,false 有接口用 JDK 代理)
  • 为什么 Spring Boot 2.x 默认改用 CGLIB?(避免同一 Bean 的接口代理和类方法调用不一致问题)
  • InvocationHandler 中的 proxy 参数能不能用来调用方法?(不能,会死循环——它就是代理对象本身,调用它的方法又会进入 invoke
  • What does the proxy-target-class parameter do in Spring AOP? (true forces CGLIB; false uses JDK proxy when interfaces are present)
  • Why did Spring Boot 2.x switch to CGLIB by default? (To avoid inconsistencies between interface proxy and direct class method invocations on the same bean)

风控关联:

  • 风控系统的日志切面、限流切面、审计切面都通过 AOP 代理实现
  • 动态代理导致 this 引用问题——同一 Bean 内部方法调用不走代理(事务失效场景同理),风控审计日志可能遗漏内部调用链路
  • 关联 Spring | 风控技术架构题
  • Risk system logging, rate-limiting, and audit aspects are all implemented via AOP proxies
  • Dynamic proxy this reference pitfall — internal method calls within the same bean bypass the proxy (same root cause as transaction失效), potentially missing audit log coverage for internal call chains

English Answer:

JDK dynamic proxy is interface-based. Proxy.newProxyInstance() generates a proxy class at runtime that implements the target interfaces. Method calls are forwarded to InvocationHandler.invoke(). Because the generated proxy only implements interfaces, the target class must implement at least one interface, and only interface methods are proxied.

CGLIB proxy is subclass-based. It uses bytecode generation, commonly through ASM, to create a subclass of the target class at runtime. It overrides non-final methods and routes calls through MethodInterceptor.intercept(). Because it depends on subclassing, it cannot proxy final classes or final methods.

In Spring AOP, the proxy choice affects behavior. Traditionally, if a bean has an interface, Spring can use JDK proxy; otherwise it uses CGLIB. Spring Boot 2.x commonly defaults to class-based CGLIB proxies through spring.aop.proxy-target-class=true, reducing inconsistency between interface-level and class-level method calls.

Both proxy mechanisms have the same important pitfall: self-invocation does not go through the proxy. If a method inside the same bean calls another proxied method through this, AOP advice such as transactions, audit logging, rate limiting, or metrics may not run. In risk systems, this can cause missing audit logs or bypassed control logic unless the call path is designed carefully.


Q8. Java 8-21 新特性:Optional / Stream / Record / Sealed Class / Virtual Threads

EN: Key Java 8-21 features?

难度: ★★★★★ | 出现频率: 高(阿里、美团、字节、京东,侧重实践深度)

Key Terms: Optional (可选值), Stream API (流式 API), Record (记录类), Sealed Class (密封类), Virtual Threads (虚拟线程)

答案要点:

Optional(JDK 8)

  • 解决 NullPointerException 的容器类型
  • 正确用法:作为返回值类型,表达"可能为空"的语义
  • 反模式:作为字段类型、方法参数类型、直接调用 get() 不检查

// 正确用法
Optional<RiskResult> result = riskService.evaluate(context);
String level = result.map(RiskResult::getLevel)
                     .orElse("UNKNOWN");

// 反模式
public void process(Optional<String> param) { ... } // 不要这样用

Stream(JDK 8)

  • 函数式数据处理管道:source → intermediate ops → terminal op
  • 中间操作(lazy):filtermapflatMapsorteddistinctpeek
  • 终端操作(eager):collectforEachreducecountfindFirstanyMatch
  • 并行流:parallelStream(),基于 ForkJoinPool,适合 CPU 密集型 + 大数据量

// 风控规则过滤示例
List<Rule> matchedRules = rules.stream()
    .filter(rule -> rule.isEnabled())
    .filter(rule -> rule.match(context))
    .sorted(Comparator.comparingInt(Rule::getPriority).reversed())
    .limit(10)
    .collect(Collectors.toList());

// 并行处理大数据量特征计算
Map<String, Double> featureMap = features.parallelStream()
    .collect(Collectors.toMap(Feature::getName, Feature::compute));

Record(JDK 16 正式版)

  • 不可变数据载体,自动生成 equals()hashCode()toString()getter
  • 适合 DTO、值对象,替代 Lombok 的 @Value

public record RiskEvent(
    String eventId,
    String userId,
    double amount,
    LocalDateTime timestamp
) {}

// 可以添加紧凑构造器做校验
public record FeatureValue(String name, double value) {
    public FeatureValue {
        if (name == null || name.isBlank())
            throw new IllegalArgumentException("特征名不能为空");
    }
}

Sealed Class(JDK 17 正式版)

  • 密封类:限制哪些类可以继承/实现,实现类型安全的穷举匹配

public sealed interface RiskDecision
    permits Approve, Reject, Review, ManualCheck {}

public record Approve(String reason) implements RiskDecision {}
public record Reject(String ruleId, String reason) implements RiskDecision {}
public record Review(String assignee) implements RiskDecision {}
public record ManualCheck(String reason, int priority) implements RiskDecision {}

// Pattern Matching + switch(JDK 21 正式版)
String action = switch (decision) {
    case Approve(var reason) -> "放行: " + reason;
    case Reject(var ruleId, var reason) -> "拒绝: 规则=" + ruleId;
    case Review(var assignee) -> "人工审核: " + assignee;
    case ManualCheck(var reason, var priority) -> "人工复核(P" + priority + ")";
    // 无需 default,编译器检查穷举
};

Virtual Threads 虚拟线程(JDK 21 正式版)

  • 轻量级线程(协程),由 JVM 调度而非 OS 调度,创建成本极低(约 1KB 栈内存)
  • 适合 IO 密集型高并发场景(如大量 HTTP/RPC/DB 调用),不适合 CPU 密集型
  • 底层原理:挂起时卸载载体线程(carrier thread,平台线程),恢复时挂载到可用载体线程

// 使用虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<RiskResult>> futures = userIds.stream()
        .map(userId -> executor.submit(() -> riskService.queryRisk(userId)))
        .toList();
    // 百万级并发查询,无需线程池调参
}

// 或直接创建虚拟线程
Thread.startVirtualThread(() -> {
    RiskResult result = riskService.evaluate(context);
    callback.onComplete(result);
});

常见误区:

  • Optional.get() 不检查直接调用 → ✅ 比 null 还危险,会抛 NoSuchElementException,应使用 orElse/orElseThrow
  • ❌ 并行流(parallelStream)在 IO 密集型场景使用 → ✅ 会阻塞公共 ForkJoinPool,影响全局
  • ❌ 虚拟线程中 synchronized 会 pin(固定)载体线程 → ✅ 应改用 ReentrantLock
  • ❌ Calling Optional.get() without checking → ✅ Even more dangerous than null — throws NoSuchElementException; use orElse/orElseThrow instead
  • ❌ Using parallelStream for IO-intensive work → ✅ It blocks the shared ForkJoinPool, impacting the entire application
  • ❌ Using synchronized inside virtual threads → ✅ It pins the carrier thread; switch to ReentrantLock

延伸追问:

  • Streampeek() 能替代 forEach() 做有副作用的操作吗?(不能,peek() 是中间操作,不触发流的执行,且规范不保证执行次数)
  • Virtual Threads 和 Kotlin Coroutines / Go Goroutines 的区别?(VT 是 JVM 级别的调度,无需改代码范式,兼容现有线程 API;协程需要 async/await 语法)
  • Record 能继承类/实现接口吗?(不能继承类(隐式继承 Record),可以实现接口)
  • Can Stream.peek() replace forEach() for side effects? (No — peek() is an intermediate operation that doesn't trigger stream execution, and execution count is not guaranteed)
  • How do Virtual Threads differ from Kotlin Coroutines or Go Goroutines? (VTs are JVM-scheduled, require no code paradigm change, and are fully compatible with existing Thread APIs)

风控关联:

  • 风控特征计算用 Stream 并行处理批量特征,注意共享 ForkJoinPool 的线程隔离
  • Record 作为风控事件 DTO,天然不可变,避免数据在传递过程中被篡改
  • Sealed Class 实现风控决策类型的安全穷举——编译器保证所有决策类型都被处理,杜绝遗漏
  • Virtual Threads 在风控系统中适合并发调用多个外部服务(征信、黑名单、设备指纹),大幅降低线程池配置复杂度
  • 关联 并发编程 | 风控技术架构题
  • Sealed Class enables exhaustive pattern matching on risk decisions — the compiler guarantees all decision types are handled, eliminating missed cases
  • Virtual Threads are ideal for fan-out calls to multiple risk data sources (credit bureau, blacklist, device fingerprint), drastically simplifying thread pool configuration

English Answer:

Optional, introduced in JDK 8, is a container for values that may be absent. It should mainly be used as a return type to express "possibly empty". It should not usually be used as a field type or method parameter, and calling get() without checking is an anti-pattern. Prefer map, flatMap, orElse, orElseGet, and orElseThrow.

Stream API provides a functional data-processing pipeline: source, intermediate operations, and terminal operation. Intermediate operations such as filter, map, flatMap, sorted, distinct, and peek are lazy. Terminal operations such as collect, forEach, reduce, count, findFirst, and anyMatch trigger execution. parallelStream() uses the common ForkJoinPool and is suitable mainly for CPU-bound work with enough data; using it for blocking I/O can harm the whole application.

Record, finalized in JDK 16, is an immutable data carrier. It automatically provides constructor, accessors, equals, hashCode, and toString. It is useful for DTOs and value objects, and it can include compact constructors for validation.

Sealed classes, finalized in JDK 17, restrict which classes can extend or implement a type. They are useful when the domain has a closed set of variants. With pattern matching and switch in modern Java, they allow the compiler to check exhaustiveness, which is valuable for risk decision types such as approve, reject, review, and manual check.

Virtual threads, finalized in JDK 21, are lightweight JVM-managed threads. They are designed for high-concurrency I/O-bound workloads, such as many HTTP, RPC, or database calls. Blocking code can remain simple, while the JVM parks and unparks virtual threads on a smaller set of carrier platform threads. They are not a replacement for CPU parallelism, and synchronized blocks that pin carrier threads should be avoided; ReentrantLock is often a better choice in virtual-thread-heavy code.


Q9. equals() 与 hashCode() 的契约关系

EN: equals() and hashCode() contract?

难度: ★★★★ | 出现频率: 极高(阿里、美团、字节、京东、腾讯)

Key Terms: equals Contract (equals 契约), hashCode (哈希码), Objects.hash (哈希工具), HashMap Bucket Index (HashMap 桶下标), Record (记录类)

答案要点:

核心契约(Object 规范)

  1. 一致性:多次调用 equals() 结果不变(对象未被修改的前提下)
  2. 自反性x.equals(x) 返回 true
  3. 对称性x.equals(y)y.equals(x)
  4. 传递性x.equals(y)y.equals(z)x.equals(z)
  5. hashCode 契约
  6. - equals() 相同 → hashCode() 必须相同 - hashCode() 相同 → equals() 不一定相同(哈希碰撞) - 对象未被修改时,多次调用 hashCode() 返回值一致

为什么重写 equals 必须同时重写 hashCode

如果只重写 equals() 不重写 hashCode(),两个逻辑相等的对象可能 hash 不同,导致在 HashMap/HashSet 中表现为"两个不同的元素"——查找时先按 hash 定位桶,equals 相等但 hash 不同则分到不同桶,永远找不到。

代码示例:


// 标准写法(JDK 7+ Objects 工具类)
public class RiskRule {
    private final String ruleId;
    private final String ruleName;
    private final int priority;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        RiskRule riskRule = (RiskRule) o;
        return priority == riskRule.priority
            && Objects.equals(ruleId, riskRule.ruleId)
            && Objects.equals(ruleName, riskRule.ruleName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(ruleId, ruleName, priority);
    }
}

// JDK 17+ Record 自动生成正确的 equals 和 hashCode
public record RiskRule(String ruleId, String ruleName, int priority) {}

常见误区:

  • ❌ 只重写 equals() 忘记重写 hashCode() → ✅ HashMap/HashSet 行为异常,两者必须同时重写
  • hashCode() 中使用了可变字段 → ✅ 对象放入集合后修改字段导致 hash 变化,"丢失"元素
  • ❌ 误以为 hashCode() 返回值就是 HashMap 的桶下标 → ✅ 实际是 (n - 1) & hash(hashCode()) 做了二次扰动
  • ❌ Overriding equals() without overriding hashCode() → ✅ HashMap/HashSet will behave incorrectly — both must be overridden together
  • ❌ Using mutable fields in hashCode() → ✅ Changing a field after inserting into a collection changes the hash, effectively "losing" the element
  • ❌ Assuming hashCode() return value is the HashMap bucket index → ✅ The actual index is (n - 1) & hash(hashCode()) with a secondary perturbation

延伸追问:

  • Objects.hash() 底层实现?(Arrays.hashCode() 遍历元素组合,基于 31 乘法)
  • 为什么 String 的 hashCode 可以缓存?(String 不可变,hash 值不会变,延迟计算后缓存)
  • TreeSet 需要重写 hashCode() 吗?(不需要,TreeSet 基于 Comparator 排序,不依赖 hash)
  • How is Objects.hash() implemented internally? (It delegates to Arrays.hashCode() combining elements with a 31-multiplier algorithm)
  • Does TreeSet require overriding hashCode()? (No — TreeSet uses Comparator for ordering, not hash-based lookup)

风控关联:

  • 风控规则去重(HashSet<RiskRule>)、特征缓存键(HashMap<FeatureKey, FeatureValue>)必须正确实现 equalshashCode
  • 风控决策缓存如果 hashCode 实现有缺陷,会导致相同请求无法命中缓存,重复执行规则链路,影响性能
  • 关联 风控技术架构题
  • Risk rule deduplication (HashSet<RiskRule>) and feature cache keys (HashMap<FeatureKey, FeatureValue>) must correctly implement both equals and hashCode
  • A flawed hashCode implementation in risk decision caches causes cache misses on identical requests, forcing redundant rule evaluation and degrading performance

English Answer:

The equals() contract requires consistency, reflexivity, symmetry, and transitivity. If an object is not modified, repeated equals() calls should return the same result. x.equals(x) must be true. If x.equals(y) is true, then y.equals(x) must also be true. If x.equals(y) and y.equals(z) are both true, then x.equals(z) must be true.

The hashCode() contract is tied to equals(). If two objects are equal according to equals(), they must have the same hash code. However, the reverse is not guaranteed: two different objects may share the same hash code because collisions are possible. If an object is not modified, repeated hashCode() calls should return the same value.

Whenever you override equals(), you must override hashCode() using the same logical fields. If two logically equal objects have different hash codes, HashMap and HashSet may treat them as different entries. This happens because hash-based collections first locate a bucket by hash and then use equals() only within that bucket.

Do not use mutable fields in hashCode() if the object may be inserted into a hash-based collection. If the field changes after insertion, the object may become unreachable from the expected bucket. In modern Java, Objects.equals() and Objects.hash() are common helpers, and records automatically generate correct equals() and hashCode() based on their components.


Q10. 接口 vs 抽象类

EN: Interface vs abstract class?

难度: ★★★ | 出现频率: 高(阿里、美团、字节、京东)

Key Terms: Interface (接口), Abstract Class (抽象类), Default Method (default 方法), Template Method (模板方法), Multiple Implementation (多实现)

答案要点:

维度 接口(Interface) 抽象类(Abstract Class)
实例化 都不能直接实例化
构造方法
成员变量 只能有 public static final 常量 可以有任意访问修饰符的实例变量
方法 JDK 8: default/static 方法;JDK 9: private 方法 可以有抽象方法和具体方法
多继承 类可实现多个接口 类只能继承一个抽象类(单继承)
设计层面 定义行为契约("能做什么") 定义模板和部分实现("是什么")
JDK 8+ default 方法允许接口提供默认实现 优势缩小,但抽象类仍有状态管理能力

JDK 8/9 对接口的增强


public interface RiskEvaluator {
    // 抽象方法
    RiskResult evaluate(RiskContext context);

    // JDK 8: default 方法——提供默认实现
    default String getEvaluatorName() {
        return this.getClass().getSimpleName();
    }

    // JDK 8: static 方法——工具方法
    static RiskResult reject(String ruleId) {
        return new RiskResult("REJECT", ruleId);
    }

    // JDK 9: private 方法——复用 default 方法的公共逻辑
    private double calculateScore(RiskContext context, double weight) {
        return context.getBaseScore() * weight;
    }
}

设计选择原则

  • 用接口:定义能力/行为,如 SerializableComparableRiskEvaluator
  • 用抽象类:提供模板实现(Template Method 模式),子类只需填充部分逻辑,如 AbstractRiskEngine
  • IS-A 关系用抽象类,CAN-DO 关系用接口

常见误区:

  • ❌ 误以为接口不能有方法实现 → ✅ JDK 8+ 有 default 方法
  • ❌ 误以为接口和抽象类可以互换 → ✅ 抽象类有状态(实例变量),接口只有行为
  • ❌ 滥用 default 方法在接口中放业务逻辑 → ✅ 接口应保持轻量,复杂逻辑放在抽象类中
  • ❌ Assuming interfaces cannot have method implementations → ✅ JDK 8+ supports default methods
  • ❌ Assuming interfaces and abstract classes are interchangeable → ✅ Abstract classes hold state (instance variables); interfaces define behavior only
  • ❌ Overusing default methods to embed business logic in interfaces → ✅ Interfaces should stay lightweight; move complex logic to abstract classes

延伸追问:

  • 接口的 default 方法和抽象类的具体方法冲突时怎么办?(类优先:具体类的方法优先于接口的 default 方法;两个接口有相同 default 方法时,子类必须显式覆写)
  • 接口能继承接口吗?抽象类能实现接口吗?(都能)
  • 为什么 Collections.sort()List 移到了接口的 default 方法?(减少对工具类的依赖,List.sort() 更面向对象)
  • What happens when an interface default method conflicts with an abstract class method? (Class wins: the concrete class method takes priority over the interface default; if two interfaces provide the same default, the subclass must explicitly override)
  • Why was Collections.sort() moved to a default method on List? (To reduce utility-class dependency — list.sort() is more object-oriented)

风控关联:

  • 风控引擎架构中:RiskEvaluator(接口)定义评估行为契约,AbstractRiskEngine(抽象类)提供模板实现(特征获取 → 规则匹配 → 决策输出),具体引擎(如 FraudRiskEngineCreditRiskEngine)继承抽象类并实现接口
  • 接口的多实现特性允许一个风控服务同时实现 RiskEvaluatorHealthCheckableMetricsReportable 等多个能力契约
  • 关联 风控技术架构题
  • Risk engine architecture: RiskEvaluator (interface) defines the evaluation contract, AbstractRiskEngine (abstract class) provides the template implementation (feature fetch → rule match → decision output), and concrete engines extend the abstract class while implementing the interface
  • Multiple interface implementation allows a single risk service to simultaneously implement RiskEvaluator, HealthCheckable, MetricsReportable, and other capability contracts

English Answer:

Both interfaces and abstract classes define incomplete types that cannot be directly instantiated, but their design purposes are different. An interface defines a behavior contract, or "what this type can do". A class can implement multiple interfaces, which makes interfaces suitable for capabilities such as Serializable, Comparable, RiskEvaluator, HealthCheckable, or MetricsReportable.

An abstract class defines a common base type and can provide shared state and partial implementation. It can have constructors, instance fields, concrete methods, and abstract methods. Because Java supports single class inheritance, a class can extend only one abstract class.

Since JDK 8, interfaces can have default and static methods, and since JDK 9 they can have private helper methods. This narrows the gap between interfaces and abstract classes, but interfaces still should not be overloaded with complex business logic. They are best kept as lightweight contracts.

The practical rule is: use an interface to model capabilities or pluggable behavior, and use an abstract class when subclasses share state, construction logic, or a template method workflow. In a risk engine, RiskEvaluator can be an interface, while AbstractRiskEngine can implement the common workflow: feature fetching, rule matching, score calculation, and decision output.


Q11. Java 对象布局与 synchronized 锁升级

EN: Object layout and lock escalation?

难度: ★★★★★ | 出现频率: 高(阿里、字节、美团、京东)

Key Terms: Mark Word (标记字), Lock Escalation (锁升级), Biased Lock (偏向锁), Lightweight Lock (轻量级锁), Heavyweight Lock (重量级锁)

答案要点:

  1. 对象内存布局(64 位 JVM,开启指针压缩):
  2. - 对象头(Mark Word + Klass Pointer):12 字节(Mark Word 8 字节 + Klass Pointer 4 字节) - 实例数据:对象实际字段 - 对齐填充:补齐到 8 字节的整数倍

  1. Mark Word 结构(存储锁状态信息):
  2. - 无锁:对象 hashCode、GC 分代年龄 - 偏向锁:线程 ID、Epoch - 轻量级锁:指向栈中 Lock Record 的指针 - 重量级锁:指向 Monitor(ObjectMonitor)的指针

  1. 锁升级过程(不可降级,GC 时可批量重偏向/撤销):
  2. - 无锁 → 偏向锁:第一个线程访问同步代码块,CAS 将线程 ID 写入 Mark Word - 偏向锁 → 轻量级锁:第二个线程尝试获取锁,偏向锁撤销(safe point),升级为轻量级锁,竞争线程通过自旋(CAS)尝试获取 - 轻量级锁 → 重量级锁:自旋超过阈值(自适应自旋),膨胀为重量级锁,未获得锁的线程进入 BLOCKED 状态(用户态 → 内核态切换)

代码示例:


// 使用 JOL (Java Object Layout) 查看对象布局
RiskRule riskRule = new RiskRule("R001", "金额异常", 1);
System.out.println(ClassLayout.parseInstance(riskRule).toPrintable());

// 锁升级观察
Object lock = new Object();
System.out.println(ClassLayout.parseInstance(lock).toPrintable()); // 无锁

synchronized (lock) {
    // 第一次进入——偏向锁(注意有延迟,JVM 默认 4 秒后才开启偏向锁)
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}

常见误区:

  • ❌ 误以为轻量级锁一定比重量级锁快 → ✅ 自旋消耗 CPU,高竞争下重量级锁反而更优
  • ❌ 误以为偏向锁可以降级 → ✅ 只能撤销不能降级,批量撤销发生在全局安全点(STW)
  • ❌ 误以为 synchronized 一直是重量级锁 → ✅ JDK 6+ 已引入锁升级优化
  • ❌ Assuming lightweight locks are always faster than heavyweight locks → ✅ Spinning burns CPU; under high contention, heavyweight locks are often better
  • ❌ Assuming biased locks can be downgraded → ✅ They can only be revoked, not downgraded; bulk revocation occurs at a global safepoint (STW)
  • ❌ Assuming synchronized is always heavyweight → ✅ JDK 6+ introduced lock escalation optimizations

延伸追问:

  • 偏向锁为什么在 JDK 15 被默认废弃?(现代系统中多线程竞争普遍,偏向锁的撤销成本高,收益低)
  • synchronizedReentrantLock 的选择?(synchronized 足够简单,JVM 层面持续优化;ReentrantLock 提供公平锁、可中断、超时获取、Condition 等高级特性)
  • 锁消除(Lock Elimination)是什么?(JIT 通过逃逸分析发现锁对象不会逸出,自动消除同步)
  • Why was biased locking deprecated by default in JDK 15? (Multi-thread contention is common in modern systems; revocation cost outweighs benefits)
  • What is lock elimination? (The JIT uses escape analysis to detect that a lock object never escapes, then automatically removes the synchronization)

风控关联:

  • 实时风控引擎的规则匹配方法如果加 synchronized,在高并发下会经历完整的锁升级过程。建议用 ReentrantLockConcurrentHashMap 替代,减少锁竞争
  • 风控特征缓存的读写场景:用 ReadWriteLockReentrantReadWriteLock)替代 synchronized,读操作不互斥,提升吞吐
  • 关联 并发编程 | JVM | 风控技术架构题
  • If rule-matching methods in a real-time risk engine use synchronized, they undergo full lock escalation under high concurrency. Prefer ReentrantLock or ConcurrentHashMap to reduce contention
  • Risk feature cache read/write scenarios: use ReadWriteLock (ReentrantReadWriteLock) over synchronized — reads are non-exclusive, boosting throughput

English Answer:

A Java object normally consists of an object header, instance data, and padding. On a 64-bit JVM with compressed ordinary object pointers enabled, the object header is typically 12 bytes: an 8-byte Mark Word plus a 4-byte Klass Pointer. Instance data stores the fields of the object, and padding aligns the object size to a multiple of 8 bytes.

The Mark Word stores runtime metadata, including lock state, hash code, GC age, biased-lock thread ID, or pointers to lock records / monitors depending on the current state. In the unlocked state, it can store the object's hash code and age. In biased-lock state, it records the owning thread ID and epoch. In lightweight-lock state, it points to a lock record on the stack. In heavyweight-lock state, it points to an ObjectMonitor.

The lock escalation path is generally: no lock, biased lock, lightweight lock, and heavyweight lock. The first thread may bias the lock by writing its thread ID into the Mark Word. When another thread competes, the JVM revokes the bias at a safepoint and upgrades to a lightweight lock, where threads use CAS and spinning. If contention remains high or spinning is not effective, the lock inflates into a heavyweight monitor, and blocked threads are parked by the operating system.

Locks generally do not downgrade in normal execution, although the JVM can bulk rebias or revoke biased locks during GC-related safepoints. Lightweight locks are not always faster: under high contention, spinning wastes CPU and a heavyweight monitor can be more appropriate. In low-latency risk engines, putting synchronized on hot rule-matching paths can cause serious contention; concurrent collections, ReentrantLock, or read-write locks are often better choices.


Q12. Java 引用类型与 GC Roots

EN: Four reference types?

难度: ★★★★ | 出现频率: 中高(阿里、美团、字节)

Key Terms: StrongReference (强引用), SoftReference (软引用), WeakReference (弱引用), PhantomReference (虚引用), GC Roots (GC 根集)

答案要点:

Java 提供四种引用类型,引用强度从强到弱:

引用类型 回收时机 用途
强引用(Strong) 永不回收(除非不可达) 普通对象引用
软引用(SoftReference) 内存不足时回收 缓存(如 Guava Cache 底层可选策略)
弱引用(WeakReference) 下一次 GC 时回收 WeakHashMap、ThreadLocal 的 Entry
虚引用(PhantomReference) 随时回收,仅用于跟踪回收 管理堆外内存(DirectByteBuffer)

GC Roots 包括

  1. 栈帧中的局部变量引用
  2. 方法区中静态变量引用
  3. 方法区中常量引用
  4. JNI(Native 方法)引用
  5. JVM 内部引用(基本类型对应的 Class 对象、常驻异常、类加载器等)

代码示例:


// 软引用——风控特征缓存
Map<String, SoftReference<List<Feature>>> featureCache = new ConcurrentHashMap<>();
featureCache.put("user:123", new SoftReference<>(loadFeatures("123")));

List<Feature> features = Optional.ofNullable(featureCache.get("user:123"))
    .map(SoftReference::get)
    .orElseGet(() -> {
        List<Feature> loaded = loadFeatures("123"); // 缓存未命中,重新加载
        featureCache.put("user:123", new SoftReference<>(loaded));
        return loaded;
    });

// 弱引用——临时关联
WeakHashMap<Key, Metadata> tempMapping = new WeakHashMap<>();
// Key 没有其他强引用时,Entry 自动被 GC 清理

// ThreadLocal 的弱引用——理解内存泄漏的关键
// ThreadLocalMap.Entry extends WeakReference<ThreadLocal<?>>
// ThreadLocal 对象被回收后,Entry 的 key 变为 null,但 value 仍被强引用
// 如果不调用 remove(),value 就泄漏了

常见误区:

  • ❌ 误以为软引用一定会被 GC 回收 → ✅ 只在内存不足时才回收,内存充足时不会被回收
  • ❌ ThreadLocal 内存泄漏的根本原因是弱引用 → ✅ 根本原因是 Entry 的 value 被 ThreadLocalMap 强引用,线程池中线程复用时尤为严重
  • ❌ 误以为虚引用的 get() 方法能获取对象 → ✅ 永远返回 null,必须配合 ReferenceQueue 使用
  • ❌ Assuming soft references are always GC'd → ✅ They are only collected when memory is low; under sufficient memory they persist
  • ❌ Thinking ThreadLocal memory leaks are caused by weak references → ✅ The root cause is that Entry values are strongly referenced by ThreadLocalMap — especially severe with thread pool reuse
  • ❌ Assuming PhantomReference.get() can retrieve the object → ✅ It always returns null; must be used with a ReferenceQueue

延伸追问:

  • WeakHashMap 适合做什么?(缓存映射,Key 没有外部引用时自动清理 Entry)
  • ThreadLocal 内存泄漏如何避免?(使用完后在 finally 中调用 remove()
  • JVM 如何处理循环引用?(GC Roots 可达性分析不关心循环引用,只看是否从 Roots 可达)
  • What is WeakHashMap good for? (Cache mappings where entries are auto-cleaned when keys lose all external references)
  • How does the JVM handle circular references? (GC Roots reachability analysis ignores cycles — it only checks if objects are reachable from Roots)

风控关联:

  • 风控特征缓存使用 SoftReference:内存紧张时自动释放缓存,保证主流程不 OOM;内存充足时命中缓存减少特征计算延迟
  • ThreadLocal 在风控链路中用于传递 RiskContext(链路追踪 ID、请求来源等),必须在请求结束后的 finally 块中 remove() 防止线程池场景下的内存泄漏和数据串扰
  • 关联 JVM | 并发编程 | 风控技术架构题
  • Risk feature caches use SoftReference to auto-release under memory pressure, preventing OOM in the main pipeline; when memory is ample, cache hits reduce feature computation latency
  • ThreadLocal is used in risk pipelines to propagate RiskContext (trace IDs, request sources, etc.) — always call remove() in a finally block after request completion to prevent memory leaks and data cross-contamination in thread pool scenarios

English Answer:

Java has four main reference strengths: strong, soft, weak, and phantom. A strong reference is the normal object reference; as long as an object is strongly reachable from GC Roots, it will not be collected. A soft reference is cleared when memory is low, so it can be used for memory-sensitive caches, although modern cache libraries often provide more predictable policies.

A weak reference is cleared on the next GC once the object is no longer strongly reachable. WeakHashMap uses weak keys so entries can disappear automatically when the key has no other strong references. ThreadLocal also involves weak references: ThreadLocalMap.Entry weakly references the ThreadLocal key, but the value is still strongly referenced by the map. If the thread is reused in a pool and remove() is not called, the value can leak even after the key is cleared.

A phantom reference cannot retrieve the object through get(); it always returns null. It is used with ReferenceQueue to track object reclamation and perform cleanup, such as managing off-heap resources.

GC Roots include local variables in stack frames, static fields, constants, JNI references, class loaders, and JVM internal references. The JVM uses reachability from GC Roots, so circular references are not a problem if the cycle is not reachable from any root.

In risk-control systems, soft references may be used for feature caches under memory pressure, while ThreadLocal is often used to carry RiskContext such as trace IDs and request source. In thread-pool environments, ThreadLocal values must be removed in a finally block to avoid memory leaks and cross-request data contamination.


关联

  • 并发编程 — synchronized 锁升级、CAS、线程池、并发集合
  • JVM — 对象布局、GC 算法、类加载机制、内存模型
  • Spring — 动态代理(AOP)、反射(IoC)、事务传播
  • 风控技术架构题 — 实时风控引擎中的 Java 基础知识应用