(转)Java中的容器详细总结
Java中的容器详细总结(编辑中)
原文链接:http://anxpp.com/index.php/archives/656/
注:本文基于 Jdk1.8 编写
通常程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,也不知道要创建的对象的数量(甚至是确切的对象类型)。为了解决这个问题,java提供了数组来解决这个问题,但是数组是长度固定的,很多时候简单的数组是不能满足我们的需求的,所以java还提供了一套相当完整的容器类来解决这个问题,这也是本文要介绍的。
1、概述
Java容器类类库的用途是保存对象(引用),主要分两个概念:
1、Collection:一个独立元素的序列。List、Set、Queue。
2、Map:一组成对的键值对对象。其实ArrayList也可以看做是数字与对象的关联关系,而如HashMap等,是对象与对象关联。
List,Set,Queue都是继承自Collection的,而Collection是继承Iterable的,这样的实现可以返回一个迭代器。所以Collection及其子类可以使用迭代器来遍历数组元素。而对于List,有专门的迭代器,可以用于设置初始迭代位置和双向遍历,后面都会慢慢介绍。
Collection的所有操作如下(不含Object类的方法):
检查Collection中的元素,推荐使用迭代器。
Map并不是继承自Collection的,后面会单独介绍。
涉及到hashCode的地方(如HashSet和HashMap)会在1.6中进一步解释。
涉及到线程同步的相关容器,也会放到后面单独介绍。
所有常用实现或接口都会给出源码分析,在文章后面部分会给出源码分析的索引,读者可以方便的找到具体实现或接口的源码。
1.1、List
List可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。
有两种类型的List实现:
- 基本的ArrayList,随机访问速度很快,但是在List中间插入和移除元素时较慢
- LinkedList,特性与ArrayList相反,善于顺序访问,在List中间插入删除代价也较低。但随机访问比较慢,特性集比ArrayList更大。
ArrayList
ArrayList是List接口的一种底层采用数组,并可以自动调整容量的实现,因为底层使用了数组,所以随机访问元素的时间复杂度是常数级别的,迭代是线性的,但是在其中插入或删除元素,必须将插入点之后的元素全部前移或者后移,速度就相对比较慢了。
ArrayList默认的初始容量为10,可以使用带参数的构造函数修改这个默认值,而且数组默认增长的方式是增加原来长度的一半(oldLength>>1),而默认的最大容量为Integer.MAX-8,java的int为4字节的,所以0x7fffffff-8已经是一个相当大的数了。
ArrayList不是线程安全的,多线程同时读不会出问题,但是如果有一个线程在进行包含写的操作,就很容易发生错误。但是在迭代ArrayList的时候,有快速报错机制,即在迭代过程中,数组被修改了,将会引发异常,这个异常在next()方法和remove()方法的调用上都可能发生。而这种机制也是有父类提供支持的。
这种机制也不是仅为了多线程设置的,多线程有多线程相关的容器,而且即使用ArrayList,我们也会加上我们自己的同步策略。在单线程中,谁也不能阻止你在遍历的过程中修改这个表的结构,虽然你并不应该这么做,为了安全,就只能抛个异常了。
ArrayList直接继承自AbstractList。在AbstractList中有定义这样一个变量:protected transient int modCount = 0;什么意思呢?其实就是指已从结构上修改此列表的次数。从结构上修改是指更改列表的大小,或者打乱列表。我们知道,这样的修改很容易导致遍历发生错误的,但是有了这个变量,我们就能提前发现这个隐患,并抛出异常:在调用iterator()返回迭代器时,这个迭代器会记录此时列表被修改的次数,每次遍历或删除元素,都会判断列表修改次数是不是发生改变,若发生改变就会抛出异常。
关于性能:
- 倘若一开始我们能根据一些条件适当的初始化这个列表默认容量,可能会减少数组自动增长开销。
- 使用elementData()获取元素比get()会高效一点,因为不会检查索引值。
- 如果我们确保能正确使用,可以省掉一些安全检查(包括迭代过程的异常我们也可以省略),但这通常是不好的设计,为了更高一点的效率而采用危险的做法不可取,当然也没人硬性规定你不能这么做,为了更高的性能,完全可以自己实现一个ArrayList。
而更多的详细的分析和介绍,请移步:Java之ArrayList源码解读(JDK 1.8),里面会对其实现有更为详细的介绍。
LinkedList
LinkedList相对ArrayList添加了更多的方法,这些方法也很好的为使用LinkedList实现队列、双向队列、栈等提供了很好的支持。
LinkedList底层使用双向链表实现,一个节点持有前一个节点的引用、需要保存的元素的引用和后一节点的引用。空的LinkedList中first和last都为null,只有一个元素(节点)的LinkedList中first->next==last、last.prev==first。
所有操作的底层就是对链表的操作,对链表实现了各种增删改查的方法,然后对外提供了更多操作行为,如队列的操作,栈的操作和普通List的操作,这些操作可能实际的实现是相同的,但是对外提供不同的接口以展示更多的行为。
LinkedList不是线程安全的,但也支持快速报错,起机制与ArrayList时相同的(可以见上文介绍,此处不再赘述了)。
LinkedList中一些表面相同的行为的方法,结果可能是不同的,不如队列的出队,若队列为空,有返回null和抛出异常之分,也有是否删除出队的节点之分,详情请看源码解读。
LinkedList中表现为栈的操作的实现,是从链表的前端操作的,好处显而易见,当我们需要它表现为栈的特性时,通过迭代器返回的顺序也应为出栈的顺序。
关于性能:
- LinkedList因为使用链表实现,所以在列表中间插入删除元素时非常快速的,相反,随机访问时,必须从首节点(或尾节点,索引小于列表长度一半时从队首开始遍历,反之从队尾)开始逐个遍历,相对于ArrayList会更慢。查找元素的时间复杂度是相同的,实际上ArrayList可能会略快一点。
- LinkedList提供了比较丰富的操作,支持多种数据结构,使用起来固然方便。不过如果要追求完满,比如仅仅需要一个普通的队列,采用LinkedList总感觉又太“奢侈”了,我们完全可以自己基于单项链表来实现,然后值提供我们需要的方法。当然,这在时间上也是成本!
总之,LinkedList是一个很强大的工具。
虽然LinkedList提供了很多操作,诸如栈和队列的,但有时候我们任然希望一个功能更专一的栈或队列,下面就演示以代理模式通过LinkedList实现的栈和队列:
- //LinkedList的使用
- public class TestUse {
- public static void main(String[] args) {
- //栈
- Stack<Integer> stack = new Stack<Integer>();
- for(int i=0;i<10;i++)
- stack.push(i);
- System.out.println(stack.peek());
- System.out.println(stack.peek());
- System.out.println(stack.pop());
- System.out.println(stack.pop());
- System.out.println(stack.pop());
- Iterator<Integer> iterator = stack.iterator();
- while(iterator.hasNext())
- System.out.print(iterator.next());
- System.out.println();
- //队列
- Queue<Integer> queue = new Queue<Integer>();
- for(int i=0;i<10;i++)
- queue.enqueue(i);
- System.out.println(queue.peek());
- System.out.println(queue.peek());
- System.out.println(queue.dequeue());
- System.out.println(queue.dequeue());
- System.out.println(queue.dequeue());
- iterator = queue.iterator();
- while(iterator.hasNext())
- System.out.print(iterator.next());
- }
- }
- //很使用使用代理模式利用LinkedList实现一个栈
- class Stack<T> implements Iterable<T>{
- private LinkedList<T> stack = new LinkedList<T>();
- public T pop(){//出栈,会删除栈顶元素
- return stack.poll();
- }
- public T peek(){//出栈,但不删除栈顶元素
- return stack.peek();
- }
- public void push(T t){//入栈
- stack.push(t);
- }
- @Override
- public Iterator<T> iterator() {
- return stack.iterator();
- }
- }
- //很使用使用代理模式利用LinkedList实现一个队列
- class Queue<T> implements Iterable<T>{
- private LinkedList<T> queue = new LinkedList<T>();
- public void enqueue(T t){
- queue.offer(t);
- }
- public T dequeue(){
- return queue.poll();
- }
- public T peek(){
- return queue.peek();
- }
- @Override
- public Iterator<T> iterator() {
- return queue.iterator();
- }
- }
而更多的详细的分析和介绍,请移步:Java之LinkedList源码解读(JDK 1.8),里面会对其实现有更为详细的介绍。
1.2、Set
存入Set中的每个元素都必须是唯一的,因为Set不保存重复的元素,加入Set的元素必须定义equals()方法以确保对象的唯一性。
Set与Collection有完全一样的接口,所以就没有任何额外的功能。实际上,Set也就是Collection,只是行为不同。
Set不保证维护元素的次序。
下面介绍其中的一些实现。
HashSet
为快速查找儿设计的Set。存入HashSet的元素必须定义HashCose()方法。
如果没有其他限制,这应该是我们的首选,因为它对速度做了优化。
TreeSet
保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable()接口。
LinkedHashSet
具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的顺序)。于是在使用迭代器遍历的时候,结果会按元素插入的次序显示。元素也必须定义hashCode()方法。
SortedSet
比如TreeSet就是其中一种实现方式。
SortedSet中的元素可以保证处于排序状态(按对象的比较函数排序,而不是插入的顺序)。
1.3、Map
Map可以将对象映射到其他对象。
映射表(也称为关联数组)的基本思想是它维护的是键-值(对)关联,因此可以使用键来查找值。
标准的JAVA类库中包含以下实现:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap等。他们的基类接口是一样的(Map),但是行为特性各不相同,比如效率、键值对的保持及呈现次序、对象的保持周期、映射表如何在多线程程序中工作和判定“键”等价的策略等。
Map中key的使用与对Set中的元素要求一样,任何键都必须有一个eauals()方法。如果键被用于散列Map,则还必须具有恰当的hashCode()方法。如果键被用于TreeMap,它必须实现Compareble。
性能
在映射表中使用get()方法做线性搜索时,执行速度会很慢,儿使用HashMap可以提高速度。HashMap使用散列码,HashMap使用hashCode()进行快速查询。如果对速度还有更高的需求,可以自己创建Map的实现,并移除泛型支持等。
HashMap
基于散列表实现(取代HashTable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调整容器性能。
如果没有其他限制,这应该是我们首选的实现方式,因为它对速度做了优化。其他实现有更多增强型特性,所以速度上回有取舍。
LinkedHashMap
类似于HashMap,但是迭代访问时,取得的“键值对”的顺序就是插入的顺序,或者是最近最少使用(LRU)的次序。比HashMap稍慢。在使用迭代器访问时,有更快的速度,因为内部使用链表维护次序。
为提高速度,LinkedHashMap散列化所有的元素,但是在遍历键值对时,却又以元素的插入顺序返回键值对。
可以在构造器中设定LinkedHashMap,使之采用LRU(最近最少使用)算法,所以没有被访问的元素就会出现在队列的前面。对于需要定期清理元素节约空间的程序来说,这个功能就能使程序和容易实现。
TreeMap
基于红黑树的实现。查看“键”或“值”时,他们会被排序(具体次序有比较函数决定)。TreeMap特点在于,所得到的结果都是经过排序的。TreeMap是唯一带有subMap()方法的Map,可以返回一个子树。
WeakHashMap
弱键映射,允许释放映射所指向的对象。是为了一些特殊的问题设计的。如果映射之外没有引用指向某个“键”,这个键就可以被垃圾回收器回收。
ConcurrentHashMap
一种线程安全的Map,不涉及同步锁。
IdentityHashMap
使用==代替equals()对键进行比较的散列映射。也是为解决特殊问题设计的。
SortedMap
TreeMap是目前SortedMap的一种实现方式,可以确保键处于排序状态。
1.4、QUEUE
1.5、迭代器
迭代器
一种设计模式,在这里是一个对象。功能是遍历并选择序列中的对象。java中的迭代器只能单向移动:
- 1、使用方法iterator使容器返回一个Iterator,Iterator会准备好返回序列的第一个元素。
- 2、使用next()获得序列的下一个元素
- 3、使用hasNext()判断是否还有元素
- 4、使用remove()将迭代器新近返回的元素删除
1.6、散列与散列码
标准类库中的类可以被用作HashMap的键,因为他们具备了键所需的全部性质。
当我们自己创建用作HashMap的键的类时,就必须在其中添加必要的方法。
我们创建的类,默认会继承Object类,Object的hashCode()方法生成的散列码默认使用的是对象的地址来计算散列码。所以,试过仅仅是编写一个普通类,是不能用于HashMap的键的,首先要编写恰当的hashCode方法的覆盖版本。而仅仅是这样,依然不能工作,我们还需要编写eauals()方法,它也是Object的一部分。HashMap使用equals()方法判断当前的键是否与表中存在的键相同,默认的equals()方法同样是比较的对象的地址。
正确的equals()方法必须满足下面的5个条件:
- 自反性:对任意的x,x.equals(x)一定成立
- 对称性:
- 传递性:
- 一致性:
- 对任何不为null的x,x.equals(null)一定返回false。
如果不为键覆盖hashCode()和equals()方法,在使用散列的数据结构时就不能正确处理这个键。
hashCode()
首先,使用散列的目的在于:想要使用一个对象来查找另外一个对象。但是使用TreeMap或者自己实现Map也能达到目的(比如使用两个ArrayList分别存放key和值就能很容易的实现一个Map)。
所以创建一个新的Map并不困难,但是,我们设计的时候,首先就应该考虑到速度问题,散列的价值就在于速度:散列可以得到一个非常快速的查询速度。
查询的速度主要取决于key的查找,其中一种方法就是保持键的排序状态,然后使用二叉搜索树进行查询。
但是散列是一种更优的方式,它将key存储在没某个地方以便于能过快速找到。存储一组元素(key的相关信息)最快的是数组,但是数组不能调整容量,这将是一个很大的限制。然后,可以如下设计:数组并不保存key本身,我们通过对象生成一个数字,将其作为数组的下标。儿这个数字就是散列码,由定义在Object或者由其子类覆盖的hashCode()方法生成(散列函数)。而由于数组的容量被固定了,不同的键也可能产生相同的下标(即产生冲突),所以数组有多大就不重要了,只要任何键都能在数组中找到它的位置。
于是查询一个值的过程首先就是计算散列码,然后使用散列码查询数组。
如果有没有冲突的查询过程,那就是有一个完美的散列函数,但这毕竟是特例。通常,冲突由外部链接处理:数组不直接保存值,二十保存值的list。然后使用eauals()方法对list中的值进行线性查找(其中较慢的一部分)。如果散列函数比较好,数组的每个位置都只有少量的值,因此查找的时候不是针对整个list,而是快速跳到数组的某个位置,只对很少的元素进行比较。所以HashMap就会如此快。
下面我们来简单实现一个散列Map:
由于散列表中的槽位通常称为桶位,因此我们将表示实际散列表的数组命名为为bucket。为师散列分布均匀,同的数量通常使用质数(其实,质数并不是散列桶的理想同期,java的散列函数都使用2的整数次方,除法和求余数是最慢的操作,详情请查阅更多相关资料)。
经过以上的介绍,这里就探讨如何编写自己的hashCode()方法。
设计hashCode()应该保证,无论何时调用同一个对象的hashCode()都能生成相同的值。而且也不应该使用具有唯一性的对象信息,如this的值,这可能会产生一个很糟糕的hashCode()。
《effective java programming language guide》中给出了一些hashCode()的基本指导:
1、给int变量result赋予某个非零值常量
2、为对象内每个有意义的域f(每个可以做equals()操作的域)计算出一个int散列码c
3、合并计算得到的散列码
4、返回result
5、检查hashCode()最后生成的结果,确保相同的对象有相同的散列码。
根据以上指导实现的hashCode()的一个例子:
1.7、小结
Java中提供的其实只有4中容器:Map、List、Set和Queue。
Java提供了大量的持有对象方式:
1、数组将数字与对象联系起来。它保存类型明确的对象,查询对象是,不需要对结果做类型转换。他可以是多维的,可以保存基本类型的数据。但是,数组一旦生成,其容量就不能改变。
2、Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,就可以指定容器中存放的对象类型,因此就不会将错误类型的对象防止到容器中,而且在容器中获取元素时,不必进行类型转换。各种Collection和Map都可以在向其中添加更多的元素时,自动调整其尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。
3、像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排序好的容器。List能够自动扩充容量。
4、如果要进行大量的随机访问,就是用ArrayList。如果要经常从表中间插入或删除元素,则应该使用LinkedList。
5、各种Queue以及栈的行为,有LinkedList提供支持。
6、Map是一种将对象与对象相关联的设计。HashMap设计用来快速访问TreeMap保存“键”始终处于排序状态(没有HashMap快);LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问能力。
7、Set不接受重复元素。HashSet提供最快的查询速度,TreeSet保持元素处于排序状态。LinkedHashSet以出入顺序保存元素。
2、接口不同实现的选择
Java中实际上只有4中容器:Map、Set、List、Queue。但是每种接口都有不止一个实现版本,所以合理的选择具体的实现。
每种不同的实现由各自的特征,优缺点。比如Hashtable、Vector、Stack的特征就是他们已经过时o(^▽^)o(目的只是为了支持老的程序)。
我们现在具体实现时,首要就是考虑我们要实现什么样的数据结构。
比如:ArrayList和LinkedList都实现了List接口,两者基本的List操作都是相同的。但是ArrayList底层有数组支持;而LinkedList由双向链表实现,每个对象除了包含数据本身的同时还包含只想前后两个元素的引用。因此,如果需要经常在表中插入或是删除数据的话,LinkedList更佳合适;否则,应该使用ArrayList达到更多的速度。
同样的,Set可被实现为TreeSet,HashSet或LinkedHashSet。每一种都有不同的行为:HashSet最常用,以为其查询速度最快;LinkedHashSet保持元素插入的顺序;TreeSet给予TreeMap,生成一个总是处于排序状态的Set。根据不同的需求以选择不同的实现。
下面会介绍一个性能测试框架(程序来自Java编程思想)。
2.1、性能测试框架
3、Java容器框架图
来看一下相关java容器相关的结构:
Sorry,以上是JDK1.8 前的一张图,本人也基于JDK1.8 绘制了一张,下文也是基于这张图来写的:
4、源码分析索引
基于JDK 1.8 。
4.1、Iterable
我们从最上面开始看,根为Iterable,Iterable是一个接口,看下源码:
- 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);
- }
- }
Iterator<T> iterator():返回一个在一组 T 类型的元素上进行迭代的迭代器。
实现此接口允许对象使用"for-each 循环"语句(在Java8发布之际,有件事情就显得非常重要,即在不破坏java现有实现架构的情况下能往接口里增加新方法。引入Default方法到Java8,正是为了这个目的:优化接口的同时,避免跟现有实现架构的兼容问题)。
再看下Iterator:
- public interface Iterator<E> {
- E next();
- default void remove() {
- throw new UnsupportedOperationException("remove");
- }
- default void forEachRemaining(Consumer<? super E> action) {
- Objects.requireNonNull(action);
- while (hasNext())
- action.accept(next());
- }
- }
迭代其实我们可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类,它是一个很典型的设计模式。Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。
ListIterator
ListIterator为专为LIst设计的增强版本迭代器,可以双向移动。
- package java.util;
- public interface ListIterator<E> extends Iterator<E> {
- boolean hasNext();
- E next();
- boolean hasPrevious();
- E previous();
- int nextIndex();
- int previousIndex();
- void remove();
- void set(E e);
- void add(E e);
- }
可以看到hasPrevious()和previous()就是判断是否前面还有值和得到前一项。
2.2、Collection
Collection是直接继承Iterable的一个接口,所以Collection的导出类都应该事先迭代器的,并都能使用foreach遍历。
- public interface Collection<E> extends Iterable<E> {
- int size();
- boolean isEmpty();
- boolean contains(Object o);
- Iterator<E> iterator();
- Object[] toArray();
- <T> T[] toArray(T[] a);
- boolean add(E e);
- boolean remove(Object o);
- boolean containsAll(Collection<?> c);
- boolean addAll(Collection<? extends E> c);
- boolean removeAll(Collection<?> c);
- 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;
- }
- boolean retainAll(Collection<?> c);
- boolean equals(Object o);
- int hashCode();
- @Override
- default Spliterator<E> spliterator() {
- return Spliterators.spliterator(this, 0);
- }
- default Stream<E> stream() {
- return StreamSupport.stream(spliterator(), false);
- }
- default Stream<E> parallelStream() {
- return StreamSupport.stream(spliterator(), true);
- }
- }
其实通过函数名我们已经知道这些方法都是干什么的了,不过具体是如何操作的,返回代表什么?我们得往下看具体实现才行。
2.3、List
- public interface List<E> extends Collection<E> {
- int size();
- boolean isEmpty();
- boolean contains(Object o);
- Iterator<E> iterator();
- Object[] toArray();
- <T> T[] toArray(T[] a);
- boolean add(E e);
- boolean remove(Object o);
- boolean containsAll(Collection<?> c);
- boolean addAll(Collection<? extends E> c);
- boolean addAll(int index, Collection<? extends E> c);
- boolean removeAll(Collection<?> c);
- boolean retainAll(Collection<?> c);
- default void replaceAll(UnaryOperator<E> operator) {
- Objects.requireNonNull(operator);
- final ListIterator<E> li = this.listIterator();
- while (li.hasNext()) {
- li.set(operator.apply(li.next()));
- }
- }
- 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);
- }
- }
- void clear();
- boolean equals(Object o);
- int hashCode();
- E get(int index);
- E set(int index, E element);
- void add(int index, E element);
- E remove(int index);
- int indexOf(Object o);
- int lastIndexOf(Object o);
- ListIterator<E> listIterator();
- ListIterator<E> listIterator(int index);
- List<E> subList(int fromIndex, int toIndex);
- @Override
- default Spliterator<E> spliterator() {
- return Spliterators.spliterator(this, Spliterator.ORDERED);
- }
- }
List可以像数组一样持有对象,但是大小可以自动调整,并且提供了更多的操作。我们来看一下有哪些实现:
其中有些事接口,有些事抽象类,这里主要看两个实现类:LinkedList和ArrayList。
ArrayList
限于篇幅,此处将源码解读放到另外一篇文章:ArrayList源码解读
LinkedList
2.4、Set
2.5、Queue
2.6、Map
posted on 2016-06-23 17:24 Yanspecial 阅读(14240) 评论(0) 编辑 收藏 举报