Skip to content

最新Java面试题全集

JDK、JRE、JVM之间的区别

JVM是执行字节码的引擎,是运行程序的基础

JRE = JVM + 核心类库,它是运行 Java 程序的最小环境
单靠 JVM 是运行不了复杂的 Java 字节码程序的,比如 HTTP 请求,数据库连接等等。
java.lang.Object 类也位于核心类库中,所以即使只运行一个执行基本类型的加法操作的 main 方法时,也需要使用 JRE 来运行。
因为你定义的包含 main 方法的类也会隐式的继承 java.lang.Object 类。

JDK = JRE + 开发工具(主要是编译器 javac)。它是开发 Java 程序的完整套件。 换句话说,如果你是开发者,需要编写代码、编译代码,那么你需要安装 JDK。 如果你只是最终用户,需要运行一个 Java 程序,你只需要安装 JRE。

hashCode()与equals()之间的关系

在 Java 中,每个对象都可以调用自己类中的 hashCode() 方法得到自己的哈希值(hashCode),相当于对象的指纹信息。
通常来说世界上没有完全相同的两个指纹,但是在 Java 中做不到这么绝对,但是仍然可以利用 hashCode 来做一些提前的判断,规则如下

  • 如果两个对象的 hashCode 不相同,那么这两个对象肯定不同的两个对象。
  • 如果两个对象的 hashCode 相同,那么这两个对象可能相同的。
  • 如果两个对象相等,那么它们的 hashCode一定相同

在 Java 的一些集合类的实现中,通常会根据上面的原则来进行比较对象是否相同:

  1. 先调用对象的 hashCode() 方法得到 hashCode 进行比较,如果 hashCode 不相同,就可以直接认为这两个对象不相同。
  2. 如果 hashCode 相同,那么就会进一步调用 equals() 方法进行比较,就是用来确定两个对象是不是相等的。

WARNING

如果我们重写了 hashCode()、equals() 方法,那么就要注意,一定要保证能遵循上述规则

为什么判断对象是否相同时,都会先使用 hashCode() 判断?

通常 equals 方法的实现会比较重,逻辑比较多,而 hashCode() 主要就是得到一个哈希值,实际上就一串数字,相比而言较轻,所以在比较两个对象时,通常会先根据 hashCode 比较一下。

String、StringBuffer、StringBuilder的区别

特性StringStringBufferStringBuilder
可变性不可变(Immutable)
创建后内容不能修改
可变(Mutable)
内容可以通过方法修改
可变(Mutable)
内容可以通过方法修改
线程安全本身不可变,天然安全安全(Synchronized)
方法用synchronized修饰
不安全(非同步)
没有锁,性能更高
性能拼接时效率最低
(大量新建对象)
效率中等
(有同步开销)
效率最高
(无锁)
适用场景1. 定义常量字符串
2. 不需要频繁修改
1. 多线程环境下字符串频繁修改
2. 需要线程安全
1. 单线程环境下字符串频繁修改
2. 需要最高性能
继承关系直接继承Object继承AbstractStringBuilder继承AbstractStringBuilder

为什么在编码中,还是用 String 直接拼接的情况更多?

  1. Java 编译器自动优化了静态字符串拼接,编译器能确定内容的拼接,JVM会自动优化,不存在性能问题。

    java
    String s = "a" + "b" + "c";

    会被编译器直接优化为 "abc",不会运行时创建多个对象。

    java
    String s = "a" + someConstant;

    如果 someConstant 是常量(static final),也会被优化。

  2. 线程安全不是刚需,现代开发中,大多数场景都是单线程操作(如 HTTP 请求处理)。

泛型中 extends 和 super 的区别

  1. ? extends T 表示包括 T 在内的任何 T 的子类
  2. ? super T 表示包括 T 在内的任何 T 的父类

== 和 equals() 的区别

  • ==:如果是基本数据类型,比较的是值,如果是引用类型,比较的是引用地址。
  • equals:
    • 重写equals方法:具体看各类重写 equals 方法之后的比较逻辑,比如 String 类,虽然是引用类型,但 String 类中重写了 equals 方法,方法内部比较的是字符串中的各个字符是否全部相等。
    • 未重写equals方法:如果某一个引用类型并没有重写 java.lang.Object 类的 equals 方法,那么对于该引用类型来说, equals 和 == 是相同的,因为 java.lang.Object 中 equals 方法的逻辑内部就是用 == 判断的。

