Java8函数式编程_2--Lambda表达式

1,免责声明,本文大部分内容摘自《Java8函数式编程》。在这本书的基础上,根据自己的理解和网上一些博文,精简或者修改。本次分享的内容,只用于技术分享,不作为任何商业用途。当然这本书是非常值得一读,强烈建议买一本!
2,本次分享的样例代码均上传到github上,请点击这里

第2章 Lambda表达式

主要内容如下:

  • 2.1 第一个Lambda表达式
  • 2.2 如何识别Lambda表达式
  • 2.3 引用的是值,而不是变量
  • 2.4 函数接口
  • 2.5 类型推导
  • 2.6 要点回顾

2.1 第一个Lambda表达式

Android Button 点击监听器语法如下:

// 例2-1 使用匿名内部类将行为和按钮单击进行关联
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Log.v("View", "button clicked!");
    }
});

在这个例子中,我们创建一个新对象,它实现了View.OnClickListener接口。这个接口只有一个方法onClick(),当用户点击屏幕上的button,button就会被调用这个方法。匿名内部类实现了该方法,这实际上是一个代码即数据的例子 —— 我们给button传递了一个代表某种行为的对象。

设计匿名内部类的目的,就是为了方便Java程序员将代码作为数据传递。不过,匿名内部类还是不够简便。为了调用一行重要的逻辑代码,不得不加上4行冗繁的样板代码。

尽管如此,样板代码并不是唯一的问题:这些代码还相当难读,因为它没有清楚地表达程序员的意图。我们不想传入对象,只想传入行为。在Java8中,上述代码可以写成一个Lambda表达式,如下:

// 例2-2 使用 Lambda 表达式将行为和按钮单击进行关联
button.setOnClickListener(v -> Log.v("View", "button clicked!"));

和传入一个实现某种接口的对象不同,我们传入了一段代码块——一个没有名字的函数。v是参数名,和上面匿名内部类示例中的是同一个参数。->将参数和Lambda表达式的主体分开,而主体是用户点击按钮时会运行的一些代码。

和使用匿名内部类另一处不同在于声明view参数的方式。使用匿名内部类时需要显示地声明参数类型View,而在Lambda表达式中无需指定类型,程序依然可以编译。这是因为javac根据程序的上下文(setOnClickListener方法的签名)在后台推断出了参数View的类型。这意味着如果参数类型不言明,则无需显示指定。为了增加可读性并能够迁就我们的习惯,声明参数也可以包含类型信息,因为有时候编译器不一定能根据上下文推断出参数类型。

2.2 如何识别Lambda表达式

Lambda表达式除了基本的形式之外,还有几种变体:

  • (params) -> expression
  • (params) -> statement
  • (params) -> { statements }

具体例子如下:

//2-3 编写 Lambda 表达式的不同形式

// 方式一,不包含参数,使用空括号()表示没有参数。
Runnable noArguments = () -> System.out.println("Hello World!");

// 方式二,包含且只包含一个参数,可省略参数的括号。
View.OnClickListener clickListener = view -> System.out.println("Button Click!");

// 方式三,Lambda 表达式可以包含多个参数的方法。
BinaryOperator<Long> add = (x, y) -> x + y;

// 方式四,与方式三类似,只不过指定了参数类型,如Long。
BinaryOperator<Long> add = (Long x, Long y) -> x + y;

// 方式五,Lambda 表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号{}将代码块括起来。
BinaryOperator<Long> add2 = (x, y) -> {
    System.out.println("Hello BinaryOperator!");
    return x + y;
};

方式一,Lambda表达式不包含参数,使用空括号()表示没有参数。此时小括号不可以省略。该Lambda表达式实现了Runnable接口,该接口也只有一个run方法,没有参数,且放回类型为void。

方式二,Lambda表达式包含且只包含一个参数,可省略参数的小括号。

方式三,Lambda表达式可以包含多个参数的方法,此时需要小括号将参数括起来。这时就有必要考虑怎样去阅读该Lambda表达式。这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。变量add的类型BinaryOperator,它不是两个数字的和,而是将两个数字相加的那行代码。

方式四,Lambda表达式参数类型由编译器推断出来,这当然不错,但有时建议显示声明参数类型。

方式五,Lambda表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号({})将代码块括起来。该代码块和普通方法遵循的规则一模一样,可以用返回或抛出异常退出。只有一行代码的Lambda表达式也可使用大括号,用以证明Lambda表达式从何时开始、到哪里结束。


目标类型:是指Lambda表达式所在上下文环境的类型。比如:将Lambda表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是Lambda表达式的目标类型。下面的 2.4章节会具体介绍

