Tại sao ArrayList của tôi chứa N bản sao của mục cuối cùng được thêm vào danh sách?


83

Tôi đang thêm ba đối tượng khác nhau vào ArrayList, nhưng danh sách chứa ba bản sao của đối tượng cuối cùng tôi đã thêm.

Ví dụ:

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

Hy vọng:

0
1
2

Thực tế:

2
2
2

Tôi đã phạm sai lầm gì?

Lưu ý: đây được thiết kế để trở thành một Câu hỏi và Đáp chính tắc cho nhiều vấn đề tương tự phát sinh trên trang web này.

Câu trả lời:


146

Vấn đề này có hai nguyên nhân điển hình:

  • Các trường tĩnh được sử dụng bởi các đối tượng bạn đã lưu trữ trong danh sách

  • Vô tình thêm cùng một đối tượng vào danh sách

Trường tĩnh

Nếu các đối tượng trong danh sách của bạn lưu trữ dữ liệu trong các trường tĩnh, thì mỗi đối tượng trong danh sách của bạn sẽ có vẻ giống nhau vì chúng có cùng giá trị. Hãy xem xét lớp học dưới đây:

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

Trong ví dụ đó, chỉ có một int valuecái được chia sẻ giữa tất cả các trường hợp Foobởi vì nó được khai báo static. (Xem hướng dẫn "Hiểu về các thành viên trong lớp" .)

Nếu bạn thêm nhiều Foođối tượng vào danh sách bằng cách sử dụng mã bên dưới, mỗi đối tượng sẽ trả về 3từ một lệnh gọi đến getValue():

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

Giải pháp rất đơn giản - không sử dụng các statictừ khóa cho các trường trong lớp của bạn trừ khi bạn thực sự muốn các giá trị được chia sẻ giữa mọi trường hợp của lớp đó.

Thêm cùng một đối tượng

Nếu bạn thêm một biến tạm thời vào danh sách, bạn phải tạo một phiên bản mới của đối tượng bạn đang thêm, mỗi lần lặp lại. Hãy xem xét đoạn mã sai sau:

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

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

Ở đây, tmpđối tượng được xây dựng bên ngoài vòng lặp. Kết quả là, cùng một đối tượng được thêm vào danh sách ba lần. Cá thể sẽ giữ giá trị 2, vì đó là giá trị được truyền trong lần gọi cuối cùng setValue().

Để khắc phục điều này, chỉ cần di chuyển cấu trúc đối tượng bên trong vòng lặp:

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
xin chào @Duncan, giải pháp tốt đẹp, tôi muốn hỏi bạn rằng trong "Thêm cùng một đối tượng" tại sao ba phiên bản khác nhau sẽ giữ giá trị 2, không phải cả ba phiên bản đều phải giữ 3 giá trị khác nhau? hy vọng bạn sẽ trả lời sớm, cảm ơn
Dev

3
@Dev Vì cùng một đối tượng ( tmp) được thêm vào danh sách ba lần. Và đối tượng đó có giá trị là hai, vì lệnh gọi tới tmp.setValue(2)trong lần lặp cuối cùng của vòng lặp.
Duncan Jones

1
ok, vì vậy vấn đề ở đây là do cùng một đối tượng, Có phải vậy nếu tôi thêm cùng một đối tượng ba lần trong một danh sách mảng, cả ba vị trí của danh sách mảng obj sẽ tham chiếu đến cùng một đối tượng?
Dev

1
@Dev Yup, chính xác là như vậy.
Duncan Jones

1
Một sự thật hiển nhiên mà ban đầu tôi đã bỏ qua: liên quan đến phần you must create a new instance each time you looptrong phần Adding the same object: Xin lưu ý rằng trường hợp được đề cập liên quan đến đối tượng bạn đang thêm, KHÔNG phải đối tượng bạn đang thêm nó vào.
lúc

8

Vấn đề của bạn là với kiểu staticyêu cầu khởi tạo mới mỗi khi lặp lại một vòng lặp. Nếu bạn đang ở trong một vòng lặp, tốt hơn là giữ nguyên khởi tạo cụ thể bên trong vòng lặp.

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

Thay vì:

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
}

Đây taglà một biến SomeStaticClassđể kiểm tra tính hợp lệ của đoạn mã trên; bạn có thể có một số triển khai khác dựa trên trường hợp sử dụng của bạn.


Bạn có nghĩa là gì bởi "loại static"? Đối với bạn, một lớp không tĩnh sẽ là gì?
4castle

ví dụ như không tĩnh: public class SomeClass{/*some code*/} & tĩnh: public static class SomeStaticClass{/*some code*/}. Tôi hy vọng nó rõ ràng hơn bây giờ.
Shashank

1
Vì tất cả các đối tượng của một lớp tĩnh đều chia sẻ cùng một địa chỉ, nếu chúng được khởi tạo trong một vòng lặp và được đặt với các giá trị khác nhau trong mỗi lần lặp. Tất cả chúng sẽ có giá trị giống hệt nhau sẽ bằng giá trị của lần lặp cuối cùng hoặc khi đối tượng cuối cùng được sửa đổi. Tôi hy vọng nó rõ ràng hơn bây giờ.
Shashank

6

Gặp sự cố tương tự với phiên bản lịch.

Sai mã:

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

Bạn phải tạo một đối tượng MỚI của lịch, có thể được thực hiện với 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

Mỗi khi bạn thêm một đối tượng vào ArrayList, hãy đảm bảo rằng bạn thêm một đối tượng mới và đối tượng chưa được sử dụng. Điều đang xảy ra là khi bạn thêm cùng 1 bản sao của đối tượng, thì đối tượng đó sẽ được thêm vào các vị trí khác nhau trong ArrayList. Và khi bạn thay đổi một bản sao, vì cùng một bản sao được thêm vào nhiều lần, tất cả các bản sao sẽ bị ảnh hưởng. Ví dụ: Giả sử bạn có ArrayList như sau:

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

Bây giờ nếu bạn thêm Thẻ c này vào danh sách, nó sẽ được thêm vào không có vấn đề gì. Nó sẽ được lưu ở vị trí 0. Nhưng, khi bạn lưu cùng một Thẻ c trong danh sách, nó sẽ được lưu ở vị trí 1. Vì vậy, hãy nhớ rằng bạn đã thêm cùng một đối tượng vào hai vị trí khác nhau trong một danh sách. Bây giờ nếu bạn thực hiện thay đổi đối tượng Thẻ c, các đối tượng trong danh sách ở vị trí 0 và 1 cũng sẽ phản ánh thay đổi đó, vì chúng là cùng một đối tượng.

Một giải pháp sẽ là tạo một hàm tạo trong lớp Thẻ, nó chấp nhận một đối tượng Thẻ khác. Sau đó, trong hàm tạo đó, bạn có thể đặt các thuộc tính như sau:

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
}

Và giả sử bạn có cùng 1 bản sao của Thẻ, vì vậy tại thời điểm thêm đối tượng mới, bạn có thể thực hiện điều này:

list.add(new Card(nameOfTheCardObjectThatYouWantADifferentCopyOf));

2

Nó cũng có thể là hậu quả của việc sử dụng cùng một tham chiếu thay vì sử dụng một tham chiếu mới.

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

 setdata();
......

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

Thay vì:

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

public void setdata(int i) {
  tmp.setValue(i);
  list.add(tmp);
} 
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.