Sự khác biệt giữa <là gì? siêu E> và <? kéo dài E>?


147

Sự khác biệt giữa <? super E>và là <? extends E>gì?

Chẳng hạn, khi bạn nhìn vào lớp, java.util.concurrent.LinkedBlockingQueuecó chữ ký sau cho hàm tạo:

public LinkedBlockingQueue(Collection<? extends E> c)

và cho một cho phương thức:

public int drainTo(Collection<? super E> c)

Câu trả lời:


182

Người đầ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 là lớp con của E". (Trong cả hai trường hợp, bản thân E đều ổn.)

Vì vậy, hàm tạo sử dụng ? extends Ebiểu mẫu để nó đảm bảo rằng khi nó tìm nạp các giá trị từ bộ sưu tập, tất cả chúng sẽ là E hoặc một số lớp con (tức là nó tương thích). Các drainTophương pháp đang cố gắng để đưa các giá trị vào bộ sưu tập, vì vậy bộ sưu tập phải có một loại yếu tố E hoặc một lớp cha .

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

và a LinkedBlockingQueue<Parent>. Bạn có thể xây dựng điều này trong List<Child>đó sẽ sao chép tất cả các yếu tố một cách an toàn, bởi vì mỗi phần tử Childlà cha mẹ. Bạn không thể vượt qua List<Object>vì một số yếu tố có thể không tương thích Parent.

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


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

1
@Jon bạn có ý gì bởi (Trong cả hai trường hợp, bản thân E đều ổn.) Trong đoạn đầu tiên?
Geek

2
@Geek: Ý tôi là nếu bạn có một cái gì đó giống như ? extends InputStreamhoặc ? super InputStreamsau đó bạn có thể sử dụng một InputStreamđối số.
Jon Skeet

Tôi chưa bao giờ thực sự có được lời giải thích PECS của Josh Block trong Java hiệu quả. Tuy nhiên @Yishai, đây là một cách hữu ích để ghi nhớ. Có lẽ chúng ta có thể đề xuất một bản ghi nhớ mới, SAGE: Super -> Thêm / Nhận -> Mở rộng
dcompiled

Vì vậy, nếu tôi đọc đúng, "<? Extends E>" yêu cầu "?" là một lớp con của "E" và "<? super E>" yêu cầu "E" là một lớp con của "?", phải không?
El Suscriptor Justiciero

130

Những lý do cho điều này dựa trên cách Java triển khai các tổng quát.

Một ví dụ Mảng

Với mảng bạn có thể làm điều này (mảng là covariant)

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

Nhưng, điều 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 một ArrayStoreException. Bởi vì bạn đang cố gắng đặt một số kép vào một mảng số nguyên (bất kể được truy cập thông qua 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à như vậy bởi vì mảng là những gì chúng ta gọi là các loại có thể xác nhận lại . Đ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 mà đơn giản là đượ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 điều khác là loại tham chiếu mà bạn sử dụng để truy cập nó, phải không?

Vấn đề với Java Generics

Bây giờ, vấn đề với các kiểu chung Java là thông tin kiểu bị trình biên dịch loại bỏ và nó không có sẵn trong thời gian chạy. Quá trình này được gọi là loại tẩy . Có nhiều lý do để triển khai các khái quát như thế này trong Java, nhưng đó là một câu chuyện dài và nó phải làm, trong số những điều khác, với khả năng tương thích nhị phân với mã có sẵn (xem Làm thế nào chúng ta có các tổng quát chúng ta có ).

Nhưng điểm quan trọng ở đây là vì, trong thời gian chạy không có thông tin loại, không có cách nào để đảm bảo rằng chúng ta không phạm phải ô nhiễm đống.

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 ngăn bạn thực hiện việc này, thì hệ thống loại thời gian chạy cũng không thể ngăn bạn, bởi vì không có cách nào, trong thời gian chạy, để xác định rằng danh sách này chỉ được coi là danh sách các số nguyên. Thời gian chạy Java sẽ cho phép bạn đặt bất cứ thứ 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, 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 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 sử dụng .

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ột mã với các 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 erros trình biên dịch 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 tổng quát Java được gọi là hiệp phương sai và chống đối.

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ả đều là những tuyên bố 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ứ thứ gì trong danh sách thực tế có chứa, nó có thể được đưa lên một Số (sau tất cả mọi thứ mở rộng Số là một Số, phải không?)

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

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

Điều này sẽ không được phép, bởi vì Java không thể đảm bảo loại thực tế của đối tượng trong cấu trúc chung là gì. Nó có thể là bất cứ thứ gì mở rộng Number, 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.

Chống chỉ định

Với chống chỉ định bạn có thể làm ngược lại. Bạn có thể đặt mọi thứ vào một cấu trúc chung, nhưng bạn không thể đọ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 tế của đối tượng là Danh sách các Đối tượng và thông qua chống chỉ định, 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ố đều 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 chống chỉ định này với 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.

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

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

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

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

Nhờ vào sức mạnh của hiệp phương sai và chống chỉ định, tác dụng này 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);

27
Câu trả lời này nên gập lên đầu. Giải thích tốt đẹp.
Suresh Atta

