Почему это входит в бесконечный цикл?


493

У меня есть следующий код:

public class Tests {
    public static void main(String[] args) throws Exception {
        int x = 0;
        while(x<3) {
            x = x++;
            System.out.println(x);
        }
    }
}

Мы знаем, что он должен был написать просто x++или x=x+1, но x = x++он должен сначала приписать xсамому себе, а затем увеличить его. Почему xпродолжать с 0ценностью?

--Обновить

Вот байт-код:

public class Tests extends java.lang.Object{
public Tests();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[])   throws java.lang.Exception;
  Code:
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   iconst_3
   4:   if_icmpge   22
   7:   iload_1
   8:   iinc    1, 1
   11:  istore_1
   12:  getstatic   #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   15:  iload_1
   16:  invokevirtual   #3; //Method java/io/PrintStream.println:(I)V
   19:  goto    2
   22:  return

}

Я прочитаю инструкции, чтобы попытаться понять ...


8
Я подозреваю, что происходит: 1. загрузить х в регистр (= 0); 2. приращение х (х = 1); 3. сохранить значение регистра в x (x = 0). В C / C ++ это было бы неопределенным поведением, потому что нет формальной точки последовательности для определения порядка 2 и 3. Надеюсь, кто-то может процитировать вам что-то эквивалентное из спецификации Java.
Руп

19
Мы попробовали это в C ++, чтобы увидеть, что произойдет, и он печатает 1,2,3 и завершает работу. Я этого не ожидал. Я предполагаю, что это зависит от компилятора, так как это неопределенное поведение. Мы использовали GNU G ++.
оплакивать

13
@saj x++- постинкрементное ; x=присвоение результата ; результат от x++оригинального x(и есть побочный эффект прироста, но это не меняет результата), так что это может быть истолковано какvar tmp = x; x++; x = tmp;
Марк Gravell

5
Теперь, когда у меня есть популярный вопрос, я сожалею, что (повторные) ответы не прекращаются, даже после того, как был выбран правильный ответ. Мой экран «Недавние действия» полон одних и тех же ответов, и скоро будет ...
Том Брито,

3
@Rob Vermeulen Вы можете прочитать полный вопрос, прежде чем комментировать ..;) Это был код, созданный моим студентом, и мне было любопытно, почему так происходит.
Том Брито

Ответы:


354

Примечание : Первоначально я написал код C # в этом ответе в целях иллюстрации, поскольку C # позволяет передавать intпараметры по ссылке с refключевым словом. Я решил обновить его с помощью действующего легального Java-кода, используя первый MutableIntкласс, который я нашел в Google, чтобы приблизиться к тому, что refделает в C #. Я не могу действительно сказать, помогает ли это или ранит ответ. Я скажу, что лично я не столько занимался разработкой Java; поэтому, насколько я знаю, может быть гораздо больше идиоматических способов проиллюстрировать этот момент.


Возможно, если мы напишем метод, чтобы сделать эквивалент того, что x++он делает, это станет понятнее.

public MutableInt postIncrement(MutableInt x) {
    int valueBeforeIncrement = x.intValue();
    x.add(1);
    return new MutableInt(valueBeforeIncrement);
}

Правильно? Увеличьте переданное значение и верните исходное значение: это определение оператора постинкремента.

Теперь давайте посмотрим, как это проявляется в вашем примере кода:

MutableInt x = new MutableInt();
x = postIncrement(x);

postIncrement(x)делает то, что? Приращения xда. И затем возвращает то, что x было до приращения . Это возвращаемое значение затем присваивается x.

Таким образом, порядок значений присваивается x0, затем 1, затем 0.

Это может быть еще яснее, если мы переписаем выше:

MutableInt x = new MutableInt();    // x is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
x = temp;                           // Now x is 0 again.

Ваша зацикленность на том факте, что когда вы заменяете xв левой части вышеприведенного задания на y«вы можете видеть, что сначала он увеличивает x, а позже приписывает его y», меня смущает. Это не xто, что присваивается y; это значение, ранее присвоенноеx . Действительно, инъекция yделает вещи ничем не отличающимися от сценария выше; мы просто получили:

MutableInt x = new MutableInt();    // x is 0.
MutableInt y = new MutableInt();    // y is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
y = temp;                           // y is still 0.

Так что понятно: x = x++эффективно не меняет значение х. Это всегда приводит к тому, что x принимает значения x 0 , затем x 0 + 1, а затем снова x 0 .


Обновление : Между прочим, чтобы вы не сомневались, что xкогда-либо будет присвоено 1 "между" операцией приращения и назначением в примере выше, я собрал короткую демонстрацию, чтобы проиллюстрировать, что это промежуточное значение действительно "существует", хотя оно и будет никогда не будет "замечен" в исполняющем потоке.

Демонстрация вызывает x = x++;в цикле, в то время как отдельный поток непрерывно выводит значение xна консоль.

public class Main {
    public static volatile int x = 0;

    public static void main(String[] args) {
        LoopingThread t = new LoopingThread();
        System.out.println("Starting background thread...");
        t.start();

        while (true) {
            x = x++;
        }
    }
}

class LoopingThread extends Thread {
    public @Override void run() {
        while (true) {
            System.out.println(Main.x);
        }
    }
}

Ниже приведена выдержка из вышеприведенной программы. Обратите внимание на нерегулярное появление 1 и 0.

Начало фоновой темы ...
0
0
1
1
0
0
0
0
0
0
0
0
0
0
1
0
1

1
Вам не нужно создавать класс для передачи по ссылке в Java (хотя это, безусловно, будет работать). Вы можете использовать Integerкласс, который является частью стандартной библиотеки, и он даже имеет преимущество в том, что он автоматически упакован в и из int почти прозрачно.
rmeador

3
@rmeador Integer является неизменным, поэтому вы все еще не можете изменить его значение. AtomicInteger, однако, является изменчивым.
ILMTitan

5
@Dan: Между прочим, xв вашем последнем примере должно быть объявлено volatile, в противном случае это неопределенное поведение, а просмотр 1s зависит от реализации.
Axtavt

4
@burkestar: Я не думаю, что в этом случае эта ссылка вполне уместна, поскольку это вопрос Java и (если я не ошибаюсь) поведение в C ++ на самом деле не определено.
Дэн Тао

5
@Tom Brito - в Си это не определено ... это ++ можно сделать до или после назначения. С практической точки зрения, может существовать компилятор, который делает то же самое, что и Java, но вы бы не хотели на него ставить.
детально

170

x = x++ работает следующим образом:

  • Сначала он оценивает выражение x++. Оценка этого выражения производит значение выражения (которое является значением xдо приращения) и увеличивается x.
  • Позже он присваивает значение выражения x, перезаписывая увеличенное значение.

Итак, последовательность событий выглядит следующим образом (это фактический декомпилированный байт-код, созданный javap -cмоими комментариями):

   8: iload_1 // Запомним текущее значение x в стеке
   9: iinc 1, 1 // Инкремент x (не меняет стек)
   12: istore_1 // Записать запомненное значение из стека в x

Для сравнения x = ++x:

   8: iinc 1, 1 // Приращение х
   11: iload_1 // Вставить значение x в стек
   12: istore_1 // Вывести значение из стека в x

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

2
@ Хотя в этом и заключается суть - потому что все это одна последовательность, она делает вещи в неочевидном (и, вероятно, неопределенном) порядке. Пытаясь проверить это, вы добавляете точку последовательности и получаете другое поведение.
Руп

Относительно вашего вывода байт-кода: обратите внимание, что iincприращение переменной не увеличивает значение стека и не оставляет значения в стеке (в отличие от почти всех других арифметических операций). Вы можете добавить код, сгенерированный ++xдля сравнения.
Anon

3
@Rep Это не может быть определено в C или C ++, но в Java, это хорошо определено.
ILMTitan

