前些日子,阿里妹(妹子出题也这么难)发表了一篇文章《悬赏征集!
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);
float b = 0.9f - 0.8f;
System.out.println(b);
加上两条打印语句后,我明白了,原来发生了精度问题。
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);
Float b = Float.valueOf(0.9f - 0.8f);
System.out.println(b);
加上两条打印语句后,我明白了,原来包装器并不会解决精度的问题。
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;
}
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);
BigDecimal b = new BigDecimal("0.1");
System.out.println(b);
这究竟又是怎么回事呢?
这就必须看官方文档了,是时候搬出
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)));
System.out.println(BigDecimal.valueOf(a));
第一种,使用
String.valueOf()
把 double 转为字符串。
第二种,使用
valueOf()
方法,该方法内部会调用
Double.toString()
将 double 转为字符串,源码如下:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
05、ReentrantLock
最后一题,也就是第五题,代码如下: