HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是由于它是通过计算散列码来决定存储的位置。
HashMap中主要是通过key的hashCode来计算hash值的。仅仅要hashCode同样。计算出来的hash值就一样。假设存储的对象对多了,就有可能不同的对象所算出来的hash值是同样的,这就出现了所谓的hash冲突。
学过数据结构的同学都知道。解决hash冲突的方法有非常多,HashMap底层是通过链表来解决hash冲突的。
HashMap事实上就是一个Entry数组。Entry对象中包括了键和值,当中next也是一个Entry对象。它就是用来处理hash冲突的。形成一个链表。数组的每一个元素都是一个单链表的头节点,链表是用来解决冲突的。假设不同的key映射到了数组的同一位置处,就将其放入单链表中。
Hashtable 与 HashMap类似,可是主要有6点不同。
1.HashTable的方法是同步的,HashMap未经同步,所以在多线程场合要手动同步HashMap这个差别就像Vector和ArrayList一样。
2.HashTable不同意null值,key和value都不能够,HashMap同意null值,key和value都能够。HashMap同意key值仅仅能由一个null值,由于hashmap假设key值同样。新的key,
value将替代旧的。
3.HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。
4.HashTable使用Enumeration,HashMap使用Iterator。
5.HashTable中hash数组默认大小是11,添加的方式是 old*2+1。HashMap中hash数组的默认大小是16,并且一定是2的指数。
1、HashTable的方法是同步的,HashMap未经同步
Hashtable 提供的几个主要方法,包含 get(), put(), remove() 等。每一个方法本身都是 synchronized 的,会对整个对象进行加锁操作,不会出现两个线程同一时候对数据进行操作的情况。因此保证了线程安全性,可是也大大的减少了运行效率。
2、HashMap中hash数组的默认大小是16,并且一定是2的指数
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
将key的二次hash值。与长度减一进行与操作,这一步可谓经典,通常我们会用取模的方式来定位数组中的某个位置,我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比較均匀的。
可是,“模”运算的消耗还是比較大的。能不能找一种更高速,消耗更小的方式那?hashMap用这样的方法,并且length即capacity的值。面capacity又是2的倍数,减1之后。表示成二进制就所有是1了。那么与所有为1的一个数进行与操作,速度会大大提升了。这就是为什么"capacity的值是2的倍数"
3、HashMap解决冲突的办法
1)再哈希法
2)拉链发
- public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
依据key的hash值得到这个元素在数组中的位置(即下标),然后就能够把这个元素放到相应的位置中了。假设这个元素所在的位子上已经存放有其它元素了,那么在同一个位子上的元素将以链表的形式存放,新增加的放在链头。最先增加的放在链尾。从hashmap中get元素时。首先计算key的hashcode,找到数组中相应位置的某一元素,然后通过key的equals方法在相应位置的链表中找到须要的元素。
调用int hash = hash(key.hashCode());这是hashmap的一个自己定义的hash,在key.hashCode()基础上进行二次hash。再哈希法可以解决开放地址法的聚集现象,可是却添加了时间复杂度。
- static int hash(int h) {
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
改进传统的hash方法,并且尽量保证key的每一位都会影响到最后的hash值。以达到降低hash冲突的目的.
4、HashMap的构造函数
- if (initialCapacity < 0)
- throw new IllegalArgumentException("Illegal initial capacity: " +
- initialCapacity);
- if (initialCapacity > MAXIMUM_CAPACITY)
- initialCapacity = MAXIMUM_CAPACITY;
- if (loadFactor <= 0 || Float.isNaN(loadFactor))
- throw new IllegalArgumentException("Illegal load factor: " +
- loadFactor);
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- this.loadFactor = loadFactor;
- threshold = (int)(capacity * loadFactor);
- table = new Entry[capacity];
- init();
- }
loadFactor :载入因子,载入因子与HashMap resize有关。
默觉得0.75
capacity:容器大小。默认值为16 其大小为上面所说的数据结构中数组的长度。
table:即为上面数据结构图中。X方向的数组(transient Entry[] table;)
threshold :resize的临界值,即当HashMap中无素个数达到该值时,HashMap就会调用其resize方法。又一次扩充大小。
while (capacity < initialCapacity)
capacity <<= 1;
这段代码说明什么:capacity的值是2的倍数,即使设置初始值是1000,hashmap也自己主动会将其设置为1024。
5、哈希表解决冲突的经常用法
1)开放地址法
2)拉链法
3)再哈希法
拉链法的长处 ,与开放定址法相比,拉链法有例如以下几个长处:
①拉链法处理冲突简单。且无堆积现象。即非同义词决不会发生冲突。因此平均查找长度较短。
②因为拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为降低冲突,要求装填因子α较小。故当结点规模较大时会浪费非常多空间。
而拉链法中可取α≥1,且结点较大时,拉链法中添加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。仅仅要简单地删去链表上对应的结点就可以。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是由于各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在
用开放地址法处理冲突的散列表上运行删除操作,仅仅能在被删结点上做删除标记。而不能真正删除结点。
拉链法的缺点
拉链法的缺点是:指针须要额外的空间。故当结点规模较小时,开放定址法较为节省空间,而若将节省的指