CoreJava Reading Note(9:Collection)

1.Java集合框架

  1)将集合的接口与实现分离

  与现代的数据结构类库的常见情况一样,Java集合类库也将接口与实现分离。首先,看一下人们熟悉的数据结构--队列(queue)是如何分离的。

  队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中的元素个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列

  队列接口的最简形式可能类似下面这样:

public interface Queue<E> //a simlified form of the interface in the standard library
{
    void add(E element);
    E remove();
    int size();
}

  这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表

  每个实现都可以通过一个实现了Queue接口的类表示。

public class CircularArrayQueue<E> implements Queue<E>// not an actual library class
{
    private int head;
    private int tail;
    
    CircularArrayQueue(int capacity){...}
    public void add(E element){...}
    public E remove(){...}
    public int size(){...}
    public E[] elements;
}

  

public class LinkedListQueue<E> implements Queue<E>// not an actual library class
{
    private Link head;
    private Link tail;

    LinkedListQueue(){...}
    public void add(E element){...}
    public E remove(){...}
    public int size(){...}
}

  注:实际上,Java类库没有名为CircularArrayQueue和LInkedListQueue的类。这里,只是以这些类作为示例,解释一下集合接口与实现在概念上的不同。如果需要一个循环数组队列,就可以使用ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList类,这个类实现了Queue接口

  当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用

Queue<Customer> expressLane=new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));

  利用这种方式,一旦改变了想法,可以轻松地使用另外一种不同的实现。只需要对程序的一个地方做出修改,即调用构造器的地方。如果觉得LInkedListQueue是个更好的选择,就将代码修改为:

Queue<Customer> expressLane=new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

  在研究API文档时,会发现另外一组名字以Abstract开头的类,例如,AbstractQueue。这些类是为类库实现者而设计的。如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多

  2)Collection接口

  在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法

public interface Collection<E>{
    boolean add(E element);
    Iterator<E> iterator();
    ...
}

  除了这两个方法之外,还有几个方法,将在稍后介绍。

  add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。例如,如果试图向集中添加一个对象,而这个对象在集中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象。

  iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素

  3)迭代器

  Iterator接口包含4个方法

public interface Iterator<E>{
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

  通过反复调用next方法,可以逐个访问集合中的每个元素。但是到达了集合的末尾,next方法讲抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。例如:

Collection<String> c=...;
Iterator<String> iter=c.iterator();
while(iter.hasNext()){
    String element=iter.next();
    do something with element
}

  用"for each"循环可以更加简练地表示同样的循环操作

for(String element : c){
    do something with element
}

  编译器简单地将"for each"循环翻译为带有迭代器的循环

  "for each"循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个抽象方法

public interface Iterable<E>{
    Iterator<E> iterator();
    ...
}

  Collection接口扩展了Iterable接口。因此,对于标准类库中任何集合都可以使用"for each"循环

  在Java SE 8中,甚至不用写循环。可以调用forEachRemaining方法并提供一个lambda表达式(它会处理一个元素)。将对迭代器的每一个元素调用这个lambda表达式,直到再没有元素为止

iterator.forEachRemaining(element -> do something with element);

  元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1.然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。这对于计算总和或统计符合某个条件的元素个数这类与顺序无关的操作来说,并不是什么问题

  注:编程老手会注意到:Iterator接口的next和hasNext方法与Enumeration接口的nextEnumeration和hasMoreElements方法的作用是一样的。Java集合类库的设计者可以选择使用Enumeration接口。但是,他们不喜欢这个接口累赘的方法名,于是引入了具有较短方法名的新接口

  Java集合类库中的迭代器与其他类库中的迭代器在概念上有着重要的区别。在传统的集合类库中,例如,C++的标准模板库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引i就可以查看数组元素a[i]一样。不需要查找元素,就可以讲迭代器向前移动一个位置。这与不需要执行查找操作就可以通过i++将数组索引向前移动一样

  但是,Java迭代器并不是这样操作的。查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动

  因此,应该将Java迭代器任务是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用

  注:这里还有一个有用的推论。可以将Iterator.next与InputStream.read看作为等效的。从数据流中读取一个字节,就会自动地“消耗掉”这个字节。下一次调用read将会消耗并返回输入的下一个字节。用同样的方式,反复地调用next就可以读取集合中的所有元素

  Iterator接口的remove方法将会删除上次调用next方法时返回的元素在大多数情况下,在决定删除某个元素之前一个先看一下这个元素是很具有实际意义的。然而,如果想要删除指定位置上的元素,仍然需要越过这个元素。例如,下面是如何删除字符串集合中第一个元素的方法

Iterator<String> it=c.iterator();
it.next();//skip over the first element
it.remove();//now remove it

  更重要的是,对next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateException异常

  如果想删除两个相邻的元素,不能直接地这样调用

it.remove();
it.remove();//Error!

  相反地,必须先调用next越过将要删除的元素

it.remove();
it.next();
it.remove();// OK

