Różnica między ostatecznym a efektywnym końcowym


350

Gram z lambdami w Javie 8 i natknąłem się na ostrzeżenie local variables referenced from a lambda expression must be final or effectively final. Wiem, że kiedy używam zmiennych wewnątrz anonimowej klasy, muszą one być ostateczne w klasie zewnętrznej, ale nadal - jaka jest różnica między ostateczną a efektywną ostateczną ?


2
Wiele odpowiedzi, ale w zasadzie wszystkie stanowią „bez różnicy”. Ale czy to naprawdę prawda? Niestety nie mogę znaleźć specyfikacji języka dla Javy 8.
Aleksandr Dubinsky

3
@AleksandrDubinsky docs.oracle.com/javase/specs
lodów

@AleksandrDubinsky nie „naprawdę” prawda. Znalazłem jeden wyjątek od tej reguły. Zmienna lokalna zainicjowana stałą nie jest stałym wyrażeniem kompilatora. Nie możesz użyć takiej zmiennej dla przypadku w przełączniku / przypadku, dopóki nie dodasz końcowego słowa kluczowego. Np. „Int k = 1; switch (someInt) {case k: ...”.
Henno Vermeulen

Odpowiedzi:


233

... począwszy od Java SE 8 klasa lokalna może uzyskać dostęp do zmiennych lokalnych i parametrów otaczającego bloku, które są ostateczne lub faktycznie ostateczne. Zmienna lub parametr, którego wartość nigdy nie ulega zmianie po zainicjowaniu, jest faktycznie ostateczny.

Załóżmy na przykład, że zmienna numberLengthnie jest zadeklarowana jako ostateczna i dodajesz zaznaczoną instrukcję przypisania w PhoneNumberkonstruktorze:

public class OutterClass {  

  int numberLength; // <== not *final*

  class PhoneNumber {

    PhoneNumber(String phoneNumber) {
        numberLength = 7;   // <== assignment to numberLength
        String currentNumber = phoneNumber.replaceAll(
            regularExpression, "");
        if (currentNumber.length() == numberLength)
            formattedPhoneNumber = currentNumber;
        else
            formattedPhoneNumber = null;
     }

  ...

  }

...

}

Z powodu tej instrukcji przypisania zmienna numberLength nie jest już efektywnie ostateczna. W rezultacie kompilator Java generuje komunikat o błędzie podobny do „zmiennych lokalnych przywoływanych z klasy wewnętrznej musi być ostateczny lub faktycznie końcowy”, gdy klasa wewnętrzna PhoneNumber próbuje uzyskać dostęp do zmiennej numberLength:

http://codeinventions.blogspot.in/2014/07/difference-between-final-and.html

http://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html


68
+1 Uwaga: jeśli odniesienie nie zostanie zmienione, jest ono faktycznie ostateczne, nawet jeśli odwołany obiekt zostanie zmieniony.
Peter Lawrey,

1
@stanleyerror Może to być pomocne: stackoverflow.com/questions/4732544/…

1
Myślę, że bardziej użyteczne niż przykład nie skutecznie finału, jest przykładem, gdy coś jest skutecznie finału. Chociaż opis wyjaśnia. Var nie musi być uznany za ostateczny, jeśli żaden kod nie zmienia jego wartości.
Skychan

1
Przykład jest niepoprawny. Ten kod doskonale się kompiluje (oczywiście bez kropek). Aby uzyskać błąd kompilatora, ten kod powinien znajdować się w jakiejś metodzie, aby numberLengthstała się zmienną lokalną tej metody.
mykola

1
Czy istnieje powód, dla którego ten przykład jest tak skomplikowany? Dlaczego większość kodu zajmuje się całkowicie nieistotną operacją wyrażenia regularnego? I, jak już powiedział @mykola, całkowicie brakuje znaku dotyczącego efektywnej właściwości końcowej , ponieważ dotyczy to tylko zmiennych lokalnych i w tym przykładzie nie ma zmiennej lokalnej.
Holger

131

Uważam, że najprostszym sposobem na wyjaśnienie „skutecznie ostatecznego” jest wyobrażenie sobie dodania finalmodyfikatora do deklaracji zmiennej. Jeśli przy tej zmianie program nadal będzie zachowywał się w ten sam sposób, zarówno w czasie kompilacji, jak i w czasie wykonywania, wówczas ta zmienna jest faktycznie ostateczna.


4
Jest to prawdą, o ile zrozumienie „ostatecznego” java 8 jest dobrze zrozumiane. W przeciwnym razie spojrzałbym na zmienną, która nie została zadeklarowana jako ostateczna, do której przypisałeś później zadanie, i błędnie sądzę, że nie była ona ostateczna. Można powiedzieć „oczywiście” ... ale nie wszyscy zwracają tyle uwagi na najnowsze zmiany wersji językowej, ile powinni.
fool4jesus

8
Jednym wyjątkiem od tej reguły jest to, że zmienna lokalna zainicjowana stałą nie jest stałym wyrażeniem kompilatora. Nie możesz użyć takiej zmiennej dla przypadku w przełączniku / przypadku, dopóki nie dodasz końcowego słowa kluczowego. Np. „Int k = 1; switch (someInt) {case k: ...”.
Henno Vermeulen

2
@HennoVermeulen skrzynka rozdzielcza nie jest wyjątkiem od reguły zawartej w tej odpowiedzi. Język określa, że case kwymaga stałego wyrażenia, które może być zmienną stałą („Zmienna stała jest zmienną końcową pierwotnego typu lub typu Łańcuch, który jest inicjowany stałym wyrażeniem” JLS 4.12.4 ), co jest szczególnym przypadkiem ostatecznego zmienna.
Colin D Bennett

3
W moim przykładzie kompilator narzeka, że ​​k nie jest stałym wyrażeniem, więc nie można go użyć do przełącznika. Podczas dodawania końcowego zachowanie kompilacji zmienia się, ponieważ jest to zmienna stała i może być używana w przełączniku. Masz rację: reguła jest nadal poprawna. Po prostu nie ma zastosowania do tego przykładu i nie mówi, czy k jest faktycznie ostateczny, czy nie.
Henno Vermeulen

36

Według dokumentów :

Zmienna lub parametr, którego wartość nigdy nie ulega zmianie po zainicjowaniu, jest faktycznie ostateczny.

Zasadniczo, jeśli kompilator znajdzie zmienną, która nie pojawia się w zadaniach poza jej inicjalizacją, wówczas zmienną uważa się za faktycznie ostateczną .

Rozważmy na przykład klasę:

public class Foo {

    public void baz(int bar) {
        // While the next line is commented, bar is effectively final
        // and while it is uncommented, the assignment means it is not
        // effectively final.

        // bar = 2;
    }
}

Dokumenty mówią o zmiennych lokalnych. barw twoim przykładzie nie ma zmiennej lokalnej, ale pole. „Skutecznie końcowy” w komunikacie o błędzie, jak powyżej, w ogóle nie dotyczy pól.
Antti Haapala

6
@AnttiHaapala barjest tutaj parametrem, a nie polem.
peter.petrov

30

„Skutecznie końcowy” to zmienna, która nie dawałaby błędu kompilatora, gdyby miał być dołączony przez „końcowy”

Z artykułu Briana Goetza:

Nieformalnie, zmienna lokalna jest faktycznie ostateczna, jeśli jej początkowa wartość nigdy się nie zmienia - innymi słowy, zadeklarowanie jej jako ostatecznej nie spowodowałoby niepowodzenia kompilacji.

finał stanu lambda - Brian Goetz


2
ta odpowiedź jest cytatem, jednak w artykule Briana nie ma takiego dokładnego tekstu, na pewno nie dołączono tego słowa . Zamiast tego jest to cytat: nieoficjalnie, zmienna lokalna jest faktycznie ostateczna, jeśli jej początkowa wartość nigdy się nie zmienia - innymi słowy, zadeklarowanie jej jako ostatecznej nie spowodowałoby niepowodzenia kompilacji.
lcfd,

Z pełnego tekstu artykułu: Nieoficjalnie, zmienna lokalna jest faktycznie ostateczna, jeśli jej początkowa wartość nigdy się nie zmienia - innymi słowy, uznanie jej za ostateczną nie spowodowałoby niepowodzenia kompilacji.
Ajeet Ganga

26

Ta zmienna poniżej jest ostateczna , więc nie możemy zmienić jej wartości po zainicjowaniu. Jeśli spróbujemy, otrzymamy błąd kompilacji ...

final int variable = 123;

Ale jeśli stworzymy taką zmienną, możemy zmienić jej wartość ...

int variable = 123;
variable = 456;

Ale w Javie 8 wszystkie zmienne są domyślnie ostateczne . Ale istnienie drugiej linii w kodzie sprawia, że nie jest ona ostateczna . Jeśli więc usuniemy 2. wiersz z powyższego kodu, nasza zmienna będzie teraz „efektywnie końcowa” ...

int variable = 123;

Więc .. Każda zmienna, która jest przypisana tylko raz, jest „efektywnie ostateczna” .


Tak prosta, jak powinna być odpowiedź.
superigno

@Eurig, Cytowanie wymagane dla „wszystkie zmienne są domyślnie ostateczne”.
Pacerier

10

Zmienna jest ostateczna lub faktycznie ostateczna, gdy jest inicjowana raz i nigdy nie jest mutowana w swojej klasie właściciela. I nie możemy zainicjować go w pętlach lub klasach wewnętrznych .

Końcowe :

final int number;
number = 23;

Skutecznie końcowy :

int number;
number = 34;

Uwaga : Ostateczne i skuteczne ostateczne są podobne (ich wartość nie zmienia się po przypisaniu), ale efektywne zmienne końcowe nie są deklarowane słowem kluczowym final.


7

Gdy wyrażenie lambda używa przypisanej zmiennej lokalnej z otaczającej jej przestrzeni, istnieje ważne ograniczenie. Wyrażenie lambda może wykorzystywać tylko zmienną lokalną, której wartość się nie zmienia. To ograniczenie określa się jako „ przechwytywanie zmiennych ”, które jest opisane jako; wartości przechwytywania wyrażenia lambda, a nie zmienne .
Zmienne lokalne, których może użyć wyrażenie lambda, są znane jako „ efektywnie końcowe ”.
Skutecznie zmienna końcowa to taka, której wartość nie zmienia się po pierwszym przypisaniu. Nie ma potrzeby jawnego deklarowania takiej zmiennej jako końcowej, chociaż takie działanie nie byłoby błędem.
Zobaczmy to na przykładzie: mamy lokalną zmienną i, która jest inicjowana wartością 7, przy czym w wyrażeniu lambda próbujemy zmienić tę wartość, przypisując nową wartość do i. Spowoduje to błąd kompilatora - „ Zmienna lokalna i zdefiniowana w zakresie obejmującym musi być ostateczna lub faktycznie ostateczna

@FunctionalInterface
interface IFuncInt {
    int func(int num1, int num2);
    public String toString();
}

public class LambdaVarDemo {

    public static void main(String[] args){             
        int i = 7;
        IFuncInt funcInt = (num1, num2) -> {
            i = num1 + num2;
            return i;
        };
    }   
}

2

Efektywny temat końcowy opisano w JLS 4.12.4, a ostatni akapit zawiera jasne wyjaśnienie:

Jeśli zmienna jest faktycznie ostateczna, dodanie końcowego modyfikatora do jej deklaracji nie spowoduje żadnych błędów w czasie kompilacji. I odwrotnie, lokalna zmienna lub parametr, który jest zadeklarowany jako końcowy w prawidłowym programie, staje się faktycznie końcowy, jeśli ostatni modyfikator zostanie usunięty.


2

final to zmienna deklarowana ze słowem kluczowym final, przykład:

final double pi = 3.14 ;

pozostaje finalprzez cały program.

efektywnie końcowy : dowolna zmienna lokalna lub parametr, któremu w tej chwili przypisano wartość tylko raz (lub zaktualizowano tylko raz). W trakcie programu może nie pozostać efektywnie ostateczny . oznacza to zatem, że efektywnie zmienna końcowa może utracić swoją efektywną właściwość końcową natychmiast po tym, jak zostanie przypisana / zaktualizowana co najmniej o jeszcze jedno przypisanie. przykład:

class EffectivelyFinal {

    public static void main(String[] args) {
        calculate(124,53);
    }

    public static void calculate( int operand1, int operand2){   
     int rem = 0;  //   operand1, operand2 and rem are effectively final here
     rem = operand1%2  // rem lost its effectively final property here because it gets its second assignment 
                       // operand1, operand2 are still effectively final here 
        class operators{

            void setNum(){
                operand1 =   operand2%2;  // operand1 lost its effectively final property here because it gets its second assignment
            }

            int add(){
                return rem + operand2;  // does not compile because rem is not effectively final
            }
            int multiply(){
                return rem * operand1;  // does not compile because both rem and operand1 are not effectively final
            }
        }   
   }    
}

Jest to niepoprawne zgodnie ze specyfikacją języka Java: „ Ilekroć występuje w wyrażeniu przypisania jako lewa strona, jest zdecydowanie nieprzypisane i nie przypisane przed przypisaniem”. Zmienna / parametr jest zawsze albo nigdy efektywnie ostateczny. Mówiąc dokładniej, jeśli nie możesz dodać finalsłowa kluczowego do deklaracji bez wprowadzenia błędów kompilacji, to nie jest to ostatecznie ostateczne . Jest to przeciwieństwo tego stwierdzenia: „Jeśli zmienna jest faktycznie końcowa, dodanie końcowego modyfikatora do jej deklaracji nie spowoduje żadnych błędów w czasie kompilacji”.
AndrewF

Komentarze w przykładowym kodzie są niepoprawne ze wszystkich powodów opisanych w moim komentarzu. „Skutecznie ostateczny” nie jest stanem, który może się zmieniać z czasem.
AndrewF,

@AndrewF, jeśli nie zmienia się w czasie, jak myślisz, co nie kompiluje ostatniego wiersza? rem był efektywnie końcowy na linii 1 w metodzie obliczania. Jednak w ostatnim wierszu kompilator skarży się, że rem nie jest skutecznie ostateczny
Metoda naukowa

Masz rację, że część kodu musi zostać usunięta z bloku kodu w celu skompilowania, ale nie odzwierciedla to zachowania środowiska wykonawczego. W czasie kompilacji możesz zdecydować, czy zmienna jest faktycznie ostateczna, czy nie - w oparciu o specyfikację jest ona zawsze zawsze ostateczna lub nigdy nie jest efektywna. Kompilator może stwierdzić poprzez statyczne spojrzenie na użycie zmiennej w całym jej zakresie. Nie można uzyskać ani utracić tej właściwości w trakcie działania programu. Termin jest dobrze zdefiniowany przez specyfikację - sprawdź inne odpowiedzi, które dość dobrze to wyjaśniają.
AndrewF,

1
public class LambdaScopeTest {
    public int x = 0;        
    class FirstLevel {
        public int x = 1;    
        void methodInFirstLevel(int x) {

            // The following statement causes the compiler to generate
            // the error "local variables referenced from a lambda expression
            // must be final or effectively final" in statement A:
            //
            // x = 99; 

        }
    }    
}

Jak powiedzieli inni, zmienna lub parametr, którego wartość nigdy nie ulega zmianie po zainicjowaniu, jest faktycznie ostateczny. W powyższym kodzie, jeśli zmienisz wartość xklasy wewnętrznej, FirstLevelkompilator wyświetli komunikat o błędzie:

Zmienne lokalne, do których odwołuje się wyrażenie lambda, muszą być ostateczne lub faktycznie ostateczne.


1

Jeśli mógłbyś dodać finalmodyfikator do zmiennej lokalnej, był on faktycznie ostateczny.

Dostęp do wyrażeń lambda

  • zmienne statyczne,

  • zmienne instancji,

  • skutecznie końcowe parametry metody, oraz

  • skutecznie końcowe zmienne lokalne.

Źródło: OCP: Oracle Certified Professional Java SE 8 Programmer II Study Guide, Jeanne Boyarsky, Scott Selikoff

Dodatkowo,

effectively finalZmienna jest zmienną, której wartość nie zmienia się, ale to nie jest zadeklarowana ze finalsłów kluczowych.

Źródło: Zaczynając od Java: Od struktur kontrolnych poprzez obiekty (wydanie 6), Tony Gaddis

Ponadto nie zapomnij o znaczeniu tego final, że jest on inicjowany dokładnie raz, zanim zostanie użyty po raz pierwszy.


0

Zadeklarowanie zmiennej finallub jej nie deklarowanie final, ale utrzymanie jej w ostateczności może spowodować (w zależności od kompilatora) inny kod bajtowy.

Rzućmy okiem na mały przykład:

    public static void main(String[] args) {
        final boolean i = true;   // 6  // final by declaration
        boolean j = true;         // 7  // effectively final

        if (i) {                  // 9
            System.out.println(i);// 10
        }
        if (!i) {                 // 12
            System.out.println(i);// 13
        }
        if (j) {                  // 15
            System.out.println(j);// 16
        }
        if (!j) {                 // 18
            System.out.println(j);// 19
        }
    }

Odpowiedni kod bajtowy mainmetody (Java 8u161 w systemie Windows w wersji 64-bitowej):

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_1
       3: istore_2
       4: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: iconst_1
       8: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      11: iload_2
      12: ifeq          22
      15: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
      18: iload_2
      19: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      22: iload_2
      23: ifne          33
      26: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
      29: iload_2
      30: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      33: return

Odpowiednia tabela numerów linii:

 LineNumberTable:
   line 6: 0
   line 7: 2
   line 10: 4
   line 15: 11
   line 16: 15
   line 18: 22
   line 19: 26
   line 21: 33

Jak widzimy w liniach kodu źródłowego 12, 13, 14nie pojawia się w kodzie bajtów. To dlatego, że ijest truei nie zmieni swojego stanu. Dlatego ten kod jest nieosiągalny (więcej w tej odpowiedzi ). Z tego samego powodu 9brakuje też kodu w linii . Stan inie musi być oceniany, ponieważ jest truena pewno.

Z drugiej strony, chociaż zmienna jjest faktycznie ostateczna , nie jest przetwarzana w ten sam sposób. Nie zastosowano takich optymalizacji. Stan jocenia się dwa razy. Kod bajtowy jest taki sam, niezależnie od tego, czy jjest faktycznie ostateczny .


Uznałbym to za nieefektywność kompilatora i niekoniecznie taką, która nadal byłaby prawdziwa w nowszych kompilatorach. W idealnym kompilacji, jeśli zmienna jest faktycznie ostateczna, wygeneruje dokładnie takie same optymalizacje jak jedna deklarowana ostateczna. Nie polegaj więc na przekonaniu, że faktycznie ostateczne jest automatycznie wolniejsze niż deklarowanie czegoś ostatecznego.
AndrewF,

@AndrewF Ogólnie masz rację, zachowanie może się zmienić. Dlatego napisałem „ może spowodować (zależy od kompilatora) inny kod bajtowy ”. Tylko z powodu brakującej optymalizacji (inny kod bajtowy) nie założyłbym, że wykonanie będzie wolniejsze. Ale to wciąż różnica w pokazanym przypadku.
LuCio,

-6

Jednak począwszy od Java SE 8 klasa lokalna może uzyskać dostęp do lokalnych zmiennych i parametrów> otaczającego bloku, które są ostateczne lub faktycznie ostateczne.

Nie zaczęło się to na Javie 8, używam tego od dawna. Ten kod używał (przed java 8), aby być legalnym:

String str = ""; //<-- not accesible from anonymous classes implementation
final String strFin = ""; //<-- accesible 
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
         String ann = str; // <---- error, must be final (IDE's gives the hint);
         String ann = strFin; // <---- legal;
         String str = "legal statement on java 7,"
                +"Java 8 doesn't allow this, it thinks that I'm trying to use the str declared before the anonymous impl."; 
         //we are forced to use another name than str
    }
);

2
Instrukcja odnosi się do faktu, że w <Java 8 można uzyskać dostęp tylko do final zmiennych, ale w Javie 8 również te, które są faktycznie ostateczne.
Antti Haapala

Widzę tylko kod, który nie działa, niezależnie od tego, czy używasz Java 7, czy Java 8.
Holger
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.