Java 基础面试题
本文整理 Java 后端开发高频面试题的 Java 基础部分。每道题统一格式:题目 → 答案 → 对比表 / 代码示例 / 一句话记忆。
1. Java 的三大特性
Java 面向对象的三大特性:封装、继承、多态。
(1)封装
把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口(getter / setter)。封装隐藏了对象内部的实现细节,降低了耦合,提升了安全性和可维护性。
(2)继承
从已有类派生出新的类,子类可以获得父类的属性和方法,并可以扩展或重写。继承提高了代码的复用性,Java 只支持单继承(通过接口实现多继承的效果)。
(3)多态
同一种类型的对象执行同一个方法时可以表现出不同的行为特征。不用修改源程序代码,就可以让引用变量绑定到不同的类实现上,从而让程序在运行时选择多个运行状态,提高了灵活性和扩展性。
多态分为:
- 编译时多态:方法重载(Overload),在编译期确定调用哪个方法。
- 运行时多态:方法重写(Override),在运行期根据实际对象类型确定调用哪个方法。
实现运行时多态需要两个条件:
- 子类继承父类并重写父类中的方法。
- 用父类型引用子类型对象(向上转型),调用同样的方法会根据子类对象的不同而表现出不同的行为。
// 多态示例
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.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 扩容并发问题 | 可能形成环形链表死循环 | 不会死循环(但仍非线程安全) |
| 节点类型 | Entry | Node / 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:
// 获取 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 动态代理示例:
// 实现 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() 唤醒(也可设超时) |
| 用途 | 暂停当前线程一段时间 | 线程间通信/协作 |
| 对象 | 作用于当前线程 | 作用于特定对象(监视器) |
synchronized (lock) {
lock.wait(); // 释放 lock 的锁,进入等待队列
Thread.sleep(1000); // 不释放锁,休眠 1 秒
}💡 一句话记忆:sleep 占着锁睡(Thread 类),wait 放锁等通知(Object 类)。
9. throw 和 throws 的区别
| 对比项 | throws | throw |
|---|---|---|
| 位置 | 方法声明后面 | 方法体内 |
| 后跟内容 | 异常类名(可多个,逗号隔开) | 异常对象(只能一个) |
| 处理者 | 由该方法的调用者处理 | 由方法体内的语句处理 |
| 含义 | 声明可能抛出的异常,不一定发生 | 抛出异常,执行到这里一定抛出 |
// 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 方法,改为比较内容。
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 的区别
| 对比项 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 安全(synchronized) | 不安全 |
| 性能 | 低(每次修改创建新对象) | 中 | 高 |
| 底层结构 | final char[](JDK 9+ 为 byte[]) | char[],可扩容 | char[],可扩容 |
| 引入版本 | JDK 1.0 | JDK 1.0 | JDK 1.5 |
| 适用场景 | 字符串不常修改 | 多线程下频繁拼接 | 单线程下频繁拼接 |
为什么 String 不可变?
String 类被 final 修饰,内部字符数组也被 final 修饰,赋值后不可修改。不可变性带来三个好处:
- 线程安全:多线程共享无需同步。
- 安全性:作为 HashMap 的 key 不会变化,类加载器、数据库连接等场景安全。
- 字符串常量池:不可变才能安全共享常量池中的对象。
💡 一句话记忆:String 不可变、StringBuffer 线程安全、StringBuilder 单线程最快。
12. Collection 和 Collections 的区别
| 对比项 | Collection | Collections |
|---|---|---|
| 类型 | 接口 | 工具类 |
| 作用 | 集合体系的顶层接口 | 提供集合操作的静态方法 |
| 包 | java.util | java.util |
| 关系 | List、Set 的父接口 | 操作 Collection 的工具类 |
Collection 是集合的根接口,继承体系:Collection ← List(ArrayList、LinkedList)/ Set(HashSet、TreeSet)。
Collections 是工具类,常见方法:
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 类。
public final class MyUtils {
// 该类不能被继承
}final 关键字的三个用法:
| 修饰对象 | 作用 | 示例 |
|---|---|---|
| 修饰类 | 该类不能被继承 | final class String |
| 修饰方法 | 该方法不能被子类重写 | public final void doSomething() |
| 修饰变量 | 引用不可变(基本类型值不变,引用类型地址不变) | final int MAX = 100; |
💡 一句话记忆:final 修饰类不可继承、修饰方法不可重写、修饰变量不可重新赋值。
14. 单例模式的所有写法
单例模式保证一个类只有一个实例,并提供全局访问点。共有 5 种主流写法:
(1)饿汉式
类加载时即创建实例,线程安全,但可能浪费资源。
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return INSTANCE;
}
}(2)懒汉式(线程不安全 / 同步方法)
第一次调用时创建,但同步方法性能差。
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 防止指令重排,性能好且线程安全。
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)静态内部类(推荐)
利用类加载机制保证线程安全,且实现懒加载。
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》推荐写法。
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 语句的结果集。
| 对比项 | UNION | UNION ALL |
|---|---|---|
| 去重 | 去除重复记录 | 保留所有记录(不去重) |
| 排序 | 内部进行去重排序 | 不排序 |
| 性能 | 较慢(需去重 + 排序) | 较快 |
| 结果集 | 唯一 | 可能有重复 |
| 执行计划 | 通常含 SORT/UNIQUE 操作 | 直接合并 |
执行原理:
UNION在合并结果后会执行去重操作,底层需要对结果集排序后再去重,CPU 和内存开销较大。UNION ALL直接将多个结果集合并返回,无去重和排序,性能优于 UNION。
使用建议:
- 确定无重复数据时优先使用
UNION ALL,性能更优。 - 需要去重时使用
UNION,但要注意排序成本,可在外层加ORDER BY控制。
💡 一句话记忆:UNION 去重排序慢、UNION ALL 全保留快;确定无重复用 ALL。