Hashmap
Map是一种键-值(key-value)映射表,Hashmap
就是map的一种,HashMap
之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引:
如果key
的值为"a"
,计算得到的索引总是1,因此返回value
为Person("Xiao Ming")
,如果key的值为"b",计算得到的索引总是5,因此返回value为Person("Xiao Hong")
,这样,就不必遍历整个数组,即可直接读取key对应的value。
当我们使用key存取value的时候,就会引出一个问题:
我们放入Map的key是字符串"a",但是,当我们获取Map的value时,传入的变量不一定就是放入的那个key对象。
换句话讲,两个key应该是内容相同,但不一定是同一个对象。
import java.util.HashMap;//记得写HashMap的包
import java.util.Map;
public class Main {
public static void main(String[] args) {
String key1 = "a";
Map<String, Integer> map = new HashMap<>();//构建新映射的方法
map.put(key1, 123);
String key2 = new String("a");
map.get(key2); // 123
System.out.println(key1 == key2); // false因为地址不同
System.out.println(key1.equals(key2)); // true值是相同的
}
}
因为在Map
的内部,对key
做比较是通过equals()
实现的,这一点和List
查找元素需要正确覆写equals()
是一样的,即正确使用Map
必须保证:作为key
的对象必须正确覆写equals()
方法。
我们经常使用String
作为key
,因为String
已经正确覆写了equals()
方法。但如果我们放入的key
是一个自己写的类,就必须保证正确覆写了equals()
方法。我们再看一下HashMap
为什么能通过key
直接计算出value
存储的索引。相同的key
对象(使用equals()
判断时返回true
)必须要计算出相同的索引,否则,相同的key
每次取出的value
就不一定对。通过key
计算索引的方式就是调用key
对象的hashCode()
方法,它返回一个int
整数。HashMap
正是通过这个方法直接定位key
对应的value
的索引,继而直接返回value
。(这种方式就像数据结构算法里面的散列查找一样)
因此,正确使用Map
必须保证:
①作为key的对象必须正确覆写equals()方法
②相等的两个key实例调用equals()必须返回true
②作为key的对象还必须正确覆写hashCode()方法
hashCode()方法要严格遵循以下规范:
①如果两个对象相等,则两个对象的hashCode()必须相等;
②如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。
比如我们给person编写hashcode方法
public class Person {
String firstName;
String lastName;
int age;
//这3个字段分别相同的实例,hashCode()返回的int必须相同
@Override
int hashCode() {
int h = 0;
h = 31 * h + firstName.hashCode();
h = 31 * h + lastName.hashCode();
h = 31 * h + age;
return h;
}
}
String
类已经正确实现了hashCode()
方法,我们在计算Person
的hashCode()
时,反复使用31*h
,这样做的目的是为了尽量把不同的Person
实例的hashCode()
均匀分布到整个int范围(类似散列查找里为了解决冲突的散列表构造方法一样避免产生聚集现象)。和实现equals()
方法遇到的问题类似,如果firstName
或lastName
为null,上述代码工作起来就会抛NullPointerException
。
为了解决这个问题,我们在计算hashCode()
的时候,经常借助Objects.hash()
来计算:
int hashCode() {
return Objects.hash(firstName, lastName, age);
}
编写equals()和hashCode()遵循的原则是:
equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算,没有使用到的字段,绝不可放在hashCode()中计算。
拓展延伸
既然HashMap
内部使用了数组,通过计算key
的hashCode()
直接定位value
所在的索引,那么问题来了:hashCode()
返回的int
范围高达±21亿
,就算不考虑负数,HashMap
内部使用的数组得有多大?
实际上HashMap
初始化时默认的数组大小只有16
,任何key
,无论它的hashCode()
有多大,都可以简单地通过:
int index = key.hashCode() & 0xf; //16进制 0xf = 15
把索引确定在0~15
,即永远不会超出数组范围,上述算法只是一种最简单的实现。
如果添加超过16
个key-value
到HashMap
,数组不够用了怎么办?
添加超过一定数量的key-value
时,HashMap
会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()
计算的索引位置。例如,对长度为32的数组计算hashCode()
对应的索引,计算方式要改为:
int index = key.hashCode() & 0x1f; // 0x1f = 31
由于扩容会导致重新分布已有的key-value
,所以,频繁扩容对HashMap
的性能影响很大。如果我们确定要使用一个容量为10000
个key-value
的HashMap
,更好的方式是创建HashMap
时就指定容量:
Map<String, Integer> map = new HashMap<>(10000);
虽然指定容量是10000
,但HashMap
内部的数组长度总是2n,因此,实际数组长度被初始化为比10000
大的16384
(214)。
和散列查找一样在编写hashcode
方法时要尽量避免产生相同的hash
值,那样hashmap
在用的时候效率会更高。