1
Интересная статья angelikalanger.com/Articles/VSJ/SequencePoints/…
Jaydee

104

Это происходит потому, что значение xвообще не увеличивается.

x = x++;

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

int temp = x;
x++;
x = temp;

Объяснение:

Давайте посмотрим на байт-код для этой операции. Рассмотрим пример класса:

class test {
    public static void main(String[] args) {
        int i=0;
        i=i++;
    }
}

Теперь, запустив дизассемблер класса, мы получим:

$ javap -c test
Compiled from "test.java"
class test extends java.lang.Object{
test();
  Code:
   0:    aload_0
   1:    invokespecial    #1; //Method java/lang/Object."<init>":()V
   4:    return

public static void main(java.lang.String[]);
  Code:
   0:    iconst_0
   1:    istore_1
   2:    iload_1
   3:    iinc    1, 1
   6:    istore_1
   7:    return
}

Теперь Java VM основана на стеке, что означает, что для каждой операции данные будут помещаться в стек, а из стека данные будут появляться для выполнения операции. Существует также другая структура данных, обычно массив для хранения локальных переменных. Локальным переменным присваиваются идентификаторы, которые являются просто индексами массива.

Давайте посмотрим на мнемонику в main()методе:

  • iconst_0: Постоянное значение 0 помещается в стек.
  • istore_1: Верхний элемент стека выталкивается и сохраняется в локальной переменной с индексом, 1
    который есть x.
  • iload_1: Значение в месте , 1которое является значением , x которое является 0, выталкивается в стек.
  • iinc 1, 1: Значение в ячейке памяти 1увеличивается на 1. Так xтеперь и становится 1.
  • istore_1: Значение в верхней части стека сохраняется в ячейке памяти 1. То есть 0назначается x перезапись его увеличенного значения.

Следовательно, значение xне изменяется, что приводит к бесконечному циклу.


5
На самом деле он увеличивается (это значение ++), но переменная перезаписывается позже.
Progman

10
int temp = x; x = x + 1; x = temp;лучше не использовать тавтологию в вашем примере.
Скотт Чемберлен

53
  1. Префиксная нотация будет увеличивать переменную ДО того, как выражение будет оценено.
  2. Постфиксная запись будет увеличиваться ПОСЛЕ вычисления выражения.

Однако " =" имеет более низкий приоритет оператора, чем " ++".

Поэтому x=x++;следует оценить следующим образом

  1. x подготовлен к назначению (оценен)
  2. x порядковым
  3. Предыдущее значение xприсваивается x.

Это лучший ответ. Некоторая разметка помогла бы ей выделиться немного больше.
Джастин Форс

1
Это не верно. Это не о приоритете. ++имеет более высокий приоритет, чем =в C и C ++, но оператор не определен в этих языках.
Мэтью Флэшен

2
Оригинальный вопрос о Java
Jaydee

34

Ни один из ответов, на которых нет места, так что здесь идет:

Когда вы пишете int x = x++, вы не присваиваете xсебе новое значение, а присваиваете xвозвращаемое значение x++выражения. Который, оказывается, является первоначальной ценностью x, как намекает в ответе Колин Кокрейн .

Для удовольствия протестируйте следующий код:

public class Autoincrement {
        public static void main(String[] args) {
                int x = 0;
                System.out.println(x++);
                System.out.println(x);
        }
}

Результат будет

0
1

Возвращаемым значением выражения является начальное значение x, равное нулю. Но позже, читая значение x, мы получаем обновленное значение, то есть единицу.


Я постараюсь понять строки байт-кода, посмотреть мое обновление, так что все будет ясно .. :)
Том Брито

Использование println () очень помогло мне понять это.
ErikE

29

Это уже хорошо объяснили другие. Я просто включаю ссылки на соответствующие разделы спецификации Java.

х = х ++ это выражение. Java будет следовать порядку оценки . Сначала он вычислит выражение x ++, которое будет увеличивать x и устанавливать значение результата в предыдущее значение x . Затем он присваивает результат выражения переменной x. В конце х возвращается к своему предыдущему значению.


