HashSet
首先看一看三个单值类型集合都能调用的方法,也就是无论是List还是Set无论是ArrayList、LinkedList、Vector或者HashSet、TreeSet都可以使用
a.addAll(c)
将c集合的每一个元素都添加进a集合当中,即a集合尝试添加c集合当中的每一个元素a.removeAll(c)
将c集合当中的每一个元素都从a集合当中删除,即a集合尝试删除c集合当中的每一个元素*:注意,如果要删除的对象根本不存在,则原集合当中不会产生任何变化...
a.retainAll(c)
让a集合当中只保存c集合也存在的元素,也就是求a和c的交集-------
public class Test{
public static void main(String[] args){
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
Collections.addAll(set1,11,33,88,55,44);
Collections.addAll(set2,22,33,55);
//addAll()
/*set1.addAll(set2);
System.out.println(set1);//[33, 22, 55, 88, 11, 44] 无序但唯一*/
//removeAll()
/*set1.removeAll(set2);
System.out.println(set1);//[88, 11, 44]*/
//retainAll()
set1.retainAll(set2);
System.out.println(set1);//[33, 55]
}
}
-------
HashSet -> 底层采用哈希表,实现Set接口的无序唯一的单值类型集合
1:验证HashSet的无序、唯一的特点,并且强化基本用法
*:这里所谓的无序是指既不是添加的顺序也不是升序降序但是哈希表本身也有自己的一套机制来计算元素存放的位置
所以每次执行,元素的顺序都是相同的
*:基本用法当中最最重要的就是无序的Set集合不再提供get(下标)和remove(下标)
所有通过下标索引值进行操作的方法Set都不提供
所以受影响的还有遍历集合...不能使用for循环结合get(int)方法
------
public class Test01 {
public static void main(String[] args){
Set<Integer> set = new HashSet<>();
set.add(22);
set.add(55);
Collections.addAll(set,66,88,99,55);
System.out.println(set.size());//5
System.out.println(set);//[66, 99, 22, 55, 88]无序唯一
System.out.println(set.contains(66));//true
System.out.println("----------");
for(Integer x : set){
System.out.println(x);
}
System.out.println("----------");
for(Iterator<Integer> car = set.iterator();car.hasNext();){
Integer x = car.next();
System.out.println(x);
}
}
}
------
2:HashSet所谓的唯一,不是真正的唯一,而是"唯一"
这里的唯一也是根据程序员制定的规则来看,比如覆写hashCode()方法,里面返回(int)(Math.random()*10000),这样即使两个完全相等
的对象也有可能会加入到HashSet中,所以不是真正的唯一
通过如下代码验证即使内存当中完全不同的两个对象,也有可能被
HashSet视作同一个对象 从而舍弃一个
所以不能单纯的理解不同的对象就一定能添加成功
------
public class Test02{
public static void main(String[] args){
Set<String> set = new HashSet<>();
String str1 = new String("ETOAK");
String str2 = new String("ETOAK");
System.out.println(str1 == str2);//false
set.add(str1);
set.add(str2);
System.out.println(set.size());//1
/*
Set<Integer> set = new HashSet<>();
Integer num1 = new Integer(222);
Integer num2 = new Integer(222);
System.out.println(num1 == num2);//?
set.add(num1);
set.add(num2);
System.out.println(set.size());
*/
}
}
------
3:HashSet所谓的唯一取决于程序员如何定义hashCode()和equals()
如果只写equals()和只写hashCode(),发现相同内容的Student对象依然不能被视作相等而舍弃。只有当两个方法同时覆盖的时候才能让HashSet视作逻辑相等
而我们应当根据自己的需求来覆盖hashCode()方法和equals()方法
例如:如果想让名字相同而且年龄相等的学生视作同一个,那么euqals()就应该通过比较名字和年龄得出
同样hashCode()也应该用名字和年龄综合得出
------
public class Test03 {
public static void main(String[] args){
Set<Student> set = new HashSet<>();
Student s1 = new Student("Jay");
Student s2 = new Student("Jay");
set.add(s1);
set.add(s2);
System.out.println(set.size());//1
}
}
class Student{
String name;
public Student(String name){
this.name = name;
}
@Override
public String toString(){
return name;
}
@Override
public boolean equals(Object obj){
if(obj == null)return false;
if(!(obj instanceof Student))return false;
if(obj == this)return true;
return this.name.equals(((Student)obj).name);
}
@Override
public int hashCode(){
return name.hashCode();
}
}
-----
4: 其实验证元素重复的过程涉及三个比较而不是两个
真正的比较步骤 hashCode() == equals()1st 2nd 3rd
其中在hashCode相同的情况下,会直接使用==比较新老元素
而且比较的代码逻辑是 1st && (2nd || 3rd)
比较流程如下图:
------
public class Test04 {
public static void main(String[] args){
Set<Student> set = new HashSet<>();
Student s1 = new Student("Jay");
set.add(s1);
set.add(s1);
System.out.println(set.size());//打印 1,虽然覆写了equals()并返回false 以及hashCode()返回1,但是并没有认定两个s1是两个不同对象
}
}
class Student{
String name;
public Student(String name){
this.name = name;
}
@Override
public String toString(){
return name;
}
@Override
public boolean equals(Object obj){
return false;
}
@Override
public int hashCode(){
return 1;
}
}
------
5:HashSet在添加元素的时候,如果认定新元素重复,则直接放弃添加
而不会去替换原有元素,所以说HashSet先入为主,先到先得后来的重复元素直接舍弃,集合当中存放的是先放入的对象
------
public class Test05 {
public static void main(String[] args){
Set<Teacher> set = new HashSet<>();
Teacher tea1 = new Teacher("Jay");
Teacher tea2 = new Teacher("Gay");
set.add(tea1);
set.add(tea2);
System.out.println(set);//[Jay]
}
}
class Teacher{
String name;
public Teacher(String name){
this.name = name;
}
@Override
public String toString(){
return name;
}
@Override
public boolean equals(Object obj){
return true;
}
@Override
public int hashCode(){
return 1;
}
}
------
6:HashSet删除元素不再提供remove(int),只提供了remove(Obejct)
而这个传对象的remove方法同样尊重三步比较机制也就是同样通过hashCode() == equals() ,来判断参数是不是要删除的元素
其过程和添加相同,只是添加如果认定重复就放弃添加;而删除只有认定重复才会执行删除操作
------
public class Test06 {
public static void main(String[] args){
Set<Teacher> set = new HashSet<>();
Teacher tea1 = new Teacher("Jay");
Teacher tea2 = new Teacher("Gay");
set.add(tea1);
set.add(tea2);
System.out.println(set.size());//1
set.remove(tea2);
System.out.println(set.size());//0
}
}
class Teacher{
String name;
public Teacher(String name){
this.name = name;
}
@Override
public String toString(){
return name;
}
@Override
public boolean equals(Object obj){
return true;
}
@Override
public int hashCode(){
return 1;
}
}
------
7:ConcurrentModificationException并发修改异常
在使用迭代器遍历集合的过程当中不允许对集合整体进行添加或者删除操作,否则迭代器的next()方法会触发并发修改异常
*:虽然我们可以使用break来防止下一次next()触发异常,但是这样只能操作一个对象
如果一定要去执行删除操作,应该使用迭代器遍历,
过程中应该使用迭代器自身的car.remove();
如果一定要去执行添加,可以使用另一个集合暂存数据,待遍历完成之后再整体添加回原集合
------
public class Test07 {
public static void main(String[] args){
Set<Integer> set = new HashSet<>();
Collections.addAll(set,11,33,55,77,22);
System.out.println(set); //[33, 22, 55, 11, 77]
//请删除大于30的元素
for(Iterator<Integer> car = set.iterator();car.hasNext();){
Integer i = car.next();
if(i > 30){
//set.remove(i);//报ConcurrentModificationException
car.remove();
}
}
System.out.println(set);//[22, 11]
}
}
------
8:使用HashSet千万不要在添加元素之后,直接修改对象生成哈希码生成哈希码生成哈希码的属性,
否则会导致这个元素在集合当中再也无法删除...如果一定要去修改,可以使用三个步骤完成
a. 删除 - 如果在遍历过程中 注意防止CME
b. 修改
c.重新添加
重新添加的过程会使用修改后的新属性重新计算hash码
*:如果不按照abc去做,也能修改成功对象的属性
但是潜在的隐患无限!!!
-----
public class Test08 {
public static void main(String[] args){
Set<Student> set = new HashSet<>();
Student s1 = new Student("张三",16);
Student s2 = new Student("李四",25);
Student s3 = new Student("王五",34);
set.add(s1);
set.add(s2);
set.add(s3);
//王五的年龄错了 请修改为24岁
Set<Student> temp = new HashSet<>();
for(Iterator<Student> car = set.iterator();car.hasNext();){
Student s = car.next();
if("王五".equals(s.name)){
//先删除
car.remove();
//再修改
s.age = 24;
temp.add(s);//暂存数据
}
}
//再重新添加
set.addAll(temp);
System.out.println(set);//[张三:16, 李四:25, 王五:24]
}
}
class Student{
String name;
int age;
public Student(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 Student))return false;
if(obj == this)return true;
return this.name.equals(((Student)obj).name) &&
this.age == (((Student)obj).age);
}
}
-----
9:HashSet构造方法的两个参数:分组组数,加载因子
分组组数 int类型 默认16组我们可以随意指定,但是最终一定会是大于指定数值的最小2的n次方
*:因为底层采用&(分组组数 - 1) 来计算散列小组,效率更高
所以必须保证分组组数是2的n次方数
//底层查找大于指定数值x的最小2的n次方的方法
int ok = 1;
while(ok < x){
ok << 1;
}
System.out.println(ok);
加载因子 float类型 默认0.75F
用于调整阈值,可以大于1.0F
阈值 = 分组组数*加载因子 = 达到扩容条件的最小临界值
当整个集合元素总量(不是一个小组的总量)大于阈值时,HashSet开始扩容,分组组数*2 得到一个新的哈希表
而所有已经存在的元素需要重新散列确定位置
*:分4组的时候在第一组的元素,分8组未必还在第一组
所以我们在知道最终存放数据总量的情况下,应当保证
分组组数*加载因子 > 数据总量
在这个前提下,分组组数越大,效率越高,越浪费空间
加载因子越大,越节约空间,但是会影响效率
-----
import java.util.*;
public class ExecHashSet{
public static void main(String[] args){
Set<Student> set = new HashSet<>();
Student s1 = new Student("小翔",90,60,90);
Student s2 = new Student("小俐",60,59,95);
Student s3 = new Student("小黑",100,100,100);
Student s4 = new Student("大白",80,60,100);//美国人~
Collections.addAll(set,s1,s2,s3,s4);
//2nd.三个平均成绩高于80分的同学 点名表扬~
//set.stream().filter(x -> (x.chinese+x.math+x.english)>240).forEach(System.out::println); //JDK8.0 lambda表达式
/*
for(Student stu : set){
int total = stu.chinese + stu.math + stu.english;
if(total > 240){
System.out.println(stu.name);
}
}
*/
//3rd.请删除三科都是100分的那个同学~
/*
for(Iterator<Student> car = set.iterator(); car.hasNext(); ){
Student x = car.next();
if(x.chinese + x.math + x.english == 300){
car.remove();
}
}
*/
//4th.已知大白在数学考试中作弊 成绩清零
/*
List<Student> qbs = new ArrayList<>();
for(Iterator<Student> car = set.iterator(); car.hasNext(); ){
Student x = car.next();
if("大白".equals(x.name)){
car.remove();//1.删除
x.math = 0;//2.修改
qbs.add(x);//3.添加 -> CME
}
}
set.addAll(qbs);
*/
//5th.语文考试当中有一个选择题 老师做错了
// 除小翔-2分外 所有同学+2分
/*
List<Student> qbs = new ArrayList<>();
for(Iterator<Student> car = set.iterator(); car.hasNext(); ){
Student x = car.next();
car.remove();
x.chinese += 2;
if("小翔".equals(x.name)){
x.chinese -= 4;
}
}
set.addAll(qbs);
*/
}
}
//1st.请尝试定义Student类的比较规则 让其在HashSet当中
// 如果名字和各科分数都相同 则视作相同对象~~~~
class Student{
String name;
int chinese;
int math;
int english;
public Student(String name,int chinese,int math,int english){
this.name = name;
this.chinese = chinese;
this.math = math;
this.english = english;
}
@Override
public String toString(){
return name + " : " + chinese + " : " + math + " : " + english;
}
@Override
public boolean equals(Object obj){
if(obj == null) return false;
if(!(obj instanceof Student)) return false;
if(obj == this) return true;
Student s1 = this;
Student s2 = (Student)obj;
return s1.name.equals(s2.name) && s1.chinese == s2.chinese && s1.math == s2.math && s1.english == s2.english;
}
@Override
public int hashCode(){
return name.hashCode() + chinese + math + english;
}
}
-----
常见问题:
*:HashSet或者HashMap如何判断添加的元素是否重复?
当HashSet或者HashMap添加元素的时候,会去判断添加的元素是否重复,1.首先调用hashCode()方法,得到这个对象的哈希值,然后对这个哈希值进行进一步的运算处理得到真正的散列依据
2.用真正的散列依据&(分组组数-1)得到这个对象要去的小组,然后才开始依次比较小组里的每一个元素
a>hashCode() 比较新老元素的hash值是否相等,如果不等则说明两个元素不相等,比较小组内下一个元素,全都不相等,则添加进集合;
如果相等,与该老元素进入连等比较
b>连等比较 比较新老元素的地址是否相同,如果相同,说明是同一个对象,舍弃新元素;如果不等,进入equals()比较
c>equals() 虽然不是同一个对象,但是程序员需要视作同一个对象,此时,舍弃新元素;如果不等,则比较下一个或者添加进集合
*:HashSet或者HashMap如何调整性能和空间的取舍?
HashSet以及HashMap构造方法可以传递两个参数,第一个是分组组数,第二个是加载因为,两者乘起来表示达到扩容条件的最小临界值,称为阈值,也就是当元素个数达到阈值时,HashSet会考虑扩容,2倍扩容,得到一个新的哈希表,再重新散列,效率很低
所以我们需要避免扩容,也就是当我们知道元素总量的情况下,传入参数,使得 分组组数*加载因子 > 数据总量,在这个条件下,增大分组组数,效率提高,但浪费空间
增大加载因子,节省空间,但效率较低。所以实际中如果内存不够使,应当让增大加载因子,节省空间,对效率要求较高,则提高分组组数
*:为什么集合在遍历过程中不能直接进行删除操作?
可以进行删除操作,但是不能在使用迭代器遍历时调用集合的remove()方法进行删除操作,必须使用迭代器的remove()进行删除,否则会触发ConcurrentModificationException,之所以会触发并发修改异常。是因为在用Iterator进行遍历时,实际上是通过集合中的一个实现Iterator接口的内部类调用集合的删除方法进行添加删除操作,ArrayList类中有Itr内部类,HashSet底层是HashMap,HashMap类中
存在HashIterator类操作数据,如果在用内部类对象操作集合元素的同时,再用集合对象添加或删除集合元素,就会触发并发修改异常。
另外底层集合通过modCount属性记录整个集合被修改的次数,Iterator内部类通过expectedModCount记录Iterator修改集合的次数,当创建Iterator的实现类对象时,会先把集合的modCount值
赋值给内部类的expectedModCount,当用Iterator修改集合元素时,会先把modCount加一,然后赋值给自己的expectedModCount,而用集合操作时,只是modCount
加一,并不赋值把增加后的值赋值给expectedModCount,当执行到iterator.next()方法时,会先判断两个值是否相等,如果相等,则取出元素,如果不等,则报并发修改异常。所以我们在使用集合
对象修改集合元素时,modCount加一,Iterator中的属性没加,两者不等,报并发修改异常。