Skip to content

一、Java 基础(20 题)

1.1 ★★★★==equals() 的区别是什么?为什么重写 equals() 时必须同时重写 hashCode()?如果不重写会导致什么问题?

📖 点击查看答案

==equals() 的区别:

  • ==:比较的是引用地址(基本类型比较值,引用类型比较内存地址)
  • equals():默认实现也是比较地址(继承自 Object),但通常被重写为比较内容(如 String、Integer)
java
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);        // false,不同对象
System.out.println(a.equals(b));   // true,内容相同

为什么重写 equals 必须重写 hashCode:

  • Java 规范:equals 相等的两个对象,hashCode 必须相等;反之不成立
  • 哈希容器(HashMap、HashSet)先通过 hashCode 定位桶,再通过 equals 判断是否相等
  • 如果只重写 equals 不重写 hashCode,两个"相等"的对象会有不同的 hashCode,会被放到不同的桶中

不重写会导致的问题:

java
class Person {
    String name;
    // 只重写了 equals,没重写 hashCode
    public boolean equals(Object o) { return name.equals(((Person)o).name); }
}

Set<Person> set = new HashSet<>();
set.add(new Person("Tom"));
set.add(new Person("Tom"));
// set.size() == 2,明明逻辑相等却被当作两个对象,破坏集合语义
  • HashSet/HashMap 中出现"重复"键
  • 无法正确查找已存在的 key

hashCode 推荐写法:

java
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

1.2 ★★深拷贝和浅拷贝的区别是什么?如何实现深拷贝?

📖 点击查看答案

核心区别:

  • 浅拷贝:只复制对象本身,不复制引用类型字段指向的对象(新旧对象共享内部引用对象)
  • 深拷贝:递归复制对象及其所有引用对象,新旧对象完全独立
java
class Address { String city; }
class Person implements Cloneable {
    String name;
    Address address;  // 引用类型
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 浅拷贝:address 仍指向同一对象
    }
}

浅拷贝的实现方式:

  • 实现 Cloneable 接口,重写 clone() 方法调用 super.clone()
  • 拷贝构造方法(只复制基本类型和引用地址)

深拷贝的实现方式:

  1. 递归 clone:每个引用类型字段也实现 Cloneable 并在 clone 中递归调用
java
@Override
protected Person clone() throws CloneNotSupportedException {
    Person p = (Person) super.clone();
    p.address = (Address) address.clone(); // 深拷贝
    return p;
}
  1. 序列化方式(推荐):实现 Serializable,通过序列化/反序列化生成全新对象
java
@SuppressWarnings("unchecked")
public Person deepCopy() throws Exception {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(this);
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    return (Person) ois.readObject();
}
  1. JSON 工具:如 Jackson objectMapper.convertValue、Gson toJson/fromJson
  2. 第三方库:如 Apache Commons Lang 的 SerializationUtils.clone()

1.3 ★★★finalfinallyfinalize 的区别是什么?final 修饰的变量、方法、类分别有什么影响?finally 代码块一定会执行吗?

📖 点击查看答案

三者毫无关系,仅是名字相似:

关键字作用
final修饰符,表示"不可变"
finally异常处理块,保证资源释放
finalizeObject 方法,GC 回收前调用(已废弃)

final 的三种用法:

  • 修饰类:该类不能被继承(如 String、Integer)
  • 修饰方法:该方法不能被子类重写
  • 修饰变量:基本类型值不可变;引用类型引用不可变(但对象内部可变)
java
final int x = 10;
final List<String> list = new ArrayList<>();
list.add("a"); // OK,对象内部可变
// list = new ArrayList<>(); // 编译错误,引用不可变

finally 不执行的情况:

  1. 在 try 块之前就异常退出(try 都没进入)
  2. 在 try 块中调用了 System.exit(0)
  3. JVM 崩溃或被强制杀死
  4. 当前线程被"死锁"或被无限阻塞(理论不退出)
  5. 守护线程被 JVM 终止时
java
try {
    System.exit(0);
} finally {
    System.out.println("不会执行"); // 不会输出
}

finalize(JDK 9 已废弃):

  • GC 回收对象前由 GC 线程调用,执行时间不确定,可能导致复活对象
  • 已被 Cleaner / try-with-resources 取代
  • 性能差、易导致内存泄漏

1.4 ★★★★接口(interface)和抽象类(abstract class)在设计层面有什么本质区别?JDK 8 之后接口的 default 方法解决了什么问题?

📖 点击查看答案

设计层面的本质区别:

  • 抽象类:is-a 关系,表示"是什么",用于模板抽象(有状态、有构造器、有具体方法)
  • 接口:can-do 关系,表示"能做什么",是行为契约(无状态、多实现)

