Mengapa Java tidak dapat menyimpulkan supertipe?

19

Kita semua tahu Long memanjang Number. Jadi mengapa ini tidak dikompilasi?

Dan bagaimana mendefinisikan metode withsehingga program dapat dikompilasi tanpa ada panduan manual?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}
  • Tidak dapat menyimpulkan argumen tipe untuk <F, R> with(F, R)
  • Jenis getNumber () dari tipe Builder.MyInterface adalah Number, ini tidak kompatibel dengan tipe pengembalian deskriptor: Panjang

Untuk kasus penggunaan, lihat: Mengapa jenis pengembalian lambda tidak dicentang pada waktu kompilasi

jukzi
sumber
Bisakah kamu memposting MyInterface?
Maurice Perry
sudah ada di dalam kelas
jukzi
Hmm saya mencoba <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)tetapi mendapatkan java.lang.Number cannot be converted to java.lang.Long), yang mengejutkan karena saya tidak melihat dari mana kompiler mendapatkan ide bahwa nilai pengembalian getterharus dikonversi ke returnValue.
jingx
@ jukzi OK Maaf, saya melewatkan itu.
Maurice Perry
Mengubah Number getNumber()untuk <A extends Number> A getNumber()membuat barang berfungsi. Tidak tahu apakah ini yang Anda inginkan. Seperti yang orang lain katakan, masalahnya adalah MyInterface::getNumberfungsi yang mengembalikan Doublemisalnya dan tidak Long. Deklarasi Anda tidak mengizinkan penyusun untuk mempersempit jenis pengembalian berdasarkan informasi lain yang ada. Dengan menggunakan tipe pengembalian generik Anda mengizinkan kompiler untuk melakukannya, maka itu berfungsi.
Giacomo Alzetta

Jawaban:

9

Ungkapan ini:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

dapat ditulis ulang sebagai:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

Memperhatikan tanda tangan metode akun:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R akan disimpulkan Long
  • F akan Function<MyInterface, Long>

dan Anda melewati referensi metode yang akan disimpulkan sebagai Function<MyInterface, Number>Ini adalah kuncinya - bagaimana seharusnya kompiler memprediksi bahwa Anda benar-benar ingin kembali Longdari fungsi dengan tanda tangan seperti itu?Itu tidak akan melakukan downcasting untuk Anda.

Karena Numbermerupakan superclass dari Longdan Numbertidak perlu Long(ini sebabnya ia tidak dapat dikompilasi) - Anda harus melemparkannya sendiri:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

membuat Fmenjadi Function<MyIinterface, Long>atau meneruskan argumen umum secara eksplisit selama pemanggilan metode seperti yang Anda lakukan:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

dan tahu Rakan dilihat Numberdan kode akan dikompilasi.

michalk
sumber
Itu typecast yang menarik. Tetapi saya masih mencari definisi metode "with" yang akan membuat pemanggil mengkompilasi tanpa ada pemain. Bagaimanapun terima kasih untuk ide itu.
jukzi
@ jukzi Anda tidak bisa. Tidak masalah bagaimana Anda withditulis. Anda MJyInterface::getNumbermemiliki tipe Function<MyInterface, Number>jadi R=Numberdan kemudian Anda juga memiliki R=Longargumen lain (ingat bahwa literal Java bukan polimorfik!). Pada titik ini kompiler berhenti karena tidak selalu mungkin untuk mengonversi a Numberke Long. Satu-satunya cara untuk memperbaikinya adalah dengan mengubah MyInterfaceagar digunakan <A extends Number> Numbersebagai tipe pengembalian, ini membuat kompiler memiliki R=Adan kemudian R=Longdan karena A extends Numberia dapat menggantikanA=Long
Giacomo Alzetta
4

