Java 基础
这篇是 Java 基础的背诵版,整合了《Java 基础常见面试题总结(上)(中)(下)》和“重要知识点”里的高频内容。低频、过浅、只需要知道概念的问题已经压缩,不再单独展开。
背诵时按这个顺序走:
- JVM/JDK/字节码:说明 Java 为什么跨平台、为什么“编译与解释并存”。
- 基本类型/包装类/BigDecimal/String:这是基础题里最常追问细节的一组。
- 面向对象/Object/异常:必须能用自己的话讲清楚。
- 泛型/反射/代理/注解/SPI/序列化/IO:偏框架底层,回答要带应用场景。
1. JVM、JDK、JRE、字节码怎么回答?
JVM 是运行字节码的虚拟机,屏蔽操作系统差异,是 Java 跨平台的关键。JRE 是运行环境,包含 JVM 和基础类库。JDK 是开发工具包,包含 JRE、javac、javadoc、jdb、jconsole、javap 等工具。
字节码是 .class 文件中的指令,不直接面向某个 CPU,而是面向 JVM。Java 源码先被 javac 编译成字节码,运行时由 JVM 解释执行;热点代码会被 JIT 编译成机器码缓存起来,所以 Java 常被说成“编译与解释并存”。
面试追问:
- JDK 9 之后通过模块化和
jlink可以裁剪运行时,JDK 11 之后 Oracle 不再单独提供传统 JRE 下载。 - “一次编写,到处运行”的本质不是源码跨平台,而是字节码加不同平台的 JVM 实现。
- AOT 是提前编译,启动快、运行期优化少;JIT 是运行时根据热点代码优化,启动慢一点但长期运行性能更好。
2. Java 和 C++ 的核心区别?
Java 更偏向托管语言,运行在 JVM 上,有自动内存管理、反射、丰富生态和跨平台能力;C++ 直接操作内存和对象生命周期,性能控制更细,但复杂度更高。
常见对比:
- Java 不支持指针直接操作,C++ 支持指针。
- Java 单继承、多实现,C++ 支持多继承。
- Java 有 GC,C++ 需要显式管理对象生命周期。
- Java 方法默认是虚方法效果,C++ 需要
virtual才支持运行时多态。 - Java 没有运算符重载和头文件,C++ 有。
3. 基本类型和包装类型怎么背?
Java 有 8 种基本类型:
| 类型 | 关键字 |
|---|---|
| 整数 | byte、short、int、long |
| 浮点 | float、double |
| 字符 | char |
| 布尔 | boolean |
包装类型是基本类型对应的对象形式,比如 int 对应 Integer。为什么需要包装类?因为集合、泛型、反射、空值表达、对象方法调用都需要对象。
基本类型和包装类型的区别:
- 存储:基本类型存值,包装类型存对象引用。
- 默认值:成员变量里
int默认0,Integer默认null。 - 比较:基本类型用
==比值,包装类型用equals()比值。 - 泛型:泛型只能使用引用类型,所以
List<int>不合法,必须用List<Integer>。
4. 自动装箱、拆箱和 Integer 缓存池
自动装箱是基本类型转包装类,编译后通常调用 valueOf();自动拆箱是包装类转基本类型,编译后调用 xxxValue()。
Integer a = 100; // Integer.valueOf(100)
int b = a; // a.intValue()必须记住两个坑:
- 包装对象为
null时自动拆箱会抛NullPointerException。 Integer.valueOf()默认缓存-128到127,所以这个范围内装箱对象可能==为true,超出范围通常为false。比较包装类型数值应该用equals(),不要依赖==。
5. 浮点数为什么会丢精度?金额为什么用 BigDecimal?
浮点数采用二进制表示,很多十进制小数无法被二进制精确表示,比如 0.1。计算时会出现近似值,所以浮点数不适合做金额计算。
金额计算用 BigDecimal。注意:
- 推荐用字符串构造:
new BigDecimal("0.1")。 - 不推荐用
new BigDecimal(0.1),因为传入时已经是有误差的double。 BigDecimal.equals()会比较值和精度,比如1.0和1.00不相等。- 数值等值比较优先用
compareTo():a.compareTo(b) == 0。
6. 值传递还是引用传递?
Java 只有值传递。
基本类型传的是值的副本;引用类型传的是引用地址的副本。方法里可以通过这个副本修改对象内部状态,但不能让调用方的引用变量改指向另一个对象。
void change(User user) {
user.setName("new"); // 调用方对象会变
user = new User(); // 调用方引用不会改指向
}一句话背诵:Java 传对象时传的是“对象地址的副本”,不是引用本身。
7. 面向对象三大特征怎么讲?
封装:把数据和行为放在对象内部,通过访问控制隐藏实现细节,对外暴露稳定接口。
继承:子类复用父类能力,表达 is-a 关系。Java 类只能单继承,避免多继承带来的菱形继承问题,但接口可以多实现。
多态:同一个父类引用指向不同子类对象,运行时调用实际对象的方法。多态依赖继承、方法重写和向上转型。
面试里不要只背定义,要能补一句价值:封装提高可维护性,继承提高复用性,多态提高扩展性。
8. 重载和重写有什么区别?
重载发生在同一个类中,方法名相同,参数列表不同,和返回值无关,属于编译期多态。
重写发生在父子类之间,子类改写父类方法,方法名、参数列表要相同,返回值类型要兼容,访问权限不能更小,抛出的异常不能更宽,属于运行期多态。
记忆点:
private、static、final方法不能被真正重写。- 构造方法不能被重写。
- 重载看参数,重写看继承和运行时对象。
9. 接口和抽象类怎么选?
共同点:都不能直接实例化,都可以定义抽象行为,都可以被子类实现或继承。
区别:
- 抽象类描述“是什么”,接口描述“能做什么”。
- 类只能继承一个抽象类,但可以实现多个接口。
- 抽象类可以有成员变量、构造方法、普通方法;接口更偏能力约束,字段默认
public static final。 - Java 8 之后接口可以有
default和static方法,Java 9 后还可以有私有方法,但接口仍不适合承载状态。
选择:多个类共享状态和模板逻辑时用抽象类;只定义能力规范、希望多实现时用接口。
10. Object 常见方法和 equals/hashCode 关系
Object 常见方法:
equals():比较对象是否相等,默认比较引用地址。hashCode():返回哈希值,常用于哈希容器定位桶。toString():返回对象字符串表示。getClass():返回运行时类型。clone():对象复制,默认浅拷贝。wait()、notify()、notifyAll():线程协作方法。finalize():对象被回收前可能调用,已经不推荐使用。
equals() 和 hashCode() 契约:
- 两个对象
equals()为true,hashCode()必须相同。 - 两个对象
hashCode()相同,equals()不一定为true。 - 只重写
equals()不重写hashCode(),对象放进HashMap、HashSet这类容器时可能出现查不到、去重失败的问题。
11. == 和 equals() 的区别?
==:
- 比基本类型:比较值。
- 比引用类型:比较地址。
equals():
- 默认来自
Object,本质也是比较地址。 - 很多类会重写它,比如
String、Integer,用于比较内容。
所以回答时要分类型:基本类型用 ==,对象内容比较看是否重写 equals()。
12. 浅拷贝、深拷贝、引用拷贝
引用拷贝:只复制引用,两个引用指向同一个对象。
浅拷贝:复制对象本身,但对象里的引用字段仍指向原来的子对象。
深拷贝:复制对象本身,也递归复制引用字段指向的对象,新旧对象完全独立。
Object.clone() 默认是浅拷贝,因为它只复制当前对象字段值。实现深拷贝可以手写复制逻辑、序列化反序列化、使用复制构造器或映射工具。
13. String 为什么不可变?
String 不可变主要因为:
- 内部字符数据被
final修饰,外部不能直接修改。 - 类本身是
final,不能被继承破坏不可变性。 - 不可变对象线程安全,可被多个线程共享。
- 支持字符串常量池复用。
- 可以缓存
hashCode(),适合作为HashMap的 key。
注意:final 只能保证引用不变,不代表引用对象内容一定不可变。String 不可变还依赖内部不暴露可变数组。
14. String、StringBuilder、StringBuffer 的区别?
String 不可变,每次修改都会产生新对象。适合少量字符串和常量。
StringBuilder 可变,非线程安全,性能高,适合单线程大量拼接。
StringBuffer 可变,方法加了同步,线程安全但性能较低,现在使用场景较少。
字符串拼接:
- 编译期常量拼接会被优化进常量池。
- 循环中大量拼接应该用
StringBuilder。 - 普通一两次
+拼接可读性更好,编译器通常会优化。
15. new String("abc") 创建几个对象?
分情况:
- 如果字符串常量池中没有
"abc",会创建两个对象:常量池里的"abc",堆上的String对象。 - 如果常量池中已经有
"abc",只创建一个堆上的String对象。
String.intern() 会尝试把字符串放入常量池,并返回常量池中的引用。JDK 7 之后字符串常量池移到堆中,intern() 的具体表现和早期版本不同,面试中重点说清“返回常量池引用”即可。
16. 异常体系怎么背?
Java 异常顶层是 Throwable,下面分为 Error 和 Exception。
Error 表示程序一般无法处理的严重问题,比如 OutOfMemoryError、StackOverflowError、NoClassDefFoundError。
Exception 表示程序可以处理的异常:
- Checked Exception:编译期必须处理,比如
IOException、SQLException、ClassNotFoundException。 - Unchecked Exception:运行时异常,不强制处理,比如
NullPointerException、IllegalArgumentException、ClassCastException、ArithmeticException。
ClassNotFoundException 和 NoClassDefFoundError 区别:前者是反射等动态加载时找不到类,是异常;后者是编译时存在、运行时链接不到类,是错误。
17. finally 一定会执行吗?
通常会执行,即使 try 或 catch 中有 return,finally 也会在方法返回前执行。
不会执行的典型情况:
- JVM 退出,比如
System.exit()。 - JVM 崩溃或进程被强杀。
- 当前线程被永久阻塞或死循环,执行不到
finally。
不要在 finally 中写 return,它会覆盖 try 或 catch 中的返回值和异常,导致问题难排查。
18. try-with-resources 有什么用?
try-with-resources 用于自动关闭资源,资源类必须实现 AutoCloseable 或 Closeable。
try (BufferedReader reader = new BufferedReader(new FileReader("a.txt"))) {
return reader.readLine();
}它比手写 finally 关闭资源更安全,能避免忘记关闭、关闭顺序错误和异常覆盖问题。I/O、数据库连接、网络连接都适合用。
19. 泛型和泛型擦除
泛型的作用是把类型检查提前到编译期,提高类型安全,减少强制类型转换。
List<String> list = new ArrayList<>();
String value = list.get(0);Java 泛型是通过类型擦除实现的。编译后大多数泛型信息会被擦除成原始类型,必要时插入强制类型转换。比如 List<String> 和 List<Integer> 在运行时都是 List。
常见追问:
- 为什么不能
new T()?因为运行时不知道T的具体类型。 - 为什么不能创建泛型数组?数组运行时需要知道精确元素类型,而泛型擦除后类型信息不足。
- 上界
<? extends T>适合读,下界<? super T>适合写,可以记 PECS:Producer Extends,Consumer Super。
20. 反射是什么?优缺点和场景?
反射是在运行时获取类的信息并操作类、方法、字段、构造器的机制。
常见能力:
Class.forName()加载类。- 通过构造器创建对象。
- 读取和修改字段。
- 调用方法。
- 解析注解。
优点:动态、灵活,适合框架做通用能力。缺点:性能低于直接调用,破坏封装,编译期类型检查弱,安全限制更多。
典型场景:Spring 依赖注入、MyBatis 结果映射、JUnit 测试、注解扫描、JSON 序列化框架。
优化思路:缓存 Class、Method、Field 对象;减少反复查找;能用方法句柄或字节码增强时避免高频反射。
21. 静态代理、JDK 动态代理、CGLIB 动态代理
静态代理:代理类在编译期就写好,简单直观,但类多、维护成本高。
JDK 动态代理:运行时生成代理类,要求目标类实现接口,核心是 InvocationHandler 和 Proxy.newProxyInstance()。
CGLIB 动态代理:通过继承目标类生成子类代理,不要求接口,但不能代理 final 类和 final 方法。
Spring AOP 常见选择:
- 有接口时默认倾向 JDK 动态代理。
- 没有接口时使用 CGLIB。
动态代理的价值:在不改业务代码的情况下增强方法,比如事务、日志、权限、监控、缓存。
22. 注解是什么?如何生效?
注解是给代码添加元数据的机制,本身不直接改变业务逻辑,必须被编译器、运行时反射或框架解析后才有意义。
常见元注解:
@Target:注解能用在哪里。@Retention:注解保留到哪个阶段,常见有SOURCE、CLASS、RUNTIME。@Documented:是否进入文档。@Inherited:是否允许子类继承。
运行时框架通常要求 @Retention(RetentionPolicy.RUNTIME),这样才能通过反射读取注解。
23. SPI 机制怎么回答?
SPI 是 Service Provider Interface,服务提供方接口。调用方定义接口和加载规则,具体实现由第三方提供。
JDK SPI 的典型步骤:
- 定义接口。
- 实现方提供实现类。
- 在
META-INF/services/接口全限定名文件中写入实现类全限定名。 - 调用方通过
ServiceLoader.load()加载实现。
典型场景:JDBC 驱动加载、Dubbo 扩展点思想、日志框架适配。
SPI 和 API 的区别:API 是调用方使用服务;SPI 是框架反过来发现并加载服务实现。
缺点:JDK SPI 会一次性加载实现,缺少按需加载和依赖注入能力,复杂场景通常会使用框架自定义 SPI。
24. 序列化和反序列化
序列化是把对象转成可存储或传输的数据形式;反序列化是把数据还原成对象。
常见用途:网络传输、缓存、持久化、RPC。
注意点:
- 不想序列化的字段用
transient。 static字段属于类,不属于对象,不会被普通对象序列化保存。serialVersionUID用于版本兼容校验。- 不推荐 JDK 原生序列化用于高性能系统,因为它性能较差、体积较大,并且有安全风险。
常见协议:JSON、Protobuf、Hessian、Kryo、Avro、MessagePack。
25. Java I/O、BIO、NIO、AIO
Java I/O 流是数据输入输出的抽象。按数据单位分为字节流和字符流,字节流处理二进制数据,字符流处理文本并涉及编码解码。
BIO:同步阻塞 I/O,一个连接通常对应一个线程,简单但高并发下线程成本高。
NIO:同步非阻塞 I/O,核心是 Buffer、Channel、Selector。一个线程可以通过 Selector 监听多个 Channel 事件,适合高并发网络编程。
AIO:异步非阻塞 I/O,操作系统完成 I/O 后回调通知应用,Java 中使用较少。
Channel 是双向数据通道,读写都要经过 Buffer。Selector 是多路复用器,负责监听多个 Channel 的就绪事件,比如连接、读、写。
26. wait() 和 sleep() 的区别?
wait() 是 Object 的方法,必须在同步代码块或同步方法中调用,会释放锁,等待其他线程 notify() 或 notifyAll() 唤醒。
sleep() 是 Thread 的静态方法,让当前线程休眠指定时间,不会释放已经持有的锁。
一句话:wait() 用于线程协作,会释放锁;sleep() 用于暂停线程,不释放锁。
27. start() 和 run() 的区别?
调用 start() 会让线程从 NEW 进入可运行状态,由 JVM 调度后执行 run()。
直接调用 run() 只是普通方法调用,不会创建新线程。
同一个 Thread 对象只能调用一次 start()。重复调用会抛 IllegalThreadStateException。
28. final、finally、finalize 的区别?
final 是关键字:
- 修饰类:不能被继承。
- 修饰方法:不能被重写。
- 修饰变量:只能赋值一次。
finally 是异常处理结构中的代码块,通常用于释放资源。
finalize() 是 Object 的方法,GC 前可能调用,不可靠,已经不推荐使用。
29. static 关键字怎么理解?
static 表示属于类,而不是属于某个对象。
可修饰变量、方法、代码块和内部类:
- 静态变量:类级共享数据。
- 静态方法:无需对象即可调用,只能直接访问静态成员。
- 静态代码块:类加载时执行一次,常用于初始化。
- 静态内部类:不依赖外部类实例。
静态方法不能调用非静态成员,因为调用时可能还没有具体对象。
30. 访问修饰符怎么背?
从大到小:
| 修饰符 | 同类 | 同包 | 子类 | 任意位置 |
|---|---|---|---|---|
public | 是 | 是 | 是 | 是 |
protected | 是 | 是 | 是 | 否 |
| 默认 | 是 | 是 | 否 | 否 |
private | 是 | 否 | 否 | 否 |
实际回答时补一句:访问控制服务于封装,字段通常用 private,通过方法暴露必要能力。
31. Java 常见语法糖有哪些?
语法糖是编译器帮我们简化书写的语法,编译后会还原成更基础的结构。
常见语法糖:
- 自动装箱和拆箱。
- 增强
for循环。 - 泛型。
- 可变参数。
- 枚举。
- 内部类。
- Lambda 表达式。
- try-with-resources。
- 字符串
+拼接。 - switch 支持字符串。
面试重点不是背清单,而是能说出一两个底层转换例子,比如增强 for 对集合会转成迭代器,自动装箱会调用 valueOf()。
32. JDK 版本新特性怎么简答?
只背高频版本即可:
- Java 8:Lambda、Stream、Optional、接口默认方法、新日期时间 API。
- Java 9:模块化、集合工厂方法、接口私有方法。
- Java 10:
var局部变量类型推断。 - Java 11:标准 HTTP Client、长期支持版本。
- Java 14:
switch表达式正式可用。 - Java 16:
record正式可用。 - Java 17:长期支持版本、密封类。
- Java 21:长期支持版本、虚拟线程、模式匹配增强。
回答策略:先说自己项目常用的版本,再讲最熟悉的 3 个特性和实际用途,别把版本清单背成流水账。
33. 可以直接删掉或只需了解的问题
这些问题太基础或单独背诵价值不高,面试中遇到时用一句话带过即可:
- Java SE、Java EE、Java ME:知道 SE 是基础平台,EE 是企业规范集合即可。
- 注释、标识符、关键字清单:除非考语法,不需要单独背。
break、continue、return:分别是跳出循环/本次循环/方法返回。- PO、VO、BO、DTO、DAO、POJO:项目分层命名,能结合项目说明即可。
- 迭代器、栈、队列、网络编程:更适合放到集合、数据结构、网络专题中系统背。
- 外部命令调用:知道
ProcessBuilder比Runtime.exec()更推荐即可,不算 Java 基础高频。
34. 面试口述模板
遇到 Java 基础题,尽量按这个模板回答:
- 先给结论。
- 再讲底层机制或规则。
- 补一个易错点。
- 最后带一个项目或框架场景。
示例:问“反射是什么?”
可以答:反射是在运行时获取类信息并操作字段、方法、构造器的机制。它让程序可以在编译期不知道具体类型的情况下动态创建对象或调用方法。Spring 注入、MyBatis 映射、注解扫描都大量用到反射。缺点是性能和类型安全不如直接调用,所以框架通常会缓存反射对象,减少重复查找。
