java理解和运用Java中的Lambda

Posted 九师兄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java理解和运用Java中的Lambda相关的知识,希望对你有一定的参考价值。

1.概述

转载:理解和运用Java中的Lambda

前提

回想一下,JDK8是2014年发布正式版的,到现在为(2020-02-08)止已经过去了5年多。JDK8引入的两个比较强大的新特性是Lambda表达式(下文的Lambda特指JDK提供的Lambda)和Stream,这两个强大的特性让函数式编程在Java开发中发扬光大。这篇文章会从基本概念、使用方式、实现原理和实战场景等角度介绍Lambda的全貌,其中还会涉及一些函数式编程概念、JVM一些知识等等。

基本概念

下面介绍一些基本概念,一步一步引出Lambda的概念。

函数式接口

函数式接口和接口默认方法都是JDK8引入的新特性。函数式接口的概念可以从java.lang.FunctionalInterface注解的API注释中得知:

An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification.

Conceptually, a functional interface has exactly one abstract method. Since @linkplain java.lang.reflect.Method#isDefault() default methods have an implementation, they are not abstract.

简单来说就是:@FunctionalInterface是一个提供信息的接口(其实就是标识接口),用于表明对应的接口类型声明是一个Java语言规范定义的函数式接口。从概念上说,一个函数式接口有且仅有一个抽象方法,因为接口默认方法必须予以实现,它们不是抽象方法。

所以可以这样给函数式接口定义:如果一个接口声明的时候有且仅有一个抽象方法,那么它就是函数式接口,可以使用@FunctionalInterface注解标识。

JDK中已经定义了很多内置的函数式接口,例如:

// java.lang.Runnable
@FunctionalInterface
public interface Runnable 

    public abstract void run();
  

// java.util.function.Supplier
@FunctionalInterface
public interface Supplier<T> 

    T get();


也可以自定义函数式接口,例如:

JAVA
@FunctionalInterface
public interface CustomFunctionalInterface 
    
    // 可以缩写为void process();  接口方法定义的时候,默认使用public abstract修饰
    public abstract void process();

接口默认方法

接口默认方法的含义可以见Java官方教程中对应的章节,在文末的参考资料可以查看具体的链接:

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

简单来说就是:默认方法允许你在你的类库中向接口添加新的功能,并确保新增的默认方法与这些接口的较早版本编写的代码二进制兼容。

接口默认方法(下称默认方法)通过default关键字声明,可以直接在接口中编写方法体。也就是默认方法既声明了方法,也实现了方法。这一点很重要,在默认方法特性出现之前,Java编程语言规范中,接口的本质就是方法声明的集合体,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:

JAVA
public interface DefaultMethod 

    default void defaultVoidMethod() 

    

    default String sayHello(String name) 
        return String.format("%s say hello!", name);
    

    static void main(String[] args) throws Exception 
        class Impl implements DefaultMethod 

        
        DefaultMethod defaultMethod = new Impl();
        System.out.println(defaultMethod.sayHello("throwable"));  // throwable say hello!
    

如果继承一个定义了默认方法的接口,那么可以有如下的做法:

  • 完全忽略父接口的默认方法,那么相当于直接继承父接口的默认方法的实现(方法继承)。

  • 重新声明默认方法,这里特指去掉default关键字,用public abstract关键字重新声明对应的方法,相当于让默认方法转变为抽象方法,子类需要进行实现(方法抽象)。

  • 重新定义默认方法,也就是直接覆盖父接口中的实现(方法覆盖)。

结合前面一节提到的函数式接口,这里可以综合得出一个结论:函数式接口,也就是有且仅有一个抽象方法的接口,可以定义0个或者N(N >= 1)个默认方法。这一点正是Stream特性引入的理论基础。举个例子:

JAVA
@FunctionalInterface
public interface CustomFunctionalInterface 

    public abstract void process();

    default void defaultVoidMethod() 

    

    default String sayHello(String name) 
        return String.format("%s say hello!", name);
    

这里说点题外话。

在写这篇文章的时候,笔者想起了一个前同事说过的话,大意如下:在软件工程中,如果从零做起,任何新功能的开发都是十分简单的,困难的是在兼容所有历史功能的前提下进行新功能的迭代。试想一下,Java迭代到今天已经过去十多年了,Hotspot VM源码工程已经十分庞大(手动编译过OpenJDK Hotspot VM源码的人都知道过程的痛苦),任何新增的特性都要向前兼容,否则很多用了历史版本的Java应用会无法升级新的JDK版本。既要二进制向前兼容,又要迭代出新的特性,Java需要进行舍夺,默认方法就是一个例子,必须舍去接口只能定义抽象方法这个延续了多年在Java开发者中根深蒂固的概念,夺取了基于默认方法实现构筑出来的流式编程体系。笔者有时候也在思考:如果要我去开发Stream这个新特性,我会怎么做或者我能怎么做?

