Java的函数式编程理解

Java的函数式编程理解-Java8新特性

Java引入了lambda表达式并添加了一些对应的API以及stream流。这块东西在我之前也是断断续续地看了一些,始终觉得不够系统。在最近仔细研究了以后好像突然懂了一些东西。接下来我想以我的一种视角来仔细聊聊这个话题。

假如你是初学Java,按照这个思路下你应该也可以看懂。如果看不懂,请在评论给我一些反馈。

什么是函数式编程

英文的叫法是:Functional programming。函数式编程是一种编程的范式。当然还有大名鼎鼎地面向对象编程,英文地叫法是:Object-oriented programming。

函数式编程,针对的是一个函数的输入和输出。输入是这个函数的参数,而输出是这个函数的返回值。函数本身也可也作为输入传给另一个函数的参数中,而且也可以作为一个返回值来输出。

函数式编程的优点(网上搜的)

  1. 开发速度快、易于复用:易于复用倒是真的,不过开发速度快这个倒不一定吧,看使用的熟练程度。
  2. 便于理解,接近自然语言的逻辑:调用关系很清晰是真的,但不能说接近自然语言的逻辑吧。
  3. 易于并发编程:函数式编程都在方法中进行处理,你可以理解为每调用一次方法,这些方法中的变量都是独立的。函数式编程只关注自身的输入与输出,它不会出现面向对象的死锁问题。

缺点:

由于是函数式编程,在代码的运行过程中,会产生许多的变量来占用内存,这也是函数式编程自身最大的问题所在。函数式编程相比于面向对象编程,不太适合做大型项目的开发(相对而言)。

核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

Java一直是面向对象编程的杰出典范,不过在Java8引入了lambda表达式之后,Java也可以以一种神奇的方式来实现函数式编程的行为。面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象。

JavaScript中的函数式编程

哪怕你不动JavaScript也没关系,你可以感受一下。

1
2
3
4
5
6
7
8
9
10
11
12
function each(array, fn) {
for (var index in array) {
//call调用 此时的fn是参数变量,不会当做一个真正的函数
fn.call(window, index, array[index]); //等同于window.fn(index, array[index]) 但是这
//种调用方式会把fn当成一个函数,window里并没有定义fn这个函数所以这种调用方式是不可行的。

}
}

each([23, 45, 67, 89], function(index, elem) {
document.writeln(index + "->" + elem + "<br>");
});

Java中的匿名方法与匿名内部类

在说所有之前,我想回顾一下Java中匿名方法与匿名内部类。众所周知,Java内部类有4种:成员内部类,静态内部类,局部内部类,匿名内部类。在这里,我重点介绍一下匿名内部类。

匿名内部类

匿名顾名思义就是没有名字,内部意味着不能单独存在。

1
2
3
4
5
6
7
package myGift;

public class Simple {
public static void main(String[] args) {
System.out.println("你好,世界");
}
}

上述是一个最简单的Java程序,Simple代表着类名字。我们不能把Simple删掉,这样程序会报错。所以匿名类是不能单独存在的,必须依附于一个类或接口。下面是一个类正常实现方法的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package myGift;

public class Simple {
public static void main(String[] args) {
System.out.println("你好,世界");

//正常实现接口的类
PrintName havaName = new HaveName();
havaName.sendToMe("你是正常的内部类");
}
}

interface PrintName{
void sendToMe(String str);
}

class HaveName implements PrintName{
@Override
public void sendToMe(String str) {
System.out.println("我是有名字的普通类:");
}
}

当处于这样的情况下(不一定是接口,抽象类也可以),HaveName类必须依赖于接口PrintName而存在(HaveName类必须实现接口PrintName的抽象方法)。在这样的情况下,我们可以将HavaName做一个改造,将其改造为匿名内部类。下面代码是例子:

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
28
29
package myGift;

public class Simple {
public static void main(String[] args) {
System.out.println("你好,世界");

//正常实现接口的类
PrintName havaName = new HaveName();
havaName.sendToMe("你是正常的内部类");
PrintName noName = new PrintName() {
@Override
public void sendToMe(String str) {
System.out.println("我是没有名字的匿名内部类:"+str);
}
};
noName.sendToMe("我的");
}
}

