源码浅析:LinkedHashMap、HashTable

本文分析了LinkedHashMap如何通过双向链表维护插入顺序,并探讨了accessOrder属性对遍历的影响;同时介绍了Hashtable的线程安全特性及null值处理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上周对HashMap的源码进行了简单的分析,Map还有一些其他的实现类,本文对其他主要几个类的实现进行简单的分析,主要包括有LinkedHashMap,HashTable。

本文的两个结构与HashMap有较大的关联,关于HashMap可以参考上一篇文章:HashMap源码浅析

1 LinkedHashMap

LinkedHashMap继承了HashMap,同时也实现了Map接口。在使用迭代器遍历的时候,可以按照put的顺序,进行遍历。

LinkedHashMap

1.1 底层实现

LinkedHashMap听名字可以知道是 HashMap和LinkedList(双向链表)的集合体,整体的结构在java8前后也有较大变化。

java7及之前,只记录了双向链表的表头,private transient Entry<K,V>header,header既作表头也作表尾,本身不存储数据,只作为标记。整体的实现就是Hash桶(继承自HashMap),外加维护了一个双向循环链表,通过链表可以顺序访问数据。

java8之后,既标记了表头,也标记了表尾,二者本身也记录了数据:

/**
 * 双向链表表头,记录了最旧的结点
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * 双向链表的表尾,记录了最新的结点
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * 迭代顺序 true-会将更改过的结点移动到最后  false-保持初始put的顺序不变
 */
final boolean accessOrder;

基础构成``Entry`的结构如下:在父类的基础上,增加了记录前后结点的before,after引用。

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

当调用迭代器进行遍历时,通过head开始遍历,通过before属性可以不断找到下一个,直到tail尾结点,从而实现顺序性。 这里不同于HashMap的链表结构,before和after可以连接不同hash的数据。

因为本身继承了HashMap的结构,所以研究重点就在于对链表的维护:①新建结点,②替换结点,③删除节点。

①新建结点时,插入链表。

这里重写了HashMap的newNode()方法和newTreeNode()方法。

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

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

两个方法都在实例化结点之后,调用了linkNodeLast()方法。该方法实现了保存数据到双向链表的功能,本质就是简单的双向链表尾插。

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

②替换结点时,更新链表指向

同样的,重写了HashMap的replacementNode()replacementTreeNode(),保证了结点在替换的时候,维护好它们原本在双向链表的原始插入顺序,调用了transferLinks()

主要就在替换结点类型的时候,同时更新一下双向链表的指向,增加对头尾的判断。

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

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;
    if (b == null)
        head = dst;
    else
        b.after = dst;
    if (a == null)
        tail = dst;
    else
        a.before = dst;
}

③删除结点时,将链表也删除

主要的方法是afterNodeRemoval(),就是双向链表的删除。

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

1.2 遍历顺序

afterNodeAccess()主要是将节点移动到链表最后,和1.1中介绍的linkNodeLast()相比,二者的使用场景不同:

在插入结点的时候,如果在 hash位置上没有元素,那么直接new Node()放到数组table中,这个时候调用了newNode()方法,就会用到linkNodeLast(),将新node放到最后。如果对应的hash位置上有元素,进行元素值的覆盖时,就会调用afterNodeAccess(),将原本可能不是最后的node节点拿到了最后 。

LinkedHashMap的三个成员变量中,accessOrder还没有介绍,在初始化的时候, 会赋初值为false。

看一下afterNodeAccess()方法的实现,该方法是当accessOrder为true时,将节点移动到最后。

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

不仅仅是put的时候,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, 就是保证操作过的Node节点永远在最后, 否则就只是按照第一次操作的顺序进行排序。

一个简单的例子:

LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("a", 1);
map.put("y", 2);
map.put("c", 3);
map.put("p", 4);
map.put("c", 5);
map.get("a");

map.forEach((k,v) -> System.out.print(v));

System.out.println("");
map = new LinkedHashMap<>(16, 0.75f, false);
map.put("a", 1);
map.put("y", 2);
map.put("c", 3);
map.put("p", 4);
map.put("c", 5);
map.get("a");

map.forEach((k,v) -> System.out.print(v));

根据前面的结论分析,为true时,最新改动的结点会移动到最后,为false时,顺序是第一次put的顺序,故打印结果为:

2451

1254

总的来说,LinkedHashMap继承了HashMap,关于Map的功能都有实现,所以这里主要是实现对链表的维护操作,以及accessOrder属性丰富顺序的维护方式。

2 HashTable

在这里插入图片描述

可见 Hashtable继承了Dictionary类,HashMap继承自AbstractMap类,但二者都实现了Map接口。

众所周知,Hashtable 是线程安全的,它的方法都由synchronized修饰,但是采用这种方法实现的多线程安全的容器在大并发量的情况下效率比较低,对此,java引入了专门在大并发量使用的并发容器,在实现的时候,使用更加西粒度的锁,以提升效率。

2.1 null的处理

众所周知,HashTable的key和value都不可以为null,而HashMap无此限制,且会将null key放到下标为0的位置。下面我们来看看源码,找找原因。

我们看一下HashMap的put方法,以及hash方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

不难看出,在计算hash的时候,当key是null的时候,将hash置为0;

而在HashMap的文章中,我们知道,下标i = (n - 1) & hash,所以下标计算,与运算始终为0,即key为null的时候,始终放在hash桶0的位置。

我们再来看一下HashTable的put方法:

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

在第一步就对value进行了判断,如果value是null直接抛出空指针异常;

同时在取hash的时候,直接使用key调用hashCode()方法:

int hash = key.hashCode();

如果key是null,那么也会报空指针异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值