第九章——集合
目录
对于不同的功能需求,在程序设计中选择合适的数据结构非喜关键。对于实现效率有非常大的影响。
本章讲述如何利用 Java 类库帮助我们实现传统的数据结构。
1、Java 集合框架
将集合的接口与实现分离
先看队列(queue),队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,查询队列的元素个数,在需要手机对象时,按照“先进先出”的规则进行元素检索。
队列的实现方式有两种:1、使用循环数组;2、使用链表。
但在队列的接口中除了第一自然段提到的之外,没有说明队列是如何实现的,即使用的是第二自然段中的哪种方式。
每一个实现都可以通过一个实现了 Queue 接口的类表示,这样,对于每一个 Queue 对象,一旦构建了集合就不需要知道是哪一种实现方式,因为可以在想法改变的时候,轻松地使用另外一种不同的实现(实例化另一个实现了Queue接口的类),如:
Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));
//若改变想法,希望通过链表实现,只需要对一个地方作出修改,即调用构造器的地方
Quwuw<Customer> expressLane = new LinkedListQueue<>(100);
expressLane.add(new Customer("Harry"));
Collection 接口
在 Java 类库中,集合类的基础接口是 Collection 接口。该接口有两个基本方法:
public interface Collection<E> {
boolean add(E element);
Iterator<E> iterator();
...
}
add 方法用于向集合中添加元素,确实改变了集合就返回 true,否则返回 false。
iterator 方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
迭代器
Iterator 接口包含 4 个方法:
public interface Iterator<EE> {
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
next 方法可以逐个访问集合中的元素,但到了集合的末尾,将抛出一个 NoSuchElementException。因此需要使用 hasNext 方法进行判断。迭代集合中的所有元素,示例如下:
Collection<String> s = ...;
Itrator<String> iter = c.iterator();
while(item.hasNext()) {
String Element = iter.next();
//do something to Element
}
//或直接使用 for-each 循环
for(String Element : c) {
//do something to Element
}
以上的 for-each 循环变编译为带有迭代器的循环。for-each 循环可以与任何实现了 Iterator 接口的对象一起工作,而 Collection 接口扩展了该接口。因此,对于标准类库中的任何集合都可以使用 for-each 循环。
元素被访问的顺序取决于集合类型,ArrayList 将会从索引为0的元素开始迭代,但对于 HashSet,将按随机顺序访问元素。
Java 集合类库的迭代器与其他类库的迭代器是有差别的。Java 迭代器可以理解为在两个元素中间,当调用 next 方法时,迭代器越过一个元素到下一个间隙中,返回越过的这个元素的引用。
泛型实用方法
因为 Collection 接口和 Iterator 接口都是范型的,所以可以编写操作任何集合类型的实用方法。如以下是一个检测任何集合类型是否包含制定元素的范型方法:
public static <E> boolean contains(Collection<E> c, Object obj) {
for(E element : c) {
if (element.equals(obj)) {
return true;
}
return false;
}
2、集合框架中的接口
Java 结合框架为不同类型的各种集合定义了大量接口。
集合有两个基本接口:Collection 和 Map。
具体的集合:以Map结尾的集合都实现了Map接口,其他的都实现了Collection接口。
集合类型 | 描述 |
---|---|
ArrayList |
一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效的插入和删除操作的有序序列 |
ArrayDeque | 一种用过循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效的删除最小元素的集合 |
HashMap | 一种存储键/值关联的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键/值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用==而不是用equals比较键值的映射表 |
链表(LinkedList)
List 接口的实现类有:LinkedList、ArrayList
使用链表的唯一理由就是尽可能的减少在列表中间插入/删除元素所付出的系统开销代价(普通数组或数组列表ArrayList在插入/删除元素后其后的所有元素都需要后移或前移一位),如果列表只有少数几个元素,就完全可以使用ArrayList,而不用担心结构更改时的开销,因为此时这个数量的列表进行结构改变开销不会太大。
为了解决因元素数量而在列表中插入/删除元素时带来巨大开销的问题,可以使用链表(Linked List)。链表将每个元素存放在单独的结点中,而且每个结点中还存放着下一个和上一个结点的引用。在Java程序设计语言中,所有链表实际上都是双向连接的(double linked)。在链表中插入或删除一个元素,只需要更新附近元素的链接。
List<String> staff = new LinkedList<>(); //LinkedList 类实现了 List 接口
staff.add("Aby");
staff.add("Henry");
staff.add("Mike");
Iterator iter = staff.iterator();
String first = iter.next();
String second = iter.next();
iter.remoce(); //remove the second element
链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add 方法会将元素添加到链表末尾,如果要添加到其他地方,可以由迭代器(迭代器是描述集合中位置的)负责,而使用迭代器添加元素也只对这种有序集合才有实际意义,若无序,则没有实际意义,如下一节中的集(set)类型,其中的元素完全无序,因此,在Iterator接口中就没有add方法。
集合类库提供了子接口 ListIterator,其中包含 add 方法。与 Collection 接口中的 add 方法(返回boolean)不同,这个 add 方法无返回值。同时,该接口还有两个方法可以反向遍历链表:E previous(); boolean hasPrevious()
LinkedList 类的 listiterator 方法返回一个实现了 ListIterator 接口的迭代器对象:
ListIterator<String> iter = staff.listiterator();
add方法在迭代器位置之前添加一个新元素。
add 方法只依赖于迭代器的位置,而 remove 方法依赖于迭代器的状态(调用 next 时,删除左边元素;调用 previous 时,删除右边元素)
set 方法用于更新链表元素。get 方法用于获取具体位置的元素,但 get 方法在元素数量过多时的效率不高。
为了避免并发修改链表的异常,可以根据需要给容器附加许多迭代器,但只能读取列表,另外,再多单独附加一个既能读又能写的迭代器
列表都迭代器接口还有一个方法,可以告知当前位置的索引:nextIndex 和 previousIndex
LinkedList 的弊端:链表不支持快速的随机访问。如果要查看链表中第 n 个元素,就需要从头开始,越过 n-1 个元素,没有捷径可走。但如果链表元素数量较少,就不用担心 get 方法的开销。
综上,如果元素数量较少,完全可以使用数组或ArrayList,如果元素较多,且需要频繁插入/删除,则使用链表。如果需要随机访问,不应该使用链表,因为效率不高。
package com.company;
import java.util.*;
/**
* A modification of the image viewer program that logs various events
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
List<String> a = new LinkedList<>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
List<String> b = new LinkedList<>();
b.add("Bob");
b.add("Doug");
b.add("Frances");
b.add("Glory");
ListIterator<String> aIter = a.listIterator();
Iterator<String> bIter = b.iterator();
while(bIter.hasNext()){
if(aIter.hasNext()){
aIter.next();
}
aIter.add(bIter.next());
}
System.out.println(a);
bIter = b.iterator();
while(bIter.hasNext()){
bIter.next();
if (bIter.hasNext()){
bIter.next();
bIter.remove();
}
}
System.out.println(b);
a.removeAll(b);
System.out.println(a);
}
}
数组列表(ArrayList)
上一节中讲到 List 接口和实现了这个接口的 LinkedList 类。List 接口用于描述一个有序集合,。有两种访问元素的方法,一是迭代器,二是使用 get 和 set 方法。第二种方法不适用于列表(LinkedList),但对数组很有用。集合类库提供了类:ArrayList,这个类也实现了 List 接口,它封装了一个动态再分配的数组对象。
散列集(HashSet)
有一种数据结构,可以快速地查找所需要的对象,这就是散列表(hash table),散列表为每个对象计算一个整数,足欧威散列码(hash code)。如果是自定义类,就要负责实现这个类的 hashcode 方法来生成散列码
在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。要想查找表中对象的位置在,就要先计算他的散列码,然后与桶的总数取余,所得到的结果就是保存着元素的痛的索引。如:想找对象的散列码为76268,且有128个桶,则对象应该保存在第(76268 % 128 = 108)。
在插入对象时,若所在的桶满了,这种现象称为散列冲突(hash collision)
如果想更多地控制散列表的运行性能,就要制定一个初始的桶数(指用于收集具有相同散列值的桶的数目)。如果最初的估计过低,散列表太满,就需要再散列(rehashed)。装填因子(load factor)决定何时对散列表进行再散列,如散列因子 0.75 表示若散列表中 75% 的位置都以插入元素,这个表就会用双倍的桶数自动地进行再散列。
散列表用于实现几个重要的数据结构。其中,最简单的是:集(set)类型。set 是无序且无重复元素的元素集合,其 add 方法首先在集中查找要添加的对象,若不存在,就将这个对象添加进去。
Set 接口的实现类有:HashSet、TreeSet。
只有不关心集合中元素的顺序时才使用 HashSet 类,
package com.company;
import java.util.*;
/**
* A modification of the image viewer program that logs various events
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
Set<String> words = new HashSet<>();
long totalTime = 0;
try(Scanner in = new Scanner(System.in)){
while(in.hasNext()){
String word = in.next();
long callTime = System.currentTimeMillis();
words.add(word);
callTime = System.currentTimeMillis() - callTime;
totalTime += callTime;
}
}
Iterator<String> iter = words.iterator();
for(int i=0; i<20 && iter.hasNext(); i++){
System.out.println(iter.next());
}
System.out.println(".......");
System.out.println(words.size() + " distinct words. " + totalTime + " milliseconds.");
}
}
树集(TreeSet)
树集是一个有序集合(sorted collection)。排序使用树结构完成的,当前实现使用的是红黑树(red-black tree)。
将一个元素添加到 TreeSet 中要比添加到散列表中慢,但是,与检查数组/链表中的重复元素相比还是快很多。
要使用树集(TreeSet),必须能够比较元素。这些元素必须实现 Comparable 接口,或者构造集时必须提供一个 Comparator。
如果不要求元素顺序,还是使用散列集(HashSet)更好。
队列与双端队列(Queue & Deque)
队列可以让人们有效的在队列尾部添加一个元素,在头部删除一个元素。
有两个端的队列,就是双端队列,可以让人们有效的在头部和尾部同时添加或删除元素。
队列和双端队列不支持在中间添加元素
在Java SE 6 中添加了Deque 接口,并由 ArrayDeque 和 LinkedList 类实现。这两个类都提供了双端队列,而且可以在必要时增加队列长度。而在第十四章中还会讲到有限队列和有限双端队列
package com.company;
import java.util.*;
/**
* A modification of the methods of some Queue's APIs
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
//API: java.util.Queue<E>
queue.add("fjdksla;fj"); //return boolean; if is full, throws exception: IllegalStateException;
queue.offer("djksla"); //return boolean; if is full, return false;
System.out.println(queue.remove()); //return E; if is empty, throws exception: NoSuchElementException
System.out.println(queue.poll()); //return E; if is empty, return null
System.out.println(queue.element()); //return E; if is empty, throws exception: NoSuchElementException
System.out.println(queue.peek()); //return E; if is empty, return null
//API: java.util.Deque<E>
Deque<String> queue2 = new ArrayDeque<>();
queue2.addFirst("addfirst"); //void;
queue2.addLast("addlast"); //void; if is full, these two methods throws exception: IllegalStateException;
queue2.offerFirst("offerfirst"); //return boolean;
queue2.offerLast("offerlast"); //return boolean; if is full, these two methods return false;
System.out.println(queue2.removeFirst()); //return E;
System.out.println(queue2.removeLast()); //return E; if is empty, these two methods throws exception NoSuchElementException;
System.out.println(queue2.pollFirst()); //return E;
System.out.println(queue2.pollLast()); //return E; if is empty, these two methods return null;
queue2.getFirst(); //return E;
queue2.getLast(); //return E; if is empty,these two methods throws NoSuchElementException
queue2.peekFirst(); //return E;
queue2.peekLast(); //return E; if is empty, these two methods return null;
//API: java.util.ArrayDeque
ArrayDeque<String> queue3 = new ArrayDeque<>();
ArrayDeque<String> queue4 = new ArrayDeque<>(20); //param: int initialCapacity
}
}}
优先级队列(Priority queue)
优先级队列(priority queue)中的元素可以按任意顺序插入,却总是按照排序的顺序进行检索。
优先级队列使用了一个优雅且高效的数据结构堆(heap)。堆是一个可以自我调整的二叉树,对树执行插入操作或删除操作,可以让最小的元素移动到根,而不必花费时间进行排序。
与 TreeSet 一样,一个 priority queue 既可以保存实现了 Comparable 接口的类对象,也可以保存在构造器中提供的 Comparator 对象。
一个使用优先级队列的典型示例就是任务调度。任务以随机顺序添加到优先级队列中,每当启动一个新的任务时,都将优先级最高的任务从队列中删除。
package com.company;
import java.time.LocalDate;
import java.util.*;
/**
* A modification of the priority queue
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
PriorityQueue<LocalDate> pq = new PriorityQueue<>();
pq.add(LocalDate.of(1906,12,9));
pq.add(LocalDate.of(1815,12,10));
pq.add(LocalDate.of(1903,12,3));
pq.add(LocalDate.of(1910,6,22));
System.out.println("Iterating over elements...");
for (LocalDate date : pq){
System.out.println(date);
}
System.out.println("Removing elements...");
while (!pq.isEmpty()){
System.out.println(pq.remove());
}
}
}
运行结果:
Iterating over elements...
1815-12-10
1906-12-09
1903-12-03
1910-06-22
Removing elements...
1815-12-10
1903-12-03
1906-12-09
1910-06-22
Process finished with exit code 0
3、映射
映射(map)数据结构用来存放键/值对。
基本映射操作
1、Java类库为映射提供了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。
散列映射(hashmap)对键进行散列,树映射(treemap)用键的整体顺序对元素进行抛许,并将其组织称搜索树。
散列/比较函数只能作用于键,与键关联的值不能进行散列或比较。
与集(set)一样,散列映射会快一些,如果不需要按照炮裂顺序访问键,就最好选择散列。
2、添加一个对象时,需要提供一个键,使用 put 方法;
检索一个对象时,必须使用一个键,使用 get 方法,如果在映射中没有相应的键,get 方法将返回 null,也可以使用 getOrDefault 方法,在第二参数指定若对象不存在时返回的默认值;
删除一个对象,使用 remove 方法;
size 方法用于返回映射中的元素数;
要迭代处理映射中的对象时,最容易的方法是使用 forEach 方法,如:
Map<String, Integer> scores = new HashMap<>();
...
scores.forEach((k, v) ->
System.out.println("key=" + k + "value=" + v));
应用代码示例:
package com.company;
import java.time.LocalDate;
import java.util.*;
/**
* A modification of the Map
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
Map<String, Employee> staff = new HashMap<>();
staff.put("144-25-5464",new Employee("Amy Lee"));
staff.put("567-24-2546",new Employee("Harry Hacker"));
staff.put("157-62-7935",new Employee("Gary Coppper"));
staff.put("456-62-5527",new Employee("Francesca Crus"));
//print all entries
System.out.println(staff);
//remove an entry
staff.remove("567-24-2546");
//replace an entry
staff.put("456-62-5527",new Employee("Francesca Miller"));
//took up a value
System.out.println(staff.get("157-62-7935"));
//iterate through all entries
staff.forEach((k,v)->
System.out.println("key=" + k + ", value=" + v));
}
}
class Employee{
private String name;
public Employee(String name){
this.name = name;
}
@Override
public String toString() {
return name;
}
}
运行结果:
{157-62-7935=Gary Coppper, 144-25-5464=Amy Lee, 456-62-5527=Francesca Crus, 567-24-2546=Harry Hacker}
Gary Coppper
key=157-62-7935, value=Gary Coppper
key=144-25-5464, value=Amy Lee
key=456-62-5527, value=Francesca Miller
Process finished with exit code 0
更新映射项
有四种方法:
package com.company;
import java.time.LocalDate;
import java.util.*;
/**
* A modification of updating the map
* Example: read a file, and count the freq of each word.
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
Map<String, Integer> freq = new HashMap<>();
//while meet a word from a file, frequency add 1:
//method 1:
freq.put(word, freq.get(word)+1);
//问题:当遇到一个新词时,此时 get 返回 null,因此会抛出 NullPointerException
//method 2: 设置默认值
freq.put(word, freq.getOrDefault(word, 0)+1);
//method 3: 当键存在时,才会放入一个值
freq.putIfAbsent(word, 0); //若不存在则添加键/值对
freq.put(word, freq.get(word)+1); //new the method "get" will not meet exception
//method 4: 使用 merge 方法
freq.merge(word, 1, Integer::sum);
//当键 word 与一个非 null 值 v 关联时(即word键值对存在),将函数应用于 v 和第二个参数 value(此处为将v加一),并将结果与键word重新关联,若新结果为 null,则删除该键值对;
//否则(即word键值对不存在),将键 word 和第二个参数关联(即新建键值对),并返回 get(word)
}
}
映射视图
映射的视图(view)——这是实现了 Collection 接口或 Collection 某个子接口的对象
有三种视图:键集、值集合(不是一个集)、键/值对集
1、键集:调用 Set<K> keySet() 方法,会返回一个Set,存储着映射的键;keySet 不是HashSet或TreeSet,而是实现了 Set 接口的一个类的对象,Set 接口扩展了 Collection 类,因此,可以像使用集合一样使用 keySet
2、值集合:调用 Collection<V> values() 方法,会返回一个 Collection,存储映射的值
3、键/值对集:调用 Set<Map.Emtry<K, V>> entrySet() 方法,返回一个条目集,其元素是实现了 Map.Entry 接口的类的对象。
可以枚举一个映射的所有键或者所有值,例如键:
Set<String> keys = map.keySet();
for(String key : keys){
...
}
如果想同时查看键和值,可以通过枚举条目来避免查找(get)值,如:
for( Map.Entry<String, Employee> entry ; staff.entrySet()0{
String k = entry.getKey();
Employee v = entry.getValue();
//do something with k & v
}
访问映射条目,除了第三个方法,还可以直接使用 forEach 方法
map.forEach((k,v) -> {
//do something with k & v
});
集合类库中几个专用的映射类
弱散列映射
设计 WeakHashMap 类是为了解决对一个不再使用的值,将其键进行垃圾回收的问题。
WeakHashMap 使用弱引用(weak reference)保存键,WeakReference 对象将引用保存到另一个对象中,这里指的是散列键。通常,Java 的垃圾回收器在检测到某个特定对象已经没有他人引用了,就会将其回收,而对于只能由 WeakReference 引用的对象,垃圾回收器仍然会回收它,但同时会将引用这个对象的 WeakReference 放进一个队列中。WeakHashMap 将周期性的检查队列,以便找出队列中新添加的 WeakReference。一个 WeakReference 进入队列意味着这个散列键不再被他人使用,并且已经被收集起来。于是,WeakHashMap 将删除对应的条目。
链接散列集与链接散列映射
LinkedHashSet 和 LinkedHashMap 类用来记住插入元素项的顺序。
链接散列集使用插入顺序进行迭代,而链接散列映射使用访问顺序,而不是插入顺序,对映射的条目进行迭代。每次使用 get 或 put,受影响的条目就会从当前位置删除,并放到条目链表的尾部(只有条目在链表中的位置受影响,而散列表中的桶不会受到影响)
构造一个链接散列映射:new LinkedHashMap<K, V>(initialCapacity, loadFactor, true) //第二个参数为装填因子,见上文HashSet。
链接散列映射对于“最近最少使用”原则非常有用,即将访问量最少的条目放在了链表的最前端
枚举集与映射
1、枚举集(EnumSet)是一个枚举类型元素集的高效实现。
由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1.
EnumSet 类没有公共构造器。可以使用静态工厂方法构造这个集:
enum Weekday {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};
EnumSet<Weekday> always = EnumSet.allof(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.Friday);
EnumSet<Weekday> wmf = EnumSet.of(Weekday.MONDAY, Weekday.WENDSDAY, Weekday.FRIDAY);
可以使用 Set 接口的常用方法来修改EnumSet。
2、枚举映射(EnumMap)是一个键类型为枚举类型的映射。它可以直接且高效的用一个值数组实现。在使用时,需要在构造器中指定键类型。
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
标识散列映射
IdentityHashMap 类中,键的散列值不使用 hashCode 函数计算的,而使用 System.identityHashCode 方法计算的。这是 Object.hashCode 方法根据对象的内存地址来计算散列码时所使用的方式。
而且,在两个对象比较时,IdentityHashMap 类使用==,而不是用 equals。也就是说,不同的键对象,即使内容相同,也被视为不同的对象。
在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。
4、视图与包装器
一个示例:映射类的 keySet 方法初看起来好像是创建了一个新集,并肩映射的键都填进去 ,然后返回这个集。但事实并非如此,取而代之的是:keySet 方法返回一个实现 Set 接口的类对象,这个类的方法对原映射进行操作。这种集合称为视图(views)
轻量级集合包装器
1、Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。如:
Card[] cardDeck = new Card[52];
...
List<Card> cardList = Arrays.asList(cardDeck);
返回的对象不是 ArrayList,它是一个视图对象,带有访问底层数组的 get 和 set 方法。改变数组大小的所有方法(如与迭代器相关的add和remove方法)都会抛出一个 UnsupportedOperationException 异常。
asList 方法可以接受可变数目的参数,如:Arrays.asList("Amy","Bob","Carl");
2、Collections.nCopies(n, anObject); 方法将返回一个实现了 List 接口的不可修改的对象,并给人一种包含 n 个元素,每个元素都像是一个 anObject 的错觉。如:
List<String> settings = Collections.nCopies(100,"DEFAULT");
//创建一个包含100个字符串的List,每个串都是“DEFAULT”。
存储代价很小,这是视图技术的一种巧妙应用。
Collections 类有很多实用方法,这些方法的参数和返回值都是集合。不要将其与 Collection 接口混淆。
3、Collections.singleton(anObject); 将返回一个视图对象。该对象实现了 Set 接口。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。
子范围(subrange)
可以为很多集合建立子范围(subrange)视图。如:
List group = staff.subList(10,20); //获得第10~19个元素,
//可以将任何操作应用于子范围,并且能自动地反映到整个列表,如:
group.clear(); //元素自动的从 staff 中删除,并且 group 为空了。
对于有序集和映射,可以使用排序顺序而不是位置顺序来建立子范围。SortedSet 接口声明了 3 个方法:
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)
映射也有类似方法:
SortedMap<K, V> subMap(K from, K to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap<K from)
Java SE 6 引入到 NavigableSet 接口赋予了子范围操作更多的控制力。可以追定是否包含边界:
NavigableSet<E> subSet(E from, booleana fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
NavigableSet<E> tailSet(E from, boolean fromInclusive)
不可修改的视图(unmodifiable views)
Collections 类还有几个方法,用于产生集合的不可修改视图(unmodifiable views)。如果发现视图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态。
使用以下 8 种方法获得 unmodifiable views:
Collections.ummodifiableCollection();
Collections.unmodifiableList();
Collections.unmodifiableSet();
Collections.unmodifiableSortedSet();
Collections.unmodifiableNavigableSet();
Collections.unmodifiableMap();
Collections.unmodifiableSortedMap();
Collections.unmodifiableNavigableMap();
如想要查看某部分代码,又不触及某个集合的内容,就可以进行下列操作:
List<String> staff = new LinkedList<>();
....
lookAt(Collections.unmodifiableList(staff));
//将返回一个实现了List接口的类对象。
//lookAt 方法可以调用List接口中的所有方法,只是所有的更改器方法,如add,已经被重新定义为抛出一个异常,而不是将调用传递给底层集合
由于视图知识包装了接口,而不失时机地集合对象,所以只能访问接口中定义的方法。
同步视图
如果有多个线程访问集合,就必须确保集不会被意外的破坏。类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。图:Collections 类的静态 synchronizedMap 方法可以将任何一个映射表转换成具有同步访问方法的 Map:
Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());
受查视图
受查视图用来对泛型类型发生问题时提供调试支持。
上一篇: 《八月游母校有感》
下一篇: 10. 集合映射-bag