java.util之集合框架

JDK8在线Api中文手册

JDK8在线Api英文手册

1 集合概述

   Java集合框架标准化了程序处理对象组的方式。集合不是原始Java发布版本的一部分,但是在J2SE1.2版本就已经添加。在集合框架之前,Java提供了特定的类以存储和管理对象组。例如Dictionary、Vector、Stack和Properties。尽管这些类相当有用,但是它们缺少集中、统一的主题。例如,Vector的使用方式与Properties不同。此外,早期的专业方式没有被设计成以扩展、易改造的形式。集合是解决这些(以及其他)问题的答案。
   集合框架设计上需要满足几个目标。首先,框架必须是高性能的。基本集合(动态数组、链表、树以及哈希表)的实现是高效率的。很少需要手动编写这些“数据引擎”中的某个。其次,框架必须允许不同类型的集合以类似的方式进行工作,并且具有高度的互操作性。再次,扩展和/或改造集合必须易于实现。为了满足这些目标,整个集合框架基于一套标准接口进行构造,提供了这些接口的一些可以直接使用的标准实现(例如LinkedList、HashSet和TreeSet)。作为一种选择,也可以实现自己的集合。为了方便,提供各种特定目的的实现,并且提供一些部分实现,从而使我们可以更容易地创建自己的集合类。最后,必须添加可以将标准数组集成到集合框架中的机制
   算法是集合机制的另外一个重要部分。算法操作集合,并且被定义为Collections类中的静态方法。因此所有集合都可以使用它们。每个集合类都不需要实现特定于自己的版本,算法为操作集合提供了标准的方式。
   与集合框架密切相关的另一个内容是Iterator接口。迭代器为访问集合中的元素提供了通用、标准的方式,每次访问一个元素。因此,迭代器提供了美剧集合内容的一种方式。因为每个集合都提供了迭代器,所以可以通过Iterator定义的方法访问所有集合类的元素。因此,对循环遍历集合的代码进行很小的修改,就可以将其用于循环遍历列表。
   JDK8添加了另一种类型的迭代器,叫做spliterator。简单来说,spliterator就是并行迭代支持的迭代器。支持spliterator的接口有Spliterator和支持基本类型的几个嵌套接口。JDK8还添加了用于基本类型的迭代器接口,例如PrimitiveIterator和PrimitiveIterator.OfDouble。
   除了集合外,框架还定义了一些映射接口和类。映射存储键/值对。尽管映射是集合框架的组成部分,但是从严格意义上讲,它们不是集合。不过,可以获得映射的集合视图(collection-view)。这种视图包含来自映射的元素,并存储在集合中。因此,作为一种选择,可以像处理集合那样处理映射的内容。
   集合机制改造了java.util定义的某些原始类,从而使它们也可以被集成到新系统中。尽管几个替代了许多原始使用工具类的架构,但是不会造成任何类过时,集合只是为做某些事情提供了更好的方式。

2 JDK5对集合框架的修改

   在发布JDK5时,对集合框架进行了一些根本性的改变,这些修改显著增强了集合框架的功能并简化了它的使用。这些修改包括增加泛型、自动装箱/拆箱以及for-each风格的for循环。尽管JDK8是JDK5之后的第三个主要Java发布版本,但是JDK5特性的效果是如此深远,以至于它们仍然令人特别关注。主要原因是大量JDK5以前的代码仍然在使用。

2.1 泛型从根本上改变了集合框架

   新增的泛型造就了集合框架的一个重要改变,因为整个集合框架针对泛型进行了重新设计。所有集合现在都是泛型的。并且许多操作集合的方法现在采用泛型参数,简单地讲,新增的泛型影响了集合框架的每一部分。
   泛型添加了集合一直确实的一个特性:类型安全性。在泛型以前,所有集合都存储Object引用,这意味着所有集合可以存储任何类型的对象。因此,可能会无意地在集合中存储不兼容的类型。如果出现这种情况,就会导致运行时类型不匹配错误。使用泛型,可以明确地表明将要存储的数据类型,从而可以避免运行时类型不匹配错误。
   尽管新增的泛型彻头彻尾地改变了大部分集合和接口声明,以及它们的一些方法。但总体而言,集合框架的工作方法会与泛型出现以前是相同的。当然,为了获取泛型为集合带来的好处,需要重新编写旧代码。这也很重要,因为使用现代Java编译器编译在泛型出现之前的代码,会产生警告信息。为了消除这些警告,需要为所有集合代码添加类型信息。

2.2 自动装箱使得使用基本类型更加容易

   自动装箱/拆箱方便了在集合中存储基本类型。集合只能存储引用,不能存储基本类型值。在过去,如果希望在集合中存储基本类型值,如int,就必须手动将数值装箱到相应的类型封装器中。当检索数值时,需要(使用显式的类型转换)手动拆箱为正确的基本类型。因为提供了自动装箱/拆箱特性。当存储或检索基本类型时,Java可以自动执行需要的装箱和拆箱操作。不再需要手动执行这些操作。

2.3 for-each风格的for循环

   Java对集合框架中的所有集合进行了重新设计以实现Iterable接口。这意味着可以使用foreach风格的for循环遍历集合。在过去,遍历集合需要手动构造循环来使用迭代器。尽管在许多情况下,为了某些用途仍然需要迭代器,但是可以使用for循环替代基于迭代器的循环。

3 集合接口

   集合框架定义了一下核心接口,下面是对每个接口进行简要介绍。首先介绍集合接口是必要的,因为它们决定了集合的本质特性。换句话说,具体类只是提供了标准接口的不同实现。表1汇总了奠定集合基础的那些接口。

表 1 奠定集合基础的接口
接 口描 述
Collection允许操作一组对象,位于集合层次结构的顶部
Deque扩展Queue以处理双端队列
List扩展Collection以处理序列(对象列表)
NavigableSet扩展SortedSet以基于最接近匹配原则检索元素
Queue扩展Collection以处理特殊类型的列表,这种类型的列表只能从列表顶部删除元素
Set扩展Collection以处理集合,集合中的元素必须唯一
SortedSet扩展Set以处理已排序的集合

   除了集合接口外,集合还使用Comparator、RandomAccess、Iterator以及ListIterator接口。从JDK8开始,还使用Spliterator接口。简单地讲,Comparator定义了如何比较两个对象:Iterator、ListIterator和Spliterator枚举集合中的对象。通过实现RandomAccess,可以表明列表支持高效、随机的元素访问。
   为了提供最大的灵活性,集合接口允许某些方法是可选的,可选方法允许修改集合的内容。支持这些方法的集合被称为是可修改的,不允许修改内容的集合被称为不可修改集合,如果试图在不可修改的集合上使用这些方法,就会抛出UnsupportedOperationException异常。所有内置集合都是可修改的。
   接下来分析各种集合接口。

3.1 Collection接口

   Collection接口是构建集合框架的基础,因为定义集合的所有类都必须实现该接口。Collection接口是泛型接口,其声明如下:

interface Collection<E>

   其中,E指定了集合将要存储的对象的类型。Collection扩展了Iterable接口,这意味着所有集合都可以使用for-each风格的for循环进行遍历(只有实现了Iterable接口的类才能够通过for循环进行遍历)。
   Collection声明了所有集合都将拥有的核心方法。表2对这些方法进行了总结。因为所有集合都实现了Collection接口,所以为了清晰理解框架,熟悉接口方法时有必要的。这些方法中的某些方法可能抛出UnsupportedOperationException异常。如果集合不能修改就可能发生这种情况。如果一个对象和另一个对象不兼容,那么会生成ClassCastException异常,例如当试图将一个不兼容的对象添加到集合中时,就会发生这种情况。如果试图在不允许存储null对象的集合中存储null对象,那么就会跑出NullPointerException异常,如果使用的参数无效,那么会抛出IllegalArgumentException异常。如果试图为长度固定并且已经满了的集合添加元素,那么会抛出IllegalStateException异常。

表2 Collection接口声明的方法
方 法描 述
boolean add(E obj)将obj添加到调用集合中,如果obj被添加到集合中,就返回true;如果obj已经是集合的成员并且集合的元素不允许重复,就返回false
boolean addAll(Collection<? extends E> c)将c中的所有元素添加到调用集合中,如果调用几个发生了变化(例如添加了元素),就返回true,否则就返回false
boolean contains(Object obj)如果obj是调用集合中的元素,就返回true;否则返回false
boolean containsAll(Collection<?> c)如果调用集合包含c的所有元素,就返回true;否则返回false
boolean equals(Object obj)如果调用集合与obj相等,就返回true;否则返回false
int hashCode()返回调用集合的散列码
boolean isEmpty()如果调用集合为空,就返回true;否则返回false
Iterator<E> iterator返回调用集合的一个迭代器
default Stream<E> parallelStream返回一个使用调用集合作为元素来源的流,该流能够支持并行操作(JDK8新增)
boolean remove(Object obj)从调用集合中移除obj的一个实例,如果移除了元素,就返回true;否则返回false
boolean removeAll(Collection<?> c)从调用集合中移除c的所有元素。如果操作集合发生了变化(移除了元素),就返回true;否则返回false
default boolean removeIf(Predicate<? super E> predicate)从调用集合中移除满足predicate指定条件的那些元素(JDK8新增)
boolean retainAll(Collection<?> c)移除调用集合中除c中元素之外的所有元素。如果集合发生了变化(即移除了元素),就返回true;否则返回false
int size()返回调用集合元素的数量
default Spliterator<E> spliterator()返回调用集合的Spliterator(JDK8新增)
default Stream<E> stream()返回一个使用调用集合作为元素来源的流。该流是顺序流(JDK8 新增)
Object[] toArray()返回包含调用集合中存储的所有元素的数组,数组元素是集合元素的副本
<T> T[] toArray(T array[])返回包含调用集合中元素的数组。数组元素是集合元素的副本。如果array的长度等于元素的数量,就将返回的元素保存在array中;如果array的长度小于元素的数量,就分配必需大小的新数组并返回这个新数组;如果array长度大于元素的数量,就将最后一个集合元素之后的数组元素设置为null;如果所有集合元素的类型都不是array的子类型,那么就抛出ArrayStoreException异常

   通过调用add()方法可以将对象添加到集合中。注意,add()方法采用类型为E的参数,这意味着添加到集合中的对象必须和集合所期望的对象相兼容。通过调用addAll()方法,可以将一个集合的所有内容添加到另一个集合中。
   通过使用remove()方法可以移除一个对象。为了移除一组对对象,需要调用removeAll()方法。通过调用retainAll()方法,可以移除除了制定元素之外的所有元素。从JDK8开始,要想仅移除满足某些条件的元素,可以使用removeIf()方法(Precidate是JDK8新增的一个函数式接口)。为了清空集合,可以调用clear()方法。
   通过调用contains()方法,可以确定集合是否包含特定的对象。为了确定一个集合是否包含另一个集合的所有成员,可以调用containsAll()方法。通过调用isEmpty()方法可以确定集合是否为空。通过调用size()方法可以确定集合当前包含的元素的数量。
   toArray()方法返回一个数组,其中包含调用集合中存储的元素。该方法的第一种形式返回Object类型的数组;第二种形式返回的数组,与指定为参数的数组具有相同的类型。通常,第二种形式更方便,因为能够返回期望的数组类型。这些方法比它们初看起来更重要。通常,使用类似数组的语法处理集合的内容是有利的。通过提供集合和数组之间的转换途径,可以更好地利用两者的优点。
   通过调用equals()方法可以比较;两个集合的相等性。"相等"的精确含义依据集合的不同可以有所区别。例如,可以实现equals()方法,从而比较存储在集合中的元素的值。或者,equals()方法也可以比较对这些元素的引用。
   另一个重要的方法是iterator(),它返回集合的一个迭代器。新的Spliterator()方法返回集合的一个spliterator。当操作集合时会频繁用到迭代器。最后,stream()和parallelStream()方法返回使用集合作为元素来源的流。

3.2 List接口

   List接口扩展了Collection,并且声明了用来存储一连串元素的集合的行为。在列表中,可以使用从0开始的索引,通过元素的位置插入或访问元素。列表可以包含重复的元素。List是泛型接口,其声明如下:

interface List<E>

   其中,E指定了将存储于列表中的对象的类型。
   除了Collection定义的方法外,List还定义了自己的一些方法,表3汇总了这些方法。请注意,如果列表是不能修改的,那么这些方法中的某些方法会抛出UnsupportedOpreationException异常;并且如果一个对象和另一个对象不兼容,那么抛出ClassCastException异常,例如当试图将不兼容的对象添加到列表中时,就会排除该异常。此外,如果使用的索引无效,一些方法会抛出IndexOutOfBoundsException异常。如果试图在不允许存储null对象的列表中存储null对象,那么会抛出NullPointerException异常。如果使用的参数无效,那么会抛出IllegalArgumentException异常。

表3 List接口声明的方法
方 法描 述
void add(int index,E obj)将obj插入到调用列表中由index指定的索引位置。在插入点及之后位置存储的元素被后移,因此没有元素会被覆盖
boolean addAll(int index,Collection<? extends E> c)将c中的所有元素插入到列表中由index指定的索引位置。任何在插入点以及之后位置存储的元素都将后移,因此没有元素会被覆盖。如果调用列表发生了变化,就返回true;否则返回false
E get(int index)返回调用集合中在指定索引位置存储的对象
int indexOf(Object obj)返回调用列表中第一个obj实例的索引。如果obj不是列表中的元素,就返回-1
int lastIndexOf(Object obj)返回调用列表中最后一个obj实例的索引。如果obj不是列表中的元素,就返回-1
ListIterator<E> listIterator返回调用列表的一个迭代器,该迭代器从列表的开头开始
ListIterator<E> listIterator(int index)返回调用列表的一个迭代器,该迭代器从列表中index指定的索引位置开始
E remove(int index)从调用列表中移除位于index索引位置的元素,并返回被移除的元素,结果列表被压缩。也就是说,后面所有元素的索引都被减1
default void replaceAll(UnaryOperator<E> opToApply)使用opToApply函数获取得的值更新
E set(int index,E obj)将调用列表中由index指定的索引位置的元素设置为obj,返回原来的值
default void sort(Comparator<? super E> comp)使用comp指定的比较器排序列表(JDK8新增)
List<E> subList(int start,int end)返回的子列表包含调用列表中索引位置在start到end-1之间的元素。返回列表中的元素仍然被调用对象引用

   相对于Collection接口定义的add()和addAll()版本,List添加了add(int ,E)和addAll(int,Collection)方法,这些方法在指定的索引位置插入元素。此外,List改变了Collection定义的add(E)和addAll(Collection)方法的语义,以便它们将元素添加到列表的尾部。使用replaceAll()方法可以修改集合中的每个元素(UnaryOperator是JDK8添加的一个函数式接口)。
   为了获得存储在特定位置的元素,使用对象的索引调用get()方法。为了给列表中的元素赋值,可以调用set()方法,并指定将要修改的对象的索引。为了查找对象的索引,可以使用indexOf()或lastIndexOf()方法。
   通过调用subList()方法,指定子列表的开始索引和结束索引,可以获得列表的子列表。subList()方法极大地简化了列表处理。List定义的sort()方法是排序列表的一种方法。

3.3 Set接口

   Set接口定义了组(set)。它扩展了Collection接口,并且声明了集合中不允许有重复元素的组行为。所以,如果为组添加重复的元素,add()方法就会返回false。Set接口没有定义自己的其他方法。Set是泛型接口,其声明如下:

interface Set<E>

   其中,E指定了组将包含的对象的类型。

3.4 SortedSet接口

   SortedSet接口扩展了Set接口,并且声明了以升序进行排序的组行为。SortedSet是泛型接口,其声明如下:

interface SortedSet<E>

   其中,E指定了组将包含的对象的类型。
   除了Set提供的那些方法外,SortedSet接口还声明了表4中汇总的方法。如果调用组中没有包含条目,其中的几个方法会抛出NotSuchElementException异常;如果对象和组中的元素不兼容,就抛出ClassCastException异常;如果试图为不允许存储null对象的组添加null对象,就抛出NullPointerException;如果使用的参数无效,会抛出IllegalArgumentException异常。

表4 SortedSet接口声明的方法
方 法描 述
Comparator<? super E> comparator返回已排序调用的比较器。如果这个组使用自然排序,就返回null
E first()返回已排序调用组的第一个元素
SortedSet<E> headSet(E end)返回的SortedSet对象包含已排序调用组中那些小于end的元素,对于返回的已排序组中的元素,也将被已排序调用组引用
E last()返回已排序调用组中的最后一个元素
SortedSet<E> subSet(E start,E end)返回的SortedSet对象包含索引位置在start与end-1之间的元素。返回组中的元素也将被调用对象引用
SortedSet<E> tailSet(E start)返回的SortedSet对象包含排序组中大于或等于start的元素,返回组中的元素也将被调用对象引用

   SortedSet定义了一些便于进行组处理的方法。为了获得组中的第一个对象,可以调用first()方法;为了得到最后一个元素,可以使用last()方法;通过调用subSet()方法可以获得已排序组的子组,其参数指定了子组中的第一个和最后一个对象;如果需要获得从组中第一个元素开始的子组,可以使用headSet()方法;如果希望得到以组的末尾结尾的子组,可以使用tailSet()方法。

3.5 NavigableSet接口

   NavigableSet接口扩展了SortedSet接口,并且该接口声明了基于最接近匹配原则检索元素的集合行为。NavigableSet是泛型接口,其声明如下:

interface NavigableSet<E>

   其中,E指定了将包含的对象的类型。除了继承自SortedSet接口的方法外,NavigableSet接口还添加了表5中汇总的方法。如果对象与组中的元素不兼容,会抛出ClassCastException异常;如果试图在不允许存储null对象的组中使用null对象,会抛出NullPointerException异常;如果使用的参数无效,会抛出IllegalArgumentException异常。

