线程池

day07【线程池、Lambda表达式】

主要内容

  • 线程池 池化思想
  • Lambda表达式 重点
  • 常用的函数式接口重点

教学目标

-[ ] 能够描述Java中线程池运行原理
-[ ] 能够理解函数式编程相对于面向对象的优点
-[ ] 能够掌握Lambda表达式的标准格式
-[ ] 能够使用Lambda标准格式使用Runnable与Comparator接口
-[ ] 能够掌握Lambda表达式的省略格式与规则
-[ ] 能够使用Lambda省略格式使用Runnable与Comparator接口
-[ ] 能够通过Lambda的标准格式使用自定义的接口(有且仅有一个抽象方法)
-[ ] 能够通过Lambda的省略格式使用自定义的接口(有且仅有一个抽象方法)
-[ ] 能够明确Lambda的两项使用前提

第一章 线程池

1.1 线程池思想概述

当需要使用线程的时候,可以直接创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有办法使得线程可以复用?执行完一个任务,线程不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

1.2 线程池概念

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

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

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

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

1.3 线程池的使用

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

java.util.concurrent.ExecutorService。

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

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

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

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

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

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

// 创建线程任务。
class RunnableImpl implements Runnable {
    @Override // 线程任务
    public void run() {
        System.out.println( Thread.currentThread().getName() + "执行线程任务..." );
    }
}

线程池测试类:

/*
    演示线程池的使用:
    线程池技术是JDK1.5提供的。
    如果需要使用线程池,就需要创建一个线程池,线程池的创建是通过一个工厂类完成的:
    java.util.concurrent.Executors :用来创建线程池的工厂类。提供了静态的创建线程的方法
        static ExecutorService newFixedThreadPool(int nThreads)
           int nThreads : 线程池中线程的个数。
           ExecutorService:线程池的接口对象,使用此接口接收Executors工厂创建的线程池。
    java.util.concurrent.ExecutorService: 线程池接口。
        接口内部提供了各种操作线程池的方法。
        1、使用线程池中的线程执行线程任务的方法:
            submit( Runnable task ):启动并执行传递的线程任务
        2、当线程池使用结束,不在需要时销毁线程的方法
            shutdown() : 销毁线程池,不建议使用。
    线程池的使用步骤:
        1、通过Executors线程池工厂类中的newfixedThreadPool方法创建一个线程池
        2、使用线程池接口ExecutorService中的submit方法启动线程并执行线程任务
           需要先准备线程任务,Runnable接口
        3、如果线程池在执行结束之后不需要在继续使用,调用shutdown方法销毁(不建议)
 */
public class ExecutorsDemo {
    public static void main(String[] args) {
        //1、通过Executors线程池工厂类中的newfixedThreadPool方法创建一个线程池
        ExecutorService es = Executors.newFixedThreadPool(2);

        /*
            2、使用线程池接口ExecutorService中的submit方法启动线程并执行线程任务
            线程池会一直开启,当线程执行完任务之后,会归还到线程池中继续使用
       */
        es.submit( new RunnableImpl() ); // pool-1-thread-1执行线程任务...
        es.submit( new RunnableImpl() ); // pool-1-thread-2执行线程任务...
        es.submit( new RunnableImpl() ); // pool-1-thread-2执行线程任务...

        // 3、如果线程池在执行结束之后不需要在继续使用,调用shutdown方法销毁(不建议)
        //es.shutdown();

        /*
            创建第二个线程池,执行线程任务。
            每个线程池有默认的名称:pool-x,从1开始。
         */
        ExecutorService es1 = Executors.newFixedThreadPool(2);
        es1.submit( new RunnableImpl() );
        es1.submit( new RunnableImpl() );
        es1.submit( new RunnableImpl() );
    }
}

第二章 Lambda表达式

2.1 函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。如:y=2x+1;给不同的x值,就能够获取的相对应的y值。

相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。

面向对象的思想:有一个需求,找一个能解决这个需求的对象,然后调用对象的方法,完成需求。

函数式编程思想:只要能完成需求,谁去做,怎么做都不重要。重视的是结果,不重视过程。

2.2 冗余的Runnable代码

传统写法

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

