Java8新特性Lambda表达式

其实lambda表达式和C#的lambda很像,用法也大体一致,只不过需要函数式编程接口的支持。自从Java8加入这个特性之后,也就把函数式编程的概念引入到了java当中来,lambda的出现可以让以前看起臃肿的代码更加清晰。例如java8之前使用匿名类实现接口的方式(也称 Model Code AS Data),简单的逻辑却需要大量冗余的代码。同时java8中的新增加的其他特性如:方法引用,Stream API等配合lambda使用起来更加的简洁高效。

函数式接口

lambda表达式可能看成是函数式接口的实现,那是什么是函数式接口呢?其实很简单,就是接口内只有一个抽象方法。但Java8针对接口新加入的默认方法,接口的静态方法还有继承自Object对象的方法(如:ToString)均不影响函数式接口的这个特性。此外还新加入了一个标记注解@FunctionalInterface用来验证接口是否为函数式接口。

@FunctionalInterface
public interface StudentMapping {

    /**
     *  获取学生列表
     * @return 返回学生列表
     */
    List<Object> GetStudents();

    /**
     *  默认方法对函数式接口是免疫的
     * @version java8 新特性
     * @return
     */
    default String GetTableName(){
        return "student";
    }

    /**
     *  静态方法对函数式接口是免疫的
     * @version java8 新特性
     * @param id 学生id
     * @return 验证是否通过 [true/false]
     */
    static boolean VerifyStuId(Integer id) {
        if (id > 0) {
            return true;
        }
        return false;
    }

    /**
     *  继承自Object的方法对函数式接口也是免疫的
     * @return
     */
    String toString();
}

为什么函数式接口内仅允许有一个抽象函数呢?上面说到lambda其实是函数式接口的实现(后面讲实现原理的时候会说),一个lambda表达式又只能代表一个函数。

Java8中新加入的内置函数式接口

在java8中新加入了一个包java.util.function,所有内置的函数式接口都在这个包内。除了这些内置的外,只要满足函数式接口的规范也是函数式接口。像java8之前的,我们比较熟悉的 Runnable 、Comparable 等。在这些新加入的内置接口中,其中有几个比较常用。

# 1.传入一个T类型参数,返回boolean类型
Predicate<String> p = (String name) -> name.equals("vitamin"); 
调用:p.test("vitamin"); // 返回true

# 2.传入一个T类型的参数,没有返回值
Consumer<String> c =(String msg)-> System.out.println(msg);
调用:c.accept("hello,world"); // 打印hello,world

# 3.传入一个T类型的参数,返回一个R类型
Function<String,Integer> f  = (String name) name.equals("vitamin") ? 1 : 0;
调用:f.apply("vitamin"); //返回1

# 4.不传入任何参数,返回一个T类型
Supplier<String> s = () -> "hello,world";
调用:s.get(); // 返回hello,world

# 5.传入一个T类型,返回一个T类型。多用于转换。
UnaryOperator<String> u = (String name) -> "name:"+ name;
调用:u.apply("vitamin"); // 返回name:vitamin

# 6.传入两个T类型参数,返回一个T类型参数
BinaryOperator<Integer> b = (Integer a,Integer b) -> a + b;
调用:b.apply(1,2); // 返回3 

在上述的示例中,lambda表达式中的类型参数可以根据类型推断而省略。如果参数列表中只有一个参数,那么圆括号也可以省略。
譬如:(msg)-> System.out.println(msg) 等同于 (String msg) -> System.out.println(msg) 还等同于 msg -> System.out.println(msg)。

除了上述内置的函数式接口外,还可以自定义函数式接口,来实现不同的lambda表达式结构来满足需求。

变量引用

Lambda表达式和使用匿名类实现接口的方式在变量引用方面有着很大的不同。

public class Demo{
    private String global= "global"; //全局变量
    
    public void Test(){
        String local= "lcoal"; //本地变量
        Predicate<String> p1 = vari -> vari.equals(this.global) || vari.equals(local); //lambda表达式可以使用this访问变量global
        Predicate<String> p2 = new Predicate<String>() {
            String inner = "inner"; //内部变量可随意修改
            @Override
            public boolean test(String vari) {
                return vari.equals(global) || vari.equals(local);//匿名类的方式无法使用this,因为此时的this指向已经改变。
            }
        };
        local = "new-local"; //这里无法修改,否则会在编译时报错。无论是lambda还是匿名类对local的访问都认为是final的。
        global = "new-global"; //全局变量可随意修改
    }
}