表5 NavigableSet接口声明的方法
方 法描 述
E celling(E obj)在组中查找大于等于obj的最小元素。如果找到了这样的元素就返回元素;否则返回null
Iterator<E> descendingIterator()返回一个从最大元素向最小元素移动的迭代器。换句话说,返回一个反向迭代器
NavigableSet<E> descendingSet()返回用来翻转调用组的NavigableSet对象。如果找到了这样的元素就返回元素;否则返回null
E floor(E obj)查找组中小于等于obj的最大元素。如果找到了这样的元素就返回元素;否则返回null
NavigableSet<E> headSet(E upperBound,boolean incl)返回的NavigableSet对象包含调用组中小于upperBound的所有元素。如果incl为true,那么包含等于upperBound的那个元素。结果组基于调用组
E higher(E obj)在组中查找大于obj的最大元素。如果找到了这样的元素,就返回元素;否则返回nyll
E lower(E obj)在组中查找小于obj的最大元素。如果找到了这样的元素,就返回元素;否则返回null
E pollFirst()返回第一个元素,在操作过程中移除该元素,因为组是排过序的,所以该元素具有最小值。如果组为空,那么返回null
E pollLast()返回最后一个元素,在操作过程中移除该元素。因为组是排过序的,所以该元素具有最大值。如果组为空,那么返回null
NavigableSet<E> subSet(E lowerBound,boolean lowIncl,E upperBound,boolean highIncl)返回的NavigableSet对象包含调用组中大于lowerBound且小于upperBound的所有元素。如果lowIncl为true,那么包含等于lowerBound的那个元素;如果highIncl为true,那么包含等于upperBound的那个元素。结果组基于调用组
NavigableSet<E> tailSet(E lowerBound,boolean incl)返回的NavigableSet对象包含调用组中大于lowerBound的所有元素。如果incl为true,那么包含等于lowerBound的那个元素。结果组基于调用组

3.6 Queue接口

   Queue接口扩展了Collection接口,并且声明了队列的行为,队列通常是先进先出的列表。但是,还有基于其他准则的队列类型,其声明如下:

interface Queue<E>

   其中,E指定队列将包含的对象的类型。表6显示了Queue定义的方法。

表6 Queue接口声明的方法
方 法描 述
E element()返回队列头部的元素,不移除该元素。如果队列为空,那么抛出NoSuchElementException异常
boolean offer(E obj)试图将obj添加到队列中。如果将obj添加到队列中,那么返回true;否则返回false
E peek()返回队列头部的元素。如果队列为空,那么返回null。不移除该元素
E poll()返回队列头部的元素,在操作过程中移除该元素。如果队列为空,那么返回null
E remove()移除队列头部的元素,并在操作过程中返回该元素。如果队列为空,那么抛出NoSuchElementException异常

   如果对象与队列中的元素不兼容,有些方法会抛出ClassCastException异常;如果试图在不允许有null元素的队列中存储null对象,会抛出NullPointerException异常;如果使用的参数无效,会抛出IllegalArgumentException异常;如果试图从空的队列中移除元素,会抛出NoSuchElementException异常。
   Queue接口尽管比较简单,但却提供了几个有趣的功能。首先,只能从队列的头部移除元素。其次,有两个方法用于获取并移除元素:poll()和remove()。它们之间的区别是:如果队列为空,poll()方法会返回null,而remove()方法会抛出异常。再次,有两个方法可以获取但并不从队列的头部移除元素:element()和peek()。它们之间的唯一区别是:如果队列为空,element()方法会抛出异常,而peek()方法会返回null。最后,注意offer()方法只是试图向队列中添加元素。因为有些队列具有固定长度,并且可能已满,所以offer()方法可能会失败。

3.7 Deque接口

   Deque接口扩展了Queue接口,并且声明了双端队列的行为。双端队列既可以像标准队列那样先进先出,也可以像堆栈那样后进先出。Deque是泛型接口,其声明如下:

interface Deque<E>

   其中,E指定了双端队列将包含的对象的类型。除了继承自Queue接口的方法外,Deque接口还添加了表7中汇总的方法。如果对象与双端队列中的元素不兼容,有些方法会抛出ClassCastException异常;如果试图向不允许为null元素的双端队列中存储null对象,会抛出NullPointerException异常;如果使用的参数无效,会抛出IllegalArgumentException异常;如果想长度固定并且已满的双端队列中添加元素,会炮术IllegalStateException异常;如果试图从空的双端队列中移除元素,会抛出NoSuchElementException异常。

表7 Deque接口声明的方法
方 法描 述
void addFirst(E obj)将obj添加到双端队列的头部。如果超出了容量有限的双端队列的空间,就抛出IllegalStateException异常
void addLast(E obj)将obj添加到双端队列的尾部。如果超出了容量有限的双端队列的空间,就抛出IllegalException异常
Iterator<E> descendingIterator()返回一个双端队列尾部向头部移动的迭代器。换句话说,返回一个反方向迭代器
E getFirst()返回双端队列的第一个元素,不从双端队列中移除该对对象。如果双端队列为空,就抛出NoSuchElemetException异常
E getLast()返回双端队列的最后一个元素,不从双端队列中移除该对象。如果双端队列为空,就抛出NoSuchElementException异常
boolean offerFirst(E obj)将obj添加到双端队列的头部。如果obj被添加到双端队列中,就返回true;否则返回false。因为,如果试图想一个已满并且容量有限的双端队列中添加obj,那么会返回false
boolean offerLast(E obj)试图将obj添加到双端队列的尾部,如果obj被添加到双端队列中,就返回true;否则返回false
E peekFirst()返回双端队列头部的元素。如果双端队列为空,就返回null。对象不被移除
E peekLast()返回双端队列的尾部元素。如果双端队列为空,就返回null。对象不被移除
E pollFirst()返回双端队列头部的元素,在操作过程中移除该元素。如果双端队列为空,就返回null
E pollLast返回双端队列尾部的元素,在操作过程中移除该元素,如果双端队列为空,就返回null
E pop()返回双端队列头部的元素,在操作过程中移除该元素。如果双端队列为空,就抛出NoSuchException异常
void push(E obj)将obj添加到双端队列的头部,如果超出了容量有限的双端队列的空间,就抛出IllegalStateException异常
E removeFirst()返回双端队列头部的元素,在操作过程中移除该元素。如果双端队列为空,就抛出NoSuchException异常
boolean removeFirstOccurrence(Object obj)在双端队列中移除第一次出现的obj对象。如果成,就返回true;如果双端队列中不包含obj,就返回false
E removeLast()返回双端队列尾部的元素,在操作过程中移除该元素。如果双端队列为空,就抛出NoSuchElementException异常
boolean removeLastOccurence(Object obj)从双端队列中移除最后一次出现的obj对象。如果成功,就返回true,如果双端队列中不包含obj,就返回false

   注意Deque接口提供了push()和pop()方法。这些方法使得Deque接口的功能与堆栈类似。此外,还应当注意descendingIterator()方法,该方法返回的迭代器以相反的顺序返回元素。换句话说,返回一个从集合的尾部向头部移动的迭代器。可以将Deque接口实现为容量有限的队列,这以为着只能从双端队列中添加数量有限的元素。如果数量有限,那么当试图向双端队列中添加元素时可能会失败。Deque接口允许以两种方法处理这类失败。首先,如果容量有限的双端队列已满,addFirst()和addLast()这类方法会抛出IllegalStateException异常,其次,如果不能添加元素,offerFirst()和offerLast()方法会返回false。

4 集合类

   现在已经熟悉了集合接口,下面开始分析实现他们的标准类。其中一些类提供了可以使用的完整实现。其他一些类是抽象的,它们提供了可以创建具体集合开始点的大体实现。作为一般规则,集合类不是同步的,但是可以获得它们的同步版本。表8汇总了标准集合类。

表8 标准集合类
描 述
AbstractCollection实现了Collection接口的大部分
AbstractList扩展AbstractCollection类并实现了List接口的大部分
AbstractQueue扩展AbstractCollection类并实现了Queue接口的大部分
AbstractSequentialList扩展AbstractList类,用于顺序访问而不是随机访问集合中的元素
LinkedList通过扩展AbstractSequentialList类实现链表
ArrayList通过扩展AbstractList类实现动态数组
ArrayDeque通过扩展AbstractCollection类并实现Deque接口,实现动态双端队列
AbstractSet扩展AbstractCollection类并实现Set接口的大部分
EnumSet扩展AbstractSet类,用于enum元素
HashSet扩展AbstractSet类,用于哈希表
LinkedHashSet扩展HashSet类以允许按照插入的顺序进行迭代
PriorityQueue扩展AbstractQueue类以支持基于优先级的队列
TreeSet实现存储于树种的组,扩展AbstractSet类

   接下来将分析具体集合类并演示它们的使用
   注意:
   除了集合类之外,还有一些遗留的类,例如Vector、Stack以及HashTable,也进行了重新设计以支持集合。

