Java Collection框架 顶级接口Iterable与Spliterator,forEach与forEachRemaining
2018拍摄于京都音羽山清水寺
微信公众号
王皓的GitHub:https://github.com/TenaciousDWang
今天开始复习Java 集合框架,多线程部分还差同步容器会在Java集合最后后面填坑。
在说Java集合框架之前,先简单看看数据结构。
程序=数据结构+算法,数据结构指的是数据组织的形式及处理方式,在Java2及以后的版本中使用了船新的集合框架体系Collection来作为数据结构的载体,用来存储和处理各种各样的对象。
处理方式如,增加,修改,查找,删除,遍历。
以下类是Java遗留下来的,基本不再使用,而是开始使用集合框架Collection,感兴趣的朋友,可以去自行百度或google。
- 枚举(Enumeration)
- 位集合(BitSet)
- 向量(Vector)
- 栈(Stack)
- 字典(Dictionary)
- 哈希表(Hashtable)
- 属性(Properties)
常用数据组织方式:
线性结构,循序表,链表,栈,队列等,其中栈为LIFO,Last in,First out,后进先出,单门旅游大巴,队列为FIFO,First in,First Out先进先出,公交车前门上,后门下。
树形结构,有层次非线性结构,像我们平常的看到的树一样,有树根,树杈,由一个个节点构成树的模样,二叉树,B树,B+树,红黑树。
哈希结构,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
不同的数据结构,即不同的数据组织方式与处理方式的性能问题,我们通常分为两个维度去考量,一个是空间维度,一个是时间维度,空间维度暂时我们不会考虑,因为现在存储空间越来越便宜,2019年4TB的机械硬盘只需要500元左右,并且已经出现了16TB的怪物,固态硬盘也越来越便宜,所以我们这里只考虑时间维度,即程序执行时间或者查找时间随输入规模而增长的量级,可以在很大程度上反映数据结构的性能。
最好到最坏的常用算法时间复杂度:
常数级O(1),最低复杂度,不管输入规模多大,耗时不变,例如哈希结构,在不考虑冲突的情况下,只计算一次即可找到对应的目标。
对数级O(log n),当数据增大n倍时,复杂度只增加log n倍,这里的log n以2为底数,即增加256倍输入量时,时间复杂度只增加8倍。例如二分查找法,每次排除一半可能,256个元素,只需要找8次即可。例如1-100随机查找一个数字,使用二分法,执行一亿次,平均查找次数为7.47.接近log 100即5.8
线性级O(n),输入数据增大几倍,查找耗时也增加几倍,例如遍历算法。256个元素,找256次。
线性对数级O(nlog n),n乘以log n,256倍输入量,查找时间为256*8=2048倍,这个复杂度高于线性低于平方级。
平方级O(n^2)。如冒泡排序,需要计算n*n次。
立方级O(n^3)。
指数级O(2^n)。
在实际编码中,我们应该根据实际使用场景,来确定使用什么类型的数据结构或者说使用哪种类型的集合,数据结构没有好坏之分,需要与场景与数据量结合起来考虑,优秀的程序不会因为随着输入量的增大,而使执行时间急剧上升。
常见数据结构时间复杂度。
接下来我们来看一下Java的常用集合框架图:
图片摘自《码出高效》
Java集合框架是用于存储对象的容器,实现了常用的数据结构,提供公开增加,修改,删除,查找,遍历的方法,集合的种类较多,形成了一个Java集合框架,主要分类两类,一个是Collection,set与List都实现了Collection接口,一个是Map接口,按照key-Value存储的数据结构。
这里借用《码出高效》的Java集合框架图,网上大部分框架图没有包含并发包中的类,最主要是我懒了不想画了。。。红色为接口,蓝色为抽象类,绿色为并发包中的类,灰色为早期线程安全类,由于性能低下现在基本弃用。
首先我们先从顶层接口来看,Iterable在java.lang中,而并不是java.util,网上很多文章都写错了。Java集合类的基本接口是Collection接口。而Collection接口必须继承java.lang.Iterable接口。
Iterator为传统迭代器,位于java.util.Iterator中。
JDK1.8在Iterable中添加默认方法for-each循环可以与任何实现了Iterable接口的对象一起工作。而java.util.Collection接口继承java.lang.Iterable,故标准类库中的任何集合都可以使用for-each循环。大家注意JDK1.8中已经可以在接口类中定义非抽象普通方法了。
Spliterator为JDK1.8最新添加的分割迭代器,位于java.util.Spliterator。
Iterator
首先我们先说一下迭代器Iterator:是一个接口—Iterator接口,其作用:用于取集合中的元素。
hasNext()如果仍有元素可以迭代,则返回true。
next()返回迭代的下一个元素。
remove()从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)这里注意一下使用方式,迭代时使用时,注意写法,例子中查看。
这里需要注意:
迭代器绑定前对数据集合的修改可以反应在迭代器遍历结果中。
迭代器绑定后对数据集合的修改会报ConcurrentModificationException异常。
forEachRemaining()是Iterator接口在1.8的时候引入的一个默认方法,与Iterable中forEach()对比,forEach可以调用多次,来遍历元素,forEachRemaining调用第一次与forEach相同,但是调用第二次则没有迭代,因为没有下一个元素了,现在写一个例子,演示一下。
运行结果:
大家可以看到两次forEachRemaining什么也没迭代出来,因为我们上面已经用传统方法迭代过一次了,已经不存在元素了,接下来我们注释掉转筒迭代器Iterator,再来运行一下,看一下结果。
第一次forEachRemaining可以迭代,第二次迭代剩余元素已经不存在,所以没有迭代出值。
Spliterator
接下来我们看一下Spliterator接口,我们主要看三个方法:
int characteristics();
返回特征值,根据每个容器不同的性质,这个迭代器也对应了相应的特征量。
例如一个Collection的spliterator会返回一个Spliterator.SIZED特征。
例如一个Set及其实例的spliterator会返回一个Spliterator.DISTINCT特征。
例如ArrayList为Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED。
例如TreeMap为Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED。
long estimateSize();
返回当前Spliterator实例中将要迭代的元素的数量的估计大小,如果无限,未知,或者计算成本太高,则返回Long.MAX_VALUE。
trySplit();
分割迭代器,每调用一次,将原来的迭代器等分为两份,并返回索引靠前的那一个子迭代器。
接下来我们来简单看一下Spliterator的一个实现例子,以最常用的ArrayList为例。
ArrayList提供获取Spliterator的方法,返回一个接下来是ArrayListSpliterator。ArrayListSpliterator为ArrayList的内部类实现了Spliterator。
private final ArrayList<E> list;// 数据集合
private int index; // 起始位置所有,会被advance()和split()函数修改。
private int fence; // fence则代表当前结束位置的最后一个下标,-1表示第一次使用,然后指最后一个索引
private int expectedModCount; // expectedModCount存放了当该迭代器所对应的ArrayList的modCount来保证迭代器在迭代数据中原本数组中的数据并没有发生变化,该变量会被fence更改。
estimateSize(),用来估算将要迭代的元素的数量。characteristics()返回特征值。之前已经在Spliterator接口中提到过了。
getFence(),会确定当前的迭代器的最后分隔下标,如果是-1,则代表此次是第一次使用,更新当前迭代器的expectedModCount为对应容器的modCount,同时更新fence为对应容器的size。
trySplit()每次分割,都将原来的迭代器等分为两个,并返回索引靠前的那个,除非实在太小,正常是二分。
tryAdvance()数组的ModCount进行验证。
forEachRemaining(),这里是一次性对所有数据进行操作。
接下来写一个例子来看一下使用方法:
运行结果为:
ListIterator
Iterator只为我们提供remove方法,如果要添加元素,可以使用ListIterator,它继承了Iterator接口,提供add与set操作。ListIterator是List的特有迭代器。