Чи остаточно не визначено?


186

По-перше, головоломка: Що друкує наступний код?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

Відповідь:

0

Спойлери нижче.


Якщо ви друкуєте Xмасштабним (довгим) та перевизначаєте X = scale(10) + 3, друк буде X = 0тоді X = 3. Це означає, що Xтимчасово встановлено 0та пізніше встановлено 3. Це порушення final!

Статичний модифікатор у поєднанні з кінцевим модифікатором також використовується для визначення констант. Кінцевий модифікатор вказує, що значення цього поля не може змінюватися .

Джерело: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [наголос додано]


Моє запитання: це помилка? Чи finalне визначено?


Ось код, який мене цікавить. XПрисвоюється два різних значення: 0і 3. Я вважаю, що це порушення final.

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

Це питання позначено як можливий дублікат порядку статичного остаточного ініціалізації поля Java . Я вважаю, що це питання не є дублікатом, оскільки інше питання стосується порядку ініціалізації, тоді як моє запитання стосується циклічної ініціалізації в поєднанні з finalтегом. З іншого питання я не зміг би зрозуміти, чому код у моєму запитанні не робить помилки.

Це особливо зрозуміло, дивлячись на результат, який отримує ernesto: коли aпозначений тегом final, він отримує такий результат:

a=5
a=5

що не включає головну частину мого питання: Як finalзмінна змінює свою змінну?


17
Такий спосіб посилання на Xчлен схожий на посилання на члена підкласу до закінчення конструктора суперкласу, це ваша проблема, а не визначення final.
Даніу

4
З JLS:A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Іван

1
@Ivan, мова йде не про постійну, а про змінну екземпляра. Але чи можете ви додати главу?
AxelH

9
Тільки як зауваження: Ніколи нічого не робимо у виробничому коді. Для всіх це дуже заплутано, якщо хтось починає експлуатувати лазівки в JLS.
Забузар

13
Ви можете створити цю саму ситуацію і в C #. C # обіцяє, що петлі в постійних деклараціях будуть зафіксовані під час компіляції, але не дають подібних обіцянок щодо декларацій, які читаються лише зараз , і на практиці ви можете потрапити в ситуації, коли початкове нульове значення поля спостерігається іншим ініціалізатором поля. Якщо вам боляче, коли ви це робите, не робіть цього . Компілятор вас не врятує.
Ерік Ліпперт

Відповіді:


217

Дуже цікава знахідка. Щоб зрозуміти це, нам потрібно скористатися специфікацією мови Java ( JLS ).

Причина в тому, що finalдозволяє лише одне завдання . Типовим значенням, однак, не є призначення . Фактично кожна така змінна ( змінна класу, змінна інстанція, компонент масиву) вказує на її значення за замовчуванням з початку, перед призначенням . Перше призначення потім змінює посилання.


Змінні класу та значення за замовчуванням

Погляньте на наступний приклад:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

Ми не призначали явно значення x, хоча воно вказує на nullце значення за замовчуванням. Порівняйте це з §4.12.5 :

Початкові значення змінних

Кожна змінна клас, змінна інстанція або компонент масиву ініціалізуються зі значенням за замовчуванням, коли воно створюється ( §15.9 , §15.10.2 )

Зауважте, що це стосується лише таких змінних, як у нашому прикладі. Він не містить місцевих змінних, див. Наступний приклад:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

З того ж пункту JLS:

Локальна змінна ( §14.4 , §14.14 ) має бути явно присвоєно значення , перш ніж вона використовується, або ініціалізації ( §14.4 ) або привласнення ( §15.26 ), таким чином , що можна перевірити , використовуючи правила для певного присвоювання ( § 16 (Визначене призначення) ).


Кінцеві змінні

Тепер ми розглянемо finalз §4.12.4 :

остаточні змінні

Змінна може бути оголошена остаточною . Остаточна змінна може бути тільки призначені один раз . Це помилка часу компіляції, якщо присвоєно остаточну змінну, якщо вона точно не призначена безпосередньо перед призначенням ( §16 (Визначення призначення) ).


Пояснення

Тепер повернемось до вашого прикладу, трохи зміненого:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

Він виводить

Before: 0
After: 1