语法层面区别:

维度抽象类接口
继承单继承多实现
成员变量任意类型默认 public static final
方法任意访问级别JDK 8 前 abstract;JDK 8+ 可有 default/static;JDK 9+ 可有 private
构造器
状态有实例字段无实例字段

default 方法解决的问题:

  • JDK 8 之前,给已有接口添加方法会破坏所有实现类
  • default 方法允许接口提供默认实现,实现类可不重写
java
interface Vehicle {
    default void start() {
        System.out.println("启动");
    }
    static Vehicle create() { return new Car(); }
}
  • 典型应用:java.util.Collection 在 JDK 8 加入 stream()forEach() 等 default 方法,兼容旧实现

default 方法冲突规则(多继承):

  • 类优先于接口:父类方法覆盖接口 default
  • 子接口优先于父接口
  • 冲突时必须显式重写:Interface.super.method()
java
class C implements A, B {
    public void hello() { A.super.hello(); } // 显式选择
}

选择建议:

  • 描述本质类型用抽象类(如 AbstractList)
  • 描述能力/契约用接口(如 Comparable、Iterable)

1.5 ★★★★StringStringBuilderStringBuffer 三者的核心区别是什么?String 被设计为不可变的原因有哪些?

📖 点击查看答案

三者核心区别:

可变性线程安全性能应用场景
String不可变安全(不可变)拼接慢少量字符串、常量
StringBuilder可变不安全最快单线程拼接
StringBuffer可变安全(synchronized)较慢多线程拼接
java
// String 拼接会创建多个对象
String s = "a" + "b" + "c"; // 编译期优化为 "abc"
String s2 = "";
for (int i = 0; i < 100; i++) s2 += i; // 每次循环都创建 StringBuilder 和新 String

// 推荐:显式使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.append(i);

String 不可变的原因:

  1. 线程安全:不可变对象天然线程安全,可自由共享
  2. 支持字符串常量池:多个引用可指向同一常量池对象,节省内存
  3. hashCode 可缓存:String 的 hashCode 计算后缓存,作为 HashMap 的 key 时性能高
  4. 安全性:作为参数传递时不会被恶意修改(如类加载器、数据库 URL)
  5. 不可变才有 hashCode 缓存:作为 key 时不会出现"放进去后取不出"的问题

实现方式:

java
public final class String  // final 防止子类破坏不可变
        implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char[] value; // JDK 8;JDK 9+ 改为 byte[] + coder
}
  • 类声明 final
  • 字段 private final
  • 不提供任何修改内部数组的方法
  • 构造时进行防御性拷贝

1.6 ★★自动装箱和拆箱的原理是什么?Integer 缓存池(-128~127)的原理是什么?

📖 点击查看答案

自动装箱与拆箱原理:

  • 装箱:基本类型 → 包装类型,编译器自动调用 Integer.valueOf(int)
  • 拆箱:包装类型 → 基本类型,编译器自动调用 Integer.intValue()
java
Integer a = 100;         // 编译为 Integer a = Integer.valueOf(100);
int b = a;               // 编译为 int b = a.intValue();

Integer 缓存池原理:

  • Integer.valueOf-128 ~ 127 范围内返回缓存对象(IntegerCache
  • 超出范围则 new Integer() 创建新对象
java
Integer a = 100, b = 100;
System.out.println(a == b);        // true,命中缓存

Integer c = 200, d = 200;
System.out.println(c == d);        // false,超出范围 new 了两个对象
System.out.println(c.equals(d));   // true

缓存源码(IntegerCache):

java
private static class IntegerCache {
    static final Integer[] cache;
    static final int low = -128;
    static final int high; // 默认 127,可通过 -XX:AutoBoxCacheMax 调整
    static {
        int h = 127;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for (int k = 0; k < cache.length; k++) cache[k] = new Integer(j++);
    }
}

其他包装类的缓存:

  • Byte、Short、Long:-128 ~ 127
  • Character:0 ~ 127
  • Boolean:TRUE、FALSE 两个常量

注意坑点:

  • 包装类型比较一定要用 equals(),不要用 ==
  • 自动拆箱遇到 null 会抛 NPE
  • 大量装箱运算(如 Long sum = 0L; for...sum += i;)会反复创建对象,性能差

1.7 ★★★★Java 泛型中的类型擦除是什么意思?通配符 <? extends T><? super T> 的区别和使用场景是什么?

📖 点击查看答案

类型擦除:

  • Java 泛型只在编译期检查类型安全,运行时所有泛型类型都被擦除为原始类型(Object 或上界)
  • List<String>List<Integer> 在运行时都是 List
java
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true

类型擦除带来的限制:

  • 不能 new T()new T[]
  • 不能用基本类型作泛型参数(要 Integer 不能 int
  • 不能 instanceof List<String>,只能 instanceof List
  • 静态方法/字段不能使用类的泛型参数
  • 不能创建泛型数组

通配符 PECS 原则(Producer Extends, Consumer Super):

  • <? extends T>:上界通配符,能读不能写

    • 适合"生产者"场景:从集合读取数据
    • 编译器只允许读出 T(实际是 T 或子类,可安全当 T 读),不允许写入(无法确定集合实际存的子类类型)
  • <? super T>:下界通配符,能写不能读

    • 适合"消费者"场景:往集合写入数据
    • 允许写入 T 或其子类,但读取只能得到 Object
java
// 读取场景:把任意 Number 子类列表复制到目标
public static double sum(List<? extends Number> list) {
    double s = 0;
    for (Number n : list) s += n.doubleValue(); // 读安全
    return s;
}

// 写入场景:向列表中添加 Integer
public static void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); // 写安全
}

