Warum ist ein Typparameter stärker als ein Methodenparameter?


12

Warum ist

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

dann strenger

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

Dies ist eine Nachverfolgung von Warum wird der Lambda-Rückgabetyp beim Kompilieren nicht überprüft? . Ich fand mit der MethodewithX() wie

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

erzeugt den gewünschten Fehler bei der Kompilierungszeit:

Der Typ von getLength () vom Typ BuilderExample.MyInterface ist lang. Dies ist nicht kompatibel mit dem Rückgabetyp des Deskriptors: String

während der Verwendung der Methode with() nicht.

vollständiges Beispiel:

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

Erweitertes Beispiel

Das folgende Beispiel zeigt das unterschiedliche Verhalten von Methoden- und Typparametern, die auf einen Lieferanten reduziert wurden. Außerdem wird der Unterschied zu einem Consumer-Verhalten für einen Typparameter angezeigt. Und es zeigt, dass es keinen Unterschied macht, ob es sich um einen Verbraucher oder einen Lieferanten für einen Methodenparameter handelt.

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
Wegen der Folgerung mit letzterem. Obwohl beide auf dem Anwendungsfall basieren, den man implementieren muss. Für Sie könnte das erstere streng und gut sein. Aus Gründen der Flexibilität kann jemand anderes Letzteres bevorzugen.
Naman

Versuchen Sie dies in Eclipse zu kompilieren? Wenn Sie nach Fehlerzeichenfolgen des von Ihnen eingefügten Formats suchen, handelt es sich um einen Eclipse (ecj) -spezifischen Fehler. Haben Sie das gleiche Problem beim Kompilieren mit Raw javacoder einem Build-Tool wie Gradle oder Maven?
user31601

@ user31601 Ich habe ein vollständiges Beispiel mit Javac-Ausgabe hinzugefügt. Die Fehlermeldungen sind wenig unterschiedlich formatiert, aber Eclipse und Javac haben immer noch das gleiche Verhalten
Jukzi

Antworten:


12

Das ist eine wirklich interessante Frage. Ich fürchte, die Antwort ist kompliziert.

tl; dr

Das Herausarbeiten des Unterschieds erfordert ein ziemlich gründliches Lesen der Typinferenzspezifikation von Java, läuft jedoch im Wesentlichen auf Folgendes hinaus:

  • Wenn alle anderen Dinge gleich sind, leitet der Compiler den spezifischsten Typ ab, den er kann.
  • Wenn jedoch eine Substitution für einen Typparameter gefunden werden kann, der alle Anforderungen erfüllt, ist die Kompilierung erfolgreich , auch wenn die Substitution vage ist.
  • Denn withes gibt eine (zugegebenermaßen vage) Substitution, die alle Anforderungen erfüllt an R:Serializable
  • Denn withXdie Einführung des zusätzlichen Typparameters Fzwingt den Compiler, Rzuerst aufzulösen , ohne die Einschränkung zu berücksichtigen F extends Function<T,R>. Rlöst sich auf das (viel spezifischere) auf, Stringwas dann bedeutet, dass die Folgerung von Ffehlschlägt.

Dieser letzte Punkt ist der wichtigste, aber auch der handgewellteste. Ich kann mir keine präzisere Formulierung vorstellen. Wenn Sie also mehr Details wünschen, empfehlen wir Ihnen, die vollständige Erklärung unten zu lesen.

Ist das beabsichtigtes Verhalten?

Ich werde hier auf die Beine gehen und nein sagen .

Ich schlage nicht , dass es einen Fehler in der Spezifikation, mehr , dass (im Falle withX) die Sprache Designer ihre Hände gelegt haben und sagte : „Es gibt einige Situationen , in denen Typinferenz zu hart wird, so werden wir nur scheitern“ . Auch wenn das Verhalten des Compilers in Bezug auf withXdas zu sein scheint, was Sie wollen, würde ich dies eher als zufälligen Nebeneffekt der aktuellen Spezifikation als als eine positiv beabsichtigte Entwurfsentscheidung betrachten.

Dies ist wichtig, da es die Frage informiert. Soll ich mich bei meinem Anwendungsdesign auf dieses Verhalten verlassen? Ich würde argumentieren, dass Sie dies nicht tun sollten, da Sie nicht garantieren können, dass sich zukünftige Versionen der Sprache weiterhin so verhalten.

