2009-09-02 90 views

Trả lời

143

Đầu tiên nói rằng đó là " một số loại là tổ tiên của E "; thứ hai nói rằng đó là "một số loại mà là một phân lớp của E". (Trong cả hai trường hợp E chính nó là ổn.)

Vì vậy, các nhà xây dựng sử dụng các hình thức ? extends E nên nó đảm bảo rằng khi nó lấy về giá trị từ bộ sưu tập, tất cả họ sẽ được E hoặc một số lớp con (tức là nó tương thích). Phương pháp drainTo đang cố gắng đặt các giá trị vào bộ sưu tập, do đó bộ sưu tập phải có loại phần tử là Ehoặc một siêu lớp.

Như một ví dụ, giả sử bạn có một hệ thống phân cấp lớp như thế này:

Parent extends Object 
Child extends Parent 

LinkedBlockingQueue<Parent>. Bạn có thể xây dựng thông số này trong một List<Child> sẽ sao chép tất cả các phần tử một cách an toàn, bởi vì mỗi Child là một phụ huynh. Bạn không thể chuyển vào số List<Object> vì một số yếu tố có thể không tương thích với Parent.

Tương tự như vậy bạn có thể tiêu hao hàng đợi đó vào một List<Object> vì mỗi Parent là một Object ... nhưng bạn không thể thoát nó thành một List<Child>List<Child> hy vọng tất cả các yếu tố của nó để tương thích với Child.

+16

+1. Đó thực sự là sự khác biệt thực tế. mở rộng để tìm nạp, siêu để chèn. – Yishai

+0

Rất cám ơn. Giải thích tuyệt vời! –

+0

@Jon ý của bạn là gì (Trong cả hai trường hợp, chính nó là không sao.) Trong đoạn đầu tiên? – Geek

7

Bạn có thể đặt want to google for the termscontravariance (<? super E>) và hiệp phương sai (<? extends E>). Tôi thấy rằng điều hữu ích nhất khi Generics thấu hiểu đối với tôi để hiểu được chữ ký phương pháp Collection.addAll:

public interface Collection<T> { 
    public boolean addAll(Collection<? extends T> c); 
} 

Cũng như bạn muốn để có thể thêm một String đến một List<Object>:

List<Object> lo = ... 
lo.add("Hello") 

Bạn cũng sẽ có thể để thêm một List<String> (hoặc bất kỳ bộ sưu tập của String s) thông qua addAll phương pháp:

List<String> ls = ... 
lo.addAll(ls) 

Tuy nhiên, bạn nên nhận ra rằng List<Object>List<String> không tương đương và cũng không phải là lớp con của lớp trước. Điều cần thiết là khái niệm về tham số loại covariant - tức là các bit <? extends T>.

Khi bạn có điều này, thật đơn giản để nghĩ về các tình huống mà bạn muốn contravariance cũng (kiểm tra giao diện Comparable).

3

Ký tự đại diện có giới hạn trên trông giống như "? Extends Type" và viết tắt cho họ của tất cả các loại là loại con của Loại, loại Loại được bao gồm. Loại được gọi là giới hạn trên.

Ký tự đại diện có giới hạn dưới trông giống như "? Loại siêu" và viết tắt cho họ của tất cả các loại là siêu kiểu của Loại, nhập Loại được bao gồm. Loại được gọi là giới hạn dưới.

38

<? extends E> định nghĩa E làm giới hạn trên: "Điều này có thể được truyền tới E".

<? super E> định nghĩa E làm giới hạn dưới: "E có thể được truyền tới điều này".

+4

Đây là một trong những tóm tắt đơn giản/thực tế tốt nhất về sự khác biệt mà tôi đã thấy. – JAB

+1

Trong nhiều thập kỷ *** bây giờ (với OOP) Tôi đã chiến đấu với một bản năng đảo ngược của các khái niệm "trên" và "thấp hơn". Làm nặng thêm! Đối với tôi, 'Object' vốn dĩ là một tầng lớp thấp hơn, mặc dù nó là vị trí là siêu lớp cuối cùng (và được vẽ theo chiều dọc trong UML hoặc các cây thừa kế tương tự). Tôi đã không bao giờ có thể hoàn tác điều này mặc dù eons cố gắng. –

+1

@ tgm1024 "siêu lớp" và "phân lớp" phải cung cấp cho bạn rất nhiều rắc rối. –

10

Tôi sẽ thử và trả lời câu hỏi này. Nhưng để có được một câu trả lời thực sự tốt, bạn nên kiểm tra sách của Joshua Bloch Effective Java (2nd Edition). Ông mô tả PECS ghi nhớ, viết tắt của "Producer Extends, Consumer Super".

