Java基础篇——第三部

88 阅读22分钟

第8章:序列化与反序列化

8.1 序列化基础

8.1.1 什么是序列化和反序列化?

序列化定义

**序列化(Serialization)**是将对象转换为字节流的过程,使得对象可以在网络中传输或保存到文件中。

**反序列化(Deserialization)**是将字节流恢复为对象的过程,从字节流中重建对象。

为什么需要序列化?

应用场景:

  1. 网络传输: 对象需要在网络上传输
  2. 对象持久化: 将对象保存到文件或数据库
  3. 远程方法调用(RPC): 分布式系统中传递对象
  4. 缓存存储: 将对象缓存到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
特性SerializableExternalizable
实现方式自动序列化手动实现
性能较慢更快
控制粒度粗粒度细粒度
必须无参构造
使用场景一般场景性能要求高

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原生基准基准
Kryo10x+
Hessian2x
Protobuf5x

推荐:

  • 高性能场景: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;  // 明确版本变更

📊 本章总结

核心要点:

  1. 序列化是将对象转换为字节流,反序列化是恢复对象
  2. 类必须实现Serializable接口才能序列化
  3. serialVersionUID用于版本控制
  4. transient关键字标记字段不序列化
  5. 序列化存在安全风险,需要防护
  6. 可以使用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,没有被修改
    }
}

执行过程:

  1. 调用modify(a)时,传递的是a的值(5)的副本
  2. 方法内部修改的是副本x,不影响原始变量a
  3. 方法结束后,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,被修改了
    }
}

执行过程:

  1. 传递的是引用的副本(指向同一个对象)
  2. 通过引用修改对象内容,会影响原始对象
  3. 因为引用副本和原始引用指向同一个对象
改变引用
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,没有被修改
    }
}

执行过程:

  1. 传递的是引用的副本
  2. 方法内部改变的是副本的指向,不影响原始引用
  3. 原始引用仍然指向原来的对象

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 单个类的初始化顺序

初始化顺序

执行顺序:

  1. 静态字段和静态代码块(按代码顺序)
  2. 实例字段和实例代码块(按代码顺序)
  3. 构造方法
示例代码
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 父子类的初始化顺序

初始化顺序

执行顺序:

  1. 父类静态 → 子类静态
  2. 父类实例 → 父类构造
  3. 子类实例 → 子类构造
示例代码
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为什么是不可变的?

答案:

原因:

  1. 安全性: 作为参数传递时不会被修改
  2. 线程安全: 不可变对象天然线程安全
  3. 缓存优化: 字符串常量池可以缓存字符串
  4. 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的区别?

答案:

特性StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全
性能
使用场景常量字符串单线程拼接多线程拼接

推荐:

  • 一般场景: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的区别?

答案:

特性ArrayListLinkedList
数据结构数组双向链表
随机访问O(1)O(n)
插入删除O(n)O(1)
内存占用
使用场景随机访问多插入删除多
面试题10:HashMap和Hashtable的区别?

答案:

特性HashMapHashtable
线程安全
null键值允许不允许
性能
推荐使用否(已淘汰)
面试题11:HashMap的底层实现原理?

答案:

JDK8: 数组 + 链表/红黑树

核心机制:

  1. 计算hash值,定位数组下标
  2. 如果位置为空,直接插入
  3. 如果位置不为空,处理冲突(链表或红黑树)
  4. 链表长度>=8时转为红黑树

关键参数:

  • 默认容量:16
  • 负载因子:0.75
  • 转树阈值:8

9.4.4 并发编程面试题

面试题12:synchronized和volatile的区别?

答案:

特性synchronizedvolatile
原子性保证不保证
可见性保证保证
有序性保证保证
性能较低较高
使用场景同步代码块单变量可见性
面试题13:线程池的核心参数?

答案:

ThreadPoolExecutor参数:

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:空闲线程存活时间
  • workQueue:工作队列
  • threadFactory:线程工厂
  • handler:拒绝策略
面试题14:死锁的产生条件和解决方法?

答案:

产生条件:

  1. 互斥条件
  2. 请求与保持
  3. 不剥夺条件
  4. 循环等待

解决方法:

  • 避免嵌套锁
  • 按顺序获取锁
  • 使用超时锁
  • 死锁检测

9.4.5 JVM面试题

面试题15:JVM内存模型?

答案:

内存区域:

  • 程序计数器:当前执行指令地址
  • 虚拟机栈:方法执行的内存模型
  • 本地方法栈:Native方法
  • 堆:对象实例
  • 方法区:类信息、常量
面试题16:GC算法有哪些?

答案:

常见算法:

  • 标记-清除:标记无用对象,清除
  • 标记-复制:复制存活对象
  • 标记-整理:标记后整理
  • 分代收集:新生代、老年代不同策略
面试题17:类加载过程?

答案:

加载过程:

  1. 加载:读取class文件
  2. 验证:验证class文件格式
  3. 准备:为静态变量分配内存
  4. 解析:将符号引用转为直接引用
  5. 初始化:执行静态代码块

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:如何设计一个线程安全的单例模式?

答案:

设计要点:

  1. 延迟初始化: 需要时才创建
  2. 线程安全: 多线程环境下安全
  3. 防止反射攻击: 防止通过反射创建实例
  4. 序列化安全: 防止反序列化创建新实例

完整实现:

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个对象:

  1. 如果"abc"不在常量池: 创建2个对象

    • 字符串常量"abc"(在常量池)
    • new String()对象(在堆)
  2. 如果"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:如何避免内存泄漏?

答案:

常见原因和解决方法:

  1. 集合类持有对象引用:

    • 及时清理不用的对象
    • 使用WeakHashMap
  2. 监听器未移除:

    • 及时移除监听器
  3. 内部类持有外部类引用:

    • 使用静态内部类
    • 及时置null
  4. ThreadLocal未清理:

    • 使用完后调用remove()
面试题24:如何优化Java程序性能?

答案:

优化策略:

  1. 算法优化: 选择合适的数据结构和算法
  2. 减少对象创建: 使用对象池、缓存
  3. 合理使用集合: 预分配容量、选择合适的实现
  4. 字符串操作: 使用StringBuilder
  5. 并发优化: 使用线程池、减少锁竞争
  6. JVM调优: 调整堆大小、GC参数
面试题25:Java 8的新特性有哪些?

答案:

主要特性:

  1. Lambda表达式: 函数式编程
  2. Stream API: 流式处理
  3. Optional: 避免空指针
  4. 默认方法: 接口可以有默认实现
  5. 新的日期时间API: LocalDate、LocalTime等
  6. 方法引用: 简化Lambda表达式