zoukankan      html  css  js  c++  java
  • 各种集合、对象初始创建默认大小

    一、字符串类别(只详细说了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

                      --SortedSet接口

                            --TreeSet实现类

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

      CopyOnWriteArrayList 在读写的时候使用ReentrantLock来保证线程安全,为什么却说CopyOnWriteArrayList 非绝对线程安全啦?原因在于CopyOnWriteArrayList在于为保证读写效率,在写和修改的时候可以进行数据的读取,那么在数据删除的时候就会存在线程安全(报错)问题,当数据在进行删除的时候,同时在读取该数据,删除完成的瞬间,然后读数据发生,就会报错:数组下标越界。可以参考:https://www.jianshu.com/p/fc0ee3aaf2df

    关于更多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要低。

    特此说明:

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

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

     
  • 相关阅读:
    Java实现 蓝桥杯VIP 算法提高 排队打水问题
    Java实现 蓝桥杯VIP 算法提高 排队打水问题
    Java实现 蓝桥杯VIP 算法提高 排队打水问题
    Java实现 蓝桥杯VIP 算法提高 特殊的质数肋骨
    Java实现 蓝桥杯VIP 算法提高 特殊的质数肋骨
    Java实现 蓝桥杯VIP 算法提高 特殊的质数肋骨
    Java实现 蓝桥杯VIP 算法提高 特殊的质数肋骨
    现在使用控件, 更喜欢继承(覆盖控件已有的函数,很奇怪的一种使用方式)
    Controls 属性与继承 TShape 类的小练习(使用TShape可以解决很多图形问题)
    QT创建窗口程序、消息循环和WinMain函数(为主线程建立了一个QEventLoop,并执行exec函数)
  • 原文地址:https://www.cnblogs.com/yanzige/p/12599505.html
Copyright © 2011-2022 走看看