Java常用集合类

Java常用集合类

Java中的集合分别在Collection、Map接口下

Collection接口

在这里插入图片描述
可以看到我们常用的List,Queue,Set

List

在这里插入图片描述
常用的类有ArrayList, Vector, LinkenList

ArrayList
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    
    private static final int DEFAULT_CAPACITY = 10;
    
    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    transient Object[] elementData; // non-private to simplify nested class access

    private int size;

    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */

可以看到,DEFAULT_CAPACITY = 10;
这是默认容量,初始容量是多少呢?

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

得到答案 : 0

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

可以指定初始容量。

接下来我们看看扩容的情况

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

如果是无参方法new的ArrayList,那么第一次扩容数组长度是default,刚才看到了,是10.

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

看,数组长度不够了,需要grow(minCapacity)。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }

newCapacity = oldCapacity + (oldCapacity >> 1);
我们可以得知,无参情况下数扩容情况是0->10->15->22->33->49…

ok, ArrayList暂时到此为止。

Vector

众所周知,Vector是线程安全的,那么他是怎么实现的呢?让我们来看看。

    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

有答案了,使用了synchronized关键字同步。单线程情况下,效率肯定不如ArrayList,至少现在为止我几乎没有使用过Vector。据了解,并发情况下,CopyAndWriteArrayList使用较多,原因可以在后续学习中探究。

LinkedList
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */

成员变量只有三个,可以看出这是一个双向链表。

对比

ArrayList,动态数组,增删效率低,查询效率高。
LinkedList,双向链表,增删效率高,查询效率低。
Vector,应该是在ArrayList出现之前比较常用,线程安全。

Set

在这里插入图片描述
比较常用的是HashSet, TreeSet

HeshSet

看看成员变量

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

底层应该是HashMap, 继续往下看吧。

    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

看来是这样,意思就是Hashmap的Value不用,不看了,一会看HashMap吧。

TreeSet