Kunci untuk kesalahan Anda dalam deklarasi generik jenis F: F extends Function<T, R>. Pernyataan yang tidak berfungsi adalah: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Pertama, Anda punya yang baru Builder<MyInterface>. Oleh karena itu, deklarasi kelas menyiratkan T = MyInterface. Sesuai deklarasi Anda tentang with, Fharus a Function<T, R>, yang merupakan Function<MyInterface, R>dalam situasi ini. Oleh karena itu, parameter getterharus mengambil MyInterfaceparameter as (dipenuhi oleh referensi metode MyInterface::getNumberdan MyInterface::getLong), dan kembali R, yang harus jenis yang sama dengan parameter kedua ke fungsi with. Sekarang, mari kita lihat apakah ini berlaku untuk semua kasus Anda:

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Anda dapat "memperbaiki" masalah ini dengan opsi berikut:

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

Di luar titik ini, sebagian besar merupakan keputusan desain yang opsi mengurangi kompleksitas kode untuk aplikasi khusus Anda, jadi pilihlah apa pun yang paling cocok untuk Anda.

Alasan Anda tidak dapat melakukan ini tanpa melakukan casting terletak pada yang berikut, dari Spesifikasi Bahasa Jawa :

Konversi tinju memperlakukan ekspresi dari tipe primitif sebagai ekspresi dari tipe referensi yang sesuai. Secara khusus, sembilan konversi berikut ini disebut konversi tinju :

  • Dari tipe boolean ke tipe Boolean
  • Dari tipe byte ke tipe Byte
  • Dari tipe pendek ke tipe pendek
  • Dari tipe char ke tipe Character
  • Dari tipe int ke tipe Integer
  • Dari tipe panjang ke tipe panjang
  • Dari tipe float ke tipe Float
  • Dari tipe double ke tipe Double
  • Dari jenis nol ke jenis nol

Seperti yang dapat Anda lihat dengan jelas, tidak ada konversi tinju tersirat dari panjang ke Angka, dan konversi pelebaran dari Panjang ke Angka hanya dapat terjadi ketika kompiler yakin bahwa itu membutuhkan Angka dan bukan Panjang. Karena ada konflik antara referensi metode yang memerlukan angka dan 4L yang menyediakan panjang, kompiler (untuk beberapa alasan ???) tidak dapat membuat lompatan logis bahwa Long is-a Number dan menyimpulkan bahwa Fa Function<MyInterface, Number>.

Sebaliknya, saya berhasil menyelesaikan masalah dengan sedikit mengedit tanda tangan fungsi:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

Setelah perubahan ini, berikut ini terjadi:

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Sunting:
Setelah menghabiskan lebih banyak waktu untuk itu, sangat sulit untuk menegakkan keselamatan tipe berbasis pengambil. Berikut ini adalah contoh kerja yang menggunakan metode setter untuk menegakkan keamanan tipe pembangun:

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

Memberikan kemampuan tipe-aman untuk membangun objek, mudah-mudahan di beberapa titik di masa mendatang kita akan dapat mengembalikan objek data yang tidak dapat diubah dari pembangun (mungkin dengan menambahkan toRecord()metode ke antarmuka, dan menetapkan pembangun sebagai a Builder<IntermediaryInterfaceType, RecordType>), jadi Anda bahkan tidak perlu khawatir tentang objek yang dihasilkan sedang dimodifikasi. Sejujurnya, ini memalukan sekali karena membutuhkan begitu banyak upaya untuk mendapatkan pembangun bidang-jenis-aman yang aman, tetapi mungkin tidak mungkin tanpa beberapa fitur baru, pembuatan kode, atau jumlah refleksi yang mengganggu.

