Java从入门到高级(第三天)


通过前两天Java的学习

学到了不少进步了不少

今天我们继续我们的Java高级之路


目录

1. 多线程

1.1 并发与并行

1.2 线程与进程

1.3 创建线程类

1.4 Thread类

1.5 Runnable接口

1.6 Thread和Runnable的区别

1.7 匿名内部类方式实现线程的创建

2. 线程安全问题

2.1 线程同步

2.2 同步代码块

2.3 同步方法

2.4 Lock锁

3. 线程状态

3.1 Timed Waiting(计时等待)

3.2 BLOCKED(阻塞态)

3.3 Waiting(等待态)

3.4 Timed Waiting(计时等待态) 与 Waiting(等待态) 联系

4. 线程间通信

4.1 等待唤醒机制

4.2 等待唤醒中的方法

5. 线程池

5.1 线程池的使用

6. Lambda表达式

6.1 冗余的Runnable代码与Lambda写法

6.2 匿名内部类的好处与弊端

6.3 Lambda标准格式

6.4 Lambda的使用前提

6.5 Lambda的参数、返回值及省略写法


1. 多线程

多线程在Java语言中有十分重要的作用,在《操作系统》这门课中也十分重要,关乎系统或者进程的运行时间和效率。


1.1 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。

  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单CPU系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。(目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率)


1.2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

注意:一个程序运行后至少有一个进程,一个进程中可以包含多个线程,但一个进程中至少包含一个线程。

(操作系统知识)