// Collections.copy 的经典签名
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

记忆口诀: 频繁往外读取用 extends,频繁插入用 super。


1.8 ★★★Java 反射机制的原理是什么?为什么说反射会影响性能?在高并发场景下使用反射有哪些注意事项?

📖 点击查看答案

反射原理:

  • Class 对象是反射的入口,每个被加载的类在 JVM 中有唯一一个 Class 对象
  • 通过 Class 对象可获取字段(Field)、方法(Method)、构造器(Constructor)
  • JVM 在方法区存储类的元数据,反射 API 通过这些元数据动态操作类
java
Class<?> clazz = Class.forName("com.example.User");
Method m = clazz.getDeclaredMethod("setName", String.class);
Object obj = clazz.getDeclaredConstructor().newInstance();
m.setAccessible(true);
m.invoke(obj, "Tom");

反射影响性能的原因:

  1. 方法查找开销:需要遍历方法表,按名字+参数匹配
  2. 参数装箱与类型检查:基本类型参数需要装箱
  3. 安全检查:每次调用都做 AccessController 检查
  4. JIT 难以优化:方法调用通过 native invoke,无法内联
  5. setAccessible(true) 本身也有开销

高并发场景下的注意事项:

  1. 缓存反射元数据:避免每次调用都重新查找 Method/Field
java
private static final Method METHOD = getMethod(); // 类加载时缓存
  1. 避免反射创建对象:改用工厂模式或预创建实例
  2. 关闭访问检查setAccessible(true) 可跳过安全检查,提升性能
  3. 使用 MethodHandle(JDK 7+):接近原生调用性能
  4. 使用字节码增强:如 ASM、CGLIB、ByteBuddy 生成代理类
  5. 优先用注解处理器(编译期)代替运行时反射:如 Lombok、MapStruct
  6. 缓存 Constructor:避免反复 newInstance
  7. 避免在 hot path 中反射:核心循环不要用反射

性能对比(量级):

  • 直接调用:1x
  • MethodHandle:~1.5x
  • 缓存的 Method + setAccessible:~3x
  • 未缓存的 Method:~30x

1.9 ★★★Java 动态代理和 CGLIB 代理的实现原理分别是什么?各自有什么限制?Spring 中是如何选择代理方式的?

📖 点击查看答案

JDK 动态代理:

  • 基于接口实现,通过 Proxy.newProxyInstance 生成实现指定接口的代理类
  • 代理类继承 java.lang.reflect.Proxy,实现传入的所有接口
  • 所有方法调用转发给 InvocationHandler.invoke
java
Object proxy = Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (p, method, args) -> {
        System.out.println("before");
        Object result = method.invoke(target, args);
        System.out.println("after");
        return result;
    }
);
  • 限制:目标类必须实现至少一个接口;不能代理 final 方法

CGLIB 代理:

  • 基于继承实现,通过 ASM 生成目标类的子类
  • 子类重写非 final 方法,在方法中插入拦截逻辑(MethodInterceptor)
  • FastClass 机制:通过索引直接调用原方法,避免反射
java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Target.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    System.out.println("before");
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("after");
    return result;
});
Target proxy = (Target) enhancer.create();
  • 限制:不能代理 final 类、final 方法、private 方法;不能代理构造方法

