Tại sao mảng covariant nhưng generic là bất biến?


160

Từ Java hiệu quả của Joshua Bloch,

  1. Mảng khác với loại chung theo hai cách quan trọng. Mảng đầu tiên là covariant. Thế hệ là bất biến.
  2. Covariant đơn giản có nghĩa là nếu X là kiểu con của Y thì X [] cũng sẽ là kiểu con của Y []. Mảng là covariant Vì chuỗi là kiểu con của Object So

    String[] is subtype of Object[]

    Bất biến đơn giản có nghĩa là không phân biệt X có phải là kiểu con của Y hay không,

     List<X> will not be subType of List<Y>.

Câu hỏi của tôi là tại sao quyết định tạo mảng covariant trong Java? Có các bài viết SO khác như Tại sao Mảng bất biến, nhưng Danh sách covariant? , nhưng họ dường như tập trung vào Scala và tôi không thể theo dõi.


1
Đây không phải là vì thuốc generic đã được thêm vào sau này?
Sotirios Delimanolis

1
Tôi nghĩ rằng so sánh giữa các mảng và các bộ sưu tập là không công bằng, Bộ sưu tập sử dụng các mảng trong nền !!
Ahmed Adel Ismail

4
@ EL-conteDe-monteTereBentikh Chẳng hạn, tất cả các bộ sưu tập chẳng hạn LinkedList.
Paul Bellora

@PaulBellora tôi biết rằng Bản đồ khác với những người triển khai Bộ sưu tập, nhưng tôi đọc trong SCPJ6 rằng Bộ sưu tập thường dựa vào mảng !!
Ahmed Adel Ismail

Bởi vì không có ArrayStoreException; khi chèn phần tử sai trong Bộ sưu tập như mảng có nó. Vì vậy, Bộ sưu tập chỉ có thể tìm thấy điều này tại thời điểm truy xuất và điều đó cũng vì đúc. Vì vậy, thuốc generic sẽ đảm bảo giải quyết vấn đề này.
Kanagavelu Sugumar

Câu trả lời:


150

Qua wikipedia :

Các phiên bản ban đầu của Java và C # không bao gồm thuốc generic (hay còn gọi là đa hình tham số).

Trong một thiết lập như vậy, làm cho các mảng bất biến quy tắc ra các chương trình đa hình hữu ích. Ví dụ, hãy xem xét việc viết một hàm để xáo trộn một mảng hoặc một hàm kiểm tra hai mảng về sự bằng nhau bằng cách sử dụng Object.equalsphương thức trên các phần tử. Việc thực hiện không phụ thuộc vào loại phần tử chính xác được lưu trữ trong mảng, do đó có thể viết một hàm duy nhất hoạt động trên tất cả các loại mảng. Thật dễ dàng để thực hiện các chức năng của loại

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Tuy nhiên, nếu các kiểu mảng được coi là bất biến, thì chỉ có thể gọi các hàm này trên một mảng có chính xác kiểu Object[]. Chẳng hạn, người ta không thể xáo trộn một chuỗi các chuỗi.

Do đó, cả Java và C # đều xử lý các kiểu mảng một cách ngẫu nhiên. Chẳng hạn, trong C # string[]là một kiểu con của object[]và trong Java String[]là một kiểu con của Object[].

Này trả lời cho câu hỏi "Tại sao các mảng hiệp biến?", Hay chính xác hơn, "Tại sao mảng làm hiệp biến vào thời điểm đó ?"

Khi thuốc generic được giới thiệu, chúng cố tình không tạo ra sự đồng biến vì những lý do được chỉ ra trong câu trả lời này của Jon Skeet :

Không, a List<Dog>không phải là a List<Animal>. Xem xét những gì bạn có thể làm với một List<Animal>- bạn có thể thêm bất kỳ động vật nào vào đó ... bao gồm cả một con mèo. Bây giờ, bạn có thể thêm một cách hợp lý một con mèo vào một lứa chó con? Tuyệt đối không.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Đột nhiên bạn có một con mèo rất bối rối.

Động lực ban đầu để tạo ra các mảng đồng biến được mô tả trong bài viết trên wikipedia không áp dụng cho khái quát vì các ký tự đại diện có thể biểu thị hiệp phương sai (và chống chỉ định), ví dụ:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);

3
có, Mảng cho phép hành vi đa hình, tuy nhiên, nó giới thiệu các ngoại lệ thời gian chạy (không giống như các ngoại lệ thời gian biên dịch với tổng quát). ví dụ:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagerto Tìm hiểu

21
Đúng rồi. Mảng có các loại đáng chú ý và ném ArrayStoreExceptions khi cần thiết. Rõ ràng đây được coi là một sự thỏa hiệp xứng đáng vào thời điểm đó. Trái ngược với ngày nay: nhiều người coi hiệp phương sai là một sai lầm, khi nhìn lại.
Paul Bellora

1
Tại sao "nhiều" coi đó là một sai lầm? Nó hữu ích hơn nhiều so với việc không có hiệp phương sai mảng. Tần suất bạn đã thấy một ArrayStoreException; chúng khá hiếm Điều trớ trêu ở đây là imo không thể tha thứ ... trong số những sai lầm tồi tệ nhất từng xảy ra trong Java là phương sai sử dụng trang web hay còn gọi là ký tự đại diện.
Scott

3
@ScottMcKinney: "Tại sao" nhiều người "coi đó là một sai lầm?" AIUI, điều này là do hiệp phương thức mảng yêu cầu kiểm tra kiểu động trên tất cả các hoạt động gán mảng (mặc dù tối ưu hóa trình biên dịch có thể giúp ích?) Có thể gây ra chi phí thời gian chạy đáng kể.
Dominique Devriese

Cảm ơn, Dominique, nhưng từ quan sát của tôi, có vẻ như lý do "nhiều người" coi đó là một sai lầm nằm dọc theo dòng vẹt mà một số người khác đã nói. Một lần nữa, nhìn một cách mới mẻ về hiệp phương sai mảng, nó hữu ích hơn nhiều so với gây hại. Một lần nữa, lỗi LỚN thực tế mà Java mắc phải là phương sai chung của trang sử dụng thông qua các ký tự đại diện. Điều đó đã gây ra nhiều vấn đề hơn tôi nghĩ rằng "nhiều người" muốn thừa nhận.
Scott

30

Lý do là mọi mảng đều biết loại phần tử của nó trong thời gian chạy, trong khi bộ sưu tập chung không vì loại xóa.

Ví dụ:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

Nếu điều này được cho phép với các bộ sưu tập chung:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

Nhưng điều này sẽ gây ra vấn đề sau này khi ai đó sẽ cố gắng truy cập danh sách:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String

Tôi nghĩ câu trả lời của Paul Bellora là phù hợp hơn khi ông bình luận về lý do TẠI SAO Mảng là đồng biến. Nếu mảng được thực hiện bất biến, thì nó ổn. bạn sẽ có loại tẩy xóa với nó. Lý do chính cho loại Erasure thuộc tính là để tương thích ngược chính xác?
eagerto Tìm hiểu

@ user2708477, vâng, loại tẩy được giới thiệu vì khả năng tương thích ngược. Và vâng, câu trả lời của tôi cố gắng trả lời câu hỏi trong tiêu đề, tại sao thuốc generic là bất biến.
Katona

Thực tế là các mảng biết loại của chúng có nghĩa là trong khi hiệp phương sai cho phép mã yêu cầu lưu trữ thứ gì đó vào một mảng mà nó không phù hợp - điều đó không có nghĩa là một cửa hàng như vậy sẽ được phép diễn ra. Do đó, mức độ nguy hiểm được đưa ra bằng cách có các mảng là covariant ít hơn nhiều so với nếu họ không biết các loại của chúng.
supercat

@supercat, chính xác, điều tôi muốn chỉ ra là đối với thuốc generic có loại tẩy tại chỗ, hiệp phương sai không thể được thực hiện với sự an toàn tối thiểu của kiểm tra thời gian chạy
Katona

1
Cá nhân tôi nghĩ rằng câu trả lời này cung cấp lời giải thích đúng về lý do tại sao Mảng là đồng biến khi Bộ sưu tập không thể. Cảm ơn!
asss

22

Có thể là sự giúp đỡ này : -

Generics không covariant

Mảng trong ngôn ngữ Java là covariant - có nghĩa là nếu Integer mở rộng Số (mà nó làm), thì không chỉ là Số nguyên mà còn là Số, mà còn là một Số nguyên Number[], và bạn có thể tự do chuyển hoặc gán Integer[]trong đó a Number[]được gọi là (Chính thức hơn, nếu Number là một siêu kiểu của Integer, thì đó Number[]là một siêu kiểu của Integer[].) Bạn có thể nghĩ điều tương tự cũng đúng với các loại chung - đó List<Number>là một siêu kiểu List<Integer>, và bạn có thể vượt qua List<Integer>nơi mà một người List<Number>được mong đợi. Thật không may, nó không hoạt động theo cách đó.

