Java 8 新特性(1) - Lambda表达式

原创
2018/08/18 20:28
阅读数 154

Lambda 表达式是 Java 8 最重要的新特性之一,它让我们可以用很简洁的代码完成一个功能。

 

01

举个栗子

 

 

我们知道,Java 中编写多线程主要有两种方式 :继承 Thread 类或实现 Runnable 接口。通常情况下如果逻辑比较简单,我们会写出如下代码:

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++)
            System.out.println("Without Lambda Expression");
    }
});

上述代码创建一个匿名类传入 Thread 构造函数里。下面我们看 Java 8 中我们怎么使用 Lambda 表达式简化上面的代码。

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 10; i++)
        System.out.println("With Lambda Expression");
});

 

02

函数式接口

 

在介绍 Lambda 之前,我们需要了解 Java 8 中的一个新的概念:函数式接口。

函数式接口的定义:只包含一个方法的抽象接口。可以使用 @FunctionalInterface 注解,将一个接口标注为函数式接口,该接口只能包含一个抽象方法,如果添加第二个抽象方法的话,会编译失败。

 

像比较常用的 java.lang.Runnable、java.util.concurrent.Callable、java.util.Comparator 等都是只包含一个抽象方法的接口,默认都是函数式接口。

 

函数式接口有如下要求:

  1. 只包含一个抽象方法,如果有继承父接口,则要计算父接口的抽象方法数量。

  2. 可以声明从 java.lang.Object 继承来的方法,如果除去继承自 Object 的抽象方法,如果只有一个抽象方法,则该接口也是函数式接口。

  3. 可以有任意数量的默认方法或静态方法。

  4. @FunctionalInterface 注解可有可无,只要该接口只有一个抽象方法,则该接口默认为函数式接口,这个注解只是检查它是否是一个函数式接口。

 

Java 8 内置的四大核心函数式接口:

函数式接口 方法 用途
Consumer<T>
void accept(T t)
消费性接口,对类型 T 的对象应用操作
Supplier<T>
T get()
供给型接口,返回类型为 T 的对象
Function<T, R>
R apply(T t)
应用型接口,对类型为 T 的对象应用操作,并返回类型为 R 的对象
Predicate<T>
boolean test(T t)
断定型接口,判断类型为 T 的对象是否满足某约束

 

03

语法

 

Lambda 表达式的基本语法如下:

 (parameters) -> { statements; }

 

parameters: 参数,可以是一个或多个参数,不必指定参数类型,类型会自动推断,当只有一个参数是,小括号可省略

statements:语句,可以是一行或多行代码,当只有一行代码,大括号可省略,return 也可省略。

 

下面是我们加法和平方运算的接口:

public interface MathAdd {
    int add(int x, int y);
}
public interface MathSquare {
    int square(int x);
}

我们可以使用下面的方法来使用这两个方法:

MathAdd sum = (int x, int y) -> {return x + y;}; // 语句1
int z = sum.add(1, 2);
// 一个参数可省略参数列表的小括号,只有一行语句的话可以省略大括号
// 和 return 关键字 
MathSquare mul = x -> x*x; // 语句2
int y = mul.square(5);

 

 

我们再看上面的 语句1语句2 ,为什么能够等号右边的语句能够赋值给左边的变量呢?这个问题的本质是:一个 Lambda 表达式的类型是什么?

 

答案是:它的类型是由其上下文推导而来。

 

看下面的例子,有两个接口,方法签名完全一样:

public interface MathAdd {
    int add(int x, int y);
}
public interface MathSub {
    int sub(int x, int y);
}

// 测试方法
@Test
public void test2 () {
    MathAdd sum = (int x, int y) -> {return x + y;};
    MathSub sub = (int x, int y) -> {return x + y;};

    System.out.println("MathAdd.add(2, 4): "+sum.add(2, 4));
    System.out.println("MathSub.sub(2, 4): "+sub.sub(2, 4));
}

上面的代码能顺利编译并执行通过,结果如下:

那么同样的语句  (int x, int y) -> {return x + y;} ,为什么既能是 MathAdd 类型又能是 MathSub 类型呢?奥妙就在于 MathAdd、 MathSub 这两个接口的方法签名一样。

 

下面是 Lambda 表达式的目标类型 T 的条件:

  1. T 是一个函数式接口,只有函数式接口才可以使用 Lambda 表达式。

  2. Lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应,注意:类型必须完全一致,如 Integer 和 int 并不能对应上。

  3. Lambda 表达式的返回值和 T 的方法返回值兼容,这里并不要求返回值类型完全一致,只要类型兼容即可,如 Lambda 表达式返回 Integer 和T 的方法返回 int 也是满足的。

  4. Lambda 表达式内所抛出的异常和 T 的方法 throws 类型相兼容,这和条件3的类似的。

 

 

04

方法引用

 

方法引用是lambda表达式的一种简写形式。 如果 lambda 表达式只是调用一个特定的已经存在的方法,则可以使用方法引用,实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致。

 

语法:T::method

 

使用“::”操作符将方法名和对象或类的名字分隔开来。以下是四种使用情况:

  • 实例对象的成员方法,对象::实例方法

  • 静态方法引用,类::静态方法

  • 实例方法引用,类::实例方法

  • 构造器引用,类::new

 

下面的代码是打印字符串,其中 语句1 可改为 语句2 的形式。

Consumer<String> con = (x) -> System.out.println(x);// 语句1
con.accept("test");
//方法引用,对象::实例方法名
Consumer<String> consumer = System.out::println;// 语句2
consumer.accept("test");

 

方法引用相比 Lambda 更加简洁,能够用更少的代码量实现相同的功能。

 

下面通过一个我们开发中最常见的排序例子说明:

// 原始集合
List<String> list = Arrays.asList("Lakers", "Celtics", "Bulls", "Nets", "Magic", "Warriors", "Spurs", "Rockets", "Thunder");

//  Before java 8
Collections.sort(list, new Comparator<String>() {
    public int compare(String x, String y) {
        return x.compareTo(y);
    }
});
System.out.println("Before java 8 : "+list);

// Lambda expressions
Collections.sort(list, (x, y) -> y.compareTo(x));
System.out.println("Lambda 表达式 : "+list);

// method reference
Collections.sort(list, String::compareTo);
System.out.println("方法引用 : "+list);

输出结果如下:

使用 Lambda 能极大的减少代码量,不仅仅是不需要使用匿名类了,而且由于它的类型推断,也能减少很多 import 语句,但是可读性比较差。

 

总结一下,Lambda 的优缺点:

优点:

  1. 简洁,这可能是最大的优点了,Lambda 能减少非常多的代码量;

  2. 非常方便写并行计算,这一点会在下一篇 Stream API 中讲到;

  3. 外部变量不再必须声明为 final 的了;

  4. Lambda 不会引入新的作用域,比如在 Lambda 中使用 this 和在外部使用 this 是同一个对象。

缺点:

  1. 代码可读性变差,这也是优点1所带来的副作用,更少的代码对机器友好但对开发者不友好;特别是类型推断,如果对 API 不熟悉的话读别人的代码会很艰难;

  2. 调试不友好;

  3. 用途有限,只能用于函数式接口。



  4.  

 

识别二维码关注我

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
1 收藏
1
分享
返回顶部
顶部