第02章:创建和销毁对象
(1)优先考虑静态工厂方法而不是构造函数
一、优势
1. 名称灵活性
-
构造函数必须与类名相同(如
new ArrayList()
),而静态工厂方法可以自定义名称(如Collections.emptyList()
),更清晰地表达方法的功能。 -
示例:
// 构造函数 List<String> list = new ArrayList(); // 无法体现"空列表"的语义 // 静态工厂方法 List<String> emptyList = Collections.emptyList(); // 名称直接表明用途
2. 避免重复创建对象
- 静态工厂方法可以通过缓存或单例模式重用已有实例,减少内存开销和垃圾回收(GC)压力。
- 示例(单例模式):
public class Logger { private static final Logger INSTANCE = new Logger(); private Logger() {} // 私有构造函数 public static Logger getInstance() { // 静态工厂方法 return INSTANCE; } }
3. 返回子类对象
- 静态工厂方法可以隐藏具体实现类,直接返回子类实例,客户端无需知道继承关系。
- 示例(按需返回不同子类):
interface Shape { double area(); } class Circle implements Shape { /* ... */ } class Square implements Shape { /* ... */ } class ShapeFactory { public static Shape createShape(String type) { switch (type) { case "circle": return new Circle(); case "square": return new Square(); default: throw new IllegalArgumentException(); } } }
4. 无需创建对象时返回null
- 构造函数必须返回新对象,而静态工厂方法可以返回
null
或特定占位对象(如空集合),避免客户端处理NullPointerException
。 - 示例:
// 静态工厂方法返回空集合(不可变) List<String> list = Collections.emptyList(); // 不会为null // 构造函数无法直接返回空对象
二、对比
特性 | 静态工厂方法 | 构造函数 |
---|---|---|
名称 | 自定义名称,语义更明确 | 固定为new ClassName(...) |
实例化控制 | 可重用对象、返回子类或占位对象 | 必须新建对象 |
可读性 | 方法名可描述行为(如fromJson() ) | 仅依赖参数列表传递意图 |
与单例模式的集成 | 天然支持单例(如getInstance() ) | 需额外私有化构造函数 |
三、业务
-
工具类(如
Collections
、Arrays
):
提供静态工厂方法生成不可变集合、数组视图等。Set<Integer> uniqueNumbers = Sets.newHashSet(numbers); // 静态工厂
-
枚举与常量:
结合枚举实现工厂方法,返回预定义的实例。public enum Operation { ADD, SUBTRACT, MULTIPLY; public static Operation fromSymbol(String symbol) { switch (symbol) { case "+": return ADD; case "-": return SUBTRACT; case "*": return MULTIPLY; default: throw new IllegalArgumentException(); } } }
-
延迟初始化与缓存:
静态工厂方法可以控制实例的创建时机。public class DatabaseConnection { private static volatile Connection INSTANCE; private DatabaseConnection() {} // 私有构造函数 public static Connection getConnection() { if (INSTANCE == null) { synchronized (DatabaseConnection.class) { if (INSTANCE == null) { INSTANCE = DriverManager.getConnection("jdbc:mysql://localhost"); } } } return INSTANCE; } }
四、注意
- 不要滥用:若类需要被实例化且无特殊需求,直接使用构造函数即可。
- 文档清晰:静态工厂方法需明确标注其功能(如是否返回新对象或单例)。
- 避免混淆:静态工厂方法应与构造函数职责分离,避免同名冲突。
五、总结
静态工厂方法通过命名灵活性、实例化控制和隐藏实现细节,提供了比构造函数更强大的功能。尤其在需要优化性能、简化客户端调用或实现设计模式(如单例、工厂模式)时,静态工厂方法是更优选择。
(2)优先考虑构建器(多参数)
一、核心观点
1. 问题:多参数构造函数的痛点
- 问题描述:当类需要多个参数时,构造函数会变得冗长、难以阅读,且参数顺序容易混淆。
- 示例:
public class User { private String name; private int age; private String email; private String phone; public User(String name, int age, String email, String phone) { this.name = name; this.age = age; this.email = email; this.phone = phone; } } // 调用时需记住参数顺序: User user = new User("Alice", 30, "alice@example.com", "123-456-7890");
2. 解决方案:构建器模式(Builder Pattern)
- 优势:
- 明确参数语义:每个参数通过方法名清晰标识。
- 支持可选参数:无需强制传递所有参数。
- 链式调用:代码更流畅易读。
- 避免构造函数歧义:参数顺序不影响结果。
二、示例
1. 坏代码:多参数构造函数
public class NutritionFacts {
private final int servingSize; // 每份克数
private final int calories; // 卡路里
private final int fat; // 脂肪含量
private final int sodium; // 钠含量
private final int sugar; // 糖含量;
// 6个参数的构造函数,难以维护
public NutritionFacts(int servingSize, int calories, int fat, int sodium, int sugar) {
this.servingSize = servingSize;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.sugar = sugar;
}
}
// 调用时参数顺序容易出错:
NutritionFacts facts = new NutritionFacts(240, 200, 10, 350, 5);
2. 好代码:使用构建器重构
public class NutritionFacts {
private final int servingSize;
private final int calories;
private final int fat;
private final int sodium;
private final int sugar;
// 私有构造函数,仅通过构建器创建实例
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.sugar = builder.sugar;
}
// 静态内部构建器类
public static class Builder {
private final int servingSize;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int sugar = 0;
public Builder(int servingSize) {
this.servingSize = servingSize;
}
public Builder calories(int calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder sugar(int sugar) {
this.sugar = sugar;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
// 客户端调用示例
public static void main(String[] args) {
NutritionFacts facts = new NutritionFacts.Builder(240)
.calories(200)
.fat(10)
.sodium(350)
.sugar(5)
.build();
}
}
三、核心设计原则
1. 静态内部构建器类
- 构建器通常定义为类的静态内部类,封装构造逻辑并控制实例化流程。
2. 必需参数优先设置
- 在构建器中,强制要求某些关键参数(如
servingSize
),其余参数提供默认值(如calories=0
)。
3. 链式调用(Fluent Interface)
- 每个设置方法返回
Builder
自身,支持连续调用(如.calories(200).fat(10)
)。
4. 不可变对象
- 构建器生成的实例应为不可变类(所有字段
final
),确保线程安全和一致性。
四、对比
方案 | 优点 | 缺点 |
---|---|---|
多参数构造函数 | 简单直接 | 参数顺序易错、可读性差、无法支持可选参数 |
静态工厂方法 | 支持灵活返回子类或缓存实例 | 参数较多时仍需传递完整参数列表 |
构建器模式 | 明确参数语义、支持可选参数、链式调用 | 需额外编码构建器类 |
五、业务
- 参数较多(如4个及以上参数)。
- 存在可选参数(部分参数非必需)。
- 参数类型相似(易导致调用顺序混淆)。
- 需要生成不可变对象(如
ImmutableSet
、BigDecimal
)。
六、注意
- 避免过度使用:若参数较少(如2-3个),直接使用构造函数更简洁。
- 构建器与单例模式结合:可通过构建器限制实例化次数。
- 性能影响:构建器会引入额外对象(
Builder
实例),需权衡开销。
七、案例
- Java标准库:
java.util.Locale
、java.time.LocalDateTime
均使用构建器模式。 - Google Guava:
ImmutableMap.Builder
生成不可变映射。
八、总结
构建器模式通过分离参数设置与对象创建,显著提升了多参数场景下的代码可读性和可维护性。虽然需要额外编码,但其带来的长期收益(减少错误、简化客户端调用)远超初期成本。在参数较多或需要灵活配置时,优先考虑构建器而非传统的多参数构造函数。
(3)用私有构造器或者枚举类型强化Singleton属性
一、核心观点
1. 问题:单例模式的必要性
- 目标:确保一个类在系统中仅有一个实例存在,避免资源浪费和逻辑冲突。
- 风险:若构造器公开,客户端可能随意创建多个实例。
2. 解决方案
- 私有构造器:禁止外部直接实例化,通过静态工厂方法控制实例化逻辑。
- 枚举类型:利用Java枚举的特性(天然单例、线程安全)简化单例实现。
二、私有构造器实现单例模式
1. 基本实现
-
步骤:
- 将构造器设为
private
。 - 提供一个静态工厂方法返回唯一实例。
- 将构造器设为
-
代码示例:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); // 静态初始化 private Singleton() {} // 私有构造器 public static Singleton getInstance() { return INSTANCE; } }
2. 延迟初始化(懒加载)
-
问题:静态初始化在类加载时即创建实例,可能导致资源浪费。
-
优化方案:使用双重检查锁定(Double-Checked Locking)实现延迟加载。
-
代码示例:
public class LazySingleton { private static volatile LazySingleton INSTANCE; // 使用volatile防止指令重排序 private LazySingleton() {} // 私有构造器 public static LazySingleton getInstance() { if (INSTANCE == null) { // 第一次检查 synchronized (LazySingleton.class) { if (INSTANCE == null) { // 第二次检查 INSTANCE = new LazySingleton(); } } } return INSTANCE; } }
3. 防御反射攻击
-
问题:通过反射可绕过私有构造器创建实例。
-
解决方案:在构造器中抛出异常。
-
代码示例:
public class SecureSingleton { private static final SecureSingleton INSTANCE = new SecureSingleton(); private SecureSingleton() { if (INSTANCE != null) { // 检查是否已被实例化 throw new IllegalStateException("Already initialized"); } } public static SecureSingleton getInstance() { return INSTANCE; } }
三、枚举类型实现单例模式
1. 天然单例特性
-
优势:
- Java保证枚举实例唯一且不可变。
- 自动处理序列化(
readResolve()
方法)和克隆(禁止clone()
操作)。
-
代码示例:
public enum EnumSingleton { INSTANCE; public void doSomething() { System.out.println("Hello, Singleton!"); } }
2. 使用场景
- 简单单例:无需额外功能(如延迟初始化、配置参数)。
- 常量集合:结合枚举定义一组固定常量(如颜色、状态)。
四、私有构造器 vs 枚举类型的对比
特性 | 私有构造器 | 枚举类型 |
---|---|---|
实现复杂度 | 较高(需手动控制实例化逻辑) | 低(代码简洁) |
线程安全性 | 双重检查锁定可保证线程安全 | 天然线程安全 |
延迟初始化 | 支持 | 不支持(实例在类加载时创建) |
反射防御 | 需手动处理 | 自动防御(无法通过反射创建新实例) |
序列化兼容性 | 需自定义readResolve() 方法 | 自动处理 |
五、业务
-
私有构造器:
- 需要灵活控制实例化时机(如延迟加载)。
- 需要集成额外功能(如依赖注入、配置管理)。
-
枚举类型:
- 简单单例,无需复杂逻辑。
- 需要定义一组相关常量(如枚举单例+状态码)。
六、注意
-
枚举的局限性:
- 无法继承其他类(Java规定枚举必须继承
Enum
)。 - 实例化后无法修改字段值(除非声明为
transient
)。
- 无法继承其他类(Java规定枚举必须继承
-
序列化问题:
- 私有构造器需实现
readResolve()
防止反序列化创建新实例:private Object readResolve() { return INSTANCE; }
- 私有构造器需实现
-
测试与调试:
- 枚举单例的单元测试更简单,无需担心多实例问题。
七、案例
- 私有构造器:
java.util.Runtime
、java.awt.Desktop
。 - 枚举类型:
java.util.concurrent.TimeUnit
、java.nio.charset.Charset
。
八、总结
通过私有构造器和枚举类型,可以有效地强化单例属性,确保系统中仅存在一个实例。
- 优先选择枚举类型:若实现简单且无需延迟初始化。
- 使用私有构造器:若需要更复杂的控制逻辑(如延迟加载、依赖注入)。
无论哪种方式,均需注意反射攻击和序列化漏洞的防护。
(4)try-with-resources 优先于 try-finally
一、核心观点
1. 问题:传统 try-finally
的痛点
- 手动关闭资源的繁琐性:需显式编写
finally
块,易遗漏或因异常中断导致资源泄漏。 - 代码冗余:多个资源需重复关闭逻辑,可读性差。
2. 解决方案:try-with-resources
- 优势:
- 自动关闭资源:资源实现
AutoCloseable
接口后,无需手动关闭。 - 简洁性:代码更短,减少重复逻辑。
- 异常安全性:无论是否发生异常,资源都会被正确关闭。
- 自动关闭资源:资源实现
二、代码示例分析
1. 坏代码:传统的 try-finally
public void readFile(String filePath) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) { // 易遗漏或误写
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2. 好代码:try-with-resources
public void readFile(String filePath) {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 资源自动关闭,无需 finally 块
}
三、核心特性
1. 自动关闭机制
- 资源必须实现
AutoCloseable
接口(如BufferedReader
、FileInputStream
)。 - 语法结构:
try (Resource res1, Resource res2) { ... }
,多个资源可同时声明。
2. 执行顺序
- 资源关闭顺序:与资源声明顺序相反(LIFO)。
- 异常处理:关闭资源时发生的异常会掩盖原异常吗?
- 答案:不会,原异常会被保留,但关闭资源的异常会被抑制(可通过
addSuppressed
记录)。
- 答案:不会,原异常会被保留,但关闭资源的异常会被抑制(可通过
3. 缩短代码
- 对比:传统
try-finally
需 8~10 行,try-with-resources
可压缩至 3~4 行。
四、对比
特性 | try-with-resources | try-finally |
---|---|---|
资源关闭 | 自动关闭,无需手动编码 | 手动关闭,易遗漏或出错 |
代码简洁性 | 更简洁,减少冗余代码 | 代码冗长,重复关闭逻辑 |
异常处理 | 关闭资源异常被抑制,原异常不变 | 关闭资源异常需单独捕获处理 |
适用场景 | 资源明确且需快速释放(如 I/O、数据库连接) | 资源关闭逻辑复杂或需额外清理操作 |
五、适用场景
- 资源需立即关闭:如文件、网络连接、数据库连接等。
- 资源数量较少:多个资源可通过逗号分隔声明。
- 代码简洁性优先:减少重复代码,提升可读性。
六、注意事项
-
资源必须实现
AutoCloseable
- 若自定义资源,需实现
AutoCloseable
并重写close()
方法:public class CustomResource implements AutoCloseable { @Override public void close() throws Exception { // 释放资源逻辑 } }
- 若自定义资源,需实现
-
抑制异常的处理
- 如需记录关闭资源时的异常,可使用
addSuppressed
:try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { // 读取逻辑 } catch (IOException e) { e.printStackTrace(); Throwable suppressed = e.getSuppressed(); if (suppressed != null) { suppressed.printStackTrace(); } }
- 如需记录关闭资源时的异常,可使用
-
不适用于所有场景
- 需多次关闭同一资源:
try-with-resources
仅关闭一次,重复关闭需手动控制。 - 关闭逻辑依赖其他条件:如根据运行结果决定是否关闭资源。
- 需多次关闭同一资源:
七、经典案例参考
- Java 标准库:
java.util.Scanner
、java.sql.Connection
均支持try-with-resources
。 - 框架示例:
// Spring JDBC 使用 try-with-resources try (JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource)) { String sql = "SELECT * FROM users"; List<User> users = jdbcTemplate.query(sql, new UserRowMapper()); }
八、总结
try-with-resources
通过自动关闭资源和简洁语法,显著提升了代码的可读性和健壮性,减少了资源泄漏风险。在资源管理场景中,优先选择 try-with-resources
,仅在复杂关闭逻辑或老版本 Java(无 try-with-resources
支持)时使用 try-finally
。
(5)固定资源首选使用依赖注入
(6)避免创建不需要的对象并手动清除过期对象
第03章:对于所有对象都通用的方法
(1)重写equal()和hashCode()
(2)重写toString()
(3)重写clone()
一、核心观点
1. 避免浅拷贝陷阱
- 问题:默认的
Object.clone()
实现浅拷贝(仅复制字段值,而非引用对象的深层内容),可能导致数据不一致。 - 示例:
public class ShallowClone implements Cloneable { private int id; private List<String> data; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 浅拷贝 } } // 使用示例: ShallowClone original = new ShallowClone(); original.data.add("value"); ShallowClone cloned = (ShallowClone) original.clone(); cloned.data.add("new value"); // 原对象的 data 也被修改!
2. 深拷贝的实现
- 解决方案:手动复制所有引用类型的成员变量(递归或显式构造新对象)。
- 示例:
public class DeepCopy implements Cloneable { private int id; private List<String> data; @Override protected Object clone() throws CloneNotSupportedException { DeepCopy cloned = (DeepCopy) super.clone(); cloned.data = new ArrayList<>(this.data); // 深拷贝集合 return cloned; } }
3. 克隆与构造函数的冲突
- 问题:
clone()
方法生成的实例应与构造函数独立,避免依赖构造函数逻辑。 - 示例:
public class BadClone { private String name; public BadClone() { this.name = "Default"; } @Override protected Object clone() throws CloneNotSupportedException { BadClone cloned = (BadClone) super.clone(); cloned.name = "Cloned"; // 强行修改属性,破坏构造函数逻辑 return cloned; } }
4. 不推荐使用 clone()
方法的原因
- 可读性与维护性差:需显式处理深拷贝逻辑,易出错。
- 与
final
字段冲突:若类包含final
字段,clone()
方法无法正确复制。 - 序列化更优:现代 Java 中推荐使用
Serializable
和readObject()
实现深拷贝。
二、代码示例分析
1. 坏代码:浅拷贝导致数据共享
public class Address implements Cloneable {
private String city;
private List<String> streets;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 未复制 streets 列表
}
}
// 使用示例:
Address addr1 = new Address();
addr1.streets.add("Main St");
Address addr2 = (Address) addr1.clone();
addr2.streets.add("Second St"); // addr1.streets 也会包含 "Second St"
2. 好代码:深拷贝实现
public class SafeAddress implements Cloneable {
private String city;
private List<String> streets;
@Override
protected Object clone() throws CloneNotSupportedException {
SafeAddress cloned = (SafeAddress) super.clone();
cloned.streets = new ArrayList<>(this.streets); // 深拷贝 streets
return cloned;
}
}
3. 坏代码:克隆方法依赖构造函数
public class Config implements Cloneable {
private String configPath;
public Config(String path) {
this.configPath = path;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return new Config(this.configPath); // 调用构造函数,违背 clone 的语义
}
}
4. 好代码:独立克隆逻辑
public class Config implements Cloneable {
private String configPath;
public Config() {} // 提供无参构造函数
@Override
protected Object clone() throws CloneNotSupportedException {
Config cloned = (Config) super.clone();
cloned.configPath = this.configPath; // 直接复制字段,无需构造函数
return cloned;
}
}
三、对比
方式 | 优点 | 缺点 |
---|---|---|
clone() 方法 | 简单快速,语法直接 | 浅拷贝风险高,维护复杂 |
拷贝构造函数 | 明确复制逻辑,支持深拷贝 | 需显式调用构造函数 |
序列化 | 自动处理深拷贝,兼容性强 | 性能开销大,需实现 Serializable 接口 |
四、适用场景
- 简单对象的浅拷贝(如
Point
、Rectangle
)。 - 需要快速复制且字段不可变(如
String
、BigInteger
)。 - 无复杂引用关系的对象(避免深拷贝开销)。
五、注意事项
-
避免与
final
字段冲突:若类包含final
字段,clone()
方法无法覆盖其值。public final class Immutable implements Cloneable { private final int value; @Override protected Object clone() throws CloneNotSupportedException { throw new UnsupportedOperationException(); // 不可克隆 } }
-
抑制克隆异常:通过
Cloneable
接口标记类可克隆,但仍需处理CloneNotSupportedException
。 -
废弃
clone()
的替代方案:- 拷贝构造函数:
public class User { private String name; public User() {} // 无参构造函数 public User(User other) { // 拷贝构造函数 this.name = other.name; } }
- 序列化:
public class SerializableExample implements Serializable { private int id; private String data; private Object readResolve() { return this; // 返回当前实例,避免重复克隆 } }
- 拷贝构造函数:
六、经典案例参考
- Java 标准库:
java.util.Date
的clone()
方法实现浅拷贝。 - 反面教材:
HashMap
的克隆方法需手动复制所有条目以避免浅拷贝问题。
七、总结
- 谨慎使用
clone()
:仅在简单场景下使用,优先考虑拷贝构造函数或序列化。 - 深拷贝必手动处理:确保所有引用类型的字段均被独立复制。
- 避免克隆与构造函数的耦合:克隆生成的实例应独立于构造函数逻辑。
通过合理设计复制机制,可显著提升代码的健壮性和可维护性。
(4)实现Comparable接口
一、核心观点
1. 定义自然顺序
- 目标:通过
Comparable<T>
接口为对象提供统一的排序规则,使其能参与集合排序(如TreeSet
)和比较操作(如Collections.sort()
)。 - 关键方法:
int compareTo(T o)
,返回负数、零或正数表示当前对象小于、等于或大于o
。
2. 常见陷阱
- 不一致的比较逻辑:
compareTo()
方法与equals()
方法逻辑冲突。 - 未处理
null
值:比较时可能抛出NullPointerException
。 - 破坏传递性:若
a < b
且b < c
,但a > c
,会导致排序失败。
3. 与 Comparator
的对比
Comparable
:定义对象的自然顺序(一种固定排序规则)。Comparator
:提供灵活的比较策略(多种排序方式)。
二、代码示例分析
1. 坏代码:Comparable
实现不当
public class User implements Comparable<User> {
private String name;
private int age;
@Override
public int compareTo(User other) {
// 未处理 null,且比较逻辑依赖于外部状态
if (name == null || other.name == null) {
return Integer.compare(age, other.age);
}
return name.compareTo(other.name); // 可能抛出 NullPointerException
}
}
2. 好代码:规范的 Comparable
实现
public class User implements Comparable<User> {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(User other) {
// 优先按年龄比较,年龄相同按姓名字典序
int ageCompare = Integer.compare(this.age, other.age);
if (ageCompare != 0) {
return ageCompare;
}
return this.name.compareTo(other.name);
}
}
3. 使用示例
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 30)
);
Collections.sort(users); // 自然顺序:年龄升序,年龄相同按姓名升序
三、Comparable
vs Comparator
对比
特性 | Comparable | Comparator |
---|---|---|
目的 | 定义对象的自然顺序 | 定义对象的多种比较策略 |
实现方式 | 类直接实现接口 | 外部类或匿名内部类实现 |
灵活性 | 单一排序规则 | 支持多规则(如按价格、评分排序) |
语法复杂度 | 需在类中重写 compareTo() 方法 | 可分离比较逻辑,代码更模块化 |
四、适用场景
- 自然顺序明确:如日期(按时间先后)、数字(按大小)。
- 唯一排序规则:如
String
的字典序、Integer
的数值顺序。 - 需要集成到标准库:如
TreeMap
、TreeSet
要求键实现Comparable
。
五、注意事项
-
一致性原则
compareTo()
方法必须与equals()
方法逻辑一致:// 错误示例:equals() 与 compareTo() 冲突 public class User { @Override public boolean equals(Object obj) { // 仅比较姓名 } @Override public int compareTo(User other) { // 比较年龄和姓名 } }
-
处理
null
值- 在比较时避免调用可能为
null
的对象的compareTo()
方法:public int compareTo(User other) { if (other == null) { throw new NullPointerException("Cannot compare with null"); } // 安全比较逻辑 }
- 在比较时避免调用可能为
-
传递性保证
- 确保比较关系满足传递性:若
a < b
且b < c
,则a < c
。 - 反例:环形比较(如石头剪刀布规则)需使用
Comparator
,而非Comparable
。
- 确保比较关系满足传递性:若
六、经典案例参考
- Java 标准库:
String
(按字典序)、LocalDate
(按日期先后)。 - 反面教材:早期版本的
Integer
比较(未优化前存在性能问题)。
七、总结
- 优先使用
Comparable
:当对象有明确的自然顺序时,简化排序逻辑。 - 配合
Comparator
:当需要灵活的多规则排序时,使用Comparator
(如Collections.sort(users, Comparator.comparingInt(User::getAge))
)。 - 严格遵循原则:确保一致性、处理
null
和传递性,避免逻辑漏洞。
通过合理实现 Comparable
接口,可以显著提升代码的可维护性和功能性,使其更好地融入 Java 集合框架。
第04章:类和接口
(1)使可变性最小化
(2)接口优先于抽象类
(3)优先使用静态类
一、核心观点
1. 问题:非静态成员类的潜在问题
- 内存泄漏风险:非静态成员类(Inner Class)默认持有外围类(Outer Class)的引用,若未被及时回收,可能导致内存泄漏。
- 不必要的耦合:非静态成员类强制依赖外围类的实例,增加了代码的耦合性。
2. 解决方案:静态成员类
- 优势:
- 无额外开销:不持有外围类引用,内存占用更小。
- 更灵活:适用于工具类或与外围类无关的辅助类。
- 线程安全:避免因持有外围类引用导致的并发问题。
二、代码示例分析
1. 坏代码:非静态成员类导致内存泄漏
public class OuterClass {
private String data = "Sensitive Data";
// 非静态成员类
public class InnerClass {
public void doSomething() {
System.out.println("Data: " + data);
}
}
}
// 使用示例:
public class Client {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
// 持有 InnerClass 实例的引用,但未使用
OuterClass.InnerClass inner = outer.new InnerClass();
// 如果 outer 被丢弃,但 inner 仍持有 outer 的引用,导致内存泄漏
}
}
2. 好代码:静态成员类替代
public class OuterClass {
private String data = "Sensitive Data";
// 静态成员类
public static class StaticInnerClass {
public void doSomething() {
System.out.println("Data: " + OuterClass.getData()); // 需通过静态方法访问外围类数据
}
}
// 提供静态方法暴露数据(可选)
public static String getData() {
return data;
}
}
// 使用示例:
public class Client {
public static void main(String[] args) {
// 直接创建静态成员类实例,无需 OuterClass 对象
OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
inner.doSomething();
}
}
三、静态成员类 vs 非静态成员类对比
特性 | 静态成员类 | 非静态成员类 |
---|---|---|
对外围类的依赖 | 无(不持有外围类引用) | 强制持有外围类引用 |
内存占用 | 更小 | 更大(可能引发内存泄漏) |
访问权限 | 仅能访问外围类的 static 成员 | 可访问外围类的所有成员(包括实例变量) |
使用场景 | 工具类、算法实现、与外围类无关的功能 | 需要访问外围类实例状态的场景 |
四、适用场景
- 纯工具类:如排序算法、字符串处理工具。
- 事件监听器:若监听器不需要访问外围类的实例状态。
- 避免内存泄漏:长期存活的对象(如缓存、线程池)应避免使用非静态成员类。
五、注意事项
-
访问外围类数据:
- 静态成员类需通过静态方法访问外围类的非静态成员(如示例中的
getData()
)。 - 若需频繁访问外围类数据,可考虑将数据设为
static
(如常量)或重构设计。
- 静态成员类需通过静态方法访问外围类的非静态成员(如示例中的
-
与匿名内部类的区别:
- 匿名内部类通常是临时的、一次性使用的,而静态成员类更适合复用。
- Java 8+ 的接口静态成员类:
public interface RunnableWithFactory { void run(); // 静态成员类实现工厂方法 static RunnableWithFactory create() { return () -> System.out.println("Hello"); } }
六、经典案例参考
- Java 标准库:
java.util.concurrent.ThreadLocal
的withInitial()
方法使用静态成员类简化初始化逻辑。 - 框架示例:Spring 框架中
@Component
注解的静态内部类用于实现依赖注入。
七、总结
- 优先选择静态成员类:若无需访问外围类实例状态,可显著减少内存占用和耦合性。
- 保留非静态成员类:仅在需要访问外围类非静态成员时使用(如事件处理器)。
- 避免滥用:过度使用静态成员类可能导致代码分散,需权衡设计简洁性与可读性。
通过合理设计成员类类型,可以提升代码的性能、可维护性和健壮性。
第05章:优先使用泛型
一、核心观点
1. 类型安全保障
- 问题:原始类型(如
List
)存在类型转换风险,易引发ClassCastException
。 - 解决方案:使用泛型约束类型,编译器在编译阶段即可检查类型合法性。
- 示例:
// 原始类型:存在运行时风险 List list = new ArrayList(); list.add(1); String s = (String) list.get(0); // ClassCastException
2. 消除类型转换
- 优势:泛型在编译时完成类型检查,避免运行时类型转换。
- 示例:
// 泛型类型:编译时检查类型 List<Integer> numbers = new ArrayList<>(); numbers.add(1); int num = numbers.get(0); // 无需类型转换
3. 代码复用与抽象
- 优势:通过泛型编写通用代码(如
Collections.sort()
),适用于多种数据类型。 - 示例:
// 泛型方法:对任意类型数组排序 public static <T extends Comparable<T>> void sortArray(T[] array) { Arrays.sort(array); }
4. 避免原始类型警告
- 问题:使用原始类型会触发编译器警告(如
unchecked
),提示潜在风险。 - 解决方案:始终使用泛型替代原始类型。
二、代码示例分析
1. 坏代码:原始类型的使用
public class Box {
private Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
// 使用示例:
Box box = new Box();
box.setItem("Hello");
String s = (String) box.getItem(); // 可能抛出 ClassCastException
2. 好代码:泛型改进
public class GenericBox<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// 使用示例:
GenericBox<String> box = new GenericBox<>();
box.setItem("Hello");
String s = box.getItem(); // 类型安全,无需转换
3. 坏代码:泛型使用不当(通配符滥用)
public static void printList(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); // 输出 Object 类型,失去泛型信息
}
}
4. 好代码:合理使用通配符
public static void printList(List<? extends Number> list) {
for (Number num : list) { // 泛型擦除后仍保留类型约束
System.out.println(num);
}
}
三、泛型 vs 原始类型的对比
特性 | 泛型 | 原始类型 |
---|---|---|
类型安全 | 编译器检查,避免运行时异常 | 依赖手动转换,易出错 |
代码可读性 | 明确类型约束,提高可读性 | 类型模糊,需额外注释 |
性能影响 | 编译器擦除后运行时性能无显著差异 | 无明显区别 |
兼容性 | 支持 Java 5+,与老代码需谨慎混用 | 兼容所有 Java 版本 |
四、适用场景
- 集合框架:
List<T>
、Map<K, V>
等必须使用泛型确保类型安全。 - 通用工具类:如排序、遍历方法(需泛型参数
T
)。 - API 设计:接口和抽象类需声明泛型类型以提高复用性。
五、注意事项
-
泛型擦除:
- Java 泛型在运行时会擦除类型信息(如
List<Integer>
变为List
),需通过instanceof
或反射处理类型(谨慎使用)。
- Java 泛型在运行时会擦除类型信息(如
-
通配符边界:
- 上界(
? extends T
):用于读取操作(如List<? extends Number>
)。 - 下界(
? super T
):用于写入操作(如List<? super Integer>
)。
- 上界(
-
避免泛型堆污染:
- 不要将原始类型赋值给泛型类型变量(如
List<String> list = new ArrayList();
会触发警告)。
- 不要将原始类型赋值给泛型类型变量(如
-
有限制的类型参数:
- 使用
extends
或super
明确类型约束(如<T extends Comparable<T>>
)。
- 使用
六、经典案例参考
- Java 标准库:
java.util.Collections
中的泛型方法(如addAll(Collection<? extends T> c)
)。 - 框架示例:Spring 框架的
@Autowired
注解支持泛型类型注入。
七、总结
- 优先使用泛型:在集合、工具类和 API 设计中强制使用泛型,确保类型安全和代码复用。
- 合理处理通配符:通过上下界约束(
? extends
/? super
)优化泛型灵活性。 - 避免原始类型:将编译器警告视为代码缺陷,及时修复泛型相关问题。
通过泛型,可以显著提升代码的健壮性和可维护性,减少运行时错误。
第06章: 枚举和注解
一、枚举(Enums)
1. 核心优势
- 类型安全:替代魔法数字或字符串常量,避免类型错误。
- 简洁易读:集中管理一组相关常量。
- 可扩展性:支持添加方法、字段和实现接口。
2. 代码示例分析
-
坏代码:传统常量集合
public class Constants { public static final int STATUS_SUCCESS = 1; public static final int STATUS_ERROR = -1; } // 使用时需手动记忆值含义 int result = Constants.STATUS_ERROR;
-
好代码:枚举实现
public enum StatusCode { SUCCESS(1), ERROR(-1), PENDING(0); private final int code; StatusCode(int code) { this.code = code; } public int getCode() { return code; } } // 使用时语义清晰 StatusCode status = StatusCode.ERROR; System.out.println(status.getCode()); // 输出 -1
3. 高级用法
-
实现接口:
public interface Runnable { void run(); } public enum Operation implements Runnable { ADD { @Override public void run() { System.out.println("Add operation"); } }, SUBTRACT { @Override public void run() { System.out.println("Subtract operation"); } }; }
-
静态工厂方法:
public enum Planet { MERCURY(3.3e6), VENUS(2.25e6), EARTH(1.0); // ... 其他行星 private final double orbitalPeriod; Planet(double orbitalPeriod) { this.orbitalPeriod = orbitalPeriod; } public static Planet fromName(String name) { switch (name) { case "MERCURY": return MERCURY; case "VENUS": return VENUS; default: throw new IllegalArgumentException(); } } }
二、注解(Annotations)
1. 核心优势
- 元数据标记:为代码添加附加信息,无需修改逻辑。
- 与编译器/工具集成:如@Override检测、单元测试框架标记。
- 运行时处理:通过反射读取注解信息。
2. 代码示例分析
-
坏代码:冗余标记
public class Config { // 手动标记配置项 public static final String DB_URL = "jdbc:mysql://localhost:3306/mydb"; public static final String DB_USER = "root"; }
-
好代码:自定义注解
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DbConfig { String url(); String user(); String password(); } public class DatabaseConfig { @DbConfig(url = "jdbc:mysql://localhost:3306/mydb", user = "root", password = "password") private String config; } // 通过反射读取注解 public class AnnotationProcessor { public static void process(DatabaseConfig config) { DbConfig annotation = config.getClass() .getDeclaredField("config") .getAnnotation(DbConfig.class); System.out.println("DB URL: " + annotation.url()); } }
3. 常见注解使用
- @Override:确保方法覆盖父类。
@Override public void toString() { /* 重写逻辑 */ }
- @Deprecated:标记过时方法。
@Deprecated public void oldMethod() { /* 旧实现 */ }
- @SuppressWarnings:抑制编译器警告。
@SuppressWarnings("unchecked") public void processList(List rawList) { /* 类型未检查操作 */ }
三、枚举 vs 常量集合 vs 注解
特性 | 枚举 | 传统常量集合 | 注解 |
---|---|---|---|
类型安全 | ✅ 支持编译时检查 | ❌ 依赖开发者记忆 | ✅ 可通过工具约束(如自定义注解处理器) |
可读性 | ✅ 语义清晰 | ❌ 魔法数字难以维护 | ✅ 标记代码意图 |
扩展性 | ✅ 支持添加方法和字段 | ❌ 仅能定义静态常量 | ✅ 可动态处理运行时信息 |
适用场景 | 固定状态码、选项集合 | 简单常量集合(无需扩展功能) | 代码标记、框架集成(如AOP) |
四、适用场景
-
枚举:
- 固定状态码(如HTTP状态码)。
- 一组相关常量的逻辑分组(如星期、颜色)。
- 实现接口或包含行为的常量类。
-
注解:
- 标记代码元数据(如配置项、缓存键)。
- 与编译器或工具链集成(如单元测试、依赖注入)。
- AOP框架中的切面标记(如Spring的
@Transactional
)。
五、注意事项
-
枚举:
- 避免过度使用:枚举会增加类文件大小,复杂场景需权衡。
- 序列化问题:枚举实例序列化后需保证版本兼容性。
-
注解:
- 元注解:合理使用
@Retention
(运行时/编译时)、@Target
(字段/方法/类)。 - 禁止滥用:避免用注解替代业务逻辑或破坏封装性。
- 性能开销:反射读取注解存在一定性能损耗,需谨慎高频调用。
- 元注解:合理使用
六、经典案例参考
- Java标准库:
- 枚举:
java.util.concurrent.TimeUnit
(时间单位)。 - 注解:
java.lang.annotation.Override
、java.lang.annotation.Retention
。
- 枚举:
- 框架示例:
- Spring的
@RequestMapping
(注解驱动MVC)。 - Hibernate的
@Entity
(JPA实体标记)。
- Spring的
七、总结
- 枚举:适用于需要类型安全和可读性的固定常量场景,是传统常量集合的现代化替代。
- 注解:通过元数据标记提升代码灵活性,是框架集成和自动化工具的核心机制。
- 最佳实践:
- 用枚举替代魔法数字和字符串常量。
- 用注解标记代码意图,而非直接嵌入逻辑。
- 避免自定义注解过度复杂化代码。
第07章:Lambda和Stream
一、Lambda表达式
1. 核心优势
- 简洁性:替代冗长的匿名内部类,减少样板代码。
- 函数式接口:通过
@FunctionalInterface
定义单一抽象方法的接口(如Predicate<T>
、Consumer<T>
)。 - 可传递性:可作为参数传递给方法(如
Collections.forEach
)。
2. 代码示例分析
-
坏代码:匿名内部类实现
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello, World!"); } }; new Thread(runnable).start();
-
好代码:Lambda表达式
// 简洁且语义清晰 new Thread(() -> System.out.println("Hello, World!")).start();
3. 函数式接口与默认方法
- 示例:
@FunctionalInterface interface MathOperation { int operate(int a, int b); } public class Calculator { public static void main(String[] args) { MathOperation add = (a, b) -> a + b; System.out.println(add.operate(2, 3)); // 输出 5 } }
二、Stream API
1. 核心优势
- 声明式编程:通过链式调用中间操作(如
filter
、map
)和终端操作(如collect
、count
)处理集合数据。 - 惰性求值:中间操作不会立即执行,仅在终端操作触发时计算。
- 并行流支持:自动利用多核CPU提升处理速度。
2. 代码示例分析
-
坏代码:传统集合遍历
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = new ArrayList<>(); for (int num : numbers) { if (num % 2 == 0) { evenNumbers.add(num * 2); } } System.out.println(evenNumbers); // 输出 [2, 4, 10]
-
好代码:Stream API
List<Integer> evenNumbers = Arrays.asList(1, 2, 3, 4, 5) .stream() // 创建流 .filter(n -> n % 2 == 0) // 过滤偶数 .map(n -> n * 2) // 映射为原值的两倍 .collect(Collectors.toList()); // 收集结果 System.out.println(evenNumbers); // 输出 [2, 4, 10]
3. 短路操作
- 示例:
// 找到第一个大于10的元素 OptionalInt firstLarge = Arrays.stream(new int[]{1, 5, 15, 8}) .filter(n -> n > 10) .findFirst(); // 短路操作,找到后立即终止 System.out.println(firstLarge.orElse(-1)); // 输出 15
三、Lambda与Stream的对比
特性 | Lambda表达式 | Stream API |
---|---|---|
目标 | 替代匿名内部类,简化函数式代码 | 声明式处理集合数据 |
语法风格 | 匿名函数式表达式 | 链式调用中间操作和终端操作 |
适用场景 | - 回调函数(如事件监听器) - 简单算法实现 | - 集合数据处理 - 复杂数据转换和分析 |
核心接口 | @FunctionalInterface 的函数式接口(方法引用) | Stream<T> 和收集器(Collectors ) |
四、适用场景
-
Lambda表达式:
- 事件处理(如按钮点击监听器)。
- 简单算法或转换逻辑(如排序比较器)。
- 并行任务(如
CompletableFuture
)。
-
Stream API:
- 数据过滤、映射、聚合(如统计、分组)。
- 大数据处理和批处理任务。
- 并行流计算(利用多核CPU加速)。
五、注意事项
-
线程安全问题
- 并行流陷阱:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 错误:并行流中对共享变量进行非原子操作 numbers.parallelStream().forEach(n -> { System.out.println(Thread.currentThread().getId() + ": " + n); });
- 解决方案:避免在并行流中使用可变共享状态。
- 并行流陷阱:
-
性能开销
- 过度使用中间操作:如
distinct()
、sorted()
可能增加计算成本,需结合实际场景权衡。 - 避免不必要的装箱/拆箱:使用原始类型流(如
IntStream
)提升性能。
- 过度使用中间操作:如
-
函数式接口的陷阱
- 歧义方法:Lambda表达式可能因参数类型不匹配引发错误。
// 错误:lambda参数类型不明确 Comparator<String> comparator = (s1, s2) -> s1.length() - s2.length();
- 歧义方法:Lambda表达式可能因参数类型不匹配引发错误。
六、经典案例参考
- Java标准库:
java.util.function
包中的函数式接口(如Predicate<T>
、Consumer<T>
)。java.util.stream
中的流操作(如flatMap
、reduce
)。
- 框架示例:
- Spring Data JPA 的查询方法(使用
Pageable
和 Lambda 表达式)。 - Reactor 或 Vert.x 的响应式编程(基于函数式风格的回调)。
- Spring Data JPA 的查询方法(使用
七、总结
- Lambda表达式:用简洁的函数式语法替代冗长的匿名内部类,提升代码可读性。
- Stream API:通过声明式数据处理和惰性求值,简化集合操作逻辑。
- 最佳实践:
- 在需要函数式回调的场景优先使用Lambda。
- 处理复杂集合操作时优先使用Stream API。
- 并行流需谨慎评估线程安全和性能收益。
通过合理使用Lambda和Stream,可以显著提升Java代码的简洁性和表达能力,同时适应函数式编程和大数据处理的需求。
方法引用(:😃
在使用方法引用可以更简短更清晰的地方,就使用方法引用,如果无法使代码更简短更清晰的地方就坚持使用 lambda
第08章:方法(综合《代码简洁之道》)
(1)驼峰有意义的命名
(2)功能专一
(3)行数尽可能<30
(4)参数<3个
(5)尽可能没有返回值
(6)空数组或集合来代替返回null
(7)谨慎返回optional
(8)不使用重载
第09章:通用编程
(1)for-each循环优先于传统的for循环
(2)BigDecimal代替float或double
(3)基本类型优先于封装类
(4)StringBuilder拼接字符
(5)尽量不使用本地方法(C++库)
第10章:异常
一、核心观点
1. 异常用于错误处理,而非控制流程
- 问题:不应通过抛出异常来替代条件判断(如循环退出或分支逻辑)。
- 示例:
// 坏代码:用异常控制循环退出 public void processInput(String input) { while (true) { try { int number = Integer.parseInt(input); break; } catch (NumberFormatException e) { System.out.println("Invalid input, please try again."); } } }
2. 区分 Checked 和 Unchecked 异常
- Checked Exception(如
IOException
):必须在代码中显式处理(try-catch
或声明抛出)。 - Unchecked Exception(如
NullPointerException
):继承自RuntimeException
,无需强制处理。 - 原则:
- 用 Checked Exception 表示可恢复的错误(如文件未找到)。
- 用 Unchecked Exception 表示程序逻辑错误(如无效参数)。
3. 避免空的 catch 块
- 问题:空的
catch
块会掩盖异常细节,导致调试困难。 - 示例:
// 坏代码:忽略异常 try { readFile("nonexistent.txt"); } catch (IOException e) { // 空 catch 块 }
4. 提供有意义的异常信息
- 优势:通过异常消息和上下文信息(如参数、文件路径)快速定位问题。
- 示例:
// 好代码:包含详细信息 public void divide(int a, int b) throws IllegalArgumentException { if (b == 0) { throw new IllegalArgumentException("Division by zero: a=" + a); } System.out.println(a / b); }
5. 自定义异常的合理使用
- 问题:滥用自定义异常会增加代码复杂性。
- 原则:仅在以下场景创建自定义异常:
- 捕捉到多个 Checked Exception 需统一处理。
- 需要向调用者传递特定业务错误码。
二、代码示例分析
1. 坏代码:用异常控制流程
public class FileProcessor {
public String readFile(String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException e) {
throw new RuntimeException("Failed to read file", e);
}
}
}
- 问题:未处理
IOException
,直接抛出RuntimeException
,隐藏原始错误细节。
2. 好代码:合理分层异常处理
public class FileProcessor {
public String readFile(String filePath) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filePath));
return new String(data);
}
}
// 调用方:
public class Client {
public static void main(String[] args) {
try {
String content = new FileProcessor().readFile("config.txt");
System.out.println(content);
} catch (IOException e) {
System.err.println("Error reading config file: " + e.getMessage());
e.printStackTrace();
}
}
}
3. 坏代码:空的 catch 块
public class DatabaseService {
public void updateUser(int userId, String newName) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db")) {
String sql = "UPDATE users SET name = ? WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, newName);
pstmt.setInt(2, userId);
pstmt.executeUpdate();
} catch (SQLException e) {
// 空 catch 块,未记录日志或重新抛出
}
}
}
4. 好代码:记录异常并重新抛出
public class DatabaseService {
private static final Logger logger = LoggerFactory.getLogger(DatabaseService.class);
public void updateUser(int userId, String newName) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db")) {
String sql = "UPDATE users SET name = ? WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, newName);
pstmt.setInt(2, userId);
pstmt.executeUpdate();
} catch (SQLException e) {
logger.error("Error updating user with ID {}: {}", userId, e.getMessage(), e);
throw new DataAccessException("Failed to update user", e);
}
}
}
三、Checked vs Unchecked 异常对比
特性 | Checked Exception | Unchecked Exception |
---|---|---|
编译器检查 | 是(强制处理) | 否(运行时才抛出) |
典型场景 | I/O 操作、数据库访问、网络通信 | 逻辑错误、参数无效、空指针 |
处理方式 | try-catch 或 throws | 无需显式处理(可捕获或忽略) |
自定义异常 | 建议通过继承 Exception 实现 | 建议通过继承 RuntimeException 实现 |
四、适用场景
-
Checked Exception:
- 文件读写、网络请求、数据库操作等可能失败的 I/O 任务。
- 需要调用方明确处理恢复逻辑的场景(如重试、回滚事务)。
-
Unchecked Exception:
- 程序逻辑错误(如无效参数、索引越界)。
- 无法或难以恢复的致命错误(如
NullPointerException
)。
五、注意事项
-
异常链(Chained Exceptions)
- 通过
initCause()
或Throwable
构造函数传递原始异常:public class DataAccessException extends RuntimeException { public DataAccessException(String message, Throwable cause) { super(message, cause); } }
- 通过
-
避免过度包装异常
- 不要为每个异常都创建自定义类,导致类膨胀。
-
finally 块的局限性
- 问题:
finally
块中的代码可能因异常被跳过(如System.exit()
)。 - 示例:
public void closeResource() { Resource resource = new Resource(); try { resource.open(); } finally { resource.close(); // 总会被执行,除非 JVM 退出 } }
- 问题:
-
日志记录的最佳实践
- 捕获异常时记录堆栈跟踪(通过
printStackTrace()
或日志框架)。 - 避免重复记录:在高层代码记录异常后,低层代码可仅抛出异常。
- 捕获异常时记录堆栈跟踪(通过
六、经典案例参考
- Java 标准库:
IOException
(Checked Exception)用于文件操作。NullPointerException
(Unchecked Exception)用于空指针检查。
- 框架示例:
- Spring 框架的
DataAccessException
自定义异常,封装 JDBC 异常。 - Hibernate 的
ConstraintViolationException
处理数据校验失败。
- Spring 框架的
七、总结
- 优先使用 Checked Exception:处理可恢复的 I/O 或外部服务错误。
- 用 Unchecked Exception:标记程序逻辑错误,避免强制调用方处理。
- 避免空 catch 块:始终记录异常或重新抛出。
- 自定义异常需谨慎:仅在必要时抽象公共错误类型。
通过合理设计异常处理机制,可以提升代码的健壮性和可维护性,同时简化调试和错误排查流程。
第11章:并发
一、核心观点
1. 线程安全类设计
- 问题:多线程环境下,共享数据的非原子操作可能导致竞态条件(Race Condition)和内存可见性问题。
- 解决方案:
- 原子操作:使用
synchronized
关键字或java.util.concurrent.atomic
包。 - 不可变对象:通过
final
字段和不可变类(如String
)确保线程安全。
- 原子操作:使用
2. 同步机制的选择
synchronized
关键字:简单易用,但可能导致阻塞和性能瓶颈。Lock
接口(如ReentrantLock
):提供更灵活的控制(如尝试加锁、超时机制)。
3. 并发集合框架
- Java 并发包(
java.util.concurrent
)提供线程安全的集合类(如ConcurrentHashMap
、CopyOnWriteArrayList
),显著优于传统的ArrayList
/HashMap
。
4. 原子操作类
AtomicInteger
/LongAdder
:支持无锁的原子递增操作,适用于高并发计数场景。
5. 线程池与任务调度
ExecutorService
:避免频繁创建线程,复用线程资源。ThreadPoolExecutor
:自定义线程池参数(核心线程数、最大线程数等)。
二、代码示例分析
1. 坏代码:线程不安全的ArrayList
public class UnsafeList {
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
list.add(i);
}
};
// 启动多个线程
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
// 结果可能小于 1000(数据丢失)
System.out.println("List size: " + list.size());
}
}
2. 好代码:使用并发集合
public class SafeList {
private static List<Integer> list = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
list.add(i);
}
};
// 启动多个线程
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
// 结果一定是 1000
System.out.println("List size: " + list.size());
}
}
3. 坏代码:滥用 synchronized
方法
public class BankAccount {
private int balance = 100;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}
- 问题:每个方法都加锁,导致不必要的阻塞(如
deposit
和withdraw
无法并发执行)。
4. 好代码:细粒度锁优化
public class BankAccount {
private final Object lock = new Object();
private int balance = 100;
public void deposit(int amount) {
synchronized (lock) {
balance += amount;
}
}
public void withdraw(int amount) {
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
}
}
}
}
- 改进:使用单独的锁对象,减少锁的粒度,提高并发性能。
三、同步机制对比
特性 | synchronized 关键字 | Lock 接口 |
---|---|---|
语法简洁性 | ✅ 直接修饰方法或代码块 | ❌ 需手动调用 lock() /unlock() |
灵活性 | ❌ 无法中断等待锁的线程 | ✅ 支持 tryLock() (可超时)和 lockInterruptibly() |
性能 | ✅ 轻量级锁优化(偏向锁、轻量级锁) | ❌ 可能引入额外开销 |
适用场景 | 简单同步需求(如计数器) | 复杂并发控制(如生产者-消费者模型) |
四、适用场景
-
原子操作:
- 使用
AtomicInteger
替代int
类型的计数器。AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // 原子递增
- 使用
-
并发集合:
ConcurrentHashMap
替代HashMap
(避免死锁和数据不一致)。ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.putIfAbsent("key", 1); // 原子插入
-
线程池:
- 使用
ExecutorService
执行批量任务:ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { executor.submit(() -> System.out.println("Task executed by " + Thread.currentThread().getName())); } executor.shutdown();
- 使用
五、注意事项
-
避免死锁:
- 确保线程获取锁的顺序一致,避免循环等待。
// 错误示例:死锁风险 synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } }
- 确保线程获取锁的顺序一致,避免循环等待。
-
可见性问题:
- 使用
volatile
关键字确保变量的可见性:private volatile boolean running = true;
- 使用
-
线程池大小调优:
- 核心线程数和最大线程数的设置需根据任务类型(CPU密集型或IO密集型)调整。
- 处理中断异常:
- 捕获
InterruptedException
并恢复中断状态:public void run() { while (!Thread.currentThread().isInterrupted()) { try { // 可中断操作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断标志 break; } } }
- 捕获
六、经典案例参考
- Java 标准库:
java.util.concurrent.atomic.AtomicInteger
(原子操作)。java.util.concurrent.locks.ReentrantLock
(可重入锁)。
- 框架示例:
- Spring 框架的
@Async
注解(基于线程池的异步方法调用)。 - Netty 的
EventLoopGroup
(非阻塞I/O模型)。
- Spring 框架的
七、总结
- 优先使用并发集合和原子类:避免手动同步带来的复杂性和性能损耗。
- 合理设计锁粒度:细粒度锁减少线程阻塞,提高并发性能。
- 善用线程池:控制线程数量,避免资源耗尽。
- 处理中断和异常:确保程序健壮性,避免死锁和资源泄漏。
通过合理应用并发工具和遵循最佳实践,可以显著提升Java程序的性能和可靠性,适应高并发场景的需求。
第12章:序列化
一、核心观点
1. 序列化机制的作用
- 问题:对象需跨网络传输或持久化存储时,需将其转换为字节流。
- 解决方案:通过
Serializable
接口标记类,实现writeObject()
和readObject()
方法自定义序列化逻辑。
2. 潜在风险与优化
- 性能开销:默认序列化(
Serializable
)较慢且生成冗余数据。 - 安全漏洞:恶意反序列化可能导致远程代码执行(RCE)。
- 版本兼容性:类结构变更后需处理序列化版本冲突(
serialVersionUID
)。
3. 替代方案
Externalizable
:手动控制序列化流程,提升性能。- JSON/XML 序列化:如 Jackson、Gson,更灵活且易读。
二、代码示例分析
1. 坏代码:默认序列化的问题
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient int age; // 不参与序列化
// 默认序列化会保存所有非 transient 字段
}
// 使用示例:
User user = new User();
user.setName("Alice");
user.setAge(30);
// 序列化后 age 字段丢失
byte[] data = serialize(user);
User deserializedUser = deserialize(data);
System.out.println(deserializedUser.getAge()); // 输出 0(默认值)
2. 好代码:自定义序列化逻辑
public class User implements Externalizable {
private String name;
private int age;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}
// 使用示例:
User user = new User();
user.setName("Bob");
user.setAge(25);
byte[] data = serialize(user);
User deserializedUser = deserialize(data);
System.out.println(deserializedUser.getAge()); // 输出 25
3. 坏代码:未声明 serialVersionUID
public class Book implements Serializable {
private String title;
private int publicationYear;
}
// 修改类结构后未更新 serialVersionUID,导致反序列化失败
public class Book {
private String title;
private String author; // 新增字段
private int publicationYear;
}
4. 好代码:显式声明 serialVersionUID
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private int publicationYear;
}
三、序列化机制对比
特性 | 默认 Serializable | Externalizable | JSON/XML 序列化 |
---|---|---|---|
控制权 | 自动生成字节流(不可控) | 手动控制序列化/反序列化逻辑 | 完全自定义格式(如字段顺序、忽略空值) |
性能 | 较低(冗余数据多) | 较高(仅写入必要字段) | 较高(高效解析库支持) |
安全性 | 存在反序列化漏洞 | 可拦截非法数据 | 依赖解析库的安全配置 |
版本兼容性 | 依赖 serialVersionUID | 手动处理字段变更 | 易扩展(新增字段不影响旧客户端) |
四、适用场景
- 默认
Serializable
:快速实现简单对象的持久化,无需手动编码。 Externalizable
:需要高性能或完全控制序列化流程的场景(如网络传输)。- JSON/XML:跨平台通信、Web API 或配置文件存储(人类可读性高)。
五、注意事项
-
安全防护:
- 使用安全可靠的序列化库(如 Jackson 的
@JsonTypeInfo
防止类型混淆)。 - 避免反序列化未知来源的数据(防止 RCE 攻击)。
- 使用安全可靠的序列化库(如 Jackson 的
-
性能优化:
- 对大数据量对象使用
BufferedOutputStream
加速写入。 - 使用
SerializationProxy
实现轻量级代理序列化(Java 9+)。
- 对大数据量对象使用
-
字段管理:
transient
关键字:标记不需要序列化的字段(如临时缓存)。writeObject()
/readObject()
:手动处理敏感字段(如密码)。
六、经典案例参考
- Java 标准库:
java.io.Serializable
接口的基础实现。java.util.zip.GZIPInputStream
结合序列化压缩数据。
- 框架示例:
- Hibernate 的
hibernate Serialization
提供ORM 序列化支持。 - Jackson 的
ObjectMapper
实现 JSON 序列化(示例代码见下文)。
- Hibernate 的
七、总结
- 优先选择 JSON/XML:在跨平台或需要灵活格式的场景中,优先使用 Jackson/Gson 等库。
- 谨慎使用默认序列化:仅用于简单场景,避免性能和安全风险。
- 手动控制序列化流程:通过
Externalizable
或代理模式优化性能与安全性。
// Jackson JSON 序列化示例
public class JsonSerializerExample {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
User user = new User("Charlie", 35);
String json = mapper.writeValueAsString(user);
System.out.println(json); // {"name":"Charlie","age":35}
User deserializedUser = mapper.readValue(json, User.class);
System.out.println(deserializedUser.getAge()); // 输出 35
}
}
通过合理设计序列化策略,可以在性能、安全和可维护性之间取得平衡。