在Groovy与Java中处理地图的教程

98 阅读14分钟

通过这个实践演示发现Groovy和Java在地图处理方面的差异。

women programming

Java是一种伟大的编程语言,但有时我想要一种类似于Java的语言,只是要更加灵活和紧凑一些。这时我就选择了Groovy

在最近的一篇文章中,我回顾了在Groovy中创建和初始化地图与在Java中做同样事情之间的一些区别。简而言之,Groovy在设置地图和访问地图条目方面有一个简洁的语法,而在Java中则需要花费很多精力。

本文将深入探讨Groovy和Java之间在地图处理方面的更多差异。为此,我将使用用于演示JavaScript DataTables库的员工样本表。要跟上进度,首先要确保你的电脑上安装了最新版本的Groovy和Java。

安装Java和Groovy

Groovy是基于Java的,因此也需要安装Java。最近的和/或合适的Java和Groovy版本可能已经在你的Linux发行版的软件库中了,或者你可以从Apache Groovy网站上下载并安装Groovy。对于Linux用户来说,一个很好的选择是SDKMan,它可以用来获得多个版本的Java、Groovy和许多其他相关工具。在这篇文章中,我使用的SDK的版本为。

  • Java:OpenJDK 11的11.0.12-open版本
  • Groovy:版本3.0.8。

回到问题上:地图

首先,根据我的经验,地图和列表(或者至少是数组)经常在同一个程序中出现。例如,处理一个输入文件与传递一个列表非常相似;经常,当我想对输入文件(或列表)中遇到的数据进行分类时,我就会这样做,将某种值存储在查找表中,这只是地图。

第二,Java 8引入了整个Streams功能和lambdas(或匿名函数)。根据我的经验,将输入数据(或列表)转换为地图往往涉及到使用Java Streams。此外,在处理类型对象的流时,Java Streams是最灵活的,它提供了开箱即用的分组和累积设施。

Java中的雇员列表处理

