Java中的集合類(一)

1. 集合框架圖

Java中的集合是用于存儲對象的工具類容器挂据,它實現(xiàn)了常用的數(shù)據(jù)結(jié)構(gòu)以清,提供了一系列公開的方法用于增加、刪除崎逃、修改掷倔、查找和遍歷數(shù)據(jù),降低開發(fā)成本个绍。集合種類非常多勒葱,形成了一個比較經(jīng)典的繼承關系數(shù)勺像,稱為Java集合框架圖,如下圖所示错森∫骰拢框架圖主要分為兩類:第一類按照單個元素存儲的Collection,在繼承樹中Set和List都實現(xiàn)了Collection接口涩维;第二類是按照key-value村村的Map殃姓。以上兩類集合體系,無論在數(shù)據(jù)存儲還是遍歷瓦阐,都存在非常大的差別蜗侈。


Java集合框架圖.png

??在集合框架圖中,紅色代表接口睡蟋,藍色代表抽象類踏幻,綠色代表并發(fā)包中的類,灰色代表早期線程安全的類(基本已棄用)戳杀「妹妫可以看到,與Collection相關的4條線分別是List信卡、Set隔缀、Queue、Map傍菇,它們的子類會映射到數(shù)據(jù)結(jié)構(gòu)中的表猾瘸、數(shù)、哈希等丢习。

  • List集合
    ??List集合是線性數(shù)據(jù)結(jié)構(gòu)的主要實現(xiàn)牵触,集合元素通常存在明確的上一個和下一個元素,也存在明確的第一個元素和最后一個元素咐低。List 集合的遍歷結(jié)果是穩(wěn)定的揽思。該體系最常用的是ArrayList 和 LinkedList 兩個集合類。
    ??ArrayList 是容量可以改變的非線程安全集合渊鞋。內(nèi)部實現(xiàn)使用數(shù)組進行存儲绰更,集合擴客時會創(chuàng)建更大的數(shù)組空間,把原有數(shù)據(jù)復制到新數(shù)組中锡宋。ArrayList 支持對元素的快速隨機訪問儡湾,但是插入與刪除時速度通常很慢,因為這個過程很有可能需要移動其它元素执俩。
    ??LinkedList 的本質(zhì)是雙向鏈表徐钠。與 ArrayList 相比,LinkedList 的插入和刪除速更快役首,但是隨機訪問速度則很慢尝丐。測試表明显拜,對于 10萬條的數(shù)據(jù),與 ArrayList相比隨機提取元素時存在數(shù)百倍的差距爹袁。除繼承 AbstractList 抽象類外远荠,LinkedList 還實現(xiàn)了另一個接口 Deque,即 double-ended queue失息。這個接口同時具有隊列和棧的性質(zhì)譬淳。LinkedList 包含3個重要的成員: size、first盹兢、last邻梆。size 是雙向鏈表中節(jié)點的個數(shù),first和last分別指向第一個和最后一個節(jié)點的引用绎秒。LinkedList 的優(yōu)點在于可以將零散的內(nèi)存單元通過附加引用的方式關聯(lián)起來扮叨,形成按鏈路順序查找的線性結(jié)構(gòu)卡骂,內(nèi)存利用率較高管毙。

  • Queue集合
    ??Queue(隊列)是一種先進先出的數(shù)據(jù)結(jié)構(gòu)镜粤,隊列是一種特殊的線性表斋陪,它只許在表的一端進行獲取操作辈挂,在表的另一端進行插入操作抒痒。當隊列中沒有元素時飒责,稱為空隊列惠赫。自從BlockingQueue(阻塞隊列)問世以來把鉴,隊列的地位得到極大的提升在各種高并發(fā)編程場景中,由于其本身 FIFO的特性和阻塞操作的特點儿咱,經(jīng)常被作為Buffer(數(shù)據(jù)緩沖區(qū))使用庭砍。

  • Map集合
    ??Map集合是以Key-Value鍵值對作為存儲元素實現(xiàn)的哈希結(jié)構(gòu),Key 按某種哈函數(shù)計算后是唯一的混埠,Value 則是可以重復的怠缸。Map 類提供三種 Collection 視圖,集合框架圖中钳宪,Map 指向 Collection 的箭頭僅表示兩個類之間的依賴關系揭北。可以使用keySet()查看所有的Key吏颖,使用 values()查看所有的Value搔体,使用entrySet()查看所的鍵值對。最早用于存儲鍵值對的 Hashtable 因為性能瓶頸已經(jīng)被淘頭半醉,而如今廣使用的 HashMap疚俱,線程是不安全的。ConcurrentHashMap 是線程安全的缩多,在JDK8中進行了鎖的大幅度優(yōu)化呆奕,體現(xiàn)出不錯的性能养晋。在多線程并發(fā)場景中,優(yōu)先推薦使用ConcurrentHashMap梁钾,而不是 HashMap绳泉。TreeMap 是 Key 有序的 Map 類集合。

  • Set集合
    ??Set是不允許出現(xiàn)重復元素的集合類型姆泻。Set 體系最常用的是 HashSet圈纺、TreeSe和LinkedHashSet 三個集合類。HashSet 從源碼分析是使用HashMap 來實現(xiàn)的麦射,只是Value固定為一個靜態(tài)對象蛾娶,使用 Key 保證集合元素的唯一性,但它不保證集合元素的順序潜秋。TreeSet也是如此蛔琅,從源碼分析是使用 TreeMap 來實現(xiàn)的,底層為樹結(jié)構(gòu)峻呛,在添加新元素到集合中時罗售,按照某種比較規(guī)則將其插入合適的位置,保證插入后的人仍然是有序的钩述。LinkedHashSet 繼承自 HashSet寨躁,具有 HashSet 的優(yōu)點,內(nèi)部使用鏈表維護了元素插入順序牙勘。

