Dlaczego parametr typu jest silniejszy niż parametr metody?

12

Dlaczego jest

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

bardziej rygorystyczne

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

Jest to kontynuacja Dlaczego nie jest sprawdzany typ zwrotu lambda w czasie kompilacji . Znalazłem metodę withX()podobną

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

produkuje żądany błąd czasu kompilacji:

Typ getLength () z typu BuilderExample.MyInterface jest długi, co jest niezgodne z typem zwracanym przez deskryptor: String

podczas korzystania z metody with()nie.

pełny przykład:

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

Rozszerzony przykład

Poniższy przykład pokazuje różne zachowanie metody i parametru typu sprowadzone do Dostawcy. Ponadto pokazuje różnicę w zachowaniu konsumenta dla parametru typu. I pokazuje, że nie robi to różnicy, skoro jest parametrem metody konsumenta lub dostawcy.

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
źródło
1
Z powodu wnioskowania z tym ostatnim. Chociaż oba są oparte na przypadku, który należy wdrożyć. Dla ciebie to pierwsze może być surowe i dobre. Aby uzyskać elastyczność, ktoś inny może preferować to drugie.
Naman
Czy próbujesz skompilować to w Eclipse? Wyszukiwanie ciągów błędów w wklejonym formacie sugeruje, że jest to błąd specyficzny dla Eclipse (ecj). Czy masz ten sam problem podczas kompilacji za javacpomocą narzędzia raw lub narzędzia do budowania, takiego jak Gradle lub Maven?
user31601
@ user31601 dodałem pełny przykład z wyjściem javac. Komunikaty o błędach są nieco inaczej sformułowane, ale nadal zaćmienie i javac zachowują się tak samo
jukzi

Odpowiedzi:

12

To naprawdę interesujące pytanie. Obawiam się, że odpowiedź jest skomplikowana.

tl; dr

Wypracowanie różnicy wymaga dogłębnego odczytania specyfikacji wnioskowania o typie Java , ale w zasadzie sprowadza się do tego:

  • Wszystkie inne rzeczy są takie same, kompilator określa najbardziej konkretny typ, jaki może.
  • Jednakże, jeśli można go znaleźć na podstawienie dla parametru typu, który spełnia wszystkie wymagania, to kompilacja będzie odnieść sukces, jednak niejasne podstawienie okazuje się być.
  • Ponieważ withistnieje (co prawda niejasne) substytucja, która spełnia wszystkie wymagania dotyczące R:Serializable
  • Ponieważ withXwprowadzenie dodatkowego parametru typu Fzmusza kompilator do Rpierwszego rozwiązania , bez uwzględnienia ograniczenia F extends Function<T,R>. Rodnosi się do (znacznie bardziej szczegółowego), Stringco następnie oznacza, że ​​wnioskowanie o Fniepowodzeniu.

Ten ostatni punkt jest najważniejszy, ale także najbardziej falisty. Nie mogę wymyślić lepszego zwięzłego sposobu sformułowania go, więc jeśli chcesz uzyskać więcej szczegółów, sugeruję przeczytanie pełnego wyjaśnienia poniżej.

Czy to jest zamierzone zachowanie?

Pójdę w opałach tutaj i powiedzieć nie .

Nie sugeruję, że w specyfikacji jest błąd, a ponadto (w przypadku withX) projektanci języka podnieśli ręce i powiedzieli: „w niektórych sytuacjach wnioskowanie o typie staje się zbyt trudne, więc po prostu się nie powiedziemy” . Nawet jeśli zachowanie kompilatora w odniesieniu dowithX wydaje się być tym, czego chcesz, uważam, że jest to uboczny efekt uboczny obecnej specyfikacji, a nie pozytywnie zaplanowana decyzja projektowa.

Ma to znaczenie, ponieważ informuje o pytaniu. Czy powinienem polegać na tym zachowaniu w projekcie aplikacji? Twierdziłbym, że nie powinieneś, ponieważ nie możesz zagwarantować, że przyszłe wersje języka będą się zachowywać w ten sposób.

