Collections(一)概览

在编程实践中,容器类库对于面向对象语言来说是最重要的类库,Java Collections Framework是Java设计者提供的容器集合,通过使用这些容器,无须费力就可以完成大量有趣的工作。

某些时候,你必须更多的了解容器以便正确的使用它们。你必须对散列操作有足够的了解,从而能够编写自己的hashcode方法(并且你必需知道何时该这么做),你还必须对不同容器的实现有足够的了解,这样才能够为你的需要进行恰当的选择。--节选自Java编程思想》

本系列文章将对Java Collections Framework的一些核心数据结构API进行介绍,并从源码角度分析其实现原理,文章中将更多的探讨为什么这样设计以及性能的分析和优化。

本文对Collections API一些通用概念和设计作一个概览。

类图

image

从类图中可以看出,Java容器核心接口是Collection和Map。Collection是一个通用接口,定义了一些数据的集合,Map是个Key-Value映射集合

  • List表示一个线性(有顺序)的表结构,可以拥有重复元素
  • Queue表示一个队列,FIFO方式
  • Deque表示一个双向队列,同时支持FIFO方式或者LIFO,它集成Queue接口,即实现了队列,又实现了栈
  • Set表示一个数据集合,不允许拥有重复的元素

针对Set和Map还有两个支持排序的接口:

  • SortedSet表示一个排序的Set
  • SortedMap表示一个排序的Map

设计问题:为什么Map要独立于Collection接口?

我想Java的设计者肯定也考虑过让Map继承于Collection接口,比如Collection的元素是EntrySet来满足Map的实现。但是,Map的意义不应该只是一些数据的集合,更应该是键值对映射,它和Collection的结构和用法上有不一样的地方,所以为了更清晰的定义Ma这样的数据结构,分离了Map接口,同时提供了方法entrySet()将Map转化为Collection。

接口Collection

再具体介绍Collection接口的方法前,我们有必要了解一下集合框架中的一个设计:

设计约定:所有通用的集合实现通常提两个标准的构造器,一个无参构造器,一个参数类型为Collection的构造器(public ClazzX(Collection<? extends E> c))。

这种设计方便使用指定集合初始化新集合,允许转化集合的类型,这种构造器称为转换构造函数(conversion constructor)。

1. 基础操作(Basic Operations)

方法作用
int size()元素个数
boolean isEmpty()是否是空集合
boolean contains(Object element)是否包含元素
boolean add(E element)增加元素
boolean remove(Object element)删除指定元素
default boolean removeIf(Predicate<? super E> filter)删除断言元素
Iterator iterator()获取迭代器

其中removeIf是接口中的默认方法,在这里先给出实现源码,通过迭代器删除元素,其中Predicate是函数式接口:

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;
}

2. 批量操作(Bulk Operations)

方法作用
boolean containsAll(Collection<?> c)是否拥有集合元素
boolean addAll(Collection<? extends E> c)增加集合元素
boolean removeAll(Collection<?> c)删除集合元素
boolean retainAll(Collection<?> c)交集
void clear()清空集合

3. 数组操作(Array Operations)

方法作用
Object[] toArray()转化为数组
T[] toArray(T[] a)转化为指定类型的数组

4. 流方式的聚合操作(Aggregate Operations)

方法作用
Stream stream()
Stream parallelStream()并行流
Spliterator spliterator()分割迭代器

接口Map

Ma允许NULL值,不允许重复的KEY,一个KEY最多对应一个值VALUE。Map也提供了转换构造器,遵守了Collection接口的设计约定接下来我们会对Map接口的一些不同于Collection的方法进行介绍。

1. 基础操作(Basic Operations)

方法作用
boolean containsKey(Object key)是否包含KEY
boolean containsValue(Object value)是否包含VALUE
V get(Object key)获取Key对应的Value值
V put(K key, V value)新增键值对
default V putIfAbsent(K key, V value)如果Key不存在,则新增
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)通过Key-Value计算,remappingFunction返回NULL则删除此key(如果key存在),否则新增
default V replace(K key, V value)替换元素

其中putIfAbsent的默认实现如下:

default V putIfAbsent(K key, V value) {
  V v = get(key);
  if (v == null) {
    v = put(key, value);
  }
  return v;
}

2. 批量操作(Bulk Operations)

方法作用
void putAll(Map<? extends K, ? extends V> m)增加MAP
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)替换MAP元素
void clear()清空MAP

其中BiFunction是一个函数式接口,提供了通过Key和Value生成替换后的元素:

@FunctionalInterface
public interface BiFunction<T, U, R> {
  R apply(T t, U u);
}

3. MAP视图

方法作用
Set keySet()key的Set结婚,Map中的key本身不允许重复
Collection values()值的集合,可能会重复
Set<Map.Entry<K, V>> entrySet()key-value对的Set集合

与Collection通过Iterator遍历不同,Map遍历的方式是通过以上三种MAP视图遍历的。在JDK1.8以后,Map接口提供了foreach方式便捷的迭代Map,默认实现是通过entrySet视图遍历:

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);
  }
}

其中BiConsumer是个函数式接口。

遍历1:Iterable

Collection接口作为容器的一个根接口,它还继承了Iterable接口,所以所有的Collection实现类都提供了forEach遍历的方法。

