IT-Swarm.Net

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?

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 được biên dịch với chữ ký phương thức JUnit assertThat 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 chữ ký phương thức assertThat 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 nào trong việc thay đổi phương thức assertThat thành Matcher<? extends T> không? 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 phương thức assertThat trong JUnit không? Lớp Matcher dường như không yêu cầu nó, vì JUnit gọi phương thức khớp, không được gõ với bất kỳ chung chung nào, và trông giống như một nỗ lực để buộc một loại an toàn không làm gì cả, vì Matcher sẽ không trong 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());
    }
}
182
Yishai

Đầ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à SomeClass hoặc bất kỳ kiểu con nào 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ể chứa các đối tượng lớp Date.

Khi bạn chuyển kết quả, bạn đang đặt T thành chính xác Map của String thành các đối tượng lớp Date, không khớp với Map của String với bất kỳ thứ gì Serializable.

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

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

126
Scott Stanchfield

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 được tham số hóa (như List<Date> hoặc Map<K, V> như trong ví dụ) là buộc một downcast 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 Map<String, Class<? extends Serializable>> Nó không giống với Map<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, thì 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 một biến T với 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à List<String> nhưng sau đó vượt qua một trận đấu mà trình biên dịch xử lý thành 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, 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.

24
Yishai

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ể là List<Long>), nhưng rõ ràng không thể chuyển thành 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).

14
GreenieMeanie

Lý do mã ban đầu của bạn không biên dịch là vì <? extends Serializable> không 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ã dưới dạng văn bản, việc gán new TreeMap<String, Long.class>()> cho expected là hoàn toàn hợp lệ. Nếu trình biên dịch cho phép mã biên dịch, thì assertThat() có thể sẽ bị phá vỡ vì nó sẽ mong đợi các đối tượng Date thay vì các đối tượng Long mà nó tìm thấy trên bản đồ.

8
erickson

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. KHÔNG nghĩ về điều đó trong 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).

7
GreenieMeanie

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.Collections cung cấp phương thức này:

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

Nếu chúng ta có Danh sách T, tất nhiên, Danh sách có thể chứa các phiên bả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 tôi 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 của T, làm thế nào chúng ta có thể mong đợi phương thức này thực hiện điều đó? Một hạn chế của Trình so sánh là nó chỉ có thể so sánh hai thứ chỉ có một loại (T). Vì vậy, chỉ cần một Comparator<T> sẽ làm cho phương thức này có thể sử dụng được. Nhưng, người tạo ra phương thức này đã nhận ra rằng nếu một cái gì đó là T, thì đó cũng là một ví dụ của các siêu lớp của T. Do đó, anh ấy cho phép chúng tôi sử dụng Bộ so sánh T hoặc bất kỳ siêu lớp nào của T, tức là ? super T.

3
Lucas Ross

nếu bạn sử dụng

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