LinkedHashMap原理分析

LinkedHashMap原理分析

LinkedHashMap被称作“有序的Map”,作为对比HashMap被称作“无序的”。这里说的“有序”,“无序”到底什么意思?怎样实现的,来研究一下。

案例

在学习源码之前,先来看两个小测试案例。

@Test
public void testHashMap() {

    Map<String, String> map = new HashMap<>();
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 

    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=guanyu,value=云长
key=guojia,value=郭嘉
key=liubei,value=玄德
key=caocao,value=孟德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
@Test
public void testLinkedHashMap() {

    Map<String, String> map = new LinkedHashMap<>();
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 
    
    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=caocao,value=孟德
key=liubei,value=玄德
key=guanyu,value=云长
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉

先来解释一下有序,无序的意思,有序,无序通常指的是:遍历时能否按照插入的顺序遍历。对于HashMap来说,是无序的,插入的顺序与遍历的顺序不一致,上面的testHashMap()方法多次执行,结果都是一致的,所以无序并不是随机。LinkedHashMap插入顺序与遍历的顺序一致。

原理分析

我们先来分析HashMap无序的原理。HashMap的源码分析在这里HashMap源码分析

HashMap元素排列分析

画一图解释无序问题,下图为测试HashMap时插入元素的实际排布情况。

在这里插入图片描述
对照测试结果,可以看出实际的遍历顺序就是按照数组的索引顺序,以及链表的顺序遍历的。我们来看遍历的源码。

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Node<K,V>[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {  //遍历数组
            for (Node<K,V> e = tab[i]; e != null; e = e.next)  //如果存在链表结构,遍历链表
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

逻辑很清晰,与测试现象一致。

LinkedHashMap源码分析

再来看LinkedHashMap的实现源码。

LinkedHashMap继承了HashMap,为了实现有序,将每个节点保存了前一个,后一个节点的指针,组成双向链表结构。先来看几个关键字段。

static class Entry<K,V> extends HashMap.Node<K,V> {  //静态内部类,继承了HashMap.Node类,这时HashMap的节点类,增加了before, after两个指针
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

transient LinkedHashMap.Entry<K,V> head;  //链表头

transient LinkedHashMap.Entry<K,V> tail;  //链表尾

final boolean accessOrder;  //表示按照哪种顺序遍历,false:插入顺序   true:查询顺序,默认false,可以通过构造行数指定

LinkedHashMap并没有重写put方法,而是直接继承HashMap中的put方法,但是在HashMap方法中重写回调方法,实现类了元素的有序,我们来看HashMap的put源码,以及回调方法,研究具体的实现,put方法调用内部的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;
}

实例化新元素的方法,在LinkedHashMap中被重写,我们看一下实现逻辑。

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

这里的关键逻辑是将新创建的元素添加到了链表的尾部,通过这里可以看出,LinkedHashMap是通过一个双向链表来保持插入元素的顺序的。这时按照上面的测试案例,LinkedHashMap内部会维护下图的链表结构。
在这里插入图片描述
下来看下插入元素后,回调的 afterNodeInsertion(evict); 方法,这个方法在HashMap为空实现,在LinkedHashMap中重写,我们直接看LinkedHashMap中的实现。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {   //removeEldestEntry方法返回false,if永远不会成立
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

来看LinkedHashMap的遍历方法。

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

看到这里应该明白了,LinkedHashMap的有序实现原理,但是还没结束,LinkedHashMap还有另外一种遍历顺序,当accessOrder为true时。我们改造下上面的测试案例代码。

@Test
public void testLinkedHashMap() {

    Map<String, String> map1 = new LinkedHashMap<>(16,0.75f,true);
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 
    
    map1.get("guanyu");
    
    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=caocao,value=孟德
key=liubei,value=玄德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉
key=guanyu,value=云长

我们看到,顺序与插入顺序不一致了。这个测试方法,处理在构造器中将accessOrder设置为true,在第12行获取了一个元素。我们来看get方法的实现逻辑。

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

如果accessOrder为true会执行afterNodeAccess(e);

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {   //accessOrder为true才会执行下面的逻辑
        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;
    }
}

这段代码实现的就是将查询到的元素移动到双向链表的尾部,这就是按照获取的获取的顺序遍历。在删除,修改中也存在类似的逻辑。

总结

LinkedHashMap是有序的,通过维护一个双向链表结构维护了元素的顺序,支持两种遍历顺序:按插入顺序遍历和按照查询顺序遍历。

<think>我们首先需要澄清一个概念:在Java标准库中,实际上并没有名为`LinkedListArray`的类。用户可能是指`LinkedList`(基于双向链表)和`ArrayList`(基于动态数组),或者是`LinkedHashMap`(基于哈希表和双向链表)。由于问题中提到了`LinkedListArray`和`LinkedHashMap`,我将假设`LinkedListArray`是用户对`ArrayList`和`LinkedList`的合并称呼,但通常它们是两个不同的类。不过,为了准确起见,我将分别解释`ArrayList`、`LinkedList`和`LinkedHashMap`。###1.ArrayList(动态数组)**原理**:`ArrayList`是基于动态数组实现的。它允许随机访问,内部使用一个Object数组来存储元素。当数组容量不足时,会自动进行扩容(通常扩容为原来的1.5倍)。**源码分析(基于Java8)**:-**关键字段**:```javaprivatestaticfinalintDEFAULT_CAPACITY=10;//默认初始容量transientObject[]elementData;//存储元素的数组privateintsize;//当前元素个数```-**扩容机制**:在添加元素时,如果当前数组已满,则会触发扩容。```javapublicbooleanadd(Ee){ensureCapacityInternal(size+1);//确保容量足够elementData[size++]=e;returntrue;}privatevoidensureCapacityInternal(intminCapacity){if(elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA){minCapacity=Math.max(DEFAULT_CAPACITY,minCapacity);}ensureExplicitCapacity(minCapacity);}privatevoidensureExplicitCapacity(intminCapacity){modCount++;if(minCapacity-elementData.length>0)grow(minCapacity);}privatevoidgrow(intminCapacity){intoldCapacity=elementData.length;intnewCapacity=oldCapacity+(oldCapacity>>1);//扩容1.5倍if(newCapacity-minCapacity<0)newCapacity=minCapacity;if(newCapacity-MAX_ARRAY_SIZE>0)newCapacity=hugeCapacity(minCapacity);elementData=Arrays.copyOf(elementData,newCapacity);//复制到新数组}```###2.LinkedList(双向链表)**原理**:`LinkedList`是基于双向链表实现的。每个节点(`Node`)包含前驱、后继引用和当前元素。它不支持随机访问,但插入和删除操作效率较高(只需改变相邻节点的引用)。**源码分析**:-**节点定义**:```javaprivatestaticclassNode<E>{Eitem;Node<E>next;Node<E>prev;Node(Node<E>prev,Eelement,Node<E>next){this.item=element;this.next=next;this.prev=prev;}}```-**添加元素**(在尾部):```javapublicbooleanadd(Ee){linkLast(e);returntrue;}voidlinkLast(Ee){finalNode<E>l=last;finalNode<E>newNode=newNode<>(l,e,null);last=newNode;if(l==null)first=newNode;elsel.next=newNode;size++;modCount++;}```###3.LinkedHashMap(有序哈希表)**原理**:`LinkedHashMap`继承自`HashMap`,在`HashMap`的基础上维护了一个双向链表,用于记录插入顺序或访问顺序(取决于构造参数)。默认按插入顺序排序,也可以设置为按访问顺序排序(最近访问的元素移到链表尾部)。**源码分析**:-**关键字段**:```javatransientLinkedHashMap.Entry<K,V>head;//链表头(最老元素)transientLinkedHashMap.Entry<K,V>tail;//链表尾(最新元素)finalbooleanaccessOrder;//true表示按访问顺序,false表示按插入顺序```-**节点扩展**:`Entry`继承了`HashMap.Node`,并增加了`before`和`after`引用。```javastaticclassEntry<K,V>extendsHashMap.Node<K,V>{Entry<K,V>before,after;Entry(inthash,Kkey,Vvalue,Node<K,V>next){super(hash,key,value,next);}}```-**插入时的维护**:在`put`操作后,会调用`afterNodeInsertion`(可能移除最老元素,如果重写了`removeEldestEntry`方法)和`afterNodeAccess`(如果按访问顺序排序,则将该节点移到尾部)。```javavoidafterNodeInsertion(booleanevict){//插入后可能移除最老元素LinkedHashMap.Entry<K,V>first;if(evict&&(first=head)!=null&&removeEldestEntry(first)){Kkey=first.key;removeNode(hash(key),key,null,false,true);}}voidafterNodeAccess(Node<K,V>e){//访问后移动节点到尾部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;elseb.after=a;if(a!=null)a.before=b;elselast=b;if(last==null)head=p;else{p.before=last;last.after=p;}tail=p;modCount++;}}```###总结-`ArrayList`:动态数组,随机访问高效,插入删除可能涉及元素移动和扩容。-`LinkedList`:双向链表,插入删除高效(尤其头尾),但随机访问需要遍历。-`LinkedHashMap`:在`HashMap`基础上通过双向链表维护顺序(插入顺序或访问顺序)。###相关问题1.`ArrayList`和`LinkedList`在随机访问、插入和删除操作上的时间复杂度分别是多少?2.`LinkedHashMap`如何实现LRU缓存?请描述具体步骤。3.在`ArrayList`中,扩容时为什么选择1.5倍的扩容因子?其他扩容因子(如2倍)有什么优缺点?4.`LinkedList`是否可以作为栈或队列使用?如果可以,如何实现?5.在`LinkedHashMap`中,按访问顺序排序时,`get`操作会对结构产生什么影响?[^1]:参考《Java核心技术卷I》和Java8源码。</think>### LinkedList与ArrayList原理及源码分析 #### 一、ArrayList(动态数组) **数据结构原理** ArrayList基于**动态数组**实现,内部通过`Object[] elementData`存储元素。核心特性: 1. **随机访问高效**:通过索引直接访问,时间复杂度$O(1)$ 2. **动态扩容**:初始容量10,扩容公式:$新容量 = 旧容量 \times 1.5$ 3. **插入/删除成本**:尾部操作$O(1)$,中间操作需移动元素$O(n)$ **源码关键实现(Java 8)** ```java // 扩容机制 private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } // 添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // 检查扩容 elementData[size++] = e; // 尾部插入 return true; } ``` #### 二、LinkedList(双向链表) **数据结构原理** LinkedList基于**双向链表**实现,节点结构: ```java private static class Node<E> { E item; Node<E> next; Node<E> prev; } ``` 核心特性: 1. **插入/删除高效**:头尾操作$O(1)$,中间操作需遍历$O(n)$ 2. **不支持随机访问**:按索引访问需从头遍历$O(n)$ 3. **内存开销**:每个元素额外存储前后节点引用 **源码关键实现** ```java // 头部插入 private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; } // 节点删除 E unlink(Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) first = next; else prev.next = next; if (next == null) last = prev; else next.prev = prev; x.item = null; // GC size--; return element; } ``` #### 三、LinkedHashMap(有序哈希表) **数据结构原理** 继承自HashMap,通过**哈希表+双向链表**实现有序性: 1. **哈希桶**:数组+链表/红黑树(同HashMap) 2. **双向链表**:维护插入顺序或访问顺序 3. **顺序模式**: - 插入顺序(默认):`accessOrder=false` - 访问顺序(LRU):`accessOrder=true` **源码关键实现** ```java // 节点扩展(增加链表指针) 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); } } // LRU维护方法(访问时移动节点到尾部) void afterNodeAccess(Node<K,V> e) { LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e; removeNode(p); // 从链表断开 linkNodeLast(p); // 链接到尾部 } } ``` ### 性能对比 | 操作 | ArrayList | LinkedList | LinkedHashMap | |--------------|-----------|------------|---------------| | 随机访问 | $O(1)$ | $O(n)$ | $O(1)$ | | 头部插入 | $O(n)$ | $O(1)$ | $O(1)$ | | 尾部插入 | $O(1)$ | $O(1)$ | $O(1)$ | | 按值删除 | $O(n)$ | $O(n)$ | $O(1)$ | | 内存占用 | 低 | 高 | 中等 | ### 应用场景 - **ArrayList**:读多写少,需频繁随机访问 - **LinkedList**:写多读少,频繁在头尾增删 - **LinkedHashMap**:需要有序遍历的KV存储,如LRU缓存[^1] --- ### 相关问题 1. ArrayList扩容时为什么选择1.5倍增长因子而不是2倍? 2. LinkedList能否实现队列(Queue)和栈(Stack)的功能?如何实现? 3. LinkedHashMap如何保证在迭代时按插入顺序输出条目? 4. 当LinkedHashMap开启访问顺序模式(accessOrder=true)时,get操作会引发什么结构变化? 5. 为什么LinkedList的迭代器在并发修改时会快速失败(fail-fast)? [^1]: 参考《Java编程思想》容器章节及JDK 17源码分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值