Metode pemilihan tanda tangan untuk ekspresi lambda dengan beberapa tipe target yang cocok

11

Saya menjawab pertanyaan dan mengalami skenario yang tidak bisa saya jelaskan. Pertimbangkan kode ini:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

Saya tidak mengerti mengapa secara eksplisit mengetik paramter dari lambda (A a) -> aList.add(a)membuat kode dikompilasi. Selain itu, mengapa tautan ke kelebihan di dalam Iterabledaripada di CustomIterable?
Apakah ada penjelasan untuk ini atau tautan ke bagian spesifikasi yang relevan?

Catatan: iterable.forEach((A a) -> aList.add(a));hanya mengkompilasi ketika CustomIterable<T>diperluas Iterable<T>(secara berlebihan membebani metode dalam CustomIterablehasil dalam kesalahan ambigu)


Mendapatkan ini pada keduanya:

  • versi openjdk "13.0.2" 2020-01-14
    Compiler Eclipse
  • versi openjdk "1.8.0_232"
    Kompilator Eclipse

Sunting : Kode di atas gagal dikompilasi pada bangunan dengan pakar sementara Eclipse berhasil mengkompilasi baris kode terakhir.

ernest_k
sumber
3
Tidak satu pun dari tiga kompilasi di Java 8. Sekarang saya tidak yakin apakah ini adalah bug yang diperbaiki dalam versi yang lebih baru, atau bug / fitur yang diperkenalkan ... Anda mungkin harus menentukan versi Java
Sweeper
@Sweeper Saya awalnya ini menggunakan jdk-13. Pengujian selanjutnya dalam java 8 (jdk8u232) menunjukkan kesalahan yang sama. Tidak yakin mengapa yang terakhir tidak bisa dikompilasi di mesin Anda.
ernest_k
Tidak dapat mereproduksi pada dua kompiler online ( 1 , 2 ). Saya menggunakan 1.8.0_221 pada mesin saya. Ini semakin aneh dan aneh ...
Sweeper
1
@ernest_k Eclipse memiliki implementasi compiler sendiri. Itu bisa menjadi informasi penting untuk pertanyaan itu. Juga, fakta pakar kesalahan membangun baris terakhir juga harus disorot dalam pertanyaan menurut pendapat saya. Di sisi lain, tentang pertanyaan terkait, asumsi bahwa OP juga menggunakan Eclipse dapat diklarifikasi, karena kode tidak dapat direproduksi.
Naman
3
Sementara saya memahami nilai intelektual dari pertanyaan itu, saya hanya bisa menyarankan agar tidak pernah membuat metode kelebihan beban yang hanya berbeda dalam antarmuka fungsional dan berharap bahwa itu dapat dengan aman disebut lewat lambda. Saya tidak percaya bahwa kombinasi inferensi tipe lambda dan overloading adalah sesuatu yang rata-rata hampir dapat dipahami oleh programmer. Ini adalah persamaan dengan sangat banyak variabel yang tidak dikontrol pengguna. HINDARI INI, tolong :)
Stephan Herrmann

Jawaban:

8

TL; DR, ini adalah bug penyusun.

Tidak ada aturan yang akan diutamakan untuk metode tertentu yang berlaku ketika diwariskan atau metode default. Menariknya, ketika saya mengubah kode ke

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

yang iterable.forEach((A a) -> aList.add(a));pernyataan menghasilkan kesalahan dalam Eclipse.

Karena tidak ada properti forEach(Consumer<? super T) c)metode dari Iterable<T>antarmuka berubah ketika menyatakan kelebihan lainnya, keputusan Eclipse untuk memilih metode ini tidak dapat (secara konsisten) didasarkan pada properti metode apa pun. Itu masih satu-satunya metode yang diwariskan, masih satu-satunya defaultmetode, masih satu-satunya metode JDK, dan seterusnya. Tak satu pun dari properti ini yang akan memengaruhi pemilihan metode.

Perhatikan bahwa mengubah deklarasi menjadi

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

juga menghasilkan kesalahan "ambigu", sehingga jumlah metode kelebihan beban yang berlaku juga tidak masalah, bahkan ketika hanya ada dua kandidat, tidak ada preferensi umum terhadap defaultmetode.

Sejauh ini, masalah tampaknya muncul ketika ada dua metode yang berlaku dan defaultmetode dan hubungan warisan yang terlibat, tetapi ini bukan tempat yang tepat untuk menggali lebih jauh.


Tetapi dapat dimengerti bahwa konstruk dari contoh Anda dapat ditangani oleh kode implementasi yang berbeda di kompiler, yang satu menunjukkan bug sementara yang lain tidak.
a -> aList.add(a)adalah ekspresi lambda yang diketik secara implisit , yang tidak dapat digunakan untuk resolusi kelebihan beban. Sebaliknya, (A a) -> aList.add(a)adalah ekspresi lambda yang diketik secara eksplisit yang dapat digunakan untuk memilih metode yang cocok dari metode kelebihan beban, tetapi itu tidak membantu di sini (seharusnya tidak membantu di sini), karena semua metode memiliki tipe parameter dengan tanda tangan fungsional yang persis sama .

