关于lruCache(最近最少使用)的算法,这是一个比较重要的算法,它的应用非常广泛,不仅仅在Android中使用,Linux系统等其他地方中也有使用;今天就来看一看这其中的奥秘;
讲到LruCache,就不得不讲一讲LinkedHashMap,而对于LinkedHashMap,它继承的是HashMap,那么我们就先从HashMap开始看起吧;
注:此篇博客所讲的所有知识都是在jdk1.8环境下的,java8的hashmap相比之前的版本又做了一层优化,当链表过长时(默认超过8),会改为采用红黑树这种自平衡的数据结构去进行存储优化
HashMap
我们知道,数据结构中的存在两种常见的存储结构,一个是数组,一个是链表;两者各有优劣,首先数组的存储空间在内存中是连续的,这就就导致占用内存严重,连续的大内存进入老年代的可能性也会变大,但是正因为如此,寻址就显得简单,也就是说查询某个arr会有指定的下标,但是插入和删除比较困难,因为每次插入和删除时,如果数组在插入这个地方后面还有很多数据,那就要后面的数据整体往前或者往后移动。对于链表来说存储空间是不连续的,占用内存比较宽松,它的基本结构是一个节点(node)都会包含下一个节点的信息(如果是双向链表会存在两个信息一个指向上一个一个指向下一个),正因为如此寻址就会变得比较困难,插入和删除就显得容易,链表插入和删除的时候只需要修改节点指向信息就可以了。
那么两者各有优劣,将它们两者结合起来会有什么效果呢?自然早就有大神尝试过了,并且尝试的很成功,它的产物就是HashMap哈希表,也叫散列表;
HashMap的主干是一个数组,里面存储的是一个个的Node,Node中包含了哈希值,key,value和下一个Node的引用;
Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }存储在HashMap中的每一个值都需要一个key,这是为什么呢?这个问题可以再问细一点,hashmap是如何存放数据的?
我们先来看看他的一些基本属性:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
这个属性表示HashMap的初始容量大小是16;
static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量为2^30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
这个表示加载因子默认为0.75,代表hashmap的填充程度,加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高.
因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它默认为0.75就可以了;
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;临界值,这个字段主要是用于当HashMap的size大于它的时候,需要触发resize()方法进行扩容
构造方法:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
可以清晰的看到当new一个HashMap时,并没有为数组分配内存空间(有一个传入map参数的构造方法除外);
几个核心方法:
put方法实际调用的就是putVal方法,所以我们先看putVal方法
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; }
这里的逻辑由于是java8,所以会复杂一点,里面有几个关键点,记录下来,对比着源码看:
(1)putVal方法其实就可以理解为put方法,我们使用hashmap的时候,什么时候才会使用put方法呢,当你想要存储数据的时候会调用,那么putVal方法的逻辑就是为了把你需要存储的数据按位置存放好就可以了;
(2)具体的存放逻辑是通过复杂的if判断来完成的,首先会判断当前通过key和hash函数计算出的数组下标位置的是否为null,如果是空,直接将Node对象存进去;如果不为空,那么就将key值与桶中的Node的key一一比较,在比较的过程中,如果桶中的对象是由红黑树构造而来,那么就使用红黑树的方法去进行存储,如果不是,那么就继续判断当前桶中的元素是否大于8,大于8的话就使用红黑树处理(调用treeifybin方法),如果小于8,那么进行最后的判断是否key值相同,如果相同,就直接将旧的node对象替换为新的node对象;这样就保证了存储的正确性;
(3)在putVal中有这么一句
++modCount;
这里的modCount的作用是用来判断当前HashMap是否在由一个线程操作,因为hashmap本身是线程不安全的,多线程操作会造成其中数据不安全等多种问题,modcount记录的是put的次数,如果modcount不等于put的node的个数的话,就代表有多个线程同时操作,就会报ConcurrentModificationException异常;
再来看看get方法,get方法其实调用的是getNode方法
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 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 &