Ini pertanyaan yang sangat menarik. Jawabannya, saya khawatir, rumit.
tl; dr
Mengatasi perbedaan melibatkan beberapa pembacaan yang cukup mendalam tentang spesifikasi inferensi tipe Java , tetapi pada dasarnya bermuara pada ini:
- Semua hal lain sama, kompiler menyimpulkan jenis yang paling spesifik yang bisa dilakukannya.
- Namun, jika ia dapat menemukan sebuah substitusi untuk jenis parameter yang memenuhi semua persyaratan, maka kompilasi akan berhasil, namun samar-samar substitusi ternyata.
- Karena
with
ada substitusi (diakui samar) yang memenuhi semua persyaratan pada R
:Serializable
- Sebab
withX
, pengenalan parameter tipe tambahan F
memaksa kompiler untuk menyelesaikan R
terlebih dahulu, tanpa mempertimbangkan kendala F extends Function<T,R>
. R
memutuskan untuk (jauh lebih spesifik) String
yang kemudian berarti inferensi F
gagal.
Titik peluru terakhir ini adalah yang paling penting, tetapi juga yang paling bergelombang. Saya tidak bisa memikirkan cara ringkas yang lebih baik untuk mengutarakannya, jadi jika Anda ingin lebih detail, saya sarankan Anda membaca penjelasan lengkap di bawah ini.
Apakah ini perilaku yang dimaksudkan?
Aku akan mengambil risiko di sini, dan mengatakan tidak .
Saya tidak menyarankan ada bug di spec, lebih dari itu (dalam kasus withX
) perancang bahasa telah mengangkat tangan mereka dan berkata "ada beberapa situasi di mana tipe inferensi menjadi terlalu sulit, jadi kami hanya akan gagal" . Meskipun perilaku kompiler sehubungan dengan withX
apa yang Anda inginkan, saya akan menganggap itu sebagai efek samping insidental dari spesifikasi saat ini, daripada keputusan desain yang dimaksudkan secara positif.
Ini penting, karena ini menginformasikan pertanyaan Apakah saya harus mengandalkan perilaku ini dalam desain aplikasi saya? Saya berpendapat bahwa Anda tidak boleh, karena Anda tidak dapat menjamin bahwa versi bahasa yang akan datang akan terus berperilaku seperti ini.
Meskipun memang benar bahwa perancang bahasa berusaha sangat keras untuk tidak merusak aplikasi yang ada saat mereka memperbarui spesifikasi / desain / kompiler mereka, masalahnya adalah bahwa perilaku yang ingin Anda andalkan adalah salah satu di mana kompiler saat ini gagal (yaitu bukan aplikasi yang ada ). Pembaruan Langauge mengubah kode yang tidak dikompilasi menjadi kode kompilasi sepanjang waktu. Misalnya, kode berikut dapat dijamin tidak untuk dikompilasi di Java 7, tetapi akan dikompilasi di Java 8:
static Runnable x = () -> System.out.println();
Kasing penggunaan Anda tidak berbeda.
Alasan lain saya akan berhati-hati dalam menggunakan withX
metode Anda adalah F
parameter itu sendiri. Secara umum, parameter tipe generik pada metode (yang tidak muncul dalam tipe kembali) ada untuk mengikat tipe beberapa bagian tanda tangan secara bersamaan. Dikatakan:
Saya tidak peduli apa T
itu, tetapi ingin memastikan bahwa di mana pun saya menggunakan T
itu adalah jenis yang sama.
Maka secara logis, kita akan mengharapkan setiap parameter tipe muncul setidaknya dua kali dalam tanda tangan metode, jika tidak, "itu tidak melakukan apa-apa". F
di Anda withX
hanya muncul satu kali di tanda tangan, yang menunjukkan kepada saya penggunaan parameter tipe yang tidak sejalan dengan maksud fitur bahasa ini.
Implementasi alternatif
Salah satu cara untuk menerapkan ini dalam cara yang sedikit lebih "perilaku yang dituju" adalah dengan membagi with
metode Anda menjadi rantai 2:
public class Builder<T> {
public final class With<R> {
private final Function<T,R> method;
private With(Function<T,R> method) {
this.method = method;
}
public Builder<T> of(R value) {
// TODO: Body of your old 'with' method goes here
return Builder.this;
}
}
public <R> With<R> with(Function<T,R> method) {
return new With<>(method);
}
}
Ini kemudian dapat digunakan sebagai berikut:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Ini tidak termasuk parameter tipe asing seperti Anda withX
. Dengan memecah metode menjadi dua tanda tangan, itu juga lebih baik mengungkapkan maksud dari apa yang Anda coba lakukan, dari sudut pandang keamanan jenis:
- Metode pertama mengatur kelas (
With
) yang mendefinisikan tipe berdasarkan referensi metode.
- Metode scond (
of
) membatasi tipe value
agar kompatibel dengan apa yang Anda atur sebelumnya.
Satu-satunya cara versi masa depan bahasa akan dapat mengkompilasi ini adalah jika menerapkan bebek-mengetik penuh, yang tampaknya tidak mungkin.
Satu catatan terakhir untuk membuat semua ini tidak relevan: Saya pikir Mockito (dan khususnya fungsi mematikannya) pada dasarnya mungkin sudah melakukan apa yang Anda coba capai dengan "type generic builder builder" Anda. Mungkin Anda bisa menggunakannya saja?
Penjelasan lengkap (ish)
Saya akan bekerja melalui prosedur inferensi tipe untuk keduanya with
dan withX
. Ini cukup lama, jadi bawa perlahan. Meski sudah lama, saya masih meninggalkan banyak detail. Anda mungkin ingin merujuk pada spesifikasi untuk detail lebih lanjut (ikuti tautan) untuk meyakinkan diri sendiri bahwa saya benar (saya mungkin telah melakukan kesalahan).
Juga, untuk menyederhanakan banyak hal, saya akan menggunakan contoh kode yang lebih minimal. Perbedaan utama adalah bahwa hal itu swap keluar Function
untuk Supplier
, sehingga ada kurang jenis dan parameter dalam bermain. Berikut cuplikan lengkap yang mereproduksi perilaku yang Anda uraikan:
public class TypeInference {
static long getLong() { return 1L; }
static <R> void with(Supplier<R> supplier, R value) {}
static <R, F extends Supplier<R>> void withX(F supplier, R value) {}
public static void main(String[] args) {
with(TypeInference::getLong, "Not a long"); // Compiles
withX(TypeInference::getLong, "Also not a long"); // Does not compile
}
}
Mari kita bekerja melalui inferensi penerapan jenis dan prosedur inferensi jenis untuk setiap pemanggilan metode pada gilirannya:
with
Kita punya:
with(TypeInference::getLong, "Not a long");
Set terikat awal, B 0 , adalah:
Semua ekspresi parameter terkait dengan penerapan .
Oleh karena itu, batasan awal yang ditetapkan untuk inferensi penerapan , C , adalah:
TypeInference::getLong
kompatibel dengan Supplier<R>
"Not a long"
kompatibel dengan R
Ini dikurangi menjadi set B 2 terikat :
R <: Object
(dari B 0 )
Long <: R
(dari kendala pertama)
String <: R
(dari kendala kedua)
Karena ini tidak mengandung terikat ' palsu ', dan (saya asumsikan) resolusi dari R
berhasil (memberi Serializable
), maka doa berlaku.
Jadi, kita beralih ke inferensi tipe doa .
Set kendala baru, C , dengan variabel input dan output yang terkait , adalah:
TypeInference::getLong
kompatibel dengan Supplier<R>
- Variabel input: tidak ada
- Variabel keluaran:
R
Ini tidak mengandung saling ketergantungan antara masukan dan keluaran variabel, sehingga dapat dikurangi dalam satu langkah, dan set terikat akhir, B 4 , adalah sama dengan B 2 . Oleh karena itu, resolusi berhasil seperti sebelumnya, dan penyusun menghembuskan napas lega!
withX
Kita punya:
withX(TypeInference::getLong, "Also not a long");
Set terikat awal, B 0 , adalah:
R <: Object
F <: Supplier<R>
Hanya ekspresi parameter kedua yang berkaitan dengan penerapan . Yang pertama ( TypeInference::getLong
) tidak, karena memenuhi kondisi berikut:
Jika m
metode generik dan pemanggilan metode tidak memberikan argumen tipe eksplisit, ekspresi lambda yang diketik secara eksplisit atau ekspresi referensi metode yang tepat untuk tipe target yang sesuai (seperti yang berasal dari tanda tangan m
) adalah tipe parameter dari m
.
Oleh karena itu, batasan awal yang ditetapkan untuk inferensi penerapan , C , adalah:
"Also not a long"
kompatibel dengan R
Ini dikurangi menjadi set B 2 terikat :
R <: Object
(dari B 0 )
F <: Supplier<R>
(dari B 0 )
String <: R
(dari batasan)
Sekali lagi, karena ini tidak mengandung terikat ' palsu ', dan resolusi dari R
berhasil (memberi String
), maka doa berlaku.
Inferensi jenis doa sekali lagi ...
Kali ini, set kendala baru, C , dengan variabel input dan output yang terkait , adalah:
TypeInference::getLong
kompatibel dengan F
- Variabel input:
F
- Variabel output: tidak ada
Sekali lagi, kami tidak memiliki saling ketergantungan antara variabel input dan output . Namun kali ini, ada adalah sebuah variabel masukan ( F
), jadi kita harus menyelesaikan ini sebelum mencoba pengurangan . Jadi, kita mulai dengan set terikat B 2 .
Kami menentukan subset V
sebagai berikut:
Diberikan satu set variabel inferensi untuk diselesaikan, mari V
menjadi penyatuan set ini dan semua variabel yang menjadi dasar penyelesaian setidaknya satu variabel dalam set ini.
Dengan batas kedua pada B 2 , resolusi F
tergantung pada R
, jadi V := {F, R}
.
Kami memilih subset V
sesuai dengan aturan:
biarkan { α1, ..., αn }
menjadi subset kosong dari variabel tidak terinstalasi V
sedemikian rupa sehingga saya) untuk semua i (1 ≤ i ≤ n)
, jika αi
tergantung pada resolusi variabel β
, maka apakah β
memiliki instantiasi atau ada beberapa j
yang β = αj
; dan ii) tidak ada himpunan bagian yang tidak kosong dari { α1, ..., αn }
properti ini.
Satu-satunya bagian V
yang memenuhi properti ini adalah {R}
.
Menggunakan ikatan ketiga ( String <: R
) kita instantiate R = String
dan menggabungkan ini ke dalam set terikat kami. R
sekarang diselesaikan, dan ikatan kedua menjadi efektif F <: Supplier<String>
.
Dengan menggunakan batas kedua (revisi), kami instantiate F = Supplier<String>
. F
sudah diselesaikan.
Sekarang F
sudah teratasi, kita bisa melanjutkan dengan pengurangan , menggunakan kendala baru:
TypeInference::getLong
kompatibel dengan Supplier<String>
- ... diperkecil menjadi
Long
kompatibel dengan String
- ... yang mengurangi menjadi false
... dan kami mendapatkan kesalahan kompilator!
Catatan tambahan pada 'Contoh Diperluas'
The diperpanjang Contoh dalam penampilan pertanyaan pada kasus yang menarik beberapa yang tidak langsung ditutupi oleh cara kerja di atas:
- Di mana tipe nilai adalah subtipe dari tipe metode pengembalian (
Integer <: Number
)
- Di mana antarmuka fungsional bersifat contravarian dalam jenis yang disimpulkan (yaitu,
Consumer
bukan Supplier
)
Secara khusus, 3 dari doa yang diberikan menonjol berpotensi menyarankan perilaku kompiler 'berbeda' dengan yang dijelaskan dalam penjelasan:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
Yang kedua dari 3 ini akan melalui proses inferensi yang persis sama seperti di withX
atas (cukup ganti Long
dengan Number
dan String
dengan Integer
). Ini menggambarkan alasan lain mengapa Anda tidak harus bergantung pada perilaku inferensi tipe gagal ini untuk desain kelas Anda, karena kegagalan untuk mengkompilasi di sini kemungkinan bukan perilaku yang diinginkan.
Untuk yang lain 2 (dan memang ada dari doa lain yang melibatkan Consumer
Anda ingin bekerja melalui), perilaku harus jelas jika Anda bekerja melalui prosedur inferensi tipe yang ditetapkan untuk salah satu metode di atas (yaitu with
untuk yang pertama, withX
untuk ketiga). Hanya ada satu perubahan kecil yang perlu Anda perhatikan:
- Batasan pada parameter pertama (
t::setNumber
kompatibel dengan Consumer<R>
) akan berkurang menjadi R <: Number
bukan Number <: R
seperti yang dilakukannya Supplier<R>
. Ini dijelaskan dalam dokumentasi terkait pengurangan.
Saya meninggalkannya sebagai latihan bagi pembaca untuk bekerja dengan hati-hati melalui salah satu prosedur di atas, dipersenjatai dengan pengetahuan tambahan ini, untuk menunjukkan kepada diri mereka sendiri mengapa doa tertentu dikompilasi atau tidak.