无名函数-java 8中的lambda表达式

一、为什么要引入lamda表达式

众所周知,软件工程领域需求最大的不变之处就是变化。行为参数化就是应对频繁变化的软件需求的一种软件开发模式。我们可以先准备好一段代码块,不去执行它,而是作为参数传递给另一个方法,延迟执行。需求变化了,只需要传递代表另一个行为的参数即可。

想想java 8之前我们是怎么把代码传递给方法的?来看下面一段函数:

Collections.sort(inventory, new Comparator<Apple>() {
            public int compare(Apple a1, Apple a2){
                return a1.getWeight().compareTo(a2.getWeight());
            }
        });

为了对苹果进行排序,我们新建了对象(实现了Comparator接口),明确地实现了compare方法来描述排序的核心逻辑,是不是很啰嗦!!这里就有lamda表达式大显身手的地方了,它用一种更简单的方式来传递代码。上面的例子使用lambda表达式的版本如下:

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

接下来我们会详细讲解如何编写和使用lambda

二、Lambda表达式的基本语法

Lambda的基本语法是

(parameters) -> expression

或(请注意语句的 括号)

    (parameters) -> { statements; }
图1.png

Lambda表达式有三个部分,如图1所示。

  1. 参数列表--这里它用了Comparator中compare方法的参数,两个Apple
  2. 箭头--把参数列表与Lambda主体分 开。
  3. Lambda主体--描述比较两个Apple的重量。

lambda表达式有一些重要的特征如下:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的return关键字:如果主体只有一个表达式返回值则编译器会自动返回值,如果有大括号需要指定明表达式返回了一个数值。

我们再多看一些lambda表达式的例子,来熟悉它的基本语法。

// 如果主体只有一个表达式返回值则编译器会自动返回值
(String s) -> s.length()
// 不需要声明参数类型,编译器可以统一识别参数值
// 一个参数无需定义圆括号
s -> s.length()

这个Lambda表达式具有String类型的参数并返回一个int。以上两个lambda表达式等效。

// 主体有多条语句,需要大括号包起来
// 非void返回情况下,如果有大括号需要指明表达式返回了一个数值
(int x, int y) -> {
            System.out.println("Result:");
            return x + y;
        }

上面的Lambda表达式具有两个int类型的参数而没有返回值(void返回)。主体可以包含多行语句,这里是两行。

() -> 42

上面的表达式没有入参,返回一个int

三、哪里以及如何使用Lambda表达式

先给出一个结论:可以在函数式接口上使用Lambda表达式,那么什么是函数式接口呢?
函数式接口是只定义一个抽象方法的接口。Java API中有一些常见的函数式接口,比如Comparator和Runnable。接口还可以拥有默认方法,哪怕有很多默认方法,只要接口只定义一个抽象方法,它就仍然是一个函数式接口。

public interface Comparator<T> {
    int compare(T o1, T o2);
}
public interface Runnable {
    public abstract void run();
}

那么问题来了,函数式接口和lambda表达式之间的关联是什么呢?lambda表达式是函数式接口一个具体实现的实例。如果方法使用函数式接口作为参数,那么就可以传递一个具体的lambda表达式了。看下面的例子:

public static void process(Runnable r){
        r.run();
    }

process方法接收一个Runnable接口具体实现的实例,在java8之前,我们可以使用匿名类按照如下的方式传递参数:

 Runnable r1 = new Runnable() {
            public void run() {
                System.out.println("Hello World 1");
            }
        };
process(r1);

使用lamba表达式,我们可以按如下的方式传递参数:

Runnable r2 = () -> System.out.println("Hello World 2");
process(r2)

甚至不需要定义临时变量

process(() -> System.out.println("Hello World 3"));

可以看到,Lambda表达式可以作为参数传递给方法或存在变量中。
Java 8的库设计师帮我们在java.util.function包中引入了一些新的函数式接口,比如Predicate、Consumer和Function等。以Predicate为例,java.util.function.Predicate<T>接口定义了一个名 test的抽象方法,它接受泛型T对象,并返回一个boolean。

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

在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口,比如下面一段代码用于筛选出列表中的偶数。

List<Integer> list= Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        list.stream()
                .filter(i -> i % 2 == 0)
                .forEach(System.out::println);

其中,filter方法的参数就是一个Predicate接口类型,接受泛型类型T对象,返回boolean

Stream<T> filter(Predicate<? super T> predicate);

实际工作中可以根据需要选择匹配的函数式接口,甚至可以自己设计函数式接口

四、lambda表达式的优点和缺点

lambda表达式使我们的代码更加简洁、清晰和灵活。可以把lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
不幸的是,由于lambda表达式没有名字,它的栈跟踪可能很难分析。在下面这段简单的代码中,我们刻意地引入了一些错误:

public class Debugging{
    public static void main(String[] args) {
        List<Point> points = Arrays.asList(new Point(12, 2), null);
        points.stream().map(p -> p.getX()).forEach(System.out::println);
    }
}

运行这段代码会产生下面的栈跟踪:

Exception in thread "main" java.lang.NullPointerException
12
    at java8.Debugging.lambda$main$0(Debugging.java:15)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at java8.Debugging.main(Debugging.java:15)

错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。这个例子中,它的名字是lambdamain0,看起来非常不直观 。如果你使用了大量的类,其中又包含多个Lambda表达式,这就成了一个非常头痛的问题。
如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。比如下面这个例子:

public class Debugging {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        numbers.stream()
                .map(Debugging::divideByZero)
                .forEach(System.out::println);
    }

    public static int divideByZero(int n) {
        return n / 0;
    }
}

输出结果如下:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at java8.Debugging.divideByZero(Debugging.java:23)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at java8.Debugging.main(Debugging.java:19)

下面介绍一个流操作中对流水线进行调试的方法--peek,它能将流水线中间变量的值输出到日志中,是非常有用的工具。

public class Debugging{
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
        List<Integer> result =
                numbers.stream()
                        .peek(x -> System.out.println("from stream: " + x))
                        .map(x -> x + 17)
                        .peek(x -> System.out.println("after map: " + x))
                        .filter(x -> x % 2 == 0)
                        .peek(x -> System.out.println("after filter: " + x))
                        .limit(3)
                        .peek(x -> System.out.println("after limit: " + x))
                        .collect(toList());
    }
}

运行结果如下:

from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22

可以看出,peek方法可以帮助我们跟踪Stream流水线中的每个操作(比如map、filter、limit)产生的输出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,149评论 0 13
  • Java 8 lambda 表达式10个示例 Java 8 发布于4年前,日期是2014年3月18日,这次开创性的...
    认真期待阅读 1,385评论 2 6
  • 1.深度工作的代表者 亚当•格兰特是精英级深度工作者的代表,他是我2013年在宾夕法尼亚大学沃顿商学院遇到的最年轻...
    剽悍的今天阅读 63评论 0 0
  • 走了一路,从春天又回到了初冬,鼻子开始不适应,皎洁月光从不太严实的门缝里渗进来,邸先生起来撒尿去了…… 拧开茶杯喝...
    阿木洛克人阅读 180评论 0 0
  • 今天和先生一起回老家给祖上上坟,在回来的路上,先生说通往郑州新开了一条小路,想摸摸路,大年初三,也没有安排...
    静静岁月阅读 324评论 1 2