实战 Groovy: for each 剖析
使用最熟悉的方法进行迭代迭代是编程的基础: e.g. List、File 和 JDBC ResultSet; Java 迭代策略假设您有一个 Java 编程语言的 java.util.List。清单 1 展示了在 Java 语言中如何使用编程实现迭代: import java.util.*;
public class ListTest{
public static void main(String[] args){
List<String> list = new ArrayList<String>();
list.add("Java");
list.add("Groovy");
list.add("JavaScript");
for(Iterator<String> i = list.iterator(); i.hasNext();){
String language = i.next();
System.out.println("I know " + language);
}
}
}
由于提供了大部分集合类都可以共享的 java.lang.Iterable 接口,可以使用相同的方法遍历 java.util.Set 或 java.util.Queue。 假设该语言存储在 java.util.Map 中。在编译时,尝试对 Map 获取 Iterator 会导致失败 – Map 并没有实现 Iterable 接口。幸运的是,可以调用 map.keySet() 返回一个 Set,然后就可以继续处理。 假设该语言存在于 String 数组中。数组是一种数据结构,而不是类。不能对 String 数组调用 .iterator(),因此必须使用稍微不同的迭代策略。 清单 2. Java 数组迭代 public class ArrayTest{
public static void main(String[] args){
String[] list = {"Java","Groovy","JavaScript"};
for(int i = 0; i < list.length; i++){
String language = list[i];
System.out.println("I know " + language);
}
}
}
但是等一下 — 使用 Java 5 引入的 for-each 语法怎么样?它可以处理任何实现 Iterable 的类和数组, import java.util.*;
public class MixedTest{
public static void main(String[] args){
List<String> list = new ArrayList<String>();
list.add("Java");
list.add("Groovy");
list.add("JavaScript");
for(String language: list){
System.out.println("I know " + language);
}
String[] list2 = {"Java","JavaScript"};
for(String language: list2){
System.out.println("I know " + language);
}
}
}
可以使用相同的方法遍历数组和集合(Map 除外)。但是如果语言存储在 java.io.File,那该怎么办?如果存储在 JDBC ResultSet,或者存储在 XML 文档、java.util.StringTokenizer 中呢? Eric S. Raymond 在他的 The Art of Unix Programming一书中解释了 “最少意外原则”。 Groovy 中的列表迭代将 清单 3 中的 List 重构为 Groovy。只需要直接对列表调用 each() 方法并传递一个闭包,而不是将 List 转换成 for 循环(BTW 这样做并不是特别具有面向对象的特征)。 创建一个名为 listTest.groovy 的文件并添加清单 4 中的代码: def list = ["Java","JavaScript"]
list.each{language->
println language
}
第一行是 Groovy 用于构建 java.util.ArrayList 的便捷语法。可以将 println list.class 添加到此脚本来验证这一点。 清单 5 是经过简化的 清单 4 代码版本 // shorter,using the default it variable
def list = ["Java","JavaScript"]
list.each{ println it }
// shorter still,using an anonymous list
["Java","JavaScript"].each{ println it }
Groovy 允许对数组和 List 交替使用 each() 方法。为了将 ArrayList 改为 String 数组,必须将 as String[] 添加到行末 def list = ["Java","JavaScript"] as String[]
list.each{println it}
在 Groovy 中普遍使用 each() 方法,并且 getter 语法非常便捷(getClass() 和 class 是相同的调用),能够编写既简洁又富有表达性的代码。 def s = "Hello World"
println s
println s.class
s.class.methods.each{println it}
//output:
$ groovy reflectionTest.groovy
Hello World
class java.lang.String
public int java.lang.String.hashCode()
public volatile int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.compareTo(java.lang.String)
public boolean java.lang.String.equals(java.lang.Object)
...
脚本的最后一行调用 getClass() 方法。java.lang.Class 提供了一个 getMethods() 方法,后者返回一个数组。通过将这些操作串连起来并对 Method 的结果数组调用 each(),只使用了一行代码就完成了大量工作。 但是,与 Java for-each 语句不同的是,万能的 each() 方法并不仅限于 List 和数组。 Map 迭代在 Java 语言中,无法直接迭代 Map。在 Groovy 中,这完全不是问题 def map = ["Java":"server","Groovy":"server","JavaScript":"web"]
map.each{ println it }
要处理名称/值对,可以使用隐式的 getKey() 和 getValue() 方法,或在包的开头部分显式地命名变量 def map = ["Java":"server","JavaScript":"web"]
map.each{
println it.key
println it.value
}
map.each{k,v->
println k
println v
}
迭代 Map 和迭代其它任何集合一样自然 在继续研究下一个迭代例子前,应当了解 Groovy 中有关 Map 的另一个语法。与在 Java 语言中调用 map.get(“Java”) 不一样,可以简化对 map.Java 的调用 def map = ["Java":"server","JavaScript":"web"]
//identical results
println map.get("Java")
println map.Java
不可否认,Groovy 针对 Map 的这种便捷语法非常酷,但这也是在对 Map 使用反射时引起一些常见问题的原因。 def list = ["Java","JavaScript"]
println list.class
// java.util.ArrayList
def map = ["Java":"server","JavaScript":"web"]
println map.class
// null
map.class = "I am a map element"
println map.class
// I am a map element
println map.getClass()
// class java.util.LinkedHashMap
这是 Groovy 比较罕见的打破 “最少意外原则” 的情况,但是由于从 map 获取元素要比使用反射更加常见,因此可以接受这一例外。 String 迭代each() 方法可以出现在所有相关的位置; def name = "Jane Smith"
name.each{letter->
println letter
}
这提供了所有的可能性,比如使用下划线替代所有空格 清单 13. 使用下划线替代空格 def name = "Jane Smith"
println "replace spaces"
name.each{
if(it == " "){
print "_"
}else{
print it
}
}
// output
Jane_Smith
在替换一个单个字母时,Groovy 提供了一个更加简洁的替换方法。可以将清单 13 中的所有代码合并为一行代码:”Jane Smith”.replace(” “,“_”)。但是对于更复杂的 String 操作,each() 方法是最佳选择 Range 迭代Groovy 提供了原生的 Range 类型,可以直接迭代。使用两个点分隔的所有内容(比如 1..10)都是一个 Range。 def range = 5..10
range.each{
println it
}
//output: 5 6 7 8 9 10
Range 不局限于简单的 Integer。考虑清单 15 的代码,其中迭代 Date 的 Range: def today = new Date()
def nextWeek = today + 7
(today..nextWeek).each{
println it
}
//output:
Thu Mar 12 04:49:35 MDT 2009
Fri Mar 13 04:49:35 MDT 2009
Sat Mar 14 04:49:35 MDT 2009
Sun Mar 15 04:49:35 MDT 2009
Mon Mar 16 04:49:35 MDT 2009
Tue Mar 17 04:49:35 MDT 2009
Wed Mar 18 04:49:35 MDT 2009
Thu Mar 19 04:49:35 MDT 2009
each() 准确地出现在所期望的位置。Java 语言缺乏原生的 Range 类型,但是提供了一个类似地概念,采取 enum 的形式。毫不奇怪,在这里 each() 仍然派得上用场。 Enumeration 类型Java enum 是按照特定顺序保存的随意的值集合。清单 16 展示了 each() 方法如何自然地配合 enum,就好象它在处理 Range 操作符一样: enum DAY{
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
}
DAY.each{
println it
}
(DAY.MONDAY..DAY.FRIDAY).each{
println it
}
在 Groovy 中,有些情况下,each() 这个名称远未能表达它的强大功能。在下面的例子中,将看到使用特定于所用上下文的方法对 each() 方法进行修饰。Groovy eachRow() 方法是一个很好的例子。 SQL 迭代在处理关系数据库表时,经常会说 “我需要针对表中的每一行执行操作”。比较前面的例子。很可能会说 “我需要对列表中的每一种语言执行一些操作”。根据这个道理,groovy.sql.Sql 对象提供了一个 eachRow() 方法, import groovy.sql.*
def sql = Sql.newInstance(
"jdbc:derby://localhost:1527/MyDbTest;create=true","username","password","org.apache.derby.jdbc.ClientDriver") println("grab a specific field") sql.eachRow("select name from languages"){ row -> println row.name } println("grab all fields") sql.eachRow("select * from languages"){ row -> println("Name: ${row.name}") println("Version: ${row.version}") println("URL: ${row.url}n") }
该脚本的第一行代码实例化了一个新的 Sql 对象:设置 JDBC 连接字符串、用户名、密码和 JDBC 驱动器类。 这显然要比 Java 语言中的等效方法更加清晰。在 Java 中,必须创建单独的 DriverManager、Connection、Statement 和 JDBCResultSet,然后必须在嵌套的 try/catch/finally 块中将它们全部清除。 对于 Sql 对象,会认为 each() 或 eachRow() 都是一个合理的方法名。但是在接下来的示例中,可能会认为 each() 这个名称并不能充分表达它的功能。 文件迭代使用原始的 Java 代码逐行遍历 java.io.File,当完成了所有的嵌套的 BufferedReader 和 FileReader 后(更别提每个流程末尾的所有异常处理),已经忘记最初的目的是什么。 清单 18. Java 文件迭代 import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class WalkFile {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("languages.txt"));
String line = null;
while((line = br.readLine()) != null) {
System.out.println("I know " + line);
}
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
catch(IOException e) {
e.printStackTrace();
}
finally {
if(br != null) {
try {
br.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
清单 19. Groovy 文件迭代 def f = new File("languages.txt")
f.eachLine{language->
println "I know ${language}"
}
这正是 Groovy 的简洁性真正擅长的方面。— “Java 程序员的 DSL”。
注意,在 Groovy 和 Java 语言中同时处理同一个 java.io.File 类。如果该文件不存在,那么 Groovy 代码将抛出和 Java 代码相同的 FileNotFoundException 异常。区别在于,Groovy 没有已检测的异常。 File 类对 each() 方法进行了一些修改。其中之一就是 splitEachLine(String separator,Closure closure)。这意味着不仅可以逐行遍历文件,同时还可以将它分为不同的标记。 // languages.txt // notice the space between the language and the version
Java 1.5
Groovy 1.6
JavaScript 1.x
// splitTest.groovy
def f = new File("languages.txt")
f.splitEachLine(" "){words->
words.each{ println it }
}
// output
Java
1.5
Groovy
1.6
JavaScript
1.x
如果处理的是二进制文件,Groovy 还提供了一个 eachByte() 方法。 当然,Java 语言中的 File 并不总是一个文件 – 有时是一个目录。Groovy 还提供了一些 each() 修改以处理子目录。 目录迭代使用 Groovy 代替 shell 脚本(或批处理脚本)非常容易,能够方便地访问文件系统。要获得当前目录的目录列表: def dir = new File(".")
dir.eachFile{file->
println file
}
eachFile() 方法同时返回了文件和子目录。使用 Java 语言的 isFile() 和 isDirectory() 方法,可以完成更复杂的事情。 清单 22. 分离文件和目录 def dir = new File(".")
dir.eachFile{file->
if(file.isFile()){
println "FILE: ${file}"
}else if(file.isDirectory()){
println "DIR: ${file}"
}else{
println "Uh,I'm not sure what it is..."
}
}
由于两种 Java 方法都返回 boolean 值,可以在代码中添加一个 Java 三元操作符 清单 23. 三元操作符 def dir = new File(".")
dir.eachFile{file->
println file.isDirectory() ? "DIR: ${file}" : "FILE: ${file}"
}
如果只对目录有兴趣,可以使用 eachDir() 而不是 eachFile()。还提供了 eachDirMatch() 和 eachDirRecurse() 方法。 可以看到,对 File 仅使用 each() 方法并不能提供足够的含义。典型 each() 方法的语义保存在 File 中,但是方法名更具有描述性,从而提供更多有关这个高级功能的信息。 URL 迭代理解了如何遍历 File 后,可以使用相同的原则遍历 HTTP 请求的响应。Groovy 为 java.net.URL 提供了一个方便的(和熟悉的)eachLine() 方法。 e.g. 逐行遍历 ibm.com 主页的 HTML: def url = new URL("http://www.ibm.com")
url.eachLine{line->
println line
}
Groovy 提供了一个只包含一行代码的解决办法,这主要归功于 toURL() 方法,它被添加到所有 Strings:
但是,如果希望对 HTTP 响应执行一些更有用的操作,该怎么办呢?具体来讲,如果发出的请求指向一个 RESTful Web 服务,而该服务包含您要解析的 XML,该怎么做呢?each() 方法将在这种情况下提供帮助。 XML 迭代已经了解了如何对文件和 URL 使用 eachLine() 方法。XML 给出了一个稍微有些不同的问题 — 与逐行遍历 XML 文档相比,可能更希望对逐个元素进行遍历。 e.g. 假设语言列表存储在名为 languages.xml 的文件中, <langs>
<language>Java</language>
<language>Groovy</language>
<language>JavaScript</language>
</langs>
Groovy 提供了一个 each() 方法,但是需要做一些修改。如果使用名为 XmlSlurper 的原生 Groovy 类解析 XML,那么可以使用 each() 遍历元素。 def langs = new XmlSlurper().parse("languages.xml")
langs.language.each{
println it
}
//output
Java
Groovy
JavaScript
langs.language.each 语句从名为 如果觉得这还不够的话,那么假设这个 XML 是通过一个 RESTful Web 服务的形式获得,而不是文件系统中的文件。使用一个 URL 替换文件的路径,其余代码仍然保持不变 def langs = new XmlSlurper().parse("http://somewhere.com/languages")
langs.language.each{
println it
}
结束语在使用 each() 方法的整个过程中,最妙的部分在于它只需要很少的工作就可以处理大量 Groovy 内容。了解 each() 方法之后,Groovy 中的迭代就易如反掌了。 本文的最后一个示例简单提到使用 XmlSlurper 实现 XML 解析。在下一期文章中,将继续讨论这个问题,并展示使用 Groovy 进行 XML 解析有多么简单!
参考资料略 —END— —YCR— (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |