【Java基础】-【集合类】

news2024/11/29 20:55:52

目录

  • 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这四个接口的实现类,这四个接口将集合分成了四大类:

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

线程安全的List

  1. Vector:比较古老的API,虽然保证了线程安全,但是由于效率低一般不建议使用。
  2. Collections.SynchronizedList:SynchronizedList是Collections的内部类,Collections提供了synchronizedList方法,可以将一个线程不安全的List包装成线程安全的List,即SynchronizedList。它比Vector有更好的扩展性和兼容性,但是它所有的方法都带有同步锁,也不是性能最优的List。
  3. CopyOnWriteArrayList:CopyOnWriteArrayList是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是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();
}
  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()方法就是中间方法。中间方法的返回值是另外一个流。
    (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):用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
  2. 末端方法:末端方法是对流的最终操作。当对某个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极大地丰富了集合的功能。

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

  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只能执行一次。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166081.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【年度总结】2022回首瞻望 | 2023大展宏“兔“

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计专业大二本科在读&#xff0c;阿里云社区专家博主&#xff0c;华为云社区云享专家&#xff0c;CSDN SAP应用技术领域新兴创作者。   在学习工…

GO语言配置和基础语法应用(三)

C语言是直接影响Go语言设计的语言之一。 Go是一门编译型语言&#xff0c;Go语言的工具链将源代码及其依赖转换成计算机的机器指令&#xff08;译注&#xff1a;静态编译&#xff09;。 package mainimport "fmt"func main() {fmt.Println("Hello, 世界")…

一行代码加速Pytorch推理速度6倍

一行代码加速Pytorch推理速度6倍 Torch-TensorRT 是 PyTorch 的集成&#xff0c;它利用 NVIDIA GPU 上的 TensorRT 推理优化。 只需一行代码&#xff0c;它就提供了一个简单的 API&#xff0c;可在 NVIDIA GPU 上提供高达 6 倍的性能加速。 话不多说, 线上代码, 再解释原理!!…

【论文精读】Scaling distributed machine learning with the parameter server

Scaling distributed machine learning with the parameter server前言Abstract1. Introduction1.1 Contributions1.2 Engineering Challenges1.3 Related Work2. Machine Learning2.1 Goals2.2 Risk Minimization2.3 Generative Models3. Architecture3.1 (Key,Value) Vectors…

设计模式简要汇总

一、面向对象设计原则 开闭原则&#xff1a;一个软件实体&#xff08;类、模块、函数&#xff09;应该对扩展开放&#xff0c;对修改关闭。依赖倒置原则&#xff1a;高层模块不应该依赖底层模块&#xff0c;它们都应该依赖于抽象。抽象不应该依赖于细节&#xff0c;细节应该依…

语义分割——FCN模型pytorch实现

FCN网络简介 全卷积网络&#xff08;Fully Convolutional Networks&#xff0c;FCN&#xff09;是Jonathan Long等人于2015年在Fully Convolutional Networks for Semantic Segmentation一文中提出的用于图像语义分割的一种框架&#xff0c;是首个端对端的针对像素级预测的全卷…

BIGEMAP APP离线卫星地图数据应用

离线包&#xff08;高清卫星图&#xff09;&#xff1a; 1、一次下载永久可用&#xff0c;访问更快&#xff0c;下载更快 2、离线包数据可自动更新&#xff0c;无需重新下载&#xff0c;更新3到6个月一次 3、离线包可在无网络离线环境下使用&#xff0c;不受网络限制 4、离线包…

Springboot打成JAR包后读取配置文件

Springboot的默认配置文件为&#xff1a;application.properties或者是application.yml 如果这两个配置文件都存在&#xff0c;不冲突的话&#xff0c;就互相补充。冲突的话&#xff0c;则properties优先级高。 当我们使用IDEA创建出一个Springboot项目上时&#xff0c;配置文…

Git从入门到精通

目录 Git 配置 1. 在安装完成 Git 后&#xff0c;开始正式使用前&#xff0c;是需要有一些全局设置的&#xff0c;如用户名、邮箱。 2. 除了用户名、邮箱之外&#xff0c;还有很多的配置可以用来自定义 Git&#xff0c;如&#xff1a; 3. 查看所有的已经做出的配置&#xff…

