Lambda 使用

展示如何使用Lambda表达式替代匿名内部类,说明Lambda表达式和函数接口的关系

前言

Java Lambda表达式的一个重要用法是简化某些匿名内部类Anonymous Classes)的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的。具体原理放到下一篇。本篇我们首先感受一下使用Lambda表达式带来的便利之处。

取代某些匿名内部类

本节将介绍如何使用Lambda表达式简化匿名内部类的书写,但Lambda表达式并不能取代所有的匿名内部类,只能用来取代函数接口(Functional Interface)的简写。先别在乎细节,看几个例子再说。

例子1:无参函数的简写

如果需要新建一个线程,一种常见的写法是这样:

  1. // JDK7 匿名内部类写法
  2. new Thread(new Runnable(){// 接口名
  3. @Override
  4. public void run(){// 方法名
  5. System.out.println("Thread run()");
  6. }
  7. }).start();

上述代码给Tread类传递了一个匿名的Runnable对象,重载Runnable接口的run()方法来实现相应逻辑。这是JDK7以及之前的常见写法。匿名内部类省去了为类起名字的烦恼,但还是不够简化,在Java 8中可以简化为如下形式:

  1. // JDK8 Lambda表达式写法
  2. new Thread(
  3. () -> System.out.println("Thread run()")// 省略接口名和方法名
  4. ).start();

上述代码跟匿名内部类的作用是一样的,但比匿名内部类更进一步。这里连接口名和函数名都一同省掉了,写起来更加神清气爽。如果函数体有多行,可以用大括号括起来,就像这样:

  1. // JDK8 Lambda表达式代码块写法
  2. new Thread(
  3. () -> {
  4. System.out.print("Hello");
  5. System.out.println(" Hoolee");
  6. }
  7. ).start();

例子2:带参函数的简写

如果要给一个字符串列表通过自定义比较器,按照字符串长度进行排序,Java 7的书写形式如下:

  1. // JDK7 匿名内部类写法
  2. List<String> list = Arrays.asList("I", "love", "you", "too");
  3. Collections.sort(list, new Comparator<String>(){// 接口名
  4. @Override
  5. public int compare(String s1, String s2){// 方法名
  6. if(s1 == null)
  7. return -1;
  8. if(s2 == null)
  9. return 1;
  10. return s1.length()-s2.length();
  11. }
  12. });

上述代码通过内部类重载了Comparator接口的compare()方法,实现比较逻辑。采用Lambda表达式可简写如下:

  1. // JDK8 Lambda表达式写法
  2. List<String> list = Arrays.asList("I", "love", "you", "too");
  3. Collections.sort(list, (s1, s2) ->{// 省略参数表的类型
  4. if(s1 == null)
  5. return -1;
  6. if(s2 == null)
  7. return 1;
  8. return s1.length()-s2.length();
  9. });

上述代码跟匿名内部类的作用是一样的。除了省略了接口名和方法名,代码中把参数表的类型也省略了。这得益于javac类型推断机制,编译器能够根据上下文信息推断出参数的类型,当然也有推断失败的时候,这时就需要手动指明参数类型了。注意,Java是强类型语言,每个变量和对象都必需有明确的类型。

简写的依据

也许你已经想到了,能够使用Lambda的依据是必须有相应的函数接口(函数接口,是指内部只有一个抽象方法的接口)。这一点跟Java是强类型语言吻合,也就是说你并不能在代码的任何地方任性的写Lambda表达式。实际上Lambda的类型就是对应函数接口的类型Lambda表达式另一个依据是类型推断机制,在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。Lambda表达更多合法的书写形式如下:

  1. // Lambda表达式的书写形式
  2. Runnable run = () -> System.out.println("Hello World");// 1
  3. ActionListener listener = event -> System.out.println("button clicked");// 2
  4. Runnable multiLine = () -> {// 3 代码块
  5. System.out.print("Hello");
  6. System.out.println(" Hoolee");
  7. };
  8. BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4
  9. BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5 类型推断

上述代码中,1展示了无参函数的简写;2处展示了有参函数的简写,以及类型推断机制;3是代码块的写法;4和5再次展示了类型推断机制。

自定义函数接口

自定义函数接口很容易,只需要编写一个只有一个抽象方法的接口即可。

  1. // 自定义函数接口
  2. @FunctionalInterface
  3. public interface ConsumerInterface<T>{
  4. void accept(T t);
  5. }

上面代码中的@FunctionalInterface是可选的,但加上该标注编译器会帮你检查接口是否符合函数接口规范。就像加入@Override标注会检查是否重载了函数一样。

