List集合循环添加对象会重复的原因及解决办法

最近在开发中,遇到个以前还没怎么遇到过的问题,可能是开发经验太少了吧,不过才个坑就最好把这个坑补上,下次才不会再掉进去。

Android开发,从数据库读取一些用户的数据,我使用的是List集合来存储一个用户对象,然后传给前台,但却是发现取到的数据居然是重复的。我当时的写法大概是下面这样的

//后台读取数据库的对象数据
List<User> list = new ArrayList<>();
User u = new User();
for (int i = 0; i < 3; i++) {//遍历读出来的数据添加到List集合
    u.setName("a"+i);
    list.add(u);
}

//前台取出数据
for (User u:list) {
    System.out.println(u);
}


//用户数据对象
class User{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //toString略
}

最后输出了三次重复的a2。

其实问题就出在User对象的实例化上,以前我还特意写了一篇博客,详细记录了Java值传递和对象地址传递的问题(https://blog.csdn.net/c_o_d_e_/article/details/109219800),没想到一段时间不看就忘了,这次想了很久才发现是这个问题。

一步一步来剖析代码

①在JVM栈中创建了一个User类型的地址指针u(地址通常是一个十六进制的数字,这里假设地址为0x12345678),然后使用new关键字在JVM堆中开辟一个空间,将User对象存储到这个堆中,最后用等号将栈中的u和堆中的User对象关联起来。②i=0时,执行u.setName("a1"),这时候会通过u(0x12345678)这个地址去找到堆中的User对象,然后将该对象中的name属性赋值为"a1",那么此时我们再通过这个地址去访问name,肯定获取到的就是"a1"了。然后执行list.add(u)方法,其实是将u地址存储到了List集中。去看ArrayList源码就知道,它的底层数据结构就是一个Object类型的数组。所以这里的add方法也就是将地址u存储到了一个数组中。我们访问集合中的数据,其实也是读取这个数组中地址所指向的值。
 

//ArrayList部分源码
    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
    
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

③i=2时,执行u.setName("a2"),此时因为并没有使用new关键字去开拓新的对象,所以这个u的值还是0x12345678,并没有发生任何改变。这时候还是通过u去访问堆中的User对象,将它的name属性重新赋值为"a2"(因为上面一步,已经将name赋值为"a1")。然后执行list.add(u)方法,又将这个u存储到数组中,此时ArrayList的数组中就存储了两个相同的u。并且我们通过这两个u去访问name,得到的都是"a2",因为两个u都是指向的同一个User对象嘛。

④i=3时,执行u.setName("a3"),u的值还是没变,原理同上,将User对象中的name重新赋值为了"a3",然后执行list.add(u)将地址u存储到数组中。此时数组中就有三个相同的地址u,地址相同,指向的对象自然也相同,所以此刻List集合中的三个对象其实都是指向了同一个User,而且值均为最新的赋值"a3"。List集合此时的数组内容如下图

到这里弄懂重复原因后,解决办法就轻松了。只需要在外部声明,在循环内部每次都new 一个新的对象,即可保证每次添加进集合的地址均不相同,也不会被覆盖

        List<User> list = new ArrayList<>();
        User u;
        for (int i = 0; i < 3; i++) {
            u= new User();
            u.setName("a"+i);
            list.add(u);
        }

        for (User u:list) {
            System.out.println(u);
        }

猜你喜欢

转载自blog.csdn.net/c_o_d_e_/article/details/110084549
今日推荐