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的唯一不是指内存里面的唯一。而是取决于程序员如何定义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 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;
}
}
- 当一个对象已经添加进HashSet集合之后,修改其中未参与生成哈希码值的属性值,哈希码值不变,此时可以直接删除元素。
import java.util.*;
public class TestHashSet8{
public static void main(String[] args){
HashSet<Teacher> set = new HashSet<>();
Teacher tea = new Teacher("李四",20);
set.add(tea);
tea.name = "王五";
set.remove(tea);
System.out.println(set);//[]
}
}
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 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.age == ((Teacher)obj).age;
}
}
- 总结
在每个改写了equals()方法的类中,必须要改写hashCode()方法。 如果不这样做,就会违反Object.hashCode()的通用约定,从而导致该类无法与所有基于hash的集合类结合在一起正常运作。** 这样的集合类包括HashMap、HashSet和HashTable。