Невизначена поведінка на Java


14

Я читав це питання на SO, де обговорюється деяка загальна невизначена поведінка в C ++, і я замислювався: чи Java також має невизначене поведінку?

Якщо це так, то які поширені причини невизначеної поведінки на Java?

Якщо ні, то які функції Java звільняють від такої поведінки, і чому з цими властивостями не були реалізовані останні версії C і C ++?


4
Ява дуже жорстко визначений. Перевірте специфікацію мови Java.


4
@ user1249, "невизначена поведінка" насправді також досить жорстко визначена.
Pacerier


Що говорить Java, коли ви порушуєте "Контракт"? Таке, що відбувається, коли ви перевантажуєте .equals, щоб бути непорівнянним з .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Це розмовно невизначено, але технічно не так, як це C ++?
Mooing Duck

Відповіді:


18

У Java ви можете вважати поведінку неправильно синхронізованої програми невизначеною.

Java 7 JLS використовує слово "undefined" один раз, у 17.4.8. Виконання та вимоги до причинності :

Ми використовуємо f|dдля позначення функції , заданої, обмежуючи область fв d. Для всіх xв d, f|d(x) = f(x)і для всіх, що xне в d, f|d(x)не визначено ...

Документація Java API вказує деякі випадки, коли результати не визначені - наприклад, у (застарілому) конструкторі Дата (int рік, int місяць, int день) :

Результат не визначений, якщо даний аргумент виходить за межі ...

Javadocs для стану ExecutorService.invokeAll (колекція) :

Результати цього методу не визначені, якщо дана колекція буде змінена під час цієї операції ...

Менш формальний вид "невизначеної" поведінки можна знайти, наприклад, у ConcurrentModificationException , де документи API використовують термін "найкращі зусилля":

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


Додаток

Один із коментарів до питань стосується статті Еріка Ліпперта, яка дає корисне вступ до питань теми: поведінка, визначена реалізацією .

Я рекомендую цю статтю для мовно-агностичних міркувань, хоча варто пам’ятати, що автор націлений на C #, а не на Java.

Традиційно ми говоримо, що ідіома мови програмування має невизначену поведінку, якщо використання цієї ідіоми може мати будь-який ефект; він може працювати так, як ви очікуєте, або він може стерти ваш жорсткий диск або збити вашу машину. Крім того, автор укладача не зобов’язаний попередити вас про невизначену поведінку. (Насправді, є деякі мови, в яких програмам, які використовують ідіоми "невизначеної поведінки", мовна специфікація дозволена збивати компілятор!) ...

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

Які є фактори, які змушують комітет з розробки мов залишити певні мовні ідіоми як невизначені чи визначені реалізацією поведінки?

Перший головний фактор: чи існують дві існуючі реалізації мови на ринку, які не погоджуються з поведінкою певної програми? ...

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

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

Четвертий фактор: чи функція покладає на компілятор високий тягар для аналізу? ...

П’ятий фактор: чи ця функція покладає велике навантаження на середовище виконання? ...

Шостий фактор: чи визначає поведінку, що визначається, виключає якусь велику оптимізацію? ...

Це лише декілька факторів, які приходять на думку; Звичайно, є багато, багато інших чинників, про які обговорюють комітети з мовної розробки, перш ніж зробити функцію "визначеною" або "невизначеною".

Зверху - лише дуже коротке висвітлення; повна стаття містить пояснення та приклади до пунктів, зазначених у цьому уривку; це багато читання варто. Наприклад, деталі, наведені для "шостого фактора", можуть дати зрозуміти мотивацію багатьох тверджень у моделі пам'яті Java ( JSR 133 ), що допоможе зрозуміти, чому деякі оптимізації дозволені, що призводить до невизначеної поведінки, а інші заборонені, що призводить до такі обмеження, як вимоги, що відбудуться раніше, та вимоги до причинності .

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


Додам, що базовий апарат JMM! = І кінцевий результат виконання програми щодо одночасності може відрізнятися від WinIntel проти Solaris
Martijn Verburg,

2
@MartijnVerburg, це досить непоганий момент. Єдиною причиною, чому я не вагаюся позначити це як "невизначене", є те, що модель пам'яті створює такі обмеження, як раніше, і причинність при виконанні правильно синхронізованої програми
gnat

Правда, специфікація визначає, як вона повинна поводитися в рамках JMM, однак Intel та інші не завжди згодні ;-)
Martijn Verburg

