欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第九章——集合

程序员文章站 2022-05-13 19:58:27
...

目录

1、Java 集合框架

将集合的接口与实现分离

Collection 接口

迭代器

泛型实用方法

2、集合框架中的接口

链表(LinkedList)

数组列表(ArrayList)

散列集(HashSet)

树集(TreeSet)

队列与双端队列(Queue & Deque)

优先级队列(Priority queue)

3、映射

基本映射操作

更新映射项

映射视图

集合类库中几个专用的映射类

弱散列映射

链接散列集与链接散列映射

枚举集与映射

标识散列映射

4、视图与包装器

轻量级集合包装器

子范围(subrange)

不可修改的视图(unmodifiable views)

同步视图

受查视图

 


 

对于不同的功能需求,在程序设计中选择合适的数据结构非喜关键。对于实现效率有非常大的影响。

本章讲述如何利用 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接口。

Java库中的具体集合
集合类型 描述

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>());

受查视图

受查视图用来对泛型类型发生问题时提供调试支持。