Нескінченні цикли в 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

5
Я ненавиджу ці дурні помилки. Недоступний код повинен бути попередженням, а не помилкою.
user606723

2
@ CJ7: Я б не називав це "функцією", і це робить дуже нудним (без причини) реалізацію відповідного компілятора Java. Насолоджуйтесь вашим задумом постачальника.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Відповіді:


105

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

  • вміст змінної читався з файлу?
  • змінна не була локальною і може бути змінена іншим потоком?
  • змінна покладалася на якийсь вхід користувача?

Явно компілятор не перевіряє ваш простіший випадок, оскільки він взагалі відмовляється від цієї дороги. Чому? Тому що це набагато важче заборонено специфікацією. Див. Розділ 14.21 :

(До речі, мій компілятор робить скаржитися , коли змінна оголошена final.)


25
-1 - справа не в тому, що може зробити компілятор. Це нічого про те, щоб перевірки були простішими чи складнішими. Йдеться про те, що компілятору дозволено робити ... мовою Java Spec. Друга версія коду є дійсною Java відповідно до JLS, тому помилка компіляції буде неправильною.
Stephen C

10
@StephenC - Дякую за цю інформацію. Щасливо оновлено, щоб відображати стільки ж.
Wayne

Ще -1; жодне з ваших "що якщо" тут не застосовується. В даний час компілятор дійсно може вирішити проблему - у термінах статичного аналізу це називається постійним розповсюдженням і широко використовується з успіхом у багатьох інших ситуаціях. JLS - це єдина причина, і наскільки важко вирішити проблему, це несуттєво. Звичайно, причина того, що JLS написаний таким чином, в першу чергу може бути пов'язана зі складністю цієї проблеми, хоча я особисто підозрюю, що це насправді тому, що важко застосовувати стандартизоване рішення постійного розповсюдження для різних компіляторів.
Дуб

2
@Oak - У своїй відповіді я визнав, що "у прикладі іграшок для [ОП] це просто", і, базуючись на коментарі Стівена, що JLS є обмежуючим фактором. Я майже впевнений, що ми погоджуємось.
Wayne

55

Відповідно до специфікацій , нижче йдеться про твердження while.

Оператор while може нормально заповнити, якщо принаймні одне з наступного відповідає дійсності:

  • Оператор while є доступним, а вираз умови не є постійним виразом зі значенням true.
  • Існує доступний оператор break, який виходить із оператора while. \

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


3
+1 за зауваження, що це не просто те, що автори компіляторів можуть або не можуть придумати - JLS говорить їм, що вони можуть, а що не можуть вважати недосяжними.
yshavit

14

Оскільки true - це константа, і b можна змінювати в циклі.


1
Правильний, але неактуальний, оскільки b НЕ змінюється в циклі. Можна стверджувати, що цикл, який використовує true, може містити оператор break.
deworde

@deworde - поки існує ймовірність того, що його можна змінити (скажімо іншим потоком), тоді компілятор не може називати це помилкою. Ви не можете сказати, просто подивившись на сам цикл, чи буде b змінюватися під час роботи циклу.
Peter Recore

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 Так, це було б визначенням браузера, що відповідає стандартам, що він відповідає стандарту та компілює законний код Java. І якщо ви використовуєте браузер, що не відповідає стандартам, хоча він може мати і приємні функції, ви тепер покладаєтесь на теорію зайнятого дизайнера компілятора про те, що таке "розумне" правило. Що є справді жахливим сценарієм: "Чому хтось хотів би використовувати ДВІ умовні умови однаково, якщо? У мене ніколи не було")
deworde

Цей приклад використання ifтрохи відрізняється від a while, тому що компілятор компілює навіть послідовність if(true) return; System.out.println("Hello");без нарікань. IIRC, це особливий виняток у JLS. Компілятор зможе виявити недосяжний код після того, ifяк легко, як з while.
Крістіан Семрау

2
@emory - коли ви подаєте програму Java через сторонній процесор анотацій, це ... в цілком реальному сенсі ... вже не Java. Швидше, це Java, накладена на будь-які лінгвістичні розширення та модифікації, які реалізує процесор анотацій. Але це НЕ те, що тут відбувається. Питання OP стосуються ванільної Java, а правила JLS регулюють, що є, а що не є дійсною ванільною програмою Java.
Stephen C

