11_Java集合_01-认识集合、collection、迭代、Map
本章章节
> 11.1认识集合类
> 11.2 Collection接口及其子接口
> 11.3 Collection 接口及其子接口的常见实现类
> 11.4通过迭代方法访问集合
> 11.5 Map
> 11.6 从以前版本遗留下来的类和接口
> 11.7 hashCode()方法
> 11.8 对象克隆
> 11.9 集合工具类Collections
11.1认识集合类
集合(或容器)表示保存一个对象组的单个对象,其它对象被认为是它的元素。
集合与数组的不同在于,它是大小可变的序列,而且元素类型可以不受限定,只要是引用类型。对象数组中包含一组对象,但是对象数组使用的时候存在一个长度的限制,那么集合是专门解决这种限制的,使用集合可以方便的向数组中增加任意多个数据。
集合类使用初始容量和加载因子调整自己的大小。集合类全部支持泛型,是一种数据安全的用法。
Java集合框架定义了几个接口。这些集合接口决定了集合实现类的基本特性。对于不同的集合实现类提供了这些标准接口的不同实现。Java集合接口如表11-1所示:
表11-1 Java集合接口
Java集合类库的用途就是“保存对象”,主要分为两类:
Collection:层次结构中的根接口。Collection表示一组对象,这些对象也称为collection的元素。一些collection允许有重复的元素,而另一些则不允许。一些collection是有序的,而另一些则是无序的。JDK不提供此接口的任何直接实现,它提供更具体的子接口(如Set和List)实现。此接口通常用来传递collection,并在需要最大普遍性的地方操作这些collection。Collection常见的子接口如图11-1:
图11-1 Collection接口继承图
Collection:一组没有顺序的对象,允许出现相同的元素对象。
Set:一组没有顺序的对象(离散),不允许重复相同的元素。
SortedSet:是一个按照升序排列元素的Set。
List:一组有插入顺序的对象,允许重复相同的元素。
关于Collection更为详细的继承图谱如图11-2:
图11-2 Collection接口以及实现类继承图谱
Map:一组成对的“键—值”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数组与对象关联在一起。映射表允许我们使用一个对象来查找某个对象,它被称为“关联数组”,因为它将某些对象与另外一些对象关联在一起,也被称为“字典”,因为你可以使用键对象来查找值对象。Map常见的子接口如图11-3:
图11-3 Map接口继承图
关于Map更为详细的继承图谱如图11-4:
图11-4 Map接口以及实现类继承图谱
除了类集接口之外,类集也使用Comparator、Iterator和ListIterator接口。关于这些接口将在本章后面做更深入的讲解。简单地说,Comparator接口定义了两个对象比较方法。
Iterator和ListIterator接口类集中的对象。
为了在它们的使用中提供最大的灵活性,类集接口允许对一些方法进行选择。可选择的方法使得使用者可以更改类集的内容。支持这些方法的类集被称为可修改的(modifiable)。不允许修改其内容的类集被称为不可修改的(unmodifiable)。而所有内置的类集都是可修改的。如果对一个不可修改的类集使用这些方法,将引发一个UnsupportedOperationException异常。
11.2 Collection接口及其子接口
Collection接口是构造类集框架的基础,是保存单值集合的最大父接口。Java没有提供该接口的直接实现类。
Collection 接口定义如下:
public interface Collection<E> extends Iterable<E>
JDK1.5之后为Collection接口增加了泛型声明。所有的类集操作都存放在java.util包中。在一般的开发中,往往很少去直接使用Collection接口进行开发,而基本上都是使用其子接口。常见的Collection子接口有:List、Set、Queue、SortedSet。
Collection声明了所有类集都将拥有的核心方法。在JDK1.5之前,这些方法被总结在表11-2(A)中,在JDK1.5之后,增加了泛型,这些方法被总结在表11-2(B)中。因为所有类集实现Collection,所以熟悉它的方法对于清楚地理解框架是必要的。其中几种方法可能会引发一个UnsupportedOperationException异常。正如上面的解释,这些将发生在当类集不能被修改的时候。当一个对象与另一个对象不兼容,例如:当企图增加一个不兼容的对象到一个类集中时,将产生一个ClassCastException异常。
表11-2(A) JDK1.5之前Collection接口的方法定义
表11-2(B) JDK1.5之后Collection接口的方法定义
(注:关于JDK1.5后方法的具体说明可以参考JDK1.5之前的方法说明)
调用add()方法可以将对象加入类集。注意add()带一个泛型类型的参数。因为Object是所有类的超类,所以任何类型的对象可以被存储在一个Collection<Object>类集中。可以通过调用addAll()方法将一个类集的全部内容增加到另一个类集中。
可以通过调用remove()方法将一个对象删除。为了删除一组对象,可以调用removeAll()方法。调用retainAll()方法可以将除了一组指定的元素之外的所有元素删除。为了清空类集,可以调用clear()方法。
通过调用contains()方法,可以确定一个类集是否包含了一个指定的对象。为了确定一个类集是否包含了另一个类集的全部元素,可以调用containsAll()方法。当一个类集是空的时候,可以通过调用isEmpty()方法来予以确认。调用size()方法可以获得类集中当前元素的个数。
toArray()方法返回一个数组,这个数组包含了存储在调用类集中的元素。这个方法比它初看上去的能力要更重要。经常使用类数组语法来处理类集的内容是有优势的。通过在类集和数组之间提供一条路径,可以充分利用这两者的优点。
调用equals()方法可以比较两个类集是否相等。“相等”的精确含义可以不同于从类集到类集。例如,可以执行equals()方法以便用于比较存储在类集中的元素的值,换句话说,equals()方法能比较对象元素的引用。
一个更加重要的方法是iterator(),该方法对类集返回一个迭代程序。我们可以通过它遍历整个集合。除此之外还可以利用foreach结构遍历。
11.2.1 List接口
List是Collection的子接口,它强调元素的插入顺序,可以包含重复的元素。它扩展了Collection接口并提供了按索引访问元素的方式,可以利用索引下标在集合的指定位置插入、删除和访问元素。实现List接口的类有:ArrayList、LinkedList、Stack、Vector。其中Stack和Vector是早期集合类,他们是线程安全(同步的)集合类,一般性能会稍慢于非同步的集合类。ArrayList是利用数组实现的,LinkedList是利用链表实现的。List除了由Collection定义的方法之外,List还定义了一些它自己的方法,这些方法总结在表11-3中。需要再次注意当类集不能被修改时,其中的几种方法引发UnsupportedOperationException异常。当一个对象与另一个不兼容,例如:当企图将一个不兼容的对象加入一个类集中时,将产生ClassCastException异常。
表11-3 由List 定义的方法
对于由Collection定义的add()和addAll()方法,List增加了方法add(int, Object) 和addAll(int, Collection)。这些方法在指定的下标处插入元素。由Collection定义的add(Object)和addAll(Collection)的语义也被List改变了,用于在列表的尾部增加元素。
为了获得指定位置的存储对象,可以利用get(下标)的形式得到。为了给列表中的一个元素赋值,可以调用set()方法,指定被改变的对象的下标。调用indexOf()或lastIndexOf()可以得到一个对象的下标。通过调用subList()方法,可以获得列表的一个指定了开始下标和结束下标的子列表。
对于List而言,除了有foreach遍历方式和迭代器遍历方式之外,还可以利用get()方法通过循环完成遍历,循环次数由size()决定,此遍历操作是List接口独有的。
11.2.2 Set接口
Set继承于Collection,由于其不允许元素重复,没有顺序概念,相对于Collection无需扩展方法,限制add类方法不要放入重复元素就可以(set判断两个对象是否相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相同)。因此,如果试图将重复元素加到集合中时,add()方法将返回false。
实际创建时要使用Set的实现类,包括EnumSet,HashSet,LinkedHashSet,TreeSet,其中LinkedHashSet体现插入顺序,TreeSet体现了元素排序。性能方面“HashSet > LinkedHashSet > TreeSet”,但是对于遍历查找则可能相反。
11.2.2.1 SortedSet接口
SortedSet接口扩展了Set并说明了按升序排列的集合的特性。除了那些由Set定义的方法之外,由SortedSet接口说明的方法列在表11-4中。当没有项包含在调用集合中时,其中的几种方法会引发NoSuchElementException异常。当对象与调用集合中的元素不兼容时,将引发ClassCastException 异常。如果试图使用null 对象,而集合不允许null 时,会引发NullPointerException异常。
表11-4 由SortedSet 定义的方法
SortedSet定义了几种方法,使得对集合的处理更加方便。调用first()方法,可以获得集合中的第一个对象。调用last()方法,可以获得集合中的最后一个元素。调用subSet()方法,可以获得排序集合的一个指定了第一个和最后一个对象的子集合。如果需要得到从集合的第一个元素开始的一个子集合,可以使用headSet()方法。如果需要获得集合尾部的一个子集合,可以使用tailSet()方法。
11.3 Collection 接口及其子接口的常见实现类
现在,大家已经熟悉了类集接口,下面开始讨论实现它们的标准类。一些类提供了完整的可以被使用的工具。另一些类是抽象的,提供主框架工具,作为创建具体类集的起始点。没有一个Collection接口是同步的,在本章后面看到的那样,有可能获得同步版本。标准的Collection实现类总结表11-5中。
表11-5 Collection实现类
注意:
除了Collection接口之外,还有几个从以前版本遗留下来的类,如Vector、Stack和 Hashtable均被重新设计成支持类集的形式。这些内容将在本章后面讨论。下面讨论具体的 Collection接口,举例说明它们的用法。
11.3.1 ArrayList 类
ArrayList类扩展AbstractList并支持List接口。ArrayList支持可随需要而增长的动态数组。在Java中,标准数组是定长的。在数组创建之后,它们不能被加长或缩短,这也就意味着开发者必须事先知道数组可以容纳多少元素。但是,一般情况下,只有在运行时才能知道需要多大的数组。为了解决这个问题,类集框架定义了ArrayList。
本质上,ArrayList是对象引用的一个变长数组。也就是说,ArrayList能够动态地增加或减小其大小。数组列表以一个原始大小被创建。当超过了它的大小,类集自动增大。当对象被删除后,数组就可以缩小。注意:动态数组也被从以前版本遗留下来的类Vector所支持。关于这一点,将在后面介绍。
ArrayList有如下的构造方法:
ArrayList()
ArrayList(Collection<E> c)
ArrayList(int capacity)
其中第一个构造方法建立一个空的数组列表。第二个构造方法建立一个数组列表,该数组列表由集合c中的元素初始化。第三个构造函数建立一个数组列表,该数组有指定的初始容量(capacity)。容量是用于存储元素的基本数组的大小。当元素被追加到数组列表上时,容量会自动增加。
下面的程序是ArrayList的一个简单应用。首先创建一个数组列表,接着添加String类型的对象(回想一个引用字符串被转化成一个字符串(String)对象的方法)。接着列表被显示出来。将其中的一些元素删除后,再一次显示列表。
范例:ArrayListDemo.java
import java.util.*; public class ArrayListDemo { public static void main(String args[]) { // 创建一个ArrarList对象 ArrayList<String> al = new ArrayList<String>(); System.out.println("a1 的初始化大小:" + al.size()); // 向ArrayList对象中添加新内容 al.add("C"); // 0位置 al.add("A"); // 1位置 al.add("E"); // 2位置 al.add("B"); // 3位置 al.add("D"); // 4位置 al.add("F"); // 5位置 // 把A2加在ArrayList对象的第2个位置 al.add(1, "A2"); // 加入之后的内容:C A2 A E B D F System.out.println("a1 加入元素之后的大小:" + al.size()); // 显示Arraylist数据 System.out.println("a1 的内容: " + al); // 从ArrayList中移除数据 al.remove("F"); al.remove(2); // C A2 E B D System.out.println("a1 删除元素之后的大小: " + al.size()); System.out.println("a1 的内容: " + al); } }
输出结果:
a1 的初始化大小:0
a1 加入元素之后的大小:7
a1 的内容: [C, A2, A, E, B, D, F]
a1 删除元素之后的大小: 5
a1 的内容: [C, A2, E, B, D]
注意a1开始时是空的,当添加元素后,它的大小增加了。当有元素被删除后,它的大小又会变小。在前面的例子中,使用由toString()方法提供的默认的转换显示类集的内容,toString()方法是从AbstractCollection继承下来的。它对简短的程序来说是足够了,但很少使用这种方法去显示实际中的类集的内容。通常编程者会提供自己的输出程序。但在下面的几个例子中,仍将采用由toString()方法创建的默认输出。
尽管当对象被存储在ArrayList对象中时,其容量会自动增加。然而,也可以通过调用ensureCapacity()方法来人工地增加ArrayList的容量。如果事先知道将在当前能够容纳的类集中存储许许多多的内容时,你可能会想这样做。在开始时,通过一次性地增加它的容量,就能避免后面的再分配。因为再分配是很花时间的,避免不必要的处理可以提高性能。
范例:ArrayListToArray.java
import java.util.*; public class ArrayListToArray { public static void main(String args[]) { // 创建一个ArrayList对象al ArrayList<Integer> al = new ArrayList<Integer>(); // 向ArrayList中加入对象 al.add(new Integer(1)); al.add(new Integer(2)); al.add(3); al.add(4); System.out.println("ArrayList 中的内容: " + al); // 得到对象数组 Object ia[] = al.toArray(); int sum = 0; // 计算数组内容 for (int i = 0; i < ia.length; i++) sum += ((Integer) ia[i]).intValue(); System.out.println("数组累加结果是: " + sum); } }
输出结果:
ArrayList 中的内容: [1, 2, 3, 4]
数组累加结果是:10
程序开始时创建一个整数的类集。接下来,toArray()方法被调用,它获得了一个Object对象数组。这个数组的内容被置成整型(Integer),接下来对这些值进行求和操作。
11.3.2 LinkedList 类
LinkedList类扩展了AbstractSequentialList类并实现List接口。它提供了一个链接列表的数据结构。它具有如下的两个构造方法,说明如下:
LinkedList()
LinkedList(Collection<E> c)
第一个构造方法建立一个空的链接列表。第二个构造方法建立一个链接列表,该链接列表由另一个集合c中的元素初始化。
除了它继承的方法之外,LinkedList类本身还定义了一些有用的方法,这些方法主要用于操作和访问列表。
使用addFirst()方法或offerFirst()方法或add(0, obj)方法或push()方法可以在列表头增加元素。它们的形式如下所示:
void addFirst(E obj)
void add(int index, E e)
boolean offerFirst(E e)
void push(E e)
使用addLast()方法或offer()方法或offerLast()可以在列表的尾部增加元素。它们的形式如下所示:
void addLast(E obj)
boolean offer(E e)
boolean offerLast(F e)
调用getFirst()方法或element()方法或get(0)方法或peek()方法或peekFirst()方法可以获得第一个元素,且不从列表中删除该元素。它们的形式如下所示:
E getFirst()
E element()
E get(int index)
E peek()
E peekFirst()
调用getLast()方法或get(link.size()-1)方法或peekLase()方法可以得到最后一个元素。注意这些方法都是获得但不移除。它们的形式如下所示:
E getLast()
E get(int index)
E peekLast
为了获取并删除第一个元素,可以使用remove()或remove(0)或removeFirst()或poll()或pollFirst()或pop()方法。它们的形式如下所示:
E remove()
E remove(int index)
E removeFirst()
E poll()
E pollFrist()
E pop()
为了删除最后一个元素,可以调用remove(link.size()-1)或removeLast()或pollLast()方法。它们的形式如下所示:
E remove(int index)
E removeLast()
E pollLast()
调用removeFirstOccurrence(e)方法用于删掉从头到尾找第一次出现该元素的元素,调用removeLastOccurrence(e)方法用于删掉从后往前找第一次出现该元素的元素。它们的形式如下所示:
boolean removeFirstOccurrence(Object e)
boolean removeLastOccurrence(Object e)
利用正常迭代器Iterator可以正序遍历整个集合,利用iterator()方法:
//LinkedList正常迭代器访问方式 Iterator<String> it = link.iterator(); while(it.hasNext()){ System.out.print(it.next() + " "); } System.out.println();
利用ListIterator可以从指定位置向前或向后遍历集合,利用listIterator(int index)方法:
//LinkedList从指定位置开始向后遍历 ListIterator<String> lit=link.listIterator(3); while(lit.hasNext()){ System.out.print(lit.next()+" "); } System.out.println(); //LinkedList从指定位置开始向前遍历 ListIterator<String> rlit=link.listIterator(3); while(rlit.hasPrevious()){ System.out.print(rlit.previous()+" "); } System.out.println();
利用正常迭代器Iterator可以逆序遍历整个集合,利用descendingIterator()方法:
//LinkedList反向迭代器 Iterator<String> dit = link.descendingIterator(); while(dit.hasNext()){ System.out.print(dit.next()+ " "); } System.out.println();
下面的程序举例是对几个LinkedList支持的方法的说明:
范例:LinkedListDemo.java
import java.util.*; public class LinkedListDemo { public static void main(String args[]) { // 创建LinkedList对象 LinkedList<String> ll = new LinkedList<String>(); // 加入元素到LinkedList中 ll.add("F"); ll.add("B"); ll.add("D"); ll.add("E"); ll.add("C"); // 在链表的最后个位置加上数据 ll.addLast("Z"); // 在链表的第一个位置上加入数据 ll.addFirst("A"); // 在链表第二个元素的位置上加入数据 ll.add(1, "A2"); System.out.println("ll 最初的内容:" + ll);
// 从linkedlist中移除元素 ll.remove("F"); ll.remove(2); System.out.println("从ll中移除内容之后:" + ll); // 移除第一个和最后一个元素 ll.removeFirst(); ll.removeLast(); System.out.println("ll 移除第一个和最后一个元素之后的内容:" + ll); // 取得并设置值 Object val = ll.get(2); ll.set(2, (String) val + " Changed"); System.out.println("ll 被改变之后:" + ll); } }
输出结果:
ll 最初的内容:[A, A2, F, B, D, E, C, Z]
从ll中移除内容之后:[A, A2, D, E, C, Z]
ll 移除第一个和最后一个元素之后的内容:[A2, D, E, C]
ll 被改变之后:[A2, D, E Changed, C]
因为LinkedList实现List接口,调用add(Object)将项目追加到列表的尾部,如同addLast( )方法所做的那样。使用add()方法的add(int, Object)形式,插入项目到指定的位置,如例子程序中调用add(1, "A2")的举例。
注意如何通过调用get()和set()方法而使得ll中的第三个元素发生了改变。为了获得一个元素的当前值,通过get()方法传递存储该元素的下标值。为了对这个下标位置赋一个新值,通过set()方法传递下标和对应的新值。
11.3.3 ArrayList和LinkedList的比较
1、ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2、对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
3、在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
4、LinkedList不支持高效的随机元素访问。
5、ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
11.3.4 HashSet 类
HashSet是Set接口的典型实现类,大多数时候使用Set集合时就是使用这个实现类,HashSet按hash算法来存蓄集合中的元素,在散列(hashing)中,一个关键字的信息内容被用来确定唯一的一个值,称为散列码(hash code)。而散列码被用来当做与关键字相连的数据的存储下标。关键字到其散列码的转换是自动执行的——看不到散列码本身。程序代码也不能直接索引散列表。散列法的优点在于即使对于大的集合,它允许一些基本操作,如:add(),contains(),remove()和size( )方法的运行时间保持不变。因此具有很好的存取和查找性能。
HashSet特点:
·不能保证元素的排列顺序,顺序有可能发生变化
·HashSet不是同步的。
·集合元素值可以是null
HashSet:使用散列的方式存放内容,本身没有顺序。但是无论是怎样的顺序添加相同的一批元素,存放的顺序相同。因此,散列存储也是一种特定的顺序存储。当向HashSet集合中存入一个元素,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值来决定该对象在HashSet中存储位置。如果有两个元素通过equals方法比较返回true,但他们的hashCode()方法返回值不相等,HashSet将会把他们存储在不同位置,也就可以添加成功。
下面的构造方法定义为:
HashSet()
HashSet(Collection<E> c)
HashSet(int capacity)
HashSet(int capacity, float fillRatio)
第一种形式构造一个默认的散列集合。第二种形式用c中的元素初始化散列集合。第三种形式用capacity初始化散列集合的容量。第四种形式用它的参数初始化散列集合的容量和填充比(也称为加载容量)。填充比必须介于0.0与1.0之间,它决定在散列集合向上调整大小之前,有多少能被充满。具体的说,就是当元素的个数大于散列集合容量乘以它的填充比时,散列集合被扩大。对于没有获得填充比的构造方法,默认使用0.75。
HashSet没有定义任何超类和接口提供的其它方法。
重要的是,注意散列集合并不能确定其元素的排列顺序,因为散列法的处理通常不让自己参与创建排序集合。如果需要排序存储,另一种类集——TreeSet将是一个更好的选择。
范例:HashSetDemo.java
import java.util.*; public class HashSetDemo { public static void main(String args[]) { // 创建HashSet对象 HashSet<String> hs = new HashSet<String>(); // 加入元素到HastSet中 hs.add("B"); hs.add("A"); hs.add("D"); hs.add("E"); hs.add("C"); hs.add("F"); hs.add("A"); System.out.println(hs); } }
输出结果:
[D, A, F, C, B, E]
如上面解释的那样,元素并没有按添加顺序进行存储,而是散列存储。另外,上面添加了两次A,但是结果只有一次。
注意:
因为Set不能出现重复的元素,所以对于自定义类而言,通常需要复写equals和hashCode方法来确保元素的唯一性。还需要复写toString实现自己的输出。
11.3.5 TreeSet 类
TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。它使用树来进行存储,访问和检索是很快的。在存储了大量的需要进行快速检索的排序信息的情况下,TreeSet是一个很好的选择。
TreeSet支持两种排序方式:
自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列。
定制排序:在创建TreeSet集合对象时,并提供一个Comparator接口实现类对象与该TreeSet集合关联,由Comparator实现类对象负责集合元素的排序逻辑。
下面的构造方法定义为:
TreeSet()
TreeSet(Collection<E> c)
TreeSet(Comparator<E> comp)
TreeSet(SortedSet<E> ss)
第一种形式构造一个空的树集合,该树集合将根据其元素的自然顺序按升序排序。第二种形式构造一个包含了c的元素的树集合。第三种形式构造一个空的树集合,它按照由comp指定的比较方法进行排序(比较方法将在本章后面介绍)。第四种形式构造一个包含了ss的元素的树集合。下面是一个TreeSet的使用范例:
范例:TreeSetDemo.java
import java.util.*; public class TreeSetDemo { public static void main(String args[]) { // 创建一个TreeSet对象 TreeSet<String> ts = new TreeSet<String>(); // 加入元素到TreeSet中 ts.add("C"); ts.add("A"); ts.add("B"); ts.add("E"); ts.add("F"); ts.add("D"); ts.add("A"); System.out.println(ts); } }
输出结果:
[A, B, C, D, E, F]
正如上面解释的那样,因为TreeSet按树存储其元素,它们被按照排序次序自动排序。且不允许出现重复的元素。
注意:
因为Set不能出现重复的元素,所以对于自定义类而言,通常需要复写equals和hashCode方法来确保元素的唯一性。还需要复写toString实现自己的输出。而TreeSet还有排序功能。
对于自动排序功能而言:Java提供了一个Comparable接口,该接口里定义了一个comparaTo(Object obj)方法。Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。如:BigDecimal、BigInteger、数值类型对应的包装类,Character、Boolean、String、Date、Time等。但是对于自定义类而言,我们需要自己实现Comparable接口,然后复写该接口的compareTo方法来定义自己的排序方法(对于比较的过程中出现相同的因子,会只显示一个)。
对于定制排序功能而言:在创建TreeSet对象时提供一个Comparator实现类对象与该TreeSet集合关联,由Comparator实现类对象负责集合元素的排序逻辑。所以我们要定义一个Comparator的实现类,在该实现类中复写Comparator的compare方法来定制自己的排序规则。
11.3.6 HashSet、TreeSet和LinkedHashSet的比较
HashSet采用散列函数对元素进行排序,是专门为快速查询而设计的。存入HashSet的对象必须定义hashCode()。
TreeSet采用红黑树的数据结构进行排序元素,能保证元素的次序,使用它可以从Set中提取有序的序列。
LinkedHashSet内部使用散列以加快查询速度,同时使用链表维护元素的插入的次序,在使用迭代器遍历Set时,结果会按元素插入的次序显示。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
11.4 通过迭代方法访问集合
通常开发者希望通过循环输出集合中的元素可以采用的方法有:
1. 直接利用System.out.println(list);的形式输出,此时实际上时调用的集合的toString方法,对于自定义类而言,需要复写toString来实现自己的输出。
2. 利用foreach结构遍历输出。
3. 对于List而言,它可以利用循环和get(i)方法的方式来循环输出。
4. 利用迭代器。
iterator 是一个或者实现Iterator或者实现ListIterator接口的对象。Iterator可以完成通过循环输出类集内容。ListIterator扩展Iterator,允许双向遍历列表,并可以修改单元。Iterator 接口说明的方法总结在表11-6中。ListIterator接口说明的方法总结在表11-7中。
表11-6 Iterator 定义的方法
表11-7 ListIterator 定义的方法
11.4.1 使用迭代方法
在通过迭代方法访问类集之前,必须得到一个迭代方法。每一个Collection类都提供一个iterator()方法,该方法返回一个对类集的迭代方法。通过使用这个迭代方法对象,可以一次一个地访问类集中的每一个元素。通常,使用迭代方法通过循环输出类集的内容,步骤如下:
1、通过调用类集的iterator( )方法获得对类集的迭代方法。
2、建立一个调用hasNext()方法的循环,只要hasNext()返回true,就进行循环迭代。
3、在循环内部,通过调用next()方法来得到每一个元素。
其对应的迭代器工作原理图如图11-5所示:
图11-5 迭代器工作原理示意图
对于执行List的类集,也可以通过调用ListIterator来获得迭代方法。正如上面解释的那样,列表迭代方法提供了前向或后向访问类集的能力,并可以修改元素。否则,ListIterator如同Iterator功能一样。程序IteratorDemo.java是一个实现上述描述的例子,说明了Iterator和ListIterator的使用方法。在范例中使用的是ArrayList类,但是原则上它是适用于任何类型的类集的。当然,ListIterator只适用于那些实现List接口的类集。
范例:IteratorDemo.java
import java.util.*; public class IteratorDemo { public static void main(String args[]) { // 创建一个ArrayList数组 ArrayList<String> al = new ArrayList<String>(); // 加入元素到ArrayList中 al.add("C"); al.add("A"); al.add("E"); al.add("B"); al.add("D"); al.add("F"); // 使用iterator显示a1中的内容 System.out.print("a1 中原始内容是:"); Iterator<String> itr = al.iterator(); while (itr.hasNext()) { System.out.print(itr.next() + " "); } System.out.println(); // 在ListIterator中修改内容 ListIterator<String> litr = al.listIterator(); while (litr.hasNext()) { litr.set(litr.next() + "+");// 用set方法修改其内容 } System.out.print("a1 被修改之后的内容:"); itr = al.iterator(); while (itr.hasNext()) { System.out.print(itr.next() + " "); } System.out.println(); // 下面是将列表中的内容反向输出 System.out.print("将列表反向输出:"); // hasPreviours由后向前输出 while (litr.hasPrevious()) { System.out.print(litr.previous() + " "); } System.out.println(); } }
输出结果:
a1 中原始内容是:C A E B D F
a1 被修改之后的内容:C+ A+ E+ B+ D+ F+
将列表反向输出:F+ D+ B+ E+ A+ C+
值得注意的是列表是如何被反向显示的。在列表被修改之后,litr指向列表的末端(记住,当到达列表末端时,litr.hasNext( )方法返回false)。为了反向遍历列表,程序继续使用litr,但这一次,程序将检测它是否有前一个元素,只要它有前一个元素,该元素就被获得并被显示出来。
11.5 Map
除了类集,Java 2还在java.util中增加了映射。映射(map)是一个存储关键字和值的关联或者说是“关键字/值”对的对象,即给定一个关键字,可以得到它的值。关键字和值都是对象,关键字必须是唯一的,但值是可以被复制的。有的映射可以接收null关键字和null值,有的则不能。
11.5.1 Map接口及其子接口
正因为映射接口定义了映射的特征和本质,所以从这里开始讨论映射。表11-8所列为支持映射的接口:
表11-8 支持映射的接口
下面对每个接口依次进行讨论。
11.5.1.1 Map 接口
Map接口映射唯一关键字到值。关键字(key)是以后用于检索值的对象。给定一个关键字和一个值,可以存储这个值到一个Map对象中。当这个值被存储以后,就可以使用它的关键字来检索它。由Map说明的方法总结在表11-9中。当调用的映射中没有项存在时,其中的几种方法会引发一个NoSuchElementException异常。而当对象与映射中的元素不兼容时,引发一个ClassCastException异常。如果试图使用映射不允许使用的null对象时,则引发一个NullPointerException异常。当试图改变一个不允许修改的映射时,则引发一个UnsupportedOperationException异常。
表11-9 Map接口定义的方法
方法 |
描述 |
void clear() |
从此映射中移除所有映射关系 |
boolean containsKey(Object key) |
如果此映射包含指定键的映射关系,则返回 true |
boolean containsValue(Object value) |
如果此映射将一个或多个键映射到指定值,则返回true |
Set<Map.Entry<K,V>> entrySet() |
返回此映射中包含的映射关系的 Set 视图。 |
boolean equals(Object o) |
比较指定的对象与此映射是否相等。 |
V get(Object key) |
返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 |
int hashCode() |
返回此映射的哈希码值。 |
boolean isEmpty() |
如果此映射未包含键-值映射关系,则返回 true。 |
Set<K> keySet() |
返回此映射中包含的键的 Set 视图。 |
V put(K key, V value) |
添加元素或将指定的值与此映射中的指定键关联(修改) |
void putAll(Map<? extends K,? extends V> m) |
从指定映射中将所有映射关系复制到此映射中 |
V remove(Object key) |
如果存在一个键的映射关系,则将其从此映射中移除 |
int size() |
返回此映射中的键-值映射关系数。 |
Collection<V> values() |
返回此映射中包含的值的 Collection 视图。 |
可以获得映射的集合“视图”。为了实现这种功能,可以使用entrySet()方法,它返回一个包含了映射中元素的集合(Set)。为了得到关键字的集合“视图”,可以使用keySet()方法。为了得到值的集合“视图”,可以使用values()方法。集合“视图”是将映射集成到集合框架内的手段。
11.5.1.2 SortedMap 接口
SortedMap接口扩展了Map,它确保了各项按关键字升序排序。由SortedMap说明的方法总结在表11-10中。当调用映射中没有的项时,其中的几种方法引发一个NoSuchElementException异常。当对象与映射中的元素不兼容时,则引发一个ClassCastException异常。当试图使用映射不允许使用的null对象时,则引发一个ullPointerException异常。
表11-10 SortedMap接口定义的方法
方法 |
描述 |
Comparator<? super K> comparator() |
返回对此映射中的键进行排序的比较器;如果此映射使用键的自然顺序,则返回 null |
K firstKey() |
返回此映射中当前第一个(最低)键 |
SortedMap<K,V> headMap(K toKey) |
返回此映射的部分视图,其键值严格小于 toKey |
K lastKey() |
返回映射中当前最后一个(最高)键 |
SortedMap<K,V> subMap(K fromKey, K toKey) |
返回此映射的部分视图,其键值的范围从 fromKey(包括)到 toKey(不包括) |
SortedMap<K,V> tailMap(K fromKey) |
返回此映射的部分视图,其键大于等于 fromKey。 |
11.5.1.3 Map.Entry 接口
Map.Entry 接口是Map中内部定义的一个接口,专门用来保存<key, value>的内容。如图11-6所示:
图11-6 Map.Entry与Map的关系
调用Map接口说明的entrySet()方法,返回一个包含映射输入的集合(Set)。这些集合元素的每一个都是一个Map.Entry对象。表11-11总结了由该接口说明的方法。
表11-11 Map.Entry接口定义的方法
方法 |
描述 |
boolean equals(Object o) |
比较指定对象与此项的相等性。 |
K getKey() |
返回与此项对应的键。 |
V getValue() |
返回与此项对应的值。 |
int hashCode() |
返回此映射项的哈希码值。 |
V setValue(V value) |
用指定的值替换与此项对应的值(可选操作)。 |
11.5.2 Map接口及其子接口的实现类
有几个类提供了Map及其子接口的实现。可以被用做映射的类如表11-12所示:
表11-12 映射接口的实现类
11.5.2.1 HashMap 类
HashMap类使用散列表实现Map接口。这允许一些基本操作,如:get()和put()的运行时间保持恒定,即便对大型集合,也是这样的。下面的构造方法定义为:
HashMap()
HashMap(Map<K, V> m)
HashMap(int capacity)
HashMap(int capacity, float fillRatio)
第一种形式构造一个默认的散列映射。第二种形式用m的元素初始化散列映射。第三种形式将散列映射的容量初始化为capacity。第四种形式用它的参数同时初始化散列映射的容量和填充比。容量和填充比的含义与前面介绍的HashSet中的容量和填充比相同。
HashMap实现Map并扩展AbstractMap。它本身并没有增加任何新的方法。应该注意的是散列映射并不保证它的元素的顺序。因此,元素加入散列映射的顺序并不一定是它们被迭代方法读出的顺序。
下面的程序举例说明了HashMap。它将名字映射到账目资产平衡表。注意集合“视图”是如何获得和被使用的。
范例:HashMapDemo.java
import java.util.*; public class HashMapDemo { public static void main(String args[]) { // 创建HashMap对象 HashMap<String, Double> hm = new HashMap<String, Double>(); // 加入元素到HashMap中 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("Todd Hall", 99.22); hm.put("Ralph Smith", -19.08); // 返回包含映射中项的集合 Set<Map.Entry<String, Double>> set = hm.entrySet(); // 用Iterator得到HashMap中的项 Iterator<Map.Entry<String, Double>> it = set.iterator();
// 显示元素 while (it.hasNext()) { // Map.Entry可以操作映射的输入 Map.Entry<String, Double> me = it.next(); System.out.print(me.getKey() + ": "); System.out.println(me.getValue()); } System.out.println(); // 让John Doe中的值增加1000 double balance = ((Double) hm.get("John Doe")).doubleValue(); // 用新的值替换掉旧的值 hm.put("John Doe", new Double(balance + 1000)); System.out.println("John Doe现在的资金:" + hm.get("John Doe")); } }
输出结果:
Todd Hall: 99.22
Ralph Smith: -19.08
Tom Smith: 123.22
John Doe: 3434.34
Jane Baker: 1378.0
John Doe现在的资金:4434.34
程序开始创建一个散列映射,然后将名字的映射增加到平衡表中。接下来,映射的内容通过使用由调用方法entrySet()而获得的集合“视图”而显示出来。关键字和值通过调用由Map.Entry定义的getKey()和getValue()方法而显示。注意存款是如何被制成John Doe的账目的。put()方法自动用新值替换与指定关键字相关联的原先的值。因此,在John Doe的账目被更新后,散列映射将仍然仅仅保留一个“John Doe”账目。
对于Map 接口来说,其本身是不能直接使用迭代(例如:Iterator、foreach)进行输出的,因为Map中的每个位置存放的是一对值(key value),而Iterator中每次只能找到一个值。所以非要使用迭代进行输出的话,必须按照如下的步骤完成(以Iterator输出方法为例):
1、 将Map的实例通过entrySet()方法变为Set接口对象;
2、 通过Set接口实例为Iterator实例化;
3、 通过Iterator迭代输出,每个内容都是 Map.Entry的对象;
4、 通过Map.Entry进行key value的分离。
11.5.2.2 TreeMap 类
TreeMap类通过使用树实现Map接口。TreeMap提供了按排序顺序存储“关键字/值”对的有效手段,同时允许快速检索。应该注意的是,不像散列映射,树映射保证它的元素按照关键字升序排序。
下面的TreeMap构造方法定义为:
TreeMap( )
TreeMap(Comparator<K> comp)
TreeMap(Map<K, V> m)
TreeMap(SortedMap<K, V> sm)
第一种形式构造一个空树的映射,该映射使用其关键字的自然顺序来排序。第二种形式构造一个空的基于树的映射,该映射通过使用Comparator comp来排序(比较方法Comparators将在本章后面进行讨论)。第三种形式用从m的输入初始化树映射,该映射使用关键字的自然顺序来排序。第四种形式用从sm 的输入来初始化一个树映射,该映射将按与sm相同的顺序来排序。
TreeMap实现SortedMap并且扩展AbstractMap。而它本身并没有另外定义其它方法。
下面的程序重新运行前面的范例,以便在其中使用TreeMap:
范例:TreeMapDemo.java
import java.util.*; public class TreeMapDemo { public static void main(String args[]) { // 创建TreeMap对象 TreeMap<Integer, String> tm = new TreeMap<Integer, String>(); // 加入元素到TreeMap中 tm.put(new Integer(8000), "张三"); tm.put(new Integer(8500), "李四"); tm.put(7500, "王五"); tm.put(5000, "赵六"); Collection<String> col = tm.values(); Iterator<String> i = col.iterator(); System.out.println("按工资由低到高顺序输出:"); while (i.hasNext()) { System.out.println(i.next()); } } }
输出结果:
按工资由低到高顺序输出:
赵六
王五
张三
李四
注意对关键字进行了排序。然而,在这种情况下,是用名字而不是用姓进行了排序。可以通过在创建映射时,指定一个比较方法来改变这种排序。
TreeSet和TreeMap都按排序顺序存储元素。然而,精确定义采用何种“排序顺序”的是比较方法。通常在默认的情况下,这些类通过使用被Java称之为“自然顺序”的顺序存储它们的元素,而这种顺序通常也是所需要的(A在B的前面,1在2的前面,等等)。如果需要用不同的方法对元素进行排序,可以在构造集合或映射时,指定一个Comparator实现类对象。这样做为开发者提供了一种精确控制如何将元素储存到排序类集和映射中的功能。Comparator接口有个方法:compare()。原型如下:
int compare(Object obj1, Object obj2)
obj1和obj2是被比较的两个对象。当两个对象相等时,该方法返回0;当obj1大于obj2时,返回一个正值;否则,返回一个负值。如果用于比较的对象的类型不兼容的话,该方法引发一个ClassCastException异常。通过覆盖compare(),可以改变对象排序的方式。例如,通过创建一个颠倒比较输出的比较方法,可以实现按逆向排序。
下面是一个说明定制的比较方法能力的例子。该例子实现compare()方法以便它按正常顺序的逆向进行操作。因此,它使得一个树集合按逆向的顺序进行存储。
范例:ComparatorDemo.java
import java.util.*; class MyComp implements Comparator<String> { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); } } public class ComparatorDemo { public static void main(String args[]) { // 创建一个TreeSet对象 TreeSet<String> ts = new TreeSet<String>(new MyComp()); // 向TreeSet对象中加入内容 ts.add("C"); ts.add("A"); ts.add("B"); ts.add("E"); ts.add("F"); ts.add("D"); // 得到Iterator的实例化对象 Iterator<String> it = ts.iterator(); // 显示全部内容 while (it.hasNext()) { System.out.print(it.next() + " "); } } }
输出结果:
F E D C B A
仔细观察实现Comparator并覆盖compare()方法的MyComp类。在compare()方法内部,用String类中的compareTo()比较两个字符串。o2.compareTo(o1)逆序、o1.compareTo(o2)正序。
下面是一个更实际的范例,是用TreeMap程序实现前面介绍的存储账目资产平衡表例子的程序。下面的程序按姓对账目进行排序。为了实现这种功能,程序使用了比较方法来比较每一个账目下姓的排序,得到的映射是按姓进行排序的。
范例:TreeMapDemo2.java
import java.util.*; class Employee implements Comparator<String> { public int compare(String a, String b) { return a.compareTo(b); } } public class TreeMapDemo2 { public static void main(String args[]) { // 创建一个TreeMap对象 TreeMap<String, Double> tm = new TreeMap<String, Double>(new Employee()); // 向Map对象中插入元素 tm.put("Z、张三", 3534.34); tm.put("L、李四", 126.22); tm.put("W、王五", 1578.40); tm.put("Z、赵六", 99.62); tm.put("S、孙七", -29.08); Set<Map.Entry<String, Double>> set = tm.entrySet(); Iterator<Map.Entry<String, Double>> itr = set.iterator(); while (itr.hasNext()) { Map.Entry<String, Double> me = itr.next(); System.out.print(me.getKey() + ": "); System.out.println(me.getValue()); } System.out.println(); double balance = ((Double) tm.get("Z、张三")).doubleValue(); tm.put("Z、张三", new Double(balance + 2000)); System.out.println("张三最新的资金数为: " + tm.get("Z、张三")); } }
输出结果:
L、李四: 126.22
S、孙七: -29.08
W、王五: 1578.4
Z、张三: 3534.34
Z、赵六: 99.62
张三最新的资金数为:5534.34
感谢阅读。如果感觉此章对您有帮助,却又不想白瞟