LinkedHashMap与LinkedHashSet

2,554 阅读14分钟

LinkedHashMap

​ LinkedHashmap将hash表与链表结合起来,它实现了Map接口,它与hashmap不同的是它内部还持有一个双向链表,并且这个双向链表决定了内部的键值对的迭代顺序。通常情况下该顺序就是键值对被插入hash表的顺序,当hash表执行覆盖操作的时候,是不会影响他原来的顺序的,它通常可以用来产生一个与原来的hash表具有相同顺序的拷贝hash表,不管原先的hash表是什么实现。

​ 通过特定的构造方法,可以将LinkedHashMap中的键值对的顺序指定为其最后被访问的顺序,因此这种方式构造的hash表是比较适合做LRU缓存的。LinkedHashMap像hashmap一样也是可以允许放入空的键和空的值的。

​ LinkedHashMap不是线程安全的,因此多个线程并发的访问同一实例的时候会存在线程安全问题。

声明

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
	//代码
}

​ LinkedHashMap继承自HashMap,并实现了Map接口。

成员变量

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
    //用于支持序列化与反序列化
    private static final long serialVersionUID = 3801124242820219131L;
    /**
     * 双向链表的头结点,也是最“年长”的结点
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 双向链表的尾结点,也是最“年轻”的结点
     */
    transient LinkedHashMap.Entry<K,V> tail;
    
    /**
     * 该变量指定了迭代器遍历的顺序,如果为true,那么将根据entry被访问的
     * 顺序存放,如果为false,将根据entry被插入时的先后顺序存放。
     */
    final boolean accessOrder;
}

内部的键值对Entry

static class Entry<K,V> extends HashMap.Node<K,V> 
{
    //由于要复用之前的hashmap中的node节点,但是呢,
    //hashmap的node节点在为链表的结点的时候只有next,也就是只能是单向链表的形式
    //在hashmap的结点转化为树之后是有左子树和右子树,也无法直接复用。
    //因此需要添加这两个节点,使其构成双向链表,并能够复用之前的hashmap中的node。
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next)
    {
        super(hash, key, value, next);
    }
}

其他方法

/**
 * 该变量指定了迭代器遍历的顺序,如果为true,那么将根据entry被访问的
 * 顺序存放,如果为false,将根据entry被插入时的先后顺序存放。
*/

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) 
{
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    //说明链表为空,那么这时候相当于插入后只有一个元素p
    if (last == null)
        head = p;
    //更新链表,使新结点链接到双向链表的尾部。
    else 
    {
        p.before = last;
        last.after = p;
    }
}

/**
 * 这个方法的作用就是用dst结点来完全的替代掉src结点。并将src从链上断开,dst连接到链上。
*/
private void transferLinks(LinkedHashMap.Entry<K,V> src, LinkedHashMap.Entry<K,V> dst)
{
    LinkedHashMap.Entry<K,V> b = dst.before = src.before;
    LinkedHashMap.Entry<K,V> a = dst.after = src.after;
    //dst之前没有元素了,那它就是头结点
    if (b == null)
        head = dst;
    else
        b.after = dst;
    //dst之后没有元素了,那它就是尾结点。
    if (a == null)
        tail = dst;
    else
        a.before = dst;
}

/**
 * 调用的hashmap的reinitialize方法。
*/
void reinitialize() 
{
    super.reinitialize();
    head = tail = null;
}

/**
 * 这是hashmap的reinitialize方法,将hash表重置到初始的默认状态,通常有clone方法或者readObject方法调用
*/
void reinitialize() 
{
    table = null;
    entrySet = null;
    keySet = null;
    values = null;
    modCount = 0;
    threshold = 0;
    size = 0;
}
/**
 * 如果原来hashmap中的结点以链表的形式存放。将键值对构造成一个新的Entry结点(继承了Node),然后将其挂接到内部双端链表的末尾
 * 然后返回该节点。
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e)
{
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

/**
 * 如果原来hashmap中的结点以红黑树的形式存放。根据p的键值对构造一个新的树结点,将其连接到双向
 * 链表的末尾。
*/
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next)
{
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

/**
 * 根据p的键值对构造一个新的entry节点,其next指向next。
 * 并将p替换为新构造的这个节点。
*/
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) 
{
    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
    LinkedHashMap.Entry<K,V> t =
        new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
    transferLinks(q, t);
    return t;
}

/**
 * 根据p的键值对构造一个新的 TreeNode节点,其next指向next。
 * 并将p替换为新构造的这个节点。
*/
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next)
{
    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
    TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
    transferLinks(q, t);
    return t;
}


/**
 * 将Node节点e从双端链表中删除。
*/
void afterNodeRemoval(Node<K,V> e) 
{ // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //使p节点孤立,方便垃圾回收
    p.before = p.after = null;
    //p节点之前是头结点,那么现在他的后面的一个节点变成了头节点。
    if (b == null)
        head = a;
    else
        b.after = a;
    //p节点之前是尾结点,那么现在他的前一个节点变成了尾结点。
    if (a == null)
        tail = b;
    else
        a.before = b;
}

/**
 * 由于目前removeEldestEntry一直返回false,因此该方法目前没有什么作用
*/
void afterNodeInsertion(boolean evict) 
{ // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) 
    {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
/**
 * 如果返回true,那么意味着该map需要删除最先被放入的entry节点。
 * 该方法目前永远返回true,等到以后出现新的类继承LinkedHashMap时重写该方法可能会改变。
 * 该方法通常在调用put或者putall方法添加了新的entry到map中时调用
 * 后面如果有需求的话那么只要继承LinkedHashMap并且重写该方法即可,这使得map很适合作为缓存。
 * 例如这样。
 			private static final int MAX_ENTRIES = 100;
     *
     *     protected boolean removeEldestEntry(Map.Entry eldest) {
     *        return size() >MAX_ENTRIES;
     *     }
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
{
    return false;
}

序列化相关

/**
 * 将双向链表中保存的entry序列化到流中。
*/
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException 
{
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
    {
        s.writeObject(e.key);
        s.writeObject(e.value);
    }
}

构造方法——如何初始化

/**
 * 构造一个空的LinkedHashMap实例,内部的节点排序方式是使用元素的插入顺序。
 * 其中初始容量和负载因子将根据给定的值使用父类hashmap的方法来进行初始化。
 */
public LinkedHashMap(int initialCapacity, float loadFactor)
{
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * 构造一个空的LinkedHashMap实例,内部的节点排序方式是使用元素的插入顺序。
 * 其中初始容量将根据给定的值使用父类hashmap的方法来进行初始化。负载因子将按照默认的0.75的方式初始化。
 */
public LinkedHashMap(int initialCapacity) 
{
    super(initialCapacity);
    accessOrder = false;
}

/**
 * 构造一个空的LinkedHashMap实例,内部的节点排序方式是使用元素的插入顺序。
 * 其中初始容量和负载因子将使用父类hashmap的方法来进行默认的初始化。
 * 初始容量为默认的16,负载因子将按照默认的0.75的方式初始化。
 */
public LinkedHashMap()
{
    super();
    accessOrder = false;
}

/**
 * 根据已有的map实现类的实例,将m中保存的键值对使用hashmap的构造方法添加到该linkedHashMap实例中
 * 并且使用的是元素的插入顺序存储,同时使用父类的putMapEntries方法将其放入到内部的结合中,
 * 注意该方法在内部调用了afterNodeInsertion。而LinkedHashMap重写了该方法,因此在插入到链表之后会
 * 执行子类重写的afterNodeInsertion方法执行后续的逻辑。但是该方法也没有什么实质的实现。
 * 同时该方法会调用子类重写的afterNodeAccess方法来将节点按照插入的顺序连接到双向链表中。
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) 
{
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

/**
 * 根据传入的初始容量,负载因子,构造一个空的hash表的实例,并指定内部的元素排序方式以何种方式排序
 * 如果accessOrder=true,那么将根据元素被访问的顺序排序,否则将按照元素被插入的顺序排序。
 */
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) 
{
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

常用方法

containsValue方法

/**
 * 判断底层的hashmap的entry中是否有value等于给定的value。
 * 但是该方法并不是用底层hashmap的方法,而是通过遍历双向链表的方式,这样相比
 * hashmap在初始容量开的很大,桶很多空的时候更加的高效一点。
 */
public boolean containsValue(Object value) 
{
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
    {
        V v = e.value;
        if (v == value || (value != null && value.equals(v)))
            return true;
    }
    return false;
}

get方法

/**
 * 根据key返回指定的value,如果不存在指定的键值对,那么将返回null。
 * 即使返回null也不能说明hash表中不存在指定的键值对,因为LinkedHashMap底层使用HashMap实现
 * 可以允许空的键值对。
 * 该方法使用父类的getNode 方法实现,同时,如果是按照元素被访问的顺序,那么在访问了该键值对之后,需要
 * 将元素从双向链表中删除并添加到链表的尾部。
 * 
 */
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

/**
 * 如果是根据节点的访问顺序排序的话,那么节点每次访问之后
 * 都将节点从双向链表中删除,然后重新放到链表的尾部。
*/
void afterNodeAccess(Node<K,V> e)
{ // move node to last
    LinkedHashMap.Entry<K,V> last;
    //如果该节点本身即是尾结点了,那么就不用管了。
    if (accessOrder && (last = tail) != e) 
    {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else
        {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

getOrDefault方法

 /**
  * 该方法与hashmap的相应逻辑基本一致,只不过如果按照元素访问排序
  * 那么由于元素被访问了,需要调整元素在双向链表中的顺序。
  */
 public V getOrDefault(Object key, V defaultValue) 
 {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return defaultValue;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

clear方法

/**
 * 除了调用父类hashmap的方法,该方法还要将head和tail置为空。
 */
public void clear() 
{
    super.clear();
    head = tail = null;
}

内部的key,value视图

/**
 * 返回该map中的所有的key对应的Set视图,如果改变map中的key,将会影响到Set集合,反之亦然。
 * 如果在返回的set上使用迭代器遍历元素的过程中,map被修改了,那么将会发生不可预知的结果。
 * 返回的Set实例可以删除key,这样对应的map映射就也会相应的被删除。
 * 但是不能往这个Set中添加元素。
 */
public Set<K> keySet() 
{
    //这个keyset是祖辈相传的,从AbstractMap继承下来的。
    Set<K> ks = keySet;
    if (ks == null) 
    {
        //使用其自己定义的LinkedKeySet作为所有key的视图。
        ks = new LinkedKeySet();
        keySet = ks;
    }
    return ks;
}


final class LinkedKeySet extends AbstractSet<K>
{
    public final int size()                 
    { 
        return size;
    }
    public final void clear()               
    { 
        LinkedHashMap.this.clear();
    }
    //这个是有别有hashmap的地方,它使用双向链表遍历。
    public final Iterator<K> iterator()
    {
        return new LinkedKeyIterator();
    }
    public final boolean contains(Object o) 
    { 
        return containsKey(o);
    }
    public final boolean remove(Object key) 
    {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator()  
    {
        return Spliterators.spliterator(this, Spliterator.SIZED |
                                        Spliterator.ORDERED |
                                        Spliterator.DISTINCT);
    }
    public final void forEach(Consumer<? super K> action) 
    {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.key);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

/**
 * 返回map中包含的所有键值对的所有value的Collection视图,如果改变map中的value,将会影响到Collection集合,反之亦然。
 * 如果在返回的Collection上使用迭代器遍历元素的过程中,map被修改了,那么将会发生不可预知的结果。
 * 返回的Collection实例可以删除value,这样对应的map映射就也会相应的被删除。
 * 但是不能往这个Collection中添加元素。
 */
public Collection<V> values() 
{
    Collection<V> vs = values;
    if (vs == null) 
    {
        vs = new LinkedValues();
        values = vs;
    }
    return vs;
}

final class LinkedValues extends AbstractCollection<V> 
{
    public final int size()                 
    { 
        return size;
    }
    public final void clear()               
    {
        LinkedHashMap.this.clear();
    }
    //这个是有别有hashmap的地方,它使用双向链表遍历。
    public final Iterator<V> iterator() 
    {
        return new LinkedValueIterator();
    }
    public final boolean contains(Object o) 
    { 
        return containsValue(o); 
    }
    public final Spliterator<V> spliterator() 
    {
        return Spliterators.spliterator(this, Spliterator.SIZED |
                                        Spliterator.ORDERED);
    }
    public final void forEach(Consumer<? super V> action)
    {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

/**
 * 返回一个该map中包含的所有键值对映射的Set集合视图,如果改变map,那么Set集合会相应的收到影响,反之亦然。
 * 如果在返回的Set上使用迭代器遍历元素的过程中,map被修改了,那么将会发生不可预知的结果。
 * 返回的Set实例可以删除指定元素,这样对应的map映射就也会相应的被删除。
 * 但是不能往这个Set中添加元素。如果为空,该方法将返回LinkedEntrySet
 */
public Set<Map.Entry<K,V>> entrySet()
{
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}


final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> 
{
    public final int size()
    { 
        return size;
    }
    public final void clear()               
    { 
        LinkedHashMap.this.clear();
    }
    //这个是有别有hashmap的地方,它使用双向链表遍历。
    public final Iterator<Map.Entry<K,V>> iterator() 
    {
        return new LinkedEntryIterator();
    }
    public final boolean contains(Object o) 
    {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) 
    {
        if (o instanceof Map.Entry) 
        {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator()
    {
        return Spliterators.spliterator(this, Spliterator.SIZED |
                                        Spliterator.ORDERED |
                                        Spliterator.DISTINCT);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) 
    {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

内部迭代器实现

abstract class LinkedHashIterator 
{
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    LinkedHashIterator()
    {
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() 
    {
        return next != null;
    }

    //这里遍历的双向链表的结点,比起遍历hash表的每个桶要更加的直接一点。
    final LinkedHashMap.Entry<K,V> nextNode() 
    {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        next = e.after;
        return e;
    }

    public final void remove() 
    {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

final class LinkedKeyIterator extends LinkedHashIterator implements Iterator<K>
{
    public final K next() 
    { 
        return nextNode().getKey();
    }
}

final class LinkedValueIterator extends LinkedHashIterator implements Iterator<V> 
{
    public final V next() 
    { 
        return nextNode().value;
    }
}

final class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> 
{
    public final Map.Entry<K,V> next() 
    { 
        return nextNode();
    }
}

Java8新增API

forEach方法

public void forEach(BiConsumer<? super K, ? super V> action) 
{
    if (action == null)
        throw new NullPointerException();
    int mc = modCount;
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
        action.accept(e.key, e.value);
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

replaceAllf方法

public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) 
{
    if (function == null)
        throw new NullPointerException();
    int mc = modCount;
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
        e.value = function.apply(e.key, e.value);
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

LinkedHashSet

​ LinkedHashSet内部使用双向链表保存元素与元素之间的顺序,元素与元素之间的顺序可以是元素的插入顺序,如果是按照插入顺序的话,那么当元素重新插入的时候并不会收到影响,其也允许放入空的元素,LinkedHashSet也不是线程安全的,多个线程并发的访问的时候需要在外部施加一定的同步机制。

声明

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable 
{
    //代码
}

​ LinkedHashSet继承自HashSet,同时实现了Set接口,使得它具有Set接口所声明的一切方法的api,同时使用极少的代码就可以完成给定的功能。

成员变量

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable 
{
    //用于支持序列化机制
    private static final long serialVersionUID = -2851667679971038690L; 
}

构造方法——初始化

/**
 * 构建一个空的linked hashset 
 * 第三个参数主要用于区分是LinkedHashSet
 */
public LinkedHashSet(int initialCapacity, float loadFactor)
{
    super(initialCapacity, loadFactor, true);
}

/**
 * 使用默认的负载因子构建
 */
public LinkedHashSet(int initialCapacity) 
{
    super(initialCapacity, .75f, true);
}

/**
 * 使用默认的初始容量和负载因子构建
 */
public LinkedHashSet() 
{
    super(16, .75f, true);
}


/**
 * 根据Collection实现类c构建HashSet实例。其初始容量是2*c.size()和11的最大值。
 * 然后调用父类的addAll方法将c中保存的元素添加进去
 */
public LinkedHashSet(Collection<? extends E> c) 
{
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
}

LinkedHashSet是如何实现链表功能的呢,其实其构造方法是关键,可以看到起所有的构造方法都是调用的父类也就是HashSet的构造方法,而该构造方法可以再看一下

HashSet(int initialCapacity, float loadFactor, boolean dummy)
{
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

内部其实是创建了一个LinkedHashMap,所以LinkedHashSet的相应功能是通过底层的LinkedHashMap实现的。

Java8方法

@Override
public Spliterator<E> spliterator() 
{
    return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}

写在最后

​ 可以发现LinkedHashMap通过继承自HashMap因此具有了HashMap的所有的能力,但是因为LinkedHashMap需要保存一定的顺序规则,它是通过继承HashMap的Node结点并通过改动Node结点使其具有了双向链表的特性,从而在不怎么改变原有实现的情况下,将内部的Entry(在HashMap中是Node)以双端链表的形式连接了起来(所以整个LinkedHashMap的关键就是Entry内部的before和after)。同时在对内部的Entry进行迭代器的遍历的时候,是遍历内部的双端队列的链表(否则大费周章搞这个链表干啥)。这样带来的一个额外的优点就是当hashmap的初始容量开的很大的时候,防止大部分的桶是空的或者很稀疏,遍历带来的低效的问题。