Mảng là covariant
Mảng được gọi là covariant, về cơ bản có nghĩa là, với các quy tắc phân nhóm của Java, một mảng kiểu T[]
có thể chứa các phần tử kiểu T
hoặc bất kỳ kiểu con nào T
. Ví dụ
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Nhưng không chỉ có vậy, các quy tắc phân nhóm của Java cũng nói rằng một mảng S[]
là một kiểu con của mảng T[]
nếu S
là một kiểu con của T
, do đó, một cái gì đó như thế này cũng hợp lệ:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Bởi vì theo các quy tắc phân nhóm trong Java, một mảng Integer[]
là một kiểu con của một mảngNumber[]
vì Integer là một kiểu con của Số.
Nhưng quy tắc phân nhóm này có thể dẫn đến một câu hỏi thú vị: điều gì sẽ xảy ra nếu chúng ta 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 chúng ta chạy mã này, chúng ta sẽ nhận được một ArrayStoreException
vì chúng ta đang cố gắng đặt một số kép vào một mảng số nguyên. Thực tế là chúng ta đang truy cập vào mảng thông qua một tham chiếu Số không liên quan ở đây, điều quan trọng là mảng đó là một mảng các số nguyên.
Điều này có nghĩa là chúng ta có thể đánh lừa trình biên dịch, nhưng chúng ta không thể đánh lừa hệ thống loại thời gian chạy. Và điều này là như vậy bởi vì mảng là cái mà chúng ta gọi là kiểu có thể xác định lại. Điều này có nghĩa là tại 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ểuNumber[]
.
Vì vậy, như chúng ta có thể thấy, một điều là loại thực tế của đối tượng, một điều khác là loại tham chiếu mà chúng ta 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 trong Java là thông tin kiểu cho các tham số kiểu bị trình biên dịch loại bỏ sau khi quá trình biên dịch mã được thực hiện; do đó loại thông tin này 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 tốt để 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 với khả năng tương thích nhị phân với mã có sẵn.
Điểm quan trọng ở đây là vì tại thời điểm 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 phạm phải ô nhiễm đống.
Bây giờ hãy xem xét mã không an toàn sau:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Nếu trình biên dịch Java không ngăn chúng tôi 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 chúng tôi, vì không có cách nào, vào 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 chúng tôi đặt bất cứ thứ gì chúng tôi 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. Đó là lý do tại sao trình biên dịch từ chối dòng số 4 vì nó không an toàn và nếu được phép có thể phá vỡ các giả định của hệ thống loại.
Như vậy, các nhà thiết kế của Java đã đảm bảo rằng chúng ta không thể đánh lừa trình biên dịch. Nếu chúng ta 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) thì chúng ta cũng không thể đánh lừa hệ thống loại 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ể xác định lại, vì tại thời điểm chạy, chúng tôi không thể xác định bản chất thực sự của loại chung.
Tôi đã bỏ qua một số phần của câu trả lời này, bạn có thể đọc toàn bộ bài viết ở đây:
https://dzone.com/articles/covariance-and-contravariance