Java 8 新特性:Lambda 表达式


Lambda 表达式 


文 | 莫若吻     


注:此文乃个人查找资料然后学习总结的,若有不对的地方,请大家指出,非常感谢!另外,知识都有串联,如果某一处看不懂,就先接着往下看,之后再回头看不明白的地方就会恍然大悟了。


一.为什么Java 需要Lambda 表达式?

如果忽视注解(Annotations)、泛型(Generics)等特性,自 Java 语言诞生时起,它的变化并不大。Java 一直都致力维护其对象至上的特征,在使用过JavaScript 之类的函数式语言之后,Java 如何强调其面向对象的本质,以及源码层的数据类型如何严格变得更加清晰可感。其实,函数对Java 而言并不重要,在 Java 的世界里,函数无法独立存在。

然而,在函数式编程语言中,函数是一等公民,它们可以独立存在,你可以将其赋值给一个变量,或将他们当做参数传给其他函数。JavaScript 是最典型的函数式编程语言。函数式语言提供了一种强大的功能——闭包,相比于传统的编程方法有很多优势,闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。Java 现在提供的最接近闭包的概念便是 Lambda 表达式,虽然闭包与 Lambda 表达式之间存在显著差别,但至少 Lambda 表达式是闭包很好的替代者。

Lambda 表达式为 Java 添加了缺失的函数式编程特点,使我们能将函数当做一等公民看待。尽管不完全正确,我们很快就会见识到 Lambda 与闭包的不同之处,但是又无限地接近闭包。在支持一类函数的语言中,Lambda 表达式的类型将是函数。但是,在 Java 中,Lambda 表达式是对象,他们必须依附于一类特别的对象类型——函数式接口(functional interface)。


二.函数式接口

函数式接口是只包含一个抽象方法的接口。函数式接口有时候被称为SAM类型,意思是单抽象方法(Single Abstract Method)。

一般来说,这个抽象方法指明了接口的目标用途。因此,函数式接口通常表示单个动作。

eg:标准接口Runnable是一个函数式接口,因为它只定义了一个方法run();因此,run()定义了Runnable的动作。

此外,函数式接口定义了lambda表达式的目标类型。但lambda表达式只能用于其目标类型已经被指定的上下文中。

同时,Java 8引入了一个新的注解:@FunctionalInterface。

可以在任意函数式接口上面使用 @FunctionalInterface 来标识它是一个函数式接口,但是该注解不是强制的。


1)当你注释的接口不是有效的函数式接口时,可以使用 @FunctionalInterface 解决编译层面的错误。

eg:

自定义一个函数式接口:

@FunctionalInterface
public interface MyTestInterface {
    public void doSomeThing();
}

据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。

错误写法:

@FunctionalInterface
public interface MyTestInterface {
    public void doSomeThing();
    public void doMoreThing();
}

提示错误信息:



2)lambda表达式被转换成一个函数式接口的实例时,需要注意处理检查时异常

eg:

Runnable runnable = () -> {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
 };

若不加 try catch 语句,赋值语句就会编译错误,因为Runnablerun方法是没有异常抛出的。

3)Callable 是可以抛出任何异常,并且有返回值,当需要不返回任何数据时可这样定义:

Callable<Void> callable = () -> {
            System.out.println("zzzzz");
            return null;
};

Note:

1)函数式接口可以定义Object定义的任何公有方法,例如equals(),而不影响其作为"函数式接口"的状态。Object的公有方法被视为函数式接口的隐式成员,因为函数式接口的实例会默认自动实现它们。

2)默认方法静态方法(后面会具体解释)不会违反函数接口的约定。


三.Lambda表达式 

1.简介

在Java中,Lambda 表达式 (lambda expression)是一个匿名函数。

Lambda表达式基于数学中的λ演算得名,直接对应于其中的Lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包,但又不同于函数式语言的闭包。Lambda表达式让代码变得简洁并且允许你传递行为,在java8出现之前传递行为的方法只有通过匿名内部类。

Lambda表达式的Java实现:第一个就是Lambda表达式自身,第二个是函数式接口。

Lambda表达式本质上就是一个匿名(即未命名的方法)。但是这个方法是不能独立执行的,而是用于实现由函数式接口定义的一个方法(即:使用 Lambda 表达式实例化函数式接口)。因此,Lambda表达式会导致产生一个匿名类。


