如果你看过往期的问题,就会发现每一个都不简单。
这些试题模拟了认证考试中的一些难题。 而 “中级(intermediate)” 和 “高级(advanced)” 指的是试题难度,而不是说这些知识本身很深。 一般来说,“高级”问题会稍微难一点。先思考一个简单的问题: 两个 Integer 包装类对象。 怎样比较它们的值是否相等,有哪些方法?
问题(中级难度)
在开发中我们经常会使用包装类(例如 Boolean, Double, 以及 Integer 等等)。
请看下面的代码片段:
String one = "1";
Boolean b1 = Boolean.valueOf(one); // line n1Integer i1 = new Integer(one);
Integer i2 = 1;
if (b1) {
System.out.print(i1 == i2);
}
执行结果是什么, 请选择:
A、 抛出运行时异常
B、 true
C、 false
D、 无任何输出
答案和解析
这个问题考察原生数据的包装类(primitive wrapper),主要是 Boolean 类比较生僻的 valueOf 工厂方法。
在认证考试和面试中,这个问题可能不太容易碰到,因为主要还是靠死记硬背, 大部分考试都会避免此类问题。
但是,这个问题从多个方面综合考察了面试者对Java语言的理解和认识水平, 有一点小坑,但关键在于解答的过程。
包装类主要提供了三种获取对象实例的方法:每个包装类都有名为 valueOf 的静态工厂方法。
如果语义很清晰, 在代码中将原生数据类型赋值给包装类的变量,则会发生自动装箱 (autoboxing)。 自动装箱只是语法上的简写,它允许编译器 (javac) 自动调用valueOf方法, 目的是为了编码更简洁。
第三种方法是使用构造器, 也就是通过 new 关键字来调用构造函数。 实际上,在 Java 9 中已经不推荐使用第三种方法, 而本文的一个目标是解释为什么不赞成使用它。
在Java中,只要使用 new 关键字调用构造函数,只会发生两种情况: 要么成功创建指定类型的新对象并返回,要么就抛异常。
这实际上是一个限制,如今一般是推荐使用工厂方法, 因为工厂方法除了达成构造函数的效果之外, 还会有一些优化。
工厂方法的有些功能是用构造函数实现不了的: 比如返回与请求参数相匹配的已缓存的实例对象。
因为 Integer 包装器是不可变的, 表示相同数值的两个Integer对象一般是可以互换的。
因此,创建多个表示相同值的对象实例会浪费内存。
很多情况下,工厂方法返回的两个对象允许使用 == 来比较, 而不必每次都写成 equals(Object o) 这种方式。
对于 Integer 类来说,一般只缓存了 -128 到 +127 范围内的值。这种行为类似于在编码中直接使用 "XXX" 这种字面量表示方式, 而不是 new String("XXX")。
工厂方法更加灵活:如果有多个工厂方法,则每个方法都可以使用不同的名称,因为名称不同,也就可以使用相同的入参声明。
对于构造函数而言,因为必须参数类型不同才能形成重载,也就不可能根据同样的参数构造不同的对象。
第三个优点是, Java中用 new 调用构造函数只能返回固定类型的对象。
而用工厂方法则可以返回兼容的各种类型对象实例(例如接口的实现类,而且这是一种隐藏实现细节的绝佳方法)。
回到这个问题,最关键的地方在于, 我们使用 Boolean.valueOf(...) 方法时, 只会得到两个常量对象: Boolean.TRUE 和 Boolean.FALSE。
这两个对象可以被重复利用,不会浪费多余的内存。 如果使用 new 调用显然是不可能的。
大部分包装类的工厂方法, 如果传入了 null 参数, 或者字符串参数不符合目标值的表现形式就会抛出异常,例如,Integer.valueOf("six") 就会抛异常。
但 java.lang.Boolean 类的工厂方法是个特例, 内部实现判断的是非空(null)并且等于 “true”(忽略大小写)。
内部实现如下所示:
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
这里的输出肯定是 false。
前面提到过,new 关键字的任何调用,要么产生一个新对象, 要么抛异常。
这意味着 v2 和 v1 引用了不同的对象,== 操作的结果为 false。
换一种方式,如果有以下代码:
Integer v1 = new Integer("1");
Integer v2 = 1;
System.out.print(v1 == v2);
这与面试题中的代码很像,一个使用构造函数, 一个使用自动装箱,可以肯定这也会输出 false。
构造函数创建的对象必定是唯一的新对象,因此,不可能 == 自动装箱为工厂方法返回的对象。
不可变对象的工厂方法一般都会有特殊处理,只要在一个范围内,并且参数相等,就返回同一个(缓存的)对象。
Integer 类的API文档中,对 valueOf(int) 方法有如下说明:“此方法将始终缓存 [-128 ~ 127] 范围内的值, 可能还会缓存这个范围之外的其他值。”
Integer v1 = Integer.valueOf(1);
Integer v2 = Integer.valueOf(1);
System.out.print(v1 == v2);
也就是说,上面这段代码肯定会输出 true。
虽然只在 valueOf(int) 和 valueOf(String) 方法的文档说明中提到了这个缓存保证。
但在实际的实现中, 其他包装类也表现出相同的缓存行为。
当然,这里讨论了两个 Integer 对象: 一个是使用构造函数创建,另一个是使用自动装箱创建(Integer.valueOf(int) 方法)。
假如我们稍微改变一下面试题中 if 语句,则输出内容将为 false。
总结: 本文开始提到的面试题, 选项D是正确答案。 这里只是附带的讨论。
————————————————
版权声明:本文为CSDN博主「铁锚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。