Bir type parametresi neden bir yöntem parametresinden daha güçlü?


12

Neden ki

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

o zaman daha katı

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Bu, lambda dönüş türünün derleme zamanında neden kontrol edilmediğinin bir takibi . withX()Gibi yöntemi kullanarak buldum

.withX(MyInterface::getLength, "I am not a Long")

istenen derleme zamanı hatasını üretir:

BuilderExample.MyInterface türündeki getLength () türü uzun, bu, tanımlayıcının dönüş türüyle uyumsuz: String

yöntemi kullanırken with()değil.

tam örnek:

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

Genişletilmiş Örnek

Aşağıdaki örnek, bir Tedarikçi'ye haşlanmış yöntem ve tür parametresinin farklı davranışını gösterir. Ayrıca bir tür parametresi için Tüketici davranışı arasındaki farkı gösterir. Ve bir yöntem parametresi için Tüketici veya Tedarikçi olsun fark yaratmadığını gösterir.

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

1
İkincisi ile çıkarım nedeniyle. Her ikisi de uygulanması gereken kullanım durumuna dayandırılır. Sizinki için, ilk katı ve iyi olabilir. Esneklik için, başkası ikincisini tercih edebilir.
Naman

Eclipse'de bunu derlemeye mi çalışıyorsunuz? Yapıştırdığınız biçimin hata dizelerini aramak bunun Eclipse (ecj) 'e özgü bir hata olduğunu gösterir. Ham javacveya Gradle veya Maven gibi bir derleme aracıyla derlerken aynı sorunu mu yaşıyorsunuz?
user31601

@ user31601 Javac çıktı ile tam bir örnek ekledim. Hata mesajları biraz farklı biçimlendirilmiş ama yine de tutulma ve javac aynı davranışı
sürdürüyor

Yanıtlar:


12

Bu gerçekten ilginç bir soru. Korkarım cevap karmaşık.

tl; Dr.

Farkı çözmek, Java'nın tür çıkarım şartnamesinin oldukça derinlemesine okunmasını içerir , ancak temelde buna dayanır:

  • Diğer tüm şeyler eşit olduğunda, derleyici elinden gelenin en iyisini yapar .
  • Bulabildiği Ancak, bir bütün gereksinimleri karşılar, sonra derleme başarılı olacağı bir tür parametresi için ikame Ancak muğlak ikamesi olarak çıkıyor.
  • Çünkü withaşağıdaki şartları yerine getiren (kuşkusuz belirsiz) bir ikame vardır R:Serializable
  • Çünkü withX, ek tür parametresinin eklenmesi F, derleyiciyi Rilk önce kısıtlamayı dikkate almadan çözümlemeye zorlar F extends Function<T,R>. R(çok daha spesifik) olarak karar verir, Stringbu da çıkarımın Fbaşarısız olduğu anlamına gelir .

Bu son mermi noktası en önemlisi, aynı zamanda en dalgalı olanıdır. İfadenin daha iyi ve kısa bir yolunu düşünemiyorum, bu yüzden daha fazla ayrıntı istiyorsanız, aşağıdaki açıklamanın tamamını okumanızı öneririz.

Bu amaçlanan davranış mı?

Burada bir uzuv çıkacağım ve hayır diyeceğim .

Spesifikasyonda bir hata olduğunu öne sürmüyorum, daha fazla (durumda withX) dil tasarımcıları ellerini kaldırdı ve "tip çıkarımın çok zorlaştığı bazı durumlar var, bu yüzden sadece başarısız olacağız" dedi . Derleyicinin bu konudaki davranışı withXistediğiniz gibi görünse de, bunun olumlu bir tasarım kararı yerine mevcut spesifikasyonun tesadüfi bir yan etkisi olduğunu düşünürüm.

Bu önemlidir, çünkü soruyu bilgilendirir Uygulama tasarımımda bu davranışa güvenmeli miyim? Yapmamalısınız, çünkü dilin gelecekteki sürümlerinin bu şekilde davranmaya devam edeceğini garanti edemezsiniz.

Dil tasarımcılarının spec / design / derleyicilerini güncellediklerinde varolan uygulamaları kırmamaya çalıştıkları doğru olsa da, sorun güvenmek istediğiniz davranışın derleyicinin başarısız olduğu (yani mevcut bir uygulama değil ) olmasıdır. Dil güncellemeleri derleme yapmayan kodu her zaman derleme koduna dönüştürür. Örneğin, aşağıdaki kod olabilir garantili Java 7 derlemeye değil, ama olur Java 8'de derlemek:

static Runnable x = () -> System.out.println();

Kullanım durumunuz farklı değil.

Metodunuzu kullanma konusunda dikkatli olmamın bir başka nedeni withXde Fparametrenin kendisidir. Genellikle, bir yöntemdeki (dönüş türünde görünmeyen) genel tür parametresi , imzanın birden çok bölümünün türlerini birbirine bağlamak için vardır. Diyor ki:

Ne olduğu umrumda değil T, ama nerede kullanırsam kullanalım Taynı tipte olduğundan emin olmak istiyorum .

Mantıksal olarak, her tür parametresinin bir yöntem imzasında en az iki kez görünmesini bekleriz, aksi takdirde "hiçbir şey yapmaz". Fimzanızda withXyalnızca bir kez görünür, bu da bana dilin bu özelliğinin amacı ile satır içi olmayan bir tür parametresi kullanımını önerir .

Alternatif bir uygulama

Bunu biraz daha "amaçlanan davranış" biçiminde uygulamanın bir yolu, withyönteminizi 2 zincirine ayırmak olacaktır :

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

}

Bu daha sonra aşağıdaki gibi kullanılabilir:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Bu, yaptığınız gibi yabancı bir tür parametresi withXiçermez. Yöntemi iki imzaya bölerek, bir tür güvenlik açısından ne yapmaya çalıştığınızın amacını daha iyi ifade eder:

  • İlk yöntem , yöntem başvurusuna dayalı olarak türü tanımlayan bir class ( With) kurar .
  • Scond method ( of) yöntemi , önceden ayarladığınız yöntemle uyumlu olacak türün sınırlandırırvalue .

Dilin gelecekteki bir versiyonunun bunu derleyebilmesinin tek yolu, uygulanan tam ördek yazması, olası görünmüyorsa.

Tüm bu şeyleri alakasız hale getirmek için son bir not: Sanırım Mockito (ve özellikle de stubbing işlevselliği) "tip güvenli jenerik kurucunuz" ile elde etmeye çalıştığınız şeyi zaten yapabilir. Belki de bunun yerine kullanabilirsiniz.

Tam (ish) açıklaması

Hem ve hem de türünü çıkarma yordamı ile çalışacağım . Bu oldukça uzun, bu yüzden yavaşça alın. Uzun olmasına rağmen, hala çok fazla ayrıntı bıraktım. Kendinizi haklı olduğum konusunda ikna etmek için daha fazla ayrıntı için spesifikasyonlara başvurmak isteyebilirsiniz (bağlantıları takip edin) (bir hata yapmış olabilirim).withwithX

Ayrıca, işleri biraz basitleştirmek için, daha az kod örneği kullanacağım. Temel fark o swapları olmasıdır Functioniçin Supplier, bu yüzden daha az türleri ve oyunda parametreler vardır. Açıkladığınız davranışı yeniden üreten tam bir snippet:

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
    }

}

Her bir yöntem çağrısı için tür uygulanabilirlik çıkarımı ve tür çıkarımı prosedürü üzerinde çalışalım :

with

Sahibiz:

with(TypeInference::getLong, "Not a long");

Başlangıç ​​sınır kümesi B 0 :

  • R <: Object

Tüm parametre ifadeleri uygulanabilirlikle ilgilidir .

Dolayısıyla, uygulanabilirlik çıkarımı için ayarlanan ilk kısıtlama , C , şöyledir:

  • TypeInference::getLong ile uyumlu Supplier<R>
  • "Not a long" ile uyumlu R

