这里有🔥Java基础面试八股文精讲(2026最新版)| 大厂高频必考,含详细答案

作者前言:我是一名软件工程大三学生,备战暑期实习期间整理了这份Java基础面试题合集。这些题目都是我在牛客、各大面经社区反复出现的高频题,每道题我都尽量用最通俗的语言讲清楚,避免"背了不懂、懂了记不住"的问题。建议收藏,面试前反复过一遍。


1. Java 中 == 和 equals() 的区别

这是面试最高频的送分题,一定要答得清楚。

== 运算符

  • 对于基本数据类型(int、char、double 等):比较的是是否相等
  • 对于引用数据类型(对象):比较的是两个引用是否指向同一块内存地址

equals() 方法

  • 是 Object 类的方法,默认实现和 == 一样(比较地址)
  • 但 String、Integer 等类重写了 equals(),改为比较内容是否相等

代码演示

public class EqualsDemo {
    public static void main(String[] args) {
        // 基本类型:== 比较值
        int a = 10;
        int b = 10;
        System.out.println(a == b); // true

        // 引用类型:== 比较地址
        String s1 = new String("hello");
        String s2 = new String("hello");
        System.out.println(s1 == s2);      // false(两个不同对象)
        System.out.println(s1.equals(s2)); // true(String重写了equals,比较内容)

        // 字符串常量池的特殊情况
        String s3 = "hello";
        String s4 = "hello";
        System.out.println(s3 == s4); // true(同一个常量池对象)
    }
}

总结一句话

== 比地址,equals() 比内容(前提是类重写了 equals 方法)。


2. hashCode() 和 equals() 的关系

这道题经常跟上面连着问,要配套回答。

两大约定(必须记住)

  1. equals() 返回 true,则 hashCode() 必须相同
  2. hashCode() 相同,equals() 不一定返回 true(哈希碰撞)

为什么要同时重写?

以 HashMap 为例,put 一个 key 时:

  1. 先计算 key 的 hashCode(),定位到桶的位置
  2. 再用 equals() 判断桶中是否已存在相同的 key

如果只重写 equals() 不重写 hashCode()

  • 两个"内容相等"的对象,hashCode 可能不同
  • 导致它们被放到不同的桶里,HashMap 认为是两个不同的 key
  • 数据错乱!

代码示例

public class Person {
    private String name;
    private int age;

    // 只重写equals,不重写hashCode —— 错误示范!
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return age == other.age && Objects.equals(name, other.name);
    }

    // 正确做法:equals 和 hashCode 都要重写
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

总结

重写 equals() 必须同时重写 hashCode(),否则在使用 HashMap、HashSet 等集合时会出现严重 bug。


3. String、StringBuilder、StringBuffer 的区别

字符串三兄弟,面试必考,要说清楚三个维度:可变性、线程安全、性能。

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(不可变) 不安全 安全(synchronized)
性能 慢(每次创建新对象) 最快 比StringBuilder慢
使用场景 少量字符串操作 单线程大量拼接 多线程大量拼接

String 为什么不可变?

public final class String {
    // value 数组是 private final 的,且 String 类本身是 final 的
    private final char value[];
}

两个关键点:

  1. char[] 被声明为 final,引用不可更改
  2. String 类被 final 修饰,不能被继承,从根本上防止了被子类破坏不可变性

性能对比

// 低效写法:每次拼接都会产生新的 String 对象
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i; // 实际上创建了大量临时对象
}

// 高效写法:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String result2 = sb.toString();

总结

单线程拼接用 StringBuilder,多线程用 StringBuffer,字符串内容不变用 String。


4. Java 中的四种引用类型

JVM 调优和内存管理相关题目的前置知识,中级面试必考。

Java 提供四种引用强度,配合 GC(垃圾回收)使用:

1. 强引用(Strong Reference)

最普通的引用,只要强引用存在,对象永远不会被 GC 回收。

Object obj = new Object(); // 强引用

2. 软引用(Soft Reference)

内存充足时不回收,内存不足时才回收。适合做缓存。

SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = softRef.get(); // 可能为 null(已被GC)

3. 弱引用(Weak Reference)

只要发生 GC,无论内存是否充足,一律回收。ThreadLocal 中使用了弱引用。

WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc();
System.out.println(weakRef.get()); // null

4. 虚引用(Phantom Reference)

最弱的引用,完全无法通过它获取对象,仅用于跟踪对象被 GC 的时机,配合引用队列使用。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
System.out.println(phantomRef.get()); // 永远是 null

总结表格

引用类型 回收时机 使用场景
强引用 永不回收 普通对象
软引用 内存不足时 图片缓存、对象池
弱引用 每次GC ThreadLocal、WeakHashMap
虚引用 每次GC 监控对象回收

