Java中未定义的行为


14

在SO上阅读了这个问题,讨论了C ++中一些常见的未定义行为,我想知道:Java是否也具有未定义的行为?

如果是这样,那么Java中未定义行为的一些常见原因是什么?

如果不是,那么Java的哪些功能使其摆脱了此类行为,为什么没有使用这些属性实现最新版本的C和C ++?


4
Java非常严格地定义。检查Java语言规范。


4
@ user1249,实际上也非常严格地定义了“未定义的行为”。
Pacerier 2014年


当您违反“合同”时,Java会怎么说?例如,当您重载.equals与.hashCode不兼容时发生的情况?docs.oracle.com/javase/7/docs/api/java/lang/…总体上 是未定义的,但是从技术上讲,其含义与C ++相同吗?
Mooing Duck's

Answers:


18

在Java中,您可以考虑未正确同步程序的行为未定义。

Java 7 JLS在17.4.8中曾经使用单词“ undefined” 执行和因果要求

我们f|d用来表示通过限制fto 的域而给出的功能d。对于所有x在中的df|d(x) = f(x)对于所有x不在中的df|d(x)未定义 ...

Java API文档在某些情况下指定了结果未定义的情况-例如,在(不建议使用的)构造函数Date(int年,int月,int日)中

如果给定参数超出范围,则结果不确定

ExecutorService.invokeAll(Collection)的 Javadocs 状态:

如果在进行此操作时修改了给定的集合,则此方法的结果不确定

可以在ConcurrentModificationException中找到不太正式的“未定义”行为,其中API文档使用术语“尽力而为”:

请注意,不能保证快速故障行为,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。快速失败的操作ConcurrentModificationException尽力而为的。因此,编写依赖于此异常的程序的正确性是错误的...


附录

问题注释之一涉及Eric Lippert的一篇文章,该文章为主题提供了有益的介绍:实现定义的行为

我建议您将本文用于与语言无关的推理,尽管应牢记作者针对的是C#,而不是Java。

传统上,我们说编程语言惯用法具有不确定的行为,如果使用该惯用法会产生任何影响;它可以按您期望的方式工作,也可以擦除硬盘或使计算机崩溃。此外,编译器作者没有义务警告您未定义的行为。(实际上,在某些语言中,语言规范允许使用“未定义行为”惯用语的程序使编译器崩溃!)...

相比之下,具有实现定义的行为的习惯用法就是这样的行为:编译器作者对如何实现功能有多种选择,必须选择一种。顾名思义,至少定义了实现定义的行为。例如,C#允许实现在整数除法溢出时引发异常或产生值,但是实现必须选择一个。它无法擦除硬盘...

导致语言设计委员会将某些语言习语保留为未定义或实现定义的行为的因素有哪些?

第一个主要因素是:市场上是否存在两种针对特定程序行为的现有语言实现?...

下一个主要因素是:该功能是否自然会带来许多不同的实现可能性,其中某些明显优于其他可能性?...

第三个因素是:该功能是否如此复杂,以致于难以对其详细行为进行详细分类或指定?...

第四个因素是:该功能是否给编译器带来了沉重的分析负担?...

第五个因素是:该功能是否给运行时环境带来了沉重负担?...

第六个因素是:定义行为是否排除了一些重大的优化?...

这些只是想到的几个因素。当然,在设计功能“实现定义”或“未定义”之前,语言设计委员会会讨论许多其他因素。

以上只是非常简短的介绍;全文中包含本节中提到的要点的解释和示例;这是很多值得一读。例如,为“第六个因素”提供的详细信息可以使人们深入了解Java内存模型(JSR 133)中许多语句的动机,有助于理解为什么允许某些优化,导致未定义行为,而禁止其他优化,从而导致限制,例如事前发生因果关系要求

这篇文章的材料对我来说都不是特别新鲜,但是如果我看到它以如此优雅,简洁和可理解的方式呈现,我将是该死的。惊人。