  4)泛型实用方法

  由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。例如,下面是一个检测任意集合是否包含指定元素的泛型方法

public static <E> boolean contains(Collection<E> c,Object obj){
    for(E element : c){
        if(element.equals(obj)){
            return true;
        }
    }
    return false;
}

  Java类库的设计者认为:这些实用方法中的某些方法非常有用,应该将它们提供给用户使用。这样,类库的实用者就不必自己重新构建这些方法了。contains就是这样一个实用方法。 

  事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。下面列举了其中的一部分:

int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection<?> c)
boolean equals(Object other)
boolean addAll(Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll(Collection<?> c)
void clear()
boolean retainAll(Collection<?> c)//从这个集合中删除所有与other集合中的元素不同的元素。如果由于这个调用改变了集合,返回true。
Object[] toArray()
<T> T[] toArray(T[] arrayToFill)
//返回这个集合的对象数组。如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间填补null;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素

  当然,如果实现Collection接口的每个类都要提供如此多的例行方法将是一件很烦人的事情。为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection,它将基础方法size和iterator抽象化了,但是在此提供了例行方法

public abstract class AbstractCollection<E> implements Collection<E>{
    ...
    public abstract Iterator<E> iterator();
    
    public boolean contains(Object obj){
        for(E element : this) // calls iterator()
            if(element.equals(obj))
                return true;
        return false;
    }
    ...
}

  此时,一个具体的集合类可以扩展AbstractCollection类了。现在要由具体的集合类提供iterator方法,而contains方法已由AbstactCollection超类提供了。然而,如果子类有更加有效的方式实现contains方法,也可以由子类提供,就这点而言,没有什么限制。 

  对于Java SE 8,这种方式有些过时了。如果这些方法是Collection接口的默认方法会更好。但实际上并不是这样的。不过,确实已经增加了很多默认方法。其中大部分方法都与流的处理有关。另外还有一个很有用的方法:

default boolean removeIf(Predicate<? super E> filter)

  这个方法用于删除满足某个条件的元素

  5)集合框架中的接口

  Java集合框架为不同类型的集合定义了大量接口。

  集合有两个基本接口:Collection和Map。我们已经看到,可以用以下方法在集合中插入元素:

boolean add(E element)

  不过,由于映射包含键/值对,所以要用put方法来插入

V put(K key,V value)

  要从集合读取元素,可以用迭代器访问元素。不过,从映射中读取值则要使用get方法

V get(K key)

  List是一个有序集合。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后一种方法称为随机访问,因为这样可以按任意顺序访问元素。与之不同,使用迭代器访问时,必须顺序地访问元素

  List接口定义了多个用于随机访问的方法

void add(int index,E element)
void remove(int index)
E get(int index)
E set(int index,E element)

  ListIterator接口是Iterator的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素

void add(E element)

  坦率的讲,集合框架的这个方面设计得很不好。实际中有两种有序集合,其性能开销有很大差异。由数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问。与之不同,链表尽管是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。如果原先提供两个接口就会容易一些

  注:为了避免对链表进行随机访问操作,Java SE 1.4引入了一个标记接口RandomAccess。这个接口不包含任何方法,不过可以用它来测试一个特定的集合是否支持高效的随机访问

if(c instanceof RandomAccess){
    use random access algorithms
}
else{
    use sequential access algorithms
}

  

  Set接口等同于Collection接口,不过其方法的行为有更严谨的定义集(set)的add方法不允许增加重复的元素。要适当地定义集的equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码

  既然方法签名是一样的,为什么还要建立一个单独的接口呢?从概念上讲,并不是所有集合都是集。建立一个Set接口可以让程序员编写只接受集的方法

  SortedSet和SortedMap接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法

  最后,Java SE 6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应当直接包含在SortedSet和SortedMap接口中。)TreeSet和TreeMap类实现了这些接口

2.具体的集合

集合类型 描述
ArrayList 一种可以动态增长和缩减的索引序列
LInkedList 一种可以在任何位置进行高效地插入和删除操作的有序序列
ArrayDeque 一种用循环数组实现的双端队列
HashSet 一种没有重复元素的无序集合
TreeSet 一种有序集
EnumSet 一种包含枚举类型值的集
LInkedHashSet 一种可以记住元素插入次序的集
PriorityQueue 一种允许高效删除最小元素的集合
HashMap 一种存储键/值关联的数据结构
TreeMap 一种键值有序排列的映射表
EnumMap 一种键值属于枚举类型的映射表
LinkedHashMap 一种可以记住键/值项添加次序的映射表
WeakHashMap 一种其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap 一种用==而不是用equals比较键值的映射表

  1)链表

  在Java中,所有的链表实际上都是双向链接的----即每个结点还存放着指向前驱结点的引用

  Java集合类库提供一个类LinkedList。在下面的代码示例中,先添加3个元素,然后再将第2个元素删除:

List<String> staff=new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator iter=staff.iterator();
String first=iter.next();
String second=iter.next();
iter.remove();//remove last visited element

  但是,链表与泛型集合之间有一个重要的区别。链表是一个有序集合,每个对象的位置十分重要。LInkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义

  例如,集(set)类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法

interface ListIterator<E> extends Iterator<E>{
    void add(E element);
    ...
}

  与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表

  另外,ListIterator接口有两个方法,可以用来反向遍历链表

E previous()
boolean hasPrevious()

  与next方法一样,previous方法返回越过的对象。

  LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象

ListIterator<String> iter=staff.listIterator();

  add方法在迭代器之前添加一个对象。下面的代码将越过链表的第一个元素,并在第二个元素之前添加"Juliet":

LIst<String> staff=new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iter=staff.listIterator();
iter.next();
iter.add("Juliet);

  当用一个刚刚由Iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。

  如果链表有n个元素,有n+1个位置可以添加新元素。这些位置与迭代器的n+1可能位置相对应

  注:在用”光标“类比时要格外小心。remove操作与BACKSPACE键的工作方式不太一样。在调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器左侧的元素。但是,如果调用previous就会将右侧的元素删除掉,并且不能连续调用两次remove。add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态

  set方法用一个新元素取代调用next或previous方法返回的上一个元素。例如,下面的代码将用一个新值取代链表的第一个元素:

ListIterator<String> iter=list.listIterator();
String oldValue=iter.next();
iter.set(newValue);

  

  可以想象,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素的前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。例如,看一看下面的代码:

List<String> list=...;
ListIterator<String> iter1=list.listIterator();
ListIterator<String> iter2=list.listIterator();
iter1.next();
iter1.remove();
iter2.next();//throws Exception

  由于iter2检测出这个链表被外部修改了,所以对iter2.next的调用抛出了一个ConcurrentModificationException异常。

  为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。 

  有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致。如果不一致,抛出一个ConcurrentModificationException异常。

  注:对于并发修改列表的检测有一个奇怪的例外。链表只负责跟踪对列表的结构性修改,例如,添加元素,删除元素。set方法不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用set方法对现有结点的内容进行修改。

  

  Collection接口声明了许多用于对链表操作的有用方法。其中大部分方法都是在LInkedList类的超类AbstractCollection中实现的。例如,toString方法调用了所有元素的toString,并产生了一个很长的格式为[A,B,C]的字符串。这为调式工作提供了便利。  

  在Java类库中,还提供许多在理论上存在一定争议的方法。链表不支持快速地随机访问。如果查看链表中第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。

  尽管如此,LInkedList类还是提供了一个用来访问某个特定元素的get方法。

  注:get方法做了微小的优化:如果索引大于size()/2就从列表尾部开始搜索元素

  列表迭代器接口还有一个方法,可以告之当前位置的索引。实际上,从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以同时产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex方法返回下一次调用previous方法时返回元素的整数索引。这两个方法效率非常高,这是因为迭代器保持着当前位置的计数值

  如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素的前面位置。也就是说,调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。

  2)数组列表

  List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是用迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用于链表,但对数组却很有用。

  集合类库提供了ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态再分配的对象数组

  注:对于一个经验丰富的Java程序员来说,在需要动态数组时,可能会使用Vector类。为什么要用ArrayList取代Vector呢?原因很简单:Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。这种情况还是很常见的。而ArrayList方法不是同步的。因此,建议在不需要同步时使用ArrayList,而不要使用Vector

  3)散列集

  链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要查看某个指定元素,却又忘记了它的位置,就需要访问所有的元素直到找到为止。

  有一种众所周知的数据结构,可以快速地查找所需要的对象,这就是散列表(hash table)散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码

  如果自定义类,就要负责实现这个类的hashCode方法。注意,自己实现的hashCode方法应该与equals方法兼容,即如果a.equals(b)为true,a与b必须具有相同的散列码

  现在,最重要的问题是散列码要能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中的其他对象无关

  在Java中,散列表用链表数组实现每个列表被称为桶(bucket)要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引

  例如,如果某个对象的散列码为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)

  或许会很幸运,在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时候会遇到桶被占满的情况,这样说不可避免的。这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少

  注:在Java SE 8中,桶满时会从链表变为平衡二叉树。如果选择的散列函数不当,会产生很多冲突,或者如果有恶意代码试图在散列表中填充多个有相同散列码的值,这样就能提高性能

  如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能

  如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的%75~%150。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将自动地转换为2的下一个幂)

  

  当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。

  装填银子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过%75的位置已经填入元素,这个表就会用双倍的桶数自动地再进行再散列。

  散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set类型是没有重复元素的元素集合。set的add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去

  Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素

  散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用HashSet

  4)树集

  TreeSet与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。

  正如TreeSet类名所示,排序是用树结构完成的(当前实现使用的是红黑树),每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总数以排好序的顺序访问每个元素

  将一个元素添加到树中比添加到散列表中慢,但是,与检查数组或链表中的重复元素相比还是快很多。如果树中包含n个元素,查找新元素的正确位置平均需要log n次比较

  注:要使用树集,必须能够比较元素。这些元素必须实现Comparable接口,或者构造集时必须提供一个Comparator

  5)优先级队列

  优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。

  与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供的Comparator对象

  使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。

  3.映射

  集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有要查找的元素的精确副本。这不是一种非常通用的查找方式。

  通常,我们知道某些键的信息,并想要茶后走啊与之对应的元素。映射(map)数据结构就是为此设计的

  映射用来存放键/值对。如果提供了键,就能够查找到值

  

  1)基本映射操作

  Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。

  散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较

  应该选择散列映射还是树映射呢?与集一样,散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择散列

  下列代码将为存储的员工信息建立一个散列映射:

Map<String,Employee> staff=new HashMap<>();
Employee harry=new Employee("Harry Hacker");
staff.put("987-98-9996",harry);
...

  每当往映射中添加对象时,必须同时提供一个键。在这里,键是一个字符串,对应的值是Employee对象。

  要想检索一个对象,必须使用(因而,必须记住)一个键

String id="987-98-9996";
e=staff.get(id);

  如果在映射中没有与给定键对应的信息,get将返回null

  null返回值可能并不方便。有时可以有一个好的默认值,用作为映射中不存在的键。然后使用getOrDefault方法

Map<String,Integer> scores=...;
int score=scores.get(id,0);//Gets 0 if the id is not present

  键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次使用put方法,第二个值就会取代第一个值。实际上,put将返回用这个键参数存储的上一个值

  remove方法用于从映射中删除给定键对应的元素。size方法用于返回映射中的元素数

  要迭代处理映射的键和值,最容易的方法是使用forEach方法。可以提供一个接受键和值的lambda表达式。映射中每一项会依序调用这个表达式

scores.forEach((k,v)->
    System.out.println("key=" +k+",value=" +v));

  2)更新映射项

  处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个特殊情况,即键第一次出现

  下面来看一个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词时,我们将计数器增1,如下所示:

count.put(word,counts.get(word) +1);

  这是可以的,不过有一种情况除外:就是第一次看到word时。在这种情况下,get会返回null,因此会出现一个NullPointerException异常

  作为一个简单的补救,可以使用getOrDefault方法

counts.put(word,counts.getOrDefault(word,0)+1);

  不过还可以做得更好。merge方法可以简化这个常见的操作。如果键原先不存在,下面的调用

counts.mergoe(word,1,Integer::sum);

  将把word与1关联,否则使用Integer::sum函数组合原值和1(也就是将原值与1求和)

  3)映射视图

  集合框架不认为映射本身是一个集合。(其他数据结构认为映射是一个键/值对集合,或者是由键索引的值集合。)不过,可以得到映射的视图(view)----这是实现了Collection接口或某个子接口的对象

  有3种视图:键集,值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本下面的方法

Set<K> keySet()
Collection<V> values()
Set<Map,Entry<K,V>> entrySet()

  会分别返回这3个视图。(条目集的元素是实现Map.Entry接口的类的对象。)

  需要说明的是,keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collection接口。因此,可以像使用集合一样使用keySet。

  例如,可以枚举一个映射的所有键:

Set<String> keys=map.keySet();
for(String key : keys){
    do something with key
}

  如果想同时查看键和值,可以通过枚举条目来避免查找值。使用以下代码:

for(Map.Entry<String,Employee> entry : staff.entrySet()){
    String k=entry.getKey();
    Employee v=entry.getValue();
    do something with k,v
}

  注:原先这是访问所有映射条目的最高效的方法。如今,只需要使用forEach方法

counts.forEach((k,v)->{
    do something with k,v
});

  如果在键集试图上调用迭代器的remove方法,实际上会从映射中删除这个键与它关联的值。不过,不能向键集视图增加元素。另外,如果增加一个键而没有同时增加值也是没有意义的。如果试图调用add方法,它会抛出一个UnsupportedOperationException。条目集视图有同样的限制,尽管理论上增加一个新的键/值对好像是有意义的

  4)弱散列映射

  在集合类库中有几个专用的映射类,本节做简要介绍。

  设计WeakHashMap类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了,将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键/值对无法从映射中删除。为什么垃圾回收器不能够删除它呢?难道删除无用的对象不是垃圾回收器的工作吗?

  遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,他们不能被回收。因此,需要由程序负责从长期存货的映射表中删除那些无用的值。或者使用WeakHashMap完成这件事情。当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对

  下面是这种机制的内部运行情况。WeakHashMap使用弱引用(weak references)保存键。WeakReferences对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReferences引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应的条目

  5)链接散列集与映射

  LinkedHashSet和LinkedHashMap类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入列表中,就会并入到双向链表中

