Warum erfordert negate () eine explizite Umwandlung in Predicate?

8

Ich habe eine Liste von Namen. In Zeile 3 musste ich das Ergebnis des Lambda-Ausdrucks auf setzen Predicate<String>. Das Buch, das ich lese, erklärt, dass die Besetzung notwendig ist, um dem Compiler zu helfen, die passende Funktionsschnittstelle zu bestimmen.

Ich brauche jedoch keine solche Besetzung in der folgenden Zeile, weil ich nicht anrufe negate(). Wie macht das einen Unterschied? Ich verstehe, dass negate()hier zurückkehrt Predicate<String>, aber macht der vorhergehende Lambda-Ausdruck nicht dasselbe?

List<String> names = new ArrayList<>();
//add various names here
names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required
names.removeIf(((str -> str.length() <= 5))); //compiles without cast
K Mann
quelle
1
... die Besetzung ist notwendig, um dem Compiler zu helfen, die passende Funktionsschnittstelle zu bestimmen. antwortet trotzdem. Der Grund, warum die nächste Zeile dies nicht benötigt, ist, dass die Inferenz viel einfacher ist und nicht von einem Methodenaufruf auf der abgeleiteten Schnittstelle gefolgt wird. Ich habe gelernt, Lambda-Funktionen mit Generika zu verwenden, dass es in solchen Fällen hauptsächlich um Inferenz geht und explizite Deklarationen sehr sicher sind, wie zum BeispielPredicate<String> predicate = str -> str.length() <= 5; names.removeIf(predicate.negate()); names.removeIf(predicate);
Naman
@ Nathan Sollte der Aufruf von negate () nicht noch mehr Kontext bieten, was die explizite Besetzung noch weniger notwendig macht? Der Lambda-Ausdruck hätte sich schließlich auf eine Funktion anstelle eines Prädikats beziehen können, aber der nachfolgende Aufruf von negate () beseitigt jeglichen Raum für Mehrdeutigkeiten.
K Man
Aber was wäre das negate(), wenn es nicht in der Lage wäre, auf den Typ des Lambda-Attributs zu schließen, das Teil des Ausdrucks ist, und somit nicht feststellen kann, dass der Ausdruck a bedeutet Predicate<String>.
Naman
Ich sage nicht, dass negate () notwendig ist. Meine letzte Zeile hat eindeutig ohne funktioniert. Ich sehe jedoch nicht, wie der Aufruf von negate () den Compiler plötzlich hinsichtlich der Art der Funktionsschnittstelle verwirren würde.
K Man

Antworten:

8

Es liegt nicht nur daran, dass Sie anrufennegate() . Schauen Sie sich diese Version an, die Ihrer sehr nahe kommt, aber kompiliert:

Predicate<String> predicate = str -> str.length() <= 5;
names.removeIf(predicate.negate());

Der Unterschied zwischen dieser und Ihrer Version? Es geht darum, wie Lambda-Ausdrücke ihre Typen erhalten (der "Zieltyp").

Was glaubst du, macht das?

(str -> str.length() <= 5).negate()?

Ihre aktuelle Antwort lautet: "Es ruft negate()das Predicate<String>durch den Ausdruck Gegebene auf str -> str.length() <= 5". Recht? Aber das ist nur so, weil du es so gemeint hast. Der Compiler weiß das nicht . Warum? Weil es alles sein könnte. Meine eigene Antwort auf die obige Frage könnte lauten: "Es ruft negatemeinen anderen funktionalen Schnittstellentyp auf ... (ja, das Beispiel wird etwas bizarr sein)

interface SentenceExpression {
    boolean checkGrammar();
    default SentenceExpression negate() {
        return ArtificialIntelligence.contradict(explainSentence());
    };
}

Ich konnte den gleichen Lambda - Ausdruck verwenden , names.removeIf((str -> str.length() <= 5).negate());aber was bedeuten , str -> str.length() <= 5eine zu sein , SentenceExpressionanstatt ein Predicate<String>.

Erklärung: (str -> str.length() <= 5).negate()macht nicht str -> str.length() <= 5a Predicate<String>. Und deshalb habe ich gesagt, es könnte alles sein, einschließlich meiner Funktionsschnittstelle oben.

Zurück zu Java ... Aus diesem Grund haben Lambda-Ausdrücke das Konzept des "Zieltyps", das die Mechanik definiert, unter der ein Lambda-Ausdruck vom Compiler als einen bestimmten funktionalen Schnittstellentyp verstanden wird (dh wie Sie dem Compiler helfen, dies zu wissen dass der Ausdruck Predicate<String>eher ein als SentenceExpressionoder irgendetwas anderes ist, was es sein könnte). Vielleicht finden Sie es nützlich, durchzulesen. Was ist unter Lambda-Zieltyp und Zieltypkontext in Java zu verstehen? und Java 8: Zieltypisierung

