北京微景数科 Java开发工程师 一面
1. Java中重载与重写的核心判定条件是什么?
思路
从“定义位置、方法签名、返回值/异常、访问修饰符”四个核心维度区分重载(Overload)与重写(Override),突出判定的关键依据。
回答示例
重载(Overload)核心判定条件
重载是同一类中的方法同名不同参,核心判定:
- 方法名必须相同;
- 方法参数列表(个数、类型、顺序)必须不同;
- 与返回值类型、访问修饰符、抛出异常无关(仅改返回值不算重载)。
重写(Override)核心判定条件
重写是子类对父类的非私有方法同名同参,核心判定:
- 方法名、参数列表(个数、类型、顺序)必须完全相同;
- 返回值:
- 访问修饰符:子类不能缩小父类方法的访问范围(如父类public,子类不能private);
- 异常:子类不能抛出父类方法未声明的检查型异常,可减少/替换异常;
- 父类方法不能是final/static(final禁止重写,static是类方法,不存在重写)。
示例对比:
// 重载:同一类,参数不同
class Test {
public void say(int a) {}
public void say(String a) {} // 合法重载
}
// 重写:子类,参数相同,返回值协变
class Parent {
public Object doSomething() { return null; }
}
class Child extends Parent {
@Override
public String doSomething() { return ""; } // 合法重写
}
2. String被设计为final的根本原因有哪些?
思路
从“安全性、性能、常量池、哈希值”四个核心角度解释,突出final对String核心特性的保障。
回答示例
String被设计为final(不可变类),根本原因是保障Java核心特性的安全性和性能:
- 安全性:
- 性能优化:
- 类库设计适配:
Java核心类库(如ClassLoader、System)大量依赖String的不可变性,若String可变,会导致类加载、系统配置等核心功能出现不可预期的问题。
核心总结:不可变性是String实现常量池、线程安全、哈希缓存的基础,也是Java核心类库安全运行的保障。
3. ArrayList扩容时新容量的计算公式?
思路
明确核心公式,区分JDK1.7/1.8的一致性,补充扩容的触发条件和边界处理。
回答示例
ArrayList基于数组实现,当添加元素时数组满(size == elementData.length),触发扩容,核心计算公式:
新容量 = 旧容量 + (旧容量 >> 1) (即旧容量 * 1.5)
完整扩容逻辑:
- 触发条件:调用add()时,检查size + 1 > elementData.length;
- 计算新容量:
- 数组拷贝:通过Arrays.copyOf()将原数组元素拷贝到新数组。
示例:
旧容量10 → 新容量15;旧容量15 → 新容量22(15+7);若最小需要容量是20(如addAll(20个元素)),则新容量直接取20。
补充:JDK1.7和1.8的扩容公式完全一致,仅初始化时的默认容量逻辑略有差异(1.7默认初始容量10,1.8懒加载,首次add时初始化)。
4. HashMap在JDK1.7和JDK1.8中哈希冲突处理方式差异?
思路
从“数据结构、插入方式、扩容机制、性能”四个维度对比,突出1.8引入红黑树的核心优化。
回答示例
HashMap处理哈希冲突(不同key哈希值相同)的方式,JDK1.7和1.8有本质差异:
维度 |
JDK1.7 |
JDK1.8 |
数据结构 |
数组 + 单向链表 |
数组 + 单向链表 + 红黑树 |
链表插入方式 |
头插法(扩容时可能导致链表循环) |
尾插法(避免循环,保证插入顺序) |
树化条件 |
无 |
链表长度≥8且数组容量≥64,转为红黑树 |
退链条件 |
无 |
红黑树节点数≤6,转回链表 |
扩容后哈希计算 |
重新计算hash值 |
利用高位哈希值,直接判断位置(无需重新计算) |
核心优化点:
- JDK1.7头插法在多线程扩容时,可能导致链表成环,引发死循环;JDK1.8尾插法解决此问题;
- JDK1.8红黑树将链表查询的时间复杂度从O(n)降为O(logn),提升大链表的查询性能;
- JDK1.8扩容时,通过
(e.hash & oldCap) == 0判断元素是留在原位置还是移到原位置+旧容量,无需重新计算hash,提升扩容效率。
5. 线程start()和run()方法调用的本质区别?
思路
从“线程创建、执行方式、多线程特性”三个核心角度解释,突出start()触发线程调度,run()仅普通方法调用。
回答示例
start()和run()是Thread类的核心方法,调用的本质区别在于是否创建新线程:
- start()方法:
- run()方法:
示例对比:
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName())); t.start(); // 输出Thread-0(新线程) t.run(); // 输出main(当前线程)
6. 什么场景会触发ConcurrentModificationException?
思路
明确核心触发条件(快速失败机制),区分“单线程”和“多线程”场景,补充避免方式。
回答示例
ConcurrentModificationException(并发修改异常)由集合的快速失败(fail-fast) 机制触发,核心场景:
1. 单线程场景(最常见)
遍历集合(如for-each、Iterator)时,直接修改集合结构(add/remove/clear),触发异常:
List<String> list = new ArrayList<>();
list.add("a");
// for-each遍历,本质是Iterator,修改集合触发异常
for (String s : list) {
list.remove(s); // 抛ConcurrentModificationException
}
2. 多线程场景
一个线程遍历集合,另一个线程修改集合结构,触发异常:
List<String> list = new ArrayList<>(); //
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏在精不在多,内容分为八股文、大厂真实面经,面试通过后将offer和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