4.1 ArrayList类(源码分析地址:https://blog.csdn.net/qq_26803795/article/details/104559703)

   ArrayList类扩展了AbstractList类并实现了List接口。ArrayList是泛型类,其声明如下:

class ArrayList<E>

   其中,E指定了列表将包含的对象的类型。
   ArrayList类支持能够按需增长的动态数据,在Java中,标准数组的长度是固定的。数组在创建之后,就不能增长或缩小,这意味着必须事先指定数组将包含多少元素。但是,有时可能直到运行时才知道所需数组的准确大小。为了处理这种情况,集合框架定义了ArrayList类。本质上,ArrayList就是元素为对象引用的长度可变的数组。也就是说,ArrayList可以动态增加或减小大小。数组列表使用初始值大小创建。当超出这个大小时,集合会自动扩大。当对象被移除时,也可以减小数组。
   注意
   遗留类Vector也支持动态数组。
   ArrayList具有如下所示的构造函数:

ArrayList()
ArrayList(Collection<? extends E> c)
ArrayList(int capacity)

   第1个构造函数构建一个空的数组列表。第2个构造函数构建一个数组列表,使用集合c的元素进行初始化。第3个构造函数构建一个初始容量为capacity的数组列表。容量是用于存储元素的数组的大小。当向数组列表中添加元素时,容量会自动增长。
   下面的程序演示了ArrayList类的简单应用。该程序为String类型的对象创建了一个数组列表,然后向其中添加几个字符串(带引号的字符串会被转换为String对象)。然后显示这个列表。移除一些元素之后,再次显示这个列表。

import java.util.ArrayList;
//Demonstrate ArrayList
class ArrayListDemo {
 public static void main(String[] args) {
      //Create an array list.
      ArrayList<String> a1 = new ArrayList<String>();
      System.out.println("Initial size of a1: "+a1.size());
      //Add elements to the array list.
      a1.add("C");
      a1.add("A");
      a1.add("E");
      a1.add("B");
      a1.add("D");
      a1.add("F");
      a1.add(1,"A2");
      System.out.println("Size of a1 after additions: "+a1.size());
      //Display the array list
      System.out.println("Contents od a1: "+a1);
      //Remove elements from the array list
      a1.remove("F");
      a1.remove(2);
      System.out.println("Size of a1 after deletions: "+a1.size());
      System.out.println("Contents of a1: "+a1);
      /*
      输出结果:
      Initial size of a1: 0
      Size of a1 after additions: 7
      Contents od a1: [C, A2, A, E, B, D, F]
      Size of a1 after deletions: 5
      Contents of a1: [C, A2, E, B, D]
       */
  }
}

   a1一开始为空,并且当添加元素时会增长。当移除元素时,a1的大小会减小。
   在前面的例子中,使用toString()方法提供的默认转换显示集合的内容,该方法集成自AbstractCollection。尽管对于简单的程序来说,toString()方法是足够的,但是很少使用它来显示真实集合的内容。通常会提供自己的输出例程。但是,对于接下来的几个例子,toString()方法创建的默认输出是足够的。
   尽管存储对象时,ArrayList对象的容量会自动增长,但是可以调用ensureCapacity()方法以手动增长ArrayList对象的容量。如果事先知道将在集合中存储的元素比当前保存的元素多很多,我们可能希望这么做。在开始时,一次性增加容量,从而避免以后多次重新分配内存。因为重新分配内存很耗时,阻止不必要的内存分配次数可以提高性能。ensureCapacity()方法的签名如下:

void ensureCapacity(int cap)

   其中,cap指定集合新的最小容量。
   相反,如果希望减小ArrayList对象数组的大小,进而使其大小精确地等于当前容纳的元素数量,可以调用trimToSize()方法,改方法如下所示:

void trimToSize()

   从ArrayList获取数组
   使用ArrayList时,有时会希望获取包含列表内容的实际数组。可以调用toArray()方法来完成这一工作,该方法是由Collection接口定义的。由于某些原因,我们可能希望将集合转换为数组,例如:

  • 为特定操作获取更快的处理速度
  • 为方法传递数组,并且方法没有接收集合的重载形式
  • 将基于集合的代码集成到不支持集合的遗留代码中
       不管什么原因,将ArrayList转换成数组都是很平常的事情。
       前面解释过,toArray()方法有两个版本:
Object[] toArray()
<T> T[] toArray(T array[])

   第一个版本返回元素类型为Object的数组。第二个版本返回元素类型为T的数组。通常,第二个版本更方便,因为能返回恰当类型的数组,下面的程序演示了它的使用:

import java.util.ArrayList;

//Convert an ArrayList into an array.
class ArrayListToArray {
public static void main(String[] args) {
      //Create an array list.
      ArrayList<Integer> a1 = new ArrayList<Integer>();
      //Add elements to the array list.
      a1.add(1);
      a1.add(2);
      a1.add(3);
      a1.add(4);
      System.out.println("Contents of a1: "+a1);
      //Get the array
      Integer ia[] = new Integer[a1.size()];
      ia = a1.toArray(ia);
      int sum = 0;
      //Sum the array.
      for(int i:ia){
          sum += i;
      }
      System.out.println("Sum is: "+sum);
      /**
       * 輸出
       * Contents of a1: [1, 2, 3, 4]
       * Sum is: 10
       */
  }
}

   该程序首先创建一个整数集合。接下来,调用toArray()方法并获取一个Integer数组。然后,使用for-each风格的for循环对数组内容进行求和。
   集合只存储索引,而不能存储基本类型的值。但是,自动装箱使得add()方法传递int类型的值成为可能,而不需要像程序中那样将它们手动封装到一个Integer对象中。自动装箱使得它们被自动封装。通过这种方式自动装箱显著提高了存储基本类型的易用性。

4.2 LinkedList类

   LinkedList类扩展了AbstractSequentialList类,实现了List、Deque以及Queue接口,并且它还提供了一种链表数据结构。LinkedList是泛型类,其声明如下:

class LinkedList<E>

   其中,E指定了链表将包含的对象的类型。LinkedList具有两个构造函数,如下所示:

LinkedList()
LinkedList(Collection<? extends E> c)

   第一个构造函数构建一个空的链表。第二个构造函数构建一个使用集合c的元素进行初始化的链表。
   因为LinkedList实现了Deque接口,所以可以访问Deque定义的方法。例如,要向列表头部添加元素,可以使用addFirst()或offerFirst()方法;要向列表尾部添加元素,可以使用addLast()或offerLast()方法;为了获取第一个元素,可以使用getFirst()或peekFirst()方法;为了获取最后一个元素,可以使用getLast()或peekLast();为了移除第一个元素,可以使用removeFirst()或pollFirst()方法;为了移除最后一个元素,可以使用removeLast()或pollLast()方法。、
   下面的程序演示了LinkedList:

import java.util.LinkedList;
//Demonstrate LinkedList.
class LinkedListDemo {
  public static void main(String[] args) {
       //Create a linked list.
       LinkedList<String> l1 = new LinkedList<String>();
       //Add elements to the linked list.
       l1.add("F");
       l1.add("B");
       l1.add("D");
       l1.add("E");
       l1.add("C");
       l1.addLast("Z");
       l1.addFirst("A");
       l1.add(1,"A2");
       System.out.println("Original contents of l1: "+l1);
       //Remove elements from the linked list.
       l1.remove("F");
       l1.remove(2);
       System.out.println("Contents of l1 after deletion: "+l1);
       //Remove first and last elements.
       l1.removeFirst();
       l1.removeLast();
       System.out.println("l1 after deleting first and last: "+l1);
       //Get and set a value.
       String val = l1.get(2);
       l1.set(2,val+" Changed");
       System.out.println("l1 after change: "+l1);
       /**
        * 输出结果
        *Original contents of l1: [A, A2, F, B, D, E, C, Z]
        * Contents of l1 after deletion: [A, A2, D, E, C, Z]
        * l1 after deleting first and last: [A2, D, E, C]
        * l1 after change: [A2, D, E Changed, C]
        */
   }
}

   因为LinkedList实现了List接口,所以可以调用add(E)方法来向列表尾部追加条目,就像调用addLast()方法那样。为了在指定位置插入条目,可以使用add()方法的add(int,E)形式,就像在这个例子中调用add(1,“A2”)那样。
   注意如何通过调用get()和set()方法修改l1中的第三个元素。为了获取元素的当前值。向get()方法传递元素存储位置的索引。如果要为索引指定的元素赋新值,可以向set()方法传递相应的索引和新值。

4.3 HashSet类

   HashSet类扩展了AbstractSet类并实现了Set接口,该类用于创建使用哈希表存储元素的集合。HashSet是泛型类,其声明如下:

class HashSet<E>

   其中,E指定了组将包含的对象的类型。
   哈希表使用称之为三列的机制存储信息。在散列机制中,键的信息用于确定唯一的值,称为哈希码。然后将哈希码用作索引,在索引位置存储与键关联的数据。将键转换为哈希码是自动执行的——我们永远不会看到哈希码本身。此外,我们的代码不能直接索引哈希表。散列机制的优势是add()、contains()、remove()以及size()方法的执行时间保持布标,即使是对于较大的组也是如此。
   HashSet类定义了以下构造函数:

HashSet()
HashSet(Collection<? extends E> c)
HashSet(int capacity)
HashSet(int capacity,float fillRatio)

   第1种形式构造一个默认的哈希组。第2种形式使用集合c中的元素初始化哈希组。第3种形式将哈希组的容量设置为capacity(默认容量是16)。第4种形式根据参数同时初始化哈希组的容量和填充率(也称载入容量(load capacity))。填充率必须介于0.0到1.0之间,填充率决定了哈希组被填充到什么程度就增加容量。特别地,当元素的数量大于哈希组的容量与填充率的乘积时,将扩展哈希组。对于吗,没有填充率的构造函数,使用0.75作为填充率。
   除了超类和接口提供的方法外,HashSet没有定义任何其他方法。
   HashSet不能保证元素的顺序,注意着一点很重要,因为散列处理过程通常不创建有序的组。如果需要有序地进行存储,那么需要另外一个组,TreeSet是一个比较好的选择。
   下面是演示HashSet的一个例子:

import java.util.HashSet;
//Demonstrate HashSet.
class HashSetDemo {
  public static void main(String[] args) {
       //Create a hash set.
       HashSet<String> hs = new HashSet<String>();
       //Add elements to hash set.
       hs.add("Beta");
       hs.add("Alpha");
       hs.add("Eta");
       hs.add("Gamma");
       hs.add("Epsilon");
       hs.add("Omega");
       System.out.println(hs);
       /**
        * 输出:
        * [Gamma, Eta, Alpha, Epsilon, Omega, Beta]
        */
   }
}

   程序的输出正如前面所解释的,元素不是按有序地顺序存储的。

4.4 LinkedHashSet类

   LinkedHashSet扩展了HashSet类,他没有添加自己的方法。LinkedHashSet是泛型类,其声明如下:

class LinkedHashSet<E>

   其中,E指定了组将包含的对象的类型。LinkedHashSet的构造函数与HashSet的构造函数对应。
   LinkedHashSet维护组中条目的一个链表,链表中条目的数据也就是插入它们的顺序,这使得可以按照插入顺序迭代集合。换句话讲,当使用迭代器遍历LinkedHashSet时,元素将以插入它们的顺序返回。这也是在对LinkedHashSet对象调用toString()方法时,在返回的字符串中包含它们的顺序。为了查看LinkedHashSet的效果,尝试在前面的程序中使用LinkedHashSet代替HashSet,输出的是:

[Beta, Alpha, Eta, Gamma, Epsilon, Omega]

   元素输出的顺序就是插入它们的顺序。

4.5 TreeSet类

   TreeSet类扩展了AbstractSet类并实现了NavigableSet接口,用于创建使用树进行存储的组。对象以升序存储,访问和检索速度相当快,这使得对于存储大量的、必须能够快速查找到的有序信息来说,TreeSet是极佳选择。
   TreeSet是泛型类,其声明如下:

class TreeSet<E>

   其中,E指定了组将包含的对象的类型。
   TreeSet具有如下构造函数

TreeSet()
TreeSet(Collection<? Extends E> c)
TreeSet(Comparator<? super E> comp)
TreeSet(SortedSet<E> ss)

   第1中形式构建一个空树,将按照元素的自然顺序以升序进行存储。第2种形式构建一个包含集合c中元素的树。第3种形式构建一个空树,将按照comp指定的比较器进行存储。第4中形式构建一个包含ss中元素的树。
   下面的程序演示TreeSet类的一个例子:

import java.util.TreeSet;
//Demonstrate TreeSet.
class TreeSetDemo {
  public static void main(String[] args) {
       //Create a tree set.
       TreeSet<String> ts = new TreeSet<String>();
       //Add elements to the tree set.
       ts.add("C");
       ts.add("A");
       ts.add("B");
       ts.add("E");
       ts.add("F");
       ts.add("D");
       System.out.println(ts);
       /**
        * 输出
        * [A, B, C, D, E, F]
        */
   }
}

   前面解释过,因为TreeSet在树中存储元素,所以它们自动以排过序的顺序进行排序,正如输出所确认的。
   因为TreeSet实现了NavigableSet接口,所以可以使用NavigableSet接口定义的方法来检索TreeSet中的元素。例如,对于前面的程序,下面的语句使用subSet()方法获取ts的一个子组,该子组包含从C(包含C)到F(不包含F)的元素,然后显示结果组:
   System.out.println(ts.subSet(“C”,”F”));
   这条语句的输出如下所示:
    [C,D,E]

4.6 PriorityQueue类

   PriorityQueue扩展了AbstractQueue类并实现了Queue接口,用于创建根据队列的比较器来判定优先次序的队列。PriorityQueue是泛型类,其声明如下:

class PriorityQueue<E>

   其中,E指定了队列将存储的对象的类型。PriorityQueue是动态的、按需增长的。PriorityQueue定义了一下7个构造函数:

PriorityQueue()
PriorityQueue(int capacity)
PriorityQueue(Comparator<? super E> comp)(JDK8新增)
PriorityQueue(int capacity,Comparator<? super E> comp)
PriorityQueue(Collection<? extends E> c)
PriorityQueue(PriorityQueue<? extends E> c)
PriorityQueue(SortedSet<? extends E> c)

   第1个构造函数创建一个空队列,起始容量为11。第2个构造函数构建一个具有指定初始容量的队列。第3个构造函数指定了一个比较器,第4个构造函数构建具有指定容量和比较器的队列。最后3个构造函数构建使用参数c传递过来的集合中的元素进行初始化的队列。对于由这些构造函数创建的所有队列,当添加元素时,容量都会自动增长。
   当构建PriorityQueue对象时,如果没有指定比较器,将使用在队列中存储的数据类型的默认比较器。默认比较器以升序队列进行排序。因此,队列头部的条目将包含最小的值。但是,通过提供定值的比较器,可以指定不同的排序模式。例如,当对时间戳的条目进行排序时,可以优先考虑最早的条目。
   通过调用comparator()方法,可以获取对PriorityQueue使用的比较器的引用,该方法如下所示:

Comparator<? super E> comparator()

   该方法返回比较器。如果为调用队列使用的是自然顺序,那么返回null。
   需要注意的一点:尽管可以使用迭代器表里PriorityQueue,但是迭代的顺序是不确定的。为了正确地使用PriorityQueue,必须调用odder()和poll这类方法,这些方法是由Queue接口定义的。

4.7 ArrayDeque类

   ArrayDeque扩展了AbstractCollection类并实现了Deque接口,没有添加自己的方法。ArrayDeque是动态数组,没有容量限制(Deque接口支持限制容量的实现,但是这种限制不是必需的)。ArrayDeque是泛型类,其声明如下:

class ArrayDeque<E>

   其中,E指定了集合中将存储的对象的类型。
   ArrayDeuqe定义了以下构造函数:

ArrayDeque()
ArrayDeque(int size)
ArrayDeque(Collection<? extends E> c)

   第1个构造函数创建一个空的双端队列,初始容量是16。第2个构造函数构建一个具有指定初始容量的双端队列。第3个构造函数创建使用参数c传递过来的集合的元素进行初始化的双端队列。对于所有这些情况,当向双端队列中添加元素时,容量都会根据需要增长。
   下面的程序演示了ArrayDeque,这里使用它来创建一个堆栈:

import java.util.ArrayDeque;
//Demonstrate ArrayDeque
class ArrayDequeDemo {
  public static void main(String[] args) {
       //Create an array deque.
       ArrayDeque<String> adq = new ArrayDeque<String>();
       //Use an ArrayDeque like a stack.
       adq.push("A");
       adq.push("B");
       adq.push("D");
       adq.push("E");
       adq.push("F");
       System.out.println("Popping the stack: ");
       while(adq.peek()!=null){
           System.out.print(adq.pop()+" ");
       }
       System.out.println();
       /**
        * 输出:
        * Popping the stack:
        * F E D B A
        */
   }
}

4.8 EnumSet类

   EnumSet扩展了AbstractSet类并实现了Set接口,专门用于枚举类型的元素。EnumSet是泛型类,其声明如下:

class EnumSet<E extends Enum<E>>

   其中,E指定了元素。注意E必须扩展Enum<E>,这强制要求元素必须是指定的枚举类型。
   EnumSet类没有定义构造函数,而是使用表9中显示的工厂方法来创建对象。所有方法都可能抛出NullPointerException异常,copyOf()和range()方法还可能抛出IllegalArgumentException异常。注意of()方法被重载了多次,这是出于效率方面的考虑。当参数数量比较少时,传递已知数量的参数比使用数量可变的参数会更快一些。

表9 EnumSet类声明的方法
方 法描 述
static <E extends Enum<E>> EnumSet<E> allOf(Class<E> t)创建的EnumSet包含由t指定的枚举中的元素
static <E extends Enum<E> complementOf(EnumSet <E> e)>创建的EnumSet由未存储在e中的元素组成
static <E extends Enum<E> EnumSet copyOf(EnumSet<E> c)>根据c中存储的元素创建EnumSet
static <E extends Enum<E> EnumSet noneOf(Class<E> t)>创建的EnumSet不包含由t指定的枚举中的元素,根据定义,这是一个空组
static <E extends Enum<E> EnumSet of(E v,E …varargs)>创建的EnumSet包含v,以及0个或更多个其他枚举
static <E extends Enum<E> EnumSet of(E v)>创建的EnumSet包含v
static <E extends Enum<E> EnumSet of(E v1,E v2)>创建的EnumSet包含v1和v2
static <E extends Enum<E> EnumSet of(E v1,E v2,E v3)>创建的EnumSet包含v1、v2和v3
static <E extends Enum<E> EnumSet of(E v1,E v2,E v3,E v4)>创建的EnumSet包含v1、v2、v3和v4
static <E extends Enum<E> EnumSet of(E v1,E v2,E v3,E v4,E v5)>创建的EnumSet包含v1、v2、v3、v4和v5
static <E extends Enum<E> EnumSet range(E start,E end)>创建的EnumSet包含指定范围(由start和end指定)内的元素

5 通过迭代器访问集合

   通常,我们希望遍历集合中的元素。例如,可能希望显示每个元素。完成这一步工作的方法之一是使用迭代器,迭代器是实现了Iterator或ListIterator接口的对象。Iterator接口允许遍历集合,获取或删除元素。ListIterator接口扩展了Iterator接口,允许双向遍历列表,并且允许修改元素。Iterator和ListIterator是泛型接口,它们的声明如下:

interface Iterator<E>
interface ListIterator<E>

   其中,E指定了将被迭代的对象的类型。表10显示了Iterator接口声明的方法。表11显示了ListIterator接口声明的方法(以及从Iterator集成的方法)。对于这两种,修改集合的操作都是可选的。例如,当使用只读的集合时,remove()方法会抛出UnsupportedOperationException异常。其他各种异常都是可能发生的。

表10 Iterator接口声明的方法
方 法描 述
default void forEachRemaining(Consumer<? super E> action)对于集合中每个未处理的元素,执行action指定的动作(JDK8新增)
boolean hasNext()如果还有更多的元素,就返回true;否则返回false
E next()返回下一个元素。如果不存在下一个元素,就抛出NoSuchElementException异常
default void remove()移除当前元素。如果在调用next()方法之前试图调用remove()方法,就会抛出IllegalStateException异常。默认版本抛出UnsupportedOperationException异常
表11 ListIterator接口声明的方法
方 法描 述
void add(E obj)将obj插入到列表中,新插入的元素位于下一次next()方法调用返回的元素之前
default void forEachRemaining(Consumer<? super E> action)对于集合中每个未处理的元素,执行action指定的动作(JDK8新增)
boolean hasNext()如果存在下一个元素,就返回true;否则返回false
boolean hasPrevious()如果存在前一个元素,就返回true;否则返回false
E next()返回下一个元素。如果不存在下一个元素,就抛出NoSuchElementException异常
int nextIndex()返回下一个元素的索引,如果不存在下一个元素, 就返回列表的大小
E previous()返回前一个元素。如果不存在前一个元素,就抛出NoSuchElementException异常
int previousIndex()返回前一个元素的索引。如果不存在前一个元素,就返回-1
void remove()从列表中移除当前元素。如果在调用next()或previous()方法之前调用remove()方法,那么会抛出IllegalStateException异常
void set(E obj)将obj的值赋值给当前元素,也就是next()或previous()方法调用最后返回的元素

   注意
   从JDK8开始,也可以使用Spliterator循环遍历集合。Spliterator的工作方式与Iterator不同。

5.1 使用迭代器

   为了能够通过迭代器访问集合,首先必须获得迭代器。每个集合类都提供了iterator()方法,该方法返回一个指定集合开头的迭代器。通过使用这个迭代器对象,可以访问集合中的每个元素,每次访问一个元素。通常,为了使用迭代器遍历集合的内容,需要以下步骤:
   (1) 通过调用集合的iterator()方法,获取指向集合开头的迭代器
   (2) 建立一个hasNaxt()方法调用循环。只要hasNext()方法返回true,就继续迭代。
   (3) 在循环中,通过调用next()方法获取每个元素。
   对于实现了List接口的集合,还可以调用listIterator()方法以获取迭代器。如前所述,列表迭代器提供了向前和向后两个方向访问集合的能力,并且允许修改元素。除此之外,ListIterator与Iterator的用法类似。
   下面的例子实现了这些步骤,同时演示了Iterator和ListIterator接口。该例使用一个ArrayList对象,但是一般原则可以应用于任何类型的集合。当然,只有实现了List接口的集合才能使用ListIterator。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
//Demonstrate iterators.
class IteratorDemo {
 public static void main(String[] args) {
      //Create an array list
      ArrayList<String> a1 = new ArrayList<String>();
      //Add elements to the array list.
      a1.add("C");
      a1.add("A");
      a1.add("E");
      a1.add("B");
      a1.add("D");
      a1.add("F");
      //Use iterator to display contents os a1.
      System.out.print("Original contents of a1: ");
      Iterator<String> itr = a1.iterator();
      while (itr.hasNext()){
          String element = itr.next();
          System.out.print(element + " ");
      }
      System.out.println();
      //Modify objects being iterated.
      ListIterator<String> litr = a1.listIterator();
      while (litr.hasNext()){
          String element = litr.next();
          litr.set(element + "+");
      }
      System.out.print("Modified contents of a1: ");
      itr = a1.iterator();
      while (itr.hasNext()){
          String element = itr.next();
          System.out.print(element + " ");
      }
      System.out.println();
      //Now,display the list backwards
      System.out.print("Modified list backwards: ");
      while (litr.hasPrevious()){
          String element = litr.previous();
          System.out.print(element + " ");
      }
      System.out.println();
      /**
       * 输出:
       * Original contents of a1: C A E B D F
       * Modified contents of a1: C+ A+ E+ B+ D+ F+
       * Modified list backwards: F+ D+ B+ E+ A+ C+ 
       */
  }
}

   请特别注意反向显示列表的方式。在以正向显示修改过的列表之后,litr指向列表的末端(请记住,当到达列表末端时,litr.hasNext()方法返回false)。为了能反向遍历列表,程序继续使用litr,但是这次检查是否存在前一个元素。只要存在前一个元素,就获取该元素并显示。

5.2 使用for-each循环替代迭代器

   如果不修改集合内容,也不用反向获取元素,那么使用for-each风格的for循环遍历集合通常比使用迭代器更方便。请记住,可以使用for循环遍历任何实现了Iterator接口的集合对象。因为所有集合类都实现了Iterator接口,所以都可以使用for循环操作。
   下面的例子使用for循环对某个集合的元素求和:

import java.util.ArrayList;
//Use the for-each for loop to cycle through a collection.
class ForEachDemo {
  public static void main(String[] args) {
      //Create an array list for integers.
      ArrayList<Integer> vals = new ArrayList<Integer>();
      //Add values to the array list.
      vals.add(1);
      vals.add(2);
      vals.add(3);
      vals.add(4);
      vals.add(5);
      //User for loop to display the values.
      System.out.print("Contents of vals: ");
      for (int v : vals) {
          System.out.print(v + " ");
      }
      System.out.println();
      //Now,sum the values by using a for loop.
      int sum = 0;
      for (int v : vals) {
          sum += v;
      }
      System.out.println("Sum of values: " + sum);
      /**
       * 输出:
       * Contents of vals: 1 2 3 4 5
       * Sum of values: 15
       */
  }
}

   可以看出,与基于迭代器的方法相比,使用for循环更短也更简单。然而,这种方法只能向前遍历集合,并且不能修改集合的内容。

6 Spliterator

   JDK8新增了一种叫做spliterator的迭代器,这种迭代器由Spliterator接口定义。Spliterator用于循环遍历元素序列,在这一点上与刚才介绍过的迭代器类似。但是,使用spliterator的方法与使用迭代器不同。另外,它提供的功能远比Iterator或ListIterator多。可能对于Spliterator来说,最重要的一点是它支持并行迭代序列的一部分。Spliterator支持并行编程。然后,即使用不到并行编程,也可以使用Spliterator。这么做的一个理由是它将hasNext和next操作合并到了一个方法中,从而提高了效率。
   Spliterator是一个泛型接口,其声明如下所示:

interface Spliterator<T>

   其中,T是被迭代的元素的类型。Spliterator声明了表12中的方法。

表12 Spliterator接口声明的方法
方 法描 述
int characteristics()返回调用spliterator的特征,该特征被编码为整数
long estimateSize()估计剩余的要迭代的元素数,并返回结果。如果由于某种原因得不到元素数,就返回Long.Max_VALUE
defalut void forEachRemaining(Comsumer<? super T> action)将action应用到数据源中未被处理的每个元素
defualt Comparator<? super T> getComparator()返回调用spliterator使用的比较器;如果使用了自然顺序,就返回null,如果序列未被排序,就抛出IllegalStateException异常
defalut long getExactSizeIfKnow()如果调用spliterator是SIZED,就返回剩余的要迭代的元素数;否则返回false
default boolean hasCharacteristics(int val)如果val中传递了调用spliterator的特征,就返回true;否则返回false
boolean tryAdvance(Comsumer<? super T> action)在迭代中的下一个元素上执行action。如果有下一个元素就返回true;否则返回false
Spliterator<T> trySplit()如果可以,分割调用spliterator,并返回对分割后新spliterator的引用;失败时,返回false。因此,在操作成功时,原spliterator会迭代序列的一部分,返回的spliterator迭代序列的其他部分

   将Spliterator用于基本迭代任务:只需调用tryAdvance()方法,直到其返回false。如果要为序列中的每个元素应用相同的动作,forEachRemaining()提供了一种高效的替代方法。对于这两个方法,在每次迭代中将发生的动作都由Consumer对象对每个元素执行的操作定义。Consumer是一个函数式接口,向对象应用一个动作。它是java.util.function中声明的一个泛型函数式接口。Consumer仅指定了一个抽象方法accept(),如下所示:

void accept(T objRef)

   对于tryAdvance(),每次迭代会将序列中的下一个元素传递给objRef。通常,实现Consumer最简单的方式是使用lambda表达式。
   下面的程序给出了Spliterator的一个简单示例。注意,这个程序同时演示了tryAdvance()和forEachRamaining()。另外,还要注意这些方法如何把Iterator的next()和hasNext()方法的操作合并到一个调用中:

import java.util.ArrayList;
import java.util.Spliterator;
//A simple Spliterator demonstration.
class SpliteratorDemo {
  public static void main(String[] args) {
      //Create an array list for doubles.
      ArrayList<Double> vals = new ArrayList<Double>();
      //Add values to array list.
      vals.add(1.0);
      vals.add(2.0);
      vals.add(3.0);
      vals.add(4.0);
      vals.add(5.0);
      //Use tryAdvance() to display contents of vals.
      System.out.print("Contents of vals:\n");
      Spliterator<Double> spltitr = vals.spliterator();
      while (spltitr.tryAdvance((n) -> System.out.println(n))) ;
      System.out.println();
      //Create new list that contains square roots.
      spltitr = vals.spliterator();
      ArrayList<Double> sqrs = new ArrayList<Double>();
      while (spltitr.tryAdvance((n)->sqrs.add(Math.sqrt(n))));
      //Use forEachRemaining() to display contents of sqrs.
      System.out.print("Contents of sqrs:\n");
      spltitr = sqrs.spliterator();
      spltitr.forEachRemaining((n)-> System.out.println(n));
      /**
       * 输出
       * Contents of vals:
       * 1.0
       * 2.0
       * 3.0
       * 4.0
       * 5.0
       *
       * Contents of sqrs:
       * 1.0
       * 1.4142135623730951
       * 1.7320508075688772
       * 2.0
       * 2.23606797749979
       */
  }
}

   虽然这个程序展示了Spliterator的原理,却没有展现其强大的能力。如前所述,在涉及并行处理的地方,Spliterator的最大优势才能体现出来。
   在表12中,注意characteristics()和hasCharacteristics()方法。每个Spliterator对象都关联着一组叫做特征(characteristics)的特性。这些特性由Spliterator中的静态int域变量定义,如SORTED、DISTINCT、SIZED和IMMUTABLE等。通过调用characteristics(),可以获取这些特征。通过调用hasCharacteristics(),可以确定调用Spliterator对象是否具有某个特征,一般不需要访问Spliterator的特征,但是有些时候,它们能够帮助创建高效的、健壮的代码。
   Spliterator中包含几个嵌套子接口,它们是针对基本类型double、int和long设计的,分别叫做Spliterator.ofDouble、Spliterator.OfInt和Spliterator.OfLong。还有一个一般化的版本,叫做Spliterator.OfPrimitive(),它提供了更大的灵活性,并作为上述接口的超接口。

7 在集合中存储用户定义的类

   为了简化,前面的例子在集合中存储的是内置对象,例如String或Integer。当然,没有限制集合只能存储内置对象。事实完全相反,集合可以存储任何类型的对象,包括创建的类对象。例如,分析下面的例子,该例使用LinkedList存储邮件地址:

import com.sun.jna.WString;
//A simple mailing list example.
class Address {
    private String name;
    private String street;
    private String city;
    private String state;
    private String code;

    public Address(String name, String street, String city, String state, String code) {
        this.name = name;
        this.street = street;
        this.city = city;
        this.state = state;
        this.code = code;
    }
    @Override
    public String toString() {
        return name + "\n" + street + "\n" + city + " " + state + " " + code;
    }
}
import java.util.LinkedList;
class MailList {
 public static void main(String[] args) {
      LinkedList<Address> m1 = new LinkedList<Address>();
      //Add elements to the linked list.
      m1.add(new Address("J.W. West", "11 Oak Ave", "Urbana", "IL", "61801"));
      m1.add(new Address("Ralph Baker", "1142 Maple Lane", "Mahomet", "IL", "61853"));
      m1.add(new Address("Tom Carlton", "867 Elm St", "Champaign", "IL", "61820"));
      //Display the mailing list.
      for (Address element : m1) {
          System.out.println(element + "\n");
      }
      /**
       * 输出:
       * J.W. West
       * 11 Oak Ave
       * Urbana IL 61801
       *
       * Ralph Baker
       * 1142 Maple Lane
       * Mahomet IL 61853
       *
       * Tom Carlton
       * 867 Elm St
       * Champaign IL 61820
       */
  }
}

   除了在集合中存储用户定义的类之外,对于前面的程序,需要重点注意的另外一点是:这个程序相当短。当考虑到程序只是有大约50行代码,就构建了一个能够存储、检索以及处理邮件地址的链表时,集合框架的功能便开始显现出来了。如果所有这些功能都必须手动编写的话,程序会长好几倍。集合为大量编程问题提供了现成的解决方案。只要存在现成的解决方案,就应当使用它们。

8 RandomAccess接口

   RandomAccess接口不包含成员。然而,通过实现这个接口,可表明支持高效地随机访问其中的元素。尽管集合可能支持随机访问,但是可能没有如此高效。通过检查RandomAccess接口,客户端可以在运行时确定集合是否适合特定类型的随机访问操作——特别是当将它们应用于大的集合时(可以使用instanceof来判定是否实现了某个接口)。ArrayList和遗留的Vector类实现了RandomAccess接口。

9 使用映射

   映射是存储键和值之间关联关系(键值对)的对象。给定一个键,就可以找到对应的值。键和值都是对象。键必须唯一,但是值可以重复。某些映射可以接受null键和null值,其他映射则不能。
   关于映射需要关注的关键一点是:它们没有实现Iterable接口。这意味着不能使用for-each风格的for循环遍历映射。此外,不能为映射获取迭代器。但是,正如即将看到的,可以获取映射的集合视图,集合视图允许使用for循环和迭代器。

9.1 映射接口

   因为映射接口定义了映射的特性和本质,所以首先从它们开始讨论映射。表13中的接口支持映射。

表13 支持映射的接口
接 口描 述
Map将唯一键映射到值
Map.Entry描述映射中的元素(键/值对),这是Map的内部类
NavigableMap扩展SortedMap接口,以处理基于最接近匹配原则的键/值对检索
SortedMap扩展Map接口,从而以升序保存键

   接下来依次介绍这些接口。
   1.Map接口
   Map接口将唯一键映射到值。键是以后用于检索值的对象。给定键和值,可以在Map对象中存储值;存储值以后,可以使用相应的键检索值。Map是泛型接口,其声明如下:

interface Map<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   表14总结了Map接口定义的方法。当对象与映射中的元素不兼容时,有些方法会抛出ClassCastException异常;如果试图修改不允许修改的映射,会抛出UnsupportedOperationException异常;如果使用的参数非法,会抛出IllegalArgumentException异常。

表14 Map接口声明的方法
方 法描 述
void clear()移除调用映射中的所有键/值对
default V compute(K k,BiFunction<? super K,? super V,? extends V> func)调用func以构造一个新值。如果func的返回值不是null,就把新的键值对放到映射中,移除原来的配对,并返回新值。如果func返回null,就移除原来的配对,并返回null(JDK8新增)
default V computeIfAbsent(K k,Function<? super K,? extends V> func)返回与k关联的值。如果没有值,就通过调用func构造一个值,并把该配对输入到映射中,返回构造的值,如果无法构造新值,就返回null(JDK8 新增)
default V computeIfPresent(K k,BiFunction<? super K,? super V,? extends V> func)如果k包含在映射中,就通过调用func为其构造一个新值,替换映射中原来的值,然后返回新值。如果func返回的值为null,就从映射中删除现有的键和值,并返回null(JDK8新增)
boolean containsKey(Object k)如果调用映射包含k作为键,就返回true,否则返回false
boolean containsValue(Object v)如果映射包含v作为值,就返回true;否则返回false
Set<Map.Entry<K,V>> entrySet()返回包含映射中所有条目的Set对象,这个组包含Map.Entry类型的对象。因此,改方法提供了调用映射的一个组视图
boolean equals(Object obj)如果obj是Map对象并且与调用映射包含相同的条目,就返回true;否则返回false
default void forEach(BiConsumer<? super K,? super V> action)对调用映射中的每个元素执行action。如果在操作过程中移除了元素,会抛出ConcurrencyModificationException异常(JDK8新增)
V get(Object K)返回与键k关联的值。如果没有找到键,就返回null
default V getOrDefault(Object k,V defVal)如果映射中包含与k关联的值,就返回该值;否则,返回defVal(JDK8新增)
int hashCode()返回调用映射的散列码
boolean isEmpty()如果调用映射为空,就返回true;否则返回false
Set<K> keyset()返回包含映射中某些键的Set对象。这个方法提供了调用映射中键的一个组视图
default V merge(K k,V v,BiFunction<? super K,? super V,? extends V> func)如果k没有包含在映射中,就把k和v配对,添加到映射中,并返回v。否则,func基于原有的值返回一个新值,键被更新为使用这个新值,并且merge()方法返回这个值。如果func返回的值为null,就从映射中删除现有的键和值,并返回null(JDK8新值)
V put(K k,V v)将一个条目放入调用映射中,覆盖之前与此键关联的值。键和值分别为k和v。如果键不存在就返回null,否则返回之前与键关联的值
void putAll(Map<? extends K,? extends V> m)将m中的所有条目添加到调用映射中
default V putIfAbsent(K k,V v)如果此键值对没有包含在调用映射中,或者现有的值为null,就将此配对添加到调用映射中,并返回原来的值。如果之前不存在映射,或者值为null,就返回null(JDK8新增)
V remove(Object k)移除键等于k的条目
default boolean remove(Object k,Object v)如果k和v指定的键值对包含在调用映射中,就移除该配对,并返回true,否则返回false(JDK8新增)
default boolean replace(K k,V oldV,V newV)如果k和oldV指定的键值对包含在调用映射中,就用newV替换oldV,并返回true。否则,返回false(JDK8新增)
default void replaceAll(BiFunction<? super K,? super V,?extends V func>)对调用映射的每个元素执行func,用func返回的结果替换元素。如果在操作过程中删除了元素,会抛出ConcurrentModificationException异常(JDK8新增)
int size()返回映射中键/值对的数量
Collection<V> values()返回包含映射中所有值的集合。改方法提供了调用映射中的一个集合视图

   映射围绕两个基本方法:get()和put()。为了将值放入映射中,使用put()方法,指定键和值。为了获取值,调用get()方法,传递键作为参数,值会被返回。
   前面提到过,尽管映射是结合框架的一部分,但映射不是集合,因为没有实现Collection接口。但是,可以获得映射的集合视图。为此,可以使用entrySet()方法。该方法返回一个包含映射中元素的Set对象。为了获取键的集合视图,使用keySet()方法;为了得到值的集合视图,使用values()方法。对于这3个集合视图,集合都是基于映射的。修改其中的一个集合会影响其他集合。集合视图是将映射集成到更大的集合框架中手段。
   2.SortedMap接口
   SortedMap接口扩展了Map接口,确保条目以键的升序保存。SortedMap是泛型接口,其声明如下:

interface SortedMap<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   表15总结了SortedMap接口声明的方法。如果在调用映射中没有元素,有些方法会抛出ClassCastException异常;如果对象与映射中的元素不兼容,会抛出ClassCastException异常;如果试图为不允许使用null对象的映射使用null对象,会抛出NullPointerException异常;如果使用的参数无效,会抛出IllegalArgumentException异常。

表15 SortedMap接口声明的方法
方 法描 述
Comparator<? super K> comparator()返回调用的有序映射的比较器。如果调用映射使用的是自然排序,就返回null
K firstKey()返回调用映射中的第一个键
SortedMap<K,V> headMap(K end)返回由键小于end的那些映射条目组成的有序映射
K lastKey()返回调用映射中的最后一个键
SortedMap<K,V> subMap返回由键大于或等于start,但小于end的那些条目组成的有序映射
SortedMap<K,V> tailMap(K start)返回由键大于或等于start的那些条目组成的有序映射

   有序映射支持非常高的子映射(即映射的子集)操作。为了获取子映射,可以使用headMap()、tailMap()或subMap()方法。这些方法返回的子映射是基于调用映射的。修改其中的一个集合会影响其他集合。为获得组中的第一个键,可调用firstKey()方法;为获得最后一个键,可调用lastKey()方法。
   3.NavigableMap接口
   NavigableMap接口扩展了SortedMap接口,支持基于最接近匹配原则的条目检索行为,即支持检索与给定的一个或多个键最想匹配的条目。NavigableMap是泛型接口,其声明如下:

interface NavigableMap<K,V>

   其中,K指定了键的类型,V指定了与键关联的值的类型。除了继承自SortedMap接口的方法外,NavigableMap接口还添加了表16中总结的方法。如果对象与映射中的键不兼容,有些方法会抛出ClassCastException异常;如果试图对不允许使用null对象和null键的集合使用它们,会抛出NullPointerException异常;如果使用的参数无效,会抛出IllegalArgumentException异常。

表16 NavigableMap接口声明的方法
方 法描 述
Map.Entry<K,V> ceillingEntry(K obj)搜索映射,查找大于等于obj的最小键。如果找到这样的键就返回与之对应的条目;否则返回null
K ceillingKey(K obj)搜索映射,查找大于等于obj的最小建。如果找到这样的键,就返回该键;否则返回null
NavigableSet<K> descendingKeySet()返回的NavigableSet组以逆序形式包含调用映射中的所有键。因此,该方法返回键的逆序组视图。结果基于映射
NavigableMap<K,V> descendingMap()返回的NavigableMap映射是调用映射的逆序映射。结果映射基于调用映射
Map,Entry<K,V> firstEntry()返回映射中的第一个条目,也就是具有最小键的条目
Map.Entry<K,V> floorEntry(K obj)搜索映射,查找小于等于obj的最大键。如果找到这样的键,就返回与之对应的条目,否则返回null
K floorKey(K obj)搜索映射,查找小于obj的最大键。如果找到这样的键,就返回该键;否则返回null
NavigableMap<K,V> headMap(K upperBound,boolean incl)返回的NavigableMap映射包含调用映射中,键小于upperBound的所有条目。如果incl为true,那么包含键等于upperBound的那个元素。结果映射基于调用映射
Map.Entry<K,V> higherKey(K obj)搜索组,查找大于obj的最小建。如果找到这样键,就返回该键;否则返回null
Map.Entry<K,V> lastEntry()返回映射中的最后一个条目,也就是具有最大键的条目
Map.Entry<K,V> lowerEntry(K obj)搜索组,查找小于
obj的最大键。如果找到这样的键,就返回与之对应的条目;否则返回null
K lowerKey(K obj)搜索组,查找小于obj的最大键。如果找到这样的键,就返回该键;否则返回null
NavigableSet<K> navigableKeySet()返回包含调用映射中所有键的NavigableSet组,结果组基于调用映射
Map.Entry<K,V> pollFirstEntry()返回第一个条目,在这个过程中移除该条目。因为映射是排过序的,所以也就是具有最小键值的条目。如果映射为空,就返回null
Map.Entry<K,V> pollLastEntry()返回最后一个条目,在这个过程中移除该条目。因为映射是排过序的,所以也就是具有最大键值的条目。如果映射为空,就返回null
NavigableMap<K,V> subMap(K lowerBound,boolean lowIncl,K upperBound,boolean highIncl)返回的NavigableMap映射包含调用映射中、键大于lowerBound且小于upperBound的所有条目。如果lowIncl为true,那么包含键等于lowerBound的那个元素;如果highIncl为true,那么包含键等于upperBound的那个元素。结果映射等于调用映射
NavigableMap<K,V> tailMap(K lowerBound,boolean incl)返回的NavigableMap映射包含调用映射中、键大于lowerBound的所有条目。如果incl为true,那么包含键等于lowerBound的那个元素。结果映射基于调用映射

   4.Map.Entry接口
   Map.Entry接口提供了操作映射条目的功能。请记住,Map接口声明的entrySet()方法返回一个包含映射条目的Set对象。组的所有元素都是Map.Entry对象。Map.Entry是泛型接口,其声明如下:

interface Map.Entry<K,V>

   其中,K指定了键的类型,V指定了值的类型。表17总结了Map.Entry接口声明的非静态方法。JDK8为其添加了两个静态方法:comparingByKey()和comparingByValue()。前者返回根据键比较条目的Comparator对象,后者返回根据值比较条目的Comparator对象。

表17 Map.Entry接口声明的方法
方 法描 述
boolean equals(Object obj)如果obj是一个键和值都与调用对象相等的Map.Entry,就返回true
K getKey()返回该映射条目的键
V getValue()返回该映射条目的值
int hashCode()返回该映射条目的散列码
V setValue(V v)将这个映射条目的值设置为v。如果对于该映射v不是正确的类型就抛出ClassCastException异常;如果v存在问题,那么就抛出IllegalArgumentException异常;如果v为null但映射不允许null键,那么抛出NullPointerException异常;如果映射不允许修改,那么抛出UnsupportedOperationException异常

9.2 映射类

   有几个类实现了映射接口,表18总结了可以用于映射的类。

表18 实现了映射接口的类
描述
AbstractMap实现了Map接口的大部分
EnumMap扩展了AbstractMap,以使用enum键
HashMap扩展了AbstractMap,以使用哈希表
TreeMap扩展了AbstractMap,以使用树结构
WeakHashMap扩展了AbstractMap,以使用带有弱键的哈希表
LinkedHashMap扩展了HashMap,以允许按照插入顺序进行迭代
IdentityHashMap扩展了AbstractMap,并且当比较文档时使用引用相等性

   注意AbstractMap是所有具体映射实现的超类。
   WeakHashMap实现了使用“弱键”的映射,对于这种映射中的元素来说,键在不再使用时可以被垃圾回收。
   1.HashMap类
   HashMap扩展了AbstractMap类并实现了Map接口。它使用哈希表存储映射,这使得即使对于比较大的集合,get()和put方法的执行时间也保持不变。HashMap是泛型类,其声明如下:

class HashMap<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   HashMap类定义了以下构造函数:

HashMap()
HashMap(Map<? extends K,? extends V> m)
HashMap(int capacity)
HashMap(int capacity,float fillRatio)

   第1种形式构造了一个默认的哈希映射。第2种形式使用m中的元素构造初始化哈希映射。第3种形式将哈希映射的容量初始化capacity。第4中形式使用参数同时初始化容量和填充率。容量和填充率的含义与前面描述的HashSet相同。默认容量是16,默认填充率是0.75。
   HashMap实现了Map接口并扩展了AbstractMap类,但没有添加任何自己的方法。
   应当注意哈希映射不保证元素的顺序。所以,向哈希映射添加元素顺序不一定是通过迭代器读取它们的顺序。
   下面的程序演示了HashMap,将名字映射到账户金额。注意获取和使用组视图的方式:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
class HashMapDemo {
 public static void main(String[] args) {
      //Create a hash map.
      HashMap<String, Double> hm = new HashMap<String, Double>();
      //Put elements to the map
      hm.put("John Doe", new Double(3434.34));
      hm.put("Tom Smith", new Double(123.22));
      hm.put("Jane Baker", new Double(1378.00));
      hm.put("Tod Hall", new Double(99.22));
      hm.put("Ralph Smith", new Double(-19.08));
      //Get a set of entries.
      Set<Map.Entry<String, Double>> set = hm.entrySet();
      //Display the set.
      for (Map.Entry<String, Double> me : set) {
          System.out.print(me.getKey() + ": ");
          System.out.println(me.getValue());
      }
      System.out.println();
      //Deposit 1000 into John Doe's account.
      double balance = hm.get("John Doe");
      hm.put("John Doe", balance + 1000);
      System.out.println("John Doe's new balance: " + hm.get("John Doe"));
      /**
       * 执行结果(精确的顺序可能会有所变化):
       * Tod Hall: 99.22
       * John Doe: 3434.34
       * Ralph Smith: -19.08
       * Tom Smith: 123.22
       * Jane Baker: 1378.0
       *
       * John Doe's new balance: 4434.34
       */
  }
}

   该程序首先创建一个哈希映射,然后添加从名字到账户余额的映射。接下来,调用entrySet()方法以获取试图组,并使用视图组显示映射的内容。通过调用Map.Entry定义的getKey()和getValue()方法显示键和值的内容。请密切注意存款放入John Doe账户中的方式。put()方法使用新值自动替换之前与特定键关联的所有值。因此,在更新了John Doe账户后,哈希表映射仍然值包含"John Doe"账户。
   2.TreeMap类
   TreeMap扩展了AbstractMap类并实现了NavigableMap接口,该类用于创建存储在树结构中的映射。TreeMap提供了有序存储键/值对的高效手段,并支持快速检索。应当注意,与哈希映射不同,树映射确保元素以键升序存储。TreeMap是泛型类,其声明如下:

class TreeMap<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   TreeMap类定义了以下构造函数:

TreeMap()
TreeMap(Comparator<? super K> comp)
TreeMap(Map<? extends K,? extends V> m)
TreeMap(SortedMap<K,? extends V> sm)

   第1种形式构造一个空的树映射,使用键的自然顺序进行存储。第2种形式构造一个空的基于树的映射,使用比较器comp进行排序。第3种形式使用m中的条目初始化树映射,使用键的自然顺序进行排序。第4中形式使用sm中的条目初始化树映射,使用与sm相同的顺序进行排序。
   TreeMap类的映射方法没有超出NavigableMap接口和AbstractMap类定义的那些方法。
   下面的程序对前面的程序进行了修改,以使用TreeMap类:

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
class TreeMapDemo {
  public static void main(String[] args) {
      //Create a tree map.
      TreeMap<String, Double> hm = new TreeMap<String, Double>();
      //Put elements to the map
      hm.put("John Doe", new Double(3434.34));
      hm.put("Tom Smith", new Double(123.22));
      hm.put("Jane Baker", new Double(1378.00));
      hm.put("Tod Hall", new Double(99.22));
      hm.put("Ralph Smith", new Double(-19.08));
      //Get a set of entries.
      Set<Map.Entry<String, Double>> set = hm.entrySet();
      //Display the elements.
      for (Map.Entry<String, Double> me : set) {
          System.out.print(me.getKey() + ": ");
          System.out.println(me.getValue());
      }
      System.out.println();
      //Deposit 1000 into John Doe's account.
      double balance = hm.get("John Doe");
      hm.put("John Doe", balance + 1000);
      System.out.println("John Doe's new balance: " + hm.get("John Doe"));
      /**
       * 执行结果
       * Jane Baker: 1378.0
       * John Doe: 3434.34
       * Ralph Smith: -19.08
       * Tod Hall: 99.22
       * Tom Smith: 123.22
       *
       * John Doe's new balance: 4434.34
       */
  }
}

   注意,TreeMap对键进行排序。然而在这个例子中,它们根据名(first name)而不是根据姓(last name)进行排序。但是,在创建映射时可以通过制定比较器来改变行为。
   3.LinkedHashMap类
   LinkedHashMap扩展了HashMap类,在映射中以插入条目的顺序维护一个条目链表,从而可以按照插入顺序迭代整个映射。
   也就是说,当遍历LinkedHashMap的集合视图时,将元素的插入顺序返回元素。也可以按照最后访问的顺序返回元素的LinkedHashMap。LinkedHashMap是泛型类,其声明如下:

class LinkedHashMap<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   LinkedHashMap定义了以下构造函数:

LinkedHashMap()
LinkedHashMap(Map<? extends K,? extends V> m)
LinkedHashMap(int capacity)
LinkedHashMap(int capacity,float fillRatio)
LinkedHashMap(int capacity,float fillRatio,boolean Order)

   第1种形式构造默认的LinkedHashMap。第2种形式使用m中的元素初始化LinkedHashMap。第3种形式初始化容量。第4种形式同时初始化容量和填充率。容量和填充率的含义与HashMap相同。默认容量为16,默认填充率为0.75。最后一种形式允许指定是按照插入顺序还是按照最后访问的顺序存储元素。如果Order为true,就使用访问顺序;如果Order问false,就使用插入顺序。
   除了继承自HashMap定义的那些方法,LinkedHashMap只添加了一个方法。这个方法是removeEldestEntry(),如下所示:

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

   这个方法由put()和putAll()调用。最旧的条目是由e传入的。默认情况下,这个方法返回false,并且不执行任何操作。但是,如果重写这个方法,就可以使LinkedHashMap移除映射中最旧的条目。为此,重写版本需要返回true。要保留最旧的条目,可以返回false。
   4.IdentityHashMap类
   IdentityHashMap扩展了AbstractMap类并实现了Map接口。除了当比较元素时使用引用相等性之外,其他方面与HashMap类似。IdentityHashMap是泛型类,其声明如下:

class IdentityHashMap<K,V>

   其中,K指定了键的类型,V指定了值的类型。API文档明确指出IdentityHashMap不用于通用目的。
   5.EnumHashMap类
   EnumHashMap扩展了AbstractMap类并实现了Map接口,是专门为了使用枚举类型的键而设计的。EnumMap是泛型类,其声明如下:

class EnumMap<K extends Enum<K>,V>

   其中,K指定了键的类型,V指定了值的类型。注意K必须扩展Enum<K>,从而强制满徐键必须是枚举类型这一需求。
   EnumMap定义了以下构造函数:

EnumMap(Class<K> kType)
EnumMap(Map<K,? extends V> m)
EnumMap(EnumMap<K,? extends V> em)

   第1种形式创建一个类型为kType的空EnumMap。第2种形式创建一个包含m中相同条目的EnumMap。第3种形式创建一个使用em中的值进行初始化的EnumMap。
   EnumMap类没有定义自己的方法。

10 比较器

   TreeSet和TreeMap类以有序顺序存储元素。然而,是比较器精确定义了“有序顺序”的含义。默认情况下,这些类使用Java称为“自然顺序”的方式对元素进行排序,自然顺序通常是我们所期望的顺序(A在B之前,1在2之前,等等)。如果希望以不同的方式排序元素,可以在构造组或映射时指定比较器,这样就可以精确控制在有序集合和映射中存储元素的方式。
   Comparator是泛型接口,其声明如下:

interface Comparator<T>

   其中,T指定了将要进行比较的对象的类型。
   在JDK8之前,Comparator接口只定义了两个方法:compare()和equals()。compare()方法如下所示,用于比较两个元素以进行排序:

int compare(T obj1,T obj2)

   obj1和obj2是要进行比较的对象。正常情况下,如果对象相等,该方法返回0;如果obj1大于obj2,返回一个正直,反之返回一个负值。如果要进行比较的对象的类型不兼容,该方法会抛出ClassCastException异常。通过实现compare()方法,可以改变对象的排序方式。例如,为了按照相反的顺序进行排序,可以创建比较器以翻转比较结果。
   equals()方法如下所示,用于测试某个对象是否等于比较器:

boolean equals(Object obj)

   其中,obj是将要进行相等测试的对象。如果obj和调用对象都是比较器,并且使用相同的排序规则,那么该方法返回true;否则返回false。不必重写equals()方法,并且大多数简单的比较器都不重写该方法。
   多年来,上述两个方法是仅有的Comparator定义的方法,然而JDK8改变了这种情况。通过使用默认接口方法和静态方法,JDK8为Comparator添加了许多新功能。下面逐一进行介绍:

default Comparator<T> reversed()

   该方法返回颠倒后的比较器。例如,假设一个比较器为字母A~Z使用自然顺序,在颠倒其顺序后的比较器中,B将出现在A的前面,C将出现在B的前面,依此类推。
   reverseOrder()是reverse()关联的一个方法,如下所示:

static <T extends Comparable<? super T>> Comparator<T> naturalOrder()

   如果希望比较器能够处理null值,需要使用下面的nullsFirst()和nullsLast()方法。

static <T> Comparator<T> nullsFirst(Comparator<? super T> comp)
static <T> Comparator<T> nullsLast(Comparator<? super T> comp)

   nullsFirst()方法返回的比较器认为null比其他值小,nullsLast()方法返回的比较器认为null比其他值大。对于这两个方法,如果被比较的两个值都是非
null值,则comp执行比较。如果comp传递null,则认为所有非null值都是相等的。
   JDK8添加的另外一个默认方法是thenComparing()。该方法返回一个比较器,当第一次比较的结果指出被比较的对象相等是,返回的这个比较器将执行第二次比较。因此,可以使用该方法创建一个"根据X比较,然后根据Y比较"的序列。例如,当比较城市时,第一次可能比较城市名,第二次则比较州名。因此,如果使用字母顺序,Illiois州的SpringField将出现Missouri州的SpringField的前面。thenComparing()方法有三种形式。第一种形式如下所示:

default Comparator<T> thenComparing(Comparator<? super T> themByComp)

   其中,themByComp指定在第一次比较返回相等后调用的比较器。
   thenByComparing()的另外两个版本允许指定标准函数式接口Function(由java.util.function定义),如下所示:

default <U extends Comparable<? super U> Comparator<T> thenComparing(Function<? super T,? extends U> getKey)
default <U> Comparator<T> thenComparing(Function<? super T,? extends U> getKey,Comparator<? super U> keyComp)

   在这两个版本中,getKey引用的函数用于获得下一个比较简键,当第一次比较返回相等后,将使用这个比较键。后一个版本中keyComp指定了用于比较键的比较器(在这里以后后面的使用中,U指定了键的类型)。
   Comparator还为基本类型添加了以下专用版本的“then comparing”方法:

default Comparator<T> themComparingDouble(ToDoubleFunction<? super T> getKey)
default Comparator<T> thenComparingInt(ToIntFunction<? super T> getKey)
default Comparator<T> thenComparingLong(ToLongFunction<? super T> getKey)

   在所有这些方法中,getKey引用的函数用于获取下一个比较键。
   最后,JDK8在Comparator中添加了comparing()方法。该方法返回的比较器从传递该方法的函数中获得比较键。comparing()方法有两个版本,如下所示:

static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> getKey)
static <T,U> Comparator<T> comparing(Function<? super T,? extends U> getKey,Comparator<? super U> keyComp)

   在这两个版本中,getKey引用的函数用于获得下一个比较键。在第二个版本中,keyComp指定了用于比较键的比较器。Comparator还为基本类型添加了如下所示的专用版本的方法:

static <T> Comparator<T> ComparingDouble(ToDoubleFunction<? super T> getKey)
static <T> Comparator<T> ComparingInt(ToIntFunction<? super T> getKey)
static <T> Comparator<T> ComparingLong(ToLongFunction<? super T> getKey)

   在所有这些方法中,getKey引用的函数用于获得下一个比较键。
使用比较器
   下面是一个演示自定义比较器功能的例子。该例为字符串实现了compare()方法,以与正常顺序相反的顺序进行操作。因此,这将导致树组(tree set)以相反的顺序进行存储。

//Use a custom comparator.
import java.util.Comparator;
//A reverse comparator for strings.
class MyComp implements Comparator<String> {
    @Override
    public int compare(String aStr, String bStr) {
        //Reverse the comparison
        return bStr.compareTo(aStr);
    }
    //No need to override equals or the default methods.
}
import java.util.TreeSet;
class CompDemo {
  public static void main(String[] args) {
       //Create a tree set.
       TreeSet<String> ts = new TreeSet<String>(new MyComp());
       //Add elements to the tree set.
       ts.add("C");
       ts.add("A");
       ts.add("B");
       ts.add("E");
       ts.add("F");
       ts.add("D");
       //Display the elements
       for (String element:ts){
           System.out.print(element+" ");
       }
       System.out.println();
       /**
        * 输出结果:
        * F E D C B A
        * 树现在以相反的顺序进行存储
        */
   }
}

   下面详细分析MyComp类,该类通过实现compare()方法实现了Comparator接口(如前所示,重写equals()方法既不是必须得,也不常见。对于JDK8新增的默认方法,也不是必须重写它们)。在compare()方法内部,使用String的compareTo()方法比较两个字符串。然而,是使用bStr——而不是aStr——调用compareTo()方法,这导致比较的输出结果被翻转。
   尽管上面的程序实现逆序比较器的方法完全可以接受,但是从JDK8开始,还有另外一种方法可以获得解决方案。即,现在可以简单地对自然顺序比较器调用reversed()方法。该方法返回一个等价的比较器,不过比较器的顺序是相反的,在前面的程序中,可以把MyComp重写为自然顺序比较器,如下所示:

class MyComp implements Comparator<String> {
    @Override
    public int compare(String aStr, String bStr) {
        return aStr.compareTo(bStr);
    }
}

   然后,可以使用下面的代码创建一个反向排序字符串元素的TreeSet:

MyComp mc = new MyComp();//Create a comparator
//Pass a reverse order version of MyComp to TreeSet.
TreeSet<String>ts = new TreeSet<String>(mc.reversed());

   如果把这段代码放到前面的程序中,得到的几个与原来是一样的。在这个例子中,使用reversed()方法能够方便地获得逆序比较器,而不需要显式对其编码。
   从JDK8开始,在前面的例子中,实际上没有必要创建MyComp类,因为可以很方便地使用lambda表达式作为替代。例如,可以彻底删除MyComp类,使用下面的代码创建字符串比较器:

//Use a lambda expression to implement Comparator<String>.
Comparator<String> mc = (aStr,bStr)->aStr.compareTo(bStr)

   另外还有一点:在这个简单的示例中,通过使用lambda表达式,可以在对TreeSet()构造函数的调用中直接指定逆序比较器,如下所示:

//Pass a reversed comparator to TreeSet() via a lambda expression.
TreeSet<String> ts = new TreeSet<String>((aStr,bStr)->bStr.compareTo(aStr));

   做了上述修改后,程序得以显著缩短。下面显示了最终的版本:

import java.util.TreeSet;
//Use a lambda expression to create a reverse comparator.
class CompDemo2 {
  public static void main(String[] args) {
       //Pass a reverse comparator to TreeSet() via a lambda expression.
       TreeSet<String> ts = new TreeSet<String>((aStr, bStr) -> bStr.compareTo(aStr));
       //Add elements to the tree set.
       ts.add("C");
       ts.add("A");
       ts.add("B");
       ts.add("E");
       ts.add("F");
       ts.add("D");
       //Display the elements.
       for (String element : ts) {
           System.out.print(element + " ");
       }
       /**
        * 输出:
        * F E D C B A
        */
   }
}

   作为使用自定义比较器的更实际的例子,下面的程序是前面显示的存储账户金额的TreeSet程序的升级版。在前面的版本中,账户根据名字进行存储,但排序是从名开始的。下面的程序根据姓对账户进行排序。为此,使用比较器比较每个账户的姓,这会使得映射根据姓进行排序。

