java 集合类——Map、List

本篇博客,将介绍java(jdk1.8)基础中的一些集合类的实现原理,及对相应代码进行分析。主要对Map,List相关的类进行分析。

  • Map
Map接口的总体相关类图,(看不清可在新标签中查看)



  • HashMap
HashMap的底层实现的主要数据结构如下,以Node<K,V>[]数组为桶,每个Node后面长度小于8为链表,否则就为红黑树。数据结构为数组+链表+红黑树。如下图(看不清可在新页签查看)


这时介绍一下红黑树,首先红黑树是一个平衡二叉树,交且具有以下性质:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。



理解HashMap的原理,主要从源码中的hash,putVal,getNode这三个方法入手
hash方法用于构造hash值,如下:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

接下来,看HashMap添加元素的时候,是如何构建整个数据结构的。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //找到hash的桶的位置,没有值就创建新的Node
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        HashMap.Node<K,V> e; K k;
        //判断和入参的key,value是否匹配
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof HashMap.TreeNode)//如果是个树节点,则把入参的key,value构建到树中
            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {//binCount统计这个链表长度有多少
                if ((e = p.next) == null) {//往链表最后面插入新的节点
                    p.next = newNode(hash, key, value, null);
                    //如果链表长度大于等于7,构造为树
                    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;
            }
        }
        //入参的key值在原先的桶中已有对应的value值,
        // 根据参数onlyIfAbsent或原先值是否为null来判断是否覆盖新入参的value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //留给LinkedHashMap扩展使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;//标识被修改过了
    //是否需要扩容
    if (++size > threshold)
        resize();
    //留给LinkedHashMap扩展使用
    afterNodeInsertion(evict);
    return null;
}

添加元素的过程大概为:找到hash值对应的桶的起始位置->链表长度小于8,就往链表后插入元素->大于等于8,将链表构造为树->node节点找的key相同时,根据onlyIfAbsent参数和value值是否为null,决定是否有用新值覆盖。这样就往这个数据结构中添加一个元素了。最后,分析HashMap怎么获取一个元素:
final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            //通过hash值找到对应桶
            (first = tab[(n - 1) & hash]) != null) {
        //桶的值就是要找的值,直接返回
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //是树节点的话,遍历树查找
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
            //为链表,就遍历链表查找值
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
获取元素跟添加相对的,找到桶的初始位置,要么遍历链表,要么遍历树,找到要找的key,返回value值。

  • HashTable
通常会拿HashTable与HashMap进行比较,我们先来看下HashTable添加元素和获取元素的过程。
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.
    Hashtable.Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //获取桶的位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Hashtable.Entry<K,V> entry = (Hashtable.Entry<K,V>)tab[index];
    //遍历链表,找到key对应的value值,返回用新值覆盖旧值,并返回旧值
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    //没有找到,就往链表头新增一个Entry
    addEntry(hash, key, value, index);

}

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Hashtable.Entry<?,?> tab[] = table;
    //判断是否需要扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    //往链表头插入新的元素
    Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>) tab[index];
    tab[index] = new Hashtable.Entry<>(hash, key, value, e);
    count++;
}

HashTable的添加元素的过程就比较简单了,底层数据结构就是数组加链表,也是先通过hash找到桶,然后在找链表,找到有对应的key,就直接覆盖旧值,返回旧值。没有找到对应的key,就往链表头插入新的元素。这一个过程是线程安全的,因为在方法上有synchronized关键字。接下来看下HashTable如何获取元素:
public synchronized V get(Object key) {
    Hashtable.Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //从hash值对应的桶的位置开始查找链表里是否有key,有直接返回value,没有返回null
    for (Hashtable.Entry<?,?> e = tab[index]; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
从hash值对应的桶的位置开始查找链表里是否有key,有直接返回value,没有返回null。这个过程就比较简单,但是问题也很明显,如果链表很长,HashTable查找的效率就会退化成链表的效率了。这也是一个线程安全的方法。

  • LinkedHashMap
LinkedHashMap继承HashMap,也就是说它的底层数据结构跟HashMap是一样的。LinkedHashMap相当于是对HashMap的扩展,具体表现在两个方面:1、用一个链表维护其顺序(存取顺序或插入顺序)2、是否要启用LRU功能。
首先看下维护顺序的实现:
void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //按存取顺序,把要找的node放到维护顺序的链表的最后
    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;
    }
}

