您的位置:首页 > 编程语言 > Java开发

黑马毕向东Java课程笔记(day14-12——15-5):集合类(集合框架)——Set集合接口及其子类

2019-09-06 15:10 525 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/LKJgdut/article/details/100552361

1、Set——HashSet
  Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。Set集合的功能和Collection是一致的。因此我们不再赘述方法,重点关注Set的子类对象。Set的分类如下:

|--Set:
|--HashSet:底层数据结构是哈希表,是线程不安全的,不同步。(哈希表解释见视频14-12,6分钟开始处)
HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode和equals来完成。
如果元素的HashCode值相同,才会判断equals是否为true。
如果元素的hashcode值不同,不会调用equals。(见下面示例代码2)

注意,对于判断元素是否存在,以及删除等操作,HashSet依赖的方法是元素的hashcode和equals方法。
而ArrayList与LinkedList只依赖equals,这与他们的数据结构不同有关!

  哈希值的特点

哈希表可以允许同名的哈希值出现,存入一个哈希值,首先会比较内存中是否有相同的哈希值,
有的再比较2个对象的内容(equals),内容不同就将这个哈希值接到前一个相同哈希值的后方,内容相同后面的对象不存入内存;
没有哈希值相同的对象,就直接将哈希值放入内存。哈希表中按照哈希值顺序存放对象,不按存入顺序存放对象!

  HashSet的相关示例代码1

package pack;

import java.util.*;
class CollectionDemo
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
System.out.println(hs.add("haha"));
System.out.println(hs.add("haha"));
hs.add("heihei");
hs.add("hihi");
hs.add("huhu");
hs.add("huhu");
hs.add("hihi");

//取出元素:Set集合没有顺序,无法用循环的方式取出对象,只能使用迭代器取出
Iterator it = hs.iterator();
while(it.hasNext())
{
System.out.println(it.next());
}
}
}
/*
结果是:
haha
hihi
heihei
huhu
我们发现结果是无序的,因为每个字符串都有它自身的地址值,对应着他们的哈希值,HashSet是按照哈希值将对象存入内存的。
而且不管我们插入多少个相同的对象,始终不会出现重复的对象——HashSet中对象的唯一性!
我们再打印add方法的结果(返回boolean),发现
true
false
发现第二个相同内容的对象没有存入成功!第二个相同对象存入的时候,发现地址值(哈希值)一样(HashSet不允许对象重
3ff7
复,
同一个对象只有一个地址值),而且对象内容相同,判断2个对象为同一个元素,就不会再往里存放。
*/

  我们往HashSet集合中存放我们自定义的元素,示例2:

/*
往hashSet集合中存入自定对象,我们存放Person类,姓名和年龄相同视为同一个人,重复元素。

*/
package pack;

import java.util.*;
class CollectionDemo
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
hs.add(new Person("a1",11));
hs.add(new Person("a2",12));
hs.add(new Person("a3",13));
//		hs.add(new Person("a2",12));

//判断容器中是否包含某个对象
System.out.println("a1:"+hs.contains(new Person("a1",11)));

Iterator it = hs.iterator();
while(it.hasNext())
{
//Object obj = it.next();Person p = (Person)obj;
Person p = (Person)it.next();
System.out.println("姓名:"+p.getName()+"  年龄"+p.getAge());
}
}
}