//Use a comparator to sort accounts by last name.
import java.util.Comparator;
//Compare last whole words in two strings.
class TComp implements Comparator<String> {
  @Override
   public int compare(String aStr, String bStr) {
       int i,j,k;
       //Find index of beginning of last name.
       i = aStr.lastIndexOf(' ');
       j = bStr.lastIndexOf(' ');
       k = aStr.substring(i).compareToIgnoreCase(bStr.substring(j));
       if(k==0){
           //last name match,check entire name
           return aStr.compareToIgnoreCase(bStr);
       }else{
           return k;
       }
       //No need to override equals.
   }
}
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
class TreeMapDemo2 {
 public static void main(String[] args) {
      //Create a tree map.
      TreeMap<String, Double> tm = new TreeMap<String, Double>(new TComp());
      //Put elements to the map.
      tm.put("John Doe", new Double(3434.34));
      tm.put("Tom Smith", new Double(123.22));
      tm.put("Jane Baker", new Double(1378.00));
      tm.put("Tod Hall", new Double(99.22));
      tm.put("Ralph Smith", new Double(-19.08));
      //Get a set of the entries.
      Set<Map.Entry<String, Double>> set = tm.entrySet();
      //Display the elements.
      for (Map.Entry<String, Double> me : set) {
          System.out.print(me.getKey() + ": ");
          System.out.println(me.getValue());
      }
      System.out.println();
      //Deposit 1000 into Jhon Doe's account.
      double balance = tm.get("John Doe");
      tm.put("John Doe", balance + 1000);
      System.out.println("John Doe's new balance: " + tm.get("John Doe"));
      /**
       * 输出,现在账户根据姓进行排序
       * Jane Baker: 1378.0
       * John Doe: 3434.34
       * Tod Hall: 99.22
       * Ralph Smith: -19.08
       * Tom Smith: 123.22
       *
       * John Doe's new balance: 4434.34
       */
  }
}

   比较器类TComp比较包含名和姓的两个字符串。该类首先比较姓。为此,查找每个字符串最后一个空格的索引,然后比较每个元素从该索引位置开始字符串。如果姓相同,就比较名。这使得树映射根据姓进行排序,并且如果姓相同的话,再根据名进行排序。从输出中可以看到这一点,因为Ralph Smith在Tom Smith之前。
   如果使用的是JDK8或更高的版本,还有另外一种方法来编码前面的程序,让映射首先根据姓进行排序,然后根据名进行排序。这会用到thenComparing()方法。thenComparing()允许指定第二个比较器,当调用比较器返回相等时,就会使用第二个比较器。下面的程序演示了这种方法,它修改了前面的程序,使之使用thenComparing()方法:

//Use thenComparing() to sort by last,then first name.
import java.util.Comparator;
//A comparator that compares last names.
class CompLastNames implements Comparator<String> {
    @Override
    public int compare(String aStr, String bStr) {
        int i,j;
        //Find index of beginning of last name.
        i = aStr.lastIndexOf(' ');
        j = bStr.lastIndexOf(' ');
        return aStr.substring(i).compareToIgnoreCase(bStr.substring(j));
    }
}
import java.util.Comparator;
//Sort by entire name when last names are equals.
class CompThenByFirstName implements Comparator<String> {
    @Override
    public int compare(String aStr, String bStr) {
       return aStr.compareToIgnoreCase(bStr);
    }
}
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
class TreeMapDemo2A {
public static void main(String[] args) {
      //Use thenComparing() to create a comparator that compares
      //last names,then compares entire name when last names match.
      CompLastNames compLN = new CompLastNames();
      Comparator<String> compLastThenFirst = compLN.thenComparing(new CompThenByFirstName());
      //Create a tree map.
      TreeMap<String, Double> tm = new TreeMap<String, Double>(compLastThenFirst);
      //Put elements to the map.
      tm.put("John Doe", new Double(3434.34));
      tm.put("Tom Smith", new Double(123.22));
      tm.put("Jane Baker", new Double(1378.00));
      tm.put("Tod Hall", new Double(99.22));
      tm.put("Ralph Smith", new Double(-19.08));
      //Get a set of the entires.
      Set<Map.Entry<String, Double>> set = tm.entrySet();
      //Display the elements.
      for (Map.Entry<String, Double> me : set) {
          System.out.print(me.getKey() + ": ");
          System.out.println(me.getValue());
      }
      System.out.println();
      //Reposit 1000 into John Doe's account.
      double balance = tm.get("John Doe");
      tm.put("John Doe",balance+1000);
      System.out.println("John Doe's new balance: "+tm.get("John Doe"));
      /**
       * 输出:
       * Jane Baker: 1378.0
       * John Doe: 3434.34
       * Tod Hall: 99.22
       * Ralph Smith: -19.08
       * Tom Smith: 123.22
       *
       * John Doe's new balance: 4434.34
       */
  }
}

   这个版本产生的输入与原来的版本相同,二者唯一的区别在于完成工作的方式。首先,注意这里创建了一个叫做CompLastNames的比较器,它只比较姓。第二个比较器CompThenByFirstName比较,名。接下来,使用下面的代码创建了TreeMap:

CompLastNames compLN = new CompLastNames();
Comparator<String> compLastThenFirst = compLN.themComparing(new CompThenByFirstName());

   这里的主比较器是compLN,它是CompLastNames的一个实例。对该实例调用thenComparing()方法,并传入CompThenByFirstName的一个实例。结果被赋值给名为compLastThenfFrst的比较器。这个比较器用于构造TreeMap,如下所示:

TreeMap<String,Double> tm = new TreeMap<String,Double>(compLastThenFirst);

   现在,每当比较的姓相同时,就会比较名,以便进行排序。这意味着首先根据姓对姓名进行排序,如果姓相同的话,再根据名进行排序。
   最后,本例显示创建了两个比较器类CompLastNames和ComThenByFirstName,但是其实可以使用lambda表达式。

import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
class TreeMapDemo2A2 {
  public static void main(String[] args) {
      Comparator<String> comp1 = (String aStr,String bStr)-> aStr.substring(aStr.lastIndexOf(' ')).compareToIgnoreCase(bStr.substring(bStr.lastIndexOf(' ')));
      Comparator<String> comp2=comp1.thenComparing((aStr,bStr)->aStr.compareToIgnoreCase(bStr));
      //Create a tree map.
      TreeMap<String, Double> tm = new TreeMap<String, Double>(comp2);
      //Put elements to the map.
      tm.put("John Doe", new Double(3434.34));
      tm.put("Tom Smith", new Double(123.22));
      tm.put("Jane Baker", new Double(1378.00));
      tm.put("Tod Hall", new Double(99.22));
      tm.put("Ralph Smith", new Double(-19.08));
      //Get a set of the entires.
      Set<Map.Entry<String, Double>> set = tm.entrySet();
      //Display the elements.
      for (Map.Entry<String, Double> me : set) {
          System.out.print(me.getKey() + ": ");
          System.out.println(me.getValue());
      }
      System.out.println();
      //Reposit 1000 into John Doe's account.
      double balance = tm.get("John Doe");
      tm.put("John Doe",balance+1000);
      System.out.println("John Doe's new balance: "+tm.get("John Doe"));
      /**
       * 输出:
       * Jane Baker: 1378.0
       * John Doe: 3434.34
       * Tod Hall: 99.22
       * Ralph Smith: -19.08
       * Tom Smith: 123.22
       *
       * John Doe's new balance: 4434.34
       */
  }
}

11 集合算法

   集合框架定义了一些可以应用于集合和映射的集合算法,这些算法被定义为Collections类中的静态方法,表19汇总了这些方法。如前所述,从JDK5开始,Java对所有这些方法都进行了重写以使用泛型。

表19 Collections类定义的方法
方 法描 述
static<T> boolean addAll(Collection<? super T> c,T… elements)将由elements指定的元素插入由c指定的集合中。如果元素被插入c中,就返回true;否则返回false
static<T> Queue<T> asLifoQueue(Deque<T> c)返回c的一个后进先出的视图
static<T> int binarySearch(List<? extends T> list,T value,Comparator<? super T> c)根据c以确定的顺序在list中查找value。返回value在list中为位置,如果没有找到,就返回一个负值
static<T> int binarySearch(List<? extends Comparable<? super T>>list,T value)在list中查找value,列表必须是排过序的。返回value在list中的位置,如果没有找到value,就返回一个负值
static<E> List<E> checkedMap(Map<K,V> c,Class<K> keyT,Class<V> valueT)返回Map的一个运行时类型安全视图。如果试图插入不兼容的元素,那么会导致ClassCastException异常
static<K,V> NavigableMap<K,V> checkedNavigableMap(NavigableMap<K,V> nm,Class<E> keyT,Class<V> valueT)返回NavigableMap的一个运行时类型安全视图。如果试图插入不兼容的元素,那么会导致ClassCastException异常(JDK8新增)
static<E> Queue<E> checkedQueue(Queue<E> q,Class<E> t)返回Queue的一个运行时类型安全视图。如果试图插入不兼容的元素,那么会导致ClassCastException异常(JDK8新增)
static<E> List<E> checkedSet(Set<E> c,Class<E> t)返回Set的一个运行时类型安全视图。如果试图插入不兼容的元素。那么会导致ClassCastException异常
static<K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K,V> c,Class<K> keyT,Class<V> valueT)返回SortedMap的一个运行时类型安全视图。如果试图插入不兼容的元素,那么会导致ClassCastException异常
static<E> SortedSet<E> checkedSortedSet(SortedSet<E> c,Class<E> t)返回SortedSet的一个运行时类型安全视图。如果试图插入不兼容的元素,那么会导致ClassCastException异常。
static<T> void copy(List<? super T> list1,List<? extends T> list2)将list2中的元素复制到list1中
static boolen disjoint(Collection<?> a,Collection<?> b)比较a和b中的元素,如果两个集合不包含公共元素,就返回true;否则返回false
static<E> Enumeration<T> emptyEnumeration()返回一个空的枚举,即没有元素的迭代器
static<T> Iterator<T> emptyIterator()返回一个空的迭代器,即没有元素的迭代器
static<T> List<T> emptyList()返回一个推断类型的、不可变得空Map对象
static<T> ListIterator<T> emptyListIterator()返回一个空的列表迭代器,即没有元素的列表迭代器
static<K,V> Map<K,V> emptyMap返回一个推断类型的、不可变得空Map对象
static<K,V> NavigableMap<K,V> emptyNavigableMap()返回一个推断类型的、不可变的空NavigableMap对象(JDK8新增)
static<E> NavigableSet<E> emptyNavigableSet()返回一个推断类型的、不可变的空NavigableSet对象(JDK8新增)
static<T> Set<T> emptySet()返回一个推断类型的、不可变的空Set对象
static<K,V> SortedMap<K,V> emptySortedMap()返回一个推断类型的、不可变的空SortedMap对象(JDK8新增)
static<E> SortedSet<E> emptySortedSet()返回一个推断类型的、不可变得空SortedSet对象(JDK8新增)
static<T> Enumeration<T> enumeration(Collection<T> c)返回一个基于集合c的枚举
static<T> void fill(List<? super T> list,T obj)将obj赋给list中的每个元素
static int frequency(Collection<?> c,Object obj)计算obj在集合c中出现的次数并返回结果
static int indexOfSubList(List<?> list,List<?> subList)查找list中subList首次出现的位置。返回首次匹配时的索引位置。如果没有找到匹配,就返回-1
static int lastIndexOfSubList(List<?> list,List<?> subList)查找list中subList最后一次出现的位置。返回最后匹配时的位置索引。如果没有找到匹配,就返回-1
static<T> ArrayList<T> list(Enumeration<T> enum)返回一个包含enum中元素的ArrayList
static<T> T max(Collection<? extends T> c,Comparator<? super T> comp)返回集合c中由comp决定的最大元素
static<T extends object & Comparable<? super T>> T max(Collection<? extends T> c)返回几个c中由自然顺序决定的最大元素,集合不必是排过序的
static<T> T min(Collection<? extends T> c,Comparator<? super T> comp)返回集合c中由comp决定的最小元素,集合不必是排过序的
static<T extends object & Comparable<? super T>> T min(Collection<? extends T> c)返回集合c中自然顺序决定的最小元素
static<T> List<T> nCopies(int num,T obj)返回包含在一个不可变列表中的obj的num个副本,num必须大于等于0
static<E> Set<E> newSetFromMap(Map<E,Boolean> m)创建并返回由m指定的映射所支持的一个集合。调用该方法时,映射必须为空
static<T> boolean replaceAll<List<T> list,T old,T new>将list中的所有old替换为new。如果至少发生了一次替换就返回true;否则返回false
static void reverse(List list)颠倒list中元素的顺序
static<T> Comparator<T> reverseOrder(Comparator<T> comp)返回一个逆序比较器,基于传递到comp中的比较器,也就是说,返回的比较器会调度comp比较的结果
static<T> Comparator<T> reversedOrder()返回一个逆序比较器,也就是颠倒两个元素比较结果的比较器
static void rotate(List<T> list,int n)将list向右旋转n个位置。要向左旋转,可使用n的负值
static void shuffle(List<T> list,Random r)使用r作为随机数的来源,搅乱(即随机化)list中的元素
static void shuffle(List<T> list)搅乱(即随机化)list中的元素
static <T> Set<T> singleton(T obj)将obj作为不可变得组返回,这是将单个对象转换成组的一种简单方式
static<T> List<T> singletonList(T obj)将obj作为不可变的列表返回,这是将单个对象转换成列表的一种简单方式
static<K,V> Map<K,V> singletonMap(K k,V v)将键/值对k/v作为不可变的映射返回,这是将单个键/值对转换成映射的一种简单方式
static<T> void sort(List<T> list,Comparator<? super T> comp)按照由comp决定的顺序对list中的元素进行排序
static <T extends Comparable<? super T>> void sort(List<T> list)按照自然顺序对list中的元素进行排序
static void swap(List<?> list,int idx1,int idx2)交换list中由idx1和idx2指定的索引位置的元素
static<T> Collection<T> synchronizedCollection(Collection<T> c)返回基于集合c的线程安全的集合
static<T> List<T> synchronizedList(List<T> list)返回基于list的线程安全的列表
static<K,V> Map<K,V> synchoronizedMap(Map<K,V> m)返回基于m的线程安全的映射
static<K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> nm)返回基于nm的同步的NavigableMap对象(JDK8新增)
static<T> NavigableSet<T> synchronizedNavigableSet(Navigable<T> ns)返回基于ns的同步NavigableSet对象(JDK8新增)
static<T> Set<T> synchronizedSet(Set<T> s)返回基于s的线程安全的组
static<K,V> SortedMap<K,V> synchronizedSortedSet(SortedSet<T> ss)返回基于ss的线程安全的有序组
static<T> Collection<T> unmodifiableCollection(Collection<? extends T> c)返回基于c的不可修改的列表
static<K,V> Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m)返回基于m的不可修改的映射
static<K,V> NavigableMap<K,V> unmodifiableNavigableMap(NavigableMap<K,? extends V nm>)返回基于nm的不可修改的NavigableMap对象(JDK8新增)
static<T> NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> ns)返回基于ns的不可修改的NavigableSet对象
static<T> Set<T> unmodifiableSet(Set<? extends T> s)返回基于s的不可修改的组
static<K,V> SortedMap<K,V> unmodifiableSortedMap(SortedMap<K,?extends V> sm)返回基于sm的不可修改的有序映射
static<T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> ss)返回基于ss的不可修改的有序组

   当试图比较不兼容的类型时,有些方法会抛出ClassCastException异常;当试图修改不可修改的集合时,有些方法会抛出UnSupportedOperationException。根据具体的方法不同,还可能抛出其他异常。
   需要特别注意的是checked方法系列,例如checkedCollection(),这类方法返回API文档中所说的集合的"动态类型安全视图"。这种视图是对集合的引用,用于在运行时监视向集合中进行插入操作的类型兼容性。如果试图插入不兼容的元素,会导致ClassCastException异常。在调试期间使用这种视图特别有用,因为可以确保集合总是包含有效的元素。相关方法包括checkedSet()、checkedList()、checkedMap()等。它们为指定的集合获取类型安全的视图。
   注意有些方法用于获取各种集合的同步(线程安全的)副本,例如synchronizedList()和synchronizedSet()。作为一般规则,标准集合实现不是同步的。必须使用同步算法提供同步。另一点:同时集合的迭代器必须在synchronized代码块中使用。
   以unmodifiable开始的一组方法集返回各种不能修改的集合视图。如果希望确保对集合进行某些读取操作——但是不允许写入操作,这些方法是有用的。
   Collections定义了3个静态变量:EMPTY_SET、EMPTY_LIST和EMPTY_MAP。所有这3个变量都是不可改变得。
   下面的程序演示了一些算法,创建并初始化了一个链表。reverseOrder()方法返回翻转Integer对象比较结果的比较器。列表元素根据这个比较器进行排序,然后显示。接下来,调用shuffle()方法以随机化链表,然后显示链表中的最小值和最大值。

//Demonstrate various algorithms
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
class AlgorithmsDemo {
public static void main(String[] args) {
      //Create an initialize linked list.
      LinkedList<Integer> ll = new LinkedList<Integer>();
      ll.add(-8);
      ll.add(20);
      ll.add(-20);
      ll.add(8);
      //Create a reverse order comparator.
      Comparator<Integer> r = Collections.reverseOrder();
      //Sort list by using the comparator.
      Collections.sort(ll, r);
      System.out.print("List sorted in reverse: ");
      for (int i : ll) {
          System.out.print(i + " ");
      }
      System.out.println();
      //Shuffle list.
      Collections.shuffle(ll);
      //Display randomized list.
      System.out.print("List shuffle: ");
      for (int i : ll) {
          System.out.print(i + " ");
      }
      System.out.println();
      System.out.println("Minimum: " + Collections.min(ll));
      System.out.println("Maxinum: " + Collections.max(ll));
      /**
       * 输出:
       * List sorted in reverse: 20 8 -8 -20
       * List shuffle: 20 -20 8 -8
       * Minimum: -20
       * Maxinum: 20
       */
  }
}

   注意是在随机化之后,才对链表进行min()和max()操作的。这;两个方法的操作都不要求列表是排过序的。

12 Arrays类

   Arrays类提供了对数组操作有用的方法,这些方法有助于连接集合和数组。本节将解释Arrays类定义的每个方法。
   asList()方法返回基于指定数组的列表。换句话说,列表和数组引用相同的位置。asList()方法的签名如下所示:

static <T> List asList(T... array) 

   其中,array是包含数据的数组。
   binarySearch()方法使用二分搜索法查找特定数组。只能为排过序的数组应用这个方法,下面是这个方法的几种形式(附加形式允许查找子范围):

