Java8 Lambda表达式详解

 

1. 概述


Java 8 引入的 Lambda 表达式的主要作用就是简化部分匿名内部类的写法

能够使用 Lambda 表达式的一个重要依据是必须有相应的函数接口。所谓函数接口,是指内部有且仅有一个抽象方法的接口

Lambda 表达式的另一个依据是类型推断机制。在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。
 

           

  1. 函数式编程:可以理解为将一个函数作为参数值进行传递。
  2. 面向对象是对数据的抽象,即:各种各样的pojo类。函数式编程则是对行为的抽象,将行为作为一个参数进行传递。

Lanbda表达式
可以认为是一种特殊的匿名内部类
lambda只能用于函数式接口
lambda语法:
     ([形参列表,不带数据类型])-> {
     //执行语句
     [return..;]
}
注意:
1、如果形参列表是空的,只需要保留()即可
2、如果没有返回值。只需要在{}写执行语句即可
3、如果接口的抽象方法只有一个形参,()可以省略,只需要参数的名称即可
4、如果执行语句只有一行,可以省略{},但是如果有返回值时,情况特殊。
5、如果函数式接口的方法有返回值,必须给返回值,如果执行语句只有一句,还可以简写,即省去大括号和return及最后的;号。
6、形参列表的数据类型会自动推断,只需要参数名称。
 

2.使用场景

 

在Java8之前,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法.这与Javascript等函数式语言大为不同.

在很多时候,我们想要传递的是行为,而非数据,这就需要Lambda表达式了,下面以两个Lambda表达式典型应用场景为例,演示Lambda表达式的使用.
 

Lambda表达式使用场景1:代替匿名内部类


在GUI编程中大量使用匿名内部类,他们用来定义事件回调的行为,这是一种典型的传递行为而非传递数据的情况,因此可以使用Lambda表达式.

在Java8之前,要定义事件的回调函数,必须向事件传递一个匿名内部类,在该匿名内部类中定义事件的回调行为.

public static void main(String[] args) {
    JFrame jFrame = new JFrame("My JFrame");
    JButton jButton = new JButton("My JButton");

    jButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("Button Pressed");
            System.out.println("some other procedure");
        }
    });

    jFrame.add(jButton);
    jFrame.setVisible(true);
}

在Java8之后,可以使用Lambda表达式定义接口中需要实现的方法,用以替代匿名内部类

public static void main(String[] args) {
    JFrame jFrame = new JFrame("My JFrame");
    JButton jButton = new JButton("My JButton");

    jButton.addActionListener((ActionEvent event) -> {
           System.out.println("Button Pressed");
           System.out.println("some other procedure");
    });

    jFrame.add(jButton);
    jFrame.setVisible(true);
}


Lambda表达式使用场景2:集合操作


使用Java8中Collection类新增的forEach()方法,配合Lambda表达式,可以极大方便集合的遍历操作

在Java5之前,只能使用for循环来遍历集合:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

Java5引入了增强for循环,本质上是调用了迭代器进行迭代:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

for (Integer i : list) {
    System.out.println(list.get(i));
}

使用Java8中Collection类新增的forEach()方法遍历集合更为简洁:

list.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer i) {
        System.out.println(i);
    }
});

forEach()方法接收一个Consumer<T>对象,其accept()方法指定了遍历集合时对元素进行的行为,当然,可以用Lambda表达式代替上面的匿名内部类.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

list.forEach((Integer i) -> {
    System.out.println(i);
});

此处还可以更简略的缩写为: 

list.forEach(i -> System.out.println(i));

上面三种遍历集合的方法都属于外部迭代,使用JDK8新增的流Stream对集合进行内部迭代的效率更高.

 

3.Lambda表达式的语法

 

Java中的Lambda表达式语法如下:

(argument) -> {body}

  1. 格式一: 参数列表 -> 表达式

  2. 格式二: 参数列表 -> {表达式集合}

其中argument部分为函数参数,规定如下:

一个Lambda表达式可以有零个或多个参数.所有的参数必须包含在圆括号内,参数之间用逗号间隔,例如:(a, b)或(int a, int b)或(String a, float b).
参数的类型既可以显式声明,也可以根据上下文来自动推断,例如:(int i)与(i)的效果相同,为保证可读性,可以显式声明类型.当只有一个参数,且参数类型可推导时,圆括号()可省略,例如a -> retuan a*a
空圆括号代表参数集为空,例如:() -> 42.


其中body部分为表达式的主体,规定如下:

表达式的主体可以包含零条或多条语句
如果表达式的主体只有一条语句,花括号{}和return关键字都可以省略,匿名函数的返回值即为该主体表达式计算结果.(expression lambda)
如果Lambda表达式的主体包含一条以上语句,则表达式必须包含在{}代码块中,匿名函数的返回值必须由return语句显式指定,若没有return语句则返回值为空.(statement lambda)

Lambda的使用条件是:使用Lambda表达式必须有接口、且接口中有且仅有一个抽象方法、或必须有“函数式接口”作为方法的参数  函数式接口:只有一个抽象方法(Object类中的方法除外)的接口是函数式接口。

4.依托于函数式接口使用lambda表达式

lambda表达式的使用需要借助于函数式接口,也就是说只有函数式接口出现地方,我们才可以将其用lambda表达式进行简化。

自定义函数式接口

函数式接口定义为只具备 一个抽象方法 的接口。java8在接口定义上的改进就是引入了默认方法,使得我们可以在接口中对方法提供默认的实现,但是不管存在多少个默认方法,只要具备一个且只有一个抽象方法,那么它就是函数式接口,如下(引用上面的AppleFilter):

  1. /**

  2. * 苹果过滤接口

  3. *

  4. * @author zhenchao.wang 2016-09-17 14:21

  5. * @version 1.0.0

  6. */

  7. @FunctionalInterface

  8. public interface AppleFilter {

  9.  
  10. /**

  11. * 筛选条件抽象

  12. *

  13. * @param apple

  14. * @return

  15. */

  16. boolean accept(Apple apple);

  17.  
  18. }

AppleFilter仅包含一个抽象方法accept(Apple apple),依照定义可以将其视为一个函数式接口,在定义时我们为该接口添加了@FunctionalInterface注解,用于标记该接口是函数式接口,不过这个接口是可选的,当添加了该接口之后,编译器就限制了该接口只允许有一个抽象方法,否则报错,所以推荐为函数式接口添加该注解。

jdk自带的函数式接口

jdk为lambda表达式已经内置了丰富的函数式接口,如下表所示(仅列出部分):

函数式接口函数描述符原始类型特化
Predicate<T>T -> booleanIntPredicate, LongPredicate, DoublePredicate
Consumer<T>T -> voidIntConsumer, LongConsumer, DoubleConsumer
Funcation<T, R>T -> RIntFuncation<R>, IntToDoubleFunction, IntToLongFunction<R>, LongFuncation…
Supplier<T>() -> TBooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T>T -> TIntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T>(T, T) -> TIntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L, R>(L, R) -> boolean 
BiConsumer<T, U>(T, U) -> void 
BiFunction<T, U, R>(T, U) -> R 

下面分别就Predicate<T>Consumer<T>Function<T, R>的使用示例说明。

Predicate<T>

  1. @FunctionalInterface

  2. public interface Predicate<T> {

  3.  
  4. /**

  5. * Evaluates this predicate on the given argument.

  6. *

  7. * @param t the input argument

  8. * @return {@code true} if the input argument matches the predicate,

  9. * otherwise {@code false}

  10. */

  11. boolean test(T t);

  12. }

Predicate的功能类似于上面的AppleFilter,利用我们在外部设定的条件对于传入的参数进行校验,并返回验证结果boolean,下面利用Predicate对List集合的元素进行过滤:

  1. /**

  2. * 按照指定的条件对集合元素进行过滤

  3. *

  4. * @param list

  5. * @param predicate

  6. * @param <T>

  7. * @return

  8. */

  9. public <T> List<T> filter(List<T> list, Predicate<T> predicate) {

  10. List<T> newList = new ArrayList<T>();

  11. for (final T t : list) {

  12. if (predicate.test(t)) {

  13. newList.add(t);

  14. }

  15. }

  16. return newList;

  17. }

利用上面的函数式接口过滤字符串集合中的空字符串:

demo.filter(list, (String str) -> null != str && !str.isEmpty());

Consumer<T>

  1. @FunctionalInterface

  2. public interface Consumer<T> {

  3.  
  4. /**

  5. * Performs this operation on the given argument.

  6. *

  7. * @param t the input argument

  8. */

  9. void accept(T t);

  10. }

Consumer提供了一个accept抽象函数,该函数接收参数,但不返回值,下面利用Consumer遍历集合:

 
  1. /**

  2. * 遍历集合,执行自定义行为

  3. *

  4. * @param list

  5. * @param consumer

  6. * @param <T>

  7. */

  8. public <T> void filter(List<T> list, Consumer<T> consumer) {

  9. for (final T t : list) {

  10. consumer.accept(t);

  11. }

  12. }

