Java JDK1.8 核心特性详解------Lambda表达式与方法引用

在上一篇文章中,我们介绍了行为参数化(Java JDK1.8 核心特性详解------行为参数化),并且简单展示了Lambda表达式给我们带来的好处。今天这篇文章将会介绍如何构建Lambda,它的使用场合,以及如何利用它使代码更加简洁。


Lambda表达式与方法引用

Lambda表达式介绍

Lambda表达式可以理解为简洁地表示可传递匿名函数的一种简单方式:没有名称,但是有参数列表,函数主体,返回类型,可能还有可抛出的异常。

  • 匿名:它不像普通的方法一样有一个具体的名称,比较像匿名类
  • 函数:Lambda不像方法一样属于某个特定的类,但和方法一样,有参数列表、函数主体、返回类型以及异常
  • 传递:Lambda表达式可以作为参数传递给方法或者储存在变量中
  • 简洁:不用像匿名类那样写很多模版代码

在上一篇的最后一部分代码中,你可以发现匿名函数被下面代码取代,而这个就是Lambda表达式。

(People people)-> "男".equals(people.getName())

Lambda表达式由参数列表(People people),箭头  -> 、和Lambda主体  "男".equals(people.getName()) 三部分组成。

参数列表就是方法需要传入的参数,其写法有以下不同几种

//完整的参数列表
(People people)-> "男".equals(people.getName())
//参数类型可以省略
(people) -> "男".equals(people.getName())
//当传入参数只有一个时,括号可以省略
people -> "男".equals(people.getName())
//当传入多个参数,或者不传参数时,括号不能省略
(People people,People  people1) -> people.getName.equals(people1.getName())
//当传入多个参数,或者不传参数时,括号不能省略,多个参数的参数类型也可以省略
(people, people1) -> people.getName.equals(people1.getName())

箭头(->)用来分隔参数列表和方法主体。

主体中的表达式是方法的主要功能。其写法有以下两种

//当没有花括号时,表达式结果就是返回值,隐含了return,
(people) -> "男".equals(people.getName())
//当有花括号时,要显性返回方法要求的返回信息
(people) -> {return "男".equals(people.getName())}

在哪里使用Lambda表达式

Lambda表达式可以用来替换函数式接口的实现类。例如上面用Lambda表示式,避免了定义新的类或者使用匿名函数。那什么是函数式接口呢?

函数式接口是指接口里面只定义了一个抽象方法,换句话说,函数式接口就是只定义了一个抽象方法的接口。(上篇文章中的FilterPeople 接口就是一个函数式接口)

//这个是函数式接口,因为只定义了一个抽象方法
public interfaceFunctionalInterface1 {
    int functional();
}

//这个不是函数式接口,因为里面定义了两个抽象方法
public interfaceNotFunctionalInterface2 {
    int functional();

    int functional2();
}

//这个也是函数式接口,因为里面只定义了一个抽象方法
public interface FunctionalInterface3 {
    int functional();
    //functional2是默认方法,后面的文章会提到,这里你就把他当作普通的方法。
    default int functional2() {
        return 1;
    }
}

在Java8中,新增了一个注解@FunctionalInterface,这个注解使用来接口上,用来声明这是一个函数式接口,如果有这个注解的接口内,存在一个以上的抽象方法,在编译的时候就会报错。 类似@Override,标注方法被重写了,如果没有上级没有这个方法,就会报错。

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,我们经常使用函数描述符来描述Lambda表达式和函数式接口的签名。例如interfaceFunctionalInterface1 接口中的functional方法,它可以用()->int来表示。()->int表示传入参数为空,返回值为int。又如前一章FilterPeople接口,也是一个函数式接口,我们用(People,People)->boolean,来表示boolean test(People people)这个方法,这表明传入两个People类型的参数,然后方法返回一个boolean值。又例如Runnable的函数描述符是()->void,这表示run这个方法的传入值为空,返回值为void;

如何使用Lambda表达式

Lambda表达式不是随时随地都可以使用的,一般来说,我们是用在某些传入参数是某些函数式接口(或者函数式接口子类,但是调用了函数式接口内的方法)的地方,它可以用来代替一些匿名类。或者是赋给一个变量。最常见地方就是策略设计模式,模版方法,观察者模式,责任链模式以及工厂模式。我们会演示策略设计模式时如何使用Lambda:

例如我们需要完成一个将int类型的值按照一定的业务转成String类型的值,方法接口设计如下:

    public static String castIntToString(int i, Function a) {
        return a.cast(i);
    }

通过传入不同的Function策略来对i进行转化。这里的Function是一个函数式接口。具体代码如下:

@FunctionalInterface
public interface CastFunction {

    String castClass(int r);
}

当我们要求将传入的int按不同业务逻辑转化为String的时候,我们以前可能会这么写:

        //直接转为String
        String string = castIntToString(1, new CastFunction () {
            @Override
            public String cast(int r) {
                return r + "";
            }
        });
        //将int值乘2以后返回
        String string1 = castIntToString(1, new CastFunction () {
            @Override
            public String cast(int r) {
                return r*2 + "";
            }
        });

但是,如果我们通过Lambda表达式,方法就会简单很多。首先我们先转化它的抽象方法,(int)->String。结合上面的讲的Lambda的组成,那么我们可以这样写 :

        //直接转为String
        String string = castIntToString(1, r -> r + "");
        //将int值乘2以后返回
        String string1 = castIntToString(1, r -> r*2 + "");

你会发现代码一下子简单了很多。在这里,你可能会觉得这个代码比较难理解,因为这里隐藏了实现的接口名,以及实现的抽象方法。但是当你熟练使用Lambda以后,你会觉得这样很酷。

Java8给我们提供了部分常用的函数式接口,让我们可以直接使用

使用案例 函数描述符对应的函数式接口 功能要求Lambda 的例子 
布尔表达式T ->booleanPredicate<T> 判断List<String>数量是否为空List<String> list -> list.isEmpty()
创建对象 ()->TSupplier<T> 创建一个People() -> new People
消费一个对象 T->voidConsumer<T>打印People的年龄(People p)->System.out.print(p.getAge)
传入一个对象返回另一种类型对象 T->RFunction<T, R>将int转为String(int i) -> i+""
合并两个值(T,U)->RIntBinaryOperator返回两个值的乘积(int a, int b) -> a * b 
比较两个对象(T,T)->boolean

Comparator<T>或

BiFunction<T, T, R>
或 ToIntBiFunction<T, T> 

比较两个People谁年龄大(People p1, People p2) -> p1.getAge().compareTo(p2.getAge())

注:由于JDK1.8中接口支持默认方法,但即使一个接口中有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

类型检查、类型推断和限制

类型检查

Lambda 表达式本身并不包含它是实现哪个函数式接口的信息,编译器会根据 Lambda 表达式所处的上下文(context)环境来推断 Lambda 表达式的目标类型(target type),例如对于下面的代码:

        Runnable a = ()->System.out.println("Runnable");

Lambda 表达式会赋值给 Runnable 对象,那么该 Lambda 表达式对应的目标类型就是 Runnable 接口,该接口中的 a方法对应的函数描述符为 () -> void,这个和 ()->System.out.println("Runnable") 可以匹配,这样就完成了类型检查。下图是一个完整的例子,概述了代码的类型检查过程,这个是《Java8实战》上的一个例子,用来筛选苹果集合中重量大于150g的苹果: 

 有了目标类型的接口,因此,只要函数签名相同,Lambda就可以混合使用。例如Callable接口和PrivilegedActionj接口的都是函数式接口,函数描述都是()->T,代表什么都不接受并返回一个泛型T的实例。那么,下面两个赋值都是有效的:

        Callable<String> callable = () -> "a";
        PrivilegedAction<String> privilegedAction = () -> "a";

第一个Lambda的赋值的目标类型是Callable<String>,第二个幅值的目标类型是PrivilegedAction<String>。

特殊的void兼容规则
    如果一个Lambda的主体是一个语句表达式,他就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回的是一个boolean,而不是Consumer上下文(T->void)所要求的viod:
        //Predicate返回一个boolean
        Predicate<People> p = people -> peopleList.add(new People("张三", 10));
        //Consumer返回一个void
        Consumer<People> a = people ->  peopleList.add(new People("张三", 10));

 类型推断

