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,其键为 surname
、givenName
、role
、location
、extension
、hired
和 salary
。而且,由于这是闭包的最后一行,因此返回给调用者的值是这个 Map。无需 return 语句。无需引用这些键值;Groovy 假定它们是字符串。实际上,如果它们是变量,则需要将它们放在括号中以指示需要评估它们。分配给每个键的值出现在其右侧。请注意,这是一个值类型不同的 Map:前四个是 String
,然后是 int
、LocalDate
和 double
。可以定义具有这些不同类型元素的子列表,但我选择采用这种方法,因为数据通常会作为字符串值从文本文件中读取。
有趣的部分出现在第 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. Haki。Baeldung 网站提供了许多关于 Java 和 Groovy 的有用的操作指南。学习 Groovy 的一个真正重要的原因是学习 Grails,这是一个非常高效的全栈 Web 框架,它构建在 Hibernate、Spring Boot 和 Micronaut 等优秀组件之上。
评论已关闭。