资讯专栏INFORMATION COLUMN

Java中ArrayList的对象引用问题

h9911 / 2559人阅读

摘要:既然的构造方法是复制新的数组,那么是为什么呢这里提前透露一下结论数组元素为对象时,实际上存储的是对象的引用,进行数组复制也只是复制了对象的引用。即数组元素为对象时,实际上存储的是对象的引用。

前言

事件起因是由于同事使用ArrayList的带参构造方法进行ArrayList对象复制,修改新的ArrayList对象中的元素(对象)的成员变量时也会修改原ArrayList中的元素(对象)的成员变量。

下面会通过复盘代码向大家重现遇到的问题

复盘代码 用户类
public class User {

    private Integer id;

    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name="" + name + """ +
                "}";
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
问题重现示例
import java.util.ArrayList;
import java.util.List;

public class ArrayListReference {

    public static void main(String[] args) {
        // 原用户列表
        List users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(new User(i, "test"));
        }
        // 新用户列表
        List newUsers = new ArrayList<>(users);
        for (int j = 0; j < newUsers.size(); j++) {
            //  修改新用户列表的用户名
            newUsers.get(j).setName(String.valueOf(j));
        }
        // 打印新用户列表
        System.out.println("newUsers:" + newUsers);
        // 重新打印原用户列表
        System.out.println("After update newUsers,users:" + users);
    }
}
示例运行结果
users:[User{id=0, name="test"}, User{id=1, name="test"}, User{id=2, name="test"}, User{id=3, name="test"}, User{id=4, name="test"}, User{id=5, name="test"}, User{id=6, name="test"}, User{id=7, name="test"}, User{id=8, name="test"}, User{id=9, name="test"}]
newUsers:[User{id=0, name="0"}, User{id=1, name="1"}, User{id=2, name="2"}, User{id=3, name="3"}, User{id=4, name="4"}, User{id=5, name="5"}, User{id=6, name="6"}, User{id=7, name="7"}, User{id=8, name="8"}, User{id=9, name="9"}]
After update newUsers,users:[User{id=0, name="0"}, User{id=1, name="1"}, User{id=2, name="2"}, User{id=3, name="3"}, User{id=4, name="4"}, User{id=5, name="5"}, User{id=6, name="6"}, User{id=7, name="7"}, User{id=8, name="8"}, User{id=9, name="9"}]
分析 问题

为什么使用了ArrayList的构造方法重新构造一个新的ArrayList后,操作新ArrayList对象中的元素时会影响到原来的ArrayList中的元素呢?

首先需要分析ArrayList的构造方法

ArrayList源码分析

下面是示例中调用的ArrayList构造方法的源码

public ArrayList(Collection c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                // 此处为关键代码,此处就是数组元素的复制方法
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

从源码中得知数组复制的关键代码为

elementData = Arrays.copyOf(elementData, size, Object[].class);

下面进入Arrays.copyOf()的源码进行研究

public static  T[] copyOf(U[] original, int newLength, Class newType) {
        @SuppressWarnings("unchecked")
        // 构造一个新的数组对象
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        // 将原数组元素复制到新数组中
        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
        return copy;
    }

从上面的源码得知关键代码为

System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));

以下为System.arraycopy()方法的源码

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);

由于System.arraycopy()方法为native方法,很难跟踪其实现代码。不过可以从方法注释中可以知道这个方法的特点:

Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. A subsequence of array components are copied from the source array referenced by src to the destination array referenced by dest. The number of components copied is equal to the length argument. The components at positions srcPos through srcPos+length-1 in the source array are copied into positions destPos through destPos+length-1, respectively, of the destination array.

翻译结果为

将数组从指定的源数组(从指定位置开始)复制到目标数组的指定位置。将数组组件的子序列从src引用的源数组复制到dest引用的目标数组,复制的组件数量等于length参数。源数组中通过srcPos+length-1位置的组件分别复制到目标数组中通过destPos+length-1位置的destPos。

既然ArrayList的构造方法是复制新的数组,那么是为什么呢?这里提前透露一下结论:数组元素为对象时,实际上存储的是对象的引用,ArrayList进行数组复制也只是复制了对象的引用。所以才会出现一开始说的问题

再次验证

下面将会使用一个数组的复制示例验证结论,使用==来比较对象引用是否相同

问题重现示例
import java.util.Arrays;

public class ArrayReference {

    public static void main(String[] args) {
        // 原用户列表
        User[] users = new User[10];
        for (int i = 0; i < users.length; i++) {
            users[i] = (new User(i, "test"));
        }
        // 新用户列表
        User[] newUsers = Arrays.copyOf(users, users.length);
        for (int j = 0; j < users.length; j++) {
            // 比较对象引用
            System.out.println(j + ":" + (users[j] == newUsers[j]));
        }
    }
}
示例运行结果
0:true
1:true
2:true
3:true
4:true
5:true
6:true
7:true
8:true
9:true
结果分析

从运行结果中可以得知,上面提出的结论是正确的。即数组元素为对象时,实际上存储的是对象的引用

解决办法

解决方法很简单,只需要遍历对象数组中的元素,调用对象的构造方法构造新的对象并加入新的数组中即可

解决办法示例
public class ArrayListReferenceSolution {

    public static void main(String[] args) {
        // 原用户列表
        List users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(new User(i, "test"));
        }
        // 新用户列表
        List newUsers = new ArrayList<>();
        for (int j = 0; j < users.size(); j++) {
            // 使用构造方法构造新的对象
            newUsers.add(new User(users.get(j).getId(),users.get(j).getName()));
        }
        for (int k= 0; k < users.size(); k++) {
            // 比较对象引用
            System.out.println(k + ":" + (users.get(k) == newUsers.get(k)));
        }
    }
}
示例运行结果
0:false
1:false
2:false
3:false
4:false
5:false
6:false
7:false
8:false
9:false
结果分析

从运行结果可以得知,使用示例中的方法就可以复制出一个不会干扰原ArrayList的对象。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74779.html

相关文章

  • 聊聊Java泛型及实现

    摘要:静态变量是被泛型类的所有实例所共享的。所以引用能完成泛型类型的检查。对于这个类型系统,有如下的一些规则相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。事实上,泛型类扩展都不合法。 前言 和C++以模板来实现静多态不同,Java基于运行时支持选择了泛型,两者的实现原理大相庭径。C++可以支持基本类型作为模板参数,Java却只能接受类作为泛型参数;Java可以在泛型类的方法中取得...

    lewif 评论0 收藏0
  • java编程思想》—— 泛型

    摘要:引用泛型除了方法因不能使用外部实例参数外,其他继承实现成员变量,成员方法,方法返回值等都可使用。因此,生成的字节码仅包含普通的类,接口和方法。 为什么要使用泛型程序设计? 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义类的对应类型;如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。----摘自原书Ordinary classes and meth...

    CODING 评论0 收藏0
  • Java集合框架概述和集合遍历

    摘要:第三阶段常见对象的学习集合框架概述和集合的遍历一集合框架的概述集合的由来如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这是一个非常简单的程序。进而它们的遍历方式也应该是不同的,最终就没有定义迭代器类。 第三阶段 JAVA常见对象的学习 集合框架概述和集合的遍历 (一) 集合框架的概述 (1) 集合的由来 如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这是一...

    happyhuangjinjin 评论0 收藏0
  • 1、自定义类型定义及使用 2、自定义类内存图 3、ArrayList集合基本功能 4、随机点名

    摘要:自定义类的概述自定义类的概述代码映射成现实事物的过程就是定义类的过程。自定义类的格式自定义类的格式使用类的形式对现实中的事物进行描述。 01引用数据类型_类 * A: 数据类型 * a: java中的数据类型分为:基本类型和引用类型 * B: 引用类型的分类 * a: Java为我们提供好的类,比如说:Scanner,Random等。 * b: 我们自己创建的类...

    only_do 评论0 收藏0
  • Java 面试准备

    摘要:网站的面试专题学习笔记非可变性和对象引用输出为,前后皆有空格。假定栈空间足够的话,尽管递归调用比较难以调试,在语言中实现递归调用也是完全可行的。栈遵守规则,因此递归调用方法能够记住调用者并且知道此轮执行结束之返回至当初的被调用位置。 ImportNew 网站的Java面试专题学习笔记 1. 非可变性和对象引用 String s = Hello ; s += World ; s.tr...

    chanjarster 评论0 收藏0

发表评论

0条评论

h9911

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<