java LinkedHashMap源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本源码解析是基于JDK1.7,本篇与HashMap源码解析较强的关联性LinkedHashMap概要LinkedHashMap是基于HashTable与LinkedList原理实现的HashMap是基于数组的...

本源码解析是基于JDK1.7,本篇与HashMap源码解析较强的关联性

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);
            }
        }  

清空方法

重写的方法除了清空数组外还需要清空链表,只剩下不含真实元素的标兵节点 header

    public 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实现时却可以直接使用原架构,而只是填充这些小的空方法就可以方便实现
目录
相关文章
|
4天前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
19 4
|
11天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
2天前
|
数据采集 存储 Web App开发
Java爬虫:深入解析商品详情的利器
在数字化时代,信息处理能力成为企业竞争的关键。本文探讨如何利用Java编写高效、准确的商品详情爬虫,涵盖爬虫技术概述、Java爬虫优势、开发步骤、法律法规遵守及数据处理分析等内容,助力电商领域市场趋势把握与决策支持。
|
6天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
7天前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
|
11天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0
|
1月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
84 0
下一篇
无影云桌面