Java对象数组的存储机制
Java中的数组是引用类型,当数组的元素是对象时,数组本身存储的并不是对象的实际数据,而是对象的引用地址。也就是说,对象数组的每一个元素都指向堆内存中对应的对象实例,多个数组元素可能指向同一个对象实例,这就是共享引用产生的根源。

基础示例演示引用存储
我们可以通过一段简单的代码来验证对象数组的存储特性:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class ArrayDemo {
public static void main(String[] args) {
User user1 = new User("张三", 20);
User user2 = new User("李四", 22);
// 创建对象数组,存储两个User对象的引用
User[] userArray = new User[]{user1, user2};
// 打印数组第一个元素的引用地址
System.out.println("第一个元素引用:" + userArray[0]);
// 打印user1的引用地址
System.out.println("user1引用:" + user1);
}
}
运行上述代码会发现,userArray[0]和user1的打印结果完全一致,说明数组元素和原变量指向同一个对象。
共享引用陷阱的产生原因
当多个数组元素指向同一个对象时,修改其中任意一个元素对应的对象的成员变量,所有指向该对象的引用获取到的成员变量值都会发生变化,这就是共享引用陷阱。最常见的场景是初始化数组时,错误地将同一个对象赋值给多个数组元素。
陷阱场景代码示例
下面的代码演示了典型的共享引用陷阱:
public class ShareRefTrap {
public static void main(String[] args) {
User template = new User("默认用户", 18);
// 错误初始化:所有数组元素都指向同一个template对象
User[] userArray = new User[3];
for (int i = 0; i < userArray.length; i++) {
userArray[i] = template;
}
// 修改第一个元素的成员变量
userArray[0].name = "修改后的用户";
userArray[0].age = 25;
// 打印所有元素的成员变量
for (int i = 0; i < userArray.length; i++) {
System.out.println("第" + (i+1) + "个元素:name=" + userArray[i].name + ",age=" + userArray[i].age);
}
}
}
运行后会发现,三个数组元素的name和age都变成了修改后的数值,这是因为三个元素都指向同一个template对象,修改的是同一个对象的内容。
避免共享引用陷阱的方法
方法一:为每个数组元素创建独立对象实例
初始化数组时,不要复用同一个对象,而是为每个元素单独创建新的对象实例,确保每个数组元素指向不同的堆内存对象。
public class AvoidTrap1 {
public static void main(String[] args) {
User[] userArray = new User[3];
for (int i = 0; i < userArray.length; i++) {
// 每次循环创建新的User对象,每个元素指向不同实例
userArray[i] = new User("默认用户", 18);
}
// 修改第一个元素的成员变量
userArray[0].name = "独立修改的用户";
userArray[0].age = 25;
// 打印所有元素的成员变量
for (int i = 0; i < userArray.length; i++) {
System.out.println("第" + (i+1) + "个元素:name=" + userArray[i].name + ",age=" + userArray[i].age);
}
}
}
此时只有第一个元素的属性被修改,其他元素不受影响。
方法二:修改前复制对象
如果已经存在共享引用的数组,需要在修改某个元素的成员变量前,先为该元素创建一个新的对象副本,再修改副本的属性,避免影响原对象。
class User implements Cloneable {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
// 实现克隆方法,创建对象副本
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class AvoidTrap2 {
public static void main(String[] args) throws CloneNotSupportedException {
User template = new User("默认用户", 18);
User[] userArray = new User[]{template, template, template};
// 修改第一个元素前,先克隆一个新的对象赋值给该元素
userArray[0] = (User) userArray[0].clone();
userArray[0].name = "克隆后修改的用户";
userArray[0].age = 25;
// 打印所有元素的成员变量
for (int i = 0; i < userArray.length; i++) {
System.out.println("第" + (i+1) + "个元素:name=" + userArray[i].name + ",age=" + userArray[i].age);
}
}
}
方法三:使用不可变对象
如果对象的成员变量不需要频繁修改,可以将对象设计为不可变类,即成员变量用final修饰,不提供setter方法,这样对象创建后就无法修改其属性,从根源上避免共享引用带来的修改问题。
// 不可变User类
class ImmutableUser {
private final String name;
private final int age;
public ImmutableUser(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class AvoidTrap3 {
public static void main(String[] args) {
ImmutableUser template = new ImmutableUser("默认用户", 18);
ImmutableUser[] userArray = new ImmutableUser[]{template, template, template};
// 需要修改时,创建新的不可变对象替换数组元素
userArray[0] = new ImmutableUser("新的不可变用户", 25);
for (int i = 0; i < userArray.length; i++) {
System.out.println("第" + (i+1) + "个元素:name=" + userArray[i].getName() + ",age=" + userArray[i].getAge());
}
}
}
注意事项总结
- 操作对象数组时,首先要明确数组元素存储的是对象引用,不是对象本身数据。
- 初始化对象数组时,避免直接将同一个对象赋值给多个数组元素。
- 修改数组元素的成员变量前,先确认该元素是否和其他引用共享同一个对象,必要时先创建副本。
- 对于不需要修改属性的场景,优先使用不可变对象可以减少引用相关的问题。