Spring 的选择策略:

  • 默认策略:Spring Boot 2.0+ 强制使用 CGLIBspring.aop.proxy-target-class=true
  • Spring 5 之前:如果目标类实现接口,默认用 JDK 动态代理;否则用 CGLIB
  • 可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB
  • @Configuration 类强制用 CGLIB(保证 @Bean 方法返回同一实例,处理 Bean 间依赖)

两者对比:

维度JDK 动态代理CGLIB
原理接口实现继承子类
依赖JDK 内置第三方 ASM
限制必须有接口不能代理 final 类/方法
性能创建快,调用稍慢创建慢,调用快

1.10 ★★Java 注解(Annotation)的原理是什么?如何自定义一个注解?@Inherited@Repeatable 的作用是什么?

📖 点击查看答案

注解原理:

  • 注解本质是继承 java.lang.annotation.Annotation 的接口
  • 编译后生成动态代理类,运行时通过反射可读取
  • 注解需要"元注解"来描述其行为(@Target、@Retention、@Documented 等)

元注解:

  • @Retention:保留策略(SOURCE / CLASS / RUNTIME)
  • @Target:可标注位置(TYPE、FIELD、METHOD、PARAMETER 等)
  • @Documented:是否出现在 Javadoc
  • @Inherited:是否被子类继承
  • @Repeatable:是否可重复标注

自定义注解示例:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
    String value() default "";
    boolean required() default true;
}

@Inherited 的作用:

  • 标注在类上的注解会被子类继承
  • 只对类有效,对接口/方法/字段无效
java
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface InheritedAnno {}

@InheritedAnno
class Parent {}
class Child extends Parent {}
// Child.class.getAnnotation(InheritedAnno.class) != null

@Repeatable 的作用(JDK 8+):

  • 同一位置可多次标注同一注解
  • 需要定义容器注解
java
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Schedule {
    String cron();
}

@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
    Schedule[] value();
}

// 重复使用
@Schedule(cron = "0 0 * * *")
@Schedule(cron = "0 30 * * *")
class Task {}

注解的处理方式:

  1. 编译期:注解处理器(APT)生成代码,如 Lombok、MapStruct
  2. 运行时:反射 getAnnotation(),如 Spring、JUnit
  3. 字节码增强:ASM 修改字节码

1.11 ★★★★ArrayListLinkedList 的底层实现区别是什么?ArrayList 扩容机制是怎样的?在什么场景下 LinkedList 的性能反而不如 ArrayList

📖 点击查看答案

底层实现区别:

维度ArrayListLinkedList
底层结构动态数组双向链表
内存连续性连续不连续(每个节点有前后指针)
随机访问O(1)O(n)
头部插入O(n)O(1)
中间插入O(n)(但要先 O(n) 定位)O(n)
内存占用较小较大(每个节点额外存两个指针)

ArrayList 扩容机制:

  • 初始容量默认 10(首次 add 时才创建数组)
  • 扩容为原来的 1.5 倍:newCapacity = oldCapacity + (oldCapacity >> 1)
  • 通过 Arrays.copyOf 复制到新数组
java
private int newCapacity(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
    if (newCapacity - minCapacity <= 0) newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) return hugeCapacity(minCapacity);
    return newCapacity;
}

LinkedList 反而不如 ArrayList 的场景:

  1. 随机访问get(index) 是 O(n) vs ArrayList 的 O(1)
  2. 按索引删除remove(int) LinkedList 需 O(n) 遍历定位
  3. 中间插入:理论上 LinkedList 快,但实际上 ArrayList 整体复制内存连续,对 CPU 缓存友好;LinkedList 节点分散,访问慢
  4. 内存缓存友好性:ArrayList 连续内存,CPU cache 命中率高;LinkedList 节点分散,cache miss 多
  5. 频繁遍历:LinkedList 遍历时要反复跳转指针,慢于数组连续访问
  6. 大量元素:LinkedList 每个节点额外 ~48 字节开销(对象头+前后指针)

经验法则:

  • 几乎所有业务场景下都用 ArrayList
  • 需要队列/双端队列时用 ArrayDeque(比 LinkedList 更高效)
  • 极少需要 LinkedList

1.12 ★★★★HashMap 的底层实现原理?JDK 1.7 和 1.8 有什么区别?put 操作的完整流程是怎样的?

📖 点击查看答案

底层结构:

  • JDK 1.7:数组 + 链表(头插法)
  • JDK 1.8:数组 + 链表 + 红黑树(尾插法,链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树;树节点 ≤ 6 时退化为链表)

核心字段:

java
static final int DEFAULT_INITIAL_CAPACITY = 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; // 桶数组
int threshold; // 扩容阈值 = capacity * loadFactor

JDK 1.7 vs 1.8 区别:

维度JDK 1.7JDK 1.8
结构数组+链表数组+链表+红黑树
插入头插法尾插法
扩容重新计算 hash原位置或原位置+oldCap
并发问题头插法会导致环形链表,死循环不会死循环但仍线程不安全(数据丢失)
hash 计算9 次扰动(位运算+移位)1 次扰动(高低16位异或)

put 完整流程(JDK 1.8):

java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 1. 计算桶下标,若 table 为空或长度为 0,先 resize 扩容
    // 2. 桶为空:直接 new Node 放入
    // 3. 桶非空:
    //    a) 第一个节点 key 相等:记录 oldValue 准备覆盖
    //    b) 是 TreeNode:走红黑树插入
    //    c) 是链表:尾插法遍历,若长度达到 8 → treeifyBin(数组<64 时先扩容)
    // 4. 覆盖旧值并返回
    // 5. ++size > threshold:resize 扩容
}

要点:

  1. 计算 hash:(h = key.hashCode()) ^ (h >>> 16) 让高位也参与下标运算
  2. 下标计算:(n - 1) & hash(n 是 2 的幂,等价于 hash % n)
  3. 初始化容量 16,扩容翻倍
  4. 链表转树条件:链表长度 ≥ 8 数组容量 ≥ 64;否则只扩容不转树
  5. 退化条件:树节点数 ≤ 6

扩容时的优化:

  • 因为 n 是 2 的幂,扩容后元素要么在原位置,要么在 原位置 + oldCap
  • 只需检查 hash 的新增高位 bit 是否为 1:
java
if ((e.hash & oldCap) == 0) → 原位置
else → 原位置 + oldCap

1.13 ★★★HashMapHashtableConcurrentHashMap 三者的区别是什么?

📖 点击查看答案

核心区别:

维度HashMapHashtableConcurrentHashMap
线程安全
锁机制synchronized 锁整个对象JDK1.7 分段锁;1.8 CAS+synchronized
null key/value允许(1个 null key)不允许不允许
初始容量161116(1.8)
扩容翻倍翻倍+1翻倍
父类AbstractMapDictionaryAbstractMap
性能最快最慢高并发下快
迭代器fail-fastfail-fast弱一致性(安全失败)
引入版本JDK 1.2JDK 1.0JDK 1.5

HashMap vs Hashtable:

  • Hashtable 是线程安全的 HashMap,但所有方法都加了 synchronized,效率极低
  • Hashtable 已被废弃,不应在新代码中使用
  • 需要 synchronized 的 Map 可用 Collections.synchronizedMap(new HashMap<>())ConcurrentHashMap

ConcurrentHashMap 的优势:

  • 锁粒度细:1.7 用 ReentrantLock 分段锁,1.8 用 CAS + synchronized 锁单个桶
  • 读操作完全无锁(基于 volatile)
  • 支持 CFR 并发遍历

HashMap 允许 null:

java
map.put(null, "v"); // OK
map.put("k", null); // OK
  • ConcurrentHashMap 不允许 null 的原因:无法区分"key 不存在"和"value 是 null"(多线程下会有歧义)

fail-fast 机制:

  • HashMap/Hashtable 迭代时若被结构性修改(modCount 变化),抛 ConcurrentModificationException
  • ConcurrentHashMap 迭代器是弱一致性的,不抛异常但可能不反映最新修改

1.14 ★★★★ConcurrentHashMap 在 JDK 1.7 和 1.8 中的实现有何不同?JDK 1.8 为什么放弃分段锁?size() 方法是如何保证线程安全的?

📖 点击查看答案

JDK 1.7 实现:分段锁(Segment)

  • 结构:Segment[] + HashEntry[],每个 Segment 是一个独立的小 HashMap
  • Segment 继承 ReentrantLock,每个 Segment 有一把锁
  • 默认 16 个 Segment,并发度 16
  • 读无锁(volatile),写锁 Segment
java
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table;
}

JDK 1.8 实现:CAS + synchronized

  • 结构:Node[] 数组 + 链表/红黑树(同 HashMap)
  • 锁粒度:从 Segment 细化到每个桶的头节点
  • 写流程:
    1. 桶为空:CAS 插入(无锁)
    2. 桶非空:synchronized 锁住头节点,插入链表/树
    3. 正在扩容:协助迁移(transfer
java
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ...
    for (Node<K,V>[] tab = table;;) {
        if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) break; // CAS 插入
        } else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f); // 协助扩容
        } else {
            synchronized (f) { // 锁头节点
                // 链表/树插入
            }
        }
    }
}