Lambda表达式不是独立执行的,而是构成了一个函数式接口定义的抽象方法的实现,该函数式接口定义了它的目标类型。结果,只有在定义了lambda表达式的目标类型的上下文中,才能使用该表达式。当把一个lambda表达式赋给一个函数式接口的引用时,就创建了这样的上下文。

当目标类型上下文中出现lambda表达式时,就会自动创建实现了函数式接口的一个类的实例(类似于匿名类),函数式接口声明的抽象方法的行为由lambda表达式定义。当通过目标调用该方法时,就会执行lambda表达式。

为了在目标类型上下文中使用lambda表达式,抽象方法的类型和lambda表达式的类型必须兼容。eg:如果抽象方法指定了两个int类型的参数,那么lambda表达式也必须执行两个参数,其类型要么被显示指定为int类型,要么在上下文中可以被隐式的推断为int类型。总的来讲lambda表达式的参数的类型和数量必须与函数式接口内的抽象方法的参数兼容;返回类型必须兼容;并且lambda表达式可能抛出的异常必须能被该方法接受。


详情参考 JDK英文文档:Lambda Expressions & Virtual Extension Methods

Translation of Lambda Expressions 


2.优点

Lambda表达式的应用使代码变得更加紧凑,可读性增强;Lambda表达式使并行操作大集合变得更方便,可以充分发挥多核CPU的优势,更易于为多核处理器编写代码


3.组成(或定义)

Lambda 表达式由三个部分组成:

第一部分 为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;

第二部分 为一个箭头符号:->

第三部分 为方法体,可以是表达式和代码块。

语法:

1)方法体为表达式,该表达式的值作为返回值返回。

(parameters)-> expression

2)方法体为代码块,必须用{}来包裹起来,且需要一个return返回值,但若函数式接口里面方法返回值是void,则无需返回值。

(parameters) -> { statements; }

Note:

1) 一个Lambda表达式可以有零个或多个参数
2) 参数的类型既可以明确声明,也可以根据上下文来推断。
eg:(int a)与(a)效果相同
3) 所有参数需包含在圆括号内,参数之间用逗号相隔。
eg:(a, b)  (int a, int b) (String a, int b, float c)
4) 空圆括号代表参数集为空。
eg:() -> 50
5) 当只有一个参数,且其类型可推导时,圆括号()可省略。
eg:a -> return a*a
6) Lambda表达式的主体可包含零条或多条语句
7) 如果Lambda表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致。

8)  如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。

9)对于Lambda表达式中的多个参数,如果需要显示声明一个参数的类型,那么必须为所有的参数都声明类型。

eg:MyNumericTest是一个自定义函数式接口,详情接口代码略,看下面应用的例子:

这样写不合法:MyNumericTest isFactor = (int n,d) -> (n%d)==0;

正确的写法:MyNumericTest isFactor = (int n,int d) -> (n%d)==0;



Lambda写法示例:

eg:

1)左边是指定类型的逗号分割的输入列表,右边是带有return的代码块:

(intx, inty) -> { returnx + y; }

2)左边是推导类型的逗号分割的输入列表,右边是返回值:

(x, y) -> x + y

3)左边是推导类型的单一参数,右边是一个返回值:

x -> x * x

4)左边没有输入 (官方名称: "burger arrow"),在右边返回一个值:

() -> x

5)左边是推导类型的单一参数,右边是没返回值的代码块(返回void):

x -> { System.out.println(x); }

6)静态方法引用:(注:第一次看不懂没关系,后面会提到这种用法

String::valueOf

7)非静态方法引用:

Object::toString

8)继承的函数引用:

x::toString

9)构造函数引用

ArrayList::new


4.类型推断

在Lambda表达式中,我们不需要明确指出参数类型,javac编译器会通过上下文自动推断参数的类型信息。根据上下文推断类型的行为称为类型推断

Java8提升了Java中已经存在的类型推断系统,使得对Lambda表达式的支持变得更加强大。javac会寻找紧邻lambda表达式的一些信息通过这些信息来推断出参数的正确类型。

Note:在大多数情况下,javac会根据上下文自动推断类型。假设因为丢失了上下文信息或者上下文信息不完整而导致无法推断出类型,代码编译就不会通过。


详情请点击《JDK8新特性:泛型的目标类型推断》:

http://blog.csdn.net/sun_promise/article/details/51323241


