1. 程式人生 > >《瘋狂Java講義(第4版)》-----第8章【Java集合】(Collection、Iterator、Set)

《瘋狂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工具類的相關內容。