Danh sách <Dog> có phải là một lớp con của Danh sách <Animal> không? Tại sao các tổng quát Java không hoàn toàn đa hình?


770

Tôi hơi bối rối về cách các thế hệ Java xử lý tính kế thừa / đa hình.

Giả sử hệ thống phân cấp sau -

Động vật (Cha mẹ)

Chó - Mèo (Trẻ em)

Vì vậy, giả sử tôi có một phương pháp doSomething(List<Animal> animals). Theo tất cả các quy tắc thừa kế và đa hình, tôi sẽ giả sử rằng a List<Dog> a List<Animal>và a List<Cat> một List<Animal>- và vì vậy một trong hai có thể được truyền cho phương thức này. Không phải vậy. Nếu tôi muốn đạt được hành vi này, tôi phải nói rõ phương thức để chấp nhận một danh sách của bất kỳ lớp con nào của Động vật bằng cách nói doSomething(List<? extends Animal> animals).

Tôi hiểu rằng đây là hành vi của Java. Câu hỏi của tôi là tại sao ? Tại sao đa hình nói chung là ẩn, nhưng khi nói đến khái quát thì nó phải được chỉ định?


17
Và một câu hỏi ngữ pháp hoàn toàn không liên quan hiện đang làm phiền tôi - tiêu đề của tôi là "tại sao không phải là khái quát của Java" hay "tại sao không phải là khái quát của Java" ?? Là "generic" số nhiều vì s hay số ít vì nó là một thực thể?
froadie

25
Các khái quát như được thực hiện trong Java là một dạng đa hình tham số rất kém. Đừng đặt niềm tin quá nhiều vào họ (như tôi đã từng), bởi vì một ngày nào đó bạn sẽ gặp phải những hạn chế thảm hại của họ: Bác sĩ phẫu thuật mở rộng Handable <Scalpel>, Handable <Sponge> KABOOM! Có không tính [TM]. Có giới hạn chung về Java của bạn. Bất kỳ OOA / OOD nào cũng có thể được dịch tốt sang Java (và MI có thể được thực hiện rất độc đáo bằng cách sử dụng các giao diện Java) nhưng các tổng quát chỉ không cắt nó. Chúng tốt cho "các bộ sưu tập" và lập trình thủ tục đã nói (đó là điều mà hầu hết các lập trình viên Java làm dù sao đi nữa ...).
Cú phápT3rr0r

8
Siêu hạng của Danh sách <Dog> không phải là Danh sách <Động vật> mà là Danh sách <?> (Tức là danh sách loại không xác định). Generics xóa thông tin loại trong mã biên dịch. Điều này được thực hiện sao cho mã đang sử dụng generic (java 5 trở lên) tương thích với các phiên bản trước đó của java mà không cần generic.
rai.skumar


9
@froadie vì dường như không ai phản hồi ... nên chắc chắn là "tại sao không phải là thuốc generic của Java ...". Vấn đề khác là "generic" thực sự là một tính từ, và vì vậy "generic" đang đề cập đến một danh từ số nhiều bị bỏ đi được sửa đổi bởi "generic". Bạn có thể nói "chức năng đó là chung chung", nhưng điều đó sẽ cồng kềnh hơn so với việc nói "chức năng đó là chung chung". Tuy nhiên, thật khó hiểu khi nói "Java có các hàm và các lớp chung", thay vì chỉ "Java có các tổng quát". Là một người đã viết luận văn thạc sĩ của họ về tính từ, tôi nghĩ rằng bạn đã vấp phải một câu hỏi rất thú vị!
dantiston

Câu trả lời:


916

Không, một List<Dog>không một List<Animal>. Xem xét những gì bạn có thể làm với một List<Animal>- bạn có thể thêm bất kỳ động vật nào vào đó ... bao gồm cả một con mèo. Bây giờ, bạn có thể thêm một cách hợp lý một con mèo vào một lứa chó con? Tuyệt đối không.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Đột nhiên bạn có một con mèo rất bối rối.

Bây giờ, bạn không thể thêm a Catvào List<? extends Animal>vì bạn không biết đó là a List<Cat>. Bạn có thể truy xuất một giá trị và biết rằng đó sẽ là một giá trị Animal, nhưng bạn không thể thêm các động vật tùy ý. Điều ngược lại là đúng List<? super Animal>- trong trường hợp đó bạn có thể thêm một Animalcách an toàn, nhưng bạn không biết gì về những gì có thể được lấy từ nó, bởi vì nó có thể là một List<Object>.


