摘要

Java基础加强重温_08:
线程安全(线程安全概念、线程不安全案例)、
线程同步(同步代码块、同步方法、Lock锁,锁对象)、
线程状态(new新建、Runnable可运行、Blocked锁阻塞、Waiting无限等待、Timed Waiting计时等待、Teminated被终止)、
线程状态切换(notify唤醒、sleep计时等待、wait无限等待、sleep不释放锁,wait释放锁)、
线程池(线程池概念、工作原理、Executors创建线程池、线程池提交Runnable任务/Callable任务)
死锁(死锁概念、产生死锁条件)
Lambda表达式(函数式编程、函数式接口、使用Lambda的前提、Lambda标准格式、无参数无返回值/有参数有返回值Lambda、Lambda省略写法规则)
Stream(流式思想、获取流:集合流/数组流/双列集合流、终结方法/非终结方法)
流模型操作方法(forEach遍历、count统计、filter过滤、limit取前n个、skip跳过前n个、map映射(转换)、concat组合(合并流))
收集Stream结果(收集到集合/数组)

一、线程安全

多个线程在同时运行,这些线程同时运行同一段代码。程序每次运行结果和单线程运行结果一样,变量的值也和预期一样,就是线程安全的。

理解:多个线程在操作共享资源时仍然能得到正确的结果则称为线程安全,反则线程不安全。

1、线程不安全演示(卖票案例)

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)

需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

卖票逻辑分析

  1. 定义变量记录总票数:100
  2. 使用死循环保证能够卖完所有票
  3. 判断是否有剩余票数:有则卖一张,没有了则提示用户票卖完了

实现步骤分析

  1. 创建类实现Runnable接口
  2. 重写run方法:编写卖票逻辑的代码
  3. 创建接口实现类对象
  4. 根据实现类对象创建多个Thread对象:模拟多个窗口卖票
  5. 开启多个线程同时卖票

代码实现

模拟票:

public class Ticket implements Runnable {private int ticket = 100;//执行卖票操作@Overridepublic void run() {//每个窗口卖票的操作,窗口永远开启while (true) {if (ticket > 0) { //有票 可以卖//出票操作,使用sleep模拟一下出票时间try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//获取当前线程对象的名字String name = Thread.currentThread().getName();System.out.println(name + "正在卖:" + ticket--); //先显示ticket数量,再减1}}}
}

测试类:

public class Demo {public static void main(String[] args) {//创建线程任务对象Ticket ticket = new Ticket();//创建三个窗口对象Thread t1 = new Thread(ticket, "窗口1");Thread t2 = new Thread(ticket, "窗口2");Thread t3 = new Thread(ticket, "窗口3");//同时卖票t1.start();t2.start();t3.start();}
}

结果分析

结果中出现这种现象:

  1. 卖出相同的票数,比如5这张票被卖了两回。
  2. 卖出不存在的票,比如0票与-1票,是不存在的。

几个窗口(线程)票数不同步,这种问题称为线程不安全。

小结

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作(减少/增加),一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步问题,否则可能出现线程不安全。

2、线程同步

线程同步就是为了解决线程不安全问题。
当我们使用多个线程访同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程不安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

线程同步解决卖票线程不安全问题

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着。
窗口1操作结束,窗口1、窗口2、窗口3才有机会进入代码去执行(窗口1操作完仍有机会继续操作)。
也就是说在某个线程修改共享资源时,其他线程不能修改该资源,只能等待该线程修改完毕同步之后(与其他线程同步到等待执行状态),才能去抢夺CPU资源完成对应的操作。保证了数据的同步性,解决了线程不安全的现象。

理解:线程同步,即所有线程同步到等待执行状态,才一同抢夺CPU资源。有某一个线程不是等待执行状态,其他线程都不能抢夺CPU资源。
共享资源即Runnable/Callabale接口实现类的成员变量

二、实现线程同步的三种方式

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。有三种方式完成同步操作

  1. 同步代码块(synchronized代码块)。
  2. 同步方法(synchronized修饰的方法)。
  3. 锁机制(ReentrantLock类)。

原子操作:所谓原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何context switch (切换到另一个线程)

1、同步代码块

同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示对这个区块的资源实行互斥访问。

同步代码块的原理:能够保证同一时间只有一个线程执行同步代码块的代码。

同步代码块格式

synchronized(同步锁){需要同步操作的代码
}//通俗
synchronized(锁对象){操作共享资源的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  1. 锁对象可以是任意类型。(可以随意创建一个对象作为锁)
  2. 多个线程对象要使用同一把锁。

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

同步代码块解决卖票案例的线程不安全问题

public class Ticket implements Runnable{private int ticket = 100;//创建对象作为锁Object lock = new Object();/** 执行卖票操作*/@Overridepublic void run() {//每个窗口卖票的操作,窗口永远开启while(true){//synchronized代码块(同步代码块)synchronized (lock) {if(ticket>0){//有票 可以卖//出票操作,使用sleep模拟一下出票时间try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//获取当前线程对象的名字String name = Thread.currentThread().getName();System.out.println(name+"正在卖:"+ticket--);}}}}
}

使用了同步代码块后,线程不安全问题解决了。

2、同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

线程使用同步方法,在run()里面调用同步方法。

同步方法的原理:能够保证同一时间只有一个线程执行方法体中的代码。

同步方法格式

public synchronized void method(){可能会产生线程安全问题的代码
}//通俗
修饰符 synchronized 返回值类型 方法名(参数列表){ 操作共享资源的代码
}

同步方法的同步锁

同步方法隐藏了同步锁,不用自己再创建对象作为锁。
对于非static方法,同步锁就是this(调用当前方法的对象)。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)作为同步锁。

同步方法与静态同步方法各自的锁对象是谁:
https://blog.csdn.net/Fighting_mjtao/article/details/83061419

同步方法使用示例

public class Ticket implements Runnable{private int ticket = 100;//执行卖票操作@Overridepublic void run() {//每个窗口卖票的操作,窗口永远开启while(true){sellTicket();}}//锁对象 是 谁调用这个方法 就是谁//隐含 锁对象 就是 thispublic synchronized void sellTicket(){if(ticket>0){//有票 可以卖//出票操作。使用sleep模拟一下出票时间try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//获取当前线程对象的名字String name = Thread.currentThread().getName();System.out.println(name+"正在卖:"+ticket--);}}
}

静态同步方法示例

要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。

public static synchronized int setName(String name){Xxx.name = name;
}

等价于

public static int setName(String name){synchronized(Xxx.class){Xxx.name = name;}
}

java synchronized静态同步方法与非静态同步方法,同步语句块:
https://www.cnblogs.com/csniper/p/5478572.html

3、Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外Lock更强大,更能体现面向对象。

ReentrantLock类(Lock锁的同步锁对象)

Lock锁的同步锁,是用实现Lock接口的ReentrantLock类对象作为锁对象。
Lock接口的实现类主要有ReentrantLock、ReadLock和WriteLock,后两者接触的不多。

ReentrantLock类对象又叫互斥锁、可重入锁

锁分类

线程安全用到锁,MySql也用到锁。

互斥锁:指的是一次最多只能有一个线程持有的锁,如Lock锁
可重入锁:指定是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。synchronized和ReentrantLock都是可重入锁。(理解:可重入锁类似for嵌套,在第一次获得锁之后,还可以可以继续获得这个锁(for循环还可以写for循环),而不产生死锁。重入锁和互斥锁不冲突,等于同一线程不停获得锁。)

Java 中的悲观锁和乐观锁的实现
https://www.cnblogs.com/zhaoyan001/p/8349547.html
可重入锁详解(什么是可重入)
https://blog.csdn.net/w8y56f/article/details/89554060
一文足以了解什么是 Java 中的锁
https://baijiahao.baidu.com/s?id=1653365466720197481&wfr=spider&for=pc
自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁
https://blog.csdn.net/a314773862/article/details/54095819

Lock加锁、释放锁方法

Lock锁也称同步锁,对加锁与释放锁的操作进行方法化了

public void lock()  :加同步锁。public void unlock() :释放同步锁。

Lock接口使用注意事项
获取锁和释放锁的代码必须成对出现

Lock实现线程安全的格式

Lock l = new ReentrantLock(); //创建锁对象
//获得锁
l.lock();try{// 操作共享资源代码
} catch(...){}finally{//释放锁l.unlock();
}

Lock锁解决卖票线程不安全示例

public class Ticket implements Runnable{private int ticket = 100;Lock lock = new ReentrantLock();//执行卖票操作@Overridepublic void run() {//每个窗口卖票的操作,窗口永远开启while(true){lock.lock();if(ticket>0){ //有票可以卖//出票操作,使用sleep模拟一下出票时间               try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//获取当前线程对象的名字String name = Thread.currentThread().getName();System.out.println(name+"正在卖:"+ticket--);}lock.unlock();}}
}

小结

如何选择synchronized关键字和Lock接口

相同点:都是用来实现线程安全
不同点:
synchronized关键字是很早出现的技术,Lock接口是JDK1.5新特性
如果资源竞争不激烈(线程数量少),则Lock和synchronized效率几乎一样,没有区别。
如果资源竞争激烈(线程数量多),则Lock的效率会远远高于synchronized

如何判断代码是否是共享资源

资源就是变量,如果变量是局部变量则不是共享资源,不需要加锁。
如果是成员变量:如果会对该成员变量进行修改,则必须加锁,否则也不需要加锁。

三、线程状态

线程状态就是线程由生到死的完整过程。了解线程状态是技术素养和面试的要求。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中 java.lang.Thread.State这个枚举中给出了了六种线程生命周期的状态:

线程状态运行流程


不需要去研究这几种状态的实现原理,只需知道在做线程操作中存在这样的状态。
新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

四、线程可运行状态与非运行状态之间的转换

可运行状态:Runnable(可运行)
非运行状态:Waiting(无限等待)、Timed Waiting(计时等待)

1、睡眠sleep方法

切换Timed Waiting(计时等待)状态

public static void sleep(long time) 让当前线程进入到睡眠状态,经过指定毫秒后自动醒来继续执行

代码示例

主线程休眠1秒

public class Test{public static void main(String[] args){for(int i = 1;i<=5;i++){Thread.sleep(1000);System.out.println(i)}}
}

这时我们发现主线程执行到sleep方法会休眠1秒后再继续执行。

2、无限等待wait方法、随机唤醒notify方法

wait()、notify()都是Object类的方法。因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。

为什么wait()和notify()属于Object类:https://www.cnblogs.com/lirenzhujiu/p/5927241.html

wait()、notify()必须都由锁对象调用。

无限等待和随机唤醒方法

public void wait() 让当前线程进入到等待状态 此方法必须由锁对象调用public void notify() 唤醒当前锁对象上等待状态的线程 此⽅方法必须锁对象调⽤用.

代码示例

" " 空字符串作为锁对象

public class Demo1_wait { public static void main(String[] args) throws InterruptedException {//步骤1: 子线程开启,进入无限等待状态,没有被唤醒,无法继续运行.new Thread(() -> {try {System.out.println("begin wait ....");synchronized ("") {"".wait();}System.out.println("over");} catch (Exception e) {e.printStackTrace();}}).start();//步骤2: 加入如下代码后, 3秒后执行notify⽅方法, 唤醒wait中线程.Thread.sleep(3000);new Thread(() -> {try {synchronized ("") {System.out.println("唤醒");"".notify();}} catch (Exception e) {e.printStackTrace();}}).start();}
}

面试题:sleep和wait的区别

sleep睡眠时不会释放锁。
wait等待时会释放锁。

3、等待与唤醒,线程交替案例(包子铺卖包子)

案例说明:
包子铺生产一个包子,吃货吃一个包子,包子铺再生产一个包子,吃货再吃一个包子…

实现步骤:

  1. 创建集合:用来存储包子
  2. 创建包子铺线程:负责生产包子
  3. 创建吃货线程:负责吃包子
  4. 同时启动包子铺和吃货线程

代码实现

包子铺线程:生产包子
list作为锁对象

public class BaoZiPuThread implements Runnable {// 创建集合:存储包子private ArrayList<String> list;public BaoZiPuThread(ArrayList<String> list) {this.list = list;}private int index = 0;// 需求:生产包子代码@Overridepublic void run() {// 使用死循环:不停生产包子while (true){//list作为锁对象synchronized (list){// 判断是否有包子if (list.isEmpty()){// 没有包子,则生产一个包子// 生产肉包子String baozi = "肉包子..." + index++; list.add(baozi);System.out.println("包子铺生产了一个包子:" + baozi);  //包子铺生产了一个包子:肉包子...0,(先拿index拼接再+1)// 唤醒吃货吃包子list.notify();}try {// 如果有则等待吃货吃完包子list.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}
}

吃货线程:负责吃包子

public class ChiHuoThread implements Runnable {// 创建集合:存储包子private ArrayList<String> list;public ChiHuoThread(ArrayList<String> list) {this.list = list;}@Overridepublic void run() {while (true){synchronized (list){// 判断是否有包子if (!list.isEmpty()){// 有则吃一个String baozi = list.remove(0);System.out.println("吃货吃了一个包子:" + baozi);// 唤醒包子铺生产包子list.notify();}try {// 没有则等待包子铺生产包子list.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}
}

五、线程池(Executors类)

线程池的思想

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。Java中可以通过线程池来达到这样的效果

线程池概念

线程池:其实就是一个容纳多个线程的容器。其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池的工作原理

  1. 程序启动时先创建一定数量的线程存储在容器中
  2. 当有任务需要线程执行时,从容器中获得线程执行任务
  3. 执行完毕任务之后将线程放回池中等待复用

合理利用线程池的三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

六、使用线程池

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

1、Executors类(线程池)

如何创建线程池
通过工具类Executors的静态方法创建,该方法声明如下:

public static ExecutorService newFixedThreadPool(int nThreads) 返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)即,创建线程池对象并指定线程数量

创建线程池代码示例

// Executors类静态工厂创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2); //包含2个线程对象

线程池对象常用方法

Future<t> submit(Runnable task); 提交Runnable任务
Future<V> submit(Callable task); 提交Callable任务void shutdown(); 销毁线程池(等待线程池中所有任务执行完毕才销毁)
void shutdownNow(); 立即销毁线程池(还没有开始执行的任务就不会执行了)

只有实现Runnable接口 或者 实现Callable接口的实现类对象,调用线程池的线程执行。(因为这个实现类对象不是线程,仅仅是线程要执行的target(目标))
继承Thread类的子类对象,不能调用线程池线程执行。(因为这个子类对象就是一个线程)

Future接口(线程池submit方法返回值类型)

用来封装Callable任务的返回值,Runnable任务没有返回值不用定义变量接受。

从封装的返回值中获得具体数值,调用Future接口中的get方法获得,方法如下:

V get():获得call方法执行的返回值

2、线程任务Callable接口

Callable接口中的call方法,等价于Runnable接口的run方法:用来封装线程任务代码

V call() throws Exception

Callable接口的好处

任务执行完毕之后可以有返回值给调用者
方法可以声明异常

3、线程池案例1(提交Runnable任务)

提交Runnable任务的步骤

  1. 创建线程池对象并指定线程数量
  2. 创建类实现Runnable接口
  3. 重写run方法:将任务代码编写在该方法中
  4. 创建实现类对象
  5. 调用线程池对象的submit方法:传递实现类对象
  6. 销毁线程池(在实际开发中,一般不会销毁)

代码实现

Runnable实现类:

public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("我要一个教练");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("教练来了: " + Thread.currentThread().getName());System.out.println("教我游泳,教完后,教练回到了游泳池");}
}

线程池测试类:

public class ThreadPoolDemo {public static void main(String[] args) {// Executors类静态工厂创建线程池对象ExecutorService service = Executors.newFixedThreadPool(2); //包含2个线程对象// 创建Runnable实例对象MyRunnable r = new MyRunnable();//以前⾃己创建线程对象的方式//Thread t = new Thread(r);//t.start(); ---> 调⽤用MyRunnable中的run()//从线程池中获取线程对象,然后调用MyRunnable中的run()service.submit(r);// 再获取个线程对象,调⽤用MyRunnable中的run()service.submit(r);service.submit(r);// 注意:submit方法调⽤结束后,程序并不终止,是因为线程池控制了线程的关闭。// 自动将使用完的线程归还到了线程池中// 关闭线程池,一般不关闭//service.shutdown();}
}

4、线程池案例2(提交Callable任务)

提交Callable任务的步骤

  1. 创建线程池对象并指定线程数量
  2. 创建类实现Callable接口
  3. 重写call方法:将任务代码编写在该方法中
  4. 创建实现类对象
  5. 调用线程池对象的submit方法:传递实现类对象
  6. 销毁线程池(在实际开发中,一般不会销毁)

线程池方法

<T> Future<T> submit(Callable<T> task)
获取线程池中的某一个线程对象,并执行task。Future: 表示计算的结果。V get()
获取计算完成的结果。

代码示例

public class ThreadPoolDemo2 {public static void main(String[] args) throws Exception {// 创建线程池对象ExecutorService service = Executors.newFixedThreadPool(2); //包含2个线程对象// 创建Runnable实例对象Callable<Double> c = new Callable<Double>() {@Overridepublic Double call() throws Exception {return Math.random();}};// 从线程池中获取线程对象,然后调用Callable中的call()Future<Double> f1 = service.submit(c);// Futur调用get() 获取运算结果System.out.println(f1.get());Future<Double> f2 = service.submit(c);System.out.println(f2.get());Future<Double> f3 = service.submit(c);System.out.println(f3.get());}
}

5、线程池案例3(利用Callable任务求和)

需求

使用线程池方式执行任务,返回1-n的和

分析

因为需要返回求和结果,所以使用Callable方式的任务

实现步骤:

  1. 创建类实现Callable接口指定泛型变量为Integer
  2. 定义成员变量:接收外界传递的数字
  3. 提供带参数构造方法:接收外界传递的数字
  4. 重写call方法:计算1到n的和
  5. 创建线程池对象并指定线程数量
  6. 创建接口实现类对象并传递数字
  7. 调用线程池对象的submit方法:传递实现类对象
  8. 获得执行结果

代码实现

定义Callable接口实现类SumCallable

public class SumCallable implements Callable<Integer> {private int n;public SumCallable(int n) {this.n = n;}@Overridepublic Integer call() throws Exception {// 求1-n的和?int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return sum;}
}

测试类代码

public class Demo04 {public static void main(String[] args) throws ExecutionException,InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(3);SumCallable sc = new SumCallable(100);//接收返回值Future<Integer> fu = pool.submit(sc);//获得返回值内容Integer integer = fu.get();System.out.println("结果: " + integer);SumCallable sc2 = new SumCallable(200);Future<Integer> fu2 = pool.submit(sc2);Integer integer2 = fu2.get();System.out.println("结果: " + integer2);pool.shutdown();}
}

小结:如何选择Runnable和Callable任务

如果任务执行完毕需要返回值,则只能选择Callable任务,否则可以随便选择。

七、死锁

1、什么是死锁?

在多线程程序中,使用了多把锁,造成线程之间相互等待,程序不往下走的现象。
通俗讲就是:多个线程在执行任务过程中因争夺资源而造成的一种相互等待的现象。

2、产生死锁的条件

1、有多把锁
2、有多个线程
3、有同步代码块嵌套

死锁代码示例

//测试类
public class Demo05 {public static void main(String[] args) {MyRunnable mr = new MyRunnable();new Thread(mr).start(); //线程1new Thread(mr).start(); //线程2}
}//线程
class MyRunnable implements Runnable {Object objA = new Object();Object objB = new Object();/*嵌套1 objA嵌套1 objB嵌套2 objB嵌套1 objA*/@Overridepublic void run() {synchronized (objA) {System.out.println("嵌套1 objA");synchronized (objB) {// t2, objA, 拿不到B锁,等待System.out.println("嵌套1 objB");}}synchronized (objB) {System.out.println("嵌套2 objB");synchronized (objA) {// t1, objB, 拿不不到A锁,等待System.out.println("嵌套2 objA");}}}
}

死锁产生流程分析:

1、线程1拿到了锁对象objA,执行了sout语句。在这个过程中,线程2也进来。
2、第一个同步代码块的锁对象objA被线程1拿了,线程2无法执行第一个同步代码块。所以线程2去执行第二个同步代码,线程2拿到锁对象objB。
3、线程1执行到同步代码块嵌套,需要锁对象objB,而锁对象objB被线程2拿了未释放(线程2拿到objB先于线程1执行到同步代码块嵌套),线程1处于等待状态。
4、线程2也执行到同步代码块嵌套,需要锁对象objA,而锁对象objA被线程1拿了未释放,线程2处于等待状态。

同一个线程可以获得多个不同的锁对象,只有一个线程时,有多个锁对象、同步代码块嵌套都不会产生死锁。

3、避免死锁(不同时满足产生三个死锁的条件即可)

我们应该尽量避免死锁:一个线程/一个锁对象/同步代码块不嵌套

八、Lambda表达式

Lambda表达式编程又称为函数式编程,是JDK1.8新特性

1、函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以么形式做。

做什么,而不是怎么做。
举例,我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将 run 方法体内的代码传递给 Thread 类知晓。传递一段代码这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

2、函数式接口

只包含一个抽象方法的接口,称为函数式接口。

函数式接口是 Java8 引入的一个新特性,是一种特殊的接口:SAM类型的接口(Single Abstract Method),但本质上还是接口。相比较于其他接口,函数式接口有且只能有一个抽象方法。只要接口中出现多个抽象方法,那么就不能称之为函数式接口,运行的时候就会报错。为此 Java8 提供了一个新的注解@FunctionalInterface,如果接口被这个注解标注,就说明该接口是函数式接口,如果有多于一个的抽象方法,在编译的时候就会报错。但是这个注解不是必需的,只要接口符合函数式接口的定义,那么这个接口就是函数式接口。
函数式接口中有且只能有一个抽象方法。但是在 Java8 之后接口中也是可以定义方法的:默认方法和静态方法,这两种方法的定义并不会影响函数式接口的定义,可以随意使用。(浅谈函数式接口:https://www.jianshu.com/p/faa6d074614b)

3、JDK提供的函数式接口


java8新特性——四大内置核心函数式接口
https://www.cnblogs.com/wuyx/p/9000312.html

面试不知道JDK1.8函数式接口@FunctionalInterface被鄙视了
https://baijiahao.baidu.com/s?id=1662271606145211656&wfr=spider&for=pc

4、函数式编程相对于面向对象的优点

1、面向对象: 代码冗余
2、函数式编程: 只专注做什么,而不是怎么做

5、函数式编程–Lambda表达式简化匿名内部类语法

作用:使用Lambda简化匿名内部类的语法。
核心思想:只专注做什么,而不是怎么做

代码示例

当需要启动一个线程去完成任务时,通常会通过 java.lang.Runnable 接口来定义任务内容,并使用 java.lang.Thread 类来启动该线程。

传统写法

⾸先创建一个 Runnable 接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

public class Demo01ThreadNameless {public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println("多线程任务执⾏行行!");}}).start();}
}

代码分析:
对于 Runnable 的匿名内部类用法,可以分析出几点内容:
1、Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心;
2、为了指定 run 的方法体,不得不需要 Runnable 接口的实现类;
3、为了省去定义一个 RunnableImpl 实现类的麻烦,不得不使用匿名内部类;
4、必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
5、而实际上,似乎只有方法体才是关键所在。

Lambda表达式优化写法

借助Java 8的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class Demo02LambdaRunnable {public static void main(String[] args) {new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程}
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

九、深入了解Lambda

Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数
一个箭头
一段代码

Lambda标准格式

(参数类型 参数名称) -> { 代码语句 } //通俗
(参数列表) -> { 方法体 }//有返回值的格式
(参数列表)-> {return 返回值;
}// 小括号:就是方法的参数列表
// -> 新语法,固定写法,代表动作指向
// 大括号:就是方法的方法体

格式说明:
1、小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
2、-> 是新引入的语法格式,代表指向动作。
3、大括号内的语法与传统方法体要求基本一致。

理解:记住,箭头左边是参数,右边是代码

Java8之十分钟学会《Lambda表达式》-基础
https://www.jianshu.com/p/f258e9464a29

1、匿名内部类与lambda对比(无参数无返回值的Lambda格式)

匿名内部类

new Thread(new Runnable() {@Overridepublic void run() {System.out.println("多线程任务执行!");}
}).start();

Runnable 接口只有一个 run 方法,即制定了一种做事情的方案(其实就是一个方法)。
无参数:不需要任何条件即可执行该方案。
无返回值:该方案不产生任何结果。
代码块(方法体):该方案的具体执行步骤。

Lambda表达式

同样的语义体现在 Lambda 语法中,要更加简单:

new Thread(new Runnable() {@Overridepublic void run() -> System.out.println("多线程任务执行!")
}).start();

1、前面的一对小括号即 run 方法的参数(无),代表不需要任何条件;
2、中间的⼀箭头代表将前面的参数传递给后面的代码;
3、后面的输出语句即业务逻辑代码。

Lambda表达式案例

定义一个学生类,成员变量有:姓名,年龄,成绩。

public class Student {private String name;private int age;private int score;public Student(String name, int age, int score) {this.name = name;this.age = age;this.score = score;}public Student() {}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public int getScore() {return score;}public void setScore(int score) {this.score = score;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +", score=" + score +'}';}
}

测试类

public class Demo08 {public static void main(String[] args){// 1. 创建集合对象用来多个学生对象ArrayList<Student> list = new ArrayList<>();// 2. 创建学生对象添加到集合中list.add(new Student("张三",20,90));list.add(new Student("李四",21,80));list.add(new Student("王五",18,70));list.add(new Student("赵六",30,99));// 对集合学生排序:按照年龄升序排// 使用匿名内部类创建比较器对象/*Collections.sort(list,new Comparator<Student>(){@Overridepublic int compare(Student o1, Student o2) {return o1.getAge() - o2.getAge();}});*/// 使用lambda表达式简化匿名内部类代码Collections.sort(list,(Student o1, Student o2)->{return o2.getAge() - o1.getAge();});// 输出学生for (Student stu : list) {System.out.println(stu);}}
}

2、Comparator接口与Lambda对比(有参数和有返回值的Lambda格式)

Comparator接口

当需要对一个对象数组进行排序时, Arrays.sort 方法需要一个 Comparator 接口实例来指定排序的规则。 java.util.Comparator<T> 接口中的抽象方法为:

public abstract int compare(T o1, T o2);

代码示例

假设有一个 Person 类,含有 String name 和 int age 两个成员变量:

public class Person {private String name;private int age;// 省略构造器、toString方法与Getter Setter
}

对 Person[] 数组进行排序

public class Demo06Comparator {public static void main(String[] args) {// 本来年龄乱序的对象数组Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴", 18),new Person("马尔扎哈", 20) };// 匿名内部类Comparator<Person> comp = new Comparator<Person>() {@Overridepublic int compare(Person o1, Person o2) {return o1.getAge() - o2.getAge();}};Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例for (Person person : array) {System.out.println(person);}}
}

代码分析
1、为了排序, Arrays.sort 方法需要排序规则,即 Comparator 接口的实例,抽象方法 compare 是关键;
2、为了指定 compare 的方法体,不得不需要 Comparator 接口的实现类;
3、为了省去定义一个 ComparatorImpl 实现类的麻烦,不得不使用匿匿名内部类;
4、必须覆盖重写抽象 compare ⽅方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
实际上,只有参数和方法体才是关键。

Lambda写法

public class Demo07ComparatorLambda {public static void main(String[] args) {Person[] array = {new Person("古⼒娜扎", 19),new Person("迪丽热巴", 18),new Person("马尔扎哈", 20) };//Lambda写法Arrays.sort(array, (Person a, Person b) -> {return a.getAge() - b.getAge();});for (Person person : array) {System.out.println(person);}}
}

3、Lambda标准格式的基础上,可继续使用省略写法

Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());

Lambda省略写法规则:

  1. 小括号内的参数类型:随时可以省略
  2. 参数列表小括号:只有一个参数时可以省略
  3. 方法体大括号:方法体只有一条语句时可以省略,如果省略了大括号则return关键字和分号必须省略。
//Lambda标准格式
Arrays.sort(array, (Person a, Person b) -> {return a.getAge() - b.getAge();
});//参数类型可省
Arrays.sort(array, (a, b) -> {return a.getAge() - b.getAge();
});//单参数,参数小括号可省。这里双参数不省
Arrays.sort(array, (a, b) -> {return a.getAge() - b.getAge();
});//方法体只有一个语句,可省方法体大括号,return关键字,方法体语句分号
Arrays.sort(array, (a, b) -> a.getAge() - b.getAge()
);

4、使用Lambda的前提条件

Lambda的语法非常简洁,没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。(即函数式接口)
  2. 使用Lambda必须具有上下文推断。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

理解:
1、实现接口时才能考虑使用Lambda简化方法,接口只有一个方法时才能真正使用Lambda简化
2、必须是重写接口的方法才能使用Lambda(独有的方法不能使用Lambda,如接口的默认方法)

通俗:必须是接口且接口中有且只有一个抽象方法(函数式接口)

十、Stream流(操作集合元素)

在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

Stream流是JDK1.8的新特性
作用:用来对集合元素进行加工处理
比喻:将Stream流比喻为流水线:通过一道一道工序(一个一个方法)对产品(元素)进行加工处理

1、传统集合遍历方式

几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

public class Demo01ForEach {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("张⽆无忌");list.add("周芷若");list.add("赵敏敏");list.add("张强");list.add("张三丰");for (String name : list) {System.out.println(name);}}
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。分析代码可知:
1、for循环的语法就是“怎么做”
2、for循环的循环体才是“做什么”
用循环是为了遍历,遍历是指对每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

对上面案例增加条件:

  1. 将集合A根据条件一过滤为子集B;
  2. 根据条件二过滤为子集C。

在Java 8之前的代码做法可能为:

public class Demo02NormalFilter {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("张无忌");list.add("周芷若");list.add("赵敏敏");list.add("张强");list.add("张三丰");//根据条件一过滤List<String> zhangList = new ArrayList<>();for (String name : list) {if (name.startsWith("张")) {zhangList.add(name);}}//根据条件二过滤List<String> shortList = new ArrayList<>();for (String name : zhangList) {if (name.length() == 3) {shortList.add(name);}}//再遍历输出for (String name : shortList) {System.out.println(name);}}
}

分析代码
这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后对结果进行打印输出。

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一⽅面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始

传统集合遍历方式的弊端

1、集合的主要作用是用来存储元素,不适合直接用来操作元素
2、直接遍历操作集合元素的带来的弊端是代码冗余

2、使用Lambda的衍生物Stream改良传统遍历方式

借助Java 8的Stream API改造集合遍历案例

public class Demo03StreamFilter {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("张无忌");list.add("周芷若");list.add("赵敏");list.add("张强");list.add("张三丰");list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length() == 3).forEach(System.out::println);//双冒号用法}
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

Java8 双冒号运算符特性(简化Lambada)

Java8中的[方法引用]“双冒号”
https://blog.csdn.net/lsmsrc/article/details/41747159

3、深入了解Stream

流式思想概述

注意:请暂时忘记对传统IO流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线”。

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的 filter 、map 、skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
Stream 就如同一个迭代器(Iterator,单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

JDK1.8-Stream()使用详解
https://blog.csdn.net/young4dream/article/details/76794659

十一、获取流的方式

java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)获取一个流非常简单,有以下几种常用的方式:
1、所有的 Collection 集合都可以通过 stream 默认方法stream()获取流;
2、Stream 接口的静态方法 of 可以获取数组对应的流。

// Stream接口的默认方法
default Stream<E> stream​()// Stream接口的静态方法,参数是一个可变参数,可以传递一个数组
static <T> Stream<T> of​(T... values)

流的分类

1、获取单列集合流:集合对象.stream();
2、获取数组流:Stream.of(数组)
3、双列集合流:转换成key集合流/value集合/键值对集合流

1、根据Collection获取流(单列集合流)

在java1.8中,Collection新增了一个default方法stream(),它可以将集合转换成流,Collection的所有实现类可通过这个获取对应的流。

import java.util.*;
import java.util.stream.Stream;public class Demo04GetStream {public static void main(String[] args) {//创建ArrayList集合,并获取ArrayList集合的流List<String> list = new ArrayList<>();Stream<String> stream1 = list.stream();//创建HashSet集合,并获取List集合的流Set<String> set = new HashSet<>();Stream<String> stream2 = set.stream();//创建Vector集合,并获取Vector集合的流Vector<String> vector = new Vector<>();Stream<String> stream3 = vector.stream();}
}

2、根据Map获取流(双列集合流)

java.util.Map 接口不不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;public class Demo05GetStream {public static void main(String[] args) {Map<String, String> map = new HashMap<>();//获取key集合的流Stream<String> keyStream = map.keySet().stream();//获取value集合的流Stream<String> valueStream = map.values().stream();//获取key-value对象的流Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();}
}

3、根据数组获取流(数组流)

如果使用的不是集合或映射而是数组,由于数组对象不能添加默认方法,所以 Stream 接口中专门提供了静态方法 of 用来获取数组的流,使用很简单:

import java.util.stream.Stream;public class Demo06GetStream {public static void main(String[] args) {String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };Stream<String> stream = Stream.of(array);}
}

of 方法的参数其实是一个可变参数,所以支持数组。

十二、流模型操作的常用方法

流模型的操作方法很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

1、终结方法

常用的终结方法包括 count 和 forEach 方法。

这类方法的返回值类型不再是 Stream 接口自身类型,因此不支持类似 StringBuilder 那样的链式调用。

2、非终结方法

这类方法的返回值类型仍然是 Stream 接口自身类型,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

3、函数拼接方法(非终结方法)

非终结方法又称为函数拼接方法,返回值仍然为 Stream 接口的为函数拼接方法,它们支持链式调用;而返回值不再为 Stream 接口的为终结方法,不支持链式调用。常用的流模型操作方法如下:

除了这些常用的流模型操作方法,更多操作方法请查阅参考API文档。

十三、终结方法

1、forEach方法(逐一处理)

作用:对流中元素遍历
虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的。(理解:无序遍历流元素)

格式

void forEach(Consumer<? super T> action);
//将流中的每个元素传递给指定的消费者对象,Consumer:消费者

Consumer函数接口(forEach方法参数)

是一个函数式接口:只有一个抽象方法,抽象方法声明如下:

void accept(T t)
//用来对流元素进行消费

流遍历集合元素 示例

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。

public class Demo12StreamForEach {public static void main(String[] args) {Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");stream.forEach(System.out::println);}
}

方法引用System.out::println 就是一个 Consumer 函数式接口的示例。

2、count方法(统计个数)

作用:获得当前流元素个数
如集合 Collection 的 size 方法一样,流提供count 方法来统计流元素个数:

格式

long count();

流统计集合元素 示例

该方法返回一个long值代表流元素个数(不像集合那样是int值)。用法:

public class Demo09StreamCount {public static void main(String[] args) {Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");//过滤Stream<String> result = original.filter(s -> s.startsWith("张"));//统计System.out.println(result.count()); // 2}
}

十四、非终结方法

1、filter方法(过滤)

作用:对流中元素进行过滤,将满足条件的元素存储都新流中。
条件:通过Predicate封装过滤条件

格式

Stream<T> filter(Predicate<? super T> predicate);

filter方法参数–Predicate函数接口(筛选条件)

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

代码示例

public class Demo07StreamFilter {public static void main(String[] args) {Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");//函数式接口参数:Lambda参数Stream<String> result = original.filter(s -> s.startsWith("张"));}
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

2、limit方法(截取前几个元素)

作用:获取前n个元素到新流中
limit 方法可以对流进行截取,只取用前n个流元素。方法声明:

格式

Stream<T> limit(long maxSize);
//将当前流中的前n个元素存储到另一个流中。maxSize必须大于等于0

代码示例

参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:

import java.util.stream.Stream;public class Demo10StreamLimit {public static void main(String[] args) {Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");Stream<String> result = original.limit(2);System.out.println(result.count()); // 2}
}

3、skip方法(跳过前几个元素)

作用:将当前流中的第n个之后的元素存储到新流中(跳过前n个)
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取的新流。方法声明:

格式

Stream<T> skip(long n);
//跳过前n个元素

代码示例

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

import java.util.stream.Stream;
public class Demo11StreamSkip {public static void main(String[] args) {Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");Stream<String> result = original.skip(2);System.out.println(result.count()); // 1}
}

4、map方法(映射)

如果需要将流中的元素映射到另一个流中,可以使用 map 方法。
作用:将当前流的元素从T类型转换为R类型存储到新流中。
方法声明:

格式

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

map方法参数 – Function函数接口

Function是一个函数式接口,可以将当前流中的T类型数据转换为另一种R类型的流。抽象方法如下:

R apply(T t)将R类型转换为T类型

代码示例

import java.util.stream.Stream;
public class Demo08StreamMap {public static void main(String[] args) {Stream<String> original = Stream.of("10", "12", "18");//Integer类调用parseInt,字符串转换成IntegerStream<Integer> result = original.map(Integer::parseInt);}
}

这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer类对象)。

5、concat方法(合并流)

如果希望将两个流合并成一个流,可以使用 Stream 接口的静态方法 concat 。
作用:合并流,产生新的流

格式

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
//合并a流元素在前,b流元素在后

流concat方法 与 字符串concat方法的区别:

Stream的concat方法是一个静态方法。
String的concat方法是一个实例方法:将指定字符串连接到此字符串的结尾。 如果参数字符串的长度为 0,则返回此 String 对象。

代码示例

Stream的concat方法用法:

import java.util.stream.Stream;
public class Demo12StreamConcat {public static void main(String[] args) {Stream<String> streamA = Stream.of("张无忌");Stream<String> streamB = Stream.of("张翠山");Stream<String> result = Stream.concat(streamA, streamB);}
}

小结:流的注意事项

流只能使用1次
1、一旦调用了终结方法,流就不能继续使用了
2、当通过一个流产生新的流时,旧流就不能继续使用了,只能使用新流

十五、Stream综合案例

需求:

现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 根据姓名创建 Person 对象;
  7. 打印整个队伍的Person对象信息。

代码示例

两个队伍(集合)的代码

public class DemoArrayListNames {public static void main(String[] args) {List<String> one = new ArrayList<>();one.add("迪丽热巴");one.add("宋远桥");one.add("苏星河");one.add("老子");one.add("庄子");one.add("孙子");one.add("洪七公");List<String> two = new ArrayList<>();two.add("古力娜扎");two.add("张无忌");two.add("张三丰");two.add("赵丽颖");two.add("张二狗");two.add("张天爱");two.add("张三");}
}

Person 类的代码

public class Person {private String name;public Person() {}public Person(String name) {this.name = name;}@Overridepublic String toString() {return "Person{name='" + name + "'}";}public String getName() {return name;}public void setName(String name) {this.name = name;}
}

传统方式实现需求

使用for循环

public class DemoArrayListNames {public static void main(String[] args) {List<String> one = new ArrayList<>();List<String> two = new ArrayList<>();// 第一个队伍只要名字为3个字的成员姓名;List<String> oneA = new ArrayList<>();for (String name : one) {if (name.length() == 3) {oneA.add(name);}}// 第一个队伍筛选之后只要前3个人;List<String> oneB = new ArrayList<>();for (int i = 0; i < 3; i++) {oneB.add(oneA.get(i));}// 第二个队伍只要姓张的成员姓名;List<String> twoA = new ArrayList<>();for (String name : two) {if (name.startsWith("张")) {twoA.add(name);}}// 第二个队伍筛选之后不要前2个人;List<String> twoB = new ArrayList<>();for (int i = 2; i < twoA.size(); i++) {twoB.add(twoA.get(i));}// 将两个队伍合并为一个队伍;List<String> totalNames = new ArrayList<>();totalNames.addAll(oneB);totalNames.addAll(twoB);// 根据姓名创建Person对象;List<Person> totalPersonList = new ArrayList<>();for (String name : totalNames) {totalPersonList.add(new Person(name));}// 打印整个队伍的Person对象信息。for (Person person : totalPersonList) {System.out.println(person);}}
}

运行结果为:

Person{name='宋远桥'}
Person{name='苏星河'}
Person{name='洪七公'}
Person{name='张二狗'}
Person{name='张天爱'}
Person{name='张三'}

Stream方式实现需求

将for循环转换为等效的Stream流式处理,代码为:

public class DemoStreamNames {public static void main(String[] args) {List<String> one = new ArrayList<>();List<String> two = new ArrayList<>();// 第一个队伍只要名字为3个字的成员姓名;// 第一个队伍筛选之后只要前3个⼈人;Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);// 第二个队伍只要姓张的成员姓名;// 第二个队伍筛选之后不要前2个人;Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);// 将两个队伍合并为一个队伍;// 根据姓名创建Person对象;// 打印整个队伍的Person对象信息。Stream.concat(streamOne,streamTwo).map(Person::new).forEach(System.out::println);}
}

运行效果:

Person{name='宋远桥'}
Person{name='苏星河'}
Person{name='洪七公'}
Person{name='张二狗'}
Person{name='张天爱'}
Person{name='张三'}

十六、收集Stream结果

对流操作完成之后,如果需要将其结果进行收集,获取对应的集合、数组等。

理解:上面获得流可以理解为集合/数组转换为流,收集Stream结果其实就是:流转集合/数组

1、收集流到集合方法

Stream流提供 collect 方法收集结果,其参数需要一个 java.util.stream.Collector<T,A, R> 接口对象来指定收集到哪种集合中。

 <R, A> R collect(Collector<? super T, A, R> collector);

java.util.stream.Collectors类(集合工具类)提供一些方法,可以作为 Collector接口的实例:

public static <T> Collector<T, ?, List<T>> toList() :转换为 List 集合。
public static <T> Collector<T, ?, Set<T>> toSet() :转换为 Set 集合。

收集到List集合:stream.collect(Collectors.toList());
收集到Set集合:stream.collect(Collectors.toSet());

代码示例:

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;public class Demo15StreamCollect {public static void main(String[] args) {Stream<String> stream = Stream.of("10", "20", "30", "40", "50");List<String> list = stream.collect(Collectors.toList());Set<String> set = stream.collect(Collectors.toSet());}
}

Java8(3)Stream类的collect方法详解
https://www.jianshu.com/p/ccbb42ad9551

2、收集流结果到数组方法

Stream提供 toArray 方法来将结果放到一个数组中,由于泛型擦除的原因,返回值类型是Object[]

Object[] toArray();

代码示例:

import java.util.stream.Stream;
public class Demo16StreamArray {public static void main(String[] args) {Stream<String> stream = Stream.of("10", "20", "30", "40", "50");Object[] objArray = stream.toArray();}
}

使用匿名内部类实现

import java.util.stream.Stream;
public class Demo16StreamArray {public static void main(String[] args) {Stream<String> stream = Stream.of("10", "20", "30", "40", "50");//匿名内部类形式String[] strs = stream.toArray(new IntFunction<String[]>() {@Overridepublic String[] apply(int value) {return new String[value];}}
}

使用Lambda简化实现

import java.util.stream.Stream;
public class Demo16StreamArray {public static void main(String[] args) {Stream<String> stream = Stream.of("10", "20", "30", "40", "50");//Lambda简化实现String[] strs = stream.toArray(value -> new String[value]);}
}

使用方法引用简化实现

格式:T[] stream.toArray(T[]::new);

import java.util.stream.Stream;
public class Demo16StreamArray {public static void main(String[] args) {Stream<String> stream = Stream.of("10", "20", "30", "40", "50");//使用方法引用简化实现String[] strs = stream.toArray(String[]::new);}
}

Java基础加强重温_08:线程不安全、线程同步、线程状态、线程状态切换、线程池(Executors类、newFixedThreadPool)、死锁、Lambda表达式、Stream相关推荐

  1. Java基础加强重温_05:Iterator迭代器、增强for循环、集合综合案例-斗地主、数据结构(栈、队列、数组、链表、红黑树)、List接口、Set接口

    摘要: Java基础加强重温_05: Iterator迭代器(指针跟踪元素). 增强for循环(格式.底层). 集合综合案例-斗地主(代码规范抽取代码,集合元素打乱). 数据结构[栈(先进后出,子弹夹 ...

  2. Java基础加强重温_13:XML(可拓展标记语言)、XML语法、XML约束、XML解析(Dom4j,JAXP)、Dom4j基本使用、Xpath表达式(XML路径语言)、XML解析综合案例

    摘要 Java基础加强重温_13: XML(可拓展标记语言.作用:小型数据库.框架配置文件.不同平台数据交换). XML语法(文档.标签/元素.属性.注释.转义字符.CDTA区) XML约束(DTD约 ...

  3. 【JAVA黑马程序员笔记】四 P314到P384(特殊流、多线程编程、网络编程模块、lambda表达式、接口组成更新、方法引用、函数式接口)

    P314-315 字节/符打印流 PrintStream ps = new PrintStream("test.txt");//使用字节输出流的方法ps.write(97);// ...

  4. Java 8 Lambda 表达式Stream操作

    在jdk1.8新的stream包中针对集合的操作也提供了并行操作流和串行操作流.并行流就是把内容切割成多个数据块,并且使用多个线程分别处理每个数据块的内容. 优点: Stream API可以极大提高J ...

  5. Java基础巩固(二)异常,多线程,线程池,IO流,Properties集合,IO工具类,字符流,对象流,Stream,Lambda表达式

    一.异常,多线程 学习目标 : 异常的概述 异常的分类 异常的处理方式 自定义异常 多线程入门 1 异常的概述 1.1 什么是异常? 异常就是程序出现了不正常情况 , 程序在执行过程中 , 数据导致程 ...

  6. java 基础知识总结

    Java基础知识总结 写代码: 1,明确需求.我要做什么? 2,分析思路.我要怎么做?1,2,3. 3,确定步骤.每一个思路部分用到哪些语句,方法,和对象. 4,代码实现.用具体的java语言代码把思 ...

  7. JAVA基础+集合+多线程+JVM

    1. Java 基础 1.1. 面向对象和面向过程的区别 面向过程性能比面向对象高. 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候 等一般采用面向过程开发.但是 ...

  8. 整理的java基础知识点笔记

    java基础知识点 (涉及到图片的资源因为在电脑本地,挨个挨个找太浪费时间就不找了) 基础点 **字节:**每逢8位是一个字节,这是数据存储的最小单位. 计算机中的数据转换: ​ 1 Byte = 8 ...

  9. Java 基础入门,小白提升路线图

    1000+最新Java面试题 获取学习路线资料啦 Java的基础知识就像我们所住的房子的地基,如果地基不稳,上面所盖的楼房再宏伟,也是没人敢去入住的,同理Java的基础不牢固,以后也很难成为真正意义上 ...

最新文章

  1. vs2017开发Node.js控制台程序
  2. 【跟网上的大多数不一样】rstudio plot不显示图片了
  3. python data analysis | python数据预处理(基于scikit-learn模块)
  4. plsq卸载 删除注册表、_win10操作系统下oracle11g客户端/服务端的下载安装配置卸载总结...
  5. CSS3实现小黄人动画
  6. PHP-date(),time()函数的应用
  7. 判断无向图是否有回路有四种方法
  8. 20行Python代码检测人脸是否佩戴口罩
  9. dispatch js实现_通信:派发与广播,on与emit,自行实现dispatch和broadcast方法
  10. usb转rj45_超薄本也能有线上网,只需一个USB转网口小工具
  11. 理解WidowManager
  12. 网吧服务器ip地址修改,详解修改BXP服务器IP地址的方法
  13. java 定时任务注解
  14. 聚类——模糊c均值聚类
  15. 数据中心白皮书 2022东数西算下数据中心高性能计算的六大趋势八大技术
  16. 创业之路 - 人脉关系 VS 人际关系
  17. 【传感器大赏】80cm红外距离传感器
  18. 中年妇女,偶很想念你
  19. IntelliJ IDEA自动生成注释的author
  20. Gentoo 完整的USE参数清单中文详解(转)

热门文章

  1. 二维码识别自动对焦放大,弱光补偿(仿微信、支付宝二维码识别)android
  2. 纸鸢|工业物联网通讯协议Modbus协议详解
  3. 华为 人工智能 笔经+面经 分享
  4. python命令和python3命令_命令行找不到python3命令
  5. jq添加数组_jquery数组循环添加问题
  6. 全国领先——液力悬浮仿生型人工心脏上市后在同济医院成功植入
  7. python tello 教育版 编队飞行_tello edu 官方python接口的改进
  8. 2021/07/11 老男孩带你21周搞定Go语言 (二)
  9. 单倍型分析网络图绘制软件 network下载
  10. exposureFusion