《瘋狂Java講義(第4版)》-----第8章【Java集合】(Collection、Iterator、Set)
Java集合概述
Java集合本身是封裝的幾種常見的資料結構,只要學習過基本的資料結構課程,便可理解清楚底層的實現細節。由於Java提供封裝好的集合眾多,每個集合暴露的方法眾多,一般記住常用的,其他的知道有就行了,用的時候查詢官方API即可。
Java集合裡存放的只能是物件(嚴格的說是物件的引用變數)。如果不使用泛型的話,丟進集合內的所有物件都是Object型別的。泛型知識後續會專門一章介紹,本章暫時不用泛型
Collectioiin介面和Iterator介面
Collection介面的演示程式碼(參考官方API中的Collection書寫):
import java.util.Collection; import java.util.ArrayList; public class Hello{ public static void main(String[] args){ Collection c = new ArrayList(); System.out.println(c.isEmpty());//true c.add("Tom"); c.add(666); System.out.println(c.size());//2 System.out.println(c.contains(666));//true System.out.println(c.remove("Tom"));//true System.out.println(c);//[666] c.clear(); System.out.println(c.isEmpty());//true } }
編譯提示這個警告,因為沒有使用泛型,說白了就是沒有指定Collection裡面存放的到底是哪種型別的物件。現在集合裡放入的有666整數型別,還有Tom字串型別。
使用Lambda表示式遍歷集合
Collection介面繼承了Iterable介面,Iterable介面提供了forEach(Consumer<? super T> action) 方法,Consumer是函式式介面,可以用Lambda來遍歷集合。
官方文件提供的Iterable介面的forEach方法,引數是介面Consumer
Consumer介面只含有一個抽象方法accept,是函式式介面:
import java.util.Collection; import java.util.HashSet; public class Hello{ public static void main(String[] args){ Collection c = new HashSet(); c.add(1); //System.out.println(c.add(1));//false,不能放重複元素 c.add("Tom"); c.add('c'); c.forEach(obj->System.out.println(obj)); } }
使用Iterator遍歷集合元素
Iterator介面。Iterator物件又成為迭代器,Iterator物件依賴於Collection物件,專門為遍歷集合而存在的。Iterator官方文件裡提供了下面幾個方法:
使用Iterator裡的forEachRemaining方法遍歷集合。這個方法的引數Consumer,是函式式介面,只有一個抽象方法accept,因此可以用Lambda表示式。
import java.util.Collection; import java.util.HashSet; import java.util.Iterator; public class Hello{ public static void main(String[] args){ Collection c = new HashSet(); c.add("Jack"); c.add("Tom"); c.add("Marry"); Iterator it = c.iterator(); while(it.hasNext()){ String student = (String)(it.next()); System.out.println(student); if(student.equals("Marry")){ it.remove();//可以把Marry從集合c中刪除 //c.remove("Marry");//這樣也可以把Marry從集合中刪除,不要這麼幹,很危險 //c.remove(student);//這樣也可以把Marry從集合中刪除,不要這麼幹,很危險 } student = "test";//student僅僅是一箇中間變數,這不會改變集合元素的值 } //使用Iterator的forEachRemaining方法遍歷集合, //forEachRemaining方法的引數Consumer是函式式介面,可以用Lambda表示式 it = c.iterator(); it.forEachRemaining(obj->System.out.print(obj+" ")); System.out.println(); System.out.println(c);//[Tom, Jack] } }
使用增強for迴圈遍歷集合
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class Hello{
public static void main(String[] args){
Collection c = new HashSet();
c.add("Jack");
c.add("Tom");
c.add("Marry");
//使用增強for迴圈遍歷
for(Object obj : c){
String student = (String)obj;
System.out.println(student);
if(student.equals("Tom")){
//c.remove("Tom");//這麼做會拋異常java.util.ConcurrentModificationException
}
}
}
}
Java8新增的Predicate集合Lamb操作集合
Collection介面提供了下面的一個方法,裡面引數Predicate是一個函式式介面,只有一個抽象方法boolean test(T t),因此可以結合Lambda表示式,批量刪除集合中的元素,也可以自定義一些方法,批量處理集合中的元素。 示例程式碼見該書297~298頁
Java8新增了Stream、IntStream、LongStream、DoubleStream等流式API
這幾個介面,可以自行搞集合,存元素,對元素處理操作;也可以去操作集合。用法示例程式碼見該書299~300頁。
Set集合
Set集合元素不允許重複。
HashSet
- HashSet不能保證元素的排列順序
- HashSet元素可以是null
- HashSet不是同步的,如果多個執行緒同時訪問一個HashSet,假設有兩個或兩個以上執行緒同時修改了HashSet集合時,必須通過程式碼來保證同步。
**HashSet集合判斷兩個元素相等的標準是hashCode()相等並且equals相等。**下面程式碼重寫父類的Object的equals方法或hashCode方法:
import java.util.HashSet;
class A{
public boolean equals(Object obj){
return true;
}
}
class B{
public int hashCode(){
return 1;
}
}
class C{
public boolean equals(Object obj){
return true;
}
public int hashCode(){
return 2;
}
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new A());
hs.add(new A());
hs.add(new B());
hs.add(new B());
hs.add(new C());
hs.add(new C());
System.out.println(hs);
}
}
輸出: 說明第2個new C()沒有新增進去,因為兩個C物件equals和hashCode都是相等的。底層實現上,HashSet是根據元素的HashCode值來決定存放的位置的,萬一兩個元素的equals不等,而HashCode相等,只能在同一個位置用鏈式存起來,這樣效率就下降了。
在重寫equals和hashCode方法的時候,要保證兩個元素通過equals比較是true的時候,他們的hashCode也是相等的,逆命題亦然。
下面展示一個用物件成員變數計算hashCode並決定equals的返回值的程式碼,也就是說HashSet傳入了一個可變的元素,如果試圖修改這個可變元素的參與計算hashCode和equals比較的成員變數的值,可能會導致兩個元素的equals相等而hashCode不等,或者hashCode相等而equals不等。**所以不要去修改集合中參與計算hashCode、equals的例項變數。**具體見下面程式碼:
import java.util.HashSet;
import java.util.Iterator;
class R{
private int cnt;
public R(int cnt){
this.cnt = cnt;
}
public String toString(){
return "R[cnt:"+this.cnt+"]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == R.class){
R r = (R)obj;
return this.cnt == r.cnt;
}
return false;
}
public int hashCode(){
return this.cnt;
}
public void setCnt(int cnt){
this.cnt = cnt;
}
public int getCnt(){
return this.cnt;
}
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new R(1));
hs.add(new R(2));
hs.add(new R(3));
hs.add(new R(4));
System.out.println(hs);
Iterator it = hs.iterator();
R first = (R)it.next();
first.setCnt(2);
System.out.println(hs);
hs.remove(new R(2));
System.out.println(hs);
//注意:現在,第1個數是2,equals用2比較,但剛開始存放他的時候hashCode是1
//由於修改成員變數,導致了equals和hashCode不一致
System.out.println(hs.contains(new R(2)));//false
System.out.println(hs.contains(new R(1)));//false
}
}
LinkedHashSet
LinkedHashSet繼承HashSet,與HashSet不同的是,LinkedHashSet底層用連結串列維護,元素按照插入先後有序排列。由於要維護這個順序,效能略低於HashSet,但在遍歷的時候,LinkedHashSet效能更好。
import java.util.LinkedHashSet;
public class Hello{
public static void main(String[] args){
LinkedHashSet hs = new LinkedHashSet();
hs.add(new R(1));
hs.add(new R(3));
hs.add(new R(6));
hs.add(new R(4));
System.out.println(hs);
}
}
輸出(按照插入順序列印):
TreeSet
不允許插入null,否則報異常:java.lang.NullPointerException
TreeSet實現了SortedSet介面,利用紅黑樹演算法保證元素的有序性。TreeSet支援自然排序和定製排序,預設採用自然排序。下面將詳細說明這兩種排序方式,在此之前,先寫一段程式碼,認識下TreeSet,還是那句老話,具體用法看官方API。
import java.util.TreeSet;
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(34);
ts.add(6);
ts.add(4);
ts.add(3);
System.out.println(ts);//[3, 4, 6, 34]
//返回比6小的所有元素組成的集合
System.out.println(ts.headSet(4));//[3]
//返回大於等於4的所有元素組成的集合
System.out.println(ts.tailSet(4));//[4, 6, 34]
System.out.println(ts.first());//第一個元素3
System.out.println(ts.last());//最後一個元素34
}
}
(1)自然排序 當把一個元素插入到TreeSet集合的時候,TreeSet就會呼叫這個元素的compareTo方法和集合中已經存在的其他元素比較,保證每次插入後,集合的元素總是有序的。插入到TreeSet集合的元素必須實現Comparable介面實現compareTo方法,TreeSet判斷兩個物件是否相等的唯一標準是兩個物件通過compareTo方法比較返回值是否是0。Comparable介面只有一個方法並且是抽象方法compareTo,所以定製排序的時候可以藉助Lambda表示式。可比較大小的常見物件已經實現了compareTo方法,在實現過程中保證了參與比較的兩個物件是同類型的,即同一個類的例項。如果非要往TreeSet中新增自定義的不同類物件,可以讓這些類都去實現Comparable介面,且compareTo方法沒有強制型別轉換,但是當從TreeSet取物件的時候就會報錯ClassCastException,這是因為TreeSet要按照順序取,但無法排序啊!所以,TreeSet中加入的物件必須是同類型的。
【程式碼例項】
import java.util.TreeSet;
class A implements Comparable{
private int x;
public A(int x){
this.x = x;
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.x == a.x;
}
return false;
}
public int compareTo(Object obj){
return 1;//認為設定永遠比obj大,每次都能插入TreeSet
}
public void setX(int x){
this.x = x;
}
public int getX(){
return this.x;
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
A a = new A(99);
ts.add(a);
System.out.println(ts.add(a));//true
((A)(ts.first())).setX(23);
System.out.println(((A)(ts.last())).getX());
System.out.println(ts.size());//2
}
}
上面程式碼中,由於人為重寫的compareTo,導致每次插入的物件即使和之前的值一樣,也會被判成不一樣的,可以插入。雖然TreeSet儲存兩個元素,實質上他們指向同一個物件。記憶體情況: 從上面的例子,我們可以給出這樣的建議:在自定義compareTo的時候,要保證和equals有一致的判定結果。
在學習HashSet的時候,演示了插入可變物件,並試圖修改用於計算equals和hashCode的成員變數的值,導致了equals和hashCode判斷不一致的情況。類似地,對TreeSet來說,插入可變物件,並試圖修改用於計算compareTo的成員變數的值,導致元素順序混亂,但TreeSet不會重新排序,最終可能會引發無法刪除的現象,具體示例見該書308~309頁,通過這個示例,建議不要修改用於計算compareTo的關鍵成員變數的值。
import java.util.TreeSet;
class A implements Comparable{
private int x;
public A(int x){
this.x = x;
}
public String toString(){
return "A[x:" + this.x + "]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.x == a.x;
}
return false;
}
public int compareTo(Object obj){
A a = (A)obj;
return this.x > a.x ? 1 :
this.x < a.x ? -1 : 0;
}
public void setX(int x){
this.x = x;
}
public int getX(){
return this.x;
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(new A(5));
ts.add(new A(-3));
ts.add(new A(9));
ts.add(new A(-2));
System.out.println(ts);
A first = (A)ts.first();
A last = (A)ts.last();
first.setX(20);//修改了第一個位置的數,導致元素順序混亂
last.setX(-2);//修改成-2,和已有的-2重複。
System.out.println(ts);
System.out.println(ts.remove(new A(-2)));
System.out.println(ts);//無法刪除,修改的-2和已有-2相同
System.out.println(ts.remove(new A(5)));//執行完這個程式碼後,又重新索引(不是排序)
//之後就能刪除所有元素了,筆者通過下面的實驗大致可以說明
//重新的索引就是按照原來插入的順序來索引的。
System.out.println(ts);
System.out.println(ts.remove(new A(-2)));
System.out.println(ts);
System.out.println(ts.first());//第一個元素是20,表明這個順序還是剛開始插入的順序
}
}
(2)定製排序 在自然排序中,筆者已經說明,插入到TreeSet的元素必須要實現Comparable介面,並實現compareTo方法,這就是定製排序的核心,由於Comparable是函式式介面,因此可以用Lambda表示式實現。
其實在自然排序的第2個示例程式碼中已經進行了定製排序(自行寫了compareTo方法),現在用Lambda表示式再實現下,不過進行倒序排列吧。
import java.util.TreeSet;
class A{
private int x;
public A(int x){
this.x = x;
}
public String toString(){
return "A[x:" + this.x + "]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.x == a.x;
}
return false;
}
public void setX(int x){
this.x = x;
}
public int getX(){
return this.x;
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet((obj1, obj2)->{
A a1 = (A)obj1;
A a2 = (A)obj2;
return a1.getX() > a2.getX() ? -1 :
a1.getX() < a2.getX() ? 1 : 0;
});
ts.add(new A(5));
ts.add(new A(-3));
ts.add(new A(9));
ts.add(new A(-2));
System.out.println(ts);
}
}
輸出:
EnumSet
不允許插入null,否則報異常:java.lang.NullPointerException 只能儲存同屬於一個列舉類的列舉值。
import java.util.EnumSet;
enum Season{
SPRING, SUMMER, AUTUMN, WINTER;
}
public class Hello{
public static void main(String[] args){
//Creates an enum set containing all of the elements in the specified element type.
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1);
//Creates an empty enum set with the specified element type.
EnumSet es2 = EnumSet.noneOf(Season.class);
es2.add(Season.AUTUMN);
es2.add(Season.SPRING);
System.out.println(es2);
EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(es3);
//Creates an enum set initially containing all of the elements
//in the range defined by the two specified endpoints.
EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4);
//Creates an enum set with the same element type as the specified enum set,
//initially containing all the elements of this type that are not contained in the specified set.
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);
}
}
要複製Collection集合中的所有元素來建立EnumSet集合時,要求Collection集合中的素有元素必須是同一個列舉類的列舉值。
import java.util.EnumSet;
import java.util.HashSet;
enum Season{
SPRING, SUMMER, AUTUMN, WINTER;
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.clear();
hs.add(Season.SUMMER);
hs.add(Season.SPRING);
//java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum
//hs.add("hello");//如果寫這句程式碼會引發上面的註釋異常。
EnumSet es = EnumSet.copyOf(hs);
System.out.println(es);
}
}
HashSet、TreeSet、EnumSet都是執行緒不安全的
如果又多個執行緒同時訪問了一個Set集合,並且有超過一個執行緒修改了該Set集合,必須手動保證該Set集合的同步性。通常可以通過Collections工具類的 一個方法在建立的時候進行封裝Set集合。具體見該書312頁、339頁。或者見下篇博文的Collections工具類的相關內容。