/*
    演示传统方式完成线程的创建和启动:
 */
// 创建Runnable接口的实现类
class RunnableImpl implements Runnable {
    @Override // 线程任务 重写run方法
    public void run() {
        System.out.println("使用实现类..完成多线程执行...");
    }
}
// 测试类
public class ThreadRunnableDemo {
    public static void main(String[] args) {

        // 创建线程任务对象
        RunnableImpl r = new RunnableImpl();
        // 创建线程对象 , 并传递线程任务
        Thread t = new Thread(r);
        // 启动线程任务
        t.start();

        /*
            上述多线程多线程的创建和移动较为麻烦,改为使用匿名内部类的方式完成
        */
        new Thread(new Runnable() {
            @Override // 线程任务 重写run方法
            public void run() {
                System.out.println("使用匿名内部类..完成多线程执行...");
            }
        }).start();
    }
}

本着“一切皆对象”的思想,这种做法是无可厚非的:

首先创建一个Runnable接口的实现类来重写run方法指定任务内容,然后创建线程任务对象,再将其交给一个线程对象来启动。

上述的过程较为繁琐,使用匿名内部类可以省略创键实现类的过程。首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

相比较而言,使用匿名内部类这种操作方式更为简单。

代码分析

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在。前面做个所有动作,都是为了写出线程任务,交给线程对象去执行。

2.3 编程思想转换

做什么,而不是怎么做

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

生活举例

当我们需要从北京到上海时,可以选择高铁、汽车、骑行或是徒步。我们的真正目的是到达上海,而如何才能到达上海的形式并不重要,所以我们一直在探索有没有比高铁更好的方式——搭乘飞机。

而现在这种飞机(甚至是飞船)已经诞生:2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。

2.4 体验Lambda的更优写法

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

// 演示Lambda优化代码
public class LambdaDemo {
    public static void main(String[] args) {

        // 使用匿名内部类完成线程任务
        new Thread(new Runnable() {
            @Override // 线程任务 重写run方法
            public void run() {
                System.out.println("使用匿名内部类..完成多线程执行...");
            }
        }).start();

        // 代码优化,使用Lambda表达式
        new Thread( () -> {
                System.out.println("使用匿名内部类..完成多线程执行...");
            }
        ).start();
    }
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

2.5 回顾匿名内部类

Lambda是怎样击败面向对象的?在上例中,核心代码其实只是如下所示的内容:

() -> System.out.println("多线程任务执行!")

为了理解Lambda的语义,我们需要从传统的代码起步。

使用实现类

要启动一个线程,需要创建一个Thread类的对象并调用start方法。而为了指定线程执行的内容,需要调用Thread类的构造方法:

  • public Thread(Runnable target)

为了获取Runnable接口的实现对象,可以为该接口定义一个实现类RunnableImpl:

public class RunnableImpl implements Runnable {
	@Override
	public void run() {
		System.out.println("多线程任务执行!");
	}
}

然后创建该实现类的对象作为Thread类的构造参数:

public class Demo03ThreadInitParam {
	public static void main(String[] args) {
		Runnable task = new RunnableImpl();
		new Thread(task).start();
	}
}

使用匿名内部类

这个RunnableImpl类只是为了实现Runnable接口而存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:

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

匿名内部类的好处与弊端

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

语义分析

仔细分析该代码中的语义,Runnable接口只有一个run方法的定义:

  • public abstract void run();

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

