Java8新特性 - Lambda表达式

栏目: 编程语言 · Java · 发布时间: 6年前

内容简介:Java8新特性 - Lambda表达式

本文来源于oracle官方网站 http://www.oracle.com/technetwork/cn/articles/java/lambda-1984522-zhs.html ,有修改。

Lambda 表达式也称为闭包,是匿名类的简短形式。Lambda 表达式简化了单一抽象方法声明接口的使用,因此 lambda 表达式也称为功能接口。在 Java SE 7 中,单一方法接口可使用下列选项之一实现。

  • 创建接口实现类。
  • 创建匿名类。

可以使用 lambda 表达式实现功能接口,无需创建类或匿名类。Lambda 表达式只能用于单一方法声明接口。

Lambda 表达式旨在支持多核处理器架构,这种架构依赖于提供并行机制的软件,而该机制可以提高性能、减少完成时间。

Lambda 表达式具有以下优点:

  • 简明的语法
  • 方法引用和构造函数引用
  • 相比于匿名类,减少了运行时开销

前提条件

Lambda 表达式的语法

Lambda 表达式的语法如下所示。

(formal parameter list) ->{ expression or statements }

参数列表是一个逗号分隔的形式参数列表,这些参数与功能接口中单一方法的形式参数相对应。指定参数类型是可选项;如果未指定参数类型,将从上下文推断。参数列表必须用括号括起来,但当指定的单一参数不带参数类型时除外;指定单一形式参数时可以不带括号。如果功能接口方法不指定任何形式参数,则必须指定空括号。

参数列表后面是 -> 运算符,然后是 lambda 主体,即单一表达式或语句块。Lambda 主体的结果必须是下列值之一:

  • void,如果功能接口方法的返回结果是 void
  • Java 类型、基元类型或引用类型,与功能接口方法的返回类型相同,并且用return语句返回结果。

可以看出Lambda表达式返回的结果必须与功能接口方法的返回类型相同,并且用return语句返回结果。

Lambda 主体根据以下选项之一返回结果:

  • 如果 lambda 主体是单一表达式,则返回表达式的值。
  • 如果该方法具有返回类型,且 lambda 主体不是单一表达式,则 lambda 主体必须使用 return 语句返回值。
  • 如果功能接口方法的结果是 void,可以提供一个 return 语句,但这不是必需的。

功能接口

Lambda 表达式与功能接口一起使用,功能接口实际上是一种只有一个抽象方法的接口;功能接口可以包含一个同时也存在于 Object 类中的方法。功能接口的示例有 java.util.concurrent.Callable(具有单一方法 call())和 java.lang.Runnable(具有单一方法 run())。

区别在于,匿名接口类需要指定一个实例创建表达式,以便接口和编译器用来创建接口实现类的实例。与指定接口类型(或类类型)的匿名类不同,lambda 表达式不指定接口类型。从上下文推断调用 lambda 表达式的功能接口,也称为 lambda 表达式的目标类型。

Lambda 表达式的目标类型

Lambda 表达式有一个隐式的目标类型与之关联,因为未明确指定接口类型。在 lambda 表达式中,lambda 转换的目标类型必须是一个功能接口。从上下文推断目标类型。因此,lambda 表达式只能用在可以推断目标类型的上下文中。此类上下文包括

  • 变量声明
  • 赋值
  • return 语句
  • 数组初始值设定项
  • 方法或构造函数的参数
  • Lambda 表达式主体
  • 三元条件表达式
  • 转换表达式

用 Lambda 表达式创建 Hello 应用程序

如下是一个Hello 应用程序,声明了两个字段、两个构造函数和一个 hello() 方法来输出消息,如下所示。

public class Hello {
    String firstname;
    String lastname;

    public Hello() {
    }

    public Hello(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public void hello() {
        System.out.println("Hello " + firstname + " " + lastname);
    }

    public static void main(String[] args) {
        Hello hello = new Hello(args[0], args[1]);
        hello.hello();
    }
}

现在,我们来看看 lambda 表达式如何简化 Hello 示例中的语法。首先,我们需要创建一个功能接口,该接口包含一个返回“Hello”消息的方法。

interface HelloService {
    String hello(String firstname, String lastname);
}

创建一个 lambda 表达式,它包含两个参数,与接口方法的参数相匹配。在 lambda 表达式的主体中,使用 return 语句创建并返回根据 firstname 和 lastname 构造的“Hello”消息。返回值的类型必须与接口方法的返回类型相同,并且 lambda 表达式的目标必须是功能接口 HelloService。如下。

public class Hello {
    interface HelloService {
        String hello(String firstname, String lastname);
    }

