Java8 新特性 —— 函数式编程
本文部分摘录自 On Java 8
概述
通常,传递给方法的数据不同,结果也不同。同样的,如果我们希望方法被调用时的行为不同,该怎么做呢?结论是:只要能将代码传递给方法,那么就可以控制方法的行为。
说得再具体点,过去我们总是创建包含所需行为的对象,然后将对象传递给想要控制的方法,一般使用匿名内部类来实现。假设现在有这么一个需求:有一个员工信息列表,根据年龄过滤出符合条件的员工信息
// 过滤出大于35岁的员工
public List<Employee> filterEmployee(List<Employee> list) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(emp.getAge() > 35) {
emps.add(emp);
}
}
return emps;
}
// 过滤出大于45岁的员工
public List<Employee> filterEmployee2(List<Employee> list) {
...
}
这样写当然能实现需求,但如果需求变了,要过滤 45 岁的,那岂不是又得写一个 filterEmplyee2() 方法?如果还要过滤 50 岁的,60 岁的,那就没完没了了,而且代码的实现逻辑几乎没有区别。于是我们借助策略模式的思想来简化代码。
public interface MyPredicate<> {
boolean predicate(T t);
}
// 如果有其他过滤需求,只需要实现 MyPredicate 接口即可
public class EmployeeFilter implements MyPredicate<Employee> {
@Override
public boolean predicate(Employee employee) {
return t.getAge() >= 35;
}
}
// 根据传入的 MyPredicate 对象来实现不同的过滤逻辑
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(mp.predicate(emp)) {
emps.add(emp);
}
}
return emps;
}
public void test(List<Employee> list) {
// 创建实现类对象,传入过滤方法
MyPredicate<Employee> predicate = new EmployeeFilter<>();
List<Employee> res = filterEmployee(list, predicate);
// 更简单的方式是使用匿名内部类
List<Employee> res2 = filterEmployee(list, new MyPredicate<Employee>() {
@Override
public boolean predicate(Employee employee) {
return t.getAge() >= 100;
}
});
}
通过观察我们发现,我们需要的只有 predicate() 方法的代码,其他的我们一律不关心。如果 MyPredicate 接口还有其他抽象方法,我们又必须每一个做一次实现,但真正用上的只有 predicate() 方法,不仅显得冗余,而且可读性也很低。为了解决这个问题,Java8 为我们提供了 Lambda 表达式和方法引用两种更加简洁的方式。
Lambda 表达式
Lambda 表达式是一个匿名函数,可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样传递)。虽然在 JVM 规范规定一切都是类,但其幕后执行的各种操作使得 Lambda 看起来像是函数。因此我们可以大胆假设 Lambda 表达式产生的就是一个函数,而不是类。
Lambda 的基本语法有是:(参数) -> {方法体}
- 其中
->
可以视为将参数传递给方法体使用的一个中间桥梁 - 左侧为表达式的参数列表。使用括号包裹参数,当只有一个参数时,可以不需要括号,如果没有参数,则必须使用括号表示空参数列表。参数列表的数据类型可以省略不写,因为 Java 的编译器可以帮助我们根据上下文推断数据类型
- 右侧为表达式中所需执行的功能。方法体如果只有单行,可以省略花括号,此时执行结果自动转化为 Lambda 表达式的放回值,使用 return 关键字是非法的;如果方法体有多行,则必须放在花括号中,这时如果有返回值,就需要使用 return
Lambda 表达式能产生比匿名内部类更易读的代码,因此我们应该尽可能使用 Lambda 表达式。回到之前的例子,我们可以用 Lambda 表达式来替换匿名内部类。
public interface MyPredicate<> {
boolean predicate(T t);
}
// 根据传入的 MyPredicate 对象来实现不同的过滤逻辑
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(mp.predicate(emp)) {
emps.add(emp);
}
}
return emps;
}
public void test(List<Employee> list) {
// 使用 Lambda 表达式
List<Employee> res = filterEmployee(list, e -> e.getAge() <= 5000);
}
Lambad 表达式通常比匿名内部类产生更易读的代码,因此我们应该尽可能使用 Lambda 表达式。
如果我们想编写递归的 Lambda 表达式,必须注意:
方法引用
Lambda 表达式可以帮助我们实现仅调用方法,而不做其他多余动作(如创建对象)的目的,而有些情况下,已经存在能满足需求的方法,我们可以不必再编写 Lambda 表达式,而通过方法引用直接使用该方法。可以理解为方法引用是 Lambda 表达式的另一种表现形式。
方法引用的组成:类名或对象名,后面跟 ::
,然后跟方法名称,如果要分类的话,可以用如下组合:
引用静态方法
className::staticMethod
引用某个对象的实例方法
instance::instanceMethod
引用某个类型的任意对象的实例方法
className::instanceMethod
引用构造方法
className::new
interface Callable {
void call(String s);
}
class Describe {
void show(String msg) {
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) {
System.out.println("Hello, " + name);
}
public static void main(String[] args) {
// 对象名:: 方法名称
Describe d = new Describe();
Callable c = d::show;
c.call("call()");
// 类名::方法名
c = MethodReferences::hello;
c.call("Bob");
}
}
要注意的是,方法引用的签名(参数类型和返回类型)必须符合 Callable 的 call() 的签名。上述代码我没有演示 className::instanceMethod
和 className::new
的情况,这两个有点特殊,待会再介绍。
Runnable 接口
通过之前的学习,我们发现 Runnable 接口也符合特殊的单方法接口格式:它的 run()
方法不带参数,也没有返回值,因此我们可以使用 Lambda 表达式和方法引用作为 Runnable
class Go {
static void go() {
System.out.println("thread go");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
// 匿名内部类方式
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start();
// Lambda 表达式方式
new Thread(
() -> System.out.println("lambda")
).start();
// 方法引用方式
new Thread(Go::go).start();
}
}
未绑定的方法引用
未绑定的方法引用是指没有关联对象的普通(非静态方法),使用未绑定的引用,我们必须先提供对象
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) {
// MakeString ms = X::f; // 无法通过编译
TransformX sp = X::f;
X x = new X();
System.out.println(sp.transform(x));
System.out.println(x.f()); // 同等效果
}
}
我们看到在 MakeString ms = X::f;
中,即使 make()
和 f()
有相同的方法签名,却无法通过编译。这是因为实际上还有另一个隐藏参数 this
没有考虑,你不能在没有 X
对象的情况下调用 f()
,因为它尚未绑定到对象。
要解决这个问题,我们需要一个 X
对象,所以我们的接口需要一个额外的参数如 TransformX
,用来接收一个 X
对象。同样的,在调用 transform(X x)
方法时,也必须传递一个 X
对象作为参数。如果你的方法有多个参数,就以第一个参数接受 this
的模式来处理。
构造函数引用
还可以捕获构造函数的引用,然后通过引用去调用该构造函数。
class Dog {
String name;
int age;
Dog() { name = "stray"; }
Dog(String nm) { name = nm; }
Dog(String nm, int yrs) { name = nm; age = yrs; }
}
interface MakeNoArgs {
Dog make();
}
interface Make1Arg {
Dog make(String name);
}
interface Make2Args {
Dog make(String name, int age);
}
public class CtorReference {
public static void main(String[] args) {
MakeNoArgs mna = Dog::new;
Make1Arg m1a = Dog::new;
Make2Args m2a = Dog::new;
Dog dn = mna.make();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
函数式接口
接口中只有一个抽象方法的接口,称为函数式接口,可以使用注解 @FunctionalInterface
检查一个接口是否符合函数式接口的规范。
Lambda 表达式和方法引用都要赋值给对应的函数式接口引用。Java8 提供了一组 java.util.function
包,它包含一组完整的函数式接口,一般情况下,我们可以直接使用,而不需要自己再定义。
Java 为我们提供了内置的四大核心函数式接口:
消费型接口
有参数,无返回值类型的接口
@FunctionalInterface
public interface Consumer<T> { void accept(T t);
}
供给型接口
只有产出,没有输入,就是只有返回值,没有入参
@FunctionalInterface
public interface Supplier<T> { T get();
}
函数型接口
既有入参,也有返回值,T 表示函数的参数类型,R 表示函数的返回类型
@FunctionalInterface
public interface Function<T, R> { R apply(T t);
}
断言型接口
输入一个参数,返回一个 boolean 类型的返回值
@FunctionalInterface
public interface Predicate<T> { boolean test(T t);
}
除了上述的四个核心内置接口,Java 还为我们提供其他常用的函数式接口,如 BiFunction<T, U, R>
也是函数型接口,但可以接收两个参数,我们可以根据需要去查阅 API 文档。
函数组合
意为多个组合成新的函数,一些 java.util.function
接口包含支持函数组合的方法
andThen(Function<? super R,? extends V> after)
返回一个组合函数,前一个函数的结果作为后一个函数的入参
compose(Function<? super V,? extends T> before)
返回一个组合函数,后一个函数首先处理原始入参,再将结果交给前一个函数处理
and(Predicate<? super T> other)
返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑与
or(Predicate<? super T> other)
返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑或
negate()
返回表示此谓词的逻辑否定的谓词
闭包
考虑一个函数,x 是 其中的一个入参,i 则是其中的一个局部变量,返回一个 Lambda 表达式
public class Closure {
IntSupplier makeFun(int x) {
int i = 0;
return () -> x + i;
}
}
我们知道,函数的入参的局部变量只在方法的生命周期内有效,正常情况下,当 makeFun(int x)
方法执行完后,x 和 i 就会消失,但它返回的 Lambda 表达式却依然保存着 x 和 i 的值。相当于 makeFun(int x)
返回的 IntSupplier
关住了 x 和 i
另外要注意的一点是:被 Lambda 表达式引用的局部变量必须是 final 或是等同 final 效果的。所谓等同 final,意思是即使你没有明确声明变量是 final,但因变量值没被改变过而实际上有了 final 同等的效果。Java8 默认 Lambda 中的局部变量具有等同 final 效果。
柯里化
柯里化意为:将一个多参数的函数,转换为一系列单参数函数
public class CurryingAndPartials {
// 未柯里化
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化的函数
// a -> b -> a + b,意思是传入参数 a,返回 b -> a + b 的函数
// 由于 Lambda 表达式的闭包特性,b -> a + b 中的 a 是有保存值的
Function<String, Function<String, String>> sum = a -> b -> a + b;
System.out.println(uncurried("Hi ", "Ho"));
Function<String, String> hi = sum.apply("Hi ");
System.out.println(hi.apply("Ho"));
Function<String, String> sumHi = sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
柯里化的目的是通过提供一个参数来创建一个新函数,根据上述的例子,我们可以通过添加级别来柯里化具有更多参数的函数
Java8 新特性 —— 函数式编程的更多相关文章
- Java8新特性--函数式编程
在jdk8中什么是函数式接口: 1.被@FunctionalInterface注解修饰的. 2.接口里边只有一个非default的方法. 满足以上2个条件的即为函数式接口,ps:即使一个接口没有被@F ...
- Java8 新特性----函数式接口,以及和Lambda表达式的关系
这里来讲解一下Java8 新特性中的函数式接口, 以及和Lambda 表达式的关系.看到过很多不少介绍Java8特性的文章,都会介绍到函数式接口和lambda表达式,但是都是分别介绍,没有将两者的关系 ...
- Java8 新特性 函数式接口
什么是函数式接口 函数式接口是Java8引用的一个新特性,是一种特殊的接口:SAM类型的接口(Single Abstract Method).但是它还是一个接口,只是有些特殊罢了. 函数式接口的 ...
- java8新特性-函数式接口详细讲解及案例
一.函数式接口 1.1 概念 函数式接口在Java中是指:有且仅有一个抽象方法的接口.函数式接口,即适用于函数式编程场景的接口.而Java中的函数式编程体现就是Lambda,所以函数式接口就是可 以适 ...
- java8新特性学习:函数式接口
本文概要 什么是函数式接口? 如何定义函数式接口? 常用的函数式接口 函数式接口语法注意事项 总结 1. 什么是函数式接口? 函数式接口其实本质上还是一个接口,但是它是一种特殊的接口:SAM类型的接口 ...
- 乐字节-Java8新特性之方法引用
上一篇小乐介绍了<Java8新特性-函数式接口>,大家可以点击回顾.这篇文章将接着介绍Java8新特性之方法引用. Java8 中引入方法引用新特性,用于简化应用对象方法的调用, 方法引用 ...
- Java8 新特性 Lamdba表达式
Lamdba 表达式为什么出现 Java8是自java延生以来最大的改变,他允许java中的方法和函数成为一等公民(可以在方法间传递),所以就应运而出现了Lamdba表达式,他可以将表达式传递给另 ...
- [译]java8新特性:函数式编程(functional programming)的优点
Java8引入了函数式编程,他对java是一个极大的扩展.Java从此不在是一个单纯的面向对象语言,现在他同时混合了函数式编程.这是巨大的改变,需要我们调整面对对象的编程习惯,以适应这些变化. 但是为 ...
- 乐字节-Java8新特性之函数式接口
上一篇小乐带大家学过 Java8新特性-Lambda表达式,那什么时候可以使用Lambda?通常Lambda表达式是用在函数式接口上使用的.从Java8开始引入了函数式接口,其说明比较简单:函数式接口 ...
随机推荐
- 解决FAT32格式U盘安装Windows 10时的报错(错误代码:0x8007000D)
一.现象描述 使用UltraISO软碟通将 Windows 10 version 1909 刻录到U盘内来安装系统. 从U盘启动安装过程中,报错如下: "Windows 无法打开所需的文件 ...
- c++ 西安交通大学 mooc 第十三周基础练习&第十三周编程作业
做题记录 风影影,景色明明,淡淡云雾中,小鸟轻灵. c++的文件操作已经好玩起来了,不过掌握好控制结构显得更为重要了. 我这也不做啥题目分析了,直接就题干-代码. 总结--留着自己看 1. 流是指从一 ...
- thinkphp6.0.x 反序列化详记(二)
前言 接上文找第二条POP链. 环境配置 同上文 POP链构造 寻找__destruct方法 仍然是寻找__destruct,这次关注AbstractCache.php(/vendor/league/ ...
- protoc-c 安装记录
记录下 protobuf-c 安装过程中的问题. 1) 安装的时候没细看依赖. -- protobuf-c requires a C compiler, a C++ compiler, protob ...
- 【转】Linux-CentOS7设置程序开启自启步骤!
链接:https://blog.csdn.net/wang123459/article/details/79063703
- C++中线程安全单例模式的正确实现方式
为什么说DCLP不是线程安全的 DCLP(Double Checked Locking Pattern),即双检锁模式: class Foo { public: static Foo* getInst ...
- 怎样学习C语言(献给迷茫的C爱好者)!
一 .怎样学习C语言 很多人对学习C语言感到无从下手,经常问我同一个问题:究竟怎样学习C语言?我是一个教师,已经开发了很多年的程序,和很多刚刚起步的人一样,学习的第一个计算机语言就是C语言. 经过这些 ...
- Shell Scripting 笔记
Shell Scripting Tutorial Variables in the Bourne shell do not have to be declared, as they do in lan ...
- zabbix安装中文语言包及中文乱码的解决(zabbix5.0)
一,zabbix不能配置中文界面的问题: 1, zabbix5.0 系统安装后,web界面不能选择使用中文 系统提示: You are not able to choose some of the l ...
- 每天一个linux命令:ps命令
Linux中的ps命令是Process Status的缩写.ps命令用来列出系统中当前运行的那些进程.ps命令列出的是当前那些进程的快照,就是执行ps命令的那个时刻的那些进程,如果想要动态的显示进 ...