50
Điều thú vị là tất cả các danh sách của chó thực sự là một danh sách các loài động vật, giống như trực giác cho chúng ta biết. Vấn đề là, không phải mọi danh sách động vật là một danh sách các con chó, do đó việc đột biến danh sách bằng cách thêm một con mèo là vấn đề.
Ingo

66
@Ingo: Không, không thực sự: bạn có thể thêm một con mèo vào danh sách các loài động vật, nhưng bạn không thể thêm một con mèo vào danh sách các con chó. Một danh sách các con chó chỉ là một danh sách các động vật nếu bạn xem xét nó theo nghĩa chỉ đọc.
Jon Skeet

12
@JonSkeet - Tất nhiên, nhưng ai đang bắt buộc phải lập một danh sách mới từ một con mèo và một danh sách những con chó thực sự thay đổi danh sách những con chó? Đây là một quyết định thực hiện tùy ý trong Java. Một trong đó đi ngược lại với logic và trực giác.
Ingo

7
@Ingo: Tôi sẽ không sử dụng "chắc chắn" để bắt đầu. Nếu bạn có một danh sách ghi ở đầu "Khách sạn chúng tôi có thể muốn đến" và sau đó ai đó đã thêm một bể bơi vào đó, bạn có nghĩ rằng nó hợp lệ không? Không - đó là danh sách các khách sạn, không phải là danh sách các tòa nhà. Và nó không giống như tôi thậm chí đã nói "Một danh sách các con chó không phải là một danh sách các loài động vật" - tôi đặt nó trong các điều khoản mã , trong một phông chữ mã. Tôi thực sự không nghĩ rằng có bất kỳ sự mơ hồ nào ở đây. Dù sao, việc sử dụng lớp con sẽ không chính xác - đó là về khả năng tương thích gán, không phải phân lớp.
Jon Skeet

14
@ruakh: Vấn đề là sau đó bạn đang cố gắng thực hiện thời gian một cái gì đó có thể bị chặn tại thời gian biên dịch. Và tôi cho rằng hiệp phương sai là một lỗi thiết kế để bắt đầu.
Jon Skeet

84

Những gì bạn đang tìm kiếm được gọi là tham số loại covariant . Điều này có nghĩa là nếu một loại đối tượng có thể được thay thế cho một đối tượng khác trong một phương thức (ví dụ, Animalcó thể được thay thế bằng Dog), thì điều tương tự cũng áp dụng cho các biểu thức sử dụng các đối tượng đó (vì vậy List<Animal>có thể được thay thế bằng List<Dog>). Vấn đề là hiệp phương sai không an toàn cho các danh sách đột biến nói chung. Giả sử bạn có một List<Dog>, và nó đang được sử dụng như một List<Animal>. Điều gì xảy ra khi bạn cố gắng thêm một con mèo vào đây List<Animal>thực sự là một List<Dog>? Tự động cho phép các tham số loại là covariant phá vỡ hệ thống loại.

Sẽ rất hữu ích khi thêm cú pháp để cho phép các tham số kiểu được chỉ định là covariant, giúp tránh các ? extends Fookhai báo phương thức, nhưng điều đó làm tăng thêm độ phức tạp.


44

Lý do a List<Dog>không phải List<Animal>là, ví dụ, bạn có thể chèn a Catvào List<Animal>, nhưng không vào List<Dog>... bạn có thể sử dụng ký tự đại diện để làm cho tổng quát mở rộng hơn nếu có thể; ví dụ, đọc từ a List<Dog>tương tự như đọc từ a List<Animal>- nhưng không viết.

Các Generics trong Ngôn ngữ JavaPhần về Generics từ Hướng dẫn Java có một lời giải thích sâu sắc, rất tốt về lý do tại sao một số thứ là hoặc không đa hình hoặc được phép với khái quát.


36

Một điểm tôi nghĩ nên được thêm vào những gì câu trả lời khác đề cập đến là trong khi

List<Dog>không phải là một List<Animal> trong Java

cũng đúng

Một danh sách các con chó là - một danh sách các động vật bằng tiếng Anh (tốt, theo cách giải thích hợp lý)

