1.1. Lambda表达式
通过具体的实例去体会lambda表达式对于我们代码的简化,其实我们不去深究他的底层原理和背景,仅仅从用法上去理解,关注两方面:
- lambda表达式是Java8的一个语法糖,用来简化了函数式接口(理解什么是函数式接口)实例的代码量;
- 什么是函数式接口,只有在一个接口是函数式接口时候才能使用lambda表达式简化我们的代码;
所以通过以上两个点,我们需要贯彻始终的观念有三点:
- 明确函数式接口定义,就是有且只有一个抽象方法的接口就是函数式接口,当然加上
@FunctionalInterface
注解更可以确定这个接口是函数式接口; - lambda表达式只能用在函数式接口的实例中,即lambda表达式的语法本质就是函数式接口中那个唯一抽象方法的实现语句;
- 因为函数式接口的抽象方法唯一,所以实现(重写)该方法非常明确,不会造成使用了lambda表达式分不清是该接口的哪个方法被重写了,于是我们就可以简化省略各种不必要的语句,比如对数据类型的判断,返回值的判断,大括号之类的,这就是lambda表达式必须在函数式接口中才能使用的原因。
下面我们通过实例,对比没有lambda表达式时候跟有了lambda表达式之后代码的语法糖,以下示例代码包含了lambda表达式的语法规则
package com.ethan.lambda;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
/**
* Lambda表达式的使用
*
* 1.语法格式:
* ->:lambda表达式的操作符
* ->左边:lambda的形参列表(其实就是接口中抽象方法的形参列表)
* ->右边:lambda体,(其实就是接口中抽象方法的具体实现)
* 2.lambda表达式的使用,有六种情况
*
* 3.lambda表达式的本质:是作为对应的函数式接口的实例对象!
* 所以记住:
* 1)lambda表达式的返回值都是对应接口的实例对象;
* 2)lambda表达式的语句是对应接口的方法的具体实现;
*/
public class LambdaTest02 {
//情况1:无参,没有返回值
@Test
public void test01(){
Runnable run1 = new Runnable() {
public void run() {
System.out.println("实现Runnable接口的匿名类对象!");
}
};
run1.run();
System.out.println("================*==============");
Runnable run2 = () -> System.out.println("Lambda表达式实现!");
run2.run();
}
//情况2:有一个参数,没有返回值
@Test
public void test02(){
Consumer<String> con1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s+"是什么呢?");
}
};
con1.accept("eth");
System.out.println("************************");
Consumer<String> con2 = (String s) -> System.out.println(s+"是什么呢?");
con2.accept("eth和lambda");
}
//情况3:有参数,但是参数的数据类型可以省略,因为编译器可以进行"类型推断"
@Test
public void test03(){
System.out.println("************************");
Consumer<String> con2 = (s) -> System.out.println(s+"是什么呢?");
con2.accept("eth和lambda");
System.out.println("************举例说明类型推断************");
List<String> list = new ArrayList<>();//类型推断ArrayList<这里无需再写数据类型>
int[] arr1 = new int[]{1,2,3};//标准
int[] arr2 = {1,2,3};//进行了类型推断,简化
}
//情况4:若是只有一个参数,参数的小括号可以省略
@Test
public void test04(){
Consumer<String> con1 = (s) -> System.out.println(s+"是什么呢?");
con1.accept("此时有形参的小括号");
System.out.println("***********只有一个参数,可以去掉形参小括号*************");
Consumer<String> con2 = s -> System.out.println(s+"是什么呢?");
con2.accept("此时有形参的小括号");
}
//情况5:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
@Test
public void test05(){
Comparator<String> com1 = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
System.out.println(com1.compare("a","c"));
System.out.println("************************");
Comparator<String> com2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
System.out.println(com1.compare("abc","abcdf"));
}
//情况6:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
@Test
public void test06(){
Comparator<String> com = (o1, o2) -> o1.compareTo(o2);
}
}
总结:
- 对比了前后的代码,学会lambda表达式的语法;
- 初步知道什么是函数式接口;
最重要的一点!!!lambda表达式的本质:是作为对应的函数式接口的实例对象!
所以把握住以下两点进行理解:
- lambda表达式的返回值都是对应接口的实例对象;
- lambda表达式的语句是对应接口的方法的具体实现;
1.2 函数式(Functional)接口
函数式接口的定义:
- 只包含一个抽象方法的接口,称为函数式接口。
- 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
- 我们可以在一个接口上使用
@FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。 - 在
java.util.function
包下定义了Java 8 的丰富的函数式接口
如何理解函数式接口
1. Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还可以支持OOF(面向函数编程)
2. 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。
3. 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
4. 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。
1.2.1. Java内置四大核心函数式接口
通过例子对函数式接口和lambda表达式再进行稍微深入的一点理解,慢慢思考消化。
package com.ethan.lambda;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
*
* java内置的四大核心函数式接口
* 接口名 核心(唯一抽象)方法的作用
* 1.Consumer<T> 对类型为T的对象进行相关操作,包含方法:void accept(T t)
* 2.Supplier<T> 返回类型为T的对象,包含方法: T get()
* 3.Function<T,R> 对类型为T的对象进行操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
* 4.Predicate<T> 确定类型为T的对象是否满足某约束条件或者说某种判断规则,并返回boolean值。包含方法: boolean test(T t)
*
*/
public class LambdaTest03 {
/**
* 传统的方式和lambda表达式的对比,传递接口的实例并重写其方法
*/
@Test
public void test01(){
//1.方式1:实例一个接口
Consumer<Double> consumer = new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("方式1:花了" + aDouble + "块钱!");
}
};
happyTime(500,consumer);
System.out.println("============================");
//2.方式2:匿名的形式实例化一个接口
happyTime(500, new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("方式2:花了" + aDouble + "块钱!");
}
});
System.out.println("============================");
//3.方式3:lambda表达式的形式实例化一个接口
happyTime(300,aDouble -> System.out.println("方式3:花了" + aDouble + "块钱!"));
}
/**
* 定义一个方法,其中第二个参数需要传递为一个接口的实例
* @param money
* @param consumer
*/
public void happyTime(double money, Consumer<Double> consumer){
consumer.accept(money);
}
/**
* 传统的方式和lambda表达式的对比,传递接口的实例并重写其方法
*
*/
@Test
public void test02(){
List<String> destList = Arrays.asList("北京","天津","南京","西京","东京","普京","河南","河北","湖南","广东","湖北");
//方式一:仍然以传统的方式实现,不赘述
List<String> strList = filterString(destList, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("京");
}
});
System.out.println(strList);
System.out.println("====================");
//方式二:以lambda表达式实现
List<String> strings = filterString(destList, s -> s.contains("河"));
System.out.println(strings);
}
/**
* 传递一个字符串集合,根据某种规则过滤字符串集合,此规则由Predicate的test方法决定
* @param strList
* @param pre
* @return
*/
public List<String> filterString(List<String> strList, Predicate<String> pre){
List<String> targetList = new ArrayList<>();
for (String s : strList) {
if(pre.test(s)) {
targetList.add(s);
}
}
return targetList;
}
}
1.2.2. Java的其他函数式接口
以下函数式接口是核心四大函数式接口的子接口,其实函数式编程中,函数是一等公民,我们在java中这样理解:
- 函数就是函数式接口中的那个唯一的抽象方法;
- 其形参参数,我们理解为我们数学中的多元一次方程中的自变量x,y,z,如果有返回值,那么将返回值理解为因变量f(x);
- 函数式接口中的唯一的那个抽象方法只需要理解为对一个或多个对象进行相关的操作(这些操作就是我们自己要去写代码实现的,但是为了能够更加简化语法,此时完全可以将这些操作用lambda表达式去实现,扔掉那些不必要的语句,比如数据类型的判断,多余的括号和return关键字)
1.3 方法引用与构造器引用
1.3.1 方法引用
理解方法引用之前,需要注意,其实方法引用需要你对jdk或者你项目中的方法极其熟悉,才能够熟练使用;
建议:真实开发中其实更建议使用lambda表达式,方法引用理解为主。
方法引用的基本理解
# 1.方法引用的语法格式:类或者对象 :: 方法名
具体分为以下三种情况:
1)对象 :: 非静态方法
2)类 :: 静态方法
3)类 :: 非静态方法(该情况下重点理解非静态方法的调用者(即实例对象)其实是隐藏的形参,或者说非静态方法的形参中隐藏了一个this参数)
# 2.方法引用的使用情景:
当发现需要实现Lambda体的操作(即我们要写的lambda表达式),
其他的接口(java8开始接口可以有默认实现的方法了)或者类已经有实现相同功能的方法
此时就可以使用方法引用!!!其他情况下目前不能使用。
也就是说,使用方法引用的前提条件:
1)在能够使用lambda表达式的前提下
2)某个方法的参数类型和参数个数,返回值类型以及方法体都跟我们要写的lambda表达式一致(适用于情况1和2)
# 3.方法引用的理解:
本质上就是lambda表达式,而lambda表达式就是作为函数式接口的一个实例对象,所以方法引用,也是函数式接口的实例对象。
可以将方法引用理解为在我们需要实现lambda表达式时候,
将已经存在的其他类的某个方法(注意这个方法的功能与我们要实现的lambda表达式的功能一致)调用过来(拿来主义),替代我们要写的lambda表达式
可以这样认为:在函数(方法)的层面上消除冗余重复的方法体
总结:
JDK在Lambda表达式的基础上提出了方法引用的概念,允许我们复用当前项目(或JDK源码)中已经存在的且逻辑相同的方法。
即,如果已经存在某个方法能完成你的需求,那么你连Lambda表达式都别写了,直接引用这个方法吧。
示例中理解方法引用:
package com.ethan.methodReferences;
public class MethodRefTest {
/**
* 情况一:对象 :: 实例方法
* 我们知道Consumer是函数式接口,有唯一一个抽象方法void accept(T t)
* 我们又发现PrintStream中的void println(T t)方法,不论是返回值类型,形参类型和形参个数
* 都跟Consumer接口的void accept(T t)方法一样,唯一不同的是方法名不一样,并且假如我们要实现void accept(T t)方法
* 如果实现的功能(方法体)又跟PrintStream中的void println(T t)方法实现的一样,那么此时我们就可以使用方法引用
* 在函数(方法)层面去减少相同功能方法的重复书写,直接调用PrintStream中的void println(T t)方法来代替我们要实现void accept(T t)方法
* 对于一个方法的完整定义来说,这两个方法的方法名不一样无关大雅,但是其他的定义都完全一致,实现的功能完全一致。所以可以替换。
* 这就是方法引用的意义,从函数方法层面去消除重复代码,实现相同功能。
*/
@Test
public void test1() {
//lambda表达式
Consumer<String> con1 = s -> System.out.println(s);
con1.accept("北京");
System.out.println("======================");
//方法引用
Consumer<String> con2 = System.out::println;
con2.accept("beijing");
}
/**
* Supplier中的T get()
* Employee中的String getName()
*/
@Test
public void test2() {
Employee emp = new Employee(11,"ETHAN",22,30000);
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, 22));
System.out.println("========================");
/**
* 这里有个疑惑,我们说方法引用的条件是接口的抽象方法的返回值跟形参类型和个数都要一致,
* 但是尝试Comparator接口的int compare(T t1,T t2)方法,引用Integer的compareTo方法仍然成功
* 情况3会解惑
*/
//Comparator<Integer> com2 = Integer ::compareTo;
Comparator<Integer> com2 = Integer ::compare;
System.out.println(com2.compare(13,111));
}
/**
* Function中的R apply(T t)
* Math中的Long round(Double d)
*/
@Test
public void test4() {
Function<Double,Long> fun1 = aDouble -> Math.round(aDouble);
System.out.println(fun1.apply(12.3));
System.out.println("========================");
Function<Double,Long> fun2 = Math::round;
System.out.println(fun2.apply(12.6));
}
/**
* 情况三:类 :: 实例方法
* 这种情况比较少见,一般其实自己写lambda表达式就挺好的。
*
* Comparator中的int compare(T t1,T t2)
* String中的int t1.compareTo(t2)
* 上面跟我们一开始理解的方法引用的适用条件:
* 即某个方法的参数类型和参数个数,返回值类型以及方法体都跟我们要写的lambda表达式一致(适用于以下情况1和2)
* 不一致!!!如何理解:
* 首先我们要有一个概念,非静态方法的形参其实是隐含了一个形参变量,那就是我们这个非静态方法的实例对象this
* 所以Comparator中的int comapre(T t1,T t2)可以将String中的int t1.compareTo(t2)进行方法引用,就是因为
* String中的int t1.compareTo(t2)方法其实隐含了一个形参this(其实就是t1),
* 那么加上的this就符合方法引用的条件,
* Comparator中的int compare(T t1,T t2)
* String中的int t1.compareTo(this(t1),t2)(this其实也就是Comparator中的int comapre(T t1,T t2)的t1形参。
*
*
* 这种情况下,可以这样理解:
* 方法引用的方法的调用者t1,其实就是函数式接口的抽象方法中的第一个形参。
*/
@Test
public void test5() {
Comparator<String> c1 = (s1,s2)-> s1.compareTo(s2);
System.out.println(c1.compare("abr","abc"));
System.out.println("-==============================");
Comparator<String> c2 = String::compareTo;
System.out.println(c2.compare("abr","abc"));
}
/**
*
* BiPredicate中的boolean test(T t1, T t2);
* String中的boolean t1.equals(t2)
*/
@Test
public void test6() {
BiPredicate<String,String> bp1 = (s1,s2)->s1.equals(s2);
System.out.println(bp1.test("aka","aba"));
System.out.println("==============================");
BiPredicate<String,String> bp2 = String::equals;
System.out.println(bp2.test("aka","aka"));
}
/**
* Function中的R apply(T t)
* Employee中的String getName();
*
* 变形理解:
* Function中的R apply(T t) ================================> Function中的R apply(T t);
* Employee中的emp.getName() ===getName隐藏了一个形参this(emp)==> Employee中的String getName(Employee emp);
*/
@Test
public void test7() {
Employee emp = new Employee(11,"ETHAN",22,30000);
Function<Employee,String> fun1 = employee -> employee.getName();
System.out.println(fun1.apply(emp));
System.out.println("==============================");
Function<Employee,String> fun2 = Employee::getName;
System.out.println(fun2.apply(emp));
}
}
//省略 get/set 构造器
class Employee {
private int id;
private String name;
private int age;
private double salary;
}
1.3.2 构造器引用和数组引用
一、构造器引用
格式: ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。
和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。即对应抽象方法形参列表的构造器必须存在。
抽象方法的返回值类型即为构造器所属类的类型
二、数组引用
格式: type[] :: new
- 将数组看作一个特殊的类,即与构造器引用类型了。
package com.ethan.methodReferences;
import org.junit.Test;
import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class ConstructorRefTest {
/**
* 构造器引用
* Supplier中的T get()
* Employee中的 new Employee();
*/
@Test
public void test1(){
//最基础的方式
Supplier<Employee> sup1 = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println(sup1.get());
System.out.println("========================");
//lambda表达式的方式
Supplier<Employee> sup2 = ()-> new Employee();
System.out.println(sup2.get());
System.out.println("========================");
//构造器引用的方式
Supplier<Employee> sup3 = Employee::new;
System.out.println(sup3.get());
}
//Function中的R apply(T t)
@Test
public void test2(){
Function<Integer,Employee> fun1 = id-> new Employee(id);
System.out.println(fun1.apply(1001));
System.out.println("========================");
Function<Integer,Employee> fun2 = Employee::new;
System.out.println(fun1.apply(1002));
}
//BiFunction中的R apply(T t,U u)
@Test
public void test3(){
BiFunction<Integer,String,Employee> bf1 = (id,name)->new Employee(id,name);
System.out.println(bf1.apply(1001,"ethan"));
System.out.println("=====================");
BiFunction<Integer,String,Employee> bf2 = Employee::new;
System.out.println(bf2.apply(1002,"ethal"));
}
//数组引用
//Function中的R apply(T t)
@Test
public void test4(){
Function<Integer,String[]> fun1 = len -> new String[len];
String[] strArr1 = fun1.apply(5);
System.out.println(Arrays.toString(strArr1));
System.out.println("=====================");
Function<Integer,String[]> fun2 = String[] ::new;
String[] strArr2 = fun2.apply(10);
System.out.println(Arrays.toString(strArr2));
}
}