Разница между окончательным и фактически окончательным

351

Я играю с лямбдами в Java 8, и я столкнулся с предупреждением local variables referenced from a lambda expression must be final or effectively final. Я знаю, что когда я использую переменные внутри анонимного класса, они должны быть финальными во внешнем классе, но все же - в чем разница между финальными и эффективно финальными ?

Алекс
источник
2
Множество ответов, но все они по сути равносильны «без разницы». Но так ли это на самом деле? К сожалению, я не могу найти спецификацию языка для Java 8.
Александр Дубинский
3
@AleksandrDubinsky docs.oracle.com/javase/specs
эйс
@AleksandrDubinsky не "действительно" правда. Я нашел одно исключение из этого правила. Локальная переменная, инициализированная с константой, не является константным выражением для компилятора. Вы не можете использовать такую ​​переменную для случая в switch / case, пока не добавите явно ключевое слово final. Например, "int k = 1; switch (someInt) {case k: ...".
Хенно Вермёлен

Ответы:

234

... начиная с Java SE 8, локальный класс может обращаться к локальным переменным и параметрам заключающего блока, которые являются окончательными или фактически конечными. Переменная или параметр, значение которых никогда не изменяется после его инициализации, фактически является окончательным.

Например, предположим, что переменная numberLengthне объявлена ​​как финальная, и вы добавили отмеченный оператор присваивания в PhoneNumberконструктор:

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

  ...

  }

...

}

Из-за этого оператора присваивания переменная numberLength больше не является окончательной. В результате компилятор Java генерирует сообщение об ошибке, похожее на «локальные переменные, на которые ссылается внутренний класс, должны быть окончательными или эффективно окончательными», когда внутренний класс PhoneNumber пытается получить доступ к переменной numberLength:

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

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

Суреш Атта
источник
69
+1 Примечание: если ссылка не изменена, она становится окончательной, даже если объект, на который ссылаются, изменяется.
Питер Лори
1
@stanleyerror Это может быть полезно: stackoverflow.com/questions/4732544/…
1
Я думаю , что более полезно , чем пример не эффективно окончательные, является примером того , когда что - то есть фактически окончательное. Хотя описание действительно проясняет. Var не нужно объявлять как final, если никакой код не изменяет его значение.
Skychan
1
Пример неверный. Этот код прекрасно компилируется (без точек, конечно). Чтобы получить ошибку компилятора, этот код должен быть внутри некоторого метода, чтобы он numberLengthстал локальной переменной этого метода.
Микола
2
Есть ли причина, почему этот пример такой сложный? Почему большая часть кода имеет дело с совершенно не относящейся к операции регулярного выражения? И, как уже говорилось @mykola, в нем отсутствует отметка об эффективном конечном свойстве, поскольку оно относится только к локальным переменным, и в этом примере нет локальной переменной.
Хольгер
132

Я считаю, что самый простой способ объяснить «эффективно окончательный» - это представить добавление finalмодификатора к объявлению переменной. Если с этим изменением программа продолжает вести себя одинаково, как во время компиляции, так и во время выполнения, то эта переменная будет фактически финальной.

Морис Нафталин
источник
4
Это правда, до тех пор, пока понимание "финала" Java 8 хорошо понято. В противном случае я бы посмотрел на переменную, которая не была объявлена ​​как final, которой вы присвоили позже, и ошибочно подумал, что она не окончательная. Вы можете сказать «конечно» ... но не все обращают столько внимания на последние изменения языковой версии, сколько им следовало бы.
fool4jesus
8
Единственным исключением из этого правила является то, что локальная переменная, инициализированная константой, не является константным выражением для компилятора. Вы не можете использовать такую ​​переменную для случая в switch / case, пока не добавите явно ключевое слово final. Например, "int k = 1; switch (someInt) {case k: ...".
Хенно Вермёлен
2
Случай с коммутатором @HennoVermeulen не является исключением из правила в этом ответе. Язык определяет, что case kтребуется постоянное выражение, которое может быть константной переменной («Постоянная переменная является конечной переменной примитивного типа или типа String, которая инициализируется константным выражением» JLS 4.12.4 ), что является частным случаем финального переменная.
Колин Д. Беннетт
3
В моем примере компилятор жалуется, что k не является константным выражением, поэтому его нельзя использовать для переключения. При добавлении final поведение компиляции меняется, поскольку теперь оно является постоянной переменной и может использоваться в переключателе. Так что вы правы: правило все еще верно. Это просто не относится к этому примеру и не говорит, является ли k окончательным или нет.
Henno Vermeulen
36

Согласно документам :

Переменная или параметр, значение которых никогда не изменяется после его инициализации, фактически является окончательным.

По сути, если компилятор обнаруживает, что переменная не появляется в присваиваниях за пределами ее инициализации, то эта переменная считается фактически конечной .

