Почему этот код Java компилируется?

96

В области метода или класса следующая строка компилируется (с предупреждением):

int x = x = 1;

В области класса, где переменные получают значения по умолчанию , следующее дает ошибку «неопределенная ссылка»:

int x = x + 1;

Разве не первая x = x = 1должна закончиться той же ошибкой «неопределенная ссылка»? А может вторая строчка int x = x + 1должна компилироваться? Или мне чего-то не хватает?

Марчин
источник
1
Если вы добавите ключевое слово staticв переменную области класса, например static int x = x + 1;, получите ли вы ту же ошибку? Потому что в C # есть разница, статическая она или нестатическая.
Jeppe Stig Nielsen
static int x = x + 1не работает на Java.
Marcin
1
в С # int a = this.a + 1;и int b = 1; int a = b + 1;в области видимости класса (и то, и другое нормально в Java) происходит сбой, вероятно, из-за §17.4.5.2 - «Инициализатор переменной для поля экземпляра не может ссылаться на создаваемый экземпляр». Я не знаю, разрешено ли это где-то явно, но static не имеет такого ограничения. В Java правила другие, и они static int x = x + 1не работают по той же причине, что int x = x + 1и
msam
Этот ответ с байт-кодом развеивает любые сомнения.
rgripper

Ответы:

101

tl; dr

Для полей , int b = b + 1является незаконным , поскольку bнелегальной вперед ссылка b. Вы можете исправить это, написавint b = this.b + 1 , что компилируется без нареканий.

Для локальных переменных , int d = d + 1является незаконным , поскольку dне инициализируется перед использованием. Это не относится к полям, которые всегда инициализируются по умолчанию.

Вы можете увидеть разницу, попытавшись скомпилировать

int x = (x = 1) + x;

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

Введение

Во-первых, правила для инициализаторов полей и локальных переменных очень разные. Итак, в этом ответе правила разбиты на две части.

Мы будем использовать эту тестовую программу повсюду:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

Объявление bнедействительно и завершается illegal forward referenceошибкой.
Объявление dнедействительно и не выполняется сvariable d might not have been initialized ошибкой.

Тот факт, что эти ошибки разные, должен указывать на то, что причины ошибок также разные.

Поля

Инициализаторы полей в Java регулируются JLS §8.3.2 , Инициализация полей.

Объем поля определяется в JLS §6.3 , Область декларации.

Соответствующие правила:

  • Объем объявления члена m объявленного или унаследованного от типа класса C (§8.1.6), - это все тело C, включая любые объявления вложенных типов.
  • Выражения инициализации для переменных экземпляра могут использовать простое имя любой статической переменной, объявленной в классе или унаследованной им, даже той, объявление которой текстуально происходит позже.
  • Использование переменных экземпляра, объявления которых появляются в текстовом виде после использования, иногда ограничивается, даже если эти переменные экземпляра находятся в области видимости. См. §8.3.2.3 для точных правил, регулирующих прямую ссылку на переменные экземпляра.

В §8.3.2.3 говорится:

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

  • Использование происходит в экземпляре (соответственно статическом) инициализаторе переменной C или в экземпляре (соответственно статическом) инициализаторе C.
  • Использование не находится в левой части задания.
  • Использование происходит через простое имя.
  • C - это самый внутренний класс или интерфейс, охватывающий использование.

Фактически вы можете ссылаться на поля до того, как они были объявлены, за исключением некоторых случаев. Эти ограничения предназначены для предотвращения кода вроде

int j = i;
int i = j;

от компиляции. В спецификации Java говорится, что «указанные выше ограничения предназначены для перехвата во время компиляции циклических или иным образом искаженных инициализаций».

К чему на самом деле сводятся эти правила?

Короче говоря, правила в основном говорят, что вы должны объявить поле перед ссылкой на это поле, если (а) ссылка находится в инициализаторе, (б) ссылка не назначается, (в) ссылка является простое имя (без таких квалификаторов this.) и (d) к нему нет доступа из внутреннего класса. Таким образом, прямая ссылка, удовлетворяющая всем четырем условиям, является недопустимой, но прямая ссылка, которая не работает хотя бы по одному условию, является допустимой.

