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
发现第二个相同内容的对象没有存入成功!第二个相同对象存入的时候,发现地址值(哈希值)一样(字符串的hashCode方法仍然是比较地址值,此外,这种add方式创建的字符串,如果内容相同则地址值相同),地址值相同则哈希值相同,随后会比较对象内容,发现对象内容相同,判断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,不然不是重写,而是自己重载一个方法
{//注意这里必须判断obj是否是Person类型的
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 BufferClass
{
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())
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
*/
3、集合类补充——Set
补充1:
Set的特点
`java.util.Set`接口和`java.util.List`接口一样,同样继承自`Collection`接口,它与`Collection`接口中的方法基本一致,并没有对`Collection`接口进行功能上的扩充,只是比`Collection`接口更加严格了。(因为Set接口没有索引下标,也就没有相应的通过下标遍历集合的方法)
与`List`接口不同的是,`Set`接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
java.util.Set接口 extends Collection接口
Set接口的特点:
1.不允许存储重复的元素
2.没有索引,没有带索引的方法,也不能使用普通的for循环遍历
> tips:Set集合取出元素的方式可以采用:迭代器、增强for。
HashSet集合的特点
java.util.HashSet集合 implements Set接口
HashSet特点:
1.不允许存储重复的元素
2.没有索引,没有带索引的方法,也不能使用普通的for循环遍历
3.是一个无序的集合,存储元素和取出元素的顺序有可能不一致
4.底层是一个哈希表结构(查询的速度非常的快)
先说一下说明是哈希值
/*
哈希值:是一个十进制的整数,由系统随机给出(就是对象的地址值,是一个逻辑地址,是模拟出来得到地址,不是数据实际存储的物理地址)
在Object类有一个方法,可以获取对象的哈希值
int hashCode() 返回该对象的哈希码值。
hashCode方法的源码:
public native int hashCode();
native:代表该方法调用的是本地操作系统的方法
*/
哈希表结构见:集合类(集合框架)——泛型——集合补充的解析
补充2:
Set集合比较元素所使用的方法。
JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
Set集合存储元素不能重复的原理如下:
实际代码中是如何重写equals与hashCode方法的:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
补充3:
LinkedHashSet集合特点: 底层是一个哈希表(数组+链表/红黑树)+链表:多了一条链表(记录元素的存储顺序),保证元素有序,但是元素还是不可以重复。
我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?
在HashSet下面有一个子类java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构。
演示代码如下:
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:
bbb
aaa
abc
bbc
补充4:可变参数
示例如下
package com.itheima.demo04.VarArgs;
/*
可变参数:是JDK1.5之后出现的新特性
使用前提:
当方法的参数列表数据类型已经确定,但是参数的个数不确定,就可以使用可变参数.
使用格式:定义方法时使用
修饰符 返回值类型 方法名(数据类型...变量名){}
可变参数的原理:
可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数
传递的参数个数,可以是0个(不传递),1,2...多个
*/
public class Demo01VarArgs {
public static void main(String[] args) {
//int i = add();
//int i = add(10);
int i = add(10,20);
//int i = add(10,20,30,40,50,60,70,80,90,100);
System.out.println(i);
method("abc",5.5,10,1,2,3,4);
}
/*
可变参数的注意事项
1.一个方法的参数列表,只能有一个可变参数
2.如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
*/
/*public static void method(int...a,String...b){
}*/
/*public static void method(String b,double c,int d,int...a){
}*/
//可变参数的特殊(终极)写法
public static void method(Object...obj){
}
/*
定义计算(0-n)整数和的方法
已知:计算整数的和,数据类型已经确定int
但是参数的个数不确定,不知道要计算几个整数的和,就可以使用可变参数
add(); 就会创建一个长度为0的数组, new int[0]
add(10); 就会创建一个长度为1的数组,存储传递来过的参数 new int[]{10};
add(10,20); 就会创建一个长度为2的数组,存储传递来过的参数 new int[]{10,20};
add(10,20,30,40,50,60,70,80,90,100); 就会创建一个长度为2的数组,存储传递来过的参数 new int[]{10,20,30,40,50,60,70,80,90,100};
*/
public static int add(int...arr){
//System.out.println(arr);//[I@2ac1fdc4 底层是一个数组
//System.out.println(arr.length);//0,1,2,10
//定义一个初始化的变量,记录累加求和
int sum = 0;
//遍历数组,获取数组中的每一个元素
for (int i : arr) {
//累加求和
sum += i;
}
//把求和结果返回
return sum;
}
//定义一个方法,计算三个int类型整数的和
/*public static int add(int a,int b,int c){
return a+b+c;
}*/
//定义一个方法,计算两个int类型整数的和
/*public static int add(int a,int b){
return a+b;
}*/
}