【Java基础】-【集合类】

Java中的容器集合类

Java中的集合类主要由Collection和Map这两个接口派生而出其中Collection接口又派生出三个子接口分别是Set、List、Queue。所有的Java集合类都是Set、List、Queue、Map这四个接口的实现类这四个接口将集合分成了四大类

  1. Set代表无序的元素不可重复的集合
  2. List代表有序的元素可以重复的集合
  3. Queue代表先进先出FIFO的队列
  4. 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包下提供了大量支持高效并发访问的集合类它们既能包装良好的访问性能有能包装线程安全。这些集合类可以分为两部分它们的特征如下

  1. 以Concurrent开头的集合类代表了支持并发访问的集合它们可以支持多个线程并发写入访问这些写入线程的所有操作都是线程安全的但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合因此在并发写入时有较好的性能。
  2. 以以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的过程

  1. 首次扩容先判断数组是否为空若数组为空则进行第一次扩容resize
  2. 计算索引通过hash算法计算键值对在数组中的索引
  3. 插入数据
    如果当前位置元素为空则直接插入数据
    如果当前位置元素非空且key已存在则直接覆盖其value
    如果当前位置元素非空且key不存在则将数据链到链表末端
    若链表长度达到8则将链表转换成红黑树并将数据插入树中
  4. 再次扩容如果数组中元素个数size超过threshold则再次进行扩容操作。

HashMap添加数据的详细过程
请添加图片描述

如何得到一个线程安全的Map

使用Collections工具类将线程不安全的Map包装成线程安全的Map
使用java.util.concurrent包下的Map如ConcurrentHashMap
不建议使用Hashtable虽然Hashtable是线程安全的但是性能较差。

HashMap的特点

HashMap是线程不安全的实现可以使用null作为key或value。

HashMap的线程不安全主要体现在下面两个方面

  1. 在JDK1.7中当并发执行扩容操作时会造成环形链和数据丢失的情况。
  2. 在JDK1.8中在并发执行put操作时会发生数据覆盖的情况。JDK1.8不会成环因为改用了尾插法但因为其put和get方法都没有加锁所以仍然会造成数据覆盖的情况

JDK7和JDK8中的HashMap有什么区别

  1. JDK7中的HashMap采用头插法且是基于数组+链表来实现的它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中一旦发生hashCode冲突那么就会将该KV键值对放到对应的已有元素的后面 此时便形成了一个链表式的存储结构。JDK7中HashMap的实现方案有一个明显的缺点即当Hash冲突严重时在桶上形成的链表会变得越来越长这样在查询时的效率就会越来越低其时间复杂度为O(N)。
  2. 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的扩容机制

  1. 数组的初始容量为16而容量是以2的次方扩充的一是为了提高性能使用足够大的数组二是为了能使用位运算代替取模预算据说提升了5-8倍。
  2. 数组是否需要扩充是通过负载因子判断的如果当前元素个数为数组容量的0.75时就会扩充数组。这个0.75就是默认的负载因子可由构造器传入。我们也可以设置大于1的负载因子这样数组就不会扩充牺牲性能节省内存。
  3. 为了解决哈希碰撞数组中的元素是单向链表类型。当链表长度到达一个阈值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的区别

  1. Hashtable是一个线程安全的Map实现但HashMap是线程不安全的实现所以HashMap比Hashtable的性能高一点。
  2. Hashtable不允许使用null作为key和value如果试图把null值放进Hashtable中将会引发空指针异常但HashMap可以使用null作为key或value。

与Vector类似的是尽量少用Hashtable实现类即使需要创建线程安全的Map实现类可以通过Collections工具类把HashMap变成线程安全的Map。

HashMap与ConcurrentHashMap的区别

  1. HashMap是非线程安全的这意味着不应该在多线程中对这些Map进行修改操作否则会产生数据不一致的问题甚至还会因为并发插入元素而导致链表成环这样在查找时就会发生死循环影响到整个应用程序。Collections工具类可以将一个Map转换成线程安全的实现其实也就是通过一个包装类然后把所有功能都委托给传入的Map而包装类是基于synchronized关键字来保证线程安全的Hashtable也是基于synchronized关键字底层使用的是互斥锁性能与吞吐量比较低。
  2. ConcurrentHashMap的实现细节远没有这么简单因此性能也要高上许多。它没有使用一个全局锁来锁住自己而是采用了减少锁粒度的方法尽量减少因为竞争锁而导致的阻塞与冲突而且ConcurrentHashMap的检索操作是不需要锁的。