Lambda表达式的类型依赖于上下文环境,是由编译器推断出来的。 目标类型也不是一个全新的概念。如例 2-4 所示,Java中初始化数组,数据类型就是根据上下文推断出来的。或者null,只有将null赋值给一个变量,才能知道它的类型。如例2-4:

// 例 2-4 等号右边的代码并没有声明类型,系统根据上下文推断出类型
final String[] array = {"hello", "world"};
String str = null;

2.3 引用的是值,而不是变量

在Java匿名内部类的方法里使用外部的变量。这时,需要将变量声明为final(JDK8 之前是强制的)。将变量声明为final,意味着不能为其重复赋值,即:该变量是一个特定的值。

// 例2-5 匿名内部类中使用 final 局部变量
final String world = "Hello world!";
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Log.v("View", "button clicked! say " + world);
    }
});

Java8 放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是 final 的。虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量,否则编译器就会报错。

既成事实上的final是指只能给该变量赋值一次。换句话说,Lambda表达式引用的是值,而不是变量。 下例number就是一个既成事实上的final变量。

//2-6 Lambda 表达式中引用既成事实上的 final 变量
int number = 3;
new Thread(() -> 
    System.out.println("I am " + number)
).start();

final 有时省去之后代码更易读,但有些情况下,显式地使用final代码更易懂。是否使用final?完全取决于个人喜好。

如果你试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器提示出错信息:

local variables referenced from a Lambda expression must be final or effectively final。

//2-7 未使用既成事实上的 final 变量,导致无法通过编译
int number = 3;
number = 4; //重新再赋值,不应该在Lambda中使用

new Thread(() ->
    System.out.println("I am " + number)
).start();

这种行为也解释了为什么Lambda表达式也被称为闭包。

2.4 函数接口

函数接口:是只有一个抽象方法的接口,用作Lambda表达式的类型。你也可以理解为对一段行为的抽象,简单点说就是将一段行为方法的参数进行传递,这个行为呢,可以是一段代码,也可以是一个方法。你可以想象在 java8 之前要将一段方法作为参数传递只能通过匿名内部类来实现,而且代码很难看,也很长,函数接口就是对匿名内部类的优化。

在Java里,所有方法参数都有固定的类型。假设将字符串 “hello world” 作为参数传给一个方法,则参数的类型是String。那么,Lambda表达式的类型又是什么呢?

使用只有一个方法的接口来表示某特定方法并反复使用,是很早就有的习惯。编写Android界面交互的同学对这种方式都不陌生,如点击监听事件,如下例。Lambda表达式也使用同样的技巧,并将这种接口称为函数接口

// 例 2-8 OnClickListener 接口:接受 View 类型的参数,返回空
// OnClickListener接口:接收一个View类型的参数,放回空
public interface OnClickListener {
    void onClick(View v);
}

OnClickListener 只有一个抽象方法: onClick,被用来表示行为,即:接受一个参数,返回空。这就是函数接口,接口中这个方法的命名并不重要(如:onClick()),只要方法签名和Lambda表达式的类型匹配即可 (形参、返回值、抛出的异常)。在函数接口中为参数起一个有意义的名字,增加代码可读性。

这里的函数接口接受一个View类型的参数,返回空(void),但函数接口还可有其它形式。例如,函数接口可以接受两个参数,并返回一个值,还可以使用泛型,这完全取决于你要干什么(后面章节会深入介绍)。

后面将会使用图形来表示不同类型的函数接口。指向函数接口的箭头表示参数,如果箭头从函数接口射出,则表示方法的返回类型。OnClickListener的函数接口如图:
2.1_action_event.png
// 图 2-1:View.OnClickListener接口,接受一个View对象,返回空。

Java8提供了很多函数接口(在java.util.function包目录下),但其实没那么复杂。只要记住六大重要函数接口,其它的函数接口是基于这六个函数接口的扩展(下面的“2.4.1 几个单词”章节会具体介绍)。表 2-1 列出了这六个重要函数接口。

// 表2-1 Java 中重要的6个函数接口

接口参数返回类型示例
Function<T,R>TR获得 Artist 对象的名字
Consumer<T>Tvoid输出一个值
Supplier<T>NoneT工厂方法
Predicate<T>Tboolean这张唱片已经发行了吗
UnaryOperator<T>TT逻辑非(!)
BinaryOperator<T>(T, T)T求两个数的乘积(*)

2.4.1 几个单词

在深入学习函数接口之前,希望大家能记住几个单词,掌握这几个单词,官方3、40个函数接口都是小问题了。这几个单词分别是:

  • Function(函数)
  • Consumer(消费者)
  • Supplier(提供者)
  • Predicate (断言)
  • Operation(运算符)
  • Unary (一元)
  • Binary(二元,就是数学里二元一次方程那个二元,代表2个的意思)。

2.4.2 四大基础函数接口

