目录
- Java中的容器(集合类)
- Java中的容器,线程安全和线程不安全的分别有哪些?
- Map接口的实现类
- Map put的过程
- 如何得到一个线程安全的Map?
- HashMap的特点
- JDK7和JDK8中的HashMap有什么区别?
- HashMap底层的实现原理
- HashMap的扩容机制
- HashMap中的循环链表是如何产生的?
- HashMap为什么用红黑树而不用B树?
- HashMap为什么线程不安全?
- HashMap如何实现线程安全?
- HashMap和HashTable的区别
- HashMap与ConcurrentHashMap的区别
- ConcurrentHashMap的实现方法
- ConcurrentHashMap是怎么分段分组的?
- LinkedHashMap
- LinkedHashMap的底层原理
- TreeMap的底层原理
- Map和Set的区别
- List和Set的区别
- ArrayList和LinkedList的区别
- 线程安全的List
- ArrayList的数据结构
- CopyOnWriteArrayList的原理
- TreeSet和HashSet的区别
- HashSet的底层结构
- BlockingQueue中的方法及设计原理
- BlockingQueue的实现原理
- Stream(不是IOStream)的方法
Java中的容器(集合类)
Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,这四个接口将集合分成了四大类:
- Set代表无序的,元素不可重复的集合;
- List代表有序的,元素可以重复的集合;
- Queue代表先进先出(FIFO)的队列;
- Map代表具有映射关系(key-value)的集合。
这些接口拥有众多的实现类,其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。
Collection体系的继承树:
Map体系的继承树:
注:紫色框体代表接口,其中加粗的是代表四类集合的接口。蓝色框体代表实现类,其中有阴影的是常用实现类。
Java中的容器,线程安全和线程不安全的分别有哪些?
java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,但是它们的优点是性能好。如果需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()
方法,将这些集合类包装成线程安全的集合类。java.util包下也有线程安全的集合类,例如Vector、Hashtable。这些集合类都是比较古老的API,虽然实现了线程安全,但是性能很差。所以即便是需要使用线程安全的集合类,也建议将线程不安全的集合类包装成线程安全集合类的方式,而不是直接使用这些古老的API。从Java5开始,Java在java.util.concurrent
包下提供了大量支持高效并发访问的集合类,它们既能包装良好的访问性能,有能包装线程安全。这些集合类可以分为两部分,它们的特征如下:
- 以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
- 以以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。
java.util.concurrent包下线程安全的集合类的体系结构:
Map接口的实现类
Map接口有很多实现类,其中比较常用的有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。对于不需要排序的场景,优先考虑使用HashMap,因为它是性能最好的Map实现。如果需要保证线程安全,则可以使用ConcurrentHashMap。它的性能好于Hashtable,因为它在put时采用分段锁/CAS的加锁机制,而不是像Hashtable那样,无论是put还是get都做同步处理。对于需要排序的场景,如果需要按插入顺序排序则可以使用LinkedHashMap,如果需要将key按自然顺序排列甚至是自定义顺序排列,则可以选择TreeMap。如果需要保证线程安全,则可以使用Collections工具类将上述实现类包装成线程安全的Map。
Map put的过程
HashMap是最经典的Map实现,下面以它的视角介绍put的过程:
- 首次扩容:先判断数组是否为空,若数组为空则进行第一次扩容(resize);
- 计算索引:通过hash算法,计算键值对在数组中的索引;
- 插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度达到8,则将链表转换成红黑树,并将数据插入树中; - 再次扩容如果数组中元素个数(size)超过threshold,则再次进行扩容操作。
HashMap添加数据的详细过程:
如何得到一个线程安全的Map?
使用Collections工具类,将线程不安全的Map包装成线程安全的Map;
使用java.util.concurrent包下的Map,如ConcurrentHashMap;
不建议使用Hashtable,虽然Hashtable是线程安全的,但是性能较差。
HashMap的特点
HashMap是线程不安全的实现;可以使用null作为key或value。
HashMap的线程不安全主要体现在下面两个方面:
- 在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
- 在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。JDK1.8不会成环,因为改用了尾插法,但因为其put和get方法都没有加锁,所以仍然会造成数据覆盖的情况
JDK7和JDK8中的HashMap有什么区别?
- JDK7中的HashMap采用头插法,且是基于数组+链表来实现的,它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。
- JDK8中的HashMap采用头插法,且是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。
1.8和1.7的区别是在查询效率上,1.8在1.7的基础上新增了一个红黑树来增加查询效率
HashMap底层的实现原理
它基于hash算法,通过put方法和get方法存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用K的hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()
方法确定键值对。如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来。在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
HashMap的扩容机制
- 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5-8倍)。
- 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
- 为了解决哈希碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值(7或8)时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值(6)时,又会将红黑树转换回单向链表提高性能。
对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。
例如我们从16扩展为32时,具体的变化如下所示:
因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
HashMap中的循环链表是如何产生的?
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。
HashMap为什么用红黑树而不用B树?
B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。
HashMap为什么线程不安全?
HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。
HashMap如何实现线程安全?
直接使用Hashtable类;直接使用ConcurrentHashMap;使用Collections将HashMap包装成线程安全的Map。
HashMap和HashTable的区别
- Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点。
- Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发空指针异常,但HashMap可以使用null作为key或value。
与Vector类似的是,尽量少用Hashtable实现类,即使需要创建线程安全的Map实现类,可以通过Collections工具类把HashMap变成线程安全的Map。
HashMap与ConcurrentHashMap的区别
- HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。
- ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。
ConcurrentHashMap的实现方法
- 在jdk 1.7中,ConcurrentHashMap是由Segment数据结构和HashEntry数组结构构成,采取分段锁来保证安全性。Segment是ReentrantLock重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry数组,Segment的结构和HashMap类似,是一个数组和链表结构。
- JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
ConcurrentHashMap是怎么分段分组的?
- get操作:先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成volatile类型。
- put操作:
(1)判断是否需要扩容;
(2)定位到添加元素的位置,将其放入 HashEntry 数组中。
插入过程会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(尾插法),会通过继承ReentrantLock的tryLock()
方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()
方法去获取锁,超过指定次数就挂起,等待唤醒。
LinkedHashMap
- LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
- 优点:LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
- 缺点:LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
LinkedHashMap的底层原理
LinkedHashMap继承于HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。
如下图,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新的键值对节点插入时,新节点最终会接在tail引用指向的节点后面。而tail引用则会移动到新的节点上,这样一个双向链表就建立起来了。
TreeMap的底层原理
- TreeMap基于红黑树(Red-Black tree)实现。映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。
- TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(N)。TreeMap包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
Map和Set的区别
- Set代表无序的,元素不可重复的集合;
- Map代表具有映射关系(key-value)的集合,其所有的key是一个Set集合,即key无序且不能重复。
List和Set的区别
- Set代表无序的,元素不可重复的集合;
- List代表有序的,元素可以重复的集合。
ArrayList和LinkedList的区别
- ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;
- 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N);
- 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;
- LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全的List
- Vector:比较古老的API,虽然保证了线程安全,但是由于效率低一般不建议使用。
- Collections.SynchronizedList:SynchronizedList是Collections的内部类,Collections提供了synchronizedList方法,可以将一个线程不安全的List包装成线程安全的List,即SynchronizedList。它比Vector有更好的扩展性和兼容性,但是它所有的方法都带有同步锁,也不是性能最优的List。
- CopyOnWriteArrayList:CopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类,它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。在所有线程安全的List中,它是性能最优的方案。
ArrayList的数据结构
- ArrayList的底层是用数组来实现的,默认第一次插入元素时创建大小为10的数组,超出限制时会增加50%的容量,并且数据以
System.arraycopy()
复制到新的数组,因此最好能给出数组大小的预估值。 - 按数组下标访问元素的性能很高,这是数组的基本优势。直接在数组末尾加入元素的性能也高,但如果按下标插入、删除元素,则要用
System.arraycopy()
来移动部分受影响的元素,性能就变差了,这是基本劣势。
CopyOnWriteArrayList的原理
- CopyOnWriteArrayList是Java并发包里提供的并发类,简单来说它就是一个线程安全且读操作无锁的ArrayList。正如其名字一样,在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。这样就保证了写操作的线程安全。CopyOnWriteArrayList允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。
- 优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。在遍历传统的List时,若中途有别的线程对其进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的List容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。
- 缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。
TreeSet和HashSet的区别
HashSet、TreeSet中的元素都是不能重复的,并且它们都是线程不安全的,二者的区别是:
- HashSet中的元素可以是null,但TreeSet中的元素不能是null;
- HashSet不能保证元素的排列顺序,而TreeSet支持自然排序、定制排序两种排序的方式;
- HashSet底层是采用哈希表实现的,而TreeSet底层是采用红黑树实现的。
HashSet的底层结构
HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
BlockingQueue中的方法及设计原理
为了应对不同的业务场景,BlockingQueue 提供了4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每组方法的表现是不同的。这些方法如下:
四组不同的行为方式含义如下:
- 抛异常:如果操作无法立即执行,则抛一个异常;
- 特定值:如果操作无法立即执行,则返回一个特定的值(一般是 true / false)。
- 阻塞:如果操作无法立即执行,则该方法调用将会发生阻塞,直到能够执行;
- 超时:如果操作无法立即执行,则该方法调用将会发生阻塞,直到能够执行。但等待时间不会超过给定值,并返回一个特定值以告知该操作是否成功(典型的是true / false)。
BlockingQueue的实现原理
BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于put与take操作的原理是类似的。下面以ArrayBlockingQueue为例,来说明BlockingQueue的实现原理。
- 首先看一下ArrayBlockingQueue的构造函数,它初始化了put和take函数中用到的关键成员变量,这两个变量的类型分别是ReentrantLock和Condition。ReentrantLock是AbstractQueuedSynchronizer(AQS)的子类,它的newCondition函数返回的Condition实例,是定义在AQS类内部的ConditionObject类,该类可以直接调用AQS相关的函数。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
- put函数会在队列末尾添加元素,如果队列已经满了,无法添加元素的话,就一直阻塞等待到可以加入为止。函数的源码如下所示。我们会发现put函数使用了wait/notify的机制。与一般生产者-消费者的实现方式不同,同步队列使用ReentrantLock和Condition相结合的机制,即先获得锁,再等待,而不是synchronized和wait的机制。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
- 再来看一下消费者调用的take函数,take函数在队列为空时会被阻塞,一直到阻塞队列加入了新的元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
await操作:
我们发现ArrayBlockingQueue并没有使用Object.wait,而是使用的Condition.await,这是为什么呢?Condition对象可以提供和Object的wait和notify一样的行为,但是后者必须先获取synchronized这个内置的monitor锁才能调用,而Condition则必须先获取ReentrantLock。这两种方式在阻塞等待时都会将相应的锁释放掉,但是Condition的等待可以中断,这是二者唯一的区别。
我们先来看一下Condition的await函数,await函数的流程大致如下图所示。await函数主要有三个步骤,一是调用addConditionWaiter函数,在condition wait queue队列中添加一个节点,代表当前线程在等待一个消息。然后调用fullyRelease函数,将持有的锁释放掉,调用的是AQS的函数。最后一直调用isOnSyncQueue函数判断节点是否被转移到sync queue队列上,也就是AQS中等待获取锁的队列。如果没有,则进入阻塞状态,如果已经在队列上,则调用acquireQueued函数重新获取锁。
signal操作:signal函数将condition wait queue队列中队首的线程节点转移等待获取锁的sync queue队列中。这样的话,await函数中调用isOnSyncQueue函数就会返回true,导致await函数进入最后一步重新获取锁的状态。
我们这里来详细解析一下condition wait queue和sync queue两个队列的设计原理。condition wait queue是等待消息的队列,因为阻塞队列为空而进入阻塞状态的take函数操作就是在等待阻塞队列不为空的消息。而sync queue队列则是等待获取锁的队列,take函数获得了消息,就可以运行了,但是它还必须等待获取锁之后才能真正进行运行状态。
signal函数其实就做了一件事情,就是不断尝试调用transferForSignal函数,将condition wait queue队首的一个节点转移到sync queue队列中,直到转移成功。因为一次转移成功,就代表这个消息被成功通知到了等待消息的节点。signal函数的示意图如下所示。
Stream(不是IOStream)的方法
Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。
- 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的
map()
方法就是中间方法。中间方法的返回值是另外一个流。
(1)filter(Predicate predicate)
:过滤Stream中所有不符合predicate的元素。
(2)mapToXxx(ToXxxFunction mapper):
使用ToXxxFunction
对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction
转换生成的所有元素。
(3)peek(Consumer action)
:依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
(4)distinct()
:用于排序流中所有重复的元素(判断元素重复的标准是使用equals()
比较返回true)。这是一个有状态的方法。
(5)sorted()
:用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
(6)limit(long maxSize)
:用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。 - 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的
sum()
、count()
、average()
等方法都是末端方法。
(1)forEach(Consumer action)
:遍历流中所有元素,对每个元素执行action
。
(2)toArray()
:将流中所有元素转换为一个数组。
(3)reduce()
:该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
(4)min()
:返回流中所有元素的最小值。
(5)max()
:返回流中所有元素的最大值。
(6)count()
:返回流中所有元素的数量。
(7)anyMatch(Predicate predicate)
:判断流中是否至少包含一个元素符合Predicate条件。
(8)noneMatch(Predicate predicate)
:判断流中是否所有元素都不符合Predicate条件。
(9)findFirst()
:返回流中的第一个元素。
(10)findAny()
:返回流中的任意一个元素。
除此之外,Java 8允许使用流式API来操作集合,Collection接口提供了一个stream()
默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。
关于流的方法还有如下两个特征:
- 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
- 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
说明:
- Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
- Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤如下:
(1)使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
(2)重复调用Builder的add()方法向该流中添加多个元素。
(3)调用Builder的build()方法获取对应的Stream。
(4)调用Stream的聚集方法。
在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。