Java8新特性第1章(Lambda表达式)

转载请注明出处:https://zhuanlan.zhihu.com/p/20540175java


在介绍Lambda表达式以前,咱们先来看只有单个方法的Interface(一般咱们称之为回调接口):git

public interface OnClickListener {
    void onClick(View v);
}

咱们是这样使用它的:github

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        v.setText("lalala");
       }
});

这种回调模式在各类框架中很是流行,可是像上面这样的匿名内部类并非一个好的选择,由于:数组

  • 语法冗余;框架

  • 匿名内部类中的this指针和变量容易产生误解;ide

  • 没法捕获非final局部变量;函数

  • 非静态内部类默认持有外部类的引用,部分状况下会致使外部类没法被GC回收,致使内存泄露。this

使人高兴的是Java8为咱们带来了Lambda,下面咱们看看利用Lambda如何实现上面的功能:指针

button.setOnClickListener(v -> v.setText("lalala"));

怎么样?!五行代码用一行就搞定了!!!code

在这里补充个概念函数式接口;前面提到的OnClickListener接口只有一个方法,Java中大多数回调接口都有这个特征:好比Runnable和Comparator;咱们把这些只拥有一个方法的接口称之为函数式接口

1、Lambda表达式

匿名内部类最大的问题在于其冗余的语法,好比前面的OnClickListener中五行代码仅有一行是在执行任务。Lambda表达式是匿名方法,前面咱们也看到了它用极其轻量的语法解决了这一问题。

下面给你们看几个Lambda表达式的例子:

(int x, int y) -> x + y                      //接收x和y两个整形参数并返回他们的和
() -> 66                                     //不接收任何参数直接返回66
(String name) -> {System.out.println(name);} //接收一个字符串而后打印出来
(View view) -> {view.setText("lalala");}     //接收一个View对象并调用setText方法

Lambda表达式语法由参数列表->函数体组成。函数体既能够是一个表达式也能够是一个代码块。

  • __表达式__:表达式会被执行而后返回结果。它简化掉了return关键字。

  • __代码块__:顾名思义就是一坨代码,和普通方法中的语句同样。

<!--lambda常常出如今嵌套环境中,如做为方法的参数:

Runnable runnable = () -> {doSomething();};
new Thread(runnable);

//也能够这样写
new Thread(() -> {doSomething();});-->

2、目标类型

经过前面的例子咱们能够看到,lambda表达式没有名字,那咱们怎么知道它的类型呢?答案是经过上下文推导而来的。例如,下面的表达式的类型是OnClickListener

OnClickListener listener = (View v) -> {v.setText("lalala");};

这就意味着一样的lambda表达式在不一样的上下文里有不一样的类型

Runnable runnable = () -> doSomething();  //这个表达式是Runnable类型的
Callback callback = () -> doSomething();  //这个表达式是Callback类型的

编译器利用lambda表达式所在的上下文所期待的类型来推导表达式的类型,这个__被期待的类型__被称为目标类型。lambda表达式只能出如今__目标类型__为函数式接口的上下文中。

Lambda表达式的类型和目标类型的方法签名必须一致,编译器会对此作检查,一个lambda表达式要想赋值给目标类型T则必须知足下面全部的条件:

  • T是一个函数式接口

  • lambda表达式的参数必须和T的方法参数在数量、类型和顺序上一致(一一对应)

  • lambda表达式的返回值必须和T的方法的返回值一致或者是它的子类

  • lambda表达式抛出的异常和T的方法的异常一致或者是它的子类

因为目标类型是知道lambda表达式的参数类型,因此咱们不必把已知的类型重复一遍。也就是说lambda表达式的参数类型能够从目标类型获取:

//编译器能够推导出s1和s2是String类型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
//当表达式的参数只有一个时括号也是能够省略的
button.setOnClickListener(v -> v.setText("lalala"));

ps: Java7中的泛型方法和<>构造器也是经过目标类型来进行类型推导的,如:

List<Integer> intList = Collections.emptyList>();
List<String> strList = new ArrayList<>();

3、做用域

在内部类中使用变量名和this很是容易出错。内部类经过继承获得的成员变量(包括来讲object的)可能会把外部类的成员变量覆盖掉,未作限制的this引用会指向内部类本身而非外部类。