5. 接口和抽象类的区别

OOP 核心概念,设计模式的基础,必须答得有深度。

语法层面的区别

对比项 抽象类 接口
关键字 abstract class interface
继承/实现 单继承 可实现多个接口
构造方法 可以有 没有
成员变量 任意类型 只能是 public static final
普通方法 可以有方法体 Java8前不能,Java8后可以有default方法
访问修饰符 任意 方法默认 public

设计层面的区别(这才是面试官想听的)

  • 抽象类:表示"is-a"关系。用于有公共代码、公共属性的类族。

    • 例:Animal 抽象类,DogCat 继承它,都是动物。
  • 接口:表示"can-do"关系,是一种能力或协议。

    • 例:Flyable 接口,BirdAirplane 都可以实现,但它们不是同一类东西。
// 抽象类:模板方法模式的核心
public abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    // 抽象方法,子类必须实现
    public abstract void sound();

    // 普通方法,子类直接继承
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// 接口:定义行为能力
public interface Swimmable {
    void swim();
}

public interface Flyable {
    void fly();
}

// 一个类可以实现多个接口
public class Duck extends Animal implements Swimmable, Flyable {
    public Duck() { super("Duck"); }

    @Override public void sound() { System.out.println("Quack"); }
    @Override public void swim() { System.out.println("Duck is swimming"); }
    @Override public void fly() { System.out.println("Duck is flying"); }
}

总结

抽象类用于代码复用和模板设计,接口用于定义规范和实现多态。优先使用接口,因为 Java 是单继承的。


6. final、finally、finalize 的区别

三个长得像的关键字,考察基础细心程度,送分题。

final

修饰符,用于限制"变化":

// 修饰类:不能被继承
public final class String { ... }

// 修饰方法:不能被重写
public final void show() { ... }

// 修饰变量:只能赋值一次(引用不可变,但对象内容可变)
final int MAX = 100;
final List<String> list = new ArrayList<>();
list.add("hello"); // 合法!引用没变,内容可以变
// list = new ArrayList<>(); // 非法!引用不能变

finally

异常处理中的代码块,一定会执行(除非 JVM 崩溃或 System.exit()):

try {
    int result = 10 / 0; // 抛出异常
} catch (ArithmeticException e) {
    System.out.println("捕获异常:" + e.getMessage());
} finally {
    System.out.println("我一定会执行!"); // 无论如何都执行
    // 常用于关闭资源:connection.close(), stream.close()
}

finalize

Object 类的方法,在对象被 GC 回收调用。

@Override
protected void finalize() throws Throwable {
    System.out.println("对象被回收了");
    super.finalize();
}

⚠️ 注意:finalize() 已在 Java 9 中被标记为 @Deprecated,Java 18 中删除。不推荐使用,了解即可。

一句话总结

final 是修饰符(限制变化),finally 是代码块(必执行),finalize 是方法(GC前调用,已废弃)。


7. Java 异常体系详解

项目开发必用,面试必考,要能画出继承关系。

异常体系结构

Throwable
├── Error(严重错误,程序无法处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception(程序可以处理的异常)
    ├── RuntimeException(运行时异常,非受检异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── ArithmeticException
    │   └── ...
    └── 受检异常(编译器强制要求处理)
        ├── IOException
        ├── SQLException
        └── ...

受检异常 vs 非受检异常

类型 示例 是否必须处理
受检异常(Checked) IOException, SQLException 必须 try-catch 或 throws 声明
非受检异常(Unchecked) NullPointerException, ArrayIndexOutOfBoundsException 不强制处理

异常处理最佳实践

// ❌ 错误做法:捕获了异常但什么都不做
try {
    // ...
} catch (Exception e) {
    // 空catch,吃掉了异常,排查问题时会很痛苦
}

// ✅ 正确做法:至少要打印日志
try {
    File file = new File("test.txt");
    FileReader reader = new FileReader(file);
} catch (FileNotFoundException e) {
    log.error("文件未找到:{}", e.getMessage(), e);
    throw new BusinessException("文件处理失败", e); // 包装成业务异常抛出
} finally {
    // 关闭资源
}

// ✅ 更优雅的做法:try-with-resources(Java7+)
try (FileReader reader = new FileReader("test.txt")) {
    // reader 会自动关闭
} catch (IOException e) {
    log.error("IO异常", e);
}

自定义异常

// 自定义业务异常
public class BusinessException extends RuntimeException {
    private Integer code;

    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
        this.code = 500;
    }

    public Integer getCode() { return code; }
}

8. 深拷贝和浅拷贝的区别

对象复制问题,项目中很容易踩坑。

概念

  • 浅拷贝:创建新对象,但对象内部的引用类型属性仍指向原来的对象
  • 深拷贝:创建新对象,内部所有引用类型属性也递归地创建新对象

代码演示

// 浅拷贝示例
public class Address implements Cloneable {
    String city;
    public Address(String city) { this.city = city; }
}

public class Person implements Cloneable {
    String name;
    Address address; // 引用类型

    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // 浅拷贝:只复制了引用
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    // 深拷贝:递归复制引用类型
    public Person deepClone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = new Address(this.address.city); // 创建新的Address对象
        return cloned;
    }
}