Згадайте, що ми дізналися. Всередині методу assignзмінній Xще не було призначено значення. Отже, воно вказує на його значення за замовчуванням, оскільки це змінна категорія, і згідно з JLS, ці змінні завжди одразу вказують на їх значення за замовчуванням (на відміну від локальних змінних). Після assignметоду змінній Xприсвоюється значення, 1і finalми не можемо її більше змінити. Отже, наступне не працюватиме через final:

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

Приклад в JLS

Завдяки @Andrew я знайшов абзац JLS, який стосується саме цього сценарію, він також демонструє це.

Але спочатку давайте подивимось

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

Чому це не дозволено, тоді як доступ від методу є? Погляньте на пункт 8.3.3, який говорить про те, коли доступ до полів обмежений, якщо поле ще не було ініціалізовано.

У ньому перелічено деякі правила, що стосуються змінних класу:

Для посилання простою назвою на змінну класу, fоголошену в класі або інтерфейсі C, це помилка часу компіляції, якщо :

  • Посилання з’являється або в ініціалізаторі змінної класу, Cабо в статичному ініціалізаторі C( §8.7 ); і

  • Посилання з'являється або в ініціалізаторі fвласного декларатора, або в точці зліва від fдекларатора; і

  • Посилання не на лівій частині виразу присвоєння ( §15.26 ); і

  • Найпотаємніший клас або інтерфейс, що додає посилання C.

Це просто, X = X + 1це потрапляє під ці правила, метод не доступний. Вони навіть перераховують цей сценарій і наводять приклад:

Доступ методами не перевіряється таким чином:

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

виробляє вихід:

0

тому що ініціалізатор змінної для iвикористання методу класу peek для доступу до значення змінної jраніше jбув ініціалізований ініціалізатором змінної, і в цей момент вона все ще має своє значення за замовчуванням ( §4.12.5 ).


1
@Andrew Так, змінна категорія, дякую. Так, це буде працювати , якби не деякі додаткові-правил , які обмежують доступ до такого: §8.3.3 . Погляньте на чотири точки, вказані для змінних класів (перший запис). Методний підхід у прикладі ОП не вловлюється цими правилами, тому ми можемо отримати доступ Xіз методу. Я б не заперечував проти цього. Це просто залежить від того, як саме JLS визначає речі, щоб детально працювати. Я ніколи не використовував би такий код, він просто використовує деякі правила в JLS.
Забузар

4
Проблема полягає в тому, що ви можете викликати екземпляри методів від конструктора, що, мабуть, не повинно було бути дозволене. З іншого боку, призначення місцевих жителів перед тим, як викликати супер, що було б корисно та безпечно, заборонено. Піди розберися.
Поновіть Моніку

1
@Andrew ти, мабуть, єдиний тут, хто фактично згадав forwards references(які теж є частиною JLS). це так просто, без цієї відповіді looong stackoverflow.com/a/49371279/1059372
Євген

1
"Перше призначення потім змінює посилання." У цьому випадку це не еталонний тип, а примітивний тип.
fabian

1
Ця відповідь правильна, якщо трохи довга. :-) Я думаю, що tl; dr полягає в тому, що ОП цитувало підручник, в якому сказано, що "[остаточне] поле не може змінитися", а не JLS. Хоча навчальні посібники Oracle досить хороші, вони не охоплюють усіх крайових випадків. Що стосується питання ОП, нам потрібно перейти до фактичного визначення JLS як остаточного - і це визначення не робить твердження (що ОП справедливо оскаржує), що значення остаточного поля ніколи не може змінитися.
ішавіт

22

Тут немає нічого спільного з фіналом.

Оскільки це рівень екземпляра або класу, він містить значення за замовчуванням, якщо ще нічого не присвоєно. Саме тому ви бачите, 0коли отримуєте доступ до нього, не призначаючи.

Якщо ви отримуєте доступ Xбез повного присвоєння, він містить значення за замовчуванням long, що є 0, отже, і результатами.


3
У цьому полягає складність у тому, що якщо ви не призначите значення, воно не буде присвоєно значенню за замовчуванням, але якщо ви використали його для призначення "остаточного" значення, воно буде ...
AxelH

2
@AxelH Я бачу, що ти маєш на увазі під цим. Але саме так має працювати інакше світ розвалюється;).
Суреш Атта

20

Не помилка.

Коли перший дзвінок до scaleтелефонується з

private static final long X = scale(10);

Це намагається оцінити return X * value. Xще не було призначено значення, і тому використовується значення за замовчуванням для a long(яке є 0).

Так , що рядок коду має значення X * 10те , 0 * 10що є 0.


