本章包含了一系列項目,它們都以本書介紹的內容為基礎,並對早期的章節進行了一定程度的擴充。
與以前經歷過的項目相比,這兒的大多數項目都明顯要複雜得多,它們充分演示了新技術以及類庫的運用。
如果您有C或C++的經驗,那麼最開始可能會對Java控制文本的能力感到懷疑。事實上,我們最害怕的就是速度特別慢,這可能妨礙我們創造能力的發揮。然而,Java對應的工具(特別是String
類)具有很強的功能,就象本節的例子展示的那樣(而且性能也有一定程度的提升)。
正如大家即將看到的那樣,建立這些例子的目的都是為瞭解決本書編制過程中遇到的一些問題。但是,它們的能力並非僅止於此。通過簡單的改造,即可讓它們在其他場合大顯身手。除此以外,它們還揭示出了本書以前沒有強調過的一項Java特性。
對於本書每一個完整的代碼列表(不是代碼段),大家無疑會注意到它們都用特殊的註釋記號起始與結束(//:
和///:~
)。之所以要包括這種標誌信息,是為了能將代碼從本書自動提取到兼容的源碼文件中。在我的前一本書裡,我設計了一個系統,可將測試過的代碼文件自動合併到書中。但對於這本書,我發現一種更簡便的做法是一旦通過了最初的測試,就把代碼粘貼到書中。而且由於很難第一次就編譯通過,所以我在書的內部編輯代碼。但如何提取並測試代碼呢?這個程序就是關鍵。如果你打算解決一個文字處理的問題,那麼它也很有利用價值。該例也演示了String
類的許多特性。
我首先將整本書都以ASCII文本格式保存成一個獨立的文件。CodePackager
程序有兩種運行模式(在usageString
有相應的描述):如果使用-p
標誌,程序就會檢查一個包含了ASCII文本(即本書的內容)的一個輸入文件。它會遍歷這個文件,按照註釋記號提取出代碼,並用位於第一行的文件名來決定創建文件使用什麼名字。除此以外,在需要將文件置入一個特殊目錄的時候,它還會檢查package
語句(根據由package
語句指定的路徑選擇)。
但這樣還不夠。程序還要對包(package
)名進行跟蹤,從而監視章內發生的變化。由於每一章使用的所有包都以c02
,c03
,c04
等等起頭,用於標記它們所屬的是哪一章(除那些以com
起頭的以外,它們在對不同的章進行跟蹤的時候會被忽略)——只要每一章的第一個代碼列表包含了一個package
,所以CodePackager
程序能知道每一章發生的變化,並將後續的文件放到新的子目錄裡。
每個文件提取出來時,都會置入一個SourceCodeFile
對象,隨後再將那個對象置入一個集合(後面還會詳盡講述這個過程)。這些SourceCodeFile
對象可以簡單地保存在文件中,那正是本項目的第二個用途。如果直接調用CodePackager
,不添加-p
標誌,它就會將一個“打包”文件作為輸入。那個文件隨後會被提取(釋放)進入單獨的文件。所以-p
標誌的意思就是提取出來的文件已被“打包”(packed
)進入這個單一的文件。
但為什麼還要如此麻煩地使用打包文件呢?這是由於不同的計算機平臺用不同的方式在文件裡保存文本信息。其中最大的問題是換行字符的表示方法;當然,還有可能存在另一些問題。然而,Java有一種特殊類型的IO數據流——DataOutputStream
——它可以保證“無論數據來自何種機器,只要使用一個DataInputStream
收取這些數據,就可用本機正確的格式保存它們”。也就是說,Java負責控制與不同平臺有關的所有細節,而這正是Java最具魅力的一點。所以-p
標誌能將所有東西都保存到單一的文件裡,並採用通用的格式。用戶可從Web下載這個文件以及Java程序,然後對這個文件運行CodePackager
,同時不指定-p
標誌,文件便會釋放到系統中正確的場所(亦可指定另一個子目錄;否則就在當前目錄創建子目錄)。為確保不會留下與特定平臺有關的格式,凡是需要描述一個文件或路徑的時候,我們就使用File對象。除此以外,還有一項特別的安全措施:在每個子目錄裡都放入一個空文件;那個文件的名字指出在那個子目錄裡應找到多少個文件。
下面是完整的代碼,後面會對它進行詳細的說明:
//: CodePackager.java
// "Packs" and "unpacks" the code in "Thinking
// in Java" for cross-platform distribution.
/* Commented so CodePackager sees it and starts
a new chapter directory, but so you don't
have to worry about the directory where this
program lives:
package c17;
*/
import java.util.*;
import java.io.*;
class Pr {
static void error(String e) {
System.err.println("ERROR: " + e);
System.exit(1);
}
}
class IO {
static BufferedReader disOpen(File f) {
BufferedReader in = null;
try {
in = new BufferedReader(
new FileReader(f));
} catch(IOException e) {
Pr.error("could not open " + f);
}
return in;
}
static BufferedReader disOpen(String fname) {
return disOpen(new File(fname));
}
static DataOutputStream dosOpen(File f) {
DataOutputStream in = null;
try {
in = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(f)));
} catch(IOException e) {
Pr.error("could not open " + f);
}
return in;
}
static DataOutputStream dosOpen(String fname) {
return dosOpen(new File(fname));
}
static PrintWriter psOpen(File f) {
PrintWriter in = null;
try {
in = new PrintWriter(
new BufferedWriter(
new FileWriter(f)));
} catch(IOException e) {
Pr.error("could not open " + f);
}
return in;
}
static PrintWriter psOpen(String fname) {
return psOpen(new File(fname));
}
static void close(Writer os) {
try {
os.close();
} catch(IOException e) {
Pr.error("closing " + os);
}
}
static void close(DataOutputStream os) {
try {
os.close();
} catch(IOException e) {
Pr.error("closing " + os);
}
}
static void close(Reader os) {
try {
os.close();
} catch(IOException e) {
Pr.error("closing " + os);
}
}
}
class SourceCodeFile {
public static final String
startMarker = "//:", // Start of source file
endMarker = "} ///:~", // End of source
endMarker2 = "}; ///:~", // C++ file end
beginContinue = "} ///:Continued",
endContinue = "///:Continuing",
packMarker = "###", // Packed file header tag
eol = // Line separator on current system
System.getProperty("line.separator"),
filesep = // System's file path separator
System.getProperty("file.separator");
public static String copyright = "";
static {
try {
BufferedReader cr =
new BufferedReader(
new FileReader("Copyright.txt"));
String crin;
while((crin = cr.readLine()) != null)
copyright += crin + "\n";
cr.close();
} catch(Exception e) {
copyright = "";
}
}
private String filename, dirname,
contents = new String();
private static String chapter = "c02";
// The file name separator from the old system:
public static String oldsep;
public String toString() {
return dirname + filesep + filename;
}
// Constructor for parsing from document file:
public SourceCodeFile(String firstLine,
BufferedReader in) {
dirname = chapter;
// Skip past marker:
filename = firstLine.substring(
startMarker.length()).trim();
// Find space that terminates file name:
if(filename.indexOf(' ') != -1)
filename = filename.substring(
0, filename.indexOf(' '));
System.out.println("found: " + filename);
contents = firstLine + eol;
if(copyright.length() != 0)
contents += copyright + eol;
String s;
boolean foundEndMarker = false;
try {
while((s = in.readLine()) != null) {
if(s.startsWith(startMarker))
Pr.error("No end of file marker for " +
filename);
// For this program, no spaces before
// the "package" keyword are allowed
// in the input source code:
else if(s.startsWith("package")) {
// Extract package name:
String pdir = s.substring(
s.indexOf(' ')).trim();
pdir = pdir.substring(
0, pdir.indexOf(';')).trim();
// Capture the chapter from the package
// ignoring the 'com' subdirectories:
if(!pdir.startsWith("com")) {
int firstDot = pdir.indexOf('.');
if(firstDot != -1)
chapter =
pdir.substring(0,firstDot);
else
chapter = pdir;
}
// Convert package name to path name:
pdir = pdir.replace(
'.', filesep.charAt(0));
System.out.println("package " + pdir);
dirname = pdir;
}
contents += s + eol;
// Move past continuations:
if(s.startsWith(beginContinue))
while((s = in.readLine()) != null)
if(s.startsWith(endContinue)) {
contents += s + eol;
break;
}
// Watch for end of code listing:
if(s.startsWith(endMarker) ||
s.startsWith(endMarker2)) {
foundEndMarker = true;
break;
}
}
if(!foundEndMarker)
Pr.error(
"End marker not found before EOF");
System.out.println("Chapter: " + chapter);
} catch(IOException e) {
Pr.error("Error reading line");
}
}
// For recovering from a packed file:
public SourceCodeFile(BufferedReader pFile) {
try {
String s = pFile.readLine();
if(s == null) return;
if(!s.startsWith(packMarker))
Pr.error("Can't find " + packMarker
+ " in " + s);
s = s.substring(
packMarker.length()).trim();
dirname = s.substring(0, s.indexOf('#'));
filename = s.substring(s.indexOf('#') + 1);
dirname = dirname.replace(
oldsep.charAt(0), filesep.charAt(0));
filename = filename.replace(
oldsep.charAt(0), filesep.charAt(0));
System.out.println("listing: " + dirname
+ filesep + filename);
while((s = pFile.readLine()) != null) {
// Watch for end of code listing:
if(s.startsWith(endMarker) ||
s.startsWith(endMarker2)) {
contents += s;
break;
}
contents += s + eol;
}
} catch(IOException e) {
System.err.println("Error reading line");
}
}
public boolean hasFile() {
return filename != null;
}
public String directory() { return dirname; }
public String filename() { return filename; }
public String contents() { return contents; }
// To write to a packed file:
public void writePacked(DataOutputStream out) {
try {
out.writeBytes(
packMarker + dirname + "#"
+ filename + eol);
out.writeBytes(contents);
} catch(IOException e) {
Pr.error("writing " + dirname +
filesep + filename);
}
}
// To generate the actual file:
public void writeFile(String rootpath) {
File path = new File(rootpath, dirname);
path.mkdirs();
PrintWriter p =
IO.psOpen(new File(path, filename));
p.print(contents);
IO.close(p);
}
}
class DirMap {
private Hashtable t = new Hashtable();
private String rootpath;
DirMap() {
rootpath = System.getProperty("user.dir");
}
DirMap(String alternateDir) {
rootpath = alternateDir;
}
public void add(SourceCodeFile f){
String path = f.directory();
if(!t.containsKey(path))
t.put(path, new Vector());
((Vector)t.get(path)).addElement(f);
}
public void writePackedFile(String fname) {
DataOutputStream packed = IO.dosOpen(fname);
try {
packed.writeBytes("###Old Separator:" +
SourceCodeFile.filesep + "###\n");
} catch(IOException e) {
Pr.error("Writing separator to " + fname);
}
Enumeration e = t.keys();
while(e.hasMoreElements()) {
String dir = (String)e.nextElement();
System.out.println(
"Writing directory " + dir);
Vector v = (Vector)t.get(dir);
for(int i = 0; i < v.size(); i++) {
SourceCodeFile f =
(SourceCodeFile)v.elementAt(i);
f.writePacked(packed);
}
}
IO.close(packed);
}
// Write all the files in their directories:
public void write() {
Enumeration e = t.keys();
while(e.hasMoreElements()) {
String dir = (String)e.nextElement();
Vector v = (Vector)t.get(dir);
for(int i = 0; i < v.size(); i++) {
SourceCodeFile f =
(SourceCodeFile)v.elementAt(i);
f.writeFile(rootpath);
}
// Add file indicating file quantity
// written to this directory as a check:
IO.close(IO.dosOpen(
new File(new File(rootpath, dir),
Integer.toString(v.size())+".files")));
}
}
}
public class CodePackager {
private static final String usageString =
"usage: java CodePackager packedFileName" +
"\nExtracts source code files from packed \n" +
"version of Tjava.doc sources into " +
"directories off current directory\n" +
"java CodePackager packedFileName newDir\n" +
"Extracts into directories off newDir\n" +
"java CodePackager -p source.txt packedFile" +
"\nCreates packed version of source files" +
"\nfrom text version of Tjava.doc";
private static void usage() {
System.err.println(usageString);
System.exit(1);
}
public static void main(String[] args) {
if(args.length == 0) usage();
if(args[0].equals("-p")) {
if(args.length != 3)
usage();
createPackedFile(args);
}
else {
if(args.length > 2)
usage();
extractPackedFile(args);
}
}
private static String currentLine;
private static BufferedReader in;
private static DirMap dm;
private static void
createPackedFile(String[] args) {
dm = new DirMap();
in = IO.disOpen(args[1]);
try {
while((currentLine = in.readLine())
!= null) {
if(currentLine.startsWith(
SourceCodeFile.startMarker)) {
dm.add(new SourceCodeFile(
currentLine, in));
}
else if(currentLine.startsWith(
SourceCodeFile.endMarker))
Pr.error("file has no start marker");
// Else ignore the input line
}
} catch(IOException e) {
Pr.error("Error reading " + args[1]);
}
IO.close(in);
dm.writePackedFile(args[2]);
}
private static void
extractPackedFile(String[] args) {
if(args.length == 2) // Alternate directory
dm = new DirMap(args[1]);
else // Current directory
dm = new DirMap();
in = IO.disOpen(args[0]);
String s = null;
try {
s = in.readLine();
} catch(IOException e) {
Pr.error("Cannot read from " + in);
}
// Capture the separator used in the system
// that packed the file:
if(s.indexOf("###Old Separator:") != -1 ) {
String oldsep = s.substring(
"###Old Separator:".length());
oldsep = oldsep.substring(
0, oldsep. indexOf('#'));
SourceCodeFile.oldsep = oldsep;
}
SourceCodeFile sf = new SourceCodeFile(in);
while(sf.hasFile()) {
dm.add(sf);
sf = new SourceCodeFile(in);
}
dm.write();
}
} ///:~
我們注意到package
語句已經作為註釋標誌出來了。由於這是本章的第一個程序,所以package
語句是必需的,用它告訴CodePackager
已改換到另一章。但是把它放入包裡卻會成為一個問題。當我們創建一個包的時候,需要將結果程序同一個特定的目錄結構聯繫在一起,這一做法對本書的大多數例子都是適用的。但在這裡,CodePackager
程序必須在一個專用的目錄裡編譯和運行,所以package
語句作為註釋標記出去。但對CodePackager
來說,它“看起來”依然象一個普通的package
語句,因為程序還不是特別複雜,不能偵查到多行註釋(沒有必要做得這麼複雜,這裡只要求方便就行)。
頭兩個類是“支持/工具”類,作用是使程序剩餘的部分在編寫時更加連貫,也更便於閱讀。第一個是Pr
,它類似ANSI C的perror
庫,兩者都能打印出一條錯誤提示消息(但同時也會退出程序)。第二個類將文件的創建過程封裝在內,這個過程已在第10章介紹過了;大家已經知道,這樣做很快就會變得非常累贅和麻煩。為解決這個問題,第10章提供的方案致力於新類的創建,但這兒的“靜態”方法已經使用過了。在那些方法中,正常的異常會被捕獲,並相應地進行處理。這些方法使剩餘的代碼顯得更加清爽,更易閱讀。
幫助解決問題的第一個類是SourceCodeFile
(源碼文件),它代表本書一個源碼文件包含的所有信息(內容、文件名以及目錄)。它同時還包含了一系列String
常數,分別代表一個文件的開始與結束;在打包文件內使用的一個標記;當前系統的換行符;文件路徑分隔符(注意要用System.getProperty()
偵查本地版本是什麼);以及一大段版權聲明,它是從下面這個Copyright.txt
文件裡提取出來的:
//////////////////////////////////////////////////
// Copyright (c) Bruce Eckel, 1998
// Source code file from the book "Thinking in Java"
// All rights reserved EXCEPT as allowed by the
// following statements: You may freely use this file
// for your own work (personal or commercial),
// including modifications and distribution in
// executable form only. Permission is granted to use
// this file in classroom situations, including its
// use in presentation materials, as long as the book
// "Thinking in Java" is cited as the source.
// Except in classroom situations, you may not copy
// and distribute this code; instead, the sole
// distribution point is http://www.BruceEckel.com
// (and official mirror sites) where it is
// freely available. You may not remove this
// copyright and notice. You may not distribute
// modified versions of the source code in this
// package. You may not use this file in printed
// media without the express permission of the
// author. Bruce Eckel makes no representation about
// the suitability of this software for any purpose.
// It is provided "as is" without express or implied
// warranty of any kind, including any implied
// warranty of merchantability, fitness for a
// particular purpose or non-infringement. The entire
// risk as to the quality and performance of the
// software is with you. Bruce Eckel and the
// publisher shall not be liable for any damages
// suffered by you or any third party as a result of
// using or distributing software. In no event will
// Bruce Eckel or the publisher be liable for any
// lost revenue, profit, or data, or for direct,
// indirect, special, consequential, incidental, or
// punitive damages, however caused and regardless of
// the theory of liability, arising out of the use of
// or inability to use software, even if Bruce Eckel
// and the publisher have been advised of the
// possibility of such damages. Should the software
// prove defective, you assume the cost of all
// necessary servicing, repair, or correction. If you
// think you've found an error, please email all
// modified files with clearly commented changes to:
// Bruce@EckelObjects.com. (please use the same
// address for non-code errors found in the book).
//////////////////////////////////////////////////
從一個打包文件中提取文件時,當初所用系統的文件分隔符也會標註出來,以便用本地系統適用的符號替換它。
當前章的子目錄保存在chapter
字段中,它初始化成c02
(大家可注意一下第2章的列表正好沒有包含一個打包語句)。只有在當前文件裡發現一個package
(打包)語句時,chapter
字段才會發生改變。
(1) 構建一個打包文件
第一個構造器用於從本書的ASCII文本版裡提取出一個文件。發出調用的代碼(在列表裡較深的地方)會讀入並檢查每一行,直到找到與一個列表的開頭相符的為止。在這個時候,它就會新建一個SourceCodeFile
對象,將第一行的內容(已經由調用代碼讀入了)傳遞給它,同時還要傳遞BufferedReader
對象,以便在這個緩衝區中提取源碼列表剩餘的內容。
從這時起,大家會發現String
方法被頻繁運用。為提取出文件名,需調用substring()
的重載版本,令其從一個起始偏移開始,一直讀到字符串的末尾,從而形成一個“子串”。為算出這個起始索引,先要用length()
得出startMarker
的總長,再用trim()
刪除字符串頭尾多餘的空格。第一行在文件名後也可能有一些字符;它們是用indexOf()
偵測出來的。若沒有發現找到我們想尋找的字符,就返回-1;若找到那些字符,就返回它們第一次出現的位置。注意這也是indexOf()
的一個重載版本,採用一個字符串作為參數,而非一個字符。
解析出並保存好文件名後,第一行會被置入字符串contents
中(該字符串用於保存源碼清單的完整正文)。隨後,將剩餘的代碼行讀入,併合並進入contents
字符串。當然事情並沒有想象的那麼簡單,因為特定的情況需加以特別的控制。一種情況是錯誤檢查:若直接遇到一個startMarker
(起始標記),表明當前操作的這個代碼列表沒有設置一個結束標記。這屬於一個出錯條件,需要退出程序。
另一種特殊情況與package
關鍵字有關。儘管Java是一種自由形式的語言,但這個程序要求package
關鍵字必須位於行首。若發現package
關鍵字,就通過檢查位於開頭的空格以及位於末尾的分號,從而提取出包名(注意亦可一次單獨的操作實現,方法是使用重載的substring()
,令其同時檢查起始和結束索引位置)。隨後,將包名中的點號替換成特定的文件分隔符——當然,這裡要假設文件分隔符僅有一個字符的長度。儘管這個假設可能對目前的所有系統都是適用的,但一旦遇到問題,一定不要忘了檢查一下這裡。
默認操作是將每一行都連接到contents
裡,同時還有換行字符,直到遇到一個endMarker
(結束標記)為止。該標記指出構造器應當停止了。若在endMarker
之前遇到了文件結尾,就認為存在一個錯誤。
(2) 從打包文件中提取
第二個構造器用於將源碼文件從打包文件中恢復(提取)出來。在這兒,作為調用者的方法不必擔心會跳過一些中間文本。打包文件包含了所有源碼文件,它們相互間緊密地靠在一起。需要傳遞給該構造器的僅僅是一個BufferedReader
,它代表著“信息源”。構造器會從中提取出自己需要的信息。但在每個代碼列表開始的地方還有一些配置信息,它們的身份是用packMarker
(打包標記)指出的。若packMarker
不存在,意味著調用者試圖用錯誤的方法來使用這個構造器。
一旦發現packMarker
,就會將其剝離出來,並提取出目錄名(用一個#
結尾)以及文件名(直到行末)。不管在哪種情況下,舊分隔符都會被替換成本地適用的一個分隔符,這是用String replace()
方法實現的。老的分隔符被置於打包文件的開頭,在代碼列表稍靠後的一部分即可看到是如何把它提取出來的。
構造器剩下的部分就非常簡單了。它讀入每一行,把它合併到contents
裡,直到遇見endMarker
為止。
(3) 程序列表的存取
接下來的一系列方法是簡單的訪問器:directory()
、filename()
(注意方法可能與字段有相同的拼寫和大小寫形式)和contents()
。而hasFile()
用於指出這個對象是否包含了一個文件(很快就會知道為什麼需要這個)。
最後三個方法致力於將這個代碼列表寫進一個文件——要麼通過writePacked()
寫入一個打包文件,要麼通過writeFile()
寫入一個Java源碼文件。writePacked()
需要的唯一東西就是DataOutputStream
,它是在別的地方打開的,代表著準備寫入的文件。它先把頭信息置入第一行,再調用writeBytes()
將contents
(內容)寫成一種“通用”格式。
準備寫Java源碼文件時,必須先把文件建好。這是用IO.psOpen()
實現的。我們需要向它傳遞一個File
對象,其中不僅包含了文件名,也包含了路徑信息。但現在的問題是:這個路徑實際存在嗎?用戶可能決定將所有源碼目錄都置入一個完全不同的子目錄,那個目錄可能是尚不存在的。所以在正式寫每個文件之前,都要調用File.mkdirs()
方法,建好我們想向其中寫入文件的目錄路徑。它可一次性建好整個路徑。
(4) 整套列表的包容
以子目錄的形式組織代碼列表是非常方便的,儘管這要求先在內存中建好整套列表。之所以要這樣做,還有另一個很有說服力的原因:為了構建更“健康”的系統。也就是說,在創建代碼列表的每個子目錄時,都會加入一個額外的文件,它的名字包含了那個目錄內應有的文件數目。
DirMap
類可幫助我們實現這一效果,並有效地演示了一個“多重映射”的概述。這是通過一個散列表(Hashtable
)實現的,它的“鍵”是準備創建的子目錄,而“值”是包含了那個特定目錄中的SourceCodeFile
對象的Vector
對象。所以,我們在這兒並不是將一個“鍵”映射(或對應)到一個值,而是通過對應的Vector
,將一個鍵“多重映射”到一系列值。儘管這聽起來似乎很複雜,但具體實現時卻是非常簡單和直接的。大家可以看到,DirMap
類的大多數代碼都與向文件中的寫入有關,而非與“多重映射”有關。與它有關的代碼僅極少數而已。
可通過兩種方式建立一個DirMap
(目錄映射或對應)關係:默認構造器假定我們希望目錄從當前位置向下展開,而另一個構造器讓我們為起始目錄指定一個備用的“絕對”路徑。
add()
方法是一個採取的行動比較密集的場所。首先將directory()
從我們想添加的SourceCodeFile
裡提取出來,然後檢查散列表(Hashtable
),看看其中是否已經包含了那個鍵。如果沒有,就向散列表加入一個新的Vector
,並將它同那個鍵關聯到一起。到這時,不管採取的是什麼途徑,Vector
都已經就位了,可以將它提取出來,以便添加SourceCodeFile
。由於Vector
可象這樣同散列表方便地合併到一起,所以我們從兩方面都能感覺得非常方便。
寫一個打包文件時,需打開一個準備寫入的文件(當作DataOutputStream
打開,使數據具有“通用”性),並在第一行寫入與老的分隔符有關的頭信息。接著產生對Hashtable
鍵的一個Enumeration
(枚舉),並遍歷其中,選擇每一個目錄,並取得與那個目錄有關的Vector,使那個Vector
中的每個SourceCodeFile
都能寫入打包文件中。
用write()
將Java源碼文件寫入它們對應的目錄時,採用的方法幾乎與writePackedFile()
完全一致,因為兩個方法都只需簡單調用SourceCodeFile
中適當的方法。但在這裡,根路徑會傳遞給SourceCodeFile.writeFile()
。所有文件都寫好後,名字中指定了已寫文件數量的那個附加文件也會被寫入。
(5) 主程序
前面介紹的那些類都要在CodePackager
中用到。大家首先看到的是用法字符串。一旦最終用戶不正確地調用了程序,就會打印出介紹正確用法的這個字符串。調用這個字符串的是usage()
方法,同時還要退出程序。main()
唯一的任務就是判斷我們希望創建一個打包文件,還是希望從一個打包文件中提取什麼東西。隨後,它負責保證使用的是正確的參數,並調用適當的方法。
創建一個打包文件時,它默認位於當前目錄,所以我們用默認構造器創建DirMap
。打開文件後,其中的每一行都會讀入,並檢查是否符合特殊的條件:
(1) 若行首是一個用於源碼列表的起始標記,就新建一個SourceCodeFile
對象。構造器會讀入源碼列表剩下的所有內容。結果產生的引用將直接加入DirMap
。
(2) 若行首是一個用於源碼列表的結束標記,表明某個地方出現錯誤,因為結束標記應當只能由SourceCodeFile
構造器發現。
提取/釋放一個打包文件時,提取出來的內容可進入當前目錄,亦可進入另一個備用目錄。所以需要相應地創建DirMap
對象。打開文件,並將第一行讀入。老的文件路徑分隔符信息將從這一行中提取出來。隨後根據輸入來創建第一個SourceCodeFile
對象,它會加入DirMap
。只要包含了一個文件,新的SourceCodeFile
對象就會創建並加入(創建的最後一個用光輸入內容後,會簡單地返回,然後hasFile()
會返回一個錯誤)。
儘管對涉及文字處理的一些項目來說,前例顯得比較方便,但下面要介紹的項目卻能立即發揮作用,因為它執行的是一個樣式檢查,以確保我們的大小寫形式符合“事實上”的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 fina
l值,比如TT_EOF
。當然,這並不是一種完美的算法,但它假定我們最終會注意到任何全大寫標識符都是不合適的。
這個方法並不是報告每一個以大寫字符開頭的標識符,而是跟蹤那些已在一個名為reportSet()
的Vector
中報告過的。它將Vector
當作一個“集合”對待,告訴我們一個項目是否已在那個集合中。該項目是通過將文件名和標識符連接起來生成的。若元素不在集合中,就加入它,然後產生報告。
程序列表剩下的部分由main()
構成,它負責控制命令行參數,並判斷我們是準備在標準Java庫的基礎上構建由一系列類名構成的“倉庫”,還是想檢查已寫好的那些代碼的正確性。不管在哪種情況下,都會創建一個ClassScanner
對象。
無論準備構建一個“倉庫”,還是準備使用一個現成的,都必須嘗試打開現有倉庫。通過創建一個File
對象並測試是否存在,就可決定是否打開文件並在ClassScanner
中裝載classes
這個Properties
列表(使用load()
)。來自倉庫的類將追加到由ClassScanner
構造器發現的類後面,而不是將其覆蓋。如果僅提供一個命令行參數,就意味著自己想對類名和標識符名字進行一次檢查。但假如提供兩個參數(第二個是-a
),就表明自己想構成一個類名倉庫。在這種情況下,需要打開一個輸出文件,並用Properties.save()
方法將列表寫入一個文件,同時用一個字符串提供文件頭信息。
第11章介紹了Java 1.1新的“反射”概念,並利用這個概念查詢一個特定類的方法——要麼是由所有方法構成的一個完整列表,要麼是這個列表的一個子集(名字與我們指定的關鍵字相符)。那個例子最大的好處就是能自動顯示出所有方法,不強迫我們在繼承結構中遍歷,檢查每一級的基類。所以,它實際是我們節省編程時間的一個有效工具:因為大多數Java方法的名字都規定得非常全面和詳盡,所以能有效地找出那些包含了一個特殊關鍵字的方法名。若找到符合標準的一個名字,便可根據它直接查閱聯機幫助文檔。
但第11的那個例子也有缺陷,它沒有使用AWT,僅是一個純命令行的應用。在這兒,我們準備製作一個改進的GUI版本,能在我們鍵入字符的時候自動刷新輸出,也允許我們在輸出結果中進行剪切和粘貼操作:
//: DisplayMethods.java
// Display the methods of any class inside
// a window. Dynamically narrows your search.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.lang.reflect.*;
import java.io.*;
public class DisplayMethods extends Applet {
Class cl;
Method[] m;
Constructor[] ctor;
String[] n = new String[0];
TextField
name = new TextField(40),
searchFor = new TextField(30);
Checkbox strip =
new Checkbox("Strip Qualifiers");
TextArea results = new TextArea(40, 65);
public void init() {
strip.setState(true);
name.addTextListener(new NameL());
searchFor.addTextListener(new SearchForL());
strip.addItemListener(new StripL());
Panel
top = new Panel(),
lower = new Panel(),
p = new Panel();
top.add(new Label("Qualified class name:"));
top.add(name);
lower.add(
new Label("String to search for:"));
lower.add(searchFor);
lower.add(strip);
p.setLayout(new BorderLayout());
p.add(top, BorderLayout.NORTH);
p.add(lower, BorderLayout.SOUTH);
setLayout(new BorderLayout());
add(p, BorderLayout.NORTH);
add(results, BorderLayout.CENTER);
}
class NameL implements TextListener {
public void textValueChanged(TextEvent e) {
String nm = name.getText().trim();
if(nm.length() == 0) {
results.setText("No match");
n = new String[0];
return;
}
try {
cl = Class.forName(nm);
} catch (ClassNotFoundException ex) {
results.setText("No match");
return;
}
m = cl.getMethods();
ctor = cl.getConstructors();
// Convert to an array of Strings:
n = new String[m.length + ctor.length];
for(int i = 0; i < m.length; i++)
n[i] = m[i].toString();
for(int i = 0; i < ctor.length; i++)
n[i + m.length] = ctor[i].toString();
reDisplay();
}
}
void reDisplay() {
// Create the result set:
String[] rs = new String[n.length];
String find = searchFor.getText();
int j = 0;
// Select from the list if find exists:
for (int i = 0; i < n.length; i++) {
if(find == null)
rs[j++] = n[i];
else if(n[i].indexOf(find) != -1)
rs[j++] = n[i];
}
results.setText("");
if(strip.getState() == true)
for (int i = 0; i < j; i++)
results.append(
StripQualifiers.strip(rs[i]) + "\n");
else // Leave qualifiers on
for (int i = 0; i < j; i++)
results.append(rs[i] + "\n");
}
class StripL implements ItemListener {
public void itemStateChanged(ItemEvent e) {
reDisplay();
}
}
class SearchForL implements TextListener {
public void textValueChanged(TextEvent e) {
reDisplay();
}
}
public static void main(String[] args) {
DisplayMethods applet = new DisplayMethods();
Frame aFrame = new Frame("Display Methods");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(500,750);
applet.init();
applet.start();
aFrame.setVisible(true);
}
}
class StripQualifiers {
private StreamTokenizer st;
public StripQualifiers(String qualified) {
st = new StreamTokenizer(
new StringReader(qualified));
st.ordinaryChar(' ');
}
public String getNext() {
String s = null;
try {
if(st.nextToken() !=
StreamTokenizer.TT_EOF) {
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = null;
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = new String(st.sval);
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
}
} catch(IOException e) {
System.out.println(e);
}
return s;
}
public static String strip(String qualified) {
StripQualifiers sq =
new StripQualifiers(qualified);
String s = "", si;
while((si = sq.getNext()) != null) {
int lastDot = si.lastIndexOf('.');
if(lastDot != -1)
si = si.substring(lastDot + 1);
s += si;
}
return s;
}
} ///:~
程序中的有些東西已在以前見識過了。和本書的許多GUI程序一樣,這既可作為一個獨立的應用程序使用,亦可作為一個程序片(Applet)使用。此外,StripQualifiers
類與它在第11章的表現是完全一樣的。
GUI包含了一個名為name
的“文本字段”(TextField
),或在其中輸入想查找的類名;還包含了另一個文本字段,名為searchFor
,可選擇性地在其中輸入一定的文字,希望在方法列表中查找那些文字。Checkbox
(複選框)允許我們指出最終希望在輸出中使用完整的名字,還是將前面的各種限定信息刪去。最後,結果顯示於一個“文本區域”(TextArea
)中。
大家會注意到這個程序未使用任何按鈕或其他組件,不能用它們開始一次搜索。這是由於無論文本字段還是複選框都會受到它們的“監聽者(Listener
)對象的監視。只要作出一項改變,結果列表便會立即更新。若改變了name
字段中的文字,新的文字就會在NameL
類中捕獲。若文字不為空,則在Class.forName()
中用於嘗試查找類。當然,在文字鍵入期間,名字可能會變得不完整,而Class.forName()
會失敗,這意味著它會“拋”出一個異常。該異常會被捕獲,TextArea
會隨之設為Nomatch
(不相符)。但只要鍵入了一個正確的名字(大小寫也算在內),Class.forName()
就會成功,而getMethods()
和getConstructors()
會分別返回由Method
和Constructor
對象構成的一個數組。這些數組中的每個對象都會通過toString()
轉變成一個字符串(這樣便產生了完整的方法或構造器簽名),而且兩個列表都會合併到n
中——一個獨立的字符串數組。數組n
屬於DisplayMethods
類的一名成員,並在調用reDisplay()
時用於顯示的更新。
若改變了Checkbox
或searchFor
組件,它們的“監聽者”會簡單地調用reDisplay()
。reDisplay()
會創建一個臨時數組,其中包含了名為rs
的字符串(rs
代表“結果集”——Result Set
)。結果集要麼直接從n
複製(沒有find
關鍵字),要麼選擇性地從包含了find
關鍵字的n
中的字符串複製。最後會檢查strip Checkbox
,看看用戶是不是希望將名字中多餘的部分刪除(默認為“是”)。若答案是肯定的,則用StripQualifiers.strip()
做這件事情;反之,就將列表簡單地顯示出來。
在init()
中,大家也許認為在設置佈局時需要進行大量繁重的工作。事實上,組件的佈置完全可能只需要極少的工作。但象這樣使用BorderLayout
的好處是它允許用戶改變窗口的大小,並特別能使TextArea
(文本區域)更大一些,這意味著我們可以改變大小,以便毋需滾動即可看到更長的名字。
編程時,大家會發現特別有必要讓這個工具處於運行狀態,因為在試圖判斷要調用什麼方法的時候,它提供了最好的方法之一。
下面要介紹的程序的前身是由Larry O'Brien原創的一些代碼,並以由Craig Reynolds於1986年編制的“Boids”程序為基礎,當時是為了演示覆雜性理論的一個特殊問題,名為“凸顯”(Emergence)。
這兒要達到的目標是通過為每種動物都規定少許簡單的規則,從而逼真地再現動物的群聚行為。每個動物都能看到看到整個環境以及環境中的其他動物,但它只與一系列附近的“群聚夥伴”打交道。動物的移動基於三個簡單的引導行為:
(1) 分隔:避免本地群聚夥伴過於擁擠。
(2) 方向:遵從本地群聚夥伴的普遍方向。
(3) 聚合:朝本地群聚夥伴組的中心移動。
更復雜的模型甚至可以包括障礙物的因素,動物能預知和避免與障礙衝突的能力,所以它們能圍繞環境中的固定物體自由活動。除此以外,動物也可能有自己的特殊目標,這也許會造成群體按特定的路徑前進。為簡化討論,避免障礙以及目標搜尋的因素並未包括到這裡建立的模型中。
儘管計算機本身比較簡陋,而且採用的規則也相當簡單,但結果看起來是真實的。也就是說,相當逼真的行為從這個簡單的模型中“凸顯”出來了。
程序以組合到一起的應用程序/程序片的形式提供:
//: FieldOBeasts.java
// Demonstration of complexity theory; simulates
// herding behavior in animals. Adapted from
// a program by Larry O'Brien lobrien@msn.com
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;
class Beast {
int
x, y, // Screen position
currentSpeed; // Pixels per second
float currentDirection; // Radians
Color color; // Fill color
FieldOBeasts field; // Where the Beast roams
static final int GSIZE = 10; // Graphic size
public Beast(FieldOBeasts f, int x, int y,
float cD, int cS, Color c) {
field = f;
this.x = x;
this.y = y;
currentDirection = cD;
currentSpeed = cS;
color = c;
}
public void step() {
// You move based on those within your sight:
Vector seen = field.beastListInSector(this);
// If you're not out in front
if(seen.size() > 0) {
// Gather data on those you see
int totalSpeed = 0;
float totalBearing = 0.0f;
float distanceToNearest = 100000.0f;
Beast nearestBeast =
(Beast)seen.elementAt(0);
Enumeration e = seen.elements();
while(e.hasMoreElements()) {
Beast aBeast = (Beast) e.nextElement();
totalSpeed += aBeast.currentSpeed;
float bearing =
aBeast.bearingFromPointAlongAxis(
x, y, currentDirection);
totalBearing += bearing;
float distanceToBeast =
aBeast.distanceFromPoint(x, y);
if(distanceToBeast < distanceToNearest) {
nearestBeast = aBeast;
distanceToNearest = distanceToBeast;
}
}
// Rule 1: Match average speed of those
// in the list:
currentSpeed = totalSpeed / seen.size();
// Rule 2: Move towards the perceived
// center of gravity of the herd:
currentDirection =
totalBearing / seen.size();
// Rule 3: Maintain a minimum distance
// from those around you:
if(distanceToNearest <=
field.minimumDistance) {
currentDirection =
nearestBeast.currentDirection;
currentSpeed = nearestBeast.currentSpeed;
if(currentSpeed > field.maxSpeed) {
currentSpeed = field.maxSpeed;
}
}
}
else { // You are in front, so slow down
currentSpeed =
(int)(currentSpeed * field.decayRate);
}
// Make the beast move:
x += (int)(Math.cos(currentDirection)
* currentSpeed);
y += (int)(Math.sin(currentDirection)
* currentSpeed);
x %= field.xExtent;
y %= field.yExtent;
if(x < 0)
x += field.xExtent;
if(y < 0)
y += field.yExtent;
}
public float bearingFromPointAlongAxis (
int originX, int originY, float axis) {
// Returns bearing angle of the current Beast
// in the world coordiante system
try {
double bearingInRadians =
Math.atan(
(this.y - originY) /
(this.x - originX));
// Inverse tan has two solutions, so you
// have to correct for other quarters:
if(x < originX) {
if(y < originY) {
bearingInRadians += - (float)Math.PI;
}
else {
bearingInRadians =
(float)Math.PI - bearingInRadians;
}
}
// Just subtract the axis (in radians):
return (float) (axis - bearingInRadians);
} catch(ArithmeticException aE) {
// Divide by 0 error possible on this
if(x > originX) {
return 0;
}
else
return (float) Math.PI;
}
}
public float distanceFromPoint(int x1, int y1){
return (float) Math.sqrt(
Math.pow(x1 - x, 2) +
Math.pow(y1 - y, 2));
}
public Point position() {
return new Point(x, y);
}
// Beasts know how to draw themselves:
public void draw(Graphics g) {
g.setColor(color);
int directionInDegrees = (int)(
(currentDirection * 360) / (2 * Math.PI));
int startAngle = directionInDegrees -
FieldOBeasts.halfFieldOfView;
int endAngle = 90;
g.fillArc(x, y, GSIZE, GSIZE,
startAngle, endAngle);
}
}
public class FieldOBeasts extends Applet
implements Runnable {
private Vector beasts;
static float
fieldOfView =
(float) (Math.PI / 4), // In radians
// Deceleration % per second:
decayRate = 1.0f,
minimumDistance = 10f; // In pixels
static int
halfFieldOfView = (int)(
(fieldOfView * 360) / (2 * Math.PI)),
xExtent = 0,
yExtent = 0,
numBeasts = 50,
maxSpeed = 20; // Pixels/second
boolean uniqueColors = true;
Thread thisThread;
int delay = 25;
public void init() {
if (xExtent == 0 && yExtent == 0) {
xExtent = Integer.parseInt(
getParameter("xExtent"));
yExtent = Integer.parseInt(
getParameter("yExtent"));
}
beasts =
makeBeastVector(numBeasts, uniqueColors);
// Now start the beasts a-rovin':
thisThread = new Thread(this);
thisThread.start();
}
public void run() {
while(true) {
for(int i = 0; i < beasts.size(); i++){
Beast b = (Beast) beasts.elementAt(i);
b.step();
}
try {
thisThread.sleep(delay);
} catch(InterruptedException ex){}
repaint(); // Otherwise it won't update
}
}
Vector makeBeastVector(
int quantity, boolean uniqueColors) {
Vector newBeasts = new Vector();
Random generator = new Random();
// Used only if uniqueColors is on:
double cubeRootOfBeastNumber =
Math.pow((double)numBeasts, 1.0 / 3.0);
float colorCubeStepSize =
(float) (1.0 / cubeRootOfBeastNumber);
float r = 0.0f;
float g = 0.0f;
float b = 0.0f;
for(int i = 0; i < quantity; i++) {
int x =
(int) (generator.nextFloat() * xExtent);
if(x > xExtent - Beast.GSIZE)
x -= Beast.GSIZE;
int y =
(int) (generator.nextFloat() * yExtent);
if(y > yExtent - Beast.GSIZE)
y -= Beast.GSIZE;
float direction = (float)(
generator.nextFloat() * 2 * Math.PI);
int speed = (int)(
generator.nextFloat() * (float)maxSpeed);
if(uniqueColors) {
r += colorCubeStepSize;
if(r > 1.0) {
r -= 1.0f;
g += colorCubeStepSize;
if( g > 1.0) {
g -= 1.0f;
b += colorCubeStepSize;
if(b > 1.0)
b -= 1.0f;
}
}
}
newBeasts.addElement(
new Beast(this, x, y, direction, speed,
new Color(r,g,b)));
}
return newBeasts;
}
public Vector beastListInSector(Beast viewer) {
Vector output = new Vector();
Enumeration e = beasts.elements();
Beast aBeast = (Beast)beasts.elementAt(0);
int counter = 0;
while(e.hasMoreElements()) {
aBeast = (Beast) e.nextElement();
if(aBeast != viewer) {
Point p = aBeast.position();
Point v = viewer.position();
float bearing =
aBeast.bearingFromPointAlongAxis(
v.x, v.y, viewer.currentDirection);
if(Math.abs(bearing) < fieldOfView / 2)
output.addElement(aBeast);
}
}
return output;
}
public void paint(Graphics g) {
Enumeration e = beasts.elements();
while(e.hasMoreElements()) {
((Beast)e.nextElement()).draw(g);
}
}
public static void main(String[] args) {
FieldOBeasts field = new FieldOBeasts();
field.xExtent = 640;
field.yExtent = 480;
Frame frame = new Frame("Field 'O Beasts");
// Optionally use a command-line argument
// for the sleep time:
if(args.length >= 1)
field.delay = Integer.parseInt(args[0]);
frame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.add(field, BorderLayout.CENTER);
frame.setSize(640,480);
field.init();
field.start();
frame.setVisible(true);
}
} ///:~
儘管這並非對Craig Reynold的“Boids”例子中的行為完美重現,但它卻展現出了自己獨有的迷人之外。通過對數字進行調整,即可進行全面的修改。至於與這種群聚行為有關的更多的情況,大家可以訪問Craig Reynold的主頁——在那個地方,甚至還提供了Boids一個公開的3D展示版本:
http://www.hmt.com/cwr/boids.html
為了將這個程序作為一個程序片運行,請在HTML文件中設置下述程序片標誌:
<applet
code=FieldOBeasts
width=640
height=480>
<param name=xExtent value = "640">
<param name=yExtent value = "480">
</applet>
通過本章的學習,大家知道運用Java可做到一些較複雜的事情。通過這些例子亦可看出,儘管Java必定有自己的侷限,但受那些侷限影響的主要是性能(比如寫好文字處理程序後,會發現C++的版本要快得多——這部分是由於IO庫做得不完善造成的;而在你讀到本書的時候,情況也許已發生了變化。但Java的侷限也僅此而已,它在語言表達方面的能力是無以倫比的。利用Java,幾乎可以表達出我們想得到的任何事情。而與此同時,Java在表達的方便性和易讀性上,也做足了功夫。所以在使用Java時,一般不會陷入其他語言常見的那種複雜境地。使用那些語言時,會感覺它們象一個愛嘮叨的老太婆,哪有Java那樣清純、簡練!而且通過Java 1.2的JFC/Swing庫,AWT的表達能力和易用性甚至又得到了進一步的增強。
(1) (稍微有些難度)改寫FieldOBeasts.java
,使它的狀態能夠保持固定。加上一些按鈕,允許用戶保存和恢復不同的狀態文件,並從它們斷掉的地方開始繼續運行。請先參考第10章的CADState.java
,再決定具體怎樣做。
(2) (大作業)以FieldOBeasts.java
作為起點,構造一個自動化交通仿真系統。
(3) (大作業)以ClassScanner.java
作為起點,構造一個特殊的工具,用它找出那些雖然定義但從未用過的方法和字段。
(4) (大作業)利用JDBC,構造一個聯絡管理程序。讓這個程序以一個平面文件數據庫為基礎,其中包含了名字、地址、電話號碼、E-mail地址等聯繫資料。應該能向數據庫裡方便地加入新名字。鍵入要查找的名字時,請採用在第15章的VLookup.java
裡介紹過的那種名字自動填充技術。