# 10.6 `StreamTokenizer`
尽管`StreamTokenizer`并不是从`InputStream`或`OutputStream`派生的,但它只随同`InputStream`工作,所以十分恰当地包括在库的IO部分中。
`StreamTokenizer`类用于将任何`InputStream`分割为一系列“记号”(`Token`)。这些记号实际是一些断续的文本块,中间用我们选择的任何东西分隔。例如,我们的记号可以是单词,中间用空白(空格)以及标点符号分隔。
下面是一个简单的程序,用于计算各个单词在文本文件中重复出现的次数:
```
//: SortedWordCount.java
// Counts words in a file, outputs
// results in sorted form.
import java.io.*;
import java.util.*;
import c08.*; // Contains StrSortVector
class Counter {
private int i = 1;
int read() { return i; }
void increment() { i++; }
}
public class SortedWordCount {
private FileInputStream file;
private StreamTokenizer st;
private Hashtable counts = new Hashtable();
SortedWordCount(String filename)
throws FileNotFoundException {
try {
file = new FileInputStream(filename);
st = new StreamTokenizer(file);
st.ordinaryChar('.');
st.ordinaryChar('-');
} catch(FileNotFoundException e) {
System.out.println(
"Could not open " + filename);
throw e;
}
}
void cleanup() {
try {
file.close();
} catch(IOException e) {
System.out.println(
"file.close() unsuccessful");
}
}
void countWords() {
try {
while(st.nextToken() !=
StreamTokenizer.TT_EOF) {
String s;
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = st.sval; // Already a String
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
if(counts.containsKey(s))
((Counter)counts.get(s)).increment();
else
counts.put(s, new Counter());
}
} catch(IOException e) {
System.out.println(
"st.nextToken() unsuccessful");
}
}
Enumeration values() {
return counts.elements();
}
Enumeration keys() { return counts.keys(); }
Counter getCounter(String s) {
return (Counter)counts.get(s);
}
Enumeration sortedKeys() {
Enumeration e = counts.keys();
StrSortVector sv = new StrSortVector();
while(e.hasMoreElements())
sv.addElement((String)e.nextElement());
// This call forces a sort:
return sv.elements();
}
public static void main(String[] args) {
try {
SortedWordCount wc =
new SortedWordCount(args[0]);
wc.countWords();
Enumeration keys = wc.sortedKeys();
while(keys.hasMoreElements()) {
String key = (String)keys.nextElement();
System.out.println(key + ": "
+ wc.getCounter(key).read());
}
wc.cleanup();
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
最好将结果按排序格式输出,但由于Java 1.0和Java 1.1都没有提供任何排序方法,所以必须由自己动手。这个目标可用一个`StrSortVector`方便地达成(创建于第8章,属于那一章创建的软件包的一部分。记住本书所有子目录的起始目录都必须位于类路径中,否则程序将不能正确地编译)。
为打开文件,使用了一个`FileInputStream`。而且为了将文件转换成单词,从`FileInputStream`中创建了一个`StreamTokenizer`。在`StreamTokenizer`中,存在一个默认的分隔符列表,我们可用一系列方法加入更多的分隔符。在这里,我们用`ordinaryChar()`指出“该字符没有特别重要的意义”,所以解析器不会把它当作自己创建的任何单词的一部分。例如,`st.ordinaryChar('.')`表示小数点不会成为解析出来的单词的一部分。在与Java配套提供的联机文档中,可以找到更多的相关信息。
在`countWords()`中,每次从数据流中取出一个记号,而`ttype`信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字符串或者一个字符。
找到一个记号后,会查询`Hashtable counts`,核实其中是否已经以“键”(`Key`)的形式包含了一个记号。若答案是肯定的,对应的`Counter`(计数器)对象就会自增,指出已找到该单词的另一个实例。若答案为否,则新建一个`Counter`——因为`Counter`构造器会将它的值初始化为1,正是我们计算单词数量时的要求。
`SortedWordCount`并不属于`Hashtable`(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管`keys()`和`values()`方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量`Hashtable`方法在这里都是不适当的。除此以外,对于另一些方法来说(比如`getCounter()`——用于获得一个特定字符串的计数器;又如`sortedKeys()`——用于产生一个枚举),它们最终都改变了`SortedWordCount`接口的形式。
在`main()`内,我们用`SortedWordCount`打开和计算文件中的单词数量——总共只用了两行代码。随后,我们为一个排好序的键(单词)列表提取出一个枚举。并用它获得每个键以及相关的`Count`(计数)。注意必须调用`cleanup()`,否则文件不能正常关闭。
采用了`StreamTokenizer`的第二个例子将在第17章提供。
## 10.6.1 `StringTokenizer`
尽管并不必要IO库的一部分,但`StringTokenizer`提供了与`StreamTokenizer`极相似的功能,所以在这里一并讲述。
`StringTokenizer`的作用是每次返回字符串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字符串`"Where is my cat?"`的记号分别是`"Where"`、`"is"`、`"my"`和`"cat?"`。与`StreamTokenizer`类似,我们可以指示`StringTokenizer`按照我们的愿望分割输入。但对于`StringTokenizer`,却需要向构造器传递另一个参数,即我们想使用的分隔字符串。通常,如果想进行更复杂的操作,应使用`StreamTokenizer`。
可用`nextToken()`向`StringTokenizer`对象请求字符串内的下一个记号。该方法要么返回一个记号,要么返回一个空字符串(表示没有记号剩下)。
作为一个例子,下述程序将执行一个有限的句法分析,查询键短语序列,了解句子暗示的是快乐亦或悲伤的含义。
```
//: AnalyzeSentence.java
// Look for particular sequences
// within sentences.
import java.util.*;
public class AnalyzeSentence {
public static void main(String[] args) {
analyze("I am happy about this");
analyze("I am not happy about this");
analyze("I am not! I am happy");
analyze("I am sad about this");
analyze("I am not sad about this");
analyze("I am not! I am sad");
analyze("Are you happy about this?");
analyze("Are you sad about this?");
analyze("It's you! I am happy");
analyze("It's you! I am sad");
}
static StringTokenizer st;
static void analyze(String s) {
prt("\nnew sentence >> " + s);
boolean sad = false;
st = new StringTokenizer(s);
while (st.hasMoreTokens()) {
String token = next();
// Look until you find one of the
// two starting tokens:
if(!token.equals("I") &&
!token.equals("Are"))
continue; // Top of while loop
if(token.equals("I")) {
String tk2 = next();
if(!tk2.equals("am")) // Must be after I
break; // Out of while loop
else {
String tk3 = next();
if(tk3.equals("sad")) {
sad = true;
break; // Out of while loop
}
if (tk3.equals("not")) {
String tk4 = next();
if(tk4.equals("sad"))
break; // Leave sad false
if(tk4.equals("happy")) {
sad = true;
break;
}
}
}
}
if(token.equals("Are")) {
String tk2 = next();
if(!tk2.equals("you"))
break; // Must be after Are
String tk3 = next();
if(tk3.equals("sad"))
sad = true;
break; // Out of while loop
}
}
if(sad) prt("Sad detected");
}
static String next() {
if(st.hasMoreTokens()) {
String s = st.nextToken();
prt(s);
return s;
}
else
return "";
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
```
对于准备分析的每个字符串,我们进入一个`while`循环,并将记号从那个字符串中取出。请注意第一个if语句,假如记号既不是`"I"`,也不是`"Are"`,就会执行`continue`(返回循环起点,再一次开始)。这意味着除非发现一个`"I"`或者`"Are"`,才会真正得到记号。大家可能想用`==`代替`equals()`方法,但那样做会出现不正常的表现,因为`==`比较的是引用值,而`equals()`比较的是内容。
`analyze()`方法剩余部分的逻辑是搜索`"I am sad"`(我很忧伤、`"I am nothappy"`(我不快乐)或者`"Are you sad?"`(你悲伤吗?)这样的句法格式。若没有`break`语句,这方面的代码甚至可能更加散乱。大家应注意对一个典型的解析器来说,通常都有这些记号的一个表格,并能在读取新记号的时候用一小段代码在表格内移动。
无论如何,只应将`StringTokenizer`看作`StreamTokenizer`一种简单而且特殊的简化形式。然而,如果有一个字符串需要进行记号处理,而且`StringTokenizer`的功能实在有限,那么应该做的全部事情就是用`StringBufferInputStream`将其转换到一个数据流里,再用它创建一个功能更强大的`StreamTokenizer`。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物