Java最初的版本只为最常用的数据结构提供了很少的一组类:Vector、Stack、Hashtable、BitSet和Enumeration接口,其中的Enumeration接口提供了一种用于访问任意容器中各个元素的抽象机制。
Java集合类库将接口与实现分离。在Java类库中,集合类的基本接口是Collection接口。该接口有两个基本方法:add和iterator。add方法用于向集合添加元素。如果添加元素确实改变了集合就返回true;如果集合没有发生变化就返回false。iterator方法用于返回一个实现了Iterator接口的对象。可用使用这个迭代器对象依次访问集合中的元素。
Iterator接口包含4个方法:next(),hasNext(),remove(),forEachRemaining(Consumer<? super E> action)。有关的这个4个方法的API文档如下:
Java集合类库中的迭代器与其他类库中的迭代器在概念上有着重要的区别。传统的集合类库中,如C++的标准模板库,迭代器是根据数组索引建模的。这样一个迭代器可以查找存储在指定位置上的元素,如果不需要查找元素,也可以将迭代器向前移动。但是,Java的迭代器不是这样。查找操作与位置变更紧密耦合。查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置就会随之向前移动。因此,可以认为Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回越过的那个元素的引用。效果图如下所示:
需要特别注意,next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调用next,将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。
因为Collection与Iterator都是泛型接口,所以可以自行编写处理任何集合类型的实用方法。Collection接口的一些常用方法的API文档如下:
另外还有一个很有用的方法,方法签名如后所示: default boolean removeIf(Predicate<? super E> filter)。这个方法用于删除满足某个条件的元素。
Java集合框架为不同类型的集合定义了大量接口,如下所示:
集合有两个基本接口:Collection和Map。List是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引访问。后面这种方法称为随机访问,因为这样可以访问任意顺序的元素。与之不同,使用迭代器访问时,必须顺序地访问元素。
为了避免对链表完成随机访问操作,Java 1.4引入了一个标记接口RandomAccess。这个接口不包含任何方法,不过可以用它来测试一个特定的集合是否支持高效的随机访问。
如下展示了Java类库中的集合,除了以Map结尾的类之外,其他类都实现了Collection接口,而以Map结尾的类实现了Map接口。
下图显示了这些类之间的关系:
在Java中,所有链表实际上都是双向链接的(doubly linked)——即每个链接还存放着其前驱的引用。效果图如下所示:
链表是一个有序集合,每个对象的位置都十分重要。使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素的开销。有关链表的API文档如下:
集合类库提供一种都很熟悉的ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态在分配的对象数组。ArrayList方法不是同步的,与之不同的是Vector。Vector类的所有方法的都是同步的,可以安全地从两个线程访问一个Vector对象。
散列表为每个对象计算一个整数,称为散列码(hashcode)。散列码是由对象的实例字段得出的一个整数。更准确地说,有不同数据的对象将产生不同的散列码。在Java中,散列表用链表数组实现,每个列表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是存在这个元素的桶的索引。当把一个元素插入桶中时,如果遇到桶已经被填充了的情况。这种现象称为散列冲突(hash collision)。这时,需要将新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。在Java 8中,桶满时会从链表变为平衡二叉树。可以通过指定一个初始的桶数,以便能够更多地控制散列表的性能。桶数是指用于收集有相同散列值的桶的数目。如果散列表太满,就需要再散列(rehashed)。装填因子可以确定何时对散列表进行再散列。集是没有重复元素的元素集合。在更改集中的元素时要格外小心。如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。Java集合类库通过一个HashSet类,它实现了基于散列表的集。相关的API 如下:
树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,值将自动地按照排序后的顺序呈现。要使用树集,必须能够比较元素。这些元素必须实现Comarable接口,或者构造集时必须提供一个Comarable。每次将一个元素添加到树中时,都会将其放置在正确的排序位置上。因此,迭代器总是以有序的顺序访问每个元素。将一个元素添加到树中要比添加到散列表满,但是,与检查数组或链表中的重复元素相比,使用树会快很多。树的排序顺序必须是全序的。也就是说,任意两个元素都必须是可比的,并且只有在这两个元素相等时结果才为0。从Java 6起,TreeSet类实现了NavigableSet接口。这个接口增加了几个查找元素以及反向遍历的便利方法。有关树集TreeSet的API文档如下:
队列允许使用者高效地在尾部添加元素,并在头部删除元素。而双端队列(即deuqe)允许在头部和尾部都高效地添加或删除元素。不支持在队列中间添加元素。Java 6中引入了Deque接口,ArrayDeque和LinkedList类实现了这个接口。这两个类都可以提供双端队列,其大小可以根据需要扩展。相关的API如下:
优先队列(priority queue)中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。不过,优先队列并没有对所有元素进行排序。优先队列使用了一个精巧且高效的数据结构,称为堆(heap)。堆是一个可以自组织的二叉树,其添加和删除操作可以让最小的元素移动到根,而不必花费时间对元素进行排序。优先队列的相关API如下:
映射用来存放键/值。如果提供了键,就能查找到值。Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都是实现了Map接口。散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。与键关联的值不进行散列或比较。每当往映射添加一个对象时,必须同时提供一个键。要想检索一个对象,必须使用(因而,必须记住)键。键必须是唯一的。不能对同一个键存放两个值。映射的相关API如下所示:
更新映射条目相关的API如下:
集合框架不认为映射本身是一个集合(其他数据结构框架认为映射是一个键/值对集合,或者是按键索引的集合)。不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。有3种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。映射视图相关的API如下:
需要说明的是,keySet不是hashSet或TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collection接口。因此,可以像使用任何集合一样使用keySet。在键集视图上调用迭代器的remove方法,实际会从映射中删除这个键和它关联的值。不能向键集视图中添加元素。另外,如果添加一个键而没有同时添加值也是没有意义的。如果试图调用add方法,它会抛出一个UnsupportedOperationException。映射条目集视图也有同样的限制。WeakHashMap使用弱引用(weak references)保存键。与弱散列映射、链接散列集与映射、枚举集与映射和标识散列映射相关的API如下所示:
Java9引入了一些静态方法,可以生成给定元素的集或列表,以及给定键/值对的映射。相关API如下所示:
可以为很多集合建立子范围(subrange)视图。可以对子范围应用任何操作,而且操作会自动反映到整个列表。对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围。相关的API如下:
不可修改的视图对现有集合增加了一个运行时检查。如果发现视图对集合进行修改,就会抛出一个异常,集合仍保持不变。不可修改的视图并不是集合本身不可更改,仍然可以通过集合的原始引用对集合进行修改,并且仍然可以对集合的元素调用更改器方法。由于视图只是包装了接口而不是具体的集合对象,所以只能访问接口中定义的方法。“检查型”视图用来对泛型可能出现的问题提供调试支持。相关API以及对同步视图时所涉及的方法API如下所示:
泛型集合接口有一个很大的优点,即算法只需要实现一次。排序与混排方法的API如下:
二分查找API如下:
注意,集合必须是有序的,否则算法会返回错误的答案。如果方法返回一个非负的值,这表示匹配对象的索引,如果返回负值,则表示没有匹配的元素。
Collections类中一些简单算法API如下:
Java集合框架中的遗留类,如下所示:
枚举:遗留的集合中使用Enumeration接口遍历元素序列。API如下:
属性映射(property map)是一个特殊类型的映射结构。它有以下3个特性:
1)键与值都是字符串。
2)这个映射可以很容易地保存到文件以及从文件加载。
3)有一个二级表存放默认值。
实现属性映射的Java平台类名为properties。相关的API如下:
栈,遗留的集合中Stack API文档如下:
Java平台的BitSet类用于存储一个位序列(它不是数学上的集,如果称为位向量或位数组可能更合适)。如果需要高效地存储位序列(如标志),就可以使用位集。C++中的bitset模板与Java中的BitSet功能一样。相关的API如下: