HashMap详解
来源:互联网 发布:猫眼网络大电影数据 编辑:程序博客网 时间:2024/06/05 04:04
1、HashMap简介:
1.1、HashMap的存储结构:链表的数组
数据的主要存储结构有:顺序存储,链式存储,索引存储,散列存储。
1. 顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元里。优点:随机存取;缺点:产生碎片。例如:数组,特点是:寻址容易,插入和删除困难;
2. 链式存储:逻辑上相邻的元素存储在物理位置上不一定相邻的存储单元里。优点:没有碎片;缺点:存储的指针需要占用一定的空间,并且只能顺序存取。例如:链表,寻址困难,插入和删除容易;
3. 索引存储:在存储元素信息时,同时建立一张索引表。优点:检索速度快;缺点:增加了附加的索引表;
4. 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。优点:检索,增加和删除节点的速度都很快;缺点:散列函数不好可能会引发元素存储冲突,而解决冲突会增加额外的时间和空间开销。
哈希表有多种不同的实现方法,HashMap采用的是一种常用的实现方法——拉链法,我们可以理解为“链表的数组”。如下所示:
1.2、HashMap的继承关系:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
1.3、HashMap的成员变量:
// 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。 static final int DEFAULT_INITIAL_CAPACITY = 16; // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换) static final int MAXIMUM_CAPACITY = 1 << 30; // 默认加载因子为0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 存储数据的Entry数组,长度是2的幂。HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表 transient Entry[] table; // HashMap的底层数组中已用槽的数量 transient int size; //HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子) int threshold; // 加载因子实际大小 final float loadFactor; // HashMap被改变的次数 transient volatile int modCount;
HashMap中的静态内部类:Entry,其重要的属性有 key , value, next。我们上面说到HashMap的基础就是一个链表数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; ... }
2、HashMap常用的构造器:
HashMap():构建一个初始容量为 16,负载因子默认为 0.75 的 HashMap;
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap;
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
而其中的负载因子loadFactor的理解为:HashMap中的数据量/HashMap的总容量(initialCapacity),当loadFactor达到指定值或者0.75时候,HashMap的总容量自动扩展一倍,以此类推。
对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图所示:
3、HashMap 的常用方法
3.1 public V put(K key , V value) 方法
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //找到指定 key 与需要放入的 key 相等(hash 值相同,通过 equals 比较放回 true),那么替换。 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; }
3.1.1 key==null时,将执行下面的方法:
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
HashMap允许存放null键和null值。key值只允许有一个null,value则可以允许有多个null。
当key为null时,调用putForNullKey()方法,将value放置在数组第一个位置。也就是存在key=null时,则table[0]中的Entry存放 key=null value=”新值”。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。
3.1.1 key !=null 时,可以归纳为以下几个步骤:
根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置;若两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖;反之,如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部 。
当向 HashMap 中添加 key-value 对,由其key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。
上面程序中还调用了 addEntry(hash, key, value, i)方法:
void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K,V> e = table[bucketIndex]; // ① // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) // 把 table 对象的长度扩充到 2 倍。 resize(2 * table.length); // ② }
上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。
上面程序中还有这样两个变量:
size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。
从上面程序中②号代码可以看出,当 size++ >= threshold 时,HashMap 会自动调用resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。
3.2 public V get(Object key) 方法:
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:
当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,再根据该 hashCode 返回值找出该key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:
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; }
从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
4、Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中HashIterator(){…}会将这个值赋给迭代器的expectedModCount。
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } public void remove() { if (current == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); Object k = current.key; current = null; HashMap.this.removeEntryForKey(k); expectedModCount = modCount; } }
在迭代过程中nextEntry(),判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:注意到modCount声明为volatile,保证线程之间修改的可见性。
- HashMap 详解
- hashmap详解
- Hashmap详解
- HashMap详解
- HashMap详解
- HashMap详解
- HashMap 详解
- HashMap详解
- HashMap详解
- HashMap详解
- HashMap详解
- Hashmap详解
- HashMap详解
- HashMap详解
- HashMap详解
- hashmap详解
- HashMap详解
- HashMap详解
- Max Sum(HDU 1003)
- layoutSubviews何时调用的问题
- Android使用ImageLoader异步加载网络图片(一)读取单张图片
- STM32——GPIO输入模式下上拉和下拉的设置
- Ubuntu下安装Eclipse
- HashMap详解
- final方法和final类,内部类
- 分级基金的上折,下折
- 多线程
- Hdu 5386 Cover 拓扑排序
- git 从储藏中创建分支
- Ant实现Android代码编译混淆(“找不到程序包R”解决方法)
- HDU 5385 The path
- 被动信息收集1——DNS基础 + DNS解析工具 NSLOOKUP使用