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);
+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
Rất cám ơn. Giải thích tuyệt vời! –
@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