盡管對涉及文字處理的一些項目來說,前例顯得比較方便,但下面要介紹的項目卻能立即發揮作用,因為它執行的是一個樣式檢查,以確保我們的大小寫形式符合“事實上”的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()方法將列表寫入一個文件,同時用一個字串提供文件頭信息。