各种集合、对象的对比记忆

一、字符串类别(只详细说了StringBuffer)

StringBuffer

  1、StringBuffer为线程安全的类,所有方法都使用synchronized修饰(如:public synchronized int length() {return count;})。StringBuffer的构造器有4种,底层为创建指定大小的char数组(JDK8及以前,JDK9开始将char数组修改为了byte数组)

    注:JDK9主要是因为String的底层从char数组修改为了byte数组,所以导致以String为基础的StringBuffer和StringBuilder的底层都从char数组修改为了byte数组,动机:经过大数据统计,在创建String()对象的时候,堆中的String绝大多数是拉丁字母(ASCII表0-127的字母符号),使用byte(8位)数组即可满足,如果使用char(16位)数组,那么将浪费一半的空间。

  如下代码还是按JDK8的原来来做分析

  1)、无参的构造器,默认大小为16

    public StringBuffer() {
        super(16);
    }

  其中父类为AbstractStringBuilder,构造器为:

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

 

  2)、指定大小的构造器

    /**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the <code>capacity</code>
     *               argument is less than <code>0</code>.
     */
    public StringBuffer(int capacity) {
        super(capacity);
    }

 

   3)、指定字符的构造器,底层char数组长度为字符串长度+16(及最小16)

    /**
     * Constructs a string buffer initialized to the contents of the
     * specified string. The initial capacity of the string buffer is
     * <code>16</code> plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     * @exception NullPointerException if <code>str</code> is <code>null</code>
     */
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

 

   4)、指定CharSequence 的构造器,底层char数组长度为CharSequence串长度+16(及最小16)

    /**
     * Constructs a string buffer that contains the same characters
     * as the specified <code>CharSequence</code>. The initial capacity of
     * the string buffer is <code>16</code> plus the length of the
     * <code>CharSequence</code> argument.
     * <p>
     * If the length of the specified <code>CharSequence</code> is
     * less than or equal to zero, then an empty buffer of capacity
     * <code>16</code> is returned.
     *
     * @param      seq   the sequence to copy.
     * @exception NullPointerException if <code>seq</code> is <code>null</code>
     * @since 1.5
     */
    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

 

  2、StringBuffer和StringBuilder的扩容,两者的append(String value)都是使用父类AbstractStringBuilder的append方法:

  public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }

  父类中append的方法(下面的count表示当前数组已经存有数据的个数)

  public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

  在存储数据的时候会检查ensureCapacityInternal()当前数组的容量

  /**
     * This method has the same contract as ensureCapacity, but is
     * never synchronized.
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

 

   如果当前数据已有值的长度加上需要append的值的长度大于当前数组的长度(value.length表示当前数组的总长度),那么就会扩容。扩容后的大小为当前的2倍加2

    /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

  在扩容过程中,扩容后的长度如果还是没有当前需要存入新字符后的长度大,那么直接使用当前需要的长度。然后下一步在判断是否已经超过了int的最大长度(2的31次方-1,最大正int整数) ,超过该长度就会变成一个负整数,所以是通过<0来判断,如果超过最大int正整数,那么就报内存溢出。否则则将旧数组的内容复制到扩容后新数组中来。

 

总结:

  1)同类型的StringBuilder和StringBuffer的实现原理一样,其父类都是AbstractStringBuilder。StringBuffer是线程安全的,StringBuilder是JDK 1.5新增的,其功能和StringBuffer类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。

  2)String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象。所以尽量不要对String进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能。StringBuffer是对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,我们建议使用StringBuffer(线程安全)。

 

二、Map集合类别

HashTable 

  线程安全,默认长度为11,扩容为2*length+1,及在默认长度情况下,第一次扩容长度为23,第二次扩容长度为47。

  源码解读略。

 


 HashMap 

  非线程安全,默认长度为16,扩容为当前数组长度的2倍。

  具体扩容可以参考我另一篇文章:https://www.cnblogs.com/yanzige/p/8392142.html

 


 TreeMap

  非线程安全。继承于AbstractMap,基于红黑树(Red-Black tree)实现。其映射根据键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。其基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。TreeMap底层是树(红黑树)形结构的Enrty链表(不是单链的链表,因为每一个Entry位置存在则他的父节点、左边子节点和右边子节点,所以我将它称为树形链表结构),所以没有默认长度,也没有扩容机制,新增一个数据,找到他key对应在树形结构的位置,直接添加上去。

  首先看TreeMap中的Entry数据结构,key和value是为了存储当前数据,left存小于当前节点的Entry对象(大于小于使用的都是两个对象的key进行比较的,比较方式下面讲),Right存大于当前节点的Entry对象,parent存父节点的Entry信息。

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
    
        // 下面省略了Entry里面的一些方法,只看上面的Entry属性

  TreeMap有4中类型的构造器(由构造器可以看出,TreeMap支持两种类型的排序,一种是自然顺序的排序,另外一种就是制定比较器的排序)

  说明:自然顺序的排序原理,如果key是自然数,则按照自然数的大小排序,如果key是对象,则根据key的hashcode进行排序

    // 无参构造器,默认排序比较器为空,存入数据的顺序则按照自然顺序
    public TreeMap() {
        comparator = null;
    }

    /**
     * 指定存放顺序的比较器
     */
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    /**
     * 指定了无序Map集合的构造器,将传入的Map集合通过构造器放到TreeMap中
     */
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

    /**
     * 指定了有序Map的构造器,TreeMap初始化的时候使用指定的有序Map的比较器,并将传入的Map集合存放到TreeMap中
     */
    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

 

