7,HashMap
Posted 烟丶雨下整晚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了7,HashMap相关的知识,希望对你有一定的参考价值。
一,HashMap简介
1,HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
2,HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
3,HashMap 的实现不是同步的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
4,HashMap 实现了Cloneable接口,即覆盖了函数clone()(不覆盖该方法会报CloneNotSupportedException),能被克隆。
5,HashMap 实现Serializable接口,说明HashMap支持序列化。
二,数据结构
HashMap的数据结构如下:
上图展示了在JDK1.8中HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
三,HashMap源码
1,HashMap结构
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; 省略...... }
2,构造函数
HashMap提供了四种方式的构造器,如下
构造器一:
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小于0,否则报错 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大于最大值,否则为最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 填充因子不能小于或等于0,不能为非数字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化填充因子 this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity); }
tableSizeFor(initialCapacity)方法返回大于等于initialCapacity的最小的二次幂数值。具体代码如下:
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
构造器二:
public HashMap(int initialCapacity) { // 调用HashMap(int, float)型构造函数 this(initialCapacity, DEFAULT_LOAD_FACTOR); }
构造器三:
public HashMap() { // 初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; }
构造器四:
public HashMap(Map<? extends K, ? extends V> m) { // 初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // 将m中的所有元素添加至HashMap中 putMapEntries(m, false); }
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)方法将m的所有元素存入本HashMap实例中。具体代码如下:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size // 未初始化,s为m的实际元素个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 计算得到的t大于阈值,则初始化阈值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); // 将m中的所有元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
3,HashMap数据存储数组table,节点Node的数据结构
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; // 指向下一个节点 Node<K,V> next; // 构造函数。 // 输入参数包括"哈希值(hash)", "键(key)", "值(value)", "下一节点(next)" Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个Entry是否相等,若两个Entry的“key”和“value”都相等,则返回true,否则,返回false public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
从中,可以看出Node实际上就是一个单向链表。这也是为什么说HashMap是通过拉链法解决哈希冲突的。
Node实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。
4,部分函数
4.1,put()函数
//指定节点key,value,向hashMap中插入节点 public V put(K key, V value) { //注意待插入节点hash值的计算,调用了hash(key)函数 //实际调用 putVal()进行节点的插入 eturn putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; /*key 的hash值的计算是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16), 主要是从速度、功效、质量来考虑的,这么做可以在bat数组table的length比较小的时候, 也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销*/ return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); } /*把Map<? extends K, ? extends V> m 中的元素插入到hashMap 中, 若evict为false,代表是在创建hashMap时调用了这个函数, 例如利用上述构造函数3创建hashMap;若evict为true,代表是在创建hashMap后才调用这个函数,例如上述的putAll函数。*/ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { /*如果是在创建hashMap时调用的这个函数则table一定为空*/ if (table == null) { //根据待插入的map的size计算要创建的hashMap的容量。 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //把要创建的hashMap的容量存在threshold中 if (t > threshold) threshold = tableSizeFor(t); } //判断待插入的map的size,若size大于threshold,则先进行resize() else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); //实际也是调用putVal函数进行元素的插入 putVal(hash(key), key, value, false, evict); } } } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /*根据hash值确定节点在数组中的插入位置,若此位置没有元素则进行插入, 注意确定插入位置所用的计算方法为(n - 1) & hash,由于n一定是2的幂次,这个操作相当于hash%n */ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {//说明待插入位置存在元素 Node<K,V> e; K k; //比较原来元素与待插入元素的hash值和key值. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若原来元素是红黑树节点,调用红黑树的插入方法:putTreeVal else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//证明原来的元素是链表的头结点,从此节点开始向后寻找合适插入位置 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //找到插入位置后,新建节点插入 p.next = newNode(hash, key, value, null); //若链表上节点超过TREEIFY_THRESHOLD-1,将链表变为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { //待插入元素在hashMap中已存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //钩子函数,用于给LinkedHashMap继承后使用,在HashMap里是空的 afterNodeAccess(e); return oldValue; } } //修改次数+1 ++modCount; //实际大小+1,如果大于阈值,重新计算并扩容 if (++size > threshold) resize(); //钩子函数,用于给LinkedHashMap继承后使用,在HashMap里是空的 afterNodeInsertion(evict); return null; }
4.2,get()函数
HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。
public V get(Object key) { Node<K,V> e; //根据输入节点的hash值和key值利用getNode方法进行查找。 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 判断table是否已经初始化,并且长度大于0,并且根据hash寻找table中的项也不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 桶中第一项(数组元素)相等时 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶中不止一个结点时 if ((e = first.next) != null) { // 如果为红黑树结点 if (first instanceof TreeNode) // 在红黑树中查找 return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 否则,在链表中查找 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
4.3,remove()函数
@Override public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } @Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; } /** * @param hash key的hash值 * @param key * @param value 与下面的matchValue结合,如果matchValue为false,则忽略value。 * @param matchValue 为true,则判断是否与value相等。 * @param movable 主要跟树节点的remove有关,为false,则不移动其他的树节点。 */ final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; // 判断table是否已经初始化,并且长度大于0,并且根据hash寻找table中的项也不为空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //对下标节点进行判断,如果相同,则赋给临时节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { //为树节点,则按照树节点的操作来进行查找并返回 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //如果找到了key对应的node,则进行删除操作 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //为树节点,则进行树节点的删除操作 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //如果node == p,说明该key所在的位置为数组的下标位置,所以下标位置指向下一个节点即可 else if (node == p) tab[index] = node.next; //否则的话,key在桶中,p为node的上一个节点,p.next指向node.next即可 else p.next = node.next; //修改次数+1 ++modCount; --size; //钩子函数,用于给LinkedHashMap继承后使用,在HashMap里是空的 afterNodeRemoval(node); return node; } } return null; }
4.4,resize()函数
resize()函数用于进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize,在明确知道map要用的容量的时候,使用指定初始化容量的构造函数。
final Node<K,V>[] resize() { // 获取当前table保存 Node<K,V>[] oldTab = table; // 获得table大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获得当前阈值 int oldThr = threshold; int newCap, newThr = 0; // 之前table值大小大于0时 if (oldCap > 0) { // 之前table值大于最大容量 if (oldCap >= MAXIMUM_CAPACITY) { // 阈值为最大整形 threshold = Integer.MAX_VALUE; return oldTab; } // 容量翻倍,使用左移,效率更高 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 阈值翻倍 newThr = oldThr << 1; // double threshold } // 之前阈值大于0 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个元素会调用resize函数,会进入这一步) newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新阈值为0时 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 初始化table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 之前的table已经初始化过,不为null时 if (oldTab != null) { // 复制元素,把oldTab中的节点reHash到newTab中去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //若节点是单个节点,直接在newTab中进行重定位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若节点是TreeNode节点,要进行红黑树的rehash操作 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是链表,进行链表的rehash操作 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash do { next = e.next; //根据算法e.hash & oldCap判断节点位置rehash后是否发生改变 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
在resize前和resize后的元素布局如下:
下图只是针对了数组下标为2的桶中的各个元素在扩容后的分配布局,其他各个桶中的元素布局可以以此类推。
四,HashMap遍历方式
import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class TestHashMap { public static void main(String[] args) { HashMap<String, String> hashMap = new HashMap<String, String>(); hashMap.put("MapKey1", "MapValue1"); hashMap.put("MapKey2", "MapValue2"); hashMap.put("MapKey3", "MapValue3"); hashMap.put("MapKey4", "MapValue4"); hashMap.put("MapKey5", "MapValue5"); hashMap.put("MapKey6", "MapValue6"); TestHashMap.getHashMap_Values(hashMap); System.out.println("---------------------------"); TestHashMap.getHashMap_Entry_KeyValues(hashMap); System.out.println("---------------------------"); TestHashMap.getHashMap_Keyset_KeyValues(hashMap); } //遍历HashMap的values public static void getHashMap_Values(HashMap<?, ?> hashMap){ if(hashMap == null) return; Collection<?> c = hashMap.values(); Iterator<?> iter= c.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); } } //通过entry set遍历HashMap(效率高) public static void getHashMap_Entry_KeyValues(HashMap<?, ?> hashMap){ if(hashMap == null) return; Iterator<?> iterator = hashMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry)iterator.next(); System.out.print(entry.getKey()); System.out.println("---------" + entry.getValue()); } } //通过keyset来遍历HashMap(效率低) public static void getHashMap_Keyset_KeyValues(HashMap<?, ?> hashMap){ if(hashMap == null) return; Iterator<?> iterator = hashMap.keySet().iterator(); while(iterator.hasNext()){ String key = (String) iterator.next(); System.out.print(key); System.out.println("---------" + hashMap.get(key));以上是关于7,HashMap的主要内容,如果未能解决你的问题,请参考以下文章