2. 集合初始化

??集合初始化通常進行分配客量职恳、設置特定參數(shù)等相關工作。我們以使用頻率較高為ArayList 和 HashMap 為例方面,簡要說明初始化的相關工作放钦,并解釋為什么在任何情況下,都需要顯式地設定集合容量的初始大小恭金。ArayList是存儲單個元素的順序表結(jié)構(gòu)操禀,HashMap 是存儲 KV 鍵值對的哈希式結(jié)構(gòu)。分析兩者的初始化相關源碼横腿,洞悉它們的容量分配颓屑、參數(shù)設定等相關邏輯,有助于更好地了解集合特性耿焊,提升代碼質(zhì)量揪惦。下面先從 ArrayList 源碼說起:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final int DEFAULT_CAPACITY = 10;
   // 空表的表示方法
    private static final Object[] EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
         // 值大于 0時,根據(jù)構(gòu)造方法的參數(shù)值搀别,忠實地創(chuàng)建一個多大的數(shù)組
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
// 公開的 ada 方法調(diào)用此內(nèi)部私有方法
    private void add(E e, Object[] elementData, int s) {
// 當前數(shù)組能否容納 size+1 的元素丹擎,如果不夠,則調(diào)用grow來擴容
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
//擴容的最小要求,必須容納剛才的元素個數(shù) +1蒂培,注意再愈,newCapacity()
// 方法才是擴容的重點!
    private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }

    private Object[] grow() {
        return grow(size + 1);
    }

    private int newCapacity(int minCapacity) {
        // overflow-conscious code 防止擴容1.5 倍之后,超過 int 的表示范圍(第1處)
        int oldCapacity = elementData.length;
      // JDK6之前擴容 50%或50-1护戳,但是取ceil翎冲,而之后的版本取 Floor (第2處
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
               //無參數(shù)構(gòu)造方法,會在此時分配默認為10 的容量
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
}

