企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 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`。