杰瑞科技汇

Java可变与不可变对象,如何选择?

核心概念

  • 可变对象:对象在被创建之后,其内部的状态(即成员变量的值)可以被改变。
  • 不可变对象:对象在被创建之后,其内部的状态永远不能被改变,任何试图修改它的操作,都会返回一个新的对象,而不是修改原始对象。

可变对象

可变对象是 Java 编程中的常态,绝大多数我们自定义的类以及许多核心 API 类(如 ArrayList, HashMap, Date 等)都是可变的。

特点

  1. 状态可修改:可以通过方法(通常是 setter 方法)直接修改对象的内部属性。
  2. 引用可修改:可以改变对象的引用,使其指向另一个对象。
  3. 灵活性高:非常适合需要频繁修改状态的应用场景,例如实体类、数据模型等。

示例

import java.util.ArrayList;
import java.util.List;
public class MutableObjectExample {
    public static void main(String[] args) {
        // 1. 创建一个可变对象
        Person person = new Person("Alice", 30);
        System.out.println("原始信息: " + person); // 输出: Person{name='Alice', age=30}
        // 2. 修改对象的状态(可变性)
        person.setAge(31);
        System.out.println("修改年龄后: " + person); // 输出: Person{name='Alice', age=31}
        // 3. 修改对象的引用
        person = new Person("Bob", 25);
        System.out.println("修改引用后: " + person); // 输出: Person{name='Bob', age=25}
        // 4. List 也是可变的
        List<String> names = new ArrayList<>();
        names.add("Charlie");
        System.out.println("原始列表: " + names); // 输出: [Charlie]
        names.add("David");
        System.out.println("添加元素后: " + names); // 输出: [Charlie, David]
        names.set(0, "Charles");
        System.out.println("修改元素后: " + names); // 输出: [Charles, David]
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 提供了修改内部状态的方法,使其变得可变
    public void setAge(int age) {
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

优缺点

  • 优点
    • 灵活:可以根据业务需求随时修改对象的状态。
    • 节省内存:对于大型对象,直接修改状态比创建新对象更节省内存。
  • 缺点
    • 线程安全问题:如果多个线程同时修改一个可变对象,可能会导致数据不一致(竞态条件)。
    • 副作用:对象被传递到程序的各个部分,任何持有该引用的代码都可能修改它,导致难以追踪的错误。
    • 哈希码不稳定:如果对象作为 HashMap 的键或 HashSet 的元素,并且在修改后其 hashCode() 发生变化,会导致无法在集合中找到该对象。

不可变对象

不可变对象是并发编程的基石,也是函数式编程思想的核心,Java 中最著名的不可变对象就是 String 类。

特点

  1. 状态不可修改:对象一旦创建,其所有字段的值都不能改变。
  2. 引用可以改变:变量本身可以重新赋值,指向另一个不可变对象。
  3. 线程安全:由于状态不能被改变,多个线程可以同时访问一个不可变对象,而无需额外的同步措施,因此它们是天生线程安全的。
  4. 简单可靠:对象的状态在整个生命周期中保持不变,使得代码更容易理解和调试。

如何创建一个不可变对象?

要创建一个不可变对象,需要遵循以下规则:

  1. 将类声明为 final:防止其他类通过继承来修改其行为。
  2. 将所有字段声明为 privatefinal
    • private:防止外部直接访问和修改字段。
    • final:确保字段在初始化后不能被重新赋值。
  3. 不提供 setter 方法:只提供 getter 方法,让外部只能读取,不能修改。
  4. 确保所有可变字段也是不可变的:如果类中包含其他对象的引用(如 List, Date),这些引用所指向的对象也必须是不可变的,如果无法保证,则必须在构造函数中进行深度拷贝,以防止外部代码通过引用修改内部状态。
  5. 不要提供修改对象状态的方法:如果需要提供“修改”功能,应该返回一个新的对象实例,而不是修改当前对象。

示例

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ImmutableObjectExample {
    public static void main(String[] args) {
        // 创建一个不可变对象
        ImmutablePerson person = new ImmutablePerson("Alice", 30, List.of("Reading", "Hiking"));
        System.out.println("原始信息: " + person); // 输出: ImmutablePerson{name='Alice', age=30, hobbies=[Reading, Hiking]}
        // 1. 尝试修改基本类型字段(失败)
        // person.setAge(31); // 编译错误!没有 setAge 方法
        // 2. 尝试修改引用(成功,但只是让变量 person 指向了新对象,原对象不变)
        person = new ImmutablePerson("Bob", 25, List.of("Swimming"));
        System.out.println("变量重新赋值后: " + person); // 输出: ImmutablePerson{name='Bob', age=25, hobbies=[Swimming]}
        // 3. 尝试修改集合字段(看起来可以,但其实是安全的)
        List<String> hobbies = person.getHobbies();
        // hobbies.add("Gaming"); // 会抛出 UnsupportedOperationException!
        // 为什么?因为我们在构造函数中返回了 Collections.unmodifiableList(hobbies)
        // 所以这个列表是只读的。
        System.out.println("尝试修改集合后,原对象不变: " + person); // 输出不变
    }
}
// 1. 声明为 final
final class ImmutablePerson {
    // 2. 所有字段为 private final
    private final String name;
    private final int age;
    private final List<String> hobbies; // 注意:List 本身是可变的
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // 4. 对可变字段进行防御性拷贝
        this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
    }
    // 3. 只提供 getter
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    public List<String> getHobbies() {
        // 返回一个不可变的视图,防止外部修改
        return hobbies;
    }
    @Override
    public String toString() {
        return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
    }
}

优缺点

  • 优点
    • 线程安全:天生不需要同步,非常适合多线程环境。
    • 简单可靠:没有副作用,行为可预测,易于测试和调试。
    • 作为哈希键的完美选择:因为状态不变,hashCode()equals() 的结果也永远不变,能正确地在 HashMapHashSet 中工作。
    • 保护性拷贝:可以作为防御性编程的一部分,防止内部数据被意外修改。
  • 缺点
    • 性能开销:每次“修改”都需要创建新对象,可能会带来性能损耗和内存压力。
    • 代码冗长:创建不可变对象需要遵循更多规则,代码量相对较多。

总结与对比

特性 可变对象 不可变对象
状态修改 允许在创建后修改内部状态。 创建后,内部状态永远不能修改。
线程安全 不安全,需要额外的同步机制(如 synchronized)来保证线程安全。 天生线程安全,无需同步。
使用场景 需要频繁更新状态的对象,如实体类、数据缓存、配置对象等。 并发环境中的共享数据、作为 Mapkey、需要保证安全性的数据(如密码)、String 等。
创建方式 提供 setter 方法,字段通常不是 final 类为 final,字段为 private final,无 setter,对可变成员进行防御性拷贝。
优点 灵活,内存效率高。 简单、可靠、线程安全、适合作为哈希键。
缺点 容易引发线程安全问题,副作用多,难以追踪。 可能导致性能问题和内存开销,创建代码更复杂。

何时选择使用?

  • 优先使用不可变对象:当你需要创建线程安全的对象、对象的状态不应该被改变、或者对象将被广泛共享(例如作为缓存键)时。“不可变优先” 是一个很好的设计原则。
  • 使用可变对象:当你明确知道对象的状态需要在整个生命周期中被频繁修改,并且可以管理好其并发访问(在单线程环境中,或使用适当的锁)时。

理解并善用可变和不可变对象,是衡量一个 Java 程序员是否具备良好代码设计能力的重要标志。

分享:
扫描分享到社交APP
上一篇
下一篇