Zmienna używana w wyrażeniu lambda powinna być ostateczna lub faktycznie ostateczna

134

Zmienna używana w wyrażeniu lambda powinna być ostateczna lub faktycznie ostateczna

Kiedy próbuję użyć calTz, pokazuje ten błąd.

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
źródło
5
Nie możesz modyfikować calTzz lambda.
Elliott Frisch
2
Założyłem, że to jedna z tych rzeczy, które po prostu nie zostały zrobione na czas dla Javy 8. Ale Java 8 to rok 2014. Scala i Kotlin pozwalali na to od lat, więc jest to oczywiście możliwe. Czy Java planuje kiedykolwiek wyeliminować to dziwne ograniczenie?
GlenPeterson,
5
Oto zaktualizowany link do komentarza @MSDousti.
geisterfurz007
Myślę, że możesz użyć Completable Futures jako obejścia.
Kraulain
Zauważyłem jedną ważną rzecz - zamiast normalnych zmiennych można używać zmiennych statycznych (to chyba ostatecznie ostateczne)
kaushalpranav

Odpowiedzi:

68

A finalzmienne oznacza, że może być instancja tylko jeden raz. w Javie nie można używać zmiennych innych niż końcowe w lambdzie ani w anonimowych klasach wewnętrznych.

Możesz refaktoryzować swój kod za pomocą starej pętli 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;
}

Nawet jeśli nie rozumiem niektórych fragmentów tego kodu:

  • wywołujesz a v.getTimeZoneId();bez używania wartości zwracanej
  • z przypisaniem calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());nie modyfikujesz pierwotnie przekazanego calTzi nie używasz go w tej metodzie
  • Zawsze wracasz null, dlaczego nie ustawisz voidjako typu powrotu?

Mam nadzieję, że te wskazówki pomogą Ci poprawić.

Francesco Pitzalis
źródło
możemy użyć nieostatecznych zmiennych statycznych
Narendra Jaggi
92

Chociaż inne odpowiedzi potwierdzają ten wymóg, nie wyjaśniają, dlaczego to wymaganie istnieje.

JLS wspomina, dlaczego w §15.27.2 :

Ograniczenie do efektywnych zmiennych końcowych uniemożliwia dostęp do dynamicznie zmieniających się zmiennych lokalnych, których wychwycenie prawdopodobnie spowodowałoby problemy z współbieżnością.

Aby zmniejszyć ryzyko błędów, postanowili zapewnić, że przechwycone zmienne nigdy nie będą mutowane.

Dioksyna
źródło
10
Dobra odpowiedź +1 i jestem zaskoczony, jak małe pokrycie wydaje się mieć powód skutecznego finału. Uwaga: zmienna lokalna może być przechwycona przez lambdę tylko wtedy, gdy jest również definitywnie przypisana przed treścią lambdy. Wydaje się, że oba wymagania zapewniają, że dostęp do zmiennej lokalnej będzie bezpieczny dla wątków.
Tim Biegeleisen,
2
masz pojęcie, dlaczego jest to ograniczone tylko do zmiennych lokalnych, a nie do członków klas? Często omijam ten problem, deklarując moją zmienną jako członka klasy ...
David Refaeli
4
Członkowie klasy @DavidRefaeli są objęci / wpływają na model pamięci, który, jeśli będzie przestrzegany, da przewidywalne wyniki po udostępnieniu. Zmienne lokalne nie są, jak wspomniano w §17.4.1
Dioxin
To głupiutka sztuczka, którą należy usunąć. Kompilator powinien ostrzegać o potencjalnym dostępie do zmiennych między wątkami, ale powinien na to zezwolić. Albo powinieneś być na tyle sprytny, aby wiedzieć, czy twoja lambda działa na tym samym wątku, czy działa równolegle, itp. Jest to głupie ograniczenie, które mnie zasmuca. I jak wspominali inni, problemy nie istnieją np. W C #.
Josh M.
@JoshM. C # umożliwia również tworzenie zmiennych typów wartości , których ludzie zalecają unikanie, aby zapobiec problemom. Zamiast mieć takie zasady, Java postanowiła całkowicie temu zapobiec. Zmniejsza to liczbę błędów użytkowników kosztem elastyczności. Nie zgadzam się z tym ograniczeniem, ale jest to uzasadnione. Uwzględnienie równoległości wymagałoby dodatkowej pracy po stronie kompilatora i prawdopodobnie dlatego nie wybrano trasy „ ostrzegaj o dostępie między wątkami ”. Deweloper pracujący nad specyfikacją byłby prawdopodobnie naszym jedynym potwierdzeniem.
Dioxin
57

Z lambdy nie można uzyskać odniesienia do niczego, co nie jest ostateczne. Musisz zadeklarować końcowe opakowanie spoza lamdy, aby przechowywać zmienną.

