最近,在啃《Effective Java》(下文用《E》表示),从中学习到了不少以前在开发过程中没有注意到的一些问题,收获不少。
一、Item48 关于BigDecimal 和float double的问题。
看到它的Item48,讨论了关于float和double类型的问题。以前对此都比较疏忽的,随便使用一个float四舍五入一下就过去了,看完之后,重新认识了一下Java中关于数值的处理。
起因是,使用float或者double无法精确的描述一个数字,比如:0.1
public class Test { public static void main(String[] args) { System.out.println(1.00 - 9 * 0.10); } }
在IDE中运行的结果为:0.09999999999999998。
如何解决此类问题呢?答案是使用BigDecimal。
翻看JDK,查看BigDecimal的说明如下:
不可变的、任意精度的有符号十进制数。BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。因此,BigDecimal 表示的数值是 (unscaledValue × 10-scale)。
可以看到,该类型可以描述任意精度的有符号数。表示的方式其实就是我们常说的科学计数法,使用底数和幂来描述一下数字的大小。
public static void main(String[] args) { BigDecimal bda = new BigDecimal("1.0"); BigDecimal bdb = new BigDecimal("0.9"); System.out.println(bda.subtract(bdb)); System.out.println(1.0 - 0.9); }
IDE的运行结果如下:
0.1
0.09999999999999998
在《E》中,作者对BigDecimal提出了2种情况不推荐使用:
1、使用BigDecimal的效率比使用int long等类型效率要低,原因显而易见的;
2、如果是解决一个小的问题,就没有必要使用BigDecimal。
二、Item 9 当重写equals方法的时候,总是重写hashCode方法
《E》中的黑体字:每一个重写了equals方法的类必须重写hashCode方法。
JDK中关于hashCode的说明:
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地 返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用
hashCode
方法都必须生成相同的整数结果。 - 如果根据
equals(java.lang.Object)
方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
下面看看,如果不写hashCode方法的结果如何:
import java.util.HashMap; import java.util.Map; public class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "area code"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } private static void rangeCheck(int arg, int max, String name) { if (arg < 0 || arg > max) throw new IllegalArgumentException(name +": " + arg); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } public static void main(String[] args) { Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(m.get(new PhoneNumber(707, 867, 5309))); } }
运行结果为:NULL。导致在map.get的时候,在调用equal方法的时候失败,显然是因为违反了hashCode方法的第二条(黑体)的规则。
如果加上hashCode方法,程序运行正常:
@Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; }
注意:hashCode方法返回的是int,也就是说,有可能不同的2个object,有相同的hashCode值。见JDK文档的关于hashCode说明的第三条:
如果根据 equals(java.lang.Object)
方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。
改情况的发生叫做“hash碰撞”。如果hash碰撞的几率越大,那么在map.get方法执行的过程也越慢。原因看HashMap的get方法的实现。
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
table的定义:transient Entry[] table;
表示每个hash节点下面有多个key,所以,判断的条件为:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
三、Item8 书写规范的equals方法。
该条例作者用了很大的篇幅,可见该条例的重要性。
首先看JDK中对equals的描述:
Object
类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x
和 y
,当且仅当 x
和 y
引用同一个对象时,此方法才返回 true
(x == y
具有值 true
)。
从文档中可以清楚的看到,该方法是Object类的实现,即只有同时引用一个对象的时候,才相等。所以,我们需要在程序中,根据自己的业务规则,重写equals方法。
下面再来看看JDK是怎么描述2个object相等的:
- 自反性:对于任何非空引用值
x
,x.equals(x)
都应返回true
。 - 对称性:对于任何非空引用值
x
和y
,当且仅当y.equals(x)
返回true
时,x.equals(y)
才应返回true
。 - 传递性:对于任何非空引用值
x
、y
和z
,如果x.equals(y)
返回true
,并且y.equals(z)
返回true
,那么x.equals(z)
应返回true
。 - 一致性:对于任何非空引用值
x
和y
,多次调用 x.equals(y) 始终返回true
或始终返回false
,前提是对象上equals
比较中所用的信息没有被修改。 - 对于任何非空引用值
x
,x.equals(null)
都应返回false
。
那么,如何写出一个正确的equals方法呢?《E》给出了5点要求:
1、用==来判断是否是同一个对象
2、用instanceof来判断数据类型是否一致
3、将传入的参数强制类型转换
4、类中每个“签名”字段的比较,注意对Null对象的处理
5、写完之后,问问自己,equals是否符合JDK中的规范
下面看看一个规范的equals方法:
@Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; }