专栏名称: 纯洁的微笑
分享微服务实践与Java技术干货、偶尔讲讲故事。在人工智能的时代,一起学习微服务架构演进和大数据治理。
目录
相关文章推荐
中国证券报  ·  持续火热!300718,大涨超10倍 ·  15 小时前  
四川发布  ·  早安四川丨全力以“复” ·  昨天  
上海证券报  ·  京东出手!为外卖骑手缴纳五险一金 ·  昨天  
中国证券报  ·  就在明晚,油价或下调 ·  2 天前  
四川发布  ·  早安四川丨干字当头 ·  3 天前  
51好读  ›  专栏  ›  纯洁的微笑

阿里最强面试题,8 年 Java 经验我老泪纵横

纯洁的微笑  · 公众号  ·  · 2019-09-04 09:09

正文

前些日子,阿里妹(妹子出题也这么难)发表了一篇文章《悬赏征集! 5 道题征集代码界前 3% 的超级王者》——看到这个标题,我内心非常非常激动,因为终于可以证明自己技术很牛逼了。

但遗憾的是, 凭借 8 年的 Java 开发经验, 我发现这五道题自己全解错了! 惨痛的教训再次证明,我是那被秒杀的 97% 的工程师之一。

不过,好歹我这人脸皮特别厚,虽然全都做错了,但还是敢于坦然地面对自己。

01、原始类型的 float

第一题是这样的,代码如下

public class FloatPrimitiveTest {    public static void main(String[] args) {        float a = 1.0f - 0.9f;        float b = 0.9f - 0.8f;        if (a == b) {            System.out.println("true");        } else {            System.out.println("false");        }    }}

乍一看,这道题也太简单了吧?

1.0f - 0.9f 的结果为 0.1f, 0.9f - 0.8f 的结果为 0.1f,那自然 a == b 啊。

但实际的结果竟然不是这样的,太伤自尊了。

点击空白处查看答案(可以下拉)

float a = 1.0f - 0.9f;System.out.println(a); // 0.100000024float b = 0.9f - 0.8f;System.out.println(b); // 0.099999964

加上两条打印语句后,我明白了,原来发生了精度问题。


Java 语言支持两种基本的浮点类型:float 和 double ,以及与它们对应的包装类 Float 和 Double 。它们都依据 IEEE 754 标准,该标准用科学记数法以底数为 2 的小数来表示浮点数。


但浮点运算很少是精确的。虽然一些数字可以精确地表示为二进制小数,比如说 0.5,它等于 2-1;但有些数字则不能精确的表示,比如说 0.1。因此,浮点运算可能会导致舍入误差,产生的结果接近但并不等于我们希望的结果。


所以,我们看到了 0.1 的两个相近的浮点值,一个是比 0.1 略微大了一点点的 0.100000024,一个是比 0.1 略微小了一点点的 0.099999964。


Java 对于任意一个浮点字面量,最终都舍入到所能表示的最靠近的那个浮点值,遇到该值离左右两个能表示的浮点值距离相等时,默认采用偶数优先的原则——这就是为什么我们会看到两个都以 4 结尾的浮点值的原因。



02、包装器类型 Float

再来看第二题,代码如下:

public class FloatWrapperTest {    public static void main(String[] args) {        Float a = Float.valueOf(1.0f - 0.9f);        Float b = Float.valueOf(0.9f - 0.8f);        if (a.equals(b)) {            System.out.println("true");        } else {            System.out.println("false");        }    }}

乍一看,这道题也不难,对吧?无非是把原始类型的 float 转成了包装器类型 Float,并且使用 equals 替代 == 进行判断。

这一次,我以为包装器会解决掉精度的问题,所以我猜想输出结果为 true 。但结果再次打脸——虽然我脸皮厚,但仍然能感觉到脸有些微微的红了起来。

Float a = Float.valueOf(1.0f - 0.9f);System.out.println(a); // 0.100000024Float b = Float.valueOf(0.9f - 0.8f);System.out.println(b); // 0.099999964

加上两条打印语句后,我明白了,原来包装器并不会解决精度的问题。

private final float value;public Float(float value) {    this.value = value;}public static Float valueOf(float f) {    return new Float(f);}public boolean equals(Object obj) {    return (obj instanceof Float)           && (floatToIntBits(((Float)obj).value) == floatToIntBits(value));}

