Tipe return generik batas atas - antarmuka vs. kelas - kode yang sangat valid

171

Ini adalah contoh dunia nyata dari API pustaka pihak ketiga, tetapi disederhanakan.

Dikompilasi dengan Oracle JDK 8u72

Pertimbangkan dua metode ini:

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

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

Keduanya melaporkan peringatan "pemeran yang tidak dicentang" - Saya mengerti alasannya. Hal yang membingungkan saya adalah mengapa saya bisa menelepon

Integer x = getCharSequence();

dan mengkompilasi? Kompiler harus tahu bahwa Integertidak diimplementasikan CharSequence. Panggilan ke

Integer y = getString();

memberikan kesalahan (seperti yang diharapkan)

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

Dapatkah seseorang menjelaskan mengapa perilaku ini dianggap sah? Apa manfaatnya?

Klien tidak tahu bahwa panggilan ini tidak aman - kode klien dikompilasi tanpa peringatan. Mengapa kompilasi tidak akan memperingatkan tentang hal itu / mengeluarkan kesalahan?

Juga, apa bedanya dengan contoh ini:

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

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

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

Mencoba List<Integer>memberikan kesalahan, seperti yang diharapkan:

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

Jika itu dilaporkan sebagai kesalahan, mengapa Integer x = getCharSequence();tidak?

Adam Michalik
sumber
15
menarik! casting pada LHS Integer x = getCharSequence();akan dikompilasi, tetapi casting pada RHS Integer x = (Integer) getCharSequence();gagal dikompilasi
flakes
Apa versi kompiler java yang Anda gunakan? Silakan tentukan info ini dalam pertanyaan.
Federico Peralta Schaffner
@FedericoPeraltaSchaffner tidak dapat melihat mengapa itu penting - ini pertanyaan langsung tentang JLS.
Boris the Spider
@ BoristheSpider Karena mekanisme inferensi tipe telah berubah untuk java8
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner - Saya sudah menandai pertanyaan dengan [java-8], tapi saya menambahkan versi kompiler di pos sekarang.
Adam Michalik

Jawaban:

184

CharSequenceadalah sebuah interface. Oleh karena itu bahkan jika SomeClasstidak mengimplementasikannya CharSequenceakan sangat mungkin untuk membuat kelas

class SubClass extends SomeClass implements CharSequence

Karena itu kamu bisa menulis

SomeClass c = getCharSequence();

karena tipe yang disimpulkan Xadalah tipe persimpangan SomeClass & CharSequence.

Ini agak aneh dalam kasus Integerkarena Integerfinal, tetapi finaltidak memainkan peran apa pun dalam aturan ini. Misalnya Anda bisa menulis

<T extends Integer & CharSequence>

Di sisi lain, Stringini bukan interface, jadi tidak mungkin untuk memperluas SomeClassuntuk mendapatkan subtipe String, karena java tidak mendukung multiple-inheritance untuk kelas.

Dengan Listcontoh tersebut, Anda perlu mengingat bahwa obat generik tidak bersifat kovarian atau contravarian. Ini berarti bahwa jika Xmerupakan subtipe dari Y, List<X>bukan merupakan subtipe atau supertipe dari List<Y>. Karena Integertidak diterapkan CharSequence, Anda tidak dapat menggunakan metode List<Integer>Anda doCharSequence.

Namun, Anda bisa membuatnya untuk dikompilasi

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

Jika Anda memiliki metode yang mengembalikan sebuah List<T>seperti ini:

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

Anda dapat melakukan

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

Sekali lagi, ini karena tipe yang disimpulkan adalah Integer & CharSequencedan ini adalah subtipe dari Integer.

Jenis titik-temu muncul secara implisit ketika Anda menentukan banyak batas (misalnya <T extends SomeClass & CharSequence>).

Untuk informasi lebih lanjut, di sini adalah bagian dari JLS di mana ia menjelaskan bagaimana jenis batas pekerjaan. Anda dapat menyertakan banyak antarmuka, mis

<T extends String & CharSequence & List & Comparator>

tetapi hanya batas pertama yang bisa berupa antarmuka.

Paul Boddington
sumber
62
Saya tidak tahu Anda bisa memasukkan &definisi generik. +1
serpih
13
@ flkes Anda dapat menempatkan lebih dari satu, tetapi hanya argumen pertama yang bisa berupa non-antarmuka. <T extends String & List & Comparator>ok tapi <T extends String & Integer>tidak, karena Integerbukan antarmuka.
Paul Boddington
7
@ PaulBoddington Ada beberapa penggunaan praktis untuk metode ini. Misalnya jika jenisnya tidak benar-benar digunakan untuk data yang disimpan. Contoh untuk ini Collections.emptyList()juga Optional.empty(). Ini mengembalikan implementasi antarmuka generik, tetapi tidak menyimpan apa pun.
Stefan Dollase
6
Dan tidak ada yang mengatakan bahwa kelas yang berada finalpada waktu kompilasi akan berada finalpada saat runtime.
Holger
7
@Federico Peralta Schaffner: intinya di sini adalah, metode ini getCharSequence()berjanji untuk mengembalikan apa pun Xyang dibutuhkan penelepon, termasuk mengembalikan jenis yang diperluas Integerdan diterapkan CharSequencejika penelepon membutuhkannya dan berdasarkan janji ini, sudah benar untuk memungkinkan menetapkan hasil Integer. Ini adalah metode getCharSequence()yang rusak karena tidak memenuhi janjinya, tapi itu bukan kesalahan kompiler.
Holger
59

Jenis yang disimpulkan oleh kompiler Anda sebelum penugasan Xadalah Integer & CharSequence. Tipe ini terasa aneh, karena Integerfinal, tetapi ini adalah tipe yang benar-benar valid di Jawa. Ini kemudian dilemparkan ke Integer, yang sangat OK.

Ada tepat satu nilai yang mungkin untuk Integer & CharSequencejenis: null. Dengan implementasi sebagai berikut:

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

Tugas berikut akan berhasil:

Integer x = getCharSequence();

Karena nilai yang mungkin ini, tidak ada alasan mengapa penugasan itu harus salah, bahkan jika itu jelas tidak berguna. Peringatan akan bermanfaat.

Masalah sebenarnya adalah API, bukan situs panggilan

Bahkan, saya baru-baru ini membuat blog tentang desain anti pola API ini . Anda seharusnya (hampir) tidak pernah mendesain metode generik untuk mengembalikan tipe sewenang-wenang karena Anda dapat (hampir) tidak pernah menjamin bahwa tipe yang disimpulkan akan dikirimkan. Pengecualian adalah metode seperti Collections.emptyList(), dalam hal kekosongan daftar (dan penghapusan tipe umum) adalah alasan mengapa setiap kesimpulan untuk <T>bekerja:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
sumber