Bu azaltır bağlı grubu için B 2 arasında:

  • R <: Object( B 0'dan itibaren )
  • Long <: R (ilk kısıtlamadan itibaren)
  • String <: R (ikinci kısıtlamadan)

Bu sınır 'ihtiva etmediğinden false ' ve (ı varsayalım) çözünürlüğü ait Rbaşarılı (vererek Serializable), sonra çağırma uygulanabilir.

Bu nedenle, çağırma türü çıkarımına geçiyoruz .

İlişkili giriş ve çıkış değişkenleriyle yeni kısıt kümesi C :

  • TypeInference::getLong ile uyumlu Supplier<R>
    • Giriş değişkenleri: yok
    • Çıktı değişkenleri: R

Bu arasında bağımlılıklar içeren giriş ve çıkış böylece edilebilir değişken azaltılmış , tek bir aşamada ve son bağlı seti, B 4 , aynı B 2 . Bu nedenle, çözünürlük önceki gibi başarılı olur ve derleyici rahat bir nefes alır!

withX

Sahibiz:

withX(TypeInference::getLong, "Also not a long");

Başlangıç ​​sınır kümesi B 0 :

  • R <: Object
  • F <: Supplier<R>

Sadece ikinci parametre ifadesi uygulanabilirlikle ilgilidir . İlki ( TypeInference::getLong) değildir, çünkü aşağıdaki koşulu karşılar:

Eğer mgenel bir yöntem olup, yöntem çağırma açık tür bağımsız değişkenleri, bir açık yazılmış lambda ifade ya da denk düşen hedef türü (imza elde edilen gibi olduğu için tam bir yöntem, referans ifade sağlamaz m) bir tür parametresidir m.

Dolayısıyla, uygulanabilirlik çıkarımı için ayarlanan ilk kısıtlama , C , şöyledir:

  • "Also not a long" ile uyumlu R

Bu azaltır bağlı grubu için B 2 arasında:

  • R <: Object( B 0'dan itibaren )
  • F <: Supplier<R>( B 0'dan itibaren )
  • String <: R (kısıtlamadan)

Bu bağlı 'içermediği Yine false ' ve çözünürlük ait Rbaşarır (vererek String), sonra çağırma uygulanabilir.

Çağrı türü çıkarımı bir kez daha ...

Bu kez, ilişkili giriş ve çıkış değişkenleriyle yeni kısıt kümesi C :

  • TypeInference::getLong ile uyumlu F
    • Girdi değişkenleri: F
    • Çıktı değişkenleri: yok

Yine, girdi ve çıktı değişkenleri arasında karşılıklı bağımlılığımız yoktur . Ancak bu sefer, orada olan bir giriş değişkeni ( Fbiz gerekir böylece), çözümlemek denemeden önce bu azalmayı . Böylece, bağlı setimiz B 2 ile başlayacağız .

  1. Bir altkümeyi Vaşağıdaki gibi belirleriz :

    Çözülmesi gereken bir takım çıkarım değişkenleri göz önüne alındığında V, bu kümenin birleşmesi ve bu kümedeki en az bir değişkenin çözünürlüğünün bağlı olduğu tüm değişkenler olsun.

    Bağlanmış ikinci By B 2 , çözünürlüğü Fbağlıdır Ryüzden V := {F, R}.

  2. VKurala göre bir alt küme seçiyoruz :

    Izin vermek , i) herkes için , bir değişkenin çözünürlüğüne bağlı ise , o zaman bir örnekleme vardır ya da böyle bir şey var, örneklenmemiş { α1, ..., αn }değişkenlerin boş olmayan bir alt kümesi ; ve ii) bu özelliğin boş olmayan uygun bir alt kümesi mevcut değildir .Vi (1 ≤ i ≤ n)αiββjβ = αj{ α1, ..., αn }

    VBu özelliği karşılayan tek alt küme {R}.

  3. Üçüncü bound ( String <: R) kullanarak bunu örnekliyoruz R = Stringve bağlı setimize dahil ediyoruz. RŞimdi çözüldü ve ikinci sınır etkili bir şekilde olur F <: Supplier<String>.

  4. (Gözden geçirilmiş) ikinci sınırı kullanarak, somutlaştırırız F = Supplier<String>. Fşimdi çözüldü.

Şimdi bu Fçözüldü, yeni kısıtlamayı kullanarak azaltmaya devam edebiliriz :

  1. TypeInference::getLong ile uyumlu Supplier<String>
  2. ... Long ile uyumludur String
  3. ... yanlış olana

... derleyici hatası alıyoruz!


'Genişletilmiş Örnek' hakkında ek notlar

Sorudaki Genişletilmiş Örnek , doğrudan yukarıdaki çalışmalarla kapsanmayan birkaç ilginç duruma bakar:

  • Değer türü, yöntem döndürme türünün ( ) bir alt türü olduğundaInteger <: Number
  • Fonksiyonel arayüzün çıkarım tipinde (yani, Consumeryerine Supplier) çelişkili olduğu durumlarda

Özellikle, verilen çağrılardan 3 tanesi, açıklamalarda açıklanandan 'farklı' derleyici davranışını potansiyel olarak önermektedir:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

Olarak bu 3 ikinci tam olarak aynı çıkarım işlemi geçeceği withX(sadece yerine yukarıda Longolan Numberve Stringbirlikte Integer). Bu, sınıf tasarımınız için bu başarısız tip çıkarım davranışına güvenmemeniz için başka bir nedeni göstermektedir, çünkü burada derlenememe arzu edilen bir davranış değildir .

Diğer 2 (ve aslında Consumerüzerinde çalışmak istediğiniz bir şeyi içeren diğer çağrılardan ) için, yukarıdaki yöntemlerden biri için (örneğin with, birincisi withXiçin) üçüncü). Dikkat etmeniz gereken sadece küçük bir değişiklik var:

  • İlk parametre (üzerindeki kısıtlama t::setNumber uyumlu Consumer<R> eder) azaltmak için R <: Numberyerine Number <: Rbu için olduğu gibi Supplier<R>. Bu, indirgeme ile ilgili bağlantılı belgelerde açıklanmaktadır.

