這篇文章主要講解了“Java集合類怎么使用”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java集合類怎么使用”吧!
創(chuàng)新互聯(lián)建站專注于企業(yè)網(wǎng)絡(luò)營銷推廣、網(wǎng)站重做改版、準(zhǔn)格爾網(wǎng)站定制設(shè)計、自適應(yīng)品牌網(wǎng)站建設(shè)、H5網(wǎng)站設(shè)計、電子商務(wù)商城網(wǎng)站建設(shè)、集團公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計等建站業(yè)務(wù),價格優(yōu)惠性價比高,為準(zhǔn)格爾等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
集合是我們在Java編程中使用非常廣泛的,它就像大海,海納百川,像萬能容器,盛裝萬物,而且這個大海,萬能容器還可以無限變大(如果條件允許)。當(dāng)這個海、容器的量變得非常大的時候,它的初始容量就會顯得很重要了,因為挖海、擴容是需要消耗大量的人力物力財力的。
同樣的道理,Collection的初始容量也顯得異常重要。所以:對于已知的情景,請為集合指定初始容量。
public static void main(String[] args) { StudentVO student = null; long begin1 = System.currentTimeMillis(); Listlist1 = 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 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條數(shù)據(jù),只不過list1沒有沒有申請初始化容量,而list2初始化容量1000000。那運行結(jié)果如下:
list1 time:1638 list2 time:921
從上面的運行結(jié)果我們可以看出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++; //修改計數(shù)器 int oldCapacity = elementData.length; //當(dāng)前需要的長度超過了數(shù)組長度,進行擴容處理 if (minCapacity > oldCapacity) { Object oldData[] = elementData; //新的容量 = 舊容量 * 1.5 + 1 int newCapacity = (oldCapacity * 3)/2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; //數(shù)組拷貝,生成新的數(shù)組 elementData = Arrays.copyOf(elementData, newCapacity); } }
ArrayList每次新增一個元素,就會檢測ArrayList的當(dāng)前容量是否已經(jīng)到達臨界點,如果到達臨界點則會擴容1.5倍。然而ArrayList的擴容以及數(shù)組的拷貝生成新的數(shù)組是相當(dāng)耗資源的。所以若我們事先已知集合的使用場景,知道集合的大概范圍,我們最好是指定初始化容量,這樣對資源的利用會更加好,尤其是大數(shù)據(jù)量的前提下,效率的提升和資源的利用會顯得更加具有優(yōu)勢。
在實際開發(fā)過程中我們經(jīng)常使用asList講數(shù)組轉(zhuǎn)換為List,這個方法使用起來非常方便,但是asList方法存在幾個缺陷:
使用8個基本類型數(shù)組轉(zhuǎn)換為列表時會存在一個比較有味的缺陷。先看如下程序:
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
程序的運行結(jié)果并沒有像我們預(yù)期的那樣是5而是逆天的1,這是什么情況?先看源碼:
public staticList asList(T... a) { return new ArrayList<>(a); }
asList接受的參數(shù)是一個泛型的變長參數(shù),我們知道基本數(shù)據(jù)類型是無法發(fā)型化的,也就是說8個基本類型是無法作為asList的參數(shù)的, 要想作為泛型參數(shù)就必須使用其所對應(yīng)的包裝類型。但是這個這個實例中為什么沒有出錯呢?
因為該實例是將int類型的數(shù)組當(dāng)做其參數(shù),而在Java中數(shù)組是一個對象,它是可以泛型化的。所以該例子是不會產(chǎn)生錯誤的。既然例子是將整個int類型的數(shù)組當(dāng)做泛型參數(shù),那么經(jīng)過asList轉(zhuǎn)換就只有一個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 從這個運行結(jié)果我們可以充分證明list里面的元素就是int數(shù)組。弄清楚這點了,那么修改方法也就一目了然了:將int 改變?yōu)镮nteger。
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轉(zhuǎn)換為list 類別,然后再通過add方法加一個元素,這個實例簡單的不能再簡單了,但是運行結(jié)果呢?打出我們所料:
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)
運行結(jié)果盡然拋出UnsupportedOperationException異常,該異常表示list不支持add方法。這就讓我們郁悶了,list怎么可能不支持add方法呢?難道jdk腦袋堵塞了?我們再看asList的源碼:
public staticList asList(T... a) { return new ArrayList<>(a); }
asList接受參數(shù)后,直接new 一個ArrayList,到這里看應(yīng)該是沒有錯誤的?。縿e急,再往下看:
private static class ArrayListextends AbstractList 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的內(nèi)部類。
該內(nèi)部類提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改變list結(jié)果的方法從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的外衣,它并沒有l(wèi)ist的基本特性(變長)。該list是一個長度不可變的列表,傳入?yún)?shù)的數(shù)組有多長,其返回的列表就只能是多長。所以::不要試圖改變asList返回的列表,否則你會自食苦果。
我們經(jīng)常使用subString方法來對String對象進行分割處理,同時我們也可以使用subList、subMap、subSet來對List、Map、Set進行分割處理,但是這個分割存在某些瑕疵。
首先我們先看如下實例:
public static void main(String[] args) { List list1 = new ArrayList(); list1.add(1); list1.add(2);
//通過構(gòu)造函數(shù)新建一個包含list1的列表 list2 Listlist2 = new ArrayList (list1); //通過subList生成一個與list1一樣的列表 list3 List 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)); }
這個例子非常簡單,無非就是通過構(gòu)造函數(shù)、subList重新生成一個與list1一樣的list,然后修改list3,最后比較list1 == list2?、list1 == list3?。
按照我們常規(guī)的思路應(yīng)該是這樣的:因為list3通過add新增了一個元素,那么它肯定與list1不等,而list2是通過list1構(gòu)造出來的,所以應(yīng)該相等,所以結(jié)果應(yīng)該是:
list1 == list2:true list1 == list3: false
首先我們先不論結(jié)果的正確與否,我們先看subList的源碼:
public ListsubList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); }
subListRangeCheck方式是判斷fromIndex、toIndex是否合法,如果合法就直接返回一個subList對象,注意在產(chǎn)生該new該對象的時候傳遞了一個參數(shù) this ,該參數(shù)非常重要,因為他代表著原始list。
/** * 繼承AbstractList類,實現(xiàn)RandomAccess接口 */ private class SubList extends AbstractList implements RandomAccess { private final AbstractList parent; //列表 private final int parentOffset;
private final int offset; int size;
//構(gòu)造函數(shù) SubList(AbstractListparent, 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; } }
該SubLsit是ArrayList的內(nèi)部類,它與ArrayList一樣,都是繼承AbstractList和實現(xiàn)RandomAccess接口。同時也提供了get、set、add、remove等list常用的方法。但是它的構(gòu)造函數(shù)有點特殊,在該構(gòu)造函數(shù)中有兩個地方需要注意:
1、this.parent = parent;而parent就是在前面?zhèn)鬟f過來的list,也就是說this.parent就是原始list的引用。
2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時在構(gòu)造函數(shù)中它甚至將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返回的只是原列表的一個視圖,它所有的操作最終都會作用在原列表上。
那么從這里的分析我們可以得出上面的結(jié)果應(yīng)該恰恰與我們上面的答案相反:
list1 == list2:false list1 == list3:true
從上面我們知道subList生成的子列表只是原列表的一個視圖而已,如果我們操作子列表它產(chǎn)生的作用都會在原列表上面表現(xiàn),但是如果我們操作原列表會產(chǎn)生什么情況呢?
public static void main(String[] args) { List list1 = new ArrayList(); list1.add(1); list1.add(2);
//通過subList生成一個與list1一樣的列表 list3 Listlist3 = list1.subList(0, list1.size()); //修改list1 list1.add(3); System.out.println("list1'size:" + list1.size()); System.out.println("list3'size:" + list3.size()); }
該實例如果不產(chǎn)生意外,那么他們兩個list的大小都應(yīng)該都是3,但是偏偏事與愿違,事實上我們得到的結(jié)果是這樣的:
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花了很多力氣來講述這個異常,所以這里L(fēng)Z就不對這個異常多講了。我們再看size方法:
public int size() { checkForComodification(); return this.size; }
size方法首先會通過checkForComodification驗證,然后再返回this.size。
private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) throw new ConcurrentModificationException(); }
該方法表明當(dāng)原列表的modCount與this.modCount不相等時就會拋出ConcurrentModificationException。
同時我們知道m(xù)odCount 在new的過程中 "繼承"了原列表modCount,只有在修改該列表(子列表)時才會修改該值(先表現(xiàn)在原列表后作用于子列表)。
而在該實例中我們是操作原列表,原列表的modCount當(dāng)然不會反應(yīng)在子列表的modCount上啦,所以才會拋出該異常。
對于子列表視圖,它是動態(tài)生成的,生成之后就不要操作原列表了,否則必然都導(dǎo)致視圖的不穩(wěn)定而拋出異常。最好的辦法就是將原列表設(shè)置為只讀狀態(tài),要操作就操作子列表:
//通過subList生成一個與list1一樣的列表 list3
Listlist3 = list1.subList(0, list1.size());
//對list1設(shè)置為只讀狀態(tài)
list1 = Collections.unmodifiableList(list1);
在開發(fā)過程中我們一定會遇到這樣一個問題:獲取一堆數(shù)據(jù)后,需要刪除某段數(shù)據(jù)。例如,有一個列表存在1000條記錄,我們需要刪除100-200位置處的數(shù)據(jù),可能我們會這樣處理:
for(int i = 0 ; i < list1.size() ; i++){ if(i >= 100 && i <= 200){ list1.remove(i); /* * 當(dāng)然這段代碼存在問題,list remove之后后面的元素會填充上來, * 所以需要對i進行簡單的處理,當(dāng)然這個不是這里討論的問題。 */ } }
這個應(yīng)該是我們大部分人的處理方式吧,其實還有更好的方法,利用subList。在前面LZ已經(jīng)講過,子列表的操作都會反映在原列表上。所以下面一行代碼全部搞定:
list1.subList(100, 200).clear();
簡單而不失華麗?。。。?!
在Java中我們常使用Comparable接口來實現(xiàn)排序,其中compareTo是實現(xiàn)該接口方法。我們知道compareTo返回0表示兩個對象相等,返回正數(shù)表示大于,返回負(fù)數(shù)表示小于。同時我們也知道equals也可以判斷兩個對象是否相等,那么他們兩者之間是否存在關(guān)聯(lián)關(guān)系呢?
public class Student implements Comparable{ 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類實現(xiàn)Comparable接口和實現(xiàn)equals方法,其中compareTo是根據(jù)age來比對的,equals是根據(jù)name來比對的。
public static void main(String[] args){ Listlist = 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); }
按照常規(guī)思路來說應(yīng)該兩者index是一致的,因為他們檢索的是同一個對象,但是非常遺憾,其運行結(jié)果:
index1 = 0 index2 = 1
為什么會產(chǎn)生這樣不同的結(jié)果呢?這是因為indexOf和binarySearch的實現(xiàn)機制不同。
indexOf是基于equals來實現(xiàn)的只要equals返回TRUE就認(rèn)為已經(jīng)找到了相同的元素。
而binarySearch是基于compareTo方法的,當(dāng)compareTo返回0 時就認(rèn)為已經(jīng)找到了該元素。
在我們實現(xiàn)的Student類中我們覆寫了compareTo和equals方法,但是我們的compareTo、equals的比較依據(jù)不同,一個是基于age、一個是基于name。
比較依據(jù)不同那么得到的結(jié)果很有可能會不同。所以知道了原因,我們就好修改了:將兩者之間的比較依據(jù)保持一致即可。
對于compareTo和equals兩個方法我們可以總結(jié)為:compareTo是判斷元素在排序中的位置是否相等,equals是判斷元素是否相等,既然一個決定排序位置,一個決定相等,所以我們非常有必要確保當(dāng)排序位置相同時,其equals也應(yīng)該相等。
使其相等的方式就是兩者應(yīng)該依附于相同的條件。當(dāng)compareto相等時equals也應(yīng)該相等,而compareto不相等時equals不應(yīng)該相等,并且compareto依據(jù)某些屬性來決定排序。
今天我們來探索一下HashSet,TreeSet與LinkedHashSet的基本原理與源碼實現(xiàn),由于這三個set都是基于之前文章的三個map進行實現(xiàn)的,所以推薦大家先看一下前面有關(guān)map的文章,結(jié)合使用味道更佳。
具體代碼在我的GitHub中可以找到
https://github.com/h3pl/MyTech
文章首發(fā)于我的個人博客:
https://h3pl.github.io/2018/05/12/collection7
更多關(guān)于Java后端學(xué)習(xí)的內(nèi)容請到我的CSDN博客上查看:
https://blog.csdn.net/a724888
我的個人博客主要發(fā)原創(chuàng)文章,也歡迎瀏覽 https://h3pl.github.io/
本文參考 http://cmsblogs.com/?p=599
HashSet
定義 public class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable
HashSet繼承AbstractSet類,實現(xiàn)Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干實現(xiàn),從而最大限度地減少了實現(xiàn)此接口所需的工作。 ==Set接口是一種不包括重復(fù)元素的Collection,它維持它自己的內(nèi)部排序,所以隨機訪問沒有任何意義。==
本文基于1.8jdk進行源碼分析。
基本屬性
基于HashMap實現(xiàn),底層使用HashMap保存所有元素 private transient HashMap
//定義一個Object對象作為HashMap的value private static final Object PRESENT = new Object();
構(gòu)造函數(shù) /** * 默認(rèn)構(gòu)造函數(shù) * 初始化一個空的HashMap,并使用默認(rèn)初始容量為16和加載因子0.75。 */ public HashSet() { map = new HashMap<>(); }
/** * 構(gòu)造一個包含指定 collection 中的元素的新 set。 */ public HashSet(Collection extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } /** * 構(gòu)造一個新的空 set,其底層 HashMap 實例具有指定的初始容量和指定的加載因子 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } /** * 構(gòu)造一個新的空 set,其底層 HashMap 實例具有指定的初始容量和默認(rèn)的加載因子(0.75)。 */ public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } /** * 在API中我沒有看到這個構(gòu)造函數(shù),今天看源碼才發(fā)現(xiàn)(原來訪問權(quán)限為包權(quán)限,不對外公開的) * 以指定的initialCapacity和loadFactor構(gòu)造一個新的空鏈接哈希集合。 * dummy 為標(biāo)識 該構(gòu)造函數(shù)主要作用是對LinkedHashSet起到一個支持作用 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
從構(gòu)造函數(shù)中可以看出HashSet所有的構(gòu)造都是構(gòu)造出一個新的HashMap,其中最后一個構(gòu)造函數(shù),為包訪問權(quán)限是不對外公開,僅僅只在使用LinkedHashSet時才會發(fā)生作用。
方法
既然HashSet是基于HashMap,那么對于HashSet而言,其方法的實現(xiàn)過程是非常簡單的。 public Iterator iterator() { return map.keySet().iterator(); }
iterator()方法返回對此 set 中元素進行迭代的迭代器。返回元素的順序并不是特定的。
底層調(diào)用HashMap的keySet返回所有的key,這點反應(yīng)了HashSet中的所有元素都是保存在HashMap的key中,value則是使用的PRESENT對象,該對象為static final。 public int size() { return map.size(); } size()返回此 set 中的元素的數(shù)量(set 的容量)。底層調(diào)用HashMap的size方法,返回HashMap容器的大小。
public boolean isEmpty() { return map.isEmpty(); } isEmpty(),判斷HashSet()集合是否為空,為空返回 true,否則返回false。
public boolean contains(Object o) { return map.containsKey(o); }
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
//最終調(diào)用該方法進行節(jié)點查找 final Node
contains(),判斷某個元素是否存在于HashSet()中,存在返回true,否則返回false。更加確切的講應(yīng)該是要滿足這種關(guān)系才能返回true:(o==null ? e==null : o.equals(e))。底層調(diào)用containsKey判斷HashMap的key值是否為空。 public boolean add(E e) { return map.put(e, PRESENT)==null; }
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
map的put方法: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node
//確認(rèn)初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果桶為空,直接插入新元素,也就是entry if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Nodee; K k; //如果沖突,分為三種情況 //key相等時讓舊entry等于新entry即可 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //紅黑樹情況 else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else { //如果key不相等,則連成鏈表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
}
這里注意一點,hashset只是不允許重復(fù)的元素加入,而不是不允許元素連成鏈表,因為只要key的equals方法判斷為true時它們是相等的,此時會發(fā)生value的替換,因為所有entry的value一樣,所以和沒有插入時一樣的。
而當(dāng)兩個hashcode相同但key不相等的entry插入時,仍然會連成一個鏈表,長度超過8時依然會和hashmap一樣擴展成紅黑樹,看完源碼之后筆者才明白自己之前理解錯了。所以看源碼還是蠻有好處的。hashset基本上就是使用hashmap的方法再次實現(xiàn)了一遍而已,只不過value全都是同一個object,讓你以為相同元素沒有插入,事實上只是value替換成和原來相同的值而已。
當(dāng)add方法發(fā)生沖突時,如果key相同,則替換value,如果key不同,則連成鏈表。
add()如果此 set 中尚未包含指定元素,則添加指定元素。如果此Set沒有包含滿足(e==null ? e2==null : e.equals(e2)) 的e2時,則將e2添加到Set中,否則不添加且返回false。
由于底層使用HashMap的put方法將key = e,value=PRESENT構(gòu)建成key-value鍵值對,當(dāng)此e存在于HashMap的key中,則value將會覆蓋原有value,但是key保持不變,所以如果將一個已經(jīng)存在的e元素添加中HashSet中,新添加的元素是不會保存到HashMap中,所以這就滿足了HashSet中元素不會重復(fù)的特性。 public boolean remove(Object o) { return map.remove(o)==PRESENT; }
remove如果指定元素存在于此 set 中,則將其移除。底層使用HashMap的remove方法刪除指定的Entry。 public void clear() { map.clear(); }
clear從此 set 中移除所有元素。底層調(diào)用HashMap的clear方法清除所有的Entry。 public Object clone() { try { HashSet newSet = (HashSet) super.clone(); newSet.map = (HashMap
clone返回此 HashSet 實例的淺表副本:并沒有復(fù)制這些元素本身。
后記:
由于HashSet底層使用了HashMap實現(xiàn),使其的實現(xiàn)過程變得非常簡單,如果你對HashMap比較了解,那么HashSet簡直是小菜一碟。有兩個方法對HashMap和HashSet而言是非常重要的,下篇將詳細(xì)講解hashcode和equals。
TreeSet
與HashSet是基于HashMap實現(xiàn)一樣,TreeSet同樣是基于TreeMap實現(xiàn)的。在《Java提高篇(二七)-----TreeMap》中LZ詳細(xì)講解了TreeMap實現(xiàn)機制,如果客官詳情看了這篇博文或者多TreeMap有比較詳細(xì)的了解,那么TreeSet的實現(xiàn)對您是喝口水那么簡單。
TreeSet定義
我們知道TreeMap是一個有序的二叉樹,那么同理TreeSet同樣也是一個有序的,它的作用是提供有序的Set集合。通過源碼我們知道TreeSet基礎(chǔ)AbstractSet,實現(xiàn)NavigableSet、Cloneable、Serializable接口。
其中AbstractSet提供 Set 接口的骨干實現(xiàn),從而最大限度地減少了實現(xiàn)此接口所需的工作。
NavigableSet是擴展的 SortedSet,具有了為給定搜索目標(biāo)報告最接近匹配項的導(dǎo)航方法,這就意味著它支持一系列的導(dǎo)航方法。比如查找與指定目標(biāo)最匹配項。Cloneable支持克隆,Serializable支持序列化。 public class TreeSet extends AbstractSet implements NavigableSet, Cloneable, java.io.Serializable
同時在TreeSet中定義了如下幾個變量。 private transient NavigableMap
//PRESENT會被當(dāng)做Map的value與key構(gòu)建成鍵值對 private static final Object PRESENT = new Object();
其構(gòu)造方法: //默認(rèn)構(gòu)造方法,根據(jù)其元素的自然順序進行排序
public TreeSet() { this(new TreeMap
//構(gòu)造一個包含指定 collection 元素的新 TreeSet,它按照其元素的自然順序進行排序。 public TreeSet(Comparator super E> comparator) { this(new TreeMap<>(comparator)); }
//構(gòu)造一個新的空 TreeSet,它根據(jù)指定比較器進行排序。 public TreeSet(Collection extends E> c) { this(); addAll(c); }
//構(gòu)造一個與指定有序 set 具有相同映射關(guān)系和相同排序的新 TreeSet。 public TreeSet(SortedSet s) { this(s.comparator()); addAll(s); }
TreeSet(NavigableMap
二、TreeSet主要方法
1、add:將指定的元素添加到此 set(如果該元素尚未存在于 set 中)。 public boolean add(E e) { return m.put(e, PRESENT)==null; }
public V put(K key, V value) { Entry
root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entryparent; // split comparator and comparable paths Comparator super K> cpr = comparator; //非空樹,根據(jù)傳入比較器進行節(jié)點的插入位置查找 if (cpr != null) { do { parent = t; //節(jié)點比根節(jié)點小,則找左子樹,否則找右子樹 cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; //如果key的比較返回值相等,直接更新值(一般compareto相等時equals方法也相等) 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); } //查找的節(jié)點為空,直接插入,默認(rèn)為紅節(jié)點 Entry e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; //插入后進行紅黑樹調(diào)整 fixAfterInsertion(e); size++; modCount++; return null;
}
2、get:獲取元素 public V get(Object key) { Entry
該方法與put的流程類似,只不過是把插入換成了查找
3、ceiling:返回此 set 中大于等于給定元素的最小元素;如果不存在這樣的元素,則返回 null。 public E ceiling(E e) { return m.ceilingKey(e); }
4、clear:移除此 set 中的所有元素。 public void clear() { m.clear(); }
5、clone:返回 TreeSet 實例的淺表副本。屬于淺拷貝。 public Object clone() { TreeSet clone = null; try { clone = (TreeSet) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); }
clone.m = new TreeMap<>(m); return clone; }
6、comparator:返回對此 set 中的元素進行排序的比較器;如果此 set 使用其元素的自然順序,則返回 null。 public Comparator super E> comparator() { return m.comparator(); }
7、contains:如果此 set 包含指定的元素,則返回 true。 public boolean contains(Object o) { return m.containsKey(o); }
8、descendingIterator:返回在此 set 元素上按降序進行迭代的迭代器。 public Iterator descendingIterator() { return m.descendingKeySet().iterator(); }
9、descendingSet:返回此 set 中所包含元素的逆序視圖。 public NavigableSet descendingSet() { return new TreeSet<>(m.descendingMap()); }
10、first:返回此 set 中當(dāng)前第一個(最低)元素。 public E first() { return m.firstKey(); }
11、floor:返回此 set 中小于等于給定元素的最大元素;如果不存在這樣的元素,則返回 null。 public E floor(E e) { return m.floorKey(e); }
12、headSet:返回此 set 的部分視圖,其元素嚴(yán)格小于 toElement。 public SortedSet headSet(E toElement) { return headSet(toElement, false); }
13、higher:返回此 set 中嚴(yán)格大于給定元素的最小元素;如果不存在這樣的元素,則返回 null。 public E higher(E e) { return m.higherKey(e); }
14、isEmpty:如果此 set 不包含任何元素,則返回 true。 public boolean isEmpty() { return m.isEmpty(); }
15、iterator:返回在此 set 中的元素上按升序進行迭代的迭代器。 public Iterator iterator() { return m.navigableKeySet().iterator(); }
16、last:返回此 set 中當(dāng)前最后一個(最高)元素。 public E last() { return m.lastKey(); }
17、lower:返回此 set 中嚴(yán)格小于給定元素的最大元素;如果不存在這樣的元素,則返回 null。 public E lower(E e) { return m.lowerKey(e); }
18、pollFirst:獲取并移除第一個(最低)元素;如果此 set 為空,則返回 null。 public E pollFirst() { Map.Entry
19、pollLast:獲取并移除最后一個(最高)元素;如果此 set 為空,則返回 null。 public E pollLast() { Map.Entry
20、remove:將指定的元素從 set 中移除(如果該元素存在于此 set 中)。 public boolean remove(Object o) { return m.remove(o)==PRESENT; }
該方法與put類似,只不過把插入換成了刪除,并且要進行刪除后調(diào)整
21、size:返回 set 中的元素數(shù)(set 的容量)。 public int size() { return m.size(); }
22、subSet:返回此 set 的部分視圖 /** * 返回此 set 的部分視圖,其元素范圍從 fromElement 到 toElement。 */ public NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); }
/** * 返回此 set 的部分視圖,其元素從 fromElement(包括)到 toElement(不包括)。 */ public SortedSetsubSet(E fromElement, E toElement) { return subSet(fromElement, true, toElement, false); }
23、tailSet:返回此 set 的部分視圖 /** * 返回此 set 的部分視圖,其元素大于(或等于,如果 inclusive 為 true)fromElement。 */ public NavigableSet tailSet(E fromElement, boolean inclusive) { return new TreeSet<>(m.tailMap(fromElement, inclusive)); }
/** * 返回此 set 的部分視圖,其元素大于等于 fromElement。 */ public SortedSettailSet(E fromElement) { return tailSet(fromElement, true); }
最后
由于TreeSet是基于TreeMap實現(xiàn)的,所以如果我們對treeMap有了一定的了解,對TreeSet那是小菜一碟,我們從TreeSet中的源碼可以看出,其實現(xiàn)過程非常簡單,幾乎所有的方法實現(xiàn)全部都是基于TreeMap的。
LinkedHashSet
LinkedHashSet內(nèi)部是如何工作的
LinkedHashSet是HashSet的一個“擴展版本”,HashSet并不管什么順序,不同的是LinkedHashSet會維護“插入順序”。HashSet內(nèi)部使用HashMap對象來存儲它的元素,而LinkedHashSet內(nèi)部使用LinkedHashMap對象來存儲和處理它的元素。這篇文章,我們將會看到LinkedHashSet內(nèi)部是如何運作的及如何維護插入順序的。
我們首先著眼LinkedHashSet的構(gòu)造函數(shù)。在LinkedHashSet類中一共有4個構(gòu)造函數(shù)。這些構(gòu)造函數(shù)都只是簡單地調(diào)用父類構(gòu)造函數(shù)(如HashSet類的構(gòu)造函數(shù))。 下面看看LinkedHashSet的構(gòu)造函數(shù)是如何定義的。 //Constructor - 1
public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); //Calling super class constructor }
//Constructor - 2
public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); //Calling super class constructor }
//Constructor - 3
public LinkedHashSet() { super(16, .75f, true); //Calling super class constructor }
//Constructor - 4
public LinkedHashSet(Collection extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); //Calling super class constructor addAll(c); }
在上面的代碼片段中,你可能注意到4個構(gòu)造函數(shù)調(diào)用的是同一個父類的構(gòu)造函數(shù)。這個構(gòu)造函數(shù)(父類的,譯者注)是一個包內(nèi)私有構(gòu)造函數(shù)(見下面的代碼,HashSet的構(gòu)造函數(shù)沒有使用public公開,譯者注),它只能被LinkedHashSet使用。
這個構(gòu)造函數(shù)需要初始容量,負(fù)載因子和一個boolean類型的啞值(沒有什么用處的參數(shù),作為標(biāo)記,譯者注)等參數(shù)。這個啞參數(shù)只是用來區(qū)別這個構(gòu)造函數(shù)與HashSet的其他擁有初始容量和負(fù)載因子參數(shù)的構(gòu)造函數(shù),下面是這個構(gòu)造函數(shù)的定義, HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
顯然,這個構(gòu)造函數(shù)內(nèi)部初始化了一個LinkedHashMap對象,這個對象恰好被LinkedHashSet用來存儲它的元素。
LinkedHashSet并沒有自己的方法,所有的方法都繼承自它的父類HashSet,因此,對LinkedHashSet的所有操作方式就好像對HashSet操作一樣。
唯一的不同是內(nèi)部使用不同的對象去存儲元素。在HashSet中,插入的元素是被當(dāng)做HashMap的鍵來保存的,而在LinkedHashSet中被看作是LinkedHashMap的鍵。
這些鍵對應(yīng)的值都是常量PRESENT(PRESENT是HashSet的靜態(tài)成員變量,譯者注)。
LinkedHashSet是如何維護插入順序的
LinkedHashSet使用LinkedHashMap對象來存儲它的元素,插入到LinkedHashSet中的元素實際上是被當(dāng)作LinkedHashMap的鍵保存起來的。
LinkedHashMap的每一個鍵值對都是通過內(nèi)部的靜態(tài)類Entry
這個靜態(tài)類增加了兩個成員變量,before和after來維護LinkedHasMap元素的插入順序。這兩個成員變量分別指向前一個和后一個元素,這讓LinkedHashMap也有類似雙向鏈表的表現(xiàn)。 private static class Entry
Entry(int hash, K key, V value, HashMap.Entrynext) { super(hash, key, value, next); }
}
從上面代碼看到的LinkedHashMap內(nèi)部類的前面兩個成員變量——before和after負(fù)責(zé)維護LinkedHashSet的插入順序。LinkedHashMap定義的成員變量header保存的是 這個雙向鏈表的頭節(jié)點。header的定義就像下面這樣,
接下來看一個例子就知道LinkedHashSet內(nèi)部是如何工作的了。 public class LinkedHashSetExample { public static void main(String[] args) { //Creating LinkedHashSet
LinkedHashSetset = new LinkedHashSet (); //Adding elements to LinkedHashSet set.add("BLUE"); set.add("RED"); set.add("GREEN"); set.add("BLACK"); }
}
感謝各位的閱讀,以上就是“Java集合類怎么使用”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Java集合類怎么使用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!