从源码可以看得出来,包装器 Float 的确没有对精度做任何处理,况且 equals 方法的内部仍然使用了 == 进行判断。

03、switch 判断 null 值的字符串

来看第三题,代码如下:

public class SwitchTest {    public static void main(String[] args) {        String param = null;        switch (param) {            case "null":                System.out.println("null");                break;            default:                System.out.println("default");        }    }}


这道题就有点令我雾里看花了。

我们都知道,switch 是一种高效的判断语句,比起 if/else 真的是爽快多了。尤其是 JDK 1.7 之后,switch 的 case 条件可以是 char, byte, short, int, Character, Byte, Short, Integer, String, 或者 enum 类型。

本题中,param 类型为 String,那么我认为是可以作为 switch 的 case 条件的,但 param 的值为 null,null 和 “null” 肯定是不匹配的,我认为程序应该进入到 default 语句输出 default。

但结果再次打脸!程序抛出了异常:

Exception in thread "main" java.lang.NullPointerException  at com.cmower.java_demo.Test.main(Test.java:7)

也就是说, switch () 的括号中不允许传入 null。为什么呢?

我翻了翻 JDK 的官方文档,看到其中有这样一句描述,我直接搬过来大家看一眼就明白了。

When the switch statement is executed, first the Expression is evaluated. If the Expression evaluates to null, a NullPointerException is thrown and the entire switch statement completes abruptly for that reason. Otherwise, if the result is of a reference type, it is subject to unboxing conversion.


大致的意思就是说,switch 语句执行的时候,会先执行 switch () 表达式,如果表达式的值为 null,就会抛出 NullPointerException 异常。

那到底是为什么呢?

public static void main(String args[]){    String param = null;    String s;    switch((s = param).hashCode())    {    case 3392903:         if(s.equals("null"))        {            System.out.println("null");            break;        }        // fall through
default: System.out.println("default"); break; }}

借助 jad,我们来反编译一下 switch 的字节码,结果如上所示。原来 switch () 表达式内部执行的竟然是 (s = param).hashCode() ,当 param 为 null 的时候,s 也为 null,调用 hashCode() 方法的时候自然会抛出 NullPointerException 了。

04、BigDecimal 的赋值方式

来看第四题,代码如下:

public class BigDecimalTest {    public static void main(String[] args) {        BigDecimal a = new BigDecimal(0.1);        System.out.println(a);        BigDecimal b = new BigDecimal("0.1");        System.out.println(b);    }}

这道题真不难,a 和 b 的唯一区别就在于 a 在调用 BigDecimal 构造方法赋值的时候传入了浮点数,而 b 传入了字符串,a 和 b 的结果应该都为 0.1,所以我认为这两种赋值方式是一样的。

但实际上,输出结果完全出乎我的意料:

BigDecimal a = new BigDecimal(0.1);System.out.println(a); // 0.1000000000000000055511151231257827021181583404541015625BigDecimal b = new BigDecimal("0.1");System.out.println(b); // 0.1

这究竟又是怎么回事呢?

这就必须看官方文档了,是时候搬出 BigDecimal(double val) 的 JavaDoc 镇楼了。

The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.10000000000000000555111512312578270211815834045410...


解释:使用 double 传参的时候会产生不可预期的结果,比如说 0.1 实际的值是 0.1000000000000000055511151231257827021181583404541015625,说白了,这还是精度的问题。(既然如此,为什么不废弃呢?)

The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal(“0.1”) creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.


解释:使用字符串传参的时候会产生预期的结果,比如说 new BigDecimal("0.1") 的实际结果就是 0.1。

When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor.


解释:如果必须将一个 double 作为参数传递给 BigDecimal 的话,建议传递该 double 值匹配的字符串值。方式有两种:


double a = 0.1;System.out.println(new BigDecimal(String.valueOf(a))); // 0.1System.out.println(BigDecimal.valueOf(a)); // 0.1

第一种,使用 String.valueOf() 把 double 转为字符串。

第二种,使用 valueOf() 方法,该方法内部会调用 Double.toString() 将 double 转为字符串,源码如下:

public static BigDecimal valueOf(double val) {    // Reminder: a zero double returns '0.0', so we cannot fastpath    // to use the constant ZERO.  This might be important enough to    // justify a factory approach, a cache, or a few private    // constants, later.    return new BigDecimal(Double.toString(val));}

05、ReentrantLock

最后一题,也就是第五题,代码如下:

public class






请到「今天看啥」查看全文