1.8 放弃分段锁的原因:

  1. 并发度更高:1.7 最多 16 个并发,1.8 锁粒度是桶级别,并发度 = 桶数
  2. 减少内存开销:Segment 占用额外内存
  3. 避免两次 hash:1.7 需要先 hash 定位 Segment,再 hash 定位桶
  4. 更好的扩展性:链表转红黑树后查询从 O(n) 到 O(logn)
  5. CAS 优化无竞争场景:空桶用 CAS 无锁插入,性能更高
  6. 支持并发扩容:多线程协助迁移数据

size() 的实现(1.8):

  • 思路:先尝试无锁统计,失败再回退到加锁
  • 用 baseCount + CounterCell[] 累加,借鉴 LongAdder 思想
  • 写操作时通过 addCount 累加:先 CAS baseCount,失败则分散到 CounterCell[]
  • size() = baseCount + sumCount()(遍历 CounterCell 求和)
java
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;
}
  • size() 是弱一致性的,可能不完全准确

1.15 ★Java 集合快速失败(fail-fast)和安全失败(fail-safe)区别是什么?

📖 点击查看答案

fail-fast(快速失败):

  • 在迭代过程中,如果集合被结构性修改(增删),立即抛 ConcurrentModificationException
  • 实现原理:维护 modCount 字段,迭代器在 next() 时检查 expectedModCount == modCount
java
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
for (Integer i : list) {
    if (i == 2) list.remove(i); // 抛 ConcurrentModificationException
}
  • 代表:ArrayList、HashMap、Vector 等 java.util 包下的集合
  • 目的:尽早发现并发修改,避免不可预期的行为

fail-safe(安全失败):

  • 迭代时对集合的副本进行遍历,不会被并发修改影响

  • 不会抛 ConcurrentModificationException

  • 缺点:无法反映迭代期间的修改,且需要额外内存复制

  • 代表:java.util.concurrent 包下的并发集合

    • CopyOnWriteArrayList:遍历的是底层数组的快照
    • ConcurrentHashMap:弱一致性迭代器,遍历期间可看到部分最新修改
    • CopyOnWriteArraySet
java
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3));
for (Integer i : list) {
    if (i == 2) list.remove(Integer.valueOf(2)); // 不抛异常
}

正确删除元素的方式:

java
// 方式 1:迭代器的 remove
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    if (it.next() == 2) it.remove();
}

// 方式 2:JDK 8+ removeIf
list.removeIf(i -> i == 2);

对比总结:

维度fail-fastfail-safe
异常抛 CME不抛
实时性实时检测可能不反映最新修改
内存不复制可能复制副本
性能略低(复制开销)
典型集合ArrayList、HashMapConcurrentHashMap、CopyOnWriteArrayList

1.16 ★★★Java 异常体系是怎样的?ErrorException、受检异常和非受检异常的区别?在实际项目中如何选择和处理?

📖 点击查看答案

异常体系:

Throwable
├── Error(严重错误,程序无法处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── NoClassDefFoundError
└── Exception
    ├── RuntimeException(非受检,编译器不强制处理)
    │   ├── NullPointerException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   └── ArrayIndexOutOfBoundsException
    └── 其他 Exception(受检异常,编译器强制处理)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

Error vs Exception:

  • Error:JVM 系统级错误,程序不应尝试捕获(如 OOM、StackOverflow)
  • Exception:程序可处理的异常

受检 vs 非受检异常:

类型受检异常(Checked)非受检异常(Unchecked)
父类Exception(非 RuntimeException)RuntimeException
处理必须 try-catch 或 throws,否则编译失败不强制处理
本质可恢复的业务异常(如 IO 错误)编程错误(如 NPE)
典型IOException、SQLExceptionNPE、ClassCastException

项目中如何选择:

  1. 业务异常用受检异常或自定义异常:如 UserNotFoundException extends Exception
  2. 编程错误用 RuntimeException:如参数校验失败抛 IllegalArgumentException
  3. 第三方库的受检异常按业务处理:不要简单 throws Exception
  4. 不要捕获 Throwable / Error:会掩盖 JVM 严重问题
  5. 避免忽略异常:禁止空 catch 块
java
// 反例
try { ... } catch (Exception e) { /* 空 */ }

// 正例:记录日志 + 包装为业务异常
try {
    Files.readAllBytes(path);
} catch (IOException e) {
    log.error("读取文件失败: {}", path, e);
    throw new BusinessException("文件读取失败", e);
}

处理原则:

  • 能处理就处理,不能处理就向上抛
  • 业务层包装为统一业务异常,由全局异常处理器返回标准响应
  • 资源释放用 try-with-resources
  • 不要用异常控制流程(性能差)

1.17 ★★★BIO、NIO、AIO 三种 I/O 模型的区别是什么?NIO 的三大核心组件有哪些?

📖 点击查看答案

三种 I/O 模型对比:

维度BIONIOAIO
通信方式面向流面向缓冲面向缓冲
阻塞阻塞非阻塞(可配置)异步非阻塞
同步性同步阻塞同步非阻塞(多路复用)异步
模型一个连接一个线程多路复用,一个线程处理多个连接回调通知
适用连接数少、业务重连接数多、轻业务连接数极多
JDK 版本1.01.41.7+

BIO 示例:

java
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept(); // 阻塞
    new Thread(() -> handle(client)).start(); // 一个连接一个线程
}

NIO 三大核心组件:

  1. Buffer(缓冲区):数据容器,本质是一块可读可写的内存

    • 核心属性:capacity、position、limit、mark
    • 关键方法:flip()(写转读)、clear()compact()
  2. Channel(通道):双向数据传输通道,可读可写

    • 主要实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel
    • 与 Buffer 配合:channel.read(buffer) / channel.write(buffer)
  3. Selector(选择器):多路复用器,一个线程监控多个 Channel 的事件

    • 事件类型:OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT
java
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select(); // 阻塞到有事件
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) handleAccept(key);
        else if (key.isReadable()) handleRead(key);
    }
    keys.clear();
}

AIO(JDK 7+):

  • 真正的异步 I/O:操作系统完成 I/O 后回调 CompletionHandler
  • AsynchronousSocketChannelAsynchronousServerSocketChannel
java
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    public void completed(AsynchronousSocketChannel ch, Void att) {
        server.accept(null, this); // 接受下一个
        handle(ch);
    }
    public void failed(Throwable e, Void att) { ... }
});

Netty 的选择:

  • Linux 下 AIO 实现不成熟,Netty 主流使用 NIO(基于 epoll)
  • Windows 下 AIO 性能优秀(IOCP),但服务器多用 Linux

1.18 ★★★Java 序列化机制中 serialVersionUID 的作用是什么?transient 关键字的作用是什么?SerializableExternalizable 的区别?

📖 点击查看答案

serialVersionUID 的作用:

  • 序列化时将类的 serialVersionUID 写入字节流
  • 反序列化时比对字节流中的 UID 与当前类的 UID
  • 不一致则抛 InvalidClassException,防止版本不匹配导致反序列化错误
java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // 若不显式声明,JVM 会根据类结构自动生成,类改动后变化 → 反序列化失败
}

不显式声明的风险:

  • 编译器自动生成基于类结构的 UID,类一改(加字段/改方法)UID 就变
  • 不同 JVM 实现生成的 UID 算法可能不同
  • 建议始终显式声明

transient 关键字:

  • 标记的字段不参与序列化
  • 反序列化后值为默认值(0、null、false)
  • 用于敏感信息(密码)或可计算/临时字段
java
public class User implements Serializable {
    private String name;
    private transient String password; // 不序列化
}

Serializable vs Externalizable:

维度SerializableExternalizable
接口标记接口,无需实现方法需实现 readExternal / writeExternal
序列化自动序列化所有非 transient 字段手动指定序列化哪些字段
性能较慢(反射 + 写类信息)较快(直接调用方法)
安全自动,但可能序列化敏感数据精确控制,更安全
灵活性
java
public class User implements Externalizable {
    private String name;
    private transient int age; // Externalizable 下需要手动处理

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        // age 不写
    }
    public void readExternal(ObjectInput in) throws IOException {
        this.name = in.readUTF();
    }
}
  • Externalizable 必须有无参构造器(反序列化时先 new 再 readExternal)

序列化的替代方案:

  • Java 原生序列化:性能差、不跨语言、有安全漏洞(反序列化 RCE)
  • 推荐:JSON(Jackson、Gson)、Protobuf、Hessian、Kryo、Avro

1.19 ★★Lambda 表达式的实现原理是什么?什么是函数式接口?Stream 的核心操作有哪些?parallelStream 有什么风险?

📖 点击查看答案

Lambda 表达式原理:

  • 编译期:Lambda 不会生成匿名内部类,而是在类中生成一个私有静态方法 lambda$方法名$序号
  • 运行期:通过 invokedynamic 指令 + LambdaMetafactory 在首次调用时动态生成实现类
  • 生成的类实现了目标函数式接口,方法体调用前面生成的私有静态方法
java
Runnable r = () -> System.out.println("hello");
// 编译后:生成 lambda$main$0() 方法,运行时通过 invokedynamic 绑定
  • 优点:避免匿名内部类的字节码膨胀,多次复用同一实现

