在面向对象编程中,封装是核心特性之一,我们希望对象的内部状态只能通过自身提供的方法修改,避免外部代码随意篡改。但实际开发中,很多开发者会忽略引用类型变量的传递问题,导致内部状态被意外修改,而防御性拷贝就是解决这类问题的关键方案。

什么是防御性拷贝
防御性拷贝指的是在返回对象内部的可变引用类型变量时,不直接返回原变量的引用,而是先复制一份新的对象返回给外部。这样外部对返回对象的修改,只会作用于拷贝出来的新对象,不会影响原对象的内部状态,从而保护对象内部的变量不被意外修改。
为什么需要防御性拷贝
我们先看一个没有做防御性拷贝的反面案例,假设我们有一个Person类,内部包含一个可变的学生列表:
import java.util.ArrayList;
import java.util.List;
class Person {
private String name;
private List<String> studentList;
public Person(String name, List<String> studentList) {
this.name = name;
// 构造时直接赋值引用,存在风险
this.studentList = studentList;
}
// 直接返回内部列表的引用
public List<String> getStudentList() {
return studentList;
}
public void addStudent(String student) {
studentList.add(student);
}
}
public class Test {
public static void main(String[] args) {
List<String> originList = new ArrayList<>();
originList.add("张三");
Person person = new Person("李老师", originList);
// 外部拿到列表引用后直接修改
List<String> getList = person.getStudentList();
getList.add("李四");
// 此时person的内部列表已经被修改
System.out.println(person.getStudentList().size()); // 输出2
}
}
上面的代码中,getStudentList方法直接返回了内部studentList的引用,外部代码拿到引用后可以直接修改列表内容,完全绕过了Person类的addStudent方法,破坏了对象的封装性。如果Person类内部对列表有业务约束,比如最多只能有3个学生,外部修改就会直接打破这个约束,导致程序逻辑出错。
如何实现防御性拷贝
1. 构造方法中的防御性拷贝
构造方法接收外部传入的可变引用类型参数时,也需要做拷贝,避免外部后续修改参数影响对象内部状态:
import java.util.ArrayList;
import java.util.List;
class Person {
private String name;
private List<String> studentList;
public Person(String name, List<String> studentList) {
this.name = name;
// 构造时拷贝传入的列表,而不是直接引用
this.studentList = new ArrayList<>(studentList);
}
public List<String> getStudentList() {
// 返回时做拷贝
return new ArrayList<>(studentList);
}
public void addStudent(String student) {
studentList.add(student);
}
}
2. 返回可变对象时的防御性拷贝
如果对象内部包含自定义的可变类实例,同样需要做拷贝返回,比如我们有一个Address类:
class Address {
private String city;
private String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
class User {
private String name;
private Address address;
public User(String name, Address address) {
this.name = name;
// 拷贝Address对象
this.address = new Address(address.getCity(), address.getStreet());
}
public Address getAddress() {
// 返回时拷贝Address对象
return new Address(address.getCity(), address.getStreet());
}
}
防御性拷贝的注意事项
- 不可变对象不需要做防御性拷贝,比如
String、基本类型的包装类,因为这类对象本身无法被修改,返回引用也不会影响内部状态。 - 如果拷贝的对象层级很深,需要考虑深拷贝和浅拷贝的问题,上面的
Address案例属于浅拷贝,如果Address内部还有引用类型变量,就需要递归拷贝所有层级的可变对象。 - 防御性拷贝会带来额外的内存开销和性能消耗,所以只需要对可能被外部修改的可变引用类型做拷贝,不需要对所有变量都做拷贝。
防御性拷贝的适用场景
防御性拷贝主要适用在以下场景:
| 场景 | 说明 |
|---|---|
| 返回对象内部的可变集合 | 比如List、Map、Set等,避免外部修改集合内容 |
| 返回自定义的可变对象 | 比如自定义的实体类、数据类,避免外部修改对象内部属性 |
| 构造方法接收可变引用参数 | 避免外部后续修改参数影响对象内部状态 |
| 方法参数需要长期保存 | 如果方法会把传入的引用类型参数保存到对象内部,需要做拷贝 |
防御性拷贝是面向对象开发中保障对象封装性的重要手段,合理运用可以有效避免很多因为引用传递导致的隐蔽bug,让代码的健壮性和安全性得到提升。