8

Java坑人面试题系列: 包装类(中级难度)

 3 years ago
source link: https://renfufei.blog.csdn.net/article/details/104163518
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java Magazine上面有一个专门坑人的面试题系列: https://blogs.oracle.com/javamagazine/quiz-2

这些问题的设计宗旨,主要是测试面试者对Java语言的了解程度,而不是为了用弯弯绕绕的手段把面试者搞蒙。

如果你看过往期的问题,就会发现每一个都不简单。

这些试题模拟了认证考试中的一些难题。 而 “中级(intermediate)” 和 “高级(advanced)” 指的是试题难度,而不是说这些知识本身很深。 一般来说,“高级”问题会稍微难一点。

先思考一个简单的问题: 两个 Integer 包装类对象。 怎样比较它们的值是否相等,有哪些方法?

问题(中级难度)

在开发中我们经常会使用包装类(例如 Boolean, Double, 以及 Integer 等等)。

请看下面的代码片段:

String one = "1";
Boolean b1 = Boolean.valueOf(one);  // line n1
Integer 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语言的理解和认识水平, 有一点小坑,但关键在于解答的过程。

包装类主要提供了三种获取对象实例的方法:

    1. 每个包装类都有名为 valueOf 的静态工厂方法。
    1. 如果语义很清晰, 在代码中将原生数据类型赋值给包装类的变量,则会发生自动装箱 (autoboxing)。 自动装箱只是语法上的简写,它允许编译器 (javac) 自动调用valueOf方法, 目的是为了编码更简洁。
    1. 第三种方法是使用构造器, 也就是通过 new 关键字来调用构造函数。 实际上,在 Java 9 中已经不推荐使用第三种方法, 而本文的一个目标是解释为什么不赞成使用它。

在Java中,只要使用 new 关键字调用构造函数,只会发生两种情况: 要么成功创建指定类型的新对象并返回,要么就抛异常。
这实际上是一个限制,如今一般是推荐使用工厂方法, 因为工厂方法除了达成构造函数的效果之外, 还会有一些优化。

工厂方法的有些功能是用构造函数实现不了的: 比如返回与请求参数相匹配的已缓存的实例对象。
因为 Integer 包装器是不可变的, 表示相同数值的两个Integer对象一般是可以互换的。
因此,创建多个表示相同值的对象实例会浪费内存。
很多情况下,工厂方法返回的两个对象允许使用 == 来比较, 而不必每次都写成 equals(Object o) 这种方式。
对于 Integer 类来说,一般只缓存了 -128 到 +127 范围内的值。

这种行为类似于在编码中直接使用 "XXX" 这种字面量表示方式, 而不是 new String("XXX")

工厂方法更加灵活:

  • 如果有多个工厂方法,则每个方法都可以使用不同的名称,因为名称不同,也就可以使用相同的入参声明。
  • 对于构造函数而言,因为必须参数类型不同才能形成重载,也就不可能根据同样的参数构造不同的对象。

第三个优点是, Java中用 new 调用构造函数只能返回固定类型的对象。
而用工厂方法则可以返回兼容的各种类型对象实例(例如接口的实现类,而且这是一种隐藏实现细节的绝佳方法)。

回到这个问题,最关键的地方在于, 我们使用 Boolean.valueOf(...) 方法时, 只会得到两个常量对象: Boolean.TRUEBoolean.FALSE
这两个对象可以被重复利用,不会浪费多余的内存。 如果使用 new 调用显然是不可能的。

大部分包装类的工厂方法, 如果传入了 null 参数, 或者字符串参数不符合目标值的表现形式就会抛出异常,例如,Integer.valueOf("six") 就会抛异常。

java.lang.Boolean 类的工厂方法是个特例, 内部实现判断的是非空(null)并且等于 “true”(忽略大小写)。

内部实现如下所示:

public static boolean parseBoolean(String s) {
    return ((s != null) && s.equalsIgnoreCase("true"));
}

如果满足这两个条件则返回 Boolean.TRUE
否则直接返回 Boolean.FALSE
这意味着: 如果传入 null 或者无意义的字符串, 则会返回 Boolean.FALSE,并不会抛出异常。

基于这点,我们可以确定 n1 行那里不会抛出异常,而是返回 Boolean.FALSE, 被赋值给变量 b1
因此,可以确定 选项A不正确

然后我们看一下 if 语句和里面的比较代码。

一般来说 if 语句小括号中的表达式必须是 boolean 类型。
显然,这里会自动将 Boolean 对象进行拆箱操作, 变为 boolean 类型。
这算是Java的基础知识,当然,如果在 Java 5 之前的版本这样写, 代码确实会无法编译。
即使有这样的担忧,但因为没有【编译错误】的选项,所以我们不关注这个问题。

在这种情况下,我们已经确定 b1 所引用的对象值相当于 false。 因此,if 判断不通过,里面的代码不会被执行。
所以我们可以确定 选项D是正确的

虽然我们已经确定 if 语句内部的代码没有执行,但是面试过程中可能会问到: 如果执行了呢,又是什么结果。

Java语言中有两种形式的相等比较。

  • 第一种是 == 运算符,是Java语法的一部分。
  • 第二种是 equals(Object o) 方法,本质上是一个API。

每个对象都可以使用 equals(Object o) 方法,因为这个方法是在 java.lang.Object 类中定义的。
除非某个类覆写了equals方法,否则这个方法一般不定返回 true
下面我们主要讨论 == 运算符,如果对 equals 方法的实现感兴趣, 请参考: Java中hashCode与equals方法的约定及重写原则

== 运算符比较两个表达式的值。
听起来很简单,但是表达式的值可能有两种不同的类型。这两种类型使用 == 的结果可能会不同。
顺便说一下,这里故意使用术语“表达式”, 而变量是一种简单的表达式。

表达式主要有两种类型:

  • 原生数据类型/基本数据类型 (primitive, 共8种: boolean, byte, short, char, int, long, float, double)
  • 引用类型(reference)。 引用类似于指针, 表示内存中某个对象的地址值(可以认为是一个偏移量数值)。

如果表达式是原生数据类型,则表达式的值很直观。 例如,如果 int 表达式的值为 32,则该表达式的值就是32的二进制表示形式。

但问题是,如果变量是引用类型呢(例如,Integer 类型), 它所引用对象内部的值为32,那么这个引用的值 并不是32
而是一个神秘的数字(引用地址),通过这个引用地址,JVM可以找到对应的 Integer 对象。

也就是说,对于引用类型(即除了8种原生数据类型之外的所有类型), == 表达式判断的是这两个引用的内存地址值是否相等,即判断它们是否引用了同一个对象。
最重要的是,即使两个 Integer 对象里面的值都是 32,但如果它们是不同的对象, 那么它们的引用地址也就不同,使用==比较会返回 false

这一点应该很好理解,再看下面这样的代码:

Integer v1 = new Integer("1");
Integer v2 = new Integer("1");
System.out.print(v1 == v2);

这里的输出肯定是 false
前面提到过,new 关键字的任何调用,要么产生一个新对象, 要么抛异常。
这意味着 v2v1 引用了不同的对象,== 操作的结果为 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是正确答案。 这里只是附带的讨论。

原文链接: https://blogs.oracle.com/javamagazine/quiz-intermediate-wrapper-classes


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK