Неопределенное поведение в Java

14

Я читал этот вопрос о SO, который обсуждает некоторые общие неопределенные поведения в C ++, и я задавался вопросом: есть ли у Java также неопределенное поведение?

Если это так, то каковы некоторые распространенные причины неопределенного поведения в Java?

Если нет, то какие функции Java делают его свободным от такого поведения и почему последние версии C и C ++ не были реализованы с этими свойствами?

Восемь
источник
4
Ява очень жестко определена. Проверьте спецификацию языка Java.
1
Связанный: blogs.msdn.com/b/ericlippert/archive/2012/06/18/…
CodesInChaos
4
@ user1249, "неопределенное поведение" на самом деле тоже довольно жестко определено.
Pacerier
То же самое можно сделать и на SO: stackoverflow.com/questions/376338/…
Ciro Santilli 15 15 中心 法轮功 六四 事件
О чем говорит Java, когда вы нарушаете «Контракт»? Например, когда вы перегружаете .equals несовместимыми с .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Разве это неопределенно, но технически не так, как в C ++?
мычанка

Ответы:

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 описывает некоторые случаи, когда результаты не определены, например, в (устаревшем) конструкторе Date (int year, int month, int day) :

Результат не определен, если данный аргумент выходит за пределы ...

Javadocs для состояния ExecutorService.invokeAll (Collection) :

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

Менее формальный вид «неопределенного» поведения можно найти, например, в ConcurrentModificationException , где в документах API используется термин «лучшее усилие»:

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


аппендикс

Один из комментариев к вопросу относится к статье Эрика Липперта, в которой содержится полезное введение в тему: поведение, определяемое реализацией .

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

Традиционно мы говорим, что идиома языка программирования имеет неопределенное поведение если использование этой идиомы может иметь какой-либо эффект; он может работать так, как вы ожидаете, или он может стереть ваш жесткий диск или привести к поломке вашей машины. Более того, автор компилятора не обязан предупреждать вас о неопределенном поведении. (И действительно, есть некоторые языки, в которых программы, использующие идиомы «неопределенного поведения», разрешены спецификацией языка для сбоя компилятора!) ...

Напротив, идиома, которая имеет поведение, определяемое реализацией котором автор компилятора имеет несколько вариантов того, как реализовать функцию, и должен выбрать один. Как следует из названия, поведение, определяемое реализацией, по крайней мере, определено. Например, C # позволяет реализации генерировать исключение или создавать значение при переполнении целочисленного деления, но реализация должна выбрать его. Он не может стереть ваш жесткий диск ...

Каковы некоторые из факторов, которые побуждают комитет по проектированию языка оставлять определенные языковые идиомы неопределенными или определяемыми реализацией поведениями?

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

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

Третий фактор: настолько ли сложна эта особенность, что подробное описание ее точного поведения будет сложно или дорого определить? ...

Четвертый фактор: накладывает ли эта функция большую нагрузку на анализатор? ...

Пятый фактор: накладывает ли функция большую нагрузку на среду выполнения? ...

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

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

Выше только очень краткое освещение; полная статья содержит объяснения и примеры для пунктов, упомянутых в этом отрывке; это много чтения стоит. Например, подробности, приведенные для «шестого фактора», могут дать представление о мотивации для многих операторов в модели памяти Java ( JSR 133 ), помогая понять, почему некоторые оптимизации допускаются, что приводит к неопределенному поведению, в то время как другие запрещены, что приводит к ограничения, такие как « предвидеть» и требования причинности .

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

комар
источник
Я добавлю, что базовое оборудование JMM! = И конечный результат выполнения программы в отношении параллелизма могут отличаться от, скажем, WinIntel против Solaris
Мартейн Вербург
2
@ MartijnVerburg, это очень хороший момент. Единственная причина, по которой я не решаюсь пометить его как «неопределенный», заключается в том, что модель памяти накладывает ограничения, такие как случайность и причинно-следственная связь при выполнении правильно синхронизированной программы
комнат
Правда, спецификация определяет, как она должна вести себя при JMM, однако Intel и др. Не всегда соглашаются ;-)
Мартейн Вербург
@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 была вынуждена задним числом ввести ограниченную форму неопределенного поведения именно по этой причине: ключевое слово strictfp было введено в Java 1.2, чтобы позволить вычислениям с плавающей запятой отклоняться от точного следования стандарту IEEE 754, как ранее требовала спецификация потому что это требовало дополнительной работы и замедляло все вычисления с плавающей точкой на некоторых распространенных процессорах, в то время как в некоторых случаях это приводило к худшим результатам.

