ArrayListにリストに追加された最後のアイテムのN個のコピーが含まれているのはなぜですか?


83

ArrayListに3つの異なるオブジェクトを追加していますが、リストには最後に追加したオブジェクトの3つのコピーが含まれています。

例えば:

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

期待:

0
1
2

実際:

2
2
2

私はどんな間違いをしましたか?

注:これは、このサイトで発生する多くの同様の問題に対する標準的なQ&Aとなるように設計されています。

回答:


146

この問題には2つの典型的な原因があります。

  • リストに保存したオブジェクトによって使用される静的フィールド

  • 誤って同じオブジェクトをリストに追加してしまった

静的フィールド

リスト内のオブジェクトが静的フィールドにデータを格納している場合、リスト内の各オブジェクトは同じ値を保持しているため、同じように見えます。以下のクラスを検討してください。

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

その例では、宣言されているためint value、のすべてのインスタンス間で共有されるのは1つだけです。(「クラスメンバーについて」チュートリアルを参照してください。)Foostatic

Foo以下のコードを使用してリストに複数のオブジェクトを追加すると、各インスタンスは次3の呼び出しから戻りますgetValue()

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);
}

ここでは、tmpオブジェクトはループの外側に構築されています。その結果、同じオブジェクトインスタンスがリストに3回追加されています。の2最後の呼び出し中に渡された値であるため、インスタンスは値を保持しますsetValue()

これを修正するには、オブジェクト構造をループ内に移動するだけです。

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
こんにちは@Duncan素晴らしい解決策、「同じオブジェクトの追加」で、なぜ3つの異なるインスタンスが値2を保持するのか、3つのインスタンスすべてが3つの異なる値を保持する必要があるのではないかとお聞きしたいと思います。すぐに返信していただければ幸いです。ありがとうございます
Dev

3
@Dev同じオブジェクト(tmp)がリストに3回追加されたため。そしてtmp.setValue(2)、ループの最後の反復での呼び出しのため、そのオブジェクトの値は2です。
ダンカンジョーンズ

1
さて、ここで問題は同じオブジェクトが原因です。配列リストに同じオブジェクトを3回追加すると、arraylist objの3つの場所すべてが同じオブジェクトを参照しますか?
Dev

1
@Devうん、まさにそれだ。
ダンカンジョーンズ

1
私が最初に見落としていた明らかな事実:you must create a new instance each time you loopセクションの部分についてAdding the same object:参照されているインスタンスは、追加しているオブジェクトではなく、追加しているオブジェクトに関係していることに注意してください。
で、

8

あなたの問題は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
}

ここtagでは、変数であるSomeStaticClass上記のスニペットの有効性を確認するためには、ユースケースに基づいて、他の実装を行うことができます。


「タイプstatic」とはどういう意味ですか?あなたにとって非静的クラスは何でしょうか?
4castle 2017

例えば、非静的:public class SomeClass{/*some code*/} &静的:public static class SomeStaticClass{/*some code*/}。今はもっとはっきりしているといいのですが。
Shashank 2017

1
静的クラスのすべてのオブジェクトは同じアドレスを共有するため、ループで初期化され、反復ごとに異なる値が設定されている場合。それらはすべて、最後の反復の値または最後のオブジェクトが変更されたときの値に等しい同じ値を持つことになります。今はもっとはっきりしているといいのですが。
Shashank 2017

6

カレンダーインスタンスでも同じ問題が発生しました。

不正なコード:

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);
}

カレンダーの新しいオブジェクトを作成する必要があります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);

}

4

ArrayListにオブジェクトを追加するたびに、まだ使用されていない新しいオブジェクトを追加してください。何が起こっているのかというと、同じ1つのオブジェクトのコピーを追加すると、その同じオブジェクトがArrayListの異なる位置に追加されます。また、1つに変更を加えると、同じコピーが何度も追加されるため、すべてのコピーが影響を受けます。たとえば、次のようなArrayListがあるとします。

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

このカードcをリストに追加すれば、問題なく追加されます。場所0に保存されます。ただし、同じカードcをリストに保存すると、場所1に保存されます。したがって、同じ1つのオブジェクトをリストの2つの異なる場所に追加したことを忘れないでください。ここで、そのCardオブジェクトcを変更すると、場所0と1のリスト内のオブジェクトも同じオブジェクトであるため、その変更が反映されます。

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つあるとすると、新しいオブジェクトを追加するときに、次のことができます。

list.add(new Card(nameOfTheCardObjectThatYouWantADifferentCopyOf));

2

また、新しい参照を使用する代わりに同じ参照を使用した結果になる可能性もあります。

 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);
} 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.