int a = a = 1;компилируется , потому что он нарушает (б): ссылка a будет быть назначена, так что это законно , чтобы обратиться к aзаранее a«s полной декларации.

int b = this.b + 1также компилируется, потому что нарушает (c): ссылка this.bне является простым именем (оно дополнено this.). Эта странная конструкция по-прежнему четко определена, поскольку this.bимеет нулевое значение.

Итак, в основном, ограничения на ссылки на поля в инициализаторах препятствуют int a = a + 1успешной компиляции.

Заметим , что объявление поля int b = (b = 1) + bбудет не в состоянии компиляции, так как окончательный bпо - прежнему является незаконным опережающей ссылкой.

Локальные переменные

Объявления локальных переменных регулируются JLS §14.4 , Заявления объявления локальных переменных.

Сфера локальной переменной определяется в JLS §6.3 , Область декларации:

  • Объем объявления локальной переменной в блоке (§14.4) - это остальная часть блока, в котором появляется объявление, начиная со своего собственного инициализатора и включая любые дальнейшие деклараторы справа в операторе объявления локальной переменной.

Обратите внимание, что инициализаторы находятся в пределах объявляемой переменной. Так почему не int d = d + 1;компилируется?

Причина заключается в правиле Java об определенном назначении ( JLS §16 ). Определенное присвоение в основном говорит о том, что каждый доступ к локальной переменной должен иметь предшествующее присвоение этой переменной, а компилятор Java проверяет циклы и ветки, чтобы гарантировать, что присвоение всегда происходит до любого использования (вот почему определенному назначению посвящен целый раздел спецификации к нему). Основное правило:

  • Для каждого доступа к локальной переменной или пустой конечного поля x, xдолжны быть определенно присвоенной перед въездом, или происходит ошибка времени компиляции.

В int d = d + 1;, доступ к dразрешен для локальной переменной штраф, но, поскольку dне был назначен до dобращения, компилятор выдает ошибку. В int c = c = 1, c = 1происходит во- первых, который назначает c, а затем cинициализируется в результате этого присваивания (который является 1).

Обратите внимание, что из-за определенных правил присваивания объявление локальной переменной int d = (d = 1) + d; будет успешно скомпилировано (в отличие от объявления поля int b = (b = 1) + b), потому что dоно определенно присваивается к моменту достижения финала d.

nneonneo
источник
+1 для ссылок, однако я думаю, что вы ошиблись в этой формулировке: «int a = a = 1; компилируется, потому что он нарушает (b)», если он нарушает любое из 4 требований, он не будет компилироваться. Однако это не так , что IS на левой стороне уступке (двойной отрицательный в редакции JLS не очень помогает здесь). In int b = b + 1b находится справа (не слева) от задания, поэтому он нарушит это ...
msam
... В чем я не уверен, так это в следующем: эти 4 условия должны быть выполнены, если объявление не отображается в текстовом виде перед назначением, в этом случае я думаю, что объявление действительно отображается "текстуально" перед назначением int x = x = 1, в котором случае ничего из этого не применимо.
msam 05
@msam: Это немного сбивает с толку, но в основном вы должны нарушить одно из четырех условий, чтобы сделать прямую ссылку. Если ваша прямая ссылка удовлетворяет всем четырем условиям, это незаконно.
nneonneo 05
@msam: Кроме того, полное объявление вступает в силу только после инициализатора.
nneonneo 05
@mrfishie: Большой ответ, но в спецификации Java есть удивительная глубина. Вопрос не так прост, как кажется на первый взгляд. (Я когда-то написал компилятор подмножества Java, поэтому я хорошо знаком со многими тонкостями JLS).
nneonneo 09
86
int x = x = 1;

эквивалентно

int x = 1;
x = x; //warning here

пока в

int x = x + 1; 

сначала нам нужно вычислить, x+1но значение x неизвестно, поэтому вы получите ошибку (компилятор знает, что значение x неизвестно)

