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

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

15 ответов

Решение

... начиная с 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

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

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

final int variable = 123;

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

int variable = 123;
variable = 456;

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

int variable = 123;

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

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

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

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

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

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

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

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

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

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

Финал:

final int number;
number = 23;

Эффективно Окончательный:

int number;
number = 34;

Когда лямбда-выражение использует назначенную локальную переменную из окружающего пространства, существует важное ограничение. Лямбда-выражение может использовать только локальную переменную, значение которой не изменяется. Это ограничение называется "захват переменной", который описывается как; лямбда-выражения захватывают значения, а не переменные.
Локальные переменные, которые может использовать лямбда-выражение, известны как "эффективно финальные".
Фактически последняя переменная - это переменная, значение которой не изменяется после ее первого назначения. Нет необходимости явно объявлять такую ​​переменную как 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;
        };
    }   
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 тогда компилятор выдаст вам сообщение об ошибке:

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

В Java объявлено три типа финальных переменных.

1. Неявно объявленные финальные переменные

Существует три вида неявно объявленных финальных переменных.

  • Поля, объявленные в интерфейсах.
  • Локальные переменные, объявленные как ресурс в инструкции try-with-resource.
  • Параметры исключения в предложении многократной попытки.

2. Явно объявленные финальные переменные.

Это переменные, объявленные с помощьюключевое слово.

3. Фактически финальные переменные

Переменные считаются фактически конечными, если:

я. Локальная переменная, объявленная в операторе и чей декларатор имеет инициализатор , ИЛИ локальная переменная, объявленная по шаблону , и ей следует следовать следующим правилам:

  1. не заявлено с
  2. никогда не происходит как присвоение значения
  3. никогда не встречается в качестве операнда префиксного или постфиксного оператора увеличения или уменьшения

Пример: локальная переменная, объявленная в инструкции, декларатор которой имеет инициализатор .

      List<String> ls = Arrays.asList("A", "B", "C");

for (Iterator<String> iterator = ls.iterator(); iterator.hasNext();) {
  String s = (String) iterator.next();
  System.out.println(s);
}

Здесь итератор переменной фактически является окончательным, поскольку он соответствует приведенным выше правилам.

Примечание . Если переменная фактически является финальной, ее также можно объявить с помощью ключевого слова.

      List<String> ls = Arrays.asList("A", "B", "C");

for (final Iterator<String> iterator = ls.iterator(); iterator.hasNext();) {
  String s = (String) iterator.next();
  System.out.println(s);
}

пример: локальная переменная, объявленная шаблоном .

      public String getStringfromObj(Object o){

    if (o instanceof String s && s.startsWith("Java")) { // can be declared as final String s
      return s;
    }
    
    return "";
}

ii. Локальная переменная, объявленная в инструкции и инициализированная позже. При этом следует соблюдать следующие правила.

  1. не заявлено с final
  2. никогда не происходит как присвоение значения до и после определенного присвоения
  3. никогда не встречается в качестве операнда префиксного или постфиксного оператора увеличения или уменьшения

бывший.

      List<String> ls = Arrays.asList("A", "B", "C");
int k; // can be declared as final int k
    
    if ((k = ls.size()) > 0) {
      System.out.println(k);
}

iii. Параметр метода, конструктора, лямбда-выражения или исключения, декларатор которого имеет инициализатор.

Фактически конечная переменная - это локальная переменная, которая:

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

В то время как конечная переменная - это переменная, которая:

  1. объявлен с final ключевое слово.

Объявление переменной 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 быть эффективно окончательным.

Однако, начиная с 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
    }
);
Другие вопросы по тегам