Skip to content

Java 基础面试题 ​

本文整理 Java 后端开发高频面试题的 Java 基础部分。每道题统一格式:题目 → 答案 → 对比表 / 代码示例 / 一句话记忆。


1. Java 的三大特性 ​

Java 面向对象的三大特性:封装、继承、多态。

(1)封装

把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口(getter / setter)。封装隐藏了对象内部的实现细节,降低了耦合,提升了安全性和可维护性。

(2)继承

从已有类派生出新的类,子类可以获得父类的属性和方法,并可以扩展或重写。继承提高了代码的复用性,Java 只支持单继承(通过接口实现多继承的效果)。

(3)多态

同一种类型的对象执行同一个方法时可以表现出不同的行为特征。不用修改源程序代码,就可以让引用变量绑定到不同的类实现上,从而让程序在运行时选择多个运行状态,提高了灵活性和扩展性。

多态分为:

  • 编译时多态:方法重载(Overload),在编译期确定调用哪个方法。
  • 运行时多态:方法重写(Override),在运行期根据实际对象类型确定调用哪个方法。

实现运行时多态需要两个条件:

  1. 子类继承父类并重写父类中的方法。
  2. 用父类型引用子类型对象(向上转型),调用同样的方法会根据子类对象的不同而表现出不同的行为。
java
// 多态示例
Animal a = new Dog();
a.speak();  // 输出: 汪汪汪(运行时绑定到 Dog 的 speak)

💡 一句话记忆:封装藏细节、继承复代码、多态动绑定。


2. HashMap 底层数据结构,1.7 和 1.8 有什么不同? ​

JDK 1.7:数组 + 链表(拉链法)。插入链表采用头插法,并发扩容时可能形成环形链表导致死循环。

JDK 1.8:数组 + 链表 + 红黑树。插入链表采用尾插法,当链表长度超过 8 且数组容量达到 64 时,链表转为红黑树;当红黑树节点数退化为 6 时,恢复为链表。

JDK 1.7:  table[] -> [Entry] -> [Entry] -> [Entry]  (头插法链表)
JDK 1.8:  table[] -> [Node] -> [Node] -> ... -> 长度>=8 转红黑树(TreeNode)
对比项JDK 1.7JDK 1.8
数据结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
扩容并发问题可能形成环形链表死循环不会死循环(但仍非线程安全)
节点类型EntryNode / TreeNode
哈希算法9 次扰动(4 位 + 5 次)2 次扰动(1 位 + 1 次)
查找复杂度O(n)O(logn)(树化后)

💡 一句话记忆:1.7 数组链表头插,1.8 加红黑树尾插。


3. 为什么要用红黑树 ​

JDK 1.8 引入红黑树是为了解决哈希冲突严重时链表过长导致查询效率下降的问题。

核心原因:

  • 链表查找时间复杂度为 O(n),当冲突节点很多时,最坏情况下退化成 O(n) 的遍历。
  • 红黑树是一种自平衡二叉查找树,查找时间复杂度为 O(logn),在节点较多时性能显著优于链表。

树化与退化的阈值:

阈值数值说明
链表转红黑树链表长度达到 8 且数组容量 ≥ 64否则只触发扩容
红黑树退化链表节点数退化到 6避免链表和树频繁转换

为什么阈值是 8?(泊松分布)

HashMap 源码注释中说明,在理想哈希函数和负载因子 0.75 下,链表节点数遵循泊松分布,单个桶中节点数达到 8 的概率约为 0.00000006,几乎不会发生。因此 8 是一个兼顾性能与空间的折中值:

  • 阈值太小(如 4):树化频繁,红黑树节点比链表节点占用更多内存,得不偿失。
  • 阈值太大(如 16):链表过长,查询性能无法保证。

退化阈值设为 6 而非 8,是为了避免在 8 附近频繁树化与退化(设置 6、8 两个不同阈值形成一个缓冲区间)。

💡 一句话记忆:链表 O(n) 太慢,红黑树 O(logn) 提速;阈值 8 树化、6 退化,泊松分布保驾护航。


4. 什么是反射 ​

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

核心能力:

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。

常用 API:

java
// 获取 Class 对象
Class<?> clazz = Class.forName("com.example.User");

// 创建实例
Object obj = clazz.getDeclaredConstructor().newInstance();