static int binarySearch(byte array[],byte value)
static int binarySearch(char array[],char value)
static int binarySearch(double array[],double value)
static int binarySearch(float array[],float value)
static int binarySearch(int array[],int value)
static int binarySearch(long array[],long value)
static int binarySearch(short array[],short value)
static int binarySearch(Object array[],Object value)
static <T> int binarySearch(T[] array,T value,Comparator<? super T> c)

   其中,array是将要进行搜索的数组,value是被定位的值。如果数组包含的元素不能进行比较(例如Double和StringBuffer),或者如果value和array中的类型不兼容,那么后面两种形式会抛出ClassCastException异常。在最后一种形式中,比较器c用于确定array中元素的顺序。对于所有这些形式,如果value存在于array中,就返回元素的索引;否则返回一个负值。
   copyOf()方法返回数组的副本,具体有一下几种形式:

static boolean[] copyOf(boolean[] source,int len)
static byte[] copyOf(byte[] source,int len)
static char[] copyOf(char[] source,int len)
static double copyOf(double[] source,int len)
static float copyOf(float[] source,int len)
static int[] copyOf(int[] source,int len)
static long[] copyOf(long[] source,int len)
static short[] copyOf(short[] source,int len)
static <T> T[] copyOf(T[] source,int lem)
static<T,U> T[] copyOf(U[] source,int len,Class<? extends T[]> resultT)

   原始数组是由source指定的,副本数组的长度是由len指定的。如果副本数组的长度大于source,就是用0(对于数值数组)、null(对于对象数组)或false(对于布尔数组)填充。如果副本数组的长度小于
source,那么副本数组会被截断。对于最后一种形式,resultT的类型变成了返回的数组的类型。如果len是负值,那么抛出NegativeArraySizeExcpetion异常;如果source是null,那么抛出NullPointerException异常;如果resultT与source的类型不兼容,那么抛出ArrayStoreException异常。
   copyOfRange()方法返回数组中某个范围的副本,具有以下形式:

static boolean[] copyOfRange(boolean[] source,int start,int end)
static byte[] copyOfRange(byte[] source,int start,int end)
static char[] copyOfRange(char[] source,int start,int end)
static double[] copyOfRange(double[] source,int start,int end)
static float[] copyOfRange(float[] source,int start,int end)
static int[] copyOfRange(int[] source,int start,int end)
static long[] copyOfRange(long[] source,int start,int end)
static short[] copyOfRange(short[] source,int start,int end)
static <T> T[] copyOfRange(T[] source,int start,int end)
static <T,U> T[] copyOfRange(U[] source,int start,int end,Class<? extends T[]> resultT)

   原始数组是由source指定的。副本数组的范围是通过start和end传递的索引指定的。范围从start到end-1。如果长于source,那么副本数组使用0(对于数值数组)、null(对于对象数组)或false(对于布尔数组)填充。对于最后一种形式,resultT的类型变成了返回的数组的类型。如果start是负值或着大于source的长度,那么抛出ArrayIndexOutOfBoundsException异常;如果start大于end,那么抛出IllegalArgumentException异常;如果source是null,那么抛出NullPointerException异常;如果resultT与source的类型不兼容,那么抛出ArrayStoreException异常。
   如果两个数组相等,那么equals()方法返回true;否则返回false。equals()方法具有以下形式:

static boolean equals(boolean array1[],boolean array2[])
static boolean equals(byte array1[],byte array2[])
static boolean equals(char array1[],char array2[])
static boolean equals(double array1[],double array2[])
static boolean equals(float array1[],float array2[])
static boolean equals(int array1[],int array2[])
static boolean equals(long array1[],long array2[])
static boolean equals(short array1[],short array2[])
static boolean equals(Object array1[],Object array2[])

   其中,array1和array2是进行相等性比较的两个数组。
   deepEquals()方法用于比较两个可能包含嵌套数组的数组是否相等,其声明如下所示:

static boolean deepEquals(Object[] a,Object[] b)

   如果a和b中传递的数组包含相同的元素,该方法返回true;如果a和b包含嵌套的数组,那么还会检查这些嵌套数组的内容;如果数组或任何嵌套数组的内容不同,那么返回false。
   fill()方法可以将某个值赋给数组中的所有元素。换句话说,使用指定的值填充数组。fill()方法有两个版本。第一个版本具有以下形式,该版本填充整个数组:

static void fill(boolean array[],boolean value)
static void fill(byte array[],boolean value)
static void fill(char array[],boolean value)
static void fill(double array[],boolean value)
static void fill(float array[],boolean value)
static void fill(int array[],boolean value)
static void fill(long array[],boolean value)
static void fill(short array[],boolean value)
static void fill(Object array[],Object value)

   在此,将value赋给array中的所有元素。fill()方法的第二个版本将值赋给数组的子集。
   sort()方法用于对数组进行排序,从而使其中的元素以升序进行排序。sort()方法有两个版本,第一个版本,如下所示,对整个数组进行排序:

static void sort(byte array[])
static void sort(char array[])
static void sort(double array[])
static void sort(float array[])
static void sort(int array[])
static void sort(long array[])
static void sort(short array[])
static void sort(Object array[])
static <T> void sort(T array[],Comparator<? super T> c)

   其中,array是进行排序的数组。对于最后一种形式,c是用于确定array元素顺序的比较器。对于最后两种形式,如果进行排序的数组的元素不能惊醒比较,那么可能抛出ClassCastException异常。第二种版本的sort()方法允许指定希望进行排序的数组的范围。
   JDK8为Arrays类添加了一些新方法,其中最重要的可能是parallelSort()方法,因为该方法按升序对数组的各部分进行并行排序,然后合并结果,这种方法可以显著加快排序时间。类似于sort()方法,parallelSort()方法有两种版本,每种形式都被重载多次。第一种版本排序整个数组,如下所示:

static void parallelSort(byte array[])
static void parallelSort(char array[])
static void parallelSort(double array[])
static void parallelSort(float array[])
static void parallelSort(int array[])
static void parallelSort(long array[])
static void parallelSort(short array[])
static <T extends Comparable<? super T>> void parallelSort(T array[])
static <T> void parallelSort(T array[],Comparator<? super T> c)

   这里,array是进行排序的数组。对于最后一种形式,c是用于确定array元素顺序的比较器。对于最后两种形式,如果进行排序的数组不能进行比较,那么可能抛出ClassCastException异常。第二种版本的parallelSort()方法允许指定希望进行排序的数组的范围。
   通过包含spliterator()方法,JDK8为Arrays类添加了对spliterator的支持。该方法有两种基本形式。第一种版本返回整个数组的spliterator,如下所示:

static Spliterator.ofDouble spliterator(double array[])
static Spliterator.ofInt spliterator(int array[])
static Spliterator.ofLong spliterator(long array[])
static <T> Spliterator spliterator(T array[])

   这里,array是spliterator将循环遍历的数组。第二种版本的spliterator()方法允许指定希望进行迭代的数组的范围。
   从JDK8开始,通过包含stream()方法,Arrays类支持新的Stream接口。stream()方法有两种版本,下面显示了第一种版本:

static DoubleStream stream(double array[])
static IntStream stream(int array[])
static LongStream(long array[])
static <T> Stream stream(T array[])

   这里,array是流将引用的数组。第二种版本的stream()方法允许指定数组内的一个范围。
   除了刚才讨论的方法,JDK8还添加了另外3个新方法。其中两个彼此相关:setAll()和parallelSetAll()。这两个方法都为所有元素赋值,但是parallelSetAll()以并行方式工作。这两个方法的声明如下所示:

static void setAll(double array[],IntToDoubleFunction<? extends T> genVal)
static void parallelSetAll(double array[],IntToDoubleFunction<? extends T> genVal)

   这两个方法都有一些重载形式,用于处理int、long和泛型。
   最后,JDK8还为Arrays类添加了一个有意思的方法:parallelPrefix()。该方法对数组进行修改,是每个元素都包含对其前面的所有元素应用某个操作的累积结果。例如,如果操作是乘法,那么在返回时,数组元素将包含原始值的累积乘积。parallelPrefix()有几种重载形式,下面是其中的一种:

static void parallelPrefix(double array[],DoubleBinaryOperator func)

   这里,array是被操作的数组,func指定应用的操作(DoubleBinaryOperator是java.util.function中定义的一个函数式接口)。还有另外几种版本,包括操作int、long和泛型的版本,以及允许指定希望操作的数组范围的版本。
   Arrays类还为每种类型的数组提供了toString()和hashCode()方法。此外,还提供了deepToString()和deepHashCode()方法,这两个方法可以有效操作包含嵌套数组的数组。
   下面的程序演示了如何使用Arrays类的一些方法:

//Demonstrate Arrays
import java.util.Arrays;
class ArraysDemo {
  public static void main(String[] args) {
       //Allocate and initialize array.
       int array[] = new int[10];
       for (int i = 0; i < 10; i++) {
           array[i] = -3 * i;
       }
       //Display,sort and display the array.
       System.out.print("Original content: ");
       display(array);
       Arrays.sort(array);
       System.out.print("Sorted: ");
       display(array);
       //Fill and display the array.
       Arrays.fill(array,2,6,-1);
       System.out.print("After fill(): ");
       display(array);
       //Sort and display the array.
       Arrays.sort(array);
       System.out.print("After sorting again: ");
       display(array);
       //Binary search for -9
       System.out.print("The value -9 is at location ");
       int index = Arrays.binarySearch(array,-9);
       System.out.println(index);
       /**
        * 输出:
        Original content: 0 -3 -6 -9 -12 -15 -18 -21 -24 -27
        Sorted: -27 -24 -21 -18 -15 -12 -9 -6 -3 0
        After fill(): -27 -24 -1 -1 -1 -1 -9 -6 -3 0
        After sorting again: -27 -24 -9 -6 -3 -1 -1 -1 -1 0
        The value -9 is at location 2         */
   }

   static void display(int array[]) {
       for (int i : array) {
           System.out.print(i + " ");
       }
       System.out.println();
   }
}

13 遗留的类和接口

   早期版本的java.util包没有包含集合框架,而是定义了几个类和一个接口,用来提供存储对象的专业方法。当添加集合时(由J2SE1.2添加),对几个原始类进行了重新设计以支持集合接口。因此从技术上讲,它们是集合框架的组成部分。然而,当现在集合的功能与遗留类的功能相同时,通常会希望使用更新的一些集合类。一般来说,仍然支持遗留类是因为有些代码仍然需要它们。
   另外一点:在上面介绍的所有现代集合类都不是同步的,但是所有遗留类都是同步的。在有些情况下,这一差别可能很重要。当然,通过使用Collections提供的算法,可以很容易地同步集合。
   java.util定义的遗留类如下所示:
   Dictionary    HashTable    Properties    Stack    Vector

13.1 Enumeration接口

   使用Enumeration接口定义的方法可以枚举(每次获取一个)对象集合中的元素。这个遗留接口已经被Iterator接口取代。尽管不反对使用,但是对于新代码,Enumeration接口被认为是过时的。然而,遗留类(例如Vector和Properties)定义的一些方法使用该接口,其他一些API类也使用该接口。因为该接口仍然使用,所有JDK5对它进行了重修以使用泛型。Enumeration接口的声明如下:

interface Enumeration<E>

   在此,E指定了将被枚举的元素的类型。
   Enumeration接口定义了以下两个方法:

boolean hasMoreElements()
E nextElement()

   当实现该接口时,如果仍然有更多的元素要提取,那么hasMoreElements()方法必须返回true;如果已经枚举了所有元素,那么返回false。nextElement()方法返回枚举中的下一个元素。也就是说,每次调用nextElement()方法都会获取枚举中的下一个元素。当枚举完所有的元素时,该方法会抛出NoSuchElementException异常。

13.2 Vector类

   Vector实现了动态数组,与ArrayList类似,但是有两个区别:Vector是同步的,并且包含许多遗留方法,这些方法与集合框架定义的方法具有相同的功能。随着集合的出现,对Vector进行了重新设计以扩展AbstractList类并实现List接口。随着JDK5的发布,对Vector进行了重修以使用泛型,并进行了重新设计以实现Iterable接口。这意味着Vector与集合完全兼容,并且可以通过使用增强的for循环来迭代Vector的内容。
   Vector类的声明如下所示:

class Vector<E>

   其中,E指定了将要存储的元素的类型。
   下面是Vector类的构造函数:

Vector()
Vector(int size)
Vector(int size,int incr)
Vector(Collection<? extends E> c)

   第1种形式创建默认的向量,初始大小为10。第2中形式创建的向量,初始大小由size指定。第3种形式创建的向量,由size指定,并且增量由incr指定。增量指定了每次增加向量时分配的元素数量。第4种形式创建包含集合c中元素的向量。
   所有向量都有初始容量。在达到这个初始容量后,下一次试图在向量中存储对象时,向量会自动为对象分配空间,外加用以保存更多对象的额外空间。通过分配比所需空间更多的空间,向量减少了当增长时必须分配空间的次数。减少分配空间的次数很重要,因为分配空间就时间而言是很昂贵的。在每次重新分配空间时,分配的额外空间的数量是由创建向量时指定的增量决定了。如果没有指定增量,每次分配空间时都会是向量的大小翻倍。
   Vector类定义了以下受保护的数据成员:

int capacityIncrement;
int elementCount;
Object[] elementData;

   增量值保存在capacityIncrement中,向量中当前元素数量保存在elementCount中,保存向量的数组存储在elementData中。
   除了List定义的集合方法外,Vector还定义了一些遗留方法,表20对这些方法进行了汇总。

表20 Vector类定义的遗留方法
方 法描 述
void addElement(E element)将element指定的对象添加到向量中
int capacity()返回向量的容量
Object clone()返回调用向量的副本
boolean contains(Object element)如果向量中包含element,就返回true;否则返回false
void copyInto(Object array[])将调用向量中包含的元素复制到由array指定的数组中
E elementAt(int index)返回位于index位置的元素
Enumeration<E> elements()返回向量中元素的一个枚举
void ensureCapacity(int size)将向量的最小容量设置为size
E firstElement()返回向量的第一个元素
int indexOf(Object element)返回element首次出现的位置的索引。如果对象不在向量中,就返回-1
int indexOf(Object element,int start)返回element在start位置及之后第一次出现的位置的索引。如果对象不在向量的这一部分中,就返回-1
void insertElementAt(E element,int index)将元素element添加到向量中,插入位置由index指定
boolean isEmpty()如果向量为空,就返回true;如果向量包含一个或多个元素,就返回false
E lastElement()返回向量中的最后一个元素
int lastIndexOf(Object element)返回element最后一次出现的位置的索引。如果对象不在向量中,就返回-1
int lastIndexOf(Object element,int start)返回element在start位置之前最后一次出现的位置的索引。如果对象不在向量的这一部分中,就返回-1
void removeAllElements()情况向量。执行该方法后,向量的大小为0
boolean removeElement(Object element)从向量中移除element.如果向量中存在指定对象的对个实例,那么只移除第一个。如果移除成功,就返回true;如果没有找到对象,就返回false
void removeElementAt(int index)移除位于index指定位置的元素
void selElementAt(E element,int index)将index指定的位置设置为element
void setSize(int size)将向量中元素的数量设置为size,如果新的长度小于原来的长度,元素将丢失;如果新的长度大于原来的长度,将添加null元素
int size()返回当前向量中元素的个数
String toString()返回向量的等价字符串
void trimToSize()将向量的容量设置为与当前拥有的元素个数相同

   因为Vector类实现了List接口,所以可以像使用ArrayList实例那样使用向量,也可以使用Vector类的遗留方法操作向量。例如,在实例化Vector后,可以调用addElement()方法来添加元素;为了获取特定位置的元素,可以调用elementAt()方法;为了获取向量中的第一个元素,可以使用firstElement()方法;为了检索最后一个元素可以调用lastElement()或removeElementAt()方法。
   下面的程序使用向量存储各种数值类型的对象。该程序演示了Vector定义的几个遗留方法,另外还演示了Enumeration接口。

import java.util.Enumeration;
import java.util.Vector;
//Demonstrate various Vector operations.
class VectorDemo {
  public static void main(String[] args) {
      //Initial size is 3,increment is 2
      Vector<Integer> v = new Vector<Integer>(3,2);
      System.out.println("Initial size: "+v.size());
      System.out.println("Initial capacity: "+v.capacity());
      v.addElement(1);
      v.addElement(2);
      v.addElement(3);
      v.addElement(4);
      System.out.println("Capacity after our additions: "+v.capacity());
      v.addElement(5);
      System.out.println("Current capacity: "+v.capacity());
      v.addElement(6);
      v.addElement(7);
      System.out.println("Current capacity: "+v.capacity());
      v.addElement(9);
      v.addElement(10);
      System.out.println("Current capacity: "+v.capacity());
      v.addElement(11);
      v.addElement(12);
      System.out.println("First Element: "+v.firstElement());
      System.out.println("Last Element: "+v.lastElement());
      if(v.contains(3)){
          System.out.println("Vector contains 3.");
      }
      //Enumerate the elements in the vector
      Enumeration<Integer> vEnum = v.elements();
      System.out.println("\nElements in vector:");
      while(vEnum.hasMoreElements()){
          System.out.print(vEnum.nextElement()+" ");
      }
      /**
       * 输出结果:
       * Initial size: 0
       * Initial capacity: 3
       * Capacity after our additions: 5
       * Current capacity: 5
       * Current capacity: 7
       * Current capacity: 9
       * First Element: 1
       * Last Element: 12
       * Vector contains 3.
       *
       * Elements in vector:
       * 1 2 3 4 5 6 7 9 10 11 12
       */
  }
}

   不是依赖枚举遍历对象(向上面程序那样),而是使用迭代器。例如,可以使用下面基于迭代器的代码替换程序中的相应代码:

//Use an iterator to display contents.
Iterator<Integer> vItr = v.iterator();
System.out.println("\nElements in vector:");
while(vItr.hasNext()){
    System.out.print(vItr.next()+" ");
}

   还可以使用for-each风格的for循环遍历Vector,如前面的代码的已下版本所示:

//Use an enhanced for loop to display contents.
System.out.println("\nElements in vector:")
for(int i:v){
  System.out.print(i+" ");
}

   因为对于新代码不推荐使用Enumeration接口,所以通常会使用迭代器或for-each风格的for循环枚举向量的内容。当然,仍然存在许多使用Enumeration接口的遗留代码。幸运的是,枚举和迭代的工作方式几乎相同。

13.3 Stack类

   Stack是Vector的子类,实现了标准的后进先出的堆栈。Stack只定义了默认构造函数,用于创建空的堆栈,随着JDK5的发布,Java对Stack类进行了整修以使用泛型,其声明如下所示:

class Stack<E>

   其中,E指定了在堆栈中存储的元素的类型。
   Stack类包含Vector类定义的所有方法,并添加了一些自己的方法,表21中显示了这些方法。

表21 Stack类定义方法
方 法描 述
boolean empty()如果堆栈为空,就返回true;如果堆栈包含元素,就返回false
E peek()返回堆栈顶部的元素,但是不移除
E pop()返回堆栈顶部的元素,并在这个过程中移除
E push(E element)将element压入堆栈,element也被返回
int search(Object element)在堆栈中查找element。如果找到,返回这个元素到堆栈顶部的偏移值;否则返回-1

   为了将对象放入堆栈的顶部,可以调用push()方法;为了移除并返回顶部的元素,可以调用pop()方法;可以使用peek()方法返回但不移除顶部的元素;调用pop()或peek()方法时如果调用堆栈为空,那么会抛出EmptyStackException异常;如果在堆栈中没有内容,empty()方法将返回true;search()方法确定对象是否存在于堆栈中,并返回将对象压入堆栈顶部所需要的的弹出次数。下面的例子创建了一个堆栈,向其中压入一些Integer对象,然后将它们弹出:

//Demonstrate the Stack class.
import java.util.EmptyStackException;
import java.util.Stack;
class StackDemo {
 static void showpush(Stack<Integer> st, int a){
      st.push(a);
      System.out.println("Push("+a+")");
      System.out.println("stack: "+st);
  }
  static void showpop(Stack<Integer> st){
      System.out.print("pop -> ");
      Integer a = st.pop();
      System.out.println(a);
      System.out.println("stack: "+st);
  }

  public static void main(String[] args) {
      Stack<Integer> st = new Stack<Integer>();
      System.out.println("stack: "+st);
      showpush(st,42);
      showpush(st,66);
      showpush(st,99);
      showpop(st);
      showpop(st);
      showpop(st);
      try {
          showpop(st);
      }catch (EmptyStackException e){
          System.out.println("empty stack");
      }
      /**
       * 输出:
       * stack: []
       * Push(42)
       * stack: [42]
       * Push(66)
       * stack: [42, 66]
       * Push(99)
       * stack: [42, 66, 99]
       * pop -> 99
       * stack: [42, 66]
       * pop -> 66
       * stack: [42]
       * pop -> 42
       * stack: []
       * pop -> empty stack
       */
  }
}

   注意针对EmptyStackException的异常处理程序的捕获方式,以便能够巧妙得处理堆栈下溢。
   另外一点:尽管不反对Stack,但ArrayDeque是更好的选择。

13.4 Dictionary类

   Dictionary是表示键/值对存储库的抽象类,其操作与Map类似。给定键和值,就可以外Dictionary对象中存储值。一旦存储了值,就可以使用相应的键来检索这个值。因此,与映射类似,可以将字典想象成键/对的列表。尽管目前不反对使用,但是Dictionary被贵类为过时的遗留类,因为它已经被Map完全取代。但是Dictionary任然在使用,因此下面对其进行完整讨论。
   随着JDK5的出现,Dictionary被修改成泛型,其声明如下所示:

class Dictionary<K,V>

   其中,K指定了键的类型,V指定了值的类型。表22列出了Dictionary类定义的抽象方法。

表22 Dictionary类定义的抽象方法
方 法描 述
Enumeration<V> elements()返回字典所包含值的枚举
v get(Object key)返回的对象与key关联的值。如果key不在字典中,就返回null对象
boolean isEmpty如果字典为空,就返回true;如果至少包含一个键,就返回false
Enumeration<E> keys()返回字典所包含键的枚举
V put(K key,V value)将键及相应的值插入字典中。如果在字典中原来不存在key,就返回null;如果key已经在字典中,就返回之前与key关联的值
V remove(Object key)移除key及相应的值。返回与key关联的值。如果key不在字典中,就返回null
int size()返回字典中条目的数量

   要添加键和值,可以使用put()方法。使用get()方法可以检索给定键的值。分别通过keys()和elements()方法,键和值都可以作为Enumeration返回。size()方法返回在字典中存储的键/值对的数量,如果字典为空,isEmpty()方法将返回true。可以使用remove()方法移除键/值对。
   请记住:
   Dictionary类是过时的,应当实现Map接口来获取键/值存储功能。

13.5 Hashtable类

   Hashtable是原始java.util的一部分,是Dictionary的具体实现。但是,随着集合的出现,Java对Hashtable进行了重新设计,使其也实现了Map接口。因此,Hashtable被集成到集合框架中。Hashtable和HashMap类似,但是它是同步的。
   与HashMap类似,Hashtable在哈希表中存储键/值对。但是,键和值都不能是null。当使用Hashtable时,需要制定作为键的对象以及与键关联的值。然后对键进行散列,产生的哈希码作为索引,在哈希表中对应索引的位置存储值。
   JDK5将Hashtable修改为泛型,其声明如下所示:

class Hashtable<K,V>

   其中,K指定了键的类型,V指定了值的类型。
   哈希表只能存储重写了hashCode()和equals()方法的对象,这两个方法是由Object定义的。hashCode()方法必须计算并返回对象的哈希码。当然,equals()方法用来比较两个对象。幸运的是,许多内置的Java类已经实现了hashCode()方法。例如,HashTable的通用类型使用String对象作为键。String实现了hashCode()和equals()方法。
   Hashtable的构造函数如下所示:

Hashtable()
Hashtable(int size)
Hashtable(int size,float fillRatio)
Hashtable(Map<? extends K,? extends V> m)

   第1个版本是默认构造函数。第2个版本创建由size指定初始大小的哈希表(默认大小时11)。第3个版本创建的哈希表,初始大小由size指定并且填充率由fillRatio指定。填充率必须在0.0到1.0之间,填充率决定了哈希表在增大容量之前的充满程序。具体地说,就是当元素的数量大于哈希表的容量与填充率的乘积时,将扩展哈希表。如果没有指定填充率,就使用0.75。最后,第4个版本创建的哈希表,使用m中的元素进行初始化,使用默认载入因子0.75。
   除了Map接口定义的方法之外(HashTable现在实现了Map接口),HashTable还定义了在表23中列出的遗留方法。如果试图使用null键或值,有些方法会抛出NullPointerException异常。

表23 Hashtable类定义的遗留方法
方 法描 述
void clear()重置并清空哈希表
Object clone()返回调用对象的一个副本
boolean contains(Object value)如果哈希表中存在等于value的值,就返回true;如果没找到,就返回false
boolean containsKey(Object key)如果哈希表中存在等于key的键,就返回true;如果没找到,就返回false
boolean constainsValue(Object value)如果哈希表中存在等于value的值,就返回true;如果没找到,就返回false
Enumeration<V> elements()返回哈希表中所包含值的一个枚举
V get(Object key)返回的对象包含与key关联的值。如果key不在哈希表中,就返回null对象
boolean isEmpty()如果哈希表为空,就返回true;如果至少包含一个键,就返回false
Enumeration<K> keys()返回哈希表中所包含键的一个枚举
V put(K key,V value)返回键和值插入哈希表中。如果之前在哈希表中不存在key,就返回null;如果在哈希表中已经存在key,就返回之前与key关联的值
void rehash()增加哈希表的容量并重新散列所有键
V remove(Object key)移除key以及与之关联的值,返回与key关联的值。如果key不在哈希表中,就返回null对象
int size()返回哈希表中条目的数量
String toString()返回哈希表的等价字符串

   下面的例子重新编写了前面显示的银行账户程序,使用哈希表存储银行账户的名称以及当前余额:

//Demonstrate a HashTable
import java.util.Enumeration;
import java.util.Hashtable;
class HTDemo {
 public static void main(String[] args) {
      Hashtable<String,Double> balance = new Hashtable<String,Double>();
      Enumeration<String> names;
      String str;
      double bal;
      balance.put("John Doe",3434.34);
      balance.put("Tom Smith",123.22);
      balance.put("Jane Baker",1378.00);
      balance.put("Tod Hall",99.22);
      balance.put("Ralph Smith",-19.08);
      //Show all balances in hashtable.
      names = balance.keys();
      while (names.hasMoreElements()){
          str = names.nextElement();
          System.out.println(str+": "+balance.get(str));
      }
      //Deposit 1,000 into John Doe's account.
      bal = balance.get("John Doe");
      balance.put("John Doe",bal+1000);
      System.out.println("John Doe's new balance: "+balance.get("John Doe"));
      /**
       * 输出:
       * Tod Hall: 99.22
       * Ralph Smith: -19.08
       * John Doe: 3434.34
       * Jane Baker: 1378.0
       * Tom Smith: 123.22
       * John Doe's new balance: 4434.34
       */
  }
}

   重要的一点:与映射类类似:Hashtable不直接支持迭代器。因此,前面的程序使用枚举显示余额。但是,可以获取哈希表的组视图,进而使用迭代器。为此,简单地使用由Map定义的组视图方法,例如entrySet()或keySet()。例如,可以获取键的组视图,并使用迭代器或增强的for循环对它们进行遍历。下面是该程序修改后的版本,该版本演示了这种技术:

//Use iterators with a Hashtable.
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
class HDemo2 {
 public static void main(String[] args) {
      Hashtable<String,Double> balance = new Hashtable<String,Double>();
      String str;
      double bal;
      balance.put("John Doe",3434.34);
      balance.put("Tom Smith",123.22);
      balance.put("Jone Baker",1378.00);
      balance.put("Tod Hall",99.22);
      balance.put("Ralph Smith",-19.08);
      //Show all balance in hsahtable.
      //First,get a set view of the keys.
      Set<String> set = balance.keySet();
      //Get an iterator.
      Iterator<String> itr = set.iterator();
      while(itr.hasNext()){
          str = itr.next();
          System.out.println(str+": "+balance.get(str));
      }
      //Deposit 1,000 into John Doe's account.
      bal = balance.get("John Doe");
      balance.put("John Doe",bal+1000);
      System.out.println("John Doe's new balance: "+balance.get("John Doe"));
  }
}

13.6 Properties类

   Properties是Hashtable的子类,用于保存值的列表。在列表中,键是String类型,值也是String类型。许多其他Java类都在使用Properties类。例如,当获取环境值时,System.getProperties()方法返回的对象的类型就是Properties。尽管Properties类本身不是泛型类,但其中的一些方法是泛型方法。
   Properties定义了以下实例变量:

Properties defaults;

   该变量包含与Properties对象关联的默认属性列表。Properties定义了以下构造函数:

Properties()
Properties(Properties propDefault)

   第1个版本创建没有默认值的Properties对象。第2个版本创建使用propDefault作为默认值的Properties对象。对于这两个版本,属性列表都是空的。
   除了继承自Hashtable的方法外,Properties类还定义了表24中列出的方法。此外,Properties还包含不赞成使用的方法save(),该方法已经被store()取代,因为save()方法无法正确地处理错误。

表24 Properties类定义的方法
方 法描 述
String getProperty(String key)返回与key关联的值。如果key既不在列表中,也不在默认属性列表中,就返回null对象
String getProperty(String key,String defaultProperty)返回与key管理的值。如果key既不在列表中,也不在默认属性列表中,就返回defaultProperty
void list(PrintStream streamOut)将属性列表发送到与streamOut 链接的输出流
void list(PrintWriter streamOut)将属性列表发送到streamOut链接的输出流
void load(InputStream streamIn) throws IOException从与streamIn链接的输入六中输入一个属性列表
void load(Reader streamIn) throws Exception从与streamIn链接的输入流中输入一个属性列表
void loadFromXML(InputStream streamIn) throws IOException,InvalidPropertiesFormatException从与streamIn连接的XML文档输入一个属性列表
Enumeration<?> propertyNames()返回键的一个枚举,包括在默认属性中能找到的那些键
Object setProperty(String key,String value)将value与key相关联。返回之前与key关联的值;如果不存在这样的关联,就返回null
void store(OutputStream streamOut,String description) throws IOException在写入description指定的字符串之后,属性列表被写的与streamOut链接的输出流中
void store(Writer streamOut,String description)在写入description指定的字符串之后,属性列表被写到与streamOut链接的输出流中
void storeToXML(OutputStream streamOut,String description) throws IOException在写入description指定的字符串之后,属性列表被写到与streamOut链接的XML文档中
void storeToXML(OutputStream streamOut,String description,String enc)使用指定的字符编码,将属性列表以及由description指定的字符串写入与streamOut链接的XML文档中
Set<String> stringPropertyNames()返回一组键

   Properties类有一个有用的特性,就是可以指定默认属性,如果没有值与特性的键关联,就会返回默认属性。例如在getProperty()方法中,可以在指定键时指定默认值,例如getProperty(“name”,“default value”)。如果没有找到“name”值,就返回“default value”,当构造Properties对象时,可以传递另外一个Properties实例,用作新实例的默认属性。对于这种情况,如果对给定的Properties对象调用getProperty(“foo”),并且"foo"不存在,那么Java会在默认的Properties对象中查找"foo",这使得可以任意嵌套默认属性。
   下面的例子演示了Properties类的使用。创建一个属性列表,在该列表中,键是州的名称,值时首府城市的名称。注意在该程序中,查找弗罗里达
州首府城市的代码包含一个默认值。

//Demonstrate a Property list.
import java.util.Properties;
import java.util.Set;
class PropDemo {
public static void main(String[] args) {
      Properties defList = new Properties();
      defList.put("Florida","Tallahassee");
      defList.put("Wisconsin","Madison");
      Properties capitals = new Properties(defList);
      capitals.put("Illinois","Springfield");
      capitals.put("Missouri","Jefferson City");
      capitals.put("Washington","Olympia");
      capitals.put("California","Sacramento");
      capitals.put("Indiana","Indianapolis");
      //Get a set-view of the keys.
      Set<?> states = capitals.keySet();
      //Show all of the states and capitals
      for (Object name:states){
          System.out.println("The capital of "+name+" is "+capitals.getProperty((String)name)+".");
      }
      //Florida will now be found in the default list.
      String str = capitals.getProperty("Florida");
      System.out.println("The capital of Florida is "+str+".");
      /**
       * 输出:
       * The capital of Missouri is Jefferson City.
       * The capital of Illinois is Springfield.
       * The capital of Indiana is Indianapolis.
       * The capital of California is Sacramento.
       * The capital of Washington is Olympia.
       * The capital of Florida is Tallahassee.
       */
  }
}

13.7 使用store()和load()

   Properties类最有用的方面之一是:在Properties对象中保存的信息,可以使用stroe()和load()方法很容易地存储到磁盘中以及从磁盘加载。在任何时候,都可以将Properties对象写入到流中或从流中读取。这使得对于实现简单的数据库来说,属性列表特别方便。例如,下面的程序使用属性列表创建一个简单的计算机化的电话簿,用于存储姓名和电话号码。为了查找某个人的电话号码,输入他或她的姓名。该程序使用store()与load()方法存储和检索列表。当程序执行时,首先尝试从phonebook.dat文件加载列表。如果这个文件从中,就加载列表。然后可以向列表中添加内容。如果成功添加内容,那么当退出程序时会保存新的列表。注意,实现一个小的,但是功能完备、计算机的电话簿所需要的代码如此之少!

/*A simple telephone database that uses a property list.
 */
import java.io.*;
import java.util.Properties;
class PhoneBook {
 public static void main(String[] args) throws IOException {
      Properties ht = new Properties();
      BufferedReader br = new BufferedReader(new InputStreamReader((System.in)));
      String name,number;
      FileInputStream fin = null;
      boolean changed = false;
      //Try to open phonebook.dat file.
      try {
          fin = new FileInputStream("phonebook.dat");
      }catch (FileNotFoundException e){
          //ignore missing file
      }
      /*If phonebook file already exists,
      load existing telephone numbers.
       */
      try {
          if(fin != null){
              ht.load(fin);
              fin.close();
          }
      }catch (IOException e){
          System.out.println("Error reading file.");
      }
    //Let user enter new names and numbers.
      do{
          System.out.println("Enter new name"+" ('quit' to stop)");
          name = br.readLine();
          if(name.equals("quit")){
              continue;
          }
          System.out.println("Enter number: ");
          number = br.readLine();
          ht.put(name,number);
          changed= true;
      }while(!name.equals("quit"));
      //If phone book data has changed,save it.
      if(changed){
          FileOutputStream fout = new FileOutputStream("phonebook.dat");
          ht.store(fout,"Telephone Book");
          fout.close();
      }
      //Look up numbers given a name.
      do{
          System.out.println("Enter name to find"+" ('quit' to quit): ");
          name = br.readLine();
          if(name.equals("quit")){
              continue;
          }
          number = (String)ht.get(name);
          System.out.println(number);
      }while(!name.equals("quit"));
  }
}

14 小结

   集合框架针对最通用的编程任务,提供了一套功能强大、设计良好的解决方案。当需要存储和检索信息时,可考虑使用集合。请记住,集合不是专门针对“大任务”二设计的,例如企业数据库、邮件列表或库存系统。当应用于更小的任务时,它们也很有效。例如,TreeMap可以创建一个优秀的集合,用于容纳一组文件的目录结构。对于存储项目管理信息而言,TreeSet可能相当有用。总之,从集合中受益的问题种类可能会超出我们的想象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值