ushbackInputStream和StreamTokenizer的源码分析和使用方法详细分析
PushbackInputStream适合语法解析过程中的语法回退因为这个类提供了有限字节内部定义了一个默认长度为1的byte[] buf字节数组的缓冲式回退能力具体过程如下①、当调用unread()函数时会将任意字节可以是从被装饰的输入流中读取的字节也可以是自己定义的字节压入byte[] buf字节数组的头部②、当后续调用read()函数时优先读取这个byte[] buf字节数组中被压入的字节当使用PushbackInputStream进行语法解析时需要注意 unread()函数的调用顺序、EOF 处理及嵌套回退风险等。因为语法解析器常需要先预读一个字符判断类型如果发现不是目标类型再退回去而 InputStream.class 本身不支持回退所以PushbackInputStream.class就是为此设计的。很多语法解析需要预读多个字符才能确定 token 类型比如识别 和 、或 /* 注释起始符。PushbackInputStream默认构造函数只分配 1 字节缓冲内部定义了一个默认长度为1的byte[] buf字节数组根本不够用因此需要使用new PushbackInputStream(in, 4)构造一个长度为4的byte[] buf字节数组作为缓冲区用来覆盖大多数双字符操作符和简单分隔符场景如果要支持 Unicode 转义如 \u0061或长标识符前缀判断缓冲区需更大但别盲目设成 1024同时缓冲区byte[] buf字节数组的长度在构造后不可变运行时并无法扩容。比如下面是一个识别数字字面量含小数点时的安全回退示例伪代码如下所示...省略部分代码... int ch in.read(); if (ch .) { int next in.read(); if (Character.isDigit(next)) { // 确认是小数继续解析 parseFractionPart(); } else { // 不是小数退回两个字符. 和 next in.unread(next); in.unread(.); } } else { // 其他情况按原逻辑处理 } ...省略部分代码...1.1、PushbackInputStream的源码分析PushbackInputStream.class 的UML关系图如下所示PushbackInputStream.class的源码如下所示package java.io; public class PushbackInputStream extends FilterInputStream { //有限长度的用于回退的字节数组缓冲区默认长度为1 protected byte[] buf; //可读指针byte[] buf有限长度的用于回退的字节数组缓冲区中该指针包括该指针索引之后的所有字节都可以读 protected int pos; //检查被装饰的输入流是否关闭 private void ensureOpen() throws IOException { if (in null) throw new IOException(Stream closed); } //构造函数in为被装饰的输入流size为byte[] buf有限长度的用于回退的字节数组缓冲区的长度 public PushbackInputStream(InputStream in, int size) { super(in); if (size 0) { throw new IllegalArgumentException(size 0); } this.buf new byte[size]; this.pos size;//将可读指针指向byte[] buf有限长度的用于回退的字节数组缓冲区中最后一个索引size-1之后 } //构造函数in为被装饰的输入流 public PushbackInputStream(InputStream in) { this(in, 1);//构造一个默认长度为1的byte[] buf用于回退的字节数组缓冲区 } //如果byte[] buf用于回退的字节数组缓冲区中有可读的字节的话就从该缓冲区中读取1个字节 //如果byte[] buf用于回退的字节数组缓冲区中没有可读的字节的话就从被装饰的输入流中读取1个字节 //如果byte[] buf用于回退的字节数组缓冲区和被装饰的输入流中都没有可读的字节的话返回-1 public int read() throws IOException { ensureOpen(); if (pos buf.length) { return buf[pos] 0xff; } return super.read(); } //尽可能的从byte[] buf用于回退的字节数组缓冲区和被装饰的输入流中读取len个字节到byte[] b的[off,offlen)索引位置总共分为以下5种场景 //①、如果byte[] buf用于回退的字节数组缓冲区中有len个字节的话就从该缓冲区中读取len个字节到字节数组byte[] b的[off,offlen)索引位置 //②、如果byte[] buf用于回退的字节数组缓冲区中没有任何字节并且被装饰的输入流中有len个字节那就从被装饰的输入流中读取len个字节到字节数组byte[] b的[off,offlen)索引位置 //③、如果byte[] buf用于回退的字节数组缓冲区中没有任何字节并且被装饰的输入流中只有availavaillen个字节那就从被装饰的输入流中读取avail个字节到字节数组byte[] b的[off,offavail)索引位置 //④、如果byte[] buf用于回退的字节数组缓冲区中有availavaillen个字节的话就读取avail个字节剩余len-avail个字节从被装饰的输入流中读取如果被装饰的输入流中没有len-avail个字节的话那就从被装饰的输入流中有多少读取多少直到将被装饰的输入流读取完毕然后将以上2个地方被装饰的输入流用于回退的字节数组缓冲区读取的所有字节假如有x个放入到字节数组byte[] b的[off,offx)索引位置 //⑤、如果byte[] buf用于回退的字节数组缓冲区和被装饰的输入流中都没有任何字节的话返回-1 public int read(byte[] b, int off, int len) throws IOException { //检查被装饰的输入流是否关闭 ensureOpen(); if (b null) { throw new NullPointerException(); } else if (off 0 || len 0 || len b.length - off) {//相当于off len b.length源码中这样写代码的好处我没看出来 throw new IndexOutOfBoundsException(); } else if (len 0) { return 0;//要从PushbackInputStream 对象中读取的len个字节0时返回0 } int avail buf.length - pos;//用于回退的字节数组缓冲区中实际装载了buf.length - pos个字节 if (avail 0) { if (len avail) { avail len; } System.arraycopy(buf, pos, b, off, avail); pos avail; off avail; len - avail; } if (len 0) { len super.read(b, off, len); if (len -1) { return avail 0 ? -1 : avail; } return avail len;//场景④中的x就是这里的avail len } return avail; } //一次只可以回推1个字节数据到byte[] buf用于回退的字节数组缓冲区中 public void unread(int b) throws IOException { ensureOpen(); if (pos 0) {//pos0时表示byte[] buf用于回退的字节数组缓冲区中已经没有足够的容量再放置数据所以抛出一个IOException异常。 throw new IOException(Push back buffer is full); } buf[--pos] (byte)b; } //一次回推byte[] b字节数组中[off,offlen)索引位置的len个字节数据到byte[] buf用于回退的字节数组缓冲区中 public void unread(byte[] b, int off, int len) throws IOException { ensureOpen(); if (len pos) {//如果byte[] buf用于回退的字节数组缓冲区中没有足够的位置放置len个字节则抛出一个IOException throw new IOException(Push back buffer is full); } pos - len;//如果byte[] buf用于回退的字节数组缓冲区中有足够的位置放置len个字节则使用System.arraycopy()函数进行回退 System.arraycopy(b, off, buf, pos, len); } public void unread(byte[] b) throws IOException { unread(b, 0, b.length); } //返回byte[] buf用于回退的字节数组缓冲区被装饰的输入流中可以被使用的字节总数量 public int available() throws IOException { ensureOpen(); int n buf.length - pos;//先计算用于byte[] buf用于回退的字节数组缓冲区中可以被使用的字节总数量 int avail super.available();//再计算被装饰的输入流中可以被使用的字节总数量 return n (Integer.MAX_VALUE - avail) ? Integer.MAX_VALUE : n avail;//byte[] buf用于回退的字节数组缓冲区中可以被使用的字节总数量被装饰的输入流中可以被使用的字节总数量 } //从byte[] buf用于回退的字节数组缓冲区被装饰的输入流中跳过n个字节如果byte[] buf用于回退的字节数组缓冲区被装饰的输入流中的字节数量n则返回实际跳过的字节数量 public long skip(long n) throws IOException { ensureOpen(); if (n 0) { return 0; } long pskip buf.length - pos;//从byte[] buf用于回退的字节数组缓冲区中跳过的字节 if (pskip 0) { if (n pskip) { pskip n; } pos pskip; n - pskip; } if (n 0) { pskip super.skip(n);//从被装饰的输入流中跳过的字节累加到从byte[] buf用于回退的字节数组缓冲区中跳过的字节 } return pskip; } public boolean markSupported() { return false; } public synchronized void mark(int readlimit) { } public synchronized void reset() throws IOException { throw new IOException(mark/reset not supported); } //关闭被装饰的输入流和用于回退的字节数组缓冲区 public synchronized void close() throws IOException { if (in null) return; in.close(); in null; buf null; } }1.2、PushbackInputStream的read()函数和unread()函数package java.io; public class PushbackInputStream extends FilterInputStream { //有限长度的用于回退的字节数组缓冲区默认长度为1 protected byte[] buf; //可读指针byte[] buf有限长度的用于回退的字节数组缓冲区中该指针包括该指针索引之后的所有字节都可以读 protected int pos; ...省略部分代码... //尽可能的从byte[] buf用于回退的字节数组缓冲区和被装饰的输入流中读取len个字节到byte[] b的[off,offlen)索引位置总共分为以下5种场景 //①、如果byte[] buf用于回退的字节数组缓冲区中有len个字节的话就从该缓冲区中读取len个字节到字节数组byte[] b的[off,offlen)索引位置 //②、如果byte[] buf用于回退的字节数组缓冲区中没有任何字节并且被装饰的输入流中有len个字节那就从被装饰的输入流中读取len个字节到字节数组byte[] b的[off,offlen)索引位置 //③、如果byte[] buf用于回退的字节数组缓冲区中没有任何字节并且被装饰的输入流中只有availavaillen个字节那就从被装饰的输入流中读取avail个字节到字节数组byte[] b的[off,offavail)索引位置 //④、如果byte[] buf用于回退的字节数组缓冲区中有availavaillen个字节的话就读取avail个字节剩余len-avail个字节从被装饰的输入流中读取如果被装饰的输入流中没有len-avail个字节的话那就从被装饰的输入流中有多少读取多少直到将被装饰的输入流读取完毕然后将以上2个地方被装饰的输入流用于回退的字节数组缓冲区读取的所有字节假如有x个放入到字节数组byte[] b的[off,offx)索引位置 //⑤、如果byte[] buf用于回退的字节数组缓冲区和被装饰的输入流中都没有任何字节的话返回-1 public int read(byte[] b, int off, int len) throws IOException { //检查被装饰的输入流是否关闭 ensureOpen(); if (b null) { throw new NullPointerException(); } else if (off 0 || len 0 || len b.length - off) {//相当于off len b.length源码中这样写代码的好处我没看出来 throw new IndexOutOfBoundsException(); } else if (len 0) { return 0;//要从PushbackInputStream 对象中读取的len个字节0时返回0 } int avail buf.length - pos;//用于回退的字节数组缓冲区中实际装载了buf.length - pos个字节 if (avail 0) { if (len avail) { avail len; } System.arraycopy(buf, pos, b, off, avail); pos avail; off avail; len - avail; } if (len 0) { len super.read(b, off, len); if (len -1) { return avail 0 ? -1 : avail; } return avail len;//场景④中的x就是这里的avail len } return avail; } //一次回推byte[] b字节数组中[off,offlen)索引位置的len个字节数据到byte[] buf用于回退的字节数组缓冲区中 public void unread(byte[] b, int off, int len) throws IOException { ensureOpen(); if (len pos) {//如果byte[] buf用于回退的字节数组缓冲区中没有足够的位置放置len个字节则抛出一个IOException throw new IOException(Push back buffer is full); } pos - len;//如果byte[] buf用于回退的字节数组缓冲区中有足够的位置放置len个字节则使用System.arraycopy()函数进行回退 System.arraycopy(b, off, buf, pos, len); } ...省略部分代码... }如果使用者使用的被装饰的输入流是 ByteArrayInputStream然后执行PushbackInputStream的read()函数和unread()函数时如下代码package com.chelong.bio; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; public class PushbackInputStreamTest { public static void main(String[] args) throws IOException { String str Hello,World; InputStream inputStream new ByteArrayInputStream(str.getBytes(UTF-8)); //构建回退流 PushbackInputStream pushbackInputStream new PushbackInputStream(inputStream, 8); int len -1; System.out.println(输出内容:); while ((len pushbackInputStream.read()) ! -1) { //转为char类型 char c (char) len; if (c ,) { //为 ,号时 先往前读3个再往后倒两个 byte[] b1 new byte[3]; pushbackInputStream.read(b1); //往后倒两个 pushbackInputStream.unread(b1, 0, 2); } else { System.out.print(c); } } } }上面代码的执行结果如下上面代码的整个执行过程分为以下5步①、通过构造函数构建一个长度为8的byte[] buf用于回退的字节数组缓冲区和ByteArrayInputStream.class类型的被装饰的输入流如下所示PushbackInputStream pushbackInputStream new PushbackInputStream(inputStream, 8);②、按照顺序从ByteArrayInputStream.class类型的输入流中读取字节直到读取到,时如下所示while ((len pushbackInputStream.read()) ! -1) { //转为char类型 char c (char) len; if (c ,) { } else { System.out.print(c); } }输出如下Hello③、当读取到,之后从被装饰的输入流ByteArrayInputStream.class中往byte[] b1字节数组中读取3个字节如下所示//为 ,号时 先往前读3个再往后倒两个 byte[] b1 new byte[3]; pushbackInputStream.read(b1);④、将步骤③中读入到byte[] b1字节数组中的[0,2)索引位置的数据读取到PushbackInputStream中的byte[] buf用于回退的字节数组缓冲区的[6,8)索引位置中如下所示//往后倒两个 pushbackInputStream.unread(b1, 0, 2);⑤、再次重复执行步骤②中按照顺序从ByteArrayInputStream.class类型的输入流中读取字节时先读取PushbackInputStream中的byte[] buf用于回退的字节数组缓冲区的[6,8)索引位置再从ByteArrayInputStream.class类型的输入流中读取剩余字节如下所示while ((len pushbackInputStream.read()) ! -1) { //转为char类型 char c (char) len; if (c ,) { } else { System.out.print(c); } }Wold二、StreamTokenizer源码尽管StreamTokenizer并不是继承了InputStream.class或OutputStream.class但它的构造函数只能传入InputStream.class或者Reader.class类型的变量所以十分恰当地包括在库的IO部分中。StreamTokenizer类用于将任何InputStream分割为一系列的“Token”记号。这些“Token”记号实际是一些断续的文本块中间可以用使用者选择的任何东西分隔。StreamTokenizer.class 的UML关系图如下所示StreamTokenizer.class的源码如下所示package java.io; import java.util.Arrays; public class StreamTokenizer { //内部声明了一个Reader对象句柄和一个InputStream对象句柄用于接收读取流。 private Reader reader null; private InputStream input null; //声明了一个char类型的数组初始容量为20用于存储读取时标记的内容读取时可以根据实际需要自动扩容。 private char buf[] new char[20]; //声明了一个int型变量peekc当调用nextToken方法的时候peekc作为一个状态用于判断是否需要继续读取下一个字符放入到标记中初始化时赋值为NEED_CHAR。 private int peekc NEED_CHAR; //定义了两个常量NEED_CHAR和SKIP_LF都表示要读取下一个字符但后者如果遇到一个\n则会将它丢弃然后读取下一个字符。 private static final int NEED_CHAR Integer.MAX_VALUE; private static final int SKIP_LF Integer.MAX_VALUE - 1; //声明了一个boolean型变量pushedBack该变量用于控制执行nextToken方法时是否需要进行回退。 private boolean pushedBack; //声明了一个boolean型变量forceLower该变量用于控制sval是否需要进行小写处理。 private boolean forceLower; //声明了一个int型变量用于记录最后一次读取标记时的行数。 private int LINENO 1; private boolean eolIsSignificantP false; private boolean slashSlashCommentsP false; private boolean slashStarCommentsP false; //声明了一个数组作为一个语法表存放几种类型依次为空格数字字母引号注解等类型。 private byte ctype[] new byte[256]; private static final byte CT_WHITESPACE 1; private static final byte CT_DIGIT 2; private static final byte CT_ALPHA 4; private static final byte CT_QUOTE 8; private static final byte CT_COMMENT 16; //声明了一个int型变量表明当前标记的标记类型初始化时为TT_NOTHING类型。 public int ttype TT_NOTHING; //定义了一个常量表示此时已经读取到了流的末尾。 public static final int TT_EOF -1; //定义了一个常量表示此时已经读到了一行的末尾。 public static final int TT_EOL \n; //定义了一个常量表示此时读到的标记是一个数字标记。 public static final int TT_NUMBER -2; //定义了一个常量表示此时读到的标记是一个文本标记。 public static final int TT_WORD -3; //定义了一个常量表示此时并没有进行标记的读取用于初始化ttype。 private static final int TT_NOTHING -4; //声明了一个字符串型变量sval如果当前的标记为字符串那么此时将当前标记的值赋值给sval。 public String sval; //声明了一个double型变量nval如果当前的标记为数值那么此时将当前标记的值赋值给nval。 public double nval; /** * 一个私有的构造函数用于初始化内置的语法表即ctype数组。 */ private StreamTokenizer() { wordChars(a, z); wordChars(A, Z); wordChars(128 32, 255); whitespaceChars(0, ); commentChar(/); quoteChar(); quoteChar(\); parseNumbers(); } /** * 已废弃 * 一个带一个参数的构造函数传入的参数为一个InputStream对象先对其进行安全检测如果不为null则赋值给最初声明的InputStream对象句柄input。值得注 * 意的是该方法如今已经被弃用了。 */ Deprecated public StreamTokenizer(InputStream is) { this(); if (is null) { throw new NullPointerException(); } input is; } /** *一个带一个参数的构造函数传入的参数为一个Reader对象先对其进行安全检测如果不为null则赋值给最初声明的Reader对象句柄reader。 */ public StreamTokenizer(Reader r) { this(); if (r null) { throw new NullPointerException(); } reader r; } /** * 该方法用于重置标记的语法表通过一个循环将语法表中的每一个元素都置为0即当做普通字符进行处理。 */ public void resetSyntax() { for (int i ctype.length; --i 0;) ctype[i] 0; } /** * 用于初始化语法表传入的两个参数为语法表的前后区间将传入区间内的数据 */ public void wordChars(int low, int hi) { if (low 0) low 0; if (hi ctype.length) hi ctype.length - 1; while (low hi) ctype[low] | CT_ALPHA; } /** * 用于初始化语法表传入的两个参数为语法表的前后区间将传入区间内的数据都做为空白空格处理。 */ public void whitespaceChars(int low, int hi) { if (low 0) low 0; if (hi ctype.length) hi ctype.length - 1; while (low hi) ctype[low] CT_WHITESPACE; } /** * 用于初始化语法表传入的两个参数为语法表的前后区间将传入区间内的数据都做为普通字符处理。 */ public void ordinaryChars(int low, int hi) { if (low 0) low 0; if (hi ctype.length) hi ctype.length - 1; while (low hi) ctype[low] 0; } /** * 用于初始化语法表通过传入的参数作为语法表的索引将对应的类型改为0这样便会当做普通字符处理。 */ public void ordinaryChar(int ch) { if (ch 0 ch ctype.length) ctype[ch] 0; } /** * 用于初始化语法表以传入的int型值为索引将其对应的数组划分到CT_COMMENT注解类型。 */ public void commentChar(int ch) { if (ch 0 ch ctype.length) ctype[ch] CT_COMMENT; } /** * 用于初始化语法表以传入的int型值为索引将其对应的数组划分到CT_QUOTE引用类型。 */ public void quoteChar(int ch) { if (ch 0 ch ctype.length) ctype[ch] CT_QUOTE; } /** * 用于初始化语法表将数字0-9.-划分到CT_DIGIT数字类型。 */ public void parseNumbers() { for (int i 0; i 9; i) ctype[i] | CT_DIGIT; ctype[.] | CT_DIGIT; ctype[-] | CT_DIGIT; } /** * 该方法用于设置eolIsSignificant变量的值该值用来恒定是否将行的结尾当做一个标记来处理。 */ public void eolIsSignificant(boolean flag) { eolIsSignificantP flag; } /** * 该方法用于设置slashStarCommnetsP的值该值用于恒定是否将c语言形式的注释当做特殊字符处理如果为true则所有包含在注释内的内容会被丢弃。为false则 * 当做普通字符处理。 */ public void slashStarComments(boolean flag) { slashStarCommentsP flag; } /** * 该方法与上一个方法类似不过是用来恒定是否认可c形式的注释。 */ public void slashSlashComments(boolean flag) { slashSlashCommentsP flag; } /** * 该方法用于修改forceLower变量的值。 */ public void lowerCaseMode(boolean fl) { forceLower fl; } /** * 定义了一个read函数实际上是通过调用内置的reader/input 的read函数从中看出优先是使用reader来进去读取的。 */ private int read() throws IOException { if (reader ! null) return reader.read(); else if (input ! null) return input.read(); else throw new IllegalStateException(); } /** * 该函数用于获取下一个标记。 */ public int nextToken() throws IOException { //判断是否需要进行回退如果pushedBack值为true则直接返回上一个标记的类型同时将pushedBack的值重置为false。 if (pushedBack) { pushedBack false; return ttype; } byte ct[] ctype; sval null; int c peekc; if (c 0) c NEED_CHAR; if (c SKIP_LF) { c read(); if (c 0) return ttype TT_EOF; if (c \n) c NEED_CHAR; } if (c NEED_CHAR) { c read(); if (c 0) return ttype TT_EOF; } ttype c; /* Just to be safe */ peekc NEED_CHAR;//将peekc重置方便下一次进入方法时使用 //如果当前类型是空格进行的操作 int ctype c 256 ? ct[c] : CT_ALPHA; while ((ctype CT_WHITESPACE) ! 0) { if (c \r) { LINENO; if (eolIsSignificantP) { peekc SKIP_LF; return ttype TT_EOL; } c read(); if (c \n) c read(); } else { if (c \n) { LINENO; if (eolIsSignificantP) { return ttype TT_EOL; } } c read(); } if (c 0) return ttype TT_EOF; ctype c 256 ? ct[c] : CT_ALPHA; } //如果当前类型为数字的操作 if ((ctype CT_DIGIT) ! 0) { boolean neg false; if (c -) { c read(); if (c ! . (c 0 || c 9)) { peekc c; return ttype -; } neg true; } double v 0; int decexp 0; int seendot 0; while (true) { if (c . seendot 0) seendot 1; else if (0 c c 9) { v v * 10 (c - 0); decexp seendot; } else break; c read(); } peekc c; if (decexp ! 0) { double denom 10; decexp--; while (decexp 0) { denom * 10; decexp--; } /* Do one division of a likely-to-be-more-accurate number */ v v / denom; } nval neg ? -v : v; return ttype TT_NUMBER; } //如果当前类型为字母符号的操作 if ((ctype CT_ALPHA) ! 0) { int i 0; do { if (i buf.length) { buf Arrays.copyOf(buf, buf.length * 2);//自动扩容 } buf[i] (char) c; c read(); ctype c 0 ? CT_WHITESPACE : c 256 ? ct[c] : CT_ALPHA; } while ((ctype (CT_ALPHA | CT_DIGIT)) ! 0); peekc c; sval String.copyValueOf(buf, 0, i); if (forceLower) sval sval.toLowerCase(); return ttype TT_WORD; } //如果当前类型为引用符号的操作 if ((ctype CT_QUOTE) ! 0) { ttype c; int i 0; /* Invariants (because \Octal needs a lookahead): * (i) c contains char value * (ii) d contains the lookahead */ int d read(); while (d 0 d ! ttype d ! \n d ! \r) { if (d \\) { c read(); int first c; /* To allow \377, but not \477 */ if (c 0 c 7) { c c - 0; int c2 read(); if (0 c2 c2 7) { c (c 3) (c2 - 0); c2 read(); if (0 c2 c2 7 first 3) { c (c 3) (c2 - 0); d read(); } else d c2; } else d c2; } else { c switch (c) { case a - 0x7; case b - \b; case f - 0xC; case n - \n; case r - \r; case t - \t; case v - 0xB; default - c; }; d read(); } } else { c d; d read(); } if (i buf.length) { buf Arrays.copyOf(buf, buf.length * 2); } buf[i] (char)c; } peekc (d ttype) ? NEED_CHAR : d; sval String.copyValueOf(buf, 0, i); return ttype; } //对待注解形式的处理。 if (c / (slashSlashCommentsP || slashStarCommentsP)) { c read(); if (c * slashStarCommentsP) { int prevc 0; while ((c read()) ! / || prevc ! *) { if (c \r) { LINENO; c read(); if (c \n) { c read(); } } else { if (c \n) { LINENO; c read(); } } if (c 0) return ttype TT_EOF; prevc c; } return nextToken(); } else if (c / slashSlashCommentsP) { while ((c read()) ! \n c ! \r c 0); peekc c; return nextToken(); } else { /* Now see if it is still a single line comment */ if ((ct[/] CT_COMMENT) ! 0) { while ((c read()) ! \n c ! \r c 0); peekc c; return nextToken(); } else { peekc c; return ttype /; } } } //对于当前类型是注解时的操作。 if ((ctype CT_COMMENT) ! 0) { while ((c read()) ! \n c ! \r c 0); peekc c; return nextToken(); } return ttype c; } /** * 调用该函数时首先进行安全检测如果ttype不为TT_NOTHING,即已经调用过nextToken函数那么将pushedBack的值设为true下一次执行nextToeken函数时 * 便不会修改当前标记的类型同时也不会去修改当前nval或者sval的值。 */ public void pushBack() { if (ttype ! TT_NOTHING) /* No-op if nextToken() not called */ pushedBack true; } /** * 该函数返回LINENO的值即分割标记后最后的行数值得注意的是如果将换行符设置为普通字符的话会影响该函数的准确性。 */ public int lineno() { return LINENO; } /** * 该函数可以得到一个字符串字符串内容为当前标记的类型以及标记所在的行数。 */ public String toString() { String ret switch (ttype) { case TT_EOF - EOF; case TT_EOL - EOL; case TT_WORD - sval; case TT_NUMBER - n nval; case TT_NOTHING - NOTHING; default - { if (ttype 256 ((ctype[ttype] CT_QUOTE) ! 0)) { yield sval; } char s[] new char[3]; s[0] s[2] \; s[1] (char) ttype; yield new String(s); } }; return Token[ ret ], line LINENO; } }2.1、StreamTokenize的2种使用方法2.1.1、解析空格、英文单引号、/反斜杠我的windows操作系统的D盘下有一个StreamTokenizer.txt文件该文件的内容如下所示可以使用FileReader读取这个文件然后使用StreamTokenizer来读取该文件中的每一个token以及该token对应的行号import java.io.*; public class StreamTokenizerTest { public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException { //通过传入一个FileReader来构建一个StreamTokenizer。这里读取的是本地的一个txt文件。 StreamTokenizer stk new StreamTokenizer(new FileReader(new File( D:\\StreamTokenizer.txt))); try { //当没有读取到文件结尾时不停调用nextToken方法然后将每一个token及其行号打印出来。 while (stk.nextToken() ! StreamTokenizer.TT_EOF) { String s null; switch (stk.ttype) { case StreamTokenizer.TT_WORD: s stk.sval; break; case StreamTokenizer.TT_NUMBER: s String.valueOf(stk.nval); break; default: s stk.sval; } System.out.println(stk.toString()); } } catch (IOException e) { e.printStackTrace(); } } }上述代码的执行结果如下所示从结果中可以看出通过nextToken()函数读取的数字型数据都是double类型的如果不符合要求需自行进行转换。StreamTokenizer会把双引号中的内容作为一个Token处理将//之后的内容作为注释注释不会作为token进行读取比如修改上文中D盘下的StreamTokenizer.txt文件如下所示再次执行上文中的StreamTokenizerTest.class结果如下所示如果想让这些符号被当做普通符号来进行处理只需调用StreamTokenize.class::ordinaryChar()函数即可将特殊的字符也当做普通字符处理比如修改上文中的StreamTokenizerTest.class修改后如下所示import java.io.*; public class StreamTokenizerTest { public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException { //通过传入一个FileReader来构建一个StreamTokenizer。这里读取的是本地的一个txt文件。 StreamTokenizer stk new StreamTokenizer(new FileReader(new File( D:\\StreamTokenizer.txt))); stk.ordinaryChar(\);\\ \表示转义 stk.ordinaryChar(/);\\ /不用进行转义 try { //当没有读取到文件结尾时不停调用nextToken方法然后将每一个token及其行号打印出来。 while (stk.nextToken() ! StreamTokenizer.TT_EOF) { String s null; switch (stk.ttype) { case StreamTokenizer.TT_WORD: s stk.sval; break; case StreamTokenizer.TT_NUMBER: s String.valueOf(stk.nval); break; default: s stk.sval; } System.out.println(stk.toString()); } } catch (IOException e) { e.printStackTrace(); } } }继续读取D盘下的StreamTokenizer.txt文件该文件的内容如下所示上述代码的执行结果如下所示也可以使用StreamTokenize.class::resetSyntax()函数将每一个ASCII码表示的字符和中文字符串作为1个token进行处理比如修改上文中的StreamTokenizerTest.class修改后如下所示import java.io.*; public class StreamTokenizerTest { public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException { //通过传入一个FileReader来构建一个StreamTokenizer。这里读取的是本地的一个txt文件。 StreamTokenizer stk new StreamTokenizer(new FileReader(new File( D:\\StreamTokenizer.txt))); stk.resetSyntax(); try { //当没有读取到文件结尾时不停调用nextToken方法然后将每一个token及其行号打印出来。 while (stk.nextToken() ! StreamTokenizer.TT_EOF) { String s null; switch (stk.ttype) { case StreamTokenizer.TT_WORD: s stk.sval; break; case StreamTokenizer.TT_NUMBER: s String.valueOf(stk.nval); break; default: s stk.sval; } System.out.println(stk.toString()); } } catch (IOException e) { e.printStackTrace(); } } }继续读取D盘下的StreamTokenizer.txt文件该文件的内容如下所示上述代码的执行结果如下所示2.1.2、代替Scanner.class来读取命令行并对命令行输入的内容进行token转换代码如下所示import java.io.*; public class StreamTokenizerTest { public static void main(String[] args) throws IOException { //将标准输入流传入StreamTokenizer中。 StreamTokenizer in new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in))); PrintWriter out new PrintWriter(new OutputStreamWriter(System.out)); int a, b; while (in.nextToken() ! StreamTokenizer.TT_EOF) { a (int) in.nval; in.nextToken(); b (int) in.nval; //out.println(a b); System.out.println(a b (a b)); } //将缓存区中的数据真实写出。 out.flush(); } }上述代码的执行结果如下所示

相关新闻