JDK英文文档:Generalized Target-Type Inference


5.Lambda 表达式的使用示例

注:下面的例子若暂时看不懂,可通篇博客看完后再回过来看示例,第一次看难免会觉得不理解。

1)无参Lambda表达式:

//函数式接口 
    interface MyNumber
    {
        double getValue();
    }

    class lambdaDemo
    {
        public static void main(String[] args)
        {
            MyNumber myNum;

            myNum = ()->123.45;
            System.out.println("A fixed value: "+myNum.getValue());

            myNum = ()->Math.random()*100;
            System.out.println("A random value: "+myNum.getValue());

            //下面情况是:lambda表达式的返回值类型与函数式接口中抽象函数的类型不匹配。
			// myNum = ()->"123.03"    //error! 
        }
    }

2)带参数的lambda表达式:

interface NumericTest
    {
        boolean test(int n);
    }

    class lambdaDemo2
    {
        public static void main(String[] args)
        {
            NumericTest isEven = (n)->(n%2)==0;
            if(isEven.test(10)){
				System.out.println("10 is even");
			}
            if(!isEven.test(9)){
				System.out.println("9 is not even");
			}

            NumericTest isNonNeg = (n)-> n>=0;
            if(isNonNeg.test(1)){
				System.out.println("1 is non-negative");
			}
            if(!isNonNeg.test(-1)){
				System.out.println("-1 is negative");
			}
        }
    }


3)接受两个参数的lambda表达式:

interface NumericTest2
    {
        boolean test(int n,int d);
    }

    class lambdaDemo3
    {
        public static void main(String[] args)
        {
            //测试一个数字是否是另一个数字的因子。
            NumericTest2 isFactor = (n,d) -> (n%d)==0;

            if(isFactor.test(10,2)){ 
				System.out.println("2 is a factor of 10");
			}
            if(!isFactor.test(10,3)){ 
				System.out.println("3 is not a factor of 10");
			}
        }
    }


4)线程优化:

原线程:

 new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from thread");
            }
        }).start();

使用Lambda表达式:

new Thread(
                () -> System.out.println("Hello from thread")
        ).start();

5)事件处理

向一个 UI 组件添加 ActionListener

原java写法:

button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("The button was clicked using old java code!");
            }
        });

使用Lambda表达式:

button.addActionListener((e) -> {
            System.out.println("The button was clicked. From Lambda expressions !");
        });

6)打印出给定数组中的所有元素

Note:使用 Lambda 表达式的方法不止一种。

原java写法:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer n : list) {
            System.out.println(n);
        }
使用Lambda表达式:

第一种方式:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(n -> System.out.println(n));

第二种方式:(这里用到了方法引用)

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(System.out::println);

7)使用断言(Predicate)函数式接口创建一个测试,并打印所有通过测试的元素

代码:

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class LambdaTest {
    public static void main(String[] a) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.println("all numbers:");
        evaluate(list, (n) -> true);

        System.out.println("no numbers:");
        evaluate(list, (n) -> false);

        System.out.println("even numbers:");
        evaluate(list, (n) -> n % 2 == 0);

        System.out.println("odd numbers:");
        evaluate(list, (n) -> n % 2 == 1);

        System.out.println("numbers greater than 5:");
        evaluate(list, (n) -> n > 5);
    }
    public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
        for(Integer n: list)  {
            if(predicate.test(n)) {
                System.out.println(n + " ");
            }
        }
    }
}
结果显示:

all numbers: 1 2 3 4 5 6 7 
no numbers: 
even numbers: 2 4 6 
odd numbers: 1 3 5 7 
numbers greater than 5: 6 7

8)Lambda 表达式打印数值中每个元素的平方

以下使用了 .stream() 方法将常规数组转化为流。Java 8 新增加了流 APIs。java.util.stream.Stream接口包含许多有用的方法,能结合 Lambda 表达式产生非常棒的效果。我们将 Lambda 表达式 x -> x*x 传给 map() 方法,该方法会作用于流中的所有元素。之后,我们使用 forEach 方法打印数据中的所有元素。

原java实现方式:

 List<Integer> list = Arrays.asList(0,1,2,3,4,5);
        for(Integer n : list) {
            int x = n * n;
            System.out.println(x);
        }

使用Lambda表达式和Java8新特性 流 实现方式:

List<Integer> list = Arrays.asList(0,1,2,3,4,5);
        list.stream().map((x) -> x*x).forEach(System.out::println);

9)计算给定数值中每个元素平方后的总和

原java实现方式:

 List<Integer> list = Arrays.asList(1,2,3,4,5);
        int sum = 0;
        for (Integer n : list) {
            int x = n * n;
            sum = sum + x;
        }
        System.out.println(sum);

使用Lambda表达式:

List<Integer> list = Arrays.asList(1,2,3,4,5);
        int sum = list.stream().map(x -> x * x).reduce((x, y) -> x + y).get();
        System.out.println(sum);


6.块Lambda表达式

显示的Lambda方法体内只包含单个表达式,这种类型的Lambda体被称为表达式体,具有表达式体的Lambda表达式可以被称为表达式Lambda。在表达式体中,操作符右侧的代码必须包含单独一个表达式。

Java支持另外一种类型的Lambda表达式,其中操作符右侧的代码可以由一个代码块构成,其中可以包含多条语句。这种类型的Lambda体被称为块体。具有块体的Lambda表达式有时候被称为Lambda

Lambda表达式扩展了Lambda表达式内部可以处理的操作类型,因为它允许Lambda体包含多条语句。创建块Lambda很容易,只需要使用花括号包围Lambda体。

在块Lambda中必须显示使用return语句来返回值。必须这么做,因为块Lambda体代表的不是一个单独的表达式。

Note:当lambda表达式中出现return语句时,只是从lambda体返回,而不会导致包围lambda体的方法返回。

eg:使用块lambda来计算并返回一个int类型值的阶乘

interface NumericFunc {
    inf func(int n);
}
class BlockLambdaDemo {
    public static void main(String[] args) {
        NumericFunc factorial = (n) ->
        {
            int result = 1;
            for (int i = 1; i <= n; i++) {
                result = i * result;
            }
            return result;
        };
        System.out.println("The factorial of 3 is " + factorial.func(3));
        System.out.println("The factorial of 5 is " + factorial.func(5));
    }
}


7.泛型函数式接口

Lambda表达式自身不能指定类型参数。因此,Lambda表达式不能是泛型(当然,由于存在类型推断,所有Lambda表达式都展现出了一些类似于泛型的特征)。但是,与Lambda表达式关联的函数式接口可以泛型。此时,Lambda表达式的目标类型部分由声明函数式接口引用时指定的参数类型决定。


eg:

interface SomeFunc<T> {
    T func(T t);
}
class GenericFunctionalInterfaceDemo {
    public static void main(String[] args) {
        SomeFunc<String> reverse = (str) ->
        {
            int i;
            String result = "";
            for (i = str.length() - 1; i >= 0; i--) {
                result += str.charAt(i);
            }
            return result;
        };
        System.out.println("lambda reserved is " + reverse.func("lambda"));
        
        SomeFunc<Integer> factorial = (n) ->
        {
            int result = 1;

            for (int i = 1; i <= n; i++) {
                result = result * i;
            }
            return result;
        };
        System.out.println("The factorial of 3 is " + factorial.func(3));
    }
}

结果:

lambda reserved is adbmal
The factorial of 3 is 6


分析:

T指定了func()函数的返回类型和参数类型。这意味着它与任何 只接收一个参数,并返回一个相同类型的值的lambda表达式兼容。

SomeFunc接口用于提供对两种不同类型的lambda表达式的引用。第一种表达式使用String类型,第二种表达式使用Integer类型。因此,同一个函数式接口可以用于reserve lambda表达式和factorial lambda表达式。区别仅在于传递给SomeFunc的参数类型。


8.作为参数传递的Lambda表达式

为了将lambda表达式作为参数传递,接收lambda表达式的参数的类型必须是与该lambda表达式兼容的函数式接口的类型。

eg:Lambda表达式 作为方法参数使用

