Effective Java笔记:使可变性最小化

使可变性最小化

在面向对象编程中,最小化可变性(Immutability) 是一种重要的设计原则。不可变对象(Immutable Object)是指其状态在对象创建后不能被更改的对象。不可变对象具有简单性、健壮性和线程安全性。

《Effective Java》中明确指出,不可变对象的设计是代码安全性和可维护性的基础。在很多场景下,我们应尽可能减少类的可变性,甚至设计为完全不可变。如果类必须是可变的,则需要限制其可变范围。


1. 为什么要最小化可变性?

  1. 线程安全性(Thread-Safety)

    • 不可变对象是天然的线程安全的,多个线程可以同时访问它而无需额外同步,减少了并发编程的复杂性。
  2. 易于设计和使用(Simpler Design)

    • 不可变对象的 API 更加简单和安全,并且可以防止调用者对不应修改的字段进行更改。
  3. 提高代码的安全性

    • 减少了由于对象状态修改带来的潜在问题,例如字段被外部代码意外或恶意篡改的风险。
  4. 更容易维护

    • 不需要担心状态的变化影响行为,可以减少调试和维护成本。
  5. 缓存友好

    • 不可变对象的属性不会发生变化,因此可以被安全地用作缓存的键值。

2. 如何设计不可变类?

要点:设计不可变类需要遵循以下原则
  1. 将类声明为 final,防止子类继承和扩展

    • 子类可能会引入可变状态,破坏不可变性。
  2. 将所有的字段都声明为 privatefinal

    • private 确保字段不能被直接访问,final 确保字段一旦初始化后不能被重新赋值。
  3. 不要提供修改对象状态的方法(没有 setter 方法)

    • 不允许通过外界调用来更改字段值。
  4. 确保对象的防御性拷贝

    • 如果类包含可变对象(如数组、集合),不要直接暴露这些字段,应该返回这些对象的副本,而不是返回其引用或原始对象。
  5. 在构造器中构造对象的全部状态

    • 确保对象在构造之后处于完全初始化状态。
  6. 确保类的字段引用的对象也不可变

    • 如果一个类的字段引用了另一个可变对象,必须将其设计为不可变,或者在调用者访问时提供拷贝。

示例:一个不可变的 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 类不可变,因为:

  1. 类被声明为 final
  2. 所有字段都为 privatefinal
  3. 没有提供任何修改字段的方法(没有 setter)。
  4. 字段 nameString 类型,而 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);
    }
}

优点:

  1. Money 类是不可变的:没有 setter 方法,也没有状态变化。
  2. 增加金额时,返回一个新的 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. 总结

  1. 对于需要经常共享或操作的类,尽量设计成不可变类。
  2. 如果类既是可变的,尽量缩小可变范围:
    • 使用 private 修饰字段,限制其可见性。
    • 提供安全的访问方法和必要的验证逻辑。
    • 返回字段副本,防止外界意外修改。
  3. 不可变的设计可以提升线程安全性、降低 bug 风险,并提高代码维护性。

核心原则:默认设计为不可变类,只有确有必要时才设计为可变类,且最小化其范围。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值