我要补充一点,JMM!=底层硬件以及正在执行的程序在并发方面的最终结果可能与WinIntel与Solaris有所不同
Martijn Verburg

2
@MartijnVerburg这是一个很好的观点。我犹豫将其标记为“未定义”的唯一原因是,内存模型在执行正确同步的程序时会产生诸如事前发生因果关系的约束
gnat

没错,规范定义了它在JMM下的行为,但是,英特尔等人并不总是同意;-)
Martijn Verburg,2012年

@MartijnVerburg我认为JMM的主要目的是防止“不同意”的处理器制造商对泄漏进行过度优化。据我了解,Java 5.0之前的版本对DEC Alpha颇为头痛,当时在幕后进行的投机性写入可能会像“凭空冒出来”一样泄漏到程序中-因此,因果关系需求进入了JSR 133(JMM)
gnat

9
@MartinVerburg-确保JVM在任何受支持的硬件平台上均遵循JLS / JMM规范的行为是JVM实现者的工作。如果不同的硬件表现不同,那么JVM实现者的工作就是处理它并使其正常工作。
Stephen C

10

在我脑海中,我认为Java中没有任何未定义的行为,至少在某种意义上与C ++中没有相同。

原因是Java背后的哲学与C ++背后的哲学不同。Java的核心设计目标是允许程序在各种平台上不变运行,因此规范非常明确地定义了所有内容。

相反,C和C ++的核心设计目标是效率:即使您不需要它们,也不应有任何会影响性能的功能(包括平台独立性)。为此,该规范故意未定义某些行为,因为定义行为会在某些平台上引起额外的工作,从而降低性能,即使对于专门为一个平台编写程序并了解其所有特性的人而言。

甚至有一个例子,正是出于这个原因,Java被迫追溯引入一种有限形式的未定义行为:Java 1.2中引入了strictfp关键字,以允许浮点计算偏离规范之前所要求的完全遵循IEEE 754标准,因为这样做需要额外的工作,并使某些常见CPU上的所有浮点计算速度变慢,而实际上在某些情况下却产生较差的结果。


2
我认为必须注意Java的另一个主要目标:安全性和隔离性。我认为这也是缺少“未定义”行为的原因(例如在C ++中)。
K.Steff

3
@ K.Steff:超现代的C / C ++完全不适合与远程安全相关的任何事情。给定的int x=-1; foo(); x<<=1;超现代哲学将倾向于重写,foo因此任何不存在的路径都必须是无法到达的。如果fooif (should_launch_missiles) { launch_missiles(); exit(1); }编译器,则可以(并且根据某些人的看法)将其简化为simple launch_missiles(); exit(1);。传统的UB是随机代码执行,但以前受时间和因果关系法则的约束。新改进的UB不受任何约束。
2015年

3

正是由于早期语言的教训,Java竭尽全力消除未定义的行为。例如,类级变量会自动初始化。出于性能原因,局部变量不会自动初始化,但是存在复杂的数据流分析功能,以防止任何人编写能够检测到此问题的程序。引用不是指针,因此无效的引用不能存在,并且取消引用null会导致特定的异常。

当然,还有一些行为没有完全指定,如果您认为它们是不正确的,则可以编写不可靠的程序。例如,如果您遍历一个普通的(未排序的)对象Set,则该语言保证您将只看到一次每个元素,但不能以顺序看到它们。顺序可能在连续运行中相同,或者可能会更改;或者,只要没有其他分配发生,或者只要您不更新JDK等,它就可以保持不变。要消除所有这些影响几乎是不可能的;例如,您将必须对所有 Collections操作进行显式排序或随机化,这根本不值得再加上一些小的un-undefined-ness。


引用是使用其他名称的指针
curiousguy

