文档章节

《Java8 实战》-第三章读书笔记(Lambda 表达式-01)

火力全開
 火力全開
发布于 08/11 14:56
字数 3035
阅读 22
收藏 0

Lambda 表达式

在《 Java8 实战》中第三章主要讲的是Lambda 表达式,在上一章节的笔记中我们利用了行为参数化来因对不断变化的需求,最后我们也使用到了 Lambda,通过表达式为我们简化了很多代码从而极大地提高了我们的效率。那我们就来更深入的了解一下如何使用 Lambda 表达式,让我们的代码更加具有简洁性和易读性。

Lambda 管中窥豹

什么是 Lambda 表达式?简单的来说,Lambda 表达式是一个匿名函数,Lambda 表达式基于数学中的λ演算得名,直接对应其中的 Lambda 抽象( lambda abstraction ),是一个匿名函数,既没有函数名的函数。Lambda 表达式可以表示闭包(注意和数学传统意义的不同)。你也可以理解为,简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出异常的列表。

有时候,我们为了简化代码而去使用匿名类,虽然匿名类能简化一部分代码,但是看起来很啰嗦。为了更好的的提高开发的效率以及代码的简洁性和可读性,Java8 推出了一个核心的新特性之一:Lambda 表达式。

Java8 之前,使用匿名类给苹果排序的代码:

apples.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
});

是的,这段代码看上去并不是那么的清晰明了,使用 Lambda 表达式改进后:

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

或者是:

Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight);

不得不承认,代码看起来跟清晰了。要是你觉得 Lambda 表达式看起来一头雾水的话也没关系,我们慢慢的来了解它。

现在,我们来看看几个 Java8 中有效的 Lambda 表达式加深对 Lambda 表达式的理解:


// 这个表达式具有一个 String 类型的参数并返回一个 int,Lambda 并没有 return 语句,因为已经隐含了 return。
(String s) -> s.length() 

// 这个表达式有一个 Apple 类型的参数并返回一个 boolean (苹果重来是否大于 150 克)
(Apple a) -> a.getWeight() > 150

// 这个表达式具有两个 int 类型二的参数并且没有返回值。Lambda 表达式可以包含多行代码,不只是这两行。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}

// 这个表达式没有参数类型,返回一个 int。
() -> 250

// 显式的指定为 Apple 类型,并对重量进行比较返回 int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())

Java 语言设计者选选择了这样的语法,是因为 C#和 Scala 等语言中的类似功能广受欢迎。Lambda 的基本语法是:

(parameters) -> expression

或者(请注意花括号):

(parameters) -> {statements;}

是的,Lambda 表达式的语法看起来就是那么简单。那我们继续看几个例子,看看以下哪几个是有效的:

( 1 ) () -> {}
( 2 ) () -> "Jack"
( 3 ) () -> {return "Jack"}
( 4 ) (Interge i) -> return "Alan" + i;
( 5 ) (String s) -> {"IronMan";}

正确答案是:(1)、(2)、(3)

原因:

(1) 是一个无参并且无返回的,类似与 private void run() {}.

(2) 是一个无参并且返回的是一个字符串。

(3) 是一个无参,并且返回的是一个字符串,不过里面还可以继续写一些其他的代码(利用显式返回语句)。

(4) 它没有使用使用显式返回语句,所以它不能算是一个表达式。想要有效就必须加一对花括号, (Interge i) -> {return "Alan" + i}

(5) "IronMan"很显然是一个表达式,不是一个语句,去掉这一对花括号或者使用显式返回语句即可有效。

在哪里以及如何使用 Lambda

我们刚刚已经看了很多关于 Lambda 表达式的语法例子,可能你还不太清楚这个 Lambda 表达式到底如何使用。

还记得在上一章的读书笔记中,实现的 filter 方法中,我们使用的就是 Lambda:

List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);

我们可以在函数式接口上使用 Lambda 表达式,函数式接口听起来很抽象,但是不用太担心接下来就会解释函数式接口是什么。

函数式接口

还记得第二章中的读书笔记,为了参数化 filter 方法的行为使用的 Predicate<t>接口吗?它就是一个函数式接口。什么是函数式接口?一言蔽之,函数式接口就是只定义了一个抽象方法的接口。例如 JavaAPI 中的:Comparator、Runnable、Callable:</t>

public interface Comparable<T> {
    public int compareTo(T o);
}

public interface Runnable {
    public abstract void run();
}

public interface Callable<V> {
    V call() throws Exception;
}

当然,不只是它们,还有很多一些其他的函数式接口。

函数式接口到底可以用来干什么? Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实例,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。你也可以使用匿名类实现,只不过看来并不是那么的一目了然。使用匿名类你需要提供一个实例,然后在直接内联将它实例化。

通过下面的代码,你可以来比较一下使用函数式接口和使用匿名类的区别:

// 使用 Lambda 表达式
Runnable r1 = () -> System.out.println("HelloWorld 1");

// 使用匿名类
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("HelloWorld 2");
    }
};

// 运行结果
System.out.println("Runnable 运行结果:");
// HelloWorld 1
process(r1);
// HelloWorld 2
process(r2);
// HelloWorld 3
process(() -> System.out.println("HelloWorld 3"));
        
private static void process(Runnable r) {
    r.run();
}

酷,从上面的代码可以看出使用 Lambda 表达式你可以减少很多代码同时也提高了代码的可读性而使用匿名类却要四五行左右的代码。

函数描述符

函数接口的抽象方法的前面基本上就是 Lambda 表达式的签名。我们将这种抽象方法叫做函数描述符。例如,Runnable 接口可以看作一个什么也不接受什么也不返回的函数签名,因为它只有一个叫做 run 的抽象方法,这个方法没有参数并且是无返回的。

使用函数式接口

函数式接口很有用,因为抽象方法的签名可以描述 Lambda 表达式的签名。函数式接口的抽象方法的签名称为函数描述符。

Predicate

在第一章的读书笔记中,有提到过 Predicate 这个接口,现在我们来详细的了解一下它。

java.util.function.Predicate<t>接口定义了一个名字叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean 值。之前我们是创建了一个 Predicate<t>这样的一个接口,现在我们所说到的这个接口和之前创建的一样,现在我们不需要再去创建一个这样的接口就直接可以使用了。在你需要表示一个涉及类型 T 的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受 String 对象的 Lambda 表达式:</t></t>

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T t : list) {
        if (predicate.test(t)) {
            result.add(t);
        }
    }
    return result;
}

List<String> strings = Arrays.asList("Hello", "", "Java8", "", "In", "Action");
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List<String> stringList = filter(strings, nonEmptyStringPredicate);
// [Hello, Java8, In, Action]
System.out.println(stringList);

如果,你去查看 Predicate 这个接口的源码你会发现有一些 and 或者 or 等等一些其他的方法,并且这个方法还有方法体,不过你目前无需关注这样的方法,以后的文章将会介绍到为什么在接口中能定义有方法体的方法。

Consumer

java.util.function.Consumer<t>定义了一个叫做 accept 的抽象方法,它接受泛型 T 的对象,并且是一个无返回的方法。你如果需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个 foreach 方法,并配合 Lambda 来打印列表中的所有元素.</t>

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

private static <T> void forEach(List<T> list, Consumer<T> consumer) {
    for (T i : list) {
        consumer.accept(i);
    }
}

// 使用 Consumer
forEach(Arrays.asList("Object", "Not", "Found"), (String str) -> System.out.println(str));
forEach(Arrays.asList(1, 2, 3, 4, 5, 6), System.out::println);

Function

java.util.function.Function<T, R>接口定义了一个叫做 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。如果你需要定义一个 Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,把字符串映射为它的长度)。在下面的代码中,我们来看看如何利用它来创建一个 map 方法,将以一个 String 列表映射到包含每个 String 长度的 Integer 列表。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

private static <T, R> List<R> map(List<T> list, Function<T, R> function) {
    List<R> result = new ArrayList<>();
    for (T s : list) {
        result.add(function.apply(s));
    }
    return result;
}

 List<Integer> map = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
// [7, 2, 6]
System.out.println(map);

原始类型特化

我们刚刚了解了三个泛型函数式接口:Predicate<t>、Consumer<t>和 Function<T, R>。还有些函数式接口专为某些类而设计。</t></t>

回顾一下:Java 类型要么用引用类型(比如:Byte、Integer、Object、List ),要么是原始类型(比如:int、double、byte、char )。但是泛型(比如 Consumer<t>中的 T )只能绑定到引用类型。这是由泛型接口内部实现方式造成的。因此,在 Java 里面有一个将原始类型转为对应的引用类型的机制。这个机制叫作装箱( boxing )。相反的操作,也就是将引用类型转为对应的原始类型,叫作拆箱( unboxing )。Java 还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作都是自动完成的。比如,这就是为什么下面的代码是有效的(一个 int 被装箱成为 Integer ):</t>