接下来看是否启用LRU的功能源码:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    //这里要使用LRU功能,需要继承linkedHashMap,重写removeEldestEntry方法
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        //删除节点的具体操作
        removeNode(hash(key), key, null, false, true);
    }
}
这里的removeEldestEntry方法在LinkedHashMap中总是返回false,所以如果要使用LRU功能,需继承LinkedHashMap,重写removeEldestEntry方法。removeNode就是具体删除节点的操作。(这里的LRU实现,如果对Dubbo熟悉的话,会发现dubbo的LRUCache就是继承LinkedHashMap实现的。
  • CAS
CAS实现的是一种区别于synchronouse的乐观锁。其底层实现是依靠硬件的Cmpxchg原子指令,保证内存的可靠性。
java中的原子类实现,都是基于CAS实现的。以AtomicInteger为例子,分析下其实现代码。

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
AtomicInteger类的compareAndSet方法,其实是调用了unsafe类的compareAndSwarInt方法,这个方法有四个参数,分别是表示:第一个参数this,要修改的对象;第二个参数valueOffset,要修改的属性在内存中的偏移量;第三个参数expect,期待的要修改属性在内存中的值;第四个参数update,要更新的值;具体实现逻辑是,只在当要修改的属性在内存中的值与expect的值一样,才把这个属性值修改为update的值,否则不做改变。
再看下AtomicInteger的getAndSet方法
public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

在unsafe中的实现为:
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}
可以看到,unsafe里,如果cas失败,则会一直重试直到成功。这里使用cas固然比使用synchronouse锁要好,但是cas也有以下几个缺点:
1、ABA问题:
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
要解决这个问题,其思路是添加一个版本号,比较原先是A1->B1->A2,这样就把A1和A2区分开了。java里面用AtomicStampedReference这个类来解决ABA问题,其实现原理是增中一个时间戳来充当版本号。根据原先值和时间戳还判断是否改变。

2、自旋时间长开销大
这个问题就跟刚刚说的AtomicInteger的getAndSet方法实现一样,底层unsafe实现会循环去cas,直到成功为止。

3、只能保证一个共享变量的原子操作
这个问题就是cas只对一个属性进行操作,如果要同时对A,B两个属性保证他们原子性,就是AB要同时修改值,不能A成功B失败。这个问题在java里使用AtomicReference类来保证对象的原子性,前面说的那个例子,可以把A,B封装成一个对象,再用AtomicReference保证这个对象的原子性。

  • ConcurrentHashMap(jdk1.8)
ConcurrentHashMap是线程安全的HashMap,jdk1.8这个类的实现方式,相比于jdk1.7的分段锁的悲观锁,改为使用CAS乐观锁,CAS的实现原理上文大概介绍过。ConcurrentHashMap底层数据结构也是数组+链表+红黑树。这里我们主要分析putVal,get及其相关的方法。首先先介绍保证原子性的主要方法:
/**
 * 获取i位置上的Node节点
 * @param tab
 * @param i
 * @param <K>
 * @param <V>
 * @return
 */