Zwar bemühen sich Sprachdesigner sehr, vorhandene Anwendungen nicht zu beschädigen, wenn sie ihre Spezifikation / ihr Design / ihren Compiler aktualisieren. Das Problem besteht jedoch darin, dass das Verhalten, auf das Sie sich verlassen möchten, ein Verhalten ist, bei dem der Compiler derzeit ausfällt (dh keine vorhandene Anwendung ). Langauge-Updates verwandeln nicht kompilierten Code ständig in Kompilierungscode. Zum Beispiel könnte der folgende Code wird garantiert nicht in Java 7, zu kompilieren , aber würde in Java 8 kompilieren:

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

Ihr Anwendungsfall ist nicht anders.

Ein weiterer Grund, warum ich bei der Verwendung Ihrer withXMethode vorsichtig sein würde, ist der FParameter selbst. Im Allgemeinen ist ein generischer Typparameter für eine Methode vorhanden (der nicht im Rückgabetyp enthalten ist), um die Typen mehrerer Teile der Signatur miteinander zu verbinden. Es heißt:

Es ist mir egal, was Tist, aber ich möchte sicher sein, dass es überall dort, wo ich es benutze T, der gleiche Typ ist.

Logischerweise würden wir erwarten, dass jeder Typparameter mindestens zweimal in einer Methodensignatur erscheint, andernfalls "macht er nichts". Fin Ihrer withXerscheint nur einmal in der Signatur, was mir die Verwendung eines Typparameters nahe legt, der nicht mit der Absicht dieser Funktion der Sprache übereinstimmt.

Eine alternative Implementierung

Eine Möglichkeit, dies in einem etwas "beabsichtigteren Verhalten" zu implementieren, besteht darin, Ihre withMethode in eine Kette von 2 aufzuteilen :

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

}

Dies kann dann wie folgt verwendet werden:

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

Dies beinhaltet keinen fremden Typparameter wie Ihren withX. Indem Sie die Methode in zwei Signaturen aufteilen, drückt sie auch die Absicht dessen, was Sie versuchen, unter dem Gesichtspunkt der Typensicherheit besser aus:

  • Die erste Methode richtet eine Klasse ( With) ein, die den Typ basierend auf der Methodenreferenz definiert .
  • Die scond-Methode ( of) beschränkt den Typ des so value, dass er mit dem kompatibel ist, was Sie zuvor eingerichtet haben.

Die einzige Möglichkeit, wie eine zukünftige Version der Sprache dies kompilieren kann, besteht darin, die vollständige Enten-Typisierung zu implementieren, was unwahrscheinlich erscheint.

Ein letzter Hinweis, um diese ganze Sache irrelevant zu machen: Ich denke, Mockito (und insbesondere seine Stubbing-Funktionalität) könnte im Grunde schon das tun, was Sie mit Ihrem "typsicheren generischen Builder" erreichen wollen. Vielleicht könnten Sie das stattdessen einfach verwenden?

Die vollständige (ish) Erklärung

Ich werde das Typinferenzverfahren für withund durcharbeitenwithX . Das ist ziemlich lang, also nimm es langsam. Obwohl ich lang bin, habe ich immer noch viele Details ausgelassen. Weitere Informationen finden Sie in der Spezifikation (folgen Sie den Links), um sich davon zu überzeugen, dass ich Recht habe (möglicherweise habe ich einen Fehler gemacht).

Um die Dinge ein wenig zu vereinfachen, werde ich ein minimaleres Codebeispiel verwenden. Der wesentliche Unterschied besteht darin , dass es auslagert Functionfür Supplier, so gibt es wenige Typen und Parameter im Spiel. Hier ist ein vollständiger Ausschnitt, der das von Ihnen beschriebene Verhalten reproduziert:

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
    }

}

Lassen Sie uns nacheinander die Typanwendbarkeitsinferenz und die Typinferenzprozedur für jeden Methodenaufruf durcharbeiten:

with

Wir haben:

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

Die anfängliche gebundene Menge B 0 ist:

  • R <: Object

Alle Parameterausdrücke sind für die Anwendbarkeit relevant .

