Java8 Lambda 基础概念

2019-07-03 00:00:00 概念 基础 Java8

最近一段时间重新看了一下Java8 Lambda相关知识,纠正了之前一些错误的认识,也得到了一些新认知。打算把这些所见所得写下来,权当加深自己的认识同时也能帮助一些有需要的人。ok,下面直接进入正题。

什么是Lambda表达式

Lambda表达式是一种匿名函数,维基百科关于匿名函数的定义如下:

https://zh.wikipedia.org/wiki/%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0zh.wikipedia.org

另一个与Lambda有关的概念是λ演算

https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97zh.wikipedia.org

因为是非数学专业的学渣,Lambda表达式严谨的数学定义我是给不出来。这里就以实际应用中程序的一般用法来解释就是编程中当我们需要一个功能函数时,又不想在代码中去声明它,我们就用Lamba表达式去实现一个匿名函数。比如在Java中我们想临时给int[]数组每个元素加1,用Lambda实现的话可以这样:

int[] array = {1,2,3,4,5,6};
array = Arrays.stream(array).map(i -> i+1).toArray();

其中 i -> i+1就是一个Lambda表达式。

简单来说我觉得Lambda表达式就是形式化定义了什么是一个可计算函数,这里的计算指不仅仅是数学上的运算,也可以是各种逻辑操作。比如上面的例子,i是计算的数据,i + 1是计算的规则。如果不用Lambda表达式,我们改用普通代码的形式:

int addIncrement(int i){
        return i + 1;
 }

相比于i -> i + 1 就没有那么的直接明当,因为缺乏了对计算规则的抽象和归纳,相反这是Lambda表达式的优势。这也是为什么会用Labmda表达式写出的代码会更简洁的同时代码却具有更好的可读性。

Lambda表达式语法

Lambda表达式语法简单来说可以抽象为以下两种:

args -> expr
args -> {return expr;}

整个表达式由三个部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块

代码例子:

() -> {System.gc();return 0;};
(Thread t) -> t.start();
(int i , int j) -> return i + j;

当形式参数只有一个时,()不是必须的。相反当参数的个数不为1时,()是一定要的。参数的具体类型也不需要具体指定,如果编译器可以根据程序上下文进行类型推断。 方法体有多个语句时必须用{}包裹。如果只有一个语句,不管是否有返回值,都可以不用{}包裹,一般不推荐用{}包裹。

Lambda表达式 VS 匿名内部类

曾经有一段时间Lambda表达式一度被一些人误认为是inner class(匿名内部类)的一个语法糖,之所以被认为的原因是Lambda表达式的实现方式是以匿名对象的形式实现的,虽然我们把Lambda表达式称为匿名函数。但其实深入了解后我们发现Lambda表达式与匿名内部类是有明显区别的。

定义一个内部类

public class Outer {

    class Inner{

    }
    
    public static void main(String[] args){
        Outer outer = new Outer();
        Inner inner = outer.new Inner();
        
    }

}

我们用程序Debug

《Java8 Lambda 基础概念》
《Java8 Lambda 基础概念》

我们发现了inner实例内部其实会有一个this$0指针是指向外部类的实例outer。而我们熟悉的this指针是指向了inner实例本身

我们再定义一个Lambda表达式

public class Outer {

    private int i = 10;
    Runnable runnable1 = () -> System.out.println("this.i = " +this.i);
    Runnable runnable2 = () -> {};



    public static void main(String[] args){
        Outer outer = new Outer();
        outer.runnable1.run();
        outer.runnable2.run();
    }

}

运行结果:

this.i =  10

说明了Lambda的this指针并不是指向自身而是指向了外部,这里是outer1对象。这时我们Debug一下程序

《Java8 Lambda 基础概念》
《Java8 Lambda 基础概念》

发现outer对象的引用赋值到了runnable1.this指针上的同时,又赋值给了arg$1的变量。但同时runnable2字段上面却没任何字段引用outer对象。

我们再修改一下代码

public class Outer {

