JAVA学习笔记——容器之Map
来源:互联网 发布:it产品经理培训 编辑:程序博客网 时间:2024/06/03 23:01
本文是实现Map的几个接口的类的总结,文中引用了潘威威的博客的大部分内容,在此表示感谢。
a. 如果哈希表为空,调用resize()创建一个哈希表。
b. 如果指定参数hash在表中没有对应的桶(tab[i = (n - 1) & hash]== null),即为没有碰撞,直接将键值对插入到哈希表 中即可。
c. 如果有碰撞,遍历桶,找到key映射的节点
(1) 桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
(2) 如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
(3) 如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有 找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链 表转为红黑树(红黑树是采用hash值比较大小的,小左大右)。
d. 如果找到了key映射的节点,且节点不为null
Map的整体结构
Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承Collection接口。
- AbstractMap:实现了Map接口的抽象类。Map的基本实现,其他Map的实现类可以通过继承AbstractMap来减少编码量。
- SortedMap:继承Map。保证按照键的升序排列的映射,对entrySet、keySet和values方法返回的结果进行迭代时,顺序就会反映出来。
- NavigableMap:继承SortedMap,含有返回特定条件最近匹配的导航方法。
- HashMap:Map接口基于哈希表的实现,是使用频率最高的用于键值对处理的数据类型。它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,特点是访问速度快,遍历顺序不确定,线程不安全,最多允许一个key为null,允许多个value为null。可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap类。
- HashTable:Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null。并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以使用HashMap,需要线程安全的场合可以使用ConcurrentHashMap。
- LinkedHashMap: LinkedHashMap继承了HashMap,是Map接口的哈希表和链接列表实现。它维护着一个双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
- WeakedHashMap: 以弱键实现的基于哈希表的Map。在WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。
- TreeMap : Map接口基于红黑树的实现。
1. HashMap
HashMap的数据结构是数组+链表+红黑树(红黑树since JDK1.8)。我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8(TREEIFY_THRESHOLD)时,链表就转换为红黑树。
HashMap中有以下三个成员变量需要特别注意,
transient Node<K,V>[] table; // 扩容时按两倍增加 transient int size; // 当前Map中数据数量 int threshold; // 当size大于threshold,Map就进行扩容。threshold初始时等于 initialCapacity * loadFactor,扩容时2倍增加 final float loadFactor;
HashMap中根据Key计算所在桶的位置的方法如下:
hash = hash(key);first = tab[(n - 1) & hash]);static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Resize:
向hashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,hashMap就需要扩大数组的长度,以便能装入更多的元素。当然数组是无法自动扩容的,扩容方法使用一个新的数组代替已有的容量小的数组。resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。/** * 对table进行初始化或者扩容。 * 如果table为null,则对table进行初始化 * 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。 */final Node<K,V>[] resize() { //新建oldTab数组保存扩容前的数组table Node<K,V>[] oldTab = table; //使用变量oldCap扩容前table的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //保存扩容前的临界值 int oldThr = threshold; int newCap, newThr = 0; //如果扩容前的容量 > 0 if (oldCap > 0) { //如果当前容量>=MAXIMUM_CAPACITY if (oldCap >= MAXIMUM_CAPACITY) { //扩容临界值提高到正无穷 threshold = Integer.MAX_VALUE; //无法进行扩容,返回原来的数组 return oldTab; } //如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY) //临界值变为原来的2倍 newThr = oldThr << 1; }//如果旧容量 <= 0,而且旧临界值 > 0 else if (oldThr > 0) //数组的新容量设置为老数组扩容的临界值 newCap = oldThr; else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0 //ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值 float ft = (float)newCap * loadFactor; //当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //将扩容后hashMap的临界值设置为newThr threshold = newThr; //创建新的table,初始化容量为newCap @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //修改hashMap的table为新建的newTab table = newTab; //如果旧table不为空,将旧table中的元素复制到新的table中 if (oldTab != null) { //遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //如果旧桶不为null,使用e记录旧桶 if ((e = oldTab[j]) != null) { //将旧桶置为null oldTab[j] = null; //如果旧桶中只有一个node if (e.next == null) //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置 newTab[e.hash & (newCap - 1)] = e; //如果旧桶中的结构为红黑树 else if (e instanceof TreeNode) //将树中的node分离 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { //如果旧桶中的结构为链表。这段没有仔细研究 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; //遍历整个链表中的节点 do { next = e.next; // 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;}从代码中可以看到,扩容很耗性能。所以在使用HashMap的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。看完代码后,可以将resize的步骤总结为:
a.计算扩容后的容量,临界值。
b. 将hashMap的临界值修改为扩容后的临界值根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
c. 将旧数组的元素复制到table中。
b. 将hashMap的临界值修改为扩容后的临界值根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
c. 将旧数组的元素复制到table中。
putVal:
向HashMap中插入数据调用的是put方法,put又调用的内部putVal,此处直接分析putVal的源码:
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; 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); 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) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }putVal方法可以分为下面的几个步骤:
a. 如果哈希表为空,调用resize()创建一个哈希表。
b. 如果指定参数hash在表中没有对应的桶(tab[i = (n - 1) & hash]== null),即为没有碰撞,直接将键值对插入到哈希表 中即可。
c. 如果有碰撞,遍历桶,找到key映射的节点
(1) 桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
(2) 如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
(3) 如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有 找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链 表转为红黑树(红黑树是采用hash值比较大小的,小左大右)。
d. 如果找到了key映射的节点,且节点不为null
(1) 记录节点的vlaue。
(2) 如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
(3) 返回记录下来的节点的value。
e. 如果没有找到key映射的节点(b、c步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是 否大于临界值threshold,如果大于会使用resize方法进行扩容。
(2) 如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
(3) 返回记录下来的节点的value。
e. 如果没有找到key映射的节点(b、c步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是 否大于临界值threshold,如果大于会使用resize方法进行扩容。
HashMap的其他方法也是按照上面结构图所展示的准则来处理的,就不再做具体的介绍了。
HashMap不是线程安全的,如果想使用线程安全的HashMap,除了使用ConCurrentHashMap外,还可以使用Collections.synchronizedMap对HashMap进行封装,它使用synchronized关键字同步信号量,重写了HashMap的公共方法,因此性能不高。
2.LinkedHashMap
LinkedHashMap继承自HashMap,主要多了两个成员变量head和tail,LinkedHashMap处理保持的上面HashMap的桶结构外,还用head和tail将插入数据按先后顺序用链表链接起来,因此LinkedHashMap重写了HashMap的迭代器方法。
3.TreeMap
TreeMap是用红黑树存储数据的,它是有序的,它必须要有一个Key的比较器comparator,或者数据的Key必须是实现了Comparable接口的,否则插入数据会抛强转异常。
4.HashTable
HashTable继承自Dictionary,它的实现和HashMap大同小异,通过在public的方法上加synchronized关键字实现线程安全,但是HashTable的性能并不高,不建议使用。
5.ConcurrentHashMap
ConcurrentHashMap是线程安全的,它同HashMap的数据结构是一样的,不同之处在于它是通过分段锁来实现线程安全的,在插入或删除节点时,先同hash找到节点所在的桶,再用synchronized关键字锁定桶后再进行操作,最大的保证了性能同时保证线程安全。(更详细内容暂时没看懂,后面再补充)
final V replaceNode(Object key, V value, Object cv) { int hash = spread(key.hashCode()); for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; boolean validated = false; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { validated = true; for (Node<K,V> e = f, pred = null;;) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { V ev = e.val; if (cv == null || cv == ev || (ev != null && cv.equals(ev))) { oldVal = ev; if (value != null) e.val = value; else if (pred != null) pred.next = e.next; else setTabAt(tab, i, e.next); } break; } pred = e; if ((e = e.next) == null) break; } } else if (f instanceof TreeBin) { validated = true; TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> r, p; if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) { V pv = p.val; if (cv == null || cv == pv || (pv != null && cv.equals(pv))) { oldVal = pv; if (value != null) p.val = value; else if (t.removeTreeNode(p)) setTabAt(tab, i, untreeify(t.first)); } } } } } if (validated) { if (oldVal != null) { if (value == null) addCount(-1L, -1); return oldVal; } break; } } } return null; }6.WeakHashMap
WeakHashMap继承自AbstractMap,它的键是弱键,即当一个键不再正常使用,键对应的键值对将自动从WeakHashMap中删除。它是采用数组存储数据,它计算数组索引的算法和HashMap略有区别,方法如下:
final int hash(Object k) { int h = k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } private static int indexFor(int h, int length) { return h & (length-1); }此外,WeakHashMap没有实现用红黑树存储数据的逻辑。
WeakHashMap有一个ReferenceQueue类型的成员变量queue,往WeakHashMap里插入数据时,会将生成Key的WeakReference,当key不再正常使用后,key的WeakReference就被JVM的垃圾回收器插入到queue中,在适当的时候(put、remove、size、resize基本上所有public的操作),WeakHashMap会根据queue里数据,移除对应的Value。移除方法如下所示:
/** * Expunges stale entries from the table. */ private void expungeStaleEntries() { for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; int i = indexFor(e.hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> p = prev; while (p != null) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // Must not null out e.next; // stale entries may be in use by a HashIterator e.value = null; // Help GC size--; break; } prev = p; p = next; } } }
参考文章:
Java8源码-Map整体架构
Java8源码-HashMap
阅读全文
0 0
- JAVA学习笔记——容器之Map
- java容器学习之——Map
- C++学习笔记22——关联容器之map
- Java容器学习之Map
- java学习个人笔记---容器之Map与Set关系
- STL学习笔记——map容器
- STL学习笔记之容器--map
- Java学习笔记——容器之List
- Java学习笔记——容器之Set
- Java学习笔记——容器之Queue
- 学习笔记之--java容器
- STL学习笔记— —容器map和multimap
- STL学习笔记8— —容器map和multimap
- Java容器之Map
- java容器之Map
- java容器之Map
- C++ map容器 学习笔记
- C++ map容器 学习笔记
- 一些工具的使用
- HDOJ 2520 我是菜鸟,我怕谁
- 201314
- C#接收C++动态库返回字符串char*,在C#端如何接收并得到正确的字符串
- 交大OJ 3021 有限背包(背包问题
- JAVA学习笔记——容器之Map
- SQL Server2008 学习之(十三) :T-SQL语言基础(上)
- Scrapyd部署爬虫项目
- 【2017.11.30】3. Longest Substring Without Repeating Characters-最长字串不重复字符
- java学习的第一个成果 一 简易的计算器
- Linux--选择、循环语句
- 计算机视觉基础-相机成像的几何描述
- B树B+树B*树原理及应用
- SpringBoot中jpa批量插入的问题