嵌套类(Nested Classes)

嵌套类(Nested Classes),简单来说就是:在一个类中定义另一个类,那么在类内被定义的那个类就是嵌套类,最外层的类一般称为封闭类(Enclosing Class)。嵌套类主要分为两种:静态嵌套类和非静态嵌套类,而非静态嵌套类又称为内部类(Inner Classes)。

JAVA
// 封闭类
class OuterClass 
    ...
    // 静态嵌套类
    static class StaticNestedClass 
        ...
    
    
    // 内部类
    class InnerClass 
        ...
    

静态嵌套类可以直接使用封闭的类名称去访问例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();,这种使用形式和一般类实例化基本没有区别。

内部类实例的存在必须依赖于封闭类实例的存在,并且内部类可以直接访问封闭类的任意属性和方法,简单来说就是内部类的实例化必须在封闭类实例化之后,并且依赖于封闭类的实例,声明的语法有点奇特:

JAVA
public class OuterClass 

    int x = 1;

    static class StaticNestedClass 

    

    class InnerClass 
        // 内部类可以访问封闭类的属性
        int y = x;
    

    public static void main(String[] args) throws Exception 
        OuterClass outerClass = new OuterClass();

        // 必须这样实例化内部类 - 声明的语法相对奇特
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();

        // 静态嵌套类可以一般实例化,形式为:封闭类.静态嵌套类
        OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass();

        // 如果main方法在封闭类内,可以直接使用静态嵌套类进行实例化
        StaticNestedClass x = new StaticNestedClass();
    

内部类中有两种特殊的类型:本地类(Local Classes)和匿名类(Anonymous Classes)。

本地类是一种声明在任意块(block)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:

JAVA
public class OuterClass 

    static int y = 1;
    
        
        // 本地类A
        class A
            int z = y;
        
        A a = new A();
    

    static 
        // 本地类B
        class B
            int z = y;
        
        B b = new B();
    

    private void method()
        // 本地类C
        class C
            int z = y;
        
        C c = new C();
    


匿名类可以让代码更加简明,允许使用者在定义类的同时予以实现,匿名类和其他内部类不同的地方是:它是一种表达式,而不是类声明。例如:

JAVA
public class OuterClass 

    interface In 

        void method(String value);
    
    
    public void sayHello()
        // 本地类 - 类声明
        class LocalClass
            
        
        
        // 匿名类 - 是一个表达式
        In in = new In() 
            
            @Override
            public void method(String value) 
                
            
        ;
    

如果用Java做过GUI开发,匿名类在Swing或者JavaFx的事件回调中大量使用,经常会看到类似这样的代码:

JAVA
JButton button = new JButton();
button.addActionListener(new AbstractAction() 
    @Override
    public void actionPerformed(ActionEvent e) 
        System.out.println("按钮事件被触发...");
    
);

嵌套类的类型关系图如下:

SHELL
Nested Classes
  - Static Nested Classes
  - None Nested Classes
    - Local Classes
    - Anonymous Classes
    - Other Inner Classes

Lambda表达式

下面是来自某搜索引擎百科关于Lambda表达式的定义:

Lambda表达式(Lambda Expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的Lambda抽象(Lambda Abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

Java中的Lambda表达式(下面称Lambda)表面上和上面的定义类似,本质也是匿名函数,但其实现原理区别于一般的匿名类中的匿名函数实现,她是JDK8引入的一颗新的语法糖。

引入Lambda表达式的初衷
如果一个接口只包含一个方法,那么匿名类的语法会变得十分笨拙和不清楚,产生大量的模板代码,归结一下就是:代码冗余是匿名类的最大弊端。在编程的时候,我们很多时候希望把功能作为参数传递到另一个方法,Lambda就是为此而生,Lambda允许使用者将功能视为方法参数,将代码视为数据。引入Lambda带来了如下优势:

简化代码,引入了强大的类型推断和方法引用特性,简单的功能甚至可以一行代码解决,解放匿名类的束缚。
把功能作为参数向下传递,为函数式编程提供了支持。
至此还得出一个结论:Lambda只适用于函数式接口对应唯一抽象方法的实现

Lambda表达式的语法定义
Lambda语法的详细定义如下:

JAVA
// en_US
InterfaceType interfaceObject = [Method Argument List] -> Method Body

// zh_CN
接口类型 接口实例 = [方法参数列表] -> 方法体

更具体的描述应该是:

接口类型 接口实例临时变量 = (方法参数类型X 方法参数类型X临时变量 , 方法参数类型Y 方法参数类型Y临时变量…) -> 方法体… return 接口抽象方法返回值对应类型类型实例;

一个Lambda表达式由五个部分组成:

返回值:接口类型以及接口类型对应的临时实例变量。
等号:=。
方法参数列表:一般由中括号()包裹,格式是(类型1 类型1的临时变量,…,类型N 类型N的临时变量),在方法没有重载可以明确推断参数类型的时候,参数类型可以省略,只留下临时变量列表。特殊地,空参数列表用()表示,如果参数只有一个,可以省略()。
箭头:->。
方法体:一般由花括号包裹,格式是方法逻辑… return 函数式接口方法返回值类型的值;,有几点需要注意:
如果方法体是空实现,用表示,如Runnable runnable = () -> ;。
如果函数式接口抽象方法的返回值为void类型,则不需要return关键字语句,如Runnable runnable = () -> int i=0; i++;;。
如果函数式接口抽象方法的方法体仅仅包含一个表达式,则不需要使用包裹,如Runnable runnable = () -> System.out.println(“Hello World!”);。
举一些例子:

JAVA
// Function - 具体
java.util.function.Function<String, Integer> functionY = (String string) -> 
    return Integer.parseInt(string);
;
// Function - 简化
java.util.function.Function<String, Integer> functionX = string -> Integer.parseInt(string);

// Runnable - 具体
Runnable runnableX = () -> 
    System.out.println("Hello World!");
;
// Runnable - 简化
Runnable runnableY = () -> System.out.println("Hello World!");

// 整数1-100的和 - 具体
int reduceX = IntStream.range(1, 101).reduce(0, (int addend, int augend) -> 
    return addend + augend;
);
// 整数1-100的和 - 简化
int reduceY = IntStream.range(1, 101).reduce(0, Integer::sum);
目标类型与类型推断
先引入下面的一个场景:

JAVA
// club.throwable.Runnable
@FunctionalInterface
public interface Runnable 

    void run();

    static void main(String[] args) throws Exception 
        java.lang.Runnable langRunnable = () -> ;
        club.throwable.Runnable customRunnable = () -> ;
        langRunnable.run();
        customRunnable.run();
    

笔者定义了一个和java.lang.Runnable完全一致的函数式接口club.throwable.Runnable,上面main()方法中,可以看到两个接口对应的Lambda表达式的方法体实现也是完全一致,但是很明显最终可以使用不同类型的接口去接收返回值,也就是这两个Lambda的类型是不相同的。而这两个Lambda表达式返回值的类型是我们最终期待的返回值类型(expecting a data type of XX),那么Lambda表达式就是对应的被期待的类型,这个被期待的类型就是Lambda表达式的目标类型。

为了确定Lambda表达式的目标类型,Java编译器会基于对应的Lambda表达式,使用上下文或者场景进行综合推导,判断的一个因素就是上下文中对该Lambda表达式所期待的类型。因此,只能在Java编译器能够正确推断Lambda表达式目标类型的场景下才能使用Lambda表达式,这些场景包括:

变量声明。
赋值。
返回语句。
数组初始化器。
Lambda表达式函数体。
条件表达式(condition ? processIfTrue() : processIfFalse())。
类型转换(Cast)表达式。
Lambda表达式除了目标类型,还包含参数列表和方法体,而方法体需要依赖于参数列表进行实现,所以方法参数也是决定目标类型的一个因素。

方法参数的类型推导的过程主要依赖于两个语言特性:重载解析(Overload Resolution)和参数类型推导(Type Argument Inference)。

原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference

重载解析会为一个给定的方法调用(Method Invocation)寻找最合适的方法声明(Method Declaration)。由于不同的声明具有不同的签名,当Lambda表达式作为方法参数时,重载解析就会影响到Lambda表达式的目标类型。编译器会根据它对该Lambda表达式的所提供的信息的理解做出决定。如果Lambda表达式具有显式类型(参数类型被显式指定),编译器就可以直接使用Lambda表达式的返回类型;如果Lambda表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略Lambda表达式函数体而只依赖Lambda表达式参数的数量。

举个例子:

JAVA
// 显式类型
Function<String, String> functionX = (String x) -> x;

// 隐式类型
Function<String, Integer> functionY = x -> Integer.parseInt(x);
如果依赖于方法参数的类型推导最佳方法声明时存在二义性(Ambiguous),我们就需要利用转型(Cast)或显式Lambda表达式来提供更多的类型信息,从而Lambda表达式的目标类型。举个例子:

JAVA
// 编译不通过
Object runnableX = () -> ;

// 编译通过 - Cast
Object runnableY = (Runnable) () -> ;


// 静态方法入参类型是函数式接口
public static void function(java.util.function.Function function) 



function((Function<String, Long>) (x) -> Long.parseLong(x));
作用域

关于作用域的问题记住几点即可:

<1>:Lambda表达式内的this引用和封闭类的this引用相同。
<2>:Lambda表达式基于词法作用域,它不会从超类中继承任何变量,方法体里面的变量和它外部环境的变量具有相同的语义。
<3>:Lambda expressions close over values, not variables,也就是Lambda表达式对值类型封闭,对变量(引用)类型开放(这一点正好解释了Lambda表达式内部引用外部的属性的时候,该属性必须定义为final)。
对于第<1>点举个例子:

JAVA
public class LambdaThis 

    int x = 1;

    public void method() 
        Runnable runnable = () -> 
            int y = this.x;
            y++;
            System.out.println(y);
        ;
        runnable.run();
    

    public static void main(String[] args) throws Exception 
        LambdaThis lambdaThis = new LambdaThis();
        lambdaThis.method();   // 2
    

对于第<2>点举个例子:

JAVA
public class LambdaScope 
    
    public void method() 
        int x = 1;
        Runnable runnable = () -> 
            // 编译不通过 - Lambda方法体外部已经定义了同名变量
            int x = 2;
        ;
        runnable.run();
    

对于第<3>点举个例子:

JAVA
public class LambdaValue 

    public void method() 
        (final) int x = 1;
        Runnable runnable = () -> 
            // 编译不通过 - 外部值类型使用了final
            x ++;
        ;
        runnable.run();
    


public class LambdaValue 

    public void method() 
        (final) IntHolder holder = new IntHolder();
        Runnable runnable = () -> 
            // 编译通过 - 使用了引用类型
            holder.x++;
        ;
        runnable.run();
    

    private static class IntHolder 

        int x = 1;
    

方法引用
方法引用(Method Reference)是一种功能和Lambda表达式类似的表达式,需要目标类型和实现函数式接口,但是这个实现形式并不是通过方法体,而是通过方法名称(或者关键字)关联到一个已经存在的方法,本质是编译层面的技术,旨在进一步简化Lambda表达式方法体和一些特定表达式的实现。方法引用的类型归结如下:

类型	例子
静态方法引用	ClassName::methodName
指定对象实例方法引用	instanceRef::methodName
特定类型任意对象方法引用	ContainingType::methodName
超类方法引用	supper::methodName
构造器方法引用	ClassName::new
数组构造器方法引用	TypeName[]::new

可见其基本形式是:方法容器::方法名称或者关键字。

举一些基本的使用例子:

JAVA
// 静态方法引用
public class StaticMethodRef 

    public static void main(String[] args) throws Exception 
        Function<String, Integer> function = StaticMethodRef::staticMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    

    public static Integer staticMethod(String value) 
        return Integer.parseInt(value);
    


// 指定对象实例方法引用
public class ParticularInstanceRef 

    public Integer refMethod(String value) 
        return Integer.parseInt(value);
    

    public static void main(String[] args) throws Exception
        ParticularInstanceRef ref = new ParticularInstanceRef();
        Function<String, Integer> function = ref::refMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    


// 特定类型任意对象方法引用
String[] stringArray = "C", "a", "B";
Arrays.sort(stringArray, String::compareToIgnoreCase);
System.out.println(Arrays.toString(stringArray)); // [a, B, C]

// 超类方法引用
public 以上是关于java理解和运用Java中的Lambda的主要内容,如果未能解决你的问题,请参考以下文章

Java8 新特性----函数式接口,以及和Lambda表达式的关系

java中的bo怎么理解?怎么运用

Java中的集合类Lambda鲁棒性简述

Java1.8-Lambda

java8新特性-方法引用

Java之函数式接口