java 8新特性:lambda 表达式

小YOY 创建于 2017-04-04

简介

Lambda表达式是整个Java 8发行版中最受期待的在Java语言层面上的改变,Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。在JVM平台上的很多语言(Groovy,Scala,……)从一开始就有Lambda,但是Java程序员不得不使用毫无新意的匿名类来代替lambda。

lambda表达式一览

关于Lambda设计的讨论占用了大量的时间与社区的努力。可喜的是,最终找到了一个平衡点,使得可以使用一种即简洁又紧凑的新方式来构造Lambdas。在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );

请注意参数e的类型是由编译器推测出来的。同时,你也可以通过把参数类型与参数包括在括号中的形式直接给出参数的类型:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );

在某些情况下lambda的函数体会更加复杂,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。例如,下面两个代码片段是等价的:

String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
    ( String e ) -> System.out.print( e + separator ) );

final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
    ( String e ) -> System.out.print( e + separator ) );

Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );

语言设计者投入了大量精力来思考如何使现有的函数友好地支持lambda。最终采取的方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(Java 8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义:

@FunctionalInterface
public interface Functional {
    void method();
}

需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();

    default void defaultMethod() {            
    }        
}

Lambda是Java 8最大的卖点。它具有吸引越来越多程序员到Java平台上的潜力,并且能够在纯Java语言环境中提供一种优雅的方式来支持函数式编程。更多详情可以参考官方文档

lambda表达式的实际应用

前面已经提到简介中介绍到:“Java程序员不得不使用毫无新意的匿名类来代替lambda”。与其这样说,不如说是lambda是匿名类的一种更加简洁优雅的表现形式。可以这样说lambda适用场景基本与匿名匿名类一致。如果某个接口,往往在一个地方实现了该接口,别处基本上就用不到该类,这个时候没有必要为了这中特定的场景新建一个实现类,因为这个类无法满足其他场景的一些需求,这个时候我们常常适用匿名类类实现某个接口,再java8,我们适用lambda来代替匿名类。

且看下面的例子: 现在有这样一个需求:再jpa的条件查询时,拼接动态条件。在条件拼接的过程中,为了保证程序的健壮性,我们需要对入参进行一些条件判断,只有满足某些条件的参数,才能才能够拼到条件查询中。因此我们常常会有下面代码:

java动态查询拼接简易对象:

public class CriteriaQuery{

    CriteriaQuery andEqual(String field,Integer value){
        //拼接处理

        //返回对象本身,以支持链式代码
        return this;
    }
}

CriteriaQuery传统的使用方式:

CriteriaQuery query = new CriteriaQuery();
if(boolean ex1){
    //拼接条件1
    query.andEqual(someFiled1,someIntValue);
}
if(boolean ex2){
    //拼接条件2
    query.andEqual(someFiled2,someIntValue);
}
if(boolean ex3){
    //拼接条件3
    query.andEqual(someFiled3,someIntValue);
}

这样的代码没有任何问题,功能性能够得到满足,并且逻辑清晰,且简单易懂,但是!不够优雅简洁,并且我们的CriteriaQuery类是支持链式代码书写的,我们对每个入参都进行一次判断是否需要拼接到条件中,这使得我们的链式风格的代码也变成了鸡肋。我们试图来改变这一困境,我们声明了如下接口:

public interface AcceptDecision<V> {
    /**
     * 是否接收value值,不接受返回false.接收返回true
     * @param value 需要判断的值value
     * @return 结果
     */
    boolean accept(V value);
}

修改CriteriaQuery类,我们把是否拼接某个参数的判断交给CriteriaQuery对象来处理。

public class CriteriaQuery{

    CriteriaQuery andEqual(String field,String value,AcceptDecision decision){
        //接收判断
        if(!decision.accept(value)){
            return this;
        }

        //拼接处理

        //返回对象本身,以支持链式代码
        return this;
    }
}

完成以上改造,在条件拼接之前,我们就不必对每个参数进行条件判断了,因为我们把这个工作叫给了CriteriaQuery对象。基于以上匿名内部类是使用场景介绍,我们决定使用匿名内部类来完成这个工作,我们会有如下代码:

CriteriaQuery query = new CriteriaQuery();

query
    .andEqual(someFiled,someValue, new AcceptDecision() {
                    @Override
                    public boolean accept(V value) {
                        //decide if accept the value
                        return result;
                    }
                })
    .andEqual(someFiled,someValue, new A() {
                    @Override
                    public boolean accept(V value) {
                        //decide if accept the value
                        return result;
                    }
                })
    .andEqual(someFiled,someValue, new AcceptDecision() {
                    @Override
                    public boolean accept(V value) {
                        //decide if accept the value
                        return result;
                    }
                })

对以上代码进行一个简单的说明,以上代码很好的实现了链式风格,并且在拼接条件时,条件拼接的判断工作也交给了AcceptDecision对象来进行。但是有一个很大的问题在于,代码太冗余了,匿名类的看起来非常糟糕,还不如入之前使用if语句对每个参数进行判断来得简单直接。那好,我们就来试试lambda表达式,lambda的诞生就是为了解决这一尴尬的问题。

改造AcceptDecision,增加@FunctionalInterface注解,将其标记成函数接口。

@FunctionalInterface
public interface AcceptDecision<V> {
    /**
     * 是否接收value值,不接受返回false.接收返回true
     * @param value 需要判断的值value
     * @return 结果
     */
    boolean accept(V value);
}

改造以上查询代码:

CriteriaQuery query = new CriteriaQuery();
        query
            .andEqual("someField1",someValue1,value -> value > 0)
            .andEqual("someField2",someValue2,value -> value < 0)
            .andEqual("someField3",someValue3,value -> value <= 150);

使用lambda表达式替代匿名类,可以使得代码更加简介优雅,这也是lambda最吸引人的地方。

引用

java官方文档-lambda表达式 Java 8新特性终极指南-lambda表达式 Java 8 Features Tutorial – The ULTIMATE Guide