Например, рассмотрим некоторый класс:

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;
    }
}
Марк Эллиот
источник
Документы говорят о локальных переменных. barв вашем примере это не локальная переменная, а поле. «Эффективно окончательно» в сообщении об ошибке, как указано выше, не относится к полям вообще.
Антти Хаапала
6
@AnttiHaapala bar- это параметр, а не поле.
peter.petrov
30

'Effectively final' - это переменная, которая не выдаст ошибку компилятора, если она будет добавлена ​​'final'

Из статьи Брайана Гетца,

Неформально, локальная переменная является окончательной, если ее начальное значение никогда не изменяется - иными словами, объявление ее окончательным не приведет к сбою компиляции.

лямбда-государство-финал- Брайан Гетц

Аджит Ганга
источник
2
этот ответ показан в виде цитаты, однако в статье Брайана нет такого точного текста, наверняка, не добавлено слово . Вместо этого это цитата: неофициально, локальная переменная является окончательной, если ее начальное значение никогда не изменяется - другими словами, объявление ее окончательным не приведет к ошибке компиляции.
lcfd
Из дословной копии статьи: Неформально локальная переменная является фактически конечной, если ее начальное значение никогда не изменяется - иными словами, объявление ее окончательным не приведет к сбою компиляции.
Аджит Ганга
26

Эта переменная ниже является окончательной , поэтому мы не можем изменить ее значение после инициализации. Если мы попытаемся, мы получим ошибку компиляции ...

final int variable = 123;

Но если мы создадим такую ​​переменную, мы можем изменить ее значение ...

int variable = 123;
variable = 456;

Но в Java 8 все переменные являются окончательными по умолчанию. Но наличие 2-й строки в коде делает его не окончательным . Так что, если мы уберем 2 - й строки из кода выше, наша переменная теперь «эффективно окончательный» ...

int variable = 123;

Итак ... Любая переменная, которая назначается один раз и только один раз, является "эффективно конечной" .

Эуриг Джонс
источник
Так просто, как ответ должен быть.
Superigno
@Eurig, Цитирование требуется для "все переменные являются окончательными по умолчанию".
Pacerier
10

Переменная является окончательной или фактически конечной, когда она инициализируется один раз и никогда не мутирует в своем классе владельца. И мы не можем инициализировать его в циклах или внутренних классах .

Финал :

final int number;
number = 23;

Эффективно финал :

int number;
number = 34;

Примечание : Final и Effective Final похожи (их значение не изменяется после назначения), но только то, что эффективные переменные Final не объявляются с помощью ключевого слова final.

samadadi
источник
7

Когда лямбда-выражение использует назначенную локальную переменную из окружающего пространства, существует важное ограничение. Лямбда-выражение может использовать только локальную переменную, значение которой не изменяется. Это ограничение называется « захват переменной », который описывается как; лямбда-выражения захватывают значения, а не переменные .
Локальные переменные, которые может использовать лямбда-выражение, известны как « эффективно финальные ».
Фактически последняя переменная - это переменная, значение которой не изменяется после ее первого назначения. Нет необходимости явно объявлять такую ​​переменную как final, хотя это не будет ошибкой.
Давайте рассмотрим это на примере, у нас есть локальная переменная i, которая инициализируется значением 7, и в лямбда-выражении мы пытаемся изменить это значение, присваивая новое значение i. Это приведет к ошибке компилятора - « Локальная переменная i, определенная во включающей области видимости, должна быть конечной или фактически конечной »

@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;
        };
    }   
}
infoj
источник
2

Эффективная последняя тема описана в JLS 4.12.4, а последний абзац содержит четкое объяснение:

Если переменная является окончательной, то добавление модификатора final к ее объявлению не приведет к ошибкам во время компиляции. И наоборот, локальная переменная или параметр, который объявлен окончательным в допустимой программе, становится фактически конечным, если модификатор final удаляется.

Novdar
источник
2

final - объявить переменную с ключевым словом final, пример:

final double pi = 3.14 ;

это остается finalчерез программу.