有了上述接口定义,就可以写出类似如下的代码:

  1. ConsumerInterface<String> consumer = str -> System.out.println(str);

进一步的,还可以这样使用:

  1. class MyStream<T>{
  2. private List<T> list;
  3. ...
  4. public void myForEach(ConsumerInterface<T> consumer){// 1
  5. for(T t : list){
  6. consumer.accept(t);
  7. }
  8. }
  9. }
  10. MyStream<String> stream = new MyStream<String>();
  11. stream.myForEach(str -> System.out.println(str));// 使用自定义函数接口书写Lambda表达式

参考文献

  1. The Java® Language Specification
  2. http://viralpatel.net/blogs/lambda-expressions-java-tutorial/
  3. 《Java 8函数式编程 [英]沃伯顿》

    Lambda 原理

Lambda表达式的实现原理

前言

读过上一篇之后,相信对Lambda表达式的语法以及基本原理有了一定了解。对于编写代码,有这些知识已经够用。本文将进一步区分Lambda表达式和匿名内部类在JVM层面的区别,如果对这一部分不感兴趣,可以跳过

不是匿名内部类的简写

经过第一篇的的介绍,我们看到Lambda表达式似乎只是为了简化匿名内部类书写,这看起来仅仅通过语法糖在编译阶段把所有的Lambda表达式替换成匿名内部类就可以了。但实时并非如此。在JVM层面,Lambda表达式和匿名内部类有着明显的差别。

匿名内部类实现

匿名内部类仍然是一个类,只是不需要程序员显示指定类名,编译器会自动为该类取名。因此如果有如下形式的代码,编译之后将会产生两个class文件:

  1. public class MainAnonymousClass {
  2. public static void main(String[] args) {
  3. new Thread(new Runnable(){
  4. @Override
  5. public void run(){
  6. System.out.println("Anonymous Class Thread run()");
  7. }
  8. }).start();;
  9. }
  10. }

image.png
反编译MainAnonymous类,反编译文件中出现了一个MainAnonymousClass(主类)和MainAnonymousClass$1(匿名内部类)
image.png

进一步分析主类MainAnonymousClass.class的字节码,可发现其创建了匿名内部类的对象:

  1. // javap -c MainAnonymousClass.class
  2. public class MainAnonymousClass {
  3. ...
  4. public static void main(java.lang.String[]);
  5. Code:
  6. 0: new #2 // class java/lang/Thread
  7. 3: dup
  8. 4: new #3 // class MainAnonymousClass$1 /*创建内部类对象*/
  9. 7: dup
  10. 8: invokespecial #4 // Method MainAnonymousClass$1."<init>":()V
  11. 11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
  12. 14: invokevirtual #6 // Method java/lang/Thread.start:()V
  13. 17: return
  14. }

Lambda表达式实现

Lambda表达式通过**invokedynamic**指令实现,书写Lambda表达式不会产生新的类。如果有如下代码,编译之后只有一个class文件:

  1. public class MainLambda {
  2. public static void main(String[] args) {
  3. new Thread(
  4. () -> System.out.println("Lambda Thread run()")
  5. ).start();;
  6. }
  7. }

编译之后的结果:
image.png

通过javap反编译命名,我们更能看出Lambda表达式内部表示的不同:

  1. // javap -c -p MainLambda.class
  2. public class MainLambda {
  3. ...
  4. public static void main(java.lang.String[]);
  5. Code:
  6. 0: new #2 // class java/lang/Thread
  7. 3: dup
  8. 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令调用*/
  9. 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
  10. 12: invokevirtual #5 // Method java/lang/Thread.start:()V
  11. 15: return
  12. private static void lambda$main$0(); /*Lambda表达式被封装成主类的私有方法*/
  13. Code:
  14. 0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
  15. 3: ldc #7 // String Lambda Thread run()
  16. 5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  17. 8: return
  18. }

反编译之后我们发现Lambda表达式被封装成了主类的一个私有方法,并通过invokedynamic指令进行调用。

推论,this引用的意义

既然Lambda表达式不是内部类的简写,那么Lambda内部的this引用也就跟内部类对象没什么关系了。在Lambda表达式中this的意义跟在表达式外部完全一样。因此下列代码将输出两遍Hello Hoolee,而不是两个引用地址。

  1. public class Hello {
  2. Runnable r1 = () -> { System.out.println(this); };
  3. Runnable r2 = () -> { System.out.println(toString()); };
  4. public static void main(String[] args) {
  5. new Hello().r1.run();
  6. new Hello().r2.run();
  7. }
  8. public String toString() { return "Hello Hoolee"; }
  9. }

参考文献

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html