Java 集合体系详解——Set体系无序不重复集合

时间:2022-11-22 19:25:37

引言

前一篇博文Java 集合体系详解——List体系有序集合总结了下List体系集合的用法,这一篇主要总结Set体系集合的相关知识点,Set集合体系存储特点是元素是无序的(存入和取出的顺序不一定一致的),元素不可以重复。和List集合一样拥有Collection接口的所有方法,当然也同样可以使用iterator()获得迭代器来进行遍历。

一Set集合体系之HashSet

1 HashSet的底层存储数据结构

底层数据结构是哈希表,线程是非同步的,保存的是对象的哈希值,而保存的顺序是由HashCode值决定的,并不一定是按照我们新建对象顺序保存的而是按照哈希值的大小顺序存的,就说在哈希值的表中是有顺序的,而这个顺序与新建对象的顺序没有关系,当存储的时候首先按照哈希值读取,当哈希值相同的时候(java自动哦调用hashCode()),还会进行一种校验,即当位置相同的时候还要判断对象是否相同(java调用equals),对象不相同的时候,就会在该地址下顺延,两个共享一个地址,如果哈希值不一样的时候,就放到另一个地址。就这样的方式构造哈希表。

2 HashSet保证元素唯一性的原理——hashCode方法和equals方法

java通过依次调用两个元素的两个方法——hashCode和equals根据返回的结果来完成,如果hashCode返回值相同,java才会再调用equals方法,若equals返回值也为true的时候,就认为是相同的对象,就不再保存该对象的哈希值到哈希表;如果hashCode返回值相同,java才会再调用equals方法,若equals返回值也为false的时候,就被认为是地址相同但但对象不同,就共享一个地址并顺延保存到哈希表;如果元素的hashCode值不同,就会认定对象不相同,就不会再去调用equals方法,直接保存到哈希表中。下面通过一个例子说明,把自定义对象人保存到HashSet中,并且如果年龄姓名相同则认为是同一个人,不能重复保存。

//自定义对象Person,至于为什么要重写父类的hashCode方法和equals方法
class Person{

private String name;
private int age;

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

//为了去除重复必须重新构造hashCOde
public int hasCode()
{
return 60; //但是这个方法不高效,应根据条件构造
//return name.hasCode()+age*37;
}

//当哈希值相同的时候才会执行
public boolean equals(Object obj){
if(!(obj instanceof Person)){
return false;
}
Person p=(Person)obj;
System.out.println(this.name+"~~equals~~"+p.name);
return this.name.equals(p.name) && this.age==p.age;
}
}

//调用代码
public static void main(String[] args){
HasSet<String> hs=new HasSet<String>();
hs.add(new Person("a1",11));
hs.add(new Person("a2",12));
hs.add(new Person("a3",13));
hs.add(new Person("a2",12));

Iterator<String> it=hs.iterator();
while(it.hasNext()){
Person p=(Person)it.next();
System.out.println(p.getName()+"::"+p.getAge());
}
}

执行结果及分析(因为hashCode值相同,所以所有的对象都是共用一个地址),右边为哈希表的抽象描述
Java 集合体系详解——Set体系无序不重复集合
由此我们再一次得出HashSet 判断和删除的依据是依赖于hashCode和equals,所以我们保存到HashSet集合里的对象为了更高效,都应该根据条件重写hashCode和equals方法。

二Set集合体系之TreeSet

1 TreeSet的底层存储数据结构

TreeSet和HashSet 的不同在于,TreeSet是有序的,TreeSet的两种排序方式:第一种排序方式,元素自身(普通类型的变量)具备比较性;二实现Comparable接口,覆盖compareTo方法
TreeSet的底层数据结构是二叉树,存储数据的时候,构造二叉树只根据compareTo的返回值判断,若返回值是1就把对象存储到树的右边分支,
返回值是1就是存储到左边分支,返回值是0说明对象已经存在则不会储存。compareTo即元素唯一性、判断、删除的依据,compareTo也是遍历时输出元素顺序的依据。
Java 集合体系详解——Set体系无序不重复集合

2 TreeSet 只能保存实现了Comparable接口的多个对象

往TreeSet集合中存储自定义对象人员,要求按照人员的年龄进行排序

/**若不实现Compareable接口时,add 2个或者以上对象的时候就会报异常,因为往TreeSet中保存对象的时候,TreeSet是会自动排序的,所以你必须告诉*他以什么规则排序(Compareable接口里的compareTo方法就是规则)
*/

class Person implements Comparable{

private String name;
private int age;

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

//当存储对象时候,该方法返回0的时候,就被看作是相同对象,就不会继续存储到TreeSet里了,因为Set体系是不重复的,
public int compareTo(Object obj){
if(!(obj instanceof Person))
throw new RuntimeException("不是人员对象");
Person p=(Person)obj;
if(this.age>p.age){
return 1;
}
else if (this.age<p.age){
return -1;
}
else{
return this.name.compareTo(s.name);
}
}
}

public static void main(String args[]){
TreeSet hs=new HasSet();
hs.add(new Person("zhang3",20));
hs.add(new Person("zhang4",22));
hs.add(new Person("zhang3",20));
hs.add(new Person("zhang6",25));

Iterator it=hs.iterator();
while(it.hasNext()){
Person p=(Person)it.next();
System.out.println(p.getName()+"::"+p.getAge());
}
}

PS:Java对于eqauls方法和hashCode方法是这样规定的
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。首先equals方法必须满足自反性(x.equals(x)必须返回true)、对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false。实现高质量的equals方法的诀窍包括:1. 使用==操作符检查”参数是否为这个对象的引用”;2. 使用instanceof操作符检查”参数是否为正确的类型”;3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完equals方法后,问自己它是否满足对称性、传递性、一致性;5. 重写equals时总是要重写hashCode;6. 不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。

Jdk 1.5版本之后,出现了一个新特性————泛型
用于解决安全问题,是一个安全机制,好处是将运行时期出现问题ClassCastException转移到了编译期,方便与开发者解决问题,让运行时期问题减少。简单来说当你存入数据到集合的时候(都应该使用泛型),不需要主观去判断类型是否合法,只要你存入的数据不合理,编译时期就报错了,而不像1.5之前运行的时候才报错,避免了强制转换麻烦.当使用集合时,将集合中要存储的数据类型作为参数传递到<>就是泛型,如TreeSet ts=new TreeSet(); Iterator it=ts.iterator();