Бесконечные циклы в Java

82

Посмотрите на следующий бесконечный whileцикл в Java. Это вызывает ошибку времени компиляции для оператора под ним.

while(true) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //Unreachable statement - compiler-error.

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

boolean b=true;

while(b) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.

Во втором случае также очевидно, что оператор после цикла недоступен, поскольку логическая переменная bистинна, но компилятор вообще не жалуется. Почему?


Изменить: следующая версия whileзастревает в бесконечном цикле как очевидное, но не выдает ошибок компилятора для оператора под ней, даже если ifусловие внутри цикла всегда falseи, следовательно, цикл никогда не может вернуться и может быть определен компилятором в время самой компиляции.

while(true) {

    if(false) {
        break;
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.

while(true) {

    if(false)  { //if true then also
        return;  //Replacing return with break fixes the following error.
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

while(true) {

    if(true) {
        System.out.println("inside if");
        return;
    }

    System.out.println("inside while"); //No error here.
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

Изменить: то же самое с ifи while.

if(false) {
    System.out.println("inside if"); //No error here.
}

while(false) {
    System.out.println("inside while");
    // Compiler's complain - unreachable statement.
}

while(true) {

    if(true) {
        System.out.println("inside if");
        break;
    }

    System.out.println("inside while"); //No error here.
}      

Следующая версия whileтакже застревает в бесконечном цикле.

while(true) {

    try {
        System.out.println("inside while");
        return;   //Replacing return with break makes no difference here.
    } finally {
        continue;
    }
}

Это связано с тем, что finallyблок всегда выполняется, даже если returnоператор встречается до него внутри самого tryблока.

Лев
источник
46
Какая разница? Очевидно, это просто особенность компилятора. Не беспокойтесь об этом.
CJ7,
17
Как другой поток может изменить локальную нестатическую переменную?
CJ7
4
Внутреннее состояние объекта может одновременно изменяться посредством отражения. Вот почему JLS требует, чтобы проверялись только окончательные (постоянные) выражения.
lsoliveira
6
Ненавижу эти глупые ошибки. Недоступный код должен быть предупреждением, а не ошибкой.
user606723
2
@ CJ7: Я бы не назвал это «функцией», и это делает очень утомительным (без причины) реализацию соответствующего компилятора Java. Наслаждайтесь привязкой к поставщику.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Ответы:

105

Компилятор может легко и недвусмысленно доказать, что первое выражение всегда приводит к бесконечному циклу, но не так просто для второго. В вашем примере с игрушкой это просто, но что, если:

  • содержимое переменной читалось из файла?
  • переменная не была локальной и могла быть изменена другим потоком?
  • переменная зависит от пользовательского ввода?

Компилятор явно не проверяет ваш более простой случай, потому что он полностью отказывается от этой дороги. Почему? Потому что это запрещено спецификацией намного сложнее . См. Раздел 14.21 :

(Кстати, мой компилятор делает жаловаться , когда переменная объявлена final.)

Уэйн
источник
26
-1 - Дело не в том, что может делать компилятор. Дело не в том, чтобы проверки были проще или сложнее. Это о том, что компилятору разрешено делать ... Спецификацией языка Java. Вторая версия кода является допустимой Java согласно JLS, поэтому ошибка компиляции будет неправильной.
Stephen C
10
@StephenC - Спасибо за эту информацию. К счастью, обновлено, чтобы отразить это.
Уэйн
Еще -1; ни одно из ваших «а что, если» здесь не применимо. Как бы то ни было, компилятор действительно может решить проблему - в терминах статического анализа это называется постоянным распространением и широко используется во многих других ситуациях. JLS - единственная причина здесь, и насколько сложно решить проблему, не имеет значения. Конечно, причина, по которой JLS написан таким образом, в первую очередь может быть связана со сложностью этой проблемы, хотя я лично подозреваю, что на самом деле это связано с тем, что сложно обеспечить стандартизованное решение для распространения констант в разных компиляторах.
Oak
2
@Oak - В своем ответе я признал, что «В игрушечном примере [OP] это просто» и, основываясь на комментарии Стивена, JLS является ограничивающим фактором. Я почти уверен, что мы согласны.
Уэйн
55

Согласно спецификациям , о операторах while сказано следующее.

Оператор while может нормально завершаться, если выполняется хотя бы одно из следующих условий:

  • Оператор while доступен, а выражение условия не является постоянным выражением со значением true.
  • Есть оператор достижимого прерывания, который завершает оператор while. \

Таким образом, компилятор скажет, что код, следующий за оператором while, недоступен, если условие while является константой с истинным значением или внутри while есть оператор break. Во втором случае, поскольку значение b не является константой, код, следующий за ним, не считается недоступным. По этой ссылке есть гораздо больше информации, чтобы дать вам более подробную информацию о том, что является и что не считается недоступным.

Кибби
источник
3
+1 за то, что отметили, что это не просто то, что авторы компилятора могут или не могут придумать - JLS сообщает им, что они могут и не могут считать недостижимыми.
yshavit
14

Потому что истина постоянна и b можно изменить в цикле.

надежда_is_grim
источник
1
Верно, но не имеет значения, поскольку b НЕ изменяется в цикле. Вы также можете утверждать, что цикл, использующий истину, может включать оператор break.
deworde
@deworde - пока есть шанс, что он может быть изменен (скажем, другим потоком), компилятор не может назвать это ошибкой. Вы не можете сказать, просто глядя на сам цикл, будет ли изменяться b во время выполнения цикла.
Питер Рекор
10

Поскольку анализировать состояние переменной сложно, поэтому компилятор в значительной степени отказался и позволяет вам делать то, что вы хотите. Кроме того, в Спецификации языка Java есть четкие правила о том, как компилятору разрешено обнаруживать недоступный код .

Есть много способов обмануть компилятор - еще один распространенный пример:

public void test()
{
    return;
    System.out.println("Hello");
}

что не сработает, так как компилятор поймет, что область недоступна. Вместо этого вы могли бы сделать

public void test()
{
    if (2 > 1) return;
    System.out.println("Hello");
}

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

кба
источник
6
Как указывалось в других ответах, это (напрямую) не связано с тем, что компилятору может быть легко или сложно обнаружить недостижимый код. JLS делает все возможное, чтобы указать правила для обнаружения недостижимых операторов. Согласно этим правилам, первый пример не является допустимой Java, а второй пример - допустимой Java. Вот и все. Компилятор просто реализует указанные правила.
Stephen C
Я не слежу за аргументом JLS. Действительно ли компиляторы обязаны превращать всю допустимую Java в байт-код? даже если будет доказано, что это бессмысленно.
emory
@emory Да, это было бы определением совместимого со стандартами браузера, который подчиняется стандарту и компилирует законный код Java. И если вы используете браузер, не соответствующий стандартам, хотя он может иметь несколько хороших функций, теперь вы полагаетесь на теорию занятого разработчика компилятора о том, что такое «разумное» правило. Это действительно ужасный сценарий: «Зачем кому-то нужно использовать ДВА условных выражения в одном и том же, если? Я никогда не использовал»)
выражения
Этот пример использования ifнемного отличается от использования while, потому что компилятор скомпилирует даже последовательность if(true) return; System.out.println("Hello");без жалоб. IIRC, это особое исключение в JLS. Компилятор сможет обнаружить недостижимый код после ifтак же просто, как с while.
Christian Semrau
2
@emory - когда вы загружаете программу Java через сторонний процессор аннотаций, это ... в самом реальном смысле ... больше не Java. Скорее это Java накладываются любые лингвистические расширения и модификации, которые реализует процессор аннотаций. Но это НЕ то, что здесь происходит. Вопросы OP касаются ванильной Java, а правила JLS определяют, что является и что не является действительной ванильной программой Java.
Stephen C
6

Последнее не является недостижимым. Логическое значение b все еще может быть изменено на false где-нибудь внутри цикла, вызывая условие завершения.

Сыр
источник
Верно, но не имеет значения, поскольку b НЕ изменяется в цикле. Вы также можете утверждать, что цикл, использующий истину, может включать в себя оператор break, который даст условие завершения.
deworde
4

Я предполагаю, что переменная «b» имеет возможность изменить свое значение, так что компилятор думает, что это System.out.println("while terminated"); возможно.

xuanyuanzhiyuan
источник
4

Компиляторы не идеальны - и не должны быть

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

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

Strong Typing и OO: повышение эффективности компилятора

Некоторые ошибки носят синтаксический характер, а в Java строгая типизация позволяет перехватить множество исключений во время выполнения. Но, используя лучшие типы, вы можете помочь вашему компилятору обеспечить лучшую логику.

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

Классическим примером этого является использование шаблона итератора в сочетании с циклом foreach в Java. Эта конструкция менее уязвима для типа ошибки, которую вы иллюстрируете, чем упрощенный цикл while.

jayunit100
источник
Обратите внимание, что объектно-ориентированный подход - не единственный способ помочь статическим механизмам строгой типизации находить ошибки. Параметрические типы и классы типов Haskell (которые немного отличаются от того, что называется классами в объектно-ориентированных языках) на самом деле, возможно, лучше в этом.
leftaround около
3

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

Джонатан М
источник
4
Это не связано с «изощренностью» компилятора. JLS делает все возможное, чтобы указать правила для обнаружения недостижимых операторов. Согласно этим правилам, первый пример не является допустимой Java, а второй пример - допустимой Java. Вот и все. Автор компилятора должен реализовать правила, как указано ... иначе его компилятор не соответствует требованиям.
Stephen C
3

Я удивлен, что ваш компилятор отказался компилировать первый случай. Мне это кажется странным.

Но второй случай не оптимизирован для первого случая, потому что (а) другой поток может обновить значение b(б) вызываемая функция может изменить значение bв качестве побочного эффекта.

Сарнольд
источник
2
Если вы удивлены, значит, вы недостаточно прочитали спецификацию языка Java :-)
Стивен Си
1
Ха-ха :) Приятно знать, где я нахожусь. Благодаря!
sarnold
3

На самом деле, я не думаю, что кто-то понял это ПОЛНОСТЬЮ правильно (по крайней мере, в том смысле, в котором он изначально задавал вопрос). OQ продолжает упоминать:

Верно, но не имеет значения, так как b НЕ изменяется в цикле

Но это не имеет значения, потому что последняя строка ДОСТУПНА. Если вы взяли этот код, скомпилировали его в файл класса и передали файл класса кому-то другому (например, в качестве библиотеки), они могли бы связать скомпилированный класс с кодом, который изменяет «b» посредством отражения, выходя из цикла и вызывая последний строка для выполнения.

Это верно для любой переменной, которая не является константой (или final, которая компилируется в константу в том месте, где она используется - иногда вызывая странные ошибки, если вы перекомпилируете класс с final, а не с классом, который на него ссылается, ссылка класс по-прежнему будет хранить старое значение без каких-либо ошибок)

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

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

Билл К
источник
Хорошая точка зрения. Я предполагаю, что bэто переменная метода. Вы действительно можете изменить переменную метода с помощью отражения? Независимо от общей точки зрения, мы не должны предполагать, что последняя строка недостижима.
emory
Ой, ты прав, я предполагал, что b был участником. Если b - переменная метода, то я полагаю, что компилятор «Can» знает, что она не изменится, но не изменится (что делает все остальные ответы ВСЕГО правильными)
Билл К.
3

Это просто потому, что компилятор не слишком много работает с присмотром за детьми, хотя это возможно.

Показанный пример прост и разумен для компилятора, чтобы обнаружить бесконечный цикл. Но как насчет того, чтобы вставить 1000 строк кода без какой-либо связи с переменной b? А как насчет тех заявлений все b = true;? Компилятор определенно может оценить результат и сказать вам, что в конечном итоге он верен.while цикле, но насколько медленным будет компиляция реального проекта?

PS, инструмент lint определенно должен сделать это за вас.

Pinxue
источник
2

С точки зрения компилятора, bвwhile(b) может где-то измениться на false. Компилятор просто не проверяет.

Ради интереса попробуй while(1 < 2)и for(int i = 0; i < 1; i--)т. Д.

Воскресение понедельник
источник
2

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

Wilmer
источник
2

Если компилятор может окончательно определить, что логическое значение будет оцениваться trueво время выполнения, он выдаст эту ошибку. Компилятор предполагает, что объявленная вами переменная может быть изменена (хотя мы, как люди, знаем, что это не так).

Чтобы подчеркнуть этот факт, если переменные объявлены, как finalв Java, большинство компиляторов выдаст такую ​​же ошибку, как если бы вы подставили значение. Это связано с тем, что переменная определяется во время компиляции (и не может быть изменена во время выполнения), и поэтому компилятор может окончательно определить, что выражение оценивается trueво время выполнения.

Кэмерон С
источник
2

Первый оператор всегда приводит к бесконечному циклу, потому что мы указали константу в условии цикла while, где, как и во втором случае, компилятор предполагает, что существует возможность изменения значения b внутри цикла.

Шео
источник