Mengapa tipe parameter lebih kuat daripada parameter metode

12

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 :-)
  }
}
jukzi
sumber
1
Karena kesimpulan dengan yang terakhir. Meskipun keduanya didasarkan pada kasus penggunaan yang perlu diterapkan. Untuk Anda, yang pertama mungkin ketat dan bagus. Untuk fleksibilitas, orang lain dapat memilih yang terakhir.
Naman
Apakah Anda mencoba untuk mengkompilasi ini di Eclipse? Mencari string kesalahan dari format yang Anda tempel menyarankan ini adalah kesalahan spesifik Eclipse (ecj). Apakah Anda mendapatkan masalah yang sama saat mengompilasi dengan bahan baku javacatau alat bangunan seperti Gradle atau Maven?
user31601
@ user31601 saya menambahkan contoh lengkap dengan output javac. Pesan-pesan kesalahan yang sedikit berbeda diformat tetapi masih gerhana dan javac memiliki perilaku yang sama
jukzi

Jawaban:

12

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 withada substitusi (diakui samar) yang memenuhi semua persyaratan pada R:Serializable
  • Sebab withX, pengenalan parameter tipe tambahan Fmemaksa kompiler untuk menyelesaikan Rterlebih dahulu, tanpa mempertimbangkan kendala F extends Function<T,R>. Rmemutuskan untuk (jauh lebih spesifik) Stringyang kemudian berarti inferensi Fgagal.

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 withXapa 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 withXmetode Anda adalah Fparameter 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 Titu, tetapi ingin memastikan bahwa di mana pun saya menggunakan Titu 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". Fdi Anda withXhanya 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 withmetode 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 valueagar 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 withdan 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 Functionuntuk 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:

  • R <: Object

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 Rberhasil (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 mmetode 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 Rberhasil (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 .

  1. Kami menentukan subset Vsebagai berikut:

    Diberikan satu set variabel inferensi untuk diselesaikan, mari Vmenjadi penyatuan set ini dan semua variabel yang menjadi dasar penyelesaian setidaknya satu variabel dalam set ini.

    Dengan batas kedua pada B 2 , resolusi Ftergantung pada R, jadi V := {F, R}.

  2. Kami memilih subset Vsesuai dengan aturan:

    biarkan { α1, ..., αn }menjadi subset kosong dari variabel tidak terinstalasi Vsedemikian rupa sehingga saya) untuk semua i (1 ≤ i ≤ n), jika αitergantung pada resolusi variabel β, maka apakah βmemiliki instantiasi atau ada beberapa jyang β = αj; dan ii) tidak ada himpunan bagian yang tidak kosong dari { α1, ..., αn }properti ini.

    Satu-satunya bagian Vyang memenuhi properti ini adalah {R}.

  3. Menggunakan ikatan ketiga ( String <: R) kita instantiate R = Stringdan menggabungkan ini ke dalam set terikat kami. Rsekarang diselesaikan, dan ikatan kedua menjadi efektif F <: Supplier<String>.

  4. Dengan menggunakan batas kedua (revisi), kami instantiate F = Supplier<String>. Fsudah diselesaikan.

Sekarang Fsudah teratasi, kita bisa melanjutkan dengan pengurangan , menggunakan kendala baru:

  1. TypeInference::getLong kompatibel dengan Supplier<String>
  2. ... diperkecil menjadi Long kompatibel dengan String
  3. ... 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, Consumerbukan 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 withXatas (cukup ganti Longdengan Numberdan Stringdengan 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 ConsumerAnda ingin bekerja melalui), perilaku harus jelas jika Anda bekerja melalui prosedur inferensi tipe yang ditetapkan untuk salah satu metode di atas (yaitu withuntuk yang pertama, withXuntuk ketiga). Hanya ada satu perubahan kecil yang perlu Anda perhatikan:

  • Batasan pada parameter pertama ( t::setNumber kompatibel dengan Consumer<R> ) akan berkurang menjadi R <: Numberbukan Number <: Rseperti 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.

pengguna31601
sumber
Sangat mendalam, diteliti dan dirumuskan dengan baik. Terima kasih!
Zabuzard
@ user31601 Bisakah Anda menunjukkan di mana perbedaan Pemasok ke Konsumen berperan. Saya menambahkan Contoh yang Diperpanjang dalam pertanyaan awal untuk itu. Ini menunjukkan perilaku kovarian, contravarian dan invarian untuk versi letBe () yang berbeda, letBeX () dan let (). Be () tergantung pada Pemasok / Konsumen.
jukzi
@ jukzi Saya telah menambahkan beberapa catatan tambahan, tetapi Anda harus memiliki cukup info untuk mengerjakan sendiri contoh-contoh baru ini.
user31601
Thats intersting: ada begitu banyak kasus khusus dalam 18.2.1. untuk lambda dan referensi metode di mana saya tidak akan mengharapkan kasus khusus untuk mereka sama sekali dari pemahaman naif saya. Dan mungkin tidak ada pengembang biasa harapkan.
jukzi
Yah, saya kira alasannya adalah bahwa dengan lambdas dan referensi metode, kompiler perlu memutuskan jenis lambda mana yang harus diimplementasikan - ia harus membuat pilihan! Sebagai contoh, TypeInference::getLongbisa memperbaiki Supplier<Long>atau Supplier<Serializable>atau Supplier<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.
user31601