集合框架概述
好了,还是不感慨人生了,步入正题,本篇文章是 Java 集合框架的第一篇,从这篇开始,我们将一起来学习一下关于 Java 中集合的一些知识,集合是我们在 Java 编程中相当常用的一个数据结构集。在看这个集合系列之前,希望你对 Java 中一些常见的集合有初步的了解,这样的话这个系列对你来说就没有很大的难度了,当然,如果你没有任何关于集合的基础也没有关系,我会尽力将知识点写的简单一些。
整个集合框架大体上可以分为三种:线性集合类型(List 接口:ArrayList、LinkedList…) 、集合类型(Set 接口:HashSet、TreeSet…)、映射类型(Map 接口:HashMap、TreeMap…)。整个 Java 集合框架大致可以用以下图来表示:
在原图的基础上,我在最上面添加了一个 Iterable 接口,并且 Collection 接口虚线指向它,证明 Collection 接口继承了 Iterable 接口。我为什么要特意加上这个接口呢?我想大家都应该用过 Java 中的 for each 语句吧。不知道大家有没有想过为什么对于一些数据结构(数组、ArrayList 等)可以使用 for each 语句去遍历它,其实就是通过这个 Iterable 接口来实现的,在这个接口中有一个用于产生 Iterator (迭代器)对象的 iterator() 方法:
/**
- Returns an iterator over elements of type {@code T}.
- @return an Iterator.
*/
Iterator iterator();
可以看到这个方法返回一个 Iterator 的泛型对象,Iterator 也是一个接口,也就是迭代器,其作用就是提供了统一的方法接口来方便我们遍历容器。即我们可以通过一个集合提供的迭代器对象来遍历这个集合中的元素。同样的我们把提供了迭代器遍历元素的对象称为可迭代对象。如果你记得设计模式中的相关知识的话会发现 Collection 接口在元素遍历的设计上采用迭代器的设计模式,我们来具体看看:
迭代器 Iterator
我们来看看这个接口的定义:
public interface Iterator {
/**
* 如果这个可迭代对象(集合)中还有下一个元素,那么返回 true,否则返回 false
*/
boolean hasNext();
/**
* 返回可迭代对象(集合)中的下一个元素,
* 如果没有下一个元素,方法应该抛出一个 NoSuchElementException 异常
*/
E next();
/**
* 移除集合中最后一次访问的(最后一次调用 next 方法得到的)元素,
* 如果这个方法在第一次调用 next 方法之前调用,或者被连续调用,
* 那么方法应该抛出一个 IllegalStateException 异常,
* 默认实现是抛出一个 UnsupportedOperationException 异常,即不支持的操作
*/
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
当我们得到了一个集合对象提供的 Iterator 对象之后,一个典型的遍历这个集合的对象元素的代码块就是:
Iterator it = obj.iterator();
// 如果集合对象有下一个元素,就遍历元素
while(it.hasNext()) {
// 得到并打印出集合的下一个元素
System.out.print(it.next().toString() + " ");
}
回到上面的框架图,我们可以看到,List 接口和 Set 接口都是直接继承了 Collection 接口,那么就意味着线性集合类型 (List)和集合类型(Set)中的元素都是可以通过 for each 语句来进行遍历的,而对于 Map 接口来说,其并没有继承 Iterable 接口,因此对于映射类型,我们不能直接通过 for each 进行遍历。当然,对于映射类型元素的遍历,我们另有方法,在之后的文章中我们再一起探讨。在上图中我们还注意到对于 List 接口来说,它是可以产生一个 ListIterator 对象的,而这个接口也是继承于 Iterator 接口,我们可以看看这个接口中多了什么方法:
public interface ListIterator extends Iterator {
boolean hasNext();
E next();
/**
* 判断当前元素之前是否还有元素,即类似于反方向的 hasNext 方法
*/
boolean hasPrevious();
/**
* 返回前一个元素,如果前面没有元素,那么抛出 NoSuchElementException 异常,
* 类似于反方向的 next 方法
*/
E previous();
/**
* 下一个元素在集合中的下标位置
*/
int nextIndex();
/**
* 前一个元素在集合中的下标位置
*/
int previousIndex();
/**
* 移除集合中上一次调用 next 方法所返回的元素
*/
void remove();
/**
* 将上一次通过 next 方法 / previous 方法访问的元素替换为参数指定的对象
*/
void set(E e);
/**
* 添加一个元素到上一次通过 next 方法访问的元素之后,
* 操作完成后,通过 previous 方法可以访问到该元素
*/
void add(E e);
}
这个接口提供了更加符合 线性结构类型 特点的方法,即提供了向前访问和向后访问两种方式,同时提供了插入元素和修改元素的方法。
为了更加深入的理解 Iterable 接口和迭代器,这里举一个小例子,用自定义的类来实现 Iterable 接口和 Iterator 接口,从而我们可以通过 for each 语句和迭代器来遍历类对象中的元素,新建一个类 ForEachTest.java:
import java.util.Iterator;
/**
-
一个类要支持 for each 语句,必须实现 Iterable 接口,
-
同时在 iterator() 方法中返回一个 Iterator 对象,
-
一种常用方法是这个类本身也实现 Iterator 接口,这样的话既可以使用 for each 语句,
-
在 iterator() 方法中直接返回 this 的引用即可,也可以使用迭代器形式遍历和访问数据
*/
public class ForEachTest implements Iterable, Iterator {Object[] arr;
// 当前访问的元素所在数组下标
int curIndex = -1;
// 最后一个调用 next 方法所得元素所在数组下标
int previousIndex = Integer.MIN_VALUE;public ForEachTest(int size) {
arr = new Object[size];
}// 重置迭代器状态
public void resetIterator() {
curIndex = -1;
previousIndex = Integer.MIN_VALUE;
}@Override
public boolean hasNext() {
return curIndex < arr.length - 1;
}@Override
public E next() {
if (curIndex >= arr.length - 1) {
throw new IllegalStateException(“index is out of bound of array”);
}
return (E) arr[++curIndex];
}@Override
public void remove() {
// 如果当前没有元素可以移除,或者连续调用了多次 remove 方法,抛出异常
if (curIndex < 0 || curIndex >= arr.length || previousIndex == curIndex) {
throw new IllegalStateException(“there is not any element can be remove!”) ;
}
previousIndex = curIndex;
Object[] lastArr = arr;
int j = 0;
arr = new Object[arr.length - 1];
for (int i = 0; i < lastArr.length; i++) {
if (i != previousIndex) {
arr[j++] = lastArr[i];
}
}
// 修正访问的元素位置
previousIndex = --curIndex;
lastArr = null;
}/**
- 类本身实现了 Iterator 接口,因此这个方法返回 this 即可
*/
@Override
public Iterator iterator() {
return this;
}
public static void main(String[] args) {
ForEachTest test = new ForEachTest<>(10);
for (int i = 0; i < 10; i++) {
test.arr[i] = i + 1;
}
System.out.println(“通过 for each 语句遍历:”);
for (Integer e : test) {
System.out.print(e + " “);
}
// 因为 for each 语句已经将迭代器移动到自定义集合尾部了,
// 不重置迭代器位置将无法访问到元素,
// 因此需要重置迭代器位置,
// 这恰恰也证明了 for each 语句也是通过迭代器来运行的
test.resetIterator();
System.out.println(”\n通过迭代器遍历:");
Iterator it = test.iterator();
while (it.hasNext()) {
System.out.print(it.next() + " “);
it.remove();
}
System.out.println(”\n最后的集合数组长度:" + test.arr.length);
}
} - 类本身实现了 Iterator 接口,因此这个方法返回 this 即可
这里用一个 Object 类型的数组来模拟集合,说实话这确实是有点多此一举了,数组本身就通过 for each 语句遍历元素,但这里仅仅是为了演示 for each 语句的原理和迭代器接口的使用,我们来看看结果:
使用迭代器的好处之一是迭代器给我们提供了统一的接口来遍历实现了迭代器接口的类的对象,实现了遍历集合方法的复用,减少我们的代码量,举个例子来说:我们可以专门写一个通过迭代器遍历集合对象的方法:
public static void traversalByIterator(Iterator it) {
if (it == null) {
return ;
}
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
}
// 或者写成这样(Collection 接口继承于 Iterable 接口):
public static void traversalByIterable(Iterable iterable) {
if (it == null) {
throw new IllegalArgumentException(“iterable object can not be null!”);
}
traversalByIterator(iterable.iterator());
}
而在调用这个方法时,我们可以传入 List 、Set 、我们在上面实现的自定义的类 ForEachTest 对象的迭代器(这会调用上面写的第一个方法),当然,你也可以直接传入对应的 List、Set 对象(这会调用上面写的第二个方法)。具体怎么用得看程序的需求了:
ArrayList arr = new ArrayList();
…
traversalByIterator(arr.iterator());
// 也可以这样调用:
traversalByIterator(arr);
HashSet set = new HashSet();
…
traversalByIterator(set.iterator());
// 也可以这样调用:
traversalByIterator(set);
ForEachTest test = new ForEachTest(10);
…
traversalByIterator(test.iterator());
// 也可以这样调用:
traversalByIterator(test);
即实现了同一个方法遍历多种类型对象。这就是采用迭代器这种设计模式的好处,实现多处代码复用。好了,关于迭代器的一些知识就介绍到这里了。
最后小结一下:自定义类要使用 for each 语句必须实现 Iterable 接口,并且在 iterator 方法中返回一个 Iterator 迭代器对象,for each 语句本身也是通过对应类提供的 Iterator 对象来实现对元素的遍历 。
而对于要使用迭代器遍历元素的类,其必须实现 Iterator 接口并重写对应的方法。
集合方法概述: Collection 接口
从最基础的看起,我们先看一下 Collection 接口中的一些常用方法:
public interface Collection extends Iterable {
// Query Operations
/**
* 返回集合中元素的个数,不大于 Integer.MAX_VALUE
*/
int size();
/**
* 判断集合是否为空并且返回判断结果
*/
boolean isEmpty();
/**
* 判断对象 o 是否存在于集合中,返回判断结果(true / false),
* 方法应该通过 o.equals(e) 来判断 o 是否和 e 相等(o 不为 null 的前提下),
* e 代表集合中的每个元素
*/
boolean contains(Object o);
/**
* 返回当前集合的迭代器对象
*/
Iterator<E> iterator();
/**
* 返回一个数组对象,包含了集合中所有的元素,
* 数组中元素的遍历顺序应该和通过迭代器遍历集合的顺序一致
*/
Object[] toArray();
/**
* 该方法返回一个数组对象,包含了集合中所有的元素,
* 如果参数指定的数组容量不小于容器中元素的数量,那么将集合中的元素复制到该数组中,
* 否则新建一个数组,长度为容器元素的数量,将容器元素复制到该数组中并返回新建的数组
* 如果参数为 null,那么抛出 NullPointerException 异常,
* 如果参数数组的类型不是容器储存元素类型的父类型,那么抛出 ArrayStoreException 异常
*/
<T> T[] toArray(T[] a);
// Modification Operations
/**
* 向当前集合中添加新的元素,添加成功返回 true,失败返回 false,
* 这是一个泛型定义方法,针对不同的具体集合类应该有不同的处理方式
*/
boolean add(E e);
/**
* 移除一个当前集合中等价于 o 的元素(通过 equals 方法判断等价),
* 返回结果为当前集合元素是否改变(移除成功返回 true,移除失败或者等价元素未找到返回 false)
*/
boolean remove(Object o);
// Bulk Operations
/**
* 判断参数 c 集合中的所有元素是否包含在当前集合中
*/
boolean containsAll(Collection<?> c);
/**
* 将参数 c 集合中的所有元素添加到当前集合中,
* 此方法是一个泛型定义,针对不同具体的集合类应该有不同的处理
*/
boolean addAll(Collection<? extends E> c);
/**
* 移除当前集合中同时包含在集合 c 中的元素,即移除当前集合中和集合 c 中的交集元素
*/
boolean removeAll(Collection<?> c);
/**
* 移除当前集合中所有满足参数所指定条件的元素,
* 如果参数为 null,那么抛出一个 NullPointerException 异常
* @since 1.8
*/
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
/**
* 求出参数 c 集合中元素和当前集合元素的交集,
* 即将所有在当前集合中的元素
*/
boolean retainAll(Collection<?> c);
/**
* 清除容器中的所有元素,方法返回后,容器将为空
*/
void clear();
// Comparison and hashing
/**
* 判断当前集合是否和参数 o 等价
*/
boolean equals(Object o);
/**
* 返回该集合对象的 hashCode
*/
int hashCode();
}
可以看到,Collection 接口提供了线性类型和集合类型的集合的通用接口方法(请注意,Map 接口 和 Collection 接口没有继承关系),随着继承关系越到下面,集合方法特性越明显。
List 接口
我们接下来看看 List 接口中新增的常用方法:
public interface List extends Collection {
// ......
/**
* 对集合元素进行排序的方法,通过传入 Compartor 参数指定排序规则
*/
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
/**
* 获取参数指定下标的元素,如果下标越界,抛出一个 IndexOutOfBoundsException 异常
*/
E get(int index);
/**
* 将参数 element 替换集合中下标为 index 所指向的元素,
* 如果 element 为 null 并且集合不允许存在 null 元素,抛出一个 NullpointException 异常
* 如果下标越界,那么抛出一个 IndexOutOfBoundsException 异常
*/
E set(int index, E element);
/**
* 在 index 下标中插入一个新的元素 element,
* 如果 element 为 null 并且集合不允许存在 null 元素,抛出一个 NullpointException 异常
* 如果下标越界,抛出一个 IndexOutOfBoundsException 异常
*/
void add(int index, E element);
/**
* 移除集合中指定下标所在的元素,如果下标越界,抛出一个 IndexOutOfBoundsException 异常
*/
E remove(int index);
/**
* 求出 o 对象所在当前集合的下标(通过 equals 方法判断等价),
* 如果 o为 null 并且集合不允许存在 null 元素,抛出一个 NullpointException 异常
* 如果没有找到,那么返回 -1
*/
int indexOf(Object o);
/**
* 求出 o 对象所在当前集合的最后一个等价元素的下标(通过 equals 方法判断等价),
* 如果 o为 null 并且集合不允许存在 null 元素,抛出一个 NullpointException 异常
* 如果没有找到,那么返回 -1
*/
int lastIndexOf(Object o);
/**
* 返回一个 ListIterator 迭代器对象,初始时迭代器指向集合开头
*/
ListIterator<E> listIterator();
/**
* 返回一个指向参数 index 指定下标的 ListIterator 迭代器对象
*/
ListIterator<E> listIterator(int index);
/**
* 返回一个子 List 对象,范围为当前集合中的 [fromIndex, toIndex) 所包含的元素
*/
List<E> subList(int fromIndex, int toIndex);
}
很明显,List 接口在 Collection 接口上加入了一些更符合 / 适用于 线性结构 的方法,确实是一个很好的设计思想。
直接 / 间接实现了 List 接口的具体类有:Vector 、Stack、ArrayList、LinkedList。关于它们的用法和一些更加详细的东西,将会在之后的篇幅介绍。
Set 接口中存在的方法都是 Collection 接口中已经声明的,那么为什么 Set 接口中没有新增的方法呢?
我们联想一下 List 接口,其代表的是 线性结构类型,从数据结构中我们可以知道:线性结构之间元素和元素之间可以有线性连接关系,即每一个元素可以有直接前驱元素或者直接后继元素,也可以同时含有两者。那么我们可以通过一个元素访问到其下一个元素 / 上一个元素,而对于线性表(使用数组模拟)来说,这个特性更加明显。我们都直接可以通过下标来得到对应元素,因此通过线性结构的这种特性,我们可以新增一些更加具体的方法比如说获取线性结构中指定下标的元素(get(int index))等。而同样的,对于线性结构新设计一种迭代器(ListIterator),来满足对线性结构集合遍历的更具体的需求也自然是可行的。
我们回到 Set ,即集合,高中数学中我们知道集合有三个特性:无序、有限、无重复,对于普通的集合来说,也就仅仅这三个特性,但是这三个特性并不能提供类似于不同集合元素之间位置关系的依据。映射到我们正在学习的集合框架来说,这三个特性已经在 Collection 接口中提供了对应的方法,因此 Set 接口中没有提供另外的方法。那么为什么还要多写这么一个接口呢?只能说是为了规范和良好的扩展性吧。
来看一下直接 / 间接实现了 Set 接口的具体类:HashSet、TreeSet、LinkedHashSet,关于它们的具体用法同样会在后面的篇幅介绍。
Map 接口
最后来看一下 Map 接口,我们知道,map 即为映射,其提供了两种不同类型的数据对象进行相互关联的能力,类似于数学中的映射关系,一个对一个。比如用映射来储存员工的 ID 和姓名信息,将每个员工的两种信息建立键值对关系,那么在需要获取某个员工的姓名的时候直接通过其 ID 来对映射关系进行查找即可。此时,映射关系中的员工 ID 即为键,姓名为值,两者形成键值对映射关系。
在 Java 中,通过 Map.Entry 接口来描述这种类型的元素,我们来看看这个接口在 Map 接口中的定义:
/**
* Entry 接口代表一个 key-value 对(键值对),形成的数据结构,即为映射元素,
* 这个接口为 Map 接口中的子接口,
* 泛型 K 代表键的类型,泛型 V 代表值的类型
*/
interface Entry<K,V> {
/**
* 返回当前键值对中的 键 对象,
* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)
*/
K getKey();
/**
* 返回当前键值对中的 值 对象,
* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)
*/
V getValue();
/**
* 设置当前键值对中的 值 对象,
* 如果设置的值参数对象为 null,抛出一个 NullpointException 异常(可选),
* 如果设置的值参数对象不能转换为当前键值对中对应的 值 类型,抛出一个 ClassCastException 异常,
* 如果当前键值对不在对应的 Map 中,抛出一个 IllegalStateException 异常(可选)
*/
V setValue(V value);
/**
* 如果参数对象和当前键值对等价,那么返回 true,否则返回 false,一般可以通过以下代码实现:
* <pre>
* (e1.getKey()==null ?
* e2.getKey()==null : e1.getKey().equals(e2.getKey())) &&
* (e1.getValue()==null ?
* e2.getValue()==null : e1.getValue().equals(e2.getValue()))
* </pre>
*/
boolean equals(Object o);
/**
* 返回当前键值对的 hashCode ,用于 Map 中形成数组下标值,一般可以通过以下代码实现:
* <pre>
* (e.getKey()==null ? 0 : e.getKey().hashCode()) ^
* (e.getValue()==null ? 0 : e.getValue().hashCode())
* </pre>
* 设计 hashCode 方法时,确保当两个对象的 equals 方法返回 true 时,
* 这两个对象的 hashCode 方法返回值相同
*/
int hashCode();
// ......
这个接口提供了一些方法,用于描述一个 键值对 的行为,即通过这些方法来获取 / 设置键值对的相关信息。
而在 Map(HashMap、LinkedHashMap…) 中正是通过实现了这个接口的类对象来储存键值对的信息。
关于具体是怎么做的,我们将在之后的篇幅中继续探讨。
下面来看一下 Map 接口中的常用方法:
public interface Map<K,V> {
/**
* 返回当前映射的元素数量,不大于 Integer.MAX_VALUE
*/
int size();
/**
* 判断当前映射是否为空并返回结果
*/
boolean isEmpty();
/**
* 判断参数所代表的键是否存在当前映射的键值对元素中,
* key 允许为 null(某些映射例如 HashMap 允许键为 null),
* 对于 key 不为 null 的情况,通过 equals 方法来判断键是否等价,
*/
boolean containsKey(Object key);
/**
* 判断参数所代表的键是否存在当前映射的键值对元素中,
* value 允许为 null(某些映射例如 HashMap 允许值为 null),
* 对于 value 不为 null 的情况,通过 equals 方法来判断值是否等价,
*/
boolean containsValue(Object value);
/**
* 获取键所对应的值对象,对于 null,不同的 Map 实现类有不同的处理方式
*/
V get(Object key);
/**
* 在映射中插入新的关系,如果 key 已经在映射中某个 Entry 对象中存在(等价),
* 那么相当于更新 key 所对应的 value 对象,对于 null,不同的 Map 实现类有不同的处理方式
*/
V put(K key, V value);
/**
* 移除参数所对应的的键值对映射关系,返回移除的映射关系中的值,
* 如果 key 在当前映射中不存在,则返回 null,
* 对于 key 为 null 的情况,不同的 Map 实现类有不同的处理方法
*/
V remove(Object key);
/**
* 将参数所代表的映射关系复制一份到当前的映射中,
* 等价于对于每一个 m 中的映射键值对关系,
* 调用当前映射的 put(key, value) 方法(key、value 都是 m 中的键、值)
*/
void putAll(Map<? extends K, ? extends V> m);
/**
* 清除当前映射中的所有键值对对应关系
*/
void clear();
/**
* 返回一个包含了当前映射中所有的键对象的集合类型对象
*/
Set<K> keySet();
/**
* 返回一个包含了当前映射中所有的值对象的集合对象
*/
Collection<V> values();
/**
* 返回一个包含了所有键值对对象的集合类型对象,
* 通过 for each 语句或者迭代器来遍历集合类型对象,
* 从而完成对当前映射中所有键值对元素的遍历
*/
Set<Map.Entry<K, V>> entrySet();
/**
* 调用这个方法,可以用类似于 for each 语句的形式来遍历当前映射对象中的每一个键值对
* 方法内部还是通过遍历当前映射对的 entry 集合来实现遍历映射中的所有键值对
* @since 1.8
*/
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
// ......
/**
* 移除映射中键、值分别和参数 key、value 等价的键值对,等价于调用以下代码:
* <pre> {@code
* if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
* map.remove(key);
* return true;
* } else
* return false;
* }</pre>
* @since 1.8
*/
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
/**
* 将映射中 key、oldValue 所在的键值对中的值替换为 newValue,
* 如果替换成功,返回 true,否则返回 false
* @since 1.8
*/
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
/**
* 将映射中 key 所在的键值对中的值替换为 value,
* 如果 key 不在映射的键值对关系中,那么返回 null
* @since 1.8
*/
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
// ......
}