Okuyucunun, bu ek bilgi parçasına sahip yukarıdaki prosedürlerden birini dikkatlice çalışması için, belirli bir çağrının neden derlendiğini veya derlemediğini kendilerine göstermesini bir egzersiz olarak bırakıyorum.


Çok derinlemesine, iyi araştırılmış ve formüle edilmiştir. Teşekkürler!
Zabuzard

@ user31601 Lütfen Tedarikçi ile Tüketici arasındaki farkın nerede oynandığını gösterebilir misiniz? Bunun için orijinal soruya Genişletilmiş Örnek ekledim. Tedarikçi / Tüketici'ye bağlı olarak farklı letBe (), letBeX () ve let ().
jukzi

@jukzi Birkaç ek not ekledim, ancak bu yeni örnekleri kendiniz incelemek için yeterli bilgiye sahip olmalısınız.
user31601

Bu intersting: 18.2.1'de birçok özel durum. benim naif anlayışım için onlar için herhangi bir özel durum beklemeyeceğim lambdas ve yöntem referansları için. Ve muhtemelen sıradan bir geliştirici beklemiyordu.
jukzi

Peki, sanırım nedeni lambdas ve yöntem referansları ile derleyicinin ne tür bir lambda uygulaması gerektiğine karar vermesi gerekiyor - bir seçim yapmak zorunda! Örneğin, TypeInference::getLongimlement olabilir Supplier<Long>ya Supplier<Serializable>ya Supplier<Number>vs ama en önemlisi bu sadece onlara (tıpkı diğer sınıf gibi) birini uygulayabilirsiniz! Bu, uygulanan türlerin önceden bilindiği diğer tüm ifadelerden farklıdır ve derleyici, bunlardan birinin kısıtlama gereksinimlerini karşılayıp karşılamadığını belirlemelidir.
user31601
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.