Daher ist der Anfangs - Zwang Satz für die Anwendbarkeit Inferenz , C ist:

  • TypeInference::getLong ist kompatibel mit Supplier<R>
  • "Not a long" ist kompatibel mit R

Dies reduziert sich auf die gebundene Menge B 2 von:

  • R <: Object(von B 0 )
  • Long <: R (von der ersten Einschränkung)
  • String <: R (aus der zweiten Einschränkung)

Da dies nicht die gebundene ' falsche ' und (ich nehme an) Auflösung von RErfolg (Geben Serializable) enthält, ist der Aufruf anwendbar.

Also fahren wir mit der Inferenz des Aufruftyps fort .

Der neue Einschränkungssatz C mit zugehörigen Eingabe- und Ausgabevariablen lautet:

  • TypeInference::getLong ist kompatibel mit Supplier<R>
    • Eingangsvariablen: keine
    • Ausgabevariablen: R

Dies enthält keine Abhängigkeiten zwischen Eingabe- und Ausgabevariablen , kann also in einem einzigen Schritt reduziert werden , und die endgültige gebundene Menge B 4 ist dieselbe wie B 2 . Daher ist die Auflösung nach wie vor erfolgreich und der Compiler atmet erleichtert auf!

withX

Wir haben:

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

Die anfängliche gebundene Menge B 0 ist:

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

Nur der zweite Parameterausdruck ist für die Anwendbarkeit relevant . Das erste ( TypeInference::getLong) ist nicht, weil es die folgende Bedingung erfüllt:

Wenn mes sich um eine generische Methode handelt und der Methodenaufruf keine expliziten Typargumente, einen explizit typisierten Lambda-Ausdruck oder einen genauen Methodenreferenzausdruck enthält, für den der entsprechende Zieltyp (abgeleitet aus der Signatur von m) ein Typparameter von ist m.

Daher ist der Anfangs - Zwang Satz für die Anwendbarkeit Inferenz , C ist:

  • "Also not a long" ist kompatibel mit R

Dies reduziert sich auf die gebundene Menge B 2 von:

  • R <: Object(von B 0 )
  • F <: Supplier<R>(von B 0 )
  • String <: R (von der Einschränkung)

Da dies wiederum nicht die gebundene ' falsche ' und die Auflösung von RErfolg (Geben String) enthält, ist der Aufruf anwendbar.

Inferenz des Aufruftyps noch einmal ...

Diesmal lautet der neue Einschränkungssatz C mit den zugehörigen Eingabe- und Ausgabevariablen :

  • TypeInference::getLong ist kompatibel mit F
    • Eingabevariablen: F
    • Ausgabevariablen: keine

Auch hier bestehen keine Abhängigkeiten zwischen Eingabe- und Ausgabevariablen . Diesmal gibt es jedoch eine Eingabevariable ( F), daher müssen wir diese beheben, bevor wir versuchen, sie zu reduzieren . Wir beginnen also mit unserer gebundenen Menge B 2 .

  1. Wir bestimmen eine Teilmenge Vwie folgt:

    Bei einer Menge von Inferenzvariablen, die aufgelöst werden sollen, sei Vdie Vereinigung dieser Menge und aller Variablen, von denen die Auflösung mindestens einer Variablen in dieser Menge abhängt.

    Durch die zweite Grenze in B 2F hängt die Auflösung von Ralso ab V := {F, R}.

  2. Wir wählen eine Teilmenge von Vgemäß der Regel:

    Sei { α1, ..., αn }eine nicht leere Teilmenge von nicht fundierten Variablen, Vso dass i) für alle i (1 ≤ i ≤ n), wenn dies αivon der Auflösung einer Variablen abhängt β, entweder βeine Instanziierung hat oder es eine jsolche gibt, die β = αj; und ii) es gibt keine nicht leere richtige Teilmenge von { α1, ..., αn }mit dieser Eigenschaft.

    Die einzige Teilmenge Vdavon erfüllt diese Eigenschaft ist {R}.

  3. Mit der dritten Grenze ( String <: R) instanziieren wir diese R = Stringund integrieren sie in unsere gebundene Menge. Rist nun aufgelöst und die zweite Grenze wird effektiv F <: Supplier<String>.

  4. Mit der (überarbeiteten) zweiten Grenze instanziieren wir F = Supplier<String>. Fist jetzt gelöst.