1
+1. Это, безусловно, лучший ответ на вопрос: «Почему?»
Мэтью Флешен

18

Это утверждение:

x = x++;

оценивает так:

  1. Нажмите xна стек;
  2. Приращение x;
  3. Поп xиз стека.

Таким образом, значение не изменилось. Сравните это с:

x = ++x;

который оценивается как:

  1. Приращение x;
  2. Нажмите xна стек;
  3. Поп xиз стека.

То, что вы хотите, это:

while (x < 3) {
  x++;
  System.out.println(x);
}

13
Определенно правильная реализация, но вопрос «почему?».
p.campbell

1
Исходный код использовал постинкремент x, а затем присвоил его x. x будет связан с x до приращения, поэтому он никогда не изменит значения.
WKL

5
@cletus Я не downvoter, но ваш первоначальный ответ не содержал объяснения. Он просто сказал «х ++».
Петар Минчев

4
@cletus: Я не понизил голос, но ваш ответ изначально был просто x++фрагмент кода.
p.campbell

10
Объяснение тоже неверно. Если код сначала назначил x для x, а затем увеличил x, он будет работать нормально. Просто измените x++;свое решение на, x=x; x++;и вы делаете то, что, как вы утверждаете, делает исходный код.
Wooble

10

Ответ довольно прост. Это связано с тем, как оцениваются вещи. x++возвращает значение, а xзатем увеличивается x.

Следовательно, значение выражения x++равно 0. Таким образом, вы назначаете x=0каждый раз в цикле. Конечно, x++увеличивает это значение, но это происходит до назначения.


1
Ух ты, на этой странице так много подробностей, когда ответ короткий и простой, т.е. этот.
Чарльз Гудвин

8

С http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.html

Операторы увеличения / уменьшения могут применяться до (префикс) или после (постфикс) операнда. Код результата ++; и ++ результат; оба будут заканчиваться в результате увеличения на единицу. Единственное отличие состоит в том, что версия префикса (результат ++) оценивается в увеличенное значение, тогда как версия постфикса (результат ++) оценивается в исходное значение . Если вы просто выполняете простое увеличение / уменьшение, не имеет значения, какую версию вы выберете. Но если вы используете этот оператор в части большего выражения, то тот, который вы выберете, может иметь существенное значение.

Для иллюстрации попробуйте следующее:

    int x = 0;
    int y = 0;
    y = x++;
    System.out.println(x);
    System.out.println(y);

Который будет печатать 1 и 0.


1
Проблема не в результатах оценки, а в порядке магазинов.
Руп

2
Я не согласен. Если x = 0, то x ++ вернет 0. Поэтому x = x ++ приведет к x = 0.
Колин Кокрейн

Руп прав насчет этого. Это порядок магазинов, который обсуждается в данном конкретном случае. y = x ++ - это не то же самое, что x = x ++; На последнем x присваивается 2 значения в одном выражении. Левой руке x присваивается результат вычисления выражения x ++, который равен 0. Правая часть x увеличивается до 1. В каком порядке происходит это 2-х присваивание, в чем проблема. Из предыдущих постов ясно, что это работает так: eval = x ++ => eval == 0: увеличение вправо x => x == 1: слева x = eval => x == 0
Майкл Экока

7

Вы эффективно получаете следующее поведение.

  1. возьмите значение x (равное 0) как «результат» с правой стороны
  2. увеличить значение x (поэтому x теперь равно 1)
  3. присвойте результат правой части (который был сохранен как 0) x (x теперь 0)

Идея состоит в том, что оператор постинкрементного увеличения (x ++) увеличивает значение этой переменной ПОСЛЕ возвращения значения для использования в уравнении, в котором он используется.

Изменить: немного добавить из-за комментария. Рассмотрите это как следующее.