Python3 常用内置函数解析(共28个函数)

文章目录一&#xff1a;Python3 操作符&#xff08;大全&#xff09;二&#xff1a;函数带括号与不带括号的区别三&#xff1a;不可变数据类型与可变数据类型四&#xff1a;Python3 内置函数&#xff08;大全&#xff09;1、input()&#xff1a;用于获取控制台的输入。2、print…

Django REST framework--渲染器

Django REST framework--渲染器Django REST framework--渲染器自定义接口规范渲染器基本原理Django 项目debug调试技巧异常信息处理配置异常处理模块自定义异常处理Django REST framework–渲染器 自定义接口规范 目前使用的是REST框架默认的返回格式&#xff0c;类似这种 […

【redis6】第十章(事务和锁机制)

Redis的事务定义 Redis事务是一个单独的隔离操作&#xff1a;事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中&#xff0c;不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。 Multi、Exec、discard 从输入Mu…

2023年面试题之Dubbo基础架构

一. Dubbo 的整体架构设计有哪些分层?接口服务层&#xff08;Service&#xff09;&#xff1a;该层与业务逻辑相关&#xff0c;根据 provider 和 consumer 的业务设计对应的接口和实现配置层&#xff08;Config&#xff09;&#xff1a;对外配置接口&#xff0c;以 ServiceCon…

Docker 应用实践-镜像篇

一个 Docker 镜像往往是由多个镜像层&#xff08;可读层&#xff09;叠加而成&#xff0c;每个层仅包含了前一层的差异部分&#xff0c;单个镜像层也往往可以看作镜像使用&#xff0c;当我们启动一个容器的时候&#xff0c;Docker 会加载镜像层并在其上添加一个可写层。容器上所…

C语言学习——字符函数和字符串函数

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C语言专栏&#xff1a;https://blog.csdn.net/vhhhbb/category_12174730.html 小苏希望大家能从这篇文章中收获到许…

实战Kaggle比赛:预测房价

实战Kaggle比赛&#xff1a;预测房价 目录 下载和缓存数据集访问和读取数据集数据预处理训练K折交叉验证模型选择提交Kaggle预测 本节我们将通过Kaggle比赛&#xff0c;将所学知识付诸实践。 Kaggle的房价预测比赛是一个很好的起点。 此数据集由Bart de Cock于2011年收集 (D…

Linux--线程互斥与同步--0112 13

线程互斥 1.背景概念 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。 临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码就叫做临界区。 互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一个执行流进入临界区 &#xff0c;对临界资源起保…

36岁北邮硕士,四段大厂经历,当初为了涨薪频繁跳槽,被裁后投遍所有公司,基本都不回复!...

今天给大家分享一位36岁北邮硕士的职场经历&#xff1a;2013年北邮硕士毕业后&#xff0c;分别在乐视、字节、阿里、小米待过&#xff0c;2022年被小米裁员&#xff0c;几乎投遍了boss上所有公司&#xff0c;基本都是已读不回。只有一个小公司的hr看了简历后回了一句“加油”。…

绕过某博客查看文章验证码,关注公众号得验证码

之前也写过一篇&#xff0c;当时使用Burpsuite抓包&#xff0c;改包&#xff0c;有点杀鸡用牛刀了。 虽然我挺支持为知识那啥的&#xff0c;但是吧要我去关注公众号太麻烦了 绕过查看文章需要验证码 其实就是改一个返回的字段&#xff0c;既然后端也是改&#xff0c;那我前端…

Google Earth Engine基础使用方法(一)

Google Earth Engine 1、注册账号1.1、设置谷歌账号辅助邮箱1.2、进入Google Earth Engine(如果第一次注册失败怎么办)1.3、进入Google Earth Engine Editor2、Editor主界面2.1、上传自己的矢量数据2.2、分享代码给别人2.3、保存代码2.4、几个有效快捷键2.5、搜索框有什么用3、…