1 Lambda表达式
Lambda 表达式是在Java 8中引入的,并且成为了Java 8最大的特点。它使得功能性编程变得非常便利,极大地简化了开发工作。
1.1 语法
一个Lambda表达式具有下面这样的语法特征。它由三个部分组成:第一部分为一个括号内用逗号分隔的参数列表,参数即函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下:
parameter -> expression body
下面列举了Lambda表达式的几个最重要的特征:
- 可选的类型声明:你不用去声明参数的类型。编译器可以从参数的值来推断它是什么类型。
- 可选的参数周围的括号:你可以不用在括号内声明单个参数。但是对于很多参数的情况,括号是必需的。
- 可选的大括号:如果表达式体里面只有一个语句,那么你不必用大括号括起来。
- 可选的返回关键字:如果表达式体只有单个表达式用于值的返回,那么编译器会自动完成这一步。若要指示表达式来返回某个值,则需要使用大括号。
语言的设计者们思考了很多如何让现有的功能和lambda表达式友好兼容。于是就有了函数接口这个概念。函数接口是一种只有一个方法的接口,函数接口可以隐式地转换成 Lambda 表达式。
函数式接口的重要属性是:我们能够使用 Lambda 实例化它们,Lambda 表达式让你能够将函数作为方法参数,或者将代码作为数据对待。Lambda 表达式的引入给开发者带来了不少优点:在 Java 8 之前,匿名内部类,监听器和事件处理器的使用都显得很冗长,代码可读性很差,Lambda 表达式的应用则使代码变得更加紧凑,可读性增强;Lambda 表达式使并行操作大集合变得很方便,可以充分发挥多核 CPU 的优势,更易于为多核处理器编写代码。引用自IBM - Java 8 新特性概述。
1.2 一个Lambda表达式的例子
下面尝试写一些代码来理解Lambda表达式。请在NewFeaturesTester.java中输入下面这些代码,对于它们的解释在注释中给出。
public class NewFeaturesTester {
public static void main(String args[]){
NewFeaturesTester tester = new NewFeaturesTester();
// 带有类型声明的表达式
MathOperation addition = (int a, int b) -> a + b;
// 没有类型声明的表达式
MathOperation subtraction = (a, b) -> a - b;
// 带有大括号、带有返回语句的表达式
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 没有大括号和return语句的表达式
MathOperation division = (int a, int b) -> a / b;
// 输出结果
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 没有括号的表达式
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 有括号的表达式
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
// 调用sayMessage方法输出结果
greetService1.sayMessage("Shiyanlou");
greetService2.sayMessage("Classmate");
}
// 下面是定义的一些接口和方法
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
运行结果如下图所示:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
hello
你好
需要注意的是:
- Lambda表达式优先用于定义功能接口在行内的实现,即单个方法只有一个接口。在上面的例子中,我们用了多个类型的Lambda表达式来定义MathOperation接口的操作方法。然后我们定义了GreetingService的sayMessage的实现。
- Lambda表达式让匿名类不再需要,这为Java增添了简洁但实用的函数式编程能力。
1.3 作用域
我们可以通过下面这段代码来学习Lambda的作用域。请将代码修改至如下这些:
public class NewFeaturesTester {
final static String salutation = "Hello "; //正确,不可再次赋值
//static String salutation = "Hello "; //正确,可再次赋值
//String salutation = "Hello "; //报错
//final String salutation = "Hello "; //报错
public static void main(String args[]){
//final salutation = "Hello "; //正确,不可再次赋值
//String salutation = "Hello "; //正确,隐性为 final , 不可再次赋值
// salution = "welcome to "
GreetingService greetService1 = message ->
System.out.println(salutation + message);
greetService1.sayMessage("Shiyanlou");
}
interface GreetingService {
void sayMessage(String message);
}
}
多次试验运行可以得到以下结论:
- 可访问 static 修饰的成员变量,如果是 final static 修饰,不可再次赋值,只有 static 修饰可再次赋值;
- 可访问表达式外层的 final 局部变量(不用声明为 final,隐性具有 final 语义),不可再次赋值。
1.4 方法引用
Java 8中方法也是一种对象,可以通过名字来引用。不过方法引用的唯一用途是支持Lambda的简写,使用方法名称来表示Lambda。不能通过方法引用来获得诸如方法签名的相关信息。引用自永无止境,上下求索的博客。
方法引用提供了一个很有用的语义来直接访问类或者实例的已经存在的方法或者构造方法。
方法引用可以通过方法的名字来引用其本身。方法引用是通过::符号(双冒号)来描述的。
它可以用来引用下列类型的方法:
- 构造器引用。语法是Class::new,或者更一般的Class< T >::new,要求构造器方法是没有参数;
- 静态方法引用。语法是Class::static_method,要求接受一个Class类型的参数;
- 特定类的任意对象方法引用。它的语法是Class::method。要求方法是没有参数的;
- 特定对象的方法引用,它的语法是instance::method。要求方法接受一个参数,与3不同的地方在于,3是在列表元素上分别调用方法,而4是在某个对象上调用方法,将列表元素作为参数传入;
更多对于方法引用的介绍,可以参考这一篇博文——java8 - 方法引用(method referrance)
下面是一个简单的方法引用的例子。
public class NewFeaturesTester {
public static void main(String args[]){
List names = new ArrayList();
names.add("Peter");
names.add("Linda");
names.add("Smith");
names.add("Zack");
names.add("Bob");
// 通过System.out::println引用了输出的方法
names.forEach(System.out::println);
}
}
2 函数式接口
函数式接口通过一个单一的功能来表现。例如,带有单个compareTo方法的比较接口,被用于比较的场合。Java 8 定义了大量的函数式接口来广泛地用于lambda表达式。
Java 8 引入的一个核心概念是函数式接口(Functional Interfaces)。通过在接口里面添加一个抽象方法,这些方法可以直接从接口中运行。如果一个接口定义唯一一个抽象方法,那么这个接口就成为函数式接口。同时,引入了一个新的注解:@FunctionalInterface。可以把他它放在一个接口前,表示这个接口是一个函数式接口。这个注解是非必须的,只要接口只包含一个方法的接口,虚拟机会自动判断,不过最好在接口上使用注解 @FunctionalInterface 进行声明。在接口中添加了 @FunctionalInterface 的接口,只允许有一个抽象方法,否则编译器也会报错。引用自IBM - Java 8 新特性概述。相关的接口及描述
下面是部分函数式接口的列表。
- BiConsumer<T,U>
该接口代表了接收两个输入参数T、U,并且没有返回的操作 - BiFunction<T,U,R>
该接口代表提供接收两个参数T、U,并且产生一个结果R的方法 - BinaryOperator
代表了基于两个相同类型的操作数,产生仍然是相同类型结果的操作 - BiPredicate<T,U>
代表了对两个参数的断言操作(基于Boolean值的方法) - BooleanSupplier
代表了一个给出Boolean值结果的方法 - Consumer
代表了接受单一输入参数并且没有返回值的操作 - DoubleBinaryOperator
代表了基于两个Double类型操作数的操作,并且返回一个Double类型的返回值 - DoubleConsumer
代表了一个接受单个Double类型的参数并且没有返回的操作 - DoubleFunction
代表了一个接受Double类型参数并且返回结果的方法 - DoublePredicate
代表了对一个Double类型的参数的断言操作 - DoubleSupplier
代表了一个给出Double类型值的方法 - DoubleToIntFunction
代表了接受单个Double类型参数但返回Int类型结果的方法 - DoubleToLongFunction
代表了接受单个Double类型参数但返回Long类型结果的方法 - DoubleUnaryOperator
代表了基于单个Double类型操作数且产生Double类型结果的操作 - Function<T,R>
代表了接受一个参数并且产生一个结果的方法 - IntBinaryOperator
代表了对两个Int类型操作数的操作,并且产生一个Int类型的结果 - IntConsumer
代表了接受单个Int类型参数的操作,没有返回结果 - IntFunction
代表了接受Int类型参数并且给出返回值的方法 - IntPredicate
代表了对单个Int类型参数的断言操作
更多的接口可以参考Java 8官方API手册:java.lang.Annotation Type FunctionalInterface。在实际使用过程中,加有@FunctionalInterface注解的方法均是此类接口,位于java.util.Funtion包中。
函数接口使用的例子
public class NewFeaturesTester {
public static void main(String args[]){
List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println("All of the numbers:");
eval(list, n->true);
System.out.println("Even numbers:");
eval(list, n-> n%2 == 0 );
System.out.println("Numbers that greater than 5:");
eval(list, n -> n > 5 );
}
public static void eval(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.println(n);
}
}
}
}
上面eval函数中还可以简化为stream+lambda表达式的形式:
list.forEach(n -> {if(predicate.test(n))System.out.println(n);});
3. 接口默认方法
Java 8在接口方面引入了一个关于默认方法实现的新概念。它也是作为一种向后兼容能力而出现,旧的接口也能用到Lambda表达式中。例如,List或Collection接口是没有forEach方法的声明的。但是,通过这些默认方法能够就能轻易地打破集合框架实现的限制。Java 8引入默认方式使得List和Collection接口能够拥有forEach方法的默认实现。实现了这些接口的类也不必再实现相同的功能了。
接口默认方法语法如下:
public interface boy {
default void print(){
System.out.println("I am a boy.");
}
}
多个默认方法
接口中有了默认方法之后,在同一个类里面实现两个带有相同默认方法的接口就可行了。
下面的代码演示了如何解决这种含糊不清的情况。
首先是同一个类里面的两个接口。
public interface younger {
default void print(){
System.out.println("I am a younger.");
}
}
public interface learner{
default void print(){
System.out.println("I am a learner.");
}
}
第一个解决办法就是创建一个自有的方法,来重写默认的实现。就像这样:
public class student implements younger, learner {
public void print(){
System.out.println("I am a younger and a learner, so I am a student.");
}
}
另外一个解决办法是使用超类super来调用特定接口的默认方法。
public class student implements younger, learner {
public void print(){
learner.super.print();
}
}
静态默认方法
你也可以为这个接口增加静态的辅助方法(helper),就像下面这样:
public interface Younger {
default void print(){
System.out.println("I am a younger.");
}
static void sayHi(){
System.out.println("Young is the capital.");
}
}
一个默认方法的例子
下面我们通过一个例子来掌握如何使用默认方法。请将代码修改为下面的内容。对应知识点已在注释中写明。
public class NewFeaturesTester {
public static void main(String args[]) {
Younger younger = new Student();
younger.print();
}
}
interface Younger {
default void print() {
System.out.println("I am a younger.");
}
static void sayHi() {
System.out.println("Young is the capital.");
}
}
interface Learner {
default void print() {
System.out.println("I am a learner.");
}
}
class Student implements Younger, Learner {
public void print() {
Younger.super.print();
Learner.super.print();
Younger.sayHi();
System.out.println("I am a student!");
}
}
编辑完成后,编译运行一下。
最全的Java 8新功能说明位于Java的官方网站,你可以通过阅读《What’s New in JDK 8》来了解它们。