Chociaż prawdą jest, że projektanci języków bardzo starają się nie uszkodzić istniejących aplikacji podczas aktualizacji specyfikacji / projektu / kompilatora, problem polega na tym, że zachowanie, na którym chcesz polegać, polega na tym, że kompilator obecnie zawodzi (tzn. Nie jest aplikacją istniejącą ). Aktualizacje Langauge przez cały czas przekształcają kod niekompilujący w kod kompilujący. Na przykład, następujący kod może zostać zagwarantowane nie skompilować w Java 7, ale będzie skompilować w Java 8:

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

Twój przypadek użycia nie jest inny.

Innym powodem, dla którego byłbym ostrożny w używaniu tej withXmetody, jest Fsam parametr. Ogólnie rzecz biorąc, parametr typu ogólnego w metodzie (który nie pojawia się w typie zwracanym) istnieje w celu powiązania typów wielu części podpisu razem. Mówi:

Nie dbam o to T, co jest, ale chcę mieć pewność, że gdziekolwiek używam T, jest tego samego typu.

Logicznie więc spodziewalibyśmy się, że każdy parametr typu pojawi się co najmniej dwa razy w sygnaturze metody, w przeciwnym razie „nic nie robi”. Fw swojej withXpojawia się tylko raz w podpisie, co sugeruje mi się użycie parametru typu nie inline z intencją tej funkcji języka.

Alternatywne wdrożenie

Jednym ze sposobów na wdrożenie tego w nieco bardziej „zamierzony sposób” byłoby podzielenie withmetody na łańcuch 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);
    }

}

Można to następnie wykorzystać w następujący sposób:

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

Nie obejmuje to zewnętrznego parametru typu, jak Twój withX. Dzieląc metodę na dwie sygnatury, lepiej wyraża zamiar tego, co próbujesz zrobić, z punktu widzenia bezpieczeństwa typu:

  • Pierwsza metoda ustawia klasę ( With), która definiuje typ na podstawie odwołania do metody.
  • Metoda scond ( of) ogranicza typ, valueaby był zgodny z tym, co wcześniej skonfigurowałeś.

Jedynym sposobem, w jaki przyszła wersja języka byłaby w stanie to skompilować, jest zaimplementowanie pełnego wpisywania kaczych znaków, co wydaje się mało prawdopodobne.

Ostatnia uwaga, żeby to wszystko nie miało znaczenia: Myślę, że Mockito (a w szczególności jego funkcja stubowania) może już zasadniczo zrobić to, co próbujesz osiągnąć za pomocą „ typowego generatora bezpiecznego generycznego”. Może mógłbyś po prostu tego użyć?

Pełne (ish) wyjaśnienie

Idę do pracy przez procedury rodzaj wnioskowania zarówno dlawithiwithX. To jest dość długie, więc weź to powoli. Pomimo tego, że byłem długi, wciąż pozostawiłem sporo szczegółów. Możesz zapoznać się ze specyfikacją, aby uzyskać więcej informacji (skorzystaj z linków), aby przekonać się, że mam rację (być może popełniłem błąd).

Ponadto, aby trochę uprościć, użyję bardziej minimalnej próbki kodu. Główną różnicą jest to, że zamienia sięFunction na Supplier, więc jest mniej typy i parametry w grze. Oto pełny fragment, który odtwarza opisywane zachowanie:

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
    }

}

Przeanalizujmy wnioskowanie o zastosowaniu typu i wnioskowanie o typie procedurę o dla każdego wywołania metody:

with

Mamy:

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

Początkowy zestaw związany, B 0 , to:

  • R <: Object

Wszystkie wyrażenia parametrów są stosowalności .

Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:

  • TypeInference::getLong jest kompatybilny z Supplier<R>
  • "Not a long" jest kompatybilny z R

