Java程序员应该知道的常见未定义行为是什么?

21
与此问题相同,但是针对Java。

更新 根据一些人的评论和回复,很明显Java几乎没有未定义行为。

因此,我也想问一下哪些行为不是显而易见的。请在回答时区分这两个方面:)

10个回答

15

任何与线程有关的内容... :)

还包括:

  • 重写方法并期望在不同版本中以相同方式使用
  • 对底层平台的假设(例如文件分隔符)
  • 垃圾收集/终结的细节
  • 一些类初始化的细节
  • Integer.valueOf(及其类似方法)是否返回相同的对象
  • 性能、延迟和内存使用情况

我不会说“任何事情”,但肯定是确切的时间和安排。 - Joachim Sauer
很多这些项目虽然未定义,但永远不应该被定义。还有一些是陷阱而不是未定义的。 - Pyrolistical
15
按照C++的术语,这些都不是"未定义的",而是"实现定义的",这种区别非常重要。 - Mooing Duck
1
@MooingDuck 我认为线程排序和GC更像是“未指定的行为”,而不是实现定义:https://dev59.com/63E95IYBdhLWcg3wPbZ7 - Ciro Santilli OurBigBook.com
@MooingDuck 并不是所有的都是 C++。Java 没有强制特定实现定义行为的概念。一个实现在每次运行时做出随机选择行为是合法的。 - Tom Hawtin - tackline

12

与C/C++相比,Java中的未定义行为非常少,它是一个更加明确定义的平台。原因在于C/C++编译器旨在为非常不同的平台生成代码,因此被授予了相当广泛的自由度,以防止过于严格的要求迫使编译器为给定平台生成次优代码。

Java通过以非常精确的方式定义几乎每个行为并仅允许小的自由度来牺牲了一些自由度。当然,这使得该平台更容易处理。

未定义行为发生的主要领域是多个线程的精确时间和调度(正如Tom Hawtin已经提到的那样)。

还有一些地方行为不明显,但看起来可能是未定义的,但实际上不是(Oscar Reyes给出的字符串比较示例就是一个很好的例子)。

还有一些地方的行为被定义为未定义的(例如HashMap中元素的顺序被定义为实现相关,并且不需要是恒定的)。


是的,但确切的时间不应该被定义。因此指出它们不存在是没有意义的。 - Pyrolistical
1
Pyro:可能是这样,但有些开发人员希望在调用Thread.start()的线程之前始终安排新启动的线程,而这并没有明确定义。这就是我所说的时间详细信息。 - Joachim Sauer

8

本来我也想提到这个。它是关于奇怪和意外行为的权威来源。 - GaryF

4

序列化。它本身并不是未定义的(有一个确定性算法)。但是,对于普通观察者来说,很难明确什么会或不会导致 serialVersionUID 的变化,因此破坏了您利用 RMI、JMS 和其他缩略语的所有尝试。

因此,当您知道需要对对象进行序列化时,通常最好考虑您的选项。我特别喜欢“始终将其包含为字段”的技术:

private static final long serialVersionUID = 1L;

只有在您作为开发人员“知道”代码发生了与兼容性有关的重大变化时,才更改该字段的值。不要让JDK为您做出这个决定....


3
我不完全确定您所说的“未定义行为”的含义,但正如其他人指出的那样,核心语言在不同平台和版本的语言和JVM上非常可预测。
然而,对于图形(Swing、AWT)来说,情况并非如此,它们往往是不可预测的,并且在不同平台上可能无法重现。我曾经开发过一个基于Java的图形化应用程序,花费了很多时间“一次编写,到处调试”。
另外,Object.clone()存在一些重大问题,在大多数情况下不建议使用。请参阅 Joshua Bloch 的《Effective Java》中的第11项,以获得完整答案。

1

明确但不显然:

对象相等性测试:

== 用于测试引用(这两个对象引用是否指向同一个对象)

而 equals 用于测试对象的相等性。

例如:

new String("test") == new String("test")  

是 false,而

new String("test").equals( new String("test") )

是真的

字符串对象是被内部化的,所以下面的代码会返回true:

String a = "test";
String b = "test";

a == b  // returns true 

但是如果字符串是在其他地方创建的(例如从数据库中)

String a = "test";
String b = getFromDataBase(); // internally the remote returns "test"

a == b  // returns false.

验证失败。

我在jsp中使用脚本时看到过这种情况,新手程序员不明白为什么验证会失败。

 <%if( param == "continue" ) { %>

从未发生过


2
不,这不是“未定义”的。实际上这是很明确的,只是一个陷阱。 - Pyrolistical
1
无论特定的API是否返回一个已经interned的字符串都是未定义的。 - Tom Hawtin - tackline
第一句话应该写成“但不明显”——你漏掉了一个“T”。 - Bob Cross
1
字符串字面量是被内部化的,而不是字符串对象。非字面量字符串对象可以使用intern()方法进行内部化。 - Jason Day

-1
我记得的一件事是关于jvm与jni的兼容性。我们有一个应用程序是在jdk1.4上开发的,当在安装到一个使用ibm jvm(我相信是jikes)的机器上时,jni调用就会出错!不过那是在2006年。我认为这与Java作为一种语言关系不大,更多的是与Java作为平台有关。

-1

-1
不是未定义,而是转换为整数时浮点数的四舍五入行为出乎意料。0.6d 总是向下舍入为 0;实际上,0.9d 也向下舍入为 0。但是,0.99999999999999995 及更大的数字将向上舍入为 1。
在将 Math.random() 调用的结果强制转换时,请注意这种有趣的行为。

-1

我知道两种未定义行为:

a)方法重载,其参数是与重载方法中的同一参数的子类。例如:

void doSomething(Object obj);
void doSomething(String str);

在doSomething("Hello world!")中,无法确定将调用哪个方法,因为两个签名都是有效的。此外,这种行为可能会从VM到VM甚至从执行到执行发生变化。

b) 在构造函数中调用同一类的非final方法。如果该方法在子类中被覆盖,则会出现未定义的行为。请注意,构造发生在超类到子类之间。如果子类方法使用一些本地子类属性,则情况会变得特别复杂。在Oracle VM的情况下,将构造本地属性,超类构造函数将完成其执行,然后当子类的构造函数被调用时,属性将再次被构造,覆盖先前定义的值。


3
这两个说法都是错误的,而且它们的行为已经被明确定义和保证了。仔细阅读Java语言规范即可。 - Nayuki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接