Kiểu trả về chung giới hạn trên - giao diện so với lớp - mã hợp lệ đáng ngạc nhiên


171

Đây là một ví dụ trong thế giới thực từ API thư viện của bên thứ 3, nhưng được đơn giản hóa.

Được biên dịch với Oracle JDK 8u72

Hãy xem xét hai phương pháp sau:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Cả hai đều báo cáo cảnh báo "diễn viên không được kiểm tra" - tôi hiểu tại sao. Điều gây trở ngại cho tôi là tại sao tôi có thể gọi

Integer x = getCharSequence();

và nó biên dịch? Trình biên dịch nên biết rằng Integerkhông thực hiện CharSequence. Cuộc gọi đến

Integer y = getString();

đưa ra một lỗi (như mong đợi)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Ai đó có thể giải thích tại sao hành vi này sẽ được coi là hợp lệ? Làm thế nào nó sẽ hữu ích?

Máy khách không biết rằng cuộc gọi này không an toàn - mã của máy khách sẽ biên dịch mà không có cảnh báo. Tại sao trình biên dịch không cảnh báo về điều đó / gây ra lỗi?

Ngoài ra, nó khác với ví dụ này như thế nào:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Cố gắng vượt qua List<Integer>cho một lỗi, như mong đợi:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Nếu đó là một lỗi, tại sao Integer x = getCharSequence();không?


15
hấp dẫn! truyền trên LHS Integer x = getCharSequence();sẽ biên dịch, nhưng truyền trên RHS Integer x = (Integer) getCharSequence();không biên dịch được
flakes

Phiên bản nào của trình biên dịch java bạn đang sử dụng? Vui lòng ghi rõ thông tin này trong câu hỏi.
Federico Peralta Schaffner

@FedericoPeraltaSchaffner không thể hiểu tại sao điều đó lại quan trọng - đây là câu hỏi trực tiếp về JLS.
Boris the Spider

@BoristheSpider Bởi vì cơ chế suy luận kiểu đã thay đổi cho java8
Federico Peralta Schaffner

1
@FedericoPeraltaSchaffner - Tôi đã gắn thẻ câu hỏi đã có [java-8], nhưng tôi đã thêm phiên bản trình biên dịch trong bài đăng ngay bây giờ.
Adam Michalik

Câu trả lời:


184

CharSequencelà một interface. Do đó, ngay cả khi SomeClasskhông triển khai CharSequence, việc tạo một lớp là hoàn toàn có thể

class SubClass extends SomeClass implements CharSequence

Do đó bạn có thể viết

SomeClass c = getCharSequence();

bởi vì kiểu suy ra Xlà kiểu giao nhau SomeClass & CharSequence.

Đây là một chút kỳ lạ trong trường hợp IntegerIntegerlà cuối cùng, nhưng finalkhông đóng vai trò nào trong các quy tắc này. Ví dụ bạn có thể viết

<T extends Integer & CharSequence>

Mặt khác, Stringkhông phải là một interface, vì vậy sẽ không thể mở rộng SomeClassđể có được một kiểu con String, bởi vì java không hỗ trợ đa kế thừa cho các lớp.

Với Liství dụ này, bạn cần nhớ rằng generic không phải là covariant hay contravariant. Điều này có nghĩa là nếu Xlà một kiểu con của Y, List<X>thì không phải là một kiểu con cũng không phải là siêu kiểu List<Y>. Vì Integerkhông thực hiện CharSequence, bạn không thể sử dụng List<Integer>trong doCharSequencephương thức của mình .

Tuy nhiên, bạn có thể lấy cái này để biên dịch

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Nếu bạn có một phương pháp mà trả về một List<T>như thế này:

static <T extends CharSequence> List<T> foo() 

bạn có thể làm

List<? extends Integer> list = foo();

Một lần nữa, điều này là do loại suy ra Integer & CharSequencevà đây là một kiểu con của Integer.

Các loại giao lộ xảy ra ngầm khi bạn chỉ định nhiều giới hạn (ví dụ <T extends SomeClass & CharSequence>).

Để biết thêm thông tin, đây là một phần của JLS nơi nó giải thích cách giới hạn loại hoạt động. Bạn có thể bao gồm nhiều giao diện, ví dụ:

<T extends String & CharSequence & List & Comparator>

nhưng chỉ ràng buộc đầu tiên có thể là một giao diện không.


62
Tôi không biết bạn có thể đặt một &định nghĩa chung chung. +1
mảnh vào

13
@flkes Bạn có thể đặt nhiều hơn một, nhưng chỉ đối số đầu tiên có thể là giao diện không có giao diện. <T extends String & List & Comparator>là ok nhưng <T extends String & Integer>không phải, vì Integerkhông phải là một giao diện.
Paul Boddington

7
@PaulBoddington Có một số cách sử dụng thực tế cho các phương pháp này. Ví dụ: nếu loại không thực sự được sử dụng cho dữ liệu được lưu trữ. Ví dụ cho điều này Collections.emptyList()cũng như Optional.empty(). Những triển khai trả về của một giao diện chung, nhưng không lưu trữ bất cứ thứ gì.
Stefan Dollase

6
Và không ai nói rằng một lớp đang finaltrong thời gian biên dịch sẽ finalở thời gian chạy.
Holger

7
@Federico Peralta Schaffner: vấn đề ở đây là, phương thức getCharSequence()hứa hẹn sẽ trả về bất cứ thứ gì Xngười gọi cần, bao gồm trả về một kiểu mở rộng Integervà thực hiện CharSequencenếu người gọi cần nó và theo lời hứa này, nó đúng khi cho phép gán kết quả Integer. Đó là phương pháp getCharSequence()bị phá vỡ vì nó không giữ đúng lời hứa, nhưng đó không phải là lỗi của nhà soạn nhạc.
Holger

59

Loại được trình biên dịch của bạn suy ra trước khi gán cho XInteger & CharSequence. Loại này cảm thấy kỳ lạ, vì Integerlà cuối cùng, nhưng nó là một loại hoàn toàn hợp lệ trong Java. Sau đó Integer, nó được chuyển đến , điều này là hoàn toàn OK.

Có chính xác một giá trị có thể cho Integer & CharSequenceloại : null. Với việc thực hiện như sau:

<X extends CharSequence> X getCharSequence() {
    return null;
}

Nhiệm vụ sau sẽ hoạt động:

Integer x = getCharSequence();

Vì giá trị có thể này, không có lý do nào khiến việc chuyển nhượng bị sai, ngay cả khi nó rõ ràng là vô dụng. Một cảnh báo sẽ hữu ích.

Vấn đề thực sự là API chứ không phải trang web cuộc gọi

Trên thực tế, gần đây tôi đã viết blog về mẫu chống thiết kế API này . Bạn nên (hầu như) không bao giờ thiết kế một phương thức chung để trả về các loại tùy ý vì bạn có thể (hầu như) không bao giờ đảm bảo rằng loại suy ra sẽ được phân phối. Một ngoại lệ là các phương thức như Collections.emptyList(), trong trường hợp sự trống rỗng của danh sách (và xóa kiểu chung) là lý do tại sao mọi suy luận cho <T>sẽ hoạt động:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
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.