//Use lambda expressions as an argument to method
interface StringFunc {
    String func(String n);
}
class lambdasAsArgumentsDemo {
    static String stringOp(StringFunc sf, String s) {
        return sf.func(s);
    }
    public static void main(String[] args) {
        String inStr = "lambda add power to java";
        String outStr;
        System.out.println("Here is input string: " + inStr);
        //Lambda表达式 作为方法参数使用
        //第一种方式
        outStr = stringOp((str) -> str.toUpperCase(), inStr);
        System.out.println("The string in uppercase: " + outStr);
        //第二种方式
        outStr = stringOp((str) ->
        {
            String result = "";
            for (int i = 0; i < str.length(); i++) {
                if (str.charAt(i) != '') {
                    result += str.charAt(i);
                }
            }
            return result;
        }, inStr);
        System.out.println("The string with spaces removed: " + outStr);
        //第三种方式
        //当块lambda看上去特别长,不适合嵌入到方法的调用中时,很容易把块lambda赋给一个函数式接口变量.
        //然后,可以简单地把该引用传递给方法。
        StringFunc reverse = (str) ->
        {
            String result = "";
            for (int i = str.length() - 1; i >= 0; i--) {
                result += str.charAt(i);
            }
            result result;
        };
        System.out.println("The string reserved: " + stringOp(reverse, inStr));
    }
}

输出结果:

Here is input string: lambda add power to java
The string in uppercase: LAMBDAS ADD POWER TO JAVA 
The string with spaces removed: lambdaaddpowertojava
The string reserved: avaJ ot rewop dda sadbmal

分析:

首先注意stringOp()方法。它有两个参数,第一个参数的类型是StringFunc,而StringFunc是一个函数式接口。因此,这个参数可以接受对任何StringFunc实例的引用,包括由lambda表达式创建的实例。stringOp的第二个参数是String类型,也就是要操作的字符串。接下来,注意对stringOp()的第一次调用,如下所示:
outStr = stringOp((str)->str.uppercase(),inStr);

这里,传递了一个简单的表达式lambda作为参数。这会创建函数式接口StringFunc的一个实例,并把对该实例的一个引用传递给stringOp()方法的第一个参数这就把嵌入在一个类实例中的lambda代码传递给了方法。目标类型上下文由参数的类型决定。因此lambda表达式与该类型兼容,调用是合法的。

当块lambda看上去特别长,不适合嵌入到方法的调用中时,很容易把块lambda赋给一个函数式接口变量,正如上面代码中那样。然后,可以简单地把该引用传递给方法。


9.为什么抽象类不能通过利用lambda实例化

注:此处可以忽略.在网上看到有这样的说法,目前此处本人也不是很清晰,先记录下来,以后明白了再补充。

抽象类,哪怕只声明了一个抽象方法,也不能使用lambda来实例化。
下面有两个类 OrderingCacheLoader,都带有一个抽象方法,摘自于Guava库。如果能够声明它们的实例,像这样使用lambda表达式么?
Ordering<String> order = (a, b) -> ...;
CacheLoader<String, String> loader = (key) -> ...;
 

1这样做会增加阅读lambda的难度。

以这种方式实例化一段抽象类将导致隐藏代码的执行:抽象类的构造方法。

2它抛出了lambda表达式可能的优化。

在未来,它可能是这种情况,lambda表达式都不会计算到对象实例。放任用户用lambda来声明抽象类将妨碍像这样的优化。


此外,有一个简单地解决方法。事实上,上述两个摘自Guava库的实例类已经证明了这种方法。增加工厂方法将lambda转换成实例。
Ordering<String> order = Ordering.from((a, b) -> ...);

CacheLoader<String, String> loader = CacheLoader.from((key) -> ...);


10.lambda表达式是否只是一个匿名内部类的语法

答案是NO。原因有两点:

· 性能影响: 假如lambda表达式是采用匿名内部类实现的,那么每一个lambda表达式都会在磁盘上生成一个class文件。当JVM启动时,这些class文件会被加载进来,因为所有的class文件都需要在启动时加载并且在使用前确认,从而会导致JVM的启动变慢。

· 向后的扩展性: 如果Java8的设计者从一开始就采用匿名内部类的方式,那么这将限制lambda表达式未来的使发展范围。


11.匿名类与 lambda表达式的区别:

1)在匿名类中,this 指代的是匿名类本身;而在lambda表达式中,this指代的是lambda表达式所在的这个类。

2)lambda表达式的类型是由上下文决定的,而匿名类中必须在创建实例的时候明确指定。

3)Lambda 表达式的编译方法是:Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数,它使用 invokedynamic 指令( Java 7 ,即动态启用)动态绑定该方法。


四.Lambda表达式在Java8中的运行机制

