了解如何在Java程序中使用lambda表达式和函数式编程技术

Java中的Lambda表达式入门

 

在Java SE 8之前,通常使用匿名类将功能传递给方法。这种做法混淆了源代码,使其难以理解。Java 8通过引入lambda消除了这个问题。本教程首先介绍lambda语言功能,然后提供有关使用lambda表达式以及目标类型进行函数编程的更详细的介绍。您还将学习lambda如何与范围,局部变量,thisand super关键字以及Java异常交互。

请注意,本教程中的代码示例与JDK 12兼容。

自己发现类型

在本教程中,我不会介绍您以前没有学过的任何非lambda语言功能,但是我将通过本系列之前没有讨论过的类型来演示lambda。一个例子是java.lang.Math类。我将在以后的Java 101教程中介绍这些类型。现在,我建议阅读JDK 12 API文档以了解有关它们的更多信息。

下载获取代码下载本教程中示例应用程序的源代码。由Jeff Friesen为JavaWorld创建。

Lambdas:入门

甲lambda表达式(拉姆达)描述了一个代码块(一匿名功能可以传递到构造或方法用于后续执行)。构造函数或方法接收lambda作为参数。考虑以下示例:

[ 在这个由12部分组成的综合课程中,从入门概念到高级设计模式学习Java!]

() -> System.out.println("Hello")

此示例标识用于将消息输出到标准输出流的lambda。从左到右,()标识lambda的形式参数列表(示例中没有参数),->表示该表达式是lambda,并且System.out.println("Hello")是要执行的代码。

Lambda简化了功能性接口的使用,它们是带注释的接口,每个接口都精确地声明一个抽象方法(尽管它们也可以声明默认,静态和私有方法的任意组合)。例如,标准类库提供java.lang.Runnable具有单个抽象void run()方法的接口。该功能接口的声明如下所示:

@FunctionalInterface
public interface Runnable
{
   public abstract void run();
}

类库注释Runnable与@FunctionalInterface,这是的一个实例
java.lang.FunctionalInterface注释类型。FunctionalInterface用于注释在lambda上下文中使用的那些接口。

Lambda没有明确的接口类型。相反,编译器使用周围的上下文来推断指定lambda时要实例化的功能接口-lambda 绑定到该接口。例如,假设我指定了以下代码片段,它将前一个lambda作为参数传递给java.lang.Thread类的Thread(Runnable target)构造函数:

new Thread(() -> System.out.println("Hello"));

编译器确定将lambda传递给它,Thread(Runnable r)因为这是唯一满足lambda的构造Runnable函数:是一个函数接口,lambda的空形式参数列表()与的空参数列表匹配run(),并且返回类型(void)也一致。Lambda绑定到Runnable。

清单1将源代码提供给一个小型应用程序,让您可以使用此示例。

清单1. LambdaDemo.java(版本1)

public class LambdaDemo
{
   public static void main(String[] args)
   {
      new Thread(() -> System.out.println("Hello")).start();
   }
}

编译清单1(javac LambdaDemo.java)并运行应用程序(java LambdaDemo)。您应该观察以下输出:

Hello

Lambda可以大大简化您必须编写的源代码数量,并且还可以使源代码更易于理解。例如,如果没有lambda,您可能会指定清单2的更详细的代码,该代码基于实现的匿名类的实例Runnable。

清单2. LambdaDemo.java(版本2)

public class LambdaDemo
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         System.out.println("Hello");
                      }
                   };
      new Thread(r).start();
   }
}

编译此源代码后,运行该应用程序。您将发现与先前显示的输出相同的输出。

Lambdas和Streams API

除了简化源代码外,lambda在Java的面向功能的Streams API中也起着重要作用。它们描述了传递给各种API方法的功能单元。

深入Java Lambda

为了有效地使用lambda,您必须了解lambda表达式的语法以及目标类型的概念。您还需要了解lambda如何与范围,局部变量,thisand和super关键字以及异常进行交互。我将在以下各节中介绍所有这些主题。

lambda如何实现

Lambda是根据Java虚拟机的invokedynamic指令和java.lang.invokeAPI来实现的。观看视频“ Lambda:深入了解”以了解Lambda体系结构。

Lambda语法

每个lambda都遵循以下语法:

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

的formal-parameter-list是逗号分隔的形式参数,它必须在运行时功能接口的一个抽象方法的参数相匹配的列表。如果省略它们的类型,则编译器将从使用lambda的上下文中推断出这些类型。考虑以下示例:

(double a, double b) // types explicitly specified
(a, b) // types inferred by compiler

Lambdas和var

从Java SE 11开始,您可以将类型名称替换为var。例如,您可以指定(var a, var b)。

您必须为多个或没有形式参数指定括号。但是,在指定单个形式参数时,可以省略括号(尽管不必这样做)。(这仅适用于参数名称-当还指定类型时,必须使用括号。)请考虑以下其他示例:

x // parentheses omitted due to single formal parameter
(double x) // parentheses required because type is also present
() // parentheses required when no formal parameters
(x, y) // parentheses required because of multiple formal parameters

的formal-parameter-list后面是->令牌,其后是expression-or-statements-表达式或语句块(称为lambda的主体)。与基于表达式的主体不同,必须将基于语句的主体置于open({)和close(})大括号字符之间:

(double radius) -> Math.PI * radius * radius
radius -> { return Math.PI * radius * radius; }
radius -> { System.out.println(radius); return Math.PI * radius * radius; }

