Почему это входит в бесконечный цикл? - PullRequest
490 голосов
/ 30 сентября 2010

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

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

}

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

Ответы [ 26 ]

353 голосов
/ 30 сентября 2010

Примечание : Первоначально я разместил код 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.

Таким образом, порядок значений, присвоенных x, равен 0, затем 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, ипозже приписывает это "у меня поражает.Это не 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 принимать значения 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.

Starting background thread...
0
0
1
1
0
0
0
0
0
0
0
0
0
0
1
0
1
169 голосов
/ 30 сентября 2010

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

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

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

   8:   iload_1         // Remember current value of x in the stack
   9:   iinc    1, 1    // Increment x (doesn't change the stack)
   12:  istore_1        // Write remebered value from the stack to x

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

   8:   iinc    1, 1    // Increment x
   11:  iload_1         // Push value of x onto stack
   12:  istore_1        // Pop value from the stack to x
104 голосов
/ 30 сентября 2010

Это происходит потому, что значение 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 не изменяется, что приводит к бесконечному циклу.

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

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

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

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

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

Когда вы пишете 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, мы получаем обновленное значение, то есть единицу.

29 голосов
/ 30 сентября 2010

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

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

18 голосов
/ 30 сентября 2010

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

x = x++;

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

  1. Вставьте x в стек;
  2. Инкремент x;
  3. Поп x из стека.

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

x = ++x;

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

  1. Инкремент x;
  2. Вставить x в стек;
  3. Выскочить x из стека.

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

while (x < 3) {
  x++;
  System.out.println(x);
}
10 голосов
/ 02 октября 2010

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

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

8 голосов
/ 30 сентября 2010

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

7 голосов
/ 30 сентября 2010

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

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