interface PrintName{
void sendToMe(String str);
}

class HaveName implements PrintName{
@Override
public void sendToMe(String str) {
System.out.println("我是有名字的普通类:");
}
}

代码中的10-16行就是一个匿名内部类。在上述代码中,他们最终都能打印出字符串。但是我们并没有重新写一个类来实现这个接口。有类名字与没有类名字差异在哪里呢?我的理解是定义一个不需要被重用的类的时候可以用匿名内部类(什么是被重用呢?你可以直接new HaveName来使用,但是匿名内部类就不支持这样的操作,因为它没有名字),下面的例子是匿名内部类的另一种写法:

lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package myGift;

public class Simple {
public static void main(String[] args) {
System.out.println("你好,世界");

//正常实现接口的类
PrintName havaName = new HaveName();
havaName.sendToMe("你是正常的内部类");
PrintName noName = str -> System.out.println("我是没有名字的匿名内部类:"+str);
noName.sendToMe("我的");
}
}

interface PrintName{
void sendToMe(String str);
}

class HaveName implements PrintName{
@Override
public void sendToMe(String str) {
System.out.println("我是有名字的普通类:");
}
}

上述代码只是写法不一样,他们能实现的效果是一模一样的。应该说当接口中只有一个抽象方法存在时,就可以用lambda表达式来表示。使用lambda表示式以后代码突然少了很多行。然后还可以发现当我们使用lambda表达式以后,没有了方法的定义了,就只有一个变量的赋值。所以我的理解这就算是一个匿名函数,因为没有方法的定义了。更进一步来说,匿名函数是对匿名内部类的进一步简化抽象。

lambda的各种写法

完整写法

我们在写lambda表达式的时候,只需要关注两部分即可:参数列表和方法体。

1
2
3
(参数1,参数2,....)->{
这是方法体
};

参数部分需要和被实现的接口中的参数一致。方法体部分就是方法的实现部分,下面就是一个标准的lambda表达式,有参数,有返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
*lambda表达式
* @author wuheng
* @createDate 2023/2/23 10:31
*/
public class LambdaMain {
public static void main(String[] args) {
One one = (a,b)->{
System.out.println("参数a:"+a+",参数b:"+b);
return b+a;
};
String 我有几岁了 = one.oneMethod(25, "我有几岁了");
System.out.println(我有几岁了);
}


}

interface One{
String oneMethod(int a,String b);
}

只有一个参数的精简表达

当只有一个参数时,可以省略括号。仅仅只有一个参数时,才可以省略

1
2
3
参数1->{
这是方法体
};

下面是示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
*lambda表达式
* @author wuheng
* @createDate 2023/2/23 10:31
*/
public class LambdaMain {
public static void main(String[] args) {

//########只有一个参数的情况###########
Two two = a ->{
System.out.println("这是数字:"+a);
};
}


}

interface Two{
void twoMethod(int a);
}

方法体的精简

当一个方法体中的逻辑,有且只有一句的时候,可以省略大括号

1
(参数1,参数2,....)-> 一句这是方法体;

如果一个方法中唯一一条语句是一个返回语句,此时省略大括号的同时,也必须省略掉return

下面是代码例子

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
/**
*lambda表达式
* @author wuheng
* @createDate 2023/2/23 10:31
*/
public class LambdaMain {
public static void main(String[] args) {

//###########方法体的省略###############
Four four = ()-> System.out.println("我是方法四");
Five five = (a,b) -> a+b;
four.fourMethod();
String s = five.fiveMethod("1", "1");
System.out.println("返回结果是"+s);
}


}

interface Four{
void fourMethod();
}

interface Five{
String fiveMethod(String a,String b);
}

总结

lambda表达式是为了简化接口的实现。所以在其中,不应该出现比较复杂的逻辑。如果出现了过于复杂的逻辑,会对程序的可读性造成非常大的影响。如果lambda表达式中需要处理的逻辑比较复杂,一般会另写一个方法。lambda中直接引用这个方法即可。

