java LinkedHashMap源码解析
2016-06-13 19:01
706 查看
本源码解析是基于JDK1.7,本篇与HashMap源码解析较强的关联性
HashMap是基于数组的,而LinkedHashMap是基于循环双向链表的,即每个节点都有指向前后节点的指针,
header节点是不含真实元素的标兵节点,由于每次插入都是在header的前面,header.before指向最新插入的节点(Newest),header.after指向最先插入的节点(Eldest)
迭代次序是确定的,为插入顺序,注意插入相等的key(跟新value)并不会改变迭代的次序
它可以用来备份map,以保持备份时刻的迭代顺序,同时又不像TreeMap那样引入额外的开销
允许null键和值
构造器
LinkedHashMap可以用来实现遍历时总是首先访问最近访问的节点,但是没有实现LRU cache机制,即删除最老节点的操作(Eldest),下面判断函数总是返回false
它的map基本操作时间复杂度与HashMap一样是常数级别的(元素散列合理的情况下),由于需要维护维持迭代次序的LinkedList,通常他会比HashMap效率低,但是遍历整个Map例外,LinkedHashMap遍历时间正比于size,而HashMap正比于capacity,通常capacity>size
与HashMap一样,capacity与loadFactor会影响其基本操作效率,但是由于其遍历时间与capacity无关,所以初始capacity可以设置较大值
非线程安全,可以通过
其迭代器有fail-fast机制,迭代过程中通过迭代器以外的方法改变map结构会抛出异常结束,注意在依据访问次序来决定迭代器顺序的实现中,单纯的get方法也会引起map结构性的改变(需要调整被访问元素的位置)
fail-fast机制不能确保并发改变一定抛出异常,只是一种尽可能的校验机制
继承了HashMap的Entry并添加了头尾指针两个域
添加了将当前节点添加到某个节点之前的操作(如果是单链表无法做到)
recordAccess通过将当前节点移动到队头来实现最近访问迭代次序最前
由于其继承了HashMap且与HashMap处于同一package中,所以继承了HashMap的所有非Private成员,其元素查找机制与HashMap一样也是基于Hash的
增加了header成员,于是遍历时不用扫描整个数组,时间复杂度由与capacity成正比变为与size成正比
accessOrder用来标志是否是基于LRU机制的访问顺序
在HashMap中定义了一个空的函数init(),并由构造器来负责调用,就是为了给后续的HashMap的扩展留出初始化的位置,在LinkedHashMap中,初始化了header节点,注意header是不含真实元素的标兵节点,由于始终采用的是头插法(每次在header的前面插入),因此header.before实际指向的是链表的最新插入节点,即Newest,而header.after指向的是最先插入的节点,即Eldest
改进后的transfer不再遍历数组查找所有元素,而是直接用链表进行遍历
改进后的遍历机制直接使用循环双向链表,避免了扫描原数组中不含元素的点,提高了效率
由于每次插入都是在header的before,header不含真实元素的标兵节点,循环双向链表中header.after指向最先插入的节点即Eldest,header.before指向最新插入节点
createEntry用在以已有map创建新map时用,由于此时HashMap的大小已经根据原Map大小确定,所以不用担心扩让的问题,其扩容后也是调用createEntry方法创建新节点
扩容机制与HashMap一致,addEntry只是检测是否适用LRU缓存机制来删除最老的元素,addEntry添加了删除最老节点的机制,但是由于removeEldestEntry(node)总是返回false,所以不会有任何操作
createEntry方法与原方法的区别在于调用addBefore()方法来连接新加入节点前后的指针
由于get方法访问了元素,如果LinkedHashMap初始化为基于最近访问的迭代次序(accessOrder==true),重写的get(key)方法还需要将被访问的元素移动到header之前的位置
遍历的起点是header.after即最先插入的节点
遍历结束标志当前节点的after指向了标兵节点header
next()类的方法就是简单的返回链表下一个元素
在HashMap中每次调用remove()方法时都会调用删除节点的
在LinkedHashMap继承了HashMap,其Entry继承了HashMap.Entry,如何保证LinkedHashMap继承的HashMap的方法中的Entry是LinkedHashMap的Entry?
猜测编译器在编译期将继承自HashMap中的方法编译到LinkedHashMap类中,LinkedHashMap的Entry就成为该类的为一内部类,所以就不存在歧义。
虽然LinkedHashMap禁止了删除Eldest元素的机制,但是通过继承重写
HashMap中留空了很多方法,如 init(),recordRemoval()等,虽然在HashMap中无用,但是LinkedHashMap实现时却可以直接使用原架构,而只是填充这些小的空方法就可以方便实现
LinkedHashMap概要
LinkedHashMap是基于HashTable与LinkedList原理实现的HashMap是基于数组的,而LinkedHashMap是基于循环双向链表的,即每个节点都有指向前后节点的指针,
header节点是不含真实元素的标兵节点,由于每次插入都是在header的前面,header.before指向最新插入的节点(Newest),header.after指向最先插入的节点(Eldest)
迭代次序是确定的,为插入顺序,注意插入相等的key(跟新value)并不会改变迭代的次序
public class Demo { public static void main(String[] args) { Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(); map.put(1, 2); map.put(2, 3); map.put(1, 2); for (int i : map.keySet()) { System.out.println(map.get(i)); } } } //结果: 2 3
它可以用来备份map,以保持备份时刻的迭代顺序,同时又不像TreeMap那样引入额外的开销
void foo(Map m) { Map copy = new LinkedHashMap(m); ... }
允许null键和值
构造器
LinkedHashMap(int,float,boolean)可以使得迭代次序为从最近访问的到最后访问的,这种次序通常用来实现LRU(least-recently-used)cache,注意通过集合视图访问不计算在访问次数之内
LinkedHashMap可以用来实现遍历时总是首先访问最近访问的节点,但是没有实现LRU cache机制,即删除最老节点的操作(Eldest),下面判断函数总是返回false
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
它的map基本操作时间复杂度与HashMap一样是常数级别的(元素散列合理的情况下),由于需要维护维持迭代次序的LinkedList,通常他会比HashMap效率低,但是遍历整个Map例外,LinkedHashMap遍历时间正比于size,而HashMap正比于capacity,通常capacity>size
与HashMap一样,capacity与loadFactor会影响其基本操作效率,但是由于其遍历时间与capacity无关,所以初始capacity可以设置较大值
非线程安全,可以通过
Map m = Collections.synchronizedMap(new LinkedHashMap(...));来实现同步
其迭代器有fail-fast机制,迭代过程中通过迭代器以外的方法改变map结构会抛出异常结束,注意在依据访问次序来决定迭代器顺序的实现中,单纯的get方法也会引起map结构性的改变(需要调整被访问元素的位置)
fail-fast机制不能确保并发改变一定抛出异常,只是一种尽可能的校验机制
LinkedHashMap类头部
我们看到他是基于HashMap的实现,实现了Map接口(PS:HashMap实现了Map接口,他不用说也实现了Map接口吧,为啥还要声明。。。)public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> public interface Map<K, V> { // Query Operations int size(); boolean isEmpty(); boolean containsKey(Object key); boolean containsValue(Object value); V get(Object key); // Modification Operations V put(K key, V value); V remove(Object key); // Bulk Operations void putAll(Map<? extends K, ? extends V> m); void clear(); // Views Set<K> keySet(); Collection<V> values(); Set<Map.Entry<K, V>> entrySet(); interface Entry<K, V> { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); } // Comparison and hashing boolean equals(Object o); int hashCode(); }
基本节点 Entry
对比于HashMap其添加了前后指针来支持循环双向链表操作,并用链表来保证访问次序。并为实现LRU机制添加了记录访问的方法,移动到Map的head头部继承了HashMap的Entry并添加了头尾指针两个域
添加了将当前节点添加到某个节点之前的操作(如果是单链表无法做到)
recordAccess通过将当前节点移动到队头来实现最近访问迭代次序最前
private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } private void remove() { before.after = after; after.before = before; } private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } void recordRemoval(HashMap<K,V> m) { remove(); } }
关键成员变量
LinkedHashMap既是基于数组的也是基于循环双向链表的,用数组来定位元素,用链表来记录相对顺序,header是不含真实元素的标兵节点,由于每次都是在header的前面插入,所以其after指向最先添加元素,before指向最新添加元素由于其继承了HashMap且与HashMap处于同一package中,所以继承了HashMap的所有非Private成员,其元素查找机制与HashMap一样也是基于Hash的
增加了header成员,于是遍历时不用扫描整个数组,时间复杂度由与capacity成正比变为与size成正比
accessOrder用来标志是否是基于LRU机制的访问顺序
private transient Entry<K,V> header; private final boolean accessOrder; // 继承自HashMap的成员 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final float DEFAULT_LOAD_FACTOR = 0.75f; static final int MAXIMUM_CAPACITY = 1 << 30; transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static final Entry<?,?>[] EMPTY_TABLE = {}; transient int size; int threshold; final float loadFactor; transient int modCount;
构造函数
其所有初始化机制都是与HashMap相同的,只是同时初始化了accessOrder标志在HashMap中定义了一个空的函数init(),并由构造器来负责调用,就是为了给后续的HashMap的扩展留出初始化的位置,在LinkedHashMap中,初始化了header节点,注意header是不含真实元素的标兵节点,由于始终采用的是头插法(每次在header的前面插入),因此header.before实际指向的是链表的最新插入节点,即Newest,而header.after指向的是最先插入的节点,即Eldest
public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } public LinkedHashMap() { super(); accessOrder = false; } public LinkedHashMap(Map<? extends K, ? extends V> m) { super(m); accessOrder = false; } public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } @Override void init() { header = new Entry<>(-1, null, null, null); header.before = header.after = header; }
相对于HashMap优化的方法
由于添加了循环双向链表机制,所有需要遍历的操作都可以不用再扫描原数组(扫描数组的时间复杂度与capacity和size成正比),而是可以直接扫描所有元素(扫描元素的时间复杂度与size成正比),由于HashMap是基于Hash的,用空间换取时间,所有通常capacity要比size大transfer函数
transfer函数用来在进行扩容时将原数组中的Entry转移到新的Table数组中改进后的transfer不再遍历数组查找所有元素,而是直接用链表进行遍历
@Override void transfer(HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e = header.after; e != header; e = e.after) { if (rehash) e.hash = (e.key == null) ? 0 : hash(e.key); int index = indexFor(e.hash, newCapacity); e.next = newTable[index]; newTable[index] = e; } }
containValue函数
containValue()用来查询value是否在值集合中,需要遍历map改进后的遍历机制直接使用循环双向链表,避免了扫描原数组中不含元素的点,提高了效率
由于每次插入都是在header的before,header不含真实元素的标兵节点,循环双向链表中header.after指向最先插入的节点即Eldest,header.before指向最新插入节点
public boolean containsValue(Object value) { // Overridden to take advantage of faster iterator if (value==null) { for (Entry e = header.after; e != header; e = e.after) if (e.value==null) return true; } else { for (Entry e = header.after; e != header; e = e.after) if (value.equals(e.value)) return true; } return false; }
添加元素
addEntry在调用put等后添加元素的方法时调用,需要考虑扩容的问题createEntry用在以已有map创建新map时用,由于此时HashMap的大小已经根据原Map大小确定,所以不用担心扩让的问题,其扩容后也是调用createEntry方法创建新节点
扩容机制与HashMap一致,addEntry只是检测是否适用LRU缓存机制来删除最老的元素,addEntry添加了删除最老节点的机制,但是由于removeEldestEntry(node)总是返回false,所以不会有任何操作
createEntry方法与原方法的区别在于调用addBefore()方法来连接新加入节点前后的指针
void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } } void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; e.addBefore(header); size++; }
get方法
get(key)方法使用与HashMap相同的hash定位机制由于get方法访问了元素,如果LinkedHashMap初始化为基于最近访问的迭代次序(accessOrder==true),重写的get(key)方法还需要将被访问的元素移动到header之前的位置
public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; e.recordAccess(this); return e.value; } void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } }
清空方法
重写的方法除了清空数组外还需要清空链表,只剩下不含真实元素的标兵节点 headerpublic void clear() { super.clear(); header.before = header.after = header; }
迭代器机制
其他迭代器都是基于LinkedHashIterator的遍历的起点是header.after即最先插入的节点
遍历结束标志当前节点的after指向了标兵节点header
next()类的方法就是简单的返回链表下一个元素
private abstract class LinkedHashIterator<T> implements Iterator<T> { Entry<K,V> nextEntry = header.after; Entry<K,V> lastReturned = null; int expectedModCount = modCount; public boolean hasNext() { return nextEntry != header; } public void remove() { if (lastReturned == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); LinkedHashMap.this.remove(lastReturned.key); lastReturned = null; expectedModCount = modCount; } Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == header) throw new NoSuchElementException(); Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; } } private class KeyIterator extends LinkedHashIterator<K> { public K next() { return nextEntry().getKey(); } } private class ValueIterator extends LinkedHashIterator<V> { public V next() { return nextEntry().value; } } private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } } Iterator<K> newKeyIterator() { return new KeyIterator(); } Iterator<V> newValueIterator() { return new ValueIterator(); } Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); }
注意点
LinkedHashMap并没有重写remove(key)方法,那么在删除元素后是怎么连接删除元素前后的节点的?在HashMap中每次调用remove()方法时都会调用删除节点的
e.recordRemoval(this)方法,HashMap的Entry中该方法留空,而在LinkedHashMap中实现了该方法,完成了连接,虽然HashMap并不需要删除后处理连接但是还是预留了这种框架
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } void recordRemoval(HashMap<K,V> m) { remove(); } private void remove() { before.after = after; after.before = before; }
在LinkedHashMap继承了HashMap,其Entry继承了HashMap.Entry,如何保证LinkedHashMap继承的HashMap的方法中的Entry是LinkedHashMap的Entry?
猜测编译器在编译期将继承自HashMap中的方法编译到LinkedHashMap类中,LinkedHashMap的Entry就成为该类的为一内部类,所以就不存在歧义。
虽然LinkedHashMap禁止了删除Eldest元素的机制,但是通过继承重写
protected boolean removeEldestEntry(Entry<String, BucketWriter> eldest)方法就能很方便的实现LRU cache机制
class LRUCacheLinkedHashMap<K, V> extends LinkedHashMap<K, V> { private final int maxElements; public LRUCacheLinkedHashMap(int maxElements) { super(16, 0.75f, true); this.maxElements = maxElements; } @Override protected boolean removeEldestEntry(Entry<K, V> eldest) { if (size() >= maxElements) { return true; } else { return false; } } }
HashMap中留空了很多方法,如 init(),recordRemoval()等,虽然在HashMap中无用,但是LinkedHashMap实现时却可以直接使用原架构,而只是填充这些小的空方法就可以方便实现
相关文章推荐
- Doxygen自动文档生成工具在Eclipse中的集成及使用举例
- JVM 类的加载初始化
- jdk环境搭建
- spring中表单提交问题 400
- Exception sending context initialized ... org.springframework.web.context.ContextLoaderListener
- Java NIO:浅析I/O模型
- java.lang.UnsatisfiedLinkError: No implementation found for long com.autonavi.amap.mapcore.MapCore.n
- 用Java操作树莓派!pi4j简介与安装
- 调试eclipse javaEE所遇问题解决
- 断言assert引入包
- spring boot 入门
- java 反射详解通俗易懂
- eclipse工程转studio,生成build.gradle时报错:make sure all dependencies are opend
- 字符串子序列穷举(Java语言,递归算法)
- Spring MVC 上传文件(upload files)
- java的一些知识(七)
- SpringMVC学习--校验
- Spring MVC拦截器+注解方式实现防止表单重复提交
- java中间件
- Spring MVC拦截器+注解方式实现防止表单重复提交