[TOC]
一、Java多线程
1.Java多线程基础知识
Java 给多线程编程提供了内置的支持。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
2.Java线程的生命周期
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的Sleep()或Join发出I/O请求,线程就会进入到阻塞状态,当Sleep()状态超时,join()等待线程终止或超时,或者I/O处理完毕,线程就会进入就绪状态。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
3.线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
4.创建线程
Java创建线程提供了以下三种方法:
- 通过实现Runnable接口
- 通过继承Thread类本身
- 通过Callable和Future创建线程
4.1 通过实现Runnable接口来创建线程
通过Runnable接口需要重写run() 方法
public void run()
在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。
Thread 定义了几个构造方法,下面的这个是我们经常使用的:
Thread(Runnable threadOb,String threadName);
这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。
新线程创建之后,你调用它的 start() 方法它才会运行。
void start();
4.2 通过继承Thread来创建线程
创建一个线程的第二种方法是创建一个新的类,该类继承Thread类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
Thread方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() |
Thread静态类方法
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
4.3 通过Callable和Future创建线程
- 创建 Callable 接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
For Example:
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executorService.submit(futureTask);
executorService.shutdown();
System.out.println("主线程在执行任务...");
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
try {
System.out.println("task运行结果:"+futureTask.get());
} catch (InterruptedException ex) {
ex.printStackTrace();
} catch (ExecutionException ex) {
ex.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
}
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("子线程在执行任务...");
//模拟任务耗时
Thread.sleep(5000);
return 1000;
}
}
5.线程的取消和中断
- 不安全的取消
Stop()/suspend()/resume()是过期的API,容易导致死锁或者数据不一致 - 安全的终止线程
- interrupt() 中断线程,本质是将线程的中断标志位设为true,其他线程向需要中断的线程打个招呼。是否真正进行中断由线程自己决定
- isInterrupted() 线程检查自己的中断标志位
- 静态方法Thread.interrupted() 将中断标志位复位为false
6.常用的方法理解
- run()和start()
run就是一个普通的方法,跟其他类的实例方法没有任何区别。 - Sleep <Br>
不会释放锁,所以我们在用sleep时,要把sleep放在同步代码块的外面。 - yield()<Br>
当前线程出让cpu占有权,当前线程变成了可运行状态,下一时刻仍然可能被cpu选中,不会释放锁。 - wait()和 notify()/notiyfAll()
调用以前,当前线程必须要持有锁,调用了wait() notify()/notiyfAll()会释放锁。
通知机制:<Br>
线程 A调用了对象O的wait方法进入等待状态,线程 B调用了对象O的notify方法进行唤醒,唤醒的是在对象O上wait的线程(比如线程A)
notify() 唤醒一个线程,唤醒哪一个完全看cpu的心情(谨慎使用)
notiyfAll() 所有在对象O上wait的线程全部唤醒(应该用notiyfAll())
6.线程关键字
6.1 volatile
多个线程同时访问一个共享的变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存中。
Violate修饰字段,对这个变量的访问必须要从共享内存刷新一次。最新的修改写回共享内存。可以保证字段的可见性。绝对不是线程安全的,没有操作的原子性。
适用场景:
1、一个线程写,多个线程读;
2、volatile变量的变化很固定
public class VolatileThread implements Runnable {
private volatile int a= 0;
@Override
public void run() {
// synchronized (this){
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
// }
}
}
public class VolatileTest {
public static void main(String[] args) {
VolatileThread volatileThread = new VolatileThread();
Thread t1 = new Thread(volatileThread);
Thread t2 = new Thread(volatileThread);
Thread t3 = new Thread(volatileThread);
Thread t4 = new Thread(volatileThread);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
6.2 synchronized
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
Synchronized的类锁和对象锁,本质上是两把锁,类锁实际锁的是每一个类的class对象。对象锁锁的是当前对象实例。
public class InstanceAndClass {
//测试类锁
private static class TestClassSyn extends Thread{
@Override
public void run() {
System.out.println("TestClass is going...");
synClass();
}
}
//测试对象锁
private static class TestInstanceSyn extends Thread{
private InstanceAndClass instanceAndClass;
public TestInstanceSyn(InstanceAndClass instanceAndClass) {
this.instanceAndClass = instanceAndClass;
}
@Override
public void run() {
System.out.println("TestInstance is going..."+instanceAndClass);
instanceAndClass.synInstance();
}
}
//测试对象锁
private static class TestInstance2Syn implements Runnable{
private InstanceAndClass instanceAndClass;
public TestInstance2Syn(InstanceAndClass instanceAndClass) {
this.instanceAndClass = instanceAndClass;
}
@Override
public void run() {
System.out.println("TestInstance2 is going..."+instanceAndClass);
instanceAndClass.synInstance2();
}
}
//锁对象的方法
private synchronized void synInstance(){
SleepUtils.second(3);
System.out.println("synInstance is going...");
SleepUtils.second(3);
System.out.println("synInstance ended");
}
//锁对象的方法
private synchronized void synInstance2(){
SleepUtils.second(3);
System.out.println("synInstance2 going...");
SleepUtils.second(3);
System.out.println("synInstance2 ended");
}
//锁类的方法
private static synchronized void synClass(){
SleepUtils.second(1);
System.out.println("synClass going...");
SleepUtils.second(1);
}
public static void main(String[] args) {
InstanceAndClass instanceAndClass = new InstanceAndClass();
Thread t1 = new TestClassSyn();
Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass));
Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass));
t2.start();
t3.start();
SleepUtils.second(1);
t1.start();
}
}
6.3等待和通知机制
- 等待方原则:
1、获取对象锁
2、如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
3、条件满足以后,才能执行相关的业务逻辑
Synchronized(对象){
While(条件不满足){
对象.wait()
}
业务逻辑处理
}
- 通知方原则
1、获得对象的锁;<Br>
2、 改变条件;<Br>
3、 通知所有等待在对象的线程
Synchronized(对象){
业务逻辑处理,改变条件
对象.notify/notifyAll
}
6.4管道输入输出流
文件输入输出,网络输入输出,管道输入输出流用于线程中间的数据传递,传输媒介的内存
pipedOutputStream/input 面向的字节
pipedReader/Writer 面向的是字符
只适合线程间一对一的通信,适用范围较狭窄。
public class PipeTransfer {
private static class Print implements Runnable{
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive =0;
try {
while((receive=in.read())!=-1){
System.out.println((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
//必须进行连接
out.connect(in);
Thread t1 = new Thread(new Print(in),"PrintThread");
t1.start();
int receive =0;
try {
while((receive=System.in.read())!=-1){
out.write(receive);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
out.close();
}
}
}
6.5 join方法
线程A,执行了thread.join(),线程A等待thread线程终止了以后,A在join后面的语句才会继续执行
6.6 一些有用的方法
很多时候我们希望在元素不存在时插入元素,我们一般会像下面那样写代码
synchronized(map){
if (map.get(key) == null){
return map.put(key, value);
} else{
return map.get(key);
}
}
putIfAbsent(key,value)方法原子性的实现了同样的功能
V putIfAbsent(K key, V value)
如果key对应的value不存在,则put进去,返回null。否则不put,返回已存在的value。
boolean remove(Object key, Object value)
如果key对应的值是value,则移除K-V,返回true。否则不移除,返回false。
boolean replace(K key, V oldValue, V newValue)
如果key对应的当前值是oldValue,则替换为newValue,返回true。否则不替换,返回false。
二、并发工具和并发容器
1.Hash
散列,任意长度的输入,通过一种算法,变换成固定长度的输出。属于压缩的映射。Md5,Sha,取余都是散列算法,ConcurrentHashMap中是wang/jenkins算法
2.ConcurrentHashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3.ConcurrentHashMap在JDK1.7中的实现
分段锁的设计思路
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际是一种可重入锁(ReentrantLock),HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel(参数concurrencyLevel是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个map,根据这个来确定Segment数组的大小concurrencyLevel默认是DEFAULT_CONCURRENCY_LEVEL = 16;)。
ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,可以看到其中的对象属性要么是final的,要么是volatile的。
4.ConcurrentHashMap在1.8下的实现
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[]<Br> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。<Br>
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
5.ConcurrentSkipListMap和ConcurrentSkipListSet
ConcurrentSkipListMap TreeMap的并发实现
有序Map
ConcurrentSkipListSet TreeSet的并发实现
有序Set
6.SkipList
二分查找和AVL树查找
二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。
如果需要的是一个能够进行二分查找,又能快速添加和删除元素的数据结构,首先就是二叉查找树,二叉查找树在最坏情况下可能变成一个链表。
于是,就出现了平衡二叉树,根据平衡算法的不同有AVL树,B-Tree,B+Tree,红黑树等,但是AVL树实现起来比较复杂,平衡操作较难理解,这时候就可以用SkipList跳跃表结构。
传统意义的单链表是一个线性结构,向有序的链表中插入一个节点需要O(n)的时间,查找操作需要O(n)的时间。
如果我们使用上图所示的跳跃表,就可以减少查找所需时间为O(n/2),因为我们可以先通过每个节点的最上面的指针先进行查找,这样子就能跳过一半的节点。
比如我们想查找19,首先和6比较,大于6之后,在和9进行比较,然后在和12进行比较......最后比较到21的时候,发现21大于19,说明查找的点在17和21之间,从这个过程中,我们可以看出,查找的时候跳过了3、7、12等点,因此查找的复杂度为O(n/2)。
跳跃表其实也是一种通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。
跳跃表又被称为概率,或者说是随机化的数据结构,目前开源软件 Redis 和 lucence都有用到它。
7.ConcurrentLinkedQueue 无界非阻塞队列
常用方法:
Add,offer:添加元素
Peek:get头元素并不把元素拿走
poll():get头元素把元素拿走
8.CopyOnWriteArrayList和CopyOnWriteArraySet
写的时候进行复制,可以进行并发的读。
适用读多写少的场景:比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。
弱点:内存占用高,数据一致性弱
Tips 多用isEmpty()少用 Size()
9.Lock
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
1.接口类
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock 获取锁
Lock lock =new ReentrantLock(); # ReetrantLock 可重入锁
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
- tryLock() 尝试获取锁 返回类型: boolean
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
- lockInterruptibly() 线程中断
三、阻塞对列
队列满额,线程数据无法插入,就会被阻塞等待数据插入,直到队列可以插入为止,当队列为空时,会被阻塞,直到数据插入后才可以唤醒线程移除。
1.常用的方法
方法 | 抛出异常 | 返回值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入 | Add | offer | put | offer |
移除 | remove | poll | take | poll |
检查 | element | peek | 没有 | 没有 |
2.常用阻塞队列
- ArrayBlockingQueue
数组结构组成有界阻塞队列,先进先出原则,初始化必须传大小,take和put时候用的同一把锁。
- LinkedBlockingQueue
链表结构组成的有界阻塞队列,先进先出原则,初始化可以不传大小,put,take锁分离
- PriorityBlockingQueue
支持优先级排序的无界阻塞队列,自然顺序升序排列,更改顺序:类自己实现compareTo()方法,初始化PriorityBlockingQueue指定一个比较器Comparator
- DelayQueue
使用了优先级队列的无界阻塞队列,支持延时获取,队列里的元素要实现Delay接口。DelayQueue非常有用,可以将DelayQueue运用在以下应用场景:
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。还有订单到期,限时支付等.
- SynchronousQueue
不存储元素的阻塞队列,每个put操作必须要等take操作
- LinkedTransferQueue
链表结构组成的无界阻塞队列,Transfer,tryTransfer,生产者put时,当前有消费者take,生产者直接把元素传给消费者
- LinkedBlockingDeque
链表结构组成的双向阻塞队列,可以在队列的两端插入和移除,xxxFirst头部操作,xxxLast尾部操作。工作窃取模式。
3.阻塞队的实现原理
Java 阻塞队列使用 Condititon、Lock,在元素进入使用等待wait(),出队使用唤醒。
4.For/Join框架
把大任务拆分成很多的小任务,汇总每个小任务的结果得到大任务的结果。
Fork/Join使用两个类来完成以上两件事情。
①ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务
中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
·RecursiveAction:用于没有返回结果的任务。
·RecursiveTask:用于有返回结果的任务。
②ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。
Fork/Join有同步和异步两种方式。
5.CountDownLatch
允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
6.CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重
置,CountDownLatch.await一般阻塞主线程,所有的工作线程执行countDown,而CyclicBarrierton通过工作线程调用await从而阻塞工作线程,直到所有工作线程达到屏障。
四 、Java 8高级特性
1、Lambda 表达式
Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
- Style 1 无参,无返回值,Lambda只需要一条语句
Runnable runnable = () -> System.out.println("hello Lambda");
- Style 2 一个参数
Consumer<String> con = (t) -> System.out.println(t);
- Style 3 参数省略括号
Consumer<String> con = t -> System.out.println(t);
- Style 4 2个参数,具有返回值
Comparator<Integer> comparator = (x,y) -> {
System.out.println("相加结果是:"+(x+y));
return Integer.compare(x,y);
};
- Style 5 省略return和大括号
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
- Style 6 类型推断写法
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
2.Stream API
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
- 创建Stream
Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
default Stream stream() : #返回一个顺序流
default Stream parallelStream() : # 返回一个并行流
3.Stream 中间操作
原始集合:
List<Apple> appleList = new ArrayList<>();//存放apple对象集合
Apple apple1 = new Apple(1,"苹果1",new BigDecimal("3.25"),10);
Apple apple12 = new Apple(1,"苹果2",new BigDecimal("1.35"),20);
Apple apple2 = new Apple(2,"香蕉",new BigDecimal("2.89"),30);
Apple apple3 = new Apple(3,"荔枝",new BigDecimal("9.99"),40);
appleList.add(apple1);
appleList.add(apple12);
appleList.add(apple2);
appleList.add(apple3);
- List转Map
/**
* List -> Map
* 需要注意的是:
* toMap 如果集合对象有重复的key,会报错Duplicate key ....
* apple1,apple12的id都为1。
* 可以用 (k1,k2)->k1 来设置,如果有重复的key,则保留key1,舍弃key2
*/
Map<Integer, Apple> appleMap = appleList.stream().collect(Collectors.toMap(Apple::getId, a -> a,(k1,k2)->k1));
{1=Apple{id=1, name='苹果1', money=3.25, num=10}, 2=Apple{id=2, name='香蕉', money=2.89, num=30}, 3=Apple{id=3, name='荔枝', money=9.99, num=40}}
- 分组
//List 以ID分组 Map<Integer,List<Apple>>
Map<Integer, List<Apple>> groupBy = appleList.stream().collect(Collectors.groupingBy(Apple::getId));
System.err.println("groupBy:"+groupBy);
{1=[Apple{id=1, name='苹果1', money=3.25, num=10}, Apple{id=1, name='苹果2', money=1.35, num=20}], 2=[Apple{id=2, name='香蕉', money=2.89, num=30}], 3=[Apple{id=3, name='荔枝', money=9.99, num=40}]}
- 过滤 filter
//过滤出符合条件的数据
List<Apple> filterList = appleList.stream().filter(a -> a.getName().equals("香蕉")).collect(Collectors.toList());
System.err.println("filterList:"+filterList);
[Apple{id=2, name='香蕉', money=2.89, num=30}]
- 求和
# BigDecimal:
BigDecimal totalMoney = appleList.stream().map(Apple::getMoney).reduce(BigDecimal.ZERO, BigDecimal::add);
System.err.println("totalMoney:"+totalMoney); //totalMoney:17.48
# Integer
int sum = appleList.stream().mapToInt(Apple::getNum).sum();
System.err.println("sum:"+sum); //sum:100