ConcurrentHashMap的实现方法

  1. 在jdk 1.7中ConcurrentHashMap是由Segment数据结构和HashEntry数组结构构成采取分段锁来保证安全性。Segment是ReentrantLock重入锁在ConcurrentHashMap中扮演锁的角色HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组一个Segment里包含一个HashEntry数组Segment的结构和HashMap类似是一个数组和链表结构。
    请添加图片描述
  2. JDK1.8的实现已经摒弃了Segment的概念而是直接用Node数组+链表+红黑树的数据结构来实现并发控制使用Synchronized和CAS来操作整个看起来就像是优化过且线程安全的HashMap虽然在JDK1.8中还能看到Segment的数据结构但是已经简化了属性只是为了兼容旧版本。
    请添加图片描述

ConcurrentHashMap是怎么分段分组的

  1. get操作先经过一次再散列然后使用这个散列值通过散列运算定位到 Segment再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成volatile类型。
  2. put操作
    1判断是否需要扩容
    2定位到添加元素的位置将其放入 HashEntry 数组中。
    插入过程会进行第一次key的hash来定位Segment的位置如果该Segment还没有初始化即通过CAS操作进行赋值然后进行第二次hash操作找到相应的HashEntry的位置这里会利用继承过来的锁的特性在将数据插入指定的HashEntry位置时尾插法会通过继承ReentrantLock的tryLock()方法尝试去获取锁如果获取成功就直接插入相应的位置如果已经有线程获取该Segment的锁那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁超过指定次数就挂起等待唤醒。

LinkedHashMap

  1. LinkedHashMap使用双向链表来维护key-value对的顺序其实只需要考虑key的顺序该链表负责维护Map的迭代顺序迭代顺序与key-value对的插入顺序保持一致。
  2. 优点LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序只要插入key-value对时保持顺序即可同时又可避免使用TreeMap所增加的成本。
  3. 缺点LinkedHashMap需要维护元素的插入顺序因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序所以在迭代访问Map里的全部元素时将有较好的性能。

LinkedHashMap的底层原理

LinkedHashMap继承于HashMap它在HashMap的基础上通过维护一条双向链表解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上LinkedHashMap很多方法直接继承自HashMap仅为维护双向链表重写了部分方法。

如下图淡蓝色的箭头表示前驱引用红色箭头表示后继引用。每当有新的键值对节点插入时新节点最终会接在tail引用指向的节点后面。而tail引用则会移动到新的节点上这样一个双向链表就建立起来了。
请添加图片描述

TreeMap的底层原理

  1. TreeMap基于红黑树Red-Black tree实现。映射根据其键的自然顺序进行排序或者根据创建映射时提供的Comparator进行排序具体取决于使用的构造方法。
  2. 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的区别

  1. Set代表无序的元素不可重复的集合
  2. Map代表具有映射关系key-value的集合其所有的key是一个Set集合即key无序且不能重复。

List和Set的区别

  1. Set代表无序的元素不可重复的集合
  2. List代表有序的元素可以重复的集合。

ArrayList和LinkedList的区别

  1. ArrayList的实现是基于数组LinkedList的实现是基于双向链表
  2. 对于随机访问ArrayList要优于LinkedListArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起查找某个元素的时间复杂度是O(N)
  3. 对于插入和删除操作LinkedList要优于ArrayList因为当元素被添加到LinkedList任意位置的时候不需要像ArrayList那样重新计算大小或者是更新索引
  4. LinkedList比ArrayList更占内存因为LinkedList的节点除了存储数据还存储了两个引用一个指向前一个元素一个指向后一个元素。

线程安全的List

  1. Vector比较古老的API虽然保证了线程安全但是由于效率低一般不建议使用。
  2. Collections.SynchronizedListSynchronizedList是Collections的内部类Collections提供了synchronizedList方法可以将一个线程不安全的List包装成线程安全的List即SynchronizedList。它比Vector有更好的扩展性和兼容性但是它所有的方法都带有同步锁也不是性能最优的List。
  3. CopyOnWriteArrayListCopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时线程将会直接读取集合本身无须加锁与阻塞。当线程对此类集合执行写入操作时集合会在底层复制一份新的数组接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作因此它是线程安全的。在所有线程安全的List中它是性能最优的方案。

ArrayList的数据结构

  1. ArrayList的底层是用数组来实现的默认第一次插入元素时创建大小为10的数组超出限制时会增加50%的容量并且数据以System.arraycopy()复制到新的数组因此最好能给出数组大小的预估值。
  2. 按数组下标访问元素的性能很高这是数组的基本优势。直接在数组末尾加入元素的性能也高但如果按下标插入、删除元素则要用System.arraycopy()来移动部分受影响的元素性能就变差了这是基本劣势。