Lambda表达式的类型是一些类似于Comparator的接口。 但并不是每个接口都可以使用Lambda表达式,只有那些仅仅包含一个非实例化抽象方法的接口才能使用Lambda表达式。这样的接口被称为函数式接口。并且它们能够被@FunctionalInterface注解注释。Runnable接口就是一个典型的函数式接口的。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface注解不是必须的,但它能够让工具知道这一个接口是一个函数式接口并表现有意义的行为。

eg:试着编译一个用@FunctionalInterface注释自己并且含有多个抽象方法的接口,编译就会报错Multiple non-overriding abstract methods found

同样的,若给一个不含有任何方法的接口添加@FunctionalInterface注解,会报错No target method found.

Lambda表达式是采用动态启用Java7来延迟在运行时的加载策略javac编译代码时,它会捕获代码中的Lambda表达式并且生成一个动态启用的调用地址(称为Lambda工厂)。当动态启用被调用时,就会向Lambda表达式发生转换的地方返回一个函数式接口的实例。然后Lambda表达式的内容转换到一个将会通过动态启用来调用的方法中。在这一步中,JVM实现者有自由选择策略的权利。


五.Lambda 作用域

在lambda表达式中访问外层作用域和旧版本的匿名对象中的方式很相似。可以直接访问标记了final的外层局部变量,或者实例的字段(即:成员变量)以及静态变量

Note:Lambda表达式不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。Lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。此外,this关键字及其引用,在Lambda表达式内部和外部也拥有相同的语义。

欲看详情请点击:Lambda 作用域

注:鉴于此篇博客文字篇幅过长,容易视觉疲劳,第一次学习也容易被吓到等等原因,故将此详情内容单独写了篇博客。


六.捕获和非捕获的Lambda表达式

在lambda表达式中,可以访问其外层作用域定义的变量。
eg:lambda表达式可以使用其外层定义的实例或静态变量。lambda表达式也可以显示或隐式地访问this变量,该变量引用lambda表达式的外层类的调用的实例。因此,lambda表达式可以获取或设置其外层类的实例或静态变量的值,以及调用其外层类定义的方法。


Lambda表达式是否是捕获的和性能相关。一个非不捕获的Lambda通常比捕获的更高效,虽然这一点目前没有书面的规范说明(据我所知),而且也不能为了程序的正确性指望它做什么,非捕获的Lambda只需要计算一次. 然后每次使用到它都会返回一个唯一的实例。而捕获的lambda表达式每次使用时都需要重新计算一次,而且从目前实现来看,它非常像实例化一个匿名内部类的实例。


当Lambda表达式访问一个定义在Lambda表达式体外的非静态变量或者对象时,这个Lambda表达式称为“捕获的”。

当lambda表达式使用其外层作用域定义的局部变量时,会产生一种特殊的情况,称为变量捕获在这种情况下,lambda表达式只能使用实质上final的局部变量。实质上final的变量是指在第一次赋值以后,值不再发生变化的变量(即在后面的程序中没有改变变量的值)。没有必要显示地将这种变量声明为final,不过声明为final也不是错误(Note:外层作用域的this参数自动是实质上final变量,lambda表达式没有自己的this参数)。

eg:下面这个Lambda表达式捕捉了变量x

int x = 5; return y -> x + y;

为了保证这个Lambda表达式声明是正确的,被它捕获的变量必须是“有效final”的。所以要么它们需要用final修饰符号标记,要么保证它们在赋值后不能被改变。


Lambda表达式不能修改外层作用域内的局部变量。因为修改局部变量会移除其实质上的final状态,从而使捕获该变量变得不合法。

eg:演示实质上的final的局部变量和可变局部变量的区别

interface MyFunc {
    int func(int n);
}
class VarCapture {
    public void testMyVarCapture(){
        int num = 10;
        MyFunc mylambda = n -> {
            //这里num的使用是正确的,它不修改num变量。
            int v = num + n;
            //但是,下面的num表达式是非法的,因为它试图修改num的值。
            //  num++;
            return v;
        };
        //下面的也会导致一个错误,因为这将消除num的有效的final状态
        //  num = 9;
    }
}

分析:

num实质是final变量,所有可以在mylambda内使用。但是,如果修改了num,不管是在lambda表达式内还是表达式外,num就会丢失其实质上final的状态。这会导致发生错误,程序将无法通过编译。

Note:lambda表达式可以使用和修改调用其调用类的实例变量,只是不能使用其外层作用域内的局部变量,除非该变量实质上是final变量


