Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
1339 lines (1218 loc) · 65 KB

第17章.md

File metadata and controls

1339 lines (1218 loc) · 65 KB

第17章 項目

本章包含了一系列項目,它們都以本書介紹的內容為基礎,並對早期的章節進行了一定程度的擴充。

與以前經歷過的項目相比,這兒的大多數項目都明顯要複雜得多,它們充分演示了新技術以及類庫的運用。

17.1 文字處理

如果您有C或C++的經驗,那麼最開始可能會對Java控制文本的能力感到懷疑。事實上,我們最害怕的就是速度特別慢,這可能妨礙我們創造能力的發揮。然而,Java對應的工具(特別是String類)具有很強的功能,就象本節的例子展示的那樣(而且性能也有一定程度的提升)。

正如大家即將看到的那樣,建立這些例子的目的都是為瞭解決本書編制過程中遇到的一些問題。但是,它們的能力並非僅止於此。通過簡單的改造,即可讓它們在其他場合大顯身手。除此以外,它們還揭示出了本書以前沒有強調過的一項Java特性。

17.1.1 提取代碼列表

對於本書每一個完整的代碼列表(不是代碼段),大家無疑會注意到它們都用特殊的註釋記號起始與結束(//:///:~)。之所以要包括這種標誌信息,是為了能將代碼從本書自動提取到兼容的源碼文件中。在我的前一本書裡,我設計了一個系統,可將測試過的代碼文件自動合併到書中。但對於這本書,我發現一種更簡便的做法是一旦通過了最初的測試,就把代碼粘貼到書中。而且由於很難第一次就編譯通過,所以我在書的內部編輯代碼。但如何提取並測試代碼呢?這個程序就是關鍵。如果你打算解決一個文字處理的問題,那麼它也很有利用價值。該例也演示了String類的許多特性。

我首先將整本書都以ASCII文本格式保存成一個獨立的文件。CodePackager程序有兩種運行模式(在usageString有相應的描述):如果使用-p標誌,程序就會檢查一個包含了ASCII文本(即本書的內容)的一個輸入文件。它會遍歷這個文件,按照註釋記號提取出代碼,並用位於第一行的文件名來決定創建文件使用什麼名字。除此以外,在需要將文件置入一個特殊目錄的時候,它還會檢查package語句(根據由package語句指定的路徑選擇)。

但這樣還不夠。程序還要對包(package)名進行跟蹤,從而監視章內發生的變化。由於每一章使用的所有包都以c02c03c04等等起頭,用於標記它們所屬的是哪一章(除那些以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()會返回一個錯誤)。

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對象。然而僅需一次方法調用,我們即可把它保存到磁盤,或者從磁盤中恢復。實際上,我們只需要一個名字列表,所以為鍵和值都使用了相同的對象。

針對特定目錄中的文件,為找出相應的類與標識符,我們使用了兩個MultiStringMapclassMap以及identMap。此外在程序啟動的時候,它會將標準類名倉庫裝載到名為classesProperties對象中。一旦在本地目錄發現了一個新類名,也會將其加入classes以及classMap。這樣一來,classMap就可用於在本地目錄的所有類間遍歷,而且可用classes檢查當前標記是不是一個類名(它標記著對象或方法定義的開始,所以收集接下去的記號——直到碰到一個分號——並將它們都置入identMap)。

ClassScanner的默認構造器會創建一個由文件名構成的列表(採用FilenameFilterJavaFilter實現形式,參見第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(接口),那麼接著的記號就應當代表一個類或接口名字,並將其置入classesclassMap。若單詞是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()方法將列表寫入一個文件,同時用一個字符串提供文件頭信息。

17.2 方法查找工具

第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()會分別返回由MethodConstructor對象構成的一個數組。這些數組中的每個對象都會通過toString()轉變成一個字符串(這樣便產生了完整的方法或構造器簽名),而且兩個列表都會合併到n中——一個獨立的字符串數組。數組n屬於DisplayMethods類的一名成員,並在調用reDisplay()時用於顯示的更新。

若改變了CheckboxsearchFor組件,它們的“監聽者”會簡單地調用reDisplay()reDisplay()會創建一個臨時數組,其中包含了名為rs的字符串(rs代表“結果集”——Result Set)。結果集要麼直接從n複製(沒有find關鍵字),要麼選擇性地從包含了find關鍵字的n中的字符串複製。最後會檢查strip Checkbox,看看用戶是不是希望將名字中多餘的部分刪除(默認為“是”)。若答案是肯定的,則用StripQualifiers.strip()做這件事情;反之,就將列表簡單地顯示出來。

init()中,大家也許認為在設置佈局時需要進行大量繁重的工作。事實上,組件的佈置完全可能只需要極少的工作。但象這樣使用BorderLayout的好處是它允許用戶改變窗口的大小,並特別能使TextArea(文本區域)更大一些,這意味著我們可以改變大小,以便毋需滾動即可看到更長的名字。

編程時,大家會發現特別有必要讓這個工具處於運行狀態,因為在試圖判斷要調用什麼方法的時候,它提供了最好的方法之一。

17.3 複雜性理論

下面要介紹的程序的前身是由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>

17.4 總結

通過本章的學習,大家知道運用Java可做到一些較複雜的事情。通過這些例子亦可看出,儘管Java必定有自己的侷限,但受那些侷限影響的主要是性能(比如寫好文字處理程序後,會發現C++的版本要快得多——這部分是由於IO庫做得不完善造成的;而在你讀到本書的時候,情況也許已發生了變化。但Java的侷限也僅此而已,它在語言表達方面的能力是無以倫比的。利用Java,幾乎可以表達出我們想得到的任何事情。而與此同時,Java在表達的方便性和易讀性上,也做足了功夫。所以在使用Java時,一般不會陷入其他語言常見的那種複雜境地。使用那些語言時,會感覺它們象一個愛嘮叨的老太婆,哪有Java那樣清純、簡練!而且通過Java 1.2的JFC/Swing庫,AWT的表達能力和易用性甚至又得到了進一步的增強。

17.5 練習

(1) (稍微有些難度)改寫FieldOBeasts.java,使它的狀態能夠保持固定。加上一些按鈕,允許用戶保存和恢復不同的狀態文件,並從它們斷掉的地方開始繼續運行。請先參考第10章的CADState.java,再決定具體怎樣做。

(2) (大作業)以FieldOBeasts.java作為起點,構造一個自動化交通仿真系統。

(3) (大作業)以ClassScanner.java作為起點,構造一個特殊的工具,用它找出那些雖然定義但從未用過的方法和字段。

(4) (大作業)利用JDBC,構造一個聯絡管理程序。讓這個程序以一個平面文件數據庫為基礎,其中包含了名字、地址、電話號碼、E-mail地址等聯繫資料。應該能向數據庫裡方便地加入新名字。鍵入要查找的名字時,請採用在第15章的VLookup.java裡介紹過的那種名字自動填充技術。