1、为什么使用Lambda表达式

Lambda是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码
像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使
Java的语言表达能力得到了提升。它也是Java的一种语法糖(Java语法糖有:增强的for循环、自动拆/封箱、泛型擦除、类型推断)。

1.1、从匿名类到Lambda的转换

举个例子,之前我们在创建一个线程的时候,也许会使用匿名的Runnable类作为Thread对象的构造参数:

1
2
3
4
5
6
7
8
9
10
//匿名类的写法
@Test
public void test(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("do something....");
}
}).start();
}

这种代码看起来不舒服,且代码行数相对比较多,如果使用Lambda表达式该如何写:

1
2
3
4
5
//Lambda表达式写法
@Test
public void test2(){
new Thread(()-> System.out.println("do something....")).start();
}

是不是变的更简洁,连run方法都不需要再写。这种写法没有新增或改变任何功能,在性能上也不会比原来差,但带来了是更方便的使用,我们称这样的语法为语法糖,也称糖衣语法,是美国彼得·约翰·兰达(Peter J. Landin)发明的一个术语。

2、Lambda语法规则

Lambda 表达式的基础语法:Java8中引入了一个新的操作符 “->” 该操作符称为箭头操作符或 Lambda 操作符箭头操作符将 Lambda 表达式拆分成两部分:
左侧:Lambda 表达式的参数列表,参数列表使用()符号。;
右侧:Lambda 表达式中所需执行的功能, 即 Lambda 体,使用{}表示。
格式:

(参数列表)-> {Lambda体}

如:

(Integer p1,Double p2,String p3,…) -> {System.out.println(“do something….”)}

由于Java8对类型推断做了升级(其实类型推断也是一种语法糖),它会根据上下文去判断每个参数的实质类型,所以不用我们在指定参数类型,所以上述例子也可以简写成:

(p1,p2,p3,…) -> {System.out.println(“do something….”)}

下面我们来一一分析它的语法格式可能存在的情况。

2.1、无参数,无返回值

“无参数“”指的是左侧的参数列表的参数个数为0,“无返回值”指的是右侧的Lamdbd体没有返回值,或者说为void。
就比如1.1节的Lambda的例子

1
2
3
4
@Test
public void test2(){
new Thread(()-> System.out.println("do something....")).start();
}

无参数时,参数列表直接使用(),不带任何参数,如果Lambda中只有一行代码,则{}也可以省略。

2.2、有一个参数,无返回值

语法:

(p)->{Lambda体}

其中,p为参数。一个参数时,也可以省略参数列表的()符号:

p->{Lambda体}

1
2
3
4
5
@Test
public void test3(){
Consumer<String> con = p -> System.out.println(p);
con.accept("Hello Lambda!" );
}

其中Consumer是Java提供的函数式接口,这个后面会提到,上面代码的作用是传递一个一个参数,并打印出传递的参数,该方法执行后会在控制台打印出:Hello Lambda!

2.3、一个以上的参数,且有返回值

1
2
3
4
5
6
7
8
@Test
public void test4(){
Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
System.out.println(com.compare(1,2));
}

同理,如果Lamdba体中,只有一行代码的话,不仅可以省略{}符号,而且连return也可以省略:

1
2
3
4
5
@Test
public void test5(){
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
System.out.println(com.compare(1,2));
}

test4和test5的执行结果都是-1。

2.4、语法总结

语法规则:

(参数列表)-> {Lambda体}

①、参数列表的参数类型也可以省略,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”;
②、若参数只有一个参数,则小括号”()”可以省略不写;
③、若Lamdba体中只有一条语句,则中括号“{}”可以不写,且如果有返回值时,return也可以省略。

3、函数式接口

  • 只包含一个抽象方法的接口,称为函数式接口。
  • 可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。也可以在任意函数式接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  • 如果在使用了@FunctionalInterface注解声明了是一个函数式接口,如果添加多个未实现的方法(在Java8后可以在接口中使用default或者使用statis创建已实现的方法),将会出错,不能通过编译。

    3.1、自定义函数式接口

    使用@FunctionalInterface注解声明一个函数式接口:
    1
    2
    3
    4
    @FunctionalInterface
    public interface ICustomFunction {
    Integer getValue();
    }

也可以使用泛型定义一个函数式接口

