在 Groovy 与 Java 中处理 Map

通过这个实践演示,了解 Groovy 和 Java 在 Map 处理方面的差异。
1 位读者喜欢这篇文章。
women programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

Java 是一门很棒的编程语言,但有时我想要一种更灵活、更紧凑的类似 Java 的语言。这时我会选择 Groovy

在最近的一篇文章中,我回顾了 在 Groovy 中创建和初始化 Map 与在 Java 中执行相同操作之间的一些差异。简而言之,与 Java 中必要的努力相比,Groovy 具有简洁的语法来设置 Map 和访问 Map 条目。

本文将深入探讨 Groovy 和 Java 之间在 Map 处理方面的更多差异。为此,我将使用 员工示例表,该表用于演示 JavaScript DataTables 库。为了方便学习,首先请确保您的计算机上安装了最新版本的 Groovy 和 Java。

安装 Java 和 Groovy

Groovy 基于 Java,也需要安装 Java。您的 Linux 发行版的存储库中可能已经有最新和/或合适的 Java 和 Groovy 版本,或者您可以从 Apache Groovy 网站下载并安装 Groovy。对于 Linux 用户来说,一个不错的选择是 SDKMan,它可以用来获取多个版本的 Java、Groovy 和许多其他相关工具。在本文中,我使用的是 SDK 发布的版本:

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

回到问题:Map

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

其次,Java 8 引入了整个 Streams 功能和 lambda 表达式(或匿名函数)。根据我的经验,将输入数据(或 List)转换为 Map 通常涉及使用 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 类的 getter(和 setter),从而使这段代码更简洁。在较新版本的 Java 中,我可以将这类事物声明为记录而不是类,因为重点是存储数据。将数据存储为 Employee 实例列表有助于使用 Java 流。

第 12 行到第 85 行创建了 Employee 实例列表,因此您已经处理了 170 行中的 119 行。

前面有九行 import 语句。有趣的是,没有与 Map 相关的导入!这部分是因为我正在使用将 Map 作为结果的流方法,部分是因为我正在使用 var 关键字来声明变量,因此类型由编译器推断。

以上代码的有趣部分发生在第 86 行到第 123 行。

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

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

现在,假设您只想查看位于爱丁堡的员工。实现此目的的一种方法如第 103-112 行所示,我在其中使用流 filter() 方法创建一个仅包含位于爱丁堡的员工的列表,并使用 forEach() 方法打印他们的姓名。这里也没有 Map。

解决此问题的另一种方法如第 113-123 行所示。在这种方法中,我创建一个 Map,其中每个条目都包含按所在地划分的员工列表。首先,在第 113-116 行中,我使用 groupingBy() 方法生成我想要的 Map,其键是员工所在地,其值是该所在地员工的子列表。然后,在第 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 一直以来都为处理列表和 Map 提供了增强的功能,部分是通过扩展 Java Collections 库,部分是通过提供闭包,闭包有点像 lambda 表达式。

这样做的一个结果是,Groovy 中的 Map 可以轻松地与不同类型的值一起使用。因此,您不会被迫创建辅助的 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 对列表的语法支持,即用 [] 括起来的逗号分隔值。在本例中,有一个列表的列表;每个子列表都是员工数据。请注意,您需要在薪资字段中的 $ 前面加上 \。这是因为双引号括起来的字符串中出现的 $ 表示存在一个字段,该字段的值将被插入到字符串中。另一种选择是使用单引号。

但是我不希望使用列表的列表;我更希望有一个 Map 列表,类似于 Java 版本中的 Employee 类实例列表。我在第 90-111 行中使用了 Groovy Collection 的 .collect() 方法,将员工数据的每个子列表分解并将其转换为 Map。collect 方法接受一个 Groovy 闭包参数,创建闭包的语法是用 {} 包围代码,并将参数列为 a, b, c ->,方式类似于 Java 的 lambda 表达式。大多数代码看起来与 Java Employee 类中的构造函数方法非常相似,只是子列表中有项,而不是构造函数的参数。但是,最后两行——

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

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