class Person
{
private String name;
private int age;
Person(String name, int age)
{
this.name = name;
this.age = age;
}

public String getName()
{
return this.name;
}
public int getAge()
{
return this.age;
}
//重写Person的equals方法,用于比较name与age
//由于集合的方法对象都是Object,因此这里需要先传入Person向上类型转换形成的Object对象,再向下类型装换为Person
public boolean equals(Object obj)//注意这里重写equals方法,参数类型必须与元equals方法一样,为Object,不然不是重写,而是自己重载一个方法
{
if(!(obj instanceof Person))
return false;
//将obj向下类型转换为Person对象
Person p = (Person)obj;

System.out.println(this.name+"...equals.."+p.name);//这句话打印表明equals被调用了

return this.name.equals(p.name) && this.age == p.age;
}
//重写hashCode方法,保证name与age相同的对象哈希值相同,name与age不同的对象哈希值不同
public int hashCode()
{
System.out.println(this.name+".....hashCode");
return name.hashCode()+age*39;//我们返回与name和age相关的int值,保证哈希值的唯一性

}
}
/*
结果1:我们发现重复对象也一样输出,而且equals方法没有被调用。(见视频14-13,4.40开始处解释)
解析:HashSet对象往内存存放的时候,比较的是哈希值(哈希值包括类名与地址,比较的是地址),我们每次存放都会new新的Person对象,哈希值都不同,
那么就会直接往内存里面放,只有哈希值相同的时候才会调用equals方法比较内容是否相同!因此我们必须重写Person的hashCode方法!

由于我们是按照姓名与年龄来判断对象是否相同的,那么我们就用姓名与年龄来创建哈希值——我们只要保证姓名年龄相同哈希值相同即可,
这样传入相同姓名年龄的对象,发现哈希值(地址)相同,就会调用equals来比较name与age(见视频14-13,6.50开始处解释)
(先比地址(hashCode),相同再比内容(equals))

//返回60时结果
a1.....hashCode
a2.....hashCode
a2...equals..a1
a3.....hashCode
a3...equals..a1
a3...equals..a2
a2.....hashCode
a2...equals..a1
a2...equals..a2
姓名:a1  年龄11
姓名:a2  年龄12
姓名:a3  年龄13
发现虽然去除了内容重复对象,但是由于不同对象的哈希值相同,因此其比较了很多次!调用了很多次equals比较内容!
(先比哈希值hashCode,相同比内容equals,不同则不需要比较,而内容相同的对象在HashSet中只有一个,地址(哈希值)相同)

//返回name.hashCode()+age时
a1.....hashCode
a2.....hashCode
a3.....hashCode
a2.....hashCode
a2...equals..a2
姓名:a1  年龄11
姓名:a2  年龄12
姓名:a3  年龄13
去除了内容重复对象,不同对象的哈希值不同,只有在插入内容相同的对象时才会调用equals比较

判断是否包含的结果:(remove方法是同样的原理 )
a1.....hashCode
a2.....hashCode
a3.....hashCode//前三个先存储对象
a1.....hashCode//判断一个对象是否存在,先判断哈希值,发现哈希值重复
a1...equals..a1//再用equals判断内容是否相同,相同则对象在表中存在(既HashSet的底层调用链Person重写的hashCode与equals)
a1:true
姓名:a2  年龄12
姓名:a1  年龄11
姓名:a3  年龄13
*/

2、Set——TreeSet
  TreeSet的特点如下:

|--TreeSet:可以对Set集合中的元素进行排序。
底层数据结构是二叉树,(见视频15-3,0.40),二叉树的存取过程见视频15-3,8.40。
在二叉树中,后存入的对象对于先存入对象,大于放在右下,小于放在左下。
保证元素唯一性的依据:compareTo方法return 0。

也就是说,在判断对象是否相同的时候(或者删除元素、判断元素是否包含),
TreeSet底层调用的是compareTo来进行比较元素是否相同;
而HashSet先判断hashCode,哈希值相同再用断equals判内容是否相同;
而ArrayList与LinkedList底层只调用了equals进行比较。
我们应该根据所使用的数据结构以及所判断的对象的内容来重写集合底层所调用的方法。

TreeSet排序的第一种方式:让元素自身具备比较性。(15-3)
元素的类需要实现Comparable接口,覆盖compareTo方法。
这种方式也成为元素的自然顺序,或者叫做默认顺序。
这种方式使用的是TreeSet空的构造方法:TreeSet(),构造一个新的空 set,该 set 根据其元素的自然顺序进行排序(也就是实现Comparable)。(TreeSet示例2)

TreeSet的第二种排序方式。(15-4)
当元素自身不具备比较性时(没有实现comparable),或者具备的比较性不是所需要的。(类本身有描述compareTo,但是描述的内容不是我们所需要的)
这时就需要让集合自身具备比较性。在集合初始化时,就有了比较方式。
这里使用的是TreeSet参数包含比较器的构造方法:TreeSet(Comparator<? super E> comparator)
构造一个新的空 TreeSet,它根据指定比较器进行排序。(TreeSet示例3)

  TreeSet的示例1

import java.util.Iterator;
import java.util.TreeSet;

public class Buf
8000
ferClass
{
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add("ccc");
ts.add("aaa");
ts.add("ddd");
ts.add("bbb");

Iterator it = ts.iterator();
while(it.hasNext())
{
System.out.println(it.next());
}
}
}
/*
运行结果:
aaa
bbb
ccc
ddd
可以对Set集合中的元素进行排序,按照字母的ASCII顺序排列——具体见下面的解析
*/

  TreeSet的示例2——实现comparable接口使得元素自身具备比较性

/*
需求:往TreeSet集合中存储自定义对象学生,想按照学生的年龄进行排序。
注意,这次不是去除重复元素,而是排序!!!
记住,排序时,当主要条件相同时,一定判断一下次要条件。(如此处age相同则必须判断name)
*/
package pack;