Sebagai contoh tandingan, dengan

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

tanda tangan fungsional berbeda, dan menggunakan ekspresi lambda tipe eksplisit memang dapat membantu memilih metode yang tepat sedangkan ekspresi lambda yang diketik secara implisit tidak membantu, sehingga forEach(s -> s.isEmpty())menghasilkan kesalahan kompilator. Dan semua kompiler Java setuju tentang itu.

Perhatikan bahwa aList::addini adalah referensi metode yang ambigu, karena addmetode ini kelebihan beban juga, jadi itu juga tidak dapat membantu memilih metode, tetapi referensi metode mungkin akan diproses oleh kode yang berbeda pula. Beralih ke yang ambigu aList::containsatau berubah Listmenjadi Collection, untuk membuat addtidak ambigu, tidak mengubah hasil di instalasi Eclipse saya (saya menggunakan 2019-06).

Holger
sumber
1
@ Howlger, komentar Anda tidak masuk akal. Metode default adalah metode warisan dan metode kelebihan beban. Tidak ada metode lain. Fakta bahwa metode yang diwariskan adalah defaultmetode hanyalah poin tambahan. Jawaban saya sudah menunjukkan contoh di mana Eclipse tidak memberikan prioritas pada metode default.
Holger
1
@ Howlger ada perbedaan mendasar dalam perilaku kita. Anda hanya menemukan bahwa menghapus defaultmengubah hasil dan segera berasumsi untuk menemukan alasan perilaku yang diamati. Anda terlalu percaya diri tentang hal itu sehingga Anda menyebut jawaban lain salah, terlepas dari kenyataan bahwa mereka bahkan tidak bertentangan. Karena Anda memproyeksikan perilaku Anda sendiri terhadap orang lain, saya tidak pernah mengklaim bahwa warisan adalah alasannya . Saya membuktikan bahwa itu tidak benar. Saya menunjukkan bahwa perilaku itu tidak konsisten, karena Eclipse memilih metode tertentu dalam satu skenario tetapi tidak di yang lain, di mana ada tiga kelebihan.
Holger
1
@ howlger selain itu, saya sudah menamai skenario lain di akhir komentar ini , membuat satu antarmuka, tidak ada warisan, dua metode, satu defaultyang lain abstract, dengan dua argumen seperti konsumen dan mencobanya. Eclipse dengan benar mengatakan bahwa itu ambigu, meskipun ada satu defaultmetode. Tampaknya warisan masih relevan untuk bug Eclipse ini, tetapi tidak seperti Anda, saya tidak akan marah dan menyebut jawaban lain salah, hanya karena mereka tidak menganalisis bug secara keseluruhan. Itu bukan tugas kita di sini.
Holger
1
@ Howlger tidak, intinya adalah bahwa ini adalah bug. Sebagian besar pembaca bahkan tidak akan peduli dengan detailnya. Eclipse bisa melempar dadu setiap kali memilih metode, tidak masalah. Eclipse tidak boleh memilih metode yang ambigu, jadi tidak masalah mengapa memilihnya. Jawaban ini membuktikan bahwa perilaku tersebut tidak konsisten, yang sudah cukup untuk menunjukkan bahwa itu adalah bug. Tidak perlu menunjuk pada baris dalam kode sumber Eclipse di mana ada masalah. Itu bukan tujuan dari Stackoverflow. Mungkin, Anda membingungkan Stackoverflow dengan pelacak bug Eclipse.
Holger
1
@ Howlger Anda lagi salah mengklaim bahwa saya membuat pernyataan (salah) tentang mengapa Eclipse membuat pilihan yang salah. Sekali lagi, saya tidak melakukannya, karena gerhana seharusnya tidak membuat pilihan sama sekali. Metode ini ambigu. Titik. Alasan mengapa saya menggunakan istilah " warisan ", adalah karena membedakan metode yang identik namanya diperlukan. Saya bisa mengatakan " metode default " sebagai gantinya, tanpa mengubah logika. Lebih tepatnya, saya seharusnya menggunakan ungkapan " metode yang sangat salah Eclipse dipilih untuk alasan apa pun ". Anda dapat menggunakan salah satu dari tiga frasa tersebut secara bergantian, dan logikanya tidak berubah.
Holger
2

Kompilator Eclipse dengan benar menyelesaikan ke defaultmetode , karena ini adalah metode yang paling spesifik sesuai dengan Spesifikasi Bahasa Jawa 15.12.2.5 :

