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