8.3.3 TreeSet类 1.自然排序
8.3.3 TreeSet类
TreeSet是自动排序的集合
TreeSet
是SortedSet
接口的实现类,正如SortedSet
名字所暗示的, TreeSet
可以确保集合元素处于排序状态。
TreeSet比HashSet多出的方法
与HashSet
集合相比, TreeSet
还提供了如下几个额外的方法。
SortedSet接口方法
SortedSet接口方法 | 描述 |
---|---|
Comparator<? super E> comparator() |
如果TreeSet 采用了定制排序 ,则该方法返回定制排序所使用的Comparator ;如果TreeSet 采用了自然排序,则返回null . |
E first() |
返回集合中的第一个元素。 |
SortedSet<E> headSet(E toElement) |
返回此Set 的子集,由小于toElement 的元素组成。 |
E last() |
返回集合中的最后一个元素。 |
default Spliterator<E> spliterator() |
Creates a Spliterator over the elements in this sorted set. |
SortedSet<E> subSet(E fromElement, E toElement) |
返回此Set 的子集合,范围从fromElement (包含)到toElement (不包含)(范围前闭后开,类似String 类的substring 方法)。 |
SortedSet<E> tailSet(E fromElement) |
返回此Set 的子集,由大于或等于fromElement 的元素组成。 |
NavigableSet接口方法
方法 | 描述 |
---|---|
E lower(E e) |
返回集合中位于指定元素之前的元素(即集合中小于指定元素的最大元素,参考元素不需要是TreeSet 集合里的元素)。 |
E higher(E e) |
返回集合中位于指定元素之后的元素(即集合中大于指定元素的最小元素,参考元素不需要是TreeSet 集合里的元素)。 |
小结
表面上看起来这些方法很多,其实它们很简单:因为TreeSet
中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet
中截取子TreeSet
的方法。
实例
下面程序测试了TreeSet
的通用用法。
1 | import java.util.*; |
运行效果:
1 | [-9, 2, 5, 10] |
TreeSet排序
根据上面程序的运行结果即可看出, TreeSet
并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小
来进行排序
的。
与HashSet
集合采用hash
算法来决定元素的存储位置不同,
红黑树
TreeSet
采用红黑树
的数据结构来存储集合元素。
那么TreeSet
进行排序的规则是怎样的呢? TreeSet
支持两种排序方法:自然排序
和定制排序
。在默认情况下, TreeSet
采用自然排序。
1.自然排序
TreeSet
会调用集合元素的compareTo(Object object)
方法来比较元素之间的大小关系,然后将集合元素按升序排列
,这种方式就是自然排序。
Comparable接口
Java
提供了一个Comparable
接口,该接口里定义了一个compareTo(Object object)
方法:
Comparable 接口方法 |
描述 |
---|---|
int compareTo(T o) |
Compares this object with the specified object for order. |
该方法返回个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如object1.compareTo(object2)
- 如果该方法返回
0
,则表明这两个对象相等; - 如果该方法返回一个
正整数
,则表明object1
大于object2
; - 如果该方法返回一个
负整数
,则表明object1
小于object2
。
实现了Comparable接口的常见类
Java
的一些常用类已经实现了Comparable
接口,并提供了比较大小的标准。下面是实现了Comparable
接口的常用类。
BigDecimal
、BigInteger
以及所有的数值型对应的包装类
:按它们对应的数值大小进行比较。Character
:按字符的unicode
值进行比较。Boolean
:true
对应的包装类实例大于false
对应的包装类实例String
:按字符串中字符的unicode
值进行比较。Date
、Time
:后面的时间、日期比前面的时间、日期大。
TreeSet中的对象必须实现Comparable接口
如果试图把一个对象添加到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()
方法与集合中的其他元素进行比较。- 如果其对应的类没有实现
Comparable
接口,则会引发ClassCastException
异常。因此,上面程序将会在①号代码处引发该异常。
- 如果其对应的类没有实现
TreeSet中的第一个元素可以不实现Comparable接口
向TreeSet
集合中添加元素时,只有第一个元素无须实现Comparable
接口,后面添加的所有元素都必须实现Comparable
接口。
当然这也不是一种好做法,当试图从TreeSet
中取出元素时,依然会引发ClassCastException
异常。
还有一点必须指出:大部分类在实现compareTo(Object object)
方法时,都需要将被比较对象object
强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。
TreeSet中应该存放同一个类的对象
当试图把一个对象添加到TreeSet
集合时, TreeSet
会调用该对象的compareTo(Object object)
方法与集合中的其他元素进行比较——这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,向TreeSet
中添加的应该是同一个类的对象,否则也会引发ClassCastException
异常。如下程序示范了这个错误。
程序 TreeSet中添加不同类型的对象会出现异常
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 object)
方法与集合中的其他元素进行比较Date
对象的compareTo(Object object)
方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常
如果向TreeSet
中添加的对象是程序员自定义类的对象,则可以向TreeSet
中添加多种类型的对象,前提是用户自定义类实现了Comparable
接口,且实现compareTo(Object object)
方法没有进行强制类型转换。但当试图取出TreeSet
里的集合元素时,不同类型的元素依然会发生ClassCastException
异常。
总结 TreeSet只能添加同一种类的对象
总结起来一句话:如果希望TreeSet
能正常运作, TreeSet
只能添加同一种类型的对象。
当把一个对象加入TreeSet
集合中时, TreeSet
调用该对象的compareTo(Object object)
方法与容器中的其他对象比较大小,然后根据红黑树
结构找到它的存储位置。
相等的元素不会添加到TreeSet中
如果两个对象通过compareTo(Object object)
方法比较相等,新对象将无法添加到TreeSet
集合中。
compareTo返回0则TreeSet就认为这两个对象相等
对于TreeSet
集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object object)
方法比较是否返回0:
- 如果通过
compareTo( Object object)
方法比较返回0,TreeSet
则会认为它们相等 - 否则就认为它们不相等。
程序
1 | import java.util.*; |
程序中①代码行把同一个对象再次添加到TreeSet
集合中,因为z1
对象的compareTo(Object object)
方法总是返回1
,虽然它的equals
方法总是返回true
,但TreeSet
会认为z1
对象和它自己也不相等,因此TreeSet
可以添加两个z1
对象。图8.5显示了TreeSet
及Z对象在内存中的存储示意图
从图8.5可以看到TreeSet
对象保存的两个元素的引用,实际上是同一个元素的引用(集合里放的总是引用
)。所以当修改TreeSet
集合里第一个元素的age
变量后,该TreeSet
集合里最后一个元素的age
变量也随之改变了。
TreeSet中元素的规则
要确保equals方法返回true时compareTo返回0
**如果两个对象通过equals
方法比较返回true
时,这两个对象通过compareTo(Object object)
方法比较应该返回0
**。
equals方法返回false时compareTo返回0引起的问题
如果两个对象通过compareTo(Object object)
方法比较返回0时,但它们通过equals
方法比较返回false
,这将很麻烦,因为两个对象通过compareTo(Object object)
方法比较相等, TreeSet
不会让第二个元素添加进去,这就会与Set
集合的规则产生冲突。
TreeSet只会在添加元素时排序
如果向TreeSet
中添加一个可变对象后,并且后面程序修改了该可变对象
的实例变量,这将导致它与其他对象的大小顺序发生了改变,但**TreeSet
不会再次调整它们的顺序**,甚至可能导致TreeSet
中保存的这两个对象通过compareTo(Object object)
方法比较返回0。
程序 TreeSet存放可变对象的情况
下面程序演示了这种情况。
展开/折叠
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
可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象。
注意
当执行了④代码后, TreeSet
会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet
中的所有元素了,包括那些被修改过实例变量的元素。与HashSet
类似的是,如果TreeSet
中包含了可变对象,当可变对象的实例变量被修改时, TreeSet
在处理这些对象时将非常复杂,而且容易出错。
不要修改TreeSet中对象的关键实例变量值
为了让程序更加健壮,推荐不要修改放入HashSet
和TreeSet
集合中元素的关键实例变量