1
2
3
4
@FunctionalInterface
public interface ICustomGenFunction<T> {
T getValue(T t);
}

那要如何使用呢,以第二个泛型函数式接口为例,我们来实现对字符串做各种操作的功能,先定义一个方法:

1
2
3
4
5
6
7
8
9
/**
* 定义操作字符串的方法
* @param sourceStr 源字符串
* @param function 字符串操作接口,可以自定义实现或者传递Lambda
* @return
*/
public String strOperator(String sourceStr,ICustomGenFunction<String> function){
return function.getValue(sourceStr);
}

如果我们要实现字符串去空格以及将所有字母都转大写(当然如果我们要对字符串操作,没必要这么复杂,这里只是讲述Lambda函数式接口的使用,并无实际的意思):

1
2
3
4
5
6
7
8
9
10
@Test
public void strOperatorTest(){
String str = " Hello World! ";
//对字符串去空操作
String result1 = strOperator(str,p -> p.trim());
System.out.println(result1);
//将字符串中所有字母都变成大写
String result2 = strOperator(str,p -> p.toUpperCase());
System.out.println(result2);
}

上述代码执行结果:

Hello World! HELLO WORLD!

3.2、Java8内置四大核心函数式接口

3.2.1、消费型接口

消费型接口(Consumer),在2.2节的test3方法中,我们已经使用过,参数类型是泛型T,无返回类型(void),它的作用是对类型为T的对象应用操作,包含方法:

void accept(T t)

这里不再举例。

3.2.2、供给型接口

供给型接口(Supplier),无参数,返回泛型T,它的作用是返回类型为T的对象,包含方法:

T get()

如有时候我们需要获取n个100以内的随机数,实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取n个数,这个数的规则可以通过Supplier定义
* @param n 获取数的个数
* @param supplier 定义这些数获取的规则
* @return
*/
public List<Integer> getNumber(int n, Supplier<Integer> supplier){
List<Integer> nums = new ArrayList<>();
for (int i=0;i<n;i++){
nums.add(supplier.get());
}
return nums;
}

@Test
public void supplierTest(){
//获取10个100以内的整数
List<Integer> nums1 = getNumber(10,()->(int)(Math.random() * 100));
nums1.forEach(System.out::println);
}

其中System.out::println这种写法后面具体说明

3.2.3、函数型接口

函数型接口(Function<T,R>)参数类型T,返回类型R,它的作用是对类型T的对象应用操作,并返回结果,结果时R类型的对象,包含方法:

R apply(T t)