To zmniejsza się związanego zestaw B 2 z:

  • R <: Object(od B 0 )
  • Long <: R (od pierwszego ograniczenia)
  • String <: R (od drugiego ograniczenia)

Ponieważ nie zawiera związany „ false ”, a (zakładam) uchwałę oR powiedzie (daje Serializable), a następnie wezwanie to dotyczy.

Zatem przechodzimy do wnioskowania o typie wywołania .

Nowy zestaw ograniczeń C z powiązanymi zmiennymi wejściowymi i wyjściowymi to:

  • TypeInference::getLong jest kompatybilny z Supplier<R>
    • Zmienne wejściowe: brak
    • Zmienne wyjściowe: R

Nie zawiera żadnych współzależności między zmiennymi wejściowymi i wyjściowymi , więc można je zmniejszyć w jednym kroku, a ostateczny zestaw wiązań, B 4 , jest taki sam jak B 2 . Stąd rozdzielczość kończy się tak jak poprzednio, a kompilator odetchnął z ulgą!

withX

Mamy:

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

Początkowy zestaw związany, B 0 , to:

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

Jedynie wyrażenie drugiego parametru dotyczy zastosowania . Pierwszy (TypeInference::getLong ) nie jest, ponieważ spełnia następujący warunek:

Jeśli mjest to metoda ogólna, a wywołanie metody nie dostarcza argumentów typu jawnego, jawnie typowanego wyrażenia lambda lub dokładnego wyrażenia referencyjnego metody, dla którego odpowiedni typ docelowy (wyprowadzony z podpisu m) jest parametrem typu m.

Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:

  • "Also not a long" jest kompatybilny z R

To zmniejsza się związanego zestaw B 2 z:

  • R <: Object(od B 0 )
  • F <: Supplier<R>(od B 0 )
  • String <: R (z ograniczenia)

Ponownie, ponieważ nie zawiera związany „ false ”, a rozdzielczość z Ruda (daje String), a następnie wezwanie to dotyczy.

Wnioskowanie o typie wywołania jeszcze raz ...

Tym razem nowy zestaw ograniczeń C z powiązanymi zmiennymi wejściowymi i wyjściowymi to:

  • TypeInference::getLong jest kompatybilny z F
    • Zmienne wejściowe: F
    • Zmienne wyjściowe: brak

Ponownie nie mamy zależności między zmiennymi wejściowymi i wyjściowymi . Jednak tym razem, nie jest zmienna wejściowa ( F), więc musimy rozwiązać ten przed próbą redukcji . Zaczynamy więc od naszego zestawu B 2 .

  1. Podzbiór określamy w Vnastępujący sposób:

    Biorąc pod uwagę zestaw zmiennych wnioskowania do rozwiązania, pozwól V będzie sumą tego zbioru i wszystkich zmiennych, od których zależy rozdzielczość co najmniej jednej zmiennej w tym zestawie.

    W drugim ograniczeniu w B 2 rozdzielczość Fzależy od R, więc V := {F, R}.

  2. Wybieramy podzbiór Vwedług reguły:

    niech { α1, ..., αn }będzie niepustym podzbiorem niezainicjowanych zmiennych w Vtaki sposób, że i) dla wszystkich i (1 ≤ i ≤ n), jeśli αizależy od rozdzielczości zmiennej β, wówczas βma instancję lub istnieje jtaka możliwość β = αj; oraz ii) nie istnieje niepusty pusty podzbiór { α1, ..., αn }tej właściwości.

    Jedyny podzbiór, Vktóry spełnia tę właściwość, to {R}.

  3. Za pomocą trzeciego bound ( String <: R) tworzymy instancję R = Stringi włączamy to do naszego zestawu związanego. Rjest teraz rozwiązany, a druga granica faktycznie staje sięF <: Supplier<String> .

  4. Korzystając z (poprawionej) drugiej granicy, tworzymy instancję F = Supplier<String>. Fjest teraz rozwiązany.