Map<String,Employee> staff=new LinkedHashMap<>();
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 Cooper"));
staff.put("456-62-5527",new Employee("Francesca Cruz"));

  然后,staff.keySet().iterator()以下面的次序枚举键:

  144-25-5464

  567-24-2546

  157-62-7935

  456-62-5527

  并且staff.values().iterator()以下列顺序枚举这些值:

  Amy Lee

  Harry Hacker

  Gary Cooper

  Francesca Cruz

  链表散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)要想构造这样一个的散列映射表,请调用

LinkedHashMap<K,V>(initialCapacity,loadFactor,true)

  访问顺序对于实现高速缓存的“最近最少使用”原则十分重要。例如,可能希望将访问频率搞的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入表中,并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。

  甚至可以让这一过程自动化。即构造一个LInkedHashMap的子类,然后覆盖下面这个方法

protected boolean removeEldestEntry(Map,Entry<K,V> eldest)

  每当方法返回true时,就添加一个新条目,从而导致删除eldest条目。例如,下面的高速缓存可以存放100个元素

Map<K,V> cache=new
    LinkedHashMap<>(128,0.75F,true){
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
            return size() >100;
           }
        }
    }();

  另外,还可以对eldest条目进行评估,以此决定是否应该将它删除。例如,可以检查与这个条目一起存在的时间戳

  6)枚举集与映射

  EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。

  EnumSet类没有公共的构造器。可以使用静态工厂方法构造这个集

enum Weekday{MONDAY,TUESDAY,WEDNESDAY,THURDAY,FRIDAY,SATURDAY,SUNDAY};
EniumSet<Weekday> always=EnumSet.allOf(Weekday.class);
EniumSet<Weekday> never=EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday=EnumSet.range(Weekday.MONDAY,Weekday.FRIDAY);
EnumSet<Weekday> mwf=EnumSet.of(Weekday.MONDAY,Weekday.WEDNESDAY,Weekday.FRIDAY);

  可以使用Set接口的常用方法来修改EnumSet

  

  EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型

EnumMap<WeekDay,Employee> personInCharge=new EnumMap<>(weekday.class);

  注:在EnumSet的API文档中,将会看到E extends Enum<E>这样奇怪的类型参数。简单地说,它的意思是“E是一个枚举类型。“所有的枚举类型都扩展于泛型Enum类。例如,Weekday扩展Enum<Weekday>

4.视图与包装器

  大家可能会感觉:用如此多的接口和抽象类来实现数量并不多的具体集合类似乎没有太大必要。然而,这两张图并没有展示出全部的情况。通过使用视图(views)可以获得其他的实现了Collection接口和Map接口的对象。

  映射类的keySet方法就是一个这样的示例。初看起来,好像这个方法创建了一个新集,并将映射中的所有键都填进去,然后返回这个集。但是,情况并非如此。取而代之的是:keySet方法返回一个实现Set类接口的对象,这个类的方法对原映射进行操作。这种集合称为视图。  

  视图技术在集框架中有许多非常有用的应用。下面将讨论这些应用。

  1)轻量级集合包装器

  Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。例如:

Card[] cardDeck=new Card[52];
...
List<Card> cardList=Arrays.asList(cardDeck);

  返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的get和set方法。改变数组大小的所有方法(例如,迭代器相关的add和remove方法)都会抛出一个UnsupportedOperationException异常

  asList方法可以接收可变数目的参数。例如:

List<String> names=Arrays.asList("Amy","Bob","Carl");

  这个方法调用

Collections.nCopies(n,anObject)

  将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个anObject的错觉

  例如,下面的调用将创建一个包含100个字符串的List,每个串都被设置为"DEFAULT":

List<String> settings=Collections.nCopies(100,"DEFAULT");

  存储代价很小。这是视图技术的一种巧妙应用

  注:Collections类包含很多实用方法,这些方法的参数和返回值都是集合。不要将它与Collection接口混淆起来

  如果调用下列方法:

Collections.singleton(anObject)

  则将返回一个视图对象。这个对象实现了Set接口(与产生LIst的ncopies方法不同)。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。singletonList方法与singletonMap方法类似

  类似地,对于集合框架中的每一个接口,还有一些方法可以生成空集,列表,映射,等等。特别是,集的类型可以推导得出

Set<String> deepThoughts=Collections.emptySet();

  2)子范围

  可以为很多集合建立子范围(subrange)视图。例如,假设有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得一个列表的子范围视图

List group2=staff.subLIst(10,20);

  第一个索引包含在内,第二个索引则不包含在内。这与String类的substring操作中的参数情况相同

  可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。例如,可以删除整个子范围

group2.clear();//staff reduction

  现在,元素自动地从staff列表中清除了,并且group2为空。

  对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围。SortedSet接口声明了3个方法