public interface Iterable<T> {
  Iterator<T> iterator();

  default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
      action.accept(t);
    }
  }

  default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(iterator(), 0);
  }
}

Java语言提供的"for-each loop"语句正是基于接口Iterable实现的,编译器将会优化成iterator方式遍历,对于数组的for-each,编译器将会优化成下标法遍历(The type of the Expression must be Iterable or an array type, or a compile-time error occurs《Java语言规范》)。

正因为Collection继承了Iterable接口,因此我们可以对Collecion实现类进行遍历:

for (Object o : collection)
    System.out.println(o);

Google的Guava Collection设计:由于Google的数据集合并不一定存储在内存中,可能存储在数据库中或者其它数据中心,无法获取所有数据,因此不支持计算size大小,所以倾向于使用Iterable接口。

Whenever possible, Guava prefers to provide utilities accepting an Iterable rather than a Collection

遍历2:Iterator

正如Iterator接口所示,它提供了一个方法,返回iterator对象,我们可以通过iterator进行遍历,接口定义如下:

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());
  }
}

所有Collection实现类根本身的实现原理,创建Iterator对象。典型的遍历方式代码如下:

Iterator<?> it = c.iterator();
while (it.hasNext())
    it.next();

设计思想:这有个很重要的思想,就是Iterator统一了所有Collection集合的遍历方式,这样我们可以无缝切换集合,但是遍历方式是一样的,这也Java框架易学习的一个设计。

遍历3:Stream

JDK1.8以后,对集合的遍历更倾向于获得Stream流,然后基于进行聚合操作。

c.stream()
  .forEach(e -> System.out.println(e.getName());

相比于Iterator,聚合操作基于流元素,而不是集合元素,它使用内部迭代方式,可以更容易将问题分解为子问题,进行并行计算,支持lambda表达式,Stream的篇幅可以很长,这里不作延伸介绍。

排序:Comparable和Comparator

对数据集合排是一种常见操作,Comparable接口定义了对象的自然排序(natural ordering),实现了此接口的类的所有对象,都将拥有一个排序属性,它提供了一方法来定义顺序,负数表小于,正表示大于,0表示相等:

public interface Comparable<T> {
  public int compareTo(T o);
}

当我们希望重新定义一个对象的顺序时(不使用自然顺序),或者对象不可排序的(没有实现Comparable接口),我们就可以使用函数式接口Comparator。

int compare(T o1, T o2);

更多应用,参见java.util.Arrays.sort(T[], Comparator<? super T>)方法。

性能:Sequential Access和Random Access

性能将选用什么数据结构实现的一个重要考虑点。

RandomAccess是一个接口,实现了此接口的类表示它的元素支持快速随机访问,Java集合框架在处理相关集合的操作上,会针对Sequential Access和Random Access来优化算法,比如实现了RandomAccess的接口的List,通过下标访问法比使用iterator更快,我们来看下Collections工具类提供的一个二分法查找的源码:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

运行时异常:UnsupportedOperationException和ConcurrentModificationException

当Collection某个方法在具体实现类中不支持时,就会抛出运行时异常UnsupportedOperationException。

我们来思考下这个 运行时异常的设计,它提供了方法,却在只有我们运行时才会抛出不支持异常,而仅仅能保证代码正确的方式,就是依赖具体实现类的javadoc注释,你可能觉得有点奇怪,我们为什么不提供新的没有这些方法的接口呢?

我想可能的一个目的是保证Java Collections框架为了接口的数量。

Collection很多实现类不支持并发操作,当我们在迭代集合时,如果对集合结构进行了改变(增加或者删除操作),那么程序未来的行为都是未知的,与保持同步的方式相比,抛出UnsupportedOperationException异常能达到快速失败(fail-fast)的目的,尽可能提前终止程序的运行,注意,这个异常是由iterator抛出的,在接下来的章节中,我们将会看到JDK是如何实现fail-fast机制的,这也是集合框架 设计的一个特征

常用API实现

我们通过一个表格简单的了解下通用数据结构的实现API以及实现原理。

接口哈希表可变数组链表哈希表+链表
SetHashSet TreeSet LinkedHashSet 
List ArrayList LinkedList  
Queue     PriorityQueue(不允许NULL值)
Deque ArrayDeque LinkedList  
MapHashMap TreeMap LinkedHashMap 

这些通用实现都是线程不安全的,除了PriorityQueu,都允许NULL元素,并且都支持fail-fast的设计,所有都实现了接口Serializable。

为了性能和一些特殊用途,还有一些专用实现,为了线程安全,还有一些并发的实现,都将在后续章节讨论。

总结:Design Goals

本文讨论了Java Collections框架的一些基础知识,以及通用设计,这个框架最主要的设计目标是保小巧和轻量级的概念。

为了保证接口数量少,它没有提供诸如不可变的集合,不可修改的集合等,为了保证接口内方法数量少,它只包含了那些真正的基础操作以及为了性能,实现类有必要重写的方法。

还有个重要的设计思想是集合都是可以互操作的比如转换构造器,Ma视图等,这也包括Array,通过桥接模式,很好的将集合和数组连接在一起。

下一篇文章,我们将会对List进行详细解析,待续。

转载于:https://my.oschina.net/LeBronJames/blog/3101504

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值