linkedhashmap获取第n个元素_JDK1.8源码(九)——java.util.LinkedHashMap 类

  前面我们介绍了 Map 集合的一种典型实现 HashMap ,关于 HashMap 的特性,我们再来复习一遍:

  ①、基于JDK1.8的HashMap是由数组+链表+红黑树组成,相对于早期版本的 JDK HashMap 实现,新增了红黑树作为底层数据结构,在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率。

  ②、允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。

  ③、非线程安全

  ④、无序(遍历HashMap得到元素的顺序不是按照插入的顺序)

  HashMap 集合可以说是最重要的集合之一,上篇博客介绍的 HashSet 集合就是继承 HashMap 来实现的。而本篇博客我们介绍 Map 集合的另一种实现——LinkedHashMap,其实也是继承 HashMap 集合来实现的,而且我们在介绍 HashMap 集合的 put 方法时,也指出了 put 方法中调用的部分方法在 HashMap 都是空实现,而在 LinkedHashMap 中进行了重写。所以想要彻底了解 LinkedHashMap 的实现原理,HashMap 的实现原理一定不能不懂。

1、LinkedHashMap 定义

  LinkedHashMap 是基于 HashMap 实现的一种集合,具有 HashMap 集合上面所说的所有特点,除了 HashMap 无序的特点,LinkedHashMap 是有序的,因为 LinkedHashMap 在 HashMap 的基础上单独维护了一个具有所有数据的双向链表,该链表保证了元素迭代的顺序。

  所以我们可以直接这样说:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基础上多维护了一个双向链表,用来保证元素迭代顺序。

  更形象化的图形展示可以直接移到文章末尾。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

7de71df0e9367efdb61894df92d0d336.png

2、字段属性

  ①、Entry<K,V>

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

  LinkedHashMap 的每个元素都是一个 Entry,我们看到对于 Entry 继承自 HashMap 的 Node 结构,相对于 Node 结构,LinkedHashMap 多了 before 和 after 结构。

  下面是Map类集合基本元素的实现演变。

198af597cd161d373e116fe107e95a96.png

  LinkedHashMap 中 Entry 相对于 HashMap 多出的 before 和 after 便是用来维护 LinkedHashMap 插入 Entry 的先后顺序的。

②、其它属性

//用来指向双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
//用来指向双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
//用来指定LinkedHashMap的迭代顺序
//true 表示按照访问顺序,会把访问过的元素放在链表后面,放置顺序是访问的顺序
//false 表示按照插入顺序遍历
final boolean accessOrder;

注意:这里有五个属性别搞混淆的,对于 Node next 属性,是用来维护整个集合中 Entry 的顺序。对于 Entry before,Entry after ,以及 Entry head,Entry tail,这四个属性都是用来维护保证集合顺序的链表,其中前两个before和after表示某个节点的上一个节点和下一个节点,这是一个双向链表。后两个属性 head 和 tail 分别表示这个链表的头节点和尾节点。

3、构造函数

①、无参构造

     public LinkedHashMap() {
         super();
         accessOrder = false;
     }

  调用无参的 HashMap 构造函数,具有默认初始容量(16)和加载因子(0.75)。并且设定了 accessOrder = false,表示默认按照插入顺序进行遍历。

②、指定初始容量

     public LinkedHashMap(int initialCapacity) {
         super(initialCapacity);
         accessOrder = false;
     }

③、指定初始容量和加载因子

     public LinkedHashMap(int initialCapacity, float loadFactor) {
         super(initialCapacity, loadFactor);
         accessOrder = false;
     }

④、指定初始容量和加载因子,以及迭代规则

     public LinkedHashMap(int initialCapacity,
                          float loadFactor,
                          boolean accessOrder) {
         super(initialCapacity, loadFactor);
         this.accessOrder = accessOrder;
     }

⑤、构造包含指定集合中的元素

     public LinkedHashMap(Map<? extends K, ? extends V> m) {
         super();
         accessOrder = false;
         putMapEntries(m, false);
     }

  上面所有的构造函数默认 accessOrder = false,除了第四个构造函数能够指定 accessOrder 的值。

4、添加元素

  LinkedHashMap 中是没有 put 方法的,直接调用父类 HashMap 的 put 方法。关于 HashMap 的put 方法,可以参看我对于 HashMap 的介绍。

  我将方法介绍复制到下面:

//hash(key)就是上面讲的hash方法,对其进行了第一步和第二步处理
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * 
     * @param hash 索引的位置
     * @param key  键
     * @param value  值
     * @param onlyIfAbsent true 表示不要更改现有值
     * @param evict false表示table处于创建模式
     * @return
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
            boolean evict) {
         Node<K,V>[] tab; Node<K,V> p; int n, i;
         //如果table为null或者长度为0,则进行初始化
         //resize()方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配,后面会详细讲解该方法
         if ((tab = table) == null || (n = tab.length) == 0)
             n = (tab = resize()).length;
         //注意:这里用到了前面讲解获得key的hash码的第三步,取模运算,下面的if-else分别是 tab[i] 为null和不为null
         if ((p = tab[i = (n - 1) & hash]) == null)
             tab[i] = newNode(hash, key, value, null);//tab[i] 为null,直接将新的key-value插入到计算的索引i位置
         else {//tab[i] 不为null,表示该位置已经有值了
             Node<K,V> e; K k;
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;//节点key已经有值了,直接用新值覆盖
             //该链是红黑树
             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);
                         //链表长度大于8,转换成红黑树
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     //key已经存在直接覆盖value
                     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 的迭代顺序,在添加元素时重写了的4个方法,分别是第23行、31行以及53、60行代码:

 newNode(hash, key, value, null);
 putTreeVal(this, tab, hash, key, value)//newTreeNode(h, k, v, xpn)
 afterNodeAccess(e);
 afterNodeInsertion(evict);

  ①、对于 newNode(hash,key,value,null) 方法

HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
                new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //用临时变量last记录尾节点tail
        LinkedHashMap.Entry<K,V> last = tail;
        //将尾节点设为当前插入的节点p
        tail = p;
        //如果原先尾节点为null,表示当前链表为空
        if (last == null)
            //头结点也为当前插入节点
            head = p;
        else {
            //原始链表不为空,那么将当前节点的上节点指向原始尾节点
            p.before = last;
            //原始尾节点的下一个节点指向当前插入节点
            last.after = p;
        }
    }

  也就是说将当前添加的元素设为原始链表的尾节点。

  ②、对于 putTreeVal 方法

  是在添加红黑树节点时的操作,LinkedHashMap 也重写了该方法的 newTreeNode 方法:

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

  也就是说上面两个方法都是在将新添加的元素放置到链表的尾端,并维护链表节点之间的关系。 

  ③、对于 afterNodeAccess(e) 方法,在 putVal 方法中,是当添加数据键值对的 key 存在时,会对 value 进行替换。然后调用 afterNodeAccess(e) 方法:

//把当前节点放到双向链表的尾部
    void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //当 accessOrder = true 并且当前节点不等于尾节点tail。这里将last节点赋值为tail节点
        if (accessOrder && (last = tail) != e) {
            //记录当前节点的上一个节点b和下一个节点a
            LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //释放当前节点和后一个节点的关系
            p.after = null;
            //如果当前节点的前一个节点为null
            if (b == null)
                //头节点=当前节点的下一个节点
                head = a;
            else
                //否则b的后节点指向a
                b.after = a;
            //如果a != null
            if (a != null)
                //a的前一个节点指向b
                a.before = b;
            else
                //b设为尾节点
                last = b;
            //如果尾节点为null
            if (last == null)
                //头节点设为p
                head = p;
            else {
                //否则将p放到双向链表的最后
                p.before = last;
                last.after = p;
            }
            //将尾节点设为p
            tail = p;
            //LinkedHashMap对象操作次数+1,用于快速失败校验
            ++modCount;
        }
    }

  该方法是在 accessOrder = true 并且 插入的当前节点不等于尾节点时,该方法才会生效。并且该方法的作用是将插入的节点变为尾节点,后面在get方法中也会调用。代码实现可能有点绕,可以借助下图来理解:

b5bbd6b7ee46c731149186a96eedfcc6.png

  ④、在看 afterNodeInsertion(evict) 方法

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

  该方法用来移除最老的首节点,首先方法要能执行到if语句里面,必须 evict = true,并且 头节点不为null,并且 removeEldestEntry(first) 返回true,这三个条件必须同时满足,前面两个好理解,我们看最后这个方法条件:

     protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
         return false;
     }

  这就奇怪了,该方法直接返回的是 false,也就是说怎么都不会进入到 if 方法体内了,那这是这么回事呢?

  这其实是用来实现 LRU(Least Recently Used,最近最少使用)Cache 时,重写的一个方法。比如在 mybatis-connector 包中,有这样一个类:

package com.mysql.jdbc.util;

