Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

参考资料:

关键问题

  • Java 8中的lambda为什么要设计成这样?(为什么要一个lambda对应一个接口?而不是Structural Typing?)

  • lambda和匿名类型的关系是什么?lambda是匿名对象的语法糖吗?

  • Java 8是如何对lambda进行类型推导的?它的类型推导做到了什么程度?

  • Java 8为什么要引入默认方法?

  • Java编译器如何处理lambda?

主要内容

  • lambda表达式(又被成为“闭包”或“匿名方法”)

  • 方法引用和构造方法引用

  • 扩展的目标类型和类型推导

  • 接口中的默认方法和静态方法

1 、背景

Java API中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:

public interface ActionListener {

void actionPerformed(ActionEvent e);

}

这里并不需要专门定义一个类来实现ActionListener接口,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联(inline):

button.addActionListener(new ActionListener) {

public void actionPerformed(ActionEvent e) {

ui.dazzle(e.getModifiers());

}

}

随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的选择,因为:

  • 语法过于冗余

  • 匿名类中的this和变量名容易使人产生误解

  • 类型载入和实例创建语义不够灵活

  • 无法捕获非final的局部变量

  • 无法对控制流进行抽象

上面的多数问题均在Java SE 8中得以解决:

  • 通过提供更简洁的语法和局部作用域规则,Java SE 8彻底解决了问题1和问题2

  • 通过提供更加灵活而且便于优化的表达式语义,Java SE 8绕开了问题3

  • 通过允许编译器推断变量的“常量性”(finality),Java SE 8减轻了问题4带来的困扰

2 函数式接口(SAM类型)

  • ActionListener、Runnable、Comparator等

  • 编译器根据接口格式自行判断;使用@FunctionalInterface注解判断;

除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

  • Predicate<T>——接收T对象并返回boolean

  • Consumer<T>——接收T对象,不返回值

  • Function<T, R>——接收T对象,返回R对象

  • Supplier<T>——提供T对象(例如工厂),不接收值

  • UnaryOperator<T>——接收T对象,返回T对象

  • BinaryOperator<T>——接收两个T对象,返回T对象

3 lambda表达式

  • lambada表达式其实就是一个函数,广泛的被作为一个回调函数使用

  • 解决‘高度问题’:

    // Java 8之前:

    new Thread(new Runnable() {

    @Override

    public void run() {

    System.out.println("Before Java8, too much code for too little to do");

    }

    }).start();

    1

    2

    //Java 8方式:

    new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();


  • 几个lambda表达式:

    (int x, int y) -> x + y

    () -> 42

    (String s) -> { System.out.println(s); }

    • 表达式由参数列表、箭头符号和函数体构成;函数体既可以是一个表达式,也可以是一个语句块

      FileFilter java = (File f) -> f.getName().endsWith("*.java");
      new Thread(() -> {
       
        connectToService();
       
        sendNotification();
       
      }).start();


4 目标类型

  • 编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。

    Callable<String> c = () -> "done";

    Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

    也因此参数列表也不需要再体现参数类型,同时也为了不将‘高度问题’变成‘宽度问题’;

  • 检查是否符合目标类型

    • T是一个函数式接口

    • lambda表达式的参数和T的方法参数在数量和类型上一一对应

    • lambda表达式的返回值和T的方法返回值相兼容(Compatible)

    • lambda表达式内所抛出的异常和T的方法throws类型相兼容

  • 目标类型上下文

    List<Person> ps = ... Stream<String> names = ps.stream().map(p -> p.getName());

  • 在上面的代码中,ps的类型是List<Person>,所以ps.stream()的返回类型是Stream<Person>

  • map()方法接收一个类型为Function<T, R>的函数式接口,这里T的类型即是Stream元素的类型,也就是Person,而R的类型未知。

  • 由于在重载解析之后lambda表达式的目标类型仍然未知,我们就需要推导R的类型。

  • 通过对lambda表达式函数体进行类型检查,我们发现函数体返回String,因此R的类型是String,因而map()返回Stream<String>

  • 转型表达式(Cast expression)可以显式提供lambda表达式的类型,这个特性在无法确认目标类型时非常有用:

    Object o = (Runnable) () -> { System.out.println("hi"); };


