1. 程式人生 > >【第16天】Java集合(三)---Set介面實現的HashSet集合

【第16天】Java集合(三)---Set介面實現的HashSet集合

1 HashSet簡介

       作為Set介面的一個實現類,特點是無序、儲存元素唯一的。底層實現的資料結構為雜湊表

2 基本用法與特點

  • 建立一個HashSet集合物件
           與ArrayList集合相同,HashSet集合物件建立也依據不同的版本有不同的方法。

    • JDK5.0之前,預設往集合裡面存放的都是Object型別的物件,取元素後型別需強轉。
      HashSet set = new HashSet();

    • JDK5.0及以後,可以加泛型(不加泛型預設傳入元素為Object型別)
      HashSet<泛型> set = new HashSet<泛型>();

    • JDK7.0及以後,後面的泛型會自動推斷(不加泛型預設傳入元素為Object型別)
      HashSet<泛型> set = new HashSet<>();

但是與ArrayList不同的是,HashSet集合構造方法中傳入的引數,預設(空參)時傳入16,0.75。分別代表分組組數(int)載入因子(float)

其中,第一個引數分組組數一定是2的n次方,如傳入7,自動開闢8個分組;傳入17,自動開闢32個分組。元素根據其類中hashCode()生成雜湊碼為特徵值(若未重寫,根據Object類中定義的,以地址的雜湊碼作為特徵值),模以組數根據得到的結果去往不同的分組。


第二個引數為載入因子。
這兩個引數共同構成閾值(分組組數 * 載入因子),閾值指的是雜湊表擴容的最小臨界值,一個集合中的某一個雜湊表達到或者超出的時候這個值的時候,整個集合的雜湊表都需要擴容。擴容倍數為2倍。
最好不要使用擴容方法對HashSet集合進行擴容,因為這會改變分組數量使元素的特徵碼模以新的分組數量重新進入新的分組,效率低。

  • 如何調整HashSet效能和空間的取捨?
    分的組數越多,組內元素越少,查詢效率越高,但佔空間;
    載入因子越高,承受得擴容數量越高,查詢效率低,但省記憶體。(注意:載入因子可以大於1,只要一個分組到了閾值(集合中某一個雜湊表分組中元素達到的某個數量),統一擴容)

  • 新增元素到HashSet

      //元素的型別要與宣告HashSet時傳入的泛型相一致
      set.add(Object obj);
      Collections.addAll(set, Object obj1,Object obj2....);
      //將新的集合加入到set中
      set1.addAll(集合型別的引用);
    
  • 得到HashSet集合的大小

      set.size();
    
  • 判斷集合裡是否包含指定元素

      set.contains(Object obj);
    
  • 指定元素進行刪除

      set.remove(Object obj);
    

注意,這裡的刪除remove、判斷是否包含指定元素contains底層依賴的依據是Object類中的equals(),傳入obj引用作為一個參照,依據這個類中定義好的或者Object類中的equals()(原生比較地址)進行true/false的比較。

  • 遍歷

      //遍歷
      //foreach
      for(Integer x : set){
      	System.out.println(x);
      }
    
      //迭代器
      for(Iterator<Integer> i = set.iterator(); i.hasNext(); ){
      	Integer num = i.next();
      	System.out.println(num);
      }
    

    因為HashSet集合是無序的,在遍歷時不能根據for + 下標進行順序遍歷,只能根據迭代器進行遍歷。foreach底層也是依據迭代器遍歷。另外HashSet也缺少依據下標的get()和remove()。

//將ArrayList中重複的元素去除
import java.util.*;
public class Test{
	public static void main(String[] args){

		ArrayList<Integer> list = new ArrayList<>();//不唯一

		Collections.addAll(list,45,77,92,45,92,33);

		//將集合裡面的重複元素刪除
		HashSet<Integer> set = new HashSet<>();
		set.addAll(list);
		System.out.println(set);

	}
}
  • 與ArrayList相比,HashSet缺少的方法有

    • get(int 下標)
    • remove(int 下標)
    • for + 下標

3 HashSet的唯一性