下面查看put方法(默认不可以键值为空):

注:关于键和值是否可以传入空值参考:https://blog.csdn.net/u012156116/article/details/81073570

    public V put(K key, V value) {        
        Entry<K,V> t = root;
        // 第一次存值走该处,注意存储完后直接返回,表明根节点为默认黑,如果不是第一次看下面代码需要校验节点颜色fixAfterInsertion(e);
        if (t == null) {
            // TreeMap中是否允许存入空值,需要看我们的构造器中传入的比较器是否有对空值进行处理,如果没有处理,使用默认比较器或者有比较器但是没有对空值进行处理,都会报空指针异常
            compare(key, key); // type (and possibly null) check
            // root存储的是树形结构的根Entry对象,故他的parent为空
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        // 实现了比较器的使用实现后的比较器,默认的如Interger,String等类都实现了,如果是自定义类如Person/Car/Dog这种需要自己去(1)实现Comparable接口,然后重新compareTo方法或者(2)实现Comparator接口,重新compare方法
        if (cpr != null) {
            // 从根节点循环向左或者向右比较,直到找到该数据在整个树上的位置
            do {
                // 比较的时候从根节点开始比较,向左或者向右方向比较,直到t.left节点或者t.right节点为空(注:下面比较中,除开向左或者向右(这是key不同),如果key相同,则直接将新value覆盖旧value,并将旧value返回)
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 没有实现比较器,则使用自然排序方法进行比较
        else {
            if (key == null)
                throw new NullPointerException();
               @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
     // 将该key,value放到节点中,并将该节点的父节点信息存入节点中 Entry
<K,V> e = new Entry<>(key, value, parent); // 找到位置后,需要将它的父节点的左节点或者右节点进行更新,使整个树在加入新节点后连为一体 if (cmp < 0) parent.left = e; else parent.right = e;
     // 修复树的平衡 fixAfterInsertion(e);
     // 跟新map大小 size
++; modCount++; return null; }

关于TreeMap的键值是否可以为空的总结:

  1)当未实现 Comparator 接口时,key 不可以为null,否则抛 NullPointerException 异常;

  2)当实现 Comparator 接口时,若未对 null 情况进行判断,则可能抛 NullPointerException 异常。如果针对null情况实现了,可以存入,但是却不能正常使用get()访问,只能通过遍历去访问。

在新增put()数据的时候,fixAfterInsertion(e)个方法会对红黑树进行平衡操作,举例:第一次放15,第二次放14,13....1,如果不进行平衡的话就成了一条单链表(向左的单向链表),如果需要查询的话就会遍历整个链表。

  private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;

        //平衡树调整的条件:当前存入Entry非空,当前节点非根节点,上一个节点为红色
        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

通过rotateLeft()左旋和rotateRight()右旋,来保证树的平衡性,以减少树的深度,来保证查询的效率。

总结:TreeMap与HashMap的区别:

  数据结构不同:

    HashMap是基于哈希表,由 数组+链表+红黑树 构成。

    TreeMap是基于红黑树实现。

  存储方式不同:

    HashMap是通过key的hashcode对其内容进行快速查找。

    TreeMap中所有的元素都保持着某种固定的顺序。

  排列顺序:

    HashMap存储顺序不固定。

    TreeMap存储顺序固定,可以得到一个有序的结果集。

 


 ConcurrentHashMap 

  线程安全,默认长度为16,扩容为当前数组长度的2倍。

  JDK8源码解读:https://blog.csdn.net/ddxd0406/article/details/81389583

 


 

 Collections.SynchronizedMap

  底层使用的是Map,通过构造器传对应Map的之类,返回你线程安全的Map子类集合对象,该方式其实就是各个操作都使用synchronized进行加锁,来保证了线程安全。

    private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        // 底层还是使用Map对象
        private final Map<K,V> m;     // Backing Map

        // 使用Object mutex作为锁对象
        final Object mutex;        // Object on which to synchronize

        // 同构构造器,传Map子类,实现对应Map子类的安全类
        SynchronizedMap(Map<K,V> m) {
            this.m = Objects.requireNonNull(m);
            mutex = this;
        }

        SynchronizedMap(Map<K,V> m, Object mutex) {
            this.m = m;
            this.mutex = mutex;
        }
        
        public int size() {
            // 所有操作都进行代码块上锁
            synchronized (mutex) {return m.size();}
        }

        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
    }    
    

 


 LinkedHashMap 

  非线程安全,父类是HashMap,所以默认长度为16,负载引子为0.75,扩容为原来长度的2倍。

  继承于HashMap,故很多方法都和父类一样,底层也是一个维护了一个Entry数组,每一个位置有一个Entry链表,但是需要注意的是,每一个位置的Entry对象(因为LinkedHashMap中了的Entry继承于HashMap的Entry,但是在继承过程中新增了两个Entry对象:Entry<K,V> before, after; 这两个对象分别用来记录每个Entry对象的上一个和下一个Entry对象)会保存插入前后的其Entry对象的位置(该位置可能不是同一个数组下标的Entry),通过这种方式,可以将不同数据的Entry链表串成一整个链表,也就是一个双向链表。

  注:下面Entry的源码是使用的JDK8,故LinkedHashMap中的Entry继承HashMap中的Node对象,其实可以直接将JDK8中Node对象理解为JDK7中的Entry对象(两者结构完全一样,都是由final int hash;final K key;V value;Node<K,V> next;组成,只是名字叫法不同而已),所以文字统称Entry。

  /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

 

  由上面的Entry代码可以看到,LinkedHashMap中的Entry除了和HashMap中的Entry一样的int hash, K key, V value, Node<K,V> next(用来记录同一个数组下标的链表信息)四个对象,还多了Entry<K,V> before, after两个前后的Entry,用来记录新增数据的前后Entry(新增数据时,可能hash冲突在同一个链表上新增,也可能在其他数据位置或者数组其他位置的链表上新增)。通过该方式维护了一个双向的链表。

  相对HashMap,因为LinkedHashMap将数组下标每一个位置的链表链接起来了,维护了一个双向的链表,故LinkedHashMap可以记录数据的插入顺序,而HashMap则不能记录插入顺序。

 总结:

  1、HashMap、TreeMap 和LinkedHashMap 是非线程安全的。HashMap无序的Map集合,TreeMap 有序(这个顺序有key值决定)的Map集合,LinkedHashMap 记录了插入顺序。

  2、HashTable 是线程安全的,但是加锁的方式是在方法上加synchronized,及对整个需要操作的对象HashTable加锁,可以理解为全表锁,只有有任意一个线程访问HashTable对象,其他线程都需要等待。故并发效率很低。

  3、HashMap、TreeMap 和ConcurrentHashMap 继承于AbstractMap 类,HashTable 是继承于Dictionary 类,而LinkedHashMap 是继承于HashMap 类,他们5者都实现了Map接口。

  4、LinkedHashMap 继承于HashMap,底层实现原理和基本操作和HashMap 一样,只是多维护了一个双向的链表,使得LinkedHashMap 能够记录数据的插入顺序。

  5、ConcurrentHashMap默认长度也为16,扩容方式也和HashMap一样。

    1)ConcurrentHashMap在JDK7中使用分段锁,相比于HashTable的全表锁,多个线程访问ConcurrentHashMap对象,只要不是同一段的数据,可以多线程同时操作。

    2)ConcurrentHashMap在JDK8中使用原子操作(CAS)和代码块synchronized,故加锁的粒度更细,并发效果更好。

 

