🔥 高频题(第1~25题)
第1题:Java作为一门编程语言,其核心特点有哪些?
【核心答案】
Java 的核心特点可以概括为以下六点:
| 特点 | 说明 |
|---|---|
| 跨平台性 | 通过 JVM + 字节码实现「一次编写,到处运行」 |
| 面向对象 | 封装、继承、多态,一切皆对象(除基本类型) |
| 自动内存管理(GC) | 垃圾回收器自动回收不再使用的对象,无需手动释放内存 |
| 强类型 + 健壮性 | 编译时类型检查、异常处理机制、无指针,减少运行时错误 |
| 多线程支持 | 内置 Thread/Runnable,JUC 并发包提供高级并发工具 |
| 丰富的生态 | Spring、MyBatis、Netty 等成熟框架,适用于企业级开发 |
【通俗理解】
把 Java 想象成一家国际连锁餐厅:
- 跨平台 = 同样的菜谱(代码)在不同国家的分店(操作系统)都能做出同样的菜
- 面向对象 = 厨房分工明确------洗菜(类A)、切菜(类B)、炒菜(类C),各司其职
- GC = 自动洗碗机,你不用手动洗碗(释放内存)
- 强类型 = 菜刀就是菜刀,不能当锅铲用,类型严格限制
【常见误区】
❌ 误区 :Java 是纯解释型语言,所以很慢
✅ 正解:Java 采用「解释 + JIT 编译」混合模式,热点代码被编译为本地机器码后性能接近 C/C++
第2题:Java是如何实现"一次编写,到处运行"的跨平台特性的?
【核心答案】
Java 通过两层抽象实现跨平台:
-
编译阶段 :
.java源文件 → javac 编译 →.class字节码(与平台无关的中间码) -
运行阶段:不同平台的 JVM 将同样的字节码翻译为对应平台的机器码
┌──────────────────────────────────────────────┐
│ Java 源文件 (.java) │
└──────────────────┬───────────────────────────┘
│ javac 编译
▼
┌──────────────────────────────────────────────┐
│ 字节码 (.class) ← 一次编写 │
└──────┬──────────────┬──────────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Linux JVM│ │Windows JVM│ │ macOS JVM│ ← 到处运行
└──────────┘ └──────────┘ └──────────┘
【通俗理解】
- 字节码就像「五线谱」------同样的乐谱,无论在中国、美国、法国,钢琴家(JVM)都能演奏
- 你把字节码想象成一份「国际通用说明书」,各地工厂(JVM)拿到后各自用本地工艺(机器码)生产
【常见误区】
❌ 误区 :Java 程序直接编译成机器码运行
✅ 正解:先编译成字节码(.class),再由 JVM 的 JIT 编译器或解释器转换为机器码
第3题:请解释JVM、JDK、JRE三者分别是什么,以及它们之间的关系?
【核心答案】
| 概念 | 全称 | 作用 | 包含内容 |
|---|---|---|---|
| JVM | Java Virtual Machine | 执行字节码,提供运行时环境 | 类加载器、字节码执行引擎、GC |
| JRE | Java Runtime Environment | 运行 Java 程序的最小环境 | JVM + 核心类库 (rt.jar) |
| JDK | Java Development Kit | 开发 Java 程序的工具包 | JRE + 开发工具 (javac, jar, javadoc等) |
【关系图解】
┌─────────────────────────────────────┐
│ JDK │
│ ┌─────────────────────────────┐ │
│ │ javac / javap / jar / ... │ ← 开发工具 │
│ │ ┌───────────────────────┐ │ │
│ │ │ JRE │ │ │
│ │ │ ┌──────────────┐ │ │ │
│ │ │ │ JVM │ │ │ │
│ │ │ │ 类库 (rt.jar)│ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
包含关系 :JDK ⊃ JRE ⊃ JVM
【通俗理解】
- JVM = 汽车的发动机(核心动力)
- JRE = 发动机 + 轮胎 + 方向盘(能开起来的最小配置)
- JDK = 整车 + 工具箱(既能开车,也能修车/造车)
第4题:为什么说Java既是编译型语言又是解释型语言?JIT编译器在其中起什么作用?
【核心答案】
Java 的执行分为两个阶段:
源代码(.java) ──编译──▶ 字节码(.class) ──解释/JIT编译──▶ 机器码
↑ ↑ ↑
编译型语言特征 中间产物 解释+编译混合执行
| 阶段 | 做了什么 | 性质 |
|---|---|---|
| javac 编译 | .java → .class 字节码 |
编译型特征 |
| JVM 解释执行 | 逐条解释字节码为机器码 | 解释型特征 |
| JIT 即时编译 | 热点代码直接编译为机器码并缓存 | 编译型特征(运行时) |
JIT(Just-In-Time)编译器的作用:
-
监控方法调用频率,将热点代码(频繁执行的代码)直接编译为本地机器码
-
下次调用时直接执行机器码,无需再次解释,性能大幅提升
-
典型优化:方法内联、逃逸分析、锁消除等
解释执行速度: ████████░░ 慢(每次都翻译)
JIT编译后速度: ████████████████████ 快(只翻译一次,后续直接用)
【通俗理解】
- Java 像是一个同声传译 + 速记员 的组合:
- 平常内容逐句翻译(解释执行)
- 听到重复/重要的话,直接记下来下次复读(JIT 编译)
- 你第一次唱一首歌需要看歌词(解释),唱多了就直接会了(JIT 缓存)
第5题:值传递和引用传递有什么区别?Java中使用的是哪一种?请举例说明。
【核心答案】
Java 中只有值传递(Pass by Value),没有引用传递!
| 传递方式 | 含义 | Java 中是否存在 |
|---|---|---|
| 值传递 | 将实参的值拷贝一份传给形参 | ✅ 是 |
| 引用传递 | 将实参的地址直接传给形参(形参和实参指向同一内存地址) | ❌ 否 |
对于基本类型:传递的是值的拷贝
java
public static void main(String[] args) {
int a = 10;
modify(a);
System.out.println(a); // 输出 10,没有改变!
}
public static void modify(int x) {
x = 20; // 改变的是 x 这个副本
}
对于引用类型 :传递的是引用的拷贝(不是对象本身!)
java
public static void main(String[] args) {
User user = new User("张三");
modify(user);
System.out.println(user.name); // 输出 "李四",对象内容变了!
}
public static void modify(User u) {
u.name = "李四"; // u 是 user 引用的拷贝,但指向同一个对象
u = new User("王五"); // 改变的是 u 这个副本指向,原 user 不受影响
}
【图解】
基本类型(值传递):
main: a = [10]
│ 拷贝值
modify: x = [10] → x = [20] // 改的是拷贝
main: a = [10] // 不受影响
引用类型(传引用拷贝):
main: user ──→ [User("张三")] ← 堆中的真实对象
▲
modify: u ──────┘ // u 是 user 的引用拷贝,指向同一对象
u.name = "李四" → 修改了真实对象 ✅
u = new User("王五") → u 指向新对象,user 不变 ✅
【常见误区】
❌ 误区 :Java 基本类型是值传递,引用类型是引用传递
✅ 正解:Java 一切都是值传递。引用类型传递的是「引用地址的值」的拷贝,不是对象本身地址的直接传递
第6题:Java的八种基本数据类型分别是什么?每种各占用多少字节?
【核心答案】
| 数据类型 | 关键字 | 占用字节 | 占用位数 | 取值范围 | 默认值 |
|---|---|---|---|---|---|
| 字节型 | byte |
1 | 8 | -128 ~ 127 | 0 |
| 短整型 | short |
2 | 16 | -32768 ~ 32767 | 0 |
| 整型 | int |
4 | 32 | -2³¹ ~ 2³¹-1(约±21亿) | 0 |
| 长整型 | long |
8 | 64 | -2⁶³ ~ 2⁶³-1 | 0L |
| 单精度浮点 | float |
4 | 32 | ±3.4E-38 ~ ±3.4E+38 | 0.0f |
| 双精度浮点 | double |
8 | 64 | ±1.7E-308 ~ ±1.7E+308 | 0.0d |
| 字符型 | char |
2 | 16 | 0 ~ 65535 (Unicode) | '\u0000' |
| 布尔型 | boolean |
1(虚拟机相关) | - | true / false | false |
【记忆口诀】
byte → short → int → long → float → double (从小到大)
1-2-4-8-4-8 字节,char 占 2 字节,boolean 占 1 字节
【自动类型提升顺序】
byte → short → int → long → float → double
↑
char
第7题:为什么涉及金额计算时推荐使用BigDecimal而不是double或float?
【核心答案】
因为 float 和 double 采用二进制浮点数 表示,很多十进制小数无法精确表示,会产生精度丢失。
java
// 经典翻车现场
System.out.println(0.1 + 0.2); // 输出:0.30000000000000004 ❌
System.out.println(1.0 - 0.9); // 输出:0.09999999999999998 ❌
// 使用 BigDecimal
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 输出:0.3 ✅
【为什么会精度丢失?】
10 进制的 0.1 转换为 2 进制是无限循环小数:0.0001100110011...,计算机只能用有限的位数近似存储。
【BigDecimal 使用关键点】
java
// ❌ 错误:用 double 构造 BigDecimal,精度已经丢失
BigDecimal bad = new BigDecimal(0.1); // 0.100000000000000005551...
// ✅ 正确:用字符串构造
BigDecimal good = new BigDecimal("0.1");
// ✅ 也可以用 valueOf(内部用了字符串)
BigDecimal alsoGood = BigDecimal.valueOf(0.1);
| 对比维度 | double/float | BigDecimal |
|---|---|---|
| 精度 | 不精确(二进制近似) | 精确(十进制整数存储) |
| 性能 | 快(硬件支持) | 慢(软件计算) |
| 使用场景 | 科学计算、图形渲染 | 金融、货币计算 |
| API 复杂度 | 简单 | 复杂(加减乘除都用方法) |
【常见误区】
❌ 误区 :
new BigDecimal(0.1)就能精确表示 0.1✅ 正解 :double 的 0.1 本身就不精确,构造 BigDecimal 时已经晚了,必须用
new BigDecimal("0.1")
第8题:什么是自动装箱和自动拆箱?使用过程中有哪些常见的坑?
【核心答案】
| 概念 | 方向 | 编译器做的事 |
|---|---|---|
| 自动装箱 (Autoboxing) | 基本类型 → 包装类 | Integer.valueOf(int) |
| 自动拆箱 (Unboxing) | 包装类 → 基本类型 | Integer.intValue() |
java
// 自动装箱:int → Integer
Integer i = 100; // 编译器生成:Integer.valueOf(100)
// 自动拆箱:Integer → int
int j = i; // 编译器生成:i.intValue()
// 运算时自动拆箱
Integer a = 10;
Integer b = 20;
int c = a + b; // a.intValue() + b.intValue()
【常见坑】
坑1:NullPointerException
java
Integer x = null;
int y = x; // 自动拆箱时调用 x.intValue() → NPE!
坑2:== 比较的是引用,不是值
java
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存范围内)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(超出缓存范围,new 了新对象)!
坑3:大量装箱影响性能
java
// ❌ 坏习惯:循环中大量装箱
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // 每次循环都会装箱/拆箱
}
// ✅ 好习惯:用基本类型
long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
第9题:Integer的缓存机制是什么?缓存范围是多少?为什么这样设计?
【核心答案】
Integer 内部维护了一个静态缓存数组 ,默认缓存 [-128, 127] 范围内的 Integer 对象。
java
// Integer.valueOf() 源码(简化版)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
java
Integer a = 100; // Integer.valueOf(100) → 从缓存取
Integer b = 100; // Integer.valueOf(100) → 从缓存取(同一个对象)
Integer c = 200; // Integer.valueOf(200) → new Integer(200)
Integer d = 200; // Integer.valueOf(200) → new Integer(200)(新对象)
System.out.println(a == b); // true ✅
System.out.println(c == d); // false ❌ 容易踩坑
【为什么是 -128 ~ 127?】
- 这个范围覆盖了最常用的小整数(循环计数、数组下标等)
- 借鉴了「享元模式(Flyweight Pattern)」思想,避免频繁创建销毁小整数对象
- 这个范围的数据在大多数应用中使用频率最高
【可通过 JVM 参数调整上限】
bash
-XX:AutoBoxCacheMax=256 # 将缓存上限调整到 256
【图解】
┌─────────────────────────────────────────────┐
│ IntegerCache.cache[] │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │-128│-127│ ...│ 0 │ 1 │ ...│ 126│ 127│ │
│ └────┴────┴────┴────┴────┴────┴────┴────┘ │
│ ↑ 缓存命中,返回同一对象 │
│ │
│ Integer.valueOf(128) ──────────▶ new Integer(128) │
│ 超范围,每次 new 新对象 │
└─────────────────────────────────────────────┘
【对比其他包装类】
| 包装类 | 缓存范围 |
|---|---|
| Byte | -128 ~ 127(全部缓存) |
| Short | -128 ~ 127 |
| Integer | -128 ~ 127(可调上限) |
| Long | -128 ~ 127 |
| Character | 0 ~ 127(ASCII) |
| Float, Double | 无缓存 |
第10题:请结合代码示例,说明面向对象的三大特性:封装、继承、多态。
【核心答案】
面向对象三大特性
├── 封装(Encapsulation)
│ 隐藏内部实现,只暴露必要的接口
├── 继承(Inheritance)
│ 子类继承父类的属性和方法,实现代码复用
└── 多态(Polymorphism)
同一个方法调用,不同对象产生不同行为
【1. 封装------安全的数据访问】
java
public class BankAccount {
private double balance; // 私有字段,外部不能直接访问
// 只提供受控的访问方式
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount > balance) {
throw new IllegalArgumentException("余额不足");
}
balance -= amount;
}
}
// 外部无法 bankAccount.balance = -1000; ← 被 private 保护
【2. 继承------代码复用】
java
// 父类
public class Animal {
protected String name;
public void eat() {
System.out.println(name + " 在吃东西");
}
}
// 子类继承父类
public class Dog extends Animal {
public void bark() {
System.out.println(name + " 汪汪叫");
}
}
Dog dog = new Dog();
dog.name = "旺财";
dog.eat(); // 继承自 Animal
dog.bark(); // Dog 自己的方法
【3. 多态------同一接口,不同实现】
java
// 多态的核心:父类引用指向子类对象
Animal animal1 = new Dog(); // 向上转型
Animal animal2 = new Cat();
animal1.eat(); // 输出:"狗在吃骨头"(调用 Dog 的 eat)
animal2.eat(); // 输出:"猫在吃鱼"(调用 Cat 的 eat)
// 多态的三种形式:
// 1) 继承多态(如上)
// 2) 接口多态
List<String> list = new ArrayList<>(); // 接口引用指向实现类
// 3) 方法重载(编译时多态)
public void print(int x) { ... }
public void print(String s) { ... }
【三者关系图解】
封装
┌───────┐
│ 隐藏实现 │
│ 保护数据 │
└───┬───┘
│ 提供接口
▼
继承 ──────▶ 多态
┌───────┐ ┌───────────┐
│ 复用代码 │──▶│ 灵活替换 │
│ 建立层级 │ │ 扩展开放 │
└───────┘ └───────────┘
第11题:方法重载(Overload)和方法重写(Override)有什么区别?各自的规则是什么?
【核心答案】
| 对比维度 | 重载 (Overload) | 重写 (Override) |
|---|---|---|
| 定义 | 同一个类中,方法名相同,参数列表不同 | 子类重新定义父类的同名方法 |
| 发生范围 | 同一个类(或父子类) | 父子类之间 |
| 参数列表 | 必须不同(数量/类型/顺序) | 必须相同 |
| 返回类型 | 可以不同 | 相同或是其子类(协变返回) |
| 访问修饰符 | 可以任意 | 不能比父类更严格 |
| 异常 | 可以任意 | 不能比父类抛出更宽泛的异常 |
| static方法 | 可以重载 | 不能重写(可以隐藏) |
| final方法 | 可以重载 | 不能重写 |
| 发生时机 | 编译时确定(编译时多态) | 运行时确定(运行时多态) |
| 注解 | 无需注解 | @Override(推荐加上) |
【代码示例】
java
// ========== 方法重载 ==========
public class Calculator {
// 同一个方法名,不同参数
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; } // 参数类型不同
public int add(int a, int b, int c) { return a + b + c; } // 参数个数不同
}
// 调用时编译期就能确定调哪个
// ========== 方法重写 ==========
public class Animal {
public Animal makeSound() throws Exception {
System.out.println("动物叫");
return this;
}
}
public class Dog extends Animal {
@Override // 编译期检查是否正确重写
public Dog makeSound() { // 返回类型可以是子类(协变)
System.out.println("汪汪汪");
return this;
}
}
Animal a = new Dog();
a.makeSound(); // 运行时才确定调 Dog 的方法 → 输出"汪汪汪"
【常见误区】
❌ 误区1 :重载可以通过不同的返回类型来区分
✅ 正解 :重载只看方法名+参数列表,返回类型不同但参数相同不是重载,会编译报错
❌ 误区2 :重写可以改变 static 方法的行为✅ 正解:static 方法属于类,不能被子类重写,只能被「隐藏」(方法隐藏)
第12题:Java中抽象类和接口的区别是什么?在Java 8之后接口有哪些变化?
【核心答案】
| 对比维度 | 抽象类 (abstract class) | 接口 (interface) |
|---|---|---|
| 关键字 | abstract class |
interface |
| 继承/实现 | 单继承 (extends),只能继承一个 |
多实现 (implements),可实现多个 |
| 构造方法 | ✅ 有 | ❌ 无 |
| 成员变量 | 任意类型(可非 public) | 只能 public static final 常量 |
| 普通方法 | ✅ 可以有 | Java 8+ 可以用 default |
| 静态方法 | ✅ 可以有 | Java 8+ 可以有 |
| 私有方法 | ✅ 可以有 | Java 9+ 可以用 private |
| 设计意图 | "is-a" 关系,提取共性 | "can-do" 能力,定义规范 |
【Java 8 之后接口的变化】
Java 7 及以前:
接口中只能有:抽象方法 + 常量
Java 8:
新增 default 方法(有默认实现)
新增 static 方法
Java 9:
新增 private 方法(供 default 方法内部复用)
代码示例:
java
// Java 8+ 的接口
public interface PaymentService {
// 抽象方法(传统接口方法)
void pay(BigDecimal amount);
// Java 8: default 方法(有默认实现,子类可重写)
default void logPayment(BigDecimal amount) {
validate(amount); // 调用私有方法
System.out.println("支付金额:" + amount);
}
// Java 8: static 方法
static PaymentService createAliPay() {
return new AliPayServiceImpl();
}
// Java 9: private 方法
private void validate(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
}
}
【选择策略】
需要一个模板/骨架? → 抽象类
├── 多个类共享代码 → 抽象类
├── 需要非 public 成员 → 抽象类
├── 需要构造方法初始化 → 抽象类
└── "is-a" 关系 → 抽象类
只需要定义能力/契约? → 接口
├── 不相关的类需要共同行为 → 接口
├── 需要多重继承 → 接口
├── "can-do" 能力 → 接口
└── 解耦(面向接口编程) → 接口
第13题:final关键字在Java中可以修饰类、方法、变量,分别有什么作用?
【核心答案】
| 修饰对象 | 作用 | 效果 |
|---|---|---|
| 类 | final class A {} |
类不能被继承 (断子绝孙类),如 String、Integer |
| 方法 | final void m() {} |
方法不能被子类重写 |
| 基本类型变量 | final int x = 10; |
值不可修改(常量) |
| 引用类型变量 | final User u = new User(); |
引用不可变 (不能指向新对象),但对象内部状态可以变 |
java
// ===== 修饰类 =====
public final class StringUtils {
// 不能被继承,工具类常用
}
// class Sub extends StringUtils {} ← 编译错误!
// ===== 修饰方法 =====
public class Parent {
public final void criticalLogic() {
// 模板方法模式中保护核心算法
}
}
// ===== 修饰变量 =====
final int MAX_SIZE = 100; // 基本类型不可变
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 对象内容可以变
// list = new ArrayList<>(); // ❌ 引用不可变,编译错误
【图解】
final int x = 10;
x ──→ [10] ← 10 这个值被锁死
final User u = new User("Tom");
u ──→ [User(name="Tom")] ← 这个箭头被锁死
│
name 可以改!
【常见误区】
❌ 误区 :final 修饰的引用变量指向的对象也不能修改
✅ 正解 :引用不可变 ≠ 对象不可变。
final List不能= new ArrayList(),但可以list.add()
第14题:static关键字可以修饰哪些成员?分别产生什么效果?
【核心答案】
static 可以修饰:变量、方法、代码块、内部类 。被 static 修饰的成员属于类级别,而非实例级别。
| 修饰对象 | 效果 | 加载时机 | 访问方式 |
|---|---|---|---|
| 静态变量 | 所有实例共享同一份数据 | 类加载时 | 类名.变量 |
| 静态方法 | 只能访问静态成员,不能使用 this | 类加载时 | 类名.方法() |
| 静态代码块 | 类加载时执行一次,用于初始化静态变量 | 类加载时 | 自动执行 |
| 静态内部类 | 不依赖外部类实例,可独立存在 | 使用时加载 | new Outer.Inner() |
java
public class StaticDemo {
// 静态变量:所有实例共享
private static int count = 0;
// 静态代码块:类加载时执行一次
static {
System.out.println("类加载时执行");
count = 100;
}
// 静态方法:只能访问静态成员
public static int getCount() {
// instanceMethod(); ← 编译错误,不能调用非静态方法
return count;
}
// 静态内部类
static class Builder {
public StaticDemo build() { return new StaticDemo(); }
}
}
【内存图解】
方法区/元空间
┌───────────────────┐
│ StaticDemo.class │
│ static count │ ← 类级别,只有一份
│ static {} │
└───────────────────┘
堆内存
┌───────────────────┐
│ new StaticDemo() │ ← 实例1 (没有自己的 count)
│ new StaticDemo() │ ← 实例2 (没有自己的 count)
└───────────────────┘
第15题:深拷贝和浅拷贝的区别是什么?如何实现一个对象的深拷贝?
【核心答案】
| 维度 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 基本类型字段 | 复制值 | 复制值 |
| 引用类型字段 | 只复制引用地址(指向同一对象) | 递归创建新对象(完全独立) |
| 独立性 | 拷贝对象和原对象共享内部引用对象 | 拷贝对象和原对象完全独立 |
| 实现难度 | 简单 | 复杂 |
【图解】
原对象: 浅拷贝后:
Person { Person {
name: "Tom" ──复制值──▶ name: "Tom"
address ──────────┐ address ───────────┐
} │ } │
▼ ▼
Address { 同一个 Address!
city: "北京" city: "北京"
} }
深拷贝后:
Person { Person {
name: "Tom" ──复制值──▶ name: "Tom"
address ──────────┐ address ──────┐
} │ } │
▼ ▼
Address { Address {
city: "北京" city: "北京"
} } ← 独立新对象
【三种实现方式】
java
// 方式1:实现 Cloneable + 递归 clone(侵入性强,不推荐)
public class Person implements Cloneable {
private String name;
private Address address;
@Override
public Person clone() {
Person p = (Person) super.clone();
p.address = this.address.clone(); // 递归拷贝引用对象
return p;
}
}
// 方式2:序列化/反序列化(推荐,但性能一般)
public static <T> T deepCopy(T obj) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
// 方式3:第三方库(Jackson / Gson / Fastjson,最常用)
Person copy = objectMapper.readValue(
objectMapper.writeValueAsString(original), Person.class);
| 方式 | 优点 | 缺点 |
|---|---|---|
| Cloneable | 原生支持 | 侵入性强,需要递归实现 |
| 序列化 | 通用性强 | 性能差,所有类需实现 Serializable |
| JSON 序列化 | 简单,非侵入 | 有序列化成本,特殊类型可能丢失 |
【常见误区】
❌ 误区 :
Object.clone()就是深拷贝✅ 正解 :
Object.clone()默认是浅拷贝!必须手动递归拷贝内部引用对象才是深拷贝
第16题:== 运算符与 equals() 方法在比较对象时有什么区别?
【核心答案】
| 对比维度 | == |
equals() |
|---|---|---|
| 比较内容 | 比较引用地址(栈中的引用值) | 比较对象内容(取决于类的重写实现) |
| 基本类型 | 比较值 | 不能用(基本类型没有方法) |
| Object 默认 | 比较引用地址 | 比较引用地址(等价于 ==) |
| String | 比较引用地址 | 比较字符串内容(已重写) |
| 自定义类 | 比较引用地址 | 取决于是否重写(需手动重写) |
java
// 基本类型:== 比较值
int a = 10, 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(常量池中同一个对象)
【图解】
String s1 = new String("hello");
String s2 = new String("hello");
栈: 堆:
s1 ──────→ [String "hello"] ← 地址 0x1000
s2 ──────→ [String "hello"] ← 地址 0x2000
s1 == s2 → false (0x1000 != 0x2000)
s1.equals(s2) → true (内容都是 "hello")
第17题:hashCode()和equals()方法之间有什么约束关系?为什么重写equals时必须重写hashCode?
【核心答案】
「黄金三定律」------hashCode 和 equals 的约束关系:
| 规则 | 说明 |
|---|---|
| 规则1 | 两个对象 equals 为 true → hashCode 必须相等 |
| 规则2 | 两个对象 equals 为 false → hashCode 可以相等也可以不等(等就是哈希冲突) |
| 规则3 | 同一个对象多次调用 hashCode → 必须返回相同的 int(前提是 equals 用到的字段没变) |
【为什么必须同时重写?------血的教训!】
java
public class User {
private String name;
// 只重写了 equals,没重写 hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
return Objects.equals(name, ((User) o).name);
}
// hashCode 没重写!使用的是 Object 的 native hashCode
}
// 出问题的场景
Map<User, String> map = new HashMap<>();
map.put(new User("张三"), "北京");
System.out.println(map.get(new User("张三"))); // 输出 null !!!
【为什么会返回 null?】
HashMap 查找流程:
get(key) → 计算 key.hashCode() → 定位到桶
→ 在该桶内用 equals() 逐一比较
put 时: new User("张三").hashCode() = 123456 → 桶#5
get 时: new User("张三").hashCode() = 789012 → 桶#8
桶不同!根本找不到!
【正确做法】
java
public class User {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 必须使用与 equals 相同的字段
}
}
第18题:String、StringBuffer、StringBuilder三者的区别是什么?分别在什么场景下使用?
【核心答案】
| 对比维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(final char[]/byte[]) | 可变 | 可变 |
| 线程安全 | ✅ 安全(不可变天然安全) | ✅ 安全(synchronized) | ❌ 不安全 |
| 性能 | 拼接时创建新对象,最慢 | 中等(有同步开销) | 最快 |
| 继承关系 | - | 继承 AbstractStringBuilder | 继承 AbstractStringBuilder |
| 使用场景 | 字符串常量、少量拼接 | 多线程环境拼接 | 单线程环境拼接 |
【代码示例】
java
// String:每次拼接都创建新对象
String s = "Hello";
s += " World"; // 创建了新 String 对象!原 "Hello" 等待 GC
// StringBuffer:线程安全,多线程用
StringBuffer sb1 = new StringBuffer();
sb1.append("Hello").append(" World"); // 同一个对象上操作
// StringBuilder:非线程安全,单线程首选
StringBuilder sb2 = new StringBuilder();
sb2.append("Hello").append(" World"); // 同一个对象上操作,最快
【底层原理】
String:
"Hello" [H][e][l][l][o] ← final byte[],不可变
"Hello World" [H][e][l][l][o][ ][W][o][r][l][d] ← 全新对象!
StringBuilder:
[H][e][l][l][o][ ][W][o][r][l][d][ ][ ][ ][ ][ ] ← 内部可变数组
直接在原数组上追加,容量不够时扩容(2倍+2)
【选择决策图】
需要频繁拼接字符串?
├── 是 → 多线程环境?
│ ├── 是 → StringBuffer
│ └── 否 → StringBuilder
└── 否 → String
第19题:Java 8引入了哪些重要的新特性?请列举并简要说明。
【核心答案】
| 新特性 | 说明 | 解决的问题 |
|---|---|---|
| Lambda 表达式 | (参数) -> { 函数体 } |
简化匿名内部类,支持函数式编程 |
| 函数式接口 | @FunctionalInterface,如 Predicate、Function、Consumer、Supplier |
配合 Lambda 使用,定义函数签名 |
| Stream API | 对集合进行声明式的流式操作 | 告别 for 循环,链式处理数据 |
| Optional | 容器类,优雅处理 null | 避免 NullPointerException |
| 方法引用 | 类名::方法名,如 System.out::println |
进一步简化 Lambda |
| 接口默认方法/静态方法 | default / static 方法 |
接口可添加新方法而不破坏实现类 |
| 新的日期时间 API | LocalDate、LocalTime、LocalDateTime(java.time包) |
替代难用的 Date/Calendar |
| Base64 | java.util.Base64 |
官方 Base64 编解码 |
【Lambda & Stream 代码示例】
java
List<String> names = Arrays.asList("张三", "李四", "王五", "赵六");
// 以前:匿名内部类
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// Java 8:Lambda
names.sort((a, b) -> a.compareTo(b));
// Stream:过滤 + 转换 + 收集
List<String> result = names.stream()
.filter(name -> name.startsWith("张")) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:转换
.collect(Collectors.toList()); // 终端操作:收集
// 结果:["张三"]
【Optional 示例】
java
// 以前
User user = getUser();
if (user != null) {
String name = user.getName();
if (name != null) {
return name.toUpperCase();
}
}
return "Unknown";
// Java 8
return Optional.ofNullable(getUser())
.map(User::getName)
.map(String::toUpperCase)
.orElse("Unknown");
【新日期 API 示例】
java
LocalDate today = LocalDate.now(); // 2026-05-17
LocalTime time = LocalTime.of(14, 30); // 14:30
LocalDateTime dt = LocalDateTime.now(); // 2026-05-17T14:30:00
// 日期计算(不可变,返回新对象)
LocalDate nextWeek = today.plusWeeks(1);
Period period = Period.between(today, nextWeek); // P7D
第20题:什么是Java反射机制?反射有哪些典型应用场景?它有什么优缺点?
【核心答案】
反射(Reflection) 是 Java 在运行时动态获取类的信息(构造方法、字段、方法、注解等)并操作对象的能力。
编译时:不知道具体是什么类
运行时:通过 Class 对象 → 获取构造器 → 创建实例 → 调用方法 → 访问字段
【核心 API 演示】
java
// 1. 获取 Class 对象的三种方式
Class<?> clazz1 = User.class;
Class<?> clazz2 = user.getClass();
Class<?> clazz3 = Class.forName("com.example.User");
// 2. 创建实例
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object obj = constructor.newInstance("张三");
// 3. 调用私有方法
Method method = clazz.getDeclaredMethod("secretMethod");
method.setAccessible(true); // 突破私有权限
method.invoke(obj);
// 4. 访问私有字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
String value = (String) field.get(obj);
field.set(obj, "李四");
【典型应用场景】
| 场景 | 说明 | 例子 |
|---|---|---|
| 框架开发 | Spring IoC 通过反射实例化 Bean、注入依赖 | @Autowired 注入 |
| 动态代理 | JDK 动态代理依赖反射调用目标方法 | AOP 拦截 |
| 注解处理 | 运行时读取注解并执行对应逻辑 | @TableName、@RequestMapping |
| 序列化/反序列化 | JSON 库通过反射读写对象字段 | Jackson、Gson |
| 插件化开发 | 动态加载类,实现热部署 | OSGi、IDEA 插件 |
【优缺点】
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 灵活性极高,运行时动态操作;框架的基石 |
| ❌ 缺点1 | 性能差(比直接调用慢 10~100 倍),需要安全检查、包装类型 |
| ❌ 缺点2 | 破坏封装性,可以绕过访问修饰符 |
| ❌ 缺点3 | 编译期无法检查,错误推迟到运行时 |
第21题:请描述Java的异常体系结构:Error和Exception有什么区别?受检异常与非受检异常分别是什么?
【核心答案】
Throwable
/ \
Error Exception
(严重错误) / \
/ \ RuntimeException 其他 Exception
OOMError SOError (非受检异常) (受检异常)
/ \
NullPointer IllegalArgumentException
IndexOutOfBounds
| 类型 | 父类 | 特点 | 是否需要处理 | 示例 |
|---|---|---|---|---|
| Error | Throwable | JVM 级别严重错误,程序无法处理 | ❌ 不需要 | OutOfMemoryError、StackOverflowError |
| 受检异常 (Checked) | Exception | 编译期强制处理 | ✅ 必须 try-catch 或 throws | IOException、SQLException |
| 非受检异常 (Unchecked) | RuntimeException | 编译期不检查 | ❌ 不需要强制处理 | NullPointerException、IllegalArgumentException |
java
// 受检异常:必须处理
public void readFile(String path) throws IOException {
FileReader fr = new FileReader(path); // FileReader 构造方法声明了 throws
}
// 非受检异常:可处理可不处理
public int divide(int a, int b) {
return a / b; // 可能 ArithmeticException,但编译期不报错
}
【常见误区】
❌ 误区 :RuntimeException 不需要捕获,所以可以忽略
✅ 正解 :不需要强制捕获 ≠ 不会发生。应该通过代码逻辑预防(如判空),而不是靠 try-catch 掩盖
第22题:双重校验锁(DCL)实现单例模式时,为什么必须使用volatile关键字?
【核心答案】
DCL(Double-Checked Locking)单例中 volatile 防止指令重排导致的「半初始化对象」问题。
java
public class Singleton {
// volatile 是必须的!
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一重检查
synchronized (Singleton.class) {
if (instance == null) { // 第二重检查
instance = new Singleton(); // 问题在这里!
}
}
}
return instance;
}
}
【为什么 new Singleton() 不是原子操作?】
instance = new Singleton() 在 JVM 层面分为三步:
① 分配内存空间
② 调用构造方法,初始化对象
③ instance 引用指向内存地址
正常顺序:① → ② → ③
指令重排:① → ③ → ② ← 危险!
【指令重排时的灾难场景】
线程A: 线程B:
① 分配内存
③ instance 指向内存(还没初始化!)
if (instance == null) → false
return instance; ← 拿到半成品!
② 调用构造方法(晚了!)
【volatile 如何解决?】
volatile 提供内存屏障 ,禁止 instance = new Singleton() 的指令重排,保证 ① → ② → ③ 严格有序。
有了 volatile:
① 分配内存 → ② 初始化 → ③ instance 赋值
↑ 内存屏障保证有序 ↑
第23题:BIO(阻塞IO)、NIO(非阻塞IO)、AIO(异步IO)三者的区别和适用场景分别是什么?
【核心答案】
| 对比维度 | BIO | NIO | AIO |
|---|---|---|---|
| 全称 | Blocking IO | Non-blocking IO | Asynchronous IO |
| IO模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 线程模型 | 一个连接一个线程 | 一个线程管理多个连接(Selector) | 回调/系统通知 |
| API 调用 | read() 阻塞等待数据 | read() 不阻塞,有数据才读 | read() 立即返回,系统完成后回调 |
| 并发能力 | 低 | 高 | 极高 |
| 编程复杂度 | 简单 | 复杂 | 中等 |
| 适用场景 | 连接数少、稳定 | 高并发、长连接 | 超高并发、异步处理 |
【形象比喻】
BIO:去餐厅吃饭,你在窗口一直等着,直到菜做好(阻塞)
NIO:你在座位上等,时不时去看看菜好了没(轮询/Selector)
AIO:你坐下刷手机,菜好了服务员会端过来(异步回调)
【图解】
BIO: 一个连接一个线程
Client1 ──▶ Thread1
Client2 ──▶ Thread2
Client3 ──▶ Thread3
(1000个连接需要1000个线程)
NIO: 一个 Selector 管理多个 Channel
Client1 ──┐
Client2 ──┼──▶ Selector ──▶ ThreadPool (少量线程)
Client3 ──┘
(1000个连接可能只需几个线程)
AIO: 系统完成IO后主动通知
Client1 ──▶ 发起读请求 → 系统处理 → 回调通知
(线程完全解放)
第24题:Java如何利用NIO的Channel、Buffer、Selector三大组件实现网络高并发编程?
【核心答案】
NIO 的三大核心组件各司其职:
| 组件 | 角色 | 职责 |
|---|---|---|
| Channel | 通道 | 数据的双向传输管道(读+写),对应一个连接 |
| Buffer | 缓冲区 | 数据的临时存储区,所有数据都通过 Buffer 读写 |
| Selector | 多路复用器 | 一个线程监控多个 Channel,只处理就绪的 Channel |
【工作流程】
Selector 线程 工作线程
│ │
│ ① 注册 Channel 到 Selector │
│ channel.register(selector, │
│ SelectionKey.OP_READ) │
│ │
│ ② 轮询就绪的 Channel │
│ while (true) { │
│ selector.select(); │
│ Set<SelectionKey> keys │
│ = selector.selectedKeys();│
│ for (key : keys) { │
│ if (key.isReadable()) { │
│ ③ 分发到工作线程 ────────▶ ④ 从 Channel 读数据到 Buffer
│ } │ channel.read(buffer)
│ } │ ⑤ 处理数据
│ } │ ⑥ 写响应数据到 Buffer → Channel
│ │ buffer.flip(); channel.write(buffer)
【代码示例------NIO 服务端】
java
// 1. 打开 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
// 2. 创建 Selector
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 3. 轮询
while (true) {
selector.select(); // 阻塞直到有就绪事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
// 接受新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = client.read(buffer);
if (len > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
}
}
it.remove(); // 必须移除,否则下次还会处理
}
}
【底层原理:epoll 多路复用】
NIO 在 Linux 上底层使用 epoll 系统调用,epoll 通过红黑树 + 就绪链表,将主动轮询变为事件通知,O(1) 获取就绪事件。
第25题:什么是Java泛型?什么是泛型擦除?泛型擦除会带来哪些问题?
【核心答案】
泛型(Generics) 允许类/方法在定义时使用类型参数 ,编译期进行类型检查,确保类型安全。
java
// 不用泛型:需要在运行时强制转换,不安全
List list = new ArrayList();
list.add("hello");
list.add(123); // 不会报错!
String s = (String) list.get(1); // 运行时 ClassCastException!
// 用泛型:编译期检查
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误!类型安全
String s = list.get(0); // 不需要强制转换
【泛型擦除(Type Erasure)】
Java 的泛型是编译期概念。编译后泛型信息会被「擦除」,字节码中不存在泛型,这是为了兼容老版本 JVM。
编译前: 编译后(擦除后):
List<String> list = new ArrayList<>(); List list = new ArrayList();
list.add("hello"); list.add("hello");
String s = list.get(0); String s = (String) list.get(0); ← 编译器自动加转型
【泛型擦除带来的问题】
问题1:不能使用基本类型
java
// List<int> list = new ArrayList<>(); ← 编译错误!
List<Integer> list = new ArrayList<>(); // 必须用包装类
问题2:无法用 instanceof 检查泛型类型
java
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) ← 编译错误!擦除后只有 List
if (list instanceof List) { } // 只能这样
问题3:无法创建泛型数组
java
// T[] arr = new T[10]; ← 编译错误!编译器不知道 T 是什么
T[] arr = (T[]) new Object[10]; // 只能这样绕过去(有警告)
问题4:方法签名冲突
java
// 这两个方法在擦除后签名完全一样,编译错误!
public void print(List<String> list) {}
public void print(List<Integer> list) {}
// 擦除后都变成:public void print(List list) {}
问题5:无法获取泛型的 Class 对象
java
// List<String>.class ← 不存在!
// 运行时只有 List.class
【上下界通配符】
java
// ? extends T:上界通配符,只能读不能写
public void readOnly(List<? extends Number> list) {
Number n = list.get(0); // ✅ 读取安全
// list.add(1); // ❌ 不能写(不知道具体子类型)
}
// ? super T:下界通配符,只能写不能读(精确类型)
public void writeOnly(List<? super Integer> list) {
list.add(1); // ✅ 写入安全
// Integer i = list.get(0); // ❌ 读出来是 Object
}
// PECS 原则:Producer Extends, Consumer Super
⭐ 中频题(第26~72题)
第26题:相比其他编程语言,Java的核心优势和劣势分别是什么?
【核心答案】
| 维度 | 优势 ✅ | 劣势 ❌ |
|---|---|---|
| 跨平台 | JVM 屏蔽操作系统差异 | 需要安装 JRE,启动慢 |
| 内存管理 | 自动 GC,无需手动释放 | GC 暂停影响实时性(ZGC 在改善) |
| 生态 | 企业级框架极其丰富(Spring、MyBatis) | 框架过多,学习成本高 |
| 类型安全 | 强类型 + 编译检查,减少运行时错误 | 代码冗长,不如动态语言灵活 |
| 多线程 | JUC 并发包功能强大 | 相对于 Go 协程较重(虚拟线程在改善) |
| 性能 | JIT 编译后接近 C++ | 占用内存大,冷启动慢 |
Java 典型优势场景:大型企业后端、分布式系统、大数据(Hadoop/Spark)
Java 不太适合的场景:嵌入式设备、系统编程、前端、AI 模型训练
第27题:JVM(Java虚拟机)和Java语言本身有什么区别?
【核心答案】
| 对比维度 | Java 语言 | JVM |
|---|---|---|
| 是什么 | 一门编程语言(语法规范) | 一个运行时环境(执行字节码) |
| 规范载体 | JLS(Java Language Specification) | JVMS(JVM Specification) |
| 关注点 | 语法、语义、类型系统 | 类加载、字节码执行、内存管理 |
| 依赖关系 | 需要 JVM 来运行 | 不依赖 Java 语言,可运行 Kotlin/Scala/Groovy 等 |
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Java 源码 │ │ Kotlin 源码 │ │ Scala 源码 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ 编译 │ 编译 │ 编译
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ 字节码 (.class) │
│ ▲ ▲ │
│ │ │ │
│ ┌───┴────────┴───┐ │
│ │ JVM │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────┘
第28题:JVM是什么?它的核心职责有哪些?
【核心答案】
JVM 是一个虚拟计算机,核心职责有四大块:
| 职责 | 说明 |
|---|---|
| 类加载 | 将 .class 字节码加载到内存(类加载器 + 双亲委派) |
| 字节码执行 | 解释执行 + JIT 编译,将字节码转为机器码 |
| 内存管理 | 分配内存 + 垃圾回收(GC) |
| 运行时支持 | 提供 Native 接口、异常处理、线程调度等 |
┌─────────────────┐
│ 类加载子系统 │
└────────┬────────┘
▼
┌─────────────────┐
│ 运行时数据区 │
│ 堆 栈 方法区 ... │
└────────┬────────┘
▼
┌─────────────────┐
│ 字节码执行引擎 │
│ 解释器 + JIT │
└─────────────────┘
第29题:编译型语言和解释型语言有什么区别?Java属于哪一种?
| 类型 | 工作方式 | 速度 | 跨平台 | 代表 |
|---|---|---|---|---|
| 编译型 | 源码一次编译成机器码,直接运行 | 快 | 差 | C、C++、Go、Rust |
| 解释型 | 逐行翻译成机器码并执行 | 慢 | 好 | Python、JavaScript |
| Java | 源码→字节码(编译)→解释/JIT执行 | 较快 | 好 | Java |
编译型: 源码 ──一次性编译──▶ 机器码 ──直接运行──▶ 输出
解释型: 源码 ──逐行翻译──▶ 机器码 ──逐行运行──▶ 输出
Java: 源码 ──编译──▶ 字节码 ──解释+JIT──▶ 机器码 ──运行──▶ 输出
第30题:Python和Java在语言特性、运行机制、适用场景上有哪些区别?
| 对比维度 | Java | Python |
|---|---|---|
| 类型系统 | 静态强类型 | 动态强类型 |
| 运行方式 | 编译 → JVM 执行字节码 | 解释执行(CPython) |
| 性能 | 快(JIT 优化后接近 C++) | 慢(解释执行,GIL限制多线程) |
| 并发模型 | 原生多线程 + JUC | GIL 限制,多进程/协程 |
| 语法 | 严谨、冗长 | 简洁、灵活 |
| 内存 | JVM 自动管理 | 引用计数 + GC |
| 典型场景 | 企业级后端、大数据、Android | AI/ML、脚本、爬虫、Web快速开发 |
第31题:int和long类型各占多少位、多少字节?各自的取值范围是多少?
| 类型 | 字节 | 位数 | 范围 |
|---|---|---|---|
int |
4 字节 | 32 位 | -2,147,483,648 ~ 2,147,483,647(约 ±21 亿) |
long |
8 字节 | 64 位 | -9,223,372,036,854,775,808 ~ ...807(约 ±900 亿亿) |
java
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
long maxL = Long.MAX_VALUE; // 9223372036854775807
第32题:long类型和int类型之间可以互相转换吗?转换时需要注意什么?
可以相互转换,但需要注意数据溢出或精度丢失。
java
// int → long:自动转换(安全)
int i = 100;
long l = i; // 安全,自动提升
// long → int:必须强制转换(可能溢出)
long big = 3000000000L; // 超出 int 范围
int j = (int) big; // 溢出!值会变成负数或其他错误值
System.out.println(j); // 输出一个你意想不到的值
// 安全做法:先检查范围
if (big >= Integer.MIN_VALUE && big <= Integer.MAX_VALUE) {
int safe = (int) big;
}
第33题:Java中的数据类型转换分为哪几种方式?各自可能引发什么问题?
| 转换类型 | 方向 | 是否自动 | 风险 |
|---|---|---|---|
| 自动类型提升 | 小范围 → 大范围 | ✅ 自动 | 无风险 |
| 强制类型转换 | 大范围 → 小范围 | ❌ 需手动 | 数据溢出、精度丢失 |
| 表达式类型提升 | 不同类型混合运算 | ✅ 自动提升为最大类型 | 浮点数精度问题 |
java
// 自动提升
byte b = 10;
int i = b; // byte → int 自动
// 强制转换:溢出
int big = 300;
byte small = (byte) big; // 300 % 256 = 44
// 表达式提升
int a = 5;
double d = 2.0;
double result = a / d; // int 提升为 double
// 浮点转整数:截断
int x = (int) 3.99; // x = 3,直接截断,不是四舍五入!
第34题:Java中为什么既要有int基本类型,又要有Integer包装类?二者如何对比?
| 对比维度 | int | Integer |
|---|---|---|
| 类型 | 基本类型 | 引用类型(对象) |
| 内存占用 | 4 字节 | 16+ 字节(对象头 + 值) |
| 默认值 | 0 | null |
| 性能 | 快 | 慢(有对象开销) |
| 能否用于泛型 | ❌ | ✅ |
| 能否为 null | ❌ | ✅(表示"无值") |
| 提供方法 | ❌ | ✅(parseInt、valueOf 等) |
java
// Integer 的必要场景
// 1. 泛型
List<Integer> list = new ArrayList<>(); // 不能 List<int>
// 2. 表示"无值"
Integer score = null; // 数据库可能为 NULL
// 3. 工具方法
int val = Integer.parseInt("123");
String hex = Integer.toHexString(255); // "ff"
第35题:既然有了Integer包装类,为什么还要保留int基本类型?
核心原因:性能。
java
// 包装类开销对比
int a = 100; // 4 字节
Integer b = 100; // 16+ 字节(对象头 8-12 + int值 4 + 对齐填充)
// 性能差异
long start = System.currentTimeMillis();
Long sum = 0L; // 包装类
for (int i = 0; i < 100_000_000; i++) {
sum += i; // 每次循环拆箱→加法→装箱,大量对象创建!
}
// 耗时:几秒(且产生大量 GC)
long sum2 = 0L; // 基本类型
for (int i = 0; i < 100_000_000; i++) {
sum2 += i; // 纯 CPU 运算
}
// 耗时:十几毫秒
| 保留原因 | 说明 |
|---|---|
| 性能 | 无对象头开销,直接 CPU 运算 |
| 内存 | 占用极小(4 字节 vs 16+ 字节) |
| 简单 | 算术运算符直接使用 |
| 兼容性 | 与 C/C++ 底层交互需要 |
第36题:Java多态体现在哪几个方面?请分别举例说明。
多态体现在三个方面:
| 多态形式 | 发生时机 | 核心 |
|---|---|---|
| 继承多态 | 运行时 | 父类引用指向子类对象 |
| 接口多态 | 运行时 | 接口引用指向实现类对象 |
| 方法重载 | 编译时 | 同一个方法名,不同参数列表 |
java
// 1. 继承多态
Animal a = new Dog();
a.makeSound(); // 调用 Dog 的 makeSound()
a = new Cat();
a.makeSound(); // 调用 Cat 的 makeSound()
// 2. 接口多态
List<String> list = new ArrayList<>(); // 接口引用 → ArrayList
list = new LinkedList<>(); // 随时切换实现
// 3. 方法重载
public void print(int x) { /* ... */ }
public void print(String x) { /* ... */ }
第37题:多态机制解决了编程中的什么问题?
| 解决问题 | 说明 | 例子 |
|---|---|---|
| 可扩展性 | 新增子类无需修改调用方代码 | 新增 Cat 类,Animal 引用代码不变 |
| 可替换性 | 运行时灵活替换实现 | List list = new ArrayList() → new LinkedList() |
| 解耦 | 依赖抽象不依赖具体实现 | Service 依赖接口而不是具体实现 |
java
// 没有多态:每新增一个动物类型都要改代码
if (type == "dog") dogSound();
else if (type == "cat") catSound();
else if (type == "bird") birdSound(); // 无限膨胀!
// 有多态:新增动物只需新建类,调用代码不用改
Animal animal = zoo.getAnimal(); // 返回什么动物不需要关心
animal.makeSound(); // 自动调用对应的方法
【核心思想------开闭原则】:对扩展开放,对修改关闭。
第38题:面向对象的六大设计原则(SOLID + 迪米特法则)分别是什么?
| 缩写 | 原则 | 核心 |
|---|---|---|
| S | 单一职责 (SRP) | 一个类只做一件事 |
| O | 开闭原则 (OCP) | 对扩展开放,对修改关闭 |
| L | 里氏替换 (LSP) | 子类可以完全替换父类 |
| I | 接口隔离 (ISP) | 接口应小而专一 |
| D | 依赖倒置 (DIP) | 依赖抽象,不依赖具体实现 |
| LoD | 迪米特法则 | 最少知识原则,只和直接朋友通信 |
java
// 单一职责
class OrderService { void createOrder() {} } // 只管订单
class OrderPrinter { void printOrder() {} } // 只管打印
// 开闭原则
interface Shape { double area(); }
class Circle implements Shape { /* ... */ }
// 新增 Rectangle 不需要修改现有 Shape 接口和使用方
// 依赖倒置
class Controller {
private Service service; // 依赖接口
// 不是 private ServiceImpl service; // ❌ 依赖具体实现
}
第39题:抽象类和普通类有什么区别?什么时候应该使用抽象类?
| 维度 | 普通类 | 抽象类 |
|---|---|---|
| 能否实例化 | ✅ new |
❌ 不能 |
| 抽象方法 | ❌ 不能有 | ✅ 可以有 |
| 目的 | 可直接使用 | 提供模板,让子类补充细节 |
| 设计意图 | 具体实现 | 抽象模板 |
使用抽象类的时机:
- 多个类有共同的行为和状态,但某些方法无法给出通用实现
- 需要模板方法模式时
- 需要构造方法初始化公共状态
java
// 抽象类充当模板
public abstract class PaymentProcessor {
// 公共状态
protected String merchantId;
public PaymentProcessor(String merchantId) {
this.merchantId = merchantId;
}
// 模板方法
public final void process() {
validate();
doPay(); // 交给子类实现
sendNotification();
}
private void validate() { /* 公共验证逻辑 */ }
protected abstract void doPay(); // 子类必须实现
private void sendNotification() { /* 公共通知逻辑 */ }
}
第40题:抽象类可以用final修饰吗?为什么?
不能。 abstract 和 final 是互斥的。
abstract的含义:必须被继承才能使用final的含义:不能被继承
java
// public abstract final class A {} ← 编译错误!
// abstract 说"我的方法要子类来实现"
// final 说"你不能有子类"
// 矛盾的!
第41题:从Java 8到Java 9,接口中可以定义哪些类型的方法?
| 方法类型 | 关键字 | 引入版本 | 能否有方法体 | 能否被重写 |
|---|---|---|---|---|
| 抽象方法 | (无) | Java 1.0 | ❌ | ✅ 必须 |
| default 方法 | default |
Java 8 | ✅ | ✅ 可选 |
| static 方法 | static |
Java 8 | ✅ | ❌(不能被重写) |
| private 方法 | private |
Java 9 | ✅ | ❌(接口内部用) |
java
public interface ModernInterface {
// 抽象方法(所有版本都有)
void doSomething();
// Java 8:default 方法
default void log() {
privateHelper(); // 调用 Java 9 私有方法
System.out.println("logging...");
}
// Java 8:static 方法
static ModernInterface create() {
return new ModernInterface() {
public void doSomething() {}
};
}
// Java 9:private 方法
private void privateHelper() {
System.out.println("private helper");
}
}
第42题:抽象类可以直接new实例化吗?为什么?
不能。 抽象类中可能包含未实现的抽象方法,直接 new 出来的对象如果调用这些方法,没有代码可执行。
java
abstract class Animal {
abstract void makeSound(); // 没有方法体
}
// Animal a = new Animal(); ← 编译错误!
// 如果允许:a.makeSound() 执行什么??
// 但可以用匿名内部类实例化(本质是创建了子类)
Animal a = new Animal() { // 创建了 Animal 的匿名子类
@Override
void makeSound() {
System.out.println("汪汪");
}
}; // 这不是"直接"new 抽象类,而是 new 匿名子类
第43题:接口中能否定义构造函数?为什么?
不能。
- 构造函数的作用是初始化实例的状态
- 接口没有实例状态(只有常量),不需要初始化
- 接口是规范/契约,不是具体实现,不需要被实例化
java
public interface MyInterface {
// public MyInterface() {} ← 编译错误!
}
// 接口的"初始化"通过实现类构造函数完成
class MyImpl implements MyInterface {
public MyImpl() {
// 在这里初始化具体状态
}
}
第44题:静态变量和静态方法在内存中是如何存储的?它们的加载时机是什么?
| 项目 | 存储位置 | 加载时机 | 生命周期 |
|---|---|---|---|
| 静态变量 | 方法区/元空间(JDK 8+) | 类加载的准备阶段 赋默认值,初始化阶段赋真实值 | 类卸载时释放 |
| 静态方法 | 方法区/元空间 | 类加载时加载 | 类卸载时释放 |
| 实例变量 | 堆内存 | 对象创建时 | 对象被 GC 回收时 |
内存布局:
┌───────────────┐
│ 元空间 │
│ ├─ 类信息 │
│ ├─ 静态变量 │ ← count, MAX_VALUE 等
│ ├─ 静态方法 │ ← staticMethod()
│ └─ 常量池 │
├───────────────┤
│ 堆内存 │
│ └─ 实例对象 │ ← new 出来的对象
│ └─ 实例变量 │ ← name, age 等
├───────────────┤
│ 栈内存 │
│ └─ 栈帧 │ ← 局部变量
└───────────────┘
第45题:非静态内部类和静态内部类有什么区别?分别如何实例化?
| 维度 | 非静态内部类 | 静态内部类 |
|---|---|---|
| 依赖关系 | 必须有外部类实例才能存在 | 不依赖外部类实例 |
| 访问外部成员 | 可以访问外部类的所有成员 | 只能访问外部类的静态成员 |
| 持有外部引用 | ✅ 持有 Outer.this |
❌ 不持有 |
| 内存泄漏风险 | ⚠️ 高(持有外部引用) | ✅ 低 |
| static 成员 | ❌ 不能定义 static 成员 | ✅ 可以 |
java
public class Outer {
private String name = "Outer";
private static int count = 0;
// 非静态内部类
class Inner {
void print() {
System.out.println(name); // ✅ 直接访问外部非静态成员
System.out.println(count); // ✅ 访问外部静态成员
}
}
// 静态内部类
static class StaticInner {
void print() {
// System.out.println(name); // ❌ 不能访问外部非静态成员
System.out.println(count); // ✅ 只能访问静态成员
}
}
}
// 实例化区别
Outer outer = new Outer();
Inner inner = outer.new Inner(); // 先有外部,再有内部
StaticInner staticInner = new Outer.StaticInner(); // 独立实例化
第46题:非静态内部类为什么能直接访问外部类的成员?编译器背后做了什么?
编译器自动为非静态内部类添加了一个指向外部类的引用:Outer this$0。
java
// 你写的代码:
public class Outer {
private String name = "hello";
class Inner {
void print() {
System.out.println(name); // 直接访问
}
}
}
// 编译后等效代码:
class Outer {
private String name;
}
class Outer$Inner {
final Outer this$0; // ← 编译器自动加的!
Outer$Inner(Outer outer) {
this.this$0 = outer;
}
void print() {
System.out.println(this$0.name);
}
}
印证方式 :
javap -verbose Outer$Inner.class可以看到final Outer this$0字段
第47题:实现对象深拷贝有哪几种方法?各自优缺点是什么?
(详见第15题------深拷贝与浅拷贝)补充三种方式的对比:
| 方式 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| Cloneable 接口 | 重写 clone() 递归拷贝 |
JDK 原生,不需第三方 | 侵入性强,多层嵌套麻烦 |
| 序列化 | ObjectOutputStream/ObjectInputStream | 通用,自动处理嵌套 | 所有类需 Serializable,性能差 |
| JSON 序列化 | Jackson / Gson / Fastjson | 简单,非侵入 | 性能一般,特殊类型可能丢失 |
| 手动拷贝 | 构造方法 / Builder / 工厂 | 最可控,最清晰 | 代码量大 |
java
// 推荐:构造方法方式------最可控
public class User {
private String name;
private Address address;
// 深拷贝构造方法
public User(User source) {
this.name = source.name;
this.address = new Address(source.address); // 递归拷贝
}
}
第48题:Java中创建对象有哪几种方式?除了new关键字还有哪些途径?
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| new 关键字 | new User() |
最常用 |
| 反射 | Class.forName().newInstance() 或 Constructor.newInstance() |
框架开发 |
| clone() | obj.clone() |
按原型复制 |
| 反序列化 | ObjectInputStream.readObject() |
从持久化数据恢复 |
| Unsafe | Unsafe.allocateInstance() |
底层框架 |
java
// 1. new
User u1 = new User();
// 2. 反射
Class<?> clazz = Class.forName("com.example.User");
User u2 = (User) clazz.getDeclaredConstructor().newInstance();
// 3. clone(需实现 Cloneable)
User u3 = u1.clone();
// 4. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.obj"));
User u4 = (User) ois.readObject();
// 5. Unsafe(不调用构造方法)
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
User u5 = (User) unsafe.allocateInstance(User.class); // 不调构造器!
第49题:通过new创建的对象什么时候会被垃圾回收?判断依据是什么?
判断依据:可达性分析算法(GC Roots)。 当一个对象从 GC Roots 出发不可达时,会被标记为垃圾并回收。
GC Roots 包括:
├── 虚拟机栈中引用的对象
├── 方法区中静态变量引用的对象
├── 方法区中常量引用的对象
├── 本地方法栈中 JNI 引用的对象
└── 活跃线程对象
可达性分析:
GC Roots
│
├──▶ objA ← 可达,不回收
│ └──▶ objB ← 可达,不回收
│
└── objC ← 从 GC Roots 不可达 → 可以回收!
java
// 对象不可达的典型场景
public void method() {
User user = new User(); // user 是 GC Root(栈帧中的引用)
user = null; // user 不再指向对象 → 不可达 → 可回收
} // 方法结束,栈帧销毁,user 引用消失 → 不可达 → 可回收
第50题:如何访问一个类的私有字段或私有方法?有哪些途径?
| 途径 | 原理 | 示例 |
|---|---|---|
| 反射 | setAccessible(true) 关闭访问检查 |
框架开发 |
| getter/setter | 私有字段的公开访问方法 | 日常开发 |
| 内部类 | 编译器桥接方法 | 语言特性 |
java
public class User {
private String secret = "密码123";
private void secretMethod() {
System.out.println("私有方法被调用");
}
// 途径1:getter(推荐)
public String getSecret() { return secret; }
}
// 途径2:反射(慎用)
User user = new User();
Field field = User.class.getDeclaredField("secret");
field.setAccessible(true); // 突破私有限制
String value = (String) field.get(user);
Method method = User.class.getDeclaredMethod("secretMethod");
method.setAccessible(true);
method.invoke(user);
⚠️ 注意 :反射破坏封装性,JDK 17+ 默认有模块化限制,需要
--add-opens参数
第51题:Java注解的底层实现原理是什么?
注解的本质是接口 ,运行时通过动态代理生成代理实例。
1. 定义注解
@Retention(RUNTIME)
@interface MyAnnotation { String value(); }
2. 编译后生成: interface MyAnnotation extends java.lang.annotation.Annotation { }
3. 运行时获取:动态代理生成 Proxy 类
调用 getAnnotation(MyAnnotation.class) →
JDK 动态代理 →
返回 $Proxy 实例(实现 MyAnnotation)→
调用代理方法 → AnnotationInvocationHandler.invoke() →
返回注解属性值
java
// 运行时获取注解
MyAnnotation ann = clazz.getAnnotation(MyAnnotation.class);
// ann 的实际类型是:com.sun.proxy.$Proxy1(动态代理对象)
// 调用 ann.value() 时,实际执行的是
// AnnotationInvocationHandler.invoke(proxy, method, args)
第52题:@Retention注解的三种保留策略分别代表什么含义?
| 策略 | 含义 | 保留到何时 | 典型注解 |
|---|---|---|---|
RetentionPolicy.SOURCE |
源码级别,编译时丢弃 | 仅 .java 文件 |
@Override, @SuppressWarnings |
RetentionPolicy.CLASS |
类文件级别,JVM 不加载 | .class 文件(默认值) |
@NonNull(Lombok) |
RetentionPolicy.RUNTIME |
运行时级别,可通过反射读取 | JVM 内存 | @Autowired, @RequestMapping |
生命周期:
.java 源码 → javac 编译 → .class 字节码 → 类加载 → JVM 运行时
SOURCE ──✕── CLASS ──────✕─── RUNTIME ──▶ 可用反射读
第53题:@Target注解可以指定哪些作用域(ElementType)?常用的有哪些?
| ElementType | 作用域 | 常用场景 |
|---|---|---|
TYPE |
类、接口、枚举 | @Component, @Entity |
FIELD |
字段 | @Autowired, @Value |
METHOD |
方法 | @GetMapping, @Transactional |
PARAMETER |
方法参数 | @RequestParam, @PathVariable |
CONSTRUCTOR |
构造方法 | @Autowired |
LOCAL_VARIABLE |
局部变量 | 较少使用 |
ANNOTATION_TYPE |
注解类型 | 定义元注解时 |
PACKAGE |
包 | 包级别注解 |
TYPE_PARAMETER |
类型参数 | <@NonNull T> |
TYPE_USE |
任何类型使用处 | 泛型、异常等 |
第54题:Java中异常处理有哪些方式?try-with-resources语法有什么好处?
| 方式 | 语法 | 说明 |
|---|---|---|
| try-catch-finally | try {} catch {} finally {} |
传统方式 |
| try-with-resources | try (Resource r = ...) {} |
自动关闭资源(Java 7+) |
| throws | 方法签名声明 | 往上抛给调用者 |
| 多层 catch | `catch (A | B e)` |
java
// 传统方式:繁琐且容易遗漏关闭
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 读文件
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException e) { }
}
}
// try-with-resources:简洁且自动关闭
// 要求资源类实现 AutoCloseable 接口
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
// fis 和 br 自动关闭,且关闭顺序与打开顺序相反
好处:
├── 代码更简洁,减少样板代码
├── 自动关闭资源,不会遗漏
├── 关闭顺序:后打开的先关闭
└── 异常压制机制:try 块异常优先,close 异常作为 suppressed 附加
第55题:为什么有些方法抛出异常时不需要显式使用throws声明?
因为 RuntimeException 及其子类是「非受检异常」,编译期不检查。
java
// 不需要 throws(非受检异常)
public int divide(int a, int b) {
return a / b; // 可能 ArithmeticException,但不需声明
}
public String getFirst(List<String> list) {
return list.get(0); // 可能 IndexOutOfBoundsException,不需声明
}
// 必须 throws(受检异常)
public void readFile(String path) throws IOException {
new FileReader(path); // FileReader 声明了 throws IOException
}
| 异常类型 | 需要 throws? | 原因 |
|---|---|---|
| RuntimeException / Error | ❌ | 非受检,编译期不强制 |
| 其他 Exception | ✅ | 受检异常,编译期强制处理 |
第56题:如果try块中有return "a",finally块中也有return "b",最终返回什么?为什么?
返回 "b"。finally 中的 return 会覆盖 try 中的 return。
java
public static String test() {
try {
return "a";
} finally {
return "b";
}
}
// 输出:b
java
// 更微妙的例子
public static int test() {
int x = 1;
try {
return x; // 先把 x 的值(1)缓存在返回地址
} finally {
x = 2; // 改了 x,但返回的是缓存的值
}
}
// 输出:1 ← 不是 2!
// 但如果返回的是引用类型:
public static User test() {
User u = new User("Tom");
try {
return u;
} finally {
u.setName("Jerry"); // 改了对象内容
}
}
// 返回的 User 的 name 是 "Jerry"!
// 原因是:return 缓存的是引用值,指向对象没变
| 情况 | 结果 | 说明 |
|---|---|---|
finally 有 return |
返回 finally 的值 | 覆盖 try 的 return |
try return 基本类型 |
返回 try 瞬时的值 | finally 修改无效 |
try return 引用类型 |
返回引用(对象可能被 finally 修改) | 对象内容可变 |
| finally 抛异常 | 异常传播,try 的 return 被丢弃 | 异常覆盖返回 |
⚠️ 避坑指南:永远不要在 finally 中写 return 或抛异常!
第57题:Object类中有哪些常用方法?各自的作用是什么?
| 方法 | 作用 |
|---|---|
getClass() |
获取对象的 Class 对象 |
hashCode() |
返回对象的哈希码(用于 HashMap/HashSet) |
equals(Object) |
判断两个对象是否「相等」(默认比较地址) |
clone() |
创建并返回对象的拷贝(需实现 Cloneable,浅拷贝) |
toString() |
返回对象的字符串表示(默认:类名@十六进制哈希) |
notify() |
唤醒一个在此对象监视器上等待的线程 |
notifyAll() |
唤醒所有在此对象监视器上等待的线程 |
wait() / wait(long) / wait(long, int) |
使当前线程等待,直到被唤醒 |
finalize() |
对象被 GC 回收前调用(已废弃 JDK 9,JDK 18 彻底移除) |
java
public class User {
@Override
public String toString() {
return "User{name='" + name + "'}";
}
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
protected Object clone() throws CloneNotSupportedException { /* ... */ }
}
第58题:String类有哪些常用的方法?请列举并说明用途。
| 方法 | 用途 | 示例 |
|---|---|---|
length() |
字符串长度 | "abc".length() → 3 |
charAt(int) |
获取指定位置字符 | "abc".charAt(0) → 'a' |
substring(int, int) |
截取子串 | "hello".substring(0,2) → "he" |
contains(CharSequence) |
是否包含 | "abc".contains("b") → true |
indexOf(String) |
第一次出现位置 | "abca".indexOf("a") → 0 |
startsWith/endsWith |
前/后缀判断 | "abc".startsWith("a") → true |
replace/ replaceAll |
替换 | "a1b2".replaceAll("\\d","") → "ab" |
split(String) |
分割 | "a,b".split(",") → ["a","b"] |
trim() |
去除首尾空白 | " a ".trim() → "a" |
toUpperCase/toLowerCase |
大小写转换 | "Abc".toUpperCase() → "ABC" |
equals/equalsIgnoreCase |
比较 | "a".equals("a") → true |
intern() |
放入字符串常量池 | s.intern() |
format(String, Object...) |
格式化 | String.format("Hi %s", "Tom") |
join(CharSeq, CharSeq...) |
拼接 | String.join("-", "a","b") → "a-b" |
isBlank() |
是否空白(Java 11+) | " ".isBlank() → true |
isEmpty() |
是否空串 | "".isEmpty() → true |
第59题:Lambda表达式的基本语法是什么?它解决了什么问题?
Lambda 本质上是「可传递的匿名函数」,用于简化函数式接口的匿名内部类写法。
语法 :(参数列表) -> { 函数体 } 或 参数 -> 表达式
| 形式 | 示例 |
|---|---|
| 无参 | () -> System.out.println("hello") |
| 单参 | x -> x * 2 |
| 多参 | (a, b) -> a + b |
| 多行 | (a, b) -> { int c = a + b; return c; } |
java
// 以前:匿名内部类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
};
// Lambda:简洁明了
Runnable r2 = () -> System.out.println("hello");
// 以前:7 行
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// Lambda:1 行
list.sort((a, b) -> a.length() - b.length());
// 方法引用:更简洁
list.sort(Comparator.comparingInt(String::length));
解决的问题:
- 消灭样板代码(匿名内部类的冗长语法)
- 支持函数式编程风格
- 便于并行处理(配合 Stream)
第60题:Stream API有哪些常用操作?中间操作和终端操作有什么区别?
| 类型 | 特点 | 常见操作 |
|---|---|---|
| 中间操作 | 返回 Stream,惰性执行,可链式调用 | filter、map、flatMap、peek、distinct、sorted、limit、skip |
| 终端操作 | 触发计算,消费 Stream,返回结果 | collect、forEach、reduce、count、anyMatch、findFirst、max、min |
java
List<String> names = Arrays.asList("张三", "李四", "王五", "赵六", "张伟");
List<String> result = names.stream()
.filter(n -> n.startsWith("张")) // 中间:过滤
.map(String::toUpperCase) // 中间:转换
.sorted() // 中间:排序
.limit(2) // 中间:限制数量
.collect(Collectors.toList()); // 终端:收集 → ["张三", "张伟"]
// 常用终端操作
boolean any = names.stream().anyMatch(n -> n.length() > 2); // true
long count = names.stream().count(); // 5
Optional<String> first = names.stream().findFirst(); // 张三
// 常用收集器
Map<Integer, List<String>> byLen = names.stream()
.collect(Collectors.groupingBy(String::length));
String joined = names.stream()
.collect(Collectors.joining(", ")); // "张三, 李四, ..."
第61题:ParallelStream是什么?适用于什么场景?使用时需要注意哪些问题?
ParallelStream 是并行流,使用 ForkJoinPool 将任务拆分到多核 CPU 并行执行。
java
// 顺序流
list.stream().filter(...).collect(...); // 单线程
// 并行流
list.parallelStream().filter(...).collect(...); // ForkJoinPool
| 维度 | 说明 |
|---|---|
| ✅ 适用 | 大数据量、独立计算、CPU 密集型(无 IO 等待) |
| ❌ 不适用 | 小数据量、有状态操作、IO 密集型、结果依赖顺序 |
注意问题:
java
// ⚠️ 问题1:线程安全问题
List<Integer> list = new ArrayList<>();
IntStream.range(0, 1000)
.parallel()
.forEach(list::add); // ❌ ArrayList 线程不安全!结果不可预测
// ✅ 解决:用线程安全集合或 collect
List<Integer> safe = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList()); // ✅ 安全
// ⚠️ 问题2:不要用 parallelStream 做 IO 操作
// parallelStream 使用公共 ForkJoinPool,IO 阻塞会影响其他并行任务
第62题:CompletableFuture是什么?它解决了传统Future的哪些痛点?
CompletableFuture 是 Java 8 引入的异步编程工具,实现了 Future + CompletionStage 接口。
| 痛点 | 传统 Future | CompletableFuture |
|---|---|---|
| 结果获取 | get() 阻塞等待 |
thenAccept() 回调,不阻塞 |
| 链式处理 | ❌ 不支持 | ✅ thenApply/thenCompose |
| 组合多任务 | ❌ 需手动等待 | ✅ allOf/anyOf |
| 异常处理 | 仅在 get() 时暴露 |
✅ exceptionally/handle |
| 手动完成 | ❌ | ✅ complete() |
java
// 传统 Future:阻塞等待
Future<String> future = executorService.submit(() -> {
Thread.sleep(1000);
return "结果";
});
String result = future.get(); // 阻塞 1 秒!
// CompletableFuture:异步编排
CompletableFuture.supplyAsync(() -> fetchUser(1L))
.thenApply(user -> user.getName()) // 转换
.thenAccept(name -> System.out.println(name)) // 消费
.exceptionally(e -> { // 异常处理
e.printStackTrace();
return null;
});
// 多任务组合
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
String r1 = f1.join(); // 不抛受检异常
String r2 = f2.join();
System.out.println(r1 + " " + r2);
});
第63题:Java 21引入了哪些重要的新特性?虚拟线程(Virtual Thread)的原理是什么?
【主要新特性】
| 特性 | 说明 |
|---|---|
| 虚拟线程 (Virtual Thread) | 轻量级线程,几百万个也不卡 |
| 模式匹配 (Record Pattern) | 简化 instanceof + 解构 |
| Switch 模式匹配 | switch 支持类型+条件匹配 |
| 字符串模板 | STR."Hello \{name}" |
| 序列化集合 | SequencedCollection(可逆序操作) |
【虚拟线程核心原理】
传统平台线程: 虚拟线程:
Java Thread Virtual Thread
│ │
▼ ▼
OS 线程 (重量级) Carrier Thread (平台线程)
创建开销大,切换慢 │
每个约占用 1MB 栈 ┌────┼────┐
▼ ▼ ▼
VT1 VT2 VT3 ... (轻量级)
无数虚拟线程共享少量平台线程
| 对比 | 平台线程 | 虚拟线程 |
|---|---|---|
| 成本 | 高(~1MB 栈 + OS 资源) | 极低(按需分配) |
| 数量 | 几千个 | 数百万个 |
| 阻塞 | 阻塞 OS 线程 | 阻塞时自动让出 Carrier |
| 创建 | new Thread() |
Thread.startVirtualThread() |
java
// 虚拟线程使用
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("虚拟线程运行中");
});
// 用 ExecutorService 管理虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 阻塞不占 OS 线程!
return "done";
});
}
}
第64题:在进行Java对象序列化时,有哪些推荐的最佳实践?
| 实践 | 说明 |
|---|---|
| 显式声明 serialVersionUID | 避免反序列化时版本不匹配 |
| transient 标记敏感字段 | 密码等敏感数据不参与序列化 |
| 使用 JSON/Protobuf 替代 | 跨语言、更安全、性能更好 |
| 谨慎序列化复杂对象 | 注意内部对象的序列化 |
| 考虑反序列化安全性 | 防止反序列化漏洞攻击 |
java
public class User implements Serializable {
// 1. 必须声明 serialVersionUID(用 IDE 生成)
private static final long serialVersionUID = 1L;
private String name;
// 2. 敏感字段用 transient
private transient String password;
// 3. 静态变量不会被序列化
private static int count;
// 4. 自定义序列化逻辑
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先写非 transient
oos.writeObject(encrypt(password)); // 加密后写入
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.password = decrypt((String) ois.readObject());
}
}
💡 更推荐 :使用 JSON(Jackson/Gson)或 Protobuf 做序列化,不依赖 Java 原生序列化
第65题:Java原生序列化如何实现?serialVersionUID和transient关键字的作用是什么?
java
// 实现 Serializable 接口即可
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // 不参与序列化
}
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(user);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
User user = (User) ois.readObject();
| 关键字 | 作用 | 典型场景 |
|---|---|---|
serialVersionUID |
版本控制,反序列化时校验类是否匹配 | 所有 Serializable 类 |
transient |
标记字段不参与序列化 | 密码、连接、缓存等 |
| serialVersionUID 场景 | 结果 |
|---|---|
| 不声明 | 编译器自动生成(类变化后值不同 → 反序列化失败) |
| 显式声明固定值 | 类有小改动仍可反序列化(需你自己保证兼容) |
第66题:代理模式和适配器模式有什么区别?各自的适用场景是什么?
| 对比维度 | 代理模式 (Proxy) | 适配器模式 (Adapter) |
|---|---|---|
| 目的 | 控制对原对象的访问 | 转换接口以适配客户端 |
| 接口 | 代理与被代理实现相同接口 | 适配器改变接口 |
| 关注点 | 访问控制、增强功能 | 兼容性、接口转换 |
| 关系 | 代理持有被代理对象 | 适配器包装被适配对象 |
java
// ===== 代理模式 =====
// 接口相同,控制访问
interface UserService {
void save();
}
class UserServiceImpl implements UserService {
public void save() { /* 实际业务 */ }
}
class UserServiceProxy implements UserService { // 实现同一接口
private UserService target;
public void save() {
System.out.println("开始事务"); // 增强
target.save();
System.out.println("提交事务"); // 增强
}
}
// ===== 适配器模式 =====
// 接口不同,转换适配
interface TypeC { void chargeTypeC(); } // 新标准
class LightningPhone { void chargeLightning() {} } // 旧接口
class LightningAdapter implements TypeC { // 适配器
private LightningPhone phone;
public void chargeTypeC() {
phone.chargeLightning(); // 转换为旧接口调用
}
}
选择策略:
├── 需要新增功能/控制访问,接口不变 → 代理
└── 需要兼容不同接口 → 适配器
第67题:责任链模式和策略模式有什么区别?分别适用于什么业务场景?
| 对比维度 | 责任链模式 (Chain of Responsibility) | 策略模式 (Strategy) |
|---|---|---|
| 目的 | 多个处理器依次尝试处理请求 | 从多个算法中选择一个执行 |
| 关系 | 处理器之间形成链条 | 策略之间互斥独立 |
| 结果 | 可能被多个处理器处理(或截断) | 只有一个策略被执行 |
java
// ===== 责任链:审批流程 =====
abstract class Approver {
protected Approver next;
public void setNext(Approver next) { this.next = next; }
public abstract void approve(int amount);
}
class Manager extends Approver {
public void approve(int amount) {
if (amount <= 1000) System.out.println("经理审批");
else if (next != null) next.approve(amount);
}
}
// ===== 策略:支付方式 =====
interface PayStrategy {
void pay(int amount);
}
class AliPay implements PayStrategy {
public void pay(int amount) { System.out.println("支付宝支付" + amount); }
}
class WechatPay implements PayStrategy {
public void pay(int amount) { System.out.println("微信支付" + amount); }
}
class PaymentContext {
private PayStrategy strategy;
public void setStrategy(PayStrategy s) { strategy = s; }
public void pay(int amount) { strategy.pay(amount); }
}
| 场景 | 推荐模式 | 例子 |
|---|---|---|
| 审批流、拦截器链、过滤器链 | 责任链 | Spring Interceptor、Servlet Filter |
| 支付方式、排序算法、折扣规则 | 策略 | 多种支付、多种排序 |
第68题:NIO的三大核心组件Channel、Buffer、Selector分别起什么作用?
(详见第24题------NIO 实现高并发),在此做对比总结:
| 组件 | 比喻 | 核心职责 |
|---|---|---|
| Channel | 铁路/水管 | 数据的双向传输通道,连接文件或网络 |
| Buffer | 货车/水桶 | 数据的临时存储容器,所有 I/O 数据必经 Buffer |
| Selector | 调度中心 | 单线程监控多个 Channel,仅处理就绪的 |
Channel:
├── FileChannel 文件读写
├── SocketChannel 客户端 TCP
├── ServerSocketChannel 服务端 TCP
└── DatagramChannel UDP
Buffer:
├── ByteBuffer 最常用,字节
├── CharBuffer、IntBuffer、...
└── MappedByteBuffer 内存映射
第69题:哪些主流框架底层使用了Java NIO技术?
| 框架 | 使用方式 | 说明 |
|---|---|---|
| Netty | 封装 NIO | 最流行的网络框架,Dubbo/RocketMQ/Elasticsearch 底层 |
| Tomcat 8+ | NIO Connector(默认) | 高并发 HTTP 处理 |
| Jetty | NIO | 嵌入式容器 |
| Undertow | NIO (XNIO) | WildFly 默认,Spring Boot 可替代 Tomcat |
| ZooKeeper | NIO | 分布式协调服务 |
| Kafka | NIO + 零拷贝 | 高吞吐消息队列 |
💡 Netty 几乎统治了 Java 网络编程:Dubbo、RocketMQ、Elasticsearch、gRPC 都基于 Netty
第70题:如何用Comparable接口对学生列表按分数降序、学号升序排序?
java
public class Student implements Comparable<Student> {
private int id; // 学号
private int score; // 分数
@Override
public int compareTo(Student other) {
// 分数降序
if (this.score != other.score) {
return Integer.compare(other.score, this.score);
// 注意:other 在前 = 降序
}
// 分数相同,学号升序
return Integer.compare(this.id, other.id);
}
}
// 使用
List<Student> students = new ArrayList<>();
Collections.sort(students); // 直接排序
// ===== 或用 Comparator(不侵入类) =====
students.sort(Comparator
.comparingInt(Student::getScore).reversed() // 分数降序
.thenComparingInt(Student::getId)); // 学号升序
第71题:Native方法是什么?Java中为什么要使用Native方法?
native 方法是用其他语言(C/C++/汇编)实现的,通过 JNI(Java Native Interface)调用。
java
// native 方法声明
public class Thread {
private native void start0(); // 用 C/C++ 实现,只有声明没有方法体
}
// Object 中的 native 方法
public class Object {
public native int hashCode();
protected native Object clone() throws CloneNotSupportedException;
public final native void notify();
public final native void wait(long timeout) throws InterruptedException;
}
| 使用原因 | 说明 |
|---|---|
| 与 OS 交互 | 线程创建、文件 IO 等必须调用 OS 系统调用 |
| 性能关键代码 | 数学运算、加密算法等 |
| 复用 C/C++ 库 | 已有成熟库不必用 Java 重写 |
| 操作硬件 | 直接访问内存、寄存器等 |
Java ──JNI──▶ C/C++ .so/.dll ──系统调用──▶ OS Kernel
第72题:Java进程如何与操作系统进行交互?从用户态到内核态的调用链路是怎样的?
完整的调用链路:
┌─────────────────────────────────────────────┐
│ 用户态 (User Space) │
│ │
│ Java 代码 │
│ │ new Thread().start() │
│ ▼ │
│ JVM (C++ 实现) │
│ │ 调用 native start0() │
│ ▼ │
│ JNI 层 (C 代码) │
│ │ jvm.dll / libjvm.so │
│ ▼ │
│ libc / glibc │
│ │ pthread_create() / clone() │
│ ▼ │
│ ═══════════════════════════════════════ │
│ 系统调用 (syscall) │
│ ═══════════════════════════════════════ │
│ ▼ │
└─────────────────────────────────────────────┘
│ clone 系统调用
▼
┌─────────────────────────────────────────────┐
│ 内核态 (Kernel Space) │
│ │
│ 中断/陷阱门 │
│ ▼ │
│ 系统调用处理程序 │
│ ▼ │
│ do_fork() → 创建内核线程 │
│ ▼ │
│ 调度器 分配 CPU 时间片 │
│ ▼ │
│ 返回用户态 │
└─────────────────────────────────────────────┘
关键步骤:
| 步骤 | 发生了什么 |
|---|---|
① Java 调用 new Thread().start() |
调用到 JVM 的 Thread 类 |
② JVM 调用 native start0() |
进入 JNI 层 C/C++ 代码 |
③ C 代码调用 pthread_create() 或 clone() |
准备系统调用 |
| ④ 用户态 → 内核态切换 | 触发软中断,CPU 切换到内核模式 |
| ⑤ 内核创建线程、分配资源 | 进程控制块(PCB)、栈、调度 |
| ⑥ 内核态 → 用户态切换 | 返回用户态,线程进入就绪队列 |
java
// 你写的代码:
new Thread(() -> System.out.println("Hello")).start();
// 底层发生了什么:
// Java Thread.start()
// → native start0() [JVM C++ 代码]
// → JVM_StartThread() [HotSpot 源码]
// → os::create_thread() [OS 相关实现]
// → pthread_create() [Linux glibc]
// → clone() 系统调用 [内核]
📝 总结
以上是对「一、Java 基础」全部 72 道题的详细解答,涵盖:
| 段落 | 题号 | 核心主题 |
|---|---|---|
| 高频 1-12 | 第1~12题 | 语言特性、JVM体系、面向对象、抽象类/接口 |
| 高频 13-25 | 第13~25题 | final/static、拷贝、比较、字符串、Java8/反射/异常/DCL、IO/NIO、泛型 |
| 中频 26-47 | 第26~47题 | 语言对比、类型转换、多态/设计原则、抽象类、内部类、深拷贝 |
| 中频 48-72 | 第48~72题 | 创建对象/GC、注解、异常、Stream/Optional、Java21、序列化、设计模式、NIO、Native |