前面已经介绍过 Set 集合,它类似于一个罐子,“丢进” Set,集合里的多个对象之间没有明显的顺序。Set 集合与 Collection 基本上完全一样,它没有提供任何额外的方法。实际上 Set 就是 Collection,只是行为略有不同(Set 不允许包含重复元素)。

Set 集合不允许包含相同的元素,如果试图把两个相同的元素放入同一个 Set 集合中,则添加操作失败,add 方法返回 false,且新元素不会被加入。

Set 判断两个对象相同不是使用 == 运算符,而是根据 equals 方法。也就是说,只要两个对象用 equals 方法比较返回 true,Set 就不会接受这两个对象;反之,只要两个对象用 equals 方法比较返回 false,Set 就会接受这两个对象(甚至这两个对象是同一个对象,Set 也可把它们当成两个对象处理,在后面程序中可以看到这种极端的情况)。下面是使用普通 Set 的示例程序。

package com.demo;

import java.util.HashSet;
import java.util.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);
	}
}

从上面程序中可以看出,books集合两次添加的字符串对象明显不是同一个对象(因为两次都调用了 new 关键字来创建字符串对象),这两个字符串对象使用==运算符判断肯定返回 false,但通过 equals 方法比较将返回 true,所以添加失败。最后输出 books 集合时,将看到暑促结果只有一个元素。

上面介绍的是 Set 集合的通用知识,因此完全适合后面介绍的 HashSet、TreeSet 和 EnumSet 三个实现类,只是三个实现类还各有特色。

1,HashSet 类

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。

HashSet 具有以下特点。

不能保证元素的排列顺序,顺序有可能发生变化。

HashSet 不是同步的,如果多个线程同时访问一个 HashSet,假设有两个或者两个以上线程同时修改了 HashSet 集合时,则必须通过代码来保证其同步。

集合元素值可以是 null。

当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回 true,但它们的 hashCode() 方法返回值不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。

简单地说,HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等,并且两个对象的 hashCode() 方法返回值也相等。

下面程序分别提供了三个类A、B和C,它们分别重写了 equals()、hashCode() 两个方法的一个或全部,通过此程序可以让读者看到 HashSet 判断集合元素怒相同的标准。

package com.demo;

import java.util.HashSet;

//类 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 的 equals() 方法总是返回2,且重写了 hashCode() 方法
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 对象当成同一个对象。运行上面程序,看到如下运行结果:

[com.demo.B@1, com.demo.B@1, com.demo.C@2, com.demo.A@2f57d162, com.demo.A@2e739136]

从上面程序可以看出,即使两个A 对象通过 equals() 方法比较返回 true,但 HashSet 依然把它们当成两个对象;即使两个 B 对象的 hashCode() 返回相同值(都是1

),但 HashSet 依然把它们当成两个对象。

这里有一个问题需要注意:当把一个对象放入HashSet中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其 hashCode() 方法。其规则是:如果两个对象通过 equals() 方法比较返回 true,这两个对象的 hashCode 值也应该相同。

如果两个对象通过 equals() 方法比较返回 true,但这两个对象的 hashCode() 方法返回不同的 hashCode 值时,这将导致 HashSet 会把这两个对象保存在 Hash 表的不同位置,从而使两个对象都可以添加成功,这就与 Set 集合的规则有些出入了。

如果两个对象的 hashCode() 方法返回的 hashCode 值相同,但它们通过 equals() 方法比较返回 false 时将更麻烦:因为两个对象的 hashCode 值相同, HashSet 将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象:而 HashSet 访问集合元素时也是根据元素的 hashCode 值来快速定位的,如果 HashSet 中两个以上的元素具有相同的 hashCode 值,将会导致性能下降。

如果需要把某个类的对象保存到 HashSet 集合中,重写这个类的 equals() 方法和 hashCode() 方法时,应该尽量保证两个对象通过 equals() 方法比较返回 true 时,它们的 hashCode() 方法返回值也相等。

HashSet 中每个能存储元素的“槽位”(slot)通常称为“桶”(bucket),如果有多个元素的 hashCode 值相同,但它们通过 equals 方法比较返回 false,就需要在一个“桶”里放多个元素,这样会导致性能下降。