如果在lambda表达式中,使用到了局部变量,那么这个局部变量会被隐式的声明为 final。是⼀个常量,不能修改值。

方法引用

那么还能不能进一步抽象呢?还是可以。下面介绍方法引用,把抽象贯彻到底。

静态方法的引用

语法:类::静态方法

参数调用:静态方法的方法签名,因为没有 this, 不会追加任何东西。主要本来在静态方法中,就是直接用类名.方法来直接引用方法的。

注意事项:在引用的方法后面不要加小括号,引用的这个方法,必须和接口中定义的一样。

1
2
// Stream<Double> stream = Stream.generate(() -> Math.random());
Stream<Double> stream = Stream.generate(Math::random);

下面是代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test05 {
public static void main(String[] args) {
//实现多个参数,一个返回值的接口
//对一个静态方法的引用,语法:类::静态方法
Test1 test1 = Calculator::calculate;
System.out.println(test1.test(4,5));
}
}

class Calculator{
public static int calculate(int a,int b ){
// 稍微复杂的逻辑:计算a和b的差值的绝对值
if (a > b) {
return a - b;
}
return b - a;
}
}

interface Test1{
int test(int a,int b);
}

非静态方法的引用

语法:类::静态方法

参数调用:成员方法的方法签名,前面会追加 this 的类型,当 :: 前是一个实例时,这个实例会作为第一个参数给绑定到目标方法签名上。

注意事项:在引用的方法后面不要加小括号,引用的这个方法,必须和接口中定义的一样。

1
2
//TreeSet<String> set = new TreeSet<>((s1,s2) -> s1.compareTo(s2));
TreeSet<String> set = new TreeSet<>(String::compareTo);

下面是代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test06 {
public static void main(String[] args) {
//对非静态方法的引用,需要使用对象来完成
Test2 test2 = new Calculator()::calculate;
System.out.println(test2.calculate(2, 3));
}
private static class Calculator{
public int calculate(int a, int b) {
return a > b ? a - b : b - a;
}
}
}
interface Test2{
int calculate(int a,int b);
}

构造方法的引用

语法:类名::new

注意事项:可以通过接口中的方法的参数, 区分引用不同的构造方法。

1
2
// Supplier<Student> s = () -> new Student();
Supplier<Student> s = Student::new;

下面是代码举例

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Test {
private static class Dog{
String name;
int age;
//无参构造
public Dog(){
System.out.println("一个Dog对象通过无参构造被实例化了");
}
//有参构造
public Dog(String name,int age){
System.out.println("一个Dog对象通过有参构造被实例化了");
this.name = name;
this.age = age;
}
}
//定义一个函数式接口,用以获取无参的对象
@FunctionalInterface
private interface GetDog{
//若此方法仅仅是为了获得一个Dog对象,而且通过无参构造去获取一个Dog对象作为返回值
Dog test();
}

//定义一个函数式接口,用以获取有参的对象
@FunctionalInterface
private interface GetDogWithParameter{
//若此方法仅仅是为了获得一个Dog对象,而且通过有参构造去获取一个Dog对象作为返回值
Dog test(String name,int age);
}

// 测试
public static void main(String[] args) {
//lambda表达式实现接口
GetDog lm = Dog::new; //引用到Dog类中的无参构造方法,获取到一个Dog对象
Dog dog = lm.test();
System.out.println("修狗的名字:"+dog.name+" 修狗的年龄:"+dog.age); //修狗的名字:null 修狗的年龄:0
GetDogWithParameter lm2 = Dog::new;//引用到Dog类中的有参构造,来获取一个Dog对象
Dog dog1 = lm2.test("萨摩耶",2);
System.out.println("修狗的名字:"+dog1.name+" 修狗的年龄:"+dog1.age);//修狗的名字:萨摩耶 修狗的年龄:2

}
}

总结