import java.util.*;
class CollectionDemo
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
ts.add(new Student("lisi02",22));
ts.add(new Student("lisi007",20));
ts.add(new Student("lisi007",20));//重复元素不会进去
ts.add(new Student("lisi09",19));
ts.add(new Student("lisi08",19));//添加年龄相同但是姓名不同的对象
//		ts.add(new Student("lisi01",40));

Iterator it = ts.iterator();
while(it.hasNext())
{
Student stu = (Student)it.next();
System.out.println("姓名:"+stu.getName()+"  年龄"+stu.getAge());
}
}
}

class Student implements Comparable//该接口强制让学生具备比较性。
{
private String name;
private int age;
Student(String name, int age)
{
this.name = name;
this.age = age;
}

public String getName()
{
return this.name;
}
public int getAge()
{
return this.age;
}

@Override
public int compareTo(Object obj) {
//首先,进来先判断传入的Object对象是否属于Student,不属于就抛出RuntimeException异常,该异常不需要处理,因为是使用者传入的错误
if(!(obj instanceof Student))
throw new RuntimeException("不是学生对象");

//先将Object对象向下类型转换为Student对象
Student stu = (Student)obj;
//测试看看
System.out.println(this.name+"....compareto....."+stu.name);

//按照compareTo的规则:负整数、零或正整数,根据此对象是小于、等于还是大于指定对象。(只有按照规则底层代码才会识别!)TreeSet集合只看compareTo比较的int结果
//obj的age小于this的age,就返回1;如果年龄相同就比较姓名(字符串)是否相同(同样是返回);最后只有小于的情况,直接返回-1
if(this.age > stu.age)
{
return 1;
}
if(this.age == stu.age)
{//String已经复写了compareTo方法,会自动排序,这里字符串比较结果也是整数/负数/0
//那么如果age相同,name相同,返回0,同Student对象,TreesSet不存入;name不同,返回1/-1,不是同一个Student对象,存入
return this.name.compareTo(stu.name);
}
return -1;
}

}
/*
结果1:
java.lang.ClassCastException: pack.Student cannot be cast to java.lang.Comparable
抛出类型转换异常:Student不能被转换为Comparable接口.

分析:(视频15-2,6.20)
TreeSet集合会对传入的对象进行排序,但是它并不知道我们的需求(按年龄排序),Student对象根本不具备比较性。
也就是说,TreeSet对于传入的元素,要求该元素必须具备比较性,那么该传入元素的类必须实现Comparable接口。
Comparable:此接口强行对实现它的每个类的对象进行整体排序,这种排序被称为类的自然排序。(使得实现它的类对象具有可比较性)

结果2:添加Comparable接口并重写其compareTo方法之后
姓名:lisi09  年龄19
姓名:lisi007  年龄20
姓名:lisi02  年龄22
姓名:lisi01  年龄40
在添加Student对象的时候,TreeSet底层会调用Student的compareTo方法比较并按我们复写的规则,对年龄进行排序

结果3:插入查看比较过程的语句
lisi02....compareto.....lisi02
lisi007....compareto.....lisi02
lisi09....compareto.....lisi02
lisi09....compareto.....lisi007
姓名:lisi09  年龄19
姓名:lisi007  年龄20
姓名:lisi02  年龄22

结果4:添加年龄相同但是姓名不同的对象
lisi02....compareto.....lisi02
lisi007....compareto.....lisi02
lisi09....compareto.....lisi02
lisi09....compareto.....lisi007
lisi08....compareto.....lisi007
lisi08....compareto.....lisi09
姓名:lisi09  年龄19
姓名:lisi007  年龄20
姓名:lisi02  年龄22
发现只比较年龄,而年龄如果相同返回0,表示此对象等于比较的对象,那么年龄相同但是姓名不同的对象在TreeSet中不会被存入。也就是说我们在年龄比较完之后还需要比较姓名。

结果5:完全修改完毕
lisi02....compareto.....lisi02
lisi007....compareto.....lisi02
lisi09....compareto.....lisi02
lisi09....compareto.....lisi007
lisi08....compareto.....lisi007
lisi08....compareto.....lisi09
姓名:lisi08  年龄19
姓名:lisi09  年龄19
姓名:lisi007  年龄20
姓名:lisi02  年龄22
正常
*/

  TreeSet的示例3——实现比较器Comparator

/*
需求:假设我们使用的Student类是从别人那里取的,它实现了Comparable,但是它是先比较年龄,年龄相同再比较姓名
我们要求先比较姓名,姓名相同再比较年龄,因此这个Student的compareTo方法不能满足我们的需求。

当元素自身不具备比较性,或者具备的比较性不是所需要的,这时需要让容器自身具备比较性。
我们定义比较器,并将比较器对象作为参数传递给TreeSet集合的构造函数。
对于比较器,定义一个类,实现Comparator接口,覆盖compare方法。

当两种排序都存在时,以比较器为主。
*/
package pack;

