• Java8揭秘(二)Java 8中的 Lambda表达式


    http://blog.csdn.net/wwwsssaaaddd/article/details/24212693


    第一章:Java 8中的 Lambda表达式

    在这一章,我们说一说Lambda表达式的语法。我们将从经典的Java语法过渡到新式的Lambda表达式语法。

    我们也会讲一讲Lambda表达式的原理-即在运行时Lambda表达式如何表示,涉及哪些字节码指令。

    入门

    如果你熟悉Groovy或者Ruby这些支持Lambda表达式的编程语言,那么你一开始可能会认为Java的Lambda表达式并不像其他编程语言中的那样简洁。在java中,Lambda表达式是SAM类型,SAM类型是一个具有单一抽象方法的接口。(对了,Java8接口可以包含非抽象方法了-即default/defender方法,我们将在后面讲到它)

    举个例子,众所周知的Runnable接口就是SAM类型的:

    Runnable r =()->System.out.println("hello Lambda!");

    Comparator 接口也算一个:

    Comparator cmp=(x, y)->(x < y)?-1:((x > y)?1:0);

    像下面这样写也可以:

    Comparator cmp= (x, y)->{
    return (x < y) ? -1: ((x > y)?1:0);
    };

    如此看来上面单行的Lambda表达式中隐含有return关键词。

           我来用下面的例子提示一下,使用Java8之前的语法,如何实现同样的比较器代码:

    Comparator cmp=newComparator(){

      @Override

        publicint compare(Integer x,Integer y){

                     return (x< y)? -1:((x> y)?1:0);

        }

    };

    如你所见,在这两个例子中有相当一部分代码是相同的,相同部分正是Comparator 的比较逻辑:

    (x< y)? -1:((x> y)?1:0)

    当把经典风格java语法转变为Lambda表达式语法时,我们主要关注接口方法的参数和功能逻辑。

    看另外一个例子。如果我打算写一个方法,此方法接收一个Lambda表达式作为参数,那么该怎么写?嗯…你得把方法参数声明成函数接口,然后才能传递Lambda表达式进来,如下所示:

    Interface Action{

          void run(Stringparam);

    }

     

    public void execute(Action action){

                action.run("Hello!");

    }

    如果我们想要调用execute(..)方法,那么通常地做法,给execute方法传递一个Action的匿名实现类。如下所示:

    execute(new Action{
    publicvoid run(String param){
    System.out.println(param);
    }
    });

    但是因为我们现在有函数接口做参数类型,所以我们可以用下面的方式调用execute(..):

    execute((Stringparam)->System.out.println(param));

    实际上,可以不需要声明Lambda表达式的参数类型:

    execute(param->System.out.println(param));

    一般来说,Lambda表达式的类型声明规则如下:要么为所有参数声明类型,要么去掉所有参数的类型声明。

    既然这个Lambda表达式仅调用一个方法,且(该方法和函数接口中定义的方法)使用相同的参数,那么可以用方法引用(method reference)替代这个Lambda表达式。如下所示:

    execute(System.out::println);

    但是,如果(调用的方法)使用参数形式有变,就不能使用方法引用了,得使用完整的Lambda表达式,如下面这种情况:

    execute(s->System.out.println("*"+ s +"*"));

    尽管Java本质上并没有函数类型(的变量),但是上面展示的语法已经相当不错,对于在java语言中应用Lambda表达式来说,我们也算有一个非常优雅的解决方案了。

    函数接口

    如刚才讲的,Lambda表达式在运行期表示为一个函数接口(functionalinterface)(或者说一个SAM类型),函数接口是一种只定义了一个抽象方法的接口。尽管JDK已经有一些接口都符合函数接口定义,比如Runnable 和 Comparator,但是这对API演进来说是显然不够的。我们又不能到处在代码里使用像Runnable这样的接口,因为这么做不合乎逻辑。

    JDK8中新增了一个包,java.util.function,这个包里有一些专门给新增的API使用的函数接口。此处就不列出所有的函数接口了,有兴趣可以自行学习下java.util.function:)

    下面列出几个java.util.function中定义的接口,都非常有趣:

     

    •   Consumer<T> – 在T上执行一个操作,无返回结果
    •   Supplier<T> –无输入参数,返回T的实例
    •   Predicate<T> –输入参数为T的实例,返回boolean值
    •   Function<T,R> –输入参数为T的实例,返回R的实例

     

    java.util.function 中新定义了超过40个函数接口。通常可以从接口的名字看出其含义。举个例子,BiFunction和上面提到的Function接口非常相似,只是唯一不同点是BiFunction有两个输入参数而Function有一个。

    我们可以从那些新接口中看到另一个常见模式,该模式是在一个接口继承另一个接口的时候,把多个参数声明成同一种类型。例如,BinaryOperator 继承BiFunction,目的仅仅是为了把两个输入参数声明为同类型,如下所示:

    @FunctionalInterface public interface BinaryOperator extends BiFunction<T,T,T>{}

    为了强调接口是函数接口,可以使用新注释@FunctionalInterface,来防止你的团队成员往这个接口里增加方法。这个注释除了在运行时使用,还给javac用来验证该接口是否真是函数接口,其内部的抽象方法是否不多于一个。

    下面代码不能正常编译:

    @FunctionalInterface interface Action{
    void run(String param);
    void stop(String param);
    }

    编译器抛出错误:

    java: Unexpected @FunctionalInterface annotation
        Action is not a functional interface
        multiple non-overriding abstract methods found in interface Action

    而下面的会编译通过

    @FunctionalInterface interface Action {
    void run(String param);
    default void stop(String param){}

    }

    获取变量

    如果Lambda表达式需要访问非静态变量或定义在其外部的对象,那么我们会碰到一种情况,就是Lambda表达式需要获取非体内变量,此时我们称之为一种“获取态”的Lambda表达式。

    思考下面比较器的例子:

    int minus_one =-1;
    int one =1;
    int zero =0;
    Comparator cmp =(x, y)->(x < y)? minus_one :((x > y)? one : zero);

    为了使Lambda表达式生效,Lambda表达式获取的变量minus_one、one和 zero必须是“实质的常量”。这意味着这些变量要么应该声明成final类型,要么不能二次赋值。

    返回值是Lambda表达式

    虽然在上面讲到的例子中,函数接口可以用作其他某个方法的参数,然而函数接口的用法并不限于当参数,函数接口还可以用作方法的返回值。也就是说我们可以从方法返回一个Lambda表达式,如下例子:

    public class Comparator Factory{
        public Comparator makeComparator(){
            return Integer::compareUnsigned;
        }   
    }

    上面的例子展示了一段有效的方法代码,这个方法返回了一个方法引用。然而实际上仅像那样是不能从方法中返回一个方法引用的,其实编译器还会使用invokedynamic 字节码指令,生成一些代码来使它成为一个方法调用,该方法调用返回一个Comparator接口的实例对象。因此客户端代码只认为自己是在使用一个接口:

    Comparator cmp=new ComparatorFactory().makeComparator();
    cmp.compare(10, -5);// -1

    序列化Lambda表达式

    前一部分中使用的那段代码,创建了一个Comparator 实例对象,该实例对象可以让客户端代码使用。所有工作看似相当成功。但是,有个严重的问题,即是如果我们尝试序列化那个Comparator实例对象,代码就会抛出NotSerializableException异常。

    因为序列化可能存在安全隐患,所以默认情况下,Lambda表达式不能序列化。为了能序列化,java8引入了所谓的类型关联(TypeIntersection),如下所示:

    public class ComparatorFactory{
        public Comparator makeComparator(){
            return(Comparator&Serializable)Integer::compareUnsigned;
        }
    }

    Serializable接口一般认为是标记性的接口,该接口中没有声明任何方法,因此Serializable接口也可以称作ZAM类型(ZAM即Zero Abstract Methods)。

    使用类型关联的一般规则如下:

    SAM & ZAM1 & ZAM2 & ZAM3

    也就是说,如果返回结果是SAM类型的,那么我们可以用SAM类型和一个甚至多个ZAM类型“相关联”。我们现在事实上认为作为返回结果的Comparator 实例对象也是Serializable类型的。

    经过上面对返回结果强制转换类型后,编译器在编译后的class文件中多生成了一个方法,如下所示:

    private  static  java.lang.Object  $deserializeLambda$(java.lang.invoke.SerializedLambda);

    此外,通过使用invokedynamic 字节码指令策略,当通过makeComparator()方法 创建一个Comparator 的实例对象的时候,编译器就会调用$deserializeLambda$(..) 方法。 

    反编译Lambda表达式

    现在给大家讲一讲这背后的实现原理。当我们在代码中使用Lambda表达式的时候,同时也了解下代码实际上是怎么编译的,这会很有趣。

    目前(像Java 7之前的版本),如果你想在java中模仿Lambda表达式,那么你得定义一个匿名内部类。这样会在编译后生成一个相应的class文件。如果你在代码中定义多个匿名内部类,那么这些匿名类只不过是在其相应的class文件名字中增加一个数字后缀。Lambda表达式编译后会是怎样呢?

    仔细思考下面的代码:

    public class Main { 
      @FunctionalInterface  interface Action{
    void run(String s);

        }  

    public  void  action(Action action){

        action.run("Hello!");

        }     

    public  static  void main(String[] args){

        new  Main().action((String s)->System.out.print("*"+ s +"*"));

        } 

    }

    编译后产生两个类文件:Main.class和Main$Action.class,但并没有生成带编号的类,带编号的类通常在匿名类编译后产生。这样在Main.class中一定有什么东西,实现了我们在main方法中定义的Lambda表达式。(我们反编译下Main.class看看究竟)

    $ javap -p Main
     
    Warning: Binary file Main contains com.zt.Main
    Compiled from "Main.java"
    public class com.zt.Main {
      public com.zt.Main();
      public void action(com.zt.Main$Action);
      public static void main(java.lang.String[]);
      private static java.lang.Object Lambda$0(java.lang.String);

    }

    哈!在编译后的class中生成了一个方法Lambda$0! -C- V选项会给我们展示实际的字节码和常量池定义。

    下面的main方法揭示了invokedynamic指令用来分派方法调用。

    public static void main(java.lang.String[]);
       Code:
       0: new          #4   // class com/zt/Main
       3: dup
       4: invokespecial#5    // Method "":()V
        7: invokedynamic #6,  0  // InvokeDynamic #0:run:()Lcom/zt/Main$Action;
       12: invokevirtual  #7      // Method action:(Lcom/zt/Main$Action;)V
       15: return

    可以在常量池中找到引导方法,该引导方法负责在运行时把所有内容链接起来:

    BootstrapMethods:
      0: #40 invokestatic java/lang/invoke/LambdaMetafactory.metaFactory:(    
    Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;             
    Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;          
    Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)         
    Ljava/lang/invoke/CallSite;
      Method arguments:
        #41 invokeinterface com/zt/Main$Action.run:(Ljava/lang/String;)Ljava/lang/Object;
        #42 invokestatic com/zt/Main.Lambda$0:(Ljava/lang/String;)Ljava/lang/Object;
        #43 (Ljava/lang/String;)Ljava/lang/Object;

    你可以看见到处都有MethodHandle的影子,但我们现在不会深入去讲MethodHandle。到现在,我们可以确认上面说的那个定义恰恰指的是生成的方法Lambda$0。

    如果我定义一个名字是Lambda$0的静态方法会这么样?Lambda$0毕竟算一个有效标识符!于是,我定义了Lambda$0方法,如下所示:

    Public static void Lambda$0(String s){

        return null;

    }

    结果编译失败,不允许我在代码中定义Lambda$0方法。

    java: the symbol Lambda$0(java.lang.String) conflicts with a

            compiler-synthesized symbol in com.zt.Main

    这实际上告诉我们在编译过程中,在Main类里构建其他方法前就先构建的Lambda表达式。

    总结

    在此为本文的第一章做一个小结。我敢肯定,Lambda表达式在不久的将来会对Java产生巨大的影响。又因为Lambda表达式语法结构相当不错,所以一旦开发者认识到像Lambda这些特性有益于提升开发效率,那么我们将会看到Lambda表达式更广泛的应用。


  • 相关阅读:
    android部分控件应用解析
    CodeForces Round #179 (295A)
    面试题27:连续子数组的最大和
    java写文件时,输出不完整的原因以及解决方法
    序列化和反序列化--转
    Java多线程编程那些事:volatile解惑--转
    转变--一个平凡人的2017年总结及2018年展望
    系列文章--批处理学习
    set命令
    bat计算两个时间差
  • 原文地址:https://www.cnblogs.com/leeeee/p/7276097.html
Copyright © 2020-2023  润新知