Java 函数式编程
什么是函数式编程
函数式编程是一种是一种编程范式,它将计算视为函数的运算,并避免变化状态和可变数据。它是一种声明式编程范式,也就是说,编程是用表达式或声明而不是语句来完成的,即强调做什么,而不是以什么形式去做。
Lamda 表达式:
(a,b)->a+b
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
1. Lambda 表达式
在了解 Lambda 表达式之前,先看这样一个需求:
通过 Runnable 启动一个线程,并打印:线程已启动
对于这样一个需求,有以下三种方式:
方式一
实现 Runnable 接口,如下:
class MyRunnable implements Runnable{
@Override
public void run() {
log.info("线程已启动");
}
}
@Test
void test_runnable_01() {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
方式二
匿名内部类,如下:
@Test
void test_runnable_02() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("线程已启动");
}
});
thread.start();
}
方式三
Lambda 表达式,如下:
@Test
void test_runnable_03() {
Thread thread = new Thread(() -> {
log.info("线程已启动");
});
thread.start();
}
可以看到使用 Lambda 表达式使得代码看起来更简单。
1.1 标准格式
匿名内部类中重写 run() 方法的代码分析:
- 方法形式参数为空,说明调用方法时不需要传递参数
- 方法返回值类型为 void,说明方法执行没有结果返回
- 方法体中的内容,是我们具体要做的事情
Lambda 表达式的代码分析:
- ():里面没有内容,可以看成是方法形式参数为空
- ->:用箭头指向后面要做的事情
- {}:包含一段代码,我们称之为代码块,可以看成是方法体中的内容
由此可得知,组成 Lambda 表达式的三要素:形式参数、箭头、代码块。
Lambda 表达式的格式:
- 格式:(形式参数)-> { 代码块 }
- 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
- ->:由英文中画线和大于符号组成,固定写法,代表指向动作
- 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容
1.2 使用前提
Lambda表达式的使用前提:
- 有一个接口
- 接口中有且仅有一个抽象方法
在 Runnable 的测试中,其 run()
是一个无参数的方法,接下来我们按照上述前提来创建一个参数和多个参数的接口来实现 Lambda 表达式。
1.2.1 一个参数
定义一个名为 LambdaFunction 的接口,在定义其抽象方法为 func(String str)
,如下:
public interface LambdaFunction {
public abstract void func(String str);
}
测试:
@Test
void test_Lambda_function() {
Lambda_function((String str) -> {
log.info(str);
}, "一个形参");
}
void Lambda_function(LambdaFunction function, String str) {
function.func(str);
}
1.2.2 多个参数
同上,定义包含 2 个参数的抽象方法。
public interface LambdaFunction2 {
public abstract void func2(String str, int i);
}
测试:
@Test
void test_Lambda_function2() {
Lambda_function2((String str, int i) -> {
log.info(str + ": " + i);
}, "多个参数", 2);
}
void Lambda_function2(LambdaFunction2 function, String str, int i) {
function.func(str, i);
}
1.2.3 有返回值
除了多参数,还要存在返回值的情况,如下定义:
public interface LambdaFunction3 {
public abstract int add(int i1, int i2);
}
测试:
@Test
void test_Lambda_function3() {
int i = Lambda_function3((int i1, int i2) -> {
return i1 + i2;
}, 1, 2);
log.info(i + "");
}
int Lambda_function3(LambdaFunction3 function, int i1, int i2) {
return function.add(i1, i2);
}
1.3 省略简化
Lambda 表达式可以省略简化,它的规则如下:
- 参数类型可以省略,如果有多个参数的情况下,同时省略
- 如果参数有且仅有一个,那么小括号可以省略
- 如果代码块的语句只有一条,可以省略大括号和分号,包括返回值 return
因此我们可以把上面的简化,如下:
@Test
void test_Lambda_function_simplify() {
Lambda_function(str -> log.info(str), "一个参数");
}
@Test
void test_Lambda_function2_simplify() {
Lambda_function2((str, i) -> log.info(str + ": " + i), "多个参数", 2);
}
@Test
void test_Lambda_function3_simplify() {
int i = Lambda_function3((i1, i2) -> i1 + i2, 1, 2);
log.info(i + "");
}
1.4 函数式接口
函数式接口:有且仅有一个抽象方法的接口
Java 中的函数式编程体现就是 Lambda 表达式,所以函数式接口就是可以使用于 Lambda 使用的接口只有确保接口中有且仅有一个抽象方法,Java 中的 Lambda 才能顺利地进行推导。
如何检测一个接口是不是函数式接口呢?
@FunctionalInterface
注解,放在接口定义的上方,如果接口是函数接口,编译通过;如果不是,编译失败。
注意:
我们自己定义函数式接口的时候,@FunctionalInterface
是可选的,就算我们不写这个注解,只要保证满足函数式接口定义的条件,也照样是函数式接口。但是,建议加上注解。
Java 8 在 java.util.function
包下预定了大量的函数式接口供我们使用,常用如下:
- Supplier 接口
- Consumer 接口
- Predicate 接口
- Function 接口
1.4.1 Supplier
方法 | 说明 |
---|---|
T get() | 生产数据的接口 |
Supplier 接口被称为生产型接口,如果我们指定了接口的泛型是什么类型,那么接口中的 get 方法就会生产什么类型的数据供我们使用。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
该方法不需要参数,它会按照某种实现逻辑(由Lambda表达式实现)返回一个数据,如下:
@Test
void supplier_test() {
String string = getString(() -> "String");
log.info(string);
Integer integer = getInteger(() -> 1);
log.info(integer + "");
}
String getString(Supplier<String> supplier) {
return supplier.get();
}
Integer getInteger(Supplier<Integer> supplier) {
return supplier.get();
}
1.4.2 Consumer
方法 | 说明 |
---|---|
void accept(T t) | 消费一个指定泛型的数据 |
default Consumer andThen(Consumer<? super T> after) | 消费数据的时候,首先做一个操作, 然后再做一个操作,实现组合 |
Consumer 接口被称为消费型接口,与 Supplier 接口相反,它消费的数据类型由泛型指定。
① accept
@FunctionalInterface
public interface Consumer<T> {
/**
* 消费一个指定泛型的数据
*
* @param t the input argument
*/
void accept(T t);
/**
* 默认方法
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> {
accept(t); after.accept(t); };
}
}
对于 accept()
方法,它是消费,如下:
@Test
void consumer_test() {
consumer_accept(100, money -> log.info("本次消费: " + money));
}
/**
* 定义一个方法
*
* @param money 传递一个int类型的 money
* @param consumer
*/
void consumer_accept(Integer money, Consumer<Integer> consumer) {
consumer.accept(money);
}
② andThen
如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作, 然后再做一个操作,实现组合。
要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是一步接一步操作。例如两个步骤组 合的情况:
@Test
void consumer_andThen_test() {
consumer_andThen("abcdefg",
// 先转为大写
str -> log.info(str.toUpperCase()),
// 在转为小写
str -> log.info(str.toLowerCase()));
}
void consumer_andThen(String str, Consumer<String> consumer1, Consumer<String> consumer2) {
consumer1.andThen(consumer2).accept(str);
}
例子:
给出一个字符数组:
String[] array = {
"张三,女", "李四,女", "王五,男" };
让它们按照以下格式打印:
姓名: XX 性别: XX
代码如下:
@Test
void printInfo() {