    public static void main(String[] args) {
        HelloService service = (String firstname, String lastname) -> {
            String hello = "Hello " + firstname + " " + lastname;
            return hello;
        };
        System.out.println(service.hello("csdn", "com"));
    }
}

运行结果如下:

Java8新特性 - Lambda表达式

Lambda 表达式中的局部变量

Lambda 表达式不会定义新的作用域;lambda 表达式的作用域与封闭作用域相同。例如,如果 Lambda 主体声明的局部变量与封闭作用域内的变量重名,将产生编译器错误Lambda expression’s local variable i cannot re-declare another local variable defined in an enclosing scope,如下图所示。

Java8新特性 - Lambda表达式

局部变量无论是在 lambda 表达式主体中声明,还是在封闭作用域中声明,使用之前都必须先初始化,否则将产生编译器错误The local variable i may not have been initialized,如下图所示。

Java8新特性 - Lambda表达式

lambda 表达式中使用的变量必须处于终态或等效终态。要证明这一点,请声明并初始化局部变量:

int i=5;

给 lambda 表达式主体中的变量赋值。将产生编译器错误Variable i is required to be final or effectively final Java8新特性 - Lambda表达式

可按如下方式将变量 i 声明为终态。

final int i=5;

否则,该变量必须为等效终态,即不能在 lambda 表达式中对该变量赋值。封闭上下文中的方法参数变量和异常参数变量也必须处于终态或等效终态。

Lambda 主体中的 this 和 super 引用与封闭上下文中一样,因为 lambda 表达式不会引入新的作用域,这与匿名类不同。

Lambda 表达式是一种匿名方法

Lambda 表达式实际上是一种匿名方法实现;指定形式参数,并使用 return 语句返回值。匿名方法必须按照以下规则所规定的与其实现的功能接口方法兼容。

  • Lambda 表达式返回的结果必须与功能接口方法的结果兼容。如果结果是 void,则 lambda 主体必须与 void 兼容。如果返回一个值,则 lambda 主体必须与值兼容。返回值的类型可以是功能接口方法声明中返回类型的子类型。
  • Lambda 表达式签名必须与功能接口方法的签名相同。Lambda 表达式签名不能是功能接口方法签名的子签名。
  • Lambda 表达式只能抛出那些在功能接口方法的 throws 子句中声明了异常类型或异常超类型的异常。

可变参数与数组参数之间不作区分。例如,功能接口方法按如下方式声明数组类型参数:

interface MyInt {
    void setInt(int[] i);
}

Lambda 表达式的参数列表可以声明可变参数:

MyInt myInt = (int... in)->{};

异常处理

Lambda 表达式主体抛出的异常不能超出功能接口方法的 throws 子句中指定的异常数。如果 lambda 表达式主体抛出异常,功能接口方法的 throws 子句必须声明相同的异常类型或其超类型。

如在 HelloService 接口的 hello 方法中不声明 throws 子句,从 lambda 表达式主体抛出异常。将产生编译器错误Unhandled exception type Exception,如下图 所示。

Java8新特性 - Lambda表达式

如果在功能接口方法中添加与所抛出异常相同的异常类型,编译器错误将得以解决,如下图所示。

Java8新特性 - Lambda表达式

Lambda 表达式是一种多态表达式

Lambda 表达式的类型是从目标类型推导出来的类型。相同的 lambda 表达式在不同的上下文中可以有不同的类型。此类表达式称为多态表达式。要证明这一点,定义两个具有相同抽象方法签名的功能接口,例如:

interface HelloService {
        String hello(String firstname, String lastname) throws Exception;
    }

