La variabile utilizzata nell'espressione lambda deve essere finale o effettivamente definitiva


134

La variabile utilizzata nell'espressione lambda deve essere finale o effettivamente definitiva

Quando provo a usarlo calTzmostra questo errore.

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

5
Non è possibile modificare calTzdalla lambda.
Elliott Frisch,

2
Ho pensato che questa fosse una di quelle cose che non sono state fatte in tempo per Java 8. Ma Java 8 era il 2014. Scala e Kotlin lo hanno permesso per anni, quindi è ovviamente possibile. Java ha mai intenzione di eliminare questa strana restrizione?
GlenPeterson,

5
Ecco il link aggiornato al commento di @MSDousti.
geisterfurz007,

Penso che potresti usare Futures Completabili come soluzione alternativa.
Kraulain,

Una cosa importante che ho osservato - Puoi usare variabili statiche invece di variabili normali (Questo lo rende effettivamente definitivo, immagino)
kaushalpranav

Risposte:


68

Una finalvariabile significa che può essere istanziata una sola volta. in Java non è possibile utilizzare variabili non finali in lambda e in classi interne anonime.

Puoi riformattare il tuo codice con il vecchio ciclo for-each:

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

Anche se non ho la sensazione di alcuni pezzi di questo codice:

  • chiami a v.getTimeZoneId();senza usare il suo valore di ritorno
  • con l'assegnazione calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());non modifichi l'originale passato calTze non lo usi in questo metodo
  • Ritorni sempre null, perché non imposti voidcome tipo di ritorno?

Spero anche che questi suggerimenti ti aiutino a migliorare.


possiamo usare variabili statiche non finali
Narendra Jaggi,

93

Sebbene altre risposte dimostrino il requisito, non spiegano perché esiste il requisito.

Il JLS menziona perché in § 15.27.2 :

La restrizione a variabili finali efficaci proibisce l'accesso a variabili locali che cambiano dinamicamente, la cui acquisizione introdurrebbe probabilmente problemi di concorrenza.

Per ridurre il rischio di bug, hanno deciso di garantire che le variabili acquisite non siano mai mutate.


11
Buona risposta +1, e sono sorpreso dalla scarsa copertura che sembra avere la ragione di un efficace finale. Di nota: Una variabile locale può essere catturato solo da un lambda se viene anche definitivamente assegnato prima del corpo del lambda. Entrambi i requisiti sembrano garantire che l'accesso alla variabile locale sia thread-safe.
Tim Biegeleisen,

2
qualche idea sul perché questo sia limitato solo alle variabili locali e non ai membri della classe? Mi trovo spesso a eludere il problema dichiarando la mia variabile come membro della classe ...
David Refaeli

4
I membri della classe @DavidRefaeli sono coperti / influenzati dal modello di memoria, che se seguito, produrrà risultati prevedibili se condivisi. Le variabili locali non sono, come menzionato in §17.4.1
Diossina

Questo è un trucco stupido, che dovrebbe essere rimosso. Il compilatore dovrebbe avvertire del potenziale accesso variabile tra thread, ma dovrebbe consentirlo. Oppure, dovrebbe essere abbastanza intelligente da sapere se la tua lambda sta girando sullo stesso thread, o sta funzionando in parallelo, ecc. Questa è una limitazione sciocca, che mi rende triste. E come altri hanno già detto, i problemi non esistono ad es. In C #.
Josh M.,

@JoshM. C # consente inoltre di creare tipi di valori modificabili , che le persone consigliano di evitare per evitare problemi. Invece di avere tali principi, Java ha deciso di impedirlo completamente. Riduce l'errore dell'utente, al prezzo della flessibilità. Non sono d'accordo con questa limitazione, ma è giustificabile. La contabilità per il parallelismo richiederebbe un lavoro extra alla fine del compilatore, motivo per cui non è stata presa la strada dell '" avvertimento di accesso incrociato ". Uno sviluppatore che lavora sulle specifiche sarebbe probabilmente la nostra unica conferma per questo.
Diossina,

58

Da una lambda, non puoi ottenere un riferimento a qualcosa che non sia definitivo. È necessario dichiarare un wrapper finale dall'esterno della lamda per contenere la variabile.

Ho aggiunto l'oggetto finale "di riferimento" come questo wrapper.

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

Stavo pensando allo stesso approccio o approccio simile - ma mi piacerebbe vedere alcuni consigli / feedback di esperti su questa risposta?
YoYo

