[疯狂Java]集合:SortedSet、TreeSet

  • Post author:
  • Post category:java


1. SortedSet:

1) 顾名思义就是有序的Set,但是它的有序和LinkedHashSet不一样,LinkedHashSet维护的是插入时的顺序,而SortedSet维护的是元素之间大小关系的顺序(比如升序、降序等,是根据大小关系来维护顺序的);

2) 这种维护是时刻维护的(就跟堆维护堆序一样),每次插入元素的时候就会根据其大小放入合适的位置,删除一个元素后也会调整剩余元素的位置使之符合升序/降序状态;

3) SortedSet的实现类不多,基本只会用TreeSet这个实现类,之所以叫TreeSet,是因为它使用红黑树实现的,因此内部是一个树的结构;

2. TreeSet常用方法:都是其对象方法,里面的E表示模板类型,可以先理解为Object类型

1) 因为它是维护大小顺序的,因此其方法基本上都是与大小顺序有关的;

2) 找首尾元素:E first | last();

3) 找指定元素之前/之后的第一个元素:E lower | higher(E e);

i. 这个之前之后不是指大小,而是在序列中的位置;

ii. 如果是升序排列那么前后对应小大,如果是降序排列,那前后对应大小!!

iii. 指定的参数可以是不属于集合中的元素,但只要能和集合中的元素比较大小即可!例如集合:1, 2, 4, 5,那么lower(3) = 2

4) 获取子集:区间端点所代表的元素同样可以不属于集合中的元素

i. 区间子集:SortedSet<E> subSet(E fromElement, E toElement); // 获取[fromElement, toElement)区间内的子集(由于集合中元素是可比的,因此可以用区间表示)

ii. 头子集:SortedSet<E> headSet(E toElement);  // 获取(-∞, toElement)之间的子集

iii. 尾子集:SortedSet<E> tailSet(E fromElement); // 获取[fromElement, +∞)之间的子集

!!Java所有API中的区间都是左闭右开的!!

3. 如何设定排序:

1) 既然是有序的(大小顺序)那么应该有方法设定如何排序(升序还是降序),总公共有两种方法来设置排序,一种是自然排序,另一种是定值排序;

!!排序是自动的,但是排序的依据是大小的比较,因此排序设置也是围绕着大小比较进行的;

2) 自然排序:

i. 集合直接在底层(隐式)调用集合元素自定义的compareTo方法所设定的大小顺序进行排序;

ii. compareTo是一个接口方法,它位于Comparable接口中:

public interface Comparable<T> {
    public int compareTo(T o);
}

!这也是一个标志方法,很多需要比较大小的API都只认这个接口;

!!实现这个接口就必须实现compareTo方法才行!该方法就是拿自己和其它的o进行比较;

!!和C语言cmp的实现原理完全相同,前 – 后表示升序,后 – 前表示降序;

iii. 很多Java基础类像String、Date、BigDecimal、Integer等包装器类都已经实现了Comparable接口,所以可以放心使用;

!!特别的Boolean也实现了大小比较,规定true大于false;

3) 定制排序:

i. SortedSet的一个构造器版本中可以指定一个Comparator接口:TreeSet(Comparator comparator);

ii. Comparator顾名思义就是比较器的意思,就是指该TreeSet中元素的顺序由该比较起来规定!

iii. 其实SortedSet中有一个成员就是comparator,如果是自然排序(使用无参的构造器)就会默认把元素自己的compareTo赋给它,如果是使用的是上面的这个构造器就会把指定的比较器赋给comparator成员,因此如果同时使用自然排序和定制排序,那么定制排序将会覆盖自然排序(即只有定制排序起到作用!);

iv. Comparator是一个函数式接口:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

!里面也是一个比较方法,比较原理和compareTo完全相同;

v. 示例:定制一个降序排列的TreeSet:TreeSet ts = new TreeSet((o1, o2) -> ((A).o2).a – ((A)o1).a);  // 后 – 前就是降序

!!这里假设元素是A类型的,A类中只包含一个int a成员;

4. SortedSet(TreeSet)判断元素重复的标准:

1) 由于SortedSet也是Set的一种,因此也不允许元素重复!

2) SortedSet判断元素重复的标准还是由compareTo(自然排序)/Comparator(定制排序)规定的,返回0时表示相等;

3) 注意!并不是由元素的equals方法决定!但是设计类的时候一定要让equals和compare保持一致,否则会产生很多逻辑上的错误!!养成这个良好的习惯!

5. SortedSet注意事项1——必须实现compare:

1) 这里的实现compare是指compareTo或者Comparator二者之一(必须实现自然排序或者定值排序);

2) 虽然会有一些小的例外:如果两者都不实现,当你添加第一个元素的时候没有问题,但是添加第二个元素的时候就会抛出异常!因为添加第一个元素的时候不用和任何元素比较,但是添加第二个元素的时候需要和第一个元素比较大小来决定插入到什么位置,而此时发现没有compare方法提供比较服务,因此报错了!

3) 但是不要抱着这样的侥幸,好像如果集合中只有一个元素就不需要实现compare了!这是非常不好的习惯,必须要实现compare!

6. SortedSet注意事项2——类型问题:

1) 由于在实现compare方法(compareTo/Comparator时)中,接口API提供的接口方法要么是模板类型T或者是Object类型,因此在比较方法体中都要先进行类型转换成元素本身的类型之后再进行比较;

2) 正是由于上面的这个原因,SortedSet中应该只能存放类型相同的元素,如果两者类型不同,则在进入比较方法后类型转换将会失败!比如同时存放String和Date,那么在比较两者大小的时候不是调用String的compare就是调用Date的compare,两者的compare中都会将另一个参数的类型转化成自己的类型,但显然这两种类型是不兼容的,转换是必然会抛出异常!

3) 因此一句话总结就是:为了不发生错误,SortedSet中最好只放相同类型的元素,除非你的所有元素都是Object类型的!养成好习惯!

7. Collection保存的是引用——注意事项3(重申)最好不要保存可变数据:

1) 这里需要指出的是不管是Set还是Map,即所有的Collection保存的全都是引用(只能保存对象类型数据,即使直接存放基本类型,也会被自动隐式地包装成相应的包装器类型);

2) 因此像很多返回查找到的元素的方法(first、last等),返回的都是集合重元素的引用,如果你通过该引用修改了那个元素,那么就意味着集合中相应的元素也被修改了!

3) 正式因为这个问题,集合中最好不要保存可变数据,因为你在集合外的修改会影响到集合内;

4) 最致命的就是这样的修改会破坏集合本身的规则:由于修改可能会导致集合元素的hashCode、equals、compareTo/Comparator等结果的改变,但是这种改变并不会调整元素在数据结构中的位置,这可能会导致严重的错误!

5) 这之前讲过,在这里(SorteSet中)例子就是:集合[1, 2, 3, 4],如果你通过查找的方式获取了3的引用,并在集合外修改成了5,那么原集合就变成了[1, 2, 5, 4],并不会因为这种修改而更新排序,这就违背了SortedSet排序的原则,可能会在后面的逻辑中出现非常致命的错误!

6) 因此,如果你强行想修改元素同时也不想破坏集合原有的结构规则,那通用的方法就是:

i. 先获取想修改的元素;

ii. 删除该元素;

iii. 将修改后的元素再重新加入集合;

!!按照这个顺序就可以保持原有的结构规则了!即“拿出来”修改,然后再“放回去”;