SortedSet<E> subSet(E from,E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)

  这些方法将返回大于等于from且小与to的所有元素子集。有序映射也有类似的方法

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,boolean fromInclusive,E to,boolean toInclusive)
NavigableSet<E> headSet(E to,boolean toInclusive)
NavigableSet<E> tailSet(E from,boolean fromInclusive)

  3)不可修改的视图

  Collections还有几个方法,用于产生集合的不可修改视图(unmodifiable views)。这些视图对现有集合增加了一个运行时检查。如果发现视图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态

  可以使用下面8种方法获得不可修改视图

Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap

  每个方法都定义于一个接口。例如,Collections.unmodifiableList与ArrayList,LinkedList或者任何实现了List接口的其他类一起协同工作。

  例如,假设想要查看某部分代码,但又不触及某个集合的内容,就可以进行下列操作

LIst<String> staff=new LInkedList<>();
...
lookAt(Collections.unmodifiableList(staff));

  Collections.unmodifiableList方法将返回一个实现List接口的类对象。其访问器方法将从staff集合中获取值。当然,lookAt方法可以调用List接口中的所有方法,而不只是访问器。但是所有的更改器方法(例如,add)已经被重新定义为抛出一个UnsupportedOperationException异常,而不是将调用传递给底层集合。

  不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是staff)对集合进行修改。并且仍然可以让集合的元素调用更改器方法

  由于视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的方法。例如,LinkedList类有一些非常方便的方法,addFirst和addLast,它们都不是List接口的方法,不能通过不可修改视图进行访问

  注:unmodifiableCollection方法(与稍后讨论的synchronizedCollection和checkedCollection方法一样)将返回一个集合,它的equals方法不调用底层的equals方法。相反,它继承了Object类的equals方法,这个方法只是检测两个对象是否是同一个对象。如果将集或列表转换成集合,就再也无法检测其内容是否相同了。视图就是以这种方式运行的,因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以同样的方式处理hashCode方法。

         然而,unmodifiableSet类和unmodifiableList类却使用底层集合的equals方法和hashCode方法

  4)同步视图

  如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正对散列表进行再散列,其结果将是灾难性的。

  类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。例如,Collections类的静态synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map

Map<String,Employee> map=Collections.synchronizedMap(new HashMap<String,Employee>());

  现在,就可以由多线程访问map对象了。像get和put这类方法都是同步操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成

  5)受查视图

  ”受查“视图用来对泛型类型发生问题时提供调试支持。实际上将错误类型的元素混入泛型集合中的问题极有可能发生。例如:

ArrayList<String> strings=new ArrayList<>();
ArrayList rawList=strings;//warining only,not an error,for compatibility with legacy code
rawList.add(new Date());//now strings contains a Date object!

  这个错误的add命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用get方法,并将结果转化为String时,这个类才会抛出异常

  受查视图可以探测到这类问题。下面定义了一个安全列表

List<String> safeStrings=Collections.checkedList(strings,String.class);

  视图的add方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException。这样做的好处是错误可以在正确的位置得以报告:

ArrayList rawList=safeStrings;
rawList.add(new Date());//checked list throws a ClassCastException

  注:受查视图受限于虚拟机可以运行的运行时检查。例如,对于ArrayList<Pair<String>>,由于虚拟机有一个单独的”原始“Pair类,所以,无法阻止插入Pair<Date>

  6)关于可选操作的说明

  通常,视图有一些局限性,即可能只可以读,无法改变大小,只支持删除而不支持插入,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个UnsupportedOperationException。

  在集合和迭代器接口的API文档中,许多方法描述为”可选操作“。这看起来与接口的概念有所抵触。毕竟,接口的设计目的难道不是负责给出一个类必须实现的方法吗?

  确实,从理论的角度看,在这里给出的方法很难令人满意。一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。不过,这将会使接口的数量成倍增长,这让类库设计者无法接受。

  是否应该将”可选“方法这技术扩展到用户的设计中呢?我们认为不应该。尽管集合被频繁地使用,其实现代码的风格也未必适用于其他问题领域。集合类库的设计者必须解决一组特别严格且又相互冲突的需求。用户希望类库应该易于学习,使用方便,彻底泛型化,面向通用性,同时又与手写算法一样高效。要同时达到所有目标的要求,或者尽量兼顾所有目标完全是不可能的。但是,在自己的变成问题中,很少遇到这样极端的局限性。应该能够找到一种不必依靠极端衡量”可选的“接口操作来解决这类问题的方案。  

5.算法

  泛型集合接口有一个很大的优点,即算法只需要实现一次

  例如,考虑以下计算集合中最大元素这样一个简单的算法。但是,编写循环代码有些乏味,并且很容易出错。是否存在严重错误吗?对于空容器循环能正常工作吗?对于只含有一个元素的容器又会发生什么情况呢?我们不希望每次都测试和调试这些代码,也不想实现下面这一系列方法