前面介绍了 hashCode() 方法对于 HashSet 的重要性(实际上,对象的 hashCode 值对于后面的 HashMap 同样重要),下面给出重写 hashCode() 方法的基本规则。

在程序运行过程中,同一个对象多次调用 hashCode() 方法应该返回相同的值。

当两个对象通过 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法返回相等的值。

对象中用作 equals() 方法比较标准的 Filed,都应该用来计算 hashCode 值。

下面给出重写 hashCode() 方法的一般规则。

(1)把对象内每个有意义的 Field(即每个用做 equals() 方法比较标准的 Field)计算出一个 int 类型的 hashCode 值。计算方式如表 8.1 所示。

java中set集合如何排序 java set集合_System

(2)用第一步计算出来的多个 hashCode 值组合计算出一个 hashCode 值返回。例如如下代码:

return f1.hashCode() + (int)f2;

为了避免直接相加产生偶然相等(两个对象的f1、f2 Filed并不相等,但它们的和恰好相等),可以通过为各 Field 乘以任意一个质数后再相加。例如如下代码:

return f1.hashCode() * 17 + (int)f2 * 13;

如果向 HashSet 中添加一个可变对象后,后面程序修改了该可变对象的 Field,则可能导致它与集合中其他元素相同(即两个对象通过 equals() 方法比较返回 true, 两个对象的 hashCode 值也相等),这就有可能导致 HashSet 中包含两个相同的对象。下面程序演示了这种情况。

package com.sym.demo;

import java.util.HashSet;
import java.util.Iterator;

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 对象与集合中的其他对象相同。程序运行集合如下:


[R[count:5], R[count:9], R[count:-3], R[count:-2]]
[R[count:-3], R[count:9], R[count:-3], R[count:-2]]
[R[count:-3], R[count:9], R[count:-2]]
hs 是否包含 count 为 -3 的 R 对象?false
hs 是否包含 count 为 5 的R 对象?false

HashSet 集合中的第一个元素和第三个元素完全相同,这表明两个元素已经重复,但因为 HashSet 把它们添加到了不同的地方,所以 HashSet 完全可以容纳两个相同的元素。

此时 HashSet 会比较混乱: 当试图删除 count 为 -3 的 R对象时,HashSet 会计算出该对象的 hashCode 值,从而找出该对象在集合中的保存位置,然后把此处的对象与 count 为 -3 的 R 对象通过 equals() 方法进行比较,如果相等则删除该对象----- HashSet 只有第三个元素才满足该条件(第一个元素实际上保存在 count 为 5 的 R对象对应的位置),所以第三个元素被删除。至于第一个 count 为 -3 的 R 对象比较时又返回 false ---- 这将导致 HashSet 不可能准确访问该元素。

当向 HashSet 中添加可变对象时,必须十分小心。如果修改 HashSet 集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致 HashSet 无法准确访问该对象。

2,LinkedHashSet 类

HashSet 还有一个子类 LinkedHashSet,LinkedHashSet 集合也是根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet 集合里的元素时,LinkedHashSet 将会按元素的添加顺序来访问集合里的元素。

LinkedHashSet 需要维护元素的插入顺序,因此性能略低于 HashSet 的性能,但在迭代访问 Set 里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

package com.sym.demo;

import java.util.LinkedHashSet;

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讲义, 轻量级Java EE企业应用实战]

输出 LinkedHashSet 集合的元素时,元素的顺序总是与添加顺序一致。

虽然 LinkedHashSet 使用了链表记录集合元素的添加顺序,但LinkedHashSet 依然是 HashSet,因此它依然不允许集合元素重复。

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 的方法。

下面程序测试了 TreeSet 的通用用法。

package com.sym.demo;

import java.util.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]

根据上面程序的运行结果即可看出,TreeSet 并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的。

与 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 接口,否则程序将会抛出异常。如下程序示范了这个错误。

package com.sym.demo;

import java.util.TreeSet;

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 异常。如下程序示范了这个错误。

package com.sym.demo;

import java.util.Date;
import java.util.TreeSet;

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 则会认为它们相等;否则就认为它们不相等。

package com.sym.demo;

import java.util.TreeSet;