import java.util.LinkedHashMap;
import java.util.Map.Entry;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 1L;
    protected int maxElements;

    public LRUCache(int maxSize) {
        super(maxSize, 0.75F, true);
        this.maxElements = maxSize;
    }

    protected boolean removeEldestEntry(Entry<K, V> eldest) {
        return this.size() > this.maxElements;
    }
}

  可以看到,它重写了 removeEldestEntry(Entry<K,V> eldest) 方法,当元素的个数大于设定的最大个数,便移除首元素。

5、删除元素

  同理也是调用 HashMap 的remove 方法,这里我不作过多的讲解,着重看LinkedHashMap 重写的第 46 行方法。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    final Node<K,V> removeNode(int hash, Object key, Object value,
            boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //(n - 1) & hash找到桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        //如果键的值与链表第一个节点相等,则将 node 指向该节点
        if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        node = p;
        //如果桶节点存在下一个节点
        else if ((e = p.next) != null) {
            //节点为红黑树
        if (p instanceof TreeNode)
         node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到需要删除的红黑树节点
        else {
         do {//遍历链表,找到待删除的节点
             if (e.hash == hash &&
                 ((k = e.key) == key ||
                  (key != null && key.equals(k)))) {
                 node = e;
                 break;
             }
             p = e;
         } while ((e = e.next) != null);
        }
        }
        //删除节点,并进行调节红黑树平衡
        if (node != null && (!matchValue || (v = node.value) == value ||
                      (value != null && value.equals(v)))) {
        if (node instanceof TreeNode)
         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
        else if (node == p)
         tab[index] = node.next;
        else
         p.next = node.next;
        ++modCount;
        --size;
        afterNodeRemoval(node);
        return node;
        }
        }
        return null;
    }

  我们看第 46 行代码实现:

void 

  该方法其实很好理解,就是当我们删除某个节点时,为了保证链表还是有序的,那么必须维护其前后节点。而该方法的作用就是维护删除节点的前后节点关系。

6、查找元素

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

  相比于 HashMap 的 get 方法,这里多出了第 5,6行代码,当 accessOrder = true 时,即表示按照最近访问的迭代顺序,会将访问过的元素放在链表后面。

  对于 afterNodeAccess(e) 方法,在前面第 4 小节 添加元素已经介绍过了,这就不在介绍。

7、遍历元素

  在介绍 HashMap 时,我们介绍了 4 中遍历方式,同理,对于 LinkedHashMap 也有 4 种,这里我们介绍效率较高的两种遍历方式:

  ①、得到 Entry 集合,然后遍历 Entry

LinkedHashMap<String,String> map = new LinkedHashMap<>();
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        map.get("B");
        Set<Map.Entry<String,String>> entrySet = map.entrySet();
        for(Map.Entry<String,String> entry : entrySet ){
            System.out.println(entry.getKey()+"---"+entry.getValue());
        }

  ②、迭代

         Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
         while(iterator.hasNext()){
             Map.Entry<String,String> entry = iterator.next();
             System.out.println(entry.getKey()+"----"+entry.getValue());
         }

  这两种效率都还不错,通过迭代的方式可以对一边遍历一边删除元素,而第一种删除元素会报错。

  打印结果:

b6e49fbecc420d3b6003b6be44724156.png

8、迭代器

  我们把上面遍历的LinkedHashMap 构造函数改成下面的:

LinkedHashMap<String,String> map = new LinkedHashMap<>(16,0.75F,true);

  也就是说将 accessOrder = true,表示按照访问顺序来遍历,注意看上面的 第 5 行代码:map.get("B)。也就是说设置 accessOrder = true 之后,那么 B---2 应该是最后输出,我们看一下打印结果:

d00d3705b36d503facdb3c83a3eca4de.png

  结果跟预期一致。那么在遍历的过程中,LinkedHashMap 是如何进行的呢?

  我们追溯源码:首先进入到 map.entrySet() 方法里面:

776b5f534ac5fdfb5b227149c4a11af7.png

  发现 entrySet = new LinkedEntrySet() ,接下来我们查看 LinkedEntrySet 类。

cf3d14f523c06bbfafeca549ee163207.png

  这是一个内部类,我们查看其 iterator() 方法,发现又new 了一个新对象 LinkedEntryIterator,接着看这个类:

58f7c85afc3d1284c3a99e6566dd2aa9.png

  这个类继承 LinkedHashIterator。

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

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

9、总结

  通过上面的介绍,关于 LinkedHashMap ,我想直接用下面一幅图来解释:

9993dc681c1d7d20ee96a097edc34d0a.png

  去掉红色和蓝色的虚线指针,其实就是一个HashMap。

本系列教程持续更新,可以微信搜索「 IT可乐 」第一时间阅读。回复《电子书》有我为大家特别筛选的书籍资料
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在JDK 1.8中,API文档是用英文编写的,但是我可以为你提供一些英文和中文对照的示例。 1. Object (Object class) - equals()方法: 判断两个对象是否相等。 - toString()方法: 返回对象的字符串表示。 - hashCode()方法: 返回对象的哈希码值。 2. String (String class) - length()方法: 返回字符串的长度。 - charAt()方法: 返回指定位置的字符。 - substring()方法: 返回子串。 3. ArrayList (ArrayList class) - add()方法: 将元素添加到ArrayList。 - get()方法: 获取指定位置的元素。 - remove()方法: 删除指定位置的元素。 4. HashMap (HashMap class) - put()方法: 将键值对添加到HashMap。 - get()方法: 根据键获取对应的值。 - remove()方法: 根据键删除键值对。 5. DateTimeFormatter - parse()方法: 将字符串解析为日期时间对象。 - format()方法: 将日期时间对象格式化为字符串。 请注意,这只是一小部分JDK 1.8 API中的方法示例,整个API文档提供了更多详细的说明和示例。您可以参考官方的JDK 1.8 API文档以获取更全面的信息。 ### 回答2: 1. java.lang 包下的常用: - Object:对象 - Class: - String:字符串 - StringBuilder:字符串构建器 - StringBuffer:字符串缓冲 - System:系统,提供与系统相关的方法和属性 - Math:数学,提供数学相关的方法和常量 - Boolean:布尔 - Byte:字节 - Character:字符 - Short:短整数 - Integer:整数 - Long:长整数 - Float:单精度浮点数 - Double:双精度浮点数 - Enum:枚举 - Throwable:可抛出对象,是所有错误和异常的基 2. java.util 包下的常用: - ArrayList:动态数组 - LinkedList:链表 - HashMap:哈希映射 - LinkedHashMap:有序哈希映射 - TreeSet:树集 - TreeMap:树映射 - HashSet:哈希集合 - LinkedHashSet:有序哈希集合 - Queue:队列接口 - PriorityQueue:优先队列 - Stack:栈 - Iterator:迭代器接口 - ListIterator:列表迭代器接口 - Comparator:比较器接口 - Calendar:日历 - Date:日期 - Timer:定时器 3. java.io 包下的常用: - File:文件 - InputStream:输入流接口 - OutputStream:输出流接口 - Reader:字符输入流接口 - Writer:字符输出流接口 - BufferedReader:缓冲字符输入流 - BufferedWriter:缓冲字符输出流 - FileReader:文件字符输入流 - FileWriter:文件字符输出流 - FileInputStream:文件字节输入流 - FileOutputStream:文件字节输出流 - ObjectInputStream:对象输入流 - ObjectOutputStream:对象输出流 - PrintWriter:打印输出流 - RandomAccessFile:随机访问文件 - ByteArrayInputStream:字节数组输入流 - ByteArrayOutputStream:字节数组输出流 - CharArrayReader:字符数组输入流 - CharArrayWriter:字符数组输出流 这些只是jdk1.8 API中的一部分常用和接口,还有很多其他和接口不一一列举。 ### 回答3: JDK 1.8中的API是指Java开发工具包1.8版本中提供的所有、接口和方法,它们用于开发Java应用程序。下面是一些常用API的中英文对照: 1. java.lang包: - Object: 对象 - String: 字符串 - Integer: 整型 - Double: 双精度浮点型 - Math: 数学计算 2. java.util包: - ArrayList: 数组列表 - LinkedList: 链表 - HashMap: 哈希表 - TreeSet: 树集合 - PriorityQueue: 优先队列 3. java.io包: - File: 文件 - InputStream: 输入流 - OutputStream: 输出流 - BufferedReader: 缓冲读取器 - BufferedWriter: 缓冲写入器 4. java.awt包: - Frame: 框架 - Button: 按钮 - Label: 标签 - TextField: 文本框 - Checkbox: 复选框 5. java.net包: - Socket: 套接字 - ServerSocket: 服务器套接字 - URL: 统一资源定位符 - HttpURLConnection: HTTP连接 这只是JDK 1.8 API中一小部分常用和接口的中英文对照,实际上API包含了更多的和接口,用于满足不同的编程需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值