这里有一个基于那些虚构的雇员记录的具体例子。下面是一个Java程序,它定义了一个Employee类来保存雇员信息,建立了一个Employee实例的列表,并以几种不同的方式处理这个列表。

     1  import java.lang.*;
     2  import java.util.Arrays;
       
     3  import java.util.Locale;
     4  import java.time.format.DateTimeFormatter;
     5  import java.time.LocalDate;
     6  import java.time.format.DateTimeParseException;
     7  import java.text.NumberFormat;
     8  import java.text.ParseException;
       
     9  import java.util.stream.Collectors;
       
    10  public class Test31 {
       
    11      static public void main(String args[]) {
       
    12          var employeeList = Arrays.asList(
    13              new Employee("Tiger Nixon", "System Architect",
    14                  "Edinburgh", "5421", "2011/04/25", "$320,800"),
    15              new Employee("Garrett Winters", "Accountant",
    16                  "Tokyo", "8422", "2011/07/25", "$170,750"),
                                                        ...
    
    81              new Employee("Martena Mccray", "Post-Sales support",
    82                  "Edinburgh", "8240", "2011/03/09", "$324,050"),
    83              new Employee("Unity Butler", "Marketing Designer",
    84                  "San Francisco", "5384", "2009/12/09", "$85,675")
    85          );
       
    86          // calculate the average salary across the entire company
       
    87          var companyAvgSal = employeeList.
    88              stream().
    89              collect(Collectors.averagingDouble(Employee::getSalary));
    90          System.out.println("company avg salary = " + companyAvgSal);
       
    91          // calculate the average salary for each location,
    92          //     compare to the company average
       
    93          var locationAvgSal = employeeList.
    94              stream().
    95              collect(Collectors.groupingBy((Employee e) ->
    96                  e.getLocation(),
    97                      Collectors.averagingDouble(Employee::getSalary)));
    98          locationAvgSal.forEach((k,v) ->
    99              System.out.println(k + " avg salary = " + v +
   100                  "; diff from avg company salary = " +
   101                  (v - companyAvgSal)));
       
   102          // show the employees in Edinburgh approach #1
       
   103          System.out.print("employee(s) in Edinburgh (approach #1):");
   104          var employeesInEdinburgh = employeeList.
   105              stream().
   106              filter(e -> e.getLocation().equals("Edinburgh")).
   107              collect(Collectors.toList());
   108          employeesInEdinburgh.
   109              forEach(e ->
   110                  System.out.print(" " + e.getSurname() + "," +
   111                      e.getGivenName()));
   112          System.out.println();
       
       
   113          // group employees by location
       
   114          var employeesByLocation = employeeList.
   115              stream().
   116              collect(Collectors.groupingBy(Employee::getLocation));
       
   117          // show the employees in Edinburgh approach #2
       
   118          System.out.print("employee(s) in Edinburgh (approach #2):");
   119          employeesByLocation.get("Edinburgh").
   120              forEach(e ->
   121                  System.out.print(" " + e.getSurname() + "," +
   122                      e.getGivenName()));
   123          System.out.println();
       
   124      }
   125  }
       
   126  class Employee {
   127      private String surname;
   128      private String givenName;
   129      private String role;
   130      private String location;
   131      private int extension;
   132      private LocalDate hired;
   133      private double salary;
       
   134      public Employee(String fullName, String role, String location,
   135          String extension, String hired, String salary) {
   136          var nn = fullName.split(" ");
   137          if (nn.length > 1) {
   138              this.surname = nn[1];
   139              this.givenName = nn[0];
   140          } else {
   141              this.surname = nn[0];
   142              this.givenName = "";
   143          }
   144          this.role = role;
   145          this.location = location;
   146          try {
   147              this.extension = Integer.parseInt(extension);
   148          } catch (NumberFormatException nfe) {
   149              this.extension = 0;
   150          }
   151          try {
   152              this.hired = LocalDate.parse(hired,
   153                  DateTimeFormatter.ofPattern("yyyy/MM/dd"));
   154          } catch (DateTimeParseException dtpe) {
   155              this.hired = LocalDate.EPOCH;
   156          }
   157          try {
   158              this.salary = NumberFormat.getCurrencyInstance(Locale.US).
   159                  parse(salary).doubleValue();
   160          } catch (ParseException pe) {
   161              this.salary = 0d;
   162          }
   163      }
       
   164      public String getSurname() { return this.surname; }
   165      public String getGivenName() { return this.givenName; }
   166      public String getLocation() { return this.location; }
   167      public int getExtension() { return this.extension; }
   168      public LocalDate getHired() { return this.hired; }
   169      public double getSalary() { return this.salary; }
   170  }

哇,对于一个简单的演示程序来说,这可是一大堆代码啊!我先分块看一下。我先把它分成几块来看看。

从最后开始,第126行到第170行定义了用于存储雇员数据的Employee 类。这里要提到的最重要的事情是,雇员记录的字段有不同的类型,在Java中,这一般会导致定义这种类型的类。你可以通过使用Project Lombok的@Data注解来自动生成Employee 类的getters(和setters),使这段代码更加紧凑。在最近的Java版本中,我可以将这类东西声明为一个记录而不是一个类,因为其全部意义在于存储数据。将数据存储为一个Employee 实例的列表,有利于使用Java流。

第12行到第85行创建了Employee 实例的列表,所以现在你已经处理了170行中的119行。

前面有9行导入语句。有趣的是,没有任何与地图相关的导入!这部分是因为我使用了一个与地图相关的导入。这一方面是因为我使用的流方法产生的结果是地图,另一方面是因为我使用了var 关键字来声明变量,所以类型是由编译器推断出来的。

上述代码中有趣的部分发生在第86至123行。

在第87-90行中,我将employeeList转换为一个流(第88行),然后使用collect() ,将Collectors.averagingDouble() 方法应用于Employee::getSalary (第89行)方法,以计算整个公司的平均工资。这是纯函数式的列表处理;不涉及地图。