HashSet的唯一性判斷

       HashSet的唯一不是指記憶體裡面的唯一。而是取決於程式設計師如何定義hashCode()、equals()。此時add()都需要通過hashCode()、equals()判斷唯一性;remove(Object obj)、contains(Object obj)在操作元素時也需要判斷hashCode()、equals()。這一點與ArrayList這種不唯一的集合不同。

  • 因包裝類、String類中的equals()和hashCode()方法均在定義類時重寫,所以被認定為唯一。因HashSet的唯一性,若被驗證兩個元素重複了,為保證效率,新加入的那個元素被捨棄。
	HashSet<String> set = new HashSet<>();
	String x = new String("OK");
	String y = new String("OK");
	set.add(x);
	System.out.println(set.size());//--->1
	set.add(y);
	System.out.println(set.size());//--->1
	
	HashSet<Integer> set = new HashSet<>();
	Integer a = new Integer(77);
	Integer b = new Integer(77);
	set.add(a);
	System.out.println(set.size());//--->1
	set.add(b);
	System.out.println(set.size());//--->1
  • 只重寫了hashCode(),只能保證特徵碼相同的物件能去往同一個小組,
    若未重寫equals(),Object類的equals()預設比較地址,此時兩個物件地址不同,不能被認為是同一個元素,size()增加,如Test1;
    若重寫了equals(),則使用程式設計師自定義的規則判斷兩物件是否相同,此時這兩個元素被判為相同元素,size()不變。
import java.util.*;
public class Test1{
	public static void main(String[] asrgs){

		HashSet<Student> set = new HashSet<>();

		Student s1 = new Student("張三");
		Student s2 = new Student("張三");

		System.out.println("s1:" + s1.hashCode());
		System.out.println("s2:" + s2.hashCode());

		set.add(s1);
		System.out.println("size1:" + set.size());

		set.add(s2);
		System.out.println("size2:" + set.size());
	}
}

class Student{
	String name;
	public Student(String name){
		this.name = name;
	}

	//hashCode方法得到一個物件的雜湊特徵碼
	//並用來決定物件應該取到哪一個小組
	@Override
	public int hashCode(){
		return name.hashCode();
	}
}

//輸出結果:
/**
s1:774889
s2:774889
size1:1
size2:2
*/
import java.util.*;
public class Test2{
	public static void main(String[] asrgs){

		HashSet<Student> set = new HashSet<>();

		Student s1 = new Student("張三");
		Student s2 = new Student("張三");

		System.out.println("s1:" + s1.hashCode());
		System.out.println("s2:" + s2.hashCode());

		set.add(s1);
		System.out.println("size1:" + set.size());

		set.add(s2);
		System.out.println("size2:" + set.size());
	}
}

class Student{
	String name;
	public Student(String name){
		this.name = name;
	}

	//hashCode方法得到一個物件的雜湊特徵碼
	//並用來決定物件應該取到哪一個小組
	@Override
	public int hashCode(){
		return name.hashCode();
	}

	//當這個物件A取到某個小組之後 發現這個小組裡面有一個物件的雜湊碼值和A物件的雜湊碼值完全相同
	//需要使用equals()定義的比較規則作比較
	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Student)) return false;
		if(obj == this) return true;
		System.out.println("===========================");
		return this.name.equals(((Student)obj).name);
	}
}

//輸出結果:
/**
s1:774889
s2:774889
size1:1
===========================
size2:1
*/
  • 為保證兩個意義本來就不同的兩個物件不因偶然的重碼而被唯一性拒絕新增,需要使用equals()進行進一步確認,這也是equals()存在的必要性。
import java.util.*;
public class Test3{
	public static void main(String[] args){
		HashSet<Student> set = new HashSet<>();

		Student s1 = new Student("小花",6,'女');//hashCode():6 + 1 = 7
		Student s2 = new Student("小黑",7,'男');//hashCode():7 + 0 = 7,此時兩物件造成了重碼

		set.add(s1);
		set.add(s2);

		System.out.println(set.size());
	}
}

class Student{
	String name;
	int age;
	char gender

	public Student(String name,int age,char gender){
		this.name = name;
		this.age = age;
		this.gender = gender;
	}

	@Override
	public int hashCode(){
		return age + (gender == '男'? 0 : 1);
	}