static <T extends Comparable> T max(T[] a)
static <T extends Comparable> T max(ArrayList<T> v)
static <T extends Comparable> T max(LinkedList<T> l)

  这正是集合接口的用武之地。仔细考虑一下,为了高效地使用这个算法所需要的最小集合接口。采用get和set方法进行随机访问要比直接迭代层次高。在计算链表中最大元素的过程中已经看到,这项任务并不需要进行随机访问。直接用迭代器遍历每个元素就可以计算最大元素。因此,可以将max方法实现为能够接收任何实现了Collection接口的对象

public static <T extends Comparable> T max(Collection<T> c){
    if(c.isEmpty()) throw new NoSuchElementException();
    Iterator<T> iter=c.iterator();
    T largest=iter.next();
    while(iter.hasNext()){
        T next=iter.next();
        if(largest.compareTo(next)<0)
            largest=next;
    }
    return largest;
}

  现在就可以使用一个方法计算链表,数组猎豹或数组中最大元素了。

  这是一个非常重要的概念。事实上,标准的C++已经有几十种非常有用的算法,每个算法都是在泛型集合上操作的。Java类库的算法没有如此丰富,但是,也包含了基本排序,二分查找等实用算法。

  1)排序与混排

  Collections类中的sort方法可以对实现了LIst接口的集合进行排序

List<String> staff=new LinkedList<>();
fill collection
Collections.sort(staff)

  这个方法假定列表元素实现了Comparable接口。如果想采用其他方式对列表进行排序,可以使用List接口的sort方法并传入一个Comparator对象。可以如下按工资对一个员工列表排序

staff.sort(Comparator.comparingDouble(Employee::getSalary));

  如果想按照降序对列表进行排序,可以使用一种非常方便的静态方法Collections.reverseOrder()。这个方法将返回一个比较器,比较器则返回b.compareTo(a)。例如:

staff.sort(Comparator.reverseOrder())

  这个方法根据元素类型的compareTo方法给定排序顺序,按照逆序对列表staff进行排序同样

staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())

  将按工资逆序排序

  人们可能对sort方法所采用的排序手段感到好奇。通常,在翻阅有关算法书籍中的排序算法时,会发觉介绍的都是有关数组的排序算法,而且使用的是随机访问方式但是,对列表进行随机访问的效率很低。实际上,可以使用归并排序对列表进行高效的排序。然而,Java并不是这样实现的。它直接将所有元素转入一个数组,对数组进行排序,然后,再将排序后的序列复制回列表

  集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是,归并排序有一个主要的优点:稳定,即不需要交换相同的元素。为什么要关注相同元素的顺序呢?下面是一种常见的情况。假设有一个已经按照姓名排列的员工列表。现在,要按照工资再进行排序。如果两个雇员的工资相等发生什么情况呢?如果采用稳定的排序算法,将会保留按名字排列的顺序。换句话说,排序的结果将会产生这样一个列表,首先按照工资排序,工资相同者再按照姓名排序

  因为集合不需要实现所有的“可选方法”,因此,所有接受集合参数的方法必须描述什么时候可以安全地将集合传递给算法。例如,显然不能将unmodifiableList列表传递给排序算法。可以传递什么类型的列表呢?根据文档说明,列表必须是可修改的,但不必是可以改变大小的。

  下面是有关的术语定义:

  1)如果列表支持set方法,则是可修改的。

  2)如果列表支持add和remove方法,则是可以改变大小的

  Collections类有一个算法shuffle,其功能与排序刚好相反,即随机地混排列表中元素的顺序。例如:

ArrayList<Card> cards=...;
Collections.shuffle(cards);

  如果提供的列表没有实现RandomAccess接口,shuffle方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表

  2)二分查找

  Collections类的binarySearch方法实现了这个算法。注意,集合必须是排好序的,否则算法将返回错误的答案。要想查找某个元素,必须提供集合(这个集合要实现List接口)以及要查找的元素。如果集合没有采用Comparable接口的CompareTo方法进行排序,就还要提供一个比较器对象

i=Collections.binarySearch(c,element);
i=Collections.binarySerach(c,element,comparator);

  如果binarySearch方法返回的数值大于等于0,则表示匹配对象的索引。也就是说,c.get(i)等于这个比较顺序的element。如果返回负值,则表示没有匹配的元素。但是,可以利用返回值计算应该将element插入到集合的哪个位置,以保持集合的有序性。插入的位置是

insertionPoint=-i-1;

  这并不是简单的-i,因为0值是不确定的。也就是说,下面这个操作

if(i<0)
    c.add(-i-1,element);

  将把元素插入到正确的位置上。

  只有采用随机访问,二分查找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分查找就完全失去了优势。因此,如果为binarySearch算法提供一个链表,它将自动地变为线性查找

  3)简单算法

  在Collections类中包含了几个简单且很有用的算法。前面介绍的查找集合中最大元素的示例就在其中。另外还包括,将一个列表中的元素复制到另外一个列表中;用一个常量值填充容器;逆置一个列表的元素顺序

  我们之所以喜欢这些算法是因为:它们可以让程序员阅读算法变成一件轻松的事情。当阅读由别人实现的循环时,必须要揣摩编程者的意图。而在看到诸如Collections.max这样的方法调用时,一定会立刻明白其用途。

  Java SE 8增加了默认方法Collection.removeIf和LIst.replaceAll,这两个方法稍有些复杂。要提供一个lambda表达式来测试或转换元素例如,下面的代码将删除所有短词,并将其余单词改为小写