三、Set集合类

HashSet 

  非线程安全,父类AbstractSet,底层使用HashMap实现,所有初始默认大小为16,负载因子为0.75,扩容为原来长度的两倍

  看HashSet源码就知道,HashSet存值的时候其实就是利用的HashMap的key,故HashMap中key的特性正好对应HashSet的特性,如:HashMap中的key值不能重复,HashSet中对象不能重复,HashMap中key值可以为空,HashSet中可以有空值等。

    /**
     * 无参构造器
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * 指定集合上线的的构造器
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    /**
     * 指定初始容量和负载因子的构造器
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * 指定初始容量的构造器
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     * 非Public,为子类LinkedHashMap提供
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

 

从构造器我们可以发现,实例化HashSet,其实底层就是创建了HashMap,添加元素的时候就是往key中存值

    private static final Object PRESENT = new Object();
    //所有的value都为static final修饰的固定值Object对象 
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

 


 LinkedHashSet

  非线程安全,父类是HashSet,底层实现原理是LinkedHashMap。所以特性和LinkedHashMap一样。初始默认大小为16,负载因子为0.75,扩容为原来长度的2倍。底层也维护了一个双向的链表,故能记录插入数据的顺序。

  LinkedHashSet在实例化的时候就是使用HashSet中的默认修饰符的构造器构造了一个LinkedHashMap。

 


 TreeSet

  非线程安全,父类是AbstractSet,底层是用TreeMap实现,没有初始大小和扩容机制。

总结:

      --Set接口

            --AbstractSet抽象类

      --TreeSet实现类(同时通过实现接口的形式实现了SortedSet接口

      --HashSet实现类

                    --LinkedHashSet实现类

  1、HashSet和TreeSet都是继承于AbstractSet类,HashSet底层为HashMap实现,TreeSet底层为TreeMap实现。

  2、LinkedHashSet继承HashSet,底层为LinkedHashMap实现。

  3、HashSet、TreeSet和LinkedHashSet都是非线程安全的,HashSet是无序Set集合,TreeSet是有序Set集合,LinkedHashSet记录的插入顺序。

 

四、List集合类别

ArrayList 

  非线程安全,除指定大小外默认长度为10,扩容为上次长度的1.5倍。若第一次创建对象使用无参构造器,则在put的时候将空数组(0)扩容到默认长度10。

  1、ArrayList有三种构造器

  0)首先看类中定义的变量

    /**
     * 初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 初始化数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * ArrayList对象存放数据的对象 底层为Object数组
     */
    private transient Object[] elementData;

    /**
     * ArrayList存放数据量的大小
     */
    private int size;

  

  1)无参构造器

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    } 

  在无参构造器创建对象的时候,并没有指定容器大小,指定容器大小是在第一次add()的时候

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

  在ensureCapacityInternal(size + 1)方法中,放入数据的时候,会检查该数组(ArrayList对象是否为初始化数组),如果为初始化对象的话,则将当前长度+1后和默认长度10比较,取最大值,然后执行ensureExplicitCapacity(minCapacity)

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

  进入ensureExplicitCapacity()会比较当前大小和elementData.length大小,如果当前大于ArrayList中的数据长度,则进行扩容。第一次进来肯定扩容grow(minCapacity),因为当前elementData.length的值为0,所以第一次进来肯定要扩容。

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

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

  下面为扩容的代码,默认将新数组长度扩充到原数组长度的1.5倍,下面代码使用原来的长度加上原长度右移1位(>>1),即新长度=(1+0.5)原长度,并将原数组数据copy到新的数组中。如果为第一次新增add()数据的时候,oldCapacity 为0,则newCapacity = minCapacity即newCapacity =10。所以第一次扩容扩将原来长度为0的Object数组扩容到长度为10的Object数组。