4
Questo codice manca un'iniziale reference.set(calTz);o il riferimento deve essere creato usando new AtomicReference<>(calTz), altrimenti il ​​fuso orario non nullo fornito come parametro andrà perso.
Julien Kronegg,

8
Questa dovrebbe essere la prima risposta. Un AtomicReference (o una classe Atomic___ simile) aggira questa limitazione in modo sicuro in ogni possibile circostanza.
GlenPeterson,

1
D'accordo, questa dovrebbe essere la risposta accettata. Le altre risposte forniscono informazioni utili su come ricorrere a un modello di programmazione non funzionale e sul perché ciò è stato fatto, ma in realtà non ti dicono come aggirare il problema!
Jonathan Benn,

2
@GlenPeterson ed è anche una decisione terribile, non solo è molto più lento in questo modo, ma stai anche ignorando la proprietà degli effetti collaterali che la documentazione impone.
Eugene,

41

Java 8 ha un nuovo concetto chiamato variabile "efficacemente finale". Significa che una variabile locale non finale il cui valore non cambia mai dopo l'inizializzazione è chiamata "efficacemente finale".

Questo concetto è stato introdotto perché prima di Java 8 non potevamo usare una variabile locale non finale in una classe anonima . Se vuoi avere accesso a una variabile locale in classe anonima , devi renderla definitiva.

Quando è stata introdotta la lambda, questa restrizione è stata attenuata. Da qui la necessità di rendere definitiva la variabile locale se non viene cambiata una volta inizializzata come lambda in sé non è altro che una classe anonima.

Java 8 ha realizzato il dolore di dichiarare la variabile locale finale ogni volta che uno sviluppatore ha utilizzato lambda, ha introdotto questo concetto e ha reso superfluo rendere definitive le variabili locali. Quindi se vedi che la regola per le classi anonime non è cambiata, è solo che non devi scrivere la finalparola chiave ogni volta che usi lambdas.

Ho trovato una buona spiegazione qui


La formattazione del codice deve essere utilizzata solo per il codice , non per termini tecnici in generale. effectively finalnon è codice, è terminologia. Vedi Quando utilizzare la formattazione del codice per il testo non di codice? su Meta Stack Overflow .
Charles Duffy,

(Quindi "la finalparola chiave" è una parola di codice e corretta per formattare in quel modo, ma quando si utilizza "finale" in modo descrittivo piuttosto che come codice, è invece la terminologia).
Charles Duffy,

9

Nel tuo esempio, puoi sostituire the forEachcon lamdba con un semplice forloop e modificare liberamente qualsiasi variabile. O, probabilmente, refactoring il codice in modo da non dover modificare alcuna variabile. Tuttavia, spiegherò per completezza cosa significa l'errore e come aggirarlo.

Specifiche del linguaggio Java 8, §15.27.2 :

Qualsiasi variabile locale, parametro formale o parametro di eccezione utilizzato ma non dichiarato in un'espressione lambda deve essere dichiarato finale o essere effettivamente finale ( §4.12.4 ), oppure si verifica un errore in fase di compilazione nel tentativo di utilizzo.

Fondamentalmente non è possibile modificare una variabile locale ( calTzin questo caso) dall'interno di una lambda (o una classe locale / anonima). Per ottenere ciò in Java, devi usare un oggetto mutabile e modificarlo (tramite una variabile finale) dalla lambda. Un esempio di un oggetto mutabile qui sarebbe una matrice di un elemento:

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

Un altro modo è utilizzare un campo di un oggetto. Ad esempio MyObj result = new MyObj (); ...; result.timeZone = ...; ....; return result.timezone; Si noti tuttavia che, come spiegato sopra, ciò espone a problemi di sicurezza del thread. Vedere stackoverflow.com/a/50341404/7092558
Gibezynu Nu

0

se non è necessario modificare la variabile di una soluzione generale per questo tipo di problema sarebbe estrarre la parte di codice che usa lambda e usare la parola chiave finale sul parametro-metodo.


0

Una variabile utilizzata nell'espressione lambda dovrebbe essere un finale o effettivamente finale, ma è possibile assegnare un valore a un array di un elemento finale.

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

Questo è molto simile al suggerimento di Alexander Udalov. A parte questo, penso che questo approccio si basi su effetti collaterali.
Scratte
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.