JDK8新特性—Lambda表达式
1. 函数式编程思想概述
在数学中,
函数就是有输入量、输出量的一套计算方案
,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”
,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
1.1 冗余的Runnable代码
1.2 TreeSet的定制排序
1.3 为什么使用Lambda表达式呢?
Lambda 是一个 匿名函数,我们可以把 Lambda 表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递)。
使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
2. Lambda表达式语法☆
Lambda 表达式:
在Java 8 语言中引入的一种新的语法元素和操作符
。这个操作符为 “->”
, 该操作符被称为Lambda 操作符 或 箭头操作符
。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的
参数列表
。无参数则留空,多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能。
// Lambda表达式的标准格式为: (参数类型 参数名称) ‐> { 代码语句 }
2.1 类型推断
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的
“类型推断”
。
2.2 Lambda表达式练习
体验Lambda的更优写法
借助Java 8的全新语法,
Runnable
接口的匿名内部类写法可以通过更简单的Lambda
表达式达到等效:
public class DemoLambdaRunnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
/`
* 覆盖重写抽象方法
*/
@Override
public void run() {
System.out.println("多线程任务执行!");
}
};
Thread thread = new Thread(task);
// 启动线程
thread.start();
// ||
// ||
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有
“不得不创建接口对象”
的束缚,不再有“抽象方法覆盖重写”
的负担,就是这么简单!
使用Lambda标准格式(无参无返回)
给定一个厨子
Cook
接口,内含唯一的抽象方法makeFood
,且无参数、无返回值。
/**
* @Date 2020/11/14 14:14
* @Version 10.21
* @Author DuanChaojie
*/
@FunctionalInterface
public interface Cook {
void makeFood();
}
在下面的代码中,请使用Lambda的
标准格式
调用invokeCook 方法
,打印输出“吃饭啦!”字样:
/**
* @Date 2020/11/14 14:15
* @Version 10.21
* @Author DuanChaojie
*/
public class InvokeCook {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
invokeCook(()->
System.out.println("吃饭了!")
);
}
private static void invokeCook(Cook cook){
cook.makeFood();
}
}
小括号代表 Cook 接口 makeFood 抽象方法的参数为空,大括号代表 makeFood 的方法体。
Lambda的参数和返回值
下面举例演示
java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
/**
* @Date 2020/11/14 14:22
* @Version 10.21
* @Author DuanChaojie
*/
public class Person {
private String name;
private int age;
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;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Lambda写法
/**
* @Date 2020/11/14 14:23
* @Version 10.21
* @Author DuanChaojie
*/
public class ComparatorLambda {
public static void main(String[] args) {
Person[] arr = {new Person("古力娜扎",19), new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) };
// 降序
Arrays.sort(arr,(Person p1, Person p2) -> p2.getAge() - p1.getAge());
for (Person person: arr){
System.out.println(person);
}
}
}
使用Lambda标准格式(有参有返回)
给定一个计算器
Calculator
接口,内含抽象方法calc
可以将两个int数字相乘得到和值:
/**
* @Date 2020/11/14 14:31
* @Version 10.21
* @Author DuanChaojie
*/
@FunctionalInterface
public interface Calculator {
int calc(int a,int b);
}
在下面的代码中,请使用Lambda的
标准格式
调用invokeCalc
方法,完成5和6的相乘计
/**
* @Date 2020/11/14 14:31
* @Version 10.21
* @Author DuanChaojie
*/
public class InvokeCalc {
public static void main(String[] args) {
int result = invokeCalc(5, 6, (int a, int b) -> a * b);
// result = 30
System.out.println("result = " + result);
}
public static int invokeCalc(int a,int b,Calculator calc){
return calc.calc(a,b);
}
}`
小括号代表
Calculator
接口calc
抽象方法的参数,大括号代表calc
的方法体。
2.3 Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
小括号内参数的类型可以省略;
如果小括号内
有且仅有一个参
,则小括号可以省略;如果大括号内
有且仅有一个语句
,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
2.4 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求
接口中有且仅有一个抽象方法
。无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。- 使用Lambda必须具有
上下文推断
。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。- 备注:有且仅有一个抽象方法的接口,称为
“函数式接口”
。
3. 函数式接口
- 只包含一个抽象方法的接口,称为 函数式接口。
- 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
- 我们可以在一个接口上使用
@FunctionalInterface 注解
,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。在java.util.function包下定义了Java 8 的丰富的函数式接口。
3.1 如何理解函数式接口?
- Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也
即java不但可以支持OOP还可以支持OOF(面向函数编程)
- 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。
在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。
- 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。
3.2 函数式接口举例
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
3.3 自定义函数式接口
MyFunction
/**
* @Date 2020/11/14 11:38
* @Version 10.21
* @Author DuanChaojie
*/
@FunctionalInterface
public interface MyFunction<T> {
public T getValue(T t);
}
MyFunctionTest
/`
* @Date 2020/11/14 11:41
* @Version 10.21
* @Author DuanChaojie
*/
public class MyFunctionTest {
public static void main(String[] args) {
String haha = myFunctionTest((str) -> str.toUpperCase(), "haha");
// HAHA
System.out.println(haha);
}
public static String myFunctionTest(MyFunction<String> mf,String str){
return mf.getValue(str);
}
}
3.4 Java内置四大核心函数式接口
Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法: T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供
”一个符合泛型类型的对象数据。
/**
* @Date 2020/11/14 14:45
* @Version 10.21
* @Author DuanChaojie
*/
public class DemoSupplier {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Justweb";
String result = getString(() -> str1 + str2);
// result = HelloJustweb
System.out.println("result = " + result);
}
private static String getString(Supplier<String> sup) {
return sup.get();
}
}
练习:求数组元素最大值:
- 使用
Supplier
接口作为方法参数类型,通过Lambda
表达式求出int
数组中的最大值。提示:接口的泛型请使用java.lang.Integer
类。
/**
* @Date 2020/11/14 14:48
* @Version 10.21
* @Author DuanChaojie
*/
public class ArrayMax {
public static void main(String[] args) {
int[] arr = {1,4,5,7,9,11,43,22};
int result = getMax(() -> {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max){
max = arr[i];
}
};
return max;
});
// 43
System.out.println(result);
}
private static int getMax(Supplier<Integer> sup){
return sup.get();
}
}
Consumer接口
java.util.function.Consumer<T>
接口则正好相反,它不是生产一个数据,而是消费
一个数据,其数据类型由泛型参数决定。Consumer 接口中包含抽象方法
void accept(T t)
,意为消费一个指定泛型的数据。基本使用如:
/**
* @Date 2020/11/14 14:57
* @Version 10.21
* @Author DuanChaojie
*/
public class ConsumerDemo {
public static void main(String[] args) {
//consumerString(s -> System.out.println(s));
consumerString(System.out::println);
}
private static void consumerString(Consumer<String> consumer){
consumer.accept("Hello");
}
}
Consumer接口默认方法andThen
如果一个方法的参数和返回值全都是
Consumer
类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer
接口中的default
方法andThen
。下面是JDK8 Consumer类
的源代码:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
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 的语义正是“一步接一步”操作
。例如两个步骤组合的情况:
/**
* @Date 2020/11/14 15:06
* @Version 10.21
* @Author DuanChaojie
*/
public class ConsumerAndThen {
public static void main(String[] args) {
consumerString(
s -> System.out.println(s.toUpperCase()),
s -> System.out.println(s.toLowerCase()));
}
private static void consumerString(Consumer<String> one, Consumer<String> two) {
one.andThen(two).accept("Hello");
}
}
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。
练习:格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式
“ 姓名:XX 性别:XX”
的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer
接口的Lambda
实例,将打印性别的动作作为第二个Consumer
接口的Lambda
实例,将两个Consumer
接口按照顺序“拼接”到一起。
/**
* @Date 2020/11/14 15:13
* @Version 10.21
* @Author DuanChaojie
*/
public class DemoConsumer {
public static void main(String[] args) {
String[] array = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
printInfo(
s -> System.out.println("姓名:" + 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);
}
}
}
4. 方法引用和构造器引用
Employee
public class Employee {
private int id;
private String name;
private int age;
private double salary;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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 double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Employee() {
}
public Employee(int id) {
this.id = id;
}
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", salary=" + salary + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Employee employee = (Employee) o;
if (id != employee.id)
return false;
if (age != employee.age)
return false;
if (Double.compare(employee.salary, salary) != 0)
return false;
return name != null ? name.equals(employee.name) : employee.name == null;
}
@Override
public int hashCode() {
int result;
long temp;
result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
temp = Double.doubleToLongBits(salary);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
4.1 方法引用
- 使用场景:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
- 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,
可以认为是Lambda表达式的一个语法糖
。- 要求:
实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!
- 格式:使用
操作符 “::” 将类(或对象) 与 方法名分隔开来
。如下三种主要使用情况:
对象:: 实例方法名
类:: 静态方法名
类:: 实例方法名
/**
* 方法引用的使用
* 1.使用场景:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
* 2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例,所以方法引用,也是函数式接口的实例。
* 3.使用格式: 类(或对象):: 方法名
* 4.具体分为如下三种情况:
* 1 对象:: 非静态方法
* 2 类:: 静态方法
* 3 类:: 非静态方法
* 5.方法引用使用的要求:
* 要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同(针对情况1和2)。
*/
public class MethodRefTest {
// 情况一:对象 :: 实例方法
// Consumer中的void accept(T t)
// PrintStream中的void println(T t)
@Test
public void test1() {
Consumer con1 = str -> System.out.println(str);
con1.accept("北京");
System.out.println("----------------------------");
PrintStream ps = System.out;
Consumer con2 = ps::println;
con2.accept("beijing");
}
// Supplier中的T get()
// Employee中的String getName()
@Test
public void test2() {
Employee emp = new Employee(1001, "Tom", 23, 5600);
Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());
System.out.println("----------------------------");
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}
// 情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
@Test
public void test3() {
Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
System.out.println(com1.compare(12, 21));//-1
Comparator<Integer> com2 = Integer::compare;
System.out.println(com2.compare(12, 3));//1
}
//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test4() {
Function<Double, Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double d) {
return Math.round(d);
}
};
System.out.println("---------------------------");
Function<Double, Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));//12
System.out.println("---------------------------");
Function<Double, Long> func2 = Math::round;
System.out.println(func2.apply(12.6));//13
}
// 情况三:类 :: 实例方法
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
@Test
public void test5() {
Comparator<String> com1 = (s1, s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abc", "abd"));//-1
System.out.println("------------------------------");
Comparator<String> com2 = String::compareTo;
System.out.println(com2.compare("abc", "abm"));//-9
}
//BiPredicate中的boolean test(T t1, T t2);
//String中的boolean t1.equals(t2)
@Test
public void test6() {
BiPredicate<String, String> pre1 = (s1, s2) -> s1.equals(s2);
System.out.println(pre1.test("abc", "abc"));
System.out.println("--------------------------");
BiPredicate<String, String> pre2 = String::equals;
System.out.println(pre2.test("abc", "abc"));
}
// Function中的R apply(T t)
// Employee中的String getName();
@Test
public void test7() {
Employee employee = new Employee(1001, "Jerry", 23, 22222);
Function<Employee, String> func1 = (e) -> e.getName();
System.out.println(func1.apply(employee));
System.out.println("------------------------------");
Function<Employee, String> func2 = Employee::getName;
System.out.println(func2.apply(employee));
}
}
4.2 构造器引用
- 构造器引用:
格式: ClassName::new
- 与函数式接口相结合,自动与函数式接口中方法兼容。
- 可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
- 数组引用:
格式: type[] :: new
/**
* 一、构造器引用
* 和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。
* 抽象方法的返回值类型即为构造器所属的类的类型。
* 二、数组引用
*/
public class ConstructorRefTest {
//构造器引用
//Supplier中的T get()
@Test
public void test1() {
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
Supplier<Employee> sup1 = () -> new Employee();
// Employee{id=0, name='null', age=0, salary=0.0}
Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());
}
//Function中的R apply(T t)
@Test
public void test2() {
Function<Integer, Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println("--------------------------------");
Function<Integer, Employee> func2 = Employee::new;
System.out.println(func2.apply(1002));
}
//BiFunction中的R apply(T t,U u)
@Test
public void test3() {
BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);
System.out.println(func1.apply(1001, "tom"));
System.out.println("------------------------------------");
BiFunction<Integer, String, Employee> func2 = Employee::new;
System.out.println(func2.apply(1002, "Jerry"));
}
//数组引用
//Function中的R apply(T t)
@Test
public void test4() {
Function<Integer, String[]> func1 = (length) -> new String[length];
String[] strs = func1.apply(5);
System.out.println(Arrays.toString(strs));
Function<Integer, String[]> func2 = String[]::new;
String[] strs2 = func2.apply(3);
System.out.println(Arrays.toString(strs2));
}
}