  • 无参数:不需要任何条件即可执行该方案。
  • 无返回值:该方案不产生任何结果。
  • 代码块(方法体):该方案的具体执行步骤。

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

() -> { System.out.println("多线程任务执行!") }
  • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。

2.6 Lambda的使用前提-函数式接口

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

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

概念

函数式接口在Java中是指:有且仅有一个抽象方法的接口。

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。

格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {	
	void myMethod();
}

@FunctionalInterface注解

与@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
	void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

2.7 Lambda标准格式

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

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

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

格式说明:

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

代码演示

/*
    Lambda表达式的标准格式,由三部分组成:
    1、一些参数
    2、一个箭头→
    3、一段代码
    Lambda表达式格式:
        ( 参数列表 )->{ 重写的方法体代码 }
    格式说明:
    (): 接口中抽象方法的参数列表,没有参数就空着不写。有参数就写出参数,多少参数使用逗号分隔
    ->: 将参数列表中的参数,传递给方法体
    {}:重写接口中抽象方法的具体代码。

*/
public class LambdaDemo {
    public static void main(String[] args) {
        // 使用匿名内部类完成线程任务
        new Thread(new Runnable() {
            @Override // 线程任务 重写run方法
            public void run() {
                System.out.println("使用匿名内部类..完成多线程执行...");
            }
        }).start();

        // 代码优化,使用Lambda表达式
        new Thread( () -> {
                System.out.println("使用匿名内部类..完成多线程执行...");
            }
        ).start();
    }
}

2.8 练习:使用Lambda标准格式(无参无返回)

题目

给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数、无返回值。如下:

// 厨师接口
public interface Cook {
  //做菜的抽象方法
  public abstract void makeFood();
}

在下面的代码中,请使用Lambda的标准格式调用invokeCook方法,打印输出“吃饭啦!”字样:

// 演示无参无返回值的Lambda表达式
public class InvokeCook {
    public static void main(String[] args) {
        // TODO 请在此使用Lambda【标准格式】调用invokeCook方法
      
      	// 演示无参无返回值的Lambda表达式

        // 使用匿名内部类完成
        invokeCook(new Cook() {
            @Override // 重写做饭的方法
            public void makeFood() {
                System.out.println("请给我烧一份西红柿炒番茄....");
            }
        });

        // 使用Lambda表达式完成
        invokeCook( () -> {
            System.out.println("请给我烧一份土豆炖马铃薯....");
        } );
    }
    
	// 定义请求厨师做菜的方法
    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

备注:小括号代表Cook接口makeFood抽象方法的参数为空,->表示将参数传递给方法体,大括号代表makeFood的方法体。

2.9 Lambda的参数和返回值

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

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

当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String name和int age两个成员变量:

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

传统写法

如果使用传统的代码对Person[]数组进行排序,写法如下:

public class LambdaDemo {
    public static void main(String[] args) {

        Person[] ps = {
                new Person("如来佛祖",90000),
                new Person("观音菩萨",18),
                new Person("孙悟空",700)
        };

        // 需求:将数组中的对象,按年龄从小到大的方式排序
      
        // 使用匿名内部类的方式完成
        Arrays.sort(ps, new Comparator<Person>() {
            @Override // 重写比较的方法,按年龄排序
            public int compare(Person o1, Person o2) {
                return o1.age - o2.age;
            }
        });

        // 打印排序后的数组
        for( Person p : ps ) {
            System.out.println( p );
        }
    }
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

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

Lambda写法

// 演示有参有返回值 Lambda表达式
public class LambdaDemo {
    public static void main(String[] args) {
        Person[] ps = {
                new Person("如来佛祖",90000),
                new Person("观音菩萨",18),
                new Person("孙悟空",700)
        };

        // 需求:将数组中的对象,按年龄从小到大的方式排序

//        // 使用匿名内部类的方式完成
//        Arrays.sort(ps, new Comparator<Person>() {
//            @Override // 重写比较的方法,按年龄排序
//            public int compare(Person o1, Person o2) {
//                return o1.age - o2.age;
//            }
//        });

        // 使用Lambda方式完成
        Arrays.sort( ps , ( Person p1 , Person p2 ) -> {
            return p1.age - p2.age;
        });

        // 打印排序后的数组
        for( Person p : ps ) {
            System.out.println( p );
        }
    }
}

2.10 练习:使用Lambda标准格式(有参有返回)

题目

给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数字相加得到和值:

// 计算器接口
public interface Calculator {
  	// 计算两个int类型和值的方法
    public abstract int calc(int a, int b);
}

在下面的代码中,请使用Lambda的标准格式调用invokeCalc方法,完成120和130的相加计算:

public class LambdaDemo {
    public static void main(String[] args) {
        // 使用匿名内部类完成
        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;
        });
    }

