Java函数式系列-lambda表达式

今天来讲讲Java8对于函数式编程的支持,同时也是Java8版本新特性之一的lambda表达式。

在我自己的理解中,我认为要想快速理解lambda的关键很简单,只要记住一句话,lambda表达式其实不是什么新东西,他的本质就是匿名内部类,仅此而已,只是编译器对匿名内部类的创建做了简化处理,只要我们书写更为简洁的lambda语句即可,有关这一点可以使用eclipse的反编译工具对生成的class文件进行处理,应该就会明白我们所写的lambda语句被反编译后其实就是一个个匿名内部类。

下面我就来顺着思路说说lambda表达式

首先,我们回顾下Java怎样创建线程,可以继承Thread类、实现Runnable接口或者使用线程池来实现。

下面我使用实现Runnable接口来创建一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

public class Main {

public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}

输出:

Thread-0

线程被成功创建,并且打印出了当前线程名称。

那通过我的上一篇文章《Java函数式系列-闭包和内部类》的学习,我们知道接口Runnable可以通过匿名内部类的方式直接被new出来,这样子就省去了MyThread类的创建

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

public static void main(String[] args) {
new Thread(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}

代码相比上面少了点吧,但是我觉得还是不够爽!因为Runnable接口只有一个抽象方法run,我们要实现这个接口势必要重写这个方法,而且new Runnable()这个过程完全可以通过Thread类的构造函数推断出来,所以lambda表达式来了

这段代码重新用lambda写一遍是这种场景

1
2
3
4
5
6
public class Main {

public static void main(String[] args) {
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
}

直接变一行代码了!

So,Lambda 表达式是创建匿名内部类的语法糖。在编译器的帮助下,可以让开发人员用更少的代码来完成工作。

函数式接口

在对上述代码进行简化时,我们定义了两个前提条件。第一个前提是要求接口类型,如示例中的 Runnable,可以从当前上下文中推断出来;第二个前提是要求接口中只有一个抽象方法。如果一个接口仅有一个抽象方法(除了来自 Object 的方法之外),它被称为函数式接口(functional interface)。函数式接口的特别之处在于其实例可以通过 Lambda 表达式或方法引用来创建。Java 8 的 java.util.function 包中添加了很多新的函数式接口。如果一个接口被设计为函数式接口,应该添加@FunctionalInterface 注解。编译器会确保该接口确实是函数式接口。当尝试往该接口中添加新的方法时,编译器会报错。

这会体会到lambda的酸爽了吧,还不够,那我再搞几个操作吧

lambda表达式遍历字符串数组

1
2
3
4
5
6
7
8
9
import java.util.Arrays;

public class Main {

public static void main(String[] args) {
String[] name = { "one", "two", "three", "four", "five" };
Arrays.asList(name).stream().forEach((x) -> System.out.println(x));
}
}

我们来看看forEach的源码

1
void forEach(Consumer<? super T> action);

跟进一下Consumer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@FunctionalInterface
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

如果之前的都看懂的话,到这里就很容易明白forEach是如何实现的了,不就是利用jdk提供的函数式接口来帮助我们完成这个lambda表达式嘛。

Java8提供了四大核心函数式接口Function、Consumer、Supplier、Predicate

告诉你怎么记,Function的翻译为函数,那么它提供的抽象方法就是既有入参也有出参,Consumer叫消费者,只懂消费,不懂生产,所以他只有入参,Supplier叫提供者,所以只有出参,Predicate译为断言,所以他有入参,出参是booleen类型的

Function<T, R>

T:入参类型,R:出参类型

调用方法:R apply(T t);

定义函数示例:Function<Integer, Integer> func = p -> p * 10; // 输出入参的10倍

调用函数示例:func.apply(10); // 结果100

Consumer

T:入参类型;没有出参

调用方法:void accept(T t);

定义函数示例:Consumer consumer= p -> System.out.println(p); // 因为没有出参,常用于打印、发送短信等消费动作

调用函数示例:consumer.accept(“18800008888”);

Supplier

T:出参类型;没有入参

调用方法:T get();

定义函数示例:Supplier supplier= () -> 100; // 常用于业务“有条件运行”时,符合条件再调用获取结果的应用场景;运行结果须提前定义,但不运行。

调用函数示例:supplier.get();

Predicate

T:入参类型;出参类型是Boolean

调用方法:boolean test(T t);

定义函数示例:Predicate predicate = p -> p % 2 == 0; // 判断是否、是不是偶数

调用函数示例:predicate.test(100); // 运行结果true

又来新操作了,看好,不知道你有没有感觉上述的lambda遍历数组还是有点优化的余地,哪里呢?

就是这一句

Arrays.asList(name).stream().forEach((x) -> System.out.println(x));

lambda的参数我们定义的x,既然只有一个参数我们为什么不可以再省略呢?直接让编译器帮我们推断出来不就好了?

还真有这操作,Java8的另一个新特性,双冒号“::”操作符

我们看看简化后的样子

1
2
3
4
5
6
7
8
9
import java.util.Arrays;

public class Main {

public static void main(String[] args) {
String[] name = { "one", "two", "three", "four", "five" };
Arrays.asList(name).stream().forEach(System.out::println);
}
}

我们可以通过“::”关键字配合函数式接口来访问类的构造方法,对象方法,静态方法,学会了吧。

目前只懂这么多了,完毕!

文章作者: Shawn Qin
文章链接: https://qinshuang1998.github.io/2019/02/07/function-coding-03/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Shawn's Blog