8.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(Object fromElement, Object toElement) |
返回此Set 的子集合,范围从fromElement (包含)到toElement (不包含)(范围前闭后开,类似String 类的substring 方法)。 |
||
SortedSet headSet(Object toElement) |
返回此Set 的子集,由小于toElement 的元素组成。 |
||
SortedSet tailSet(Object fromElement) |
返回此Set 的子集,由大于或等于fromElement 的元素组成。 |
||
表面上看起来这些方法很多,其实它们很简单:因为TreeSet 中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet 中截取子TreeSet 的方法。 |
|||
下面程序测试了TreeSet 的通用用法。 |
|||
|
运行效果:
1 | [-9, 2, 5, 10] |
根据上面程序的运行结果即可看出, TreeSet
并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的.
与HashSet
集合采用hash
算法来决定元素的存储位置不同, TreeSet
采用红黑树
的数据结构来存储集合元素。那么TreeSet
进行排序的规则是怎样的呢? TreeSet
支持两种排序方法:自然排序
和定制排序
。在默认情况下, TreeSet
采用自然排序。
1.自然排序
TreeSet
会调用集合元素的compareTo(Object obj)
方法来比较元素之间的大小关系,然后将集合元素按升序排列
,这种方式就是自然排序。Java
提供了一个Comparable
接口,该接口里定义了一个compareTo(Object obj)
方法,该方法返回个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2)
,
- 如果该方法返回0,则表明这两个对象相等;
- 如果该方法返回一个正整数,则表明
obj1
大于obj2
; - 如果该方法返回一个负整数,则表明
obj
小于obj2
。
Java
的一些常用类已经实现了Comparable
接口,并提供了比较大小的标准。下面是实现了Comparable
接口的常用类。
BigDecimal
、BigInteger
以及所有的数值型对应的包装类
:按它们对应的数值大小进行比较。Character
:按字符的unicode
值进行比较。Boolean
:true
对应的包装类实例大于false
对应的包装类实例String
:按字符串中字符的unicode
值进行比较.Date
、Time
:后面的时间、日期比前面的时间、日期大。
如果试图把一个对象添加到TreeSet
时,则该对象的类必须实现Comparable
接口,否则程序将会抛出异常。如下程序示范了这个错误。
1 | import java.util.*; |
运行效果:
1 | Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable |
- 上面程序试图向
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
异常。如下程序示范了这个错误。
1 | import java.util.*; |
运行效果:
1 | Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Date |
- 上面程序先向
TreeSet
集合中添加了一个字符串对象,这个操作完全正常。 - 当添加第二个
Date
对象时,TreeSet
就会调用该对象的compareTo(Object obj)
方法与集合中的其他元素进行比较Date
对象的compareTo(Object obj)
方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常
如果向TreeSet
中添加的对象是程序员自定义类的对象,则可以向TreeSet
中添加多种类型的对象,前提是用户自定义类实现了Comparable
接口,且实现compareTo(Object obj)
方法没有进行强制类型转换。但当试图取出TreeSet
里的集合元素时,不同类型的元素依然会发生ClassCastException
异常。
总结起来一句话:如果希望TreeSet
能正常运作, TreeSet
只能添加同一种类型的对象。
当把一个对象加入TreeSet
集合中时, TreeSet
调用该对象的compareTo(Object obj)
方法与容器中的其他对象比较大小,然后根据红黑树
结构找到它的存储位置。如果两个对象通过compareTo(Object obj)
方法比较相等,新对象将无法添加到TreeSet
集合中。
对于TreeSet
集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo( Object ob)
方法比较是否返回0:
- 如果通过
compareTo( Object obj)
方法比较返回0,TreeSet
则会认为它们相等 - 否则就认为它们不相等。
1 | import java.util.*; |
程序中①代码行把同一个对象再次添加到TreeSet
集合中,因为z1
对象的compareTo(Object obj)
方法总是返回1,虽然它的equals
方法总是返回true
,但TreeSet
会认为z1
对象和它自己也不相等,因此TreeSet
可以添加两个z1
对象。图8.5显示了TreeSet
及Z对象在内存中的存储示意图
从图8.5可以看到TreeSet
对象保存的两个元素的引用,实际上是同一个元素的引用(集合里放的总是引用
)。所以当修改TreeSet
集合里第一个元素的age
变量后,该TreeSet
集合里最后一个元素的age
变量也随之改变了。
TreeSet中元素的规则
如果两个对象通过equals
方法比较返回true
时,这两个对象通过compareTo( Object obj)
方法比较应返回0
.
如果两个对象通过compareTo(Object obj)
方法比较返回0时,但它们通过equals
方法比较返回false
将很麻烦,因为两个对象通过compareTo(Object obj)
方法比较相等, TreeSet
不会让第二个元素添加进去,这就会与Set
集合的规则产生冲突。
如果向TreeSet
中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet
不会再次调整它们的顺序,甚至可能导致TreeSet
中保存的这两个对象通过compareTo(Object obj)
方法比较返回0。下面程序演示了这种情况。
1 | import java.util.*; |
上面程序中的R对象对应的类正常重写了equals()
方法和compareTo()
方法,这两个方法都以R对象的count
实例变量作为判断的依据。当程序执行①行代码时,看到程序输出的Set
集合元素处于有序状态;
因为R类是一个可变类,因此可以改变R对象的count
实例变量的值,程序后续代码行改变了该集合里第一个元素和最后一个元素的count
实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。运行上面程序,看到如下所示的结果.
1 | [R[count:-3], R[count:-2], R[count:5], R[count:9]] |
一旦改变了TreeSet
集合里可变元素的实例变量,当再试图删除该对象时, TreeSet
也会删除失败(甚至集合中原有的、实例变量没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count
为-2
的R对象时,没有任何元素被删除;程序执行④代码时,可以看到删除了count
为5的R对象,这表明TreeSet
可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象。
注意
当执行了④代码后, Tree Set
会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet
中的所有元素了,包括那些被修改过实例变量的元素。与HashSet
类似的是,如果TreeSet
中包含了可变对象,当可变对象的实例变量被修改时, TreeSet
在处理这些对象时将非常复杂,而且容易出错。为了让程序更加健壮,推荐不要修改放入HashSet
和TreeSet
集合中元素的关键实例变量
原文链接: 8.3.3 TreeSet类 1.自然排序