static final <K,V> ConcurrentHashMap.Node<K,V> tabAt(ConcurrentHashMap.Node<K,V>[] tab, int i) {
    return (ConcurrentHashMap.Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

/**
 * 利用cas算法去修改i位置上的node节点
 * @param tab
 * @param i
 * @param c
 * @param v
 * @param <K>
 * @param <V>
 * @return
 */
static final <K,V> boolean casTabAt(ConcurrentHashMap.Node<K,V>[] tab, int i,
                                    ConcurrentHashMap.Node<K,V> c, ConcurrentHashMap.Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

/**
 * 利用volatile方法设置i位置上node节点 
 * @param tab
 * @param i
 * @param v
 * @param <K>
 * @param <V>
 */
static final <K,V> void setTabAt(ConcurrentHashMap.Node<K,V>[] tab, int i, ConcurrentHashMap.Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

通过tabAt、casTabAt、setTabAt这三个方法,让ConCurrentHashMap方法免去加锁,实现线程安全。接下来看下putVal的实现源码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
        ConcurrentHashMap.Node<K,V> f; int n, i, fh;
        //tab为空,初始化table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //tabAt获取i位置上的node节点,如果为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //cas修改i位置上的node节点
            if (casTabAt(tab, i, null,
                    new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //static final int MOVED     = -1; // hash for forwarding nodes
        //扩容相关处理
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {//对要找的hash相同的位置上的节点加锁
                if (tabAt(tab, i) == f) {
                    // static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
                    //是链表结点
                    if (fh >= 0) {
                        binCount = 1;
                        for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //找到相同key,根据onlyIfAbsent参数看是否替换
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            //没有找到key就往链表最后加入新节点
                            ConcurrentHashMap.Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    //如果是红黑树节点
                    else if (f instanceof ConcurrentHashMap.TreeBin) {
                        ConcurrentHashMap.Node<K,V> p;
                        binCount = 2;
                        //根据红黑树找到相同key的节点,根据onlyIfAbsent参数决定是否替换值
                        if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //如果链表的长度大于8,就构建为红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //长度增加1
    addCount(1L, binCount);
    return null;
}
这里在获取节点和设置节点都是无锁实现线程安全,这里增加元素的操作也基本是跟HashMap差不多。但是多线程的扩容就比较复杂了。

synchronized (f) {
    if (tabAt(tab, i) == f) {
        ConcurrentHashMap.Node<K,V> ln, hn;
        if (fh >= 0) {
            int runBit = fh & n;
            ConcurrentHashMap.Node<K,V> lastRun = f;
            for (ConcurrentHashMap.Node<K,V> p = f.next; p != null; p = p.next) {
                int b = p.hash & n;
                if (b != runBit) {
                    runBit = b;
                    lastRun = p;
                }
            }
            if (runBit == 0) {
                ln = lastRun;
                hn = null;
            }
            else {
                hn = lastRun;
                ln = null;
            }
            for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) {
                int ph = p.hash; K pk = p.key; V pv = p.val;
                if ((ph & n) == 0)
                    ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln);
                else
                    hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn);
            }
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
        }
先看下链表的扩容,假设原table的如下:


按 hash&n 分为蓝红两种节点,当前f是14号桶。那么14号的扩容过程如下:




1、通过遍历链表,记录runBit和lastRun,分别为1和节点6,所以设置hn为节点6,ln为null;
2、重新遍历链表,以lastRun节点为终止条件,根据第X位的值分别构造ln链表和hn链表:
	for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) {
                int ph = p.hash; K pk = p.key; V pv = p.val;
                if ((ph & n) == 0)
                    ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln);
                else
                    hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn);
            }

ln链:和原来链表相比,顺序已经不一样了


hn链:

通过CAS把ln链表设置到新数组的i位置,hn链表设置到i+n的位置;

	    setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
在多线程的时候,如果遍历到fwd的节点,就继续往后遍历;如果是不为空的node节点,就加锁。并把ln链表和hn链表分别设置在nextTable的i和i+n位置上,然后把原table的节点设置为fwd。遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。
接下来分析get方法:
public V get(Object key) {
    ConcurrentHashMap.Node<K,V>[] tab; ConcurrentHashMap.Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        //如果对应位置的桶就是要找的key,直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果是树节点,则查询红黑树
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //如果是链表,就遍历链表查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    //找不到返回null
    return null;
}
查找过程跟HashMap大致相同,只不过是通过tabAt无锁获取节点。之后要么遍历红黑树查找,要么遍历链表查找,找不到返回null。

  • List
List的类图如下:(可在新页签查看更清楚)


  • ArrayList
在新增元素前要进行检查是否需要扩容
public boolean add(E e) {
    //在增加元素之前需要检查是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

具体实现扩容的代码:
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //相当于原来的3/2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

容量扩展成原来的3/2,并且检查上下限,没有超过就把原先的数据复制到新的数组中。
  • LinkedList
LinkedList添加元素比较简单,就是直接往链表后面添加
public boolean add(E e) {
    linkLast(e);
    return true;
}

添加过程中记录modCount
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
比较ArrayList与LinkedList:ArrayList相比LinkedList查询速度比较快,但是不适合频繁添加删除元素。LinkedList则刚好相反,适合频繁添加删除元素,但是查询速度较慢。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值