words.removeIf(w->w.length()<=3);
words.replaceAll(String::toLowerCase);

  Collections API:

  static <T extends Comparable<? super T>> T min(Collection<T> elements)

  static <T extends Comparable<? super T>> T max(Collection<T> elements)

  static <T> min(Collection<T> elements,Comparator<? super T> c)

  static <T> max(Collection<T> elements,Comparator<? super T> c)

  返回集合中最小的或最大的元素(为清楚起见,参数的边界被简化了)。

  static <T> void copy(List<? super T> to ,List<T> from)

  将原列表中的所有元素复制到列表的相应位置上。目标列表的长度至少与原列表一样。

  static <T> void fill(List<? super T> l,T value)

  将列表中所有位置设为相同的值

  static <T> boolean addAll(Collection<? super T> c,T... values)

  将所有的值添加到集合中。如何集合改变了,则返回true。

  static <T> boolean replaceAll(List<T> l,T oldValue,T newValue)

  用newValue取代所有值为oldValue的元素

  

  4)集合与数组的转换

  如果需要把一个数组转换为集合,Arrays.asList包装器可以达到这个目的。例如,

String[] values=...;
HashSet<String> staff=new HashSet<>(Arrays.asList(values));

  从集合得到数组会更困难一些。当然,可以使用toArray方法

Object[] values=staff.toArray();

  不过,这样做的结果是一个对象数组。尽管你知道集合中包含一个特定类型的对象,但不能使用强制类型转换

String[] values=(String[]) staff.toArray();//Error!

  toArray方法返回的数组是一个Object[]数组,不能改变它的类型。实际上,必须使用toArray方法的一个变体形式,提供一个所需类型而且长度为0的数组。这样一来,返回的数组就会创建为相同的数组类型

String[] values=staff.toArray(new String[0]);

  如果愿意,可以构造一个指定大小的数组

staff.toArray(new String[staff.size()]);

  在这种情况下,不会创建新数组。

  注:你可能奇怪为什么不能直接将一个Class对象(如String.class)传递到toArray方法。原因是这个方法有“双重职责”,不仅要填充一个已有数组(如果它足够长),还要创建一个新数组

  6)编写自己的算法

  如果编写自己的算法(实际上,是以集合作为参数的任何方法),应该尽可能地使用接口,而不要使用具体的实现

  例如,假设想用一组菜单项填充JMenu。传统上,这种方法可能会按照下列方式实现:

void fillMenu(JMenu menu,ArrayList<JMenuItem> items){
    for(JMenuItem item: items)
        menu.add(item);
}

  但是,这样会限制方法的调用程序,即调用程序必须在ArrayList中提供选项。如果这些选项需要放在另一个容器中,首先必须对它们重新包装,因此,最好接收一个更加通用的集合

  什么是完成这项工作的最通用集合接口?在这里,只需要访问所有的元素,这是Collection接口的基本功能。下面的代码说明了如何重新编写fillMenu方法使之接受任意类型的集合。

void fillMenu(JMenu menu,Collection<JMenuItem> items){
    for(JMenuItem item:items)
        menu.add(item);
}

  现在,任何人都可以用ArrayList或LInkedLIst,甚至用Arrays.asLIst包装器包装的数组调用这个方法。

  如果编写了一个返回集合的方法,可能还想要一个返回接口,而不是返回类的方法,因为这样做可以在日后改变想法,并用另一个集合重新实现这个方法

  在这种情况下,必须提醒调用者返回的对象是一个不可修改的列表

6.遗留的集合

  1)Hashtable类

  Hashtable类与HashMap类的作用是一样的,实际上,它们拥有相同的接口。与Vector类的方法一样。Hashtable的方法也是同步的。如果对同步性与遗留代码的兼容性没有任何要求,就应该使用HashMap。如果需要并发访问,则要使用ConcurrentHashMap

  2)枚举

  3)属性映射

  属性映射(property map)是一个类型非常特殊的映射结构。它有下面3个特性:

  a.键与值都是字符串

  b.表可以保存到一个文件中,也可以从文件中加载。

  c.使用一个默认的辅助表

  实现属性映射的Java平台类称为Properties。

  属性映射通常用于程序的特殊配置选项

  4)栈

  从1.0版本开始,标准类库中就包含了Stack类,其中有大家熟悉的push方法和pop方法。但是,Stack类扩展为Vector类,从理论角度看,Vector类并不太令人满意,它可以让栈使用不属于栈操作的insert和remove方法,即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶

  5)位集 

  








  

  

    

  

  

原文地址:https://www.cnblogs.com/Miromiaosang/p/8609707.html