Java的函数式编程理解-Java8新特性 Java引入了lambda表达式并添加了一些对应的API以及stream流。这块东西在我之前也是断断续续地看了一些,始终觉得不够系统。在最近仔细研究了以后好像突然懂了一些东西。接下来我想以我的一种视角来仔细聊聊这个话题。
假如你是初学Java,按照这个思路下你应该也可以看懂。如果看不懂,请在评论给我一些反馈。
什么是函数式编程 英文的叫法是:Functional programming。函数式编程是一种编程的范式。当然还有大名鼎鼎地面向对象编程,英文地叫法是:Object-oriented programming。
函数式编程,针对的是一个函数的输入和输出。输入是这个函数的参数,而输出是这个函数的返回值。函数本身也可也作为输入传给另一个函数的参数中,而且也可以作为一个返回值来输出。
函数式编程的优点(网上搜的)
开发速度快、易于复用:易于复用倒是真的,不过开发速度快这个倒不一定吧,看使用的熟练程度。
便于理解,接近自然语言的逻辑:调用关系很清晰是真的,但不能说接近自然语言的逻辑吧。
易于并发编程 :函数式编程都在方法中进行处理,你可以理解为每调用一次方法,这些方法中的变量都是独立的。函数式编程只关注自身的输入与输出,它不会出现面向对象的死锁问题。
缺点:
由于是函数式编程,在代码的运行过程中,会产生许多的变量来占用内存,这也是函数式编程自身最大的问题所在。函数式编程相比于面向对象编程,不太适合做大型项目的开发(相对而言)。
核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
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) { fn.call(window , index, array[index]); } } 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 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 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 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);
下面是代码举例:
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 ) { 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<>(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 = 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 test () ; } @FunctionalInterface private interface GetDogWithParameter { Dog test (String name,int age) ; } public static void main (String[] args) { GetDog lm = Dog::new ; Dog dog = lm.test(); System.out.println("修狗的名字:" +dog.name+" 修狗的年龄:" +dog.age); GetDogWithParameter lm2 = Dog::new ; Dog dog1 = lm2.test("萨摩耶" ,2 ); System.out.println("修狗的名字:" +dog1.name+" 修狗的年龄:" +dog1.age); } }
总结 匿名类、匿名函数、lambda表达式、方法引用。这些抽象程度越来越搞,适用范围会越来越小,实际上这些都是对代码的精简,所以请不要写太过于复杂的逻辑。
自定义函数式接口 从之前的lambda表达式的介绍可以看出,由于语法的简略缩进。其实我们已经可以把一个方法当作一种变量赋值了。就像GetDog lm = Dog::new;
这行代码一样,等号右边是一个方法,等号左边是承载方法的实体。这一节就主要讨论如何去书写这个实体。
一般都用只有一个抽象方法的接口作为承接的实体
@FunctionlInterface 这个注解是Java官方的注解,用来指定某个接口必须是函数式接口。该注解只是告诉编译器检查这个接口,保证该接口中只能包含一个抽象方法。否则,编译器就会报错,该接口不是必须的,它只是一个语法检查的接口,并没有其他特别的东西。
无参无返回值的函数式接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @FunctionalInterface 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 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 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包中,以下是经常用到的函数式接口。
参考资料 函数式编程
Java的匿名类、匿名函数与方法引用
Java中Lambda表达式使用及详解
java8-函数编程
Java8 lambda表达式及自定义函数式接口入门 - 个人文章 - SegmentFault 思否