Эффективно final vs final - другое поведение

104

До сих пор я думал, что фактически final и final более или менее эквивалентны и что JLS будет рассматривать их одинаково, если не идентично в реальном поведении. Затем я нашел этот надуманный сценарий:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

По-видимому, JLS делает здесь важное различие между ними, и я не уверен, почему.

Я читаю другие темы вроде

но они не вдавались в подробности. В конце концов, на более широком уровне они кажутся в значительной степени эквивалентными. Но копнув глубже, они явно различаются.

Что вызывает такое поведение, может ли кто-нибудь предоставить некоторые определения JLS, которые это объясняют?


Изменить: я нашел еще один связанный сценарий:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

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

Забузард
источник
2
Очень интересный вопрос! Я ожидал, что Java будет вести себя одинаково в обоих случаях, но теперь я просвещен. Я спрашиваю себя, всегда ли было такое поведение или оно отличается от предыдущих версий
Лино
8
@Lino Формулировка для последней котировке в большой ответ ниже тот же весь путь обратно в Java 6 : «Если один из операндов имеет тип T , где T является byte, shortили char, а другой операнд является постоянным выражением тип int, значение которого может быть представлено в типе T , то тип условного выражения - T. " --- Даже нашел в Беркли документ Java 1.0. Тот же текст . --- Да, так было всегда.
Андреас
1
Интересно, как ты «находишь» вещи: P
Пожалуйста

Ответы:

65

В первую очередь, речь идет только о локальных переменных . Фактически final не применяется к полям. Это важно, поскольку семантика для finalполей очень различается и зависит от серьезных оптимизаций компилятора и обещаний модели памяти, см. $ 17.5.1 о семантике конечных полей.

На поверхностном уровне finalи effectively finalдля локальных переменных действительно идентичны. Однако JLS проводит четкое различие между ними, что на самом деле имеет широкий спектр эффектов в особых ситуациях, подобных этой.


Посылка

Из JLS§4.12.4 о finalпеременных:

Переменная константа является finalпеременной примитивного типа или типа String , который инициализируется с постоянным выражением ( §15.29 ). Независимо от того, является ли переменная постоянной переменной или нет, может иметь значение в отношении инициализации класса ( §12.4.1 ), двоичной совместимости ( §13.1 ), достижимости ( §14.22 ) и определенного присваивания ( §16.1.1 ).

Поскольку intона примитивна, переменная aявляется такой постоянной переменной .

Далее из той же главы о effectively final:

Некоторые переменные, которые не объявлены окончательными, вместо этого считаются фактически окончательными: ...

Таким образом, из того, как это сформулировано, ясно, что в другом примере aэто не считается постоянной переменной, поскольку она не является окончательной , а только фактически окончательной.


Поведение

Теперь, когда у нас есть различие, давайте посмотрим, что происходит и почему результат отличается.

Здесь вы используете условный оператор ? :, поэтому мы должны проверить его определение. Из JLS§15.25 :

Существует три вида условных выражений, классифицируемых в соответствии со вторым и третьим выражениями операндов: логические условные выражения , числовые условные выражения и ссылочные условные выражения .

В этом случае мы говорим о числовых условных выражениях из JLS§15.25.2 :

Тип числового условного выражения определяется следующим образом:

И это та часть, где два дела классифицируются по-разному.

фактически окончательный

Версия, которая effectively finalсоответствует этому правилу:

В противном случае ко второму и третьему операндам применяется общее числовое продвижение ( §5.6 ), а тип условного выражения - это повышенный тип второго и третьего операндов.

Это такое же поведение, как если бы вы это сделали 5 + 'd', т. int + charЕ. В результате int. См. JLS§5.6.

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

[...]

Затем расширяющееся примитивное преобразование ( §5.1.2 ) и сужающее примитивное преобразование ( §5.1.3) ) применяются к некоторым выражениям в соответствии со следующими правилами:

В контексте числового выбора применяются следующие правила:

Если какое-либо выражение относится к типу intи не является константным выражением ( §15.29 ), тогда повышенный тип имеет тип int, а другие выражения, не принадлежащие к типу, intподвергаются расширяющемуся примитивному преобразованию в int.