??第1處說明:正數(shù)帶符號右移的值肯定是正值媳荒,所以oldCapacity+(oldCapacity>>l)的結(jié)果可能超過int可以表示的最大值抗悍,反而有可能比參數(shù)的 minCapacity 更小,則返回值為(size+1)的minCapacity钳枕。
??第2處說明:如果原始容量是 13缴渊,當新添加一個元素時,依據(jù)程序中的計算方法得出13的二進制數(shù)為 1101鱼炒,隨后右移1位操作后得到二進制數(shù) 110衔沼,即十進制數(shù)6最終擴容的大小計算結(jié)果為 oldCapacitiy +(oldCapacity>>1)= 13+6=19。使用位算主要是基于計算效率的考慮昔瞧。在JDK7之前的公式指蚁,擴容計算方式和結(jié)果為 oldCapacitiy x3÷2+1=13x3÷2+1=20。
??當ArrayList 使用無參構(gòu)造時自晰,默認大小為 10凝化,也就是說在第一次 add 的時候分配為10的容量,后續(xù)的每次擴容都會調(diào)用 Array.copyof方法酬荞,創(chuàng)建新數(shù)組再復制搓劫,可以想象,假如需要將 1000個元素放在 ArrayList中袜蚕,采用默認構(gòu)造方法糟把,需要被動擴容13次才可以究成存。反之牲剃,如果在初始化時便指定了容量new ArrayList(1000),那么在初始化 ArrayList對象的時候就直接分配 1000個儲空間而避免被動擴容和數(shù)組復制的額外開銷雄可。最后凿傅,進一步設想,如果這個值達到更大量級数苫,卻沒有注意初始的容量分配問題聪舒,那么無形中造成的性能損耗是非常大的,甚至導致 0OM 的風險虐急。
??再來看一下HashMap,如果它需要放置1000個元素箱残,同樣沒有設置初始容量大小隨著元素的不斷增加,則需要被動擴客7次才可以完成存儲。擴容時需要重建hash表非常影響性能被辑。在 HashMap 中有兩個比較重要的參數(shù) Capacity 和 Load Factor燎悍,其中Capacity 決定了存儲容量的大小,默認為 16;而 Lod Factor 決定了填充比例-般使用默認的0.75盼理√干剑基于這兩個參數(shù)的乘積,HashMap 內(nèi)部用 threshold 變量表示HashMap中能放入的元素個數(shù)宏怔。HashMap 容量并不會在 new 的時候分配奏路,而是在第一次put 的時候完成創(chuàng)建的,源碼如下(jdk1.7).

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       .........
    }

    /**
     * Inflates the table. 第一次 put 時臊诊,調(diào)用如下方法鸽粉,初始化 table
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        // 找到大于參數(shù)值且最接近 2 的冪值,假如輸入?yún)?shù)是 27抓艳,則返回32
        int capacity = roundUpToPowerOf2(toSize);
        //threshold 在不超過限制最大值的前提下等于 capacity * loadFactor
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

??為了提高運算速度潜叛,設定 HashMap 容量大小為2?,這樣的方式使計算落槽位置更快壶硅。如果初始化 HashMap 的時侯通過構(gòu)造器指定了 initialCapacity威兜,則會先計算出比 initialCapacity 大的2 的冪存入 threshold,在第一次 put 時會按照這個2的冪初始化數(shù)組大小庐椒,此后每次擴容都是增加2倍椒舵。如果沒有指定初始值,log?1000 =9.96,結(jié)合源碼分析可知约谈,如果想要容納 1000 個元素笔宿,必須經(jīng)過7次擴容。HashMap的擴容還是有不小的成本的棱诱,如果提前能夠預估出 HashMap 內(nèi)要放置的元素數(shù)量泼橘,就可在初始化時合理設置容量大小,避免不斷擴容帶來的性能損耗迈勋。
??綜上所述炬灭,集合初始化時,指定集合初始值大小靡菇。如果暫時無法確定集合大小那么指定相應的默認值重归,這也要求我們記得各種集合的默認值大小,ArrayList大小10厦凤,而 HashMap 默認值為 16鼻吮。

3. 數(shù)組與集合

??數(shù)組是一種順序表,在各種高級語言中较鼓,它是組織和處理數(shù)據(jù)的一種常見方式椎木,我們可以使用索引下標進行快速定位并獲取指定位置的元素。數(shù)組的下標從0開始,但這并不符合生活常識香椎,這源于BCPL 語言漱竖,它將指針設置在0的位置,用數(shù)組下標作為直接偏移量進行計算士鸥。為什么下標不從1 開始呢?如果是這樣闲孤,計算偏移量就要使用當前下標減1的操作。加減法運算對 CPU 來說是一種雙數(shù)運算烤礁,在數(shù)組下標使用頻率極高的場景下讼积,這種運算是十分耗時的。在Java 體系中脚仔,數(shù)組用以存儲同-類型的對象勤众,一旦分配內(nèi)存后則無法擴容。提倡類型與中括號緊挨相連來定義數(shù)組鲤脏,因為在Java的世界里们颜,萬物皆為對象。String[] 用來指代String數(shù)組對象猎醇,示例代碼如下.

String[] strings = {"a", "b"};//數(shù)組引用賦值給 Object
Object obj = strings;//使用類名string[]進行強制轉(zhuǎn)化窥突,并成功賦值,strings[0]的值由a變?yōu)?object
((String[]) obj)[0] = "object";

??聲明數(shù)組和賦值的方式示例代碼如下:

// 初始化完成硫嘶,容量的大小即等于大括號內(nèi)元素的個數(shù)阻问,使用頻率并不高
String[] args3 = {"a", "b"};
String[] args4 = new String[2];
args4[0] = "a";
args4[1] = "h";

??上述源碼中的 args3 是靜態(tài)初始化,而 args4 是動態(tài)初始化沦疾。無論靜態(tài)初始化還是動態(tài)初始化称近,數(shù)組是固定容量大小的。注意在數(shù)組動態(tài)初始化時哮塞,出現(xiàn)了 new刨秆,這意味著需要在 new String[]的方括號內(nèi)填寫一個整數(shù)忆畅。如果寫的是負數(shù),并不會編譯出錯眠屎,但運行時會拋出異帶:NegativeArraySizeException。對于動態(tài)大小的數(shù)組肆饶,集合提供了Vector和 AmayLsit 兩個類岖常,前者是線程安全,性能校差,基本棄用板惑,而后者是線程不安全橄镜,它是使用頻率最高的集合之一冯乘。
??數(shù)組的遍歷優(yōu)先推薦 JDK5引進的 foreach 方式,即 for(元素:數(shù)組名)的方式姊氓,可以在不使用下標的情況下遍歷數(shù)組喷好。如果需要使用數(shù)組下標,則使用for(int i=0;i<array.lengt;i++)的方式禾唁,注意 length 是數(shù)組對象的一個屬性荡短,而不是方法哆键。string類是使用 length()方法來獲取字符串長度的)洼哎。也可以使用JDK8 的函數(shù)式接口進行遍歷:

Arrays.asList(args3).stream().forEach(x-> System.out.println(x));
Arrays.asList(args3).stream().forEach(System.out::println);

??Arrays 是針對數(shù)組對象進行操作的工具類,包括數(shù)組的排序锭沟、查找族淮、對比凭涂、拷貝等操作切油。尤其是排序,在多個JDK 版本中在不斷地進化孕荠,比如原來的歸并排序改成Timsort,明顯地改善了集合的排序性能娩鹉。另外,通過這個工具類也可以把數(shù)組轉(zhuǎn)成集合稚伍。
??數(shù)組與集合都是用來存儲對象的容器,前者性質(zhì)單一锈嫩,方便易用;后者類型安全呼寸,功能強大悼沿,且兩者之間必然有互相轉(zhuǎn)換的方式糟趾。畢竟它們的性格迥異义郑,在轉(zhuǎn)換過程中,如果不注意轉(zhuǎn)換背后的實現(xiàn)方式交汤,很容易產(chǎn)生意料之外的問題芙扎。轉(zhuǎn)換分成兩種情況:數(shù)組轉(zhuǎn)集合和集合轉(zhuǎn)數(shù)組填大。在數(shù)組轉(zhuǎn)集合的過程中允华,注意是否使用了視圖方式直接返回數(shù)組中的數(shù)據(jù)靴寂。我們以Arrays.asList()為例,它把數(shù)組轉(zhuǎn)換成集合時褐隆,不能使用其修改集合相關的方法妓灌,它的add/remove/clear 方法會拋出UnsupportedOperationException 異常蜜宪。示例源碼如下:

public class ArraysAsList {
    public static void main(String[] args) {
        String[] stringArray = new String[3];
        stringArray[0] = "one";
        stringArray[1] = "two";
        stringArray[2] = "three";
        List<String> stringList = Arrays.asList(stringArray);// 修改轉(zhuǎn)換后的集合圃验,成功地把第一個元素“one”改成“oneList
        stringList.set(0, "oneList");
// 運行結(jié)果是 oneList澳窑,數(shù)組的值隨之改變
        System.out.println(stringArray[0]);
// 這是重點:以下三行編譯正確摊聋,但都會拋出運行時異常
        stringList.add("four");
        stringList.remove(2);
        stringList.clear();
    }
}

??事實證明,可以通過set()方法修改元素的值箍镜,原有數(shù)組相應位置的值同時也會被修改色迂,但是不能進行修改元素個數(shù)的任何操作手销,否則均會拋出UnsupportedOperationException 異常锋拖。Arays.asList 體現(xiàn)的是適配器模式兽埃,后臺的數(shù)據(jù)仍是原有數(shù)組,set()方法即間接對數(shù)組進行值的修改操作慕趴。asList 的返回對象是一個Arrays 的內(nèi)部類冕房,它并沒有實現(xiàn)集合個數(shù)的相關修改方法耙册,這也正是拋出異常的原因毫捣。Arrays.asList 的源碼如下:

    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

??返回的明明是ArrayList 對象,怎么就不可以隨心所欲地對此集合進行修改呢蹲诀?注意此ArrayList 非彼ArrayList脯爪,雖然Arrays 與ArrayList 同屬于一個包矿微,但是在Arrays類中還定義了一個ArrayList的內(nèi)部類(或許命名為InnerArrayList更容易識別)涌矢,根據(jù)作用域就近原則娜庇,此處的ArrayList是李鬼,即這是個內(nèi)部類俺叭。此李鬼十分簡單只提供了個別方法的實現(xiàn)熄守,如下所示:

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
// final修飾不準修改其引用 (第1處)
        private final E[] a;
// 直接把數(shù)組引用賦值給 a裕照,而 objects 是 JDK7引入的工具包
// requireNonNul1 僅僅判斷是否為 null
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
// 實現(xiàn)了修改特定位置元素的方法
        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
// 注意 set 成功返回的是此位置上的舊值
            return oldValue;
        }
    }

??第1處的 final 引用晋南,用于存儲集合的數(shù)組引用始終被強制指向原有數(shù)組负间。這個內(nèi)部類并沒有實現(xiàn)任何修改集合元麥個數(shù)的相關方法姜凄,那這個UnspportedOperationException 異常 是 從哪里 出 來的呢? 是李鬼的父類AbstractList:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
// clear()方法調(diào)用 remove 方法态秧,依然拋出異常
    public void clear() {
        removeRange(0, size());
    }
}

??如果李鬼Arrays.ArrayList 內(nèi)部類覆寫這些方法不拋出異常,避免使用者踩進這個坑會不會更好?數(shù)組具有不為五斗米折腰的氣節(jié)云头,傳遞的信息是“要么直接用我,要么小心異常!”數(shù)組轉(zhuǎn)集合引發(fā)的故障還是十分常見的溃槐。比如,某業(yè)務調(diào)用某接口時撮慨,對方以這樣的方式返回一個 List 類型的集合對象砌溺,本方獲取集合數(shù)據(jù)時规伐,99.9%是只讀操作猖闪,但在小概率情況下需要增加一個元素肌厨,從而引發(fā)故障柑爸。在使用數(shù)組轉(zhuǎn)集合時表鳍,需要使用李逵iava.util.ArrayList 直接創(chuàng)建一個新集合,參數(shù)就是ArraysasList返回的不可變集合瓮恭,源碼如下:

List<Object> objectList = new java.util.ArrayList<Object>(Arrays.asList(stringArray));

??相對于數(shù)組轉(zhuǎn)集合來說屯蹦,集合轉(zhuǎn)數(shù)組更加可控登澜,畢竟是從相對自由的集合容器轉(zhuǎn)為更加苛刻的數(shù)組帖渠。什么情況下集合需要轉(zhuǎn)成數(shù)組呢?適配別人的數(shù)組接口空郊,或者進行局部方法計算等。先看一個源碼锁摔,猜猜執(zhí)行結(jié)果

public class ListToArray {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>(3);
        list.add("one");
        list.add("two");
        list.add("three");
//泛型丟失谐腰,無法使用 string[] 接收無參方法返回的結(jié)果 (第1處)
        Object[] array1 = list.toArray();
// array2 數(shù)組長度小于元素個數(shù) (第2處)
        String[] array2 = new String[2];
        list.toArray(array2);
        System.out.println(Arrays.asList(array2));
// array3 數(shù)組長度等于元素個數(shù) (第3處)
        String[] array3 = new String[3];
        list.toArray(array3);
        System.out.println(Arrays.asList(array3));

    }
}

執(zhí)行結(jié)果如下:
[null,null]
[one十气,two砸西, three]
第1處比較容易理解芹枷,不要用toArray()無參方法把集合轉(zhuǎn)換成數(shù)組莲趣,這樣會致泛型丟失;
在第2處執(zhí)行成功后喧伞,輸出卻為 null;
第3處正常執(zhí)行絮识,成功地把集合數(shù)據(jù)復制到array3數(shù)組中次舌。
第2處與第3處的區(qū)別在于即將復制進去的數(shù)組容量是否足夠。如果容量不夠挪圾,則棄用此數(shù)組哲思,另起爐灶棚赔,關于此方法的源碼如下.

// 注意入?yún)?shù)組的 length 大小是重中之重靠益,如果大于或等于集合的大小
// 則集合中的數(shù)據(jù)復制進入數(shù)組即可,如果空間不夠芋浮,入?yún)?shù)組 a 就會被無視
// 重新分配一個空間纸巷,復制完成后返回一個新的數(shù)組引用
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
// 如果數(shù)組長度小于集合 size瘤旨,那么執(zhí)行此語句裆站,直接 return黔夭。(第1處)
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 如果容量足夠本姥,則直接復制 (第2處)
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
// 只有在數(shù)組容量足夠的情況下婚惫,才返回傳入?yún)?shù)
        return a;
    }

??第1處和第 2 處均 復制 java.util.ArrayList 的 elementData到數(shù)組中先舷,這個elementData是 ArrayList 集合對象中真正用于存儲數(shù)據(jù)的數(shù)組蒋川,它的定義為:transient Object[] elementData
??這個存儲ArrayList 真正數(shù)據(jù)的數(shù)組由 transient 修飾捺球,表示此字段在類的序列化時將被忽略夕冲。因為集合序列化時系統(tǒng)會調(diào)用 writeObject 寫入流中歹鱼,在網(wǎng)絡客戶端反序列化的readObject 時,會重新賦值到新對象的 elementData 中掺涛。為什么多此一舉鸽照?因為 elementData 容量經(jīng)常會大于實際存儲元素的數(shù)量矮燎,所以只需發(fā)送真正有實際值的數(shù)組元素即可诞外≡制保回到剛才的場景刊苍,當入?yún)?shù)組客量小于集合大小時正什,使用Amsys.copy0f()方法婴氮,它的源碼如下

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
// 新創(chuàng)建一個數(shù)組 copy
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

如果數(shù)組初始大小設置不當主经,不僅會降低性能罩驻,還會浪費空間鉴腻。使用集合的toArray(T[] array)方法爽哎,轉(zhuǎn)換為數(shù)組時,注意需要傳入類型完全一樣的數(shù)組厨内,并且的容量大小為 list.size()雏胃。

4. 集合與泛型

??泛型與集合的聯(lián)合使用瞭亮,可以把泛型的功能發(fā)揮到極致统翩,很多小伙伴不清楚List厂汗、List<Object>娶桦、List<?> 三者的區(qū)別衷畦,更加不能區(qū)分<? extends T> 與<? super T>的使用場景祈争。List 完全沒有類型限制和賦值限定,如果天馬行空地亂用,遲早會遭類型轉(zhuǎn)換失敗的異常墨吓。很多程序員覺得 List<Object> 的用法完全等同于 List帖烘,但在接受其他泛型賦值時會編譯出錯秘症。List<?> 是一個泛型乡摹,在沒有賦值之前聪廉,表示它可以接受任何類型的集合賦值板熊,賦值之后就不能便往里添加元素了。下方的例子很好活明了三者的區(qū)別干签,以List為原型展開說明:

public class ListNoGeneric {
    public static void main(String[] args) {
// 第一段:泛型出現(xiàn)之前的集合定義方式
        List a1 = new ArrayList();
        a1.add(new Object());
        a1.add(new Integer(111));
        a1.add(new String("hello alal"));
//第二段:把a1引用賦值給 a2津辩,注意 a2與al的區(qū)別是增加了泛型原制<opject>
        List<Object> a2 = a1;
        a2.add(new Object());
        a2.add(new Integer(222));
        a2.add(new String("hello a2a2"));
//第三段:把al引用賦值給 a3,注意a3與a1的區(qū)別是增加了泛型<Integer>
        List<Integer> a3 = a1;
        a3.add(new Integer(333));
//下方兩行編譯出錯容劳,不允許增加非 Integer 類型進入集合
        a3.add(new Object());
        a3.add(new String("hello a3a3"));

// 第四段:把a1 引用賦值給 a4喘沿,a1 與a4的區(qū)別是增加了通配符
        List<?> a4 = a1;
        // 允許刪除和清除元素
        a1.remove(0);
        a4.clear();
// 編譯出錯。不允許增加任何元素
        a4.add(new Object());
    }
}

??第一段說明:在定義 List 之后鸭蛙,毫不猶豫地往集合里裝入三種不同的對象:Object摹恨、Integer 和 String娶视,遍歷沒有問題晒哄,但是貿(mào)然以為里邊的元素都是 Integer,使用強制轉(zhuǎn)化肪获,則拋出 ClassCastException 異常寝凌。
??第二段說明:把 a1 賦值給 a2,a2 是 List<Objec> 類型的孝赫,也可以再往里裝入三種不同的對象较木。很多程序員認為 List 和 List<Object> 是完全相同的,至少從目前這兩段來看是這樣的青柄。
??第三段說明:由于泛型在JDK5 之后才出現(xiàn)伐债,考慮到向前兼客,因此歷史代碼有時需要賦值給新泛型代碼致开,從編譯器角度是允許的峰锁。這種代碼似乎有點反人類,在實際故障案例中經(jīng)常出現(xiàn)双戳,來看一段問題代碼虹蒋。

JsoNobject jsonobject = JSoNobject.fromobject ("(\"level\":[\"3 \"])"):
List<Integer> intList= new ArrayList<Integer>(10);

if (jsonObject != nul1) {
    intList.addAll(jsonObject.getJSONArray("level"));
    int amount=0;
    for (Integer t : intList) (
        //拋出classCastException異帶 : string cannot be cast to Integer
        if (condition) {
            amount = amount + t;
        }
    }
}   

addAll的定義如下:

public boolean addAll(Collection<? extends E> c) {...]

進行了泛型限制,示例中addAll的實際參數(shù)是getJSONArray 返回的JSONArray對象飒货,它并非是List魄衅,更加不是Integer集合的子類,為何編譯不報錯?查看JSONArray 的定義:

public final class JSONArray extends AbstractJSON implements JSON, List {}

??JSONArray 實現(xiàn)了 List塘辅,是非泛型集合晃虫,可以賦值給任何泛型限制的集合。編譯可以通過莫辨,但在運行時報錯傲茄,這是一個隱藏得比較深的Bug毅访,最終導致發(fā)生線上故障。在JDK5 之后盘榨,應盡量使用泛型定義喻粹,以及使用類、集合草巡、參數(shù)等守呜。
??如果把al的定義從List a1修改為 List<Object>a1,那么第三段就會編譯出錯List<Objec> 賦值給 List<Integer> 是不允許的山憨,若是反過來賦值:

List<Integer> intList = new ArrayList<Integer>(3);
intList.add(111);
List<Object> objectlist = intList;

??事實上查乒,依然會編譯出錯,提示如下:

Error:(10, 26) java: incompatible types: java.util.List<java.lang.Integer> cannot be converted tojava.util.List<java.lang.Object>

??注意郁竟,數(shù)組可以這樣賦值玛迄,因為它是協(xié)變的,而集合不是棚亩。
??第四段說明:間號在正則表達式中可以匹配任何字符蓖议,List<?>稱為通配待集合可以接受任何類型的集合引用賦值,不能添加任何元素讥蟆,但可以remove和clear,并非 immutable 集合勒虾。List<?>一般作為參數(shù)來接收外部的集合,或者返回一個不知具體元素類型的集合瘸彤。
??List<T>最大的問題是只能放置一種類型修然,如果隨意轉(zhuǎn)換類型的話,就是“破窗像論”质况,泛型就失去了類型安全的意義愕宋。如果需要放置多種受泛型約束的類型呢?JDK 的開發(fā)者順應了民意,實現(xiàn)了 <? extends T>與<? super>兩種語法结榄,但是兩的區(qū)別非常微妙掏婶。簡單來說,<?extends T>是 Get First潭陪,適用于,消費集合元素為主的場景,<?super T>是 Put First最蕾,適用于依溯,生產(chǎn)集合元素為主的場景。
??<? extends T>可以賦值給任何T及T子類的集合瘟则,上界為T黎炉,取出來的類型帶有泛型限制,向上強制轉(zhuǎn)型為 T醋拧。null 可以表示任何類型慷嗜,所以除 ull外淀弹,任何元素都不得添加進<?extends T>集合內(nèi)。
??<? super T>可以賦值給任何T及T的父類集合庆械,下界為 T薇溃。在生活中,投票選舉類似于<?super T>的操作缭乘。選舉代表時沐序,你只能往里投選票,取數(shù)據(jù)時堕绩,根本不知道是誰的票策幼,相當于泛型丟失。有人說奴紧,這只是一種生活場景特姐,在系統(tǒng)設計中,很難有這樣的情形黍氮。再舉例說明一下唐含,我們在填寫對主管的年度評價時,提交后若想再次訪問之前的鏈接修改評價滤钱,就會被告之:“您已經(jīng)完成對主管的年度反饋觉壶,謝謝參與〖祝”extends的場景是put 功能受限铜靶,而 super的場景是get功能受限。
??下例中他炊,以加菲貓争剿、貓、動物為例痊末,說明 extends和super的詳細語法差異:

public class AnimalCatGarfield {
    public static void main(String[] args) {
        //第1段;聲明三個依次承的類的集合: Object>動物>貓>加菲貓
        List<Animal> animal = new ArrayList<Animal>();
        List<Cat> cat = new ArrayList<Cat>();
        List<Garfield> garfield = new ArrayList<Garfield>();

        animal.add(new Animal());
        cat.add(new Cat());
        garfield.add(new Garfield());
        //第二段測試賦值操作
        // 下行編譯出錯蚕苇。只能賦值 Cat 或 cat 子類的集合
        List<? extends Cat> extendsCatFromAnimal = animal;
        List<? super Cat> superCatFromAnimal = animal;

        List<? extends Cat> extendsCatFromCat = cat;
        List<? super Cat> superCatFromCat = cat;

        List<? extends Cat> extendsCatFromGarfield = garfield;
        //下行編譯出錯。只能制值Cat或Cat父類的集合
        List<? super Cat> superCatFromGarfield = garfield;

        //第3段:測試add 方法
        // 下面三行中所有的<? extends T> 都無法進行add操作凿叠,編譯均出錯
        extendsCatFromCat.add(new Animal());
        extendsCatFromCat.add(new Cat());
        extendsCatFromCat.add(new Garfield());

        // 下行編譯出錯涩笤。只能添加 cat 或 Ca 子類的集合
        superCatFromCat.add(new Animal());
        superCatFromCat.add(new Cat());
        superCatFromCat.add(new Garfield());

        //第4段:測試get 方法
        // 所有的 super 操作能夠返回元素,但是泛型丟失盒件,只能返回 object 對象

        //以下extends 操作能夠返回元素
        Object catExtends2 = extendsCatFromCat.get(0);
        Cat catExtends1 = extendsCatFromCat.get(0);
        // 下行編譯出錯蹬碧。雖然 Cat 集合從 Garfield 賦值而來,但類型擦除后炒刁,是不知道的
        Garfield garfield1 = extendsCatFromGarfield.get(0);
    }
}

??第1段恩沽,聲明三個泛型集合,可以理解為三個不同的籠子翔始,List<Anima>住的是動物(反正就是動物世界里的動物)罗心,List<Ca住的是貓(反正就是貓科動物)里伯,List<Garfield>住的是加菲貓(又懶又可愛的一種貓)。Garfield 繼承于Cat渤闷,而Ca繼承自Animal俐东。
??第2段享潜,以Cat 類為核心寨典,因為它有父類也有子類调鬓。定義類型限定集合,分別為 List<? extends Cat>和List<? super Cat>补憾。在理解這兩個概念時漫萄,暫時不要引入上界和下界,專注于代碼本身就好盈匾。
??把 List<Cat> 對象賦值給兩者都是可以的腾务。但是把 List<Animal> 賦值給 List<? extends Cat> 時會編譯出錯,因為能賦值給 <? extend Cat> 的類型削饵,只有 Cat 自己和它的子類集合岩瘦。盡管它是類型安全的,但依然有泛型信息窿撬,因而從籠子里取出來的必然是只貓,而List<Animal>里邊有可能住著毒蛇启昧、鱷魚蝙蝠等其他動物。把 List<Garfield> 賦值給 List<? super Cat> 時劈伴,也會編譯報錯密末。因為能賦值給<?super Cat>的類型,只有 Cat自己和它的父類跛璧。
??第3段严里,所有的 List<?extends T>都會編譯出錯,無法進行add 操作追城,這是因為除 null外刹碾,任何元素都不能被添加進<? extends T> 集合內(nèi)。List<? super Cat> 可以往里增加元素座柱,但只能添加Cat 自身及子類對象迷帜,假如放入一塊石頭,則明顯違背了Animal大類的性質(zhì)色洞。
??第4段瞬矩,所有 List<? super T> 集合可以執(zhí)行 get操作,雖然能夠返回元素锋玲,但是類型丟失,即只能返回Object 對象涵叮。List<?extends Cat>可以返回帶類型的元素惭蹂,但只能返回 Cat 自身及其父類對象伞插,因為子類類型被擦除了。
??對于一個籠子盾碗,如果只是不斷地向外取動物而不向里放的話媚污,則屬于 Get First,應采用<?extends T>;相反,如果經(jīng)常向里放動物的話廷雅,則應采用<? super T>耗美,屬于Put First。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末航缀,一起剝皮案震驚了整個濱河市商架,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芥玉,老刑警劉巖蛇摸,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異灿巧,居然都是意外死亡赶袄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門抠藕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饿肺,“玉大人,你說我怎么就攤上這事盾似【蠢保” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵颜说,是天一觀的道長购岗。 經(jīng)常有香客問我,道長门粪,這世上最難降的妖魔是什么喊积? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮玄妈,結(jié)果婚禮上乾吻,老公的妹妹穿的比我還像新娘。我一直安慰自己拟蜻,他們只是感情好绎签,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酝锅,像睡著了一般诡必。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天爸舒,我揣著相機與錄音蟋字,去河邊找鬼。 笑死扭勉,一個胖子當著我的面吹牛鹊奖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涂炎,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼忠聚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唱捣?” 一聲冷哼從身側(cè)響起两蟀,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爷光,沒想到半個月后垫竞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡蛀序,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年欢瞪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片徐裸。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡遣鼓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出重贺,到底是詐尸還是另有隱情骑祟,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布气笙,位于F島的核電站次企,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏潜圃。R本人自食惡果不足惜缸棵,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谭期。 院中可真熱鬧堵第,春花似錦、人聲如沸隧出。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胀瞪。三九已至针余,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背圆雁。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工傍妒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摸柄。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像既忆,于是被迫代替她去往敵國和親驱负。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 集合類 集合類存放于java.util包中患雇。集合類存放的都是對象的引用跃脊,而非對象本身,出于表達上的便利苛吱,我們稱集合...
    狐言H閱讀 148評論 1 0
  • Java中的集合類包括ArrayList酪术、LinkedList、HashMap等類翠储,下列關于集合類描述正確的是()...
    文茶君閱讀 351評論 0 0
  • 1. HashMap (1). 常量和構(gòu)造方法 ? 如果指定容量的話,會先進行判斷容量不能小于0,否則...
    Benjamin_Lee閱讀 184評論 0 0
  • List 特點:元素有放入順序绘雁,元素可重復 ArrayList :層數(shù)據(jù)結(jié)構(gòu)使用數(shù)組結(jié)構(gòu), 查詢速度快援所。但是增刪稍...
    zhaoyunxing閱讀 317評論 0 0
  • 面向?qū)ο笳Z言對事物的描述都是通過對象來體現(xiàn)的庐舟,那么肯定要涉及到對多個對象的操作。 其中肯定免不了對多個對象的存儲住拭,...
    DeeJay_Y閱讀 120評論 0 0