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

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 ,然后是int ,LocalDate ,和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等优秀组件之上的、非常高效的全栈网络框架。