// 获取并调用方法
Method method = clazz.getDeclaredMethod("setName", String.class);
method.setAccessible(true);  // 访问私有方法
method.invoke(obj, "Tom");

// 获取字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(obj, "Jerry");

应用场景: Spring 的 IoC(根据配置创建 Bean)、MyBatis 的 Mapper 代理、JDBC 加载驱动、动态代理、注解处理等。

💡 一句话记忆:反射 = 运行时"看透"类,动态创建对象、调用方法。代价是性能略低、破坏封装。


5. 什么是动态代理 ​

代理模式有静态代理和动态代理两种。

  • 静态代理:需要手动编写代理类,实现与目标对象相同的接口,每个接口都要写一个代理类,维护成本高。
  • 动态代理:在运行时动态生成代理类,无需为每个接口单独编写代理类。

动态代理有两种主流实现方式:

对比项JDK 动态代理CGLIB 代理
实现方式基于接口(Proxy.newProxyInstance)基于继承(生成子类)
依赖条件目标类必须实现接口无需接口,但不能代理 final 类/方法
性能创建快、调用稍慢创建慢、调用快
底层反射字节码增强(ASM)
Spring 默认有接口时使用 JDK无接口时使用 CGLIB

JDK 动态代理示例:

java
// 实现 InvocationHandler
public class MyHandler implements InvocationHandler {
    private final Object target;
    public MyHandler(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强");
        Object result = method.invoke(target, args);
        System.out.println("后置增强");
        return result;
    }
}

// 生成代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new MyHandler(target)
);

💡 一句话记忆:JDK 代理靠接口、CGLIB 代理靠继承;Spring AOP 默认有接口走 JDK、无接口走 CGLIB。


6. 进程和线程的区别 ​

进程是资源分配的基本单位,线程是 CPU 调度的基本单位。

对比项进程线程
资源拥有独立的地址空间、堆栈共享进程的地址空间和资源,有独立栈和寄存器
调度进程切换成本高(需切换地址空间)线程切换成本低(同进程内切换)
通信进程间通信(管道、消息队列、共享内存、Socket)较复杂线程间通信简单(共享内存,需注意同步)
切换成本高(涉及虚拟内存、TLB 刷新)低(仅切换寄存器、栈指针)
健壮性一个进程崩溃不影响其他进程一个线程崩溃可能导致整个进程崩溃
并发粒度粗细

💡 一句话记忆:进程是资源单位、线程是调度单位;进程隔离稳定但切换贵,线程轻量但共享需同步。


7. Overload 和 Override 的区别 ​

重载(Overload):同一个类中,方法名相同但参数列表不同(参数类型、个数、顺序不同),与返回值类型、访问修饰符无关。属于编译时多态。

重写(Override):发生在父类与子类之间,子类对父类的允许访问的方法进行重新编写。属于运行时多态。

对比项Overload(重载)Override(重写)
发生位置同一个类中父子类之间
方法名相同相同
参数列表必须不同必须相同
返回值类型无关必须相同(或为其子类型/协变返回)
访问修饰符无关不能比父类更严格
异常无关不能抛出更宽泛的检查异常
绑定时机编译时(静态分派)运行时(动态分派)
不可重写的方法-final、static、private、构造方法不能被重写

💡 一句话记忆:Overload 同类同名不同参(编译期),Override 子类同方法签名(运行期)。


8. sleep() 和 wait() 的区别 ​

相同点:两者都可以暂停线程的执行。

对比项sleep()wait()
所属类Thread 类(静态方法)Object 类
是否释放锁不释放锁释放锁
使用位置任意位置必须在 synchronized 块中调用
唤醒方式时间到自动苏醒notify() / notifyAll() 唤醒(也可设超时)
用途暂停当前线程一段时间线程间通信/协作
对象作用于当前线程作用于特定对象(监视器)
java
synchronized (lock) {
    lock.wait();    // 释放 lock 的锁,进入等待队列
    Thread.sleep(1000);  // 不释放锁,休眠 1 秒
}

💡 一句话记忆:sleep 占着锁睡(Thread 类),wait 放锁等通知(Object 类)。


9. throw 和 throws 的区别 ​