第一个示例的基于表达式的lambda主体不必放在括号之间。第二个示例将基于表达式的主体转换为基于语句的主体,return必须在其中指定以返回表达式的值。最后的示例演示了多个语句,没有括号就无法表达。

Lambda实体和分号

请注意;,在前面的示例中不存在分号()。在每种情况下,lambda主体都不会以分号终止,因为lambda并非语句。但是,在基于语句的lambda主体中,每个语句必须以分号结尾。

清单3提供了一个简单的应用程序,该应用程序演示了lambda语法;请注意,此清单建立在前两个代码示例的基础上。

清单3. LambdaDemo.java(版本3)

@FunctionalInterface
interface BinaryCalculator
{
   double calculate(double value1, double value2);
}
@FunctionalInterface
interface UnaryCalculator
{
   double calculate(double value);
}
public class LambdaDemo
{
   public static void main(String[] args)
   {
      System.out.printf("18 + 36.5 = %f%n", calculate((double v1, double v2) ->
                        v1 + v2, 18, 36.5));
      System.out.printf("89 / 2.9 = %f%n", calculate((v1, v2) -> v1 / v2, 89,
                        2.9));
      System.out.printf("-89 = %f%n", calculate(v -> -v, 89));
      System.out.printf("18 * 18 = %f%n", calculate((double v) -> v * v, 18));
   }
   static double calculate(BinaryCalculator calc, double v1, double v2)
   {
      return calc.calculate(v1, v2);
   }
   static double calculate(UnaryCalculator calc, double v)
   {
      return calc.calculate(v);
   }
}

清单3首先介绍BinaryCalculator和UnaryCalculator接口,它们的calculate()方法分别对两个输入参数或单个输入参数执行计算。此清单还引入了一个LambdaDemo类,该类的main()方法演示了这些功能接口。

功能接口在static double calculate(BinaryCalculator calc, double v1, double v2)和static double calculate(UnaryCalculator calc, double v)方法中进行了演示。Lambda将代码作为数据传递给这些方法,这些方法作为BinaryCalculator或UnaryCalculator实例接收。

编译清单3并运行该应用程序。您应该观察以下输出:

18 + 36.5 = 54.500000
89 / 2.9 = 30.689655
-89 = -89.000000
18 * 18 = 324.000000

目标类型

Lambda与隐式目标类型关联,该目标类型标识Lambda绑定到的对象的类型。目标类型必须是从上下文推断出的功能接口,它将lambda限制为出现在以下上下文中:

  • 变量声明
  • 分配
  • 退货声明
  • 数组初始化器
  • 方法或构造函数参数
  • λ体
  • 三元条件表达式
  • 演员表

清单4提供了一个演示这些目标类型上下文的应用程序。

清单4. LambdaDemo.java(版本4)

import java.io.File;
import java.io.FileFilter;
import java.nio.file.Files;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
public class LambdaDemo
{
   public static void main(String[] args) throws Exception
   {
      // Target type #1: variable declaration
      Runnable r = () -> { System.out.println("running"); };
      r.run();
      // Target type #2: assignment
      r = () -> System.out.println("running");
      r.run();
      // Target type #3: return statement (in getFilter())
      File[] files = new File(".").listFiles(getFilter("txt"));
      for (int i = 0; i < files.length; i++)
         System.out.println(files[i]);
      // Target type #4: array initializer
      FileSystem fs = FileSystems.getDefault();
      final PathMatcher matchers[] =
      {
         (path) -> path.toString().endsWith("txt"),
         (path) -> path.toString().endsWith("java")
      };
      FileVisitor<Path> visitor;
      visitor = new SimpleFileVisitor<Path>()
                {
                   @Override
                   public FileVisitResult visitFile(Path file,
                                                    BasicFileAttributes attribs)
                   {
                      Path name = file.getFileName();
                      for (int i = 0; i < matchers.length; i++)
                      {
                         if (matchers[i].matches(name))
                            System.out.printf("Found matched file: '%s'.%n",
                                              file);
                      }
                      return FileVisitResult.CONTINUE;
                   }
                };
      Files.walkFileTree(Paths.get("."), visitor);
      // Target type #5: method or constructor arguments
      new Thread(() -> System.out.println("running")).start();
      // Target type #6: lambda body (a nested lambda)
      Callable<Runnable> callable = () -> () ->
         System.out.println("called");
      callable.call().run();
      // Target type #7: ternary conditional expression
      boolean ascendingSort = false;
      Comparator<String> cmp;
      cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2)
                            : (s1, s2) -> s2.compareTo(s1);
      List<String> cities = Arrays.asList("Washington", "London", "Rome",
                                          "Berlin", "Jerusalem", "Ottawa",
                                          "Sydney", "Moscow");
      Collections.sort(cities, cmp);
      for (int i = 0; i < cities.size(); i++)
         System.out.println(cities.get(i));
      // Target type #8: cast expression
      String user = AccessController.doPrivileged((PrivilegedAction<String>) ()
                                           -> System.getProperty("user.name"));
      System.out.println(user);
   }
   static FileFilter getFilter(String ext)
   {
      return (pathname) -> pathname.toString().endsWith(ext);
   }
}

若哪里有错误或您有更好的方法,请留言指出~

若要获取更多Java干货知识,请关注我~

谢谢~

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