而lambda表达式的语义就十分简单:它不会从父类中继承任何变量,也不用引入新的做用域。lambda表达式的参数及函数体里面的变量和它外部环境的变量具备相同的语义(this关键字也是同样)。

下面咱们举个栗子吧!

public class HelloLambda {

    Runnable r1 = () -> System.out.println(this);
    Runnable r2 = () -> System.out.println(toString());

    @Override
    public String toString() {
        return "Hello, lambda!";
    }

    public static void main(String[] args) {
        new HelloLambda().r1.run();  
        new HelloLambda().r2.run();
    }
}

上面的代码最终会打印两个Hello, lambda!,与之相相似的内部类则会打印出相似HelloLambda$1@32a890HelloLambda$1@6b32098这种出乎意料的字符串。

总结:基于词法做用域的理念,lambda表达式不能够掩盖任何其所在上下文的局部变量。

4、变量捕获

在Java7中,编译器对内部类中引用的外部变量(即捕获的变量)要求很是严格:若是捕获的变量没有被声明为final就会产生一个编译错误。可是在Java8中放宽了这一限制--对于lambda表达式和内部类,容许在其中捕获那些符合有效只读的局部变量(若是一个局部变量在初始化后从未被修改过,那么它就是有效只读)。

Runnable getRunnable(String name){
    String hello = "hello";
    return () -> System.out.println(hello+","+name);
}

对于this的引用以及经过this对未限定字段的引用和未限定方法的调用本质上都属于使用final局部变量。包含此类引用的lambda表达式至关于捕获了this实例。在其余状况下,lambda对象不会保留任何对this的应用。

这个特性对内存管理是极好的:要知道在java中一个非静态内部类会默认持有外部类实例的强引用,这每每会形成内存泄露。而在lambda表达式中若是没有捕获外部类成员则不会保留对外部类实例的引用。

不过尽管Java8放宽了对捕获变量的语法限制,但试图修改捕获变量的行为是被禁止的,好比下面这个例子就是非法的:

int sum  = 0;
list.forEach(i -> {sum += i;});

为何要禁止这种行为呢?由于这样的lambda表达式很容易引发race condition

lambda表达式不支持修改捕获变量的另一个缘由是咱们可使用更好的方式来实现一样的效果:使用规约(condition)。java.util.stream包提供了各类规约操做,关于Java8中的Stream API咱们放到下一章介绍。

5、方法引用

lambda表达式容许咱们定义一个匿名方法,并以函数式接口的方式使用它。Java8可以在已有的方法上实现一样的特性。

方法引用和lambda表达式拥有相同的特性(他们都须要一个目标类型,而且须要被转化为函数式接口的实例),不过咱们不须要为方法引用提供方法体,咱们能够直接经过方法名引用已有方法。

如下面的代码为例,假设咱们要按照userName排序

class User{

    private String userName;

    public String getUserName() {
        return userName;
    }
    ...
}

List<User> users = new ArrayList<>();
Comparator<User> comparator = Comparator.comparing(u -> u.getUserName());
Collections.sort(users, comparator);

咱们能够用方法引用替换上面的lambda表达式

Comparator<User> comparator = Comparator.comparing(User::getUserName);

这里的User::getUserName被看作是lambda表达式的简写形式。尽管方法引用不必定会把代码变得更紧凑,但它拥有更明确的语义--若是咱们想要调用的方法拥有一个名字,那么咱们就能够经过方法名调用它。

<!--由于函数式接口的方法参数对应于隐式方法调用时的参数,因此被引用方法签名能够经过放宽类型,装箱以及组织到参数数组中的方式对其参数进行操做,就像在调用实际方法同样:

Consumer<Integer> b1 = System::exit;    // void exit(int status)
Consumer<String[]> b2 = Arrays:sort;    // void sort(Object[] a)
Consumer<String> b3 = MyProgram::main;  // void main(String... args)
Runnable r = Myprogram::mapToInt        // void main(String... args)-->

方法引用有不少种,它们的语法以下:

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

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

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

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

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

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

若是你们喜欢这一系列的文章,欢迎关注个人知乎专栏、GitHub、简书博客。

相关文章
相关标签/搜索