利用上面的函数式接口,遍历字符串集合,并打印非空字符串:

 
  1. demo.filter(list, (String str) -> {

  2. if (StringUtils.isNotBlank(str)) {

  3. System.out.println(str);

  4. }

  5. });

Function<T, R>

 
  1. @FunctionalInterface

  2. public interface Function<T, R> {

  3.  
  4. /**

  5. * Applies this function to the given argument.

  6. *

  7. * @param t the function argument

  8. * @return the function result

  9. */

  10. R apply(T t);

  11. }

Funcation执行转换操作,输入是类型T的数据,返回R类型的数据,下面利用Function对集合进行转换:

  1. /**

  2. * 遍历集合,执行自定义转换操作

  3. *

  4. * @param list

  5. * @param function

  6. * @param <T>

  7. * @param <R>

  8. * @return

  9. */

  10. public <T, R> List<R> filter(List<T> list, Function<T, R> function) {

  11. List<R> newList = new ArrayList<R>();

  12. for (final T t : list) {

  13. newList.add(function.apply(t));

  14. }

  15. return newList;

  16. }

下面利用上面的函数式接口,将一个封装字符串(整型数字的字符串表示)的接口,转换成整型集合:

demo.filter(list, (String str) -> Integer.parseInt(str));

上面这些函数式接口还提供了一些逻辑操作的默认实现,留到后面介绍java8接口的默认方法时再讲吧~

使用过程中需要注意的一些事情

类型推断

在编码过程中,有时候可能会疑惑我们的调用代码会去具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等做正确的判定
在具体调用时,在一些时候可以省略参数的类型,从而进一步简化代码:

  1. // 筛选苹果

  2. List<Apple> filterApples = filterApplesByAppleFilter(apples,

  3. (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

  4.  
  5. // 某些情况下我们甚至可以省略参数类型,编译器会根据上下文正确判断

  6. List<Apple> filterApples = filterApplesByAppleFilter(apples,

  7. apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

局部变量

上面所有例子我们的lambda表达式都是使用其主体参数,我们也可以在lambda中使用局部变量,如下:

 
  1. int weight = 100;

  2. List<Apple> filterApples = filterApplesByAppleFilter(apples,

  3. apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);

该例子中我们在lambda中使用了局部变量weight,不过在lambda中使用局部变量必须要求该变量 显式声明为final或事实上的final ,这主要是因为局部变量存储在栈上,lambda表达式则在另一个线程中运行,当该线程视图访问该局部变量的时候,该变量存在被更改或回收的可能性,所以用final修饰之后就不会存在线程安全的问题。

5.方法引用

方法引用也是一个语法糖,可以用来简化开发。

在我们使用 Lambda 表达式的时候,如果“->”的右边要执行的表达式只是调用一个类已有的方法,那么就可以用「方法引用」来替代 Lambda 表达式。方法引用通过::将方法隶属和方法自身连接起来

方法引用可以分为 4 类:

方法引用类别语法结构举例
引用静态方法

对象引用::实例方法名

Integer::sum
引用某个对象的方法类名::静态方法名list::add
引用某个类的方法类名::实例方法名String::length
引用构造方法类名::构造方法HashMap::new

 5.1引用静态方法


当我们要执行的表达式是调用某个类的静态方法,并且这个静态方法的参数列表和接口里抽象函数的参数列表一一对应时,我们可以采用引用静态方法的格式。

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])

我们可以简写成如下格式:

类名::静态方法名

注意这里静态方法名后面不需要加括号,也不用加参数,因为编译器都可以推断出来。下面我们继续使用 2.3 节的示例来进行说明。

首先创建一个工具类,代码如下:

public class Utils {
    public static int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

注意这里的 compare() 函数的参数和 Comparable 接口的 compare() 函数的参数是一一对应的。然后一般的 Lambda 表达式可以这样写:

Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));

如果采用方法引用的方式,可以简写成这样:

Collections.sort(list, Utils::compare);


5.2 引用对象的方法


当我们要执行的表达式是调用某个对象的方法,并且这个方法的参数列表和接口里抽象函数的参数列表一一对应时,我们就可以采用引用对象的方法的格式。

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])

我们可以简写成如下格式:

对象引用::方法名