CopyOnWriteArrayList的原理

  1. CopyOnWriteArrayList是Java并发包里提供的并发类简单来说它就是一个线程安全且读操作无锁的ArrayList。正如其名字一样在写操作时会复制一份新的List在新的List上完成写操作然后再将原引用指向新的List。这样就保证了写操作的线程安全。CopyOnWriteArrayList允许线程并发访问读操作这个时候是没有加锁限制的性能较高。而写操作的时候则首先将容器复制一份然后在新的副本上执行写操作这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意在上锁执行写操作的过程中如果有需要读操作会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。
  2. 优点读操作性能很高因为无需任何同步措施比较适用于读多写少的并发场景。在遍历传统的List时若中途有别的线程对其进行修改则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想遍历和修改操作分别作用在不同的List容器所以在使用迭代器进行遍历时候也就不会抛出ConcurrentModificationException异常了。
  3. 缺点一是内存占用问题毕竟每次执行写操作都要将原容器拷贝一份数据量大时对内存压力较大可能会引起频繁GC。二是无法保证实时性Vector对于读写操作均加锁同步可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因写和读分别作用在新老不同容器上在写操作执行过程中读不会阻塞但读取到的却是老容器的数据。

TreeSet和HashSet的区别

HashSet、TreeSet中的元素都是不能重复的并且它们都是线程不安全的二者的区别是

  1. HashSet中的元素可以是null但TreeSet中的元素不能是null
  2. HashSet不能保证元素的排列顺序而TreeSet支持自然排序、定制排序两种排序的方式
  3. HashSet底层是采用哈希表实现的而TreeSet底层是采用红黑树实现的。

HashSet的底层结构

HashSet是基于HashMap实现的默认构造函数是构建一个初始容量为16负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存而 HashMap 的 value 则存储了一个 PRESENT它是一个静态的 Object 对象。

BlockingQueue中的方法及设计原理

为了应对不同的业务场景BlockingQueue 提供了4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话每组方法的表现是不同的。这些方法如下
请添加图片描述
四组不同的行为方式含义如下

  1. 抛异常如果操作无法立即执行则抛一个异常
  2. 特定值如果操作无法立即执行则返回一个特定的值(一般是 true / false)。
  3. 阻塞如果操作无法立即执行则该方法调用将会发生阻塞直到能够执行
  4. 超时如果操作无法立即执行则该方法调用将会发生阻塞直到能够执行。但等待时间不会超过给定值并返回一个特定值以告知该操作是否成功(典型的是true / false)。

BlockingQueue的实现原理

BlockingQueue是一个接口它的实现类有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。它们的区别主要体现在存储结构上或对元素操作上的不同但是对于put与take操作的原理是类似的。下面以ArrayBlockingQueue为例来说明BlockingQueue的实现原理。

  1. 首先看一下ArrayBlockingQueue的构造函数它初始化了put和take函数中用到的关键成员变量这两个变量的类型分别是ReentrantLock和Condition。ReentrantLock是AbstractQueuedSynchronizerAQS的子类它的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();
}
  1. 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();
    }
}
  1. 再来看一下消费者调用的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提供了大量的方法进行聚集操作这些方法既可以是“中间的”也可以是“末端的”。

  1. 中间方法中间操作允许流保持打开状态并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。
    1filter(Predicate predicate)过滤Stream中所有不符合predicate的元素。
    2mapToXxx(ToXxxFunction mapper)使用ToXxxFunction对流中的元素执行一对一的转换该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
    3peek(Consumer action)依次对每个元素执行一些操作该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
    4distinct()用于排序流中所有重复的元素判断元素重复的标准是使用equals()比较返回true。这是一个有状态的方法。
    5sorted()用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
    6limit(long maxSize)用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
  2. 末端方法末端方法是对流的最终操作。当对某个Stream执行末端方法后该流将会被“消耗”且不再可用。上面程序中的sum()count()average()等方法都是末端方法。
    1forEach(Consumer action)遍历流中所有元素对每个元素执行action
    2toArray()将流中所有元素转换为一个数组。
    3reduce()该方法有三个重载的版本都用于通过某种操作来合并流中的元素。
    4min()返回流中所有元素的最小值。
    5max()返回流中所有元素的最大值。
    6count()返回流中所有元素的数量。
    7anyMatch(Predicate predicate)判断流中是否至少包含一个元素符合Predicate条件。
    8noneMatch(Predicate predicate)判断流中是否所有元素都不符合Predicate条件。
    9findFirst()返回流中的第一个元素。
    10findAny()返回流中的任意一个元素。

除此之外Java 8允许使用流式API来操作集合Collection接口提供了一个stream()默认方法该方法可返回该集合对应的流接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作因此Stream极大地丰富了集合的功能。

关于流的方法还有如下两个特征

  1. 有状态的方法这种方法会给流增加一些新的属性比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
  2. 短路方法短路方法可以尽早结束对流的操作不必检查所有的元素。

说明

  1. Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中Stream是一个通用的流接口而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
  2. 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只能执行一次。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java