Die im Lambda-Ausdruck verwendete Variable sollte endgültig oder effektiv endgültig sein

134

Die im Lambda-Ausdruck verwendete Variable sollte endgültig oder effektiv endgültig sein

Wenn ich versuche, calTzes zu verwenden , wird dieser Fehler angezeigt.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
user3610470
quelle
5
Sie können nicht calTzvom Lambda ändern .
Elliott Frisch
2
Ich nahm an, dass dies eines der Dinge war, die für Java 8 nicht rechtzeitig erledigt wurden. Aber Java 8 war 2014. Scala und Kotlin haben dies jahrelang zugelassen, also ist es offensichtlich möglich. Plant Java jemals, diese seltsame Einschränkung zu beseitigen?
GlenPeterson
5
Hier ist der aktualisierte Link zu @MSDoustis Kommentar.
geisterfurz007
Ich denke, Sie könnten Completable Futures als Workaround verwenden.
Kraulain
Eine wichtige Sache, die ich beobachtet habe - Sie können statische Variablen anstelle von normalen Variablen verwenden (dies macht es effektiv endgültig, denke ich)
kaushalpranav

Antworten:

68

Eine finalVariable bedeutet, dass sie nur einmal instanziiert werden kann. In Java können Sie keine nicht endgültigen Variablen sowohl in Lambda als auch in anonymen inneren Klassen verwenden.

Sie können Ihren Code mit der alten for-each-Schleife umgestalten:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

Auch wenn ich einige Teile dieses Codes nicht verstehe:

  • Sie rufen a auf, v.getTimeZoneId();ohne seinen Rückgabewert zu verwenden
  • Mit der Zuweisung calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());ändern Sie die ursprünglich übergebene calTznicht und verwenden sie in dieser Methode nicht
  • Sie kehren immer zurück null, warum setzen Sie nicht voidals Rückgabetyp?

Hoffe auch diese Tipps helfen dir, dich zu verbessern.

Francesco Pitzalis
quelle
Wir können nicht endgültige statische Variablen verwenden
Narendra Jaggi
92

Obwohl andere Antworten die Anforderung belegen, erklären sie nicht, warum die Anforderung besteht.

Die JLS erwähnt in §15.27.2 , warum :

Die Beschränkung auf effektiv endgültige Variablen verbietet den Zugriff auf sich dynamisch ändernde lokale Variablen, deren Erfassung wahrscheinlich zu Parallelitätsproblemen führen würde.

Um das Risiko von Fehlern zu verringern, haben sie beschlossen, sicherzustellen, dass erfasste Variablen niemals mutiert werden.

Dioxin
quelle
10
Gute Antwort +1, und ich bin überrascht, wie wenig Berichterstattung der Grund für das effektive Finale zu bekommen scheint. Bemerkenswert: Eine lokale Variable kann nur von einem Lambda erfasst werden, wenn sie auch definitiv vor dem Lambda-Körper zugewiesen ist. Beide Anforderungen scheinen sicherzustellen, dass der Zugriff auf die lokale Variable threadsicher ist.
Tim Biegeleisen
2
Gibt es eine Idee, warum dies nur auf lokale Variablen und nicht auf Klassenmitglieder beschränkt ist? Ich finde mich oft dabei, das Problem zu umgehen, indem ich meine Variable als Klassenmitglied deklariere ...
David Refaeli
4
@ DavidRefaeli Klassenmitglieder sind vom Speichermodell abgedeckt / betroffen, das, wenn es befolgt wird, vorhersagbare Ergebnisse liefert, wenn es gemeinsam genutzt wird. Lokale Variablen sind nicht, wie in §17.4.1
Dioxin
Dies ist ein dummer Hack, der entfernt werden sollte. Der Compiler sollte vor einem möglichen Thread-übergreifenden Variablenzugriff warnen, diesen jedoch zulassen. Oder sollte klug genug sein, um zu wissen, ob Ihr Lambda auf demselben Thread läuft oder parallel läuft usw. Dies ist eine dumme Einschränkung, die mich traurig macht. Und wie andere bereits erwähnt haben, gibt es die Probleme beispielsweise in C # nicht.
Josh M.
@ JoshM. Mit C # können Sie auch veränderbare Werttypen erstellen , die vermieden werden sollten, um Probleme zu vermeiden. Anstatt solche Prinzipien zu haben, beschloss Java, dies vollständig zu verhindern. Es reduziert Benutzerfehler zum Preis der Flexibilität. Ich bin mit dieser Einschränkung nicht einverstanden, aber sie ist gerechtfertigt. Die Berücksichtigung der Parallelität würde zusätzliche Arbeit am Ende des Compilers erfordern, weshalb wahrscheinlich der Weg des " Warnens über Cross-Threaded-Zugriff " nicht eingeschlagen wurde. Ein Entwickler, der an der Spezifikation arbeitet, wäre wahrscheinlich unsere einzige Bestätigung dafür.
Dioxin
57

Von einem Lambda kann man keinen Verweis auf etwas bekommen, das nicht endgültig ist. Sie müssen einen endgültigen Wrapper von außerhalb der Lamda deklarieren, um Ihre Variable zu halten.