Jika tepat salah satu metode spesifik maksimal adalah konkret (yaitu, non- abstractatau default), itu adalah metode yang paling spesifik.

javac(digunakan oleh Maven dan IntelliJ secara default) memberi tahu pemanggilan metode bersifat ambigu di sini. Tetapi menurut Spesifikasi Bahasa Jawa itu tidak ambigu karena salah satu dari dua metode adalah metode yang paling spesifik di sini.

Ekspresi lambda yang diketik secara implisit ditangani secara berbeda dari ekspresi lambda yang diketik secara eksplisit di Jawa. Secara implisit diketik berbeda dengan ekspresi lambda yang diketik secara eksplisit, jatuh pada fase pertama untuk mengidentifikasi metode doa yang ketat (lihat Spesifikasi Bahasa Jawa jls-15.12.2.2 , poin pertama). Oleh karena itu, pemanggilan metode di sini bersifat ambigu untuk ekspresi lambda yang diketik secara implisit .

Dalam kasus Anda, solusi untuk javacbug ini adalah menentukan jenis antarmuka fungsional alih-alih menggunakan ekspresi lambda yang diketik secara eksplisit sebagai berikut:

iterable.forEach((ConsumerOne<A>) aList::add);

atau

iterable.forEach((Consumer<A>) aList::add);

Berikut adalah contoh Anda yang diperkecil lebih lanjut untuk pengujian:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}
Howlger
sumber
4
Anda melewatkan prasyarat tepat sebelum kalimat yang dikutip: " Jika semua metode spesifik yang maksimal memiliki tanda tangan yang setara-override " Tentu saja, dua metode yang argumennya sepenuhnya antarmuka yang tidak terkait tidak memiliki tanda tangan override-setara. Selain itu, ini tidak akan menjelaskan mengapa Eclipse berhenti memilih defaultmetode ketika ada tiga metode kandidat atau ketika kedua metode dinyatakan dalam antarmuka yang sama.
Holger
1
@ Holger Jawaban Anda mengklaim, "Tidak ada aturan yang akan memberikan prioritas pada metode tertentu yang berlaku saat diwariskan atau metode default." Apakah saya mengerti Anda dengan benar bahwa Anda mengatakan bahwa prasyarat untuk aturan yang tidak ada ini tidak berlaku di sini? Harap dicatat, parameter di sini adalah antarmuka fungsional (lihat JLS 9.8).
Howlger
1
Anda merobek kalimat di luar konteks. Kalimat tersebut menggambarkan pemilihan metode override-equivalent , dengan kata lain, pilihan antara deklarasi yang semuanya akan memanggil metode yang sama saat runtime, karena hanya akan ada satu metode konkret di kelas beton. Ini tidak relevan dengan kasus metode yang berbeda seperti forEach(Consumer)dan forEach(Consumer2)yang tidak pernah berakhir pada metode implementasi yang sama.
Holger
2
@StephanHerrmann Saya tidak tahu JEP atau JSR, tetapi perubahannya terlihat seperti perbaikan agar sejalan dengan arti “beton”, yaitu membandingkan dengan JLS§9.4 : “ Metode standar berbeda dari metode beton (§8.4. 3.1), yang dideklarasikan di kelas. ”, Yang tidak pernah berubah.
Holger
2
@StephanHerrmann ya, sepertinya memilih seorang kandidat dari metode override-equivalen telah menjadi lebih rumit dan akan menarik untuk mengetahui alasan di baliknya, tetapi tidak relevan dengan pertanyaan yang ada. Harus ada dokumen lain yang menjelaskan perubahan dan motivasi. Di masa lalu, ada, tetapi dengan kebijakan "versi baru setiap ½ tahun" ini, menjaga kualitas tampaknya tidak mungkin ...
Holger
2

Kode di mana Eclipse mengimplementasikan JLS §15.12.2.5 tidak menemukan metode mana pun lebih spesifik daripada yang lain, bahkan untuk kasus lambda yang diketik secara eksplisit.

Jadi idealnya Eclipse akan berhenti di sini dan melaporkan ambiguitas. Sayangnya, implementasi resolusi kelebihan memiliki kode non-sepele selain menerapkan JLS. Dari pemahaman saya kode ini (yang berasal dari saat Java 5 baru) harus disimpan untuk mengisi beberapa celah di JLS.

Saya telah mengajukan https://bugs.eclipse.org/562538 untuk melacak ini.

Terlepas dari bug khusus ini, saya hanya bisa sangat menyarankan terhadap gaya kode ini. Overloading baik untuk sejumlah kejutan di Jawa, dikalikan dengan inferensi tipe lambda, kompleksitas cukup di luar proporsi sesuai dengan perolehan yang dirasakan.

Stephan Herrmann
sumber
Terima kasih. Sudah login bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , mungkin Anda dapat membantu menautkannya atau menutupnya sebagai duplikat ...
ernest_k