фактически окончательный : любая локальная переменная или параметр, которому присваивается значение только один раз (или обновляется только один раз). Это может не оставаться эффективно окончательным в течение всей программы. таким образом, это означает, что эффективная конечная переменная может потерять свое фактически конечное свойство сразу после того, как она получит / обновит по крайней мере еще одно назначение. пример:

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
            }
        }   
   }    
}
Научный метод
источник
Это неверно в соответствии со спецификацией языка Java: « Всякий раз, когда это происходит как левая часть в выражении присваивания, оно определенно не присваивается и не обязательно присваивается перед присваиванием». Переменная / параметр либо всегда, либо никогда не являются окончательными. Более конкретно, если вы не можете добавить finalключевое слово в объявление, не внося ошибок компиляции, тогда оно не является окончательным . Это противоречит положению этого утверждения: «Если переменная является окончательной, то добавление модификатора final к ее объявлению не приведет к ошибкам во время компиляции».
AndrewF
Комментарии в примере кода некорректны по всем причинам, описанным в моем комментарии. «Фактически окончательный» - это не состояние, которое со временем может измениться.
AndrewF
@AndrewF, если он не меняется со временем, как вы думаете, что последняя строка не компилируется? rem был фактически последним в строке 1 в методе вычисления. Тем не менее, в последней строке компилятор жалуется, что rem не является окончательно финальным
The Scientific Method
Вы правы в том, что для компиляции часть кода необходимо удалить из блока кода, но это не отражает поведение во время выполнения. Во время компиляции вы можете решить, является ли переменная действительно конечной или нет - в зависимости от спецификации, она либо всегда эффективно финальная, либо она никогда не бывает эффективно окончательной. Компилятор может сказать, статически глядя на то, как переменная используется во всей своей области видимости. Свойство не может быть получено или потеряно во время работы программы. Термин хорошо определен спецификацией - посмотрите другие ответы, которые объясняют это довольно хорошо.
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; 

        }
    }    
}

Как уже говорили другие, переменная или параметр, значение которых никогда не изменяется после инициализации, фактически является окончательным. В приведенном выше коде, если вы измените значение xво внутреннем классе, FirstLevelто компилятор выдаст вам сообщение об ошибке:

Локальные переменные, на которые ссылается лямбда-выражение, должны быть окончательными или эффективно конечными.

Гириш
источник
1

Если бы вы могли добавить finalмодификатор к локальной переменной, это было бы эффективно.

Лямбда-выражения могут получить доступ

  • статические переменные,

  • переменные экземпляра,

  • эффективно окончательные параметры метода, и

  • эффективно финальные локальные переменные.

Источник: OCP: Oracle Certified Professional Java SE 8 Programmer II Учебное пособие, Жанна Боярски, Скотт Селикофф

Дополнительно,

effectively finalПеременная является переменной, значение которой никогда не изменяется, но он не объявлен с finalключевым словом.

Источник: Начиная с Java: от структур управления до объектов (6-е издание), Тони Гаддис

Кроме того, не забывайте о finalтом, что он инициализируется ровно один раз перед первым использованием.

ОСШ
источник
0

Объявление переменной finalили не объявление ее final, но ее эффективное сохранение в конечном итоге может привести (зависит от компилятора) к другому байт-коду.

Давайте посмотрим на небольшой пример:

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

Соответствующий байт-код mainметода (Java 8u161 в Windows 64 Bit):

  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

Соответствующая таблица номеров строк:

 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

Как мы видим , исходный код на линиях 12, 13, 14не отображается в байт - код. Это потому что iесть trueи не изменит его состояние. Таким образом, этот код недоступен (больше в этом ответе ). По той же причине код в строке 9тоже отсутствует. Состояние iне нужно оценивать, так как оно trueточно.

С другой стороны, хотя переменная jявляется окончательной, она не обрабатывается таким же образом. Таких оптимизаций не применено. Состояние jоценивается два раза. Байт-код один и тот же, независимо от того, jявляется ли он окончательным .

Лусио
источник
Я бы посчитал это неэффективностью компилятора, а не обязательно той, которая все еще будет верна в новых компиляторах. В идеальной компиляции, если переменная фактически является финальной, тогда она будет генерировать все те же оптимизации, что и одна объявленная финальная. Так что не полагайтесь на то, что финал автоматически медленнее, чем объявление финала.
AndrewF
@AndrewF Вообще вы правы, поведение может измениться. Вот почему я написал " может привести (зависит от компилятора) в другой байт-код ". Только из-за отсутствующей оптимизации (другой байт-код) я бы не предположил, что выполнение будет медленнее. Но это все еще разница в показанном случае.
Лусио
0

Окончательная переменная Effectively является локальной переменной, которая:

  1. Не определено как final
  2. Назначается ТОЛЬКО один раз.

В то время как финальная переменная является переменной, которая:

  1. объявлено с finalключевым словом.
Jimmy_Rw
источник
-6

Однако, начиная с Java SE 8, локальный класс может обращаться к локальным переменным и параметрам> заключающего блока, которые являются окончательными или фактически конечными.

Это не началось на Java 8, я использую это с давних времен. Этот код используется (до Java 8), чтобы быть законным:

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
    }
);
FiruzzZ
источник
2
Утверждение относится к тому , что в <Java 8, только final переменные могут быть доступны, но в Java 8 также те, которые фактически окончательным.
Антти Хаапала
Я вижу только код, который не работает, независимо от того, используете ли вы Java 7 или Java 8.
Хольгер