七.Lambdas不能做的事

记住:有一些Lambdas不提供的特性。为了Java 8,它们被考虑到了,但是没有被包括进去,由于简化以及时间限制的原因。

1.Non-final* 变量捕获 

如果一个变量被赋予新的数值,它将不能被用于lambda之中。"final"关键字不是必需的,但变量必须是“有效final”的(上面讨论过)。下面代码不会被编译:

eg:

int count = 0;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
    count++; // error: 不能修改count值
});

2.例外的透明度 

如果一个已检测的例外可能从Lambda内部抛出,功能性的接口也必须声明已检测例外可以被抛出。这种例外不会散布到其包含的方法。下面代码不会被编译:

即:方法上抛出的异常无法作用给Lambda表达式内部出现的异常,Lambda表达式内部出现的异常必须单独处理。

eg:

    void appendAll(Iterable<String> values, Appendable out) throws IOException { // doesn't help with the error
        values.forEach(s -> {
            out.append(s); // error: can't throw IOException here 
            // Consumer.accept(T) doesn't allow it
        });
    }

有绕过这个的办法,可以定义自己的功能性接口,扩展Consumer的同时通过像RuntimeException类抛出 IOException。

3.控制流程 (break, early return)

在上面的 forEach例子中,传统的继续方式有可能通过在Lambda之内放置 "return;"来实现。但是,没有办法中断循环或者从Lambda中通过包含方法的结果返回一个数值。

eg:

final String secret = "foo"; 
boolean containsSecret(Iterable<String> values) {
    values.forEach(s -> { 
		if (secret.equals(s)) {
            ??? // want to end the loop and return true, but can't 
			}
    });
}


八.函数式通用接口的出现

想使用 Lambda 表达式,需要定义一个函数式接口,这样往往会让程序充斥着过量的仅为 Lambda 表达式服务的函数式接口。为了减少这样过量的函数式接口,Java 8 在 java.util.function 中增加了不少新的函数式通用接口,有UnaryOperator<T>,BinaryOperator<T>,Consumer<T>,Supplier<T>,Function<T,R>,Predicate<T>等等。

1.Predicate<T> :将 T 作为输入。用来定义对一些条件的检查。Predicate接口有一个叫test的方法,它需要一个T类型的值,返回值为布尔类型。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(与、或、非)。

eg:在一个names列表中找出所有以a开头的name,可以这样使用Predicate

Predicate<String> nameWithA = name -> name.startsWith("a");

2.Consumer<T>:将 T 作为输入,不返回任何内容,表示在单个参数上的操作。

用于表现那些不需要产生任何输出的行为。Consumer接口中有一个叫做accept的方法,它需要一个T类型的参数并且没有返回值。

eg:输出指定信息

Consumer<String> messageConsumer = message -> System.out.println(message); 

3.Function<T, R>:将 T 作为输入,返回 R 作为输出,它还包含了与其他函数组合的默认方法。需要一个值并返回一个结果。

eg:如果需要将所有names列表中的name转换为大写,可以像下面这样写一个Function:

Function<String, String> toUpperCase = name -> name.toUpperCase();


4.Supplier<T>: 这个函数式接口不需要传值,但是会返回一个值。

eg: 用来生成唯一的标识符

Supplier<String> uuidGenerator= () -> UUID.randomUUID().toString();

5.BinaryOperator<T>:两个T作为输入,返回一个T作为输出,对于“reduce”操作很有用

这些最原始的特征同样存在。他们以int,long和double的方式提供。

eg:IntConsumer -int作为输入,执行某种动作,没有返回值

这里存在性能上的一些原因,主要是在输入或输出的时候避免装箱和拆箱操作。


九.方法引用

方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。

方法引用详情地址:http://blog.csdn.net/sun_promise/article/details/51190256

注:鉴于此篇博客文字篇幅过长,容易视觉疲劳等等原因,故将此详情内容单独写了篇博客。


十.在AndroidStudio中设置某项目支持使用Java 8 新特性Lambda 表达式

既然了解了Lambda 表达式,那就要在项目中使用,那么又如何设置在项目中支持使用Lambda 表达式呢?

请点击详情地址:http://blog.csdn.net/sun_promise/article/details/51161252

注:鉴于此篇博客文字篇幅过长,容易视觉疲劳等等原因,故将此详情内容单独写了篇博客。




  • 14
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值