在大学时,我们肯定都学过一门叫数据结构的课,里面详细的介绍了链表、栈、队列、散列、树、图等数据结构的概念及其实现。接下来的几篇文章将详细介绍一下Java API对常见数据结构的实现原理及应用,也就是Java API中的集合类(集合框架)。首先看一下Java集合框架的总体概况,如下:
总体而言,基本上可以分为两类,表示集合的Collection和表示K-V的Map。可以看到,所有的集合都间接实现了Collection接口,所有的Map都实现了Map接口。
1. 集合接口
1.1 Java集合设计思想
从上面集合框架的图中也可以发现一个规律,所有的具体集合类都会实现一个或多个接口,这其实就是Java API设计中,接口(interfaces)与实现(implementations)分离的思想。下面通过队列(queue)分离设计的例子,来解释一下这种实现思想的细节及优势。
我们都知道,队列是一种“先进先出”的数据结构,并且入队操作只能在队尾,出队操作只能在队头。所以队列接口应该提供方法,在队列尾部添加元素,在队列头部删除元素,并且可以查看队列中元素的个数,如下所示:
public interface Queue<E> {
void add(E element);
E remove();
int size();
}
接口定义中,并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组,另一种是使用链表,如下所示:
public class CircularArrayQueue<E> implements Queue<E> {
public CircularArrayQueue(int capacity) {……}
public void add(E element) {……}
public E remove() {……}
public int size() {……}
private E[] elements;
private int head;
private int tail;
}
public class LinkedListQueue<E> implements Queue<E> {
public LinkedListQueue() {……}
public void add(E element) {……}
public E remove() {……}
public int size() {……}
private Link head;
private Link tail;
}
注意:这里讲的CircularArrayQueue和LinkedListQueue并不是Java API中真是存在的类,这里只是为了讲述一下Java集合的设计思想——接口和实现分离。实际上在Java API中,如果要使用一个循环数组队列,就可以使用ArrayDequeue,如果需要使一个链表队列,就可以使用LinkedList,这个类实现了Queue接口。
有如上两种队列的定义,在使用队列时,根据泛型类的继承规则,可以将实现类对象赋值给相同泛型类型的接口变量引用,如下:
Queue<Integer> queue = new CircularArrayQueue<>(100);
queue.add(1);
如果一旦改变了想法,就可以轻松的使用另一种不同的实现,只要对程序的一个地方做出修改,即修改调用构造器的地方就可以。如果觉得LinkedListQueue是个更好的选择,只需要将代码改为:
Queue<Integer> queue = new LinkedListQueue<>(100);
queue.add(1);
具体选用哪种实现,要根据具体需求而定。接口本身并不能说明哪种实现效率高,哪种实现效率低。循环数组队列一般情况下比链表队列效率高,但是循环数组是一个有界集合,容量有限。如果程序中要收集的对象没有上限,最好使用链表队列。
另外,仔细观察上面的集合框架图,也可以发现一个规律,一个具体的类一般会继承一个Abstract打头的抽象类,实现一个(多个)接口。这些Abstract打头的抽象类,其实也实现了相同的接口,并对接口中的一部分方法进行了实现。这些抽象类就是为类库实现者更方便的实现接口而设计的,比如如果我们要实现一个自定义队列,那么通过继承AbstractQueue类同时实现Queue接口的方式,要比直接实现Queue接口的方式要方便的多,因为一些通用方法已经在AbstractQueue中实现了。
1.2 Collection接口
所有的具体集合类都直接或间接实现了Collection接口,Collection接口定义了集合的一些通用方法,如下:
S.N. | 方法 | 说明 |
---|---|---|
1 | boolean add(E e) | 向集合中添加元素,如果调用改变了集合返回true |
2 | boolean addAll(Collection<? extends E> c) | 将集合c中所有元素添加到集合中,如果调用改变了集合返回true |
3 | void clear() | 清空集合 |
4 | boolean contains(Object o) | 判断集合中是否存在一个与o相等的元素,存在返回true |
5 | boolean containsAll(Collection<?> c) | 判断集合是否包含集合c中的所有元素,包含返回true |
6 | boolean equals(Object o) | equals方法 |
7 | int hashCode() | hashCode |
8 | boolean isEmpty() | 判断集合是否为空,为空返回true |
9 | Iterator<E> iterator() | 返回集合迭代器 |
10 | default Stream<E> stream() | Java 8新方法,返回集合流,用于支持集合函数式编程 |
11 | default Stream<E> parallelStream() | Java 8新方法,返回集合并行流 |
12 | boolean remove(Object o) | 删除集合中等于o的元素,如果有元素被删除,返回true |
13 | boolean removeAll(Collection<?> c) | 从集合中删除另一个集合c中的所有元素,如果调用改变了集合返回true |
14 | default boolean removeIf(Predicate<? super E> filter) | Java 8新方法,如果集合中的元素满足Predicate,则删除元素,如果调用改变了集合返回true |
15 | default void replaceAll(UnaryOperator<E> operator) | Java 8新方法,根据UnaryOperator规则,将集合中每一个元素映射为另一个元素 |
16 | boolean retainAll(Collection<?> c) | 从集合中保留另一个集合c中的所有元素,如果调用改变了集合返回true |
17 | int size() | 返回集合中元素的个数 |
18 | default Spliterator<E> spliterator() | Java 8新方法,返回集合的Spliterator迭代器 |
19 | Object[] toArray() | 集合转数组 |
20 | <T> T[] toArray(T[] a) | 集合转数组 |
上面讲过,Java集合框架中,一个接口往往会对应一个抽象类,实现一些接口中的通用方法。Collection接口也不例外,在Java API中存在一个叫AbstractCollection的抽象类,提供一些基于迭代器Iterator的contains、remove、removeAll等方法的实现,这样就能让实现者更容易的实现Collection接口。如下:
public abstract class AbstractCollection<E> implements Collection<E> {
public abstract Iterator<E> iterator();
//……
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}
//……
}
1.3 迭代器Iterator
Java API中迭代器接口定义如下:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
1.3.1 遍历元素
通过反复调用next方法,可以逐个访问集合中的每一个元素。但是到达集合的末尾,next方法将抛出一个noSuchElementException异常。因此在调用next之前要调用hasNext方法,判断是否存在下一个元素。如果要遍历一个集合中的所有元素,就可以请求一个迭代器,并在hasNext返回true时反复地调用next方法,如下:
Collection<String> c = …… ;
Iterator<String> iterator = c.iterator();
while(c.hasNext()) {
String element = iterator.next();
//处理element
}
Java 5之后,这种循环可以采用一种更优雅的方式进行遍历——for each循环。Java编译器会简单地将“for each”循环翻译为带有迭代器的循环。如下:
for (String element : c) {
//处理element
}
1.3.2 删除元素
Iterator接口的remove方法用于删除上次调用next方法时返回的元素。通过如下方式,可以删除集合中第一个元素:
Iterator<String> iterator = c.iterator();
iterator.next();
iterator.remove();
一定要注意的是,remove方法的调用对next方法具有依赖性。如果调用remove方法之前没有调用next方法将是不合法的,会抛出一个IllegalStateException异常。比如要删除两个连续的相邻元素,不能直接这样调用:
iterator.remove();
iterator.remove(); //remove前未调用next方法,异常
1.3.3 forEachRemaining
forEachRemaining是Java 8中新引入的default方法,方法的参数Consumer用于逐个消费集合中的元素,如下:
List<String> list = Lists.newArrayList("this", "is", "list");
list.iterator().forEachRemaining(ele -> System.out.println("consume :" + ele));
2. Map
Java API中Map用于保存具有映射关系(K-V)的数据,Map的key不允许重复。HashMap、LinkedHashMap、TreeMap都实现了Map接口,下面来看一下Map接口的方法定义:
S.N. | 方法 | 说明 |
---|---|---|
1 | void clear() | 清除Map中所有的KV映射 |
2 | default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | Java 8新方法,如果key在Map中不存在,则根据Function规则计算key对应的value值, 并进行put操作(前提计算得到的value值也不为null) |
3 | default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
Java 8新方法,如果key在Map中存在,则根据BiFunction规则通过key和对应的value 计算newValue,如果newValue为null,则删除key对应的键值对,否则替换key对应的value值 |
4 | default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | Java 8新方法,相当于上述两个方法的综合,根据BiFunction,通过key和对应的value 计算newValue,如果newValue为null,会删除对应KV映射,否则会插入或替换原KV映射 |
5 | boolean containsKey(Object key) | 判断Map中是否存在key键 |
6 | boolean containsValue(Object value) | 判断Map中是否存在value值 |
7 | Set<Map.Entry<K, V>> entrySet() | 将Map所有的键值对转换为Set集合 |
8 | boolean equals(Object o) | equals方法 |
9 | default void forEach(BiConsumer<? super K, ? super V> action) | Java 8新方法,遍历Map,通过BiConsumer处理Map中每一个键值对的key、value |
10 | V get(Object key) | 获取Map中key对应的value |
11 | default V getOrDefault(Object key, V defaultValue) | Java 8新方法,获取Map中key对应的value,如果key不存在,则返回defaultValue |
12 | int hashCode() | hashCode方法 |
13 | boolean isEmpty() | 判断Map是否为空 |
14 | Set<K> keySet() | 将Map中所有的key转化为Set集合 |
15 | default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) |
如果key在Map中不存在或者对应的值为null,则给其一个对应的非null值value。否则根据 BiFunction规则通过oldValue和value计算一个newValue,替换或删除(newValue为null)原 键值对 |
16 | V put(K key, V value); | 为Map添加一个键值对 |
17 | void putAll(Map<? extends K, ? extends V> m) | 将另一个Map的所有键值对添加到该Map中 |
18 | default V putIfAbsent(K key, V value) | Java 8新方法,如果Map中key不存在,则添加一个key-value键值对 |
19 | V remove(Object key) | 从Map中移除key对应的键值对 |
20 | default boolean remove(Object key, Object value) | Java8新方法,删除key-value键值对 |
21 | default V replace(K key, V value) | Java8新方法,将Map中key对应的值替换为value |
22 | default boolean replace(K key, V oldValue, V newValue) | Java8新方法,将Map中key-oldValue键值对的value值替换为newValue |
23 | default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) | Java8新方法,将Map中所有的键值对的value,替换为根据BiFunction规则通过key-value 得到的新值 |
24 | int size() | 返回Map中EntrySet的数目 |
25 | Collection<V> values() | 返回Map中所有value组成的集合 |