一、 异常
1.1 异常的概念
异常:是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在java等面向对象的编程语言中,异常本身就是一个类,产生异常就是创建异常对象并抛出一个异常对象。
java处理异常的方式是中断处理
异常并不是语法错误,语法错了,编译时不会通过的,不通过就不会产生class(字节码)文件,根本运行不了。 |
---|
1.2异常的体系
异常的根类是java.lang.Throwable
,其下边有类两个子类继承:java.lang.Error
与java.lang.Exception
。java.lang.Exception
为平常所说的异常。
Throwable体系:
- Error:严重错误error,无法通过处理的错误。
- Exception:表示编译期异常,异常产生后可以通过代码的方式纠正。
1.3 异常产生过程解析
二、异常处理
java异常处理的五个关键字:try、catch、finally、throw、throws
2.1 抛出异常throw
throw是用在方法内,用来抛出异常对象,将异常对象传递到调用者处,并结束当前方法的执行。
使用格式:
throw new 异常类名(参数)
2.2 Objects非空判断
查看指定引用对象是否为null
public static<T>T requireNonNull(T obj)
2.3 声明异常throws
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。
**声明异常:**将问题表识出来,报告给调用者。
格式:
修饰符 返回值类型 方法名(参数)throws 异常类名1,异常类名2...{...}
public class ThrowsDemo{
public static void main(String[] args) throws FileNotFoundException{
read.("a.txt");
}
public static void read(String path) throws FileNotFoundException{
if(!path.equals("a.txt")){
throw new FileNotFoundException("文件不存在!");
}
}
}
2.4 捕获异常try catch
如果异常出现的话,会立刻终止程序,所以我们得处理异常:
- 不处理,声明抛出。使用throws关键字
- 在方法中使用try catch是处理异常
try catch的方式就是捕获异常。
捕获异常:java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理
格式:
try{
//可能产生异常的代码
} catch{
//异常处理的逻辑
}
补充:
- try中可能会抛出多个异常 ,就需要多个catch来处理异常。
- try中出现异常,就会执行catch异常处理逻辑,并会对catch之后的代码正常运行;try中不出现异常,就不会执行catch,并继续执行之后的代码。
2.5 finally
finally不能单独使用,且无论是否出现异常都会执行
与try catch 搭配使用。
Throwable补充
-
public String getMessage()
:返回此throwable的详细消息字符串(字符串可能为null) -
public String toString()
:返回此可抛出的简短描述。结果是:
- 这个对象的类的name
- “:”(一个冒号和一个空格)
- 调用这个对象的getLocalizedMessage()方法的结果
如果getLocalizedMessage返回null ,那么只返回类名。
-
public void printStackTrace()
:JVM打印异常对象,默认此方法,打印异常信息最全面的。
三、多线程
3.1 并发与并行
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
- 并发:指两个或者多个事件同一时间段内发生。(交替执行)
- 并行:指两个或者多个事件同一时刻发生。(同时执行)
3.2 线程与进程
-
进程:是程序的一次执行,是操作系统资源分配和调度的最小单位(基本单位)。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
-
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
两者的关系:进程是指程序执行时的一个实例,线程是进程的一个实体。
3.2.1 线程与进程区别
进程:
- 拥有独立的堆栈空间和数据段,系统开销大
- 由于进程之间是独立的特点 使得进程的安全性比较高 有独立的地址空间 一个进程崩溃 不影响其他进程
- 进程的通信机制相对复杂 譬如管道、信号、消息队列、套接字等
线程:
- 线程拥有独立的堆栈空间 但是共享数据段,它们彼此之间使用相同的地址空间,比进程开销小
- 线程是一个进程中不同的执行路径 ,线程的死亡就等于整个进程的死亡。
- 通信相对方便
3.2.2 线程的生命周期
- 新建状态(new):当线程对象创建后,即进入了新建状态,如:Thread t = new Thread();
- 就绪状态(Runnable):当调用线程对象的start()方法(例:t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经准备好,随时等待CPU调度执行,并不是线程立即执行。
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入到阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行了wait()方法,使线程进入到等待阻塞状态;
- 同步阻塞:线程在获取synchronized同步锁失败(锁被其他线程锁占用),就会进入到同步阻塞状态;
- 其他阻塞:通过调用线程的sleep()或者join(),又或者发出I/O请求时,线程就会进入到阻塞状态。当sleep()状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态;
- 死亡状态(Dead):线程执行完或者因异常退出了run()方法,该线程结束周期。
3.2.3 线程调度
-
分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
-
抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用抢占式调度。
3.3 创建线程类
java使用
java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例
java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表线程所需要完成的任务。因此run()方法被称为线程执行体。
- 创建Thread子类的实例,即创建线程对象。
- 调用线程对象的start()方法来启动线程。
3.3.1 多线程的原理
3.4 Thread类
线程是程序中执行的线程。Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守护程序。 当在某个线程中运行的代码创建一个新的Thread
对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护线程。
当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为main
的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:
- 已经调用了
Runtime
类的exit
方法,并且安全管理器已经允许进行退出操作。 - 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到
run
方法还是抛出超出run
方法的run
。
构造方法:
常用方法:
public String getName();
– 获取当前线程名称public String setName(String name);
– 更改当前线程名称为参数namepublic void start();
– 导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run();
– 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。public static void sleep(long millis);
– 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。public static Thread currentThread();
– 返回对当前正在执行的线程对象的引用。public void setPriority(int newPriority);
--更改此线程的优先级,最小为1,最大为10。
3.5 创建线程方式二
采用java.lang.Runnable
常见的一种方式
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
案例代码如下:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("小区"+i);
try {
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
3.6 Thread类和接口Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同程序代码的线程去共享一个资源。
- 可以避免java中单继承的局限性。
- 增强程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runnable或者Callable类线程,不能直接放入继承Thread的类。
扩充:在java中,每次程序运行至少启动2个线程。一个主(main)线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每个JVM就是操作系统中启动了一个进程。
3.7匿名内部类方式实现线程的创建
public class InnerClassThread {
/*
匿名内部类方式实现线程的创建
匿名内部类作用:把子类继承父类,重写父类方法,创建子类对象一步完成,接口相同。
格式:
new 父类/接口(){
重写方法;
}
*/
public static void main(String[] args) {
//创建线程的第一种方式:
//MyThread extend Thread
//MyThread mt = new MyThread();
//mt.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}.start();
//第二种方式:Runnable接口
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"程序员"+i);
}
}
}).start();
}
}
3.8 线程池
/**
* Java通过Executors提供四种线程池,分别为:
* newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
* newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
* newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
* newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
*/
public class TestPool {
public static void main(String[] args) {
//创建服务,创建线程池
//newFixedThreadPool线程池的大小
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThreade());
service.execute(new MyThreade());
service.execute(new MyThreade());
//结束服务
service.shutdown();
}
}
class MyThreade implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
四、线程安全
4.1 线程安全案例
案例:电影院卖票
public class RunnableImp implements Runnable{
//定义票数
private Integer ticket = 100;
/**
* 卖票操作
*/
@Override
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"票");
ticket--;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunnableImp runnableImp = new RunnableImp();
new Thread(runnableImp,"窗口1").start();
new Thread(runnableImp,"窗口2").start();
new Thread(runnableImp,"窗口3").start();
//可以使用匿名内部类来实现
}
}
窗口1正在卖第100票
窗口3正在卖第100票
窗口2正在卖第100票
窗口2正在卖第97票
窗口1正在卖第97票
窗口3正在卖第97票
窗口2正在卖第94票
窗口3正在卖第94票
窗口1正在卖第94票
窗口1正在卖第91票
窗口3正在卖第91票
发现程序出现了两个问题:
- 相同的票数,比如91这张票被卖了两回。
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
4.2 线程同步
当使用多个线程去访问同一个资源时,且多线程中对资源有写的操作,就会容易出现线程安全问题。
要解决上述电影案例多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制。
4.3 同步代码块
-
同步代码块:
synchronized
关键字可以用与方法中的某个区块中,表示只对这个区域的资源实现互斥访问。格式:
synchronized(同步锁){ 需要同步操作的代码 }
同步锁:
对象的同步锁只是一个概念,可以想像在对象上标记了一个锁。
-
锁对象可以是任意类型。
-
多个线程对象要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁谁进代码块,其他线程只能在外面等着释放锁。
-
public class RunnableImp implements Runnable{
//定义票数
private Integer 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--;
}
}
}
}
}
注意:程序会频繁去判断锁,释放锁,就会导致程序效率低。
4.4 同步方法
- 同步方法:使用
synchronized
修饰的方法,叫做同步方法,保证线程A执行该方法时,其他线程只能在方法外等待锁的释放。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
/**
* 使用步骤:
* 1. 把访问了共享数据的代码抽取出来,放到一个方法中。
* 2. 在方法上添加synchronized修饰符
*/
public class RunnableImp implements Runnable{
//定义票数
private Integer ticket = 100;
/**
* 卖票操作
*/
@Override
public void run() {
while(true){
payTicket();
}
}
public synchronized void payTicket(){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"票");
ticket--;
}
}
}
4.5 Lock锁
java.util.concurrent.locks.lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有。
Lock锁也称同步锁,加锁与释放锁方法化,如下:
public void lock()
;加同步锁。public void unlock
;释放同步锁。
public class RunnableImp implements Runnable{
//定义票数
private Integer ticket = 100;
Lock l = new ReentrantLock();
/**
* 卖票操作
*/
@Override
public void run() {
while(true){
l.lock(); //获取锁
try {
Thread.sleep(20);
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"票");
ticket--;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
l.unlock(); //释放锁
}
}
}
}
五、线程状态
在API中java.lang.Thread.State
这个枚举类中给出了六种线程状态:
线程状态 | 导致状态发生条 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还未调用start()方法。 |
Runnable(可运行) | 可运行线程的线程状态。 可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(例如处理器)的其他资源。 |
Blocked(锁阻塞) | 一个线程的线程状态阻塞等待监视器锁定。 处于阻塞状态的线程正在等待监视器锁定进入同步块/方法,或者在调用Object.wait 后重新输入同步的块/方法。 |
Waiting(无限等待) | 等待线程的线程状态由于调用以下方法之一,线程处于等待状态: 1.Object.wait 没有超时。 2.Thread.join 没有超时。3. LockSupport.park 。 等待状态的线程正在等待另一个线程执行特定的动作。 例如,已经在对象上调用Object.wait() 线程正在等待另一个线程调用该对象上Object.notify() Object.notifyAll() 或 调用Thread.join()的 线程正在等待指定的线程终止。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
六、等待与唤醒机制
6.1 线程间通信
概念:多个线程在处理同一个资源,但线程任务是不相同的。
6.1.1 为什么要处理线程间的通信
多个线程并发执行时,默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务时,并且有规律的执行,那么就需要多线程协调通信。
6.2 等待唤醒机制
6.2.1 什么是等待唤醒机制
在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
6.2.2 等待唤醒的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程进入到休眠,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程即是WAITING。它还要等着别的线程执行 通知(notify),在这个对象上等待的线程从wait set释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
6.3 生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
包子案例分析:
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。
案例代码:
public class Main {
public static void main(String[] args) {
Bun bun = new Bun();
new Producer(bun).start();
new Consumer(bun).start();
}
}
public class Consumer extends Thread{
private Bun bun;
public Consumer(Bun bun){
this.bun = bun;
}
@Override
public void run() {
while (true){
synchronized (bun){
if (bun.flag == false){
//没有包子
try {
bun.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒线程
System.out.println("消费者正在购买"+bun.BunSkin+bun.Stuffing+"包子");
System.out.println("=================================================");
//修改状态
bun.flag = false;
bun.notify();
System.out.println("要生产包子");
}
}
}
}
public class Producer extends Thread{
//创建包子变量
private Bun bun;
//构成方法为包子赋值
public Producer(Bun bun){
this.bun = bun;
}
@Override
public void run() {
int count = 0;
while (true){
synchronized(bun){
//有包子就等待
if (bun.flag == true){
try {
bun.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后执行,包子铺生产包子,交替生产两个包子
if (count%2 == 0){
//生产肉包
bun.BunSkin ="脆皮";
bun.Stuffing="海鲜馅";
} else {
bun.BunSkin ="薄皮";
bun.Stuffing="韭菜馅";
}
count++;
System.out.println("包子正在生产中"+bun.BunSkin+bun.Stuffing+"包子");
System.out.println("=================================================");
//生产包子需要等待5秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子铺生产好包子,修改包子状态并唤醒消费者线程
bun.flag = true;
bun.notify();
System.out.println("包子铺包子生产好,"+bun.BunSkin+bun.Stuffing+"包子消费者购买");
}
}
}
}
七、线程池
**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池的好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- ==提高响应速度。==当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- ==提高线程的可管理性。==可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
7.1线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
官方建议使用Executors工程类来创建线程池对象。Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)。参数int nThreads
是代表创建线程池中包含的线程数量。
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
-
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行线程任务。 -
void shutdown()
:启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。 如果已经关闭,调用没有额外的作用。Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
public class Demo {
public static void main(String[] args) {
//1.使用线程池的工厂类Executors里边提供静态方法newnewFixedThreadPool(int nThreads)生产指定的线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
//3.调用ExecutorService 中的submit方法,传递线程任务(实现类),开启线程,执行run方法。
//线程池一直开启,使用完了线程,会自动把线程归还到线程池,线程还可以继续使用。
service.submit(new RunnableImp());
//最后可以调用shutdown方法销毁线程池。(不建议去销毁)
}
}
八、Lambda表达式
public class Demo {
public static void main(String[] args) {
//匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程1");
}
}).start();
//lambda表达式
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程2");
}).start();
//优化lambda表达式
new Thread(() ->System.out.println(Thread.currentThread().getName()+"线程2")).start();
}
}
8.1 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+“线程1”);
}
}).start();
//lambda表达式
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程2");
}).start();
//优化lambda表达式
new Thread(() ->System.out.println(Thread.currentThread().getName()+"线程2")).start();
}
}
## 8.1 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
1. 使用Lambda必须具有接口,且要求**接口中有且仅有一个抽象方法**。
无论是JDK内置的`Runnable`、`Comparator`接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
2. 使用Lambda必须具有**上下文推断**。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
> 备注:有且仅有一个抽象方法的接口,称为“**函数式接口**”。