1
@edwindalorzo, có một lỗi đánh máy nhỏ mà bạn muốn sửa theo Contravariance. Bạn nói List<Object> myObjs = new List<Object();(thiếu phần đóng >thứ hai Object).

Ví dụ tuyệt vời, dễ dàng và rõ ràng của các khái niệm tinh tế!
db1234

Một cái gì đó bạn có thể thêm để giúp người khác ghi nhớ mọi thứ. Khi bạn muốn gọi một phương thức từ một siêu lớp, bạn sử dụng super.methodName. Khi sử dụng <? super E>, nó có nghĩa là "một cái gì đó supertheo hướng" trái ngược với một cái gì đó extendstheo hướng. Ví dụ: Objectlà trong superhướng Number(vì nó là một lớp siêu) và Integerđang trong extendshướng (vì nó kéo dài Number).
BrainStorm.exe

59

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

<? super E>định nghĩa Elà giới hạn dưới: " Ecó thể được sử dụng cho điều này."


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

1
Trong nhiều thập kỷ nay (với OOP) Tôi đã chiến đấu với sự đảo ngược bản năng của các khái niệm "trên" và "dưới". Làm nặng thêm! Đối với tôi, Objectvốn dĩ là một lớp cấp thấp hơn, mặc dù vị trí của nó 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 chưa bao giờ có thể hoàn tác điều này mặc dù có rất nhiều cố gắng.

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

@DavidMoles, tại sao? Bạn rõ ràng không làm theo những gì tôi đang nói. "Siêu lớp" gần giống với "siêu lớp"; khái niệm chuyên môn hóa là giảm khả năng áp dụng theo mối quan hệ IS-A. Quả táo IS-A. Fruit (superclass) là một superset bao gồm Apple (lớp con) là một tập hợp con. Mối quan hệ bằng lời nói đó là tốt. Điều tôi đang nói bị phá vỡ là khái niệm rằng "trên" và "dưới" có ánh xạ nội tại thành "superset" và "tập hợp con". Trên và dưới nên tránh như các điều khoản trong OO.

1
@ tgm1024 "Super-" xuất phát từ siêu Latin "ở trên, trên" và "sub-" từ phụ Latin "bên dưới, bên dưới". Đó là để nói, về mặt từ nguyên, siêu là lên và phụ là xuống.
David Moles

12

Tôi sẽ cố gắng trả lời điều này. Nhưng để có được câu trả lời thực sự tốt, bạn nên kiểm tra cuốn sách Java hiệu quả (Phiên bản 2) của Joshua Bloch. Ông mô tả PECS ghi nhớ, viết tắt của "Nhà sản xuất mở rộng, Siêu tiêu dùng".

Ý tưởng là nếu mã của bạn đang tiêu thụ các giá trị chung từ đối tượng thì bạn nên sử dụng phần mở rộng. nhưng nếu bạn đang tạo ra các giá trị mới cho loại chung, bạn nên sử dụng siêu.

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 xem cuốn sách này: http://java.sun.com/docs/books/effective/


12

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

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


Câu trả lời ngắn gọn và ngọt ngào.
chirag soni

7

Bạn có thể muốn google cho các điều khoản contravariance ( <? super E>) và hiệp phương sai ( <? extends E>). Tôi thấy rằng điều hữu ích nhất khi hiểu khái quát là để tôi hiểu chữ ký của phương thức Collection.addAll:

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

Giống như bạn muốn có thể thêm Stringmột List<Object>:

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

Bạn cũng có thể thêm List<String>(hoặc bất kỳ tập hợp Strings) nào thông qua addAllphương thức:

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

Tuy nhiên, bạn nên nhận ra rằng a List<Object>và a 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ề một tham số loại covariant - tức là <? extends T>bit.

Một khi bạn có điều này, nó đơn giản để nghĩ về kịch bản mà bạn muốn contravariance cũng (đánh dấu vào Comparablegiao diện).


4

Trước câu trả lời; Xin hãy nói rõ rằng

  1. Generics chỉ biên dịch tính năng thời gian để đảm bảo TYPE_SAFETY, nó sẽ không khả dụng 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 generic thì nó sẽ hoạt động mà không cần loại an toàn.

Thí 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 được ký tự đại diện 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);      


            } 
    }

    }
}



1
Giải thích tốt với mã. Nhưng nếu bạn đã sử dụng các bình luận trong mã bên ngoài khối mã, nó sẽ tốt hơn để xem và dễ đọc hơn.
Bohhu

3

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

Một ký tự đại diện có ràng buộc thấp hơn trông giống như "? Super Type" và là viết tắt của gia đình của tất cả các loại là siêu kiểu Loại, Loại được bao gồm. Loại được gọi là giới hạn dưới.


1

Bạn có một lớp Parent và một lớp con được thừa hưởng từ lớp Parent. Lớp cha mẹ được kế thừa từ một lớp khác gọi là GrandParent Class. Vì vậy, thứ tự kế thừa là GrandParent> Parent> Child. Bây giờ, <? mở rộng Parent> - Điều này chấp nhận lớp Parent hoặc lớp Child <? super Parent> - Điều này chấp nhận lớp Parent hoặc lớp GrandParent

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.