@curiousguy-通常假定“引用”不允许对它们的数值进行算术运算,“指针”通常允许这样做。因此,前者比后者更安全。结合了内存管理系统,该内存管理系统不允许在对象的有效引用存在时重复使用该对象的存储,因此引用可以防止内存使用错误。即使使用了适当的内存管理,指针也不能这样做。
Jules '18

@Jules然后是术语问题:您可以将一件事称为指针或引用,并决定在“安全”语言中使用“引用”,并在允许使用指针算术和手动内存管理的语言中使用“指针”。(AFAIK“指针算术”仅在C / C ++中完成。)
curiousguy

2

您必须了解“未定义行为”及其来源。

未定义行为是指标准未定义的行为。C / C ++具有太多不同的编译器实现和其他功能。这些附加功能将代码绑定到编译器。这是因为没有集中的语言开发。因此,某些编译器的某些高级功能成为“未定义的行为”。

在Java中,语言规范是由Sun-Oracle控制的,没有其他人试图制定规范,因此没有未定义的行为。

编辑专门回答问题

  1. Java没有未定义的行为,因为标准是在编译器之前创建的
  2. 现代C / C ++编译器对实现的实现进行了更多的/更少的标准化,但是在标准化之前实现的功能仍被标记为“未定义的行为”,因为ISO在这些方面保持沉默。

2
您可能是对的,Java中没有UB,但是即使当一个实体控制所有内容时,也可能有理由拥有UB,因此您给出的理由并不能得出结论。
AProgrammer 2012年

2
此外,C和C ++均通过ISO标准化。尽管可能有多个编译器,但一次只有一个标准。
MSalters 2012年

1
@SarvexJatasra,我不同意它是UB的唯一来源。例如,一个UB正在取消引用悬空指针,并且有充分的理由将其保留为没有GC的任何语言的UB,即使您现在开始规范。这些原因与现有实践或现有编译器无关。
AProgrammer 2012年

2
@SarvexJatasra,带符号的溢出是UB,因为标准明确指出了这一点(甚至是带有UB定义的示例)。由于同样的原因,取消引用无效的指针也是一个UB。
AProgrammer 2012年

2
@ bames53:引用的优势都不要求使用UB的纬度超现代编译器的水平。除了超出范围的内存访问和堆栈溢出(可以“自然地”引起随机代码执行)外,我想不到任何有用的优化都需要更广泛的自由度,而不是说大多数UB-ish操作产生不确定性值(可能表现得好像它们具有“额外的位”),并且只有在实现的文档明确保留施加此类值的权利的情况下,才可能产生其他后果;文档可能会给出“不受约束的行为” ...
supercat 2015年

1

Java基本上消除了C / C ++中发现的所有未定义行为。(例如:有符号整数溢出,零除,未初始化的变量,空指针取消引用,移位超过位宽,双倍释放,甚至“源代码末尾没有换行符。”)但是Java具有一些模糊的未定义行为,这些行为程序员很少遇到。

  • Java本机接口(JNI),Java调用C或C ++代码的一种方式。有很多方法可以弄乱JNI,例如弄错函数签名,对JVM服务进行无效调用,破坏内存,不正确地分配/释放东西等等。我之前犯过这些错误,并且通常任何执行JNI代码的线程提交错误都会导致整个JVM崩溃。

  • Thread.stop(),已弃用。引用:

    为什么Thread.stop不推荐使用?

    因为它本质上是不安全的。停止线程会使它解锁它已锁定的所有监视器。(当ThreadDeath异常在堆栈中传播时,监视器将被解锁。)如果以前由这些监视器保护的任何对象处于不一致状态,则其他线程现在可能会以不一致状态查看这些对象。据说这些物体已损坏。当线程对损坏的对象进行操作时,可能会导致任意行为。此行为可能是微妙的,难以检测,或者可能是明显的。与其他未经检查的异常不同,它会ThreadDeath无声地杀死线程。因此,用户没有警告其程序可能已损坏。在实际损坏发生后的任何时间,甚至在未来数小时或数天,腐败都可能显示出来。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.