为什么我的 ArrayList 包含添加到列表中的最后一项的 N 个副本?

Why does my ArrayList contain N copies of the last item added to the list?

提问人:Duncan Jones 提问时间:11/8/2013 最后编辑:BraiamDuncan Jones 更新时间:4/28/2022 访问量:72871

问:

我将向 ArrayList 添加三个不同的对象,但该列表包含我添加的最后一个对象的三个副本。

例如:

for (Foo f : list) {
  System.out.println(f.getValue());
}    

预期:

0
1
2

实际:

2
2
2

我犯了什么错误?

注意:这旨在成为本网站上出现的众多类似问题的规范问答。

爪哇岛

评论


答:

170赞 7 revs, 6 users 92%Duncan Jones #1

此问题有两个典型原因:

  • 存储在列表中的对象使用的静态字段

  • 意外地将同一对象添加到列表中

静态字段

如果列表中的对象将数据存储在静态字段中,则列表中的每个对象将显示为相同,因为它们具有相同的值。考虑以下类:

public class Foo {
  private static int value; 
  //      ^^^^^^------------ - Here's the problem!
  
  public Foo(int value) {
    this.value = value;
  }
  
  public int getValue() {
    return value;
  }
}

在该示例中,只有一个实例在 的所有实例之间共享,因为它是声明的。(请参阅“了解类成员”教程。int valueFoostatic

如果使用以下代码将多个对象添加到列表中,则每个实例都将从调用中返回:Foo3getValue()

for (int i = 0; i < 4; i++) {      
  list.add(new Foo(i));
}

解决方案很简单 - 不要将关键字用于类中的字段,除非您确实希望在该类的每个实例之间共享值。static

添加相同的对象

如果将临时变量添加到列表中,则每次循环时都必须创建要添加的对象的新实例。请考虑以下错误的代码片段:

List<Foo> list = new ArrayList<Foo>();    
Foo tmp = new Foo();

for (int i = 0; i < 3; i++) {
  tmp.setValue(i);
  list.add(tmp);
}

在这里,对象是在循环之外构造的。因此,同一对象实例被添加到列表中三次。实例将保存值 ,因为这是上次调用 期间传递的值。tmp2setValue()

要解决此问题,只需在循环中移动对象构造:

List<Foo> list = new ArrayList<Foo>();        

for (int i = 0; i < 3; i++) {
  Foo tmp = new Foo(); // <-- fresh instance!
  tmp.setValue(i);
  list.add(tmp);
}

评论

1赞 Dev 12/10/2014
您好@Duncan不错的解决方案,我想问您,在“添加相同的对象”中,为什么三个不同的实例将保持值 2,难道所有三个实例都不应该包含 3 个不同的值吗?希望您能尽快回复,谢谢
3赞 Duncan Jones 12/10/2014
@Dev 因为同一个对象()被添加到列表中三次。该对象的值为 2,因为在循环的最后一次迭代中调用了 。tmptmp.setValue(2)
1赞 Dev 12/10/2014
好的,所以这里的问题是因为同一个对象,如果我在一个数组列表中添加同一个对象三次,arraylist obj 的所有三个位置都会引用同一个对象吗?
1赞 Duncan Jones 12/10/2014
@Dev 是的,就是这样。
1赞 a.t. 5/28/2019
我最初忽略了一个明显的事实:关于本节中的部分:请注意,所引用的实例涉及您要添加的对象,而不是您要添加的对象。you must create a new instance each time you loopAdding the same object
7赞 Shashank 7/13/2016 #2

您的问题出在每次迭代循环时都需要重新初始化的类型。如果你在一个循环中,最好将具体的初始化保持在循环中。static

List<Object> objects = new ArrayList<>(); 

for (int i = 0; i < length_you_want; i++) {
    SomeStaticClass myStaticObject = new SomeStaticClass();
    myStaticObject.tag = i;
    // Do stuff with myStaticObject
    objects.add(myStaticClass);
}

而不是:

List<Object> objects = new ArrayList<>(); 

SomeStaticClass myStaticObject = new SomeStaticClass();
for (int i = 0; i < length; i++) {
    myStaticObject.tag = i;
    // Do stuff with myStaticObject
    objects.add(myStaticClass);
    // This will duplicate the last item "length" times
}

这是一个变量,用于检查上述代码片段的有效性;您可以根据自己的用例进行一些其他实现。tagSomeStaticClass

评论

0赞 4castle 2/3/2017
“类型”是什么意思?对你来说,什么是非静态类?static
0赞 Shashank 2/4/2017
例如,non-static: & static: .我希望现在更清楚了。public class SomeClass{/*some code*/}public static class SomeStaticClass{/*some code*/}
1赞 Shashank 2/4/2017
因为,如果静态类的所有对象在循环中初始化并在每次迭代中设置不同的值,则它们共享相同的地址。它们最终都将具有相同的值,该值将等于上次迭代或修改最后一个对象时的值。我希望现在更清楚了。
0赞 Lino 9/28/2021
由于静态类的所有对象共享相同的地址,因此在 Java 的上下文中>这是完全错误的。Java 中不存在纯类,您可以添加到嵌套类中,但这并不能自动使其成为单个实例/相同的引用......staticstatic
5赞 Faraz 2/27/2017 #3

每次向 ArrayList 添加对象时,请确保添加一个新对象而不是尚未使用的对象。发生的情况是,当您添加相同的 1 个对象副本时,相同的对象将添加到 ArrayList 中的不同位置。当您对一个副本进行更改时,由于一遍又一遍地添加相同的副本,所有副本都会受到影响。 例如 假设你有一个 ArrayList,如下所示:

ArrayList<Card> list = new ArrayList<Card>();
Card c = new Card();

现在,如果您将此卡c添加到列表中,则不会有问题。它将保存在位置 0。但是,当您在列表中保存相同的卡 c 时,它将保存在位置 1。因此,请记住,您将相同的 1 个对象添加到列表中的两个不同位置。现在,如果对 Card 对象 c 进行更改,则位置 0 和 1 处的列表中的对象也将反映该更改,因为它们是同一个对象。

一种解决方案是在 Card 类中创建一个构造函数,它接受另一个 Card 对象。然后,在该构造函数中,可以设置如下属性:

public Card(Card c){
this.property1 = c.getProperty1();
this.property2 = c.getProperty2(); 
... //add all the properties that you have in this class Card this way
}

假设您有相同的 1 个 Card 副本,因此在添加新对象时,您可以执行以下操作:

list.add(new Card(nameOfTheCardObjectThatYouWantADifferentCopyOf));

评论

0赞 Mohammad_Hosein 7/24/2021
亲爱的@Faraz这是一个错误,不是吗?我认为 java 程序员必须纠正这一点。在我的问题中还有另一种问题(特别是,请参阅问题的最后一行)。
0赞 Mohammad_Hosein 7/24/2021
问题出在“某种”对象上!不是全部。例如,字符串没有问题。而字符串数组有问题!
1赞 Faraz 7/24/2021
我亲爱的兄弟穆罕默德·侯赛因(Mohammad Hosein),阿萨拉姆·奥·阿莱库姆(Assalam o Alaikum)。字符串没有这个问题的原因是,字符串是不可变的。每次修改 String 时,它都会在内部创建一个新的 String 并返回它。这就是为什么你看不到字符串的问题。
1赞 Faraz 7/24/2021
我得回去睡觉了。当我醒来时,我会试着看看你的问题。
0赞 Mohammad_Hosein 7/24/2021
va alaikom alsalaam,非常感谢@Faraz兄弟。睡个好觉。我得到了我的答案(特别是你的这个答案)。只有我想要这些引用/链接的帮助/教程......以避免进一步的问题。(我不知道我应该在谷歌中搜索什么?谢谢
7赞 basti12354 10/25/2017 #4

日历实例也有同样的问题。

错误的代码:

Calendar myCalendar = Calendar.getInstance();

for (int days = 0; days < daysPerWeek; days++) {
    myCalendar.add(Calendar.DAY_OF_YEAR, 1);

    // In the next line lies the error
    Calendar newCal = myCalendar;
    calendarList.add(newCal);
}

您必须创建日历的 NEW 对象,这可以通过calendar.clone();

Calendar myCalendar = Calendar.getInstance();

for (int days = 0; days < daysPerWeek; days++) {
    myCalendar.add(Calendar.DAY_OF_YEAR, 1);

    // RIGHT WAY
    Calendar newCal = (Calendar) myCalendar.clone();
    calendarList.add(newCal);

}

评论

1赞 MC Emperor 3/30/2021
致未来的读者:你根本不应该使用。Calendar
2赞 Shaikh Mohib 4/30/2020 #5

使用相同的引用而不是使用新的引用也可能是结果。

 List<Foo> list = new ArrayList<Foo>();        

 setdata();
......

public void setdata(int i) {
  Foo temp = new Foo();
  tmp.setValue(i);
  list.add(tmp);
}

而不是:

List<Foo> list = new ArrayList<Foo>(); 
Foo temp = new Foo();       
setdata();
......

public void setdata(int i) {
  tmp.setValue(i);
  list.add(tmp);
}