Birden fazla eşleşen hedef tipine sahip lambda ifadesi için yöntem imza seçimi


11

Ben yanıtlayan oldu bir sorum ben açıklayamam bir senaryo içine ve ran. Bu kodu düşünün:

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
    }
}

Neden lambda parametresini açıkça yazarak (A a) -> aList.add(a)kodu derlediğini anlamıyorum . Buna ek olarak, neden içerideki yükten Iterableziyade aşırı yüke bağlanıyor CustomIterable?
Bununla ilgili bir açıklama veya spesifikasyonun ilgili bölümüne bir bağlantı var mı?

Not: iterable.forEach((A a) -> aList.add(a));Yalnızca CustomIterable<T>genişletildiğinde derler Iterable<T>( CustomIterablebelirsiz yöntemlerle sonuçlanan yöntemlerin aşırı yüklenmesi )


Bunu her ikisinde de almak:

  • openjdk "13.0.2" sürümü 2020-01-14
    Eclipse derleyicisi
  • openjdk sürüm "1.8.0_232"
    Eclipse derleyici

Düzenleme : Eclipse son kod satırını başarıyla derlerken yukarıdaki kod maven ile bina derleme başarısız.


3
Java 8 üzerinde üç derleme hiçbiri. Şimdi bu daha yeni bir sürümünde düzeltilmiş bir hata veya tanıtılan bir hata / özellik olup olmadığından emin değilim ... Muhtemelen Java sürümünü belirtmelisiniz
Süpürge

@ Süpürge Başlangıçta bunu jdk-13 kullanarak aldım. Java 8'de (jdk8u232) sonraki testler aynı hataları gösterir. Sonuncunun neden makinenizde derlenmediğinden emin değilim.
ernest_k

İki çevrimiçi derleyicide de çoğaltılamaz ( 1 , 2 ). Makinemde 1.8.0_221 kullanıyorum. Bu garip ve garip oluyor ...
Süpürge

1
@ernest_k Eclipse'ın kendi derleyici uygulaması vardır. Bu soru için çok önemli bir bilgi olabilir. Ayrıca, temiz bir maven derleme hatası son satırın da bence soruda vurgulanmalıdır. Öte yandan, bağlantılı soru hakkında, OP'nin Eclipse'i de kullandığı varsayımı, kod yeniden üretilemediğinden açıklığa kavuşturulabilir.
Naman

2
Sorunun entelektüel değerini anlasam da, sadece fonksiyonel arayüzlerde farklılık gösteren ve bunun güvenli bir şekilde lambda geçirme olarak adlandırılabileceğini beklerken aşırı yüklenmiş yöntemler oluşturmaya karşı tavsiyede bulunabilirim. Lambda tipi çıkarım ve aşırı yük kombinasyonunun ortalama bir programcının anlamaya yaklaşacağı bir şey olduğuna inanmıyorum. Kullanıcıların kontrol etmediği çok değişkenli bir denklemdir. BUNU KAÇININ lütfen :)
Stephan Herrmann

Yanıtlar:


8

TL; DR, bu bir derleyici hatasıdır.

Devralındığında belirli bir geçerli yönteme veya varsayılan bir yönteme öncelik verecek bir kural yoktur. İlginç bir şekilde, kodu değiştirdiğimde

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
}

iterable.forEach((A a) -> aList.add(a));ifadesi Eclipse bir hata üretir.

Başka bir aşırı yük bildirilirken arabirimdeki forEach(Consumer<? super T) c)yöntemin hiçbir özelliği Iterable<T>değişmediğinden, Eclipse'nin bu yöntemi seçme kararı yöntemin hiçbir özelliğine dayanamaz (tutarlı bir şekilde). Hala tek miras alınan yöntem, hala tek defaultyöntem, hala tek JDK yöntemi, vb. Bu özelliklerin hiçbiri yine de yöntem seçimini etkilememelidir.

Bildirgenin şu şekilde değiştirildiğine dikkat edin:

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

ayrıca “belirsiz” bir hata üretir, bu nedenle uygulanabilir aşırı yüklenmiş yöntemlerin sayısı da önemli değildir, sadece iki aday olsa bile, defaultyöntemlere karşı genel bir tercih yoktur .

Şimdiye kadar, sorun iki uygulanabilir yöntem olduğunda ve bir defaultyöntem ve bir miras ilişkisi söz konusu olduğunda ortaya çıkıyor gibi görünüyor , ancak bu daha fazla kazmak için doğru yer değil.


Ancak, örneğinizin yapılarının derleyicideki farklı uygulama koduyla işlenebileceği anlaşılabilir, biri hata yaparken diğeri hata göstermez.
a -> aList.add(a)Bir olan dolaylı olarak yazılmış aşırı çözünürlük için kullanılamaz lambda ifade. Buna karşılık, aşırı yüklenmiş yöntemlerden bir eşleştirme yöntemi seçmek için kullanılabilen açık(A a) -> aList.add(a) bir şekilde yazılmış bir lambda ifadesidir, ancak tüm yöntemlerin tam olarak aynı işlevsel imzayla parametre türlerine sahip olması nedeniyle burada yardımcı olmaz (burada yardımcı olmamalıdır). .

Karşı örnek olarak,

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

işlevsel imzalar farklıdır ve açık bir şekilde lambda ifadesi kullanmak doğru yöntemi seçmeye yardımcı olabilirken örtük olarak yazılan lambda ifadesi yardımcı olmaz, bu nedenle forEach(s -> s.isEmpty())bir derleyici hatası üretir. Ve tüm Java derleyicileri bu konuda hemfikir.

Yöntem de aşırı yüklendiğinden aList::addbelirsiz bir yöntem başvurusu olduğuna dikkat edin add, bu nedenle bir yöntem seçmeye de yardımcı olamaz, ancak yöntem başvuruları yine de farklı kodlarla işlenebilir. Kesin bir geçiş aList::containsya da değişen Listiçin Collectionyapmak, addkesin, benim Eclipse kurulumda sonucunu değiştirmedi (kullandım 2019-06).


1
@howlger yorumunuzun bir anlamı yok. Varsayılan yöntem olan kalıtsal yöntem ve metot aşırı yüklü. Başka bir yöntem yok. Kalıtsal yöntemin bir defaultyöntem olması sadece ek bir noktadır. Benim cevabım zaten Eclipse etmeyen bir örneğini gösterir değil varsayılan yöntemine öncelik vermek.
Holger

1
@howlger davranışlarımızda temel bir fark var. Yalnızca kaldırmanın defaultsonucu değiştirdiğini ve gözlemlenen davranışın nedenini hemen bulduğunu keşfettiniz . Bu konuda o kadar fazla güvendesiniz ki, çelişkili bile olmamasına rağmen, diğer cevapları yanlış olarak adlandırıyorsunuz. Kendi davranışınızı diğerlerine göre yansıttığınız için , neden kalıtım olduğunu hiç söylemedim . Öyle olmadığını kanıtladım. Eclipse'in belirli bir senaryoda belirli yöntemi seçtiği ancak üç aşırı yüklenmenin olduğu başka bir yöntemde seçmediği için davranışın tutarsız olduğunu gösterdim.
Holger

1
@howlger Bunun yanı sıra, zaten bu yorumun sonunda başka bir senaryo adlandırdım , bir arayüz oluşturdum, kalıtım yok, iki yöntem, defaultdiğeri abstract, iki tüketici gibi argümanlarla ve denedim. Tutulma, bir defaultyöntem olmasına rağmen belirsiz olduğunu doğru bir şekilde söylüyor . Görünüşe göre kalıtım hala bu Tutulma böceği ile ilgilidir, ama senden farklı olarak, deliye gitmiyorum ve diğer cevapları yanlış çağırmıyorum, çünkü hatayı bütünüyle analiz etmedikleri için. Bu bizim işimiz değil.
Holger

1
@howlger no, mesele şu ki bu bir hata. Çoğu okuyucu ayrıntıları bile önemsemez. Eclipse, her yöntem seçtiğinde zar atar, önemli değil. Tutulma, belirsiz olduğunda bir yöntem seçmemelidir, bu yüzden neden birini seçtiği önemli değildir. Bu cevap, davranışın tutarsız olduğunu kanıtlıyor, bu da bunun bir hata olduğunu güçlü bir şekilde belirtmek için zaten yeterli. Eclipse'nin kaynak kodunda işlerin yanlış gittiği yere işaret etmek gerekli değildir. Stackoverflow'un amacı bu değil. Belki de Stackoverflow'u Eclipse'ın hata izleyicisiyle karıştırıyorsunuz.
Holger

1
@howlger, yine yanlış bir şekilde Eclipse'in neden bu yanlış seçimi yaptığını (yanlış) ifade ettiğimi iddia ediyorsun . Yine, ben tutmadım, çünkü tutulma hiç bir seçim yapmamalıdır. Yöntem belirsiz. Nokta. “ Miras alınan ” terimini kullanmamın nedeni, aynı adlı yöntemleri ayırmanın gerekli olmasıdır. Bunun yerine mantığı değiştirmeden “ varsayılan yöntem ” diyebilirdim . Daha doğrusu, “ Eclipse yöntemi ne olursa olsun yanlış seçilmiş ” ifadesini kullanmalıydım . Üç ifadeden birini birbirinin yerine kullanabilirsiniz ve mantık değişmez.
Holger

2

Eclipse derleyicisi , Java Dil Spesifikasyonu 15.12.2.5'e göre en spesifik yöntem olduğu için defaultyönteme doğru şekilde çözümlenir :

Maksimal olarak spesifik yöntemlerden tam olarak somutsa (yani, abstractvarsayılan olmayan veya varsayılan), en spesifik yöntemdir.

javac(Maven ve IntelliJ tarafından varsayılan olarak kullanılır) yöntem çağrısının burada belirsiz olduğunu söyler. Ancak Java Dil Spesifikasyonu'na göre, bu iki yöntemden biri buradaki en spesifik yöntem olduğundan belirsiz değildir.

Örtülü olarak yazılan lambda ifadeleri, Java'da açıkça yazılan lambda ifadelerinden farklı şekilde ele alınır . Açıkça yazılan lambda ifadelerinin aksine örtük olarak yazılan, katı çağırma yöntemlerini tanımlamak için ilk aşamaya düşer (bkz. Java Dil Spesifikasyonu jls-15.12.2.2 , ilk nokta). Bu nedenle, buradaki yöntem çağrısı, örtük olarak yazılan lambda ifadeleri için belirsizdir .

Sizin durumunuzda, bu hatanın çözümü , açık bir şekilde yazılmış bir lambda ifadesini aşağıdaki gibi kullanmak yerine işlevsel arabirimin türünüjavac belirtmektir :

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

veya

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

Test için daha da küçültülmüş örneğiniz:

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)
    }

}

3
Atıfta bulunulan cümlenin hemen ön koşulunu kaçırdınız: “ Eğer azami özgül yöntemlerin hepsinde geçersiz kılma eşdeğeri imzalar varsa ” Tabii ki, argümanları tamamen ilgisiz arayüzleri olan iki yöntemde geçersiz kılma eşdeğeri imzalar yoktur. Bunun yanı sıra, bu defaultüç aday yöntem olduğunda veya her iki yöntem de aynı arabirimde bildirildiğinde Eclipse'in yöntemi seçmeyi neden durdurduğunu açıklamayacaktır .
Holger

1
@Holger Yanıtınız, "Devralındığında belirli bir geçerli yönteme veya varsayılan bir yönteme öncelik verecek bir kural yoktur." Var olmayan bu kuralın ön koşulunun burada geçerli olmadığını söylediğinizi doğru anladım mı? Buradaki parametrenin işlevsel bir arayüz olduğunu lütfen unutmayın (bkz. JLS 9.8).
howlger

1
Bağlam dışında bir cümle kopardın. Cümle, geçersiz kılma eşdeğeri yöntemlerin seçimini, başka bir deyişle, tümünün çalışma zamanında aynı yöntemi çağıracağı bildirimler arasında bir seçimi açıklar , çünkü beton sınıfında yalnızca bir somut yöntem olacaktır. Bu ayrı gibi yöntemlerin davaya alakasız forEach(Consumer)ve forEach(Consumer2)aynı uygulama metoduna gider asla.
Holger

2
@StephanHerrmann Bir JEP veya JSR bilmiyorum, ancak değişiklik “beton” anlamına uygun bir düzeltme gibi görünüyor, yani JLS§9.4 ile karşılaştırın : “ Varsayılan yöntemler somut yöntemlerden farklıdır (§8.4. 3.1). Hiç değişmedi.
Holger

2
@StephanHerrmann evet, geçersiz kılma eşdeğer yöntemlerden bir aday seçmek daha karmaşık hale geldi ve arkasındaki mantığı bilmek ilginç olurdu, ancak eldeki soru ile ilgili değil. Değişiklikleri ve motivasyonları açıklayan başka bir belge olmalıdır. Geçmişte vardı, ama bu "her yıl yeni bir sürüm" politikasıyla kaliteyi korumak imkansız görünüyor ...
Holger

2

Eclipse'nin JLS §15.12.2.5'i uyguladığı kod, açıkça yazılan lambda durumunda bile her iki yöntemi de diğerinden daha spesifik bulmaz.

İdeal olarak Tutulma burada duracak ve belirsizliği rapor edecektir. Ne yazık ki, aşırı yük çözünürlüğünün uygulanması, JLS'nin uygulanmasının yanı sıra önemsiz bir koda sahiptir. Anladığım kadarıyla, bu kodun (Java 5'in yeni olduğu tarihten itibaren) JLS'deki bazı boşlukları doldurması için saklanması gerekir.

Bunu izlemek için https://bugs.eclipse.org/562538 dosyaladım .

Bu hatadan bağımsız olarak, sadece bu kod stiline karşı şiddetle tavsiye edebilirim. Aşırı yükleme, Java'da çok sayıda sürpriz için iyidir, lambda tipi çıkarımla çarpılır, karmaşıklık algılanan kazançla orantısızdır.


Teşekkür ederim. Bugs.eclipse.org/bugs/show_bug.cgi?id=562507 giriş yapmış olabilirsiniz, belki onları bağlamanıza yardımcı olabilir veya yinelenen olarak kapatabilirsiniz ...
ernest_k
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.