class Z implements Comparable{
	int age;
	public Z(int age){
		this.age = age;
	}
	//重写 equals() 方法,总是返回 false
	public boolean equals(Object obj){
		return true;
	}
	//重写了 compareTo(Object obj) 方法,总是返回正整数
	@Override
	public int compareTo(Object arg0) {
		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 集合,将看到有两个元素
		((Z)(set.first())).age = 9;
		//输出set 集合的最后一个元素的 age 变量,将看到也成了 9
		System.out.println(((Z)(set.last())).age);
	}
}

程序中①代码行把同一个对象再次添加到 TreeSet集合中,因为 z1 对象的 compareTo(Object obj) 方法总是返回 1,虽然它的 equal() 方法总是返回 true,但 TreeSet 会认为 z1 对象和它自己也不相等,因此 TreeSet 可以添加两个 z1 相等。图8.5 显示了 TreeSet 及 Z 对象在内存中的存储中的存储示意图。

java中set集合如何排序 java set集合_Java_02

从图 8.5 可以看到 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。下面程序演示了这种情况。

package com.sym.demo;

import java.util.TreeSet;

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 来比较大小
	@Override
	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);
	}
}

上面程序中的 R 对象对应的类正常重写了 equals() 方法和 compareTo() 方法,这两个方法都以 R 对象的 count 实例变量作为判断的依据。当程序执行①行代码时,看到程序输出的 Set 集合元素处于有序状态;因为 R 类是一个可变类,因此可以改变 R 对象的 count 实例变量的值,程序中1和2之间的代码行改变了该集合里第一个元素和最后一个元素的 count 实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。运行上面程序,得到如下结果。


[R[count:-3], R[count:-2], R[count:5], R[count:9]]
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
false
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
true
[R[count:20], R[count:-2], R[count:-2]]

一旦改变了 TreeSet 集合里可变元素的 Field,当再试图删除该对象时,TreeSet 也会删除失败(甚至集合中原有的、Field 没被修改但与修改元素相等的元素也无法删除),所以在上面程序的③代码处,删除 count 为 -2 的 R 对象时,没有任何元素被删除;程序执行④代码时,可以看到删除了 count 为 5 的 R 对象,这表明 TreeSet 可以伤处没有被修改 Field,且不与其他被修改 Field 的对象重复的对象。

当执行了④代码后,TreeSet 会对集合中的元素重新索引(不是重新排序),接下来就可以删除 TreeSet 中的所有元素了,包括那些被修改过 Field 的元素。与 HashSet 类似的是,如果 TreeSet 中包含了可变对象,当可变对象的 Field 被修改时,TreeSet 在处理这些对象时将非常复杂,而且容易出错。为了让程序更加健壮,推荐 HashSet 和 TreeSet 集合中只放入不可变对象。

(2),定制排序

TreeSet 的自然排序是根据集合的大小,TreeSet 将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过 Comparator 接口的帮助。该接口里包含一个 int compare(T o1, T o2) 方法,该方法用于比较 o1 和 o2 的大小;如果该方法返回正整数,则表明 o1 大于 o2;如果该方法返回 0,则表明 o1 等于 o2;如果该方法返回负整数,则表明 o1 小于 o2。

如果需要实现定制排序,则需要在创建 TreeSet集合对象时,提供一个 Comparator 对象与该 TreeSet 集合关联,有该 Comparator 对象负责集合元素的排序逻辑。

package com.sym.demo;

import java.util.Comparator;
import java.util.TreeSet;

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 属性来决定大小
			@Override
			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 不会把第二个元素添加到集合中。

4,EnumSet 类

EnumSet 是一个专为枚举类设计的集合类,EnumSet 中的所有元素都必须是指定枚举类型的枚举值,该枚举类型的在创建 EnumSet 时显式或隐式地指定。EnumSet 的集合元素也是有序的,EnumSet 以枚举值在 Enum 类内的定义顺序来决定集合元素的顺序。

EnumSet 在内部以位向量的形式存储,这种存储形式非常紧凑、搞笑,因此 EnumSet 对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用 containsAll 和 remainAll 方法)时,如果其参数也是 EnumSet 集合,则该批量操作的执行速度也非常快。

EnumSet 集合不允许加入 null 元素,如果试图出入null 元素,EnumSet 将抛出 NullPointerException异常。如果只是想判断 EnumSet 是否包含 null 元素都不会抛出异常,只是删除操作将返回 false,因为没有任何 null 元素被删除。

EnumSet 类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的 static 方法来创建 EnumSet 对象。EnumSet 类它提供了如下常用的 static 方法来创建 EnumSet 对象。

static EnumSet allOf(Class elementType): 创建一个包含指定枚举类里所有枚举值的 EnumSet 集合。

static EnumSet complementOf(EnumSet s): 创建一个其元素类型与指定 EnumSet 里元素类型相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的、此枚举类剩下的枚举值(即新 EnumSet 集合和原 EnumSet 集合的集合元素加起来就是该枚举类的所有枚举值)。

static EnumSet copyOf(Collection c): 使用一个普通集合来创建 EnumSet 集合。

static EnumSet copyOf(EnumSet s): 创建一个与指定 EnumSet 具有相同元素类型、相同集合元素的 EnumSet 集合。

static EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的空 EnumSet。

static EnumSet of(E first, E... rest): 创建一个包含一个或多个枚举值的 EnumSet 集合,传入的多个枚举值必须属于同一个枚举类。

static EnumSet range(E from, E to): 创建一个包含从 from 枚举值到 to 枚举值范围内所有枚举值的 EnumSet 集合。

下面程序示范了如何使用 EnumSet 来保存枚举类的多个枚举值。

package com.sym.demo;

import java.util.EnumSet;

enum Season{
	SPRING, SUMMER, FALL, WINTER
}
public class EnumSetTest {
	public static void main(String[] args) {
		//创建一个 EnumSet 集合,集合元素就是 Season 枚举类的全部枚举值
		EnumSet es1 = EnumSet.allOf(Season.class);
		//输出[SPRING, SUMMER, FALL, WINTER]
		System.out.println(es1);
		//创建一个 EnumSet 空集合,指定其集合元素是 Season 类的枚举值
		EnumSet es2 = EnumSet.noneOf(Season.class);
		//输出[]
		System.out.println(es2);
		es2.add(Season.WINTER);
		es2.add(Season.SPRING);
		//输出[SPRING, WINTER]
		System.out.println(es2);
		//以指定枚举值创建 EnumSet 集合
		EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
		//输出[SUMMER, WINNTER]
		System.out.println(es3);
		EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
		//输出[SUMMER, WINTER]
		System.out.println(es4);
		//新创建的 EnumSet 集合元素和 es4 集合元素有相同的类型
		//es5 集合元素 + es4 集合元素 = Season 枚举类的全部枚举值
		EnumSet es5 = EnumSet.complementOf(es4);
		//输出[SPRING]
		System.out.println(es5);
	}
}

上面程序中粗体字标识的代码示范了 EnumSet 集合的常规用法。 除此之外,还可以复制另一个 EnumSet 集合中的所有元素来创建新的 EnumSet 集合, 或者复制另一个 Collection 集合中的所有元素来创建心得 EnumSet 集合。当复制 Collection 集合中的所有元素来创建信的 EnumSet 集合时,要求 Collection 集合中的所有元素必须是同一个枚举类的枚举值。下面程序示范了这个用法。

package com.sym.demo;

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;

public class EnumSetTest2 {
	public static void main(String[] args) {
		Collection c = new HashSet();
		c.clear();
		c.add(Season.FALL);
		c.add(Season.SPRING);
		//复制 Collection 集合中的所有元素来创建 EnumSet 集合
		EnumSet enumSet = EnumSet.copyOf(c);//①
		//输出[SPRING, FALL]
		System.out.println(enumSet);
		c.add("疯狂Java讲义");
		c.add("轻量级Java EE企业应用实战");
		//下面代码出现异常,以为c集合里的元素不是全部都为枚举值
		enumSet = EnumSet.copyOf(c);//②
	}
}

上面程序中两处粗体字标识的代码没有任何区别,只是因为执行②行代码时,c集合中的元素不全是枚举值,而是包含了两个字符串对象,所以在②行代码出抛出 ClassCastException 异常。

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 集合的同步性。通常可以通过 Collection 工具类的 synchronizedSortedSet 方法来“包装”该 Set 集合。此操作最好在创建时进行,以防止对 Set 集合的以外非同步访问。

SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));