美文网首页
无名函数-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)产生的输出。

    相关文章

      网友评论

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

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