- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
17.1.2 检查大小写样式
尽管对涉及文字处理的一些项目来说,前例显得比较方便,但下面要介绍的项目却能立即发挥作用,因为它执行的是一个样式检查,以确保我们的大小写形式符合“事实上”的 Java 样式标准。它会在当前目录中打开每个.java 文件,并提取出所有类名以及标识符。若发现有不符合 Java 样式的情况,就向我们提出报告。
为了让这个程序正确运行,首先必须构建一个类名,将它作为一个“仓库”,负责容纳标准 Java 库中的所有类名。为达到这个目的,需遍历用于标准 Java 库的所有源码子目录,并在每个子目录都运行 ClassScanner。至于参数,则提供仓库文件的名字(每次都用相同的路径和名字)和命令行开关-a,指出类名应当添加到该仓库文件中。
为了用程序检查自己的代码,需要运行它,并向它传递要使用的仓库文件的路径与名字。它会检查当前目录中的所有类和标识符,并告诉我们哪些没有遵守典型的 Java 大写写规范。
要注意这个程序并不是十全十美的。有些时候,它可能报告自己查到一个问题。但当我们仔细检查代码的时候,却发现没有什么需要更改的。尽管这有点儿烦人,但仍比自己动手检查代码中的所有错误强得多。
下面列出源代码,后面有详细的解释:
//: ClassScanner.java // Scans all files in directory for classes // and identifiers, to check capitalization. // Assumes properly compiling code listings. // Doesn't do everything right, but is a very // useful aid. import java.io.*; import java.util.*; class MultiStringMap extends Hashtable { public void add(String key, String value) { if(!containsKey(key)) put(key, new Vector()); ((Vector)get(key)).addElement(value); } public Vector getVector(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (Vector)get(key); } public void printValues(PrintStream p) { Enumeration k = keys(); while(k.hasMoreElements()) { String oneKey = (String)k.nextElement(); Vector val = getVector(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.elementAt(i)); } } } public class ClassScanner { private File path; private String[] fileList; private Properties classes = new Properties(); private MultiStringMap classMap = new MultiStringMap(), identMap = new MultiStringMap(); private StreamTokenizer in; public ClassScanner() { path = new File("."); fileList = path.list(new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); scanListing(fileList[i]); } } void scanListing(String fname) { try { in = new StreamTokenizer( new BufferedReader( new FileReader(fname))); // Doesn't seem to work: // in.slashStarComments(true); // in.slashSlashComments(true); in.ordinaryChar('/'); in.ordinaryChar('.'); in.wordChars('_', '_'); in.eolIsSignificant(true); while(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') eatComments(); else if(in.ttype == StreamTokenizer.TT_WORD) { if(in.sval.equals("class") || in.sval.equals("interface")) { // Get class name: while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval); } if(in.sval.equals("import") || in.sval.equals("package")) discardLine(); else // It's an identifier or keyword identMap.add(fname, in.sval); } } } catch(IOException e) { e.printStackTrace(); } } void discardLine() { try { while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_EOL) ; // Throw away tokens to end of line } catch(IOException e) { e.printStackTrace(); } } // StreamTokenizer's comment removal seemed // to be broken. This extracts them: void eatComments() { try { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } catch(IOException e) { e.printStackTrace(); } } public String[] classNames() { String[] result = new String[classes.size()]; Enumeration e = classes.keys(); int i = 0; while(e.hasMoreElements()) result[i++] = (String)e.nextElement(); return result; } public void checkClassNames() { Enumeration files = classMap.keys(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector cls = classMap.getVector(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.elementAt(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: " + file + ", class: " + className); } } } public void checkIdentNames() { Enumeration files = identMap.keys(); Vector reportSet = new Vector(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector ids = identMap.getVector(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.elementAt(i); if(!classes.contains(id)) { // Ignore identifiers of length 3 or // longer that are all uppercase // (probably static final values): if(id.length() >= 3 && id.equals( id.toUpperCase())) continue; // Check to see if first char is upper: if(Character.isUpperCase(id.charAt(0))){ if(reportSet.indexOf(file + id) == -1){ // Not reported yet reportSet.addElement(file + id); System.out.println( "Ident capitalization error in:" + file + ", ident: " + id); } } } } } } static final String usage = "Usage: \n" + "ClassScanner classnames -a\n" + "\tAdds all the class names in this \n" + "\tdirectory to the repository file \n" + "\tcalled 'classnames'\n" + "ClassScanner classnames\n" + "\tChecks all the java files in this \n" + "\tdirectory for capitalization errors, \n" + "\tusing the repository file 'classnames'"; private static void usage() { System.err.println(usage); System.exit(1); } public static void main(String[] args) { if(args.length < 1 || args.length > 2) usage(); ClassScanner c = new ClassScanner(); File old = new File(args[0]); if(old.exists()) { try { // Try to open an existing // properties file: InputStream oldlist = new BufferedInputStream( new FileInputStream(old)); c.classes.load(oldlist); oldlist.close(); } catch(IOException e) { System.err.println("Could not open " + old + " for reading"); System.exit(1); } } if(args.length == 1) { c.checkClassNames(); c.checkIdentNames(); } // Write the class names to a repository: if(args.length == 2) { if(!args[1].equals("-a")) usage(); try { BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(args[0])); c.classes.save(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } } class JavaFilter implements FilenameFilter { public boolean accept(File dir, String name) { // Strip path information: String f = new File(name).getName(); return f.trim().endsWith(".java"); } } ///:~
MultiStringMap 类是个特殊的工具,允许我们将一组字串与每个键项对应(映射)起来。和前例一样,这里也使用了一个散列表(Hashtable),不过这次设置了继承。该散列表将键作为映射成为 Vector 值的单一的字串对待。add() 方法的作用很简单,负责检查散列表里是否存在一个键。如果不存在,就在其中放置一个。getVector() 方法为一个特定的键产生一个 Vector;而 printValues() 将所有值逐个 Vector 地打印出来,这对程序的调试非常有用。
为简化程序,来自标准 Java 库的类名全都置入一个 Properties(属性)对象中(来自标准 Java 库)。记住 Properties 对象实际是个散列表,其中只容纳了用于键和值项的 String 对象。然而仅需一次方法调用,我们即可把它保存到磁盘,或者从磁盘中恢复。实际上,我们只需要一个名字列表,所以为键和值都使用了相同的对象。
针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个 MultiStringMap:classMap 以及 identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为 classes 的 Properties 对象中。一旦在本地目录发现了一个新类名,也会将其加入 classes 以及 classMap。这样一来,classMap 就可用于在本地目录的所有类间遍历,而且可用 classes 检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入 identMap)。
ClassScanner 的默认构建器会创建一个由文件名构成的列表(采用 FilenameFilter 的 JavaFilter 实现形式,参见第 10 章)。随后会为每个文件名都调用 scanListing()。
在 scanListing() 内部,会打开源码文件,并将其转换成一个 StreamTokenizer。根据 Java 帮助文档,将 true 传递给 slashStartComments() 和 slashSlashComments() 的本意应当是剥除那些注释内容,但这样做似乎有些问题(在 Java 1.0 中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,'/'必须作为一个原始字符捕获,而不是让 StreamTokeinzer 将其当作注释的一部分对待。
此时要用 ordinaryChar() 方法指示 StreamTokenizer 采取正确的操作。同样的道理也适用于点号('.'),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被 StreamTokenizer 当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在 static final 值中以 TT_EOF 等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars() 方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用 eollsSignificant(true),换行符(EOL)会被显示出来,而不是被 StreamTokenizer 吸收。
scanListing() 剩余的部分将读入和检查记号,直至文件尾。一旦 nextToken() 返回一个 final static 值——StreamTokenizer.TT_EOF,就标志着已经抵达文件尾部。
若记号是个'/',意味着它可能是个注释,所以就调用 eatComments(),对这种情况进行处理。我们在这儿唯一感兴趣的其他情况是它是否为一个单词,当然还可能存在另一些特殊情况。
如果单词是 class(类)或 interface(接口),那么接着的记号就应当代表一个类或接口名字,并将其置入 classes 和 classMap。若单词是 import 或者 package,那么我们对这一行剩下的东西就没什么兴趣了。其他所有东西肯定是一个标识符(这是我们感兴趣的),或者是一个关键字(对此不感兴趣,但它们采用的肯定是小写形式,所以不必兴师动众地检查它们)。它们将加入到 identMap。
discardLine() 方法是一个简单的工具,用于查找行末位置。注意每次得到一个新记号时,都必须检查行末。
只要在主解析循环中碰到一个正斜杠,就会调用 eatComments() 方法。然而,这并不表示肯定遇到了一条注释,所以必须将接着的记号提取出来,检查它是一个正斜杠(那么这一行会被丢弃),还是一个星号。但假如两者都不是,意味着必须在主解析循环中将刚才取出的记号送回去!幸运的是,pushBack() 方法允许我们将当前记号“压回”输入数据流。所以在主解析循环调用 nextToken() 的时候,它能正确地得到刚才送回的东西。
为方便起见,classNames() 方法产生了一个数组,其中包含了 classes 集合中的所有名字。这个方法未在程序中使用,但对代码的调试非常有用。
接下来的两个方法是实际进行检查的地方。在 checkClassNames() 中,类名从 classMap 提取出来(请记住,classMap 只包含了这个目录内的名字,它们按文件名组织,所以文件名可能伴随错误的类名打印出来)。为做到这一点,需要取出每个关联的 Vector,并遍历其中,检查第一个字符是否为小写。若确实为小写,则打印出相应的出错提示消息。
在 checkIdentNames() 中,我们采用了一种类似的方法:每个标识符名字都从 identMap 中提取出来。如果名字不在 classes 列表中,就认为它是一个标识符或者关键字。此时会检查一种特殊情况:如果标识符的长度等于 3 或者更长,而且所有字符都是大写的,则忽略此标识符,因为它可能是一个 static final 值,比如 TT_EOF。当然,这并不是一种完美的算法,但它假定我们最终会注意到任何全大写标识符都是不合适的。
这个方法并不是报告每一个以大写字符开头的标识符,而是跟踪那些已在一个名为 reportSet() 的 Vector 中报告过的。它将 Vector 当作一个“集合”对待,告诉我们一个项目是否已在那个集合中。该项目是通过将文件名和标识符连接起来生成的。若元素不在集合中,就加入它,然后产生报告。
程序列表剩下的部分由 main() 构成,它负责控制命令行参数,并判断我们是准备在标准 Java 库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个 ClassScanner 对象。
无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个 File 对象并测试是否存在,就可决定是否打开文件并在 ClassScanner 中装载 classes 这个 Properties 列表(使用 load())。来自仓库的类将追加到由 ClassScanner 构建器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用 Properties.save() 方法将列表写入一个文件,同时用一个字串提供文件头信息。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论