理解和运用Java中的Lambda(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 回想一下,JDK8是2014年发布正式版的,到现在为(2020-02-08)止已经过去了5年多。JDK8引入的两个比较强大的新特性是Lambda表达式(下文的Lambda特指JDK提供的Lambda)和Stream,这两个强大的特性让函数式编程在Java开发中发扬光大。这篇文章会从基本概念、使用方式、实现原理和实战场景等角度介绍Lambda的全貌,其中还会涉及一些函数式编程概念、JVM一些知识等等。

前提



回想一下,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();
}
复制代码


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


@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编程语言规范中,接口的本质就是方法声明的集合体,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:


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特性引入的理论基础。举个例子:


@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


// 封闭类
class OuterClass {
    ...
    // 静态嵌套类
    static class StaticNestedClass {
        ...
    }
    // 内部类
    class InnerClass {
        ...
    }
}
复制代码


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

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


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)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:


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();
    }
}
复制代码


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


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的事件回调中大量使用,经常会看到类似这样的代码:


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


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


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语法的详细定义如下:


// en_US
InterfaceType interfaceObject = [Method Argument List] -> Method Body
// zh_CN
接口类型 接口实例 = [方法参数列表] -> 方法体
复制代码


更具体的描述应该是:


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


微信截图_20220512204511.png


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


  • 返回值:接口类型以及接口类型对应的临时实例变量。
  • 等号:=
  • 方法参数列表:一般由中括号()包裹,格式是(类型1 类型1的临时变量,...,类型N 类型N的临时变量),在方法没有重载可以明确推断参数类型的时候,参数类型可以省略,只留下临时变量列表。特殊地,空参数列表用()表示,如果参数只有一个,可以省略()
  • 箭头:->
  • 方法体:一般由花括号{}包裹,格式是{方法逻辑... return 函数式接口方法返回值类型的值;},有几点需要注意:


  • 如果方法体是空实现,用{}表示,如Runnable runnable = () -> {};
  • 如果函数式接口抽象方法的返回值为void类型,则不需要return关键字语句,如Runnable runnable = () -> {int i=0; i++;};
  • 如果函数式接口抽象方法的方法体仅仅包含一个表达式,则不需要使用{}包裹,如Runnable runnable = () -> System.out.println("Hello World!");


举一些例子:


// 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);
复制代码


目标类型与类型推断


先引入下面的一个场景:


// 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表达式参数的数量。


举个例子:


// 显式类型
Function<String, String> functionX = (String x) -> x;
// 隐式类型
Function<String, Integer> functionY = x -> Integer.parseInt(x);
复制代码


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


// 编译不通过
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>点举个例子:


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>点举个例子:


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


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


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


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


举一些基本的使用例子:


// 静态方法引用
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 class SupperRef {
    public static void main(String[] args) throws Exception {
        Sub sub = new Sub();
        System.out.println(sub.refMethod("10086")); // 10086
    }
    private static class Supper {
        private Integer supperRefMethod(String value) {
            return Integer.parseInt(value);
        }
    }
    private static class Sub extends Supper {
        private Integer refMethod(String value) {
            Function<String, Integer> function = super::supperRefMethod;
            return function.apply(value);
        }
    }
}
// 构造器方法引用
public class ConstructorRef {
    public static void main(String[] args) throws Exception {
        Function<String, Person> function = Person::new;
        Person person = function.apply("doge");
        System.out.println(person.getName()); // doge
    }
    private static class Person {
        private final String name;
        public Person(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
    }
}
// 数组构造器方法引用
Function<Integer, Integer[]> function = Integer[]::new;
Integer[] array = function.apply(10);
System.out.println(array.length); // 10
复制代码


相关文章
|
17天前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
19天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
21天前
|
并行计算 Java 编译器
深入理解Java中的Lambda表达式
在Java 8中引入的Lambda表达式,不仅简化了代码编写,还提升了代码可读性。本文将带你探索Lambda表达式背后的逻辑与原理,通过实例展示如何高效利用这一特性优化你的程序。
|
25天前
|
搜索推荐 Java API
探索Java中的Lambda表达式
本文将深入探讨Java 8引入的Lambda表达式,这一特性极大地简化了代码编写,提高了程序的可读性。通过实例分析,我们将了解Lambda表达式的基本概念、使用场景以及如何优雅地重构传统代码。文章不仅适合初学者,也能帮助有经验的开发者加深对Lambda表达式的理解。
|
1月前
|
Java
探索Java中的Lambda表达式
【10月更文挑战第37天】本文将带你深入理解Java的Lambda表达式,从基础语法到高级特性,通过实例讲解其在函数式编程中的应用。我们还将探讨Lambda表达式如何简化代码、提高开发效率,并讨论其在实际项目中的应用。
|
1月前
|
Java API
Java中的Lambda表达式与函数式编程####
【10月更文挑战第29天】 本文将深入探讨Java中Lambda表达式的实现及其在函数式编程中的应用。通过对比传统方法,我们将揭示Lambda如何简化代码、提高可读性和维护性。文章还将展示一些实际案例,帮助读者更好地理解和应用Lambda表达式。 ####
|
1月前
|
Java API 开发者
Java中的Lambda表达式与函数式编程####
在Java的演变过程中,Lambda表达式和函数式编程的引入无疑是一次重大的飞跃。本文将深入探讨Lambda表达式的定义、用法及优势,并结合实例说明如何在Java中利用Lambda表达式进行函数式编程。通过对比传统编程方式,揭示Lambda表达式如何简化代码、提高开发效率和可维护性。 ####
|
16天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
19天前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
24 0
|
1月前
|
Java API 数据处理
探索Java中的Lambda表达式与Stream API
【10月更文挑战第22天】 在Java编程中,Lambda表达式和Stream API是两个强大的功能,它们极大地简化了代码的编写和提高了开发效率。本文将深入探讨这两个概念的基本用法、优势以及在实际项目中的应用案例,帮助读者更好地理解和运用这些现代Java特性。