匿名类、匿名函数、lambda表达式、方法引用。这些抽象程度越来越搞,适用范围会越来越小,实际上这些都是对代码的精简,所以请不要写太过于复杂的逻辑。

自定义函数式接口

从之前的lambda表达式的介绍可以看出,由于语法的简略缩进。其实我们已经可以把一个方法当作一种变量赋值了。就像GetDog lm = Dog::new;这行代码一样,等号右边是一个方法,等号左边是承载方法的实体。这一节就主要讨论如何去书写这个实体。

一般都用只有一个抽象方法的接口作为承接的实体

@FunctionlInterface

这个注解是Java官方的注解,用来指定某个接口必须是函数式接口。该注解只是告诉编译器检查这个接口,保证该接口中只能包含一个抽象方法。否则,编译器就会报错,该接口不是必须的,它只是一个语法检查的接口,并没有其他特别的东西。

无参无返回值的函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
//使用该函数式接口 IBossService boss = () -> System.out.println(" invoke m3");
// boss.m3();
public interface IBossService {
static int m2(){
return 2;
}

default void m1(){
System.out.println("m1");
}

void m3();
}

上述代码中虽然有3个方法,但是只有一个抽象的方法m3。所以使用该注解不会报错。重点在于方法m3,定义了一个没有参数没有返回值的抽象方法来承接() -> System.out.println(" invoke m3");这个方法。

无参有返回值的函数式接口

1
2
3
4
@FunctionalInterface
public interface IBossService2 {
String m3();
}

上述中抽象方法为返回值String的方法,实际上String可以用泛型来替代。

有参无返回值的函数式接口

1
2
3
4
5
6
//使用方式
// IBossService3<String> boss3 = e -> System.out.println("param:"+e);
// boss3.m3("lamda");
public interface IBossService3<E> {
void m3(E e);
}

有参有返回值的函数式接口

1
2
3
4
@FunctionalInterface
public interface IBossService4<T,R> {
R m3(T t);
}

所有自定义函数式接口的调用情况

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
*演示Java8的新特性 lambda表达式
* @author wuheng
* @createDate 2023/2/10 11:12
*/
public class LambdaMain {

public static <T,R> String lambdaTwo(IBossService4<T,R> boss4, T t){
System.out.println("======开始运行传入进来的========");
R r = boss4.m3(t);
System.out.println("=======方法执行完毕===========");
System.out.println("========开始执行写死的方法======");
if (r instanceof Integer){
return "这是一个Integer类型的值:"+r;
} else if (r instanceof String) {
return "这是一个String类型的值:"+r;
}else {
return "不被支出的类型";
}
}

public static void main(String[] args) {

//无参无返回值,无参就是括号是空的
IBossService boss = ()-> System.out.println("invoke m3");
boss.m3();
boss.m1();

//无参有返回值,
IBossService2 boss2 = () -> 1+ 100 +"";
String result = boss2.m3();
System.out.println(result);

//有参数没有返回值
IBossService3<String> boss3 = e -> System.out.println("param:"+e);
boss3.m3("lamda");

//有参有返回值
IBossService4<String,Integer> boss4 = e -> 1;
Integer result1= boss4.m3("lambda");
System.out.println(result1);

//通过泛型方法封装实现方法作为一种封装来实现
String mothodResult = lambdaTwo((e -> {
Arrays.asList(2,3,4,1).forEach(System.out::println);
return "执行完毕,"+e;
}),"我是一个字符串");
System.out.println("我是返回结果:"+mothodResult);
}
}

最后的lambdaTwo这个方式就能很好地体现函数式编程的思想,方法作为参数也可以被传递,被调用。

官方的函数式接口介绍

其实官方的函数式接口和我们自定义写的思路差不多,只是多了很多不同的变种而已。

java1.8函数式接口都在rt.jar的java.util.function包中,以下是经常用到的函数式接口。

官方接口1

官方接口2

参考资料

函数式编程

Java的匿名类、匿名函数与方法引用

Java中Lambda表达式使用及详解

java8-函数编程

Java8 lambda表达式及自定义函数式接口入门 - 个人文章 - SegmentFault 思否


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!