呃,怎么没找到,哪去了。。。
在这里插入图片描述

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{

呃,NavigableSet,这是什么?
在这里插入图片描述
天呐、、

public interface NavigableSet<E> extends SortedSet<E> {
public interface SortedSet<E> extends Set<E> { 

意思是有序集合。据说这个扩展接口1.2的时候就有,到了1.6才实现。
在这里插入图片描述
又查了一下,呃,就先也理解成有序的吧。

NavigableSet声明了什么方法呢?

    E lower(E e);
    E floor(E e);
    E ceiling(E e);
    E higher(E e);
    E pollFirst();
    E pollLast();
    Iterator<E> iterator();
    NavigableSet<E> descendingSet();
    Iterator<E> descendingIterator();
    NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                           E toElement,   boolean toInclusive);
    NavigableSet<E> headSet(E toElement, boolean inclusive);
    NavigableSet<E> tailSet(E fromElement, boolean inclusive);
    SortedSet<E> headSet(E toElement);
    SortedSet<E> tailSet(E fromElement);

注释就不贴了,太长了
floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null;

ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回null;

higher(E e)方法用来返回最小元素在这组严格大于给定的元素,或者null,如果不存在这样的元素。

lower…

以上解释是copy其他博客的,看着好累,举例子吧,我的猜测是这样的,如果集合中有1,2,3,4,5这几个元素,那么
floor(3) = 3
ceiling(3) = 3
lower(3) = 2
higher(3) = 4

读到这里是不是恍然大悟,然而以上是我猜的,我确认一下。
在这里插入图片描述
No problem!剩下的方法都比较好理解了,开始看TreeSet吧。

接下来老规矩,看成员变量。

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    /**
     * The backing map.
     */
    private transient NavigableMap<E,Object> m;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

就俩,看来是基于TreeSet实现的啦。

LinkedHashSet

这个东西,应该是双向链表和哈希实现的,可以用它实现LRU。看看源码吧。

package java.util;
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;
    
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    public LinkedHashSet() {
        super(16, .75f, true);
    }

    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}

这啥也没有啊,这怎么用的双向链表呢?

本列文虎克终于找到了答案。

    public LinkedHashSet() {
        super(16, .75f, true);
    }

点进super

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

好吧,Set应该都是基于map实现的。那可以去看Map了。

Map接口

在这里插入图片描述
这一大堆,要挑重点看。

HashMap

这个我平时是最常用的。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;
    
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

初始容量16,加载因子默认0.75。0.75的原因是Hash冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

new一个HashMap对象最终要调用上面的构造方法。

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

tableSizeFor(int cap) 这个方法用来计算初始容量,逻辑是把入参的二进制数最高位以后都置1,再加一。保证容量是2的正整数次幂(从16开始),这样做的好处是

p = tab[i = (n - 1) & hash] 此时等价于hash % n,位运算更为高效。除此之外,这样可以使的是数据分布更均衡,举个例子,如果cap = 17, 那么计算index时,h & 10000,这样的话有些位置永远不会存放数据。

HashMap底层维护了一个Node数组,为啥叫Node呢,HashMap发生碰撞以后会将新元素以链表头节点的形式插到这个桶上。

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

上面代码是put操作,梳理一下基本逻辑。
计算index,如果该位置为null,插入。
如果不为空,判断该位置是否为树节点,如果是,插入节点。如果不是,插入链表,如果链表长度达到了8,那么转换成红黑树结构。
那么为什么是8呢?源码中有写,这里不再深究了,查了一下,与泊松分布有关,8的时候一般情况下效率比较高。
那么HashMap就先到这里吧。

LinkedHashMap
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
   /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

继承了HashMap, 维护一个head,一个tail。其他因该都是常规方法吧。

TreeMap

这个应该是红黑树实现,大名鼎鼎的红黑树,能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。源码以后有空再读吧。

源码摘自jdk1.8

Over~
created by lcy on 2020-09-21

并发集合

CopyOnWriteArrayList
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

实现了List接口

/** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

CopyOnWriteArrayList是通过“volatile数组”来保存数据的。
一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入

与ArrayList类似,看一下add和set方法

	    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

涉及更新的操作加了锁,可以保证线程安全。

CopyOnWriteArraySet
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

内部维护了一个CopyOnWriteArrayList

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

add方法

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

每一次插入最坏情况是O(n)的

private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // // 如果快照与刚获取的数组不一致说明有修改,重新判断数组中是否有e
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
ConcurrentHashMap

ConcurrentHashMap主要的作用是支持多线程的读写,可以代替HashTable,用法与HashMap基本相同。

jdk1.8中ConcurrentHashMap的实现与1.7有很大差别,放弃了使用分段锁。与HashMap类似,使用数组+Hash+红黑树实现,那么1.8中的ConcurrentHashMap在更新数据时是如何实现同步的呢?我们来看一下

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 (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果table为空,初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果对应位置桶为空,CAS插入尾部
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //f.hash = 1 协助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //开始同步
                synchronized (f) {
                	//找到table表下标为i的节点
                	// 再次检查,即double check 避免进入同步块之前,链表被修改了,不通过继续循环
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            //遍历节点
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //put元素,记录binCount
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果节点是红黑树
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            将节点转换成TreeBin,插入
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                    	//如果binCount > 8树化,但是真正把这个桶转换红黑树结构还要数组长度大于64
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

可以看出,ConcurrentHashMap在执行put方法时,如果对应位置没有节点,通过CAS方式添加Node,如果对应位置有节点,使用synchronized进行同步。使用synchronized的原因是如果该key对应的节点所在的链表已经存在的情况下,可以通过Unsafe的tabAt方法基于volatile获取到该链表最新的头节点,但是需要通过遍历该链表来判断该节点是否存在,如果不使用synchronized对链表头结点进行加锁,则在遍历过程中,其他线程可能会添加这个节点,导致重复添加的并发问题。故通过synchronized锁住链表头结点的方式,保证任何时候只存在一个线程对该链表进行更新操作。

扩容原理(待补充)

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //检查当前集合元素个数 s 是否达到扩容阈值 sizeCtl
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值