мсам
источник
4
Это плюс намек на право-ассоциативность от OpenSauce, который я нашел очень полезным.
TobiMcNamobi
1
Я думал, что возвращаемое значение присваивания - это присваиваемое значение, а не значение переменной.
zzzzBov
2
@zzzzBov правильный. int x = x = 1;эквивалентно int x = (x = 1), не x = 1; x = x; . Вы не должны получать предупреждение компилятора об этом.
nneonneo
int x = x = 1;s эквивалентно int x = (x = 1)из-за =
правоассоциативности
1
@nneonneo и int x = (x = 1)эквивалентен int x; x = 1; x = x;(объявление переменной, оценка инициализатора поля, присвоение переменной результату указанной оценки), отсюда и предупреждение
msam 05
41

Это примерно эквивалентно:

int x;
x = 1;
x = 1;

Во-первых, int <var> = <expression>;всегда эквивалентно

int <var>;
<var> = <expression>;

В этом случае ваше выражение - x = 1это тоже утверждение. x = 1является допустимым утверждением, поскольку переменная xуже была объявлена. Это также выражение со значением 1, которое затем xснова присваивается .

OpenSauce
источник
Хорошо, но если все прошло так, как вы говорите, почему в области класса второй оператор выдает ошибку? Я имею в виду, что вы получаете 0значение по умолчанию для целых чисел, поэтому я ожидал, что результат будет 1, а не undefined reference.
Marcin
Взгляните на ответ @izogfif. Вроде работает, потому что компилятор C ++ присваивает переменным значения по умолчанию. Так же, как Java делает для переменных уровня класса.
Marcin
@Marcin: в Java целые числа не инициализируются значением 0, когда они являются локальными переменными. Они инициализируются значением 0 только в том случае, если они являются переменными-членами. Итак, во второй строке x + 1нет определенного значения, потому что xон неинициализирован.
OpenSauce
1
@OpenSauce Но x это определяется как переменная члена ( «в области видимости класса»).
Джейкоб Рейл
@JacobRaihle: А, ладно, я не заметил эту часть. Я не уверен, что байт-код для инициализации переменной 0 будет сгенерирован компилятором, если он увидит, что есть явная инструкция инициализации. Здесь есть статья, в которой подробно рассказывается об инициализации классов и объектов, хотя я не думаю, что она решает именно эту проблему: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

В java или на любом современном языке назначение происходит справа.

Предположим, у вас есть две переменные x и y,

int z = x = y = 5;

Этот оператор действителен, и именно так компилятор их разбивает.

y = 5;
x = y;
z = x; // which will be 5

Но в твоем случае

int x = x + 1;

Компилятор выдал исключение, потому что он разделяется вот так.

x = 1; // oops, it isn't declared because assignment comes from the right.
Шри Харша Чилакапати
источник
предупреждение находится на x = x, а не x = 1
Асим Гаффар
8

int x = x = 1; не равно:

int x;
x = 1;
x = x;

javap снова помогает нам, это инструкции JVM, сгенерированные для этого кода:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

скорее:

int x = 1;
x = 1;

Нет причин выдавать неопределенную ссылку на ошибку. Теперь переменная используется до ее инициализации, поэтому этот код полностью соответствует спецификации. На самом деле здесь вообще нет использования переменных , только присваивания. И JIT-компилятор пойдет еще дальше, он устранит такие конструкции. Честно говоря, я не понимаю, как этот код связан со спецификацией JLS для инициализации и использования переменных. Без использования никаких проблем. ;)

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

Если мы напишем:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

равно:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

Самое правое выражение просто присваивается переменным одну за другой, без какой-либо рекурсии. Мы можем путать переменные как угодно:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Михаил
источник
7

В int x = x + 1;добавлении 1 к й, так что значениеx , это еще не создано.

Но in int x=x=1;будет компилироваться без ошибок, потому что вы присваиваете 1 x.

Аля Гамаль
источник
5

Ваш первый фрагмент кода содержит второй =вместо плюса. Это будет компилироваться где угодно, в то время как второй фрагмент кода не будет компилироваться ни в одном месте.

Джо Эллесон
источник
5

Во втором фрагменте кода x используется перед его объявлением, а в первом фрагменте кода он просто присваивается дважды, что не имеет смысла, но является допустимым.

WilQu
источник
5

Давайте разберемся по шагам, правильно ассоциативно

int x = x = 1

x = 1присвоить 1 переменной x

int x = xприсвоить самому себе значение x как int. Поскольку x ранее был назначен как 1, он сохраняет 1, хотя и в избыточной форме.

Это нормально компилируется.

