今天咱們來探索一下Java集合類中的一些技術細節。主要是對一些比較容易被遺漏和誤解的知識點作一些講解和補充。可能不全面,還請諒解。java
本文參考:http://cmsblogs.com/?cat=5git
具體代碼在個人GitHub中能夠找到github
https://github.com/h2pl/MyTech編程
後端
https://h2pl.github.io/2018/05/13/collection8數組
更多關於Java後端學習的內容請到個人CSDN博客上查看:dom
https://blog.csdn.net/a724888函數
個人我的博客主要發原創文章,也歡迎瀏覽 https://h2pl.github.io/學習
集合是咱們在Java編程中使用很是普遍的,它就像大海,海納百川,像萬能容器,盛裝萬物,並且這個大海,萬能容器還能夠無限變大(若是條件容許)。當這個海、容器的量變得很是大的時候,它的初始容量就會顯得很重要了,由於挖海、擴容是須要消耗大量的人力物力財力的。大數據
一樣的道理,Collection的初始容量也顯得異常重要。因此:對於已知的情景,請爲集合指定初始容量。
public static void main(String[] args) {
StudentVO student = null;
long begin1 = System.currentTimeMillis();
List<StudentVO> list1 = new ArrayList<>();
for(int i = 0 ; i < 1000000; i++){
student = new StudentVO(i,"chenssy_"+i,i);
list1.add(student);
}
long end1 = System.currentTimeMillis();
System.out.println("list1 time:" + (end1 - begin1));
long begin2 = System.currentTimeMillis();
List<StudentVO> list2 = new ArrayList<>(1000000);
for(int i = 0 ; i < 1000000; i++){
student = new StudentVO(i,"chenssy_"+i,i);
list2.add(student);
}
long end2 = System.currentTimeMillis();
System.out.println("list2 time:" + (end2 - begin2));
}
上面代碼兩個list都是插入1000000條數據,只不過list1沒有沒有申請初始化容量,而list2初始化容量1000000。那運行結果以下:
list1 time:1638
list2 time:921
從上面的運行結果咱們能夠看出list2的速度是list1的兩倍左右。在前面LZ就提過,ArrayList的擴容機制是比較消耗資源的。咱們先看ArrayList的add方法:
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
public void ensureCapacity(int minCapacity) {
modCount++; //修改計數器
int oldCapacity = elementData.length;
//當前須要的長度超過了數組長度,進行擴容處理
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
//新的容量 = 舊容量 * 1.5 + 1
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
//數組拷貝,生成新的數組
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
ArrayList每次新增一個元素,就會檢測ArrayList的當前容量是否已經到達臨界點,若是到達臨界點則會擴容1.5倍。然而ArrayList的擴容以及數組的拷貝生成新的數組是至關耗資源的。因此若咱們事先已知集合的使用場景,知道集合的大概範圍,咱們最好是指定初始化容量,這樣對資源的利用會更加好,尤爲是大數據量的前提下,效率的提高和資源的利用會顯得更加具備優點。
在實際開發過程當中咱們常用asList講數組轉換爲List,這個方法使用起來很是方便,可是asList方法存在幾個缺陷:
使用8個基本類型數組轉換爲列表時會存在一個比較有味的缺陷。先看以下程序:
public static void main(String[] args) {
int[] ints = {1,2,3,4,5};
List list = Arrays.asList(ints);
System.out.println("list'size:" + list.size());
}
------------------------------------
outPut:
list'size:1
程序的運行結果並無像咱們預期的那樣是5而是逆天的1,這是什麼狀況?先看源碼:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
asList接受的參數是一個泛型的變長參數,咱們知道基本數據類型是沒法泛型化的,也就是說8個基本類型是沒法做爲asList的參數的, 要想做爲泛型參數就必須使用其所對應的包裝類型。可是這個這個實例中爲何沒有出錯呢?
由於該實例是將int類型的數組當作其參數,而在Java中數組是一個對象,它是能夠泛型化的。因此該例子是不會產生錯誤的。既然例子是將整個int類型的數組當作泛型參數,那麼通過asList轉換就只有一個int 的列表了。以下:
public static void main(String[] args) {
int[] ints = {1,2,3,4,5};
List list = Arrays.asList(ints);
System.out.println("list 的類型:" + list.get(0).getClass());
System.out.println("list.get(0) == ints:" + list.get(0).equals(ints));
}
outPut: list 的類型:class [I list.get(0) == ints:true 從這個運行結果咱們能夠充分證實list裏面的元素就是int數組。弄清楚這點了,那麼修改方法也就一目瞭然了:將int 改變爲Integer。
public static void main(String[] args) {
Integer[] ints = {1,2,3,4,5};
List list = Arrays.asList(ints);
System.out.println("list'size:" + list.size());
System.out.println("list.get(0) 的類型:" + list.get(0).getClass());
System.out.println("list.get(0) == ints[0]:" + list.get(0).equals(ints[0]));
}
----------------------------------------
outPut:
list'size:5
list.get(0) 的類型:class java.lang.Integer
list.get(0) == ints[0]:true
對於上面的實例咱們再作一個小小的修改:
public static void main(String[] args) {
Integer[] ints = {1,2,3,4,5};
List list = Arrays.asList(ints);
list.add(6);
}
該實例就是講ints經過asList轉換爲list 類別,而後再經過add方法加一個元素,這個實例簡單的不能再簡單了,可是運行結果呢?打出咱們所料:
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(Unknown Source) at java.util.AbstractList.add(Unknown Source) at com.chenssy.test.arrayList.AsListTest.main(AsListTest.java:10)
運行結果盡然拋出UnsupportedOperationException異常,該異常表示list不支持add方法。這就讓咱們鬱悶了,list怎麼可能不支持add方法呢?難道jdk腦殼堵塞了?咱們再看asList的源碼:
public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
asList接受參數後,直接new 一個ArrayList,到這裏看應該是沒有錯誤的啊?別急,再往下看:
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{ private static final long serialVersionUID = -2764017481108945198L; private final E[] a; ArrayList(E[] array) { if (array==null) throw new NullPointerException(); a = array; } //................. }
這是ArrayList的源碼,從這裏咱們能夠看出,此ArrayList不是java.util.ArrayList,它是Arrays的內部類。
該內部類提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改變list結果的方法從AbstractList父類繼承過來,同時這些方法也比較奇葩,它直接拋出UnsupportedOperationException異常:
public boolean add(E e) { add(size(), e); return true; } public E set(int index, E element) { throw new UnsupportedOperationException(); } public void add(int index, E element) { throw new UnsupportedOperationException(); } public E remove(int index) { throw new UnsupportedOperationException(); }
經過這些代碼能夠看出asList返回的列表只不過是一個披着list的外衣,它並無list的基本特性(變長)。該list是一個長度不可變的列表,傳入參數的數組有多長,其返回的列表就只能是多長。因此::不要試圖改變asList返回的列表,不然你會自食苦果。
咱們常用subString方法來對String對象進行分割處理,同時咱們也可使用subList、subMap、subSet來對List、Map、Set進行分割處理,可是這個分割存在某些瑕疵。
首先咱們先看以下實例:
public static void main(String[] args) { List list1 = new ArrayList(); list1.add(1); list1.add(2); //經過構造函數新建一個包含list1的列表 list2 List<Integer> list2 = new ArrayList<Integer>(list1); //經過subList生成一個與list1同樣的列表 list3 List<Integer> list3 = list1.subList(0, list1.size()); //修改list3 list3.add(3); System.out.println("list1 == list2:" + list1.equals(list2)); System.out.println("list1 == list3:" + list1.equals(list3)); }
這個例子很是簡單,無非就是經過構造函數、subList從新生成一個與list1同樣的list,而後修改list3,最後比較list1 == list2?、list1 == list3?。
按照咱們常規的思路應該是這樣的:由於list3經過add新增了一個元素,那麼它確定與list1不等,而list2是經過list1構造出來的,因此應該相等,因此結果應該是:
list1 == list2:true //錯誤結果 list1 == list3: false //錯誤結果
首先咱們先不論結果的正確與否,咱們先看subList的源碼:
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); }
subListRangeCheck方式是判斷fromIndex、toIndex是否合法,若是合法就直接返回一個subList對象,注意在產生該new該對象的時候傳遞了一個參數 this ,該參數很是重要,由於他表明着原始list。
/** * 繼承AbstractList類,實現RandomAccess接口 */ private class SubList extends AbstractList<E> implements RandomAccess { private final AbstractList<E> parent; //列表 private final int parentOffset; private final int offset; int size; //構造函數 SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; } //set方法 public E set(int index, E e) { rangeCheck(index); checkForComodification(); E oldValue = ArrayList.this.elementData(offset + index); ArrayList.this.elementData[offset + index] = e; return oldValue; } //get方法 public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //add方法 public void add(int index, E e) { rangeCheckForAdd(index); checkForComodification(); parent.add(parentOffset + index, e); this.modCount = parent.modCount; this.size++; } //remove方法 public E remove(int index) { rangeCheck(index); checkForComodification(); E result = parent.remove(parentOffset + index); this.modCount = parent.modCount; this.size--; return result; } }
該SubList是ArrayList的內部類,它與ArrayList同樣,都是繼承AbstractList和實現RandomAccess接口。同時也提供了get、set、add、remove等list經常使用的方法。可是它的構造函數有點特殊,在該構造函數中有兩個地方須要注意:
一、this.parent = parent;而parent就是在前面傳遞過來的list,也就是說this.parent就是原始list的引用。
二、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時在構造函數中它甚至將modCount(fail-fast機制)傳遞過來了。
咱們再看get方法,在get方法中return ArrayList.this.elementData(offset + index);
這段代碼能夠清晰代表get所返回就是原列表offset + index位置的元素。一樣的道理還有add方法裏面的:
parent.add(parentOffset + index, e); this.modCount = parent.modCount; remove方法裏面的
E result = parent.remove(parentOffset + index); this.modCount = parent.modCount;
誠然,到了這裏咱們能夠判斷subList返回的SubList一樣也是AbstractList的子類,同時它的方法如get、set、add、remove等都是在原列表上面作操做,它並無像subString同樣生成一個新的對象。
因此subList返回的只是原列表的一個視圖,它全部的操做最終都會做用在原列表上。
那麼從這裏的分析咱們能夠得出上面的結果應該偏偏與咱們上面的答案相反:
list1 == list2:false list1 == list3:true
從上面咱們知道subList生成的子列表只是原列表的一個視圖而已,若是咱們操做子列表它產生的做用都會在原列表上面表現,可是若是咱們操做原列表會產生什麼狀況呢?
public static void main(String[] args) { List list1 = new ArrayList(); list1.add(1); list1.add(2); //經過subList生成一個與list1同樣的列表 list3 List<Integer> list3 = list1.subList(0, list1.size()); //修改list1 list1.add(3); System.out.println("list1'size:" + list1.size()); System.out.println("list3'size:" + list3.size()); }
該實例若是不產生意外,那麼他們兩個list的大小都應該都是3,可是恰恰事與願違,事實上咱們獲得的結果是這樣的:
list1'size:3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$SubList.checkForComodification(Unknown Source) at java.util.ArrayList$SubList.size(Unknown Source) at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)
list1正常輸出,可是list3就拋出ConcurrentModificationException異常,看過我另外一篇博客的同仁確定對這個異常很是,fail-fast?不錯就是fail-fast機制,在fail-fast機制中,LZ花了不少力氣來說述這個異常,因此這裏LZ就不對這個異常多講了。咱們再看size方法:
public int size() { checkForComodification(); return this.size; }
size方法首先會經過checkForComodification驗證,而後再返回this.size。
private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) throw new ConcurrentModificationException(); }
該方法代表當原列表的modCount與this.modCount不相等時就會拋出ConcurrentModificationException。
同時咱們知道modCount 在new的過程當中 「繼承」了原列表modCount,只有在修改該列表(子列表)時纔會修改該值(先表如今原列表後做用於子列表)。
而在該實例中咱們是操做原列表,原列表的modCount固然不會反應在子列表的modCount上啦,因此纔會拋出該異常。
對於子列表視圖,它是動態生成的,生成以後就不要操做原列表了,不然必然都致使視圖的不穩定而拋出異常。最好的辦法就是將原列表設置爲只讀狀態,要操做就操做子列表:
//經過subList生成一個與list1同樣的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//對list1設置爲只讀狀態
list1 = Collections.unmodifiableList(list1);
在開發過程當中咱們必定會遇到這樣一個問題:獲取一堆數據後,須要刪除某段數據。例如,有一個列表存在1000條記錄,咱們須要刪除100-200位置處的數據,可能咱們會這樣處理:
for(int i = 0 ; i < list1.size() ; i++){ if(i >= 100 && i <= 200){ list1.remove(i); /* * 固然這段代碼存在問題,list remove以後後面的元素會填充上來, * 因此須要對i進行簡單的處理,固然這個不是這裏討論的問題。 */ } }
這個應該是咱們大部分人的處理方式吧,其實還有更好的方法,利用subList。在前面LZ已經講過,子列表的操做都會反映在原列表上。因此下面一行代碼所有搞定:
list1.subList(100, 200).clear();
簡單而不失華麗!!!!!
在Java中咱們常使用Comparable接口來實現排序,其中compareTo是實現該接口方法。咱們知道compareTo返回0表示兩個對象相等,返回正數表示大於,返回負數表示小於。同時咱們也知道equals也能夠判斷兩個對象是否相等,那麼他們二者之間是否存在關聯關係呢?
public class Student implements Comparable<Student>{ private String id; private String name; private int age; public Student(String id,String name,int age){ this.id = id; this.name = name; this.age = age; } public boolean equals(Object obj){ if(obj == null){ return false; } if(this == obj){ return true; } if(obj.getClass() != this.getClass()){ return false; } Student student = (Student)obj; if(!student.getName().equals(getName())){ return false; } return true; } public int compareTo(Student student) { return this.age - student.age; } /** 省略getter、setter方法 */ }
Student類實現Comparable接口和實現equals方法,其中compareTo是根據age來比對的,equals是根據name來比對的。
public static void main(String[] args){ List<Student> list = new ArrayList<>(); list.add(new Student("1", "chenssy1", 24)); list.add(new Student("2", "chenssy1", 26)); Collections.sort(list); //排序 Student student = new Student("2", "chenssy1", 26); //檢索student在list中的位置 int index1 = list.indexOf(student); int index2 = Collections.binarySearch(list, student); System.out.println("index1 = " + index1); System.out.println("index2 = " + index2); }
按照常規思路來講應該二者index是一致的,由於他們檢索的是同一個對象,可是很是遺憾,其運行結果:
index1 = 0 index2 = 1
爲何會產生這樣不一樣的結果呢?這是由於indexOf和binarySearch的實現機制不一樣。
indexOf是基於equals來實現的只要equals返回TRUE就認爲已經找到了相同的元素。
而binarySearch是基於compareTo方法的,當compareTo返回0 時就認爲已經找到了該元素。
在咱們實現的Student類中咱們覆寫了compareTo和equals方法,可是咱們的compareTo、equals的比較依據不一樣,一個是基於age、一個是基於name。
比較依據不一樣那麼獲得的結果頗有可能會不一樣。因此知道了緣由,咱們就好修改了:將二者之間的比較依據保持一致便可。
對於compareTo和equals兩個方法咱們能夠總結爲:compareTo是判斷元素在排序中的位置是否相等,equals是判斷元素是否相等,既然一個決定排序位置,一個決定相等,因此咱們很是有必要確保當排序位置相同時,其equals也應該相等。