// 测试
Person p1 = new Person("张三", new Address("北京"));
Person p2 = (Person) p1.clone();      // 浅拷贝
Person p3 = p1.deepClone();           // 深拷贝

p2.address.city = "上海";
System.out.println(p1.address.city);  // 上海!p1被影响了(浅拷贝问题)

p3.address.city = "广州";
System.out.println(p1.address.city);  // 上海,p1没被影响(深拷贝安全)

实际开发中的深拷贝方式

  1. 序列化/反序列化(最简单)
  2. JSON 转换Object obj2 = JSON.parseObject(JSON.toJSONString(obj1), obj1.getClass())
  3. 手动递归复制(如上面的 deepClone)
  4. Apache Commons LangSerializationUtils.clone(obj)

9. Java 中的自动装箱与拆箱

看起来简单,但有坑!Integer 缓存池是经典考点。

什么是装箱/拆箱

// 装箱:基本类型 → 包装类型(自动调用 Integer.valueOf())
Integer a = 100; // 等价于 Integer a = Integer.valueOf(100);

// 拆箱:包装类型 → 基本类型(自动调用 intValue())
int b = a; // 等价于 int b = a.intValue();

Integer 缓存池陷阱(高频考点!)

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false !!!

原因:Integer 内部有一个缓存池,默认缓存 -128 到 127 之间的值。

  • 127 在范围内,Integer.valueOf(127) 返回同一个缓存对象,所以 == 为 true
  • 128 超出范围,每次都新建对象,所以 == 为 false
// Integer 源码(简化)
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回缓存
    return new Integer(i); // 超出范围,新建
}

自动拆箱的 NullPointerException 坑

Integer count = null;
int result = count + 1; // 拆箱时调用 count.intValue() → NullPointerException!

总结

包装类型比较大小用 equals(),不要用 ==!注意 null 的拆箱会抛 NPE。


10. 为什么 Java 中只有值传递?

这道题很多人答错,需要想清楚。

结论先说

Java 中方法参数传递只有值传递,没有引用传递。

基本类型参数

public static void change(int x) {
    x = 100;
}

int a = 10;
change(a);
System.out.println(a); // 10,没变!传的是 a 的值的副本

引用类型参数(容易误解的地方)

public static void modifyContent(StringBuilder sb) {
    sb.append(" World"); // 修改了对象内容
}

public static void modifyReference(StringBuilder sb) {
    sb = new StringBuilder("New Object"); // 只改了局部变量,对外无效
}

StringBuilder s = new StringBuilder("Hello");
modifyContent(s);
System.out.println(s); // "Hello World"(对象内容被修改了)

modifyReference(s);
System.out.println(s); // "Hello World"(引用没变)

理解关键

传引用类型时,传递的是引用的值(内存地址的副本)

  • 通过这个地址副本,可以修改它指向的对象的内容
  • 但修改这个地址副本本身(让它指向新对象),不影响外部的原引用

所以本质上还是值传递,只是传递的值是一个内存地址。

调用前:  s ──────────────→ [StringBuilder: "Hello"]
调用时:  sb(副本)──────→ [StringBuilder: "Hello"]  ← 两个变量指向同一对象
修改地址:sb = new ... → [新对象]   s 还是指向原来的对象

总结

Java 传引用类型时,传的是"引用的副本",可以通过副本修改对象内容,但不能改变原引用的指向。


🎯 面试答题技巧

  1. 结论先说:先给出一句话答案,再展开解释
  2. 举例说明:抽象概念配代码或场景,显示你真的懂
  3. 说到坑/注意点:比如Integer缓存池、NullPointerException,体现经验
  4. 适当延伸:问完可以主动提"这个知识点在XX场景下还有…",展示深度

📚 推荐学习资源

  • 《Java 核心技术 卷Ⅰ》—— 基础最扎实的教材
  • 《Effective Java》—— 进阶必读
  • JavaGuide —— 国内最全的Java面试指南
  • LeetCode 热题 Hot 100 —— 算法配合练习

💬 如果这篇文章帮到你了,点个赞再走吧!
后续还会更新 Java 集合、JVM、并发编程等专题,欢迎关注。
有疑问或错误欢迎在评论区指出,一起进步!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