Ich habe das endgültige 'Referenz'-Objekt als diesen Wrapper hinzugefügt.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   
DMozzy
quelle
Ich habe über den gleichen oder einen ähnlichen Ansatz nachgedacht - aber ich würde gerne Expertenratschläge / Feedback zu dieser Antwort erhalten?
YoYo
4
Dieser Code verfehlt eine Initiale reference.set(calTz);oder die Referenz muss mit erstellt werden new AtomicReference<>(calTz), da sonst die als Parameter angegebene Zeitzone ungleich Null verloren geht.
Julien Kronegg
7
Dies sollte die erste Antwort sein. Eine AtomicReference (oder eine ähnliche Atomic___- Klasse) umgeht diese Einschränkung unter allen möglichen Umständen sicher.
GlenPeterson
1
Einverstanden sollte dies die akzeptierte Antwort sein. Die anderen Antworten geben nützliche Informationen darüber, wie Sie auf ein nicht funktionierendes Programmiermodell zurückgreifen können und warum dies getan wurde, aber sagen Sie Ihnen nicht, wie Sie das Problem umgehen können!
Jonathan Benn
2
@GlenPeterson und ist auch eine schreckliche Entscheidung. Auf diese Weise ist es nicht nur viel langsamer, sondern Sie ignorieren auch die in der Dokumentation vorgeschriebene Eigenschaft der Nebenwirkungen.
Eugene
41

Java 8 hat ein neues Konzept namens "Effektiv endgültig". Dies bedeutet, dass eine nicht endgültige lokale Variable, deren Wert sich nach der Initialisierung nie ändert, als „effektiv endgültig“ bezeichnet wird.

Dieses Konzept wurde eingeführt, weil wir vor Java 8 keine nicht endgültige lokale Variable in einer anonymen Klasse verwenden konnten . Wenn Sie Zugriff auf eine lokale Variable in einer anonymen Klasse haben möchten , müssen Sie diese endgültig festlegen.

Mit der Einführung von Lambda wurde diese Einschränkung gelockert. Daher ist die Notwendigkeit, die lokale Variable endgültig zu machen, wenn sie nach der Initialisierung als Lambda an sich nicht geändert wird, nichts anderes als eine anonyme Klasse.

Java 8 erkannte den Schmerz, jedes Mal, wenn ein Entwickler Lambda verwendete, lokale Variablen als endgültig zu deklarieren, führte dieses Konzept ein und machte es unnötig, lokale Variablen endgültig zu machen. Wenn Sie also sehen, dass sich die Regel für anonyme Klassen nicht geändert hat, müssen Sie das finalSchlüsselwort nicht jedes Mal schreiben, wenn Sie Lambdas verwenden.

Ich habe hier eine gute Erklärung gefunden

Dinesh Arora
quelle
Die Code-Formatierung sollte nur für Code verwendet werden , nicht für technische Begriffe im Allgemeinen. effectively finalist kein Code, sondern eine Terminologie. Siehe Wann sollte die Code-Formatierung für Nicht-Code-Text verwendet werden? auf Meta Stack Overflow .
Charles Duffy
("Das finalSchlüsselwort" ist also ein Codewort, dessen Formatierung korrekt ist. Wenn Sie jedoch "final" beschreibend und nicht als Code verwenden, wird stattdessen die Terminologie verwendet.)
Charles Duffy
9

In Ihrem Beispiel können Sie das forEachdurch lamdba durch eine einfache forSchleife ersetzen und jede Variable frei ändern. Oder überarbeiten Sie Ihren Code wahrscheinlich so, dass Sie keine Variablen ändern müssen. Der Vollständigkeit halber erkläre ich jedoch, was der Fehler bedeutet und wie man ihn umgeht.

Java 8-Sprachspezifikation, §15.27.2 :

Alle lokalen Variablen, formalen Parameter oder Ausnahmeparameter, die in einem Lambda-Ausdruck verwendet, aber nicht deklariert werden, müssen entweder als endgültig oder effektiv endgültig deklariert werden ( §4.12.4 ). Andernfalls tritt ein Kompilierungsfehler auf, wenn die Verwendung versucht wird.

Grundsätzlich können Sie eine lokale Variable ( calTzin diesem Fall) nicht innerhalb eines Lambda (oder einer lokalen / anonymen Klasse) ändern . Um dies in Java zu erreichen, müssen Sie ein veränderbares Objekt verwenden und es (über eine letzte Variable) aus dem Lambda ändern. Ein Beispiel für ein veränderliches Objekt wäre hier ein Array aus einem Element:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}
Alexander Udalov
quelle
Eine andere Möglichkeit besteht darin, ein Feld eines Objekts zu verwenden. ZB MyObj result = new MyObj (); ...; result.timeZone = ...; ....; return result.timezone; Beachten Sie jedoch, dass Sie, wie oben erläutert, Probleme mit der Thread-Sicherheit haben. Siehe stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

Wenn es nicht erforderlich ist, die Variable zu ändern, besteht eine allgemeine Problemumgehung für diese Art von Problem darin, den Teil des Codes zu extrahieren, der Lambda verwendet, und das Schlüsselwort final für method-parameter zu verwenden.

robie2011
quelle
0

Eine im Lambda-Ausdruck verwendete Variable sollte endgültig oder effektiv endgültig sein. Sie können jedoch einem endgültigen Array mit einem Element einen Wert zuweisen.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
Andreas Foteas
quelle
Dies ist dem Vorschlag von Alexander Udalov sehr ähnlich. Abgesehen davon denke ich, dass dieser Ansatz auf Nebenwirkungen beruht.
Scratte