    interface HelloService2 {
        String hello(String firstname, String lastname) throws Exception;
    }

例如,相同的 lambda 表达式(下面的表达式)可用于所声明的方法具有相同签名、返回类型以及 throws 子句的两个功能接口:

(String firstname, String lastname) -> {
            String hello = "Hello " + firstname + " " + lastname;
            throw new Exception();
        };

没有上下文,前面的 lambda 表达式没有类型,因为它没有目标类型。但是,如果在具有目标类型的上下文中使用,则 lambda 表达式可以根据目标类型具有不同的类型。在以下两种情况下,前面的 lambda 表达式具有不同的类型,因为目标类型不同:HelloService 和 HelloService2。

HelloService service = (String firstname, String lastname) -> {
            String hello = "Hello " + firstname + " " + lastname;
            throw new Exception();
        };
        HelloService2 service2 = (String firstname, String lastname) -> {
            String hello = "Hello " + firstname + " " + lastname;
            throw new Exception();
        };

不支持泛型 Lambda。Lambda 表达式不能引入类型变量。

如何推断目标类型和 Lambda 参数类型?

对于 lambda 表达式,从上下文推断目标类型。因此,lambda 表达式只能用在可以推断目标类型的上下文中。这类上下文包括:变量声明、赋值语句、return 语句、数组初始值设定项、方法或构造函数的参数、lambda 表达式主体、条件表达式和转换表达式。

Lambda 的形式参数类型也从上下文推断。

return 语句中的 Lambda 表达式

Lambda 表达式可以用在 return 语句中。return 语句中使用 lambda 表达式的方法的返回类型必须是一个功能接口。例如,返回 Runnable 的方法的 return 语句中包含一个 lambda 表达式,如下所示。

public class HelloRunnable2 {

    public static Runnable getRunnable() {
        return () -> {
            System.out.println("Hello from a thread");
        };
    }

    public static void main(String args[]) {

        new Thread(getRunnable()).start();
    }

}

Lambda 表达式未声明任何参数,因为 Runnable 接口的 run() 方法没有声明任何形式参数。Lambda 表达式不返回值,因为 run() 方法的结果是 void。

Lambda 表达式作为目标类型

Lambda 表达式本身可以用作内部 lambda 表达式的目标类型。Callable 中的 call() 方法返回一个 Object,但是 Runnable 中的 run() 方法没有返回类型。如下所示的HelloCallable2 类中,内部 lambda 表达式的目标类型是 Runnable,外部 lambda 表达式的目标类型是 Callable。目标类型是从上下文(对 Callable 类型的引用变量进行赋值)推断出来的。

import java.util.concurrent.Callable;

public class HelloCallable2 {

   public static void main(String[] args) {
      try {

         Callable<Runnable> c = () -> {
            return () -> {
                System.out.println("Hello from Callable");
            };
        };
        c.call().run();

      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

内部 lambda 表达式 () -> {System.out.println(“Hello from Callable”);} 的类型被推断为 Runnable,因为参数列表为空,并且结果是 void;匿名方法签名和结果与 Runnable 接口中的 run() 方法相同。外部 lambda 表达式 () -> Runnable 的类型被推断为 Callable,因为 Callable 中的 call() 方法没有声明任何形式参数,并且结果类型是类型参数 V。HelloCallable2 的输出如下图所示。

Java8新特性 - Lambda表达式

数组初始值设定项中的 Lambda 表达式

Lambda 表达式可以用在数组初始值设定项中,但不能使用泛型数组初始值设定项(这是 java 的语法规范)。例如,以下泛型数组初始值设定项中的 lambda 表达式将产生编译器错误:

Callable<String>[] c=new Callable<String>[]{ ()->"a", ()->"b", ()->"c" };

将产生编译器错误Cannot create a generic array of Callable。

要在数组初始值设定项中使用 lambda 表达式,请指定一个非泛型数组初始值设定项,代码如下所示。

Callable<String>[] c=new Callable[]{ ()->"Hello from Callable a", 
                ()->"Hello from Callable b", ()->"Hello from Callable c" };

Callable Array 中的每个数组初始值设定项变量都是一个 Callable 类型的 lambda 表达式。Lambda 表达式的参数列表为空,并且 lambda 表达式的结果是一个 String 类型的表达式。每个 lambda 表达式的目标类型从上下文推断为 Callable 类型。CallableArray 的输出如图 19 所示。

转换 Lambda 表达式

Lambda 表达式的目标类型有时可能并不明确。例如,在下面的赋值语句中,lambda 表达式用作 AccessController.doPrivileged 方法的方法参数。Lambda 表达式的目标类型不明确,因为多个功能接口(PrivilegedAction 和 PrivilegedExceptionAction)都可以是 lambda 表达式的目标类型。

String user = AccessController.doPrivileged(() -> System.getProperty("user.name"));

将产生编译器错误The method doPrivileged(PrivilegedAction) is ambiguous for the type AccessController,如下图所示。

Java8新特性 - Lambda表达式

我们可以使用 lambda 表达式的转换将目标类型指定为 PrivilegedAction,如下代码所示。

String user = AccessController
                .doPrivileged((PrivilegedAction<String>) () -> System
                        .getProperty("user.name"));

条件表达式中的 Lambda 表达式

Lambda 表达式可以用在三元条件表达式中,后者的值是这两个操作数中的任何一个,具体取决于 boolean 条件为 true 还是 false。

如下所示的 HelloCallableConditional 类中,lambda 表达式 () -> “Hello from Callable:flag true”) 和 () -> “Hello from Callable:flag false”) 构成了用于赋值的这两个可供选择的表达式。Lambda 表达式的目标类型是从上下文(对 Callable 引用变量进行赋值)推断出来的。随后,使用该引用变量调用 call() 方法。

import java.util.concurrent.Callable;

public class HelloCallableConditional {