对比项throwsthrow
位置方法声明后面方法体内
后跟内容异常类名(可多个,逗号隔开)异常对象(只能一个)
处理者由该方法的调用者处理由方法体内的语句处理
含义声明可能抛出的异常,不一定发生抛出异常,执行到这里一定抛出
java
// throws 声明可能抛出
public void read(String path) throws IOException, ClassNotFoundException {
    if (path == null) {
        // throw 实际抛出
        throw new IllegalArgumentException("path cannot be null");
    }
}

💡 一句话记忆:throws 声明可能性(方法签名后),throw 真实抛出(方法体内)。


10. == 和 equals 的区别 ​

对比项==equals
比较类型基本类型 + 引用类型仅引用类型(Object 方法)
比较内容基本类型比较值,引用类型比较内存地址默认比较地址,可被子类重写
String 行为比较地址String 重写后比较内容

详细说明:

  • ==:对于基本数据类型,比较的是值;对于引用数据类型,比较的是对象的内存地址。
  • equals:是 Object 类的方法,默认实现等同于 ==(比较地址)。很多类(如 String、Integer 等)重写了 equals 方法,改为比较内容。
java
String a = new String("hello");
String b = new String("hello");

a == b;        // false(不同对象,地址不同)
a.equals(b);   // true(String 重写了 equals,比较内容)

// Integer 缓存陷阱
Integer i1 = 127;
Integer i2 = 127;
i1 == i2;      // true(IntegerCache 缓存 -128~127)

Integer i3 = 128;
Integer i4 = 128;
i3 == i4;      // false(超出缓存范围,返回新对象)

💡 一句话记忆:== 比地址(基本类型除外),equals 默认比地址、String 等重写后比内容。


11. String、StringBuffer、StringBuilder 的区别 ​

对比项StringStringBufferStringBuilder
可变性不可变可变可变
线程安全安全(不可变)安全(synchronized)不安全
性能低(每次修改创建新对象)中高
底层结构final char[](JDK 9+ 为 byte[])char[],可扩容char[],可扩容
引入版本JDK 1.0JDK 1.0JDK 1.5
适用场景字符串不常修改多线程下频繁拼接单线程下频繁拼接

为什么 String 不可变?

String 类被 final 修饰,内部字符数组也被 final 修饰,赋值后不可修改。不可变性带来三个好处:

  1. 线程安全:多线程共享无需同步。
  2. 安全性:作为 HashMap 的 key 不会变化,类加载器、数据库连接等场景安全。
  3. 字符串常量池:不可变才能安全共享常量池中的对象。

💡 一句话记忆:String 不可变、StringBuffer 线程安全、StringBuilder 单线程最快。


12. Collection 和 Collections 的区别 ​

对比项CollectionCollections
类型接口工具类
作用集合体系的顶层接口提供集合操作的静态方法
包java.utiljava.util
关系List、Set 的父接口操作 Collection 的工具类

Collection 是集合的根接口,继承体系:Collection ← List(ArrayList、LinkedList)/ Set(HashSet、TreeSet)。

Collections 是工具类,常见方法:

java
Collections.sort(list);              // 排序
Collections.reverse(list);           // 反转
Collections.shuffle(list);           // 随机打乱
Collections.binarySearch(list, key); // 二分查找
Collections.synchronizedList(list);  // 转为线程安全集合
Collections.emptyList();             // 返回不可变空集合

💡 一句话记忆:Collection 是接口(集合根),Collections 是工具类(操作集合)。


13. 声明一个不能被继承的类 ​

用 final 修饰类,该类不能有子类。例如 String、Integer 等都是 final 类。

java
public final class MyUtils {
    // 该类不能被继承
}

final 关键字的三个用法:

修饰对象作用示例
修饰类该类不能被继承final class String
修饰方法该方法不能被子类重写public final void doSomething()
修饰变量引用不可变(基本类型值不变,引用类型地址不变)final int MAX = 100;

💡 一句话记忆:final 修饰类不可继承、修饰方法不可重写、修饰变量不可重新赋值。


14. 单例模式的所有写法 ​

单例模式保证一个类只有一个实例,并提供全局访问点。共有 5 种主流写法:

(1)饿汉式 ​

类加载时即创建实例,线程安全,但可能浪费资源。

java
public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();
    private Singleton1() {}
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

(2)懒汉式(线程不安全 / 同步方法) ​