Hóa ra có một lý do chính đáng để nó không hoạt động theo cách đó: Nó sẽ phá vỡ các loại thuốc an toàn được cho là sẽ cung cấp. Hãy tưởng tượng bạn có thể gán a List<Integer>cho a List<Number>. Sau đó, đoạn mã sau sẽ cho phép bạn đặt thứ gì đó không phải là Số nguyên vào List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Bởi vì ln là một List<Number>, thêm một Float vào nó có vẻ hoàn toàn hợp pháp. Nhưng nếu ln được đặt bí danh li, thì nó sẽ phá vỡ lời hứa an toàn kiểu ẩn trong định nghĩa của li - rằng đó là một danh sách các số nguyên, đó là lý do tại sao các loại chung không thể đồng biến.


3
Đối với Mảng, bạn nhận được một ArrayStoreExceptionlúc chạy.
Sotirios Delimanolis

4
Câu hỏi của tôi WHYlà Mảng được thực hiện covariant. như Sotirios đã đề cập, với Mảng, người ta sẽ nhận được ArrayStoreException trong thời gian chạy, nếu Mảng được tạo bất biến, thì chúng ta có thể phát hiện lỗi này vào thời gian biên dịch chính xác không?
eagerto Tìm hiểu

@eagertoLearn: Một điểm yếu về ngữ nghĩa chính của Java là nó không có gì trong hệ thống loại của nó có thể phân biệt "Mảng không chứa gì ngoài các dẫn xuất Animal, không phải chấp nhận bất kỳ mục nào nhận được từ nơi khác" từ "Mảng không chứa gì ngoài Animal," và phải sẵn sàng chấp nhận các tham chiếu được cung cấp bên ngoài tới Animal. Mã cần cái trước nên chấp nhận một mảng Cat, nhưng mã cần cái sau thì không. Nếu trình biên dịch có thể phân biệt hai loại, nó có thể cung cấp kiểm tra thời gian biên dịch. Thật không may, điều duy nhất phân biệt họ ...
supercat

... Là liệu mã có thực sự cố lưu trữ bất cứ thứ gì vào chúng hay không, và không có cách nào để biết điều đó cho đến khi chạy.
supercat

3

Mảng là covariant vì ít nhất hai lý do:

  • Nó rất hữu ích cho các bộ sưu tập chứa thông tin sẽ không bao giờ thay đổi thành covariant. Để một tập hợp T là covariant, cửa hàng sao lưu của nó cũng phải là covariant. Mặc dù người ta có thể thiết kế một Tbộ sưu tập bất biến không sử dụng T[]làm cửa hàng sao lưu của nó (ví dụ: sử dụng một cây hoặc danh sách được liên kết), một bộ sưu tập như vậy sẽ khó có thể thực hiện cũng như được hỗ trợ bởi một mảng. Người ta có thể lập luận rằng một cách tốt hơn để cung cấp cho các bộ sưu tập bất biến đồng biến sẽ là xác định loại "mảng bất biến covariant" mà họ có thể sử dụng một cửa hàng sao lưu, nhưng đơn giản là cho phép hiệp phương sai có thể dễ dàng hơn.

  • Mảng sẽ thường xuyên bị đột biến bởi mã mà không biết loại sự việc nào sẽ xảy ra trong chúng, nhưng sẽ không đưa vào mảng bất cứ thứ gì không được đọc từ cùng một mảng. Một ví dụ điển hình của việc này là sắp xếp mã. Về mặt khái niệm, các kiểu mảng có thể bao gồm các phương thức để hoán đổi hoặc hoán vị các phần tử (các phương thức đó có thể áp dụng như nhau cho bất kỳ loại mảng nào) hoặc định nghĩa một đối tượng "trình điều khiển mảng" có tham chiếu đến một mảng và một hoặc nhiều điều đã được đọc từ nó và có thể bao gồm các phương thức để lưu trữ các mục đã đọc trước đó vào mảng mà chúng đã đến. Nếu các mảng không phải là biến số, mã người dùng sẽ không thể xác định loại như vậy, nhưng thời gian chạy có thể bao gồm một số phương thức chuyên biệt.