Ý tưởng là nếu bạn mã đang tiêu thụ các giá trị chung từ đối tượng thì bạn nên sử dụng mở rộng. nhưng nếu bạn đang tạo ra các giá trị mới cho kiểu generic bạn nên sử dụng super.

Vì vậy, ví dụ:

public void pushAll(Iterable<? extends E> src) { 
    for (E e: src) 
    push(e); 
} 

public void popAll(Collection<? super E> dst) { 
    while (!isEmpty()) 
    dst.add(pop()) 
} 

Nhưng thực sự bạn nên kiểm tra cuốn sách này: http://java.sun.com/docs/books/effective/

77

Những lý do cho điều này được dựa trên cách Java thực hiện Generics.

Một Mảng Ví dụ

Với mảng bạn có thể làm điều này (mảng là hiệp biến)

Integer[] myInts = {1,2,3,4}; 
Number[] myNumber = myInts; 

Nhưng, những gì sẽ xảy ra nếu bạn cố gắng làm điều này?

myNumber[0] = 3.14; //attempt of heap pollution 

Dòng cuối cùng này sẽ biên dịch tốt, nhưng nếu bạn chạy mã này, bạn có thể nhận được ArrayStoreException. Bởi vì bạn đang cố gắng để đặt một đôi vào một mảng số nguyên (bất kể được truy cập thông qua một tham chiếu số).

Điều này có nghĩa là bạn có thể đánh lừa trình biên dịch, nhưng bạn không thể đánh lừa hệ thống kiểu thời gian chạy. Và điều này là do mảng là những gì chúng tôi gọi là các loại có thể tái chế. Điều này có nghĩa là trong thời gian chạy Java biết rằng mảng này đã thực sự được khởi tạo như một mảng các số nguyên chỉ đơn giản xảy ra để được truy cập thông qua một tham chiếu kiểu Number[].

Vì vậy, như bạn có thể thấy, một điều là loại thực tế của đối tượng và một thứ khác là loại tham chiếu mà bạn sử dụng để truy cập vào nó, phải không?

Vấn đề với Java Generics

Bây giờ, vấn đề với các kiểu generic Java là các loại thông tin bị loại bỏ bởi trình biên dịch và nó không có sẵn tại thời gian chạy. Quá trình này được gọi là type erasure. Có lý do chính đáng để thực hiện generics như thế này trong Java, nhưng đó là một câu chuyện dài, và nó phải làm với khả năng tương thích nhị phân với mã đã tồn tại từ trước.

Nhưng điểm quan trọng ở đây là vì, khi chạy không có thông tin loại, không có cách nào để đảm bảo rằng chúng tôi không cam kết gây ô nhiễm heap.

Ví dụ,

List<Integer> myInts = new ArrayList<Integer>(); 
myInts.add(1); 
myInts.add(2); 

List<Number> myNums = myInts; //compiler error 
myNums.add(3.14); //heap pollution 

Nếu trình biên dịch Java không chỉ dừng lại bạn làm điều này, hệ thống kiểu thời gian chạy không thể ngăn bạn có, bởi vì không có cách nào, lúc chạy, để xác định rằng danh sách này là được coi là danh sách chỉ số nguyên. Thời gian chạy Java sẽ cho phép bạn đặt bất cứ điều gì bạn muốn vào danh sách này, khi nó chỉ nên chứa các số nguyên, bởi vì khi nó được tạo ra, nó được khai báo là một danh sách các số nguyên.

Như vậy, các nhà thiết kế của Java đã đảm bảo rằng bạn không thể đánh lừa trình biên dịch. Nếu bạn không thể đánh lừa trình biên dịch (như chúng ta có thể làm với các mảng), bạn cũng không thể đánh lừa hệ thống kiểu thời gian chạy.

Như vậy, chúng tôi nói rằng các loại chung là không thể tái chế.

Rõ ràng, điều này sẽ cản trở đa hình. Hãy xem xét ví dụ sau:

static long sum(Number[] numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Bây giờ bạn có thể sử dụng nó như thế này:

Integer[] myInts = {1,2,3,4,5}; 
Long[] myLongs = {1L, 2L, 3L, 4L, 5L}; 
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0}; 

System.out.println(sum(myInts)); 
System.out.println(sum(myLongs)); 
System.out.println(sum(myDoubles)); 

Nhưng nếu bạn cố gắng để thực hiện cùng mã với bộ sưu tập chung, bạn sẽ không thành công:

static long sum(List<Number> numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Bạn sẽ nhận được trình biên dịch erros nếu bạn cố gắng ...

List<Integer> myInts = asList(1,2,3,4,5); 
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L); 
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0); 

System.out.println(sum(myInts)); //compiler error 
System.out.println(sum(myLongs)); //compiler error 
System.out.println(sum(myDoubles)); //compiler error 

Giải pháp là học cách sử dụng hai tính năng mạnh mẽ của các Generics Java được gọi là hiệp phương sai và contravariance.

Hiệp phương sai

Với hiệp phương sai bạn có thể đọc các mục từ một cấu trúc, nhưng bạn không thể viết bất cứ điều gì vào nó. Tất cả những điều này là khai báo hợp lệ.

List<? extends Number> myNums = new ArrayList<Integer>(); 
List<? extends Number> myNums = new ArrayList<Float>(); 
List<? extends Number> myNums = new ArrayList<Double>(); 

Và bạn có thể đọc từ myNums:

Number n = myNums.get(0); 

Bởi vì bạn có thể chắc chắn rằng bất cứ danh sách thực tế chứa, nó có thể được upcasted đến một số (sau khi tất cả bất cứ điều gì mà kéo dài Số là một số , phải không?)

Tuy nhiên, bạn không được phép đặt bất kỳ thứ gì vào cấu trúc biến đổi.

myNumst.add(45L); //compiler error 

Điều này sẽ không được phép, vì Java không thể đảm bảo loại thực tế của đối tượng trong cấu trúc chung. Nó có thể là bất cứ điều gì mà mở rộng Số, nhưng trình biên dịch không thể chắc chắn. Vì vậy, bạn có thể đọc, nhưng không viết.

Contravariance

Với contravariance bạn có thể làm điều ngược lại. Bạn có thể đặt mọi thứ vào một cấu trúc chung chung, nhưng bạn không thể đọc được từ nó.

List<Object> myObjs = new List<Object>(); 
myObjs.add("Luke"); 
myObjs.add("Obi-wan"); 

List<? super Number> myNums = myObjs; 
myNums.add(10); 
myNums.add(3.14); 

Trong trường hợp này, bản chất thực sự của đối tượng là Danh sách đối tượng và thông qua contravariance, bạn có thể đặt số vào đó, về cơ bản vì tất cả các số đều có đối tượng là tổ tiên chung của chúng. Như vậy, tất cả các số là đối tượng, và do đó điều này là hợp lệ.

Tuy nhiên, bạn không thể đọc một cách an toàn bất cứ điều gì từ cấu trúc contravariant này giả định rằng bạn sẽ nhận được một số.

Number myNum = myNums.get(0); //compiler-error 

Như bạn có thể thấy, nếu trình biên dịch cho phép bạn viết dòng này, bạn sẽ nhận được ClassCastException khi chạy.

Nhận/Đặt Nguyên tắc

Như vậy, sử dụng hiệp phương sai khi bạn chỉ có ý định lấy các giá trị chung ra khỏi một cấu trúc, sử dụng contravariance khi bạn chỉ có ý định đưa các giá trị chung thành một cấu trúc và sử dụng chính xác chung nhập khi bạn định làm cả hai.

Ví dụ tốt nhất mà tôi có là sau đây sao chép bất kỳ loại số nào từ một danh sách vào danh sách khác. Chỉ có được mục từ nguồn và chỉ đặt mục trong số phận.

public static void copy(List<? extends Number> source, List<? super Number> destiny) { 
    for(Number number : source) { 
     destiny.add(number); 
    } 
} 

Nhờ vào quyền hạn của hiệp phương sai và contravariance này làm việc cho một trường hợp như thế này:

List<Integer> myInts = asList(1,2,3,4); 
List<Double> myDoubles = asList(3.14, 6.28); 
List<Object> myObjs = new ArrayList<Object>(); 

copy(myInts, myObjs); 
copy(myDoubles, myObjs); 
+15

Câu trả lời này sẽ xuất hiện ở đầu trang. Nice lời giải thích. –

+1

@edwindalorzo, có một lỗi nhỏ bạn muốn sửa trong Contravariance. Bạn nói 'Danh sách myObjs = new List ' cho 'Object' thứ hai). –

+0

Các ví dụ tuyệt vời, dễ dàng và rõ ràng về các khái niệm tinh tế này! – db1234

4

Trước câu trả lời; Hãy rõ ràng rằng

  1. Chỉ tạo tính năng thời gian biên dịch để đảm bảo TYPE_SAFETY, tính năng này không có sẵn trong RUNTIME.
  2. Chỉ một tham chiếu với Generics sẽ buộc loại an toàn; nếu tham chiếu không được khai báo với generics thì nó sẽ hoạt động mà không có loại an toàn.

Ví dụ:

List stringList = new ArrayList<String>(); 
stringList.add(new Integer(10)); // will be successful. 

Hy vọng điều này sẽ giúp bạn hiểu wildcard rõ ràng hơn.

//NOTE CE - Compilation Error 
//  4 - For 

class A {} 

class B extends A {} 

public class Test { 