    // 调用计算器,计算和值的方法
    public static void invokeCalc( int a, int b , Calculator calculator ) {
        // 调用方法,计算和值
        int sum = calculator.calc(a, b);
        // 打印计算的和值
        System.out.println( a + " + " + b + " = " + ( a + b ) );
    }
}

备注:小括号代表Calculator接口calc抽象方法的参数,大括号代表calc的方法体。

2.11 Lambda省略格式

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

public static void main(String[] args) {
  	invokeCalc( 120, 130, (a, b) -> a + b );
}

省略规则

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

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

备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。

2.12 练习:使用Lambda省略格式

题目

仍然使用前文含有唯一makeFood抽象方法的厨子Cook接口,在下面的代码中,请使用Lambda的省略格式调用invokeCook方法,打印输出“吃饭啦!”字样:

public class InvokeCook {
    public static void main(String[] args) {
        // TODO 请在此使用Lambda【省略格式】调用invokeCook方法
      	invokeCook( () -> System.out.println("请给我烧一份西红柿炒番茄....") );
    }
	// 请厨师做饭的方法
    public static void invokeCook( Cook cook ) {
        cook.makeFood();
    }
}

第三章 常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。前文的MySupplier接口就是在模拟一个函数式接口:java.util.function.Supplier。其实还有很多,下面是最简单的几个接口及使用示例。

3.1 Supplier接口

java.util.function.Supplier接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

import java.util.function.Supplier;

public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
      	return function.get();
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() -> msgA + msgB));
    }
}

备注:其实这个接口在前面的练习中已经模拟过了。

3.2 练习:求数组元素最大值

题目

使用Supplier接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer类。

解答

import java.util.function.Supplier;

public class DemoIntArray {
    public static void main(String[] args) {
        int[] array = { 10, 20, 100, 30, 40, 50 };
        printMax(() -> {
            int max = array[0];
            for (int i = 1; i < array.length; i++) {
                if (array[i] > max) {
                  	max = array[i];
                }
            }
            return max;
        });
    }

    private static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get();
        System.out.println(max);
    }
}

3.3 Consumer接口

java.util.function.Consumer接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

抽象方法:accept

Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。基本使用如:

import java.util.function.Consumer;

public class Demo09Consumer {
    private static void consumeString(Consumer<String> function) {
      	function.accept("Hello");
    }

    public static void main(String[] args) {
        consumeString(s -> System.out.println(s));
        consumeString(System.out::println);
    }
}

当然,更好的写法是使用方法引用。

默认方法:andThen

如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法andThen。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

备注:java.util.Objects的requireNonNull静态方法将会在参数为null时主动抛出NullPointerException异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是“一步接一步”操作。例如两个步骤组合的情况:

import java.util.function.Consumer;

public class Demo10ConsumerAndThen {
    private static void consumeString(Consumer<String> one, Consumer<String> two) {
      	one.andThen(two).accept("Hello");
    }

    public static void main(String[] args) {
        consumeString(
            s -> System.out.println(s.toUpperCase()),
            s -> System.out.println(s.toLowerCase()));
    }
}

运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。

3.4 练习:格式化打印信息

题目

下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX。”的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer接口的Lambda实例,将打印性别的动作作为第二个Consumer接口的Lambda实例,将两个Consumer接口按照顺序“拼接”到一起。

public static void main(String[] args) {
  	String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
    // 姓名:迪丽热巴。性别:女。
}

解答

import java.util.function.Consumer;

public class DemoConsumer {
    public static void main(String[] args) {
        String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
        printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
                  s -> System.out.println("。性别:" + s.split(",")[1] + "。"),
                  array);
    }

    private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
        for (String info : array) {
            one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
        }
    }
}

3.5 Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate接口。

抽象方法:test

Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:

import java.util.function.Predicate;

public class Demo15PredicateTest {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5);
    }
}

条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。

默认方法:and

既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}

如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:

import java.util.function.Predicate;

public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.and(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }

    public static void main(String[] args) {
        method(s -> s.contains("H"), s -> s.contains("W"));
    }
}

默认方法:or

与and的“与”类似,默认方法or实现逻辑关系中的“或”。JDK源码为:

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:

import java.util.function.Predicate;

public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.or(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }

    public static void main(String[] args) {
        method(s -> s.contains("H"), s -> s.contains("W"));
    }
}

默认方法:negate

