核心概念
- 可变对象:对象在被创建之后,其内部的状态(即成员变量的值)可以被改变。
- 不可变对象:对象在被创建之后,其内部的状态永远不能被改变,任何试图修改它的操作,都会返回一个新的对象,而不是修改原始对象。
可变对象
可变对象是 Java 编程中的常态,绝大多数我们自定义的类以及许多核心 API 类(如 ArrayList, HashMap, Date 等)都是可变的。
特点
- 状态可修改:可以通过方法(通常是
setter方法)直接修改对象的内部属性。 - 引用可修改:可以改变对象的引用,使其指向另一个对象。
- 灵活性高:非常适合需要频繁修改状态的应用场景,例如实体类、数据模型等。
示例
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 类。
特点
- 状态不可修改:对象一旦创建,其所有字段的值都不能改变。
- 引用可以改变:变量本身可以重新赋值,指向另一个不可变对象。
- 线程安全:由于状态不能被改变,多个线程可以同时访问一个不可变对象,而无需额外的同步措施,因此它们是天生线程安全的。
- 简单可靠:对象的状态在整个生命周期中保持不变,使得代码更容易理解和调试。
如何创建一个不可变对象?
要创建一个不可变对象,需要遵循以下规则:
- 将类声明为
final:防止其他类通过继承来修改其行为。 - 将所有字段声明为
private和final:private:防止外部直接访问和修改字段。final:确保字段在初始化后不能被重新赋值。
- 不提供
setter方法:只提供getter方法,让外部只能读取,不能修改。 - 确保所有可变字段也是不可变的:如果类中包含其他对象的引用(如
List,Date),这些引用所指向的对象也必须是不可变的,如果无法保证,则必须在构造函数中进行深度拷贝,以防止外部代码通过引用修改内部状态。 - 不要提供修改对象状态的方法:如果需要提供“修改”功能,应该返回一个新的对象实例,而不是修改当前对象。
示例
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()的结果也永远不变,能正确地在HashMap和HashSet中工作。 - 保护性拷贝:可以作为防御性编程的一部分,防止内部数据被意外修改。
- 缺点:
- 性能开销:每次“修改”都需要创建新对象,可能会带来性能损耗和内存压力。
- 代码冗长:创建不可变对象需要遵循更多规则,代码量相对较多。
总结与对比
| 特性 | 可变对象 | 不可变对象 |
|---|---|---|
| 状态修改 | 允许在创建后修改内部状态。 | 创建后,内部状态永远不能修改。 |
| 线程安全 | 不安全,需要额外的同步机制(如 synchronized)来保证线程安全。 |
天生线程安全,无需同步。 |
| 使用场景 | 需要频繁更新状态的对象,如实体类、数据缓存、配置对象等。 | 并发环境中的共享数据、作为 Map 的 key、需要保证安全性的数据(如密码)、String 等。 |
| 创建方式 | 提供 setter 方法,字段通常不是 final。 |
类为 final,字段为 private final,无 setter,对可变成员进行防御性拷贝。 |
| 优点 | 灵活,内存效率高。 | 简单、可靠、线程安全、适合作为哈希键。 |
| 缺点 | 容易引发线程安全问题,副作用多,难以追踪。 | 可能导致性能问题和内存开销,创建代码更复杂。 |
何时选择使用?
- 优先使用不可变对象:当你需要创建线程安全的对象、对象的状态不应该被改变、或者对象将被广泛共享(例如作为缓存键)时。“不可变优先” 是一个很好的设计原则。
- 使用可变对象:当你明确知道对象的状态需要在整个生命周期中被频繁修改,并且可以管理好其并发访问(在单线程环境中,或使用适当的锁)时。
理解并善用可变和不可变对象,是衡量一个 Java 程序员是否具备良好代码设计能力的重要标志。