   public static void main(String[] args) {
      try {

         boolean flag = true;
         Callable<String> c = flag ? (() -> "Hello from Callable: flag true")
               : (() -> "Hello from Callable: flag false");

         System.out.println(c.call());
      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

HelloCallableConditional 的输出如图 22 所示。

Java8新特性 - Lambda表达式

推断重载方法中的目标类型

调用重载方法时,将使用与 lambda 表达式最匹配的方法。我们将使用目标类型和方法参数类型选择最佳方法。

如下HelloRunnableOrCallable 类中,指定了两个返回类型为 String 的 hello() 方法(hello() 方法被重载):它们的参数类型分别是 Callable 和 Runnable。

将调用 hello() 方法,其中 lambda 表达式作为方法参数。由于 lambda 表达式 () -> “Hello Lambda” 返回 String,因此会调用 hello(Callable) 方法并输出 Hello from Callable,因为 Callable 的 call() 方法具有返回类型,而 Runnable 的 run() 方法没有。

import java.util.concurrent.Callable;

public class HelloRunnableOrCallable {

    static String hello(Runnable r) {
        return "Hello from Runnable";
    }

    static String hello(Callable c) {
        return "Hello from Callable";
    }

    public static void main(String[] args) {

        String hello = hello(() -> "Hello Lambda");
        System.out.println(hello);

    }
}

HelloCallableConditional 的输出如下图所示。

Java8新特性 - Lambda表达式

Lambda 表达式中的 this

在 lambda 表达式外面,this 引用当前对象。在 lambda 表达式里面,this 引用封闭的当前对象。

在如下所示的示例中,Runnable 是 lambda 表达式的目标类型。在 lambda 表达式主体中,指定了对 this 的引用。创建 Runnable r 实例并调用 run() 方法后,this 引用将调用封闭实例,并从 toString() 方法获得封闭实例的 String 值。将输出 Hello from Class HelloLambda 消息。

public class HelloLambda {
     Runnable r = () -> { System.out.println(this); };


     public String toString() { return "Hello from Class HelloLambda"; }

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

     }
   }

HelloLambda 的输出如图 24 所示。

Java8新特性 - Lambda表达式

Lambda 表达式的参数名称

为 lambda 表达式的形式参数创建新名称。如果用作 lambda 表达式参数名称的名称与封闭上下文中局部变量名称相同,将产生编译器错误。在以下示例中,lambda 表达式的参数名称被指定为 e1 和 e2,它们同时还用于局部变量 Employee e1 和 Employee e2。

Employee e1 = new Employee(1,"A", "C");   
Employee e2 = new Employee(2,"B","D" );   
List<Employee> list = new ArrayList<Employee>();   
list.add(e1);
list.add(e2);

Collections.sort(list, (Employee e1, Employee e2) -> e1.getLastName().compareTo(e2.getLastName()));

Lambda 表达式参数 e1 将导致编译器错误,如下图 所示。

Java8新特性 - Lambda表达式

对局部变量的引用

在提供匿名内部类的替代方案时,局部变量必须处于终态才能在 lambda 表达式中访问的要求被取消。JDK 8 也取消了局部变量必须处于终态才能从内部类访问的要求。在 JDK 7 中,必须将从内部类访问的局部变量声明为终态。

在内部类或 lambda 表达式中使用局部变量的要求已从“终态”修改为“终态或等效终态”。

方法引用

Lambda 表达式定义了一个匿名方法,其中功能接口作为目标类型。可以使用方法引用来调用具有名称的现有方法,而不是定义匿名方法。在如下所示的 EmployeeSort 示例中,以下方法调用将 lambda 表达式作为方法参数。

Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName()));

可以按以下方式将 lambda 表达式替换为方法引用:

Collections.sort(list, Employee::compareByLastName);

分隔符 (::)可用于方法引用。compareByLastName 方法是 Employee 类中的静态方法。

public static int compareByLastName(Employee x, Employee y){
    return x.getLastName().compareTo(y.getLastName())
};

对于非静态方法,方法引用可与特定对象的实例一起使用。通过将 compareByLastName 方法非静态化,可按如下方式将方法引用与与 Employee 实例结合使用:

Employee employee=new Employee();
Collections.sort(list, employee::compareByLastName);

方法引用甚至不必是所属对象的实例方法。方法引用可以是任何任意类对象的实例方法。例如,通过方法引用,可以使用 String 类的 compareTo 方法对 String List 进行排序。

String e1 = new String("A");   
String e2 = new String("B");    
List<String> list = new ArrayList<String>();   
list.add(e1);list.add(e2);
Collections.sort(list, String::compareTo);

方法引用是 lambda 表达式的进一步简化。

构造函数引用

方法引用的作用是方法调用,而构造函数引用的作用是构造函数调用。方法引用和构造函数引用是 lambda 转换,方法引用和构造函数引用的目标类型必须是功能接口。

我们将以 Multimap 为例讨论构造函数引用。Multimap 是一个 Google Collections 实用程序。图像类型的 Multimap 按如下方式创建:

Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps
        .newListMultimap(
              Maps.<ImageTypeEnum, Collection<String>> newHashMap(),
              new Supplier<List<String>>() { public List<String> get() { 
        return new ArrayList<String>(); 
            } 
        });

在 Multimap 示例中,使用构造函数按如下方式创建 Supplier

new Supplier<List<String>>() { 
            public List<String> get() { 
                return new ArrayList<String>(); 
            } 
        }

构造函数返回 ArrayList。通过构造函数引用,可以使用简化的语法按如下方式创建 Multimap:

Multimap<ImageTypeEnum, String> imageTypeMultiMap = 
Multimaps.newListMultimap(Maps.<ImageTypeEnum, Collection<String>> newHashMap(),ArrayList<String>::new);

如下显示了使用构造函数引用的 Multimap 示例,即

ImageTypeMultiMap 类。

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.google.common.base.Supplier;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

public class ImageTypeMultiMap {
   enum ImageTypeEnum {
      tiff, tif, gif, jpeg, jpg, png, bmp
   }

