美文网首页
无名函数-java 8中的lambda表达式

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

作者: 一路花开_8fab | 来源:发表于2018-11-16 14:32 被阅读0次

一、为什么要引入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)产生的输出。

相关文章

  • Lambda表达式

    Lambda表达式与函数式接口紧密相关,函数式接口介绍 Lambda表达式的基本语法 Java Lambda表达式...

  • 《Java 8实战》学习总结

    Java 8新特性概览 Lambda表达式 流 默认方法 Lambda表达式 Lambda和函数式接口Java 8...

  • java lambda 表达式

    java lambda 表达式 lambda 是 java 8 引入的新特性,lambda 能替换简单的函数和类,...

  • Lambda表达式

    Java为什么需要Lambda表达式 Lambda表达式为Java添加了缺少的函数式编程的特点,将函数作为一个参数...

  • Android Studio中如何支持使用Lambda表达式

    Java8引入了lambda表达式,Lambda 表达式”(lambda expression)是一个匿名函数,本...

  • Lambda表达式详解

    什么是Lambda表达式 Lambda表达式是Java 8的新特性,是函数式接口的实例。使用Lambda表达式可以...

  • Java 8 lambda 表达式

    lambda 表达式是 Java 8 支持的新特性之一。通过 lambda 表达式,Java 具备了函数式编程的能...

  • Lambda表达式总结

    Lambda表达式总结使用范例以及例子Lambda表达式基础语法函数式接口Lambda练习Java8四大内置函数式...

  • Java 8 深入剖析 | Lambda 表达式

    1. Lambda 表达式介绍 Lambda 表达式为 Java 添加了缺失的 函数式编程特性,使我们能将函数当作...

  • Java 8 新特性:Lambda表达式

    Java 8 新特性:Lambda表达式 一、是什么? Lambda表达式是一个匿名函数,即没有函数名的函数。La...

网友评论

      本文标题:无名函数-java 8中的lambda表达式

      本文链接:https://www.haomeiwen.com/subject/jlrmfqtx.html