Java8学习(一)Lambda表达式

Java8已经出来很长一段时间了,它是Java5之后变化最大的版本,不过我也是一直没去学习。它是2014年发布的,现在已经是2017年中旬了,如果现在不加紧时间把它给补上,那么肯定是跟不上时代的步伐的,也正如以前那样,不把Java5/6给吃透,那肯定连工作都难找,并且今年Java9都要出来了,时间可不等人。

什么是Lambda表达式?

Lambda表达式不止存在于Java中,有些语言早就有了该特性,Java加入该特性还算是比较晚的了。

广义定义

  • WIKIPEDIA上给出的解释

    lambda expression in computer programming, also called anonymous function,
    a function (or a subroutine) defined, and possibly called, without being bound to an identifier

  • 渣翻

    在计算机编程中,lambda表达式也被称作匿名函数,它是一个可能会被调用且没有被绑定在标识符上的函数或子程序

通俗来讲,一个普通函数就是被指定名字的一段代码,当不给这段代码指定名字,那就成了没名字的函数(匿名函数),更可称它为Lambda表达式。

Lambda表达式语法

Java中的Lambda表达式就像普通的方法一样拥有这几个关键元素:形参列表、方法体、返回类型以及异常列表这4个关键元素
既然Lambda表达式是Java8加入的新特性,那么它肯定也是用Java以前从未出现过的语法来构造。

广义语法

LambdaParameters -> LambdaBody
Lambda表达式由中间的一个箭头->(还以为是在操作C/C++的指针)分割成两部分,箭头左边是Lambda参数,箭头右边是Lambda体

Lambda参数

Lambda表达式的参数对应的类型可以显式地声明,也可以靠编译器根据上下文隐式地推断:

  1. 带括号的参数列表(parameter-list) -> LambdaBody
    无参数、有多个参数或有且仅有1个参数且其类型是显式声明的时候,参数列表的括号是一定要有的

  2. 不带括号的参数列表single-parameter-identify -> LambdaBody
    只有1个参数并且其类型是根据上下文推断的时候,才可以把参数列表的括号给省略掉

Lambda体

  1. 带大括号的Lambda体LambdaParameters -> {statement(s)}
    Lambda体有1条或多条语句,就像普通方法的方法体一样,需要一对大括号将它们包裹起来,它被称为块(block)

  2. 不带大括号的Lambda体LambdaParameters -> expression or single-statement
    Lambda体有且仅有1条表达式或语句,大括号就可以省略掉

Lambda表达式的返回值

Lambda表达式的返回类型取决于Lambda体中所执行操作:

  • 如果Lambda体中执行1+1,则返回类型是int
  • 如果调用Integer.toString(1),则返回类型是String
  • 如果只是调用打印语句,则返回类型为void
    其他情况以此类推。

Lambda表达式具体示例

当你看到箭头->这个非常显眼的标志,这说明此处正在使用Lambda表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 无参数且什么都不做
() -> {}

// 接受一个显式声明为int的参数, 并且返回该参数+1的整数值
(int a) -> { return a + 1; }

// 这个和上面那个形式上是等价的,但此处返回的是该参数+3的整数值,并且省去了块的大括号
(int a) -> a + 3

// 对上面的表达式进行简化,省去显式声明的参数类型
(a) -> a + 3

// 上面那个Lambda表达式可以进一步简化成这样,连括号都省去
a -> a + 3

// 无参数并且返回一个整数
() -> 17

// 接受两个参数, 两参数的类型都是显式声明, 一个是为String, 一个是int, 并执行1个输出语句将参数输出
(String name, int age) -> System.out.println("your name is " + name + " and your age is " + age)

// 接受1个参数, 该参数的类型是根据上下文推断的String,且返回类型也需要根据上下文进行推断
str -> str.length()

// 接受1个参数, 该参数的类型是根据上下文推断的int
(num) -> {
if (num >= 90) return "A";
else if (num >= 80 && num < 90) return "B";
else if (num >= 70 && num < 80) return "C";
else return "D";
}

// 接受两个参数, 两个参数类型都是根据上下文推断出来的
(numA, numB) -> numA + numB

这些形式的Lambda表达式都是很常见的,多看几眼多用几次什么都熟悉了。

