散列与散列码
正确的equals()方法必须满足以下5个条件:
1. 自反性.对任意x,x.equals(x)一定返回true.
2. 对称性.对任意x/y,如果y.equals(x)返回true,则x.equals(y)也返回true
3. 传递性.对任意x/y/z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true.
4. 一致性.对任意x/y,如果对象中用于等价比较的信息没有改变,那么无论如何调用x.equals(y)多少次,返回的结果应该保持一致,那么一直是true,要么一直是false
5. 对任何不是null的x,x.equals(null)一定返回false.
Object.equals()只是比较对象的地址
如果想用自己的类作为HashMap的键,必须同时重载hashCode()和equals()
instanceof会偷偷检查左侧的对象是否为null,如果位null,则返回false
理解hashCode()
散列的目的在于:想要使用一个对象来查找另一个对象.
瓶颈的原因在于查询速度.
散列将键的信息保存在某处,以便能快速找到(数组是存储元素最快的数据结构).通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码,由散列函数(Object.hashCode())生成.
为了解决数组容量被固定的问题,不同的键可以产生相同的下标.也就是说可能会有冲突,因此数组多大也就不重要了,任何键都能在数组中找到他的位置.
查询一个值的过程首先就是计算散列码,然后使用散列码查询数组,如果能够保证没有冲突(如果值的数量是固定的,那么就有可能冲突),那么就有了一个完美的散列函数.
通常,冲突由外部链接处理:数组并不直接保存值,而是保存值的list.然后对list中的值使用equals()方法进行线性的查询.
如果散列函数比较好,数组的每个list就会有比较少的值.
因此不是查询整个list,而是快速的跳到数组的某个位置,只对很少的元素进行比较.这就是HashMap会如此快的原因.
由于散列表中的”槽位 slot”通常称为捅位(bucket),为了使散列均匀分布,桶的数量通常使用2的整数次方,因为除法和求余是最慢的操作,可以用掩码来替代.
覆盖hashCode()
- 给int变量result赋予某个非零值常量
- 为对象内有意义的域(即每个可以做equals()的域)计算出一个int散列码c
- 合并计算的得到的散列码 result = 31 * result + c;
- 返回result
域类型 | 计算 |
---|---|
boolean | c = f ? 0 : 1 |
byte/char/short/int | c = (int)f; |
long | c = (int)(f ^ f>>>32) |
float | c = Float.floatToInBits(f); |
double | long l = Double.doubleToLongBits(f); c = (int)(l ^ (l>>>32)); |
Object | c = f.hashCode(); |
数组 | 对每个元素应用应用以上规则 |
性能
可以手工调整HashMap来提高性能:
1. 容量:桶位数
2. 初始容量:在创建是所拥有的捅位数.HashMap和HashSet都允许设置.
3. 尺寸:表中当前存储的项数.
4. 负载因子:尺寸/容量.空表的负载因子是0,而半满的负载因子是0.5.负载轻的表,出产生冲突的可能性小,对于插入和查找都是最理想的.
HashMap和HashSet都允许指定负载因子,当达到负载因子的水平时,容器将自动增加其容量(桶位数),实现方式是容量大致加倍,并重新将现有的对象分部到新的捅位中,称之为再散列.
A`
HashMap使用的负载因子是0.75(只有当表达到四分之三时,才进行散列),这个因子在时间和空间代价中达到了平衡,更高的负载因子可以降低表所需的空间,但是会增加查找的代价.