Cách mà trực giác của OP hoạt động - tất nhiên là hoàn toàn hợp lệ - là câu sau. Tuy nhiên, nếu chúng ta áp dụng trực giác này, chúng ta sẽ có một ngôn ngữ không phải là Java-esque trong hệ thống loại của nó: Giả sử ngôn ngữ của chúng ta cho phép thêm một con mèo vào danh sách những con chó của chúng ta. Điều đó có nghĩa là gì? Điều đó có nghĩa là danh sách này không còn là danh sách các loài chó và chỉ đơn thuần là một danh sách các loài động vật. Và một danh sách các động vật có vú, và một danh sách các con tứ giác.

Nói một cách khác: A List<Dog>trong Java không có nghĩa là "một danh sách các con chó" trong tiếng Anh, nó có nghĩa là "một danh sách có thể có chó, và không có gì khác".

Tổng quát hơn, trực giác của OP cho vay đối với một ngôn ngữ trong đó các hoạt động trên các đối tượng có thể thay đổi loại của chúng , hay đúng hơn, (các) loại đối tượng là một hàm (động) có giá trị.


Vâng, ngôn ngữ của con người là mờ nhạt hơn. Tuy nhiên, một khi bạn thêm một loài động vật khác vào danh sách chó, nó vẫn là một danh sách các loài động vật, nhưng không còn là danh sách các con chó. Sự khác biệt, một con người, với logic mờ, thường không có vấn đề nhận ra điều đó.
Vlasec

Là một người tìm thấy sự so sánh liên tục với các mảng thậm chí còn khó hiểu hơn, câu trả lời này đóng đinh nó cho tôi. Vấn đề của tôi là trực giác ngôn ngữ.
FLonLon

32

Tôi muốn nói rằng toàn bộ quan điểm của Generics là nó không cho phép điều đó. Hãy xem xét tình huống với các mảng, cho phép kiểu hiệp phương sai đó:

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

Mã đó biên dịch tốt, nhưng ném lỗi thời gian chạy ( java.lang.ArrayStoreException: java.lang.Booleantrong dòng thứ hai). Nó không phải là loại an toàn. Quan điểm của Generics là thêm an toàn kiểu thời gian biên dịch, nếu không, bạn chỉ có thể gắn bó với một lớp đơn giản mà không có chung chung.

Bây giờ có những lúc bạn cần phải linh hoạt hơn và đó là những gì ? super Class? extends Classđang làm. Cái trước là khi bạn cần chèn vào một loại Collection(ví dụ), và cái sau là khi bạn cần đọc từ nó, theo cách an toàn. Nhưng cách duy nhất để làm cả hai cùng một lúc là có một loại cụ thể.


13
Có thể cho rằng, hiệp phương sai là một lỗi thiết kế ngôn ngữ. Lưu ý rằng do xóa kiểu, hành vi tương tự là không thể đối với bộ sưu tập chung.
Michael Borgwardt

" Tôi muốn nói rằng toàn bộ quan điểm của Generics là nó không cho phép điều đó. " Bạn không bao giờ có thể chắc chắn: Các hệ thống loại của Java và Scala là không rõ ràng: Cuộc khủng hoảng hiện tại của Null Pointers (được trình bày tại OOPSLA 2016) (kể từ khi sửa nó)
David Tonhofer

15

Để hiểu vấn đề, thật hữu ích khi so sánh với mảng.

List<Dog>không phải là lớp con của List<Animal>.
Nhưng Dog[] lớp con của Animal[].

Mảng là đáng tin cậy và covariant .
Reifible có nghĩa là thông tin loại của họ có sẵn đầy đủ trong thời gian chạy.
Do đó, mảng cung cấp an toàn kiểu thời gian chạy nhưng không an toàn kiểu thời gian biên dịch.

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

Đó là ngược lại cho khái quát:
Generics bị xóa và bất biến .
Do đó, generic không thể cung cấp an toàn kiểu thời gian chạy, nhưng chúng cung cấp an toàn kiểu thời gian biên dịch.
Trong đoạn mã dưới đây nếu thuốc generic là covariant, có thể gây ô nhiễm đống ở dòng 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());

2
Có thể lập luận rằng, chính xác là do đó, Mảng trong Java bị hỏng ,
leonbloy

Mảng là covariant là một "tính năng" của trình biên dịch.
Cristik

6

Các câu trả lời ở đây không hoàn toàn thuyết phục tôi. Vì vậy, thay vào đó, tôi làm một ví dụ khác.

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

Nghe có vẻ ổn phải không? Nhưng bạn chỉ có thể truyền Consumers và Suppliers cho Animals. Nếu bạn có một Mammalngười tiêu dùng, nhưng một Ducknhà cung cấp, họ không nên phù hợp mặc dù cả hai đều là động vật. Để không cho phép điều này, các hạn chế bổ sung đã được thêm vào.

Thay vì ở trên, chúng ta phải xác định mối quan hệ giữa các loại chúng ta sử dụng.

Ví dụ.,

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

đảm bảo rằng chúng tôi chỉ có thể sử dụng một nhà cung cấp cung cấp cho chúng tôi loại đối tượng phù hợp cho người tiêu dùng.

OTOH, chúng ta cũng có thể làm

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

nơi chúng tôi đi theo cách khác: chúng tôi xác định loại Supplier và hạn chế rằng nó có thể được đưa vào Consumer.

Chúng tôi thậm chí có thể làm

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

ở đâu, có quan hệ trực quan Life-> Animal-> Mammal-> Dog, Catvv, chúng tôi có thể thậm chí đặt một Mammalthành một Lifengười tiêu dùng, nhưng không phải là một Stringthành một Lifengười tiêu dùng.


1
Trong số 4 phiên bản, # 2 có lẽ không chính xác. ví dụ: chúng ta không thể gọi nó (Consumer<Runnable>, Supplier<Dog>)trong khi đó Doglà kiểu con củaAnimal & Runnable
ZhongYu

5

Logic cơ bản cho hành vi như vậy là Genericstuân theo một cơ chế xóa kiểu. Vì vậy, trong thời gian chạy, bạn không có cách nào nếu xác định loại collectionkhông giống như arraysnơi không có quá trình tẩy xóa như vậy. Vì vậy, trở lại câu hỏi của bạn ...

Vì vậy, giả sử có một phương pháp như được đưa ra dưới đây:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

Bây giờ nếu java cho phép người gọi thêm Danh sách loại Động vật vào phương thức này thì bạn có thể thêm điều sai vào bộ sưu tập và vào thời gian chạy, nó sẽ chạy do xóa kiểu. Trong khi trong trường hợp mảng, bạn sẽ có ngoại lệ thời gian chạy cho các tình huống như vậy ...

Do đó, về bản chất, hành vi này được thực hiện để người ta không thể thêm điều sai vào bộ sưu tập. Bây giờ tôi tin rằng kiểu xóa tồn tại để cung cấp khả năng tương thích với java kế thừa mà không cần chung chung ....


4

Subtyping là bất biến cho các loại tham số. Ngay cả lớp khó khăn Doglà một kiểu con của Animal, kiểu tham số List<Dog>không phải là kiểu con của List<Animal>. Ngược lại, phân nhóm covariant được sử dụng bởi các mảng, vì vậy kiểu mảng Dog[]là một kiểu con củaAnimal[] .

Phân nhóm bất biến đảm bảo rằng các ràng buộc kiểu được thi hành bởi Java không bị vi phạm. Hãy xem xét mã sau đây được đưa ra bởi @Jon Skeet:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

Như tuyên bố của @Jon Skeet, mã này là bất hợp pháp, bởi vì nếu không, nó sẽ vi phạm các ràng buộc loại bằng cách trả lại một con mèo khi một con chó mong đợi.

Đó là hướng dẫn để so sánh ở trên với mã tương tự cho các mảng.

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

Mã này là hợp pháp. Tuy nhiên, ném một ngoại lệ cửa hàng mảng . Một mảng mang kiểu của nó trong thời gian chạy theo cách này JVM có thể thực thi sự an toàn của kiểu phân nhóm covariant.

Để hiểu rõ hơn về điều này, chúng ta hãy nhìn vào mã byte được tạo bởi javaplớp bên dưới:

import java.util.ArrayList;
import java.util.List;

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

Sử dụng lệnh javap -c Demonstration, điều này cho thấy mã byte Java sau:

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

Quan sát rằng mã dịch của các thân phương thức là giống hệt nhau. Trình biên dịch thay thế từng loại tham số bằng cách xóa . Thuộc tính này rất quan trọng có nghĩa là nó không phá vỡ tính tương thích ngược.