5 关于变量的问题

  • 内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖,lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义。

  • java 7中的变量必须被声明为final,才能在内部类中使用;java 8 放宽限制,有效只读类型的变量也允许;简而言之,lambda表达式对值封闭,对变量开放。

int sum = 0; list.forEach(e -> { sum += e.size(); }); // Illegal, close over values

List<Integer> aList = new List<>(); list.forEach(e -> { aList.add(e); }); // Legal, open over variables

对于值的修改,可以使用reduce方法。

6 方法引用

  • 方法引用和lambda表达式拥有相同的特性

    Person[] people = ...
     
    //lambda表达式
    Comparator<Person> byName = Comparator.comparing(p -> p.getName());
    //方法引用
    Comparator<Person> byName = Comparator.comparing(Person::getName);
  • 这里的Person::getName可以被看作为lambda表达式的简写形式。尽管方法引用不一定会把语法变的更紧凑,但它拥有更明确的语义

  • 函数式接口的方法参数对应于隐式方法调用时的参数

    Consumer<Integer> b1 = System::exit;    // void exit(int status)
     
    Consumer<String[]> b2 = Arrays:sort;    // void sort(Object[] a)
     
    Function<String, String> upperfier = String::toUpperCase; // String toUpperCase(String str)
    Predicate<String> isKnown = knownNames::contains; // boolean contains(String str)
  • 方法引用有很多种,它们的语法如下,静态方法和实例方法的引用并无不同,编译器会自己判断调用的方法:

  • 静态方法引用:ClassName::methodName

  • 实例上的实例方法引用:instanceReference::methodName

  • 超类上的实例方法引用:super::methodName

  • 类型上的实例方法引用:ClassName::methodName

  • 构造方法引用:Class::new

  • 数组构造方法引用:TypeName[]::new

7 默认方法和静态接口方法

  • Java SE 7时代为一个已有的类库增加功能是非常困难的

  • 默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)

  • 可以向函数式接口中添加默认方法,而不用担心函数式接口的单抽象方法限制

interface Iterator<E> {

boolean hasNext();

E next();

void remove();

default void forEachRemaining(Consumer<? super E> action) {

Objects.requireNonNull(action);

while (hasNext())

action.accept(next());

}

}

  • 当接口继承其它接口时,我们既可以为它所继承而来的抽象方法提供一个默认实现,也可以为它继承而来的默认方法提供一个新的实现,还可以把它继承而来的默认方法重新抽象化。

  • Java SE 8允许在接口中定义静态方法。这使得我们可以从接口直接调用和它相关的辅助方法(Helper method),

    而不是从其它的类中调用(之前这样的类往往以对应接口的复数命名,例如Collections)。

    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<T, U> keyExtractor) {

    return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));

    }

8 应用示例

  • java 8之前的写法

    List<Person> people = ...

    Collections.sort(people, new Comparator<Person>() {

    public int compare(Person x, Person y) {

    return x.getLastName().compareTo(y.getLastName());

    }

    })

  • 利用lambda表达式去除匿名类

    Collections.sort(people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));

  • 使用Comparator接口提供的静态方法comparing提升代码的抽象程度

    Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));

  • 通过java 8的类型推导机制,进一步简化代码

    Collections.sort(people, comparing(p -> p.getLastName()));

  • 使用方法引用

    Collections.sort(people, comparing(Person::getLastName));

  • 使用List的默认方法代替Collections,无需引入冗余的代码,开发者也无需知道还有Collections.sort方法用于处理List的排序问题。

    people.sort(comparing(Person::getLastName));


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值