zoukankan      html  css  js  c++  java
  • Java Lambda表达式


        原文作者:Jakob Jenkov     原文链接:http://tutorials.jenkov.com/java/lambda-expressions.html

    @



    Java Lambda表达式是Java8中的新特性。Java lambda表达式是Java进入函数式编程的第一步。因此,Java lambda表达式是可以单独创建的函数,而无需属于任何类。Java lambda 表达式可以像对象一样传递并按需执行。

    Java lambda表达式通常用于实现简单的事件监听/回调,或在Java Streams API 函数式编程时使用。


    Java Lambdas和函数式接口

    函数式编程通常用于实现事件侦听器。Java中的事件监听器通常被定义为具有一个抽象方法的Java接口。

    这是一个模拟的单个抽象方法接口示例:

    public interface StateChangeListener {
    	public void onStateChange(State oldState, State newState);
    }
    
    

    这个Java接口定义了一个抽象方法,只要状态发生变化(无论观察到什么),都将调用该方法。

    在Java 7中,你必须实现此接口才能监听状态的更改。假设你有一个名为StateOwner的类,可以注册状态监听器。示例如下:

    public class StateOwner {
      public void addStateLister(StateChangeListener stateChangeListener) {
       //do some thing
     };
    }
    

    在Java 7中,你可以使用匿名接口实现添加监听器,如下所示:

    		StateOwner stateOwner = new StateOwner();
    		stateOwner.addStateLister(new StateChangeListener() {
    			@Override
    			public void onStateChange(State oldState, State newState) {
    				System.out.println("State changed");
    			}
    		});
    

    在Java 8中你可以使用Lambda表达式来添加监听器,如下:

    		StateOwner stateOwner = new StateOwner();
    		stateOwner.addStateLister(
    			(oldState, newState) -> System.out.println("State change")
    		);
    

    这一部分是Lambda表达式:

    (oldState, newState) -> System.out.println("State changed")
    

    lambda表达式与addStateListener()方法的参数的参数类型匹配。如果lambda表达式与参数类型(在本例中为StateChangeListener接口)匹配,则将lambda表达式转换为实现与该参数相同的接口的函数。

    Java lambda表达式只能在它们匹配的类型是单个方法接口的地方使用。

    在上面的示例中,lambda表达式作为参数,其中参数类型为StateChangeListener接口。该接口只有一个抽象方法。因此,lambda表达式成功匹配该接口。


    将Lambda匹配到接口

    单个抽象方法接口有时也称为函数式接口。将Java lambda表达式与函数式接口进行匹配需要以下步骤:

    • 接口是否只有一个抽象方法?
    • lambda表达式的参数是否与抽象方法的参数匹配?
    • lambda表达式的返回类型是否与抽象方法的返回类型匹配?

    如果这三个条件都满足,则该接口可以匹配给定的lambda表达式。


    具有默认方法和静态方法的接口

    从Java 8开始,Java接口可以包含默认方法和静态方法。默认方法和静态方法都可以在接口中直接实现。这意味着,Java lambda表达式可以使用多种方法实现接口——只要该接口仅有一个抽象方法即可。

    可以使用lambda表达式实现以下接口:

    import java.io.IOException;
    import java.io.OutputStream;
    
    public interface MyInterface {
    
        void printIt(String text);
    
        default public void printUtf8To(String text, OutputStream outputStream){
            try {
                outputStream.write(text.getBytes("UTF-8"));
            } catch (IOException e) {
                throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
            }
        }
    
        static void printItToSystemOut(String text){
            System.out.println(text);
        }
    }
    

    即使此接口包含3个方法,也可以通过lambda表达式实现,因为只有一个抽象方法。

    实现如下:

    MyInterface myInterface = (String text) -> {
        System.out.print(text);
    };
    

    Lambda表达式 vs 匿名接口实现

    即使lambda表达式接近匿名接口实现,但也有一些区别需要注意。

    最主要的区别,匿名接口实现可以具有状态(成员变量),而lambda表达式则不能。

    看一下下面这个接口:

    public interface MyEventConsumer {
    
        public void consume(Object event);
    
    }
    

    可以使用匿名接口实现方式来实现此接口,如下所示:

    MyEventConsumer consumer = new MyEventConsumer() {
        public void consume(Object event){
            System.out.println(event.toString() + " consumed");
        }
    };
    

    此匿名MyEventConsumer实现可以具有自己的内部状态。

    重写匿名接口实现:

    MyEventConsumer myEventConsumer = new MyEventConsumer() {
        private int eventCount = 0;
        public void consume(Object event) {
            System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
        }
    };
    

    请注意,匿名MyEventConsumer接口实现现在具有一个名为eventCount的属性。

    Lambda表达式不能具有此类属性。因此,lambda表达式是无状态的。


    Lambda类型推断

    在Java 8之前,在进行匿名接口实现时,必须指定要实现的接口。这是本文开头的匿名接口实现示例:

    stateOwner.addStateListener(new StateChangeListener() {
    
        public void onStateChange(State oldState, State newState) {
            // do something with the old and new state.
        }
    });
    

    使用lambda表达式时,通常可以从相关的代码中推断出类型。例如,可以从addStateListener()方法(StateChangeListener接口上的抽象方法)的方法声明中推断参数的接口类型。

    这称为类型推断。编译器通过在其他地方寻找类型来推断参数的类型——在这种情况下为方法定义。这是本文开头的示例,lambda表达式中并未声明参数的类型:

    stateOwner.addStateListener(
        (oldState, newState) -> System.out.println("State changed")
    );
    
    

    在lambda表达式中,通常可以推断参数类型。在上面的示例中,编译器可以从onStateChange()方法声明中推断其类型。因此,从onStateChange()方法的方法声明中就可以推断出参数 oldState 和 newState 的类型。


    Lambda参数

    由于Java lambda表达式实际上只是方法,因此lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState,newState)部分指定lambda表达式使用的参数。这些参数必须与函数式接口的抽象方法参数匹配。在当前这个示例,参数必须与StateChangeListener接口的onStateChange()方法的参数匹配:

    public void onStateChange(State oldState, State newState);
    

    首先,lambda表达式中的参数数量必须与方法匹配。

    其次,如果你在lambda表达式中指定了任何参数类型,则这些类型也必须匹配。我还没有向你演示如何在lambda表达式参数上设置类型(本文稍后展示),但是在大多数情况下,你不会用到它。


    无参数

    如果lambda表达式匹配的方法无参数,则可以这样写lambda表达式:

    () -> System.out.println("Zero parameter lambda");
    

    请注意,括号中没有内容。那就是表示lambda不带任何参数。


    一个参数

    如果Java lambda表达式匹配的方法有一个参数,则可以这样写lambda表达式:

    (param) -> System.out.println("One parameter: " + param);
    

    请注意,参数在括号内列出。

    当lambda表达式是单个参数时,也可以省略括号,如下所示:

     param -> System.out.println("One parameter: " + param);
    

    多个参数

    如果Java lambda表达式匹配的方法有多个参数,则需要在括号内列出这些参数。代码如下:

    (p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
    

    仅当方法是单个参数时,才可以省略括号。


    指定参数类型

    如果编译器无法从lambda匹配的函数式接口抽象方法推断参数类型,则有时可能需要为lambda表达式指定参数类型。不用担心,编译器会在这种情况下会有提醒。这是一个Java lambda指定参数类型示例:

    (Car car) -> System.out.println("The car is: " + car.getName());
    

    如你所见,car参数的类型(Car)写在参数名称的前面,就像在其他方法中声明参数或对接口进行匿名实现时一样。


    Java 11中的var参数类型

    在Java 11中,你可以使用var关键字作为参数类型。

    var关键字在Java 10中作为局部变量类型推断引入。从Java 11开始,var也可以用于lambda参数类型。这是在lambda表达式中使用Java var关键字作为参数类型的示例:

    Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
    

    Lambda表达式主体

    lambda表达式的主体以及它表示的函数/方法的主体在lambda声明中的->的右侧指定:

    这是一个示例:

     (oldState, newState) -> System.out.println("State changed")
    

    如果你的lambda表达式需要包含多行,则可以将lambda函数主体括在{}括号内,Java在其他地方声明方法时也需要将其括起来。这是一个例子:

     (oldState, newState) -> {
        System.out.println("Old state: " + oldState);
        System.out.println("New state: " + newState);
      }
    

    Lambda表达式返回值

    你可以从Java lambda表达式返回值,就像从方法中返回值一样。你只需向lambda表达式主体添加一个return,如下所示:

     (param) -> {
        System.out.println("param: " + param);
        return "return value";
      }
    

    如果你的lambda表达式只需要计算一个返回值并将其返回,则可以用更短的方式指定返回值。例如这个:

     (a1, a2) -> { return a1 > a2; }
    

    你可以写成:

     (a1, a2) -> a1 > a2;
    

    然后,编译器会断定表达式 a1> a2 是lambda表达式的返回值。


    Lambdas作为对象

    Java lambda表达式本质上是一个对象。你可以将变量指向lambda表达式并传递,就像处理其他任何对象一样。这是一个例子:

    public interface MyComparator {
    
        public boolean compare(int a1, int a2);
    
    }
    
    MyComparator myComparator = (a1, a2) -> return a1 > a2;
    
    boolean result = myComparator.compare(2, 5);
    

    第一个代码块显示了lambda表达式实现的接口。

    第二个代码块显示了lambda表达式的定义,lambda表达式如何分配给变量,以及最后如何通过调用其实现的接口方法来调用lambda表达式。


    变量捕获

    在某些情况下,Java lambda表达式能够访问在lambda表达式主体外部声明的变量。

    Java lambdas可以捕获以下类型的变量:

    • 局部变量
    • 实例变量
    • 静态变量

    这些变量捕获的每一个将在以下各节中进行描述。


    局部变量捕获

    Java lambda可以捕获在lambda主体外部声明的局部变量的值。为了说明这一点,首先看一下这个函数式接口:

    public interface MyFactory {
        public String create(char[] chars);
    }
    

    现在,看一下实现MyFactory接口的lambda表达式:

    MyFactory myFactory = (chars) -> {
        return new String(chars);
    };
    

    现在,此lambda表达式仅引用传递给它的参数值(chars)。但是我们可以改变一下。这是引用在lambda函数主体外部声明的String变量的更新版本:

    String myString = "Test";
    
    MyFactory myFactory = (chars) -> {
        return myString + ":" + new String(chars);
    };
    

    如你所见,lambda表达式主体现在引用了在lambda表达式主体外部声明的局部变量myString。当且仅当被引用的变量是“有效只读(如果一个局部变量在初始化后从未被修改过,那么它就是有效只读)”时才有可能,这意味着在赋值之后它不会改变其值。如果myString变量的值稍后更改,则编译器将抱怨从lambda主体内部对其的引用。


    实例变量捕获

    Lambda表达式还可以捕获创建Lambda的对象中的实例变量。这是示例:

    public class EventConsumerImpl {
    
        private String name = "MyConsumer";
    
        public void attach(MyEventProducer eventProducer){
            eventProducer.listen(e -> {
                System.out.println(this.name);
            });
        }
    }
    

    注意lambda表达式主体中对this.name的引用。这将捕获封闭的EventConsumerImpl对象的 name 实例变量。甚至可以在捕获实例变量后更改其值——该值将反映在lambda内部。

    this语义实际上是Java lambda与接口的匿名实现不同的地方之一。匿名接口实现可以有自己的实例变量,这些实例变量可以通过this进行引用。但是,lambda不能拥有自己的实例变量,因此它始终指向封闭的对象。

    注意:EventConsumer的设计不是很优雅。我只是这样写来说明实例变量捕获。


    静态变量捕获

    Java lambda表达式还可以捕获静态变量。

    因为只要可以访问静态变量(包作用域或public作用域),Java应用程序中的任何地方都可以访问静态变量。

    这是一个创建lambda表达式的示例类,该lambda表达式从lambda表达式主体内部引用静态变量:

    public class EventConsumerImpl {
        private static String someStaticVar = "Some text";
    
        public void attach(MyEventProducer eventProducer){
            eventProducer.listen(e -> {
                System.out.println(someStaticVar);
            });
        }
    }
    

    lambda捕获到静态变量后,它的值也可以更改。

    同样,上述类设计不太合理。不要对此考虑太多。该类主要用于向你显示lambda表达式可以访问静态变量。


    Lambda方法引用

    如果你的lambda表达式所做的只是用传递给lambda的参数调用另一个方法,则Java lambda实现提供了更简洁的方式表示该方法调用。

    首先,这是一个函数式接口:

    public interface MyPrinter{
        public void print(String s);
    }
    

    以下是创建实现MyPrinter接口的Java lambda表达式的示例:

    MyPrinter myPrinter = (s) -> { System.out.println(s); };
    

    由于lambda主体仅由一个语句组成,因此我们实际上可以省略括号{}。另外,由于lambda方法只有一个参数,因此我们可以省略该参数周围的括号()。更改之后的lambda表达式:

    MyPrinter myPrinter = s -> System.out.println(s);
    

    由于所有lambda主体所做的工作都是将字符串参数转发给System.out.println()方法,因此我们可以将上述lambda声明替换为方法引用。以下是lambda表达式引用方法的实例:

    MyPrinter myPrinter = System.out::println;
    

    注意双冒号::。它会向Java编译器发出信号,这是方法引用。引用的方法是双冒号之后的内容。拥有被引用方法的任何类或对象都在双冒号之前。

    你可以引用以下类型的方法:

    • 静态方法
    • 参数对象的实例方法
    • 实例方法
    • 构造方法

    以下各节介绍了每种类型的方法引用。


    静态方法引用

    最容易引用的方法是静态方法。

    首先是函数式接口的示例:

    public interface Finder {
        public int find(String s1, String s2);
    }
    

    这是一个静态方法:

    public class MyClass{
        public static int doFind(String s1, String s2){
            return s1.lastIndexOf(s2);
        }
    }
    

    最后是引用静态方法的Java lambda表达式:

    Finder finder = MyClass::doFind;
    

    由于Finder.find()和MyClass.doFind()方法的参数匹配,因此可以创建实现Finder.find()并引用MyClass.doFind()方法的lambda表达式。


    参数方法引用

    也可以将其中一个参数的方法引用到lambda。

    函数式接口如下:

    public interface Finder {
        public int find(String s1, String s2);
    }
    

    该接口用于表示能在s1中搜索s2的出现的部分。下面是一个Java lambda表达式的示例,它调用indexOf() 搜索:

    Finder finder = String::indexOf;
    

    这等价以下lambda定义:

    Finder finder = (s1, s2) -> s1.indexOf(s2);
    

    请注意简洁方式版本是如何引用单个方法的。Java编译器尝试将引用的方法与第一个参数类型相匹配,使用第二个参数类型作为被引用方法的参数。


    实例方法引用

    第三,还可以从lambda表达式中引用实例方法。

    首先,让我们来看一个函数式接口定义:

    public interface Deserializer {
        public int deserialize(String v1);
    }
    

    此接口表示一个组件,该组件能够将字符串“反序列化”为int。

    现在看看这个StringConverter类:

    public class StringConverter {
        public int convertToInt(String v1){
            return Integer.valueOf(v1);
        }
    }
    

    convertToInt()方法与Deserializer deserialize()方法的deserialize()方法具有相同的签名。因此,我们可以创建StringConverter的实例,并从Java lambda表达式引用其convertToInt()方法,如下所示:

    StringConverter stringConverter = new StringConverter();
    
    Deserializer des = stringConverter::convertToInt;
    

    第二行创建的lambda表达式引用在第一行创建的StringConverter实例的convertToInt方法。


    构造方法引用

    最后,可以引用一个类的构造方法。你可以通过在类名后加上:: new来完成此操作,如下所示:

    MyClass::new
    

    来看看如何在lambda表达式中引用构造方法。

    函数式接口定义如下:

    public interface Factory {
        public String create(char[] val);
    }
    

    此接口的create()方法与String类中某个构造函数的签名匹配。因此,此构造函数可以被lambda表达式用到。下面是一个示例:

    Factory factory = String::new;
    

    等同于如下lambda表达式:

    Factory factory = chars -> new String(chars);
    


    水平有限,难免错漏,欢迎指出,或直接查看原文!

  • 相关阅读:
    生活中残忍的真相
    @ControllerAdvice 拦截异常并统一处理
    自律的人生
    50建议
    公众号自动发送红包
    增加记忆力方式
    MySQL行转列与列转行
    微人生的活法
    人生三出戏
    很重要的一点是 关注并坚持去做那些短期看不到效果,但对你影响深远的事情。
  • 原文地址:https://www.cnblogs.com/three-fighter/p/13326627.html
Copyright © 2011-2022 走看看