重载和重写的区别

  • 重载(overload):在同一个类中,同名方法如果满足以下任意条件之一
    • 参数类型不同
    • 参数个数不同
  • 重写(override):子类把父类本身有的方法重新写一遍。必须满足以下所有条件
    • 方法名相同
    • 参数列表相同(类型、数量、顺序)
    • 返回类型相同(子类中方法的返回值可以是父类中方法返回值的子类)
    • 子类方法的访问修饰符权限不能小于父类

各个方法修饰符的区别

修饰符访问级别是否可被子类重写是否可被同一包中的其他类访问是否可被不同包中的类(通过继承)访问是否可被不同包中的类(非继承)访问
public最高,无限制
protected次之,包内 + 子类可访问
默认(无修饰符)或 default包级私有,仅限于同一个包内
private最低,仅限于声明它的类内部

List 和 Set 的区别

  • List
    • 有序(按对象插入的顺序保存对象)
    • 可重复
    • 允许多个 Null 元素对象
    • 可以使用 Iterator 取出所有元素,还可以使用 get(int index) 获取指定下表的元素
  • Set
    • 无序
    • 不可重复
    • 最多允许有一个 Null 元素对象
    • 只可以使用 Iterator 取出所有元素,再逐一遍历各个元素

ArrayList 的底层工作原理

  • 添加元素
    • add(E element):在末尾添加元素的时间复杂度为 O(1),但如果需要扩容,则时间复杂度为 O(n)。
    • add(int index, E element):
      1. 先检查是否 0 <= index <= size(),如果不符,则报 IndexOutOfBoundsException。
      2. 再确认数组容量是否足够容纳 size() + 1(当前元素),不够则扩容。
      3. 如果 index = size(),则把新元素添加到指定位置,执行结束。
      4. 如果 0 <= index < size(),则把新元素添加到指定位置,然后后面元素则从 index 位置依次后移。
  • 获取元素 get(int index)
    • 由于 ArrayList 底层是一个数组,所以通过索引直接访问元素的时间复杂度为 O(1)。
  • 删除元素
    • remove(int index):根据索引删除元素时,需要将被删除元素后的所有元素向前移动一位,时间复杂度为 O(n)。
    • remove(Object o):使用对象进行删除时,首先需要遍历列表找到匹配项并删除,然后将被删除元素后的所有元素向前移动一位,时间复杂度同样为 O(n)。
  • 数组扩容
    • ArrayList 的自动扩容,只在添加元素到“末尾”时生效。
    • 数组扩容数组扩容时是按 1.5 倍扩容,向下取整。
    • 虽然默认扩容策略是 1.5 倍,但如果扩容后仍不够用,ArrayList 会强制将容量设为“当前所需最小容量”,因此即使初始容量为 1,也能正常扩容。

JDK8,刚创建 ArrayList 后为什么数组长度是 0,默认长度不是为 10 么?

这是 Java 8 及以后版本的优化行为:懒加载(Lazy Initialization),只有在第一次添加元素时才真正分配空间

ArrayList默认大小
java
public static void main(String[] args) {
    ArrayList<Object> list = new ArrayList<>();

    print(list);

    list.add("a");
    print(list);
}

private static void print(ArrayList<Object> list) {
    System.out.println(list);
    System.out.println("元素个数" + list.size());
    System.out.println("数组长度:" + getArrayListCapacity(list));
}