下面我们继续使用 2.3 节的示例来进行说明。首先创建一个类,代码如下:

public class MyClass {
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

当我们创建一个该类的对象,并在 Lambda 表达式中使用该对象的方法时,一般可以这么写:

MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));

注意这里函数的参数也是一一对应的,那么采用方法引用的方式,可以这样简写:

MyClass myClass = new MyClass();
Collections.sort(list, myClass::compare);

此外,当我们要执行的表达式是调用 Lambda 表达式所在的类的方法时,我们还可以采用如下格式:

this::方法名

例如我在 Lambda 表达式所在的类添加如下方法:

private int compare(Integer o1, Integer o2) {
    return o1.compareTo(o2);
}

当 Lambda 表达式使用这个方法时,一般可以这样写:

Collections.sort(list, (o1, o2) -> compare(o1, o2));

如果采用方法引用的方式,就可以简写成这样:

Collections.sort(list, this::compare);


5.3 引用类的方法


引用类的方法所采用的参数对应形式与上两种略有不同。如果 Lambda 表达式的“->”的右边要执行的表达式是调用的“->”的左边第一个参数的某个实例方法,并且从第二个参数开始(或无参)对应到该实例方法的参数列表时,就可以使用这种方法。

可能有点绕,假如我们的 Lambda 表达式符合如下格式:

(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])

那么我们的代码就可以简写成:

变量1对应的类名::实例方法名

还是使用 2.3 节的例子, 当我们使用的 Lambda 表达式是这样时:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

按照上面的说法,就可以简写成这样:

Collections.sort(list, Integer::compareTo);


5.4 引用构造方法


当我们要执行的表达式是新建一个对象,并且这个对象的构造方法的参数列表和接口里函数的参数列表一一对应时,我们就可以采用「引用构造方法」的格式。

假如我们的 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> new 类名([变量1, 变量2, ...])

我们就可以简写成如下格式:

类名::new

下面举个例子说明一下。Java 8 引入了一个 Function 接口,它是一个函数接口,部分代码如下:

@FunctionalInterface
public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
        // 省略部分代码
}

我们用这个接口来实现一个功能,创建一个指定大小的 ArrayList。一般我们可以这样实现:

Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() {
    @Override
    public ArrayList apply(Integer n) {
        return new ArrayList(n);
    }
};
List list = function.apply(10);

使用 Lambda 表达式,我们一般可以这样写:

Function<Integer, ArrayList> function = n -> new ArrayList(n);
1
使用「引用构造方法」的方式,我们可以简写成这样:

Function<Integer, ArrayList> function = ArrayList::new;


6.Lambda 实现原理


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

 

6.1 匿名内部类的实现


匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }).start();
    }
}

编译之后将会产生两个 class 文件:

LambdaTest.class
LambdaTest$1.class
使用 javap -c LambdaTest.class 进一步分析 LambdaTest.class 的字节码,部分结果如下:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: new           #3                  // class com/example/myapplication/lambda/LambdaTest$1
     7: dup
     8: invokespecial #4                  // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
    11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    14: invokevirtual #6                  // Method java/lang/Thread.start:()V
    17: return

可以发现在 4: new #3 这一行创建了匿名内部类的对象。

 

6.2 Lambda 表达式的实现


接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello World")).start();
    }
}

此时编译后只会产生一个文件 LambdaTest.class,再来看看通过 javap 对该文件反编译后的结果:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
     9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    12: invokevirtual #5                  // Method java/lang/Thread.start:()V
    15: return

从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用

因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic 指令实现的,并且书写 Lambda 表达式不会产生新的类

既然 Lambda 表达式不会创建匿名内部类,那么在 Lambda 表达式中使用 this 关键字时,其指向的是外部类的引用

在Lambda表达式中this的意义跟在表达式外部完全一样。因此下列代码将输出两遍Hello Hoolee,而不是两个引用地址。

public class Hello {
	Runnable r1 = () -> { System.out.println(this); };
	Runnable r2 = () -> { System.out.println(toString()); };
	public static void main(String[] args) {
		new Hello().r1.run();
		new Hello().r2.run();
	}
	public String toString() { return "Hello Hoolee"; }
}

7.优缺点

优点:

  • 可以减少代码的书写,减少匿名内部类的创建,节省内存占用。
  • 使用时不用去记忆所使用的接口和抽象函数。

缺点:

  • 易读性较差,阅读代码的人需要熟悉 Lambda 表达式和抽象函数中参数的类型。
  • 不方便进行调试。

 

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页