x = 1;        // x == 1
x = x++ * 5;
              // First, the right hand side of the equation is evaluated.
  ==>  x = 1 * 5;    
              // x == 2 at this point, as it "gave" the equation its value of 1
              // and then gets incremented by 1 to 2.
  ==>  x = 5;
              // And then that RightHandSide value is assigned to 
              // the LeftHandSide variable, leaving x with the value of 5.

Хорошо, но что определяет порядок шагов 2 и 3?
Руп

@Rup - язык определяет это. Сначала вычисляется правая часть уравнения (в данном случае «x ++»), а результат присваивается переменной с левой стороны. Вот как работает язык. Что касается «x ++», «возвращающего» x для уравнения, то так работает оператор приращения postfix (возвращает значение x, затем увеличивает его). Если бы это был «--x», то это было бы (увеличьте x, затем верните значение). Возвращение не то слово, но вы поняли.
RHSeeger

7

Вам не нужен машинный код, чтобы понять, что происходит.

Согласно определениям:

  1. Оператор присваивания вычисляет выражение правой части и сохраняет его во временной переменной.

    1.1. Текущее значение x копируется во временную переменную

    1.2. х увеличивается сейчас.

  2. Затем временная переменная копируется в левую часть выражения, что случайно равно x! Вот почему старое значение x снова копируется в себя.

Это довольно просто.


5

Это потому, что в этом случае оно никогда не увеличивается. x++сначала будет использовать его значение перед увеличением, как в этом случае это будет выглядеть так:

x = 0;

Но если вы ++x;это сделаете, это увеличится.


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

1
@Tom: посмотрите мой ответ - в тесте я показываю, что x ++ на самом деле возвращает старое значение x. Вот где это ломается.
Роберт Мунтяну

«если вы делаете тест» - некоторые люди думают, что тест, написанный на C, говорит нам, что будет делать Java, когда он даже не скажет нам, что будет делать C.
Джим Балтер

3

Значение остается равным 0, потому что значение x++равно 0. В этом случае не имеет значения, xувеличивается значение или нет, присваивание x=0выполняется. Это перезапишет временное увеличенное значение x(которое было 1 для «очень короткого времени»).


Но x ++ - это почтовая операция. Таким образом, х должен быть увеличен после завершения назначения.
Сагар V

1
@Sagar V: только для выражения x++, а не для всего заданияx=x++;
Progman

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

1

Это работает так, как вы ожидаете от другого. Это разница между префиксом и постфиксом.

int x = 0; 
while (x < 3)    x = (++x);

1

Думайте о x ++ как о вызове функции, которая «возвращает» то, что X было до приращения (поэтому оно называется постинкрементом).

Таким образом, порядок операций:
1: кэшировать значение x до приращения
2: приращение x
3: возвращать кэшированное значение (x до его приращения)
4: возвращаемое значение присваивается x


Хорошо, но что определяет порядок шагов 3 и 4?
Руп

«возвращает то, что было X до приращения» неправильно, смотрите мое обновление
Том Брито

В действительности шаги 3 и 4 не являются отдельными операциями - на самом деле это не вызов функции, которая возвращает значение, а просто помогает думать об этом таким образом. Когда у вас есть присвоение, правая часть «оценивается», тогда результат присваивается левой стороне, результат оценки можно рассматривать как возвращаемое значение, поскольку это помогает вам понять порядок операций, но на самом деле это не так. ,
Джабботт

Ой, правда. Я имел в виду шаги 2 и 4 - почему возвращаемое значение хранится поверх увеличенного значения?
Руп

1
Это часть определения операции присваивания, сначала полностью оценивается правая часть, затем результат присваивается левой стороне.
Джабботт

1

Когда ++ находится на правой стороне, результат возвращается до увеличения числа. Измените на ++ x, и это было бы хорошо. Java оптимизировал бы это для выполнения одной операции (присваивания x x), а не приращения.


1

Ну, насколько я вижу, ошибка возникает из-за присваивания, переопределяющего увеличенное значение значением до увеличения, то есть оно отменяет приращение.

В частности, выражение «x ++» имеет значение «x» до приращения, а не «++ x», которое имеет значение «x» после приращения.

Если вы заинтересованы в изучении байт-кода, мы рассмотрим три строки:

 7:   iload_1
 8:   iinc    1, 1
11:  istore_1

7: iload_1 # Поместит значение 2-й локальной переменной в стек
8: iinc 1,1 # увеличит 2-ю локальную переменную на 1, обратите внимание, что стек остается нетронутым!
9: istore_1 # Извлечет вершину стека и сохранит значение этого элемента во 2-й локальной переменной
(Вы можете прочитать эффекты каждой инструкции JVM здесь )

Вот почему приведенный выше код будет зацикливаться бесконечно, в то время как версия с ++ x не будет. Байт-код для ++ x должен выглядеть совсем иначе, насколько я помню из компилятора Java 1.3, который я написал чуть больше года назад, байт-код должен выглядеть примерно так:

iinc 1,1
iload_1
istore_1

Так что, просто поменяв местами две первые строки, измените семантику так, чтобы значение, оставленное на вершине стека, после приращения (то есть «значение» выражения) было значением после приращения.


1
    x++
=: (x = x + 1) - 1

Так:

   x = x++;
=> x = ((x = x + 1) - 1)
=> x = ((x + 1) - 1)
=> x = x; // Doesn't modify x!

В то время как

   ++x
=: x = x + 1

Так:

   x = ++x;
=> x = (x = x + 1)
=> x = x + 1; // Increments x

Конечно, конечный результат такой же, как просто x++;или ++x;на отдельной строке.



0

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

Заметьте, из байт-кода Тома, ключевые строки 7, 8 и 11. Строка 7 загружает х в стек вычислений. Строка 8 увеличивает x. Строка 11 сохраняет значение из стека обратно в x. В обычных случаях, когда вы не присваиваете значения обратно самим себе, я не думаю, что была бы какая-либо причина, по которой вы не могли бы загрузить, сохранить, затем увеличить. Вы бы получили тот же результат.

Например, предположим, у вас был более нормальный случай, когда вы написали что-то вроде: z = (x ++) + (y ++);

Будь то сказано (псевдокод, чтобы пропустить технические детали)

load x
increment x
add y
increment y
store x+y to z

или

load x
add y
store x+y to z
increment x
increment y

должно быть неуместно. Я думаю, что любая реализация должна быть действительной.

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


0

Я думаю, потому что в Java ++ имеет более высокий приоритет, чем = (присваивание) ... Так ли это? Посмотрите на http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html ...

Точно так же, если вы пишете x = x + 1 ... + имеет более высокий приоритет, чем = (назначение)


Это не вопрос приоритета. ++имеет более высокий приоритет, чем =в C и C ++, но оператор не определен.
Мэтью Флэшен

0

x++Выражение имеет значение x. ++Часть влияет на стоимость после оценки , а не после заявления . так x = x++эффективно переводится на

int y = x; // evaluation
x = x + 1; // increment part
x = y; // assignment

0

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


0

Это происходит потому, что пост увеличивается. Это означает, что переменная увеличивается после вычисления выражения.

int x = 9;
int y = x++;

х теперь 10, а у 9, значение х перед его увеличением.

См. Больше в Определении Пост Прироста .


1
Ваш x/ yпример отличается от реального кода, и разница актуальна. Ваша ссылка даже не упоминает Java. Для двух языков это делает упомянуть, заявление в вопросе не определено.
Мэтью Флэшен

0

Проверьте код ниже,

    int x=0;
    int temp=x++;
    System.out.println("temp = "+temp);
    x = temp;
    System.out.println("x = "+x);

выход будет,

temp = 0
x = 0

post incrementозначает увеличить значение и вернуть значение до приращения . Именно поэтому значение tempявляется 0. Так что, если temp = iи это в цикле (за исключением первой строки кода). прямо как в вопросе !!!!


-1

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.