第九章:多线程
第一节:线程实现方式
1.1 并发与并行
- 并发:指两个或多个事件在同一个时间段内发生
- 并行:指两个或多个事件在同一时刻发生(同时发生)
1.2 线程与进程
-
**进程:**指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程
-
**线程:**线程是进程的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
1.3 线程概念
**cup:**中央处理器,对数据进行计算,指挥电脑中软件和硬件干活
**cup的分类:**AMD Inter:Inter Core(核心) i7 8846 4核心8线程
**8线程:**同时执行8个任务
点击功能执行,就会开启一条程序到cpu的执行路径,cup就可以通过这个路径执行功能,这个路径有一个名字,叫做线程
线程属于进程,是进程中的一个执行单元,负责程序的执行
多线程的好处:
1、效率高
2、多个线程之间互不影响
1.4 线程调度
分时调度:
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
抢占式调度:
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
1.5 主线程
**主线程:**执行(main)方法的线程
**单线程程序:**java程序中只有一个线程,执行从main方法开始,从上到下依次执行
JVM执行main方法,main方法会进入到栈内存,JVM会找操作系统开辟一条main方法通向cpu的执行路径,cup就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程
1.6 创建线程类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或者其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流,即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
Java中通过继承Thread类来创建并启动多线程的步骤如下:
1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
2、创建Thread子类的实例,即创建了线程对象
3、调用线程对象的start()方法来启动线程
创建多线程程序的第一种方式:
创建Thread类的子类
实现步骤:
-
1、创建一个Thread类的子类
-
2、在Thread类的子类中重写Thread类中的run()方法,设置线程任务(开启线程要做什么)
-
3、创建Thread类的子类对象
-
4、调用Thread类中的start()方法,开启新的线程,执行run()方法
-
public void start()
:使线程开始执行;Java虚拟机调用该线程的run()方法结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新进程,执行其run方法)。多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动
Java程序属于抢占式调度,哪个线程优先级高,哪个线程就优先执行;同一个优先级,随机选择一个执行
-
创建线程方式二:
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可
步骤:
1、创建一个Runnable接口的实现类
2、在实现类中重写Runnadle接口的run方法,设置线程任务
3、创建一个Runnadle接口的实现类对象
4、创建Thread类对象,构造方法中传递Runnalde接口的实现类对象
5、调用Thread类中的start方法,开启新的线程执行run方法
public class Thread_Creat_Second implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
main:
Thread_Creat_Second run=new Thread_Creat_Second();
Thread t=new Thread(run);
t.start();
1.7 多线程原理
呈现随机执行的原因:
多线程内存图解:
当执行start方法时,会开辟一个新的栈空间,并执行此对象的run方法
1.8 Thread类
构造方法:
public Thread()
:分配一个新的线程对象public Thread(String name)
:分配一个指定名字的新的线程对象public Thread(Runnable target)
:分配一个带有指定目标的线程对象public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字
常用方法:
public String getName()
:获取当前线程名称
public class getName_Demo extends Thread{
@Override
public void run() {
System.out.println(getName());
}
}
public String setName()
:设置线程名称
public class Thread_setName extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
main方法中:
Thread_setName s=new Thread_setName();
s.setName("s1");
-
public void start()
:导致此线程开始执行;Java虚拟机调用此线程的run方法 -
public void run()
:此线程要执行的任务在此定义代码 -
public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)-
for (int i = 0; i < 60; i++) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
-
-
public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
获取线程的名称:
-
使用Thread类中的方法getName()
-
可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
-
static Thread currentThread()
:返回对当前正在执行的线程对象的引用 -
public class getName_Demo extends Thread{ @Override public void run() { System.out.println(Thread.currentThread.getName()); } }
-
设置线程的名称:
-
使用Thread类中的方法setName(名字)
void setName(String name)
:改变线程名称,使之与参数name相同
-
创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字。
-
Thread(String name)
:分配新的Thread对象 -
public MyThread(String name){ super(name); } new MyThread("张三").start();
-
1.9 Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享,但是如果实现Runnable接口的话,很容易实现资源共享
总结:
实现Runnable接口比继承Thread类所具有的优势:
1、适合多个相同的程序代码的线程去共享同一个资源
2、可以避免java中的单继承的局限性
- 一个类只能继承一个类,类继承了Thread类就不能继承其他的类,实现了Runnable接口,还可以继承其他的类,实现其他的接口
3、增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
- 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦),实现类中,重写了run方法:用来设置线程任务;创建Thread类对象,调用start方法:用来开启新线程
4、线程池只能放入实现Runnable或Callable类线程,不能直接放入继承Thread的类
**扩充:**在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程
1.10 匿名内部类方式实现线程的创建
**匿名:**没有名字
**内部类:**写在其他内部的类
**匿名内部类作用:**简化代码
把子类继承父类,重写父类的方法,创建子类对象合成一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
**匿名内部类的最终产物:**子类/实现类对象,而这个类没有名字
格式:
new 父类/接口{
重写父类/接口中的方法
};
public class Thread_Creat_Annoy {
//实现父类
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}.start();
//实现接口
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}).start();
}
}
第二节:线程同步机制
2.1 线程安全即产生安全问题的原理
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其它的变量的值也和预期是一样的,就是线程安全的
多线程访问了共享的数据,会产生线程安全问题
产生线程安全问题的原理:
2.2 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复数据与不存在数据的问题,Java中提供了**同步机制(synchronized)**来解决
同步操作的三种方式:
1、同步代码块
2、同步方法
3、锁机制
2.3 同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
格式:
synchronized(同步锁){
需要同步操作的代码
}
**同步锁:**对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
1、锁对象,可以是任意类型
2、多个线程对象,要使用同一把锁
锁对象的作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
public class Block implements Runnable{
//定义多个线程共享的票源
private int ticket=100;
Object obj=new Object();
@Override
public void run() {
//先判断票是否存在
while(true)
synchronized (obj){
if (ticket>0) {
System.out.println(Thread.currentThread().getName() + "------>正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
同步技术的原理:
使用了一个锁对象,这个锁叫做同步锁,也叫对象锁,也叫对象监视器
3个线程一起抢夺cpu的使用权,谁抢到了谁执行run方法买卖票
t0抢到了cup的执行权,执行run方法,遇到synchronized
代码块,这时t0会检查synchronized
代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行;
t1抢到了cup的执行权,执行run方法,遇到synchronized
代码块,这时t0会检查synchronized
代码块是否有锁对象,发现没有,就会进入到阻塞状态,会一直等待t0线程归还锁对象,直到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步
同步保证了只能有一个线程在同步中执行共享数据,保证了安全,程序频繁地判断锁,获取锁,程序的效率会降低
2.4 同步方法
- **同步方法:**使用
synchronized
修饰的方法就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会产生线程安全问题的代码
}
同步方法的锁对象是谁:就是实现类对象
- 对于非static方法,同步锁就是this
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
使用步骤:
1、把访问了共享数据的代码抽取出来,放到一个方法中
2、在方法上添加synchronized
修饰符
原理:
同步方法也会把方法内部的代码锁住,只让一个线程执行。
public class Common_Method implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true)
payTicket();
}
//定义一个同步方法
public synchronized void payTicket(){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "------>正在卖第" + ticket + "张票");
ticket--;
}
}
}
2.5 静态同步方法
在同步方法前面添加static关键字即可
**静态同步方法的锁对象:**不能是this,this是创建对象之后产生的,静态方法优先于对象,静态方法的锁对象是本类的class属性---->class文件对象(反射)
public class Static_Method implements Runnable{
private static int ticket=100;
@Override
public void run() {
while (true)
payTicket();
}
private static synchronized void payTicket(){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "------>正在卖第" + ticket + "张票");
ticket--;
}
}
}
2.6 Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁public void unlock()
:释放同步锁
使用步骤:
1、在成员位置创建一个RenntrantLock
对象
java.util.concurrent.locks.ReentrantLock
implements Lock
接口
2、在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3、在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁
public class Lock_Thread implements Runnable{
private int ticket=100;
Lock l=new ReentrantLock();
@Override
public void run() {
while (true){
l.lock();
if (ticket>0){
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "------>正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
l.unlock();//无论程序是否异常,都会把锁释放
}
}
}
}
}
第三节:线程状态
3.1 线程状态概述
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一致处于执行状态。在线程的生命周期中,共有6中线程状态,在API中java.lang.Thread.State
这个枚举中给出
线程状态 | 导致状态发生条件 |
---|---|
New(新建) | 线程刚被创建,但是并未启动。还没调用start方法 |
Runnable(可运行) | 线程可以在Java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waitting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
3.2 Timed Waiting(计时等待)
Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。
注意:
1、进入Timed_Waiting状态的一种常见情形是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系
2、为了让其他线程有机会执行,可以Thread.sleep()的调用**放线程run()之内。**这样才能保证该线程执行过程中会睡眠
3、sleep与锁无关,线程睡眠到期自动苏醒,并返回Runnable(可运行)状态
sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行
3.3 Blocked(锁阻塞)
Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态
3.4 Waiting(无限等待)
Waiting状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态
等待唤醒案例:线程之间的通信
wait与notify的用法:
public class Waiting_Sale {
public static void main(String[] args) {
Object obj=new Object();
new Thread(){
@Override
public void run() {
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("告知老板包子的种类和数量");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//唤醒之后执行的代码
System.out.println("包子做好了,开吃");
}
}.start();
new Thread(){
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("老板5s之后做好包子告知顾客");
obj.notify();
}
}
}.start();
}
}
进入到TimeWaiting(计时等待)有两种方式:
1、使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2、使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来进入到Runnable/Blocked状态
唤醒的方法:
void notify()
:如果有多个等待线程,随机唤醒一个
void notifyAll()
:唤醒所有等待的线程
3.5 线程间通信
**概念:**多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同
为什么要处理线程间的通信?
多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效地利用资源,而这种手段就是等待唤醒机制
3.6 等待唤醒机制
概述:
等待唤醒机制是多线程间的一种协作机制。在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码之后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程
等待机制使用的3种方法:
wait
:线程不再活动,不再参与调度,进入wait set中,因此不会浪费cpu资源,也不会去竞争锁了,这时的线程状态即是Waiting。他还要等着别的线程执行一个特别的动作,也即是**“通知(noify)”**在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中notify
:选取所通知对象的wait set中的一个线程释放notifyAll
:释放所通知对象的wait set上的全部线程
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不再持有锁,所以它需要再次尝试获取锁(很可能面临其它线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行
调用wait和notify方法需要注意的细节:
1、wait方法与notify方法必须要由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
2、wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类型都是继承了Object类的
3、wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用者两个方法
第四节:线程池
4.1 线程池概述
- 线程池:其实就是一个容纳多个线程的容器(集合),其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源
当程序第一次启动的时候,创建多个线程,保存到一个集合中,当我们想要使用线程的时候,就可以从集合中取出来线程使用。
Thread t=list.remove(0);
返回的是被移除的元素(线程只能被一个任务使用)
Thread t=linked.removeFirst();
当我们使用完毕线程,需要把线程归还给线程池
list.add(t);
linked.addLast(t);
在JDK1.5之后,JDK内置了线程池,我们可以直接使用线程池
合理使用线程池带来的三点好处:
1、降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行
3、提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗太多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
4.2 线程池的使用
java.util.concurrent.Executors
:线程池的工厂类,用来生产线程池
Executors
类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
:创建一个可重用固定线程数的线程池
参数:
int nThreads
:创建线程池中包含的线程数量
返回值:
ExecutorService
接口,返回的是ExecutorService
接口的实现类对象,我们可以使用ExecutorService
接口接收(面向接口编程)
java.util.concurrent.ExecutorService
:线程池接口,用来从线程池中获取线程,调用start方法,执行线程任务。
submit(Runnable task)
:提交一个Runnable任务用于执行
void shutdown
:销毁线程池
使用步骤:
1、使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool
生产一个指定线程数量的线程池
2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
3、调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4、调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
ExecutorService es= Executors.newFixedThreadPool(5);
es.submit(new RunnableImpl());
第十章:Lambda表达式
第一节:函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做
**面向对象的思想:**做一件事情,找一个能解决这个事情的对象,调用对象的方法完成事情
**函数式编程思想:**只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
第二节:冗余的Runnable代码
原始代码:
RunnableImpl run=new RunnableImpl();
Thread th=new Thread(run);
th.start();
匿名内部类用法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"又创建了");
}
}).start();
代码分析:
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类 - 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类 - 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不重写一遍,且不能写错 - 而实际上,似乎只有方法体才是关键所在
第三节:编程思想转换
做什么。而不是怎么做
我们真的希望创建一个匿名内部类对象吗?不,我们只是伪类做这件事情而不得不创建一个对象,我们真正希望做的事情,是将run
方法体内的代码传递给Thread
类知晓
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。如果我们将关注点从“怎么做“回归到”做什么“的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要
在2014年3月Oracle发布的Java8(JDK1.8)中,加入了Lambda表达式的重量级新特性
Lambda表达式创建多线程与匿名内部类创建多线程对比:
public class Lambda_Creat {
public static void main(String[] args) {
//使用匿名内部类方式实现多线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
//使用Lambda表达式实现多线程
new Thread(()->{
System.out.println(Thread.currentThread().getName());
}).start();
}
}
语义分析:
仔细分析该代码中的语义,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
- **无参数:**不需要任何条件即可执行该方案
- **无返回值:**该方案不产生任何结果
- **代码块(方法体):**该方案的具体执行步骤
同样的语义体现在Lanbda
语法中,要更加简单:
()->{System.out.println("多线程任务执行");}
- 前面的一对小括号即
run
方法的参数(无),代表不需要任何条件 - 中间的一个箭头代表将前面的参数传递给后面代码
- 后面的输出语句即业务逻辑代码
第四节:Lambda标准格式
Lambda格式由三部分组成:
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为:
(参数列表)->{一些重写方法的代码}
解释说明格式:
():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数使用逗号分隔
->:传递的意思,把参数传递给方法体{}
{}:重写接口的抽象方法的方法体
第五节:练习:使用Lambda标准格式(无参数返回)
题目:
给定一个厨子Cook
接口,内含唯一的抽象方法makeFood
,且无参数、无返回值。如下:
public interface Cook{
void makeFood();
}
在下面的代码中,请使用Lambda的标准格式调用invokeCook
方法,打印输出“吃饭了”字样:
public class Cook_Main {
public static void main(String[] args) {
//请再此使用Lambda【标准格式】调用invokeCook方法
invokeCook(()->{
System.out.println("吃饭了");
});
}
private static void invokeCook(Cook cook){
cook.makeFood();
}
}
第六节:Lambda的参数和返回值
需求:
1、使用数组存储多个Person对象
2、对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
public class Lambda_Main {
public static void main(String[] args) {
Person[] arr={
new Person("柳岩",18),
new Person("迪丽热巴",19),
new Person("古力娜扎",15)
};
//使用匿名内部类实现重写方法
Arrays.sort(arr, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});
//使用Lambda表达式实现重写方法
Arrays.sort(arr,(Person o1,Person o2)->{
return o1.getAge()-o2.getAge();
});
for (Person person:arr) {
System.out.println(person);
}
}
}
第七节:练习:使用Lambda标准格式(有参有返回)
题目:
给定一个计算器Calculator
接口,内含抽象方法calc
可以将两个int数字相加得到和值:
public interface Calculator{
int calc(int a,int b);
}
public class Main {
public static void main(String[] args) {
//请在此使用Lambda表达式【标准格式】调用invokeCalc方法计算120+130的结果
//使用匿名内部类
invokeCalc(120, 130, new Calculator() {
@Override
public int calc(int a, int b) {
return a+b;
}
});
//使用Lambda表达式
invokeCalc(120,130,(int a,int b)->{
return a+b;
});
}
private static void invokeCalc(int a,int b,Calculator calculator){
int result=calculator.calc(a,b);
System.out.println("结果是:"+result);
}
}
第八节:Lambda省略格式
Lambda表达式:是可推导、可省略的
范式根据上下文推导出来的内容,都可以省略
可以省略的内容:
1、(参数列表):括号中的参数列表的数据类型可以省略
2、(参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
3、(一些代码):如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,;分号)
- 注意:要省略{},return,;必须一起省略
第九节:Lambda使用前提
1、使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
- 无论是JDK内置的
Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda
2、使用Lambda必须具有上下文推断
-
也就是方法的参数或者局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为接口的实例
备注:有且仅有一个抽象方法的接口,称为“函数式接口”