import java.util.*;
class CollectionDemo
{
public static void main(String[] args)
{
//如果这里不添加比较器。则按Student的CompareTo方法排序
TreeSet ts = new TreeSet(new MyComparator());//将我们创建的比较器对象作为参数传递给TreeSet集合的构造函数
//在添加的时候TreeSet会在底层调用MyComparator的compare方法
ts.add(new Student("lisi02",22));
ts.add(new Student("lisi02",21));
ts.add(new Student("lisi007",20));
ts.add(new Student("lisi007",29));
ts.add(new Student("lisi09",19));
ts.add(new Student("lisi06",18));
ts.add(new Student("lisi06",18));

Iterator it = ts.iterator();
while(it.hasNext())
{
Student stu = (Student)it.next();
System.out.println("姓名:"+stu.getName()+"  年龄"+stu.getAge());
}
}
}

class Student implements Comparable
{
private String name;
private int age;
Student(String name, int age)
{
this.name = name;
this.age = age;
}

public String getName()
{
return this.name;
}
public int getAge()
{
return this.age;
}

@Override
public int compareTo(Object obj) {

if(!(obj instanceof Student))
throw new RuntimeException("不是学生对象");

Student stu = (Student)obj;

//		System.out.println(this.name+"....compareto....."+stu.name);

if(this.age > stu.age)
{
return 1;
}
if(this.age == stu.age)
{
return this.name.compareTo(stu.name);
}
return -1;
}

}

class MyComparator implements Comparator//定义一个类作为容器的比较器,实现Comparator接口
{

//重写Comparator的compare方法。
//这里不需要重写equals方法,因为MyComparator继承Object,而Object中已经有非抽象的equals方法,那么MyComparator不需要自己重写
@Override
public int compare(Object o1, Object o2) {//参数必须是Object,因为compare一开始不知道需要比较什么类
//这里需要判断instanceof,但是为了省事,这里不做描述
//首先,我们将2个需要进行比较的Object对象向下转型为我们比较的类对象
Student stu1 = (Student)o1;
Student stu2 = (Student)o2;

//接下来我们进行姓名的比较:定义一个int型的变量保存比较的结果
int compareResult = stu1.getName().compareTo(stu2.getName());

//姓名相同我们还需要比较年龄(compareResult=0)
if(compareResult == 0)
{
/*
if(stu1.getAge() == stu2.getAge())
return 0;
else if(stu1.getAge() > stu2.getAge())

8000
return 1;
else
return -1;
*/
//当然,我们可以利用包装类的特性将上面的代码简化
//包装类Integer有compareTo方法,可以比较2个Integer对象,同样返回正整数/负整数(不同),0(相同)
//我们利用Integer的构造方法将age变为Integer对象,再调用compareTo方法
return new Integer(stu1.getAge()).compareTo(new Integer(stu2.getAge()));
//			return Integer.valueOf(stu1.getAge()).compareTo(Integer.valueOf(stu2.getAge()));//形式2
}

//姓名不相同直接返回比较的结果(正整数或者负整数),表示2个对象不同
return compareResult;
}

}
/*
结果:
姓名:lisi007  年龄20
姓名:lisi007  年龄29
姓名:lisi02  年龄21
姓名:lisi02  年龄22
姓名:lisi06  年龄18
姓名:lisi09  年龄19
我们发现先按姓名对对象进行排序,姓名相同就会按年龄进行排序。
*/

  TreeSet的示例4——练习

/*
练习:按照字符串长度排序。
字符串本身具备比较性。但是它的比较方式不是所需要的,这时就只能使用比较器。
(15-5,5.30:用匿名内部类的方法写,不过这样写代码太繁琐!)
*/
package pack;

import java.util.*;
class CollectionDemo
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet(new MyString());
ts.add("abcd");
ts.add("abcdefi");
ts.add("abcdefh");
ts.add("abcdefg");
ts.add("abcde");
ts.add("abcdef");

Iterator it = ts.iterator();
while(it.hasNext())
{
String s = (String)it.next();
System.out.println(s.toString());
}
}
}

class MyString implements Comparator
{

@Override
public int compare(Object o1, Object o2)
{
if(!(o1 instanceof String) && !(o2 instanceof String))
throw new RuntimeException("输入的不是字符串对象");
String s1 = (String)o1;
String s2 = (String)o2;

int num = new Integer(s1.length()).compareTo(new Integer(s2.length()));
if(num == 0)
return s1.compareTo(s2);//当字符串长度相同,再比较内容
return num;
}

}
/*
结果:先比较长度,后比较内容
abcd
abcde
abcdef
abcdefg
abcdefh
abcdefi
*/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