   public static void main(String[] args) {
      Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps
            .newListMultimap(
                  Maps.<ImageTypeEnum, Collection<String>> newHashMap(),
               ArrayList<String>::new);

      imageTypeMultiMap.put(ImageTypeEnum.tiff, "tiff");
      imageTypeMultiMap.put(ImageTypeEnum.tif, "tif");
      imageTypeMultiMap.put(ImageTypeEnum.gif, "gif");
      imageTypeMultiMap.put(ImageTypeEnum.jpeg, "jpeg");
      imageTypeMultiMap.put(ImageTypeEnum.jpg, "jpg");
      imageTypeMultiMap.put(ImageTypeEnum.png, "png");
      imageTypeMultiMap.put(ImageTypeEnum.bmp, "bmp");

      System.out.println("Result: " + imageTypeMultiMap);
   }
}

总结

本文介绍了 JDK 8 的新特性 — lambda 表达式,其语法简洁,是匿名类的简短形式。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

电商产品经理宝典:电商后台系统产品逻辑全解析

电商产品经理宝典:电商后台系统产品逻辑全解析

刘志远 / 电子工业出版社 / 2017-10-1 / 49.00元

时至今日,对于产品经理的要求趋向业务型、平台型,甚至产生了细分领域专家。纯粹的前端产品经理(页面、交互)逐渐失去竞争力。而当后台产品经理的视野开始从功能延伸到模块,再延伸到子系统,最后关注整体系统时,就有了把控平台型产品的能力。 《电商产品经理宝典:电商后台系统产品逻辑全解析》围绕“电商后台产品”,从电商的整体产品架构入手,逐步剖析各支撑子系统。通过学习电商产品后台的架构和逻辑,可以让读者从......一起来看看 《电商产品经理宝典:电商后台系统产品逻辑全解析》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器