Ефективно фінал проти фіналу - інша поведінка


104

Поки що я думав, що фактично фінал та фінал більш-менш рівнозначні, і що 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 Ласкаво просимо :)
phant0m

Відповіді:


66

Перш за все, мова йде лише про локальні змінні . Фактично остаточний не застосовується до полів. Це важливо, оскільки семантика 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.
Zabuzard

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.
Holger

7

Інший аспект полягає в тому, що якщо змінна оголошена остаточною в тілі методу, вона буде відрізнятися від остаточної змінної, переданої як параметр.

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"може бути взаємозамінним без проблем. Інакше, якщо aне визначено finalабо воно визначено, finalале його значення присвоюється під час виконання (як у прикладі вище, де final - aпараметр), компілятор нічого не знає перед його використанням. Отже, об’єднання відбувається під час виконання, і генерується новий рядок, не використовуючи пул стажерів.


В основному поведінка така: якщо компілятор знає, що змінна є константою, можна використовувати її так само, як використання константи.

Якщо змінна не визначена кінцевою (або вона остаточна, але її значення визначено під час виконання), немає підстав для компілятора обробляти її як константу, також якщо її значення дорівнює константі, а значення ніколи не змінюється.


4
У цьому немає нічого дивного :)
dbl

2
Це інший аспект питання.
Давіде Лоренцо МАРІНО

5
finelключове слово, застосоване до параметра, має семантику, що відрізняється від finalзастосованої до локальної змінної тощо ...
dbl

6
Використання параметра тут непотрібне заплутування. Ви можете просто зробити final String a; a = "a";та отримати таку ж поведінку
yawkat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.