@MartijnVerburg Я думаю, що головним моментом JMM є запобігання надмірної оптимізації витоків від "не згоденних " виробників процесорів. Наскільки я розумію, у Java до 5.0 був такий головний біль з DEC Alpha, коли спекулятивні записи, зроблені під кришкою, могли просочитися до програми на кшталт "з повітря" - отже, вимога причинності перейшла в JSR 133 (JMM)
gnat

9
@MartinVerburg - завдання реалізатора JVM - переконатися, що JVM поводиться відповідно до специфікацій JLS / JMM на будь-якій підтримуваній апаратній платформі. Якщо інше обладнання поводиться по-різному, це завдання реалізатора JVM вирішити його ... і змусити його працювати.
Стівен С

10

Зверху в голові, я не думаю, що в Java є не визначена поведінка, принаймні, не в тому ж сенсі, як у C ++.

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

На відміну від цього, основна мета дизайну C і C ++ - це ефективність: не повинно бути ніяких функцій (включаючи незалежність платформи), які б затратили продуктивність, навіть якщо вони вам не потрібні. З цією метою специфікація навмисно не визначає деяку поведінку, оскільки їх визначення спричинить додаткову роботу на деяких платформах і, таким чином, знизить продуктивність навіть для людей, які пишуть програми спеціально для однієї платформи та знають про всі її ідіосинкратії.

Навіть є приклад, коли Java змушена була ввести заднім числом обмежену форму невизначеної поведінки саме з цієї причини: ключове слово строгий FF було введено в Java 1.2, щоб дозволити обчислення з плаваючою комою відхилятися від точно слідування стандарту IEEE 754, як вимагала раніше специфікація , тому що це вимагало додаткової роботи і зробило все обчислення з плаваючою комою повільнішими на деяких загальних процесорах, в той час як насправді дає гірші результати в деяких випадках.


2
Я думаю, що важливо відзначити іншу головну мету Java: безпеку та ізоляцію. Я думаю, що це теж є причиною відсутності «невизначеної» поведінки (як у С ++).
K.Steff

3
@ K.Steff: Гіперсучасний C / C ++ абсолютно не підходить для нічого, що стосується безпеки. Враховуючи int x=-1; foo(); x<<=1;гіперсучасну філософію, було б переважно переписати fooтак, що будь-який шлях, який не виходить, повинен бути недосяжним. Це, якщо fooце if (should_launch_missiles) { launch_missiles(); exit(1); }компілятор може (і на думку деяких людей , повинні) спростити , що просто launch_missiles(); exit(1);. Традиційним UB було випадкове виконання коду, але раніше воно було пов'язане законами часу та причинності. Нове вдосконалене UB не пов'язане ні одним.
supercat

3

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

Звичайно, залишаються певні форми поведінки, які не визначені повністю, і ви можете писати ненадійні програми, якщо вважати, що вони є. Наприклад, якщо ви повторюєте звичайні (не сортовані) Set, мова гарантує, що ви побачите кожен елемент точно один раз, але не в тому порядку, коли ви їх побачите. Порядок може бути однаковим у послідовних прогонах або може змінюватися; або він може залишатися таким же, доки не відбудуться інші виділення, або поки ви не оновлюєте JDK тощо. Позбутися від усіх таких ефектів майже неможливо ; наприклад, вам доведеться явно замовляти або рандомізувати всі операції колекцій, і цього просто не варто мати невеликих додаткових неозначених можливостей.


Посилання - покажчики під іншою назвою
curiousguy

@curiousguy - "посилання", як правило, не дозволяють використовувати арифметичні маніпуляції їх числового значення, що часто дозволено для "покажчиків". Тому перший є більш безпечною конструкцією, ніж другий; у поєднанні із системою управління пам’яттю, яка не дозволяє зберігати об’єкт повторно, хоча дійсна посилання на нього існує, посилання запобігають помилкам використання пам'яті. Покажчики не можуть цього зробити, навіть якщо використовується відповідне управління пам’яттю.
Жуль

@Jules Тоді це питання термінології: ви можете назвати одну річ вказівником чи посиланням та вирішити використовувати "посилання" в "безпечних" мовах та "вказівник" на мовах, які дозволяють використовувати арифметику вказівника та керування пам'яттю вручну. (AFAIK "вказівна арифметика" робиться лише в C / C ++.)
цікаво

2

Ви повинні зрозуміти «Невизначене поведінку» та його походження.