Avi
sumber
Terima kasih untuk semua pekerjaan Anda, tetapi saya tidak melihat adanya perbaikan untuk menghindari typecast manual. Pemahaman saya yang naif adalah bahwa seorang kompiler harus dapat menyimpulkan (yaitu typecast) segala sesuatu yang dapat dilakukan manusia.
jukzi
@ jukzi Pemahaman naif saya adalah sama, tetapi untuk alasan apa pun itu tidak berfungsi seperti itu. Saya telah datang dengan solusi yang cukup banyak mencapai efek yang diinginkan, meskipun
Avi
Terima kasih lagi. Tetapi proposal baru Anda terlalu luas (lihat stackoverflow.com/questions/58337639 ) karena memungkinkan kompilasi ".dengan (MyInterface :: getNumber," I AM NOT A NUMBER ")";
jukzi
kalimat Anda "Kompiler tidak dapat menyimpulkan jenis referensi metode dan 4L pada saat yang sama" keren. Tapi saya hanya ingin sebaliknya. compiler harus mencoba Number berdasarkan parameter pertama dan melakukan pelebaran ke Number dari parameter kedua.
jukzi
1
WHAAAAAAAAAAAT? Mengapa BiConsumer berfungsi seperti yang dimaksudkan sedangkan Function tidak? Saya tidak mengerti. Saya akui ini persis jenis pengaman yang saya inginkan tetapi sayangnya tidak bekerja dengan getter. MENGAPA MENGAPA MENGAPA.
jukzi
1

Tampaknya kompiler telah menggunakan nilai 4L untuk memutuskan bahwa R adalah Long, dan getNumber () mengembalikan Nomor, yang belum tentu merupakan Long.

Tapi saya tidak yakin mengapa nilai lebih diutamakan daripada metode ini ...

Rick
sumber
0

Kompiler Java secara umum tidak bagus dalam menyimpulkan tipe generik ganda / bersarang atau wildcard. Seringkali saya tidak bisa mendapatkan sesuatu untuk dikompilasi tanpa menggunakan fungsi pembantu untuk menangkap atau menyimpulkan beberapa tipe.

Tapi, apakah Anda benar-benar perlu untuk menangkap jenis persis Functionsebagai F? Jika tidak, mungkin karya berikut berfungsi, dan seperti yang Anda lihat, juga tampaknya berfungsi dengan subtipe Function.

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
        return null;
    }

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}
machfour
sumber
"with (MyInterface :: getNumber," NOT A NUMBER ")" tidak boleh dikompilasi
jukzi
0

Bagian yang paling menarik terletak pada perbedaan antara 2 garis itu, saya pikir:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Dalam kasus pertama, Tsecara eksplisit Number, demikian 4Ljuga Number, tidak ada masalah. Dalam kasus kedua, 4Ladalah a Long, begitu Tjuga a Long, jadi fungsi Anda tidak kompatibel, dan Java tidak bisa tahu apakah yang Anda maksudkan Numberatau Long.

njzk2
sumber
0

Dengan tanda tangan berikut:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

semua contoh Anda dikompilasi, kecuali yang ketiga, yang secara eksplisit membutuhkan metode untuk memiliki dua variabel tipe.

Alasan mengapa versi Anda tidak berfungsi, adalah karena referensi metode Java tidak memiliki satu jenis tertentu. Sebaliknya, mereka memiliki tipe yang diperlukan dalam konteks yang diberikan. Dalam kasus Anda, Rdisimpulkan Longkarena 4L, tetapi pengambil tidak dapat memiliki jenis Function<MyInterface,Long>karena di Jawa, tipe generik adalah invarian dalam argumen mereka.

Hoopje
sumber
Kode Anda akan mengkompilasi with( getNumber,"NO NUMBER")mana yang tidak diinginkan. Juga tidak benar bahwa obat generik selalu invarian (lihat stackoverflow.com/a/58378661/9549750 untuk pembuktian bahwa obat generik setter berperilaku selain yang digunakan oleh pengambil)
jukzi
@ jukzi Ah, solusi saya sudah diusulkan oleh Avi. Sangat buruk... :-). By the way, memang benar bahwa kita dapat menetapkan a Thing<Cat>ke Thing<? extends Animal>variabel, tetapi untuk kovarians nyata saya akan berharap bahwa a Thing<Cat>dapat ditugaskan ke a Thing<Animal>. Bahasa lain, seperti Kotlin, memungkinkan untuk mendefinisikan variabel tipe co dan contravarian.
Hoopje