Kenapa
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
lebih ketat dari itu
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Ini adalah tindak lanjut pada Mengapa jenis pengembalian lambda tidak diperiksa pada waktu kompilasi . Saya menemukan menggunakan metode withX()
suka
.withX(MyInterface::getLength, "I am not a Long")
menghasilkan kesalahan waktu kompilasi yang diinginkan:
Tipe getLength () dari tipe BuilderExample.MyInterface panjang, ini tidak kompatibel dengan tipe pengembalian deskriptor: String
saat menggunakan metode with()
ini tidak.
contoh lengkap:
import java.util.function.Function;
public class SO58376589 {
public static class Builder<T> {
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
return this;
}
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
return this;
}
}
static interface MyInterface {
public Long getLength();
}
public static void main(String[] args) {
Builder<MyInterface> b = new Builder<MyInterface>();
Function<MyInterface, Long> getter = MyInterface::getLength;
b.with(getter, 2L);
b.with(MyInterface::getLength, 2L);
b.withX(getter, 2L);
b.withX(MyInterface::getLength, 2L);
b.with(getter, "No NUMBER"); // error
b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
b.withX(getter, "No NUMBER"); // error
b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
}
}
javac SO58376589.java
SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
b.with(getter, "No NUMBER"); // error
^
required: Function<MyInterface,R>,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where R,T are type-variables:
R extends Object declared in method <R>with(Function<T,R>,R)
T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
b.withX(getter, "No NUMBER"); // error
^
required: F,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where F,R,T are type-variables:
F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
R extends Object declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
b.withX(MyInterface::getLength, "No NUMBER"); // error
^
(argument mismatch; bad return type in method reference
Long cannot be converted to String)
where R,F,T are type-variables:
R extends Object declared in method <R,F>withX(F,R)
F extends Function<T,R> declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
3 errors
Contoh Diperpanjang
Contoh berikut menunjukkan perilaku metode dan tipe parameter yang berbeda yang dirubah menjadi Pemasok. Selain itu itu menunjukkan perbedaan perilaku konsumen untuk parameter tipe. Dan itu menunjukkan itu tidak membuat perbedaan apakah itu Konsumen atau Pemasok untuk parameter metode.
import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {
Number getNumber();
void setNumber(Number n);
@FunctionalInterface
interface Method<R> {
TypeInference be(R r);
}
//Supplier:
<R> R letBe(Supplier<R> supplier, R value);
<R, F extends Supplier<R>> R letBeX(F supplier, R value);
<R> Method<R> let(Supplier<R> supplier); // return (x) -> this;
//Consumer:
<R> R lettBe(Consumer<R> supplier, R value);
<R, F extends Consumer<R>> R lettBeX(F supplier, R value);
<R> Method<R> lett(Consumer<R> consumer);
public static void main(TypeInference t) {
t.letBe(t::getNumber, (Number) 2); // Compiles :-)
t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
t.letBe(t::getNumber, 2); // Compiles :-)
t.lettBe(t::setNumber, 2); // Compiles :-)
t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)
t.let(t::getNumber).be(2); // Compiles :-)
t.lett(t::setNumber).be(2); // Compiles :-)
t.let(t::getNumber).be("NaN"); // Does not compile :-)
t.lett(t::setNumber).be("NaN"); // Does not compile :-)
}
}
java
generics
lambda
type-inference
jukzi
sumber
sumber
javac
atau alat bangunan seperti Gradle atau Maven?Jawaban:
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:
with
ada substitusi (diakui samar) yang memenuhi semua persyaratan padaR
:Serializable
withX
, pengenalan parameter tipe tambahanF
memaksa kompiler untuk menyelesaikanR
terlebih dahulu, tanpa mempertimbangkan kendalaF extends Function<T,R>
.R
memutuskan untuk (jauh lebih spesifik)String
yang kemudian berarti inferensiF
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 denganwithX
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:
Kasing penggunaan Anda tidak berbeda.
Alasan lain saya akan berhati-hati dalam menggunakan
withX
metode Anda adalahF
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 menggunakanT
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 AndawithX
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:Ini kemudian dapat digunakan sebagai berikut:
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:With
) yang mendefinisikan tipe berdasarkan referensi metode.of
) membatasi tipevalue
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
danwithX
. 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
untukSupplier
, sehingga ada kurang jenis dan parameter dalam bermain. Berikut cuplikan lengkap yang mereproduksi perilaku yang Anda uraikan:Mari kita bekerja melalui inferensi penerapan jenis dan prosedur inferensi jenis untuk setiap pemanggilan metode pada gilirannya:
with
Kita punya:
Set terikat awal, B 0 , adalah:
R <: Object
Semua ekspresi parameter terkait dengan penerapan .
Oleh karena itu, batasan awal yang ditetapkan untuk inferensi penerapan , C , adalah:
TypeInference::getLong
kompatibel denganSupplier<R>
"Not a long"
kompatibel denganR
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 (memberiSerializable
), 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 denganSupplier<R>
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:
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:Oleh karena itu, batasan awal yang ditetapkan untuk inferensi penerapan , C , adalah:
"Also not a long"
kompatibel denganR
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 (memberiString
), 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 denganF
F
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:Dengan batas kedua pada B 2 , resolusi
F
tergantung padaR
, jadiV := {F, R}
.Kami memilih subset
V
sesuai dengan aturan:Satu-satunya bagian
V
yang memenuhi properti ini adalah{R}
.Menggunakan ikatan ketiga (
String <: R
) kita instantiateR = String
dan menggabungkan ini ke dalam set terikat kami.R
sekarang diselesaikan, dan ikatan kedua menjadi efektifF <: 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 denganSupplier<String>
Long
kompatibel denganString
... 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:
Integer <: Number
)Consumer
bukanSupplier
)Secara khusus, 3 dari doa yang diberikan menonjol berpotensi menyarankan perilaku kompiler 'berbeda' dengan yang dijelaskan dalam penjelasan:
Yang kedua dari 3 ini akan melalui proses inferensi yang persis sama seperti di
withX
atas (cukup gantiLong
denganNumber
danString
denganInteger
). 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 (yaituwith
untuk yang pertama,withX
untuk ketiga). Hanya ada satu perubahan kecil yang perlu Anda perhatikan:t::setNumber
kompatibel denganConsumer<R>
) akan berkurang menjadiR <: Number
bukanNumber <: R
seperti yang dilakukannyaSupplier<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.
sumber
TypeInference::getLong
bisa memperbaikiSupplier<Long>
atauSupplier<Serializable>
atauSupplier<Number>
dll, tetapi yang terpenting itu hanya dapat mengimplementasikan salah satu dari mereka (sama seperti kelas lainnya)! Ini berbeda dari semua ekspresi lain, di mana tipe yang diimplementasikan semua dikenal di muka, dan kompiler hanya harus mengetahui apakah salah satu dari mereka memenuhi persyaratan kendala.