Einer der Kontexte, in denen Zieltypen abgeleitet werden (wenn Sie die Antworten in diesen Beiträgen lesen), ist der Aufrufkontext, in dem Sie einen Lambda-Ausdruck als Argument für einen Parameter eines funktionalen Schnittstellentyps übergeben names.removeIf(((str -> str.length() <= 5)));: Es ist nur der Lambda-Ausdruck, der als Argument für eine Methode angegeben wird, die a verwendet Predicate<String>. Dies gilt nicht für die Anweisung, die nicht kompiliert wird.

Also mit anderen Worten ...

names.removeIf(str -> str.length() <= 5);verwendet einen Lambda-Ausdruck an einer Stelle, an der der Argumenttyp klar definiert, wie der Typ des Lambda-Ausdrucks erwartet wird (dh der Zieltyp von str -> str.length() <= 5ist eindeutig Predicate<String>).

Ist (str -> str.length() <= 5).negate()jedoch kein Lambda-Ausdruck, sondern nur ein Ausdruck, der zufällig einen Lambda-Ausdruck verwendet. Dies bedeutet, dass sich str -> str.length() <= 5in diesem Fall nicht der Aufrufkontext befindet, der den Zieltyp des Lambda-Ausdrucks bestimmt (wie im Fall Ihrer letzten Anweisung). Ja, der Compiler weiß, dass a removeIfbenötigt wird Predicate<String>, und er weiß mit Sicherheit, dass der gesamte an die Methode übergebene Ausdruck a sein muss Predicate<String>, aber er würde nicht annehmen, dass ein Lambda-Ausdruck im Argument expression a ist Predicate<String>(selbst wenn Sie ihn behandeln) als Prädikat, indem man negate()es aufruft ; es könnte alles gewesen sein, was mit dem Lambda-Ausdruck kompatibel ist).

Aus diesem Grund müssen Sie Ihr Lambda mit einer expliziten Besetzung eingeben (oder anders, wie im ersten Gegenbeispiel, das ich gegeben habe).

ernest_k
quelle
names.removeIf (((str -> str.length () <= 5))); klar ohne Probleme kompiliert. Ich verstehe immer noch nicht, wie der Aufruf von negate (), der ein Prädikat zurückgibt, den Compiler plötzlich bezweifeln lässt, dass der gesamte Ausdruck auf ein Prädikat verweist.
K Man
@KMan Ich denke, Sie brauchen mehr Details darüber, wie Lambda-Ausdrücke eingegeben werden. Ich werde einen Link bearbeiten und hinzufügen.
ernest_k
Ich habe schon einiges über Lambda-Ausdrücke gelesen. Ob der gesamte Ausdruck ein Lambda-Ausdruck ist oder nicht, spielt keine Rolle, nicht wahr? Gibt negate () in meinem obigen Code kein Prädikat <String> zurück? Ich könnte mich irren, aber ich glaube nicht, dass ich es bin.
K Man
1
@KMan Gibt negate () in meinem obigen Code kein Prädikat <String> zurück? Nein, erst, wenn es aufgerufen wurdePredicate<String> , worauf der Compiler aufgrund der Verwendung von sich negateselbst, wie in dieser Antwort erläutert, überhaupt nicht schließen konnte .
Naman
@KMan Ich habe bearbeitet - ich denke, Sie müssen die Antwort erneut lesen. Kurz gesagt, wie Naman sagt, kocht es auf das Verständnis , dass (str -> str.length() <= 5).negate()nicht machen str -> str.length() <= 5ein Predicate<String>, auch wenn das, was Sie beabsichtigen.
ernest_k
4

Ich weiß nicht, warum das so verwirrend sein muss. Dies kann mit 2 Gründen erklärt werden, IMO.

  • Lambda-Ausdrücke sind Poly- Ausdrücke.

Ich werde Sie herausfinden lassen, was dies bedeutet und was die JLSWörter um es herum sind. Aber im Wesentlichen sind dies wie Generika:

static class Me<T> {
    T t...
}

Was ist der Typ Thier? Es hängt davon ab. Wenn Sie tun :

Me<Integer> me = new Me<>(); // it's Integer
Me<String>  m2 = new Me<>(); // it's String

Poly-Ausdrücke sollen vom Kontext abhängen, in dem sie verwendet werden. Lambda-Ausdrücke sind gleich. Nehmen wir den Lambda-Ausdruck hier isoliert :

(String str) -> str.length() <= 5