    public static void main(String args[]) { 

     A aObj = new A(); 
     B bObj = new B(); 

     //We can add object of same type (A) or its subType is legal 
     List<A> list_A = new ArrayList<A>(); 
     list_A.add(aObj); 
     list_A.add(bObj); // A aObj = new B(); //Valid 
     //list_A.add(new String()); Compilation error (CE); 
     //can't add other type A aObj != new String(); 


     //We can add object of same type (B) or its subType is legal 
     List<B> list_B = new ArrayList<B>(); 
     //list_B.add(aObj); CE; can't add super type obj to subclass reference 
     //Above is wrong similar like B bObj = new A(); which is wrong 
     list_B.add(bObj); 



     //Wild card (?) must only come for the reference (left side) 
     //Both the below are wrong; 
     //List<? super A> wildCard_Wrongly_Used = new ArrayList<? super A>(); 
     //List<? extends A> wildCard_Wrongly_Used = new ArrayList<? extends A>(); 


     //Both <? extends A>; and <? super A> reference will accept = new ArrayList<A> 
     List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<A>(); 
         list_4__A_AND_SuperClass_A = new ArrayList<Object>(); 
         //list_4_A_AND_SuperClass_A = new ArrayList<B>(); CE B is SubClass of A 
         //list_4_A_AND_SuperClass_A = new ArrayList<String>(); CE String is not super of A 
     List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<A>(); 
          list_4__A_AND_SubClass_A = new ArrayList<B>(); 
         //list_4__A_AND_SubClass_A = new ArrayList<Object>(); CE Object is SuperClass of A 


     //CE; super reference, only accepts list of A or its super classes. 
     //List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<String>(); 

     //CE; extends reference, only accepts list of A or its sub classes. 
     //List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<Object>(); 

     //With super keyword we can use the same reference to add objects 
     //Any sub class object can be assigned to super class reference (A)     
     list_4__A_AND_SuperClass_A.add(aObj); 
     list_4__A_AND_SuperClass_A.add(bObj); // A aObj = new B(); 
     //list_4__A_AND_SuperClass_A.add(new Object()); // A aObj != new Object(); 
     //list_4__A_AND_SuperClass_A.add(new String()); CE can't add other type 

     //We can't put anything into "? extends" structure. 
     //list_4__A_AND_SubClass_A.add(aObj); compilation error 
     //list_4__A_AND_SubClass_A.add(bObj); compilation error 
     //list_4__A_AND_SubClass_A.add(""); compilation error 

     //The Reason is below   
     //List<Apple> apples = new ArrayList<Apple>(); 
     //List<? extends Fruit> fruits = apples; 
     //fruits.add(new Strawberry()); THIS IS WORNG :) 

     //Use the ? extends wildcard if you need to retrieve object from a data structure. 
     //Use the ? super wildcard if you need to put objects in a data structure. 
     //If you need to do both things, don't use any wildcard. 


     //Another Solution 
     //We need a strong reference(without wild card) to add objects 
     list_A = (ArrayList<A>) list_4__A_AND_SubClass_A; 
     list_A.add(aObj); 
     list_A.add(bObj); 

     list_B = (List<B>) list_4__A_AND_SubClass_A; 
     //list_B.add(aObj); compilation error 
     list_B.add(bObj); 

     private Map<Class<? extends Animal>, List<? extends Animal>> animalListMap; 

     public void registerAnimal(Class<? extends Animal> animalClass, Animal animalObject) { 

      if (animalListMap.containsKey(animalClass)) { 
       //Append to the existing List 
       /* The ? extends Animal is a wildcard bounded by the Animal class. So animalListMap.get(animalObject); 
       could return a List<Donkey>, List<Mouse>, List<Pikachu>, assuming Donkey, Mouse, and Pikachu were all sub classes of Animal. 
       However, with the wildcard, you are telling the compiler that you don't care what the actual type is as long as it is a sub type of Animal.  
       */ 
       //List<? extends Animal> animalList = animalListMap.get(animalObject); 
       //animalList.add(animalObject); //Compilation Error because of List<? extends Animal> 
       List<Animal> animalList = animalListMap.get(animalObject); 
       animalList.add(animalObject);  


      } 
    } 

    } 
} 
+0

cảm ơn bạn! ... các công cụ tuyệt vời – Ris

+0

@Dana Cũng Xem http://stackoverflow.com/questions/15255929/generics-wildcards-assigning-super-class-object-to-a-subclass-object-list –

+0

Câu trả lời của tôi trên loại Generic Class http://stackoverflow.com/questions/462297/how-to-use-classt-in-java/19545123#19545123 –

6

<? super E> nghĩa any object including E that is parent of E

<? extends E> nghĩa any object including E that is child of E .

Các vấn đề liên quan