线程状态:

  • 新建(NEW):新创建了一个线程对象。

  • 可运行(RUNNABLE):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权 。

  • 运行态(RUNNING):可运行状态(runnable)的线程获得了CPU时间片,执行程序代码。

  • 阻塞态(BLOCKED):阻塞状态是指线程因为某种原因放弃了CPU使用权,暂时停止运行。

  • 死亡态(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程调度:

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。


1.3 创建线程类

Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务,因此把 run() 方法称为线程执行体;

  2. 创建 Thread 子类的实例,即创建了线程对象;

  3. 调用线程对象的 start() 方法来启动该线程。


1.4 Thread类

构造方法:

  •   public Thread() :分配一个新的线程对象。

  •   public Thread(String name) :分配一个指定名字的新的线程对象。

  •   public Thread(Runnable target) :分配一个带有指定目标新的线程对象。

  •   public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  •   public String getName() :获取当前线程名称。

  •   public void start() :导致此线程开始执行;JVM调用此线程的 run() 方法。

  •   public void run() :此线程要执行的任务在此处定义代码。

  •   public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  •   public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

代码实现:继承 Thread 类来创建并启动多线程

// 测试类实现
public class Demo {
	public static void main(String[] args) {
		// 创建自定义线程对象
		MyThread mt = new MyThread("新线程!");
		
                // 开启新线程
		mt.start();
		
                // 在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}
// 自定义线程类
public class MyThread extends Thread {
	// 定义指定线程名称的构造方法
	public MyThread(String name) {
		// 调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	
	// 重写run方法
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

1.5 Runnable接口

翻阅API后得知创建线程的方式总共有两种,一种是继承 Thread 类方式,一种是实现 Runnable 接口方式,采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写 run() 方法即可。步骤如下:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体;

  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;

  3. 调用线程对象的 start() 方法来启动线程。

代码实现:实现 Runnable 接口方式来创建并启动多线程

// 定义Runnable接口的实现类
public class MyRunnable implements Runnable{
    // 重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}
// 测试类
public class Demo {
    public static void main(String[] args) {
        // 创建自定义类对象
        MyRunnable mr = new MyRunnable();

        // 创建线程对象
        Thread t = new Thread(mr, "玩家1");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("玩家2" + i);
        }
    }
}

通过实现 Runnable 接口,使得该类有了多线程类的特征。 run() 方法是多线程程序的一个执行目标。所有的多线程代码都在 run() 方法里面。 Thread 类实际上也是实现了Runnable接口的类。在启动的多线程的时候,需要先通过Thread类的构造方法:

 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start() 方法来运行多线程代码。

实际上所有的多线程代码都是通过运行 Thread start() 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的API来控制线程的,熟悉 Thread 类的API是进行多线程编程的基础。


1.6 Thread和Runnable的区别

如果一个类继承 Thread ,则不适合资源共享。但是如果实现了 Runnable 接口的话,则很容易的实现资源共享。

实现 Runnable 接口比继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现 Runnable Callable 类线程,不能直接放入继承 Thread 的类。


1.7 匿名内部类方式实现线程的创建

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

代码实现:使用匿名内部类的方式实现 Runnable 接口

public class NoNameInnerClassThread {
    public static void main(String[] args) {
        // 第一种方式:调用线程接口重写run方法
        Runnable r = new Runnable(){
            @Override
            public void run(){
                // 代码段
            }
        };
        new Thread(r).start();

        
        // 第二种方式:重写Thread类中run方法
        new Thread(new Runnable(){
            @Override
            public void run(){
                // 代码段
            }
        }).start();
}

2. 线程安全问题

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

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。


2.1 线程同步

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

同步操作有三种方式:

  1. 同步代码块。

  2. 同步方法。

  3. 锁机制。


2.2 同步代码块

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

格式:

synchronized(同步锁){
          // 需要同步操作的代码
}

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

  1. 锁对象可以是任意类型。

  2. 多个线程对象要使用同一把锁。

代码实现:使用同步代码块实现电影院三个窗口同时卖票问题

public class Ticket implements Runnable{
    private int ticket = 100;
    Object lock = new Object();
    
    // 执行卖票操作
    @Override
    public void run() {
    // 每个窗口卖票的操作,窗口都开启
    while(true){
        synchronized (lock){
            if(ticket > 0){
                //出票操作
                try {
                    // 模拟出票时间
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            
            // 获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
            }
        }
    }
}

2.3 同步方法

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

格式:

public synchronized void method(){
        // 可能会产生线程安全问题的代码
}

同步锁:对于非static方法,同步锁就是this,对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

代码实现:使用同步方法实现电影院三个窗口同时卖票问题

public class Ticket implements Runnable{
    private int ticket = 100;

    // 重写run()方法
    @Override
    public void run() {
        // 每个窗口卖票窗口都开启
        while(true){
            sellTicket();
        }
    }

    // 同步方法
    public synchronized void sellTicket(){
        if(ticket>0){
            //出票操作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
    }
}

2.4 Lock锁

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

 Lock 锁也称同步锁,有如下方法:

  •   public void lock() :加同步锁。

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

代码实现:使用 Lock 锁实现电影院三个窗口同时卖票问题

public class Ticket implements Runnable{
    private int ticket = 100;
    Lock lock = new ReentrantLock();

    // 重写run方法
    @Override
    public void run() {
        // 每个窗口卖票
        while(true){
            // 加同步锁
            lock.lock();
            if(ticket>0){
                //出票操作
                try{
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
            }
            // 释放同步锁
            lock.unlock();
        }
    }
}

3. 线程状态

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

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(阻塞态) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(等待态) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待态) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleepObject.wait
Teminated(终止态) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

3.1 Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在 run() 方法中添加了 sleep 语句,这样就强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。其实当我们调用了 sleep 方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。

 sleep 方法使用的几点注意事项:

  1. 进入Timed Waiting状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。

  2. 为了让其他线程有机会执行,可以将 Thread.sleep() 的调用放线程 run() 之内,这样才能保证该线程执行过程中会睡眠。

  3.  sleep 与锁无关,线程睡眠到期自动苏醒,并返回到Runnable可运行状态。

Timed Waiting 线程状态图


3.2 BLOCKED(阻塞态)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked阻塞态。这是由Runnable状态进入Blocked状态。除此Waiting状态以及Time Waiting状态也会在某种情况下进入阻塞状态。

Blocked 线程状态图


3.3 Waiting (等待态)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

其实Waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。当多个线程协作时,比如A,B线程,如果A线程在Runnable可运行状态中调用了 wait() 方法那么A线程就进入了Waiting等待态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了 notify() 方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable可运行状态;如果没有获取锁对象,那么就进入到Blocked阻塞态。

Waiting 线程状态图


3.4 Timed Waiting(计时等待态) 与 Waiting(等待态) 联系

我们在翻阅API的时候会发现Timed Waiting(计时等待态) 与 Waiting(等待态) 联系还是很紧密的,比如Waiting等待态中 wait() 方法是空参的,而Timed Waiting计时等待态中 wait() 方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。


4. 线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即——等待唤醒机制。


4.1 等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,线程间也会有协作机制。就是在一个线程进行了规定操作后,就进入等待状态 wait() ,等待其他线程执行完他们的指定代码过后 再将其唤醒 notify() ;在有多个线程进行等待时, 如果需要,可以使用 notifyAll() 来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。


4.2 等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1.  wait() :线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列中。

  2.  notify() :则选取所通知对象的 wait set 中的一个线程释放。

  3.  notifyAll() :则释放所通知对象的 wait set 上的全部线程。

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;

  • 否则,从 wait set 出来,又进入 entry set ,线程就从 WAITING 状态又变成 BLOCKED 状态。

调用wait和notify方法需要注意的细节:

  1.  wait() 方法与 notify() 方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过 notify() 唤醒使用同一个锁对象调用的 wait() 方法后的线程。

  2.  wait() 方法与 notify() 方法是属于 Object 类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的。

  3.  wait() 方法与 notify() 方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。


5. 线程池

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

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累死。


5.1 线程池的使用

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用 Executors 工程类来创建线程池对象。

 Executors 类中有个创建线程池的方法如下:

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

使用线程池对象的方法如下:

  •  public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。

  3. 提交Runnable接口子类对象。

  4. 关闭线程池(一般不做)。

代码实现:创建使用线程池(模拟餐厅点单)

// Runnable实现类
public class MyRunnable implements Runnable {
    // 重写run()方法
    @Override
    public 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) {
        // 创建线程池对象,包含2个线程对象
        ExecutorService service = Executors.newFixedThreadPool(2);
        
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);

        // 关闭线程池
        //service.shutdown();
    }
}

6. Lambda表达式

面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。

函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。


6.1 冗余的Runnable代码与Lambda写法

代码实现:冗余的Runnable代码与Lambda写法对比

// 冗余的Runnable代码
public class DemoRunnable {
    public static void main(String[] args) {
        // 匿名内部类
	Runnable task = new Runnable() {
	    // 覆盖重写run()方法
            @Override
	    public void run() { 
	        System.out.println("多线程任务执行!");
	    }
        };
        // 启动线程
        new Thread(task).start(); 
    }
}
// Lambda的更优写法
public class DemoLambdaRunnable {
    public static void main(String[] args) {
        // 启动线程
        new Thread(() -> System.out.println("多线程任务执行!")).start(); 
    }
}

6.2 匿名内部类的好处与弊端

一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!

 Runnable 接口只有一个 run 方法的定义:

  •  public abstract void run()

即制定了一种做事情的方案(其实就是一个函数):

  • 无参数:不需要任何条件即可执行该方案。

  • 无返回值:该方案不产生任何结果。

  • 代码块(方法体):该方案的具体执行步骤。

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

() -> System.out.println("多线程任务执行!")
  • 前面的一对小括号即 run 方法的参数(无),代表不需要任何条件;

  • 中间的一个箭头代表将前面的参数传递给后面的代码;

  • 后面的输出语句即业务逻辑代码。


6.3 Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数

  • 一个箭头

  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。

  • ->是新引入的语法格式,代表指向动作。

  • 大括号内的语法与传统方法体要求基本一致。


6.4 Lambda的使用前提

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

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的 Runnable Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。

  2. 使用Lambda必须具有上下文推断。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。


6.5 Lambda的参数、返回值及省略写法

下面举例演示 java.util.Comparator<T> 接口的使用场景代码,其中的抽象方法定义为:

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

当需要对一个对象数组进行排序时, Arrays.sort 方法需要一个 Comparator 接口实例来指定排序的规则。

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

代码实现:对数组中的 Person 对象使用 Arrays sort 方法通过年龄进行升序排序(附Lambda省略写法)

// 定义一个Person类
public class Person { 
    private String name;
    private int age;
    
    // 省略构造器、toString方法与Getter Setter等
}
// Lambda写法
public class DemoComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
          	new Person("玩家1", 19),
          	new Person("玩家2", 18),
          	new Person("玩家3", 20) };

        // Lambda写法
        Arrays.sort(array, (Person a, Person b) -> {
          	return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}
// Lambda省略写法
public class DemoComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
          	new Person("玩家1", 19),
          	new Person("玩家2", 18),
          	new Person("玩家3", 20) };

        // Lambda省略写法
        Arrays.sort(array, (a, b) ->a.getAge() - b.getAge());

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;

  2. 如果小括号内有且仅有一个参,则小括号可以省略;

  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。


 

欢迎关注博主,欢迎互粉,一起学习!

感谢您的阅读,不足之处欢迎指正!

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章