Java8笔记十一(函数式编程)
1.1 概述
1.2 Lambda表达式
1.3 Runnable接口
1.4 函数式接口
1.5 高阶函数
1.6 闭包
1.7 函数组合
1.8 柯里化参数
1.9 方法引用
1.1 概述
函数式编程(FP)通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。
Java8之前希望方法在调用时行为不同,只能在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作,Java8增加了把 方法(你的代码)作为参数传递给另一个方法的能力,可以直接通过Lambda表达式和方法引用完成此操作。
1.2 Lambda表达式
Lambda 表达式是使用最小可能语法编写的函数定义,其参数和函数体被箭头 ->
分隔开,箭头左侧是参数,箭头右侧是从 Lambda 返回的表达式。
package com.java8.study;
public class Java8Study {
interface A {
String m1();
}
interface B {
String m1(String p);
}
interface C {
String m1(String p1, String p2);
}
public static void main(String[] args) {
// java8之前
A a = new A() {
@Override
public String m1() {
return "Hello World";
}
};
// java8之后
// 如果没有参数,则必须使用括号 () 表示空参数列表
A a1 = () -> "Hello World";
// 当只用一个参数,可以不需要括号()
B b = p -> p + "Hello World";
C c = (p1, p2) -> p1 + p2;
// 如果有多行,需要加上花括号和return,单行不需要
C c1 = (p1, p2) -> {
String result = p1 + p2;
return result;
};
}
}
递归函数是一个自我调用的函数,可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。
package com.java8.study;
import java.util.function.Function;
import java.util.stream.Stream;
public class Java8Study {
static Function<Integer, Integer> fib;
public static void main(String[] args) {
fib = n -> n == 0 ? 1 : n * fib.apply(n - 1);
Stream.iterate(0, n -> n + 1)
.limit(11)
.forEach(n -> System.out.println(fib.apply(n)));
}
}
// 输出
1
1
2
6
24
120
720
5040
40320
362880
3628800
1.3 Runnable接口
Runnable接口它的方法 run()
不带参数,也没有返回值,因此,我们可以使用 Lambda 表达式和方法引用作为Runnable。
package com.java8.study;
public class Java8Study {
static class A {
static void m1() {
System.out.println("m1");
}
}
public static void main(String[] args) {
Runnable r = () -> System.out.println("lambda");
Runnable r1 = A::m1;
new Thread(r).start();
new Thread(r1).start();
}
}
// 输出
lambda
m1
1.4 函数式接口
方法引用和 Lambda 表达式都必须被赋值,同时赋值需要类型信息才能使编译器保证类型的正确性,为了让编译器知道传递给方法的参数的类型,Java8 引入了 java.util.function
包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型,使得我们一般情况下不需再定义自己的接口。每个接口只包含一个抽象方法,称为 函数式方法 ,这个接口称为函数式接口。
在编写接口时,可以使用 @FunctionalInterface
,它是可选的,它的作用是当接口中抽象方法多于一个时产生编译期错误。
下面是java.util.function
中接口的基本命名准则:
-
如果只处理对象而非基本类型,名称则为 Function
,Consumer
,Predicate
等。参数类型通过泛型添加。 -
如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer
,DoubleFunction
,IntPredicate
等,但返回基本类型的Supplier
接口例外。 -
如果返回值为基本类型,则用 To
表示,如ToLongFunction <T>
和IntToLongFunction
。 -
如果返回值类型与参数类型相同,则是一个 Operator
:单个参数使用UnaryOperator
,两个参数使用BinaryOperator
。 -
如果接收参数并返回一个布尔值,则是一个 谓词 ( Predicate
)。 -
如果接收的两个参数类型不同,则名称中有一个 Bi
。
下面是常用函数式接口:
// 无参数,无返回值
Runnable runnable = () -> System.out.println("Hello World");
// 无参数,返回任意类型
Supplier<String> supplier = () -> "Hello World";
Callable<String> callable = () -> "Hello World";
// 1个参数,无返回值
Consumer<String> consumer = s -> System.out.println(s);
// 2个参数,无返回值,第一个参数是引用,第二个参数是基本类型
ObjIntConsumer<Character> objIntConsumer = (p1, p2) -> System.out.println(p1 + p2);
// 1个参数,返回不同类型
Function<String, Boolean> function = p1 -> "p1".equals(p1);
// 1个参数,返回类型和参数类型相同
UnaryOperator<String> unaryOperator = p1 -> "parmas:" + p1;
// 2个参数,类型相同,返回整型
Comparator<String> comparator = (c1, c2) -> c1.compareTo(c2);
// 1个参数,返回布尔值
Predicate<String> predicate = p1 -> "p1".equals(p1);
请注意,任何函数式接口都不允许抛出受检异常( checked exception)。如果你需要Lambda表达式来抛出异常, 有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch
块中。
1.5 高阶函数
高阶函数只是一个消费或产生函数的函数。
package com.java8.study;
import java.util.function.Consumer;
import java.util.function.Function;
public class Java8Study {
static Consumer<String> product() {
return s -> System.out.println(s);
}
static boolean consume(String p1, Function<String, Boolean> function) {
return function.apply(p1);
}
public static void main(String[] args) {
// 产生一个函数
Consumer<String> product = product();
product.accept("Hello World");
// 消费一个函数
System.out.println(consume("p1", p1 -> "p1".equals(p1)));
}
}
// 输出
Hello World
true
1.6 闭包
考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决,那问题将变得非常棘手。能够解决这个问题的语言被称作 支持闭包,
Java8 提供了有限但合理的闭包支持,被Lambda 表达式引用的局部变量必须是 final
或者是等同 final
效果的。等同 final
效果表示虽然没有明确地声明变量是 final
的,但是因变量值没被改变过而实际有了 final
同等的效果。如果局部变量的初始值永远不会改变(不管在Lambda表达式内还是外面),那么它实际上就是 final
的。
总结Lambda可以没有限制地引用实例变量和静态变量,因为它们有独立的生命周期。但局部变量必须显式声明为final
,或者是等同 final
效果的 。
内部类也会有闭包,Java 8 只是简化了闭包操作。
1.7 函数组合
函数组合意为多个函数组合成新函数。
package com.java8.study;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class Java8Study {
public static void main(String[] args) {
UnaryOperator<String> uo = p -> p;
UnaryOperator<String> uo1 = p -> p + " World";
UnaryOperator<String> uo2 = p -> p.toUpperCase(Locale.ROOT);
System.out.println(uo.apply("Hello"));
// compose(argument)方法先执行参数操作,再执行原操作,andThen(argument)先执行原操作,再执行参数操作
System.out.println(uo.compose(uo1).andThen(uo2).apply("Hello"));
Predicate<String> predicate = p -> p.contains("p1");
Predicate<String> predicate1 = p -> p.length() <= 5;
Predicate<String> predicate2 = p -> p.contains("p2");
// negate()该谓词的逻辑非
System.out.println(predicate.negate().test("p1"));
// and(argument)原谓词和参数谓词的短路逻辑与
System.out.println(predicate.and(predicate1).test("p1dddddd"));
// or(argument)原谓词和参数谓词的短路逻辑或
System.out.println(predicate.or(predicate2).test("p2dddddd"));
}
}
// 输出
Hello
HELLO WORLD
false
false
true
1.8 柯里化参数
柯里化参数表示将一个多参数的函数,转换为一系列单参数函数。
package com.java8.study;
import java.util.function.Function;
public class Java8Study {
public static void main(String[] args) {
Function<String, Function<String, String>> function = p -> p1 -> p + p1;
System.out.println(function.apply("Hello").apply(" World"));
}
}
// 输出
Hello World
1.9 方法引用
方法引用组成:类名或对象名,后面跟 ::
,然后跟方法名称。
package com.java8.study;
public class Java8Study {
interface A {
void call(String s);
}
static class A1 {
void m1(String msg) {
System.out.println(msg);
}
static void m2(String msg) {
System.out.println("static:" + msg);
}
}
interface B {
void call(B1 b1, String msg);
}
static class B1 {
void m1(String msg) {
System.out.println("B1::m1 " + msg);
}
}
interface C {
C1 create(String msg);
}
static class C1 {
public C1(String msg) {
System.out.println(msg);
}
}
public static void main(String[] args) {
// 只要一个方法的签名(方法参数和返回类型)和接口方法的签名相同,就可以将方法引用赋给接口
A1 a1 = new A1();
// 未绑定的方法引用是指没有关联对象的普通方法,使用未绑定的引用时,我们必须先提供对象。
A a = a1::m1;
a.call("m1");
// 静态方法可以通过类名绑定
a = A1::m2;
a.call("m2");
// 未绑定的方法引用还可以通过在接口上添加一个额外参数(对象实例)来实现方法引用
B b = B1::m1;
b.call(new B1(), "Hello");
// 可以捕获构造函数的引用,然后通过引用调用该构造函数
C c = C1::new;
C1 c1 = c.create("C1创建");
}
}
// 输出
m1
static:m2
B1::m1 Hello
C1创建
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法,事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。
package com.java8.study;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public class Java8Study {
public static void main(String[] args) {
// 指向静态方法的方法引用,等同于 p -> Integer.parseInt(p)
Function<String, Integer> fun1 = Integer::parseInt;
// 指向指向任意类型实例方法的方法引用 ,等同于 p -> p.length()
Function<String, Integer> fun2 = String::length;
// 指向已经存在的外部对象中的方法的方法引用,等同于 () -> str.length()
String str = "Hello World";
Supplier<Integer> fun3 = str::length;
// 指向构造器的方法引用,等同于 () -> new ArrayList<>()
Supplier<List<String>> fun4 = ArrayList::new;
}
}
参考链接
https://lingcoder.github.io/OnJava8/#/