Nachdem dies Fbehoben ist, können wir mit der Reduzierung fortfahren , indem wir die neue Einschränkung verwenden:

  1. TypeInference::getLong ist kompatibel mit Supplier<String>
  2. ... reduziert auf Long ist kompatibel mit String
  3. ... was sich auf false reduziert

... und wir bekommen einen Compilerfehler!


Zusätzliche Hinweise zum 'Extended Example'

Das erweiterte Beispiel in der Frage befasst sich mit einigen interessanten Fällen, die von den obigen Arbeiten nicht direkt abgedeckt werden:

  • Wobei der Werttyp ein Subtyp der Methode return type ( Integer <: Number) ist
  • Wenn die funktionale Schnittstelle im abgeleiteten Typ kontravariant ist (dh Consumernicht Supplier)

Insbesondere 3 der angegebenen Aufrufe weisen möglicherweise auf ein anderes Compilerverhalten hin als in den Erläuterungen beschrieben:

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

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

Der zweite dieser 3 wird genau Prozess des gleiche Folgerung durchlaufen wie withXoben (nur ersetzen Longmit NumberundString mit Integer). Dies zeigt einen weiteren Grund, warum Sie sich bei Ihrem Klassendesign nicht auf dieses fehlgeschlagene Typinferenzverhalten verlassen sollten, da das Nichtkompilieren hier wahrscheinlich kein wünschenswertes Verhalten ist.

Für die anderen 2 (und in der Tat alle anderen Aufrufe, die a Consumer Sie arbeiten möchten) sollte das Verhalten offensichtlich sein, wenn Sie das für eine der oben genannten Methoden beschriebene Typinferenzverfahren durcharbeiten (dh withfür die erste, withXfür die dritte). Es gibt nur eine kleine Änderung, die Sie beachten müssen:

  • Die Einschränkung für den ersten Parameter ( t::setNumber ist kompatibel mit Consumer<R> ) wird auf statt reduziertR <: NumberNumber <: R wie fürSupplier<R> . Dies ist in der verknüpften Dokumentation zur Reduzierung beschrieben.

Ich überlasse es dem Leser als Übung, eines der oben genannten Verfahren, das mit diesem zusätzlichen Wissen ausgestattet ist, sorgfältig durchzuarbeiten, um sich selbst genau zu demonstrieren, warum ein bestimmter Aufruf kompiliert wird oder nicht.


Sehr gründlich, gut recherchiert und formuliert. Vielen Dank!
Zabuzard

@ user31601 Können Sie bitte darauf hinweisen, wo der Unterschied zwischen Lieferant und Verbraucher ins Spiel kommt? Ich habe dazu in der ursprünglichen Frage ein erweitertes Beispiel hinzugefügt. Es zeigt kovariantes, kontravariantes und invariantes Verhalten für die verschiedenen Versionen von letBe (), letBeX () und let (). Be (), abhängig vom Lieferanten / Verbraucher.
Jukzi

@jukzi Ich habe ein paar zusätzliche Notizen hinzugefügt, aber Sie sollten genügend Informationen haben, um diese neuen Beispiele selbst durchzuarbeiten.
user31601

Das ist interessant: so viele Sonderfälle in 18.2.1. für Lambdas und Methodenreferenzen, bei denen ich nach meinem naiven Verständnis überhaupt keinen Sonderfall für sie erwartet hätte. Und wahrscheinlich würde kein gewöhnlicher Entwickler damit rechnen.
Jukzi

Nun, ich denke, der Grund ist, dass der Compiler bei Lambdas und Methodenreferenzen entscheiden muss, welchen richtigen Typ das Lambda implementieren soll - er muss eine Wahl treffen! Zum Beispiel TypeInference::getLongkönnte imlement Supplier<Long>oder Supplier<Serializable>oder Supplier<Number>etc, aber entscheidend ist, dass es nur eine von ihnen implementieren kann (genau wie jede andere Klasse)! Dies unterscheidet sich von allen anderen Ausdrücken, bei denen die implementierten Typen alle im Voraus bekannt sind und der Compiler nur herausfinden muss, ob einer von ihnen die Einschränkungsanforderungen erfüllt.
user31601
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.