Майкл Боргвардт
источник
2
Я думаю, что важно отметить другую главную цель Java: безопасность и изоляция. Я думаю, что это также является причиной отсутствия «неопределенного» поведения (как в C ++).
К.Стефф
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 не связан ни с кем.
суперкат
3

Java очень старается уничтожить неопределенное поведение, именно из-за уроков более ранних языков. Например, переменные уровня класса автоматически инициализируются; локальные переменные не инициализируются автоматически по соображениям производительности, но существует сложный анализ потока данных, чтобы никто не смог написать программу, способную обнаружить это. Ссылки не являются указателями, поэтому недопустимые ссылки не могут существовать, а разыменование nullвызывает конкретное исключение.

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

Килиан Фот
источник
Ссылки являются указателями под другим именем
curiousguy
@curiousguy - обычно подразумевается, что «ссылки» не допускают использования арифметических манипуляций с их числовым значением, что часто допускается для «указателей». Таким образом, первое является более безопасной конструкцией, чем второе; в сочетании с системой управления памятью, которая не позволяет повторно использовать хранилище объекта при наличии действительной ссылки на него, ссылки предотвращают ошибки использования памяти. Указатели не могут этого сделать, даже если используется соответствующее управление памятью.
Жюль
@Jules Тогда это вопрос терминологии: вы можете однозначно назвать указатель или ссылку, и решите использовать «ссылку» в «безопасных» языках и «указатель» в языках, которые позволяют использовать арифметику указателей и ручное управление памятью. (AFAIK "арифметика указателей" выполняется только в C / C ++.)
curiousguy
2

Вы должны понимать «неопределенное поведение» и его происхождение.

Неопределенное поведение означает поведение, которое не определено стандартами. В C / C ++ слишком много разных реализаций компилятора и дополнительных функций. Эти дополнительные функции привязали код к компилятору. Это было потому, что не было никакого централизованного развития языка. Таким образом, некоторые из расширенных функций некоторых компиляторов стали «неопределенным поведением».

Принимая во внимание, что в Java спецификация языка контролируется Sun-Oracle, и больше никто не пытается создавать спецификации, и, следовательно, нет неопределенного поведения.

Отредактировано Конкретно отвечая на вопрос

  1. Java свободна от неопределенного поведения, потому что стандарты были созданы до компиляторов
  2. Современные компиляторы C / C ++ более / менее стандартизировали реализации, но функции, реализованные до стандартизации, все еще остаются помеченными как «неопределенное поведение», потому что ИСО помнила об этих аспектах.
Sarvex
источник
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-ish дают неопределенный результат. значения (которые могут вести себя так, как будто они имеют «дополнительные биты») и могут иметь последствия, выходящие за рамки этого, только если документы реализации прямо оставляют за собой право навязывать такие; Документы могут давать «Неограниченное поведение» ...
суперкат
1

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

  • Собственный интерфейс Java (JNI), способ для Java вызывать код C или C ++. В JNI есть много способов облажаться, например, неправильно указывать сигнатуру функции, делать недопустимые вызовы служб JVM, повреждать память, неправильно распределять / освобождать данные и т. Д. Я делал эти ошибки раньше, и обычно вся JVM падает, когда какой-либо поток, выполняющий код JNI, совершает ошибку.

  • Thread.stop(), что устарело. Quote:

    Почему не Thread.stopрекомендуется?

    Потому что это небезопасно. Остановка потока приводит к тому, что он разблокирует все заблокированные мониторы. (Мониторы разблокированы, поскольку ThreadDeathисключение распространяется вверх по стеку.) Если какой-либо из объектов, ранее защищенных этими мониторами, находился в несогласованном состоянии, другие потоки теперь могут просматривать эти объекты в несогласованном состоянии. Говорят, что такие объекты повреждены. Когда потоки работают с поврежденными объектами, это может привести к произвольному поведению. Это поведение может быть тонким и его трудно обнаружить, или оно может быть выражено. В отличие от других непроверенных исключений, ThreadDeathмолча убивает потоки; таким образом, пользователь не предупреждает, что его программа может быть повреждена. Коррупция может проявиться в любое время после фактического ущерба, даже часов или дней в будущем.

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

Nayuki
источник