Khi nào Java generic yêu cầu <? mở rộng T> thay vì <T> và có bất kỳ nhược điểm nào của việc chuyển đổi không?


205

Cho ví dụ sau (sử dụng JUnit với bộ so khớp Hamcrest):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

Điều này không biên dịch với assertThatchữ ký phương thức JUnit của:

public static <T> void assertThat(T actual, Matcher<T> matcher)

Thông báo lỗi trình biên dịch là:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

Tuy nhiên, nếu tôi thay đổi assertThatchữ ký phương thức thành:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Sau đó, công việc biên dịch.

Vì vậy, ba câu hỏi:

  1. Tại sao chính xác không biên dịch phiên bản hiện tại? Mặc dù tôi mơ hồ hiểu các vấn đề hiệp phương sai ở đây, tôi chắc chắn không thể giải thích nó nếu tôi phải làm.
  2. Có bất kỳ nhược điểm trong việc thay đổi assertThatphương pháp thành Matcher<? extends T>? Có trường hợp nào khác sẽ phá vỡ nếu bạn làm điều đó?
  3. Có bất kỳ điểm nào để khái quát hóa assertThatphương thức trong JUnit không? Các Matcherlớp học dường như không cần đến nó, kể từ JUnit gọi phương thức trận đấu, mà không gõ với bất kỳ vẻ chung chung, và cũng giống như một nỗ lực để buộc một an toàn loại mà không làm gì cả, như Matchersẽ chỉ là không thực tế phù hợp, và thử nghiệm sẽ thất bại bất kể. Không có hoạt động không an toàn liên quan (hoặc có vẻ như vậy).

Để tham khảo, đây là triển khai JUnit của assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

đây là liên kết là rất hữu ích (generics, thừa kế và kiểu phụ): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Câu trả lời:


145

Đầu tiên - tôi phải hướng dẫn bạn đến http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - cô ấy làm một công việc tuyệt vời.

Ý tưởng cơ bản là bạn sử dụng

<T extends SomeClass>

khi tham số thực tế có thể là SomeClasshoặc bất kỳ kiểu con của nó.

Trong ví dụ của bạn,

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

Bạn đang nói rằng expected có thể chứa các đối tượng Class đại diện cho bất kỳ lớp nào thực hiện Serializable. Bản đồ kết quả của bạn cho biết nó chỉ có thể giữDate các đối tượng lớp.

Khi bạn vượt qua trong kết quả, bạn thiết lập đang Tđể chính xác Mapcủa Stringđể Dateđối tượng lớp, mà không làm trận đấuMap của Stringbất cứ điều gì mà nhân Serializable.

Một điều cần kiểm tra - bạn có chắc chắn muốn Class<Date>và không Date? Bản đồ StringđếnClass<Date> không có âm thanh khủng khiếp hữu ích nói chung (tất cả những gì có thể giữ được Date.classnhư các giá trị chứ không phải là trường hợp Date)

Đối với việc khái quát hóa assertThat, ý tưởng là phương pháp có thể đảm bảo rằng một kiểu Matcherphù hợp với loại kết quả được truyền vào.


Trong trường hợp này, vâng tôi muốn có một bản đồ các lớp. Ví dụ tôi đã đưa ra được sử dụng các lớp JDK tiêu chuẩn thay vì các lớp tùy chỉnh của tôi, nhưng trong trường hợp này, lớp thực sự được khởi tạo thông qua sự phản chiếu và được sử dụng dựa trên khóa. (Một ứng dụng phân tán trong đó máy khách không có sẵn các lớp máy chủ, chỉ là khóa của lớp nào được sử dụng để thực hiện công việc phía máy chủ).
Yishai

6
Tôi đoán bộ não của tôi bị kẹt ở đâu là lý do tại sao Bản đồ chứa các loại loại Ngày không chỉ phù hợp với một loại Bản đồ có chứa các loại Loại nối tiếp. Chắc chắn các lớp loại Nối tiếp cũng có thể là các lớp khác, nhưng chắc chắn nó bao gồm loại Ngày.
Yishai

Trên assert. Đảm bảo rằng diễn viên được thực hiện cho bạn, phương thức matcher.matches () không quan tâm, vì vậy T không bao giờ được sử dụng, tại sao lại liên quan đến nó? (kiểu trả về của phương thức là void)
Yishai

Ahhh - đó là những gì tôi nhận được khi không đọc def của assert. Điều đó đủ gần. Có vẻ như chỉ để đảm bảo rằng một Matcher phù hợp được thông qua ...
Scott Stanchfield

28

Cảm ơn tất cả mọi người đã trả lời câu hỏi, nó thực sự đã giúp làm rõ mọi thứ cho tôi. Cuối cùng, câu trả lời của Scott Stanchfield gần nhất với cách tôi kết thúc việc hiểu nó, nhưng vì tôi không hiểu anh ấy khi anh ấy viết nó lần đầu tiên, tôi đang cố gắng khôi phục vấn đề để hy vọng người khác sẽ có lợi.