8
Я не думаю, що це те, що ОП бентежить. Що бентежить X = scale(10) + 3. Оскільки Xпри посиланні з методу є 0. Але згодом це 3. Тож ОП вважає, що Xприсвоєно два різні значення, які б суперечили final.
Забузар

4
@Zabuza чи не це пояснити з « Він намагається оцінити return X * value. XНе було присвоєно значення ще й тому приймає значення за замовчуванням для longяких це 0. »? Не сказано X, що присвоєно значення за замовчуванням, але те, що X"замінено" (будь ласка, не цитуйте цей термін;)) за замовчуванням.
AxelH

14

Це зовсім не помилка, просто кажучи, це не незаконна форма пересилання посилань, більше того.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

Це просто дозволено Специфікацією.

Для прикладу це саме тут:

private static final long X = scale(10) + 3;

Ви робите пряме посилання на scaleте, що не є незаконним, як це було сказано раніше, але дозволяє отримати значення за замовчуванням X. знову ж таки, це дозволено Spec (якщо бути точніше, це не заборонено), тому це працює просто чудово


хороша відповідь! Мені просто цікаво, чому специфікація дозволяє складати другий випадок. Це єдиний спосіб бачити "непослідовний" стан остаточного поля?
Андрій Тобілко

@Andrew це турбує мене досить багато часу, я схильний думати, що це C ++ або C це робить (не уявляю, чи це правда)
Євген

@Andrew: Тому що інакше було б вирішити теорему неповноти Тьюрінга.
Джошуа

9
@ Джошуа: Я думаю, ви тут змішуєте кілька різних понять: (1) проблема зупинки, (2) проблема рішення, (3) теорема про незавершеність Годеля та (4) мови програмування, що завершуються Тьюрінгом. Автори-компілятори не намагаються вирішити проблему "чи визначена ця змінна, перш ніж її використовувати?" ідеально, тому що ця проблема еквівалентна вирішенню проблеми зупинки, і ми знаємо, що не можемо цього зробити.
Ерік Ліпперт

4
@EricLippert: Ха-ха. Усунення неповноти та проблеми зупинки займають те саме місце в моїй свідомості.
Джошуа

4

Члени рівня класу можуть бути ініціалізовані в коді в межах визначення класу. Скомпільований байт-код не може ініціалізувати вбудованих членів класу. (Члени інстанцій обробляються аналогічно, але це не стосується наданого питання.)

Коли пише щось подібне:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

Створений байт-код буде подібний до наступного:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Код ініціалізації розміщується в статичному ініціалізаторі, який запускається, коли завантажувач класів завантажує клас. З урахуванням цих знань ваш оригінальний зразок буде подібний до наступного:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM завантажує RecursiveStatic в якості точки входу банку.
  2. Коли завантажується визначення класу, завантажувач класів запускає статичний ініціалізатор.
  3. Ініціалізатор викликає функцію scale(10)для призначення static finalполя X.
  4. У scale(long)функції виконується в той час як клас частково инициализирован читання неініціалізованих значення з Xяких є за замовчуванням довгих або 0.
  5. Значення 0 * 10присвоюється Xі завантажувач класу завершує.
  6. JVM запускає загальнодоступний статичний недійсний основний метод виклику, scale(5)який множить 5 на тепер ініціалізоване Xзначення 0 повернення 0.

Статичне остаточне поле Xпризначається лише один раз, зберігаючи гарантію, що зберігається за finalключовим словом. Для наступного запиту щодо додавання 3 у призначенні, крок 5 вище стає оцінкою, 0 * 10 + 3яке є значенням, 3і основним методом буде надруковано результат, 3 * 5який є значенням 15.


3

Читання неініціалізованого поля об’єкта повинно призвести до помилки компіляції. На жаль для Java, це не так.

Я думаю, що фундаментальна причина, чому це так, «прихована» глибоко у визначенні того, як об’єкти інстанціюються та будуються, хоча деталі стандарту я не знаю.

У певному сенсі, фінал є неправильним визначенням, оскільки він навіть не досягає того, що його ціль обумовлена ​​цією проблемою. Однак якщо всі ваші класи правильно записані, у вас немає цієї проблеми. Значення всіх полів завжди задається у всіх конструкторах, і жоден об'єкт ніколи не створюється без виклику одного з його конструкторів. Це здається природним, поки вам не доведеться використовувати бібліотеку серіалізації.

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