“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法negate的JDK源代码为:

default Predicate<T> negate() {
    return (t) -> !test(t);
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在test方法调用之前调用negate方法,正如and和or方法一样:

import java.util.function.Predicate;

public class Demo17PredicateNegate {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.negate().test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }

    public static void main(String[] args) {
      	method(s -> s.length() < 5);
    }
}

3.6 练习:集合信息筛选

题目

数组当中有多条“姓名+性别”的信息如下,请通过Predicate接口的拼装将符合要求的字符串筛选到集合ArrayList中,需要同时满足两个条件:

  1. 必须为女生;

  2. 姓名为4个字。

    public class DemoPredicate {
    public static void main(String[] args) {
    String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男”, “赵丽颖,女” };
    }
    }

解答

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class DemoPredicate {
    public static void main(String[] args) {
        String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
        List<String> list = filter(array,
                                   s -> "女".equals(s.split(",")[1]),
                                   s -> s.split(",")[0].length() == 3);
        System.out.println(list);
    }

    private static List<String> filter(String[] array, Predicate<String> one, 
                                       Predicate<String> two) {
        List<String> list = new ArrayList<>();
        for (String info : array) {
            if (one.and(two).test(info)) {
                list.add(info);
            }
        }
        return list;
    }
}

3.7 Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。

抽象方法:apply

Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。使用的场景例如:将String类型转换为Integer类型。

import java.util.function.Function;

public class Demo11FunctionApply {
    private static void method(Function<String, Integer> function) {
        int num = function.apply("10");
        System.out.println(num + 20);
    }

    public static void main(String[] args) {
        method(s -> Integer.parseInt(s));
    }
}

当然,最好是通过方法引用的写法。

默认方法:andThen

Function接口中有一个默认的andThen方法,用来进行组合操作。JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

该方法同样用于“先做什么,再做什么”的场景,和Consumer中的andThen差不多:

import java.util.function.Function;

public class Demo12FunctionAndThen {
    private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
        int num = one.andThen(two).apply("10");
        System.out.println(num + 20);
    }

    public static void main(String[] args) {
        method(s -> Integer.parseInt(s), i -> i *= 10);
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过andThen按照前后顺序组合到了一起。

请注意,Function的前置条件泛型和后置条件泛型可以相同。

3.8 练习:自定义函数模型拼接

题目

请使用Function进行函数模型的拼接,按照顺序需要执行的多个函数操作为:

  1. 将字符串截取数字年龄部分,得到字符串;
  2. 将上一步的字符串转换成为int类型的数字;
  3. 将上一步的int数字累加100,得到结果int数字。

解答

import java.util.function.Function;

public class DemoFunction {
    public static void main(String[] args) {
        String str = "赵丽颖,20";
        int age = getAgeNum(str, s -> s.split(",")[1],
                            s -> Integer.parseInt(s),
                            n -> n += 100);
        System.out.println(age);
    }

    private static int getAgeNum(String str, Function<String, String> one,
                                 Function<String, Integer> two,
                                 Function<Integer, Integer> three) {
        return one.andThen(two).andThen(three).apply(str);
    }
}

3.9 总结:延迟方法与终结方法

在上述学习到的多个常用函数式接口当中,方法可以分成两种:

  • 延迟方法:只是在拼接Lambda函数模型的方法,并不立即执行得到结果。
  • 终结方法:根据拼好的Lambda函数模型,立即执行得到结果在这里插入代码片值的方法。

通常情况下,这些常用的函数式接口中唯一的抽象方法为终结方法,而默认方法为延迟方法。但这并不是绝对的。下面的表格中进行了方法分类的整理:

接口名称 方法名称 抽象/默认 延迟/终结
Supplier生产型接口 T get 抽象 终结
Consumer消费型接口 accept( T t ) 抽象 终结
andThen 默认 延迟
Predicate判断型接口 boolean test( T t ) 抽象 终结
and 对应 && 默认 延迟
or 对应 || 默认 延迟
negate 对应 ! 默认 延迟
Function类型转换接口 R apply( T t ) 抽象 终结
andThen 默认 延迟

备注:JDK中更多内置的常用函数式接口,请参考java.util.function包的API文档。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值