第九章——集合

目录

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

受查视图

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

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JonathanRJt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值