第8章:序列化与反序列化
8.1 序列化基础
8.1.1 什么是序列化和反序列化?
序列化定义
**序列化(Serialization)**是将对象转换为字节流的过程,使得对象可以在网络中传输或保存到文件中。
**反序列化(Deserialization)**是将字节流恢复为对象的过程,从字节流中重建对象。
为什么需要序列化?
应用场景:
- 网络传输: 对象需要在网络上传输
- 对象持久化: 将对象保存到文件或数据库
- 远程方法调用(RPC): 分布式系统中传递对象
- 缓存存储: 将对象缓存到Redis等存储系统
序列化过程
对象 → 序列化 → 字节流 → 网络/文件 → 字节流 → 反序列化 → 对象
示例:
// 序列化:对象 → 字节流
User user = new User("张三", 25);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"));
oos.writeObject(user); // 对象转换为字节流
// 反序列化:字节流 → 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"));
User restoredUser = (User) ois.readObject(); // 字节流恢复为对象
8.1.2 Serializable接口
接口定义
Serializable接口是一个标记接口,没有任何方法:
public interface Serializable {
// 空接口,只是标记
}
实现方式
类必须实现Serializable接口才能序列化:
public class User implements Serializable {
private String name;
private int age;
// 必须实现Serializable接口
// 否则会抛出NotSerializableException
}
特点:
- 标记接口,不需要实现任何方法
- 如果类未实现Serializable,序列化会抛出异常
- 所有字段都会被序列化(除非使用transient)
8.1.3 serialVersionUID的作用
版本控制
serialVersionUID用于版本控制,确保序列化和反序列化的类版本一致:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
作用机制
1. 版本标识:
- 每个可序列化类都有一个版本号
- serialVersionUID就是这个版本号
2. 版本验证:
- 反序列化时,会检查类的serialVersionUID
- 如果与序列化时的UID不一致,会抛出InvalidClassException
3. 兼容性控制:
- 修改类结构时,应该更新serialVersionUID
- 或者保持UID不变,只添加新字段(向前兼容)
生成方式
1. 手动指定:
private static final long serialVersionUID = 1L;
2. IDE自动生成:
- 大多数IDE可以自动生成serialVersionUID
3. 不指定时的风险:
- 如果不指定,JVM会根据类结构自动计算
- 修改类结构后,UID会改变,导致反序列化失败
8.1.4 transient关键字
作用
transient关键字用于标记字段不被序列化:
public class User implements Serializable {
private String name;
private transient String password; // 不会被序列化
// password字段不会写入字节流
}
使用场景
1. 敏感数据:
- 密码、密钥等敏感信息不应该序列化
2. 临时数据:
- 缓存、计算结果等临时数据
3. 不可序列化的对象:
- 某些对象无法序列化,使用transient标记
示例:
public class User implements Serializable {
private String name;
private transient String password; // 密码不序列化
private transient Thread thread; // 线程不可序列化
// 序列化时,password和thread不会被保存
}
8.1.5 序列化的使用场景
网络传输
场景: 对象需要在网络上传输
// 客户端序列化对象
User user = new User("张三", 25);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(user);
// 服务端反序列化对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User receivedUser = (User) ois.readObject();
对象持久化
场景: 将对象保存到文件
// 保存对象到文件
User user = new User("张三", 25);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"));
oos.writeObject(user);
// 从文件恢复对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"));
User restoredUser = (User) ois.readObject();
远程方法调用(RPC)
场景: 分布式系统中传递对象
// RPC框架内部使用序列化传输对象
// 例如:Dubbo、gRPC等
缓存存储
场景: 将对象缓存到Redis等存储系统
// 序列化对象后存入缓存
User user = new User("张三", 25);
byte[] bytes = serialize(user);
redis.set("user:1", bytes);
// 从缓存恢复对象
byte[] cached = redis.get("user:1");
User cachedUser = deserialize(cached);
8.2 序列化机制
8.2.1 默认序列化机制
实现原理
Java使用ObjectOutputStream和ObjectInputStream实现序列化:
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(user);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.dat"))) {
User user = (User) ois.readObject();
}
序列化过程
1. 写入类信息:
- 类的全限定名
- serialVersionUID
- 字段信息
2. 写入对象数据:
- 所有非transient字段的值
- 递归序列化引用对象
3. 写入结束标记:
- 标记对象序列化完成
反序列化过程
1. 读取类信息:
- 验证类是否存在
- 检查serialVersionUID
2. 创建对象:
- 使用反射创建对象实例
- 不调用构造方法
3. 恢复数据:
- 读取字段值
- 递归反序列化引用对象
8.2.2 自定义序列化
writeObject和readObject
可以自定义序列化逻辑:
public class User implements Serializable {
private String name;
private transient String sensitiveData; // 敏感数据
// 自定义序列化
private void writeObject(ObjectOutputStream oos)
throws IOException {
oos.defaultWriteObject(); // 默认序列化
// 自定义加密
oos.writeObject(encrypt(sensitiveData));
}
// 自定义反序列化
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 默认反序列化
// 自定义解密
sensitiveData = decrypt((String) ois.readObject());
}
private String encrypt(String data) {
// 加密逻辑
return "encrypted_" + data;
}
private String decrypt(String data) {
// 解密逻辑
return data.substring("encrypted_".length());
}
}
特点:
- 方法必须是private
- 可以控制序列化过程
- 可以对敏感数据进行加密
8.2.3 Externalizable接口
接口定义
Externalizable接口需要手动实现序列化逻辑:
public interface Externalizable extends Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
实现示例
public class User implements Externalizable {
private String name;
private int age;
// 必须有无参构造器
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name); // 手动序列化
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
name = in.readUTF(); // 手动反序列化
age = in.readInt();
}
}
Serializable vs Externalizable
| 特性 | Serializable | Externalizable |
|---|---|---|
| 实现方式 | 自动序列化 | 手动实现 |
| 性能 | 较慢 | 更快 |
| 控制粒度 | 粗粒度 | 细粒度 |
| 必须无参构造 | 否 | 是 |
| 使用场景 | 一般场景 | 性能要求高 |
8.2.4 序列化的继承关系
规则说明
规则1:父类实现Serializable
- 子类自动可序列化
- 不需要显式实现Serializable
class Parent implements Serializable {
private String parentField;
}
class Child extends Parent {
private String childField;
// Child自动可序列化
}
规则2:父类未实现Serializable
- 子类序列化时,父类字段不会被序列化
- 反序列化时,父类必须有无参构造器
class Parent {
private String parentField;
public Parent() {} // 必须有无参构造器
}
class Child extends Parent implements Serializable {
private String childField;
// 只有childField会被序列化
// parentField不会被序列化
}
8.2.5 静态字段和瞬态字段
静态字段
静态字段不会被序列化:
public class User implements Serializable {
private String name;
private static int count = 0; // 不会被序列化
// count属于类,不属于对象
// 序列化时不会保存count的值
}
原因:
- 静态字段属于类,不属于对象
- 序列化是针对对象的,所以不序列化静态字段
瞬态字段
使用transient修饰的字段不会被序列化:
public class User implements Serializable {
private String name;
private transient String password; // 不会被序列化
// password字段不会写入字节流
}
使用场景:
- 敏感数据
- 临时数据
- 不可序列化的对象
8.3 序列化安全性
8.3.1 序列化漏洞原理
问题描述
序列化漏洞: 反序列化时可以执行任意代码
原理:
- 反序列化时会调用readObject()方法
- 如果readObject()方法被恶意重写,可能执行恶意代码
示例:
public class Malicious implements Serializable {
private void readObject(ObjectInputStream in)
throws Exception {
// 恶意代码:反序列化时执行
Runtime.getRuntime().exec("恶意命令");
}
}
攻击场景
1. 反序列化恶意对象:
- 攻击者构造恶意序列化数据
- 目标系统反序列化时执行恶意代码
2. 常见漏洞:
- Apache Commons Collections漏洞
- Java反序列化RCE漏洞
8.3.2 反序列化攻击防护
验证输入来源
只反序列化可信来源的数据:
// 好的实践
if (isTrustedSource(data)) {
Object obj = deserialize(data);
} else {
throw new SecurityException("不可信的数据源");
}
使用白名单验证类
自定义ObjectInputStream,验证类名:
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED_CLASSES =
Set.of("com.safe.User", "com.safe.Order");
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
// 白名单验证
String className = desc.getName();
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException("不允许的类: " + className);
}
return super.resolveClass(desc);
}
}
使用安全过滤器
使用JEP 290过滤器:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.safe.*;!*"
);
ois.setObjectInputFilter(filter);
8.3.3 安全的序列化实践
最佳实践
1. 使用final serialVersionUID:
private static final long serialVersionUID = 1L;
2. 避免序列化敏感数据:
private transient String password; // 使用transient
3. 验证反序列化对象:
if (user.getAge() < 0 || user.getAge() > 150) {
throw new InvalidObjectException("无效的数据");
}
4. 使用白名单:
- 只允许反序列化已知的类
8.3.4 替代方案
JSON序列化
使用Jackson:
// 序列化
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
// 反序列化
User user2 = mapper.readValue(json, User.class);
优势:
- 可读性好
- 跨语言
- 安全性高
Protobuf
定义.proto文件:
message User {
string name = 1;
int32 age = 2;
}
优势:
- 性能高
- 数据量小
- 跨语言
方案对比
| 方案 | 性能 | 安全性 | 可读性 | 跨语言 |
|---|---|---|---|---|
| Java原生 | 基准 | 低 | 差 | 否 |
| JSON | 低 | 高 | 好 | 是 |
| Protobuf | 高 | 高 | 差 | 是 |
推荐:
- 一般场景:JSON
- 性能要求高:Protobuf
- 避免使用:Java原生序列化(除非必要)
8.4 序列化性能优化
8.4.1 序列化性能问题
主要问题
1. 序列化/反序列化开销大:
- 反射操作开销大
- 对象创建和初始化耗时
2. 序列化后数据量大:
- 包含类信息、字段信息等
- 比实际数据大很多
3. 频繁GC压力:
- 创建大量临时对象
- 增加GC压力
8.4.2 序列化框架对比
性能对比
| 框架 | 性能 | 大小 | 易用性 |
|---|---|---|---|
| Java原生 | 基准 | 基准 | 高 |
| Kryo | 10x+ | 小 | 中 |
| Hessian | 2x | 中 | 高 |
| Protobuf | 5x | 小 | 低 |
推荐:
- 高性能场景:Kryo或Protobuf
- 一般场景:Hessian或JSON
8.4.3 序列化大小优化
优化策略
1. 使用transient排除不必要字段:
public class OptimizedUser implements Serializable {
private transient int tempField; // 不序列化临时字段
private int age; // 使用基本类型
private String name;
}
2. 使用基本类型而非包装类:
// 优化前
private Integer age; // 包装类,占用更多空间
// 优化后
private int age; // 基本类型,占用更少空间
3. 压缩序列化数据:
// 使用GZIP压缩
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzos = new GZIPOutputStream(baos);
ObjectOutputStream oos = new ObjectOutputStream(gzos);
oos.writeObject(user);
8.4.4 序列化版本兼容性
兼容性策略
1. 只添加新字段(向前兼容):
// 版本1
public class User implements Serializable {
private String name;
}
// 版本2:添加新字段
public class User implements Serializable {
private String name;
private int age; // 新字段,向前兼容
}
2. 不删除字段,只标记为废弃:
@Deprecated
private String oldField; // 不删除,标记为废弃
3. 修改serialVersionUID时明确版本变更:
// 版本1
private static final long serialVersionUID = 1L;
// 版本2:不兼容变更
private static final long serialVersionUID = 2L; // 明确版本变更
📊 本章总结
核心要点:
- 序列化是将对象转换为字节流,反序列化是恢复对象
- 类必须实现Serializable接口才能序列化
- serialVersionUID用于版本控制
- transient关键字标记字段不序列化
- 序列化存在安全风险,需要防护
- 可以使用JSON、Protobuf等替代方案
第9章:其他重要概念与Java常见面试题
9.1 Java中的值传递和引用传递
9.1.1 Java参数传递机制
核心概念
Java中只有值传递,没有引用传递
重要理解:
- 基本类型:传递的是值的副本
- 引用类型:传递的是引用的副本(不是对象本身)
常见误解
误解: Java有引用传递
事实: Java传递的是引用的副本(值传递引用)
9.1.2 基本类型参数传递
示例代码
public class Test {
public static void modify(int x) {
x = 10; // 只修改了副本,不影响原始值
}
public static void main(String[] args) {
int a = 5;
modify(a);
System.out.println(a); // 输出5,没有被修改
}
}
执行过程:
- 调用modify(a)时,传递的是a的值(5)的副本
- 方法内部修改的是副本x,不影响原始变量a
- 方法结束后,a仍然是5
9.1.3 引用类型参数传递
修改对象内容
public class Test {
public static void modifyArray(int[] arr) {
arr[0] = 10; // 修改对象内容,影响原始对象
}
public static void main(String[] args) {
int[] array = {1, 2, 3};
modifyArray(array);
System.out.println(array[0]); // 输出10,被修改了
}
}
执行过程:
- 传递的是引用的副本(指向同一个对象)
- 通过引用修改对象内容,会影响原始对象
- 因为引用副本和原始引用指向同一个对象
改变引用
public class Test {
public static void changeReference(int[] arr) {
arr = new int[]{100}; // 改变引用,不影响原始引用
}
public static void main(String[] args) {
int[] array = {1, 2, 3};
changeReference(array);
System.out.println(array[0]); // 仍然输出1,没有被修改
}
}
执行过程:
- 传递的是引用的副本
- 方法内部改变的是副本的指向,不影响原始引用
- 原始引用仍然指向原来的对象
9.1.4 常见的误解澄清
误解分析
误解: Java有引用传递
事实:
- Java传递的是引用的副本(值传递引用)
- 不是传递对象本身
- 不是传递引用的引用
关键理解:
- 引用类型传递的是引用的值(地址)的副本
- 副本和原始引用指向同一个对象
- 修改对象内容会影响原始对象
- 改变引用指向不会影响原始引用
9.1.5 实际案例分析
交换对象案例
class Person {
String name;
Person(String name) { this.name = name; }
}
public class Test {
static void swap(Person a, Person b) {
Person temp = a;
a = b;
b = temp; // 只交换了副本,不影响原始引用
}
public static void main(String[] args) {
Person p1 = new Person("Alice");
Person p2 = new Person("Bob");
swap(p1, p2);
System.out.println(p1.name); // 仍然是"Alice"
System.out.println(p2.name); // 仍然是"Bob"
}
}
为什么交换失败?
- 传递的是引用的副本
- 方法内部交换的是副本,不影响原始引用
- p1和p2的引用没有改变
如何真正交换?
- 需要修改对象的内容,而不是交换引用
- 或者使用包装类(如AtomicReference)
9.2 对象初始化顺序
9.2.1 单个类的初始化顺序
初始化顺序
执行顺序:
- 静态字段和静态代码块(按代码顺序)
- 实例字段和实例代码块(按代码顺序)
- 构造方法
示例代码
public class InitOrder {
// 1. 静态字段
static int staticField = initStaticField();
static int initStaticField() {
System.out.println("静态字段初始化");
return 1;
}
// 2. 静态代码块
static {
System.out.println("静态代码块");
}
// 3. 实例字段
int instanceField = initInstanceField();
int initInstanceField() {
System.out.println("实例字段初始化");
return 1;
}
// 4. 实例代码块
{
System.out.println("实例代码块");
}
// 5. 构造方法
public InitOrder() {
System.out.println("构造方法");
}
public static void main(String[] args) {
new InitOrder();
}
}
输出顺序:
静态字段初始化
静态代码块
实例字段初始化
实例代码块
构造方法
9.2.2 父子类的初始化顺序
初始化顺序
执行顺序:
- 父类静态 → 子类静态
- 父类实例 → 父类构造
- 子类实例 → 子类构造
示例代码
class Parent {
static {
System.out.println("Parent静态代码块");
}
{
System.out.println("Parent实例代码块");
}
Parent() {
System.out.println("Parent构造方法");
}
}
class Child extends Parent {
static {
System.out.println("Child静态代码块");
}
{
System.out.println("Child实例代码块");
}
Child() {
System.out.println("Child构造方法");
}
}
public class Test {
public static void main(String[] args) {
new Child();
}
}
输出顺序:
Parent静态代码块
Child静态代码块
Parent实例代码块
Parent构造方法
Child实例代码块
Child构造方法
9.2.3 初始化总结
执行时机和次数
| 初始化阶段 | 执行时机 | 执行次数 |
|---|---|---|
| 静态成员 | 类加载时 | 1次 |
| 实例成员 | 每次new对象时 | N次 |
| 构造方法 | 对象创建最后 | N次 |
关键点:
- 静态成员只初始化一次
- 实例成员每次创建对象都初始化
- 父类先于子类初始化
9.3 编码规范与最佳实践
9.3.1 命名规范
命名规则
类名: 大驼峰(PascalCase)
public class UserService { }
public class OrderController { }
方法名: 小驼峰(camelCase)
public void getUserName() { }
public void calculateTotal() { }
常量: 全大写,单词间用下划线
public static final int MAX_SIZE = 100;
public static final String DEFAULT_NAME = "Unknown";
包名: 全小写
package com.example.util;
package com.example.service;
9.3.2 代码格式
好的格式
public class Example {
private String name;
public void doSomething() {
if (condition) {
// 缩进4个空格
methodCall();
}
}
}
规范:
- 使用4个空格缩进(不用Tab)
- 大括号换行
- 方法之间空一行
- 变量声明后空行
9.3.3 注释规范
JavaDoc注释
/**
* 用户服务类
* 提供用户相关的业务逻辑
*
* @author 作者
* @version 1.0
* @since 2024-01-01
*/
public class UserService {
/**
* 根据ID获取用户
*
* @param id 用户ID,必须大于0
* @return 用户对象,如果不存在返回null
* @throws UserNotFoundException 用户不存在时抛出
*/
public User getUserById(int id) {
// 单行注释:实现逻辑
return userRepository.findById(id);
}
}
9.3.4 异常处理规范
好的实践
try {
processFile();
} catch (FileNotFoundException e) {
log.error("文件未找到", e);
throw new BusinessException("文件处理失败", e);
} catch (IOException e) {
log.error("IO异常", e);
throw new BusinessException("文件处理失败", e);
} finally {
cleanup(); // 清理资源
}
不好的实践
// 不好的实践1:捕获所有异常
try {
// ...
} catch (Exception e) {
// 太宽泛
}
// 不好的实践2:空的catch块
try {
// ...
} catch (Exception e) {
// 忽略异常,不处理
}
// 不好的实践3:吞掉异常
try {
// ...
} catch (Exception e) {
e.printStackTrace(); // 只打印,不处理
}
9.3.5 性能优化建议
字符串操作
// 优化前:每次循环创建新对象
String result = "";
for (int i = 0; i < 100; i++) {
result += i; // 性能差
}
// 优化后:使用StringBuilder
StringBuilder sb = new StringBuilder(100);
for (int i = 0; i < 100; i++) {
sb.append(i);
}
String result = sb.toString();
集合初始化
// 优化前:频繁扩容
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add("item" + i);
}
// 优化后:预分配容量
List<String> list = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
list.add("item" + i);
}
避免重复计算
// 优化前:重复计算
for (int i = 0; i < list.size(); i++) {
// list.size()每次循环都计算
}
// 优化后:缓存结果
int size = list.size();
for (int i = 0; i < size; i++) {
// 只计算一次
}
使用基本类型
// 优化前:使用包装类
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 自动装箱拆箱,性能差
}
// 优化后:使用基本类型
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 性能好
}
9.4 Java常见面试题精选
9.4.1 Java基础面试题
面试题1:Java是值传递还是引用传递?
答案:
Java只有值传递,没有引用传递
基本类型: 传递的是值的副本
void modify(int x) { x = 10; } // 不影响原始值
引用类型: 传递的是引用的副本(值传递引用)
void modifyArray(int[] arr) { arr[0] = 10; } // 影响原始对象
void changeRef(int[] arr) { arr = new int[10]; } // 不影响原始引用
面试题2:String为什么是不可变的?
答案:
原因:
- 安全性: 作为参数传递时不会被修改
- 线程安全: 不可变对象天然线程安全
- 缓存优化: 字符串常量池可以缓存字符串
- HashCode缓存: hashCode值可以缓存,提高性能
实现方式:
- String类使用final修饰
- 内部char数组使用final修饰
- 没有提供修改字符的方法
面试题3:equals和==的区别?
答案:
== 操作符:
- 基本类型:比较值
- 引用类型:比较引用(地址)
equals()方法:
- 比较对象的内容
- 可以重写自定义比较逻辑
示例:
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
s1 == s2; // true(字符串常量池)
s1 == s3; // false(不同对象)
s1.equals(s3); // true(内容相同)
面试题4:final、finally、finalize的区别?
答案:
final:
- 修饰类:不能被继承
- 修饰方法:不能被重写
- 修饰变量:常量,不能修改
finally:
- try-catch-finally中的finally块
- 无论是否异常都会执行
- 用于清理资源
finalize():
- Object类的方法
- GC回收对象前调用
- 不推荐使用,已废弃
面试题5:String、StringBuilder、StringBuffer的区别?
答案:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是 | 否 | 是 |
| 性能 | 低 | 高 | 中 |
| 使用场景 | 常量字符串 | 单线程拼接 | 多线程拼接 |
推荐:
- 一般场景:StringBuilder
- 多线程:StringBuffer
- 常量:String
9.4.2 面向对象面试题
面试题6:重载和重写的区别?
答案:
重载(Overload):
- 同一个类中,方法名相同,参数不同
- 编译时确定调用哪个方法
- 返回类型可以不同
重写(Override):
- 子类重写父类方法
- 方法签名必须相同
- 运行时确定调用哪个方法
示例:
// 重载
class Test {
void method(int x) { }
void method(String s) { } // 参数不同
}
// 重写
class Parent {
void method() { }
}
class Child extends Parent {
@Override
void method() { } // 重写父类方法
}
面试题7:抽象类和接口的区别?
答案:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 方法实现 | 可以有 | JDK8前不能有 |
| 字段 | 可以有实例字段 | 只能是常量 |
| 继承 | 单继承 | 多实现 |
| 构造方法 | 可以有 | 不能有 |
| 访问修饰符 | 任意 | public |
使用场景:
- 抽象类:is-a关系,有共同实现
- 接口:has-a关系,定义契约
面试题8:多态的实现原理?
答案:
多态: 同一个方法调用,根据对象类型执行不同实现
实现原理:
- 方法表(Method Table)
- 虚方法调用(Virtual Method Invocation)
- 运行时确定实际调用的方法
示例:
Animal animal = new Dog();
animal.sound(); // 调用Dog的sound()方法
9.4.3 集合框架面试题
面试题9:ArrayList和LinkedList的区别?
答案:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 数据结构 | 数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 插入删除 | O(n) | O(1) |
| 内存占用 | 小 | 大 |
| 使用场景 | 随机访问多 | 插入删除多 |
面试题10:HashMap和Hashtable的区别?
答案:
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 否 | 是 |
| null键值 | 允许 | 不允许 |
| 性能 | 好 | 差 |
| 推荐使用 | 是 | 否(已淘汰) |
面试题11:HashMap的底层实现原理?
答案:
JDK8: 数组 + 链表/红黑树
核心机制:
- 计算hash值,定位数组下标
- 如果位置为空,直接插入
- 如果位置不为空,处理冲突(链表或红黑树)
- 链表长度>=8时转为红黑树
关键参数:
- 默认容量:16
- 负载因子:0.75
- 转树阈值:8
9.4.4 并发编程面试题
面试题12:synchronized和volatile的区别?
答案:
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 性能 | 较低 | 较高 |
| 使用场景 | 同步代码块 | 单变量可见性 |
面试题13:线程池的核心参数?
答案:
ThreadPoolExecutor参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
面试题14:死锁的产生条件和解决方法?
答案:
产生条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
解决方法:
- 避免嵌套锁
- 按顺序获取锁
- 使用超时锁
- 死锁检测
9.4.5 JVM面试题
面试题15:JVM内存模型?
答案:
内存区域:
- 程序计数器:当前执行指令地址
- 虚拟机栈:方法执行的内存模型
- 本地方法栈:Native方法
- 堆:对象实例
- 方法区:类信息、常量
面试题16:GC算法有哪些?
答案:
常见算法:
- 标记-清除:标记无用对象,清除
- 标记-复制:复制存活对象
- 标记-整理:标记后整理
- 分代收集:新生代、老年代不同策略
面试题17:类加载过程?
答案:
加载过程:
- 加载:读取class文件
- 验证:验证class文件格式
- 准备:为静态变量分配内存
- 解析:将符号引用转为直接引用
- 初始化:执行静态代码块
9.4.6 设计模式面试题
面试题18:单例模式的实现方式?
答案:
1. 饿汉式:
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
2. 懒汉式(双重检查):
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 枚举(推荐):
public enum Singleton {
INSTANCE;
public void doSomething() {}
}
面试题19:工厂模式的应用场景?
答案:
应用场景:
- 创建对象逻辑复杂
- 需要根据条件创建不同对象
- 解耦对象创建和使用
示例:
interface Shape {
void draw();
}
class Circle implements Shape { }
class Rectangle implements Shape { }
class ShapeFactory {
Shape createShape(String type) {
if ("circle".equals(type)) return new Circle();
if ("rectangle".equals(type)) return new Rectangle();
return null;
}
}
9.4.7 综合面试题
面试题20:如何设计一个线程安全的单例模式?
答案:
设计要点:
- 延迟初始化: 需要时才创建
- 线程安全: 多线程环境下安全
- 防止反射攻击: 防止通过反射创建实例
- 序列化安全: 防止反序列化创建新实例
完整实现:
public class Singleton implements Serializable {
private volatile static Singleton instance;
private Singleton() {
// 防止反射攻击
if (instance != null) {
throw new RuntimeException("单例模式,禁止反射创建");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 防止反序列化创建新实例
private Object readResolve() {
return getInstance();
}
}
面试题21:String s = new String("abc")创建了几个对象?
答案:
可能创建1个或2个对象:
-
如果"abc"不在常量池: 创建2个对象
- 字符串常量"abc"(在常量池)
- new String()对象(在堆)
-
如果"abc"已在常量池: 创建1个对象
- 只创建new String()对象(在堆)
- 常量池中已存在"abc"
面试题22:Java中的四种引用类型?
答案:
1. 强引用(Strong Reference):
Object obj = new Object(); // 强引用
2. 软引用(Soft Reference):
SoftReference<Object> softRef = new SoftReference<>(obj);
// 内存不足时可能被回收
3. 弱引用(Weak Reference):
WeakReference<Object> weakRef = new WeakReference<>(obj);
// GC时会被回收
4. 虚引用(Phantom Reference):
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
// 用于跟踪对象被回收
面试题23:如何避免内存泄漏?
答案:
常见原因和解决方法:
-
集合类持有对象引用:
- 及时清理不用的对象
- 使用WeakHashMap
-
监听器未移除:
- 及时移除监听器
-
内部类持有外部类引用:
- 使用静态内部类
- 及时置null
-
ThreadLocal未清理:
- 使用完后调用remove()
面试题24:如何优化Java程序性能?
答案:
优化策略:
- 算法优化: 选择合适的数据结构和算法
- 减少对象创建: 使用对象池、缓存
- 合理使用集合: 预分配容量、选择合适的实现
- 字符串操作: 使用StringBuilder
- 并发优化: 使用线程池、减少锁竞争
- JVM调优: 调整堆大小、GC参数
面试题25:Java 8的新特性有哪些?
答案:
主要特性:
- Lambda表达式: 函数式编程
- Stream API: 流式处理
- Optional: 避免空指针
- 默认方法: 接口可以有默认实现
- 新的日期时间API: LocalDate、LocalTime等
- 方法引用: 简化Lambda表达式