虽然类库中的基本函数接口特别多,但其实总体可以分成四类,就好像阿拉伯数字是无限多的,但总共就10个基本数字一样,理解了这四类,其它的自然就明白了。

2.4.2.1 Function 函数接口

顾名思义,函数的意思。它接受一个参数并返回一个值。永远都是这样,是一个恒定的,状态不可改变的方法。上面说到,函数接口是对行为的抽象。Function接口是对接受一个T类型参数,返回R类型的结果的方法的抽象,通过调用apply方法执行内容。参见例子

// 这个方法接受一个int类型参数a,返回a+1
private static int addOne(int a) {
        return a + 1;
}

// 方法第二个参数接受一个function类型的行为,然后调用apply,对a执行这段行为。
private static int oper(int a, Function<Integer, Integer> action) {
    return action.apply(a);
}

private static void testFunction() {
    int x = 1;

    int y = oper(x, z -> addOne(z)); // 将addOne方法作为参数传递
    System.out.printf("result -> x= %d, y = %d \r\n", x, y); //打印结果 x=1, y=2

    /* 使用lambda表达式来表示这段行为,只要保证一个参数,一个返回值就能匹配 */
    y = oper(x, z -> z + 3);
    System.out.printf("result -> x= %d, y = %d \n", x, y); //打印结果 x=1, y=4

    y = oper(x, z -> z * 3);
    System.out.printf("result -> x= %d, y = %d \n", x, y); //打印结果 x=1, y=3
}

testFunction()函数中的xz ->到底指的是什么?看下图。

2.4.2_function
这里的箭头指向的位置就是形参,可以看到第二个箭头的Lambda表达式指向了Funtion接口。

2.4.2.2 Consumer 函数接口

Consumer 消费者,该接口对应的方法类型为接收一个参数,但没有返回值。可以通俗的理解成将这个参数被“消费掉了”。一般来说,使用Consumer接口往往伴随着一些期望状态的改变或者事件的发生,例如最典型的forEach就是使用的Consumer接口,虽然没有任何的返回值,但是却向控制台输出了语句(后期章节会提到forEach)。Consumer 使用accept对参数执行行为。

private static void testConsumer() {
    Consumer<String> printString = s -> System.out.println(s);
    printString.accept("testConsumer() -> Hello World!"); //控制台输出 helloWorld!
}
2.4.2.3 Supplier 函数接口

Supplier 提供者,和Consumer相反,该接口对应的方法类型为不接受参数,但是提供一个返回值,通俗的理解为这种接口是无私的奉献者,不仅不要参数,还返回一个值,使用get()方法获得这个返回值。

private static void testSupplier() {
    Supplier<String> getInstance = () -> "Hello World!";
    System.out.println("testSupplier() -> " + getInstance.get());  // 控偶值台输出 Hello World
}
2.4.2.4 Predicate 函数接口

predicate 断言,即:“是” 或 “非”,该接口接收一个参数,返回一个Boolean类型值,用于判断与过滤,当然你可以把他理解成特殊的Function,但是为了便于区分语义,还是单独的划了一个接口,使用test()方法执行这段行为。

private static void testPredicate() {
    Predicate<Integer> oddNumber = integer -> integer % 2 == 1;
    System.out.println("testPredicate() 奇数?:" + oddNumber.test(2));
}

2.4.3 其它函数接口

介绍完四种最基本函数接口,剩余的接口就可以很容易理解了,java8 中定义了几十种的函数接口,但是剩下的接口都是上面几种接口的变种,大多为限制参数类型参数的数量。参数类型主要是intlongdouble这三种类型,因为使用频率最高。

1, 类型限制接口
(1),参数类型
可以理解为限制了函数的入参类型。例如:

  • IntPredicate, LongPredicate, DoublePredicate,这几个接口,都是基于Predicate接口,不同的就是他们的泛型类型分别变成了IntegerLongDouble,也就是说参数必须传递为IntegerLongDouble
  • IntConsumer, LongConsumer, DoubleConsumer比如这几个,对应的就是Consumer<Integer>, Consumer<Long>, Consumer<Double>
  • IntFunctionDoubleSupplier等等,其余的是一样的道理。

(2),返回值类型
和上面类似,只是命名的规则上多了一个To, 例如:IntToDoubleFunctionIntToLongFunction, 很明显就是对应的Function<Integer, Double>Function<Integer, Long>,其余同理。另外需要注意的是,参数限制与返回值限制的命名唯一不同就是To。简单来说,前面不带To的都是参数类型限制,带To的是返回值类型限制。

2, 数量限制接口
有些接口需要接受两个参数,此类接口的所有名字前面都是附加上Bi,是Binary的缩写,即:二元的意思,例如BiPredicate, BiFunction等等,而由于 java 没有多返回值的设定,所以Bi指的都是参数为两个。

