Java集合类概述
Java SE包含了由一组类和接口组成的Java集合框架(Java Collections Framework,简称JCF),其主要功能是用来将存储的数据以某种结构组织,并以特定的方式来访问这些数据,其目标是提供一个处理对象集合的通用框架,减少程序员处理不同对象集合时的编码量。
被添加到集合中的对象称为一个元素。有些集合类允许有重复的元素,有些则不允许。那么什么元素算是重复的呢?当两个元素通过使用equals()方法进行比较时,如果返回true值,那么这两个元素就是重复的。
集合类中的一些区别,除了它们是否支持重复元素以外,还包括元素是否有顺序,以及是否允许添加null元素。Java集合框架中根据这三个区别,将对象的存储方式分为三种类型,分别是:
- Set(集):对象容器中的对象没有顺序,且不能重复。
- List(列表):对象容器中的对象按照索引顺序排序,而且可以有重复的对象。
- Map(映射):对象容器中的元素包含一对"键对象-值对象"映射,其中键对象不能重复,值对象可以重复。
这三种对象的存储方式,对应了Java集合框架中的三个核心接口Set、List和Map。
在Set和List接口之上,定义接口Collection,用于定义存取Set和List类型容器中对象的一些通用操作,包括对象的增加、删除、遍历等。
为支持对象的排序和遍历访问操作,Java集合框架中又提供了几个接口:
- 接口SortedSet为Set类型容器提供排序功能。
- 接口SortedMap是为Map类型容器提供对键对象的排序。
- 接口Iterator提供了对集合对象进行遍历访问的遍历器。
- 接口Comparable和Comparator用来实现集合中对象的排序。
此外,为方便集合和数组的操作,Java集合框架中还提供了Collections和Arrays这两个功能强大的工具类,封装了一些复杂的数据结构算法等操作。
Collection接口和Iterator接口
在集合框架中,集合(Collection)接口位于Set接口和List接口的最顶层,是Set接口和List接口的父接口。Collection接口中定义了Collection对象共有的一些基本方法,这些方法分为基本操作、批量操作和数组操作。基本操作是针对单个元素的操作,批量操作是同时对一批元素进行操作,数组操作是将集合转化为数组的操作。通过在Collection接口中定义这些共用的方法,可以方便程序员用统一的方法来存取对象,从而简化了程序员编写对象容器时的编码,提高了开发效率。
操作分类 | 方 法 | 描 述 | |
基本操作 | int size() | 返回当前集合中包含的元素个数 | |
| isEmpty() | 判断集合中是否含有元素 | |
| boolean contains(Object o) | 判断集合中是否包含某一指定元素 | |
| boolean add(Object o) | 向集合中添加到某一元素 | |
| boolean remove(Object o) | 从集合中删除某一元素 | |
| Iterator iterator() | 返回一个遍历器,用来访问集合中的各个元素 | |
批量操作 | containsAll(Collection c) | 判断集合中是否包含某一指定集合中的全部元素 | |
| boolean addAll(Collection c) | 将另一个集合中的所有元素插入到当前集合中 | |
| boolean removeAll(Collection c) | 从集合中删除指定集合中的全部元素 | |
| boolean retainAll(Collection c) | 只保留指定集合中包含的元素,其他的删除 | |
| void clear() | 删除集合中所有元素 | |
数组操作 | Object[] toArray() | 返回一个包含集合所有元素的arr |
Iterator接口是一种用于遍历集合的接口。所谓遍历,是指从集合中取出每一个元素的过程。在Collection接口中,有一个iterator()方法,通过该方法可以返回一个Iterator对象。通过这个对象,可以遍历集合中所有元素。
表15-2 Iterator接口的方法 | |
实 现 方 法 | 描 述 |
hasNext() | 如果集合中还有更多元素,该方法返回true |
next() | 返回集合中的下一个元素 |
remove() | 删除iterator返回的最后一个元素 |
List接口
List接口继承自Collection接口,它有如下特点:
- List中的元素是有顺序的。
- List通常允许重复元素。
- List的实现类通常支持null元素。
- 可以通过索引访问List对象容器中的元素。
当我们使用List接口的实现,并返回对该List中元素的引用时,这些元素以一种可预测的序列来返回。这个序列通过集合中元素的位置来定义,在添加元素时,可以显式或者隐式地指定该位置。除了通过遍历器和foreach循环来连续访问元素外,List允许我们通过指定元素在集合中基于零的位置(也就是索引),来直接引用一个特定的元素。
表15-3 List接口中的方法 | ||
方 法 | 描 述 | |
void add(int index, Object o) | 在列表指定的位置插入对象 | |
boolean addAll(int index, Collection c) | 将另一个集合中的所有对象插入到列表指定位置 | |
Object get(int index) | 返回列表中指定位置的对象 | |
Object set(int index, Object o) | 用指定对象替换列表中指定位置的对象 | |
Object remove(int index) | 删除列表指定位置的对象 |
List接口的实现类包括AbstractList、AbstractSequentialList、ArrayList、AttributeList、CopyOnWriteArrayList、LinkedList、RoleList、RoleUnresolvedList、Stack、Vector。实际项目开发中,最常用的是ArrayList、LinkedList。
ArrayList
ArrayList在概念上与数组类似,表示一组编入索引的元素,区别之处在于ArrayList没有预先确定的大小,其长度可按需增大。
- 泛型
研究代码清单15.1,我们会发现,当我们向ArrayList集合中添加对象时,我们既可以加入Integer对象(上面代码通过add()方法添加1、2、3等数字,实际上是通过自动装箱转换为Integer对象),又可以加入String类型对象。当然,还可以加入其它不同类型的对象。
实际上,当我们把对象放入集合后,集合就会忘记这个对象的数据类型;当再次取出对象时,该对象的编译类型就变成了Object类型(当然,运行时类型是没有变化的)。Java集合之所以设计成这样,是因为设计集合的程序员是不可能知道我们需要用它来保存什么类型的对象,所以为通用性着想,他们就将集合设计成能保存任何类型的对象。但是,这样做也带来两个问题:
- 集合对元素类型没有任何限制。这样可能会引发一些问题。例如,如果我们想创建一个只能保存Employee类型的集合,但是别人一样可以把一个Integer类型的对象放入进去,所以可能会引发异常。
由于把对象放入集合后,集合丢失了对象的状态信息,集合只知道它装的是Object,所以取出来之后通常还要进行强制类型转换。这种转换既会增加编程的复杂度,也可能引发ClassCastException异常。
LinkedList
LinkedList是实现了双向链表功能的列表,它将列表中的每个对象放在独立的空间中,而且每个空间中还保存有上一个和下一个链接的索引,如图15.3所示。JDK5.0开始对LinkedList进行了改写,让它也实现了Queue接口,从而也实现了先进先出(FIFO,First In First Out)的队列数据结构的功能。
表15-4 LinkedList类的基本方法 | |
方 法 | 描 述 |
void addFirst(Object o) | 在链表开头添加一个对象 |
void addLast(Object o) | 在链表末尾添加一个对象 |
Object getFirst() | 返回链表中的第一个元素 |
Object getLast() | 返回链表中的最后一个元素 |
Object removeFirst() | 删除链表中的第一个元素 |
Object removeLast() | 删除链表最后一个元素 |
LinkedList不支持快速随机访问,如果要访问LinkedList中第n个元素,必须从头开始查找,然后跳过前面的n-1个元素。并且,虽然LinkedList也提供了一个get()方法,可以根据指定的索引来获取对应的元素,但是正因为它不支持快速随机访问,所以效率比较低下。
LinkedList和ArrayList的选择
ArrayList采用数组的方式存储对象,这种方式将对象放在连续的位置中,它有一个很大的缺点就是对它们进行删除或插入操作的时候非常麻烦。例如,如果我们要删除列表中某个元素,那么这个元素之后的其它元素都必须向前挪动。而插入元素的时候,在插入位置之后的所有元素都必须向后挪动。
LinkedList不支持快速随机访问,如果要访问LinkedList中第n个元素,必须从头开始查找,然后跳过前面的n-1个元素。所以访问LinkedList元素时,性能会比较低下。
因此,如果列表需要快速存取,但不经常进行元素的插入和删除操作,那么选择ArrayList会好一些;如果需要对列表进行频繁的插入和删除操作,那么就应该选择LinkedList。
Set接口
Set接口也继承自Collection接口,同时也继承了Collection接口的全部方法。它有如下特点:
- Set类型容器中不能包含重复元素。当加入一个元素到容器中时候,要比较元素的内容是否存在重复的,所以加入Set类型对象容器的对象必须重写equals()方法。
- 元素可能有顺序,也可能没有顺序。
- 因为元素可能没有顺序,所以不能基于索引访问Set中的元素。
实现Set接口的类包括AbstractSet、ConcurrentSkipListSet、CopyOnWriteArraySet、 EnumSet、HashSet、JobStateReasons、LinkedHashSet、TreeSet,但是最常用的是HashSet和TreeSet类。下面我们分别详细讲述HashSet和TreeSet类的使用。
1)HashSet
HashSet类是基于哈希算法的Set接口实现,它主要有如下几个特点:
- 当遍历HashSet时,其中的元素是没有顺序的。
- HashSet中不允许出现重复元素。这里的重复元素是指有相同的哈希码,并且用equals()方法进行比较时,返回true的两个对象。
允许包含null元素。
TreeSet
TreeSet类不仅实现类Set接口,还实现了SortedSet接口,从而保证集合中的对象按照一定的顺序排序。当向TreeSet集合中添加一个对象时,会把它插入到有序的对象序列中。但是这种排序并不是按照对象添加的顺序排序,而是按照一定的算法来排序。
表15-5 TreeSet类的基本方法 | |
方 法 | 描 述 |
Object first() | 返回TreeSet中第一个元素 |
Object last() | 返回TreeSet中最后一个元素 |
Object pollFirst() | 获取并删除第一个元素 |
Object pollLast() | 获取并删除最后一个元素 |
Object higher(Object o) | 返回此集合中严格大于给定元素的最小元素 |
Object lower(Object o) | 返回此集合中严格小于给定元素的最大元素 |
SortedSet headSet(Object o) | 返回集合的部分视图,其元素严格小于指定的元素o |
SortedSet tailSet(Object o) | 返回集合的部分视图,其元素大于指定的元素o |
TreeSet使用使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。也就是说,TreeSet支持自然排序和自定义排序两种排序方式。默认情况下,TreeSet采用自然排序方式。
我们首先来看看什么是TreeSet的自然排序。
在JDK类库中,有一部分类实现了Comparable接口,例如Integer、Double、String等等。Comparable接口在java.lang包中,该接口有一个compareTo(Object o)方法,返回整型数据。对于表达式x.compareTo(y),如果返回值为0,则表示x和y相等;如果返回值大于0,则表示x大于y;如果返回值小于0,则表示x小于y。TreeSet集合调用对象的compareTo()方法比较集合中的大小,然后进行升序排列,这种方式称为自然排序。
对于JDK中常用类,例如Double、Integer、Long、Short、Float等,是按照数字的大小排序。而Character类,按照Unicode值的数字大小进行排序。String对象,按照字符串中字符的Unicode值排序。必须注意的是,使用自然排序时,只能向集合中加入同类型的对象,并且这些对象的类必须实现了Comparable接口,否则程序会抛出异常。