在3.1节我么使用了自定义的函数接口来处理字符串,这样需要自己创建一个接口,相对比较麻烦,我们是否可以使用Java自带的函数型接口呢(当然R和T可以是同一种类型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test3(){
String str = "  Hello World!  ";
String newStr = strHandler(str, (str)->str.trim());
System.out.println(newStr);

String subStr = strHandler(str, (str) -> str.substring(2, 5));
System.out.println(subStr);
}

//需求:用于处理字符串
public String strHandler(String str, Function<String, String> fun){
return fun.apply(str);
}

3.2.4、断言型接口

断言型接口(Predicate),参数类型T,返回类型boolean,它的作用是确定类型为T的对象是否满足约束,并返回boolean的值。它包含方法:

boolean test(T t)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test4(){
List<String> list = Arrays.asList("Hello", "Java", "Lambda", "www", "ok");
List<String> strList = filterStr(list, (s) -> s.length() > 3);

for (String str : strList) {
System.out.println(str);
}
}

//需求:将满足条件的字符串,放入集合中
public List<String> filterStr(List<String> list, Predicate<String> pre){
List<String> strList = new ArrayList<>();

for (String str : list) {
if(pre.test(str)){
strList.add(str);
}
}

return strList;
}

3.2.5、其他接口

函数式接口 参数类型 返回类型 用途 方法
BiFunction<T,U,R> T,U R 对类型为T,U参数应用操作,返回R类型的结果 R apply(T t,U u)
UnaryOperator (Function子接口) T T 对类型为T的对象进行一元运算,并返回T类型的结果 T apply(T t)
BinaryOperator (BiFunction 子接口) T,T T 对类型为T的对象进行二 元运算, 并返回T类型的 结果 T apply(T t1, T t2)
BiConsumer<T, U> T, U void 对类型为T, U 参数应用 操作 void accept(T t, U u)
ToIntFunction ToLongFunction ToDoubleFunction T int long double 分 别 计 算 int 、 long 、 double、 值的函数
IntFunction LongFunction DoubleFunction int long double R 参数分别为int、 long、 double 类型的函数

4、引用操作符“::”

4.1、方法引用

当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
(实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致! )
方法引用:使用操作符 “::” 将方法名和对象或类的名字分隔开来。如下三种主要使用情况:

对象::实例方法
类::静态方法
类::实例方法

4.1.1、对象::实例方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test1(){
//之前的写法
PrintStream ps = System.out;
Consumer<String> con = (str) -> ps.println(str);
con.accept("Hello Java8!");

System.out.println("--------------------------------");
//现在的写法
Consumer<String> con2 = ps::println;
con2.accept("Hello Java8!");
//相当于
Consumer<String> con3 = System.out::println;
}

又如:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test2(){
Employee emp = new Employee(101, "张三", 18, 9999.99);

Supplier<String> sup = () -> emp.getName();
System.out.println(sup.get());

System.out.println("----------------------------------");

Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}

其中getName为Employee类属性name的getter方法。需要注意的是:

Lambda的参数列表(个数、顺序、类型)需要和函数式接口的方法参数列表一致
::后面的方法的返回类要和函数式接口的方法返回类型一致

4.1.2、类::静态方法

1
2
3
4
5
6
7
8
@Test
public void test3(){
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

System.out.println("-------------------------------------");

Comparator<Integer> com2 = Integer::compare;
}

其中compare()方法为Integer的静态方法,用于比较两个值的大小,同理::后面的方法的参数列表(个数、顺序、类型)、返回类型必须和函数式接口定义的方法一致。

4.1.3、类::实例方法

可能大多数兄弟都会觉得奇怪,为什么类可以直接使用实例的方法(非静态),对,要能使用这种方式必须满足:

①、Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数
②、同4.1.1的参数列表和返回值需要一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test5(){
/*
* 原先的写法,比较两个字符串是否相同
* 实例方法quals方法是第一个参数x的调用者
* 第二个参数y是实例方法equals的参数
* BiPredicate接口的的方法参数和lambda的参数列表一致,
* boolean test(T var1, U var2) 和equals返回类型一致
*/
BiPredicate<String, String> bp = (x, y) -> x.equals(y);
System.out.println(bp.test("abcde", "abcde"));
System.out.println("-----------------------------------------");
BiPredicate<String, String> bp2 = String::equals;
System.out.println(bp2.test("abc", "abc"));


Function<Employee, String> fun = (e) -> e.show();
System.out.println(fun.apply(new Employee()));
System.out.println("-----------------------------------------");
Function<Employee, String> fun2 = Employee::show;
System.out.println(fun2.apply(new Employee()));
}

4.2、构造方法引用

构造器的参数列表,需要与函数式接口中参数列表保持一致!

使用方式:

类名 :: new

Employee有构造器如下:

1
2
3
4
5
6
7
8
9
10
11
public Employee() {
}

public Employee(String name) {
this.name = name;
}

public Employee(String name, int age) {
this.name = name;
this.age = age;
}
1
2
3
4
5
6
7
8
9
10
@Test
public void test6(){
Supplier<Employee> sup = () -> new Employee();
System.out.println(sup.get());

System.out.println("------------------------------------");

Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());
}

使用Java自带的函数接口:

1
2
3
4
5
6
@Test
public void test7(){
Function<String, Employee> fun = Employee::new;

BiFunction<String, Integer, Employee> fun2 = Employee::new;
}

4.3、数组引用

使用方式:

类型[] :: new

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test8(){
Function<Integer, String[]> fun = (args) -> new String[args];
String[] strs = fun.apply(10);
System.out.println(strs.length);

System.out.println("--------------------------");

Function<Integer, Employee[]> fun2 = Employee[] :: new;
Employee[] emps = fun2.apply(20);
System.out.println(emps.length);
}

最后更新: 2019年07月31日 15:33

原始链接: https://www.sunnymaple.cn/2019/07/29/Lambda表达式/

× 请我吃糖~
打赏二维码