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

2018-11-16  本文已影响0人  一路花开_8fab

一、为什么要引入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表达式有一些重要的特征如下:

我们再多看一些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)产生的输出。

上一篇下一篇

猜你喜欢

热点阅读