北京微景数科 Java开发工程师 一面

1. Java中重载与重写的核心判定条件是什么?

思路

从“定义位置、方法签名、返回值/异常、访问修饰符”四个核心维度区分重载(Overload)与重写(Override),突出判定的关键依据。

回答示例

重载(Overload)核心判定条件

重载是同一类中的方法同名不同参,核心判定:

  1. 方法名必须相同;
  2. 方法参数列表(个数、类型、顺序)必须不同;
  3. 与返回值类型、访问修饰符、抛出异常无关(仅改返回值不算重载)。

重写(Override)核心判定条件

重写是子类对父类的非私有方法同名同参,核心判定:

  1. 方法名、参数列表(个数、类型、顺序)必须完全相同;
  2. 返回值:
  3. 访问修饰符:子类不能缩小父类方法的访问范围(如父类public,子类不能private);
  4. 异常:子类不能抛出父类方法未声明的检查型异常,可减少/替换异常;
  5. 父类方法不能是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核心特性的安全性和性能:

  1. 安全性
  2. 性能优化
  3. 类库设计适配

Java核心类库(如ClassLoader、System)大量依赖String的不可变性,若String可变,会导致类加载、系统配置等核心功能出现不可预期的问题。

核心总结:不可变性是String实现常量池、线程安全、哈希缓存的基础,也是Java核心类库安全运行的保障。

3. ArrayList扩容时新容量的计算公式?

思路

明确核心公式,区分JDK1.7/1.8的一致性,补充扩容的触发条件和边界处理。

回答示例

ArrayList基于数组实现,当添加元素时数组满(size == elementData.length),触发扩容,核心计算公式

新容量 = 旧容量 + (旧容量 >> 1) (即旧容量 * 1.5)

完整扩容逻辑:

  1. 触发条件:调用add()时,检查size + 1 > elementData.length;
  2. 计算新容量
  3. 数组拷贝:通过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值

利用高位哈希值,直接判断位置(无需重新计算)

核心优化点:

  1. JDK1.7头插法在多线程扩容时,可能导致链表成环,引发死循环;JDK1.8尾插法解决此问题;
  2. JDK1.8红黑树将链表查询的时间复杂度从O(n)降为O(logn),提升大链表的查询性能;
  3. JDK1.8扩容时,通过(e.hash & oldCap) == 0判断元素是留在原位置还是移到原位置+旧容量,无需重新计算hash,提升扩容效率。

5. 线程start()和run()方法调用的本质区别?

思路

从“线程创建、执行方式、多线程特性”三个核心角度解释,突出start()触发线程调度,run()仅普通方法调用。

回答示例

start()和run()是Thread类的核心方法,调用的本质区别在于是否创建新线程:

  1. start()方法
  2. 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和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏

全部评论

相关推荐

985柜员:开发还敢还叫,全部让自测就老实了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务