Set集合
Set集合概述
- Set集合类似于一个罐子,他只管把对象放进罐子中,而不管放入的次序
- Set集合与Collection集合基本相同,没有提供任何额外的方法
- Set集合不允许添加重复元素,若试图添加两个相同元素进入同一个Set集合中,则添加失败,add()方法返回false, 且新元素不会被加入
Set集合的实现类——HashSet类
- HashSet是Set接口的典型实现
- HashSet按Hash算法来存储集合中的元素,具有很好的存取和查找性能
- HashSet的特点:
- 不能保证元素的排列顺序
- HashSet不是同步的,若多个线程同时访问同一个HashSet,假设两个或以上线程同时修改HashSet集合时,必须通过代码来保证其同步。
- 集合元素可以是null
- HashSet集合判断两个元素是否相等是根据equals()方法的返回值和hashCode()的返回值来判断的,当equals()方法的返回值为true且hashCode()返回值相等时会认为两个元素相等
HashSet集合通过hashCode()方法的返回值来决定元素的位置,若两个元素通过equals()方法返回true,但是hashCode()方法返回值不同,HashSet()集合会将这两个元素存储到不同的位置
若需要将某个类的对象保存到HashSet集合时,建议重写equals()方法的同时也重写hashCode()方法,并且应尽量保证equals()方法返回true时它们的hashCode()方法返回值也相等
HashSet中每个能存储元素的“槽位“”称为“桶”, 如果有多个hashCode值相同,但equals()却返回false,就需要在一个“桶”中放多个元素,这样会导致性能下降。
- 重写hashCode()方法的基本规则:
- 在程序运行过程中,同一对象多次调用hashCode()方法应返回相同的值
- 当两个对象equals()方法返回true时两个hashCode值应相等
- 对象中用作equals()方法比较标准的实例变量,都应用于计算hashCode值
- 如果像HashSet集合中添加一个可变对象后,然后修改了可变对象的引用值(实例对象),则可能导致它与集合中另一个元素相等,这就导致了HashSet集合中有两个相同的元素,导致性能下降。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
class R {
int count;
public R(int count) {
this.count = count;
}
@Override
public String toString() {
return "R[count:" + count + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
R r = (R) o;
return count == r.count;
}
@Override
public int hashCode() {
return this.count;
}
}
public class HashSetTest {
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(3));
hs.add(new R(-2));
hs.add(new R(20));
System.out.println(hs);
Iterator it = hs.iterator();
R first = (R)it.next();
first.count = 5;
System.out.println(hs);
hs.remove(new R(5));
System.out.println("hs是否包含一个count值为-2的对象:"+hs.contains(new R(-2)));
System.out.println("hs是否包含一个count值为5的对象:"+hs.contains(new R(5)));
System.out.println(hs);
}
}
/*
运行结果:
[R[count:-2], R[count:3], R[count:20], R[count:5]]
[R[count:5], R[count:3], R[count:20], R[count:5]]
hs是否包含一个count值为-2的对象:false
hs是否包含一个count值为5的对象:false
[R[count:5], R[count:3], R[count:20]]
*/
当试图删除count值为5的元素时,HashSet会计算出count值为5的hashCode值,从而找到该对象的保存位置,然后通过equals()方法进行比较,如果相同,则删除。
由此可见,当可变对象添加到HashSet集合中后尽量不要修改,否则会导致无法正常操作集合中元素
LinkedHashSet类
LinkedHashSet类是HashSet类的子类,他在HashSet类的基础上使用链表维护元素的次序,当遍历LinkedHashSet集合时,将会暗元素的添加顺序来访问集合里的元素,因为要维护添加次序,因此会导致性能略差,但在迭代访问Set集合里的全部元素时会有很好的性能,因为它以链表维护内部顺序。
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet lhs = new LinkedHashSet();
lhs.add("a");
lhs.add("s");
lhs.add("d");
lhs.add("f");
System.out.println(lhs);
}
}
//运行结果:[a, s, d, f]
TreeSet类
- TreeSet是SortedSet接口的实现类
- TreeSet可以保证集合元素处于排序状态
- TreeSet集合使用红黑树的数据结构来存储集合元素
- 一下是TreeSet集合相对于HashSet集合的额外方法的代码体现
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.util.Scanner;
import java.util.TreeSet;
class Err {
}
public class TreeSetTest {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add("y");
ts.add("j");
ts.add("h");
ts.add("yjh");
ts.add("hjy");
//TreeSet集合要求自然排序的集合必须都实现该接口,
// 添加未实现Comparable接口的类时抛出ClassCastException异常
/*实现该接口的常用类有:
BigDecimal,BigInteger,Character,Boolean,String,Date, Time*/
//ts.add(new Err());
//向TreeSet集合中添加不同类时会引发ClassCastException异常
//ts.add(new Scanner(System.in));
System.out.println(ts);
//返回该集合的第一个元素
System.out.println(ts.first());
//返回该集合的最后一个元素
System.out.println(ts.last());
//返回该集合在参数之前的所有元素(不包括参数本身),参数不一定是该集合中的元素
System.out.println(ts.headSet("j"));
//返回该集合在参数之后的所有元素(包括参数本身),参数不一定是该集合中的元素
System.out.println(ts.tailSet("j"));
//返回该集合的一个大于第一个参数且小于第二个参数的元素子集合,参数不一定是该集合中的元素
System.out.println(ts.subSet("hjy", "yjh"));
System.out.println(ts.subSet("a", "yjh"));
System.out.println(ts.subSet("a", "z"));
//返回集合中比参数小的最大元素,参数不一定是该集合中的元素
System.out.println(ts.lower("l"));
//返回集合中比参数大的最小元素,参数不一定是该集合中的元素
System.out.println(ts.higher("l"));
}
}
TreeSet集合的排序方式
自然排序
- TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将几何元素按照升序排列,这就是自然排序
- 将一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,否则会出现下面代码演示的结果
import java.util.Objects;
import java.util.TreeSet;
class Z implements Comparable {
int count;
public Z(int count) {
this.count = count;
}
@Override
public boolean equals(Object o) {
return true;
}
@Override
public String toString() {
return "Z{" + "count=" + count + '}';
}
@Override
public int compareTo(Object o) {
return 1;
}
}
public class TreeSetTest2 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
Z z = new Z(7);
ts.add(z);
System.out.println(ts);
//TreeSet集合判断两个元素是否相同的唯一标准是compareTo()返回0
//因为Z对象的compareTo()永远返回1,因此认为两个同意元素不等,因此可以添加同一元素
ts.add(z);
System.out.println(ts);
((Z) ts.first()).count = 9;
System.out.println(((Z) ts.last()).count);
}
}
/*
运行结果:
[Z{count=7}]
[Z{count=7}, Z{count=7}]
9
*/
- 向TreeSet集合中添加可变对象,修改可变对象的实例变量,可能会导致与其他元素的大小发生了改变,但TreeSet集合不会再次调整他们的顺序,甚至可能导致TreeSet的compareTo()方法返回0。
import java.util.TreeSet;
class T implements Comparable{
int count;
public T(int count) {
this.count = count;
}
@Override
public String toString() {
return "T{" + "count=" + count + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
T t = (T) o;
return count == t.count;
}
@Override
public int compareTo(Object o) {
T t = (T) o;
return count > t.count ? 1 :
count < t.count ? -1 : 0;
}
}
public class TreeSetTest3 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new T(3));
ts.add(new T(1));
ts.add(new T(-5));
ts.add(new T(10));
System.out.println(ts);
((T)(ts.first())).count = 7;
((T)(ts.last())).count = -3;
System.out.println(ts);
}
}
定制排序
- 实现定制排序可以借助Comparator接口的int compare(T o1, T o2)方法
- 在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该方法负责集合元素的排序逻辑,Comparator是一个函数式接口,可以用Lambda表达式进行替代
import java.util.TreeSet;
class T {
int count;
public T(int count) {
this.count = count;
}
@Override
public String toString() {
return "T{" + "count=" + count + '}';
}
}
public class TreeSetTest4 {
public static void main(String[] args) {
//此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet((o1, o2)->{
T t1 = (T)o1;
T t2 = (T)o2;
//根据T对象的count属性来决定大小,count越大,T对象反而越小
return t1.count > t2.count ? -1 :
t1.count < t2.count ? 1 : 0;
});
ts.add(new T(3));
ts.add(new T(1));
ts.add(new T(-5));
ts.add(new T(10));
System.out.println(ts);
}
}
EnumSet类
- EnumSet类是专门为枚举类设置的一个集合类,EnumSet集合中的所有元素都必须是指定枚举类型的枚举值
- EnumSet集合的元素也是有序的,它是根据枚举值在Enum类内的定义顺序来决定的
- EnumSet集合在内部以位向量的形式存储,这使EnumSet对象占用内存小,并且运行效率很好。尤其是进行批量操作时,如果参数也是 EnumSet集合,执行速度会非常快
- EnumSet集合不允许加入null元素,若试图加入null元素,会抛出NullPointerException异常
- EnumSet集合没有提供构造器,只能通过类方法来创建对象
import java.util.EnumSet;
enum Season {
SPRING, SUMMER, FALL, WINTER
}
public class EnumSetTest {
public static void main(String[] args) {
//创建一个枚举集合,集合元素为Season枚举类所有元素
EnumSet es = EnumSet.allOf(Season);
System.out.println(es);
//创建一个空枚举集合,集合元素类型为Season
EnumSet es2 = EnumSet.noneOf(Season);
System.out.println(es2);
es2.add(Season.SPRING);
es2.add(Season.WINTER);
System.out.println(es2);
//以指定枚举值创建一个枚举集合
EnumSet es3 = EnumSet.of(Season.SPRING, Season.FALL);
System.out.println(es3);
//创建一个枚举集合,元素是枚举类中的一个范围中的元素
EnumSet es4 = EnumSet.range(Season.SPRING, Season.WINTER);
System.out.println(es4);
//创建一个枚举集合,集合中的元素与集合es4是补集关系
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);
}
}
各Set集合性能分析以及选择
- HashSet和TreeSet是Set的典型实现
- HashSet的性能总比TreeSet性能好,因为TreeSet集合需要维护集合元素的次序
- HashSet的一个子类LinkedHashSet,在HashSet的基础上按添加顺序来保存,所以比HashSet性能略差
- EnumSet性能是最好的,但是他只能存储同一类型的枚举值
- 仅仅只需要插入查找的话可以选择HashSet集合
- 若需要保持排序的时候,可以使用TreeSet集合
- 若想要遍历集合更快的话可以选择LinkedHashSet集合
Set接口的三个实现类都是线程不安全的,若有多个线程同时参与同一个集合,并且有一个以上线程同时对集合进行修改时必须手动保证该Set集合的同步性。
通常可以通过Collection工具类的synchronizedSortedSet方法来“包装”该集合。
此操作最好在创建时进行,以防止对Set集合的意外非同步访问
SortSet ss = Collection.synchronizedSortedSet(new HashSet());