// 使用反射获取 elementData 的长度
private static int getArrayListCapacity(ArrayList<?> list) {
    try {
        java.lang.reflect.Field field = ArrayList.class.getDeclaredField("elementData");
        field.setAccessible(true);
        return ((Object[]) field.get(list)).length;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

执行以上代码时,如果使用的是 JDK9 或更新版本的 JDK,发生了以下报错:

执行报错
java
java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.lang.Object[] java.util.ArrayList.elementData accessible: module java.base does not "opens java.util" to unnamed module

请添加 VM options

VM options
java
--add-opens java.base/java.util=ALL-UNNAMED

这是因为从 Java 9 开始,Oracle 引入了模块化系统(JPMS),很多 JDK 内部类和字段默认不再对反射开放访问权限
ArrayList 的 elementData 字段属于 java.util 包,而该包在 Java 17 及以后版本中 默认不对外开放反射访问

执行结果
java
[]
元素个数0
数组长度:10
[a]
元素个数1
数组长度:10

ArrayList 的扩容机制

ArrayList 自动扩容报错

既然 ArrayList 能够自动扩容,为什么以下 Java 代码执行会报错?

ArrayList 扩容报错
java
ArrayList<Object> list = new ArrayList<>();
list.add(10, "a");  // 报错:IndexOutOfBoundsException
报错信息
java
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 10, Size: 0

这是因为ArrayList的自动扩容,只在添加元素到“末尾”时生效

  • add(E element):因为每次都是在“末尾”添加元素,所以可以动态扩容,不需关心当前容量,不会发生 IndexOutOfBoundsException。
  • add(int index, E element):如果 index != list.size(),则不会自动帮助你进行数组扩容,且 必须 0 <= index <= size(),如果 index > size(), 则会报 IndexOutOfBoundsException。

ArrayList 扩容时是新建数组还是在原数组上扩容

使用 Java 代码验证,通过反射获取 ArrayList 的底层数组地址(或对象标识),来验证扩容前后是否是同一个数组。

验证 ArrayList 的数组地址
java
import java.util.ArrayList;
import java.lang.reflect.Field;

public class ArrayListExpandTest {

    public static void main(String[] args) throws Exception {
        ArrayList<String> list = new ArrayList<>(3);

        System.out.println("初始容量: " + getCapacity(list));
        printArrayIdentity(list);

        // 添加元素触发扩容
        list.add("A");
        list.add("B");
        list.add("C");
        System.out.println("添加3个元素后,容量: " + getCapacity(list));
        printArrayIdentity(list);

        list.add("D"); // 触发扩容
        System.out.println("再添加1个元素后,容量: " + getCapacity(list));
        printArrayIdentity(list);
    }

    // 使用反射获取底层数组对象
    private static Object getInternalArray(ArrayList<?> list) throws Exception {
        Field field = ArrayList.class.getDeclaredField("elementData");
        field.setAccessible(true);
        return field.get(list);
    }

    // 打印数组对象的唯一标识
    private static void printArrayIdentity(ArrayList<?> list) throws Exception {
        Object array = getInternalArray(list);
        System.out.println("当前底层数组对象 hashcode: " + array.hashCode() + "\n");
    }

    // 获取当前容量
    private static int getCapacity(ArrayList<?> list) throws Exception {
        return ((Object[]) getInternalArray(list)).length;
    }
}
执行结果
java
初始容量: 3
当前底层数组对象 hashcode: 1030870354

添加3个元素后,容量: 3
当前底层数组对象 hashcode: 1030870354

再添加1个元素后,容量: 4
当前底层数组对象 hashcode: 485815673

可以看到,扩容之后 hashcode 改变了 → 说明底层数组已经换了! ArrayList 在扩容时,是新建一个更大的数组,然后将原数组的内容复制到新数组中,并不是在原数组上扩容。 因为 Java 中的数组一旦创建后大小是固定的,不能动态改变长度。所以扩容的本质就是:

  • 创建一个新的、更大的数组
  • 把旧数组内容拷贝到新数组
  • 用新数组替换原来的数组

ArrayList 和 LinkedList 的区别

  • ArrayList
    • 基于动态数组实现。
    • 允许直接访问元素(通过索引),因为它是基于数组的,所以访问速度非常快。
    • 当元素数量超过当前容量时,会自动扩容,这涉及到创建新数组并复制旧数组的内容,这个过程比较耗时。
  • LinkedList
    • 基于双向链表实现。
    • 不支持随机访问,要访问某个元素必须从头或尾开始遍历链表,直到找到目标元素。
    • 插入和删除操作不需要移动其他元素,只需改变相关节点的指针即可,所以在插入和删除操作上效率较高。

ConcurrentHashMap 扩容机制

多线程篇

wait 和 sleep 的区别

  • wait 方法必须在 synchronized 保护的代码块中,而 sleep 方法并没有这个要求
  • wait 方法会主动释放 monitor 锁,在同步代码块中执行 sleep 方法时,并不会释放 monitor 锁
  • wait 方法意味着永久等待,直到被中断或被唤醒才能恢复。sleep 方法中会定义一个时间,时间到期后会主动恢复
  • wait / notify 是 Object 类的方法,而 sleep 是 Thread 类的方法

线程创建方式

  • 实现 Runnable 接口 (优先使用)
  • 实现 Callback 接口 (有返回值可抛出异常)
  • 继承 Thread 类
  • 使用线程池 (底层都是实现 run 方法)

线程池参数

优点:通过复用已创建的线程池,降低资源消耗、线程可以直接处理队列中的任务加快响应速度、同时便于统一监控和管理

参数名描述默认值
corePoolSize线程池中的核心线程数。即使线程处于空闲状态,也会保持在池中,除非设置了 allowCoreThreadTimeOut 为 trueN/A
maximumPoolSize线程池中允许的最大线程数。如果任务数量超过这个数值,则会根据拒绝策略处理这些任务。N/A
keepAliveTime当线程数大于核心线程数时,这是多余的空闲线程等待新任务之前活动的最长时间。60秒
unitkeepAliveTime参数的时间单位,可以是纳秒、微秒、毫秒、秒、分、小时或天等。
workQueue用于保存等待执行的任务的队列,通常是BlockingQueue类型的实例。N/A
threadFactory用于设置创建线程的工厂,可以通过自定义ThreadFactory来设置线程名称、是否守护线程等属性。默认工厂
handler当线程和队列都满时,新的任务将根据此处理器进行处理。通常有四种预设策略:抛出异常、直接由调用线程运行、丢弃一个旧任务或丢弃当前任务。抛出异常

线程池任务分配流程

thread-process

  • 当线程池大小 小于 corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
  • 当线程池大小达到 corePoolSize 时,新提交任务将被放入 workQueue中,等待线程池中任务调度执行
  • 当 workQueue 已满,且当前线程池中线程数 小于 maxnumPoolSize 时,新提交任务会创建新线程执行任务
  • 当已提交但未被处理完成的任务数 大于 maxnumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理
  • 当线程池中线程数超过 corePoolSize 时,并且部分线程的空闲时间到达 keepAliveTime 时,空闲线程将被关闭,直到数量 小于等于 corePoolSize

TIP

在Java的线程池实现中(如ThreadPoolExecutor),并没有严格区分“核心”线程和“非核心”线程的身份标识。所谓的“核心线程数”更多是指线程池应该维持的最小线程数量,即使这些线程是空闲的(除非设置了allowCoreThreadTimeOut(true)允许核心线程超时)。

任何超出corePoolSize并处于空闲状态超过指定时间的线程都可能被终止,以确保线程池内的线程数量符合预期的配置(核心线程数)。这意味着,最终哪些线程保持活跃状态,完全取决于任务调度的情况以及线程池的动态调整机制。

线程拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候就需要拒绝策略来处理这个问题。

  • AbortPolicy(默认策略)
    • 行为
      直接抛出一个 RejectedExecutionException 异常,阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略
    • 适用场景
      当你不希望忽略任何提交的任务,并且想要快速发现并处理这种错误情况时使用。
    java
    ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
  • CallerRunsPolicy
    • 行为
      只要线程池未关闭,由调用线程(通常是主线程)来执行当前在线程池中被丢弃的任务。不会造成任务丢失,同时减缓提交任务的速度,给执行任务缓冲时间。
    • 适用场景
      当你希望避免丢弃任务,并且能够承受调用线程执行额外任务带来的性能影响时。
  • DiscardPolicy
    • 行为
      直接丢弃不能处理的任务,不做任何通知或记录
    • 适用场景
      当你完全不关心被拒绝的任务是否得到处理,并且不想因为任务被拒绝而产生额外的开销时。
  • DiscardOldestPolicy
    • 行为
      丢弃队列中最旧的任务(也就是即将被执行的任务),然后尝试重新提交当前任务

除了上述四种内置的决绝策略外,还可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略,根据自己的需求来决定如何处理被拒绝的任务

Executors类实现线程池

线程池类型描述特点
Single Thread Executor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。所有任务按顺序运行;确保所有任务都在同一个线程中依次执行,线程结束后能重新创建新的线程来替代原来的。
Single Thread Scheduled Executor创建一个单线程执行程序,能够调度命令在指定延迟后运行或者定期执行。类似于ScheduledThreadPoolExecutor但使用单一线程;保证所有任务按序执行,并且是周期性和延迟任务的理想选择。
Scheduled Thread Pool创建一个支持定时及周期性任务执行的线程池。可以安排任务在给定延迟后运行,或定期执行;适用于需要计划执行任务的场景。
Fixed Thread Pool创建一个拥有固定数量线程的线程池。线程数固定,空闲线程会一直存在直到线程池被关闭;适用于负载较稳定的应用场景。
Cached Thread Pool创建一个可根据需要创建新线程的线程池,但在以前构造的线程可用时将重用它们。没有核心线程,最大线程数为Integer.MAX_VALUE;线程空闲60秒后会被回收;适合执行大量短期异步任务。