Java 小谜题






4.86/5 (28投票s)
三个 Java 代码示例,其行为可能出乎您的意料。
介绍
总的来说,Java是一门很棒的语言。然而,它的一些行为怪癖会给不留神的程序员带来麻烦。有时事情的运作方式并非你所期望的那样,而这正是我们要在这里探讨的。以下部分代码改编自我参与过的项目,部分是我专门编写的,旨在帮助那些备考Sun Java 6认证程序员考试的人,让他们了解自己对某个主题的掌握程度。
我们先从一个相对简单的谜题开始,更难的留到后面。
路漫漫兮
在一个项目中,我需要将一个String
变量转换为Long
。我知道Long
类(与其他包装类一样)有执行此类操作的static
方法,于是我在IDE中开始输入,发现有四个方法可能胜任。以下是候选方法列表:
Long.decode(String nm)
Long.getLong(String nm)
Long.parseLong(String s)
Long.valueOf(String s)
鉴于它们每个都接收一个String
并返回一个Long
,以下代码会返回什么?(注意:longValue()
方法返回Long
对象的值,为一个原始的long
类型。)
public long getValueFromString() {
String aLong = “23”;
Long retrievedLong = Long.getLong( aLong );
return retrievedLong.longValue();
}
以下是可能的答案列表
- 23
- 0(零)
- 视情况而定
在继续阅读之前,你认为正确答案是什么?
这么快就结束了?好吧,让我们看看可能的答案,了解一下你的表现。
现在看来,Long
的getLong()
方法应该返回String
的值作为Long
,所以答案“A”看起来很不错。话又说回来,如果真有这么简单,我何必费心要介绍这个呢?答案“B”似乎不太合理——除非getLong()
实际上并没有转换String
的值。所以剩下的主要嫌疑犯是答案“C”。它不明显,显得有些含糊。这正是你可能从谜题中预期的答案——这本身就足以让你重新考虑答案“A”。<笑>
好了,思考够了。是时候给你答案了,这样我们才能继续——正确答案是“C”。——视情况而定。
原来,Long
的getLong()
方法并不会直接尝试转换传入的String
的值。相反,它会取这个值,并查找一个同名的System
属性。如果找到,它会检索关联的值并将其转换为Long
。这意味着getLongValue()
方法返回什么取决于System
属性“23
”的值。
那么,如果不存在名为“23
”的System
属性会怎样?在这种情况下,getLong()
会返回null
,而getValueFromString()
方法在尝试调用getValue()
方法时会抛出NullPointerException
,因为retrievedLong
是null
。哎呀!
我是付出了沉痛的代价才了解到这一点。我需要转换一个String
,并从IDE提供的列表中选择了一个看似合适的方法。当我的单元测试失败时,我非常惊讶,于是查阅了Java文档,才意识到我的错误。
这个故事有几个寓意:
- 单元测试是你的朋友。
- 使用Java文档,并确保为使用你代码的人编写文档。
准备好迎接下一个谜题了吗?它叫做...
静态闪电
我正在修改一些遗留代码(定义为没有单元测试的任何代码),在尝试使用它时发现了一些意想不到的东西。原始代码相当复杂(可能因为没有测试),但这个简化版本具有相同的奇怪行为。
public class Greeting {
static {
initName();
}
static String name = "Sue";
static Test instance = new Test();
public static String getName() {
return name;
}
private static void initName() {
name = "Bond, James Bond";
}
public static void main(String[] args) {
System.out.println("My name is " + Test.getName() + ".");
}
}
在我们提出问题之前,让我们看一下Greeting
类。它有一个static
变量,三个static
方法和一个static
初始化块。(那是以“static
”开头并调用initName()
方法的代码块。)除了static
初始化块,它看起来是一个非常简单的类,很容易理解,对吧?
如果你不熟悉static
初始化块,让我稍微概括一下Sun的Java教程:
“静态初始化块是包含在花括号{}中的普通代码块,前面加上static关键字。它们可以出现在类体的任何位置,Java运行时保证它们按照在源代码中出现的顺序被调用。”
听起来不错,但它的意义是什么?你为什么要使用它?嗯,static
初始化块在类被加载到内存时执行,通常用于对类进行某种一次性处理。在本例中,当Greeting
类被加载到内存时——在其main()
方法执行之前——初始化块会调用initName()
方法。
所以,回到我们现在的问题(运行这个类会做什么?),以下是可能的答案:
- 打印“My name is Sue.”
- 打印“My name is Bond, James Bond.”
- 什么都不做,因为它无法编译。
- 以上都不是。
我建议在继续阅读之前花些时间弄清楚你自己的答案。这样做你会受益更多。别担心,我不介意等待。
准备好看答案了吗?很好。
首先,答案C,“什么都不做,因为它无法编译”是不正确的。我之所以包含它,只是为了告诉你,我个人不喜欢谜题或Java认证考试中出现这类答案。虽然这可能曾经是一项有用的技能,但如今我们有了更好的工具,要求一个“无法编译”是正确答案的问题似乎不太合理。好了,抱怨结束——回到谜题。
那么答案B呢?好吧,读到这里你应该知道,这一切的重点在于向你展示代码,它看起来很简单但行为却很奇怪。所以你可能没有选择答案B,而且你是对的。代码没有打印“My name is Bond, James Bond.”
那么备受欢迎的“以上都不是”呢?抱歉,这次不是。答案D也不正确。
通过排除法,正确答案一定是A。但它打印“My name is Sue.”似乎说不通——不是吗?不要相信我的话,把它复制到文件中自己运行一遍,然后再回来找解释。
说我第一次看到这个的时候很惊讶,那都是轻描淡写的说法。我不敢相信结果;它对我来说毫无意义,所以我用调试器一步步地跟踪。如果你也这样做,你可以看到initName()
方法确实在main()
方法之前被调用了,并且当initName()
完成时,“name
”的值是“Bond, James Bond
”——正如我们所料。
那么到底是怎么回事?“name
”是如何被设置为“Sue
”的?如果你继续逐步执行代码,你就会明白。注意,在static
初始化块之后,我们声明并初始化了“name
”变量。这似乎有点奇怪,因为initName()
方法已经为“name
”设置了一个值——所以它一定已经存在了,对吧?
差不多。编译器会为变量预留空间,并包含初始化它的代码。但是,因为“name
”是一个static
变量,它的初始化发生在类被加载到内存时,而不是在类构造函数被调用时。它还似乎,就像static
初始化块一样,static
变量的初始化发生在“它在源代码中出现的顺序”。所以,在static
初始化块被调用后,声明并初始化“name
”为“Sue
”的代码执行,并覆盖了我们预期的值。
这就是为什么答案A是正确的原因。信不信由你。
殊途同归
对于最后一个谜题,让我们来看看我在准备领导一个专注于Sun Java 6认证程序员考试的学习小组时发现的东西。
代码不多,但这个谜题涉及自动装箱和拆箱,并结合了前缀增量运算符(使其更有趣),因为我们要比较两个变量。
public class Bitwise {
public static void main(String[] args) {
Integer a = 5; Integer b = 5;
System.out.println(++a == ++b);
Integer c = 12345; Integer d = 12345;
System.out.println(++c == ++d);
}
}
运行这段代码会发生什么?
- 打印两行,“
true
”后跟“true
”。 - 打印两行,“
false
”后跟“false
”。 - 打印两行,“
true
”后跟“false
”。 - 打印两行,“
false
”后跟“true
”。
好了,我们来分析一下。所有变量的类型都是Integer
,所以当我们使用‘==
’比较它们时,我们实际上是在比较引用值,而不是它们的基本值。所以即使‘a
’和‘b
’具有相同的基础值(‘c
’和‘d
’也是如此),由于自动装箱,它们各自都指向一个新的Integer
实例。这意味着引用应该是不同的,‘==
’比较应该解析为‘false
’,这意味着答案应该是B。
显然,如果这么简单就太容易了,所以我们来看看其他的可能性。
虽然两个新创建的Integer
实例具有相同的引用似乎不合逻辑,但答案A是一致的,这似乎比答案C和D更合理一些。虽然这可能说得通,但事实是答案A不正确,答案B也不正确。
什么?我是在告诉你,有时你可以创建两个新的Integer
实例,它们会指向内存中的同一个对象吗?是的,我正是这个意思。
在继续阅读之前,你可能会觉得运行代码并查看结果会让你感觉好一些,这样你就知道我不是在编造。
根据Kathy Sierra和Bert Bates(SCJP 6 Study Guide的作者)的说法,Java在原始值适合一个字节时会重用包装对象,以节省内存。所以如果两个布尔值或字节具有相同的基础值,它们的引用也相同。对于值在\u0000到\u007F之间的字符,以及值在-128到127之间的Short
和Integer
,情况也是如此。
所以,当Integer
‘a
’和‘b
’被设置为5
时,它们都指向同一个Integer
实例,‘==
’比较为true
。但是,因为12345
超出了这个神奇的范围,变量‘c
’和‘d
’得到了它们自己的Integer
实例,所以‘==
’比较为false
。奇怪,但这是真的。
摘要
Java是一门很棒的语言,但有些不寻常的行为确实会让你头疼。不要因此而气馁。写下来并分享,这样我们所有人都可以从你的发现中受益。
如果你对此有任何反馈,请发表评论。
历史
- 2009年5月1日:初始帖子