Не визначена поведінка означає поведінку, яка не визначена стандартами. C / C ++ має занадто багато різних реалізацій компілятора та додаткових функцій. Ці додаткові функції прив’язували код до компілятора. Це було тому, що не було централізованого розвитку мови. Тож деякі вдосконалені функції деяких компіляторів стали "невизначеними поведінками".

В той час, як у Java мовна специфікація контролюється Sun-Oracle, і ніхто більше не намагається скласти специфікації і, таким чином, не визначено поведінки.

Відредаговано спеціально, відповідаючи на запитання

  1. Java не містить визначених форм поведінки, оскільки стандарти були створені перед компіляторами
  2. Сучасні компілятори C / C ++ мають більш / менш стандартизовану реалізацію, але функції, реалізовані до стандартизації, як і раніше залишаються позначеними як "невизначена поведінка", оскільки ISO тримає маму з цих аспектів.

2
Можливо, ви маєте рацію, що у Java немає UB, але навіть коли одна організація контролює все, може бути причини, що мають UB, тому причина, яку ви наводите, не призводить до висновку.
AProgrammer

2
Крім того, і C, і C ++ стандартизовані ISO. Хоча може бути кілька компіляторів, є лише один стандарт за один раз.
MSalters

1
@SarvexJatasra, я не погоджуюся, що це єдине джерело UB. Наприклад, один UB перенаправляє висячий покажчик, і є вагомі причини, щоб залишити його UB будь-якою мовою, яка не має GC, навіть якщо ви зараз розпочали свою специфікацію. І ці причини не мають нічого спільного з існуючою практикою або існуючими компіляторами.
AProgrammer

2
@SarvexJatasra, підписаний переповнення є UB, оскільки стандарт прямо говорить про це (це навіть приклад, наведений з визначенням UB). Перенаправлення недійсного покажчика також є UB з тієї ж причини, стандарт говорить про це.
AProgrammer

2
@ bames53: Жодна з цитованих переваг не вимагала б рівня широтних гіпермодерних компіляторів, які приймають з UB. За винятком доступу до пам'яті поза межами та переповнення стека, що може «природно» викликати випадкове виконання коду, я не можу придумати жодної корисної оптимізації, яка вимагала б більш широкої широти, ніж сказати, що більшість операцій з UB-результатом приносять невизначений характер значення (які можуть вести себе так, ніби вони мають "зайві біти") і можуть мати наслідки, крім цього, лише якщо документи програми прямо залишають за собою право накладати такі; Документи можуть дати "Нестримну поведінку" ...
supercat

1

Java виключає по суті всю не визначену поведінку, виявлену в C / C ++. (Наприклад: підписане ціле число переповнення, поділ на нуль, неініціалізовані змінні, нульова відмітка покажчика, зміщення більше бітової ширини, подвійне вільне, навіть "немає нового рядка в кінці вихідного коду".) Але у Java є кілька незрозумілих невизначених способів поведінки, які рідко стикаються програмісти.

  • Java Native Interface (JNI) - спосіб виклику Java на C або C ++. Існує багато способів виправити JNI, наприклад, помилка підпису функції, здійснення недійсних дзвінків до служб JVM, пошкодження пам’яті, неправильне розподілення / звільнення речей тощо. Я раніше робив ці помилки, і взагалі весь JVM виходить з ладу, коли будь-який один потік, що виконує код JNI, робить помилку.

  • Thread.stop(), яка застаріла. Цитата:

    Чому Thread.stopзастаріла?

    Тому що це по своїй суті небезпечно. Якщо зупинити потік, це призведе до розблокування всіх заблокованих моніторів. (Монітори розблоковані, оскільки ThreadDeathвиняток поширює стек.) Якщо будь-який із об'єктів, попередньо захищених цими моніторами, знаходився в непослідовному стані, інші потоки тепер можуть переглядати ці об'єкти в непослідовному стані. Кажуть, такі об'єкти пошкоджені. Коли нитки діють на пошкоджені об'єкти, може виникати довільна поведінка. Така поведінка може бути тонкою і важко виявити, або вона може бути яскраво вираженою. На відміну від інших неперевірених винятків, ThreadDeathмовчки вбиває теми; таким чином, користувач не попереджає, що його програма може бути пошкоджена. Корупція може проявитись у будь-який час після фактичної шкоди, навіть годин чи днів у майбутньому.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

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