6

Останнє не є недосяжним. Логічне значення b все ще має можливість змінити значення false десь усередині циклу, викликаючи умову закінчення.


Правильний, але неактуальний, оскільки b НЕ змінюється в циклі. Можна стверджувати, що цикл, який використовує true, може містити оператор break, який дасть умову закінчення.
deworde

4

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


4

Компілятори не є ідеальними - і вони не повинні бути

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

Практичне рішення - мати батареї модульних тестів, щоб доповнити перевірки компіляторів АБО використовувати об'єктно-орієнтовані компоненти для реалізації логіки, які, як відомо, надійні, а не покладатися на примітивні змінні та умови зупинки.

Сильна типізація та ОО: підвищення ефективності компілятора

Деякі помилки мають синтаксичний характер - і в Java сильний набір тексту робить багато винятків часу виконання вловлюваними. Але, використовуючи кращі типи, ви можете допомогти своєму компілятору застосувати кращу логіку.

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

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


Зауважте, що ОО - не єдиний спосіб допомогти статичним механізмам сильного набору тексту знаходити помилки. Параметричні типи і класи типів Хаскелла (які дещо відрізняються від тих, що називаються класами в мовах OO) насправді цілком впевнені в цьому.
залишилося близько

3

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


4
Це не пов'язано з "вишуканістю" компілятора. JLS докладає всіх зусиль, щоб вказати правила виявлення недосяжних тверджень. Згідно з цими правилами, перший приклад є недійсною Java, а другий приклад - дійсною Java. Це воно. Письменник компілятора повинен реалізувати правила, як зазначено ... інакше його компілятор не відповідає вимогам.
Stephen C

3

Я здивований, що ваш компілятор відмовився складати перший випадок. Мені це здається дивним.

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


2
Якщо ви здивовані, то ви недостатньо прочитали специфікацію мови Java :-)
Стівен С

1
Ха-ха :) Приємно знати, де я стою. Дякую!
Сарнольд

3

Насправді я не думаю, що хтось зрозумів це ЦІЛЬКО правильно (принаймні не в початковому сенсі запитувача). OQ постійно згадує:

Правильний, але неактуальний, оскільки b НЕ змінюється в циклі

Але це не має значення, оскільки до останнього рядка ДОСТУПНО. Якщо ви взяли цей код, скомпілювали його у файл класу і передали файл класу комусь іншому (скажімо як бібліотеці), вони могли б зв'язати скомпільований клас із кодом, який модифікує "b" через відображення, виходячи з циклу та викликаючи останній рядок для виконання.

Це стосується будь-якої змінної, яка не є константою (або фіналом, який компілюється до константи в місці, де вона використовується, - іноді спричиняючи химерні помилки, якщо ви перекомпілюєте клас із кінцевим, а не класом, який посилається на нього, посилання клас все ще зберігатиме старе значення без будь-яких помилок)

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

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


Гарна думка. Я припускаю, що bце змінна методу. Чи можете ви насправді змінити змінну методу за допомогою відображення? Незалежно від загальної суті, ми не повинні вважати, що останній рядок недоступний.
emory

Ой, ти маєш рацію, я припустив, що b був членом. Якщо b є змінною методу, тоді я вважаю, що компілятор "може" знати, що він не зміниться, але не зміниться (що зрештою робить ЦІЛЬКИ правильними всі інші відповіді)
Білл К

3

Це просто тому, що компілятор не надто багато працює нянями, хоча це можливо.

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

PS, інструмент для ворсу, безумовно, повинен зробити це за вас.


2

З точки зору компілятора, bin while(b)може десь змінитися на false. Компілятор просто не турбується перевіркою.

Для розваги спробуйте while(1 < 2)і for(int i = 0; i < 1; i--)т.д.


2

Вирази обчислюються під час виконання, тому, замінюючи скалярне значення "true" чимось на зразок логічної змінної, ви змінили скалярне значення на логічний вираз, і, таким чином, компілятор не має можливості дізнатися його під час компіляції.


2

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

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


2

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

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