Tóm lại, an toàn thời gian chạy là không thể đối với các loại tham số hóa, vì trình biên dịch thay thế từng loại tham số hóa bằng cách xóa nó. Điều này làm cho các loại tham số hóa không có gì nhiều hơn đường cú pháp.


3

Trên thực tế bạn có thể sử dụng một giao diện để đạt được những gì bạn muốn.

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

sau đó bạn có thể sử dụng các bộ sưu tập bằng cách sử dụng

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());

1

Nếu bạn chắc chắn rằng các mục danh sách là các lớp con của loại siêu cụ thể đó, bạn có thể truyền danh sách bằng cách sử dụng phương pháp này:

(List<Animal>) (List<?>) dogs

Điều này hữu ích khi bạn muốn chuyển danh sách trong hàm tạo hoặc lặp qua nó


2
Điều này sẽ tạo ra nhiều vấn đề hơn nó thực sự giải quyết
Ferrybig

Nếu bạn cố gắng thêm một con mèo vào danh sách, chắc chắn nó sẽ tạo ra vấn đề, nhưng với mục đích lặp, tôi nghĩ đó là câu trả lời không dài dòng duy nhất.
sagits 2/2/2016

1

Câu trả lời cũng như các câu trả lời khác là chính xác. Tôi sẽ thêm vào những câu trả lời đó bằng một giải pháp mà tôi nghĩ sẽ hữu ích. Tôi nghĩ rằng điều này xuất hiện thường xuyên trong lập trình. Một điều cần lưu ý là đối với Bộ sưu tập (Danh sách, Bộ, v.v.), vấn đề chính là thêm vào Bộ sưu tập. Đó là nơi mọi thứ đổ vỡ. Ngay cả loại bỏ là OK.

Trong hầu hết các trường hợp, chúng ta có thể sử dụng Collection<? extends T>thay vào đó Collection<T>và đó nên là lựa chọn đầu tiên. Tuy nhiên, tôi đang tìm trường hợp không dễ để làm điều đó. Nó là tranh luận về việc liệu đó luôn luôn là điều tốt nhất để làm. Tôi đang trình bày ở đây một lớp DownCastCollection có thể chuyển đổi a Collection<? extends T>thành Collection<T>(chúng ta có thể định nghĩa các lớp tương tự cho List, Set, Navigableset, ..) để sử dụng khi sử dụng phương pháp tiêu chuẩn rất bất tiện. Dưới đây là một ví dụ về cách sử dụng nó (chúng ta cũng có thể sử dụng Collection<? extends Object>trong trường hợp này, nhưng tôi giữ cho nó đơn giản để minh họa bằng DownCastCollection.

/**Could use Collection<? extends Object> and that is the better choice. 
* But I am doing this to illustrate how to use DownCastCollection. **/

public static void print(Collection<Object> col){  
    for(Object obj : col){
    System.out.println(obj);
    }
}
public static void main(String[] args){
  ArrayList<String> list = new ArrayList<>();
  list.addAll(Arrays.asList("a","b","c"));
  print(new DownCastCollection<Object>(list));
}

Bây giờ lớp học:

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;

public DownCastCollection(Collection<? extends E> delegate) {
    super();
    this.delegate = delegate;
}

@Override
public int size() {
    return delegate ==null ? 0 : delegate.size();
}

@Override
public boolean isEmpty() {
    return delegate==null || delegate.isEmpty();
}

@Override
public boolean contains(Object o) {
    if(isEmpty()) return false;
    return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
    Iterator<? extends E> delegateIterator;

    protected MyIterator() {
        super();
        this.delegateIterator = delegate == null ? null :delegate.iterator();
    }

    @Override
    public boolean hasNext() {
        return delegateIterator != null && delegateIterator.hasNext();
    }

    @Override
    public  E next() {
        if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
        return delegateIterator.next();
    }

    @Override
    public void remove() {
        delegateIterator.remove();

    }

}
@Override
public Iterator<E> iterator() {
    return new MyIterator();
}



@Override
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

@Override
public boolean remove(Object o) {
    if(delegate == null) return false;
    return delegate.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
    if(delegate==null) return false;
    return delegate.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public boolean removeAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.retainAll(c);
}

@Override
public void clear() {
    if(delegate == null) return;
        delegate.clear();

}

}


