JDK1.8之后的HashMap底层结构中,在数组(Node<K,V> table)长度大于64的时候且链表(依然是Node)长度大于8的时候,链表在转换为红黑树时,链表长度小于等于6时将不会进行转化为红黑树。目的是为了保证效率。其中链表的结点只有next,LinkedHashMap是在Entry<K,V>中添加before, after(双向链表的定义),保证可迭代,遍历时为存入顺序。
下面是LinkedHashMap中的双向链表定义
//HashMap方法 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } //LinkedHashMap---------------------------------------- static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } //添加结点,添加次序为从右到左,移动last指针 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
LinkedHashMap中有个布尔值accessOrder可以改变访问顺序(get),默认是false,不改变,true则把访问的键值对放到尾部,是实现LRU的关键,且这个值在构造方法中可以获得。
0.75f为默认加载因子,若用户自定义size容量大于capacity*0.75(积为threshold)时,数组就会进行扩容,加载不宜太大太小,太大容易哈希冲突,太小浪费空间
关于removeEldestEntry方法:默认返回false,若为true则会执行以下源码摘除头结点
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { //拿到最右边key - 最早添加的数据key K key = first.key; removeNode(hash(key), key, null, false, true); } }
//定义双列集合的结点结构(双向链表的结点) private class Node { //key的作用是cache满的时候,hashmap便于淘汰尾部和移除操作,还有遍历 int key; int value; Node pre; Node post; //构造方法初始化Node数据 public Node(int key, int value) { this.key = key; this.value = value; } public Node() { } }
//定义头尾结点 private Node head; private Node tail; //定义当前缓存大小 private int count; //定义总缓存大小 private int size; //定义双列集合存储数据 private HashMap<Integer, Node> cache; //构造方法初始化数据 public LRUCache(int size) { //双向链表初始化 head = new Node(); tail = new Node(); //结点外的指针置空 head.pre = null; tail.post = null; //头尾结点的互连 head.post = tail; tail.pre = head; //容量初始化 this.count = 0; this.size = size; cache = new HashMap<>(size); }
//get方法得到key中缓存的数据 public int get(int key) { //取得hashmap中的结点数据 Node node = cache.get(key); //如果没有返回-1 if (node == null) return -1; //有,访问后将结点移动到开头,成为最近使用结点 moveToHead(node); //并返回查询的值 return node.value; }
//摘除双链表结点 private void removeNode(Node node) { node.pre.post = node.post; node.post.pre = node.pre; } //结点插入头部之后 private void insertNode(Node node) { //前连插入结点 node.pre = head; node.post = head.post; //后连插入结点 head.post.pre = node; head.post = node; } private void moveToHead(Node node) { //摘除结点 removeNode(node); //插入头结点之后 insertNode(node); }
//put方法存入数据,同时将值放入hashmap的node public void put(int key, int value) { //获取Node仓库 Node node = cache.get(key); //如果没有命中就调进 if (node == null) { //如果cache满了淘汰尾部 if (count >= size) { cache.remove(tail.pre.key); //摘除tail尾部前一个结点 removeNode(tail.pre); //数量-- count--; } Node newNode = new Node(key, value); cache.put(key, newNode); //由于刚添加,把新数据结点移动到头部 insertNode(newNode); count++; } //如果命中更新该key索引的node值,并移至开头 else { node.value = value; //如果目前只有一个节点不用摘 if(count == 1) { return; } moveToHead(node); } }
//移除缓存中的一个数据 public void remove(int key) { Node node = cache.get(key); //没有就什么也不干,有就删除 if (node == null) return; cache.remove(key); removeNode(node); }
package cn.work.demo.demo02; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; public class LRULinkedHashMapCache<K,V> extends LinkedHashMap<K,V> { //Cache容量 private int size; //定义一个锁保证线程安全 private final ReentrantLock lock = new ReentrantLock(); //初始化,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在尾部,最早访问的放在头部(数据由last添加至尾部) public LRULinkedHashMapCache(int size) { super(size,0.75f,true); this.size = size; } //重写removeEldestEntry方法,若当前容量>size,弹出尾部 @Override public boolean removeEldestEntry(Map.Entry<K,V> eldest) { //size()方法,每当LinkedHashMap添加元素时就会++ return size() > size; } //重写LinkedHashMap的方法,加锁保证线程安全 @Override public V put(K key, V value) { try { lock.lock(); return super.put(key, value); } finally { lock.unlock(); } } @Override public V get(Object key) { try { lock.lock(); return super.get(key); } finally { lock.unlock(); } } @Override public V remove(Object key) { try { lock.lock(); return super.remove(key); } finally { lock.unlock(); } } }
package cn.work.demo.demo02; import java.util.*; //主要思路是:常命中(使用多)和刚添加的移至头部,缓存满了先淘汰尾部 //最近最久未使用 - 指的是Node里的数据,需要变换的也是Node,key只作为索引不考虑交换 //tail和head结点用于摘除结点,其中并不包含任何数据 public class LRUCache { //定义双列集合的结点结构(双向链表的结点) private class Node { //key的作用是cache满的时候,hashmap便于淘汰尾部和移除操作,还有遍历 int key; int value; Node pre; Node post; //构造方法初始化Node数据 public Node(int key, int value) { this.key = key; this.value = value; } public Node() { } } //定义头尾结点 private Node head; private Node tail; //定义当前缓存大小 private int count; //定义总缓存大小 private int size; //定义双列集合存储数据 private HashMap<Integer, Node> cache; //构造方法初始化数据 public LRUCache(int size) { //双向链表初始化 head = new Node(); tail = new Node(); //结点外的指针置空 head.pre = null; tail.post = null; //头尾结点的互连 head.post = tail; tail.pre = head; //容量初始化 this.count = 0; this.size = size; cache = new HashMap<>(size); } //get方法得到key中缓存的数据 public int get(int key) { //取得hashmap中的结点数据 Node node = cache.get(key); //如果没有返回-1 if (node == null) return -1; //有,访问后将结点移动到开头,成为最近使用结点 moveToHead(node); //并返回查询的值 return node.value; } //摘除双链表结点 private void removeNode(Node node) { node.pre.post = node.post; node.post.pre = node.pre; } //结点插入头部之后 private void insertNode(Node node) { //前连插入结点 node.pre = head; node.post = head.post; //后连插入结点 head.post.pre = node; head.post = node; } private void moveToHead(Node node) { //摘除结点 removeNode(node); //插入头结点之后 insertNode(node); } //put方法存入数据,同时将值放入hashmap的node public void put(int key, int value) { //获取Node仓库 Node node = cache.get(key); //如果没有命中就调进 if (node == null) { //如果cache满了淘汰尾部 if (count >= size) { cache.remove(tail.pre.key); //摘除tail尾部前一个结点 removeNode(tail.pre); //数量-- count--; } Node newNode = new Node(key, value); cache.put(key, newNode); //由于刚添加,把新数据结点移动到头部 insertNode(newNode); count++; } //如果命中更新该key索引的node值,并移至开头 else { node.value = value; //如果目前只有一个节点不用摘 if (count == 1) { return; } moveToHead(node); } } //移除缓存中的一个数据 public void remove(int key) { Node node = cache.get(key); //没有就什么也不干,有就删除 if (node == null) return; cache.remove(key); removeNode(node); } //用于遍历cache public void print() { Set<Integer> keyset = cache.keySet(); Iterator iterator = keyset.iterator(); while (iterator.hasNext()) { int key = (int) iterator.next(); System.out.println(cache.get(key).key + "-->" + cache.get(key).value); } } }
package cn.work.demo.demo02; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; public class LRULinkedHashMapCache<K,V> extends LinkedHashMap<K,V> { //Cache容量 private int size; //定义一个锁保证线程安全 private final ReentrantLock lock = new ReentrantLock(); public LRULinkedHashMapCache(int size) { super(size,0.75f,true); this.size = size; } //初始化,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面 //重写removeEldestEntry方法,若当前容量>size,弹出尾部 @Override public boolean removeEldestEntry(Map.Entry<K,V> eldest) { //size()方法,每当LinkedHashMap添加元素时就会++ return size() > size; } //重写LinkedHashMap的方法,加锁保证线程安全 @Override public V put(K key, V value) { try { lock.lock(); return super.put(key, value); } finally { lock.unlock(); } } @Override public V get(Object key) { try { lock.lock(); return super.get(key); } finally { lock.unlock(); } } @Override public V remove(Object key) { try { lock.lock(); return super.remove(key); } finally { lock.unlock(); } } }
package cn.work.demo.demo02; import java.util.Map; public class LRU { public static void main(String[] args) { LRUCache lru = new LRUCache(4); lru.put(1,2); lru.put(2,5); lru.put(8,10); lru.put(6,5); lru.get(1); lru.put(3,8); lru.get(8); lru.put(5,2); lru.put(6,2); lru.put(7,2); lru.print(); System.out.println("//-------------------------------"); LRULinkedHashMap<Integer,Integer> linkedHashMap= new LRULinkedHashMap<>(4); linkedHashMap.put(1,2); linkedHashMap.put(2,5); linkedHashMap.put(8,10); linkedHashMap.put(6,5); linkedHashMap.get(1); linkedHashMap.put(3,8); linkedHashMap.get(8); linkedHashMap.put(5,2); linkedHashMap.put(6,2); linkedHashMap.put(7,2); for (Map.Entry<Integer,Integer> entry:linkedHashMap.entrySet()){ System.out.println(entry.getKey()+"-->"+entry.getValue()); } } }
8-->10
5-->2
6-->2
7-->2
//-------------------------------
8-->10
5-->2
6-->2
7-->2