在我的上一篇文章中,我回顾了在 Groovy 中创建和初始化列表与在 Java 中执行相同操作之间的一些差异。我表明,与 Java 中必要的步骤相比,Groovy 具有用于设置列表的直接而紧凑的语法。
本文探讨了 Groovy 和 Java 中列表处理之间的一些更多差异。我将探讨如何在两种语言中为该目的进行行程长度编码列表。简而言之,行程长度编码是一种紧凑地表示列表中相同值的重复序列的方法。
您需要确保您的计算机上安装了 Groovy 和 Java 才能继续学习。
安装 Java 和 Groovy
Groovy 基于 Java,也需要安装 Java。Java 和 Groovy 的最新和不错的版本可能在您的 Linux 发行版的存储库中。或者您可以按照上面链接中提到的说明安装 Groovy。对于 Linux 用户来说,一个不错的选择是 SDKMan,您可以使用它来获取多个版本的 Java、Groovy 和许多其他相关工具。对于本文,我使用的是 SDK 发布的版本:
- Java:OpenJDK 11 的 11.0.12-open 版本
- Groovy:3.0.8 版本
回到问题
行程长度编码 用“行程”(指示元素数量和元素值的对)替换列表中相同元素的序列。例如,如果我有列表
[“a”, ”a”, ”a”, ”b”, ”c”, ”c”]
那么当我对其进行行程长度编码时,我将得到一个列表的列表
[[3, ”a”], [1, ”b”], [2, ”c”]]
这种类型的非破坏性压缩相对容易实现,并且对于具有许多重复值组的列表来说非常有效。这种类型列表的一个例子是每日温度波动有限的地方的每小时温度——例如,加拿大温哥华的冬季。
Java 中的行程长度编码
我将研究 Java 中解决此问题的两种方法。首先,迭代方法
1 import java.lang.*;
2 import java.util.List;
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 public class Test11 {
6 static public void main(String args[]) {
7 // Hourly temperature Monday 17 January 2022 in Vancouver
8 var hourlyTemp = Arrays.asList(5, 5, 5, 5, 5, 4, 4, 5, 5, 5, 6, 6, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5);
9 // Method 1: using forEach()
10 var hourlyTempRLE = new ArrayList<ArrayList<Integer>>();
11 hourlyTempRLE.
12 add(new ArrayList<Integer>(Arrays.asList(1,hourlyTemp.get(0))));
13 hourlyTemp.subList(1,hourlyTemp.size()).forEach(temp -> {
14 var hourlyTempRLETail = hourlyTempRLE.get(hourlyTempRLE.size() - 1);
15 if (hourlyTempRLETail.get(1) == temp) {
16 hourlyTempRLETail.
17 set(0,hourlyTempRLETail.get(0) + 1);
18 } else {
19 hourlyTempRLE.
20 add(new ArrayList<Integer>(Arrays.asList(1,temp)));
21 }
22 });
23 System.out.println("hourlyTempRLE using iterator (method 1): " + hourlyTempRLE);
24 }
25 }
在第 8 行,我将 hourlyTemp
定义为 2022 年 1 月 17 日在温哥华观察到的每小时温度列表。我使用 Java List 接口的 asList()
静态方法,该方法创建一个不可修改的列表(对于此应用程序来说,这是可以的)。
在第 10 行,我将 hourlyTempRLE
定义为将包含行程长度编码温度的“列表的列表”。我在这里使用 ArrayList
的 ArrayList
的 Integer
来获得列表的列表结构。
在第 11 行,我添加编码的初始子列表或“行程”,将其第一个元素设置为 1(计数),将其第二个元素设置为 hourlyTemp
中的第一个温度。
在第 12 行到第 22 行中,我处理 hourlyTemp
的剩余值——表达式 hourlyTemp.subList(1,hourlyTemp.size())
是 hourlyTemp
的子列表,从第二个元素 (1) 开始,到最后一个元素 (hourlyTemp.size() - 1
) 结束。我使用 ArrayList
的 forEach()
方法来迭代该子列表的元素,该方法将每个元素传递到其 Java lambda 参数中,参数为 temp
)。
在第 13 行,我将 hourlyTempRLETail
设置为当前在 hourlyTempRLE
中的最后一个元素。
在第 14 行,我检查传递到 lambda 的 temp 值是否与 hourlyTempRLETail
的值相同。如果是,我在第 15 行到第 17 行中增加其计数。否则,在第 19 行和第 20 行中,我添加一个新的“行程”,其初始值是 1(计数)的子列表和 temp
的当前值。
这非常紧凑。很高兴能够使用 var
而不是声明变量的类型。我看不到更紧凑的方式在同一语句中声明 hourlyTempRLE
并对其进行初始化。
运行此脚本,我得到以下结果
$ javac Test11.java
$ java Test11
hourlyTempRLE using iterator (method 1): [[5, 5], [2, 4], [3, 5], [2, 6], [2, 7], [4, 6], [6, 5]]
$
这是我通过检查 hourlyTemp
列表所期望的结果。
上述代码的一个不太理想的方面是 hourlyTempRLE
是可变的。这意味着后续使用它的代码必须小心不要更改它。此外,任何引用 hourlyTempRLE
的多线程或并行执行代码都应在设计时考虑到这种可变性。
一种简单的解决方法是添加一行代码,使用 Java List 接口的 copyOf()
方法创建一个不可变副本。但这仍然使可变数据结构悬而未决。
我可以使用 Java Streams 以更函数式的方式编写此代码
1 import java.lang.*;
2 import java.util.List;
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 public class Test12 {
6 static public void main(String args[]) {
7 // Hourly temperature Monday 17 January 2022 in Vancouver
8 var hourlyTemp = Arrays.asList(5, 5, 5, 5, 5, 4, 4, 5, 5, 5, 6, 6, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5);
9 // Method 2: using collect()
10 var hourlyTempRLE = hourlyTemp.stream()
11 .collect(RLE::new, RLE::accept, RLE::combine)
12 .getRLE();
13 System.out.println("hourlyTempRLE using collect (method 2): " + hourlyTempRLE);
14 }
15 }
16 class RLE implements java.util.function.IntConsumer {
17 private ArrayList<ArrayList<Integer>> rle = new ArrayList<ArrayList<Integer>>();
18 public void accept(int temp) {
19 if (rle.size() > 0) {
20 var rleTail = rle.get(rle.size() - 1);
21 if (rleTail.get(1) == temp)
22 rleTail.set(0,rleTail.get(0) + 1);
23 else
24 rle.add(new ArrayList<Integer>(Arrays.asList(1,temp)));
25 } else {
26 rle.add(new ArrayList<Integer>(Arrays.asList(1,temp)));
27 }
28 }
29 public void combine(RLE other) {
30 rle.addAll(other.getRLE());
31 }
32 public ArrayList<ArrayList<Integer>> getRLE() {
33 return rle;
34 }
35 }
有什么不同?似乎有很多。
在第 10 行到第 12 行中,我有一个不错的紧凑型函数式解决方案,用于定义和累积 hourlyTemp
的元素。在第 10 行中,我使用 stream()
函数将 hourlyTemp
转换为 Java Stream。在第 11 行中,我将 Streams collect()
函数与我在下面定义的类 (RLE
) 结合使用,以累积和减少值。在第 12 行中,我在 RLE
类上调用一个方法 getRLE()
,以获取列表的列表。因此,这既好又紧凑。代价是我必须在第 16 行到第 35 行中定义这个辅助类 RLE
。
我在辅助类中看到的第一件事是列表的列表数据结构 rle
的定义,在第 17 行。这在创建时被初始化为空的 ArrayList<ArrayList<Integer>>
。
然后,在第 18 行到第 31 行中,您可以看到 Streams collect()
方法所需的两个方法的定义——accept()
,用于累积传入值(在本例中为整数)和 combine()
,用于将单独的 RLE
实例合并到此实例中,这发生在并行累积时。第三个标准方法 new()
通过 RLE
构造函数隐式提供。
累积工作在第 18 行到第 28 行的 accept()
方法中完成。此方法具有与前一个方法类似的逻辑,不同之处在于我从一个空列表开始,我必须检测并初始化该列表。将另一个 RLE
实例与此实例组合在第 29 行到第 31 行中使用 ArrayList
类提供的 addAll()
方法处理。
最后,在第 32 行到第 34 行中,我定义了方法 getRLE()
,它返回列表的列表。
运行此代码会产生
$ javac Test12.java
$ java Test12
hourlyTempRLE using collect (method 2): [[5, 5], [2, 4], [3, 5], [2, 6], [2, 7], [4, 6], [6, 5]]
$
这正如我所期望的那样。
Groovy 中的行程长度编码
我将在 Groovy 中研究解决此问题的相同两种方法。首先,迭代方法
1 // Hourly temperature Monday 17 January 2022 in Vancouver
2 def hourlyTemp = [5, 5, 5, 5, 5, 4, 4, 5, 5, 5, 6, 6, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5]
3 // Method 1: using each {}
4 def hourlyTempRLE = [[1,hourlyTemp[0]]]
5 hourlyTemp[1..-1].each { temp ->
6 if (hourlyTempRLE[-1][1] == temp) {
7 hourlyTempRLE[-1][0]++
8 } else {
9 hourlyTempRLE << [1,temp]
10 }
11 }
12 println "hourlyTempRLE using iterator (method 1): $hourlyTempRLE"
在第 2 行,我将 hourlyTemp
定义为 2022 年 1 月 17 日在温哥华观察到的每小时温度列表。
在第 4 行,我将行程长度编码版本 hourlyTempRLE
初始化为列表的列表,初始值设置为 hourlyTemp
中的第一个温度,计数为 1。
在第 5 行到第 11 行中,我处理 hourlyTemp
的其余值。表达式 hourlyTemp[1..-1]
是 hourlyTemp
的切片,从第二个元素 (1) 开始,到最后一个元素 (-1) 结束。为了处理该切片,我应用 Groovy List 方法 each()
,它接受一个 Groovy Closure,这里看起来很像 Java lambda,其参数是 temp
,并迭代列表中的所有值,为每个值调用 Closure。
在第 6 行和第 7 行中,如果最后一个“行程”中的温度值(即 hourlyTempRLE
中最后一个子列表的第二个元素,表示为 hourlyTempRLE[-1][1]
)与 temp
的值相同,则计数会递增。
但是,如果温度不同,则在第 8 行到第 10 行中,会将一个新的“行程”附加到列表中,计数为 1,值设置为 temp
的值。
运行此脚本,我观察到
$ groovy test11.groovy
hourlyTempRLE using iterator (method 1): [[5, 5], [2, 4], [3, 5], [2, 6], [2, 7], [4, 6], [6, 5]]
$
一个不错的替代方案是使用函数式方法
1 // Hourly temperature Monday 17 January 2022 in Vancouver
2 def hourlyTemp = [5, 5, 5, 5, 5, 4, 4, 5, 5, 5, 6, 6, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5]
3 // Method 2: using inject {}
4 def hourlyTempRLE = hourlyTemp[1..-1].inject([[1,hourlyTemp[0]]]) { rle, temp ->
5 if (rle[-1][1] == temp) {
6 rle[-1][0]++
7 } else {
8 rle << [1,temp]
9 }
10 rle
11 }
12 println "hourlyTempRLE using inject (method 2): $hourlyTempRLE"
在这里,我定义了与之前相同的每小时温度列表 hourlyTemp
。这种函数式方法使用列表归约方法将 hourlyTemp
转换为其行程长度编码等效项。这使列表创建数据保留在迭代 hourlyTemp
的代码内部,而不是在列表创建代码外部定义变量,从而允许我在需要时将 hourlyTempRLE
声明为不可变的。
在 Groovy 中,列表归约由 Groovy List inject()
方法实现,该方法接受两个参数:初始值和一个包含归约代码的 Closure。
在上面的第 4 行到第 11 行中,您看到我将 hourlyTempRLE
定义为执行 inject()
的结果,其中
[[1,hourlyTemp[0]]]
的初始值——也就是说,hourlyTempRLE
的初始值是一个列表,其中包含一个子列表,其元素为 1(计数)和来自hourlyTemp
的第一个温度(值)- 在第 4 行到第 11 行定义的 Closure
{ rle, temp -> ... }
,它具有参数rle
(到目前为止inject()
操作的部分结果)和temp
,inject()
将其设置为切片hourlyTemp[1..-1]
的连续值
在第 5 行和第 6 行中,如果最后一个“行程”的值与 temp
的值相同(同样,到目前为止累积的部分结果的第二个元素,表示为 rle[-1][1]
),则计数会递增。
否则,在第 7 行到第 9 行中,会将一个新的“行程”附加到到目前为止累积的部分结果中,其中包含计数 1 和 temp
的值。
在第 10 行中,到目前为止累积的(修改后的)部分结果作为 Closure 的值返回,inject()
使用该值替换先前的部分结果。通常,在 Groovy 方法或 Closure 定义中,最后一个语句的值作为方法或 Closure 定义的值返回——仅在从方法或 Closure 定义的中间返回一个值时才需要使用 Groovy return
语句。
正如预期的那样,运行此脚本会产生
$ groovy test12.groovy
hourlyTempRLE using inject (method 2): [[5, 5], [2, 4], [3, 5], [2, 6], [2, 7], [4, 6], [6, 5]]
$
为了更熟悉 inject()
,您可以通过将以下行附加到两个脚本中的每一个来计算 hourlyTemp
和 hourlyTempRLE
的总和
def hourlyTempSum = hourlyTemp.inject(0) { sum, temp -> sum + temp}
def hourlyTempRLESum = hourlyTempRLE.inject(0) { sum, run ->
sum + (run[0] * run[1])
}
Groovy List 定义了一个 sum()
方法,这将是实现上面第一行结果的更简洁方法。不过,由于这是可能的归约操作的最简单示例,因此展示它是有启发性的。在第二行中,您可以看到我可以轻松地将温度值乘以重复出现的次数以产生相同的总和。请注意,在两种情况下,传递给 inject()
的初始值均为零。
我可以使用 Groovy assert
语句来检查总和是否相等,如下所示
assert hourlyTempSum == hourlyTempRLESum
最后,我可以通过将以下行附加到两个脚本中的每一个的末尾来解压缩行程长度编码列表
def hourlyTempRLEU = hourlyTempRLE.inject([]) { uncompressed, run ->
uncompressed << [run[1]] * run[0]
}.flatten()
在这里,我利用了 Groovy 的运算符重载和 Groovy List multiply()
方法。“乘以”一个列表(在左侧)乘以一个整数(在右侧)会复制该列表。在这种情况下, inject()
生成一个子列表列表,我想将其展平为一个列表,因此我在 inject()
的结果上使用 Groovy List flatten()
方法。
我可以使用 Groovy assert
语句来检查解压缩后的列表是否与原始列表相同
assert hourlyTemp == hourlyTempRLEU
注意: Groovy 相等性与 Java 相等性不同!在 Groovy 中,==
是任何 object 实例的 .equals()
方法的简写,而 ===
是任何 object 实例的 .is()
方法的简写,并且等效于 Java 的 ==
。
Java 和 Groovy 解决方案的简要比较
比较 Java
var hourlyTempRLE = new ArrayList<ArrayList<Integer>>();
hourlyTempRLE.
add(new ArrayList<Integer>(Arrays.asList(1,hourlyTemp.get(0))));
与 Groovy
def hourlyTempRLE = [[1,hourlyTemp[0]]]
在这里,您可以看到 Groovy 可以多么紧凑。这不仅仅是为了紧凑而紧凑。Groovy 代码通过其紧凑性显然更具可读性——您可以看到 hourlyTempRLE
是列表的列表,因为使用了嵌套括号。这种紧凑性在其他地方也很明显。
比较 Java
hourlyTempRLE.
get(le).
set(0,hourlyTempRLE.get(le).get(0) + 1);
与 Groovy
hourlyTempRLE[-1][0]++
在这里,您可以看到 Groovy 语言对 List 结构的语法支持使该语句更具可读性和可理解性。
可读性对于代码维护非常重要,因为可读性促进了理解。使用您喜欢的搜索引擎快速搜索“阅读代码与编写代码”之类的短语,将找到许多关于可读性的有力论据,例如 Opensource.com 上的这篇文章。Groovy 中对 Collection 结构的语法支持极大地提高了 Groovy 代码的可读性。
从上面的内容得出的另一个结论是,Groovy 确实增强了您可以使用 List 结构执行的操作。虽然当前版本的 Groovy 也支持 Java Streams,但添加到 Groovy Collection 结构的附加功能提供了许多相同的功能。在此示例中,使用 List 的扩展允许您避免整个定义 RLE
辅助类的事情,Java Streams 方法以某种形式或另一种形式需要它。您只需 12 行 Groovy 代码,而不是 34 行 Java 代码。也许我对 Streams 的理解遗漏了某些内容,但我不认为这会变得更小。
现在是建议查看 Groovy 集合的文档 以了解更多信息的好时机。
Groovy 资源
Apache Groovy 网站 有很多很棒的文档。另一个很棒的 Groovy 资源是 Mr. Haki。学习 Groovy 的一个非常好的理由是继续学习 Grails,这是一个非常高效的全栈 Web 框架,构建在 Hibernate、Spring Boot 和 Micronaut 等优秀组件之上。
1 条评论