65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 小谜题

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (28投票s)

2009 年 4 月 30 日

CPOL

9分钟阅读

viewsIcon

41861

三个 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(); 
  }

以下是可能的答案列表

  1. 23
  2. 0(零)
  3. 视情况而定

在继续阅读之前,你认为正确答案是什么?

这么快就结束了?好吧,让我们看看可能的答案,了解一下你的表现。
现在看来,LonggetLong()方法应该返回String的值作为Long,所以答案“A”看起来很不错。话又说回来,如果真有这么简单,我何必费心要介绍这个呢?答案“B”似乎不太合理——除非getLong()实际上并没有转换String的值。所以剩下的主要嫌疑犯是答案“C”。它不明显,显得有些含糊。这正是你可能从谜题中预期的答案——这本身就足以让你重新考虑答案“A”。<笑>

好了,思考够了。是时候给你答案了,这样我们才能继续——正确答案是“C”。——视情况而定。

原来,LonggetLong()方法并不会直接尝试转换传入的String的值。相反,它会取这个值,并查找一个同名的System属性。如果找到,它会检索关联的值并将其转换为Long。这意味着getLongValue()方法返回什么取决于System属性“23”的值。

那么,如果不存在名为“23”的System属性会怎样?在这种情况下,getLong()会返回null,而getValueFromString()方法在尝试调用getValue()方法时会抛出NullPointerException,因为retrievedLongnull。哎呀!

我是付出了沉痛的代价才了解到这一点。我需要转换一个String,并从IDE提供的列表中选择了一个看似合适的方法。当我的单元测试失败时,我非常惊讶,于是查阅了Java文档,才意识到我的错误。

这个故事有几个寓意:

  1. 单元测试是你的朋友。
  2. 使用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()方法。

所以,回到我们现在的问题(运行这个类会做什么?),以下是可能的答案:

  1. 打印“My name is Sue.”
  2. 打印“My name is Bond, James Bond.”
  3. 什么都不做,因为它无法编译。
  4. 以上都不是。

我建议在继续阅读之前花些时间弄清楚你自己的答案。这样做你会受益更多。别担心,我不介意等待。

准备好看答案了吗?很好。

首先,答案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);

    }

}

运行这段代码会发生什么?

  1. 打印两行,“true”后跟“true”。
  2. 打印两行,“false”后跟“false”。
  3. 打印两行,“true”后跟“false”。
  4. 打印两行,“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之间的ShortInteger,情况也是如此。

所以,当Integera’和‘b’被设置为5时,它们都指向同一个Integer实例,‘==’比较为true。但是,因为12345超出了这个神奇的范围,变量‘c’和‘d’得到了它们自己的Integer实例,所以‘==’比较为false。奇怪,但这是真的。

摘要

Java是一门很棒的语言,但有些不寻常的行为确实会让你头疼。不要因此而气馁。写下来并分享,这样我们所有人都可以从你的发现中受益。

如果你对此有任何反馈,请发表评论。

历史

  • 2009年5月1日:初始帖子
© . All rights reserved.