文章目录
简介
为什么要使用Lambda?
多核 CPU 的兴起,人们开发了 java.util.concurrent 包和很多第三方类库, 试图将并发抽象化, 帮助程
序员写出在多核 CPU 上运行良好的程序。很可惜, 到目前为止, 我们的成果还远远不够。
开发类库的程序员使用 Java 时, 发现抽象级别还不够。 处理大型数据集合就是个很好的例子, 面对大型数据集合, Java 还欠缺高效的并行操作。 开发者能够使用 Java 8 编写复杂的集合处理算法, 只需要简单修改一个方法, 就能让代码在多核 CPU 上高效运行。 为了编写这类处理批量数据的并行类库, 需要在语言层面上修改现有的 Java: 增加 Lambda 表达式。
面向对象编程是对数据进行抽象, 而函数式编程是对行为进行抽象。
什么是函数式编程?
函数式编程的核心是: 在思考问题时, 使用不可变值和函数, 函数对一个值进行处理, 映射成另一个值。
Lambda 表达式
Java 8 的最大变化是引入了 Lambda 表达式: 一种紧凑的、 传递行为的方式。
- Lambda表达式支持将代码块作为方法参数
设计匿名内部类的目的, 就是为了方便 Java 程序员将代码作为数据传递。不过, 匿名内部类还是不够简便。 为了调用一行重要的逻辑代码, 不得不加上 多行冗繁的样板代码。 - Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口的实例(这种接口被称为 函数式接口)。
在 Lambda 表达式中无需指定类型, 程序依然可以编译。 这是因为 javac 根据程序的上下文( 即方法的签名) ,在后台推断出了参数的类型。 这意味着如果参数类型明确, 则无需显式指定。
Lambda表达式的常用写法
Lambda表达式的主要作用就是代替匿名内部类的繁琐语法,它由有3部分组成:
- 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头(->)。必须使用英文中的画线和大于符号组成。
- 代码块。如果代码块只有一条语句,Lambda表达式允许省略代码块的花括号。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
写法1
Runnable noArguments = () -> System.out.println("Hello world");
该Lambda 表达式不包含参数, 使用空括号 () 表示没有参数。 该 Lambda表达式实现了 Runnable 接口, 该接口也只有一个 run 方法, 没有参数, 且返回类型为 void。
写法2
ActionListener oneArgument = event -> System.out.println("button clicked");
该Lambda 表达式包含且只包含一个参数, 可省略参数的括号, 且返回类型为 void。
写法3
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
Lambda 表达式的主体不仅可以是一个表达式, 而且也可以是一段代码块, 使用大括号( {}) 将代码块括起来, 如写法3所示。 该代码块和普通方法遵循的规则别无二致, 可以用返回或抛出异常来退出。 只有一行代码的 Lambda 表达式也可使用大括号, 用以明确 Lambda表达式从何处开始、 到哪里结束。
写法4
BinaryOperator<Long> add = (x, y) -> x + y;
Lambda 表达式也可以表示包含多个参数的方法, 如写法4所示。 这时就有必要思考怎样去阅读该 Lambda 表达式。 这行代码并不是将两个数字相加, 而是创建了一个函数, 用来计算两个数字相加的结果。 变量 add 的类型是 BinaryOperator, 它不是两个数字的和,而是将两个数字相加的那行代码。
写法5
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
到目前为止, 所有 Lambda 表达式中的参数类型都是由编译器推断得出的。 这当然不错,但有时最好也可以显式声明参数类型, 此时就需要使用小括号将参数括起来, 多个参数的情况也是如此。 如写法5所示。
目标类型
是指 Lambda 表达式所在上下文环境的类型。 比如, 将 Lambda 表达式赋值给一个局部变量, 或传递给一个方法作为参数, 局部变量或方法参数的类型就是 Lambda 表达式的目标类型。
Lambda表达式和匿名内部类的联系和区别
关于匿名内部类,可以看下面的文章
Lambda表达式是匿名内部类的一种简化。
Lambda表达式和匿名内部类相同点:
- Lambda表达式和匿名内部类一样,都可以直接访问”effectively final”的
局部变量
(换句话说, Lambda 表达式引用的是值,而不是变量),以及外部类的成员变量(包括实例变量和类变量) - Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
例子:
public class LambdaAndInner {
private int age = 20;
private static String name = "Tom";
public void test() {
String teacher = "Jerry";
Displayable displayable = () -> {
// 访问"effectively final"的局部变量
System.out.println("teacher: " + teacher);
// 外部类的成员变量(包括实例变量和类变量)
System.out.println("外部类age:" + age);
System.out.println("外部类name:" + name);
// Lambda表达式的代码块不允许调用接口中定义默认方法
// 编译错误
// System.out.println(add(2, 3));
};
displayable.display();
System.out.println(displayable.add(1, 2));
}
public static void main(String[] args) {
new LambdaAndInner().test();
}
}
interface Displayable {
void display();
default int add(int a, int b) {
return a + b;
}
}
输出:
teacher: Jerry
外部类age:20
外部类name:Tom
3
Lambda表达式和匿名内部类的区别:
- 匿名内部类可以为任意接口创建实例,不管接口中包含多少个抽象方法,只要在匿名内部类中实现所有抽象方法即可。但Lambda表达式只能为函数式接口创建实例。
- 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
- 匿名内部类实现的抽象方法可以允许调用接口中定义默认方法。但Lambda表达式的代码块不允许调用接口中定义的默认方法。
函数式接口
Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口 (functional interface)”。
函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
使用只有一个方法的接口来表示某特定方法并反复使用, 是Java中很早就有的习惯。
如:ActionListener 接口: 只有一个抽象方法: actionPerformed, 被用来表示行为,接受 ActionEvent 类型的参数, 返回空。
Java8专门为函数式接口提供了@FunctionalInterface
注解,该注解通常放在接口定义的前面,该注解对程序功能没有任何作用,它告诉编译器执行更严格的检查 — 检查该接口必须是函数式接口,否则编译器就会报错。
Lambda表达式实现的是匿名方法—-因此它只能实现特定函数式接口的唯一方法。这就意味着Lambda表达式有如下两个限制:
- Lambda表达式的目标类型必须是明确的函数式接口。
- Lambda表达式只能为函数式接口创建实例。
对上面第一点详细解释,为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种方式:
- 将Lambda表达式赋值给函数式接口类型的变量。
- 将Lambda表达式作为函数式接口类型的参数传给某个方法。
- 使用函数式接口对Lambda表达式进行强制类型转换。
具体的看下面的案例:
public static void main(String[] args) {
// Runnable 是Java本身提供的一个函数式接口。
// 1. 将Lambda表达式赋值给函数式接口类型的变量
Runnable r = () -> {
System.out.println("Thread1");
};
// 2. 将Lambda表达式作为函数式接口类型的参数传给某个方法。
ThreadLocal<DateFormatter> formatter = ThreadLocal.withInitial(() -> new DateFormatter(new SimpleDateFormat("dd-MMM-yyyy")));
// 3. 使用函数式接口对Lambda表达式进行强制类型转换。.
Object r2 = (Runnable) () -> {
System.out.println("Thread2");
};
}
使用 Java 编程, 总会遇到很多函数式接口, Java 8在java.util.function
包下预定义了大量函数式接口。 下表罗列了一些最重要的函数接口:
接口 | 参数 | 返回类型 | 示例 |
---|---|---|---|
Predicate<T> | T | boolean | 这场球赛结束了吗 |
Consumer<T> | T | void | 输出一个值 |
Function<T,R> | T | R | 获得 Artist 对象的名字 |
Supplier<T> | None | T | 工厂方法 |
UnaryOperator<T> | T | T | 逻辑非( !) |
BinaryOperator<T> | (T, T) | T | 求两个数的乘积( *) |
其他典型的如以下4类接口:
接口 | 说明 |
---|---|
XxxFunction | 通常包含一个apply()抽象方法,该方法对参数进行处理、转换(apply()方法的实现逻辑由Lambda表达式来实现),然后返回一个新的值。该函数式接口通常用于对指定数据进行转换处理。 |
XxxConsumer | 通常包含一个accept()抽象方法,该方法与XxxFunction接口中的apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果。 |
XxxPredicate | 通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断(test()方法的判断逻辑由Lambda表达式来实现),然后返回一个boolean值。该函数式接口通常用于判断参数是否满足特定条件,经常用于进行筛滤数据。 |
XxxSupplier | 通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,该方法会按某种逻辑算法(getAsXxx()方法的判断逻辑由Lambda表达式来实现)返回一个数据。 |
方法引用与构造器引用
前面有介绍到:
如果代码块只有一条语句,Lambda表达式允许省略代码块的花括号。
Lambda代码块只有一条return语句,甚至可以省略return关键字。
Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用与构造器引用。
方法引用与构造器引用,让Lambda表达式的代码块更加简洁。
方法引用与构造器引用都需要使用两个英文冒号
。
Lambda表达式支持如下所示的几种引用方式:
种类 | 示例 | 说明 | 对应的Lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法 | 函数式接口中被实现方法的全部参数传给该类方法作为参数 | (a,b,…) -> 类名.类方法(a,b,…) |
引用特定对象的实例方法 | 特定对象::实例方法 | 函数式接口中被实现方法的全部参数传给该方法作为参数 | (a,b,…) -> 特定对象.实例方法(a,b,…) |
引用某类对象的实例方法 | 类名::实例方法 | 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 | (a,b,…) -> a.实例方法(b,…) |
引用构造器 | 类名::new | 函数式接口中被实现方法的全部参数传给该构造器作为参数 | (a,b,…) -> new 类名(a,b,…) |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/155715.html