Java8函数式编程学习
1. 背景
- Java8是目前主流的版本,之前断断续续的学习过Java8中新增的函数式编程API,但由于缺乏实践,项目中一般只是偶尔用到一些简单的部分(foreach函数),主要还是以旧版本的foreach循环和传统的集合API的形式操作,其他内容慢慢的也遗忘了。但我们的代码需要更新,不能将其变成考古代码。
- 通过对最新的第五版《OnJava8》电子书中的第十三章《函数式编程》进行针对性的学习,为流式API打下基础,再将两者联合应用于实际项目中。
2. 参考资料
- 《OnJava8》
- 涉及到的部分类或方法的文档
3. 问题
1. Lambda表达式中通常无法直观地从看出方法参数类型(参数名称除非携带类型,否则不一定能提供有效信息),不同参数类型之间的顺序和返回值,需要对该接口比较熟悉或者翻阅该类的源代码进行查看;方法引用同理,在阅读时更严重,需要同时查看源接口和目标方法,有没有合理的解决方案?
基本无解,这本身就是一组矛盾点。看起来简洁同时必然会丢失部分细节(语法本身是支持显式指定参数类型等辅助描述信息的,但基本上没人会这么干,本身就是为了更简洁的写法去的)。通过IDE的各种智能提示以及文档的快捷查看功能可以一定程度上的解决这个问题,随着熟练度的提高这个问题应该会随之减少。
4. 关键点
1. 方法引用
- 核心要素:方法签名要一致,返回值可以稍微有些差别,具体表现在函数式接口方法的返回类型如果为void,则不限制被引用的方法返回类型,反之不行。
-
绑定的方法引用和未绑定的方法引用绑定的方法引用:对已实例化对象的方法引用称为绑定的方法引用
Describe d = new Describe(); Callable c = d::show;未绑定的方法引用:指没有关联对象的普通(非静态)方法,使用未绑定的引用之前,我们必须要提供对象。下面的例子中,
X::f表示未绑定的方法引用,因为它尚未绑定到对象,需要一个X对象调用f()。class X { String f() { return "X::f()"; } } interface MakeString { String make(); } interface TransformX { String transform(X x); } public class UnboundMethodReference { public static void main(String[] args) { // 缺少X对象类型的参数,需要一个X对象来调用未绑定的方法引用 // MakeString ms = X::f; // [1] TransformX sp = X::f; X x = new X(); System.out.println(sp.transform(x)); // [2] System.out.println(x.f()); // 同等效果 } }原文说得比较绕,参考python中的非绑定的方法引用语法以及原文中下一段示例代码。这里说的意思其实就是接口方法第一个参数必须为该对象实例,以达到让java通过该对象调用方法的目的。
2. 函数式接口
1. 定义:
- 只有一个抽象方法(重写Object,默认方法不计算在内)
Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.
- 只要满足前一条规则,
@FunctionalInterface注解可加可不加
the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a Functional Interface annotation is present on the interface declaration.
2. JDK内置函数式接口命名规则
-
如果只处理对象而非基本类型,名称则为
Function(T to R),Consumer (T to void),Predicate(T to boolean)等,参数类型通过泛型添加。 -
如果接收的参数是基本类型,则由名称的第一部分表示,如
LongConsumer,DoubleFunction,IntPredicate等,但`Supplier相关的基本类型例外注:Supplier接口没有参数,名称的第一部分代表其提供并返回的基本类型,如:
public interface LongSupplier { long getAsLong(); } -
如果返回值为基本类型,则用
To表示,如ToLongFunction<T>和IntToLongFunction,如:public interface ToLongFunction<T> { long applyAsLong(T value); }public interface IntToLongFunction { long applyAsLong(int value); } -
如果返回值类型与参数类型一致,则是一个运算符:单个参数使用
UnaryOperator,两个参数使用BinaryOperator。(注:unary:一元的) -
如果接收两个参数且返回值为布尔值,则是一个
谓词(Predicate)。 -
如果接收的两个参数类型不同,则名称中有一个
Bi。
3. 高阶函数(Higher-order Function)
定义: 产生或消费一个函数的函数.
缩白啦, 接入函数作为参数传入或者作为结果输出的, 都算是高阶函数.
如: 基于一个消费函数(Function#andThen())来产生一个新函数示例:Function#andThen()源码:default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)) }public class ComposeFunction { static Function<Integer, Boolean> transform(Function<Integer, String> input) { return input.andThen(o -> { System.out.println(o.getClass().getName()); System.out.println("后置状态, 从String->Boolean"); return true; }); } public static void main(String[] args) { final Function<Integer, Boolean> transform = transform(i -> { System.out.println(i.getClass().getName()); System.out.println("前置状态, 从Int->String"); return "前置状态, 从Int->String"; }); // 此处的apply函数实际上调用的就是上面transform()返回的andThen方法. final Boolean result = transform.apply(17); } }整个流程不复杂,但是
andThen()方法夹在里面进行函数组合,想准确的描述各个函数调用的代码块会比较绕。4. 闭包
持有外部环境变量的函数就是闭包
自由变量: 闭包函数所持有的外部环境变量
1. 与其他语言的闭包差异:
在Java中,lambda表达式或者是匿名内部类都差不多,被闭包所引用的
自由变量必须是final或effectively final(java8新增),即不可改变引用(引用类型)/不可改变值(不能进行重新赋值或++/--操作,基本类型)。至于为啥会有这个要求: 参考链接: 为什么说Java匿名内部类是残缺的闭包
5. 函数组合
andThen(after): 原函数执行之后the function to apply before this function is applied
compose(before):原函数执行之前the function to apply after this function is applied
``and
()/or()/negate()`除了andThen和compose需要注意,其他逻辑判断函数接口与正常逻辑一致。
// f2 -> f1 -> f3 f1.compose(f2).andThen(f3); // f3 -> f2 -> f1 f1.compose(f2).compose(f3);6. 柯里化
定义: 柯里化(又称部分求值): 将一个多参数的函数,转换为一系列单参数函数。
感觉在Java日常开发中没多大实际用处,更像是屠龙术,了解即可。
代码示例:
public class CurryingAndPartials { /** * 未柯里化的多参数写法 */ static String uncurried(String a, String b){ return a + b; } public static void main(String[] args) { System.out.println(uncurried("Hello ", "world")); /* * Lambda表达式的柯里化写法, 需要加上括号分隔参数和返回值部分看起来才比较直观, * 且刚开始不太好理解变量的作用域 */ Function<String, Function<String, String>> sum1 = // 未加括号不太好理解 // a -> b -> a + b; // 加了括号稍微好点 a -> (b -> a + b); /* * 可以很直观的看出返回的闭包函数在外围函数的作用域内, * 可以引用外围函数内的自由变量. * 但是过于臃肿,刚开始看不太习惯时可以用来参考,不推荐写出来用。 */ Function<String, Function<String, String>> sum2 = new Function<String, Function<String, String>>() { @Override public Function<String, String> apply(String a) { return new Function<String, String>() { @Override public String apply(String b) { return a + b; } }; } }; final Function<String, String> hello = sum1.apply("hello "); final String world = hello.apply("world"); System.out.println(world); } }
5. Lambda表达式与匿名内部类的对比(需要借助流创建的知识):
public static void main(String[] args) {
float[] source = {1.0f, 3.0f, 4.0f};
/*
方式2: 受自由变量index必须是final的影响,
该lambda表达式用不了(在不通过转数组/集合这种操作规避这个问题的情况下).
*/
// int index = 0;
// Stream.generate(() -> source[index++ % source.length]);
// System.out.println("------------");
// 方式3: 使用匿名内部类, 通过成员变量的方式很好的解决了方式2中存在的闭包变量捕获问题.
Stream.generate(new Supplier<Float>() {
int index = 0;
@Override
public Float get() {
final float v = source[index++];
index = index % source.length;
return v;
}
})
.limit(6)
.forEach(System.out::println);
}
文中的原因和解决方案:
原因:
本质上就是因为lambda表达式在方法内部,那么lambda表达式的内存分配就是在栈上。栈内存不存在线程安全问题,因为栈内存存的都是变量的副本。 对于局部变量count而言,它的生命周期就是所在方法的生命周期。这就决定了count无法被位于同一个栈帧上的lambda修改,因为这种修改毫无意义,你无法将你的修改传递出当前栈帧。栈内存不会被共享,也就意味着你没有权利和其他栈帧通信。
解决方法:
有两种方式:使用
数组/集合或者把局部变量定义为全局变量。这2种方式,其实本质是一样的:内存都分配在堆上。这就决定了,使用这2种方式来修改变量的值,是可行的。
使用全局变量也可以,但是有个问题,全局变量需要考虑线程安全问题。
6. 总结
-
刚开始看不太适应复杂的函数式编程,但不用太慌,本质上还是一个匿名内部类。如果复杂的表达式看起打脑壳可以考虑转换为匿名内部类的方式看看它的调用流程。参考
高阶函数示例。在不修改代码写法的情况下,能够准确的描述
高阶函数示例中每个函数调用的代码块我觉得差不多够了。 -
lambda表达式在大部分时候能简化代码编写,看起来更简洁,但有的写法也会加大理解难度,比如刚开始接触柯里化里面的写法,像书上那样不加括号看起跟别人写的复杂正则表达式一样,写注释都很难拯救。
-
闭包的概念搞清楚了会有助于理解
高阶函数和柯里化的例子