java学习脚印:集合(Collection)之接口
写在前面
java集合框架内容还是比较多的,我认为首先应该把注意力放在集合框架实现思想和接口的关键点上。这节不会过多的涉及到集合框架中的具体实现类和算法,这部分内容在《java学习脚印:集合(Collection)之实现类 》和《java学习脚印:集合(Collection)之算法》有所介绍 。
1.集合框架概述
1.1 集合框架的优势
java提供了一套高效、广泛使用的集合框架,供程序员使用。
使用集合框架的优势就在于:
- 减少劳动量。避免重复编写像链表这类底层数据结构以及排序查找之类的基础算法;
- 提高程序性能和质量。集合框架提供了高质量和性能的数据结构和算法实现,把程序员从编写基础数据结构和算法的劳动中解脱出来,让他们将重心放在提高程序质量和性能之上;
- 集合框架提供了不相关类的API之间的互操作。例如构造转换函数,可以利用链表构造一个哈希表,Arrays.asList()可以将数组转换为列表来使用等等;
- 集合框架提高程序可读性。当我们看到别人的代码中出现集合框架提供的Collections.max时我们立刻就明白了程序员的意图,而避免了像getMax,Max之类函数名引起的阅读歧义;
- 减少设计和学习API的复杂度。利用集合框架提供的标准接口,不用重复发明*。
- 促进软件重用。只要遵循既定的接口,新的数据结构和算法都可以很好的被重用;
1.2 为什么叫集合?
首先看一下官网对Collection的定义:
A collection — sometimes called a container — is simply an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data.
也就是说collection,实际上可以理解为用来存储,取回,操作和以及与聚合数据进行交互的数据容器。
个人理解,简而言之,这里的集合不简单是数学概念上的集合,而是聚合数据的容器附加上操作数据的方法。
1.3 Collection与Collections区别
在学习的时候要注意区分这两个概念。
java.util.Collection 是一个集合接口。
它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
java.util.Collections 是一个包装类。
它包含有各种有关集合操作的静态多态方法。此类未提供public构造器,不能实例化,就像一个工具类,服务于Java的Collection框架。
2.集合框架实现的思想
java集合框架接口与实现分离的思想,很值得借鉴,使用这种思想,有明显的优点:
1)接口与实现分离,是抽象数据类型的体现。
在这种方式下,数据操作与具体的数据表示分隔开来,只要实现了接口就一定可以执行相关的操作。
例如,你无需关心是用顺序存贮实现队列(ArrayDeque)还是链式存贮实现队列(LinkedList),只要他们都实现了Deque接口,他们就能执行先进先出(FIFO)的操作。
例如,Deque<String> deque = new LinkedList<>();
可以很快的切换到:
Deque<String> deque = new ArrayDeque<>();
2)提取集合类共性,抽象为最高层接口供使用。
Collection接口,就是这样一个类,他是各种集合类操作的最小公分母(the least common denominator),所有的其他接口都继承此接口,这是一种高度抽象,它为集合类中的构造转换提供了极大的方便。
另外,java集合框架中为了避免接口数量膨胀,还通过定义可选操作(optional operations),控制接口的数量。
例如,在某些集合类的只读视图上的操作,包括删除,更改等操作无法执行,如果为其建立独立的接口的话,接口数量将成倍增长。java将这些不一定能执行的操作描述为可选操作,如果该操作不支持的话就抛出UnsupportedOperationException。
既然实现了接口,又不能执行相关的操作,看起来是个自相矛盾,这一点是一种特殊的处理。《java核心技术》建议,不应该将可选操作这一方式应用到自己的用户设计中。
3.主要的接口
3.1 接口关系
集合框架中提供了很多接口,他们的关系如下图所示:
3.2 接口关键点介绍
集合框架包括接口,实现(具体类),以及算法三部分。首先学习接口。
java接口和实现类数量都比较多,重点把握每个接口的独特之处,以数据结构的几种数据类型为基础,然后结合java语言的特性去理解就显得容易很多。
3.2.1 Collection接口
Collection接口是集合类的最小公分母,也就是提供公共操作的最大交集。
基本操作包括:
int size(), boolean isEmpty(), boolean contains(Object element), boolean add(E element), boolean remove(Object element), and Iterator<E> iterator().
也包括批操作(Bulk Operations):
boolean containsAll(Collection<?> c), boolean addAll(Collection<? extends E> c), boolean removeAll(Collection<?> c), boolean retainAll(Collection<?> c), and void clear().
还提供了返回集合对象的数组的操作:
Object[] toArray() 和 <T> T[] toArray(T[] a) 。
3.2.2 Set接口
Set接口用于模拟数学上的集合的概念,它不能包含重复元素。
Set中只包含从Collection继承而来的方法,并且禁止重复元素。
重点掌握两点:
1)实现Set的三个通常类是HashSet, TreeSet, 和 LinkedHashSet。
其中HashSet利用哈希表存贮元素性能最好,但是不保证元素的迭代顺序;
TreeSet利用红黑树存贮元素,元素以值为参考排序,由于每次添加的元素都要放在正确的排序位置上,因此比HashSet明显要慢;
而LinkedHashSet可以看做是在HashSet的基础上通过双向链表保存了元素顺序的Set。通过使用双向链表将添加到哈希表中的元素按添加顺序链接到一起,就保存了元素添加到哈希表时的顺序。可参见代码清单3-1 CollectionsDemo1.java 。
2)Set加强了equals和hashCode操作的约定,只要两个集包含相同元素,不管具体实现就认为他们相等,这种比较更有意义,也就是比较逻辑相等性。
下面通过一段代码清单3-1来加深理解。
代码清单3-1: CollectionsDemo1.java
package com.learningjava;
import java.util.*;
/**
* This program illustrate usage of Set
* @author wangdq
* 2013-11-2
*/
public class CollectionsDemo1 {
public static void main(String[] args) {
String[] ads = new String[]{"what", "you", "see",
"is", "what" ,"you","get"};
String[] filmName = new String[]{"now","you","see","me"};
List<String> strings = Arrays.asList(ads);
System.out.println("original string:"+strings);
//no guarantees concerning the order
Set<String> normalSet = new HashSet<String>(strings);
System.out.println(normalSet.size()
+ " distinct words: " + normalSet);
//orders its elements based on their values
Set<String> valueOderedset = new TreeSet<String>(strings);
System.out.println(valueOderedset.size()
+ " distinct words: " + valueOderedset);
//based on the order in which they were inserted into the set
Set<String> insertionOrderedSet = new LinkedHashSet<String>(strings);
System.out.println(insertionOrderedSet.size()
+ " distinct words: " + insertionOrderedSet);
//a stronger contract on the behavior of the equals and hashCode
System.out.println("equal method result:"
+normalSet.equals(valueOderedset));
System.out.println("hashCode method result:"
+(normalSet.hashCode() == valueOderedset.hashCode()));
//Bulk Operations ,get symmetric set difference
Set<String> filmNameSet = new HashSet<>(Arrays.asList(filmName));
Set<String> symmetricDiff = new HashSet<>(filmNameSet);
symmetricDiff.addAll(normalSet);
Set<String> tmp = new HashSet<String>(filmNameSet);
tmp.retainAll(normalSet);
symmetricDiff.removeAll(tmp);
System.out.println("symmetric set difference is:"
+symmetricDiff);
}
}
程序输出:
original string:[what, you, see, is, what, you, get]
5 distinct words: [is, get, see, what, you]
5 distinct words: [get, is, see, what, you]
5 distinct words: [what, you, see, is, get]
equal method result:true
hashCode method result:true
symmetric set difference is:[is, get, what, now, me]
分析上述程序及运行结果,就可以理解上述两个关键点了。
3.2.3 List接口
List就是线性表,官网原文为:
A List is an orderedCollection (sometimes called a sequence).
部分中文翻译,将Ordered Collection 翻译为有序集合,我认为是不妥当的,
这里应该理解为线性,即所谓的Linear。
对于List接口重点把握4点:
1)List上的按索引访问(Positional access)。
线性表的顺序存贮实现类是ArrayList,链式存贮实现类是LinkedList,这两个类均实现了List接口。
在List接口中声明了支持随机访问的一些方法,例如E get(int index);方法。我们知道,顺序存贮的支持随机访问,而链式存贮并不支持随机访问,但是java语言中并未严格禁止在LinkedList上调用get(index)方法访问链表,当然这样效率很低,程序员需要自己注意不要这样去使用。
为了避免在链表这种不支持随机访问的线性表上,采用随机访问,jdk 1.4 引入了上图之中的标记接口RandomAccess,该接口没有任何方法,只是用来表明该集合类支持快速随机访问。例如,ArrayList就实现了该接口,而LinkedList则没有实现该接口。
总之,注意这里的按索引访问的一些限制,代码清单3-2中也给出了调用随机访问方法,交换ArrayList和LinkedList上两个结点的所消耗时间的对比,借助这个例子可以更好的理解这一点。
2)List上的迭代。
注意List 支持两种迭代器,即Iterator和ListIterator。
ListIterator继承Iterator而来,它支持前向和后向遍历,删除和更改操作。
注意两点细节:
a. java语言中的迭代器概念,迭代器的游标(cursor)始终位于两个元素之间的位置之上,如下图所示,其中^表示游标位置:
元素 : ^Element(0) ^ Element(1) ^Element(2) ^ ... Element(n-1)^
也就是说,有n元素的话,就有n+1个游标位置。
b. ListIterator的remove和Set操作都是可选操作,他们不一定能正确执行。
并且这两个操作作用的对象都是next()或者previous方法刚刚访问过的元素。
3)List的相等性测试。
equals和hashCode方法,也是作逻辑相等性测试,即不管表的具体实现,只要包含相同的元素且顺序一致则相等。
4) 范围视图(range-view)操作,主要体现为sublist方法上。
List<E> subList(int fromIndex,int toIndex)取原表的[fromIndex,toIndex)范围的列表。
a. 在sublist上执行的修改将会反映到原列表中,反之亦然。例如:
list.subList(from, to).clear();删除了子列表,那么原列表中的内容也将被删除。
b. 在视图中执行的查询操作,返回的结果是在子列表中的索引,
而不是在原列表中的索引。
c. 如果原列表不是通过sublist返回的list执行了添加或者删除操作,
那么子列表将变为未定义状态,因此建议把sublist操作返回的子列表作为瞬时对象。
d. 允许在修改子列表的子列表的同时保持对原子列表的使用。
对于列表的上述关键点,可通过代码清单3-2 :CollectionsDemo2.java来加深理解。
代码清单3-2 :CollectionsDemo2.java
package com.learningjava;
import java.util.*;
/**
* This program illustrate usage of List
* @author wangdq
* 2013-11-2
*/
public class CollectionsDemo2 {
public static void main(String[] args) {
String[] ads = new String[]{"what", "you", "see", "is", "what" ,"you","get"};
String[] filmName = new String[]{"now","you","see","me"};
List<String> arrayList1 = new ArrayList<>(Arrays.asList(ads));
List<String> arrayList2 = new ArrayList<>(Arrays.asList(filmName));
List<String> linkList = new LinkedList<>(Arrays.asList(filmName));
//part1: positional access
//swap linkedlist elements
TimeCounter.start();
swap(linkList,1,3);
final long duration = TimeCounter.end();
System.out.println("time consumed in linkList : "
+duration+" nanoseconds");
System.out.println(linkList);
//swap arraylist elements
TimeCounter.start();
swap(arrayList2,1,3);
final long duration2 =TimeCounter.end();
System.out.println("time consumed in arrayList : "
+duration2+" nanoseconds");
System.out.println(arrayList2);
//part2: Bulk Operations
List<String> listBk = new ArrayList<String>(arrayList1);
listBk.addAll(linkList);
System.out.println(listBk);
//part3: Iterators and ConcurrentModificationException
ListIterator<String> biDirectionalIt = arrayList2.listIterator();
System.out.println("nextIndex= "
+biDirectionalIt.nextIndex()+
" previousIndex= "+biDirectionalIt.previousIndex());
Iterator<String> forwardOnlyIt = arrayList2.iterator();
biDirectionalIt.next();
biDirectionalIt.remove();
try{
forwardOnlyIt.next();
} catch(ConcurrentModificationException e) {
System.err.println(e);
}
//part4: Range-View(sublist) Operation
List<String> subList = arrayList1.subList(2, 6);
System.out.println("sublist :"+subList);
System.out.println(subList.indexOf("is"));
System.out.println(arrayList1.indexOf("is"));
Collections.reverse(subList);
System.out.println("after reverse :"+arrayList1);
}
//swap element ,positional access
public static <E> void swap(List<E> a, int i, int j) {
E tmp = a.get(i);
a.set(i, a.get(j));
a.set(j, tmp);
}
}
/**
* calculate time consumed
*/
class TimeCounter {
public static void start() {
startTime = System.nanoTime();
}
public static long end() {
return System.nanoTime() - startTime;
}
private static long startTime;
}
程序输出:
time consumed in linkList : 47771 nanoseconds
[now, me, see, you]
time consumed in arrayList : 4470 nanoseconds
[now, me, see, you]
[what, you, see, is, what, you, get, now, me, see, you]
nextIndex= 0 previousIndex= -1
java.util.ConcurrentModificationException
sublist :[see, is, what, you]
1
3
after reverse :[what, you, you, what, is, see, get]
这里可以看出交换顺序列表和链式列表中两个元素所消耗的时间差距是很明显的;
同时看到了当连个迭代器在访问同一个列表时引起的ConcurrentModificationException异常,以及子列表中查询操作的索引不等于原列表中的索引。
3.2.4 Queue与Deque接口
Queue就是队列的接口,Deque继承Queue接口,是双端队列接口。
对这两个接口,重点把握5点:
1)Queue与Deque中对于操作失败的处理有两套API方案。
例如取队头元素,有两个方法,element()和peek()方法,其中element在队列为空操作失败时抛出NoSuchElementException异常,而peek方法返回null。这样可以根据安全性级别或者统一风格的要求来选择使用那一套API。
2)每个队列必须指定其排序的方法。例如FIFO队列,所有添加的元素加入到队列尾部,还有一些特别的队列,例如PriorityQueue按照元素的值来排序。不管具体怎么样实现,都得指明排序这一属性(specify its ordering properties)。
3)队列有有效和无限(对于容纳元素个数而言)两种,一般java.util中的队列是无限的,即对元素个数没有限制,而java.util.concurrent中的部分队列却是有限队列。
4)queue没有逻辑相等性测试,它从Object继承equals和hashCode方法。
5)Deque双端队列中也有两套API,利用它既可以模拟栈又可以模拟队列。
下面通过代码清单3-3 CollectionsDemo3 来加深理解。
代码清单3-3 :CollectionsDemo3
package com.learningjava;
import java.util.*;
/**
* This program illustrate usage of Queue and Deque
* @author wangdq
* 2013-11-2
*/
public class CollectionsDemo3 {
public static void main(String[] args) {
String[] ads = new String[]{"what", "you", "see", "is", "what" ,"you","get"};
String[] filmName = new String[]{"now","you","see","me"};
//part1: two forms of methods
Queue<String> queue = new PriorityQueue<>();
try{
queue.element();
}catch(NoSuchElementException e) {
System.err.println(e);
}
System.out.println(queue.peek());
//part2: priority queue will sort its elements
queue.addAll(Arrays.asList(filmName));
System.out.println(queue);
//part3: double ended queue
Deque<String> deque1 = new LinkedList<>(Arrays.asList(ads));
Deque<String> deque2 = new ArrayDeque<>(Arrays.asList(filmName));
System.out.println("before add operation: "+deque2);
deque2.addFirst(deque1.peekFirst());
deque2.addLast(deque1.peekLast());
System.out.println(deque2);
}
}
运行输出
java.util.NoSuchElementException
null
[me, now, see, you]
before add operation: [now, you, see, me]
[what, now, you, see, me, get]
3.2.5 Map接口
Map即是键和值的映射表,它用来模拟数学上函数这一概念的。
函数一定是映射,但不一定是一一映射,这一概念同样使用于Map,即Map中不能出现重复键,每个键最多只能映射到一个值;但是也允许多个键映射到同一个值。
对于Map重点把握4点:
1)实现Map的三个通常类是HashMap,TreeMap,LinkedHashMap。这与实现Set的三个通常类非常相似,你可以类比Set上的HashSet,TreeSet和LinkedHashSet来学习他们。
2)Map提供逻辑相等性测试,只要键值映射完全相同则认为两者逻辑相等,那么
equals和hashCode方法就相等。
3)Map有三个视图,即键视图(KeySet),值视图(values)和键值视图(entrySet)。
KeySet是一个Set<K>,values可以包含相同值它是Collection<K>,
entrySet是包含键值对(Map.Entry<K,V>)的set<Map.Entry<K,V>>。
这三个视图都可以迭代。
4)虽然Map不能实现一对多的映射,但是可以利用一个键对应一个列表来实现一对多的映射。
例如,Map<Integer ,Set<String>> multiMaps = new HashMap<>(); 实现一个键对应一个集,集中包含无重复元素的集合。
下面通过代码清单3-4来加深理解。
代码清单3-4 :CollectionsDemo4.java
package com.learningjava;
import java.util.*;
/**
* This program illustrate usage of Map
* @author wangdq
* 2013-11-2
*/
public class CollectionsDemo4 {
public static void main(String[] args) {
String[] ads = new String[]{"what", "you", "see", "is", "what" ,"you","get"};
String[] filmName = new String[]{"now","you","see","me"};
//part1: using map to store elements
// Count Word using HashMap
Map<String, Integer> hashMap = new HashMap<String, Integer>();
CountWord(hashMap,ads);
//Count Word using TreeMap
Map<String, Integer> treeMap = new TreeMap<String, Integer>();
CountWord(treeMap,ads);
// Count Word using LinkedHashMap
Map<String, Integer> linkedMap = new LinkedHashMap<String, Integer>();
CountWord(linkedMap,ads);
Map<String, Integer> linkedMap2 = new LinkedHashMap<String, Integer>();
CountWord(linkedMap2,filmName);
//part2: two Map objects can be compared for logical equality
System.out.println("equal test result is: "+hashMap.equals(linkedMap));
System.out.println("hashCode test result is: "+(hashMap.hashCode() == linkedMap.hashCode()));
System.out.println("equal test result is: "+hashMap.equals(linkedMap2));
//part3: Collection Views
Set<String> keySet = hashMap.keySet();
try {
keySet.add("hello");
}catch(UnsupportedOperationException e) {
System.err.println(e);
}
System.out.println(hashMap);
Collection<Integer> nums = hashMap.values();
System.out.println(nums);
for( Map.Entry<String, Integer> entry:hashMap.entrySet() ) {
String key = entry.getKey();
if(key.contains("t")) {
entry.setValue(Integer.valueOf(0));//change value during iteration
}
}
System.out.println(hashMap);
//part4: bulk operations
System.out.println("before: "+linkedMap);
Set<String> commonKeys = new HashSet<>(linkedMap.keySet());
commonKeys.retainAll(linkedMap2.keySet());
linkedMap.keySet().removeAll(commonKeys);
System.out.println("after: "+linkedMap);
//part5: Multimaps map each key to multiple values
Map<Integer ,Set<String>> multiMaps = new HashMap<>();
for(String str: ads) {
Set<String> set = multiMaps.get(str.length());
if(set == null) {
set = new HashSet<String>();
multiMaps.put(str.length(), set);
}
set.add(str);
}
System.out.println(multiMaps);
}
//count word from the words Array
public static void CountWord(Map<String,Integer> map,String[] words) {
for (String a : words) {
Integer freq = map.get(a);
map.put(a, (freq == null) ? 1 : freq + 1);
}
System.out.println(map.size() + " distinct words:");
System.out.println(map);
}
}
运行输出
5 distinct words:
{is=1, get=1, see=1, what=2, you=2}
5 distinct words:
{get=1, is=1, see=1, what=2, you=2}
5 distinct words:
{what=2, you=2, see=1, is=1, get=1}
4 distinct words:
{now=1, you=1, see=1, me=1}
equal test result is: true
hashCode test result is: true
equal test result is: false
{is=1, get=1, see=1, what=2, you=2}
[1, 1, 1, 2, 2]
{is=1, get=0, see=1, what=0, you=2}
before: {what=2, you=2, see=1, is=1, get=1}
java.lang.UnsupportedOperationException
after: {what=2, is=1, get=1}
{2=[is], 3=[get, see, you], 4=[what]}
3.2.6 SortedSet与sortedMap接口
SortedSet接口是从Set接口派生的接口,它保持元素处于有序状态,根据元素的自然排序(实现了Comparable接口的类,利用int compareTo(T o)方法供排序)或者在创建时提供的比较器(Comparator接口,有一个 int compare(T o1, T o2)方法供排序)来进行排序。
前面使用的TreeSet就是一个实现了SortedSet接口的类。
对于SortedSet要重点把握3点:
1)SortedSet上的范围视图可以较长时间的使用。
与List上的范围视图(range-view)不同的是,即使原来的SortedSet集被修改了,视图仍然有效。因为,SortedSet上范围视图的端点使用的是在元素空间的绝对定位,而不是像List中的那种在原来集合中具体元素的定位。对视图做出的更改将反映到原集上,反之亦然。范围视图可通过
public SortedSet<E> subSet(E fromElement,E toElement)方法来获取。
或者通过headSet(E toElement)和tailSet(E toElement)操作来获取。
2)可以获取SortedSet的比较器,通过方法:
Comparator<? super E> comparator()获取其比较器,当然,如果按照自然排序方法排序的话则返回null。这样SortedSet被拷贝到一个新的SortedSet的时候,可以通过获取其比较器来保持相同的元素顺序。
SortedMap与SortedSet很相似,可以通过SortedSet类比学习。
下面给出代码清单3-5 CollectionsDemo5.java来加深理解。
代码清单3-5 :CollectionsDemo5.java
package com.learningjava;
import java.util.*;
/**
* This program illustrate usage of SortedSet
* @author wangdq
* 2013-11-2
*/
public class CollectionsDemo5 {
public static void main(String[] args) {
//part1: use a inner comparator class
//as we can't create generic arrays,we create a ArrayList
ArrayList<Pair<String>> nameList = new ArrayList<>();
nameList.add(new Pair<>("John", "Smith"));
nameList.add(new Pair<>("Karl", "Ng"));
nameList.add(new Pair<>("Jeff", "Smith"));
nameList.add(new Pair<>("Tom", "Rich"));
//use a anonymous comparator
SortedSet<Pair<String>> sortedSet = new TreeSet<>
( new Comparator<Pair<String>>(){
@Override
public int compare(Pair<String> name1, Pair<String> name2) {
// TODO Auto-generated method stub
int lastCmp = name1.getSecond().compareTo(name2.getSecond());
return (lastCmp != 0 ? lastCmp :
name1.getFirst().compareTo(name2.getFirst()));
}
});
sortedSet.addAll(nameList);
System.out.println(sortedSet);
SortedSet<String> alphabet = new TreeSet<>();
//part2: range-view
for (char ch = 'A'; ch <= 'Z'; ) {
alphabet.add("#"+Character.valueOf(ch).toString());
ch++;
}
System.out.println(alphabet);
SortedSet<String> subSet1 = alphabet.subSet("#C", "#G");
System.out.println(subSet1.size()
+" elements in the subSet1:"+subSet1);
//tricks to view a closed interval
SortedSet<String> subSet2 = alphabet.subSet("#C", "#G\0");
System.out.println(subSet2.size()
+" elements in the subSet2:"+subSet2);
//tricks to view a open interval
SortedSet<String> subSet3 = alphabet.subSet("#C\0", "#G");
System.out.println(subSet3.size()
+" elements in the subSet3:"+subSet3);
//headset and tailset view
SortedSet<String> part1 = alphabet.headSet("#N");
SortedSet<String> part2 = alphabet.tailSet("#N");
System.out.println(part1.size()
+" elements in the part1:"+part1);
System.out.println(part2.size()
+" elements in the part2:"+part2);
System.out.println(part1.headSet("#E").first());
System.out.println(part1.headSet("#E").last());
}
}
/**
* represent paired data
*/
class Pair<T> {
public Pair() {
this.first = null;
this.second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
@Override
public String toString() {
return first.toString()+" "+second.toString();
}
private T first;
private T second;
}
运行输出
[Karl Ng, Tom Rich, Jeff Smith, John Smith]
[#A, #B, #C, #D, #E, #F, #G, #H, #I, #J, #K, #L, #M, #N, #O, #P, #Q, #R, #S, #T, #U, #V, #W, #X, #Y, #Z]
4 elements in the subSet1:[#C, #D, #E, #F]
5 elements in the subSet2:[#C, #D, #E, #F, #G]
3 elements in the subSet3:[#D, #E, #F]
13 elements in the part1:[#A, #B, #C, #D, #E, #F, #G, #H, #I, #J, #K, #L, #M]
13 elements in the part2:[#N, #O, #P, #Q, #R, #S, #T, #U, #V, #W, #X, #Y, #Z]
#A
#D
通过代码清单中使用的方法,基本上对SortedSet关键点有一个了解了。