使可变性最小化
在面向对象编程中,最小化可变性(Immutability) 是一种重要的设计原则。不可变对象(Immutable Object)是指其状态在对象创建后不能被更改的对象。不可变对象具有简单性、健壮性和线程安全性。
《Effective Java》中明确指出,不可变对象的设计是代码安全性和可维护性的基础。在很多场景下,我们应尽可能减少类的可变性,甚至设计为完全不可变。如果类必须是可变的,则需要限制其可变范围。
1. 为什么要最小化可变性?
-
线程安全性(Thread-Safety)
- 不可变对象是天然的线程安全的,多个线程可以同时访问它而无需额外同步,减少了并发编程的复杂性。
-
易于设计和使用(Simpler Design)
- 不可变对象的 API 更加简单和安全,并且可以防止调用者对不应修改的字段进行更改。
-
提高代码的安全性
- 减少了由于对象状态修改带来的潜在问题,例如字段被外部代码意外或恶意篡改的风险。
-
更容易维护
- 不需要担心状态的变化影响行为,可以减少调试和维护成本。
-
缓存友好
- 不可变对象的属性不会发生变化,因此可以被安全地用作缓存的键值。
2. 如何设计不可变类?
要点:设计不可变类需要遵循以下原则
-
将类声明为
final,防止子类继承和扩展- 子类可能会引入可变状态,破坏不可变性。
-
将所有的字段都声明为
private和finalprivate确保字段不能被直接访问,final确保字段一旦初始化后不能被重新赋值。
-
不要提供修改对象状态的方法(没有
setter方法)- 不允许通过外界调用来更改字段值。
-
确保对象的防御性拷贝
- 如果类包含可变对象(如数组、集合),不要直接暴露这些字段,应该返回这些对象的副本,而不是返回其引用或原始对象。
-
在构造器中构造对象的全部状态
- 确保对象在构造之后处于完全初始化状态。
-
确保类的字段引用的对象也不可变
- 如果一个类的字段引用了另一个可变对象,必须将其设计为不可变,或者在调用者访问时提供拷贝。
示例:一个不可变的 Person 类
public final class Person {
private final String name; // 不可变字段
private final int age;
public Person(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.name = name;
this.age = age;
}
public String getName() {
return name; // 不需要返回副本,因为 String 是不可变的
}
public int getAge() {
return age;
}
}
上面这个 Person 类不可变,因为:
- 类被声明为
final。 - 所有字段都为
private和final。 - 没有提供任何修改字段的方法(没有
setter)。 - 字段
name是String类型,而String是不可变的。
3. 不可变类设计的优势
不可变类有以下优势:
3.1 线程安全性
不可变对象的状态不能被更改,因此可以安全地在线程之间共享,无需显式同步。
例子:
Person person = new Person("Alice", 25);
Thread thread = new Thread(() -> System.out.println(person.getName()));
thread.start();
多个线程可以安全地共享同一个不可变对象,无需担心数据竞争。
3.2 更易于理解和使用
不可变对象提供了一种明确的行为保证:它们的状态不会发生变化。这使 API 的使用更加直观。
3.3 较少的 Bug 概率
当对象的状态是不可变的时候,程序中许多异常状态根本不会发生。例如,不可变对象不会因为意外修改而进入不一致的状态。
3.4 安全的数据传递
不可变对象可以安全地作为参数传递至其他方法,而不需要担心它会被方法内部修改。
4. 使可变类的可变性最小化
问题:必须是可变类时,如何最小化其可变性?
尽管不可变类是优选,但在某些场景中,我们需要使用可变类(如对象需要频繁更新)。对于这样的类,我们可以通过以下方式最小化其可变性:
4.1 将类的字段设置为 private
确保类的字段不会被外部直接访问。只能通过控制良好的方法来修改它们。
4.2 使用访问方法控制修改逻辑
通过设计良好的访问方法(setter 和 getter),增加对字段的验证、约束和访问权限。
public class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount cannot be negative");
}
balance += amount;
}
}
4.3 不要直接暴露字段引用
如果类中包含可变对象,不要将这些引用直接公开,而应提供不可变的视图或防御性拷贝。
错误示范:直接暴露字段
public class Mutable {
public int[] data = {1, 2, 3}; // 公共字段
}
外部可以直接修改字段值:
Mutable mutable = new Mutable();
mutable.data[0] = 42; // 修改了原数组
正确做法:返回副本
public class Safe {
private final int[] data = {1, 2, 3};
// 返回数组副本
public int[] getData() {
return data.clone();
}
}
4.4 限制可变对象的作用域
如果某些字段是可变的,尽可能缩小它们的可见性。可以通过同步或其他机制保护它们不被滥用。
5. 实际代码示例
不可变类示例:Money 类(不可更改金额值)
public final class Money {
private final double amount;
private final String currency;
public Money(double amount, String currency) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount;
this.currency = currency;
}
public double getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
// 返回新的 Money 对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currencies do not match!");
}
return new Money(this.amount + other.amount, this.currency);
}
}
优点:
Money类是不可变的:没有 setter 方法,也没有状态变化。- 增加金额时,返回一个新的
Money对象,而不是修改原对象。
可变类的最小化示例:学生类
public class Student {
private String name;
private int age;
public Student(String name, int age) {
setName(name);
setAge(age);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.age = age;
}
}
6. 总结
- 对于需要经常共享或操作的类,尽量设计成不可变类。
- 如果类既是可变的,尽量缩小可变范围:
- 使用
private修饰字段,限制其可见性。 - 提供安全的访问方法和必要的验证逻辑。
- 返回字段副本,防止外界意外修改。
- 使用
- 不可变的设计可以提升线程安全性、降低 bug 风险,并提高代码维护性。
核心原则:默认设计为不可变类,只有确有必要时才设计为可变类,且最小化其范围。

381

被折叠的 条评论
为什么被折叠?