    public static void main(String[] args){
        Outer outer1 = new Outer();
        Outer outer2 = new Outer();
        Outer outer3 = new Outer();
        Runnable runnable = () -> System.out.println(outer1.toString() + outer2.toString());
        runnable.run();

    }

}

此时再Debug

《Java8 Lambda 基础概念》
《Java8 Lambda 基础概念》

发现了outer1被赋值给了arg$1, outer2赋值给了arg$2,而outer3没有被引用。

我们看到无论是在类结构还是在方法中定义Lambda,获取外部变量的方式都通过捕获的方法去获取的。连this指针都是捕获回来的。而同时我们无法在Lambda表达式方法体中去给Lambda对象加任何属性,所有定义出来的变量都是局部变量,也就是说Lambda对象是一个无状态对象,因为没有任何状态是描述Lambda对象本身的,这与函数式编程的无状态性其实是不谋而合。而匿名内部类自身可以定义属性,是一个有状态对象,天生自带了访问外部环境的引用。

所以总的来说我觉得Lambda表达式只是用了一个无状态的匿名对象去尽力实现了一个函数式编程中一个无状态的函数而已,与匿名内部类的当初的设计初衷是完全不一样的。

变量捕获

上面提到变量捕获是Lambda表达式访问外部环境变量的方式,这里的外部环境变量是相对于Lambda方法体内部自己定义的局部变量。在应用编程中我们知道能够被Lambda有效捕获的环境变量都有个特点就是effective final,即变量一量被赋值之后值就不能被改变。并且在Lambda内部这个变量也不能被修改。那为什么会有这样的限定呢?Maurice Naftalin在其著作《Mastering Lambdas: Java Programming in a Multicore World》提到了两个主要原因。

  1. 局部变量的正确性、一致性。

在多线程环境下,线程内局部变量是不存在线程间安全性、可见性的问题,因为局部变量只有当前创建的线程可见。但是Lambda表达式打破了这个限制,因为一个线程创建的Lambda可以传递给别一个线程。比如下面这段代码,真实情况下这段代码是编译不通过会提示variable used in lambda expression should be final or effective final

   public static void main(String[] args){
        Integer j = 100;
        Runnable runnable = () -> { j++; };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
    }

main线程中创建的局部变量j,被runnable捕获了,然后runnable传到了另外的两个线程t1, t2中。也就意味着t1、t2可以窥探main线程创建的局部变量j。如果j不限制是 effective final, t1,t2执行了自己的start方法就能够修改main线程的局部变量,那之前局部变量的线程隔离性就被打破了。并且Maurice Naftalin也提到如果Lambda对象被传递到其它线程情况下,很可能会出现创建线程结束了,但创建的Lambda对象还存活的情况,因为其它线程也还在引用这个对象,这时还允许修改局部变量会引发一些局部变量导致的内存泄露问题。

2.性能

如果不要求变量是effective final,为了保证多线程对可变变量访问的正确性,就需要加入同步操作。这样带来结果就是性能上的下降,同时也违背了引入了labmda并行化处理目标:将不同参数的函数的计算分到不同线程上的策略。甚至从不同线程中读取可变的局部变量都得引入同步操作或使用volatile,从而避免读到旧数据。

所以把变量限制为effective final是有一定考虑的,但在现实中effective final这个限制却是很轻易就可以被打破的,当这个变量是一个对象引用的情况下。根本原因是labmda的effective final只限制了引用不能指向其它对象,没有限制不能修改对象本身字段的属性。比如下面这个例子

public class Outer {


    public static void main(String[] args) {
        A a = new A();
        final Runnable runnable = () -> {
            a.value = 200;
        };

    }

}

class A{
    public int value = 100;
}

虽然可以这样做,但在多线程下还是会存在线程安全的问题,为了保证线程安全就又要引入同步操作,又回到之前的怪圈。所以建议是尽量避免这样做。

关于lambda的东西还有很多,比如类型推判、流与管道、流的分割、性能等等后面有时间再分享。

    原文作者:林林
    原文地址: https://zhuanlan.zhihu.com/p/58606763
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章