Так что все продвигается в intкачестве aэто intуже. Это объясняет вывод 97.

окончательный

Версия с finalпеременной соответствует этому правилу:

Если один из операндов имеет типа , Tгде Tнаходится byte, shortили char, а другой операндом является постоянным выражением ( §15.29 ) типа int, значение которого представимо в типе T, то тип условного выражения T.

Последняя переменная aимеет тип intи постоянное выражение (потому что это так final). Его можно представить как char, следовательно, результат имеет тип char. На этом вывод завершен a.


Пример строки

Пример с равенством строк основан на той же основной разнице, finalпеременные обрабатываются как постоянное выражение / переменная, а effectively finalне как.

В Java интернирование строк основано на постоянных выражениях, поэтому

"a" + "b" + "c" == "abc"

является trueтакже (не использовать эту конструкцию в реальном коде).

См. JLS§3.10.5 :

Более того, строковый литерал всегда относится к одному и тому же экземпляру класса String. Это связано с тем, что строковые литералы - или, в более общем смысле , строки, которые являются значениями константных выражений ( §15.29 ) - «интернированы», чтобы совместно использовать уникальные экземпляры с использованием метода String.intern( §12.5 ).

Легко упустить из виду, поскольку в основном речь идет о литералах, но на самом деле это применимо и к постоянным выражениям.

Забузард
источник
8
Проблема в том, что вы ожидаете, что будете ... ? a : 'c'вести себя одинаково, будь aто переменная или константа . В этом выражении нет ничего плохого. --- Напротив, a + "b" == "ab"это плохое выражение , потому что строки нужно сравнивать с помощью equals()( Как мне сравнивать строки в Java? ). Тот факт, что он «случайно» срабатывает, когда aявляется константой , - это просто причуда интернирования строковых литералов.
Андреас
5
@Andreas Да, но обратите внимание, что интернирование строк - это четко определенная функция Java. Это не совпадение, которое может измениться завтра или в другой JVM. "a" + "b" + "c" == "abc"должен быть trueв любой допустимой реализации Java.
Забузард,
10
Конечно, это четко выраженная причуда, но a + "b" == "ab"все же неправильное выражение . Даже если вы знаете, что aэто константа , она слишком подвержена ошибкам, чтобы не вызывать ее equals(). Или, может быть, « хрупкий» - лучшее слово, то есть слишком вероятно, что он развалится, когда код будет сохранен в будущем.
Андреас
2
Обратите внимание, что даже в первичной области эффективных конечных переменных, то есть их использовании в лямбда-выражениях, разница может изменить поведение во время выполнения, то есть может иметь значение между захватывающим и не захватывающим лямбда-выражением, последнее оценивается как одноэлементное , но первый создает новый объект. Другими словами, (final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);меняет свой результат, когда strесть (нет) final.
Хольгер
7

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

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

пока

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

это происходит потому , что компилятор знает , что использование final String a = "a"в aпеременном всегда будет иметь "a"значение так , что aи "a"может быть взаимозаменяемым без проблем. Иными словами, if aне определен finalили определен, finalно его значение присваивается во время выполнения (как в примере выше, где final является aпараметром), компилятор ничего не знает до его использования. Таким образом, объединение происходит во время выполнения, и создается новая строка без использования внутреннего пула.


В основном поведение таково: если компилятор знает, что переменная является константой, он может использовать ее так же, как использование константы.

Если переменная не определена как final (или она окончательная, но ее значение определяется во время выполнения), компилятор не должен обрабатывать ее как константу, даже если ее значение равно константе, а ее значение никогда не изменяется.

Давиде Лоренцо МАРИНО
источник
4
В этом нет ничего странного :)
dbl
2
Это другой аспект вопроса.
Давиде Лоренцо МАРИНО
5
finelключевое слово, примененное к параметру, имеет другую семантику, чем finalприменено к локальной переменной и т. д.
dbl
6
Использование параметра здесь излишне обфускация. Вы можете просто сделать final String a; a = "a";и добиться того же поведения
yawkat