注:右移一位的时候,如果末尾为0即偶数,右移一位减半,如果末尾为1即奇数,则右移一位相当于舍弃了1,然后在减半

举例:  原长度为10,1010(原长度)+101(右移一位)=1111(10+5) 扩容后变成15

       继续扩容 1111+111(右移一位舍弃了最右的1)=10110(15+7) 扩容后变成22

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

  在上面扩容过程中,并对长度进行校验,判断扩容的长度是否操作最大数组长度和最大整型(int)正整数长度,及2的31次方-1

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 最大整型正整数 2的31次方-1,16进制为0x7fffffff; 
     */
    public static final int   MAX_VALUE = 0x7fffffff; 
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

  

  2)指定容量的构造器

    /**
     * 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
     */
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

 

  3)指定上限的集合构造器

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

总结:

  1)ArrayList底层是一个Object数组,在初始化对象的时候,如果使用无参构造器,则默认大小为10,在第一次添加数据的时候设置初始大小。

  2)ArrayList扩容就是将原来的数组的数据复制到新的数组中,除第一次外,每次扩容都变成之前容量的1.5倍

  3)ArrayList底层是数组,所以数据存储是连续的,所以它们支持用下标来访问元素,索引数据的速度比较快

 


 Vector

  线程安全,该类基本上所有的方法都是使用synchronized修饰(如:public synchronized boolean isEmpty() {return elementCount == 0;}),故线程安全。和ArrayList一样继承于AbstractList<E>类,无参构造器默认容量大小为10,默认扩容为原来的2倍,Vector构造器有4个,一个无参构造器,一个指定容量大小的构造器,一个指定容量大小和超过存储量后按指定大小扩容的构造器,还有一个指定上限的集合构造器。

  1、前三个构造器底层都是调用两个参数的构造器:一个指定容量大小和超过存储量后按指定大小扩容的构造器

    /**
     * Constructs an empty vector with the specified initial capacity and
     * capacity increment.
     *
     * @param   initialCapacity     the initial capacity of the vector
     * @param   capacityIncrement   the amount by which the capacity is
     *                              increased when the vector overflows
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

    /**
     * Constructs an empty vector with the specified initial capacity and
     * with its capacity increment equal to zero.
     *
     * @param   initialCapacity   the initial capacity of the vector
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    /**
     * Constructs an empty vector so that its internal data array
     * has size {@code 10} and its standard capacity increment is
     * zero.
     */
    public Vector() {
        this(10);
    }

  2、指定上限的集合构造器

    /**
     * Constructs a vector containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this
     *       vector
     * @throws NullPointerException if the specified collection is null
     * @since   1.2
     */
    public Vector(Collection<? extends E> c) {
        elementData = c.toArray();
        elementCount = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }

  3、在扩容的时候,如果没有使用指定扩容大小,即没有使用上面构造器public Vector(int initialCapacity, int capacityIncrement)初始化对象,那么在扩容的时候直接扩容为原来的2倍

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

 


 LinkedList 

  非线程安全,LinkedList 底层维护了一个双向链表,该链表有一个前向节点Node<E> first和一个后向节点Node<E> last。每次新增数据add()的时候,会在最后一个节点last后添加新节点,并且把刚添加的新节点赋值给last。如果为第一次新增,那么该新增的节点既是first节点,又是last节点。

  注:Node<E>节点里面会存一个前节点的数据、后节点的数据以及自身的值。每次新增节点后,新增节点的上一个节点数据则为新增前最末节点,新增节点的后一个节点则为空,同时会将当前新增节点的数据存入上一个节点中的下节点信息中(尾插法,放入链表的尾部)。这样就串为了一整个链表,链表中除了first和last节点外,每一个节点都包含上一个节点和下一个节点的信息。因为Node节点中存有前一节点和后一节点的数据,所以只要打开任意一个Node节点数据,向前可以查看该节点前的所有数据,向后可以查看该节点后的所有数据。如果该节点处于头部,那么向后可以查看链表的所有数据,如果该节点处于尾部,向前可以查看该链表的所有数据。

  如下图处于链表尾部,该节点下一个节点为空,但是向前可以查看所有数据:

 

  1、add()添加数据的方法

  add()添加数据使用方法linkLast(),将该链表中最末节点数据存入当前Node节点的前一个节点信息,当前Node节点的下一个节点为空,并将当前Node替换为最末节点。从添加流程代码可以看出来,LinkedList 在添加的时候并没有对重复数据进行处理,所以LinkedList 中是可以存储重复数据的。添加完成后,最后将链表长度数据size大小加1,然后将操作次数modCount数据加1.

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    /**
     * Links e as last element.
     */
    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++;
    }

 

  2、下面我们看几个remove()方法

  public E remove() {
        return removeFirst();
    }

    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    } 

  remove方法通过调用unlinkFirst(f)和unlinkLast(l)进行删除,无论是删除头部还是尾部,或者是中间位置的元素,都需要把链表前后位置进行连接。

    /**
     * 删除头部数据first,先将该头部数据fist的下一个数据为新的first节点,然后将first节点中的prev节点设置为空,
   * 再将原firt头部节点数据删除,再将原头部节点的next数据置空。最后将链表长度数据size减一,将操作次数modCount加一
*/ private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } /** * 删除尾部数据last,先将该尾部节点数据的prev取出来,并且将取出的上一个节点设置为last,在设置的同时将其中的next设置为空,
   * 然后将该尾部数据以及该节点中的prev置空。最后将链表长度数据size减一和操作次数modCount加一
*/ private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; } /** * 删除中间节点数据,取出该节点的next节点和prev节点数据,然后将前一个节点prev的next设置为删除数据中的next,将后一个节点的prev节点数据的next数据设置为即将删除数据的next数据,
   * 然后将被删除的数据的所有数据都置空,最后将链表长度数据size减一和操作次数modCount加一
*/ E unlink(Node<E> x) { // assert x != null; 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; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }

 


 CopyOnWriteArrayList 

  线程安全,在并发包concurrent包下,使用了ReentrantLock锁来保证读写数据安全,同一时刻只能有一个线程进行写操作,但是可以有多个线程进行并发读数据,同时通过关键字volatile来保证读数据时候的可见性,及每次数据在主内存中修改后,工作内存中读取的数据会更新为最新的数据,来保证读数据的线程安全。CopyOnWriteArrayList没有初始容量大小。

底层也是通过维护数组来存储数据,该数组使用volatile修饰的,保证数据的可见性

private transient volatile Object[] array;

  查看add()方法源码

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 使用锁来保证线程安全,每次添加的时候只会有一个线程执行
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 将原数组的值赋值到一个新的数组,同时新数字的长度 +1 用于存放新加入的值
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 放入新值
            newElements[len] = e;
            // 将新数组替换原数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

  从上面可以看出来,CopyOnWriteArrayList在添加数据的时候通过ReentrantLock将数据进行加锁(锁的对象是this,当前copyOnWriteArrayList对象),然后将原数组进行复制,所有的写操作(增、删、改,还有clear()、sort()等)都是在新的数组上操作的,当写操作完成,在将新的数组赋值给原来的数组,这样就可以使数据串行化执行来保证写数据的安全性。但是在读的时候,如get(), indexOf(), contains(), size() 等方法不存在线程安全的情况下没有加锁,直接读取的原数组,这样就可以保证并发效率。

  总结:

    (1)CopyOnWriteArrayList 内部也是通过数组来实现的,在写的时候使用ReentrantLock来保证线程安全,会Copy原数组到一个新的数组中(如其名字CopyOnWrite),的时候在新的数组上操作。读取数据的时候不进行加锁,并且还是读取原数组来保证读的并发效率。

    (2)写的时候会加锁,保证线程安全,防止出现并发写入数据丢失的问题

    (3)写操作结束会把原数组引用地址指向新的数组

    (4)CopyOnWriteArrayList允许写操作的时候读取数据,大大的提高了读的性能,因此适合读多写少的应用场景,但CopyOnWriteArrayList会比较占用内存,同时可能存在读取的数据不是最新的情况,所以不适合性能要求很高的场景。

关于更多CopyOnWriteArrayList的相关解读可以查考:https://www.cnblogs.com/myseries/p/10877420.html

 

总结,关于ArrayList,Vector,LinkedList和CopyOnWriteArrayList 的对比:

  1、 ArrayList和Vector底层原理一样,都是使用Object数据,都是继承于AbstractList<E>类,未指定初始容量的话默认都为10,扩容的时候ArrayList为原来的1.5倍,而Vector扩容为原来的2倍。其次ArrayList为线程不安全的类,而Vector为线程安全的。因为Vector使用synchronize来保证线程安全,所以在效率上和ArrayList相比要低。

  2、CopyOnWriteArrayList底层也为数组,但是没有初始容量。CopyOnWriteArrayList线程安全,和线程安全的Vector相比,因为CopyOnWriteArrayList使用ReentrantLock锁对指定代码块,能满足多线程读的需求,并且在写的过程中可以进行读数据,相比于Vector类中的synchronize对整个方法进行加锁的方式,并发效率更高。

  3、LinkedList底层使用一个双向链表,和上面两个原理不一样,是线程不安全的。因为LinkedList底层是链表,所以LinkedList在新增和删除数据的时候效率很高,但是在查询的时候需要依次遍历,所以查询效率较低。相反,ArrayList和Vector底层使用数组,内存地址是连续的,所以在查询的时候效率很高,但是在新增和删除的时候设计扩容(数组的复制),所以效率相比LinkedList要低。

 

特此说明:

  上面所有类的相关特性对比描述,都是根据源码进行总结的,当然在总结之前也有看过网上一些资料,所以可能存在总结不完整或者总结有误的地方,还请大牛和心细的网友批评指正。此文章目的主要通过对比总结,便于记忆,并没有对每一个集合类进行深入完整的剖析。如果想要深入理解某个类底层原理和逻辑,还是需要查看源码和网上相应的说明文章。

  此外,如果问题也可以留言交流....

 
posted @ 2020-04-01 16:06  颜子歌  阅读(3210)  评论(0编辑  收藏  举报