—创建一个 Map,其键为 surnamegivenNamerolelocationextensionhiredsalary。而且,由于这是闭包的最后一行,因此返回给调用者的值是这个 Map。无需 return 语句。无需引用这些键值;Groovy 假定它们是字符串。实际上,如果它们是变量,则需要将它们放在括号中以指示需要评估它们。分配给每个键的值出现在其右侧。请注意,这是一个值类型不同的 Map:前四个是 String,然后是 intLocalDatedouble。可以定义具有这些不同类型元素的子列表,但我选择采用这种方法,因为数据通常会作为字符串值从文本文件中读取。

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

在第 112-114 行中,我使用了 Groovy Collection 的 average() 方法,该方法与 collect() 一样,接受一个闭包参数,这里迭代员工 Map 列表并挑出 salary 值。请注意,在 Collection 类上使用这些方法意味着您不必学习如何将列表、Map 或其他元素转换为流,然后学习流方法来处理您的计算,就像在 Java 中一样。对于喜欢 Java Streams 的人来说,较新的 Groovy 版本中提供了它们。

在第 115-125 行中,我计算了按所在地划分的平均工资。首先,在第 117-119 行中,我使用 Collection 的 groupBy() 方法将 employeeList(Map 列表)转换为 Map,其键是所在地值,其值是与该所在地相关的员工 Map 的链接子列表。然后,我使用 collectEntries() 方法处理这些 Map 条目,并使用 average() 方法计算每个所在地的平均工资。

请注意,collectEntries() 将每个键(所在地)和值(该所在地的员工子列表)传递到闭包(l, el -> 字符串)中,并期望返回一个包含键(所在地)和值(该所在地的平均工资)的二元素列表,将它们转换为 Map 条目。一旦我有了按所在地划分的平均工资 Map,locationAvgSal,我就可以使用 Collection 的 each() 方法将其打印出来,该方法也接受闭包。当 each() 应用于 Map 时,它会以与 collectEntries() 相同的方式传入键(所在地)和值(平均工资)。

在第 126-134 行中,我过滤 employeeList 以获取 employeesInEdinburgh 的子列表,使用 findAll() 方法,该方法类似于 Java Streams 的 filter() 方法。然后,我再次使用 each() 方法打印出爱丁堡的员工子列表。

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

为什么我通常更喜欢 Groovy

也许只是因为我对 Groovy 的熟悉,这种熟悉是在过去 12 年左右的时间里建立起来的,但我感觉 Groovy 增强 Collection 的方法更舒服,所有这些方法都将闭包作为参数,而不是 Java 的方法,即将列表、Map 或手头的任何东西转换为流,然后使用流、lambda 表达式和数据类来处理处理步骤。在使某些东西工作之前,我似乎花费了更多的时间在 Java 等价物上。

我也是强静态类型和参数化类型的忠实拥护者,例如 Java 中的 Map 。但是,在日常工作中,我发现列表和 Map 容纳不同类型的更宽松方法在实际数据世界中更好地支持了我,而无需大量额外的代码。动态类型肯定会反噬程序员。尽管如此,即使知道我可以在 Groovy 中打开静态类型检查,但我敢打赌我这样做的次数不超过几次。也许我对 Groovy 的欣赏来自于我的工作,我的工作通常涉及将大量数据塑造成形,然后对其进行分析;我当然不是你们的普通开发者。那么 Groovy 真的更像 Pythonic Java 吗?值得思考。

我希望在 Java 和 Groovy 中看到更多像 average()averagingDouble() 这样的工具。用于生成加权平均值和平均值以外的统计方法的双参数版本(如中位数、标准差等)也将有所帮助。Tabnine 为实现其中一些功能提供了有趣的建议。

Groovy 资源

Apache Groovy 网站有很多很棒的文档。其他好的来源包括 Groovy 对 Java Collection 类的增强功能的参考页面,使用集合的更像教程的介绍,以及 Mr. HakiBaeldung 网站提供了许多关于 Java 和 Groovy 的有用的操作指南。学习 Groovy 的一个真正重要的原因是学习 Grails,这是一个非常高效的全栈 Web 框架,它构建在 Hibernate、Spring Boot 和 Micronaut 等优秀组件之上。

接下来阅读什么
标签
Chris Hermansen portrait Temuco Chile
自从 1978 年毕业于不列颠哥伦比亚大学以来,我几乎从不离开电脑,自 2005 年以来一直是全职 Linux 用户,1986 年至 2005 年一直是全职 Solaris 和 SunOS 用户,在此之前是 UNIX System V 用户。

评论已关闭。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.