List<Integer> list = new ArrayList<>;
for (int i = 0; i < 100; i++) {
    list.add(i);
}

但是像这种自动装箱和拆箱的操作,性能方面是要付出一些代价的。装箱的本质就是将原来的原始类型包起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

Java8 为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时,避免自动装箱的操作。比如,在下面的代码中,使用 IntPredicate 就避免了对值 1000 进行装箱操作,但要使用 Predicate<integer>就会把参数 1000 装箱到一个 Integer 对象中:</integer>

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
// 无装箱
evenNumbers.test(1000);

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
// 装箱
oddNumbers.test(1000);

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等。Function 接口还有针对输出参数类型变种:ToIntFunction<t>、IntToDoubleFunction 等。</t>

Java8 中还有很多常用的函数式接口,如果你有兴趣可以去查找一些相关的资料,了解了这些常用的函数接口之后,会对你以后了解 Stream 的知识有很大的帮助。

代码示例:

Github:chap3 Gitee: chap3

本文转载自:https://www.v2ex.com/t/478831#reply0

火力全開
粉丝 23
博文 229
码字总数 19332
作品 0
卢湾
高级程序员
《Java8实战》读书笔记

Java8(https://book.douban.com/subject/26772632/) 引入更多编程概念、编程工具,简洁、灵活地编写代码。 优化步骤: 1)进一步复用代码,抽取公共的逻辑代码,参数化可变的逻辑代码 2)声明...

v1-alpha
2016/12/16
20
0
敏捷教练成长记:秋高气爽第四周

不知不觉坚持了四周了,这周很忙,写读书笔记时尽显疲态,有厌倦的感觉了。需要自我调整,继续坚持。 1、敏捷方面读不少于50页的书或者文章。 阅读《持续交付-发布可靠软件的系统方法》第三章...

转型实践者
2017/11/26
0
0
敏捷教练成长记:漫漫长路第三周

看到跆拳道的软文,讲到: 学跆拳道的正确顺序: 第一阶段:有兴趣 第二阶段:没兴趣 第三阶段:逼练习 第四阶段:成习惯 第五阶段:有兴趣 第六阶段:真热爱 大部分家长在孩子第二阶段时放弃...

转型实践者
2017/11/17
0
0
Java8中的简易并发

我们在《实战Java高并发程序设计》一书中,专门介绍过java 8中对并发的改进。这里我通过转载一篇博客来说简单介绍下。 有人曾经说过(很抱歉,我们找不到原句了): 初级程序员认为并发很难。...

吴小编
2016/04/12
132
0
java8新特性(二)_lambda表达式

最近一直找java8相关新特性的文章,发现都太没有一个连贯性,毕竟大家写博客肯定都有自己的侧重点,这里找到一本书,专门介绍java8新特性的,感觉大家可以看看《写给大忙人看的JavaSE8》.这里...

阳光温暖
08/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

day123-20181021-英语流利阅读-待学习

这款新字体,比记忆面包还管用 Lala 2018-10-21 1.今日导读 字体能跟学习效果有什么关系?你还别说,来自澳洲的心理学家和设计师们,还真创造了一款号称能够帮助大家记忆信息、增强学习效果的...

飞鱼说编程
38分钟前
1
0
CMD命令行:查看 Windows 操作系统的安装时间

电脑越用越卡,计划以后每两个月重新安装一次系统。 那,怎么查看自己系统的安装日期? 问题抛出来了,其实很简单的。 cmd 中输入 systeminfo 命令,回车,等一会 …… 出来结果后,查找下面...

LivingInFHL
今天
3
0
复习

10月19日任务 打印某行到某行之间的内容 sed转换大小写 sed在某一行最后添加一个数字 删除某行到最后一行 打印1到100行含某个字符串的行 一.打印某行到某行之间的内容 #sed -n '/\[abcfd\]/...

hhpuppy
今天
3
0
精通Spring Boot——第十一篇:使用自定义配置

今天这篇文章给大家介绍自定义配置的两种方式 第一式: 使用@ConfigurationProperties,且看代码 package com.developlee.customconfig.config;import org.springframework.boot.context.p...

developlee的潇洒人生
今天
3
0
python:pycharm启动出现异常:io.netty.channel.ChannelException.....

尝试用管理员权限启动终端, 输入: netsh winsock reset 重启电脑. 360的优化搞出来的幺蛾子........

Oh_really
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部