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