Tôi sẽ trình bày lại câu hỏi theo Danh sách, vì nó chỉ có một tham số chung và điều đó sẽ giúp bạn dễ hiểu hơn.

Mục đích của lớp tham số hóa (như Danh sách <Date>hoặc Bản đồ <K, V>như trong ví dụ) là để ép buộc và để trình biên dịch đảm bảo rằng điều này an toàn (không có ngoại lệ thời gian chạy).

Hãy xem xét trường hợp của Danh sách. Bản chất của câu hỏi của tôi là tại sao một phương thức lấy loại T và Danh sách sẽ không chấp nhận Danh sách một thứ gì đó nằm sâu hơn trong chuỗi thừa kế so với T. Hãy xem xét ví dụ giả định này:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

Điều này sẽ không biên dịch, bởi vì tham số danh sách là danh sách các ngày, không phải là danh sách các chuỗi. Generics sẽ không hữu ích nếu điều này đã được biên dịch.

Điều tương tự áp dụng cho Bản đồ <String, Class<? extends Serializable>>Nó không giống với Bản đồ <String, Class<java.util.Date>>. Chúng không phải là biến số, vì vậy nếu tôi muốn lấy một giá trị từ bản đồ chứa các lớp ngày và đưa nó vào bản đồ chứa các phần tử tuần tự hóa, điều đó tốt, nhưng một chữ ký phương thức có nội dung:

private <T> void genericAdd(T value, List<T> list)

Muốn có thể làm cả hai:

T x = list.get(0);

list.add(value);

Trong trường hợp này, mặc dù phương thức Junit không thực sự quan tâm đến những điều này, chữ ký phương thức yêu cầu hiệp phương sai mà nó không nhận được, do đó nó không được biên dịch.

Về câu hỏi thứ hai,

Matcher<? extends T>

Sẽ có nhược điểm của việc thực sự chấp nhận bất cứ điều gì khi T là một Đối tượng, đó không phải là mục đích của API. Mục đích là để đảm bảo tĩnh rằng đối sánh khớp với đối tượng thực tế và không có cách nào để loại trừ Object khỏi tính toán đó.

Câu trả lời cho câu hỏi thứ ba là sẽ không có gì bị mất, về mặt chức năng không được kiểm tra (sẽ không có lỗi đánh máy không an toàn trong API JUnit nếu phương pháp này không được khái quát hóa), nhưng họ đang cố gắng thực hiện một cái gì đó khác - đảm bảo tĩnh hai tham số có khả năng khớp nhau.

EDIT (sau khi chiêm nghiệm và trải nghiệm thêm):

Một trong những vấn đề lớn với chữ ký phương thức assertThat là cố gắng đánh đồng biến T với biến tham số chung của T. Điều đó không hoạt động, bởi vì chúng không phải là biến số. Vì vậy, ví dụ bạn có thể có T là một List<String>nhưng sau đó vượt qua một trận đấu mà trình biên dịch làm việc Matcher<ArrayList<T>>. Bây giờ nếu nó không phải là một tham số kiểu, mọi thứ sẽ ổn, bởi vì List và ArrayList là covariant, nhưng vì Generics, theo như trình biên dịch có liên quan yêu cầu ArrayList, nó không thể chấp nhận Danh sách vì những lý do mà tôi hy vọng là rõ ràng từ trên.


Tôi vẫn không hiểu tại sao tôi không thể u ám. Tại sao tôi không thể biến danh sách ngày thành một danh sách tuần tự?
Thomas Ahle

@ThomasAhle, vì sau đó các tài liệu tham khảo cho rằng đó là danh sách các Ngày sẽ gặp lỗi khi họ tìm thấy Chuỗi hoặc bất kỳ chuỗi nối tiếp nào khác.
Yishai

Tôi hiểu, nhưng nếu tôi bằng cách nào đó thoát khỏi tham chiếu cũ, như thể tôi trả về List<Date>từ một phương thức có kiểu List<Object>? Điều đó nên được an toàn ngay cả khi java không cho phép.
Thomas Ahle

14

Nó đun sôi xuống:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

Bạn có thể thấy tham chiếu Lớp c1 có thể chứa một thể hiện Dài (vì đối tượng cơ bản tại một thời điểm nhất định có thể có List<Long>), nhưng rõ ràng không thể chuyển sang Ngày vì không có gì đảm bảo rằng lớp "không xác định" là Ngày. Nó không phải là typsesafe, vì vậy trình biên dịch không cho phép nó.

Tuy nhiên, nếu chúng tôi giới thiệu một số đối tượng khác, giả sử List (trong ví dụ của bạn thì đối tượng này là Matcher), thì điều sau đây trở thành đúng:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Tuy nhiên, nếu loại Danh sách trở thành? mở rộng T thay vì T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Tôi nghĩ bằng cách thay đổi Matcher<T> to Matcher<? extends T>, về cơ bản, bạn đang giới thiệu kịch bản tương tự như gán l1 = l2;