Teraz, gdy Fproblem został rozwiązany, możemy kontynuować redukcję , stosując nowe ograniczenie:

  1. TypeInference::getLong jest kompatybilny z Supplier<String>
  2. ... redukuje do Long jest kompatybilny z String
  3. ... co sprowadza się do fałszu

... i pojawia się błąd kompilatora!


Dodatkowe uwagi na temat „Rozszerzonego przykładu”

Rozszerzony przykład w wyglądzie pytanie na kilku interesujących przypadków, które nie są bezpośrednio objęte wyrobisk powyżej:

  • Gdzie typ wartości jest podtypem zwracanego typu metody (Integer <: Number )
  • Gdzie interfejs funkcjonalny jest sprzeczny w wywnioskowanym typie (tj. ConsumerZamiast Supplier)

W szczególności 3 z podanych wywołań wyróżniają się jako potencjalnie sugerujące „inne” zachowanie kompilatora niż opisane w objaśnieniach:

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

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

Drugi z tych 3 przejdzie dokładnie ten sam proces wnioskowania, jak withXwyżej (wystarczy wymienić Longz Numberi Stringz Integer). To ilustruje jeszcze jeden powód, dla którego nie powinieneś polegać na tym błędnym wnioskowaniu typu w projekcie klasy, ponieważ niepowodzenie kompilacji tutaj prawdopodobnie nie jest pożądanym zachowaniem.

W przypadku pozostałych 2 (i rzeczywiście wszystkich innych wywołań, w Consumerktórych chcesz przeprowadzić pracę) zachowanie powinno być widoczne, jeśli przejdziesz przez procedurę wnioskowania typu określoną dla jednej z powyższych metod (tj. withDla pierwszej withXdla trzeci). Jest tylko jedna niewielka zmiana, na którą musisz zwrócić uwagę:

  • Ograniczenie pierwszego parametru ( t::setNumber jest kompatybilny z Consumer<R> ) będzie zmniejszać się R <: Numberzamiast Number <: R, jak to robi dla Supplier<R>. Jest to opisane w powiązanej dokumentacji dotyczącej redukcji.

Pozostawiam to czytelnikowi jako ćwiczenie, aby ostrożnie przepracować jedną z powyższych procedur, uzbrojoną w tę dodatkową wiedzę, aby zademonstrować sobie dokładnie, dlaczego dane wywołanie się kompiluje, czy nie.

użytkownik31601
źródło
Bardzo dogłębne, dobrze zbadane i sformułowane. Dzięki!
Zabuzard
@ user31601 Czy możesz wskazać różnicę między dostawcą a konsumentem. Do tego dodałem Rozszerzony Przykład w pierwotnym pytaniu. Pokazuje zachowanie kowariantne, przeciwwariantowe i niezmiennicze dla różnych wersji letBe (), letBeX () i let (). Be () w zależności od dostawcy / konsumenta.
jukzi
@jukzi Dodałem kilka dodatkowych notatek, ale powinieneś mieć wystarczającą ilość informacji, aby samodzielnie przejrzeć te nowe przykłady.
user31601
To ciekawe: tyle specjalnych przypadków w 18.2.1. dla lambd i odniesień do metod, w których z mojego naiwnego zrozumienia nie spodziewałbym się żadnego specjalnego przypadku. I prawdopodobnie nie spodziewałby się tego zwykły programista.
jukzi
Wydaje mi się, że powodem jest to, że w przypadku lambd i odwołań do metod, kompilator musi zdecydować, jaki właściwy typ powinna zaimplementować lambda - musi dokonać wyboru! Na przykład, TypeInference::getLongmógłby imlement Supplier<Long>lub Supplier<Serializable>lub Supplier<Number>itp, ale co najważniejsze może realizować tylko jeden z nich (podobnie jak każdej innej klasie)! Różni się to od wszystkich innych wyrażeń, w których zaimplementowane typy są znane z góry, a kompilator musi tylko sprawdzić, czy jeden z nich spełnia wymagania ograniczenia.
user31601