Đây là một ý tưởng tốt, đến nỗi nó đã tồn tại trong Java SE rồi. ; )Collections.unmodifiableCollection
Radiodef

1
Phải nhưng bộ sưu tập tôi xác định có thể được sửa đổi.
dan b

Vâng, nó có thể được sửa đổi. Collection<? extends E>đã xử lý chính xác hành vi đó, trừ khi bạn sử dụng nó theo cách không an toàn về kiểu (ví dụ: truyền nó sang thứ khác). Ưu điểm duy nhất tôi thấy là, khi bạn gọi addthao tác, nó sẽ ném ngoại lệ ngay cả khi bạn bỏ nó.
Vlasec

0

Hãy lấy ví dụ từ hướng dẫn JavaSE

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

Vì vậy, tại sao một danh sách các con chó (vòng tròn) không nên được coi là một danh sách các động vật (hình dạng) là do tình huống này:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

Vì vậy, "kiến trúc sư" Java đã có 2 tùy chọn giải quyết vấn đề này:

  1. đừng nghĩ rằng một kiểu con là ngầm định nó là siêu kiểu và đưa ra một lỗi biên dịch, giống như nó xảy ra bây giờ

  2. coi kiểu con là siêu kiểu và hạn chế biên dịch phương thức "add" (vì vậy trong phương thức drawAll, nếu một danh sách các vòng tròn, kiểu con hình dạng, sẽ được thông qua, trình biên dịch sẽ phát hiện ra điều đó và hạn chế bạn biên dịch lỗi cái đó).

Vì lý do rõ ràng, điều đó đã chọn cách đầu tiên.


0

Chúng ta cũng nên xem xét cách trình biên dịch đe dọa các lớp chung: trong "tức thời" một kiểu khác bất cứ khi nào chúng ta điền vào các đối số chung.

Như vậy chúng ta có ListOfAnimal, ListOfDog, ListOfCat, vv, đó là các lớp học riêng biệt mà cuối cùng bị "tạo ra" bởi trình biên dịch khi chúng tôi xác định các đối số chung. Và đây là một hệ thống phân cấp phẳng (thực sự liên quan đến Listkhông phải là một hệ thống phân cấp nào cả).

Một lập luận khác tại sao hiệp phương sai không có ý nghĩa trong trường hợp các lớp chung là thực tế là ở tất cả các lớp đều giống nhau - là các Listthể hiện. Chuyên môn hóa Listbằng cách điền vào đối số chung không mở rộng lớp, nó chỉ làm cho nó hoạt động cho đối số chung cụ thể đó.


0

Vấn đề đã được xác định rõ. Nhưng có một giải pháp; làm doSomething chung:

<T extends Animal> void doSomething<List<T> animals) {
}

bây giờ bạn có thể gọi doS Something với Danh sách <Dog> hoặc Danh sách <Cat> hoặc Danh sách <Animal>.


0

một giải pháp khác là xây dựng một danh sách mới

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());

0

Thêm vào câu trả lời của Jon Skeet, sử dụng mã ví dụ này:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Ở cấp độ sâu nhất, vấn đề ở đây là dogsanimalschia sẻ một tài liệu tham khảo. Điều đó có nghĩa là một cách để thực hiện công việc này là sao chép toàn bộ danh sách, điều này sẽ phá vỡ sự bình đẳng tham chiếu:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

Sau khi gọi List<Animal> animals = new ArrayList<>(dogs);, bạn không thể trực tiếp gán animalscho một trong hai dogshoặc cats:

// These are both illegal
dogs = animals;
cats = animals;

do đó, bạn không thể đưa loại con sai Animalvào danh sách, vì không có kiểu con sai - bất kỳ đối tượng nào của kiểu con ? extends Animalcó thể được thêm vào animals.

Rõ ràng, điều này thay đổi ngữ nghĩa, vì các danh sách animalsdogskhông còn được chia sẻ, vì vậy việc thêm vào một danh sách không thêm vào danh sách khác (đó chính xác là những gì bạn muốn, để tránh vấn đề chỉ Catcó thể được thêm vào danh sách lẽ ra phải chứa Dogđồ vật). Ngoài ra, sao chép toàn bộ danh sách có thể không hiệu quả. Tuy nhiên, điều này không giải quyết vấn đề tương đương loại, bằng cách phá vỡ đẳng thức tham chiếu.

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.