	@Override
	public boolean equals(Object obj){
		return this.age == ((Student)obj).age &&
			this.gender == ((Student)obj).gender;
	}
}
  • 當什麼都沒有重寫,但傳入地址相同的一個物件時,hashCode()預設以地址生成特徵碼。地址相同,特徵碼模以組數的結果也相同,兩個物件去往同一個小組,之後比較地址,因地址相同視為同一物件,捨棄新加入的那個重複物件。
import java.util.*;
public class Test4{
	public static void main(String[] args){
		HashSet<Student> set = new HashSet<>();
		Student s = new Student("Tom",21);
		set.add(s);
		set.add(s);
		System.out.println(set.size());//--->1
	}
}
class Student{
	String name;
	int age;

	public Student(String name,int age){
		this.name = name;
		this.age = age;
	}
}
  • remove(Object obj)、contains(Object obj)在操作元素時也需要判斷hashCode()、equals()。
import java.util.*;
public class Test5{
	public static void main(String[] args){

		HashSet<Student> set = new HashSet<>();

		Student s1 = new Student("張三");
		Student s2 = new Student("李四");

		set.add(s1);
		System.out.println(set.contains(s2));//false
		System.out.println(set);//[張三]
		set.remove(s2);
		System.out.println(set);//[張三]
	}
}

class Student{
	String name;

	public Student(String name){
		this.name = name;
	}

	@Override
	public int hashCode(){
		return 1;
	}

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Student)) return false;
		if(obj == this) return true;
		return false;
	}

	@Override
	public String toString(){
		return name;
	}
}
  • 例:
import java.util.*;
public class Example{
	public static void main(String[] args){

		HashSet<Teacher> set = new HashSet<>();

		Teacher t1 = new Teacher("Tom",33,8000.0);
		Teacher t2 = new Teacher("Tom",35,8000.0);

		Collections.addAll(set,t1,t2);

		System.out.println(set.size());//--->1
	}
}

class Teacher{
	String name;
	int age;
	double salary;

	public Teacher(String name,int age,double salary){
		this.name = name;
		this.age = age;
		this.salary = salary;
	}

	@Override
	public int hashCode(){
		return name.hashCode() + (int)salary;
	}

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Teacher)) return false;
		if(obj == this) return true;
		return this.name.equals(((Teacher)obj).name) &&
			this.salary == ((Teacher)obj).salary;
	}
}

4 增刪改時需要注意

       除注意迭代器中使用呼叫這個迭代器的集合的remove()、add()會出現CME異常,需要使用迭代器的remove()方法以及新建立LinkedList來臨時接收新新增的元素在迴圈結束時addAll到老集合中以外,HashSet集合還需要注意當一個物件已經新增進HashSet集合之後,不要隨意修改其中參與生成雜湊碼值的屬性值

import java.util.*;
public class Test{
	public static void main(String[] args){

		HashSet<Teacher> set = new HashSet<>();
		Teacher tea = new Teacher("張三",21);
		set.add(tea);
		//在原有的物件上直接操作,會致使其hashCode發生改變
		//但還是會位於原來的分組,分組出現錯誤
		//所以後面要刪除時,參照物到正確的分組中找不到這個修改後的元素
		tea.age += 11;
		//tea這個參照物無法根據hashCode()、地址以及equals()在這個分組找到要刪除的元素
		//既而無法刪除改變後的這個元素
		set.remove(tea);
		System.out.println(set);//--->[張三:32]
		//再新增相同的元素還可以新增,因為正確的分組中還不存在這個元素
		//直接修改後的元素在錯誤的分組中
		set.add(tea);
		System.out.println(set);//--->[張三:32, 張三:32]
	}
}

class Teacher{
	String name;
	int age;

	public Teacher(String name,int age){
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString(){
		return name + ":" + age;
	}

	@Override
	public int hashCode(){
		return name.hashCode() + age;
	}

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Teacher)) return false;
		if(obj == this) return true;
		return this.name.equals(((Teacher)obj).name) &&
			this.age == ((Teacher)obj).age;
	}
}
  • 改後
import java.util.*;
public class Test{
	public static void main(String[] args){
		HashSet<Teacher> set = new HashSet<>();
		Teacher tea = new Teacher("張三",21);
		set.add(tea);
		set.remove(tea);
		tea.age += 11;
		set.add(tea);
		System.out.println(set);//--->[張三:32]
		set.add(tea);
		System.out.println(set);//--->[張三:32]
	}
}

class Teache