在第93-101行,我再次将employeeList转换为一个流。然后我使用Collectors.groupingBy() 方法创建一个地图,其键值是雇员的位置,由e.getLocation() 返回,其值是每个位置的平均工资,由Collectors.averagingDouble() 返回,再次应用于Employee::getSalary 方法,应用于位置子集中的每个雇员,而不是整个公司。也就是说,groupingBy()方法按地点创建子集,然后进行平均化。第98-101行使用forEach() ,通过地图条目打印位置、平均工资以及位置平均数和公司平均数之间的差异。

现在,假设你想只看那些位于爱丁堡的员工。第103-112行显示了实现这一目的的方法,在这里我使用流filter()方法来创建一个只有那些在爱丁堡的雇员的列表,并使用forEach() 方法来打印他们的名字。这里也没有地图。

另一种解决这个问题的方法显示在第113-123行。在这个方法中,我创建了一个地图,其中每个条目都持有按地点划分的雇员列表。首先,在第113-116行中,我使用groupingBy() 方法来产生我想要的地图,其键是雇员位置,其值是该位置雇员的子列表。然后,在第117-123行,我使用forEach() 方法来打印出爱丁堡地点的雇员名字的子列表。

当我们编译并运行上述内容时,输出结果是。

company avg salary = 292082.5
San Francisco avg salary = 284703.125; diff from avg company salary = -7379.375
New York avg salary = 410158.3333333333; diff from avg company salary = 118075.83333333331
Singapore avg salary = 357650.0; diff from avg company salary = 65567.5
Tokyo avg salary = 206087.5; diff from avg company salary = -85995.0
London avg salary = 322476.25; diff from avg company salary = 30393.75
Edinburgh avg salary = 261940.7142857143; diff from avg company salary = -30141.78571428571
Sydney avg salary = 90500.0; diff from avg company salary = -201582.5
employee(s) in Edinburgh (approach #1): Nixon,Tiger Kelly,Cedric Frost,Sonya Flynn,Quinn Rios,Dai Joyce,Gavin Mccray,Martena
employee(s) in Edinburgh (approach #2): Nixon,Tiger Kelly,Cedric Frost,Sonya Flynn,Quinn Rios,Dai Joyce,Gavin Mccray,Martena

Groovy中的雇员列表处理

Groovy一直在为处理列表和地图提供增强的设施,部分是通过扩展Java集合库,部分是通过提供闭包,这有点像lambdas。

这样做的一个结果是,Groovy中的映射可以很容易地与不同类型的值一起使用。因此,你不能被逼着做辅助的Employee类;相反,你可以直接使用一个map。让我们来看看相同功能的Groovy版本。

     1  import java.util.Locale
     2  import java.time.format.DateTimeFormatter
     3  import java.time.LocalDate
     4  import java.time.format.DateTimeParseException
     5  import java.text.NumberFormat
     6  import java.text.ParseException
       
     7  def employeeList = [
     8      ["Tiger Nixon", "System Architect", "Edinburgh",
     9          "5421", "2011/04/25", "\$320,800"],
    10      ["Garrett Winters", "Accountant", "Tokyo",
    11          "8422", "2011/07/25", "\$170,750"],

                           ...

    76      ["Martena Mccray", "Post-Sales support", "Edinburgh",
    77          "8240", "2011/03/09", "\$324,050"],
    78      ["Unity Butler", "Marketing Designer", "San Francisco",
    79          "5384", "2009/12/09", "\$85,675"]
    80  ].collect { ef ->
    81      def surname, givenName, role, location, extension, hired, salary
    82      def nn = ef[0].split(" ")
    83      if (nn.length > 1) {
    84          surname = nn[1]
    85          givenName = nn[0]
    86      } else {
    87          surname = nn[0]
    88          givenName = ""
    89      }
    90      role = ef[1]
    91      location = ef[2]
    92      try {
    93          extension = Integer.parseInt(ef[3]);
    94      } catch (NumberFormatException nfe) {
    95          extension = 0;
    96      }
    97      try {
    98          hired = LocalDate.parse(ef[4],
    99              DateTimeFormatter.ofPattern("yyyy/MM/dd"));
   100      } catch (DateTimeParseException dtpe) {
   101          hired = LocalDate.EPOCH;
   102      }
   103      try {
   104          salary = NumberFormat.getCurrencyInstance(Locale.US).
   105              parse(ef[5]).doubleValue();
   106      } catch (ParseException pe) {
   107          salary = 0d;
   108      }
   109      [surname: surname, givenName: givenName, role: role,
   110          location: location, extension: extension, hired: hired, salary: salary]
   111  }
       
   112  // calculate the average salary across the entire company
       
   113  def companyAvgSal = employeeList.average { e -> e.salary }
   114  println "company avg salary = " + companyAvgSal
       
   115  // calculate the average salary for each location,
   116  //     compare to the company average
       
   117  def locationAvgSal = employeeList.groupBy { e ->
   118      e.location
   119  }.collectEntries { l, el ->
   120      [l, el.average { e -> e.salary }]
   121  }
   122  locationAvgSal.each { l, a ->
   123      println l + " avg salary = " + a +
   124          "; diff from avg company salary = " + (a - companyAvgSal)
   125  }
       
   126  // show the employees in Edinburgh approach #1
       
   127  print "employee(s) in Edinburgh (approach #1):"
   128  def employeesInEdinburgh = employeeList.findAll { e ->
   129      e.location == "Edinburgh"
   130  }
   131  employeesInEdinburgh.each { e ->
   132      print " " + e.surname + "," + e.givenName
   133  }
   134  println()
       
   135  // group employees by location
       
   136  def employeesByLocation = employeeList.groupBy { e ->
   137      e.location
   138  }
       
   139  // show the employees in Edinburgh approach #2
       
   140  print "employee(s) in Edinburgh (approach #1):"
   141  employeesByLocation["Edinburgh"].each { e ->
   142      print " " + e.surname + "," + e.givenName
   143  }
   144  println()

因为我在这里只是写一个脚本,所以我不需要把程序主体放在一个类的方法里面;Groovy为我们处理了这个问题。

在第1-6行,我仍然需要导入数据解析所需的类。Groovy默认导入了很多有用的东西,包括java.lang.*java.util.*

在第7-90行,我使用了Groovy对列表的语法支持,将其作为逗号分隔的值,用[] 括起来。在这种情况下,有一个列表;每个子列表是雇员数据。请注意,你需要在工资字段的\ 前面加一个$ 。这是因为在一个由双引号包围的字符串中出现的$ ,表示存在一个字段,其值将被插值到字符串中。另一种方法是使用单引号。

但是我不想用一个列表来工作;我更希望有一个类似于Java版本中Employee类实例列表的地图列表。我在第90-111行使用了Groovy Collection.collect() 方法,将雇员数据的每个子列表拆开,并将其转换为一个地图。collect方法需要一个Groovy Closure参数,创建closure的语法用{} ,并将参数列为a, b, c -> ,其方式类似于Java的lambdas。大部分代码看起来与Java Employee类中的构造方法非常相似,只是子列表中有一些项目而不是构造函数的参数。然而,最后两行

[surname: surname, givenName: givenName, role: role,

    location: location, extension: extension, hired: hired, salary: salary]

创建一个键值为surname,givenName,role,location,extension,hired, 和salary 的地图。而且,由于这是闭包的最后一行,返回给调用者的值就是这个地图。不需要一个返回语句。不需要引用这些键值;Groovy认为它们是字符串。事实上,如果它们是变量,你需要把它们放在圆括号里,以表明需要对它们进行评估。分配给每个键的值出现在其右边。请注意,这是一个值具有不同类型的地图。前四个是String ,然后是intLocalDate ,和double 。本来可以用这些不同类型的元素来定义子列表,但我选择了这种方法,因为数据经常被当作字符串值从文本文件中读入。

有趣的部分出现在第112-144行。我保留了与Java版本相同的处理步骤。

在第112-114行,我使用了Groovy的Collectionaverage() 方法,和collect() 一样,它需要一个Closure参数,在这里对雇员地图列表进行迭代并挑选出salary 的值。注意,在Collection类上使用这些方法意味着你不必像在Java中那样学习如何将列表、地图或其他一些元素转化为流,然后学习流方法来处理你的计算。对于那些喜欢Java流的人来说,它们在较新的Groovy版本中是可用的。

在第115-125行,我按地点计算了平均工资。首先,在第117-119行,我把employeeList ,这是一个地图列表,使用CollectiongroupBy() 方法转化为一个地图,其键是位置值,其值是与该位置有关的雇员地图的链接子列表。然后我用collectEntries() 方法来处理这些地图条目,用average() 方法来计算每个地点的平均工资。

请注意,collectEntries() 将每个键(位置)和值(该位置的雇员子列表)传入闭包(l, el -> 字符串),并期望返回一个键(位置)和值(该位置的平均工资)的双元素列表,将这些转换为地图条目。一旦我有了按地点划分的平均工资地图,locationAvgSal ,我就可以使用Collectioneach() 方法将其打印出来,该方法也需要一个闭包。当each() 应用于一个地图时,它以与collectEntries() 相同的方式传入键(位置)和值(平均工资)。

在第126-134行,我过滤了employeeList ,得到了employeesInEdinburgh 的一个子列表,使用了findAll() 方法,该方法类似于Java Streams的filter() 方法。然后,我再次使用each() 方法来打印出爱丁堡的雇员子列表。

在第135-144行,我采取了另一种方法,将employeeList分组为每个地点的雇员子列表地图,employeesByLocation 。然后在第139-144行,我选择了爱丁堡的雇员子列表,使用表达式employeesByLocation[“Edinburgh”]each() 方法来打印出该地点的雇员姓名子列表。

为什么我经常喜欢Groovy

也许这只是我对Groovy的熟悉,它是在过去12年左右建立起来的,但我觉得用Groovy的方法来增强Collection,用所有这些以闭包为参数的方法,比用Java的方法将列表、地图或手头的任何东西转换为流,然后用流、lambdas和数据类来处理这些处理步骤,更让我感到舒服。我似乎要花更多的时间在Java等价物上,才能得到一些工作。

我也是强静态类型和参数化类型的超级粉丝,例如Java中的Map,employee> ,employee> 。然而,在日常工作中,我发现更宽松的列表和映射方法能够适应不同的类型,在现实的数据世界中能够更好地支持我,而不需要大量的额外代码。动态类型肯定会反过来咬程序员一口。不过,即使知道我可以在Groovy中打开静态类型检查,我敢说我还没有这样做超过几次。也许我对Groovy的欣赏来自于我的工作,我的工作通常涉及到把一堆数据打造成形状,然后对其进行分析;我当然不是你的普通开发者。那么,Groovy是否真的是一个更像Pythonic的Java呢?值得思考的是。

我希望在Java和Groovy中都能看到更多像average()averagingDouble() 的设施。产生加权平均数的双参数版本和平均数以外的统计方法,如中位数、标准差等等,也会很有帮助。Tabnine提供了关于实现其中一些的有趣建议。

Groovy资源

Apache Groovy网站有很多很棒的文档。其他好的资源包括Groovy对Java集合类的增强的参考页,更像教程的集合工作介绍,以及Haki先生Baeldung网站提供了很多有用的Java和Groovy的方法。学习Groovy的一个非常好的理由是学习Grails,一个建立在Hibernate、Spring Boot和Micronaut等优秀组件之上的、非常高效的全栈网络框架。