int x = x + 1

x + 1, добавьте единицу к переменной x. Однако, если x не определен, это вызовет ошибку компиляции.

int x = x + 1, таким образом, эта строка компилирует ошибки, поскольку правая часть равных не будет компилировать добавление единицы к неназначенной переменной

Стивентноррис
источник
Нет, это правоассоциативно, когда есть два =оператора, поэтому это то же самое, что и int x = (x = 1);.
Jeppe Stig Nielsen
Ах, мои приказы прочь. Извини за это. Надо было сделать их наоборот. Я поменял его сейчас.
steventnorris
3

Второй int x=x=1- это компиляция, потому что вы присваиваете значение x, но в другом случае int x=x+1здесь переменная x не инициализируется. Помните, что в локальной переменной java не инициализируется значение по умолчанию. Примечание. Если он ( int x=x+1) также находится в области класса, тогда также будет выдана ошибка компиляции, поскольку переменная не создается.

Крушна
источник
2
int x = x + 1;

успешно компилируется в Visual Studio 2008 с предупреждением

warning C4700: uninitialized local variable 'x' used`
Изогфиф
источник
2
Интересно. Это C / C ++?
Marcin
@Marcin: да, это C ++. @msam: извините, я думаю, что видел cвместо тега, javaно, видимо, это был другой вопрос.
izogfif
Он компилируется, потому что в C ++ компиляторы назначают значения по умолчанию для примитивных типов. Используйте bool y;и y==trueвернет false.
Шри Харша Чилакапати
@SriHarshaChilakapati, это какой-то стандарт в компиляторе C ++? Потому что, когда я компилирую void main() { int x = x + 1; printf("%d ", x); }в Visual Studio 2008, в Debug я получаю исключение, Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.а в Release я получаю номер, 1896199921напечатанный в консоли.
izogfif
1
@SriHarshaChilakapati Говоря о других языках: в C # для staticполя (статическая переменная уровня класса) применяются те же правила. Например, поле, объявленное как public static int x = x + 1;компилируемое без предупреждения в Visual C #. Возможно то же самое на Java?
Jeppe Stig Nielsen
2

x не инициализируется в x = x + 1;.

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

См. Примитивные типы данных

Мохан Радж Б.
источник
3
Необходимость инициализировать переменные перед использованием их значений не имеет ничего общего со статической типизацией. Статически типизированный: вам нужно объявить, к какому типу относится переменная. Инициализировать перед использованием: оно должно иметь доказуемое значение, прежде чем вы сможете его использовать.
Джон Брайт
@JonBright: необходимость объявлять типы переменных также не имеет ничего общего со статической типизацией. Например, есть статически типизированные языки с выводом типа.
hammar 05
@hammar, на мой взгляд, вы можете аргументировать это двумя способами: с помощью вывода типа вы неявно объявляете тип переменной таким образом, чтобы система могла сделать вывод. Или вывод типа - это третий способ, при котором переменные не типизируются динамически во время выполнения, а находятся на уровне исходного кода, в зависимости от их использования и сделанных таким образом выводов. В любом случае утверждение остается верным. Но ты прав, я не думал о других системах типов.
Джон Брайт
2

Строка кода не компилируется с предупреждением из-за того, как код действительно работает. Когда вы запускаете код int x = x = 1, Java сначала создает переменную x, как определено. Затем запускается код присвоения ( x = 1). Поскольку xон уже определен, в системе нет ошибок, для которых установлено xзначение 1. Это возвращает значение 1, потому что теперь это значение x. Таким образом, xтеперь окончательно установлено значение 1.
Java в основном выполняет код, как если бы он был следующим:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Тем не менее, в своей второй части кода, int x = x + 1, то + 1утверждение требует , xчтобы определить, что к тому времени это не так . Поскольку операторы присваивания всегда означают, что код справа от =них запускается первым, код завершится ошибкой, поскольку xон не определен. Java будет запускать такой код:

int x;
x = x + 1; // this line causes the error because `x` is undefined
могила
источник
-1

Комплиер читал утверждения справа налево, а мы сделали обратное. Поэтому сначала это раздражало. Сделайте это привычкой читать операторы (код) справа налево, у вас не будет такой проблемы.

Рамиз Уддин
источник