正如我之前说的,我们在写Lambda表达式的时候可以省略参数类型。这是因为Java编译器会从上下文自动帮我们推断出用什么函数式接口。有时候显式写出参数类型有助于提高代码可读性,有时候省略有助于代码可读性,具体要你自己选择。

使用局部变量

我们之前Lambda表达式中用的都是参数列表中传入的变量,但是Lambda表达式还允许使用自由变量(不是参数,而是在外层作用域中定义的变量)。它们被称为捕捉Lambda。例如,下面Lambda捕捉了number变量:

        int number = 1;
        Runnable runnable = () -> System.out.println(number);

Lambda表达式可以没有限制的实例变量 和静态变量,但是使用局部变量时,必须显式申明为final或者实际上是final。实例变量和静态变量好理解,局部变量?下面举个例子:

        final unmber = 1;
        Runnable runnable = () -> System.out.println(number);//这里编译器不会报错

        unmber2 = 1;
        Runnable runnable = () -> System.out.println(number2);//这里编译器不会报错

        int number3 = 1;
        numbe3r=3;
        Runnable runnable = () -> System.out.println(number3);//这里编译器会报错
        
        unmber4 = 1;
        Runnable runnable = () -> {
            number4 = number4+1;
            System.out.println(number4);//这里编译器会报错
        };

对于局部变量,我们要想在Lambda中使用,必须对这个变量必须是final或者是effectively final。第二个例子中,number2在赋值过后没有再次修改,JDK1.8就在前面默认他是effectively final的。

为什么会这么要求呢?个人理解是,因为实例变量和静态变量是保存在堆中,局部变量是保存在栈里,当Lambda在一个线程运行时,想要访问自由变量(局部变量),可能会出现局部变量已经被销毁了。所以,为了保证Lambda可以访问到这个自由变量,Lambda访问的是这个自由变量的副本,即便以前的变量销毁了,Lambda还是可以通过这个副本正常访问到以前的值。如果这个副本不是final或者effectively final,可能会导致安全问题。

方法引用

方法引用可以被看做仅仅调用特定方法的Lambda表达式的简单写法(如果一个Lambda主体只是调用这个方法(主体没有{}),就可以用方法引用来替代这个Lambda表达式),方法引用可以重复使用现有的方法定义,并像Lambda一样传递他们,同时更加简洁。例如下面这两行代码是等效的:

FilterPeople filterPeople = (People people) -> people.getName();

FilterPeople filterPeople = People::getName;

那要如何使用它呢?

目标引用放在分隔符::前,方法的名称放在后面例如People::getName代表引用People类中的getName方法,这是上面Lambda表达式的快捷写法。

方法引用可以看做单一方法的Lambda的语法糖,主要有三类:

(1)指向静态方法的方法引用,例如方法主体是调用Integer的parseInt方法(String s)->Integer.parseInt(s)  可以用Integer::parseInt 来代替

Function<String, Integer> stringIntegerFunction0 = (String string) -> Integer.parseInt(string);
Function<String, Integer> stringIntegerFunction1 = Integer::parseInt;

(2)指向任意类型实例方法的方法引用,例如方法主体是调用String的length方法(String s)->s.length()  可以用   String::length 来代替。

Function<String, Integer> stringIntegerFunction = (String string) -> string.length();
Function<String, Integer> stringIntegerFunction1 = String::length;

(3)指向现有对象的实例方法的方法引用,例如方法主体是调用People的getName方法,写作People::getName。

//people是自由变量
Runnable runnable = () -> people.getName();
Runnable runnable1 = People::getName;

 

构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用。例如之前我们提到Supplier<Apple> 函数式接口可以用来创建对象实例。例如:

        Supplier<People> peopleFunction = () -> new People();
        People people = peopleFunction.get();

通过构造函数引用,可以将代码写作:

        Supplier<People> peopleFunction = People::new;
        People people = peopleFunction.get();

对于多参构造器,Java8提供了很多多参的函数式接口,让我们也可以用构造引用来创建。

到目前为止,Lambda表达式的内容基本结束了,事实上,Lambda更适合和JDK提供Stream配合使用。下面一部分将会介绍流的使用方式,以及如何和Lambda组合使用。


更多与JDK1.8相关的文章请看:Java JDK1.8 核心特性详解----(总目录篇)

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值