Vẫn còn rất khó hiểu khi có các ký tự đại diện lồng nhau, nhưng hy vọng điều đó có lý do tại sao nó giúp hiểu được khái quát bằng cách xem cách bạn có thể gán các tham chiếu chung cho nhau. Điều này còn gây nhầm lẫn nữa vì trình biên dịch đang suy ra kiểu T khi bạn thực hiện cuộc gọi hàm (bạn không nói rõ ràng đó là T).


9

Lý do mã gốc của bạn không biên dịch là điều <? extends Serializable>đó không có nghĩa là "bất kỳ lớp nào mở rộng Nối tiếp", nhưng "một số lớp không xác định nhưng cụ thể mở rộng Nối tiếp."

Ví dụ, được cung cấp mã như được viết, nó hoàn toàn hợp lệ để gán new TreeMap<String, Long.class>()>cho expected. Nếu trình biên dịch cho phép mã biên dịch, assertThat()có lẽ nó sẽ bị phá vỡ bởi vì nó sẽ mong đợi Datecác đối tượng thay vìLong đối tượng mà nó tìm thấy trong bản đồ.


1
Tôi không hoàn toàn theo dõi - khi bạn nói "không có nghĩa là ... nhưng ...": sự khác biệt là gì? (giống như, ví dụ về lớp "được biết nhưng không đặc hiệu" phù hợp với định nghĩa trước nhưng không phải là định nghĩa sau?)
poundifdef

Vâng, đó là một chút khó xử; không chắc làm thế nào để diễn đạt nó tốt hơn ... nó có ý nghĩa hơn để nói, "'?' là một loại không xác định, không phải là loại phù hợp với bất cứ điều gì? "
erickson

1
Điều có thể giúp giải thích đó là "đối với bất kỳ lớp nào mở rộng <Serializable>
Nối tiếp

8

Một cách để tôi hiểu các ký tự đại diện là nghĩ rằng ký tự đại diện không chỉ định loại đối tượng có thể có tham chiếu chung có thể "có", nhưng loại tham chiếu chung khác mà nó tương thích (điều này nghe có vẻ khó hiểu ...) Như vậy, câu trả lời đầu tiên rất sai lệch trong cách diễn đạt.

Nói cách khác, List<? extends Serializable>có nghĩa là bạn có thể gán tham chiếu đó cho các Danh sách khác trong đó loại là một loại không xác định hoặc là một lớp con của Nối tiếp. ĐỪNG nghĩ về điều đó trong một DANH SÁCH MỘT LẦN có thể giữ các lớp con của Nối tiếp (vì đó là ngữ nghĩa không chính xác và dẫn đến sự hiểu lầm về Generics).


Điều đó chắc chắn có ích, nhưng "nghe có vẻ khó hiểu" là loại được thay thế bằng một "âm thanh khó hiểu". Theo dõi, vậy tại sao, theo lời giải thích này, phương thức với Matcher có <? extends T>biên dịch không?
Yishai

Nếu chúng ta định nghĩa là Danh sách <Nối tiếp>, thì nó cũng làm điều tương tự phải không? Tôi đang nói về para thứ 2 của bạn. tức là đa hình sẽ xử lý nó?
Supun Wijerathne

3

Tôi biết đây là một câu hỏi cũ nhưng tôi muốn chia sẻ một ví dụ mà tôi nghĩ giải thích các ký tự đại diện khá tốt. java.util.Collectionscung cấp phương pháp này:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

Tất nhiên, nếu chúng ta có Danh sách T, Danh sách có thể chứa các thể hiện của các loại mở rộng T. Nếu Danh sách chứa Động vật, Danh sách có thể chứa cả Chó và Mèo (cả Động vật). Chó có một tài sản "waggerVolume" và Mèo có một tài sản "meowVolume." Mặc dù chúng ta có thể muốn sắp xếp dựa trên các thuộc tính này cụ thể cho các lớp con T, làm thế nào chúng ta có thể mong đợi phương thức này để làm điều đó? Một hạn chế của So sánh là nó chỉ có thể so sánh hai thứ chỉ có một loại ( T). Vì vậy, yêu cầu đơn giản là Comparator<T>sẽ làm cho phương pháp này có thể sử dụng được. Nhưng, người tạo ra phương pháp này đã nhận ra rằng nếu một cái gì đó là một T, thì nó cũng là một ví dụ của các siêu lớp của T. Do đó, anh ta cho phép chúng tôi sử dụng Bộ so sánh Thoặc bất kỳ siêu lớp nào T, tức là ? super T.


1

nếu bạn sử dụng

Map<String, ? extends Class<? extends Serializable>> expected = null;

Vâng, đây là loại câu trả lời trên của tôi.
GreenieMeanie

Không, điều đó không giúp ích gì cho tình hình, ít nhất là cách tôi đã thử.
Yishai
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.