Dlaczego moja ArrayList zawiera N kopii ostatniego elementu dodanego do listy?

84

Dodaję trzy różne obiekty do ArrayList, ale lista zawiera trzy kopie ostatniego dodanego obiektu.

Na przykład:

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

Spodziewany:

0
1
2

Rzeczywisty:

2
2
2

Jaki błąd popełniłem?

Uwaga: to ma być kanonicznym pytaniem i odpowiedziami na wiele podobnych problemów, które pojawiają się w tej witrynie.

Duncan Jones
źródło

Odpowiedzi:

152

Ten problem ma dwie typowe przyczyny:

  • Pola statyczne używane przez obiekty przechowywane na liście

  • Przypadkowe dodanie tego samego obiektu do listy

Pola statyczne

Jeśli obiekty na liście przechowują dane w polach statycznych, każdy obiekt na liście będzie wyglądał tak samo, ponieważ mają te same wartości. Rozważ poniższą klasę:

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

W tym przykładzie jest tylko jeden, int valuektóry jest współużytkowany przez wszystkie wystąpienia, Fooponieważ jest zadeklarowany static. (Zobacz samouczek „Zrozumienie składowych klas” ).

Jeśli dodasz wiele Fooobiektów do listy za pomocą poniższego kodu, każda instancja zwróci 3z wywołania getValue():

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

Rozwiązanie jest proste - nie używaj staticsłów kluczowych dla pól w swojej klasie, chyba że faktycznie chcesz, aby wartości były wspólne dla wszystkich wystąpień tej klasy.

Dodawanie tego samego obiektu

Jeśli dodasz zmienną tymczasową do listy, za każdym razem, gdy wykonujesz pętlę, musisz utworzyć nową instancję dodawanego obiektu. Rozważ następujący błędny fragment kodu:

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

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

Tutaj tmpobiekt został skonstruowany poza pętlą. W rezultacie ta sama instancja obiektu jest dodawana do listy trzy razy. Instancja będzie przechowywać wartość 2, ponieważ była to wartość przekazana podczas ostatniego wywołania funkcji setValue().

Aby to naprawić, po prostu przesuń konstrukcję obiektu wewnątrz pętli:

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);
}
Duncan Jones
źródło
1
cześć @Duncan fajne rozwiązanie, chcę cię zapytać, w sekcji „Dodawanie tego samego obiektu”, dlaczego trzy różne instancje będą miały wartość 2, czy wszystkie trzy instancje nie powinny mieć 3 różnych wartości? mam nadzieję, że wkrótce odpowiesz, dzięki
Dev
3
@Dev Ponieważ ten sam obiekt ( tmp) jest dodawany do listy trzy razy. A ten obiekt ma wartość dwa z powodu wywołania tmp.setValue(2)w ostatniej iteracji pętli.
Duncan Jones
1
ok, więc tutaj problem jest z powodu tego samego obiektu, czy jest tak, że jeśli dodam ten sam obiekt trzy razy na liście tablic, wszystkie trzy lokalizacje arraylist obj będą odnosić się do tego samego obiektu?
Dev
1
@Dev Tak, to jest dokładnie to.
Duncan Jones
1
Oczywisty fakt, który początkowo przeoczyłem: jeśli chodzi o część you must create a new instance each time you loopw sekcji Adding the same object: Zwróć uwagę, że wystąpienie, o którym mowa, dotyczy obiektu, który dodajesz, a NIE obiektu, do którego go dodajesz.
na
8

Twój problem dotyczy typu, staticktóry wymaga nowej inicjalizacji za każdym razem, gdy pętla jest iterowana. Jeśli jesteś w pętli, lepiej jest zachować konkretną inicjalizację wewnątrz pętli.

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

Zamiast:

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
}

Oto tagzmienna w służąca SomeStaticClassdo sprawdzenia poprawności powyższego fragmentu; możesz mieć inną implementację w oparciu o Twój przypadek użycia.

Shashank
źródło
Co masz na myśli mówiąc „typ static”? Czym byłaby dla ciebie klasa niestatyczna?
4castle
np niestatyczny: public class SomeClass{/*some code*/} & statyczny: public static class SomeStaticClass{/*some code*/}. Mam nadzieję, że teraz jest to jaśniejsze.
Shashank
1
Ponieważ wszystkie obiekty klasy statycznej mają ten sam adres, jeśli są inicjowane w pętli i mają różne wartości w każdej iteracji. Wszystkie z nich będą miały taką samą wartość, która będzie równa wartości ostatniej iteracji lub ostatniej modyfikacji obiektu. Mam nadzieję, że teraz jest to jaśniejsze.
Shashank
6

Miałem ten sam problem z wystąpieniem kalendarza.

Niepoprawny kod:

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

Musisz stworzyć NOWY obiekt kalendarza, co można zrobić za pomocą 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);

}
basti12354
źródło
4

Za każdym razem, gdy dodajesz obiekt do ArrayList, upewnij się, że dodajesz nowy obiekt, a nie już używany obiekt. Dzieje się tak, że kiedy dodajesz tę samą 1 kopię obiektu, ten sam obiekt jest dodawany do różnych pozycji w ArrayList. A kiedy wprowadzisz zmianę w jednym, ponieważ ta sama kopia jest dodawana w kółko, wszystkie kopie ulegną zmianie. Na przykład załóżmy, że masz taką ArrayList:

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

Teraz, jeśli dodasz tę kartę c do listy, zostanie dodana bez problemu. Zostanie on zapisany w lokalizacji 0. Ale kiedy zapiszesz tę samą Kartę c na liście, zostanie ona zapisana w lokalizacji 1. Pamiętaj więc, że dodałeś ten sam 1 obiekt do dwóch różnych lokalizacji na liście. Teraz, jeśli zmienisz ten obiekt karty c, obiekty na liście w lokalizacji 0 i 1 również odzwierciedlą tę zmianę, ponieważ są tym samym obiektem.

Jednym z rozwiązań byłoby stworzenie konstruktora w klasie Card, który akceptuje inny obiekt Card. Następnie w tym konstruktorze możesz ustawić właściwości w następujący sposób:

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
}

I powiedzmy, że masz tę samą 1 kopię Karty, więc podczas dodawania nowego obiektu możesz zrobić to:

list.add(new Card(nameOfTheCardObjectThatYouWantADifferentCopyOf));
Faraz
źródło
2

Może to również być konsekwencją użycia tego samego odniesienia zamiast używania nowego.

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

 setdata();
......

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

Zamiast:

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

public void setdata(int i) {
  tmp.setValue(i);
  list.add(tmp);
} 
Shaikh Mohib
źródło