函数式接口:

  • 有且只有一个抽象方法的接口(default、static 方法不算)
  • @FunctionalInterface 标注(非强制,但推荐)
  • Lambda 表达式的类型必须是函数式接口
java
@FunctionalInterface
interface MyFunction<T, R> {
    R apply(T t);
}
  • JDK 内置函数式接口:
    • Function<T,R>:T → R
    • Predicate<T>:T → boolean
    • Consumer<T>:T → void
    • Supplier<T>:无参 → T
    • BiFunction<T,U,R>UnaryOperator<T>

Stream 核心操作:

  1. 中间操作(惰性)

    • filtermapflatMapdistinctsortedlimitskippeek
  2. 终端操作(触发计算)

    • forEachcollectreducecountminmaxanyMatchfindFirst
java
List<String> result = list.stream()
    .filter(s -> s.length() > 3)        // 中间
    .map(String::toUpperCase)           // 中间
    .distinct()                         // 中间
    .sorted()                           // 中间
    .collect(Collectors.toList());      // 终端
  1. 短路操作findFirstfindAnyanyMatchlimit 找到结果立即结束

parallelStream 的风险:

  1. 线程安全:共享可变状态必须加锁或用并发集合
  2. 公共 ForkJoinPool:默认共享 ForkJoinPool.commonPool(),会阻塞其他并行流
  3. 顺序保证forEach 不保证顺序,需要 forEachOrdered
  4. 拆分成本:ArrayList 易拆分,LinkedList 拆分效率低
  5. 装箱开销:基本类型应用 IntStream 等,避免 Stream<Integer>
  6. 小数据量反优化:元素少时拆分+线程切换开销大于收益
java
// 反例
List<Integer> shared = new ArrayList<>();
list.parallelStream().forEach(shared::add); // 线程不安全!可能丢数据

// 正例:用 collect
List<Integer> result = list.parallelStream()
    .filter(...)
    .collect(Collectors.toList());
  • 适用场景:数据量大(万级以上)、CPU 密集型、操作无副作用

1.20 ★★JDK 8 到 JDK 21 之间有哪些重要新特性?Records、Sealed Classes、Pattern Matching、Virtual Thread 分别解决了什么问题?

📖 点击查看答案

JDK 各版本重要特性:

版本重要特性
8Lambda、Stream、Optional、新日期 API、default 方法
9模块化系统(Jigsaw)、JShell、集合工厂方法 List.of()
10局部变量类型推断 var
11HTTP Client API(标准)、var 用于 Lambda
14Switch 表达式(正式)
16Records(正式)、Sealed Classes(预览)
17LTS、Sealed Classes(正式)、Pattern Matching for instanceof
19Virtual Thread(预览)
21LTS、Virtual Thread(正式)、Pattern Matching for switch、Sequenced Collections

Records(JDK 16):解决样板代码

  • 一行代码定义不可变数据载体,自动生成构造器、getter、equals、hashCode、toString
java
public record Point(int x, int y) {}

// 等价于手写:final 字段 + 全参构造 + getter + equals + hashCode + toString
  • 适合 DTO、值对象;可加紧凑构造器做参数校验

Sealed Classes(JDK 17):限制继承层级

  • 显式声明哪些类可以继承/实现
java
public sealed interface Shape permits Circle, Square, Triangle {}
final class Circle implements Shape {}
final class Square implements Shape {}
non-sealed class Triangle implements Shape {}
  • 配合 Pattern Matching 实现穷尽性检查

Pattern Matching:简化类型判断和转换

  • instanceof 模式匹配(JDK 16):免去强制转换
java
if (obj instanceof String s) {
    System.out.println(s.length()); // s 已是 String
}
  • Switch 模式匹配(JDK 21):
java
String desc = switch (shape) {
    case Circle c -> "圆 r=" + c.radius();
    case Square s -> "方 w=" + s.width();
    case Triangle t -> "三角形";
};

Virtual Thread(JDK 21):解决高并发线程成本

  • 轻量级线程,由 JVM 调度,挂载在平台线程(载体线程)上
  • 编写同步阻塞代码风格,获得异步非阻塞性能
  • 数百万虚拟线程也无压力(每个约几 KB,平台线程约 1 MB)
java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}
  • 解决问题:传统 Thread per request 模型在百万连接下线程成本太高;异步编程(CompletableFuture)代码复杂难维护
  • 注意:synchronized 会"pin"虚拟线程,建议用 ReentrantLock;不适合 CPU 密集型任务

基于 4 份主流面试题库整合精选,共 200 题