Dodałem ostatni obiekt „referencyjny” jako opakowanie.

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
źródło
Myślałem o tym samym lub podobnym podejściu - ale chciałbym zobaczyć porady / opinie ekspertów dotyczące tej odpowiedzi?
YoYo,
4
Ten kod pomija inicjał reference.set(calTz);lub odwołanie musi zostać utworzone przy użyciu new AtomicReference<>(calTz), w przeciwnym razie niezerowa strefa czasowa podana jako parametr zostanie utracona.
Julien Kronegg
7
To powinna być pierwsza odpowiedź. AtomicReference (lub podobna klasa Atomic___) bezpiecznie obchodzi to ograniczenie w każdych możliwych okolicznościach.
GlenPeterson,
1
Zgoda, to powinna być akceptowana odpowiedź. Inne odpowiedzi zawierają przydatne informacje o tym, jak wrócić do niefunkcjonalnego modelu programowania i dlaczego tak się stało, ale tak naprawdę nie mówią ci, jak obejść problem!
Jonathan Benn
2
@GlenPeterson i jest to również okropna decyzja, nie tylko jest o wiele wolniejsza w ten sposób, ale także ignorujesz właściwość efektów ubocznych, którą nakazuje dokumentacja.
Eugene,
41

Java 8 ma nową koncepcję zwaną zmienną „Efektywnie ostateczna”. Oznacza to, że nieostateczna zmienna lokalna, której wartość nigdy się nie zmienia po inicjalizacji, nazywana jest „Efektywnie końcową”.

Ta koncepcja została wprowadzona, ponieważ przed wersją Java 8 nie mogliśmy używać nieostatecznej zmiennej lokalnej w klasie anonimowej . Jeśli chcesz mieć dostęp do zmiennej lokalnej w klasie anonimowej , musisz to zrobić.

Wraz z wprowadzeniem lambdy to ograniczenie zostało złagodzone. Stąd potrzeba, aby zmienna lokalna była ostateczna, jeśli nie zostanie zmieniona po zainicjowaniu, ponieważ lambda sama w sobie jest niczym innym jak klasą anonimową.

Java 8 zdała sobie sprawę z bólu deklarowania zmiennej lokalnej jako końcowej za każdym razem, gdy programista użył lambdy, wprowadziła tę koncepcję i sprawiła, że ​​nie było potrzeby, aby zmienne lokalne były ostateczne. Więc jeśli widzisz, że reguła dla klas anonimowych się nie zmieniła, to po prostu nie musisz wpisywać finalsłowa kluczowego za każdym razem, gdy używasz lambd.

Znalazłem dobre wyjaśnienie tutaj

Dinesh Arora
źródło
Formatowanie kodu powinno być używane tylko dla kodu , a nie ogólnie dla terminów technicznych. effectively finalto nie kod, to terminologia. Zobacz Kiedy należy używać formatowania kodu w przypadku tekstu niekodowego? na Meta Stack Overflow .
Charles Duffy
(Tak więc „ finalsłowo kluczowe” jest słowem kodu, które można sformatować w ten sposób, ale kiedy używa się terminu „final” raczej opisowo niż jako kod, jest to terminologia).
Charles Duffy
9

W twoim przykładzie możesz zastąpić forEachlamdba prostą forpętlą i dowolnie modyfikować dowolną zmienną. Lub prawdopodobnie zrefaktoryzuj swój kod, aby nie trzeba było modyfikować żadnych zmiennych. Jednak wyjaśnię dla kompletności, co oznacza błąd i jak go obejść.

Specyfikacja języka Java 8, §15.27.2 :

Każda zmienna lokalna, parametr formalny lub parametr wyjątku używany, ale nie zadeklarowany w wyrażeniu lambda, musi być zadeklarowany jako ostateczny lub faktycznie ostateczny ( §4.12.4 ), albo w przypadku próby użycia wystąpi błąd w czasie kompilacji.

Zasadniczo nie można modyfikować zmiennej lokalnej ( calTzw tym przypadku) z poziomu lambda (lub klasy lokalnej / anonimowej). Aby to osiągnąć w Javie, musisz użyć mutowalnego obiektu i zmodyfikować go (poprzez końcową zmienną) z lambda. Przykładem modyfikowalnego obiektu byłaby tutaj tablica jednego elementu:

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
źródło
Innym sposobem jest użycie pola obiektu. Np. Wynik MyObj = nowy MyObj (); ...; result.timeZone = ...; ....; return result.timezone; Pamiętaj jednak, że jak wyjaśniono powyżej, naraża to Cię na problemy z bezpieczeństwem wątków. Zobacz stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

jeśli modyfikacja zmiennej nie jest konieczna, ogólnym obejściem tego rodzaju problemu byłoby wyodrębnienie części kodu, która używa lambda i użycie słowa kluczowego final w metodzie-parametrze.

robie2011
źródło
0

Zmienna używana w wyrażeniu lambda powinna być ostateczną lub faktycznie ostateczną, ale można przypisać wartość do końcowej tablicy jednoelementowej.

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
źródło
Jest to bardzo podobne do sugestii Aleksandra Udałowa. Poza tym myślę, że to podejście opiera się na efektach ubocznych.
Scratte