Wenn Sie es betrachten, was ist das? Nun, es ist ein Predicate<String>? Aber kann A sein Function<String, Boolean>? Oder kann sogar sein MyTransformer<String, Boolean>, wo:

 interface MyTransformer<String, Boolean> {
     Boolean transform(String in){
         // do something here with "in"
     } 
 } 

Die Auswahlmöglichkeiten sind endlos.

  • Theoretisch könnte ein .negate()direkter Aufruf eine Option sein.

Ab 10_000 Meilen oben sind Sie richtig: Sie stellen dies str -> str.length() <= 5einer removeIfMethode zur Verfügung, die nur a akzeptiert Predicate. Es gibt keine weiteren removeIfMethoden, daher sollte der Compiler in der Lage sein, "das Richtige zu tun", wenn Sie diese bereitstellen (str -> str.length() <= 5).negate().

Wie kommt es, dass dies nicht funktioniert? Beginnen wir mit Ihrem Kommentar:

Sollte der Aufruf von negate () nicht noch mehr Kontext bieten und die explizite Besetzung noch weniger notwendig machen?

Es scheint, dass hier das Hauptproblem beginnt. So javacfunktioniert es einfach nicht . Es kann nicht das Ganze nehmen str -> str.length() <= 5).negate(), sich selbst sagen, dass dies ein ist Predicate<String>(da Sie es als Argument verwenden removeIf) und dann den Teil ohne weiter zerlegen .negate()und sehen, ob das Predicate<String>auch ein ist. javacUmgekehrt muss es das Ziel kennen , um feststellen zu können, ob es legal ist anzurufen negateoder nicht.

Außerdem müssen Sie im Allgemeinen klar zwischen Polyausdrücken und Ausdrücken unterscheiden. str -> str.length() <= 5).negate()ist ein Ausdruck, str -> str.length() <= 5ist ein Polyausdruck.

Es könnte Sprachen geben, in denen Dinge anders gemacht werden und in denen dies möglich ist, javacist einfach nicht dieser Typ.

Eugene
quelle
1

Im folgenden Beispiel

      names.removeIf(str -> str.length() <= 5); //compiles without cast

Der Ausdruck gibt true zurück. Wenn im folgenden Beispiel ohne die Besetzung, trueweiß nichts über die Methodenegate()

Auf der anderen Seite,

   names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required

Der Ausdruck wird umgewandelt Predicate<String>, um dem Compiler mitzuteilen, wo sich die Methode befindet negate. Dann ist es die Methode negate(), die die Auswertung tatsächlich wie folgt durchführt:

   (s)->!test(s) where s is the string argument

Beachten Sie, dass Sie ohne die Besetzung wie folgt dasselbe Ergebnis erzielen können:

    names.removeIf(str->!str.length <= 5) 
      // or
    name.removeIf(str->str.length > 5)
WJS
quelle
1

Ohne zu tief in die Dinge einzusteigen, ist das Fazit, dass das Lambda str -> str.length() <= 5nicht unbedingt ein ist, Predicate<String>wie Eugene erklärte.

Das heißt, negate()ist eine Mitgliedsfunktion von Predicate<T>und der Compiler kann den Aufruf nicht verwenden, um negate()auf den Typ zu schließen, als den das Lambda interpretiert werden soll. Selbst wenn es versucht würde, könnte es Probleme geben, denn wenn mehrere mögliche Klassen negate()Funktionen hätten, würde es nicht wissen, welche es wählen soll.

Stellen Sie sich vor, Sie sind der Compiler.

  • Sie sehen str -> str.length() <= 5. Sie wissen, dass es Lambda ist, das einen String nimmt und einen Booleschen Wert zurückgibt, wissen aber nicht genau, welchen Typ er darstellt.
  • Als nächstes sehen Sie die Mitgliedsreferenz, .negate()aber weil Sie den Typ des Ausdrucks links vom "." Nicht kennen. Sie müssen einen Fehler melden, da Sie den Aufruf nicht zum Negieren verwenden können, um auf den Typ schließen zu können.

Wäre negate als statische Funktion implementiert worden, würden die Dinge funktionieren. Zum Beispiel:

public class PredicateUtils {
    public static <T> Predicate<T> negate(Predicate<T> p) {
        return p.negate();
}

würde es Ihnen ermöglichen zu schreiben

names.removeIf(PredicateUtils.negate(str -> str.length() <= 5)); //compiles without cast

Da die Wahl der Negationsfunktion eindeutig ist, weiß der Compiler, dass er str -> str.length() <= 5als a interpretiert werden muss, Predicate<T>und kann den Typ richtig erzwingen.

Ich hoffe das hilft.

ajz
quelle