Java集合框架源碼分析html
本次源碼分析對Java JDK中的集合框架部分展開分析,採用的是JDK 1.8.0_171版本的源碼。java
Java集合框架(Java Collections Framework,JCF)也稱容器,便可以容納其餘Java對象的對象。JCF爲開發者提供了通用的容器,數據持有對象的方式和對數據集合的操做,優勢是:程序員
1) 下降編程難度編程
2) 提升程序性能設計模式
3) 提升API間的互操做性數組
4) 下降學習難度緩存
5) 下降設計和實現相關API的難度安全
6) 增長程序的可重用性網絡
Java容器中只能存放對象,對於基本類型(int,double,float,long等),須要將其包裝成對象類型後(Integer,Double,Float,Long等)才能放到容器裏。不少時候裝箱和拆箱都可以自動完成。這雖然會致使額外的性能和空間開銷,但簡化了設計和編程。多線程
1.整體架構分析
爲了規範容器的行爲,統一設計,JCF定義了14種容器接口(Collection interface),它們的關係以下圖所示:
Map接口沒有繼承自Collection接口,由於Map表示的是關聯式的容器而不是集合,但Java提供了從Map轉換到Collection的方法,能夠方便地將Map切換到集合視圖。上圖中提供了Queue接口,但沒有Stack,由於Stack的功能已被JDK 1.6版本引入的Deque取代。上述接口的通用實現以下表:
|
Implementations |
|||||
Hash Table |
Resizable Array |
Balanced Tree |
Linked List |
Hash Table +Linked List |
||
Inter faces |
Set |
HashSet |
|
TreeSet |
|
LinkedHashSet |
List |
|
ArrayList |
|
LinkedList |
|
|
Deque |
|
ArrayDeque |
|
LinkedList |
|
|
Map |
HashMap |
|
TreeMap |
|
LinkedHashMap |
整體上來講,從下面的框架圖能夠看出,集合框架主要包括兩種類型的容器,一種是集合(Collection),存儲一個元素集合,另外一種是圖(Map),存儲鍵/值對映射。Collection接口又有三種子類型,List,Set和Queue。再下面是一些抽象類,最後是一些具體實現類。
下面對部分具體實現類的實現作簡單描述:
1) ArrayList:線程不一樣步,默認初始容量爲10,當數組大小不足時容量擴大爲1.5倍。爲追求效率,ArrayList沒有實現同步(synchronized),若是須要多個線程併發訪問,用戶須要手動實現同步,或者用Vector代替。
2) LinkedList:線程不一樣步,雙向鏈表實現。LinkedList同時實現了List接口和Deque接口,因此既能夠當作一個順序容器,又能夠當作一個隊列,或者也能夠看成一個棧。官方聲明不建議使用Stack類,且也沒有Queue的實現(只是接口),因此能夠考慮用LinkedList來看成棧使用。首選的棧或隊列實現,仍是ArrayDeque,性能更好。
3) Vector:線程同步,默認初始容量爲10,當數組大小不足時容量擴大爲2倍。他的同步是經過Iterator方法加synchronized實現的。
4) TreeSet:線程不一樣步,內部使用NavigableMap操做。默認元素天然順序排列,能夠經過Comparator改變排序。TreeSet裏面有一個TreeMap(適配器模式)。
5) HashSet:線程不一樣步,內部使用HashMap進行數據存儲,提供的方法基本都是調用HashMap的方法,二者本質上是相同的。集合元素能夠爲null。
6) Set:Set是一種不包含重複元素的集合,最多隻有一個null元素。且Set集合一般能夠經過Map集合經過適配器模式獲得。
7) PriorityQueue:PriorityQueue實現了Queue接口,不容許放入null元素,其經過堆實現,即經過徹底二叉樹實現小頂堆(任意一個非葉子節點的權值,都不大於其左右子節點的權值),也就意味着能夠經過數組來做爲其底層實現。
8) TreeMap:線程不一樣步,基於紅黑樹的NavigableMap實現,可以把它保存的記錄根據鍵排序,默認是按照鍵值的升序排序,也能夠指定排序的比較器。當用Iterator遍歷TreeMap時,獲得的記錄是排過序的。
9) HashMap:線程不一樣步,根據key的hashcode進行存儲,內部使用靜態內部類Node的數組進行存儲,默認初始大小爲16,每次擴容一倍。當發生Hash衝突時,採用拉鍊法來解決。在1.8版本的JDK中,當單個桶中元素個數大於等於8時,鏈表改成紅黑樹實現;當元素個數小於6時,變回鏈表實現,由此來防止hashcode攻擊。HashMap是HashTable的輕量級實現,能夠接受null的key和value,而HashTable是不容許的。
10) LinkedHashMap:保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先獲得的記錄確定是先插入的。也能夠在構造時帶上參數,按照使用的次數排序,在遍歷的時候會比HashMap慢。不過有例外狀況,當HashMap容量很大,實際數據較少時,遍歷起來可能會比LinkedHashMap慢,由於後者遍歷速度只與實際數據量有關,和容量無關。
11) HashTable:線程安全,HashTable的迭代器是fail-fast迭代器。HashTable不能存儲null的key和value。
12) Collections、Arrays:集合類的工具幫助類,提供了一系列靜態方法,用於對集合中元素進行排序、搜索以及線程安全等各類操做。
13) Comparable、Comparator:通常是用於對象的比較來實現排序,略有區別。
2.源碼分析
2.1 ArrayList
ArrayList實現了List接口,是順序容器,即元素存放的數據與放入的順序相同。容許放入null元素,底層經過數組實現。除未實現同步外,其他和Vector大體相同。每一個ArrayList都有一個容量(capacity),表示底層數組的實際大小,容器內存儲元素的個數不能多於當前容量,容量不足自動擴容1.5倍。Size(),isEmpty(),get(),set()方法均能在常數時間內完成,add()方法的時間開銷和插入位置有關,addAll()方法的時間開銷跟添加元素的個數成正比,其他方法大可能是線性時間。
2.1.1 set()
因爲底層是一個數組,set()方法也就變得很是簡單,直接對數組的指定位置賦值便可。RangeCheck(index)用於檢查下標是否越界,須要注意賦值語句僅僅是引用的複製。
1 public E set(int index, E element) { 2 rangeCheck(index); 3 E oldValue = elementData(index); 4 elementData[index] = element; 5 return oldValue; 6 }
2.1.2 get()
Get()方法也很簡單,去對應下標位置獲取元素便可,惟一要注意的是因爲底層數組是Object[],獲得元素後須要進行類型轉換。
1 public E get(int index) { 2 rangeCheck(index); 3 return elementData(index); 4 }
2.1.3 add(int index,E element)
和C++的vector不一樣,ArrayList沒有push_back()方法,對應的是add(E e),也沒有insert()方法,對應的是add(int index,E element)。這兩個方法都是向容器中添加新元素,可能會致使capacity不足,所以在添加元素以前,都須要進行剩餘空間檢查。經過ensureCapacityInternal()方法判斷,該方法內部繼續嵌套調用,計算容量時,若全局數組爲空,則返回傳入參數和default capacity(爲10)中較大的那個,若minCapacity較大,而後根據二者的差值調用grow()方法完成擴容。由源碼中看出,對數組的擴容首先經過擴展爲原來的1.5倍,而後和minCapacity比較大小,以後也會判斷容量是否過大,設置超大容量,最後擴展和複製。因爲Java GC自動管理了內存,也就不須要考慮源數組釋放的問題。空間擴容後,插入過程也就變得容易,須要先對元素進行移動,而後完成插入操做,因此該方法爲線性複雜度。
1 public void add(int index, E element) { 2 rangeCheckForAdd(index); 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 System.arraycopy(elementData, index, elementData, index + 1, size - index); 5 elementData[index] = element; 6 size++; 7 }
1 private void ensureCapacityInternal(int minCapacity) { 2 3 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 4 5 } 6 7 8 private void ensureExplicitCapacity(int minCapacity) { 9 10 modCount++; 11 12 // overflow-conscious code 13 14 if (minCapacity - elementData.length > 0) 15 16 grow(minCapacity); 17 18 } 19 20 21 private void grow(int minCapacity) { 22 23 // overflow-conscious code 24 25 int oldCapacity = elementData.length; 26 27 int newCapacity = oldCapacity + (oldCapacity >> 1); 28 29 if (newCapacity - minCapacity < 0) 30 31 newCapacity = minCapacity; 32 33 if (newCapacity - MAX_ARRAY_SIZE > 0) 34 35 newCapacity = hugeCapacity(minCapacity); 36 37 // minCapacity is usually close to size, so this is a win: 38 39 elementData = Arrays.copyOf(elementData, newCapacity); 40 41 }
2.1.4 remove()
Remove()方法有兩個版本,一個以下所示,從指定位置刪除元素,另外一個是remove(Object o),刪除第一個知足o.equals(elementData[index])的元素。刪除操做是add()操做的逆過程,須要將刪除節點以後的元素向前移動一個位置。須要注意的是爲了讓GC起做用,必須顯式地爲最後一個位置賦null值。
1 public E remove(int index) { 2 3 rangeCheck(index); 4 5 modCount++; 6 7 E oldValue = elementData(index); 8 9 int numMoved = size - index - 1; 10 11 if (numMoved > 0) 12 13 System.arraycopy(elementData, index+1, elementData, index,numMoved); 14 15 elementData[--size] = null; // clear to let GC do its work 16 17 return oldValue; 18 19 }
因爲各個不一樣的集合類和結構大部分都是底層實現方式不一樣,而功能基本相同,因此這裏僅分析ArrayList一例。
2.2 容器中的設計模式
2.2.1 迭代器模式
Collection實現了Iterable接口,其中的iterator()方法可以產生一個Iterator對象,經過這個對象就能夠迭代遍歷Collection中的元素。從JDK1.5以後可使用foreach方法來遍歷實現了Iterable接口的聚合對象。
1 List<String> list = new ArrayList<>(); 2 3 list.add("a"); 4 5 list.add("b"); 6 7 for (String item : list) { 8 9 System.out.println(item); 10 11 }
2.2.2 適配器模式
Java.util.Arrays類中的asList()方法能夠把數組類型轉換爲List類型,若是要將數組類型轉換爲List類型,應該注意的是asList()的參數爲泛型的變長參數,所以不能使用基本類型做爲參數,只能使用相應的包裝類型數組。這裏數組類型和List類型的轉換就是一種典型的適配器模式,做爲兩個不兼容的接口之間的橋樑,結合了其功能。
1 public static <T> List<T> asList(T... a) { 2 3 return new ArrayList<>(a); 4 5 }
一般狀況下,客戶端能夠經過目標類的接口訪問它所提供的服務。有時,現有的類能夠知足客戶類的功能須要,可是它所提供的接口不必定是客戶類所指望的,這多是由於現有類中方法名與目標類中定義的方法名不一致等緣由所致使的。
在這種狀況下,現有的接口須要轉化爲客戶類指望的接口,這樣保證了對現有類的重用。若是不進行這樣的轉化,客戶類就不能利用現有類所提供的功能,適配器模式能夠完成這樣的轉化。
在適配器模式中能夠定義一個包裝類,包裝不兼容接口的對象,這個包裝類指的就是適配器(Adapter),它所包裝的對象就是適配者(Adaptee),即被適配的類。
適配器提供客戶類須要的接口,適配器的實現就是把客戶類的請求轉化爲對適配者的相應接口的調用。也就是說:當客戶類調用適配器的方法時,在適配器類的內部將調用適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。所以,適配器可使因爲接口不兼容而不能交互的類能夠一塊兒工做。這就是適配器模式的模式動機。
3.違反/符合高質量代碼案例
3.1 使用靜態內部類提升封裝性(符合)
LinkedList.java 970-980行
1 private static class Node<E> { 2 3 E item; 4 5 Node<E> next; 6 7 Node<E> prev; 8 9 10 11 Node(Node<E> prev, E element, Node<E> next) { 12 13 this.item = element; 14 15 this.next = next; 16 17 this.prev = prev; 18 19 } 20 21 }
靜態內部類的優勢是增強了類的封裝和提升代碼的可讀性。Node類封裝了鏈表中該節點先後節點的信息,不須要在LinkedList中所有定義全部節點,只需聲明first和last Node,便可將全部節點相互鏈接,造成鏈表,提升了封裝性。靜態內部類體現了Node和LinkedList之間的強關聯關係,加強了語意和可讀性。從代碼結構看,靜態內部類放置在外部類內,在這裏表示Node類是LinkedList類的子行爲或自屬性。與普通內部類相比,靜態內部類不持有外部類的引用:在普通內部類中,咱們能夠直接訪問外部類的屬性,方法,即便是private類型也能夠訪問,這是由於持有引用,能夠自由訪問。而靜態內部類,只能夠訪問外部類的靜態方法和靜態屬性,其餘則不能。其次,靜態內部類不依賴外部類,內部實例能夠脫離外部類實例單獨存在。而內部類則與外部類共同生死,一塊兒聲明,一塊兒被垃圾回收。第三,普通內部類不能聲明static方法和變量,而靜態內部類則沒有任何限制。方便靜態方法和變量擴展,具備優越性。
3.2 equals應該考慮null值狀況(違反)
LinkedList.java 595-610行
1 public int indexOf(Object o) { 2 3 int index = 0; 4 5 if (o == null) { 6 7 for (Node<E> x = first; x != null; x = x.next) { 8 9 if (x.item == null) 10 11 return index; 12 13 index++; 14 15 } 16 17 } else { 18 19 for (Node<E> x = first; x != null; x = x.next) { 20 21 if (o.equals(x.item)) 22 23 return index; 24 25 index++; 26 27 } 28 29 } 30 31 return -1; 32 33 }
若是傳入的Object對象o是null,且未通過檢查判斷,則在調用o.equals()方法時,就會出現空指針異常。出現這種狀況的緣由是由於equals()方法或者對其的覆寫未遵循對稱性原則,對於任何引用x和y的情形,若是x.equals(y)返回true,則y.equals(x)也應該返回true。因此最好的解決辦法就是在覆寫equals()方法時,加上對null值的判斷,保證對稱性原則,不然就須要像上述方法同樣在調用equals()方法以前完成對null值的判斷。
3.3 使用序列化類的私有方法解決部分屬性持久化問題(符合)
ArayList.java 135行,755-800行
1 transient Object[] elementData; 2 3 4 private void writeObject(java.io.ObjectOutputStream s) 5 6 throws java.io.IOException{ 7 8 // Write out element count, and any hidden stuff 9 10 int expectedModCount = modCount; 11 12 s.defaultWriteObject(); 13 14 15 16 // Write out size as capacity for behavioural compatibility with clone() 17 18 s.writeInt(size); 19 20 21 22 // Write out all elements in the proper order. 23 24 for (int i=0; i<size; i++) { 25 26 s.writeObject(elementData[i]); 27 28 } 29 30 31 32 if (modCount != expectedModCount) { 33 34 throw new ConcurrentModificationException(); 35 36 } 37 38 } 39 40 41 private void readObject(java.io.ObjectInputStream s) 42 43 throws java.io.IOException, ClassNotFoundException { 44 45 elementData = EMPTY_ELEMENTDATA; 46 47 48 49 // Read in size, and any hidden stuff 50 51 s.defaultReadObject(); 52 53 54 55 // Read in capacity 56 57 s.readInt(); // ignored 58 59 60 61 if (size > 0) { 62 63 // be like clone(), allocate array based upon size not capacity 64 65 int capacity = calculateCapacity(elementData, size); 66 67 SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); 68 69 ensureCapacityInternal(size); 70 71 72 73 Object[] a = elementData; 74 75 // Read in all elements in the proper order. 76 77 for (int i=0; i<size; i++) { 78 79 a[i] = s.readObject(); 80 81 } 82 83 } 84 85 }
序列化是將Java對象以一種形式持久化,如存放到硬盤,或者用於傳輸,反序列化是其逆過程。ArrayList也實現了Serializable接口來保證序列化。通過源碼分析可知其數據存儲都依賴於elementData數組,但注意該數組被transient關鍵字修飾。說明設計者認爲該數組不須要持久化,一般部分持久化的緣由是有一些屬性爲敏感信息,爲了安全起見,不但願在網絡操做中傳輸或本地序列化緩存。即elementData數組的生命週期僅存於調用者的內存中而不會寫到磁盤中。
那麼數組元素怎樣序列化呢?即遵循序列化的流程,經過調用ObjectOutputStream對象輸出流的writeObject()方法寫入對象狀態信息,而後就能夠經過readObject()方法讀取信息來反序列化。而對於elementData,defaultWriteObject()並不會去持久化被transient修飾的它,這裏須要重寫writeObject()和readObject()方法來手動持久化,經過循環將其中元素的值取出來,而後依次寫入輸出流。雖然重寫的兩個方法爲private,看起來不能調用,但實際上ObjectOutputStream會調用這個類的writeObject()方法,read也同理,經過反射機制來完成判斷一個類是否重寫了方法,根據傳入的ArrayList對象獲得class,而後包裝成ObjectStreamClass,在writeSerialData方法裏,會調用ObjectStreamClass的invokeWriteObject方法。
defaultReadObject和defaultWriteObject應該是readObject(ObjectInputStream o)和writeObject(ObjectOutputStream o)內部的第一個方法調用。它分別讀取和寫入該類的全部非瞬態字段。這些方法還有助於向後和向前的兼容性。若是未來在類中添加一些非瞬態字段,並嘗試經過舊版本的類對它進行反序列化,則defaultReadObject()方法將忽略新添加的字段,相似地,若是您使用新的序列化對舊的序列化對象進行反序列化版本,則新的非瞬態字段將採用JVM的默認值,即,若是其對象爲null,則爲null,不然將其從boolean設置爲false,將int設置爲0,等等。
那麼最終也對elementData元素進行了序列化,爲何要將其設置爲瞬態字段呢?由於ArrayList的自動擴容機制中,elementData數組至關於容器,容器不足時就會擴容,容量每每大於等於所存元素的個數。直接序列化會形成元素空間的浪費,特別是元素個數不少的狀況,這種浪費很是不划算。由此,這樣作的目的是爲了只序列化實際存儲的元素,節省資源。
3.4 在接口中存在實現代碼(違反)
List.java 475-484行
1 @SuppressWarnings({"unchecked", "rawtypes"}) 2 3 default void sort(Comparator<? super E> c) { 4 5 Object[] a = this.toArray(); 6 7 Arrays.sort(a, (Comparator) c); 8 9 ListIterator<E> i = this.listIterator(); 10 11 for (Object e : a) { 12 13 i.next(); 14 15 i.set((E) e); 16 17 } 18 19 }
通常而言,接口是一種契約,一種框架性協議,他能夠聲明常量,聲明抽象方法,也能夠繼承接口,但不能有具體實現。這代表其實現類都是同一種類型,或者是具有類似特徵的一個集合體,其約束着實現者,保證提供的服務是穩定的、可靠的。若是把實現代碼寫到接口中,那接口就綁定了可能變化的因素,隨時都有可能被拋棄,被更改,被重構。因此,接口中雖然能夠有實現,但應避免使用。因此合理的修改方式是,在List的子類中分別去實現sort方法。
但Java 8的新特性中,推出了默認方法(default methods),簡單來講,就是能夠在接口中定義一個已實現的方法,且該接口的實現類不須要實現該方法。這麼作的好處是爲了方便擴展已有接口。若是沒有默認方法,假如給JDK中的某個接口添加一個新的抽象方法,那麼全部實現了該接口的類都得修改,影響很大。
但使用默認方法,能夠給已有接口添加新方法,而不用修改該接口的實現類。固然,接口中新添加的默認方法,全部實現類也會繼承。這樣下降了接口與實現類之間的耦合度。
這樣來看,Java 8的新特性和高質量代碼規範產生了矛盾,因此仍是要根據實際狀況來分析,默認方法實現存在的必要性,使得工做更加高效。
3.5 警戒數組的淺拷貝(違反)
ArrayList.java 353-363行
1 public Object clone() { 2 3 try { 4 5 ArrayList<?> v = (ArrayList<?>) super.clone(); 6 7 v.elementData = Arrays.copyOf(elementData, size); 8 9 v.modCount = 0; 10 11 return v; 12 13 } catch (CloneNotSupportedException e) { 14 15 // this shouldn't happen, since we are Cloneable 16 17 throw new InternalError(e); 18 19 } 20 21 } 22 23 24 class Person implements Serializable{ 25 26 private int age; 27 28 private String name; 29 30 31 32 public Person(){}; 33 34 public Person(int age,String name){ 35 36 this.age=age; 37 38 this.name=name; 39 40 } 41 42 43 44 public String toString(){ 45 46 return this.name+"-->"+this.age; 47 48 } 49 50 }
舉例來講,對於上面這個JavaBean,經過new Person()構建3個對象,而後添加到第一個ArrayList,而後經過遍歷循環複製,添加到另外一個ArrayList,在調用add方法時,並無new Person()操做。所以,經過set方法修改屬性時,會破壞源數據,兩個ArrayList都會收到影響,緣由是淺拷貝;一樣,使用ArrayList的構造方法來複制幾個內容,一樣是淺拷貝。在clone()方法中的Arrays.copy()和System.arraycopy()也是不能對集合進行深拷貝的。
1 public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException { 2 3 ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 4 5 ObjectOutputStream out = new ObjectOutputStream(byteOut); 6 7 out.writeObject(src); 8 9 10 11 ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); 12 13 ObjectInputStream in = new ObjectInputStream(byteIn); 14 15 @SuppressWarnings("unchecked") 16 17 List<T> dest = (List<T>) in.readObject(); 18 19 return dest; 20 21 }
那麼經過實現Serializable接口後,使用序列化的方式能夠實現深拷貝,如上所示。由於在序列化的過程當中,咱們取出了原List中的值,並將其傳給了新的對象,那麼兩個List所含的對象指向的地址空間就不一樣了,因此是深拷貝,能夠解決上述例子中Person()類屬性修改則所有同時修改的問題。
3.6 顯示聲明UID(符合)
ArrayList.java 107-110行
1 public class ArrayList<E> extends AbstractList<E> 2 3 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 4 5 { 6 private static final long serialVersionUID = 8683452581122892189L;
7 }
類實現Serializable接口的目的是爲了可持久化,好比網絡傳輸或本地存儲。須要考慮的一個問題是,若是消息的生產者和消息的消費者所參考的類有差別,好比生產者中的類增長一個年齡屬性,而消費者沒有增長屬性。由於這是一個分佈式部署的應用,你甚至都不知道這個應用部署在何處,特別是經過廣播(broadcast)方式發消息的狀況,漏掉一兩個訂閱者很是正常。此時,不一致的狀況會致使InvalidClassException異常,緣由是序列化和反序列化所對應的類版本發生了變化,JVM不能把數據流轉換爲實例對象。JVM是經過SerialVersionUID來標識類的版本定義的,顯示聲明後,在反序列化時,就會比較UID是否相同,來確保類沒有發生改變,不然不執行反序列化過程。這是一個很好的校驗機制,保證類和對象數據的一致性。
3.7 基本類型數組轉換列表陷阱(違反)
Arrays.java 3799-3801行
1 public static <T> List<T> asList(T... a) { 2 3 return new ArrayList<>(a); 4 5 }
開發過程當中常常會使用Arrays和Collections這兩個工具類在數組和列表之間轉換,但也會有一些奇怪的問題。
1 public class Client65 { 2 3 public static void main(String[] args) { 4 5 int data [] = {1,2,3,4,5}; 6 7 List list= Arrays.asList(data); 8 9 System.out.println("列表中的元素數量是:"+list.size()); 10 11 } 12 13 }
如上代碼所示,理所固然地認爲list元素數量是5,但實際打印爲1。爲何經過asList方法轉換後就只有一個元素了呢?從該方法源碼可得,輸入一個變長參數,返回一個固定長度的列表。咱們知道基本類型是不能泛型化的,即8個基本類型不能做爲該方法的泛型參數,必須使用其包裝類型。但例子中傳入一個int類型的數組卻沒有報錯。由於Java中,數組能夠做爲一個對象,能夠泛型化,因此這裏咱們把int數組做爲了一個T類型,轉換時就只有一個int數組類型的元素了。打印list.get(0).getClass()發現,元素類型是class[I。JVM不可能輸出Array類型,由於其屬於java.lang.reflect包,是經過反射訪問數組元素的工具類。在java中任何一個一位數組的類型都是[I,緣由是Java並無定義數組這一個類,是編譯器編譯時產生的。
因此解決方案,一是經過程序員在調用asList方法前,封裝基本對象類型數組,二則是在該方法內部,首先判斷是否爲基本類型,對基本類型進行裝箱處理,而後傳給new ArrayList()返回給用戶。因此說asList這個方法的陷阱,不太優雅,容易致使程序邏輯混亂。
3.8 數組的真實類型必須爲泛型類型的子類型
ArrayList.java 408-416行
1 public <T> T[] toArray(T[] a) { 2 3 int size = size(); 4 5 if (a.length < size) 6 7 return Arrays.copyOf(this.a, size, 8 9 (Class<? extends T[]>) a.getClass()); 10 11 System.arraycopy(this.a, 0, a, 0, size); 12 13 if (a.length > size) 14 15 a[size] = null; 16 17 return a; 18 19 }
List接口的toArray方法能夠把一個集合轉化爲數組,可是使用不方便,返回的是一個Object數組,因此須要自行轉變。ToArray(T[] a)雖然返回的是T類型的數組,但還須要傳入一個T類型的數組,至關麻煩。咱們指望輸入的是一個泛型化的List,這樣就能夠轉化爲泛型數組。
1 public static <T> T[] toArray(List<T> list) { 2 3 T[] t = (T[]) new Object[list.size()]; 4 5 for (int i = 0, n = list.size(); i < n; i++) { 6 7 t[i] = list.get(i); 8 9 } 10 11 return t; 12 13 }
如上所示,對輸出的Object數組轉型爲T類型數組,以後遍歷List賦值給數組的每一個元素。使用List<String>做爲傳入參數,調用該方法時,編譯能夠經過,但運行異常,顯示類型轉換異常,也就是說不能把一個Object數組轉化爲String類型數組。問題在於,爲何Object數組不能向下轉型爲String數組,由於數組是容器,只有確保容器內元素類型和指望的類型有父子關係時才能轉換,Object數組只能保證數組內的元素是Object類型,不能保證他們都是String的父類型或子類型,因此轉換失敗。另外一個問題是,拋出異常的位置在main方法,而不是toArray方法,按理來講在toArray方法中進行類型的向下轉換,而不是main方法,但卻在main方法拋異常。緣由是編譯時泛型擦除,轉化時並非把Object轉換爲String類型,而是Object轉Object,徹底沒有必要。因此在main方法中,爲了可以實現對String數組的遍歷,就須要類型轉換,此時出現異常。
1 public static <T> T[] toArray(List<T> list,Class<T> tClass) { 2 3 //聲明並初始化一個T類型的數組 4 5 T[] t = (T[])Array.newInstance(tClass, list.size()); 6 7 for (int i = 0, n = list.size(); i < n; i++) { 8 9 t[i] = list.get(i); 10 11 } 12 13 return t; 14 15 }
因此爲了能實現上述需求,把泛型數組聲明爲泛型的子類型便可。經過反射類Array聲明瞭一個T類型的數組,因爲我麼你沒法在運行期得到泛型類型參數,只能經過調用者主動傳入T參數類型。
在這裏咱們看到,當一個泛型類(特別是泛型集合)轉變爲泛型數組時,泛型數組的真實類型不能是泛型的父類型(好比頂層類Object),只能是泛型類型的子類型(固然包括自身類型),不然就會出現類型轉換異常。源碼的設計給使用者形成了必定的困擾。
3.9 受檢異常儘量轉化爲非受檢異常(遵照)
ArrayList.java 655-666行
1 private void rangeCheck(int index) { 2 3 if (index >= size) 4 5 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 6 7 } 8 9 10 11 /** 12 13 * A version of rangeCheck used by add and addAll. 14 15 */ 16 17 private void rangeCheckForAdd(int index) { 18 19 if (index > size || index < 0) 20 21 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 22 23 }
受檢異常是正常邏輯的一種補償手段,特別是對於可靠性要求較高的系統。在某些條件下必須拋出受檢異常以便由城西進行補償處理。
但受檢異常使接口聲明脆弱,OOP要求儘可能多地面向接口編程,可提升代碼的擴展性、穩定性,可是涉及異常問題就不同了。隨着系統的開發,會有不少的實現類,他們一般須要拋出不一樣的異常。異常是對主邏輯的補充,但修改一個補充邏輯,會致使主邏輯也被修改,即實現類逆影響接口的情景。咱們知道實現類是不穩定的,而接口是穩定的,一旦定義了異常,則增長了接口的不穩定性,實現的變動會最終影響到調用者,破壞了封裝性,是違背迪米特法則的。
受檢異常也使得代碼的可讀性下降,一個方法增長受檢異常,則必須有一個調用者對異常進行處理。多個catch塊捕獲處理多個異常,大大下降代碼的可讀性。
當受檢異常威脅到了系統的安全性,穩定性,可靠性、正確性時,則必須處理,不能轉化爲非受檢異常,其它狀況則能夠轉化爲非受檢異常。
3.10 合理使用顯示鎖Lock(遵照)
CopyOnWriteArrayList.java 434-447行
1 public boolean add(E e) { 2 3 final ReentrantLock lock = this.lock; 4 5 lock.lock(); 6 7 try { 8 9 Object[] elements = getArray(); 10 11 int len = elements.length; 12 13 Object[] newElements = Arrays.copyOf(elements, len + 1); 14 15 newElements[len] = e; 16 17 setArray(newElements); 18 19 return true; 20 21 } finally { 22 23 lock.unlock(); 24 25 } 26 27 }
不少程序員會認爲,Lock類和synchronized關鍵字在代碼塊的併發性和內存上時語義是同樣的,都是保持代碼塊同時只有一個線程具備執行權。但實際狀況是,經過一個例子模擬,發現synchronized內部鎖保證了只有一個線性的運行權,其餘等待執行;而Lock顯示鎖未出現互斥狀況。在例子中同步資源是代碼塊,前者是類級別的鎖,然後者是對象級別的鎖。簡單來講,把Lock定義爲多線程類的私有屬性是起不到資源互斥做用的,除非把Lock定義爲全部線程共享變量。
因此,這樣來看,Lock支持更細粒度的鎖控制,假設讀寫鎖分離,寫操做不容許有讀寫操做同時存在,而讀操做時讀寫可併發執行,這樣的需求內部鎖很難實現,而Lock能夠。
爲了得到線程安全的ArrayList,可使用concurrent併發包下的CopyOnWriteArrayList類,這是一個CopyOnWrite容器,讀取元素是從原數組讀取的,添加元素是在複製的新數組上。讀寫分離,於是能夠在併發條件下進行不加鎖的讀取,讀取效率高,適用於讀操做遠大於寫操做的場景。這裏Lock顯示鎖的優越性就體現出來了,因此合理地使用Lock和synchronized能夠提升程序性能,實現不一樣的需求,也可使得代碼具備高質量。
參考:
[1] http://www.javashuo.com/article/p-sdwwwaum-mq.html
[3]https://www.cnblogs.com/selene/default.html?page=3 編寫高質量代碼:改善Java程序的151個建議
[4]https://www.cnblogs.com/chenpi/p/5897713.html Java 8默認方法
[5]https://www.cnblogs.com/aoguren/p/4767309.html ArrayList序列化技術細節詳解