持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情
3 Set集合
Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add方法返回false,且新元素不会被加入。
Set判断两个对象相同不是使用==运算符,而是根据 equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象;反之,只要两个对象用equals方法比较返回false,Set就会接受这两个对象(甚至这两个对象是同一个对象,Set也可把它们当成两个对象处理,在后面程序中可以看到这种极端的情况)。
public class SetTest
{
public static void main(String[] args)
{
Set books=new HashSet();
//添加一个字符串对象
books.add(new String("疯狂Java讲义"));
//再次添加一个字符串对象
//因为两个字符串对象通过equals方法比较相等
//所以添加失败,返回false
boolean result=books.add(new String("疯狂Java讲义"));
//从下面输出看到集合只有一个元素
System.out.println(result + "-->" + books);
}
}
3.1 HashSet类
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
- 不能保证元素的排列顺序,顺序有可能发生变化。
- HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
- 集合元素值可以是null。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
//类A的equals()方法总是返回true,但没有重写其hashCode()方法
class A
{
public boolean equals(Object obj)
{
return true;
}
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
public int hashCode()
{
return 1;
}
}
//类C的hashCode()方法总是返回2,且重写了其equals()方法
class C
{
public int hashCode()
{
return 2;
}
public boolean equals(Object obj)
{
return true;
}
}
public class HashSetTest
{
public static void main(String[] args)
{
HashSet books=new HashSet();
//分别向books集合中添加两个A对象、两个B对象、两个C对象
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
上面程序中向books集合中分别添加了两个A对象、两个B对象和两个C对象,其中C类重写了equals()方法总是返回true,hashCode()方法总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行上面程序,看到如下运行结果:
[B@1, B@1, C@2, A@5483cd, A@9931f5]
当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。其规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。
如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两个对象保存在Hash表的不同位置,从而使两个对象都可以添加成功,这就与Set集合的规则有些出入了。
如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同,HashSet将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。
hashCode()方法对于HashSet是不是十分重要?
我们先要理解hash(也被翻译为哈希、散列)算法的功能——它能保证通过一个对象快速查找到另一个对象。hash算法的价值在于速度,它可以保证查询被快速执行。当需要查询集合中某个元素时,hash算法可以直接根据该元素的值计算出该元素的存储位置,从而可以让程序快速定位该元素。为了理解这个概念,我们先看数组(数组是所有能存储一组元素里最快的数据结构)。数组可以包含多个元素,每个元素也有索引,如果需要访问某个数组元素,只需提供该元素的索引,该索引即指出了该元素在数组内存区里的存储位置。
表面上看起来,HashSet集合里的元素都没有索引,实际上当程序向HashSet集合中添加元素时,HashSet会根据该元素的hashCode值来计算它的存储位置——也就是说,每个元素的hashCode值就可以决定它的存储“索引”。
**为什么不直接使用数组,还需要使用HashSet呢?**因为数组元素的索引是连续的,而且数组的长度是固定的,无法自由增加数组的长度。而HashSet就不一样了, HashSet采用每个元素的hashCode值来计算其索引,从而可以自由增加HashSet的长度,并可以根据元素的hashCode值来访问元素。因此,当从HashSet中访问元素时,HashSet先计算该元素的hashCode值(也就是调用该对象的hashCode()方法的返回值),然后直接到该hashCode值对应的位置去取出该元素——这就是HashSet速度很快的原因。
重写hashCode()方法的基本规则。
- 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值。
- 对象中用作equals()方法比较标准的Field,都应该用来计算hashCode值。
下面给出重写hashCode()方法的一般规则。
(1)把对象内每个有意义的Field(即每个用做equals()方法比较标准的Field)计算出一个int类型的hashCode值。
(2)用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回。例如如下代码:
return f1.hashCode() + (int)f2;
为了避免直接相加产生偶然相等(两个对象的f1、f2 Field并不相等,但它们的和恰好相等),可以通过为各Field乘以任意一个质数后再相加。例如如下代码:
return f1.hashCode() * 17+ (int)f2 * 13;
如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的Field,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。下面程序演示了这种情况。
class R
{
int count;
public R(int count)
{
this.count=count;
}
public String toString()
{
return "R[count:" + count + "]";
}
public boolean equals(Object obj)
{
if(this==obj)
return true;
if (obj !=null && obj.getClass()==R.class)
{
R r=(R)obj;
if (r.count==this.count)
{
return true;
}
}
return false;
}
public int hashCode()
{
return this.count;
}
}
public class HashSetTest2
{
public static void main(String[] args)
{
HashSet hs=new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
//打印HashSet集合,集合元素没有重复
System.out.println(hs);
//取出第一个元素
Iterator it=hs.iterator();
R first=(R)it.next();
//为第一个元素的count实例变量赋值
first.count=-3; //①
//再次输出HashSet集合,集合元素有重复元素
System.out.println(hs);
//删除count为-3的R对象
hs.remove(new R(-3)); //②
//可以看到被删除了一个R元素
System.out.println(hs);
//输出false
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3)));
//输出false
System.out.println("hs是否包含count为5的R对象?"
+ hs.contains(new R(5)));
}
}
上面程序中提供了R类,R类重写了equals(Object obj)方法和hashCode()方法,这两个方法都是根据R对象的count实例变量来判断的。上面程序的①号粗体字代码处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。运行结果:
正如图中所见到的,HashSet集合中的第一个元素和第三个元素完全相同,这表明两个元素已经重复,但因为HashSet把它们添加到了不同的地方,所以HashSet完全可以容纳两个相同的元素。
此时HashSet会比较混乱:当试图删除count为-3的R对象时,HashSet会
计算出该对象的hashCode值,从而找出该对象在集合中的保存位置,然后把此处的对象与count为-3的R对象通过equals()方法进行比较,如果相等则删除该对象——HashSet只有第三个元素才满足该条件(第一个元素实际上保存在count为5的R对象对应的位置),所以第三个元素被删除。至于第一个count为-3的R对象,它保存在count为5的R对象对应的位置,但使用equals()方法拿它和count为5的R对象比较时又返回false——这将导致HashSet不可能准确访问该元素。
**注意:**当向HashSet中添加可变对象时,必须十分小心。如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。
3.2 LinkedHashSet类
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
public class LinkedHashSetTest
{
public static void main(String[] args)
{
LinkedHashSet books=new LinkedHashSet();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
System.out.println(books);
//删除 疯狂Java讲义
books.remove("疯狂Java讲义");
//重新添加 疯狂Java讲义
books.add("疯狂Java讲义");
System.out.println(books);
}
}
编译、运行上面程序,看到如下输出:
[疯狂Java讲义, 轻量级Java EE企业应用实战]
[轻量级Java EE企业应用实战, 疯狂Java讲义]
输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致。
3.3 TreeSet类
TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与HashSet集合相比,TreeSet还提供了如下几个额外的方法。
- Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null。
- Object first():返回集合中的第一个元素。
- Object last():返回集合中的最后一个元素。
- Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。
- Object higher (Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。
- SortedSet subSet(fromElement, toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。
- SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。
- SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。
注意: 表面上看起来这些方法很复杂,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。
public class TreeSetTest
{
public static void main(String[] args)
{
TreeSet nums=new TreeSet();
//向TreeSet中添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
//输出集合元素,看到集合元素已经处于排序状态
System.out.println(nums);
//输出集合里的第一个元素
System.out.println(nums.first());
//输出集合里的最后一个元素
System.out.println(nums.last());
//返回小于4的子集,不包含4
System.out.println(nums.headSet(4));
//返回大于5的子集,如果Set中包含5,子集中也包含5
System.out.println(nums.tailSet(5));
//返回大于等于-3、小于4的子集
System.out.println(nums.subSet(-3 , 4));
}
}
运行结果:
[-9, 2, 5, 10]
-9
10
[-9, 2]
[5, 10]
[2]
与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。那么TreeSet进行排序的规则是怎样的呢?TreeSet支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。
1.自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。
Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。
BigDecimal、BigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。Character:按字符的UNICODE值进行比较。Boolean:true对应的包装类实例大于false对应的包装类实例。String:按字符串中字符的UNICODE值进行比较。Date、Time:后面的时间、日期比前面的时间、日期大。
如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。
class Err
{
}
public class TreeSetErrorTest
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
//向TreeSet集合中添加两个Err对象
ts.add(new Err());
ts.add(new Err()); //①
}
}
上面程序试图向TreeSet集合中添加两个Err对象,添加第一个对象时,TreeSet里没有任何元素,所以不会出现任何问题;当添加第二个Err对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——如果其对应的类没有实现Comparable接口,则会引发ClassCastException异常。
**注意:**向TreeSet集合中添加元素时,只有第一个元素无须实现Comparable接口,后面添加的所有元素都必须实现Comparable接口。当然这也不是一种好做法,当试图从TreeSet中取出元素时,依然会引发ClassCastException异常。
还有一点必须指出:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。 当试图把一个对象添加到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException异常
public class TreeSetErrorTest2
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
//向TreeSet集合中添加两个对象
ts.add(new String("Struts权威指南"));
ts.add(new Date()); //①
}
}
上面程序先向TreeSet集合中添加了一个字符串对象,这个操作完全正常。当添加第二个Date对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——Date对象的compareTo(Object obj)方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常。
如果向TreeSet中添加的对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,实现该接口时实现的compareTo(Object obj)方法没有进行强制类型转换。但当试图取出TreeSet里的集合数据时,不同类型的元素依然会发生ClassCastException异常。
当把一个对象加入TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。
对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0——如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等;否则就认为它们不相等。
class Z implements Comparable
{
int age;
public Z(int age)
{
this.age=age;
}
// 重写equals()方法,总是返回true
public boolean equals(Object obj)
{
return true;
}
//重写了compareTo(Object obj)方法,总是返回正整数
public int compareTo(Object obj)
{
return 1;
}
}
public class TreeSetTest2
{
public static void main(String[] args)
{
TreeSet set=new TreeSet();
Z z1=new Z(6);
set.add(z1);
//输出true,表明添加成功
System.out.println(set.add(z1)); //①
//下面输出set集合,将看到有两个元素
System.out.println(set);
//修改set集合的第一个元素的age变量
((Z)(set.first())).age=9;
//输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age);
}
}
程序中①代码行把同一个对象再次添加到TreeSet集合中,因为z1对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但TreeSet会认为z1对象和它自己也不相等,因此TreeSet可以添加两个z1对象。下图显示了TreeSet及Z对象在内存中的存储示意图。
可以看到TreeSet对象保存的两个元素(集合里的元素总是引用,但我们习惯上把被引用的对象称为集合元素),实际上是同一个元素。所以当修改TreeSet集合里第一个元素的age变量后,该TreeSet集合里最后一个元素的age变量也随之改变了。
当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0。
如果两个对象通过compareTo(Object obj)方法比较返回0时,但它们通过equals()方法比较返回false将很麻烦,因为两个对象通过compareTo(Object obj)方法比较相等,TreeSet不会让第二个元素添加进去,这就会与Set集合的规则产生冲突。
如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的Field,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们的顺序,甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0。下面程序演示了这种情况。
class R implements Comparable
{
int count;
public R(int count)
{
this.count=count;
}
public String toString()
{
return "R[count:" + count + "]";
}
//重写equals()方法,根据count来判断是否相等
public boolean equals(Object obj)
{
if (this==obj)
{
return true;
}
if(obj !=null && obj.getClass()==Z.class)
{
R r=(R)obj;
if (r.count==this.count)
{
return true;
}
}
return false;
}
//重写compareTo()方法,根据count来比较大小
public int compareTo(Object obj)
{
R r=(R)obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeSetTest3
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
ts.add(new R(5));
ts.add(new R(-3));
ts.add(new R(9));
ts.add(new R(-2));
//打印TreeSet集合,集合元素是有序排列的
System.out.println(ts); //①
//取出第一个元素
R first=(R)ts.first();
//对第一个元素的count赋值
first.count=20;
//取出最后一个元素
R last=(R)ts.last();
//对最后一个元素的count赋值,与第二个元素的count相同
last.count=-2;
//再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
System.out.println(ts); //②
//删除Field被改变的元素,删除失败
System.out.println(ts.remove(new R(-2))); //③
System.out.println(ts);
//删除Field没有改变的元素,删除成功
System.out.println(ts.remove(new R(5))); //④
System.out.println(ts);
}
}
运行结果:
一旦改变了TreeSet集合里可变元素的Field,当再试图删除该对象时,TreeSet也会删除失败(甚至集合中原有的、Field没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count为-2的R对象时,没有任何元素被删除;程序执行④代码时,可以看到删除了count为5的R对象,这表明TreeSet可以删除没有被修改Field,且不与其他被修改Field的对象重复的对象。
注意:当执行了④代码后,TreeSet会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet中的所有元素了,包括那些被修改过Field的元素。与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的Field被修改时,TreeSet在处理这些对象时将非常复杂,而且容易出错。为了让程序更加健壮,推荐HashSet和TreeSet集合中只放入不可变对象。至于如何创建不可变对象,请参考final修饰符。
2.定制排序 TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口里包含一个**int compare(T o1, T o2)**方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。
class M
{
int age;
public M(int age)
{
this.age=age;
}
public String toString()
{
return "M[age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet(new Comparator()
{
//根据M对象的age属性来决定大小
public int compare(Object o1, Object o2)
{
M m1=(M)o1;
M m2=(M)o2;
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
}
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}
上面程序中粗体字部分创建了一个Comparator接口的匿名内部类对象,该对象负责ts集合的排序。所以当我们把M对象添加到ts集合中时,无须M类实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较大小,而是由与TreeSet关联的Comparator对象来负责集合元素的排序。
[M对象(age:9), M对象(age:5), M对象(age:-3)]
注意: 当通过Comparator对象来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。使用定制排序时,TreeSet对集合元素排序不管集合元素本身的大小,而是由Comparator对象负责集合元素的排序规则。TreeSet判断两个集合元素相等的标准是:通过Comparator比较两个元素返回了0,这样TreeSet不会把第二个元素添加到集合中。
3.4 EnumSet类
EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll和retainAll方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常。 如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
3.5 各Set实现类的性能分析
HashSet和TreeSet是Set的两个典型实现,到底如何选择HashSet和TreeSet呢?HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
HashSet还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的;不过,因为有了链表,遍历LinkedHashSet会更快。
EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类的synchronizedSortedSet方法来“包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。例如:
SortedSet s=Collections.synchronizedSortedSet(new TreeSet(...));