第一次调用时创建,但同步方法性能差。

java
public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2() {}
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

(3)双重检查锁(DCL,推荐) ​

两次判空 + volatile 防止指令重排,性能好且线程安全。

java
public class Singleton3 {
    // volatile 防止 new 对象时的指令重排
    private static volatile Singleton3 instance;
    private Singleton3() {}
    public static Singleton3 getInstance() {
        if (instance == null) {                  // 第一次检查,避免不必要的同步
            synchronized (Singleton3.class) {
                if (instance == null) {           // 第二次检查,防止重复创建
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

new Singleton3() 实际分三步:分配内存 → 初始化对象 → 引用指向内存。若不加 volatile,可能重排为 1→3→2,导致其他线程拿到未初始化的对象。

(4)静态内部类(推荐) ​

利用类加载机制保证线程安全,且实现懒加载。

java
public class Singleton4 {
    private Singleton4() {}
    // 静态内部类在 getInstance 时才加载
    private static class Holder {
        private static final Singleton4 INSTANCE = new Singleton4();
    }
    public static Singleton4 getInstance() {
        return Holder.INSTANCE;
    }
}

(5)枚举(最佳) ​

天然线程安全,且防反射、防反序列化破坏单例。《Effective Java》推荐写法。

java
public enum Singleton5 {
    INSTANCE;
    public void doSomething() {}
}
// 使用:Singleton5.INSTANCE.doSomething();

五种写法对比:

写法线程安全懒加载性能防反射/反序列化
饿汉式是否高否
懒汉式(同步)是是低否
双重检查锁是是高否
静态内部类是是高否
枚举是否高是

💡 一句话记忆:懒加载选静态内部类,防破坏选枚举,DCL 记得加 volatile。


15. 数据表的设计思想 ​

数据库表设计遵循三大范式,并在性能要求下适当反范式。

三大范式 ​

范式要求示例
第一范式(1NF)字段不可再分,每列原子性「联系方式」拆为「手机」「邮箱」
第二范式(2NF)非主键字段完全依赖主键(不能部分依赖)订单表中商品名称应放商品表,避免依赖联合主键的一部分
第三范式(3NF)非主键字段直接依赖主键(不能传递依赖)「部门名称」不应放员工表,应放部门表

反范式 ​

为了查询性能,适当增加冗余字段,用空间换时间。例如订单表中冗余存商品名称,避免每次关联查询商品表。原则:冗余字段应是很少修改的、高频读取的。

字段设计原则 ​

  • 选择合适的数据类型(能选 int 别选 varchar,能选 tinyint 别选 int)。
  • NOT NULL + 默认值,避免 NULL 带来的索引和计算问题。
  • 金额用 decimal,不用 float/double(精度问题)。
  • 时间根据需求选 datetime/timestamp。
  • 每张表加 id、create_time、update_time、逻辑删除字段 is_deleted。

索引设计原则 ​

  • 在 WHERE、JOIN、ORDER BY 的字段上建索引。
  • 区分度低的字段(如性别)不单独建索引。
  • 单表索引数建议不超过 5 个,组合索引遵循最左前缀原则。
  • 避免索引失效:不在索引列上做运算、函数、类型转换,避免 LIKE '%xxx'。

💡 一句话记忆:三大范式去冗余,反范式换性能;字段选小不选大,索引建在高区分度。


16. union 和 union all 的区别 ​

UNION 和 UNION ALL 都用于合并两个或多个 SELECT 语句的结果集。

对比项UNIONUNION ALL
去重去除重复记录保留所有记录(不去重)
排序内部进行去重排序不排序
性能较慢(需去重 + 排序)较快
结果集唯一可能有重复
执行计划通常含 SORT/UNIQUE 操作直接合并

执行原理:

  • UNION 在合并结果后会执行去重操作,底层需要对结果集排序后再去重,CPU 和内存开销较大。
  • UNION ALL 直接将多个结果集合并返回,无去重和排序,性能优于 UNION。

使用建议:

  • 确定无重复数据时优先使用 UNION ALL,性能更优。
  • 需要去重时使用 UNION,但要注意排序成本,可在外层加 ORDER BY 控制。

💡 一句话记忆:UNION 去重排序慢、UNION ALL 全保留快;确定无重复用 ALL。