Java集合是存储值还是引用?

3

Java初学者问题:当我拥有

Integer i = 6;
ArrayList<Integer> ar = ArrayList<Integer>();
ar.add(i);

接着我写下了 i = 8ar.get(0) 返回了 6

但如果我用我的一个类试着做同样的事情:

class MyC
{
    Integer i;
}
MyC myc = new MyC();
myc.i = 6;
ArrayList<MyC> ar = ArrayList<MyC>();
ar.add(myc);

然后执行myc.i = 8ar.get(0)返回8

您能解释一下这种行为吗?


3
你被自动装箱所迷惑了。当i是一个Integer类型时,i=8比它看起来的要稍微复杂一些。请注意不改变原意。 - President James K. Polk
这实际上是一个非常复杂的问题,尽管它看起来非常简单。点赞为自动装箱引起注意。 - EpicPandaForce
ar.get(0) 不会返回 8。而 ar.get(0).i 会返回 8。 - Sotirios Delimanolis
自动装箱与此无关。问题在于指向不同的对象与改变对象的状态之间的区别。 - Brett Okken
@BrettOkken 如果没有自动装箱,就不会有“不同的对象”指向,而是在堆栈上有一个原始的 int - Victor Sorokin
即使我们谈论的是一个原始的 int[]。 如果你有 int i=8; array[0] = i; i = 6;,那么 array[0] 的值将是 8 - Brett Okken
6个回答

3
问题与自动装箱无关,正如一些答案所说。
在第一个示例中,您创建了一个整数并将其放入ArrayList中。 然后您更改指向整数的指针,以使i指向另一个整数。 这不会影响ArrayList中的整数。
在第二个示例中,您创建一个对象并将其放入ArrayList中。 然后通过myc.i = 8改变此对象的状态。 这样,ArrayList中的对象就被更改了。

2
这是因为编译器将i = 8转换为i = new Integer(8),因为i属于Integer类型而不是int类型,所以你现在有了两个引用。在你的第二个例子中,仍然有一个引用从mycar的第一个元素引用。

正如Sotirios在评论中指出的那样,如果i是原始类型,你会得到相同的结果。但由于稍微不同的原因:
int i = 6;
ArrayList<Integer> ar = ArrayList<Integer>();
ar.add(i);
i = 8; // changes not a ref, but value on stack
System.out.println(ar.get(0)); // prints out 6

这里只有一个引用(指到ar内部自动装箱的Integer对象)。


2
我觉得你的回答会误导读者认为如果i是一个int,结果会有所不同。 - Sotirios Delimanolis
你说得对,结果是一样的。已更新答案。 - Victor Sorokin

2

除了基本类型,所有变量都存储引用而非值。

第一个例子

Integer i = 6;

创建一个新的 Integer 对象(我们称之为 I1),并将其引用存储在 i 中。
ArrayList<Integer> ar = ArrayList<Integer>();
ar.add(i);`

创建一个 ArrayList,并将 I1 的引用存储在其中。
i = 8;

创建一个新的(不同的)Integer对象(我们称之为I2),并将其引用存储在i中。
所以现在,i = I2,而ar.get(0) = I1第二个例子
MyC myc = new MyC();

创建一个新的MyC对象(我们称其为C),并将其引用存储在myc中。
myc.i = 6;

创建一个新的Integer对象(我们称之为I1),并将其引用存储在C.i中。
ArrayList<MyC> ar = ArrayList<MyC>();
ar.add(myc);

创建一个ArrayList,并将C的引用存储在其中。
myc.i = 8

创建一个新的Integer对象(我们称之为I2),并将其引用存储在C.i中。 现在myc=C,myc.i=I2,ar.get(0)=C,因此ar.get(0).i=I2。 没有任何引用指向I1,它将被垃圾回收。

1

Integer是一个包装了基本类型int的对象。

例如,为了将它们存储在一个Collection中,比如List<Integer> list = new ArrayList<Integer>(),存储的元素类型需要是Object的子类。因此,它们被作为引用存储,因为所有的对象都是以引用方式存储的(而所有方法都按值传递引用,请参见Java中的参数传递)。

需要注意的是,在某些情况下:

List<Integer> list = new ArrayList<Integer>();
list.add(5);
int number = list.get(0);
System.out.println("" + number);

使用数字5进行加法运算的原因是自动装箱。从列表中获取数字时,它也隐式地调用.intValue()方法,并将包装器的值作为原始int返回。然后在println()函数中,该数字隐式地打包为Integer,然后调用toString()方法。

0

将指针i指向另一个对象并不会改变(原始)对象。这不仅适用于基本类型。假设您有一个List<List<Object>>List<List<String>>

        List<List<String>> wrapper = new ArrayList<List<String>>();

        List<String> l1 = new ArrayList<String>();

        wrapper.add(l1);

        l1 = new ArrayList<String>();
        l1.add("hello");
        for(List<String> list : wrapper)
        {
            for(String string : list)
            {
                System.out.println(string);
            }
        }

如果你现在遍历 wrapper,你会发现它包含一个空列表。这等同于你的第一个示例。你的第二个示例看起来像:

        List<List<String>> wrapper = new ArrayList<List<String>>();

        List<String> l1 = new ArrayList<String>();

        wrapper.add(l1);

        l1.add("hello");
        for(List<String> list : wrapper)
        {
            for(String string : list)
            {
                System.out.println(string);
            }
        }

在这种情况下,wrapper 现在包含一个列表,其中有一个值。

正确,因为你在封装器中存储了原始的ArrayList,然后只是修改了变量的引用。但那实际上不是存储在封装器中的原始ArrayList。 - EpicPandaForce

0

Java中的所有变量都是引用,它们指向位于堆上的对象。

包装类(例如Integer)是特殊情况。它们实现了享元模式并且是不可变的。


1
我认为这不是自动装箱的问题。在第一个例子中,在插入后更改了指向i的指针,在第二个例子中更改了对象myc本身。 - F. Böller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接