写法建议和总结

  1. 对于Lambda参数的参数类型,有些情况显式地声明参数类型会让代码更清晰,有些情况靠隐式推断反而更加简洁易读,这点全凭经验,用得多自然就有感觉了。
  2. 对于Lambda体,有且仅有1条表达式或语句,那就把大括号省略掉吧,只有多条语句的时候才用大括号。

如何运用Lambda表达式?

现在,我们都非常清楚Lambda表达式到底长什么样了,紧接着要搞清楚的是:到底在哪里可以使用Lambda表达式

函数式接口

我们都清楚在Java中,一切皆对象,凡对象都有其所属的类型。更具体地说,表达式也是对象,同样也有所属的类型。在任意地方声明此处可接纳什么类型的对象,那么此处也同样可以接纳一个该类型的表达式。这对于Lambda表达式也是如此的,虽然Lambda表达式是一种特殊的表达式,不过,它在Java中也必定有其所属的类型,不然我们就无法得知在什么场合可以使用Lambda表达式。

所以,Java8里新增了一种接口类型——函数式接口作为Lambda表达式的所属类型。函数式接口类型就是Lambda表达式的类型!

定义

  • 在The Java Language Specication SE8 - CHAPTER 9 Interfaces - 9.8 Functional Interfaces

    A functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract.

  • 渣翻

    函数式接口是一个只有一个抽象方法(除了Object的方法外)的接口,因此它代表一个单一的函数契约。

  • 更通俗更直白来说

    函数式接口(functional interface)指的是有且仅有1个抽象方法的接口,它代表一个单一功能点。

注解FunctionalInterface

为了让真正的函数式接口有别于普通接口,Java里最标准的做法就是对该接口使用注解@FunctionalInterface进行标注。

@FunctionalInterface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* An informative annotation type used to indicate that an interface
* type declaration is intended to be a <i>functional interface</i> as
* defined by the Java Language Specification.
*
* ...
*
* @jls 4.3.2. The Class Object
* @jls 9.8 Functional Interfaces
* @jls 9.4.3 Interface Method Body
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

定义函数式接口

1
2
3
4
@FunctionalInterface
public interface FunctionalInterfaceName {
void onlyOneAbstractMethod();
}

如上面所示,这就定义了一个函数式接口,就像是定义普通接口一样,只不过多了两条规矩:

  1. 里面只能定义一个抽象方法
  2. @FunctionalInterface注解标注该接口

注意,这里第1条规矩是必要的,第2条是可有可无的,它就像是平时我们重写方法的时候在方法加@Override注解一样。

总结

  • 函数式接口是内部只有一个抽象方法的接口
  • 函数式接口是Java中特殊的接口类型,只有它才能结合Lambda表达式一起食用,其余情况是不能结合Lambda表达式一起食用的。

上下文与类型兼容

好了,现在我们知道在函数式接口存在的地方使用Lambda表达式,但这还不够,我们还必须深入了解一下它们俩之间是如何配合工作的。

Lambda表达式是特殊的表达式,更进一步地说它是特殊的对象,而函数式接口是一种特殊的接口类型,所以它们之间类型兼容规则应该也是特殊的。
更具体更实在来说,这探讨的情况之一就是某对象能否直接赋值给某类型的问题
比如基本数值类型赋值,这取决于基本数值类型之间的大小关系;又比如引用类型赋值,这取决于引用类型之间的继承关系;又比如引用类型中的参数化类型赋值,这取决于参数化类型之间类型实参的关系。所以,我们不能把经常使用的基本类型以及引用类型的类型兼容规则强行套用到Lambda表达式与函数式接口上,类型体系不同,那自然所用的类型兼容规则也不同

官方定义

想要知道Lambda表达式与函数式接口之间的类型兼容规则是怎么样的,不妨先来看看官方文档《The Java Language Specification SE8》里的定义:

  • CHAPTER 15 - 15.27 Lambda Expressions - 15.27.3 Type of a Lambda Expression

    A lambda expression is compatible in an assignment context, invocation context, or casting context with a target type T
    if T is a functional interface type (§9.8) and the expression is congruent with the function type of the ground target type derived from T.

  • 渣翻(这里ground target type的真正含义是什么我暂时不知道,英语水平不行不会翻译了)

    如果T是函数式接口类型并且Lambda表达式符合从T派生而来的子(暂时只能理解成这样)目标类型的函数类型,
    那么Lambda表达式就可以兼容于带有目标类型T的赋值、方法调用、类型转换上下文中。

    而在里面,又提及了一个新的概念:函数类型function type,不知道是什么意思?那可以继续看官方文档的定义。

  • CHAPTER 9 - 9.9 Function Types

    The function type of a functional interface I is a method type (§8.2) that can be used to override (§8.4.8) the abstract method(s) of I.
    The function type of I consists of the following:

    • Type parameters, formal parameters, and return type
    • throws clause
  • 渣翻

    函数式接口I的函数类型是一个可以被用于重写I的抽象方法的方法类型。

    然而新概念又来了,方法类型method type,我们不懂那就继续顺着章节提示去查看官方文档的定义。

  • CHAPTER 8 - 8.2 Class Members

    We use the phrase the type of a member to denote:
    • For a field, its type.
    • For a method, an ordered 4-tuple consisting of:
      • type parameters: the declarations of any type parameters of the method member.
      • argument types: a list of the types of the arguments to the method member.
      • return type: the return type of the method member.
      • throws clause: exception types declared in the throws clause of the method member.
  • 渣翻

    我们使用这些概念惯用语来指代成员的类型

    • 对于字段,就是它的类型
    • 对于方法,它的类型由一个规则的4元组组成:
      • 类型参数列表:方法成员中任何类型参数的声明
      • 参数类型列表:方法成员的参数类型列表
      • 返回类型:方法成员的返回类型
      • 异常列表:声明在方法成员的异常抛出子句中的异常类型

概念很多,定义也很严格,有时候太严格的东西反而不易于理解和学习,或许你(我)现在不能100%理解它们,但从文字分析上还是能看出一些端倪。

实际结论

结合前面所列举的所有概念的文字含义,这起码可以让我推断出一个算是准确的结论:

如果Lambda表达式对应的函数类型与上下文中的函数式接口T内的抽象方法的函数类型相匹配,那么Lambda表达式就可以兼容于函数式接口类型T。

更具体来说就是如果函数式接口以及Lambda表达式它们两者对应的函数类型相匹配,那么互相兼容最常见的情况之一就是Lambda表达式可以直接赋值给函数式接口

从上面的结论来看,理解函数类型如何相匹配是理解Lambda表达式与函数式接口如何兼容的关键点,但我们首先得充分地了解一下函数类型。

函数类型

函数类型(function type)类型参数列表参数类型列表返回类型异常列表这4个元素所组成,固定的4个元素所组成的函数类型就可以唯一地标识某一类函数
其实它就像C/C++中的函数指针函数对象,但最大的区别在于前者在Java中只是概念(暂时是这样的,以后或许会有这个特性也说不定),而后者是可以切实将该特性在代码上写出来的。

注意:在《Java 8 实战》(《Java 8 In Action》)一书中,作者用函数描述符(function descriptor)这一个概念名词进行代替,其实它的定义也等同于官方文档函数类型这一概念名词的定义。

首先,先来学习如何表示函数类型,根据官方文档所举的例子,函数类型写法的其实就是写出组成它的4个元素,通用格式如下:

1
2
3
[<type-parameter-list>] (parameter-type-list) -> return-type [throws exception-list]

[<类型参数列表>] (参数类型列表) -> 返回类型 [throws 异常列表]

一个函数类型,参数类型列表以及返回类型都是必须要写出来,而其余的两个元素则是根据情况而定。下面先来看看各种函数式接口它所对应的函数类型的具体表示。

这个函数类型非常简单,这是因为Runnnable中的run方法简单,它的形参列表为空,并且没有返回值。

1
2
3
4
5
6
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

() -> void

再看一个稍微复杂一点的:BiFunction<T, U, R>,它的函数类型就复杂很多了,里头的apply方法接受2个类型分别为TU的参数以及返回R

1
2
3
4
5
6
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}

(T, U) -> R

Callable<V>的函数类型就有异常列表了。

1
2
3
4
5
6
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

() -> V throws Exception

Java内置的函数式接口

  Java内置的大部分函数式接口可以在java.util.function包内找到,还有几个最常用函数式接口零散地分布在其他包中。其实一般情况下,除非有特殊需求,Java内置的那一整套函数式接口已经非常够用了。

下图所示的就是java.util.function包含的所有函数式接口:

根据命名可以将它们分为5大类:Predicate Consumer Supplier Operator Function

再者,以下是分布于其他包中几个最常用的函数式接口:

其中,Comparator在平常的开发中是用得最多的。

Predicate