3, Operator接口
此类接口只有2个,分别是UnaryOperator<T>一元操作符接口,与BinaryOperator<T>二元操作符接口,这类接口属于Function接口的简写,他们只有一个泛型参数,意思是Function的参数与返回值类型相同。一般多用于操作计算,计算 a + b的BiFunction如果限制条件为Integer的话,往往要这么写BiFunction<Integer, Integer, Integer>前2个泛型代表参数,最后一个代表返回值,看起来似乎是有点繁重了,这个时候就可以用BinaryOperator<Integer>来代替了。


下面是各种类型的接口的示意图,相信只要真正理解了,其实问题并不大。

2.4_all_interface_api

2.5 类型推导

某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最便于阅读的方法。有时省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信息帮助理解代码。实践证明,一开始类型信息是有用的,但随后可以只在真正需要时才加上类型信息。下面将介绍一些简单的规则,来帮助确认是否需要手动声明参数类型。

Lambda表达式中的类型推断,实际上是Java 7就引入的目标类型推断的扩展。大家可能已经知道 Java 7中的菱形操作符,它可通过 javac 推断出泛型参数的类型。参见例2-9。

// 例 2-9 使用菱形操作符,根据变量类型做推断
Map<String, Integer> oldWordCounts = new HashMap<String, Integer>();  
Map<String, Integer> diamondWordCounts = new HashMap<>(); 

我们为变量 oldWordCounts 明确指定了泛型的类型,而变量 diamondWordCounts则使用了
菱形操作符。不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!
当然,这并不是什么魔法,根据变量 diamondWordCounts的类型可以推断出 HashMap 的泛型类型,但用户仍需要声明变量的泛型类型。

如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。在例 2-10 中,我们传入了HashMap,根据方法签名已经可以推断出泛型的类型。

// 例 2-10 使用菱形操作符,根据方法签名做推断
useHashmap(new HashMap<>()); // 该语法目前在Java8上才支持
...

private void useHashmap(Map<String, String> values);

Java 7 中程序员可省略构造函数的泛型类型,Java 8更进一步,程序员可省略 Lambda 表达式中的所有参数类型。javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型罢了。这就是所谓的类型推断。


例2-11 都将变量赋给一个函数接口,使用 Lambda 表达式检测一个 Integer 是否大于 5。这实际上是一个 Predicate——用来判断真假的函数接口。

// 例 2-11 类型推断
Predicate<Integer> atLeast5 = x -> x > 5;

Predicate 也是一个 Lambda 表达式,它还返回一个boolean值。表达式 x > 5 是 Lambda 表达式的主体。这样的情况下,返回值就是 Lambda 表达式主体的值。

// 例 2-12 Predicate 接口的源码,接受一个对象,返回一个布尔值
public interface Predicate<T> { 
    boolean test(T t);
}

从例 2-12 中可以看出,Predicate 只有一个泛型类型的参数,Integer 用于其中。Lambda 表达式实现了 Predicate 接口,因此它的单一参数被推断为 Integer 类型。javac 还可检查 Lambda 表达式的返回值是不是 boolean,这正是 Predicate 方法的返回类型(如图 2-4)。
2.5_predicate.png
图 2-4: Predicate 接口图示,接受一个对象,返回一个布尔值。

例 2-13 是一个略显复杂的函数接口: BinaryOperator。该接口接受两个参数,返回一个值,参数和值的类型均相同。实例中所用的类型是 Long。

//2-13,略显复杂的类型推断
BinaryOperator<Long> addLongs = (x, y) -> x + y;

类型推断系统相当智能,但若信息不够,类型推断系统也无能为力。类型系统不会漫无边际地瞎猜,而会中止操作并报告编译错误,寻求帮助。比如,如果我们删掉例 2-5-6 中的某些类型信息,就会得到例 2-14 所示的代码。

//2-14,没有泛型,代码则通不过编译
BinaryOperator add = (x, y) -> x + y;

编译器给出的报错信息如下:

Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.

报错信息让人一头雾水,到底怎么回事? BinaryOperator 毕竟是一个具有泛型参数的函数接口,该类型既是参数 x 和 y 的类型,也是返回值的类型。上面的例子中并没有给出变量 add 的任何泛型信息,给出的正是原始类型的定义。因此,编译器认为参数和返回值都是 java.lang.Object 实例。

后面的章节还会讲到类型推断,但就目前来说,掌握以上类型推断的知识就已经足够了。

2.6 要点回顾

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda 表达式的常见结构:BinaryOperator<Integer> add = (x, y) → x + y
  • 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型。
  • 常用的函数接口: Function、Consumer、Supplier、Predicate、Operation。
  • 类型推导。

上一篇:Java8函数式编程_1–简介

下一篇:Java8函数式编程_3–流(Stream)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值