在上述的实例中,各个步骤的解释都已经在备注里面了。值得强调的lambda表达式对this的使用方式,不再像匿名类那样将this的作用域范围限制在匿名类的内部。还有一点就是对于示例中的本地变量,无论是哪种方式使用都认为它是final的,否则就会造成编译失败,提示为:

Variable 'local' is accessed from within inner class, needs to be final or effectively final

方法重载与Lambda表达式

lambda表达式在使用上还是存在一定限制的,比如遇见下面这种方法重载的情况。

# 函数式接口Param1
interface Param1{
    void test();
}

# 函数式接口Param2
interface Param2{
    void test();
}

public class Demo{
    public void print(Param1 p1){
        p1.test();
    }
    
    public void print(Param2 p2){
        p2.test();
    }
}

public class TestDemo{
    public static void main(String[] args){
        Demo d = new Demo();
        d.print(()-> System.out.println("我无法执行!!!"));
    }
}

如代码中所示,由于Param1和Param2的Lambda表达式的表现形式相同,导致在出现重载方法时,无法判断具体执行的是哪个方法。故出现编译错误:

Ambiguous method call.Both

lambda表达式的实现原理

先说结论:在底层实现上,会为lambda表达式生成一个静态方法和一个实现函数式接口的类。下面我们以一个简单的示例说明下:

# 使用 javap -p 命令和选项来查看class内部所有的类和方法
$ javap -p App.class   
Compiled from "App.java"
public class App {
  public App();
  public static void main(java.lang.String[]);
  private static boolean lambda$main$0(java.lang.String);
}

在上述代码中我们看到多出来了一个lambda$main$0(java.lang.String) 静态方法,而该静态方法的参数和返回值和lambda表达式的一致。接下来让我们看看这个静态方法的内容到底是什么,那么这里就又涉及到另外一个工具的使用:asmtools是一个汇编工具,可以将class文件中的内容,转化为类似于Java代码的可读格式。

# 执行如下命令将App.class中的内容转化为人类可读的格式并存储到App.jasm文件中。
$ java -cp /c/WorkSpace/asmtools-7.0/lib/asmtools.jar org.openjdk.asmtools.jdis.Main App.class > App.jasm

# App.jasm文件中部分内容如下
private static synthetic Method lambda$main$0:"(Ljava/lang/String;)Z"
	stack 2 locals 1
{
		aload_0;
		ldc	String "admin";
		invokevirtual	Method java/lang/String.equals:"(Ljava/lang/Object;)Z";
		ireturn;
}

在上述的代码片段中,转换后我们可以看到在生成的静态方法的内部正是lambda表达式的内容(invokevirtual 所示)。接下来在看看程序运行时发生的变化。

# 通过此命令可以查看lambda表达式在实际运行中隐藏的实现
$ java -Djdk.internal.lambda.dumpProxyClasses App

运行上述命令后,就会生成一个App$$Lambda$1.class文件,让那个我们在用汇编工具看下里面内容:

$ java -cp /c/WorkSpace/asmtools-7.0/lib/asmtools.jar org.openjdk.asmtools.jdis.Main App\$\$Lambda\$1.class > App.Lambda.jasm

查看App.Lambda.jasm文件中的部分内容如下:
super final synthetic class App$$Lambda$1
	implements java/util/function/Predicate
	version 52:0
{


private Method "<init>":"()V"
	stack 1 locals 1
{
		aload_0;
		invokespecial	Method java/lang/Object."<init>":"()V";
		return;
}

@+java/lang/invoke/LambdaForm$Hidden { }
public Method test:"(Ljava/lang/Object;)Z"
	stack 1 locals 2
{
		aload_1;
		checkcast	class java/lang/String;
		invokestatic	Method App.lambda$main$0:"(Ljava/lang/String;)Z";
		ireturn;
}

} // end Class App$$Lambda$1

运行时生成的 App$$Lambda$1 类实现了lambda表达式所使用的函数式接口Predicate,而且在实现抽象方法test的内部调用了生成的 lambda$main$0 方法。
以上就是实现一个lambda表达式的全过程。

posted @ 2020-01-10 10:21  猿记ATALL  阅读(404)  评论(0编辑  收藏  举报