2.6 使用Scanner类进行语法分析
![]()
Java 5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序。它是以前的StringTokenizer和Matcher类之间的某种结合。在上一节中,使用Matcher在一个String内搜索来查找匹配某个给定模式的数据,这是很有用的,但是局限在仅匹配单个模式。由于任何数据都必须通过同一模式的捕获组检索或通过使用一个索引来检索文本的各个部分。于是可以结合使用正则表达式和从输入流中检索特定类型数据项的方法。这样,除了能使用正则表达式之外,Scanner类还可以任意地对字符串和基本类型(如int和double)的数据进行分析。借助于Scanner,可以针对任何要处理的文本内容编写自定义的语法分析器。
下面,使用Scanner类来读取一个输入源并有计划地从文本中选取数据项。例如,从美国人口普查局使用的文件格式中读取数据(从本书的合作站点可以获取该数据)。该数据汇总了1990年人口普查中前90%的名和姓(名和姓是分别统计的,因此无法标识个人)的统计分布。人口普查局向公众提供了此数据清单以便系谱专家和统计学家使用。数据包含三个分离的文件:姓氏(dist.all.last)、女性名(dist.female.first)和男性名(dist.male.first)。每个文件包含数行文本,其中用空白符分隔以下的数据:
● 名字
● 用百分比表示的频率
● 用百分比表示的累积频率
● 次序
下面给出姓氏文件中的前两行文本:
SMITH 1.006 1.006 1
JOHNSON 0.810 1.816 2
这表明Smith占人口姓氏的1.006%,Johnson占人口姓氏的0.81%。这是一种简单的文件结构,可以使用Matcher和如下带有捕获组的正则表达式来读取此数据的每一行:
(\S+)\s+(\S+)\s+(\S+)\s+(\S+)
或者可以使用String的split方法:
// for each line of text, assume it's in a variable called line
String[] dataArray = line.split("\\s+");
String name = dataArray[0];
String frequency = dataArray[1];
String cumulativeFrequency = dataArray[2];
String rank = dataArray[3];
如果有必要,还可以将每个数据项的String转换成float和int类型。如果每一行均有相同的结构,则可以使用regex处理每一行,并且可以方便地使用Matcher或String split方法来读取数据(若想了解更多的细节,请参考本章后面的有关章节)。但是将String的split方法用于这个姓氏示例有一个缺点:当处理每一行时需创建一个没必要的String数组。在整个输入文本上使用Matcher将更加高效,但是这样做首先需要将整个数据流缓存到一个String中。使用Scanner类可以同时完成多个操作:从一个有效的输入流中读取数据,高效地分析每一行文本,使用多个正则表达式进行扫描,将检索出来的数据元素直接放入所需的基本类型的变量。以下的代码使用Scanner类读取surname数据文件(由于name文件也使用相同的结构,可以按照相同的方式读取这些文件)。
import java.io.FileReader;
import java.util.Scanner;
public class SurnameReader {
public ArrayList<String> getNames() throws IOException {
ArrayList<String> surnames = new ArrayList<String>();
FileReader fileReader =
new FileReader("/census/dist.all.last");
// create a scanner from the data file
Scanner scanner = new Scanner(fileReader);
// repeat while there is a next item to be scanned
while (scanner.hasNext()) {
// retrieve each data element
String name = scanner.next();
float frequency = scanner.nextFloat();
float cumulativeFrequency = scanner.nextFloat();
int rank = scanner.nextInt();
surnames.add(name);
}
scanner.close(); // also closes the FileReader
return surnames;
}
public SurnameReader() {
for (String s : getNames()) {
System.out.println(s);
}
}
}
Scanner使用空白符作为默认的分隔符,用户可以很容易地更改分隔符的默认设置。默认的分隔符可以方便地为我们服务。上面这个while循环中的hasNext方法用于检查输入串是否有要处理的下一个标记(token)。除了空白符以外,每一行只有四个项。由于依次处理每一项,因此可以假定当next方法检索每个名字时它会移至下一行读取输入。在这里务必要小心,因为如果文件不匹配代码所期望的内容(例如数据丢失或数据类型错误),那么Scanner将抛出一个异常。
在上面的示例中,每次循环会使得一个新名字添加到ArrayList中。这些姓氏数据和人名信息对于使用实际数据构建测试数据库是非常有用的(可以在本书的网站上找到一个获得此数据的链接)。一旦在ArrayList中拥有数据,可以从列表中随机地选择名字。若想了解有关如何从列表中随机选择数据的信息,请参考“产生随机文本”一节。