Thực tế là các mảng là covariant có thể được xem là một hack xấu xí, nhưng trong hầu hết các trường hợp, nó tạo điều kiện cho việc tạo mã làm việc.


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- điểm tốt
eagerto Tìm hiểu

3

Một tính năng quan trọng của các loại tham số là khả năng viết các thuật toán đa hình, tức là các thuật toán hoạt động trên cấu trúc dữ liệu bất kể giá trị tham số của nó, chẳng hạn như Arrays.sort().

Với thuốc generic, điều đó được thực hiện với các loại ký tự đại diện:

<E extends Comparable<E>> void sort(E[]);

Để thực sự hữu ích, các loại ký tự đại diện yêu cầu chụp ký tự đại diện và điều đó đòi hỏi phải có khái niệm về một tham số loại. Không có cái nào có sẵn tại các mảng thời gian được thêm vào Java và việc tạo các mảng của kiểu tham chiếu kiểu tham chiếu cho phép một cách đơn giản hơn nhiều để cho phép các thuật toán đa hình:

void sort(Comparable[]);

Tuy nhiên, sự đơn giản đó đã mở một lỗ hổng trong hệ thống kiểu tĩnh:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

yêu cầu kiểm tra thời gian chạy của mọi truy cập ghi vào một mảng kiểu tham chiếu.

Tóm lại, cách tiếp cận mới hơn được thể hiện bằng khái quát làm cho hệ thống loại phức tạp hơn, nhưng cũng an toàn hơn về mặt tĩnh, trong khi cách tiếp cận cũ đơn giản hơn và loại an toàn tĩnh hơn. Các nhà thiết kế ngôn ngữ đã chọn cách tiếp cận đơn giản hơn, có nhiều việc quan trọng hơn là đóng một lỗ hổng nhỏ trong hệ thống loại hiếm khi gây ra vấn đề. Sau này, khi Java được thiết lập và các nhu cầu cấp thiết được quan tâm, họ có tài nguyên để thực hiện đúng cho các tổng quát (nhưng thay đổi nó thành các mảng sẽ phá vỡ các chương trình Java hiện có).


2

Generics là bất biến : từ JSL 4.10 :

... Subtyping không mở rộng thông qua các loại chung: T <: U không ngụ ý rằng C<T><: C<U>...

và một vài dòng nữa, JLS cũng giải thích rằng
Mảng là covariant (viên đạn đầu tiên):

4.10.3 Phân nhóm giữa các loại mảng

nhập mô tả hình ảnh ở đây


2

Tôi nghĩ rằng họ đã đưa ra một quyết định sai lầm ngay từ đầu đã tạo ra sự đồng biến mảng. Nó phá vỡ sự an toàn kiểu như được mô tả ở đây và họ đã bị mắc kẹt vì điều đó vì tính tương thích ngược và sau đó họ đã cố gắng không mắc lỗi tương tự cho chung chung. Và đó là một trong những lý do khiến Joshua Bloch thích liệt kê danh sách của arra ys trong Mục 25 của cuốn sách "Java hiệu quả (ấn bản thứ hai)"


Josh Block là tác giả của khung bộ sưu tập của Java (1.2) và là tác giả của các tổng quát của Java (1.5). Vì vậy, chàng trai xây dựng các thế hệ mà mọi người phàn nàn cũng là trùng hợp, chàng trai đã viết cuốn sách nói rằng họ là cách tốt hơn để đi? Không phải là một bất ngờ lớn!
cpurdy

1

Của tôi: Khi mã đang mong đợi một mảng A [] và bạn cung cấp cho nó B [] trong đó B là một lớp con của A, chỉ có hai điều cần lo lắng: điều gì xảy ra khi bạn đọc một phần tử mảng và điều gì xảy ra nếu bạn viết nó Vì vậy, không khó để viết các quy tắc ngôn ngữ để đảm bảo rằng an toàn loại được bảo toàn trong mọi trường hợp (quy tắc chính là ArrayStoreExceptioncó thể bị ném nếu bạn cố gắn A vào B []). Tuy nhiên, đối với một người chung chung, khi bạn khai báo một lớp SomeClass<T>, có thể có bất kỳ cách nào Tđược sử dụng trong phần thân của lớp và tôi đoán rằng nó quá phức tạp để tìm ra tất cả các kết hợp có thể để viết quy tắc khi nào mọi thứ được cho phép và khi chúng không.

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.