1. <bdo id="8zfej"></bdo>
    <li id="8zfej"><meter id="8zfej"><th id="8zfej"></th></meter></li>

    南京北大青鳥

    全國咨詢電話:15195455103

    三分鐘了解北大青鳥
    當前位置:南京北大青鳥 > 學習園地 > 編程技巧

    關于Java常用工具您不知道的5件事

    來源:北大青鳥徐州校區? ? ? 作者:IT教育 ? ??

    在千禧年左右,當 XML 第一次出現在很多 Java 開發人員面前時,有兩種基本的解析 XML 文件的方法。SAX 解析器實際是由程序員對事件調用一系列回調方法的大型狀態機。DOM 解析器將整個

    StAX – 解析XML文件
     在千禧年左右,當 XML 第一次出現在很多 Java 開發人員面前時,有兩種基本的解析 XML 文件的方法。SAX 解析器實際是由程序員對事件調用一系列回調方法的大型狀態機。DOM 解析器將整個 XML 文檔加入內存,并切割成離散的對象,它們連接在一起形成一個樹。該樹描述了文檔的整個 XML Infoset 表示法。這兩個解析器都有缺點:SAX 太低級,無法使用,DOM 代價太大,尤其對于大的 XML 文件 — 整個樹成了一個龐然大物。
     幸運的是,Java 開發人員找到第三種方法來解析 XML 文件,通過對文檔建模成 “節點”,它們可以從文檔流中一次取出一個,檢查,然后處理或丟棄。這些 “節點” 的 “流” 提供了 SAX 和 DOM 的中間地帶,名為 “Streaming API for XML”,或者叫做StAX。(此縮寫用于區分新的 API 與原來的 SAX 解析器,它與此同名。)StAX 解析器后來包裝到了 JDK 中,在 javax.xml.stream 包。
     使用 StAX 相當簡單:實例化 XMLEventReader,將它指向一個格式良好的 XML 文件,然后一次 “拉出” 一個節點(通常用 while 循環),查看。例如,在清單 1 中,列舉出了 Ant 構造腳本中的所有目標:
    清單 1. 只是讓 StAX 指向目標
    import java.io.*;
    import javax.xml.namespace.QName;
    import javax.xml.stream.*;
    import javax.xml.stream.events.*;
    import javax.xml.stream.util.*;

    public class Targets
    {
        public static void main(String[] args)
            throws Exception
        {
            for (String arg : args)
            {
                XMLEventReader xsr =
                    XMLInputFactory.newInstance()
                        .createXMLEventReader(new FileReader(arg));
                while (xsr.hasNext())
                {
                    XMLEvent evt = xsr.nextEvent();
                    switch (evt.getEventType())
                    {
                        case XMLEvent.START_ELEMENT:
                        {
                            StartElement se = evt.asStartElement();
                            if (se.getName().getLocalPart().equals("target"))
                            {
                                Attribute targetName =
                                    se.getAttributeByName(new QName("name"));
                                // Found a target!
                                System.out.println(targetName.getValue());
                            }
                            break;
                        }
                        // Ignore everything else
                    }
                }
            }
        }
    }
     
     StAX 解析器不會替換所有的 SAX 和 DOM 代碼。但肯定會讓某些任務容易些。尤其對完成不需要知道 XML 文檔整個樹結構的任務相當方便。
     請注意,如果事件對象級別太高,無法使用,StAX 也有一個低級 API 在 XMLStreamReader 中。盡管也許沒有閱讀器有用,StAX 還有一個 XMLEventWriter,同樣,還有一個 XMLStreamWriter 類用于 XML 輸出。
    ServiceLoader – 加載服務(獲取接口的所有實現)
     Java 開發人員經常希望將使用和創建組件的內容區分開來。這通常是通過創建一個描述組件動作的接口,并使用某種中介創建組件實例來完成的。很多開發人員使用 Spring 框架來完成,但還有其他的方法,它比 Spring 容器更輕量級。
     java.util 的 ServiceLoader 類能讀取隱藏在 JAR 文件中的配置文件,并找到接口的實現,然后使這些實現成為可選擇的列表。例如,如果您需要一個私仆(personal-servant)組件來完成任務,您可以使用清單 2 中的代碼來實現:
    清單 2. IPersonalServant
    public interface IPersonalServant
    {
        // Process a file of commands to the servant
        public void process(java.io.File f) throws java.io.IOException;
        public boolean can(String command);
    }
     
     can() 方法可讓您確定所提供的私仆實現是否滿足需求。清單 3 中的 ServiceLoader 的 IPersonalServant 列表基本上滿足需求:
    清單 3. IPersonalServant 行嗎?
    import java.io.*;
    import java.util.*;

    public class Servant
    {
        public static void main(String[] args)
            throws IOException
        {
            ServiceLoader<IPersonalServant> servantLoader =
                ServiceLoader.load(IPersonalServant.class);

            IPersonalServant i = null;
            for (IPersonalServant ii : servantLoader)
                if (ii.can("fetch tea"))
                    i = ii;

            if (i == null)
                throw new IllegalArgumentException("No suitable servant found");
           
            for (String arg : args)
            {
                i.process(new File(arg));
            }
        }
    }
     
     假設有此接口的實現,如清單 4:
    清單 4. Jeeves 實現了 IPersonalServant
    import java.io.*;

    public class Jeeves
        implements IPersonalServant
    {
        public void process(File f)
        {
            System.out.println("Very good, sir.");
        }
        public boolean can(String cmd)
        {
            if (cmd.equals("fetch tea"))
                return true;
            else
                return false;
        }
    }
     
     剩下的就是配置包含實現的 JAR 文件,讓 ServiceLoader 能識別 — 這可能會非常棘手。JDK 想要 JAR 文件有一個 META-INF/services 目錄,它包含一個文本文件,其文件名與接口類名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口類名的內容是實現的名稱,每行一個,如清單 5:
    清單 5. META-INF/services/IPersonalServant
    Jeeves   # comments are OK
     
     幸運的是,Ant 構建系統(自 1.7.0 以來)包含一個對 jar 任務的服務標簽,讓這相對容易,見清單 6:
    清單 6. Ant 構建的 IPersonalServant
        <target name="serviceloader" depends="build">
            <jar destfile="misc.jar" basedir="./classes">
                <service type="IPersonalServant">
                    <provider classname="Jeeves" />
                </service>
            </jar>
        </target>
     
     這里,很容易調用 IPersonalServant,讓它執行命令。然而,解析和執行這些命令可能會非常棘手。這又是另一個 “小線頭”。
    Scanner
     有無數 Java 工具能幫助您構建解析器,很多函數語言已成功構建解析器函數庫(解析器選擇器)。但如果要解析的是逗號分隔值文件,或空格分隔文本文件,又怎么辦呢?大多數工具用在此處就過于隆重了,而 String.split() 又不夠。(對于正則表達式,請記住一句老話:“ 您有一個問題,用正則表達式解決。那您就有兩個問題了。”)
     Java 平臺的 Scanner 類會是這些類中您好的選擇。以輕量級文本解析器為目標,Scanner 提供了一個相對簡單的 API,用于提取結構化文本,并放入強類型的部分。想象一下,如果您愿意,一組類似 DSL 的命令(源自 Terry Pratchett Discworld 小說)排列在文本文件中,如清單 7:
    清單 7. Igor 的任務
    fetch 1 head
    fetch 3 eye
    fetch 1 foot
    attach foot to head
    attach eye to head
    admire
     您,或者是本例中稱為 Igor的私仆,能輕松使用 Scanner 解析這組違法命令,如清單 8 所示:
    清單 8. Igor 的任務,由 Scanner 解析
    import java.io.*;
    import java.util.*;

    public class Igor
        implements IPersonalServant
    {
        public boolean can(String cmd)
        {
            if (cmd.equals("fetch body parts"))
                return true;
            if (cmd.equals("attach body parts"))
                return true;
            else
                return false;
        }
        public void process(File commandFile)
            throws FileNotFoundException
        {
            Scanner scanner = new Scanner(commandFile);
            // Commands come in a verb/number/noun or verb form
            while (scanner.hasNext())
            {
                String verb = scanner.next();
                if (verb.equals("fetch"))
                {
                    int num = scanner.nextInt();
                    String type = scanner.next();
                    fetch (num, type);
                }
                else if (verb.equals("attach"))
                {
                    String item = scanner.next();
                    String to = scanner.next();
                    String target = scanner.next();
                    attach(item, target);
                }
                else if (verb.equals("admire"))
                {
                    admire();
                }
                else
                {
                    System.out.println("I don't know how to "
                        + verb + ", marthter.");
                }
            }
        }
       
        public void fetch(int number, String type)
        {
            if (parts.get(type) == null)
            {
                System.out.println("Fetching " + number + " "
                    + type + (number > 1 ? "s" : "") + ", marthter!");
                parts.put(type, number);
            }
            else
            {
                System.out.println("Fetching " + number + " more "
                    + type + (number > 1 ? "s" : "") + ", marthter!");
                Integer currentTotal = parts.get(type);
                parts.put(type, currentTotal + number);
            }
            System.out.println("We now have " + parts.toString());
        }
       
        public void attach(String item, String target)
        {
            System.out.println("Attaching the " + item + " to the " +
                target + ", marthter!");
        }
       
        public void admire()
        {
            System.out.println("It'th quite the creathion, marthter");
        }
       
        private Map<String, Integer> parts = new HashMap<String, Integer>();
    }
     假設 Igor 已在 ServantLoader 中注冊,可以很方便地將 can() 調用改得更實用,并重用前面的 Servant 代碼,如清單 9 所示:
    清單 9. Igor 做了什么
    import java.io.*;
    import java.util.*;

    public class Servant
    {
        public static void main(String[] args)
            throws IOException
        {
            ServiceLoader<IPersonalServant> servantLoader =
                ServiceLoader.load(IPersonalServant.class);

            IPersonalServant i = null;
            for (IPersonalServant ii : servantLoader)
                if (ii.can("fetch body parts"))
                    i = ii;

            if (i == null)
                throw new IllegalArgumentException("No suitable servant found");
           
            for (String arg : args)
            {
                i.process(new File(arg));
            }
        }
    }
     真正 DSL 實現顯然不會僅僅打印到標準輸出流。我把追蹤哪些部分、跟隨哪些部分的細節留待給您(當然,還有忠誠的 Igor)。
    Timer
     java.util.Timer 和 TimerTask 類提供了方便、相對簡單的方法可在定期或一次性延遲的基礎上執行任務:
    清單 10. 稍后執行
    import java.util.*;

    public class Later
    {
        public static void main(String[] args)
        {
            Timer t = new Timer("TimerThread");
            t.schedule(new TimerTask() {
                public void run() {
                    System.out.println("This is later");
                    System.exit(0);
                }
            }, 1 * 1000);
            System.out.println("Exiting main()");
        }
    }
     Timer 有許多 schedule() 重載,它們提示某一任務是一次性還是重復的,并且有一個啟動的 TimerTask 實例。TimerTask 實際上是一個 Runnable(事實上,它實現了它),但還有另外兩個方法:cancel() 用來取消任務,scheduledExecutionTime() 用來返回任務何時啟動的近似值。
     請注意 Timer 卻創建了一個非守護線程在后臺啟動任務,因此在清單 10 中我需要調用 System.exit() 來取消任務。在長時間運行的程序中,好創建一個 Timer 守護線程(使用帶有指示守護線程狀態的參數的構造函數),從而它不會讓 VM 活動。
     這個類沒什么神奇的,但它確實能幫助我們對后臺啟動的程序的目的了解得更清楚。它還能節省一些 Thread 代碼,并作為輕量級 ScheduledExecutorService(對于還沒準備好了解整個 java.util.concurrent 包的人來說)。
    JavaSound
     盡管在服務器端應用程序中不常出現,但 sound 對管理員有著有用的 “被動” 意義 — 它是惡作劇的好材料。盡管它很晚才出現在 Java 平臺中,JavaSound API 終還是加入了核心運行時庫,封裝在 javax.sound * 包 — 其中一個包是 MIDI 文件,另一個是音頻文件示例(如普遍的 .WAV 文件格式)。
     JavaSound 的 “hello world” 是播放一個片段,如清單 11 所示:
    清單 11. 再放一遍,Sam
    public static void playClip(String audioFile)
    {
        try
        {
            AudioInputStream inputStream =
                AudioSystem.getAudioInputStream(
                    this.getClass().getResourceAsStream(audioFile));
            DataLine.Info info =
                new DataLine.Info( Clip.class, audioInputStream.getFormat() );
            Clip clip = (Clip) AudioSystem.getLine(info);
            clip.addLineListener(new LineListener() {
                    public void update(LineEvent e) {
                        if (e.getType() == LineEvent.Type.STOP) {
                            synchronized(clip) {
                                clip.notify();
                            }
                        }
                    }
                });
            clip.open(audioInputStream);       
           
            clip.setFramePosition(0);

            clip.start();
            synchronized (clip) {
                clip.wait();
            }
            clip.drain();
            clip.close();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

     大多數還是相當簡單(至少 JavaSound 一樣簡單)。第一步是創建一個文件的 AudioInputStream 來播放。為了讓此方法盡量與上下文無關,我們從加載類的 ClassLoader 中抓取文件作為 InputStream。(AudioSystem 還需要一個 File 或 String,如果提前知道聲音文件的具體路徑。)一旦完成, DataLine.Info 對象就提供給 AudioSystem,得到一個 Clip,這是播放音頻片段簡單的方法。(其他方法提供了對片段更多的控制 — 例如獲取一個 SourceDataLine — 但對于 “播放” 來說,過于復雜)。
     這里應該和對 AudioInputStream 調用 open() 一樣簡單。(“應該” 的意思是如果您沒遇到下節描述的錯誤。)調用 start() 開始播放,drain() 等待播放完成,close() 釋放音頻線路。播放是在單獨的線程進行,因此調用 stop() 將會停止播放,然后調用 start() 將會從播放暫停的地方重新開始;使用 setFramePosition(0) 重新定位到開始。
     沒聲音?
     JDK 5 發行版中有個討厭的小錯誤:在有些平臺上,對于一些短的音頻片段,代碼看上去運行正常,但就是 ... 沒聲音。顯然媒體播放器在應該出現的位置之前觸發了 STOP 事件。
     這個錯誤 “無法修復”,但解決方法相當簡單:注冊一個 LineListener 來監聽 STOP 事件,當觸發時,調用片段對象的 notifyAll()。然后在 “調用者” 代碼中,通過調用 wait() 等待片段完成(還調用 notifyAll())。在沒出現錯誤的平臺上,這些錯誤是多余的,在 Windows® 及有些 Linux® 版本上,會讓程序員 “開心” 或 “憤怒”。
    使用 Java 語言進行 Unicode 代理編程
     原文地址:http://www.ibm.com/developerworks/cn/java/j-unicode/index.html#
     早期 Java 版本使用 16 位 char 數據類型表示 Unicode 字符。這種設計方法有時比較合理,因為所有 Unicode 字符擁有的值都小于 65,535 (0xFFFF),可以通過 16 位表示。但是,Unicode 后來將大值增加到 1,114,111 (0x10FFFF)。由于 16 位太小,不能表示 Unicode version 3.1 中的所有 Unicode 字符,32 位值 — 稱為碼位(code point) — 被用于 UTF-32 編碼模式。
     但與 32 位值相比,16 位值的內存使用效率更高,因此 Unicode 引入了一個種新設計方法來允許繼續使用 16 位值。UTF-16 中采用的這種設計方法分配 1,024 值給 16 位高代理(high surrogate),將另外的 1,024 值分配給 16 位低代理(low surrogate)。它使用一個高代理加上一個低代理 — 一個代理對(surrogate pair) — 來表示 65,536 (0x10000) 和 1,114,111 (0x10FFFF) 之間的 1,048,576 (0x100000) 值(1,024 和 1,024 的乘積)。
     Java 1.5 保留了 char 類型的行為來表示 UTF-16 值(以便兼容現有程序),它實現了碼位的概念來表示 UTF-32 值。這個擴展(根據 JSR 204:Unicode Supplementary Character Support 實現)不需要記住 Unicode 碼位或轉換算法的準確值 — 但理解代理 API 的正確用法很重要。
     東亞國家和地區近年來增加了它們的字符集中的字符數量,以滿足用戶需求。這些標準包括來自中國的國家標準組織的 GB 18030 和來自日本的 JIS X 0213。因此,尋求遵守這些標準的程序更有必要支持 Unicode 代理對。本文解釋相關 Java API 和編碼選項,面向計劃重新設計他們的軟件,從只能使用 char 類型的字符轉換為能夠處理代理對的新版本的讀者。
    順序訪問
     順序訪問是在 Java 語言中處理字符串的一個基本操作。在這種方法下,輸入字符串中的每個字符從頭至尾按順序訪問,或者有時從尾至頭訪問。本小節討論使用順序訪問方法從一個字符串創建一個 32 位碼位數組的 7 個技術示例,并估計它們的處理時間。
     示例 1-1:基準測試(不支持代理對)
     清單 1 將 16 位 char 類型值直接分配給 32 位碼位值,完全沒有考慮代理對:
    清單 1. 不支持代理對
    int[] toCodePointArray(String str) { // Example 1-1
        int len = str.length();          // the length of str
        int[] acp = new int[len];        // an array of code points

        for (int i = 0, j = 0; i < len; i++) {
            acp[j++] = str.charAt(i);
        }
        return acp;
    }

     盡管這個示例不支持代理對,但它提供了一個處理時間基準來比較后續順序訪問示例。
     示例 1-2:使用 isSurrogatePair()
     清單 2 使用 isSurrogatePair() 來計算代理對總數。計數之后,它分配足夠的內存以便一個碼位數組存儲這個值。然后,它進入一個順序訪問循環,使用 isHighSurrogate() 和 isLowSurrogate() 確定每個代理對字符是高代理還是低代理。當它發現一個高代理后面帶一個低代理時,它使用 toCodePoint() 將該代理對轉換為一個碼位值并將當前索引值增加 2。否則,它將這個 char 類型值直接分配給一個碼位值并將當前索引值增加 1。這個示例的處理時間比 示例 1-1 長 1.38 倍。
    清單 2. 有限支持
    int[] toCodePointArray(String str) { // Example 1-2
        int len = str.length();          // the length of str
        int[] acp;                       // an array of code points
        int surrogatePairCount = 0;      // the count of surrogate pairs

        for (int i = 1; i < len; i++) {
            if (Character.isSurrogatePair(str.charAt(i - 1), str.charAt(i))) {
                surrogatePairCount++;
                i++;
            }
        }
        acp = new int[len - surrogatePairCount];
        for (int i = 0, j = 0; i < len; i++) {
            char ch0 = str.charAt(i);         // the current char
            if (Character.isHighSurrogate(ch0) && i + 1 < len) {
                char ch1 = str.charAt(i + 1); // the next char
                if (Character.isLowSurrogate(ch1)) {
                    acp[j++] = Character.toCodePoint(ch0, ch1);
                    i++;
                    continue;
                }
            }
            acp[j++] = ch0;
        }
        return acp;
    }
     
     清單 2 中更新軟件的方法很幼稚。它比較麻煩,需要大量修改,使得生成的軟件很脆弱且今后難以更改。具體而言,這些問題是:
    需要計算碼位的數量以分配足夠的內存
    很難獲得字符串中的指定索引的正確碼位值
    很難為下一個處理步驟正確移動當前索引
     一個改進后的算法出現在下一個示例中。
     示例:基本支持
     Java 1.5 提供了 codePointCount()、codePointAt() 和 offsetByCodePoints() 方法來分別處理 示例 1-2 的 3 個問題。清單 3 使用這些方法來改善這個算法的可讀性:
    清單 3. 基本支持
    int[] toCodePointArray(String str) { // Example 1-3
        int len = str.length();          // the length of str
        int[] acp = new int[str.codePointCount(0, len)];

        for (int i = 0, j = 0; i < len; i = str.offsetByCodePoints(i, 1)) {
            acp[j++] = str.codePointAt(i);
        }
        return acp;
    }

     但是,清單 3 的處理時間比 清單 1 長 2.8 倍。
     示例 1-4:使用 codePointBefore()
     當 offsetByCodePoints() 接收一個負數作為第二個參數時,它就能計算一個距離字符串頭的絕對偏移值。接下來,codePointBefore() 能夠返回一個指定索引前面的碼位值。這些方法用于清單 4 中從尾至頭遍歷字符串:
    清單 4. 使用 codePointBefore() 的基本支持
    int[] toCodePointArray(String str) { // Example 1-4
        int len = str.length();          // the length of str
        int[] acp = new int[str.codePointCount(0, len)];
        int j = acp.length;              // an index for acp

        for (int i = len; i > 0; i = str.offsetByCodePoints(i, -1)) {
            acp[--j] = str.codePointBefore(i);
        }
        return acp;
    }

     這個示例的處理時間 — 比 示例 1-1 長 2.72 倍 — 比 示例 1-3 快一些。通常,當您比較零而不是非零值時,JVM 中的代碼大小要小一些,這有時會提高性能。但是,微小的改進可能不值得犧牲可讀性。
     示例 1-5:使用 charCount()
     示例 1-3 和 1-4 提供基本的代理對支持。他們不需要任何臨時變量,是健壯的編碼方法。要獲取更短的處理時間,使用 charCount() 而不是 offsetByCodePoints() 是有效的,但需要一個臨時變量來存放碼位值,如清單 5 所示:
    清單 5. 使用 charCount() 的優化支持
    int[] toCodePointArray(String str) { // Example 1-5
        int len = str.length();          // the length of str
        int[] acp = new int[str.codePointCount(0, len)];
        int j = 0;                       // an index for acp

        for (int i = 0, cp; i < len; i += Character.charCount(cp)) {
            cp = str.codePointAt(i);
            acp[j++] = cp;
        }
        return acp;
    }
     
     清單 5 的處理時間降低到比 示例 1-1 長 1.68 倍。
     示例 1-6:訪問一個 char 數組
     清單 6 在使用 示例 1-5 中展示的優化的同時直接訪問一個 char 類型數組:
    清單 6. 使用一個 char 數組的優化支持
    int[] toCodePointArray(String str) { // Example 1-6
        char[] ach = str.toCharArray();  // a char array copied from str
        int len = ach.length;            // the length of ach
        int[] acp = new int[Character.codePointCount(ach, 0, len)];
        int j = 0;                       // an index for acp

        for (int i = 0, cp; i < len; i += Character.charCount(cp)) {
            cp = Character.codePointAt(ach, i);
            acp[j++] = cp;
        }
        return acp;
    }

     char 數組是使用 toCharArray() 從字符串復制而來的。性能得到改善,因為對數組的直接訪問比通過一個方法的間接訪問要快。處理時間比 示例 1-1 長 1.51 倍。但是,當調用時,toCharArray() 需要一些開銷來創建一個新數組并將數據復制到數組中。String 類提供的那些方便的方法也不能被使用。但是,這個算法在處理大量數據時有用。
     示例 1-7:一個面向對象的算法
     這個示例的面向對象算法使用 CharBuffer 類,如清單 7 所示:
    清單 7. 使用 CharSequence 的面向對象算法
    int[] toCodePointArray(String str) {        // Example 1-7
        CharBuffer cBuf = CharBuffer.wrap(str); // Buffer to wrap str
        IntBuffer iBuf = IntBuffer.allocate(    // Buffer to store code points
                Character.codePointCount(cBuf, 0, cBuf.capacity()));

        while (cBuf.remaining() > 0) {
            int cp = Character.codePointAt(cBuf, 0); // the current code point
            iBuf.put(cp);
            cBuf.position(cBuf.position() + Character.charCount(cp));
        }
        return iBuf.array();
    }
     
     與前面的示例不同,清單 7 不需要一個索引來持有當前位置以便進行順序訪問。相反,CharBuffer 在內部跟蹤當前位置。Character 類提供靜態方法 codePointCount() 和 codePointAt(),它們能通過 CharSequence 接口處理 CharBuffer。CharBuffer 總是將當前位置設置為 CharSequence 的頭。因此,當 codePointAt() 被調用時,第二個參數總是設置為 0。處理時間比 示例 1-1 長 2.15 倍。
     處理時間比較
     這些順序訪問示例的計時測試使用了一個包含 10,000 個代理對和 10,000 個非代理對的樣例字符串。碼位數組從這個字符串創建 10,000 次。測試環境包括:
    OS:Microsoft Windows® XP Professional SP2
    Java:IBM Java 1.5 SR7
    CPU:Intel® Core 2 Duo CPU T8300 @ 2.40GHz
    Memory:2.97GB RAM
     表 1 展示了示例 1-1 到 1-7 的絕對和相對處理時間以及關聯的 API:
    表 1. 順序訪問示例的處理時間和 API
    示例 說明 處理時間(毫秒) 與示例 1-1 的比率 API
    1-1  不支持代理對 2031 1.00 
    1-2  有限支持 2797 1.38 Character 類:
    static boolean isHighSurrogate(char ch)
    static boolean isLowSurrogate(char ch)
    static boolean isSurrogatePair(char high, char low)
    static int toCodePoint(char high, char low)
    1-3  基本支持 5687 2.80 String 類:
    int codePointAt(int index)
    int codePointCount(int begin, int end)
    int offsetByCodePoints(int index, int cpOffset)
    1-4  使用 codePointBefore() 的基本支持 5516 2.72 String 類:
    int codePointBefore(int index)
    1-5  使用 charCount() 的優化支持 3406 1.68 Character 類:
    static int charCount(int cp)
    1-6  使用一個 char 數組的優化支持 3062 1.51 Character 類:
    static int codePointAt(char[] ach, int index)
    static int codePointCount(char[] ach, int offset, int count)
    1-7  使用 CharSequence 的面向對象方法 4360 2.15  Character 類:
    static int codePointAt(CharSequence seq, int index)
    static int codePointCount(CharSequence seq, int begin, int end)
    隨機訪問
     隨機訪問是直接訪問一個字符串中的任意位置。當字符串被訪問時,索引值基于 16 位 char 類型的單位。但是,如果一個字符串使用 32 位碼位,那么它不能使用一個基于 32 位碼位的單位的索引訪問。必須使用 offsetByCodePoints() 來將碼位的索引轉換為 char 類型的索引。如果算法設計很糟糕,這會導致很差的性能,因為 offsetByCodePoints() 總是通過使用第二個參數從第一個參數計算字符串的內部。在這個小節中,我將比較三個示例,它們通過使用一個短單位來分割一個長字符串。
     示例 2-1:基準測試(不支持代理對)
     清單 8 展示如何使用一個寬度單位來分割一個字符串。這個基準測試留作后用,不支持代理對。
    清單 8. 不支持代理對
    String[] sliceString(String str, int width) { // Example 2-1
        // It must be that "str != null && width > 0".
        List<String> slices = new ArrayList<String>();
        int len = str.length();       // (1) the length of str
        int sliceLimit = len - width; // (2) Do not slice beyond here.
        int pos = 0;                  // the current position per char type

        while (pos < sliceLimit) {
            int begin = pos;                       // (3)
            int end   = pos + width;               // (4)
            slices.add(str.substring(begin, end));
            pos += width;                          // (5)
        }
        slices.add(str.substring(pos));            // (6)
        return slices.toArray(new String[slices.size()]); }
     
     sliceLimit 變量對分割位置有所限制,以避免在剩余的字符串不足以分割當前寬度單位時拋出一個 IndexOutOfBoundsException 實例。這種算法在當前位置超出 sliceLimit 時從 while 循環中跳出后再處理后的分割。
     示例 2-2:使用一個碼位索引
     清單 9 展示了如何使用一個碼位索引來隨機訪問一個字符串:
    清單 9. 糟糕的性能
    String[] sliceString(String str, int width) { // Example 2-2
        // It must be that "str != null && width > 0".
        List<String> slices = new ArrayList<String>();
        int len = str.codePointCount(0, str.length()); // (1) code point count [Modified]
        int sliceLimit = len - width; // (2) Do not slice beyond here.
        int pos = 0;                  // the current position per code point

        while (pos < sliceLimit) {
            int begin = str.offsetByCodePoints(0, pos);            // (3) [Modified]
            int end   = str.offsetByCodePoints(0, pos + width);    // (4) [Modified]
            slices.add(str.substring(begin, end));
            pos += width;                                          // (5)
        }
        slices.add(str.substring(str.offsetByCodePoints(0, pos))); // (6) [Modified]
        return slices.toArray(new String[slices.size()]); }

     清單 9 修改了 清單 8 中的幾行。首先,在 Line (1) 中,length() 被 codePointCount() 替代。其次,在 Lines (3)、(4) 和 (6) 中,char 類型的索引通過 offsetByCodePoints() 用碼位索引替代。
     基本的算法流與 示例 2-1 中的看起來幾乎一樣。但處理時間根據字符串長度與示例 2-1 的比率同比增加,因為 offsetByCodePoints() 總是從字符串頭到指定索引計算字符串內部。
     示例 2-3:減少的處理時間
     可以使用清單 10 中展示的方法來避免 示例 2-2 的性能問題:
    清單 10. 改進的性能
    String[] sliceString(String str, int width) { // Example 2-3
        // It must be that "str != null && width > 0".
        List<String> slices = new ArrayList<String>();
        int len = str.length(); // (1) the length of str
        int sliceLimit          // (2) Do not slice beyond here. [Modified]
                = (len >= width * 2 || str.codePointCount(0, len) > width)
                ? str.offsetByCodePoints(len, -width) : 0;
        int pos = 0;            // the current position per char type

        while (pos < sliceLimit) {
            int begin = pos;                                // (3)
            int end   = str.offsetByCodePoints(pos, width); // (4) [Modified]
            slices.add(str.substring(begin, end));
            pos = end;                                      // (5) [Modified]
        }
        slices.add(str.substring(pos));                     // (6)
        return slices.toArray(new String[slices.size()]); }

     首先,在 Line (2) 中,(清單 9 中的)表達式 len-width 被 offsetByCodePoints(len,-width) 替代。但是,當 width 的值大于碼位的數量時,這會拋出一個 IndexOutOfBoundsException 實例。必須考慮邊界條件以避免異常,使用一個帶有 try/catch 異常處理程序的子句將是另一個解決方案。如果表達式 len>width*2 為 true,則可以安全地調用 offsetByCodePoints(),因為即使所有碼位都被轉換為代理對,碼位的數量仍會超過 width 的值。或者,如果 codePointCount(0,len)>width 為 true,也可以安全地調用 offsetByCodePoints()。如果是其他情況,sliceLimit 必須設置為 0。
     在 Line (4) 中,清單 9 中的表達式 pos + width 必須在 while 循環中使用 offsetByCodePoints(pos,width) 替換。需要計算的量位于 width 的值中,因為第一個參數指定當 width 的值。接下來,在 Line (5) 中,表達式 pos+=width 必須使用表達式 pos=end 替換。這避免兩次調用 offsetByCodePoints() 來計算相同的索引。源代碼可以被進一步修改以小化處理時間。
     處理時間比較
     圖 1 和圖 2 展示了示例 2-1、2-2 和 2-3 的處理時間。樣例字符串包含相同數量的代理對和非代理對。當字符串的長度和 width 的值被更改時,樣例字符串被切割 10,000 次。
    圖 1. 一個分段的常量寬度圖 2. 分段的常量計數
     示例 2-1 和 2-3 按照長度比例增加了它們的處理時間,但 示例 2-2 按照長度的平方比例增加了處理時間。當字符串長度和 width 的值增加而分段的數量固定時,示例 2-1 擁有一個常量處理時間,而示例 2-2 和 2-3 以 width 的值為比例增加了它們的處理時間。
    信息 API
     大多數處理代理的信息 API 擁有兩種名稱相同的方法。一種接收 16 位 char 類型參數,另一種接收 32 為碼位參數。表 2 展示了每個 API 的返回值。第三列針對 U+53F1,第 4 列針對 U+20B9F,后一列針對 U+D842(即高代理),而 U+20B9F 被轉換為 U+D842 加上 U+DF9F 的代理對。如果程序不能處理代理對,則值 U+D842 而不是 U+20B9F 將導致意想不到的結果(在表 2 中以粗斜體表示)。
    表 2. 用于代理的信息 API
    類 方法/構造函數 針對 U+53F1 的值 針對 U+20B9F 的值 針對 U+D842 的值
    Character  static byte getDirectionality(int cp)  0  0  0
     static int getNumericValue(int cp)  -1  -1  -1
     static int getType(int cp)  5  5  19
     static boolean isDefined(int cp)  true  true  true
     static boolean isDigit(int cp)  false  false  false
     static boolean isISOControl(int cp)  false  false  false
     static boolean isIdentifierIgnorable(int cp)  false  false  false
     static boolean isJavaIdentifierPart(int cp)  true  true  false
     static boolean isJavaIdentifierStart(int cp)  true  true  false
     static boolean isLetter(int cp)  true  true  false
     static boolean isLetterOrDigit(int cp)  true  true  false
     static boolean isLowerCase(int cp)  false  false  false
     static boolean isMirrored(int cp)  false  false  false
     static boolean isSpaceChar(int cp)  false  false  false
     static boolean isSupplementaryCodePoint(int cp)  false  true  false
     static boolean isTitleCase(int cp)  false  false  false
     static boolean isUnicodeIdentifierPart(int cp)  true  true  false
     static boolean isUnicodeIdentifierStart(int cp)  true  true  false
     static boolean isUpperCase(int cp)  false  false  false
     static boolean isValidCodePoint(int cp)  true  true  true
     static boolean isWhitespace(int cp)  false  false  false
     static int toLowerCase(int cp)  (不可更改)
     static int toTitleCase(int cp)  (不可更改)
     static int toUpperCase(int cp)  (不可更改)
    Character.UnicodeBlock  Character.UnicodeBlock of(int cp)  CJK_UNIFIED_IDEOGRAPHS  CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B  HIGH_SURROGATES
    Font  boolean canDisplay(int cp)  (取決于 Font 實例)
    FontMetrics  int charWidth(int cp)  (取決于 FontMetrics 實例)
    String  int indexOf(int cp)  (取決于 String 實例)
     int lastIndexOf(int cp)  (取決于 String 實例)
    其他 API
     本小節介紹前面的小節中沒有討論的代理對相關 API。表 3 展示所有這些剩余的 API。所有代理對 API 都包含在表 1、2 和 3 中。
    表 3. 其他代理 API
    類 方法/構造函數
    Character  static int codePointAt(char[] ach, int index, int limit)
     static int codePointBefore(char[] ach, int index)
     static int codePointBefore(char[] ach, int index, int start)
     static int codePointBefore(CharSequence seq, int index)
     static int digit(int cp, int radix)
     static int offsetByCodePoints(char[] ach, int start, int count, int index, int cpOffset)
     static int offsetByCodePoints(CharSequence seq, int index, int cpOffset)
     static char[] toChars(int cp)
     static int toChars(int cp, char[] dst, int dstIndex)
    String  String(int[] acp, int offset, int count)
     int indexOf(int cp, int fromIndex)
     int lastIndexOf(int cp, int fromIndex)
    StringBuffer  StringBuffer appendCodePoint(int cp)
     int codePointAt(int index)
     int codePointBefore(int index)
     int codePointCount(int beginIndex, int endIndex)
     int offsetByCodePoints(int index, int cpOffset)
    StringBuilder  StringBuilder appendCodePoint(int cp)
     int codePointAt(int index)
     int codePointBefore(int index)
     int codePointCount(int beginIndex, int endIndex)
     int offsetByCodePoints(int index, int cpOffset)
    IllegalFormatCodePointException  IllegalFormatCodePointException(int cp)
     int getCodePoint()
     
     清單 11 展示了從一個碼位創建一個字符串的 5 種方法。用于測試的碼位是 U+53F1 和 U+20B9F,它們在一個字符串中重復了 100 億次。清單 11 中的注釋部分顯示了處理時間:
    清單 11. 從一個碼位創建一個字符串的 5 種方法
        
    int cp = 0x20b9f; // CJK Ideograph Extension B
    String str1 = new String(new int[]{cp}, 0, 1);    // processing time: 206ms
    String str2 = new String(Character.toChars(cp));                  //  187ms
    String str3 = String.valueOf(Character.toChars(cp));              //  195ms
    String str4 = new StringBuilder().appendCodePoint(cp).toString(); //  269ms
    String str5 = String.format("%c", cp);                            // 3781ms

     str1、str2、str3 和 str4 的處理時間沒有明顯不同。相反,創建 str5 花費的時間要長得多,因為它使用 String.format(),該方法支持基于本地和格式化信息的靈活輸出。str5 方法應該只用于程序的末尾來輸出文本。
    結束語
     Unicode 的每個新版本都包含了通過代理對表示的新定義的字符。東亞字符集標準并不是這樣的字符的惟一來源。例如,移動電話中還需要支持 Emoji 字符(表情圖釋),還有各種古字符需要支持。您從本文收獲的技術和性能分析將有助于您在您的 Java 應用程序中支持所有這些字符。
    關于 JAR 您不知道的 5 件事
     把它放在 JAR 中
     通常,在源代碼被編譯之后,您需要構建一個 JAR 文件,使用 jar 命令行實用工具,或者,更常用的是 Ant jar 任務將 Java 代碼(已經被包分離)收集到一個單獨的集合中,過程簡潔易懂,我不想在這做過多的說明,稍后將繼續說明如何構建 JAR。現在,我只需要存檔 Hello,這是一個獨立控制臺實用工具,對于執行打印消息到控制臺這個任務十分有用。如清單 1 所示:
    清單 1. 存檔控制臺實用工具
    package com.tedneward.jars;

    public class Hello
    {
        public static void main(String[] args)
        {
            System.out.println("Howdy!");
        }
    }
     Hello 實用工具內容并不多,但是對于研究 JAR 文件卻是一個很有用的 “腳手架”,我們先從執行此代碼開始。
    JAR 是可執行的
     .NET 和 C++ 這類語言一直是 OS 友好的,只需要在命令行(helloWorld.exe)引用其名稱,或在 GUI shell 中雙擊它的圖標就可以啟動應用程序。然而在 Java 編程中,啟動器程序 — java — 將 JVM 引導入進程中,我們需要傳遞一個命令行參數(com.tedneward.Hello)指定想要啟動的 main() 方法的類。
     這些附加步驟使使用 Java 創建界面友好的應用程序更加困難。不僅終端用戶需要在命令行輸入所有參數(終端用戶寧愿避開),而且極有可能使他或她操作失誤以及返回一個難以理解的錯誤。
     這個解決方案使 JAR 文件 “可執行” ,以致 Java 啟動程序在執行 JAR 文件時,自動識別哪個類將要啟動。我們所要做的是,將一個入口引入 JAR 文件清單文件(MANIFEST.MF 在 JAR 的 META-INF 子目錄下),像這樣:
    清單 2. 展示入口點!
    Main-Class: com.tedneward.jars.Hello
     這個清單文件只是一個名值對。因為有時候清單文件很難處理回車和空格,然而在構建 JAR 時,使用 Ant 來生成清單文件是很容易的。在清單 3 中,使用 Ant jar 任務的 manifest 元素來指定清單文件:
    清單 3. 構建我的入口點!
        <target name="jar" depends="build">
            <jar destfile="outapp.jar" basedir="classes">
                <manifest>
                    <attribute name="Main-Class" value="com.tedneward.jars.Hello" />
                </manifest>
            </jar>
        </target>
     現在用戶在執行 JAR 文件時需要做的就是通過 java -jar outapp.jar 在命令行上指定其文件名。就 GUI shell 來說,雙擊 JAR 文件即可。
    JAR 可以包括依賴關系信息
     似乎 Hello 實用工具已經展開,改變實現的需求已經出現。Spring 或 Guice 這類依賴項注入(DI)容器可以為我們處理許多細節,但是仍然有點小問題:修改代碼使其含有 DI 容器的用法可能導致清單 4 所示的結果,如:
    清單 4. Hello、Spring world!
    package com.tedneward.jars;

    import org.springframework.context.*;
    import org.springframework.context.support.*;

    public class Hello
    {
        public static void main(String[] args)
        {
            ApplicationContext appContext =
                new FileSystemXmlApplicationContext("./app.xml");
            ISpeak speaker = (ISpeak) appContext.getBean("speaker");
            System.out.println(speaker.sayHello());
        }
    }

    關于 Spring 的更多信息
     這個技巧將幫助您熟悉依賴項注入和 Spring 框架。如果您需要溫習其他主題,見 參考資料。
     由于啟動程序的 -jar 選項將覆蓋 -classpath 命令行選項中的所有內容,因此運行這些代碼時,Spring 必須是在 CLASSPATH 和 環境變量中。幸運的是,JAR 允許在清單文件中出現其他的 JAR 依賴項聲明,這使得無需聲明就可以隱式創建 CLASSPATH,如清單 5 所示:
    清單 5. Hello、Spring CLASSPATH!
    <target name="jar" depends="build">
            <jar destfile="outapp.jar" basedir="classes">
                <manifest>
                    <attribute name="Main-Class" value="com.tedneward.jars.Hello" />
                    <attribute name="Class-Path"
                        value="./lib/org.springframework.context-3.0.1.RELEASE-A.jar
                          ./lib/org.springframework.core-3.0.1.RELEASE-A.jar
                          ./lib/org.springframework.asm-3.0.1.RELEASE-A.jar
                          ./lib/org.springframework.beans-3.0.1.RELEASE-A.jar
                          ./lib/org.springframework.expression-3.0.1.RELEASE-A.jar
                          ./lib/commons-logging-1.0.4.jar" />
                </manifest>
            </jar>
        </target>
     注意 Class-Path 屬性包含一個與應用程序所依賴的 JAR 文件相關的引用。您可以將它寫成一個絕對引用或者完全沒有前綴。這種情況下,我們假設 JAR 文件同應用程序 JAR 在同一個目錄下。
     不幸的是,value 屬性和 Ant Class-Path 屬性必須出現在同一行,因為 JAR 清單文件不能處理多個 Class-Path 屬性。因此,所有這些依賴項在清單文件中必須出現在一行。當然,這很難看,但為了使 java -jar outapp.jar 可用,還是值得的!
    JAR 可以被隱式引用
     如果有幾個不同的命令行實用工具(或其他的應用程序)在使用 Spring 框架,可能更容易將 Spring JAR 文件放在公共位置,使所有實用工具能夠引用。這樣就避免了文件系統中到處都有 JAR 副本。Java 運行時 JAR 的公共位置,眾所周知是 “擴展目錄” ,默認位于 lib/ext 子目錄,在 JRE 的安裝位置之下。
     JRE 是一個可定制的位置,但是在一個給定的 Java 環境中很少定制,以至于可以完全假設 lib/ext 是存儲 JAR 的一個安全地方,以及它們將隱式地用于 Java 環境的 CLASSPATH 上。
    Java 6 允許類路徑通配符
     為了避免龐大的 CLASSPATH 環境變量(Java 開發人員幾年前就應該拋棄的)和/或命令行 -classpath 參數,Java 6 引入了類路徑通配符 的概念。與其不得不啟動參數中明確列出的每個 JAR 文件,還不如自己指定 lib/*,讓所有 JAR 文件列在該目錄下(不遞歸),在類路徑中。
     不幸的是,類路徑通配符不適用于之前提到的 Class-Path 屬性清單入口。但是這使得它更容易啟動 Java 應用程序(包括服務器)開發人員任務,例如 code-gen 工具或分析工具。
    JAR 有的不只是代碼
     Spring,就像許多 Java 生態系統一樣,依賴于一個描述構建環境的配置文件,前面提到過,Spring 依賴于一個 app.xml 文件,此文件同 JAR 文件位于同一目錄 — 但是開發人員在復制 JAR 文件的同時忘記復制配置文件,這太常見了!
     一些配置文件可用 sysadmin 進行編輯,但是其中很大一部分(例如 Hibernate 映射)都位于 sysadmin 域之外,這將導致部署漏洞。一個合理的解決方案是將配置文件和代碼封裝在一起 — 這是可行的,因為 JAR 從根本上來說就是一個 “喬裝的” ZIP 文件。 當構建一個 JAR 時,只需要在 Ant 任務或 jar 命令行包括一個配置文件即可。
     JAR 也可以包含其他類型的文件,不僅僅是配置文件。例如,如果我的 SpeakEnglish 部件要訪問一個屬性文件,我可以進行如下設置,如清單 6 所示:
    清單 6. 隨機響應
    package com.tedneward.jars;

    import java.util.*;

    public class SpeakEnglish
        implements ISpeak
    {
        Properties responses = new Properties();
        Random random = new Random();

        public String sayHello()
        {
            // Pick a response at random
            int which = random.nextInt(5);
           
            return responses.getProperty("response." + which);
        }
    }
     可以將 responses.properties 放入 JAR 文件,這意味著部署 JAR 文件時至少可以少考慮一個文件。這只需要在 JAR 步驟中包含 responses.properties 文件即可。
     當您在 JAR 中存儲屬性之后,您可能想知道如何將它取回。如果所需要的數據與 JAR 文件在同一位置,正如前面的例子中提到的那樣,不需要費心找出 JAR 文件的位置,使用 JarFile 對象就可將其打開。相反,可以使用類的 ClassLoader 找到它,像在 JAR 文件中尋找 “資源” 那樣,使用 ClassLoader getResourceAsStream() 方法,如清單 7 所示:
    清單 7. ClassLoader 定位資源
    package com.tedneward.jars;

    import java.util.*;

    public class SpeakEnglish
        implements ISpeak
    {
        Properties responses = new Properties();
        // ...

        public SpeakEnglish()
        {
            try
            {
                ClassLoader myCL = SpeakEnglish.class.getClassLoader();
                responses.load(
                    myCL.getResourceAsStream(
                        "com/tedneward/jars/responses.properties"));
            }
            catch (Exception x)
            {
                x.printStackTrace();
            }
        }
       
        // ...
    }
     您可以按照以上步驟尋找任何類型的資源:配置文件、審計文件、圖形文件,等等。幾乎任何文件類型都能被捆綁進 JAR 中,作為一個 InputStream 獲取(通過 ClassLoader),并通過您喜歡的方式使用。
    關于 Java 性能監控您不知道的 5 件事,第 1 部分
     許多開發人員沒有意識到從 Java 5 開始 JDK 中包含了一個分析器。JConsole(或者 Java 平臺新版本,VisualVM)是一個內置分析器,它同 Java 編譯器一樣容易啟動。如果是從命令行啟動,使 JDK 在 PATH 上,運行 jconsole 即可。如果從 GUI shell 啟動,找到 JDK 安裝路徑,打開 bin 文件夾,雙擊 jconsole。
     當分析工具彈出時(取決于正在運行的 Java 版本以及正在運行的 Java 程序數量),可能會出現一個對話框,要求輸入一個進程的 URL 來連接,也可能列出許多不同的本地 Java 進程(有時包含 JConsole 進程本身)來連接。
    JConsole 或 VisualVM?
     JConsole 從 Java 5 開始就隨著 Java 平臺版本一起發布,而 VisualVM 是在 NetBeans 基礎上升級的一個分析器,在 Java 6 的更新版 12 中第一次發布。多數商店還沒有更新到 Java 6 ,因此這篇文章主要介紹 JConsole 。然而,多數技巧和這兩個分析器都有關。(注意:除了包含在 Java 6 中之外,VisualVM 還有一個獨立版下載。下載 VisualVM,參見 參考資料。)
    使用 JConsole 進行工作
     在 Java 5 中,Java 進程并不是被設置為默認分析的,而是通過一個命令行參數 — -Dcom.sun.management.jmxremote — 在啟動時告訴 Java 5 VM 打開連接,以便分析器可以找到它們;當進程被 JConsole 撿起時,您只能雙擊它開始分析。
     分析器有自己的開銷,因此好的辦法就是花點時間來弄清是什么開銷。發現 JConsole 開銷簡單的辦法是,首先獨自運行一個應用程序,然后在分析器下運行,并測量差異。(應用程序不能太大或者太小;我喜歡使用 JDK 附帶的 SwingSet2 樣本。)因此,我使用 -verbose:gc 嘗試運行 SwingSet2 來查看垃圾收集清理,然后運行同一個應用程序并將 JConsole 分析器連接到它。當 JConsole 連接好了之后,一個穩定的 GC 清理流出現,否則不會出現。這就是分析器的性能開銷。
    遠程連接進程
     因為 Web 應用程序分析工具假設通過一個套接字進行連通性分析,您只需要進行少許配置來設置 JConsole(或者是基于 JVMTI 的分析器,就這點而言),監控/分析遠程運行的應用程序。
     如果 Tomcat 運行在一個名為 “webserve” 的機器上,且 JVM 已經啟動了 JMX 并監聽端口 9004,從 JConsole(或者任何 JMX 客戶端)連接它需要一個 JMX URL “service:jmx:rmi:///jndi/rmi://webserver:9004/jmxrmi”。
     基本上,要分析一個運行在遠程數據中心的應用程序服務器,您所需要的僅僅是一個 JMX URL。更多關于使用 JMX 和 JConsole 遠程監控和管理的信息,參見 參考資料。)
    跟蹤統計
     發現應用程序代碼中性能問題的常用響應多種多樣,但也是可預測的。早期的 Java 編程人員對舊的 IDE 可能十分生氣,并開始進行代碼庫中主要部分的代碼復查,在源代碼中尋找熟悉的 “紅色標志”,像異步塊、對象配額等等。隨著編程經驗的增加,開發人員可能會仔細研究 JVM 支持的 -X 標志,尋找優化垃圾收集器的方法。當然,對于新手,直接去 Google 查詢,希望有其他人發現了 JVM 的神奇的 “make it go fast” 轉換,避免重寫代碼。
     從本質上來說,這些方法沒什么錯,但都是有風險的。對于一個性能問題有效的響應就是使用一個分析器 — 現在它們內置在 Java 平臺 ,我們確實沒有理由不這樣做!
     JConsole 有許多對收集統計數據有用的選項卡,包括:
    Memory:在 JVM 垃圾收集器中針對各個堆跟蹤活動。
    Threads:在目標 JVM 中檢查當前線程活動。
    Classes:觀察 VM 已加載類的總數。
     這些選項卡(和相關的圖表)都是由每個 Java 5 及更高版本 VM 在 JMX 服務器上注冊的 JMX 對象提供的,是內置到 JVM 的。一個給定 JVM 中可用 bean 的完整清單在 MBeans 選項卡上列出,包括一些元數據和一個有限的用戶界面來查看數據或執行操作。(然而,注冊通知是在 JConsole 用戶界面之外。)
     使用統計數據
     假設一個 Tomcat 進程死于 OutOfMemoryError。如果您想要弄清楚發生了什么,打開 JConsole,單擊 Classes 選項卡,過一段時間查看一次類計數。如果數量穩定上升,您可以假設應用程序服務器或者您的代碼某個地方有一個 ClassLoader 漏洞,不久之后將耗盡 PermGen 空間。如果需要更進一步的確認問題,請看 Memory 選項卡。
    為離線分析創建一個堆轉儲
     生產環境中一切都在快速地進行著,您可能沒有時間花費在您的應用程序分析器上,相反地,您可以為 Java 環境中的每個事件照一個快照保存下來過后再看。在 JConsole 中您也可以這樣做,在 VisualVM 中甚至會做得更好。
     先找到 MBeans 選項卡,在其中打開 com.sun.management 節點,接著是 HotSpotDiagnostic 節點。現在,選擇 Operations,注意右邊面板中的 “dumpHeap” 按鈕。如果您在第一個(“字符串”)輸入框中向 dumpHeap 傳遞一個文件名來轉儲,它將為整個 JVM 堆照一個快照,并將其轉儲到那個文件。
     稍后,您可以使用各種不同的商業分析器來分析文件,或者使用 VisualVM 分析快照。(記住,VisualVM 是在 Java 6 中可用的,且是單獨下載的。)
     作為一個分析器實用工具,JConsole 是極好的,但是還有更好的工具。一些分析插件附帶分析器或者靈巧的用戶界面,默認情況下比 JConsole 跟蹤更多的數據。
     JConsole 真正吸引人的是整個程序是用 “普通舊式 Java ” 編寫的,這意味著任何 Java 開發人員都可以編寫這樣一個實用工具。事實上,JDK 其中甚至包括如何通過創建一個插件來定制 JConsole 的示例(參見 參考資料)。建立在 NetBeans 頂部的 VisualVM 進一步延伸了插件概念。
     如果 JConsole(或者 VisualVM,或者其他任何工具)不符合您的需求,或者不能跟蹤您想要跟蹤的,或者不能按照您的方式跟蹤,您可以編寫屬于自己的工具。如果您覺得 Java 代碼很麻煩,Groovy 或 JRuby 或很多其他 JVM 語言都可以幫助您更快完成。
     您真正需要的是一個快速而粗糙(quick-and-dirty)的由 JVM 連接的命令行工具,可以以您想要的方式確切地跟蹤您感興趣的數據。
    關于 Java 性能監控您不知道的 5 件事,第 2 部分
     全功能內置分析器,如 JConsole 和 VisualVM 的成本有時比它們的性能費用還要高 — 尤其是在生產軟件上運行的系統中。因此,在聚焦 Java 性能監控的第 2 篇文章中,我將介紹 5 個命令行分析工具,使開發人員僅關注運行的 Java 進程的一個方面。
     JDK 包括很多命令行實用程序,可以用于監控和管理 Java 應用程序性能。雖然大多數這類應用程序都被標注為 “實驗型”,在技術上不受支持,但是它們很有用。有些甚至是特定用途工具的種子材料,可以使用 JVMTI 或 JDI(參見 參考資料)建立。
    jps (sun.tools.jps)
     很多命令行工具都要求您識別您希望監控的 Java 進程。這與監控本地操作系統進程、同樣需要一個程序識別器的同類工具沒有太大區別。
     “VMID” 識別器與本地操作系統進程識別器(“pid”)并不總是相同的,這就是我們需要 JDK jps 實用程序的原因。
     在 Java 進程中使用 jps
     與配置 JDK 的大部分工具及本文中提及的所有工具一樣,可執行 jps 通常是一個圍繞 Java 類或執行大多數工作的類集的一個薄包裝。在 Windows® 環境下,這些工具是 .exe 文件,使用 JNI Invocation API 直接調用上面提及的類;在 UNIX® 環境下,大多數工具是一個 shell 腳本的符號鏈接,該腳本采用指定的正確類名稱開始一個普通啟動程序。
     如果您希望在 Java 進程中使用 jps(或者任何其他工具)的功能 — Ant 腳本 — 僅在每個工具的 “主” 類上調用 main() 相對容易。為了簡化引用,類名稱出現在每個工具名稱之后的括號內。
     jps — 名稱反映了在大多數 UNIX 系統上發現的 ps 實用程序 — 告訴我們運行 Java 應用程序的 JVMID。顧名思義,jps 返回指定機器上運行的所有已發現的 Java 進程的 VMID。如果 jps 沒有發現進程,并不意味著無法附加或研究 Java 進程,而只是意味著它并未宣傳自己的可用性。
     如果發現 Java 進程,jps 將列出啟用它的命令行。這種區分 Java 進程的方法非常重要,因為只要涉及操作系統,所有的 Java 進程都被統稱為 “java”。在大多數情況下,VMID 是值得注意的重要數字。
     使用分析器開始
     使用分析實用程序開始的簡單方法是使用一個如在 demo/jfc/SwingSet2 中發現的 SwingSet2 演示一樣的演示程序。這樣就可以避免程序作為背景/監控程序運行時出現掛起的可能性。當您了解工具及其費用后,就可以在實際程序中進行試用。
     加載演示應用程序后,運行 jps 并注意返回的 vmid。為了獲得更好的效果,采用 -Dcom.sun.management.jmxremote 屬性集啟動 Java 進程。如果沒有使用該設置,部分下列工具收集的部分數據可能不可用。
    jstat (sun.tools.jstat)
     jstat 實用程序可以用于收集各種各樣不同的統計數據。jstat 統計數據被分類到 “選項” 中,這些選項在命令行中被指定作為第一參數。對于 JDK 1.6 來說,您可以通過采用命令 -options 運行 jstat 查看可用的選項清單。清單 1 中顯示了部分選項:
    清單 1. jstat 選項
    -class
    -compiler
    -gc
    -gccapacity
    -gccause
    -gcnew
    -gcnewcapacity
    -gcold
    -gcoldcapacity
    -gcpermcapacity
    -gcutil
    -printcompilation
     實用程序的 JDK 記錄(參見 參考資料)將告訴您清單 1 中每個選項返回的內容,但是其中大多數用于收集垃圾的收集器或者其部件的性能信息。-class 選項顯示了加載及未加載的類(使其成為檢測應用程序服務器或代碼中 ClassLoader 泄露的重要實用程序,且 -compiler 和 -printcompilation 都顯示了有關 Hotspot JIT 編譯程序的信息。
     默認情況下,jstat 在您核對信息時顯示信息。如果您希望每隔一定時間拍攝快照,請在 -options 指令后以毫秒為單位指定間隔時間。jstat 將持續顯示監控進程信息的快照。如果您希望 jstat 在終止前進行特定數量的快照,在間隔時間/時間值后指定該數字。
     如果 5756 是幾分鐘前開始的運行 SwingSet2 程序的 VMID,那么下列命令將告訴 jstat 每 250 毫秒為 10 個佚代執行一次 gc 快照轉儲,然后停止:
     jstat -gc 5756 250 10
     請注意 Sun(現在的 Oracle)保留了在不進行任何預先通知的情況下更改各種選項的輸出甚至是選項本身的權利。這是使用不受支持實用程序的缺點。請參看 Javadocs 了解 jstat 輸出中每一列的全部細節。
    jstack (sun.tools.jstack)
     了解 Java 進程及其對應的執行線程內部發生的情況是一種常見的診斷挑戰。例如,當一個應用程序突然停止進程時,很明顯出現了資源耗盡,但是僅通過查看代碼無法明確知道何處出現資源耗盡,且為什么會發生。
     jstack 是一個可以返回在應用程序上運行的各種各樣線程的一個完整轉儲的實用程序,您可以使用它查明問題。
     采用期望進程的 VMID 運行 jstack 會產生一個堆轉儲。就這一點而言,jstack 與在控制臺窗口內按 Ctrl-Break 鍵起同樣的作用,在控制臺窗口中,Java 進程正在運行或調用 VM 內每個 Thread 對象上的 Thread.getAllStackTraces() 或 Thread.dumpStack()。jstack 調用也轉儲關于在 VM 內運行的非 Java 線程的信息,這些線程作為 Thread 對象并不總是可用的。
     jstack 的 -l 參數提供了一個較長的轉儲,包括關于每個 Java 線程持有鎖的更多詳細信息,因此發現(和 squash)死鎖或可伸縮性 bug 是極其重要的。
    jmap (sun.tools.jmap)
     有時,您正在處理的問題是一個對象泄露,如一個 ArrayList (可能持有成千上萬個對象)該釋放時沒有釋放。另一個更普遍的問題是,看似從不會壓縮的擴展堆,卻有活躍的垃圾收集。
     當您努力尋找一個對象泄露時,在指定時刻對堆及時進行拍照,然后審查其中內容非常有用。jmap 通過對堆拍攝快照來提供該功能的第一部分。然后您可以采用下一部分中描述的 jhat 實用程序分析堆數據。
     與這里描述的其他所有實用程序一樣,使用 jmap 非常簡單。將 jmap 指向您希望拍快照的 Java 進程的 VMID,然后給予它部分參數,用來描述產生的結果文件。您要傳遞給 jmap 的選項包括轉儲文件的名稱以及是否使用一個文本文件或二進制文件。二進制文件是有用的選項,但是只有當與某一種索引工具 結合使用時 — 通過十六進制值的文本手動操作數百兆字節不是好的方法。
     隨意看一下 Java 堆的更多信息,jmap 同樣支持 -histo 選項。-histo 產生一個對象文本柱狀圖,現在在堆中大量引用,由特定類型消耗的字節總數分類。它同樣給出了特定類型的總示例數量,支持部分原始計算,并猜測每個實例的相對成本。
     不幸的是,jmap 沒有像 jstat 一樣的 period-and-max-count 選項,但是將 jmap(或 jmap.main())調用放入 shell 腳本或其他類的循環,周期性地拍攝快照相對簡單。(事實上,這是加入 jmap 的一個好的擴展,不管是作為 OpenJDK 本身的源補丁,還是作為其他實用程序的擴展。)
    jhat (com.sun.tools.hat.Main)
     將堆轉儲至一個二進制文件后,您就可以使用 jhat 分析二進制堆轉儲文件。jhat 創建一個 HTTP/HTML 服務器,該服務器可以在瀏覽器中被瀏覽,提供一個關于堆的 object-by-object 視圖,及時凍結。根據對象引用草率處理堆可能會非常可笑,您可以通過對總體混亂進行某種自動分析而獲得更好的服務。幸運的是,jhat 支持 OQL 語法進行這樣的分析。
     例如,對所有含有超過 100 個字符的 String 運行 OQL 查詢看起來如下:
     select s from java.lang.String s where s.count >= 100
     結果作為對象鏈接顯示,然后展示該對象的完整內容,字段引用作為可以解除引用的其他鏈接的其他對象。OQL 查詢同樣可以調用對象的方法,將正則表達式作為查詢的一部分,并使用內置查詢工具。一種查詢工具,referrers() 函數,顯示了引用指定類型對象的所有引用。下面是尋找所有參考 File 對象的查詢:
     select referrers(f) from java.io.File f
     您可以查找 OQL 的完整語法及其在 jhat 瀏覽器環境內 “OQL Help” 頁面上的特性。將 jhat 與 OQL 相結合是對行為不當的堆進行對象調查的有效方法。
    關于 Java Scripting API 您不知道的 5 件事
     現在,許多 Java 開發人員都喜歡在 Java 平臺中使用腳本語言,但是使用編譯到 Java 字節碼中的動態語言有時是不可行的。在某些情況中,直接編寫一個 Java 應用程序的腳本 部分 或者在一個腳本中調用特定的 Java 對象是更快捷、更高效的方法。
     這就是 javax.script 產生的原因了。Java Scripting API 是從 Java 6 開始引入的,它填補了便捷的小腳本語言和健壯的 Java 生態系統之間的鴻溝。通過使用 Java Scripting API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語言,這使您能夠在解決一些很小的問題時有更多可選擇的方法。
    使用 jrunscript 執行 JavaScript
     每一個新的 Java 平臺發布都會帶來新的命令行工具集,它們位于 JDK 的 bin 目錄。Java 6 也一樣,其中 jrunscript 便是 Java 平臺工具集中的一個不小的補充。
     設想一個編寫命令行腳本進行性能監控的簡單問題。這個工具將借用 jmap(見本系列文章 前一篇文章 中的介紹),每 5 秒鐘運行一個 Java 進程,從而了解進程的運行狀況。一般情況下,我們會使用命令行 shell 腳本來完成這樣的工作,但是這里的服務器應用程序部署在一些差別很大的平臺上,包括 Windows® 和 Linux®。系統管理員將會發現編寫能夠同時運行在兩個平臺的 shell 腳本是很痛苦的。通常的做法是編寫一個 Windows 批處理文件和一個 UNIX® shell 腳本,同時保證這兩個文件同步更新。
     但是,任何閱讀過 The Pragmatic Programmer 的人都知道,這嚴重違反了 DRY (Don't Repeat Yourself) 原則,而且會產生許多缺陷和問題。我們真正希望的是編寫一種與操作系統無關的腳本,它能夠在所有的平臺上運行。
     當然,Java 語言是平臺無關的,但是這里并不是需要使用 “系統” 語言的情況。我們需要的是一種腳本語言 — 如,JavaScript。
     清單 1 顯示的是我們所需要的簡單 shell 腳本:
    清單 1. periodic.js
    while (true)
    {
        echo("Hello, world!");
    }
     由于經常與 Web 瀏覽器打交道,許多 Java 開發人員已經知道了 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 開發的一種 ECMAScript 語言)。問題是,系統管理員要如何運行這個腳本?
     當然,解決方法是 JDK 所帶的 jrunscript 實用程序,如清單 2 所示:
    清單 2. jrunscript
    C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js
    Hello, world!
    Hello, world!
    Hello, world!
    Hello, world!
    Hello, world!
    Hello, world!
    Hello, world!
    ...
     注意,您也可以使用 for 循環按照指定的次數來循環執行這個腳本,然后才退出。基本上,jrunscript 能夠讓您執行 JavaScript 的所有操作。惟一不同的是它的運行環境不是瀏覽器,所以運行中不會有 DOM。因此,頂層的函數和對象稍微有些不同。
     因為 Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript 可以執行任何傳遞給它的 ECMAScript 代碼,不管是一個文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環境。運行 jrunscript 就可以訪問 REPL shell。
    從腳本訪問 Java 對象
     能夠編寫 JavaScript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們在 Java 語言中使用的所有代碼 — 這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪問整個 Java 生態系統,因為本質上一切代碼都還是 Java 字節碼。所以,回到我們之前的問題,我們可以在 Java 平臺上使用傳統的 Runtime.exec() 調用來啟動進程,如清單 3 所示:
    清單 3. Runtime.exec() 啟動 jmap
    var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
    p.waitFor()
     數組 arguments 是指向傳遞到這個函數參數的 ECMAScript 標準內置引用。在頂層的腳本環境中,則是傳遞給腳本本身的的參數數組(命令行參數)。所以,在清單 3 中,這個腳本預期接收一個參數,該參數包含要映射的 Java 進程的 VMID。
     除此之外,我們可以利用本身為一個 Java 類的 jmap,然后直接調用它的 main() 方法,如清單 4 所示。有了這個方法,我們不需要 “傳輸” Process 對象的 in/out/err 流。
    清單 4. JMap.main()
    var args = [ "-histo", arguments[0] ]
    Packages.sun.tools.jmap.JMap.main(args)
     Packages 語法是一個 Rhino ECMAScript 標識,它指向已經 Rhino 內創建的位于核心 java.* 包之外的 Java 包。
    從 Java 代碼調用腳本
     從腳本調用 Java 對象僅僅完成了一半的工作:Java 腳本環境也提供了從 Java 代碼調用腳本的功能。這只需要實例化一個 ScriptEngine 對象,然后加載和評估腳本,如清單 5 所示:
    清單 5. Java 平臺的腳本調用
    import java.io.*;
    import javax.script.*;

    public class App
    {
        public static void main(String[] args)
        {
            try
            {
                ScriptEngine engine =
                    new ScriptEngineManager().getEngineByName("javascript");
                for (String arg : args)
                {
                    FileReader fr = new FileReader(arg);
                    engine.eval(fr);
                }
            }
            catch(IOException ioEx)
            {
                ioEx.printStackTrace();
            }
            catch(ScriptException scrEx)
            {
                scrEx.printStackTrace();
            }
        }
    }
     eval() 方法也可以直接操作一個 String,所以這個腳本不一定必須是文件系統的一個文件 — 它可以來自于數據庫、用戶輸入,或者甚至可以基于環境和用戶操作在應用程序中生成。
    將 Java 對象綁定到腳本空間
     僅僅調用一個腳本還不夠:腳本通常會與 Java 環境中創建的對象進行交互。這時,Java 主機環境必須創建一些對象并將它們綁定,這樣腳本就可以很容易找到和使用這些對象。這個過程是 ScriptContext 對象的任務,如清單 6 所示:
    清單 6. 為腳本綁定對象
    import java.io.*;
    import javax.script.*;

    public class App
    {
        public static void main(String[] args)
        {
            try
            {
                ScriptEngine engine =
                    new ScriptEngineManager().getEngineByName("javascript");
                   
                for (String arg : args)
                {
                    Bindings bindings = new SimpleBindings();
                    bindings.put("author", new Person("Ted", "Neward", 39));
                    bindings.put("title", "5 Things You Didn't Know");
                   
                    FileReader fr = new FileReader(arg);
                    engine.eval(fr, bindings);
                }
            }
            catch(IOException ioEx)
            {
                ioEx.printStackTrace();
            }
            catch(ScriptException scrEx)
            {
                scrEx.printStackTrace();
            }
        }
    }
     訪問所綁定的對象很簡單 — 所綁定對象的名稱是作為全局命名空間引入到腳本的,所以在 Rhino 中使用 Person 很簡單,如清單 7 所示:
    清單 7. 是誰撰寫了本文?
    println("Hello from inside scripting!")

    println("author.firstName = " + author.firstName)
     您可以看到,JavaBeans 樣式的屬性被簡化為使用名稱直接訪問,這就好像它們是字段一樣。
    編譯頻繁使用的腳本
     腳本語言的缺點一直存在于性能方面。其中的原因是,大多數情況下腳本語言是 “即時” 解譯的,因而它在執行時會損失一些解析和驗證文本的時間和 CPU 周期。運行在 JVM 的許多腳本語言終會將接收的代碼轉換為 Java 字節碼,至少在腳本被第一次解析和驗證時進行轉換;在 Java 程序關閉時,這些即時編譯的代碼會消失。將頻繁使用的腳本保持為字節碼形式可以幫助提升可觀的性能。
     我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實現了 Compilable 接口,那么這個接口所編譯的方法可用于將腳本(以一個 String 或一個 Reader 傳遞過來的)編譯為一個 CompiledScript 實例,然后它可用于在 eval() 方法中使用不同的綁定重復地處理編譯后的代碼,如清單 8 所示:
    清單 8. 編譯解譯后的代碼
    import java.io.*;
    import javax.script.*;

    public class App
    {
        public static void main(String[] args)
        {
            try
            {
                ScriptEngine engine =
                    new ScriptEngineManager().getEngineByName("javascript");
                   
                for (String arg : args)
                {
                    Bindings bindings = new SimpleBindings();
                    bindings.put("author", new Person("Ted", "Neward", 39));
                    bindings.put("title", "5 Things You Didn't Know");
                   
                    FileReader fr = new FileReader(arg);
                    if (engine instanceof Compilable)
                    {
                        System.out.println("Compiling....");
                        Compilable compEngine = (Compilable)engine;
                        CompiledScript cs = compEngine.compile(fr);
                        cs.eval(bindings);
                    }
                    else
                        engine.eval(fr, bindings);
                }
            }
            catch(IOException ioEx)
            {
                ioEx.printStackTrace();
            }
            catch(ScriptException scrEx)
            {
                scrEx.printStackTrace();
            }
        }
    }
     在大多數情況中,CompiledScript 實例需要存儲在一個長時間存儲中(例如,servlet-context),這樣才能避免一次次地重復編譯相同的腳本。然而,如果腳本發生變化,您就需要創建一個新的 CompiledScript 來反映這個變化;一旦編譯完成,CompiledScript 就不再執行原始的腳本文件內容。
    Java 異常處理及其應用
    Java 異常處理引出
     假設您要編寫一個 Java 程序,該程序讀入用戶輸入的一行文本,并在終端顯示該文本。
     程序如下:
    1 import java.io.*;
    2 public class EchoInput {
    3      public static void main(String args[]){
    4          System.out.println("Enter text to echo:");
    5          InputStreamReader isr = new InputStreamReader(System.in);
    6          BufferedReader inputReader = new BufferedReader(isr);
    7          String inputLine = inputReader.readLine();
    8          System.out.println("Read:" + inputLine);
    9   }
    10 }
     分析上面的代碼,在 EchoInput 類中,第 3 行聲明了 main 方法;第 4 行提示用戶輸入文本;第 5、6 行設置 BufferedReader 對像連接到 InputStreamReader,而 InputStreamReader 又連接到標準輸入流 System.in;第 7 行讀入一行文本;第 8 行用標準輸出流 System.out 顯示出該文本。
     表面看來上面的程序沒有問題,但實際上,EchoInput 類完全可能出現問題。要在調用第 7 行的 readLine 方法時正確讀取輸入,這幾種假設都必須成立:假定鍵盤有效,鍵盤能與計算機正常通信;假定鍵盤數據可從操作系統傳輸到 Java 虛擬機,又從 Java 虛擬機傳輸 inputReader。
     大多數情況下上述假設都成立,但不盡然。為此,Java 采用異常方法,以應對可能出現的錯誤,并采取步驟進行更正。在本例中,若試圖編譯以上代碼,將看到以下信息:
    Exception in thread "main" java.lang.Error: Unresolved compilation problem:
        Unhandled exception type IOException
        at EchoInput.main(EchoInput.java:7)
     從中可以看到,第 7 行調用 readLine 方法可能出錯:若果真如此,則產生 IOException 來記錄故障。編譯器錯誤是在告訴您,需要更改代碼來解決這個潛在的問題。在 JDK API 文檔中,可以看到同樣的信息。我們可以看到 readLine 方法,如圖 1 所示。
    圖 1. BufferedReader 類的 readLine 方法的 JDK API 文檔
     由圖 1 可知,readLine 方法有時產生 IOException。如何處理潛在的故障?編譯器需要“捕獲”或“聲明”IOException。
     “捕獲 (catch)”指當 readLine 方法產生錯誤時截獲該錯誤,并處理和記錄該問題。而“聲明 (declare)”指錯誤可能引發 IOException,并通知調用該方法的任何代碼:可能產生異常。
     若要捕獲異常,必須添加一個特殊的“處理代碼塊”,來接收和處理 IOException。于是程序改為如下:
    1 import java.io.*;
    2 public class EchoInputHandle {
    3      public static void main(String args[]){
    4          System.out.println("Enter text to echo:");
    5          InputStreamReader isr = new InputStreamReader(System.in);
    6          BufferedReader inputReader = new BufferedReader(isr);
    7          try{
    8              String inputLine = inputReader.readLine();
    9              System.out.println("Read:" + inputLine);
    10          }
    11          catch(IOException exc){
    12              System.out.println(“Exception encountered: ” + exc);
    13          }
    14      }
    15 }
     新添的代碼塊包含關鍵字 try 和 catch(第 7,10,11,13 行),表示要讀取輸入。若成功,則正常運行。若讀取輸入時錯誤,則捕獲問題(由 IOException 對象表示),并采取相應措施。在本例,采用的處理方式是輸出異常。
     若不準備捕獲 IOException,僅聲明異常,則要特別指定 main 方法可能出錯,而且特別說明可能產生 IOException。于是程序改為如下:
    1 import java.io.*;
    2 public class EchoInputDeclare {
    3      public static void main(String args[]) throws IOException{
    4          System.out.println("Enter text to echo:");
    5          InputStreamReader isr = new InputStreamReader(System.in);
    6          BufferedReader inputReader = new BufferedReader(isr);
    7          String inputLine = inputReader.readLine();
    8          System.out.println("Read:" + inputLine);
    9   }
    10 }

     從上面的這個簡單的例子中,我們可以看出異常處理在 Java 代碼開發中不能被忽視。
     Java 異常以及異常處理
     可將 Java 異常看作是一類消息,它傳送一些系統問題、故障及未按規定執行的動作的相關信息。異常包含信息,以將信息從應用程序的一部分發送到另一部分。
     編譯語言為何要處理異常?為何不在異常出現位置隨時處理具體故障?因為有時候我們需要在系統中交流錯誤消息,以便按照統一的方式處理問題,有時是因為有若干處理問題的可能方式,但您不知道使用哪一種,此時,可將處理異常的任務委托給調用方法的代碼。調用者通常更能了解問題來源的上下文,能更好的確定恢復方式。
     圖 2 是一個通用消息架構。
    圖 2. 通用消息架構
     從上圖可以看出,必定在運行的 Java 應用程序的一些類或對象中產生異常。出現故障時,“發送者”將產生異常對象。異常可能代表 Java 代碼出現的問題,也可能是 JVM 的相應錯誤,或基礎硬件或操作系統的錯誤。
     異常本身表示消息,指發送者傳給接收者的數據“負荷”。首先,異常基于類的類型來傳輸有用信息。很多情況下,基于異常的類既能識別故障本因并能更正問題。其次,異常還帶有可能有用的數據(如屬性)。
     在處理異常時,消息必須有接收者;否則將無法處理產生異常的底層問題。
     在上例中,異常“產生者”是讀取文本行的 BufferedReader。在故障出現時,將在 readLine 方法中構建 IOException 對象。異常“接收者”是代碼本身。EchoInputHandle 應用程序的 try-catch 結構中的 catch 塊是異常的接收者,它以字符串形式輸出異常,將問題記錄下來。
    Java 異常類的層次結構
     在我們從總體上了解異常后,我們應該了解如何在 Java 應用程序中使用異常,即需要了解 Java 類的層次結構。圖 3 是 Java 類的層次結構圖。
    圖 3. Java 類的層次結構
     在 Java 中,所有的異常都有一個共同的祖先 Throwable(可拋出)。Throwable 指定代碼中可用異常傳播機制通過 Java 應用程序傳輸的任何問題的共性。
     Throwable 有兩個重要的子類:Exception(異常)和 Error(錯誤),二者都是 Java 異常處理的重要子類,各自都包含大量子類。
     Exception(異常)是應用程序中可能的可預測、可恢復問題。一般大多數異常表示中度到輕度的問題。異常一般是在特定環境下產生的,通常出現在代碼的特定方法和操作中。在 EchoInput 類中,當試圖調用 readLine 方法時,可能出現 IOException 異常。
     Error(錯誤)表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。
     Exception 類有一個重要的子類 RuntimeException。RuntimeException 類及其子類表示“JVM 常用操作”引發的錯誤。例如,若試圖使用空值對象引用、除數為零或數組越界,則分別引發運行時異常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
    Java 異常的處理
     在 Java 應用程序中,對異常的處理有兩種方式:處理異常和聲明異常。
     處理異常:try、catch 和 finally
     若要捕獲異常,則必須在代碼中添加異常處理器塊。這種 Java 結構可能包含 3 個部分,
     都有 Java 關鍵字。下面的例子中使用了 try-catch-finally 代碼結構。
    1 import java.io.*;
    2 public class EchoInputTryCatchFinally {
    3      public static void main(String args[]){
    4          System.out.println("Enter text to echo:");
    5          InputStreamReader isr = new InputStreamReader(System.in);
    6          BufferedReader inputReader = new BufferedReader(isr);
    7          try{
    8              String inputLine = inputReader.readLine();
    9              System.out.println("Read:" + inputLine);    
    10          }
    11          catch(IOException exc){
    12              System.out.println("Exception encountered: " + exc);
    13          }
    14          finally{
    15             System.out.println("End. ");
    16      }
    17 }
    18}
     其中:
    try 塊:將一個或者多個語句放入 try 時,則表示這些語句可能拋出異常。編譯器知道可能要發生異常,于是用一個特殊結構評估塊內所有語句。
    catch 塊:當問題出現時,一種選擇是定義代碼塊來處理問題,catch 塊的目的便在于此。catch 塊是 try 塊所產生異常的接收者。基本原理是:一旦生成異常,則 try 塊的執行中止,JVM 將查找相應的 JVM。
    finally 塊:還可以定義 finally 塊,無論運行 try 塊代碼的結果如何,該塊里面的代碼一定運行。在常見的所有環境中,finally 塊都將運行。無論 try 塊是否運行完,無論是否產生異常,也無論是否在 catch 塊中得到處理,finally 塊都將執行。
     try-catch-finally 規則:
    必須在 try 之后添加 catch 或 finally 塊。try 塊后可同時接 catch 和 finally 塊,但至少有一個塊。
    必須遵循塊順序:若代碼同時使用 catch 和 finally 塊,則必須將 catch 塊放在 try 塊之后。
    catch 塊與相應的異常類的類型相關。
    一個 try 塊可能有多個 catch 塊。若如此,則執行第一個匹配塊。
    可嵌套 try-catch-finally 結構。
    在 try-catch-finally 結構中,可重新拋出異常。
    除了下列情況,總將執行 finally 做為結束:JVM 過早終止(調用 System.exit(int));在 finally 塊中拋出一個未處理的異常;計算機斷電、失火、或遭遇病毒攻擊。
     聲明異常
     若要聲明異常,則必須將其添加到方法簽名塊的結束位置。下面是一個實例:
    public void errorProneMethod(int input) throws java.io.IOException {
        //Code for the method,including one or more method
        //calls that may produce an IOException
    }
       
     這樣,聲明的異常將傳給方法調用者,而且也通知了編譯器:該方法的任何調用者必須遵守處理或聲明規則。聲明異常的規則如下:
    必須聲明方法可拋出的任何可檢測異常(checked exception)。
    非檢測性異常(unchecked exception)不是必須的,可聲明,也可不聲明。
    調用方法必須遵循任何可檢測異常的處理和聲明規則。若覆蓋一個方法,則不能聲明與覆蓋方法不同的異常。聲明的任何異常必須是被覆蓋方法所聲明異常的同類或子類。
    Java 異常處理的分類
     Java 異常可分為可檢測異常,非檢測異常和自定義異常。
     可檢測異常
     可檢測異常經編譯器驗證,對于聲明拋出異常的任何方法,編譯器將強制執行處理或聲明規則,例如:sqlExecption 這個異常就是一個檢測異常。你連接 JDBC 時,不捕捉這個異常,編譯器就通不過,不允許編譯。
     非檢測異常
     非檢測異常不遵循處理或聲明規則。在產生此類異常時,不一定非要采取任何適當操作,編譯器不會檢查是否已解決了這樣一個異常。例如:一個數組為 3 個長度,當你使用下標為3時,就會產生數組下標越界異常。這個異常 JVM 不會進行檢測,要靠程序員來判斷。有兩個主要類定義非檢測異常:RuntimeException 和 Error。
     Error 子類屬于非檢測異常,因為無法預知它們的產生時間。若 Java 應用程序內存不足,則隨時可能出現 OutOfMemoryError;起因一般不是應用程序的特殊調用,而是 JVM 自身的問題。另外,Error 一般表示應用程序無法解決的嚴重問題。
     RuntimeException 類也屬于非檢測異常,因為普通 JVM 操作引發的運行時異常隨時可能發生,此類異常一般是由特定操作引發。但這些操作在 Java 應用程序中會頻繁出現。因此,它們不受編譯器檢查與處理或聲明規則的限制。
     自定義異常
     自定義異常是為了表示應用程序的一些錯誤類型,為代碼可能發生的一個或多個問題提供新含義。可以顯示代碼多個位置之間的錯誤的相似性,也可以區分代碼運行時可能出現的相似問題的一個或者多個錯誤,或給出應用程序中一組錯誤的特定含義。例如,對隊列進行操作時,有可能出現兩種情況:空隊列時試圖刪除一個元素;滿隊列時試圖添加一個元素。則需要自定義兩個異常來處理這兩種情況。
    Java 異常處理的原則和忌諱
    Java 異常處理的原則
    盡可能的處理異常
     要盡可能的處理異常,如果條件確實不允許,無法在自己的代碼中完成處理,就考慮聲明異常。如果人為避免在代碼中處理異常,僅作聲明,則是一種錯誤和依賴的實踐。
    具體問題具體解決
     異常的部分優點在于能為不同類型的問題提供不同的處理操作。有效異常處理的關鍵是識別特定故障場景,并開發解決此場景的特定相應行為。為了充分利用異常處理能力,需要為特定類型的問題構建特定的處理器塊。
    記錄可能影響應用程序運行的異常
     至少要采取一些永久的方式,記錄下可能影響應用程序操作的異常。理想情況下,當然是在第一時間解決引發異常的基本問題。不過,無論采用哪種處理操作,一般總應記錄下潛在的關鍵問題。別看這個操作很簡單,但它可以幫助您用很少的時間來跟蹤應用程序中復雜問題的起因。
    根據情形將異常轉化為業務上下文
     若要通知一個應用程序特有的問題,有必要將應用程序轉換為不同形式。若用業務特定狀態表示異常,則代碼更易維護。從某種意義上講,無論何時將異常傳到不同上下文(即另一技術層),都應將異常轉換為對新上下文有意義的形式。
    Java 異常處理的忌諱
    一般不要忽略異常
     在異常處理塊中,一項危險的舉動是“不加通告”地處理異常。如下例所示:
    1   try{
    2       Class.forName("business.domain.Customer");
    3   }
    4   catch (ClassNotFoundException exc){}
     經常能夠在代碼塊中看到類似的代碼塊。有人總喜歡在編寫代碼時簡單快速地編寫空處理器塊,并“自我安慰地”宣稱準備在“后期”添加恢復代碼,但這個“后期”變成了“無期”。
     這種做法有什么壞處?如果異常對應用程序的其他部分確實沒有任何負面影響,這未嘗不可。但事實往往并非如此,異常會擾亂應用程序的狀態。此時,這樣的代碼無異于掩耳盜鈴。
     這種做法若影響較輕,則應用程序可能出現怪異行為。例如,應用程序設置的一個值不見了, 或 GUI 失效。若問題嚴重,則應用程序可能會出現重大問題,因為異常未記錄原始故障點,難以處理,如重復的 NullPointerExceptions。
     如果采取措施,記錄了捕獲的異常,則不可能遇到這個問題。實際上,除非確認異常對代碼其余部分絕無影響,至少也要作記錄。進一步講,永遠不要忽略問題;否則,風險很大,在后期會引發難以預料的后果。
    不要使用覆蓋式異常處理塊
     另一個危險的處理是覆蓋式處理器(blanket handler)。該代碼的基本結構如下:
    1   try{
    2     // …
    3   }
    4   catch(Exception e){
    5     // …
    6   }
    使用覆蓋式異常處理塊有兩個前提之一:
    代碼中只有一類問題。
     這可能正確,但即便如此,也不應使用覆蓋式異常處理,捕獲更具體的異常形式有利物弊。
    單個恢復操作始終適用。
     這幾乎絕對錯誤。幾乎沒有哪個方法能放之四海而皆準,能應對出現的任何問題。
     分析下這樣編寫代碼將發生的情況。只要方法不斷拋出預期的異常集,則一切正常。但是,如果拋出了未預料到的異常,則無法看到要采取的操作。當覆蓋式處理器對新異常類執行千篇一律的任務時,只能間接看到異常的處理結果。如果代碼沒有打印或記錄語句,則根本看不到結果。
     更糟糕的是,當代碼發生變化時,覆蓋式處理器將繼續作用于所有新異常類型,并以相同方式處理所有類型。
     一般不要把特定的異常轉化為更通用的異常
     將特定的異常轉換為更通用異常時一種錯誤做法。一般而言,這將取消異常起初拋出時產生的上下文,在將異常傳到系統的其他位置時,將更難處理。見下例:
    1   try{
    2     // Error-prone code
    3   }
    4   catch(IOException e){
    5      String msg = "If you didn ’ t have a problem before,you do now!";
    6      throw new Exception(msg);
    7   }
     因為沒有原始異常的信息,所以處理器塊無法確定問題的起因,也不知道如何更正問題。
     不要處理能夠避免的異常
     對于有些異常類型,實際上根本不必處理。通常運行時異常屬于此類范疇。在處理空指針或者數據索引等問題時,不必求助于異常處理。
    Java 異常處理的應用實例
     在定義銀行類時,若取錢數大于余額時需要做異常處理。
     定義一個異常類 insufficientFundsException。取錢(withdrawal)方法中可能產生異常,條件是余額小于取額。
     處理異常在調用 withdrawal 的時候,因此 withdrawal 方法要聲明拋出異常,由上一級方法調用。
    異常類:
    class InsufficientFundsExceptionextends Exception{
       private Bank  excepbank;      // 銀行對象
       private double excepAmount;   // 要取的錢
       InsufficientFundsException(Bank ba, double  dAmount)
        {  excepbank=ba;
           excepAmount=dAmount;
       }
       public String excepMessage(){
          String  str="The balance is"+excepbank.balance
           + "\n"+"The withdrawal was"+excepAmount;
          return str;  
       }
    }// 異常類
    銀行類:
    class Bank{
       double balance;// 存款數
       Bank(double  balance){this.balance=balance;}
       public void deposite(double dAmount){
         if(dAmount>0.0) balance+=dAmount;
       }
       public void withdrawal(double dAmount)
                   throws  InsufficientFundsException{
         if (balance<dAmount)     throw new
              InsufficientFundsException(this, dAmount);
          balance=balance-dAmount;
       }
       public void showBalance(){
          System.out.println("The balance is "+(int)balance);
       }
    }
    前端調用:
    public class ExceptionDemo{
       public static void main(String args[]){
         try{
            Bank ba=new Bank(50);
              ba.withdrawal(100);
              System.out.println("Withdrawal successful!");
          }catch(InsufficientFundsException e) {
              System.out.println(e.toString());
              System.out.println(e.excepMessage());
          }
       }
    }
    關于 JVM 命令行標志您不知道的 5 件事
    DisableExplicitGC
     我已記不清有多少次用戶要求我就應用程序性能問題提供咨詢了,其實只要跨代碼快速運行 grep,就會發現清單 1 所示的問題 — 原始 java 性能反模式:
    清單 1. System.gc();
    // We just released a bunch of objects, so tell the stupid
    // garbage collector to collect them already!
    System.gc();
     顯式垃圾收集是一個非常糟糕的主意 — 就像將您和一個瘋狂的斗牛犬鎖在一個電話亭里。盡管調用的語法是依賴實現的,但如果您的 JVM 正在運行一個分代的垃圾回收器(大多數是)System.gc(); 強迫 VM 執行一個堆的 “全部清掃”,雖然有的沒有必要。全部清掃比一個常規 GC 操作要昂貴好幾個數量級,這只是個簡單數學問題。
     您可以不把我的話放在心上 — Sun 的工程師為這個特殊的人工錯誤提供一個 JVM 標志; -XX:+DisableExplicitGC 標志自動將 System.gc() 調用轉換成一個空操作,為您提供運行代碼的機會,您自己看看 System.gc() 對于整個 JVM 執行有害還是有利。
    HeapDumpOnOutOfMemoryError
     您有沒有經歷過這樣的情況:JVM 不能使用,不斷拋出 OutOfMemoryError,而您又不能為自己創建調試器來捕獲它或查看出現了什么問題?像這類偶發和/或不確定的問題,通常使開發人員發瘋。
     買者自負
     并不是任何 VM 都支持所有命令行標志,Sun/Oracle 的 VM 除外。查明一個標志是否被支持的好方法是試用它,看它是否正常工作。倘若這些標志在技術上是不支持的,那么,使用它們您要承擔全部責任。如果這些標志中的任何一個使您的代碼、您的數據、您的服務器或您的一切消失得無影無蹤,我、Sun/Oracle 和 IBM® 都將不負責任。為以防萬一,建議先在虛擬(非常生產)環境中實驗。
     在這個時刻您想要的是,在 JVM 消亡之際捕獲堆的一個快照 — 正好 -XX:+HeapDumpOnOutOfMemoryError 命令可以完成這一操作。
     運行該命令通知 JVM 拍攝一個 “堆轉儲快照”,并將其保存在一個文件中以便處理,通常使用 jhat 實用工具(我在 上一篇文章 中介紹過)。您可以使用相應的 -XX:HeapDumpPath 標志指定到保存文件的實際路徑。(不管文件保存在哪,務必確保文件系統和/或 Java 流程必須要有權限配置,可以在其中寫入。)
    bootclasspath
     定期將一個類放入類路徑是很有幫助的,這類路徑與庫存 JRE 附帶的類路徑或者以某種方式擴展的 JRE 類路徑略有不同。(新 Java Crypto API 提供商就是一個例子)。如果您想要擴展 JRE ,那么您定制的實現必須可以使用引導程序 ClassLoader,該引導程序可以加載 rt.jar 中的 java.lang.Object 及其所有相關文件。
     盡管您可以 非法打開 rt.jar 并將您的定制實現或新數據包移入其中,但從技術上您就違反了您下載 JDK 時同意的協議了。
     相反,使用 JVM 自己的 -Xbootclasspath 選項,以及皮膚 -Xbootclasspath/p 和 -Xbootclasspath/a。
     -Xbootclasspath 使您可以設置完整的引導類路徑(這通常包括一個對 rt.jar 的引用),以及一些其他 JDK 附帶的(不是 rt.jar 的一部分)JAR 文件。-Xbootclasspath/p 將值前置到現有 bootclasspath 中,并將 -Xbootclasspath/a 附加到其中。
     例如,如果您修改了庫中的 java.lang.Integer,并將修改放在一個子路徑 mods 下,那么 -Xbootclasspath/a mods 參數將新 Integer 放在默認的參數前面。
    verbose
     對于虛擬的或任何類型的 Java 應用程序,-verbose 是一個很有用的一級診斷使用程序。該標志有三個子標志:gc、class 和 jni。
     開發人員嘗試尋找是否 JVM 垃圾收集器發生故障或者導致性能低下,通常首先要做的就是執行 gc。不幸的是,解釋 gc 輸出很麻煩 — 足夠寫一本書。更糟糕的是,在命令行中打印的輸出在不同的 Java 版本中或者不在不同的 JVM 中會發生改變,這使得正確解釋變得更難。
     一般來說,如果垃圾收集器是一個分代收集器(多數 “企業級” VMs 都是)。某種虛擬標志將會出現,來指出一個全部清掃 GC 通路;在 Sun JVM 中,標志在 GC 輸出行的開始以 “[Full GC ...]” 形式出現。
     想要診斷 ClassLoader 和/或不匹配的類沖突,class 可以幫上大忙。它不僅報告類何時加載,還報告類從何處加載,包括到 JAR 的路徑(如果來自 JAR)。
     jni 很少使用,除了使用 JNI 或本地庫時。打開時,它將報告各種 JNI 事件,比如,本地庫何時加載,方法何時彈回;再一次強調,在不同 JVM 版本中,輸出會發生變化。
    Command-line -X
     我列出了 JVM 中提供的我喜歡的命令行選項,但是還有一些更多的需要您自己發現,運行命令行參數 -X,列出 JVM 提供的所有非標準(但大部分都是安全的)參數 — 例如:
    -Xint,在解釋模式下運行 JVM(對于測試 JIT 編譯器實際上是否對您的代碼起作用或者驗證是否 JIT 編譯器中有一個 bug,這都很有用)。
    -Xloggc:,和 -verbose:gc 做同樣的事,但是記錄一個文件而不輸出到命令行窗口。
     JVM 命令行選項時常發生變化,因此,定期查看是一個好主意。甚至,您深夜盯著監控器和下午 5 點回家和妻子孩子吃頓晚飯,(或者在 Mass Effect 2 中消滅您的敵人,根據您的喜好),它們都是不一樣的。
    關于 java.util.concurrent 您不知道的 5 件事,第 1 部分
     Concurrent Collections 是 Java™ 5 的巨大附加產品,但是在關于注釋和泛型的爭執中很多 Java 開發人員忽視了它們。此外(或者更老實地說),許多開發人員避免使用這個數據包,因為他們認為它一定很復雜,就像它所要解決的問題一樣。
     事實上,java.util.concurrent 包含許多類,能夠有效解決普通的并發問題,無需復雜工序。閱讀本文,了解 java.util.concurrent 類,比如 CopyOnWriteArrayList 和 BlockingQueue 如何幫助您解決多線程編程的棘手問題。
    TimeUnit
     盡管本質上 不是 Collections 類,但 java.util.concurrent.TimeUnit 枚舉讓代碼更易讀懂。使用 TimeUnit 將使用您的方法或 API 的開發人員從毫秒的 “暴政” 中解放出來。
     TimeUnit 包括所有時間單位,從 MILLISECONDS 和 MICROSECONDS 到 DAYS 和 HOURS,這就意味著它能夠處理一個開發人員所需的幾乎所有的時間范圍類型。同時,因為在列舉上聲明了轉換方法,在時間加快時,將 HOURS 轉換回 MILLISECONDS 甚至變得更容易。
    CopyOnWriteArrayList
     創建數組的全新副本是過于昂貴的操作,無論是從時間上,還是從內存開銷上,因此在通常使用中很少考慮;開發人員往往求助于使用同步的 ArrayList。然而,這也是一個成本較高的選擇,因為每當您跨集合內容進行迭代時,您就不得不同步所有操作,包括讀和寫,以此保證一致性。
     這又讓成本結構回到這樣一個場景:需多讀者都在讀取 ArrayList,但是幾乎沒人會去修改它。
     CopyOnWriteArrayList 是個巧妙的小寶貝,能解決這一問題。它的 Javadoc 將 CopyOnWriteArrayList 定義為一個 “ArrayList 的線程安全變體,在這個變體中所有易變操作(添加,設置等)可以通過復制全新的數組來實現”。
     集合從內部將它的內容復制到一個沒有修改的新數組,這樣讀者訪問數組內容時就不會產生同步成本(因為他們從來不是在易變數據上操作)。
     本質上講,CopyOnWriteArrayList 很適合處理 ArrayList 經常讓我們失敗的這種場景:讀取頻繁,但很少有寫操作的集合,例如 JavaBean 事件的 Listeners。
    BlockingQueue
     BlockingQueue 接口表示它是一個 Queue,意思是它的項以先入先出(FIFO)順序存儲。在特定順序插入的項以相同的順序檢索 — 但是需要附加保證,從空隊列檢索一個項的任何嘗試都會阻塞調用線程,直到這個項準備好被檢索。同理,想要將一個項插入到滿隊列的嘗試也會導致阻塞調用線程,直到隊列的存儲空間可用。
     BlockingQueue 干凈利落地解決了如何將一個線程收集的項“傳遞”給另一線程用于處理的問題,無需考慮同步問題。Java Tutorial 的 Guarded Blocks 試用版就是一個很好的例子。它構建一個單插槽綁定的緩存,當新的項可用,而且插槽也準備好接受新的項時,使用手動同步和 wait()/notifyAll() 在線程之間發信。(詳見 Guarded Blocks 實現。)
     盡管 Guarded Blocks 教程中的代碼有效,但是它耗時久,混亂,而且也并非完全直觀。退回到 Java 平臺較早的時候,沒錯,Java 開發人員不得不糾纏于這種代碼;但現在是 2010 年 — 情況難道沒有改善?
     清單 1 顯示了 Guarded Blocks 代碼的重寫版,其中我使用了一個 ArrayBlockingQueue,而不是手寫的 Drop。
    清單 1. BlockingQueue
    import java.util.*;
    import java.util.concurrent.*;

    class Producer
        implements Runnable
    {
        private BlockingQueue<String> drop;
        List<String> messages = Arrays.asList(
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "Wouldn't you eat ivy too?");
           
        public Producer(BlockingQueue<String> d) { this.drop = d; }
       
        public void run()
        {
            try
            {
                for (String s : messages)
                    drop.put(s);
                drop.put("DONE");
            }
            catch (InterruptedException intEx)
            {
                System.out.println("Interrupted! " +
                    "Last one out, turn out the lights!");
            }
        }   
    }

    class Consumer
        implements Runnable
    {
        private BlockingQueue<String> drop;
        public Consumer(BlockingQueue<String> d) { this.drop = d; }
       
        public void run()
        {
            try
            {
                String msg = null;
                while (!((msg = drop.take()).equals("DONE")))
                    System.out.println(msg);
            }
            catch (InterruptedException intEx)
            {
                System.out.println("Interrupted! " +
                    "Last one out, turn out the lights!");
            }
        }
    }

    public class ABQApp
    {
        public static void main(String[] args)
        {
            BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
            (new Thread(new Producer(drop))).start();
            (new Thread(new Consumer(drop))).start();
        }
    }
     ArrayBlockingQueue 還體現了“公平” — 意思是它為讀取器和編寫器提供線程先入先出訪問。這種替代方法是一個更有效,但又冒窮盡部分線程風險的政策。(即,允許一些讀取器在其他讀取器鎖定時運行效率更高,但是您可能會有讀取器線程的流持續不斷的風險,導致編寫器無法進行工作。)
     注意 Bug!
     順便說一句,如果您注意到 Guarded Blocks 包含一個重大 bug,那么您是對的 — 如果開發人員在 main() 中的 Drop 實例上同步,會出現什么情況呢?
     BlockingQueue 還支持接收時間參數的方法,時間參數表明線程在返回信號故障以插入或者檢索有關項之前需要阻塞的時間。這么做會避免非綁定的等待,這對一個生產系統是致命的,因為一個非綁定的等待會很容易導致需要重啟的系統掛起。
    ConcurrentMap
     Map 有一個微妙的并發 bug,這個 bug 將許多不知情的 Java 開發人員引入歧途。ConcurrentMap 是容易的解決方案。
     當一個 Map 被從多個線程訪問時,通常使用 containsKey() 或者 get() 來查看給定鍵是否在存儲鍵/值對之前出現。但是即使有一個同步的 Map,線程還是可以在這個過程中潛入,然后奪取對 Map 的控制權。問題是,在對 put() 的調用中,鎖在 get() 開始時獲取,然后在可以再次獲取鎖之前釋放。它的結果是個競爭條件:這是兩個線程之間的競爭,結果也會因誰先運行而不同。
     如果兩個線程幾乎同時調用一個方法,兩者都會進行測試,調用 put,在處理中丟失第一線程的值。幸運的是,ConcurrentMap 接口支持許多附加方法,它們設計用于在一個鎖下進行兩個任務:putIfAbsent(),例如,首先進行測試,然后僅當鍵沒有存儲在 Map 中時進行 put。
    SynchronousQueues
     根據 Javadoc,SynchronousQueue 是個有趣的東西:
     這是一個阻塞隊列,其中,每個插入操作必須等待另一個線程的對應移除操作,反之亦然。一個同步隊列不具有任何內部容量,甚至不具有 1 的容量。
     本質上講,SynchronousQueue 是之前提過的 BlockingQueue 的又一實現。它給我們提供了在線程之間交換單一元素的極輕量級方法,使用 ArrayBlockingQueue 使用的阻塞語義。在清單 2 中,我重寫了 清單 1 的代碼,使用 SynchronousQueue 替代 ArrayBlockingQueue:
    清單 2. SynchronousQueue
    import java.util.*;
    import java.util.concurrent.*;

    class Producer
        implements Runnable
    {
        private BlockingQueue<String> drop;
        List<String> messages = Arrays.asList(
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "Wouldn't you eat ivy too?");
           
        public Producer(BlockingQueue<String> d) { this.drop = d; }
       
        public void run()
        {
            try
            {
                for (String s : messages)
                    drop.put(s);
                drop.put("DONE");
            }
            catch (InterruptedException intEx)
            {
                System.out.println("Interrupted! " +
                    "Last one out, turn out the lights!");
            }
        }   
    }

    class Consumer
        implements Runnable
    {
        private BlockingQueue<String> drop;
        public Consumer(BlockingQueue<String> d) { this.drop = d; }
       
        public void run()
        {
            try
            {
                String msg = null;
                while (!((msg = drop.take()).equals("DONE")))
                    System.out.println(msg);
            }
            catch (InterruptedException intEx)
            {
                System.out.println("Interrupted! " +
                    "Last one out, turn out the lights!");
            }
        }
    }

    public class SynQApp
    {
        public static void main(String[] args)
        {
            BlockingQueue<String> drop = new SynchronousQueue<String>();
            (new Thread(new Producer(drop))).start();
            (new Thread(new Consumer(drop))).start();
        }
    }
     實現代碼看起來幾乎相同,但是應用程序有額外獲益:SynchronousQueue 允許在隊列進行一個插入,只要有一個線程等著使用它。
     在實踐中,SynchronousQueue 類似于 Ada 和 CSP 等語言中可用的 “會合通道”。這些通道有時在其他環境中也稱為 “連接”,這樣的環境包括 .NET (見 參考資料)。
    關于 java.util.concurrent 您不知道的 5 件事,第 2 部分
     并發 Collections 提供了線程安全、經過良好調優的數據結構,簡化了并發編程。然而,在一些情形下,開發人員需要更進一步,思考如何調節和/或限制線程執行。由于 java.util.concurrent 的總體目標是簡化多線程編程,您可能希望該包包含同步實用程序,而它確實包含。
     本文是 第 1 部分 的延續,將介紹幾個比核心語言原語(監視器)更高級的同步結構,但它們還未包含在 Collection 類中。一旦您了解了這些鎖和門的用途,使用它們將非常直觀。
    Semaphore
     在一些企業系統中,開發人員經常需要限制未處理的特定資源請求(線程/操作)數量,事實上,限制有時候能夠提高系統的吞吐量,因為它們減少了對特定資源的爭用。盡管完全可以手動編寫限制代碼,但使用 Semaphore 類可以更輕松地完成此任務,它將幫您執行限制,如清單 1 所示:
    清單 1. 使用 Semaphore 執行限制
    import java.util.*;import java.util.concurrent.*;

    public class SemApp
    {
        public static void main(String[] args)
        {
            Runnable limitedCall = new Runnable() {
                final Random rand = new Random();
                final Semaphore available = new Semaphore(3);
                int count = 0;
                public void run()
                {
                    int time = rand.nextInt(15);
                    int num = count++;
                   
                    try
                    {
                        available.acquire();
                       
                        System.out.println("Executing " +
                            "long-running action for " +
                            time + " seconds... #" + num);
                   
                        Thread.sleep(time * 1000);

                        System.out.println("Done with #" +
                            num + "!");

                        available.release();
                    }
                    catch (InterruptedException intEx)
                    {
                        intEx.printStackTrace();
                    }
                }
            };
           
            for (int i=0; i<10; i++)
                new Thread(limitedCall).start();
        }
    }
     即使本例中的 10 個線程都在運行(您可以對運行 SemApp 的 Java 進程執行 jstack 來驗證),但只有 3 個線程是活躍的。在一個信號計數器釋放之前,其他 7 個線程都處于空閑狀態。(實際上,Semaphore 類支持一次獲取和釋放多個 permit,但這不適用于本場景。)
    CountDownLatch
     如果 Semaphore 是允許一次進入一個(這可能會勾起一些流行夜總會的保安的記憶)線程的并發性類,那么 CountDownLatch 就像是賽馬場的起跑門柵。此類持有所有空閑線程,直到滿足特定條件,這時它將會一次釋放所有這些線程。
    清單 2. CountDownLatch:讓我們去賽馬吧!
    import java.util.*;
    import java.util.concurrent.*;

    class Race
    {
        private Random rand = new Random();
       
        private int distance = rand.nextInt(250);
        private CountDownLatch start;
        private CountDownLatch finish;
       
        private List<String> horses = new ArrayList<String>();
       
        public Race(String... names)
        {
            this.horses.addAll(Arrays.asList(names));
        }
       
        public void run()
            throws InterruptedException
        {
            System.out.println("And the horses are stepping up to the gate...");
            final CountDownLatch start = new CountDownLatch(1);
            final CountDownLatch finish = new CountDownLatch(horses.size());
            final List<String> places =
                Collections.synchronizedList(new ArrayList<String>());
           
            for (final String h : horses)
            {
                new Thread(new Runnable() {
                    public void run() {
                        try
                        {
                            System.out.println(h +
                                " stepping up to the gate...");
                            start.await();
                           
                            int traveled = 0;
                            while (traveled < distance)
                            {
                                // In a 0-2 second period of time....
                                Thread.sleep(rand.nextInt(3) * 1000);
                               
                                // ... a horse travels 0-14 lengths
                                traveled += rand.nextInt(15);
                                System.out.println(h +
                                    " advanced to " + traveled + "!");
                            }
                            finish.countDown();
                            System.out.println(h +
                                " crossed the finish!");
                            places.add(h);
                        }
                        catch (InterruptedException intEx)
                        {
                            System.out.println("ABORTING RACE!!!");
                            intEx.printStackTrace();
                        }
                    }
                }).start();
            }

            System.out.println("And... they're off!");
            start.countDown();       

            finish.await();
            System.out.println("And we have our winners!");
            System.out.println(places.get(0) + " took the gold...");
            System.out.println(places.get(1) + " got the silver...");
            System.out.println("and " + places.get(2) + " took home the bronze.");
        }
    }

    public class CDLApp
    {
        public static void main(String[] args)
            throws InterruptedException, java.io.IOException
        {
            System.out.println("Prepping...");
           
            Race r = new Race(
                "Beverly Takes a Bath",
                "RockerHorse",
                "Phineas",
                "Ferb",
                "Tin Cup",
                "I'm Faster Than a Monkey",
                "Glue Factory Reject"
                );
           
            System.out.println("It's a race of " + r.getDistance() + " lengths");
           
            System.out.println("Press Enter to run the race....");
            System.in.read();
           
            r.run();
        }
    }
     注意,在 清單 2 中,CountDownLatch 有兩個用途:首先,它同時釋放所有線程,模擬馬賽的起點,但隨后會設置一個門閂模擬馬賽的終點。這樣,“主” 線程就可以輸出結果。 為了讓馬賽有更多的輸出注釋,可以在賽場的 “轉彎處” 和 “半程” 點,比如賽馬跨過跑道的四分之一、二分之一和四分之三線時,添加 CountDownLatch。
    Executor
     清單 1 和 清單 2 中的示例都存在一個重要的缺陷,它們要求您直接創建 Thread 對象。這可以解決一些問題,因為在一些 JVM 中,創建 Thread 是一項重量型的操作,重用現有 Thread 比創建新線程要容易得多。而在另一些 JVM 中,情況正好相反:Thread 是輕量型的,可以在需要時很容易地新建一個線程。當然,如果 Murphy 擁有自己的解決辦法(他通常都會擁有),那么您無論使用哪種方法對于您終將部署的平臺都是不對的。
     JSR-166 專家組(參見 參考資料)在一定程度上預測到了這一情形。Java 開發人員無需直接創建 Thread,他們引入了 Executor 接口,這是對創建新線程的一種抽象。如清單 3 所示,Executor 使您不必親自對 Thread 對象執行 new 就能夠創建新線程:
    清單 3. Executor
    Executor exec = getAnExecutorFromSomeplace();
    exec.execute(new Runnable() { ... });
     使用 Executor 的主要缺陷與我們在所有工廠中遇到的一樣:工廠必須來自某個位置。不幸的是,與 CLR 不同,JVM 沒有附帶一個標準的 VM 級線程池。
     Executor 類實際上 充當著一個提供 Executor 實現實例的共同位置,但它只有 new 方法(例如用于創建新線程池);它沒有預先創建實例。所以您可以自行決定是否希望在代碼中創建和使用 Executor 實例。(或者在某些情況下,您將能夠使用所選的容器/平臺提供的實例。)
     ExecutorService 隨時可以使用
     盡管不必擔心 Thread 來自何處,但 Executor 接口缺乏 Java 開發人員可能期望的某種功能,比如結束一個用于生成結果的線程并以非阻塞方式等待結果可用。(這是桌面應用程序的一個常見需求,用戶將執行需要訪問數據庫的 UI 操作,然后如果該操作花費了很長時間,可能希望在它完成之前取消它。)
     對于此問題,JSR-166 專家創建了一個更加有用的抽象(ExecutorService 接口),它將線程啟動工廠建模為一個可集中控制的服務。例如,無需每執行一項任務就調用一次 execute(),ExecutorService 可以接受一組任務并返回一個表示每項任務的未來結果的未來列表。
    ScheduledExecutorServices
     盡管 ExecutorService 接口非常有用,但某些任務仍需要以計劃方式執行,比如以確定的時間間隔或在特定時間執行給定的任務。這就是 ScheduledExecutorService 的應用范圍,它擴展了 ExecutorService。
     如果您的目標是創建一個每隔 5 秒跳一次的 “心跳” 命令,使用 ScheduledExecutorService 可以輕松實現,如清單 4 所示:
    清單 4. ScheduledExecutorService 模擬心跳
    import java.util.concurrent.*;

    public class Ping
    {
        public static void main(String[] args)
        {
            ScheduledExecutorService ses =
                Executors.newScheduledThreadPool(1);
            Runnable pinger = new Runnable() {
                public void run() {
                    System.out.println("PING!");
                }
            };
            ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS);
        }
    }
     這項功能怎么樣?不用過于擔心線程,不用過于擔心用戶希望取消心跳時會發生什么,也不用明確地將線程標記為前臺或后臺;只需將所有的計劃細節留給 ScheduledExecutorService。
     順便說一下,如果用戶希望取消心跳,scheduleAtFixedRate 調用將返回一個 ScheduledFuture 實例,它不僅封裝了結果(如果有),還擁有一個 cancel 方法來關閉計劃的操作。
    Timeout 方法
     為阻塞操作設置一個具體的超時值(以避免死鎖)的能力是 java.util.concurrent 庫相比起早期并發特性的一大進步,比如監控鎖定。
     這些方法幾乎總是包含一個 int/TimeUnit 對,指示這些方法應該等待多長時間才釋放控制權并將其返回給程序。它需要開發人員執行更多工作 — 如果沒有獲取鎖,您將如何重新獲取? — 但結果幾乎總是正確的:更少的死鎖和更加適合生產的代碼。(關于編寫生產就緒代碼的更多信息,請參見 參考資料 中 Michael Nygard 編寫的 Release It!。)
    結束語
     java.util.concurrent 包還包含了其他許多好用的實用程序,它們很好地擴展到了 Collections 之外,尤其是在 .locks 和 .atomic 包中。深入研究,您還將發現一些有用的控制結構,比如 CyclicBarrier 等。
     與 Java 平臺的許多其他方面一樣,您無需費勁地查找可能非常有用的基礎架構代碼。在編寫多線程代碼時,請記住本文討論的實用程序和 上一篇文章 中討論的實用程序。
    關于 Java Collections API 您不知道的 5 件事,第 1 部分
     對于很多 Java 開發人員來說,Java Collections API 是標準 Java 數組及其所有缺點的一個非常需要的替代品。將 Collections 主要與 ArrayList 聯系到一起本身沒有錯,但是對于那些有探索精神的人來說,這只是 Collections 的冰山一角。
    Collections 比數組好
     剛接觸 Java 技術的開發人員可能不知道,Java 語言初包括數組,是為了應對上世紀 90 年代初期 C++ 開發人員對于性能方面的批評。從那時到現在,我們已經走過一段很長的路,如今,與 Java Collections 庫相比,數組不再有性能優勢。
     例如,若要將數組的內容轉儲到一個字符串,需要迭代整個數組,然后將內容連接成一個 String;而 Collections 的實現都有一個可用的 toString() 實現。
     除少數情況外,好的做法是盡快將遇到的任何數組轉換成集合。于是問題來了,完成這種轉換的容易的方式是什么?事實證明,Java Collections API 使這種轉換變得容易,如清單 1 所示:
    清單 1. ArrayToList
    import java.util.*;

    public class ArrayToList
    {
        public static void main(String[] args)
        {
            // This gives us nothing good
            System.out.println(args);
           
            // Convert args to a List of String
            List<String> argList = Arrays.asList(args);
           
            // Print them out
            System.out.println(argList);
        }
    }
     注意,返回的 List 是不可修改的,所以如果嘗試向其中添加新元素將拋出一個 UnsupportedOperationException。
     而且,由于 Arrays.asList() 使用 varargs 參數表示添加到 List 的元素,所以還可以使用它輕松地用以 new 新建的對象創建 List。
    迭代的效率較低
     將一個集合(特別是由數組轉化而成的集合)的內容轉移到另一個集合,或者從一個較大對象集合中移除一個較小對象集合,這些事情并不鮮見。
     您也許很想對集合進行迭代,然后添加元素或移除找到的元素,但是不要這樣做。
     在此情況下,迭代有很大的缺點:
    每次添加或移除元素后重新調整集合將非常低效。
    每次在獲取鎖、執行操作和釋放鎖的過程中,都存在潛在的并發困境。
    當添加或移除元素時,存取集合的其他線程會引起競爭條件。
     可以通過使用 addAll 或 removeAll,傳入包含要對其添加或移除元素的集合作為參數,來避免所有這些問題。
    用 for 循環遍歷任何 Iterable
     Java 5 中加入 Java 語言的大的便利功能之一,增強的 for 循環,消除了使用 Java 集合的后一道障礙。
     以前,開發人員必須手動獲得一個 Iterator,使用 next() 獲得 Iterator 指向的對象,并通過 hasNext() 檢查是否還有更多可用對象。從 Java 5 開始,我們可以隨意使用 for 循環的變種,它可以在幕后處理上述所有工作。
     實際上,這個增強適用于實現 Iterable 接口的任何對象,而不僅僅是 Collections。
     清單 2 顯示通過 Iterator 提供 Person 對象的孩子列表的一種方法。 這里不是提供內部 List 的一個引用 (這使 Person 外的調用者可以為家庭增加孩子 — 而大多數父母并不希望如此),Person 類型實現 Iterable。這種方法還使得 for 循環可以遍歷所有孩子。
    清單 2. 增強的 for 循環:顯示孩子
    // Person.java
    import java.util.*;

    public class Person
        implements Iterable<Person>
    {
        public Person(String fn, String ln, int a, Person... kids)
        {
            this.firstName = fn; this.lastName = ln; this.age = a;
            for (Person child : kids)
                children.add(child);
        }
        public String getFirstName() { return this.firstName; }
        public String getLastName() { return this.lastName; }
        public int getAge() { return this.age; }
       
        public Iterator<Person> iterator() { return children.iterator(); }
       
        public void setFirstName(String value) { this.firstName = value; }
        public void setLastName(String value) { this.lastName = value; }
        public void setAge(int value) { this.age = value; }
       
        public String toString() {
            return "[Person: " +
                "firstName=" + firstName + " " +
                "lastName=" + lastName + " " +
                "age=" + age + "]";
        }
       
        private String firstName;
        private String lastName;
        private int age;
        private List<Person> children = new ArrayList<Person>();
    }

    // App.java
    public class App
    {
        public static void main(String[] args)
        {
            Person ted = new Person("Ted", "Neward", 39,
                new Person("Michael", "Neward", 16),
                new Person("Matthew", "Neward", 10));

            // Iterate over the kids
            for (Person kid : ted)
            {
                System.out.println(kid.getFirstName());
            }
        }
    }
     在域建模的時候,使用 Iterable 有一些明顯的缺陷,因為通過 iterator() 方法只能那么 “隱晦” 地支持一個那樣的對象集合。但是,如果孩子集合比較明顯,Iterable 可以使針對域類型的編程更容易,更直觀。
    經典算法和定制算法
     您是否曾想過以倒序遍歷一個 Collection?對于這種情況,使用經典的 Java Collections 算法非常方便。
     在上面的 清單 2 中,Person 的孩子是按照傳入的順序排列的;但是,現在要以相反的順序列出他們。雖然可以編寫另一個 for 循環,按相反順序將每個對象插入到一個新的 ArrayList 中,但是 3、4 次重復這樣做之后,就會覺得很麻煩。
     在此情況下,清單 3 中的算法就有了用武之地:
    清單 3. ReverseIterator
    public class ReverseIterator
    {
        public static void main(String[] args)
        {
            Person ted = new Person("Ted", "Neward", 39,
                new Person("Michael", "Neward", 16),
                new Person("Matthew", "Neward", 10));

            // Make a copy of the List
            List<Person> kids = new ArrayList<Person>(ted.getChildren());
            // Reverse it
            Collections.reverse(kids);
            // Display it
            System.out.println(kids);
        }
    }
     Collections 類有很多這樣的 “算法”,它們被實現為靜態方法,以 Collections 作為參數,提供獨立于實現的針對整個集合的行為。
     而且,由于很棒的 API 設計,我們不必完全受限于 Collections 類中提供的算法 — 例如,我喜歡不直接修改(傳入的 Collection 的)內容的方法。所以,可以編寫定制算法是一件很棒的事情,例如清單 4 就是一個這樣的例子:
    清單 4. ReverseIterator 使事情更簡單
    class MyCollections
    {
        public static <T> List<T> reverse(List<T> src)
        {
            List<T> results = new ArrayList<T>(src);
            Collections.reverse(results);
            return results;
        }
    }
    擴展 Collections API
     以上定制算法闡釋了關于 Java Collections API 的一個終觀點:它總是適合加以擴展和修改,以滿足開發人員的特定目的。
     例如,假設您需要 Person 類中的孩子總是按年齡排序。雖然可以編寫代碼一遍又一遍地對孩子排序(也許是使用 Collections.sort 方法),但是通過一個 Collection 類來自動排序要好得多。
     實際上,您甚至可能不關心是否每次按固定的順序將對象插入到 Collection 中(這正是 List 的基本原理)。您可能只是想讓它們按一定的順序排列。
     java.util 中沒有 Collection 類能滿足這些需求,但是編寫一個這樣的類很簡單。只需創建一個接口,用它描述 Collection 應該提供的抽象行為。對于 SortedCollection,它的作用完全是行為方面的。
    清單 5. SortedCollection
    public interface SortedCollection<E> extends Collection<E>
    {
        public Comparator<E> getComparator();
        public void setComparator(Comparator<E> comp);
    }
     編寫這個新接口的實現簡直不值一提:
    清單 6. ArraySortedCollection
    import java.util.*;

    public class ArraySortedCollection<E>
        implements SortedCollection<E>, Iterable<E>
    {
        private Comparator<E> comparator;
        private ArrayList<E> list;
           
        public ArraySortedCollection(Comparator<E> c)
        {
            this.list = new ArrayList<E>();
            this.comparator = c;
        }
        public ArraySortedCollection(Collection<? extends E> src, Comparator<E> c)
        {
            this.list = new ArrayList<E>(src);
            this.comparator = c;
            sortThis();
        }

        public Comparator<E> getComparator() { return comparator; }
        public void setComparator(Comparator<E> cmp) { comparator = cmp; sortThis(); }
       
        public boolean add(E e)
        { boolean r = list.add(e); sortThis(); return r; }
        public boolean addAll(Collection<? extends E> ec)
        { boolean r = list.addAll(ec); sortThis(); return r; }
        public boolean remove(Object o)
        { boolean r = list.remove(o); sortThis(); return r; }
        public boolean removeAll(Collection<?> c)
        { boolean r = list.removeAll(c); sortThis(); return r; }
        public boolean retainAll(Collection<?> ec)
        { boolean r = list.retainAll(ec); sortThis(); return r; }
       
        public void clear() { list.clear(); }
        public boolean contains(Object o) { return list.contains(o); }
        public boolean containsAll(Collection <?> c) { return list.containsAll(c); }
        public boolean isEmpty() { return list.isEmpty(); }
        public Iterator<E> iterator() { return list.iterator(); }
        public int size() { return list.size(); }
        public Object[] toArray() { return list.toArray(); }
        public <T> T[] toArray(T[] a) { return list.toArray(a); }
       
        public boolean equals(Object o)
        {
            if (o == this)
                return true;
           
            if (o instanceof ArraySortedCollection)
            {
                ArraySortedCollection<E> rhs = (ArraySortedCollection<E>)o;
                return this.list.equals(rhs.list);
            }
           
            return false;
        }
        public int hashCode()
        {
            return list.hashCode();
        }
        public String toString()
        {
            return list.toString();
        }
       
        private void sortThis()
        {
            Collections.sort(list, comparator);
        }
    }
     這個實現非常簡陋,編寫時并沒有考慮優化,顯然還需要進行重構。但關鍵是 Java Collections API 從來無意將與集合相關的任何東西定死。它總是需要擴展,同時也鼓勵擴展。
     當然,有些擴展比較復雜,例如 java.util.concurrent 中引入的擴展。但是另一些則非常簡單,只需編寫一個定制算法,或者已有 Collection 類的簡單的擴展。
     擴展 Java Collections API 看上去很難,但是一旦開始著手,您會發現遠不如想象的那樣難。
    關于 Java Collections API 您不知道的 5 件事,第 2 部分
     java.util 中的 Collections 類旨在通過取代數組提高 Java 性能。如您在 第 1 部分 中了解到的,它們也是多變的,能夠以各種方式定制和擴展,幫助實現優質、簡潔的代碼。
     Collections 非常強大,但是很多變:使用它們要小心,濫用它們會帶來風險。
    List 不同于數組
     Java 開發人員常常錯誤地認為 ArrayList 就是 Java 數組的替代品。Collections 由數組支持,在集合內隨機查找內容時性能較好。與數組一樣,集合使用整序數獲取特定項。但集合不是數組的簡單替代。
     要明白數組與集合的區別需要弄清楚順序 和位置 的不同。例如,List 是一個接口,它保存各個項被放入集合中的順序,如清單 1 所示:
    清單 1. 可變鍵值
    import java.util.*;

    public class OrderAndPosition
    {
        public static <T> void dumpArray(T[] array)
        {
            System.out.println("=============");
            for (int i=0; i<array.length; i++)
                System.out.println("Position " + i + ": " + array[i]);
        }
        public static <T> void dumpList(List<T> list)
        {
            System.out.println("=============");
            for (int i=0; i<list.size(); i++)
                System.out.println("Ordinal " + i + ": " + list.get(i));
        }
       
        public static void main(String[] args)
        {
            List<String> argList = new ArrayList<String>(Arrays.asList(args));

            dumpArray(args);
            args[1] = null;
            dumpArray(args);
           
            dumpList(argList);
            argList.remove(1);
            dumpList(argList);
        }
    }
     當第三個元素從上面的 List 中被移除時,其 “后面” 的各項會上升填補空位。很顯然,此集合行為與數組的行為不同(事實上,從數組中移除項與從 List 中移除它也不完全是一回事兒 — 從數組中 “移除” 項意味著要用新引用或 null 覆蓋其索引槽)。
    令人驚訝的 Iterator!
     無疑 Java 開發人員很喜愛 Java 集合 Iterator,但是您后一次使用 Iterator 接口是什么時候的事情了?可以這么說,大部分時間我們只是將 Iterator 隨意放到 for() 循環或加強 for() 循環中,然后就繼續其他操作了。
     但是進行深入研究后,您會發現 Iterator 實際上有兩個十分有用的功能。
     第一,Iterator 支持從源集合中安全地刪除對象,只需在 Iterator 上調用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException,這個異常顧名思意:當打開 Iterator 迭代集合時,同時又在對集合進行修改。有些集合不允許在迭代時刪除或添加元素,但是調用 Iterator 的 remove() 方法是個安全的做法。
     第二,Iterator 支持派生的(并且可能是更強大的)兄弟成員。ListIterator,只存在于 List 中,支持在迭代期間向 List 中添加或刪除元素,并且可以在 List 中雙向滾動。
     雙向滾動特別有用,尤其是在無處不在的 “滑動結果集” 操作中,因為結果集中只能顯示從數據庫或其他集合中獲取的眾多結果中的 10 個。它還可以用于 “反向遍歷” 集合或列表,而無需每次都從前向后遍歷。插入 ListIterator 比使用向下計數整數參數 List.get() “反向” 遍歷 List 容易得多。
    并非所有 Iterable 都來自集合
     Ruby 和 Groovy 開發人員喜歡炫耀他們如何能迭代整個文本文件并通過一行代碼將其內容輸出到控制臺。通常,他們會說在 Java 編程中完成同樣的操作需要很多行代碼:打開 FileReader,然后打開 BufferedReader,接著創建 while() 循環來調用 getLine(),直到它返回 null。當然,在 try/catch/finally 塊中必須要完成這些操作,它要處理異常并在結束時關閉文件句柄。
     這看起來像是一個沒有意義的學術上的爭論,但是它也有其自身的價值。
     他們(包括相當一部分 Java 開發人員)不知道并不是所有 Iterable 都來自集合。Iterable 可以創建 Iterator,該迭代器知道如何憑空制造下一個元素,而不是從預先存在的 Collection 中盲目地處理:
    清單 2. 迭代文件
    // FileUtils.java
    import java.io.*;
    import java.util.*;

    public class FileUtils
    {
        public static Iterable<String> readlines(String filename)
         throws IOException
        {
         final FileReader fr = new FileReader(filename);
         final BufferedReader br = new BufferedReader(fr);
         
         return new Iterable<String>() {
          public <code>Iterator</code><String> iterator() {
           return new <code>Iterator</code><String>() {
            public boolean hasNext() {
             return line != null;
            }
            public String next() {
             String retval = line;
             line = getLine();
             return retval;
            }
            public void remove() {
             throw new UnsupportedOperationException();
            }
            String getLine() {
             String line = null;
             try {
              line = br.readLine();
             }
             catch (IOException ioEx) {
              line = null;
             }
             return line;
            }
            String line = getLine();
           };
          } 
         };
        }
    }

    //DumpApp.java
    import java.util.*;

    public class DumpApp
    {
        public static void main(String[] args)
            throws Exception
        {
            for (String line : FileUtils.readlines(args[0]))
                System.out.println(line);
        }
    }
     此方法的優勢是不會在內存中保留整個內容,但是有一個警告就是,它不能 close() 底層文件句柄(每當 readLine() 返回 null 時就關閉文件句柄,可以修正這一問題,但是在 Iterator 沒有結束時不能解決這個問題)。
    注意可變的 hashCode()
     Map 是很好的集合,為我們帶來了在其他語言(比如 Perl)中經常可見的好用的鍵/值對集合。JDK 以 HashMap 的形式為我們提供了方便的 Map 實現,它在內部使用哈希表實現了對鍵的對應值的快速查找。但是這里也有一個小問題:支持哈希碼的鍵依賴于可變字段的內容,這樣容易產生 bug,即使耐心的 Java 開發人員也會被這些 bug 逼瘋。
     假設清單 3 中的 Person 對象有一個常見的 hashCode() (它使用 firstName、lastName 和 age 字段 — 所有字段都不是 final 字段 — 計算 hashCode()),對 Map 的 get() 調用會失敗并返回 null:
    清單 3. 可變 hashCode() 容易出現 bug
    // Person.java
    import java.util.*;

    public class Person
        implements Iterable<Person>
    {
        public Person(String fn, String ln, int a, Person... kids)
        {
            this.firstName = fn; this.lastName = ln; this.age = a;
            for (Person kid : kids)
                children.add(kid);
        }
       
        // ...
       
        public void setFirstName(String value) { this.firstName = value; }
        public void setLastName(String value) { this.lastName = value; }
        public void setAge(int value) { this.age = value; }
       
        public int hashCode() {
            return firstName.hashCode() & lastName.hashCode() & age;
        }

        // ...

        private String firstName;
        private String lastName;
        private int age;
        private List<Person> children = new ArrayList<Person>();
    }


    // MissingHash.java
    import java.util.*;

    public class MissingHash
    {
        public static void main(String[] args)
        {
            Person p1 = new Person("Ted", "Neward", 39);
            Person p2 = new Person("Charlotte", "Neward", 38);
            System.out.println(p1.hashCode());
           
            Map<Person, Person> map = new HashMap<Person, Person>();
            map.put(p1, p2);
           
            p1.setLastName("Finkelstein");
            System.out.println(p1.hashCode());
           
            System.out.println(map.get(p1));
        }
    }

     很顯然,這種方法很糟糕,但是解決方法也很簡單:永遠不要將可變對象類型用作 HashMap 中的鍵。
    equals() 與 Comparable
     在瀏覽 Javadoc 時,Java 開發人員常常會遇到 SortedSet 類型(它在 JDK 中唯一的實現是 TreeSet)。因為 SortedSet 是 java.util 包中唯一提供某種排序行為的 Collection,所以開發人員通常直接使用它而不會仔細地研究它。清單 4 展示了:
    清單 4. SortedSet,我很高興找到了它!
    import java.util.*;

    public class UsingSortedSet
    {
        public static void main(String[] args)
        {
            List<Person> persons = Arrays.asList(
                new Person("Ted", "Neward", 39),
                new Person("Ron", "Reynolds", 39),
                new Person("Charlotte", "Neward", 38),
                new Person("Matthew", "McCullough", 18)
            );
            SortedSet ss = new TreeSet(new Comparator<Person>() {
                public int compare(Person lhs, Person rhs) {
                    return lhs.getLastName().compareTo(rhs.getLastName());
                }
            });
            ss.addAll(perons);
            System.out.println(ss);
        }
    }
     使用上述代碼一段時間后,可能會發現這個 Set 的核心特性之一:它不允許重復。該特性在 Set Javadoc 中進行了介紹。Set 是不包含重復元素的集合。更準確地說,set 不包含成對的 e1 和 e2 元素,因此如果 e1.equals(e2),那么多包含一個 null 元素。
     但實際上似乎并非如此 — 盡管 清單 4 中沒有相等的 Person 對象(根據 Person 的 equals() 實現),但在輸出時只有三個對象出現在 TreeSet 中。
     與 set 的有狀態本質相反,TreeSet 要求對象直接實現 Comparable 或者在構造時傳入 Comparator,它不使用 equals() 比較對象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。
     因此存儲在 Set 中的對象有兩種方式確定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪種方法取決于上下文。
     更糟的是,簡單的聲明兩者相等還不夠,因為以排序為目的的比較不同于以相等性為目的的比較:可以想象一下按姓排序時兩個 Person 相等,但是其內容卻并不相同。
     一定要明白 equals() 和 Comparable.compareTo() 兩者之間的不同 — 實現 Set 時會返回 0。甚至在文檔中也要明確兩者的區別。
    關于 Java 對象序列化您不知道的 5 件事
     關于本系列
     您覺得自己懂 Java 編程?事實上,大多數程序員對于 Java 平臺都是淺嘗則止,只學習了足以完成手頭上任務的知識而已。在本 系列 中,Ted Neward 深入挖掘 Java 平臺的核心功能,揭示一些鮮為人知的事實,幫助您解決棘手的編程挑戰。
     大約一年前,一個負責管理應用程序所有用戶設置的開發人員,決定將用戶設置存儲在一個 Hashtable 中,然后將這個 Hashtable 序列化到磁盤,以便持久化。當用戶更改設置時,便重新將 Hashtable 寫到磁盤。
     這是一個優雅的、開放式的設置系統,但是,當團隊決定從 Hashtable 遷移到 Java Collections 庫中的 HashMap 時,這個系統便面臨崩潰。
     Hashtable 和 HashMap 在磁盤上的格式是不相同、不兼容的。除非對每個持久化的用戶設置運行某種類型的數據轉換實用程序(極其龐大的任務),否則以后似乎只能一直用 Hashtable 作為應用程序的存儲格式。
     團隊感到陷入僵局,但這只是因為他們不知道關于 Java 序列化的一個重要事實:Java 序列化允許隨著時間的推移而改變類型。當我向他們展示如何自動進行序列化替換后,他們終于按計劃完成了向 HashMap 的轉變。
     本文是本系列的第一篇文章,這個系列專門揭示關于 Java 平臺的一些有用的小知識 — 這些小知識不易理解,但對于解決 Java 編程挑戰遲早有用。
     將 Java 對象序列化 API 作為開端是一個不錯的選擇,因為它從一開始就存在于 JDK 1.1 中。本文介紹的關于序列化的 5 件事情將說服您重新審視那些標準 Java API。
     Java 序列化簡介
     Java 對象序列化是 JDK 1.1 中引入的一組開創性特性之一,用于作為一種將 Java 對象的狀態轉換為字節數組,以便存儲或傳輸的機制,以后,仍可以將字節數組轉換回 Java 對象原有的狀態。
     實際上,序列化的思想是 “凍結” 對象狀態,傳輸對象狀態(寫到磁盤、通過網絡傳輸等等),然后 “解凍” 狀態,重新獲得可用的 Java 對象。所有這些事情的發生有點像是魔術,這要歸功于 ObjectInputStream/ObjectOutputStream 類、完全保真的元數據以及程序員愿意用 Serializable 標識接口標記他們的類,從而 “參與” 這個過程。
     清單 1 顯示一個實現 Serializable 的 Person 類。
    清單 1. Serializable Person
    package com.tedneward;

    public class Person
        implements java.io.Serializable
    {
        public Person(String fn, String ln, int a)
        {
            this.firstName = fn; this.lastName = ln; this.age = a;
        }

        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public int getAge() { return age; }
        public Person getSpouse() { return spouse; }

        public void setFirstName(String value) { firstName = value; }
        public void setLastName(String value) { lastName = value; }
        public void setAge(int value) { age = value; }
        public void setSpouse(Person value) { spouse = value; }

        public String toString()
        {
            return "[Person: firstName=" + firstName +
                " lastName=" + lastName +
                " age=" + age +
                " spouse=" + spouse.getFirstName() +
                "]";
        }   

        private String firstName;
        private String lastName;
        private int age;
        private Person spouse;

    }
     將 Person 序列化后,很容易將對象狀態寫到磁盤,然后重新讀出它,下面的 JUnit 4 單元測試對此做了演示。
    清單 2. 對 Person 進行反序列化
    public class SerTest
    {
        @Test public void serializeToDisk()
        {
            try
            {
                com.tedneward.Person ted = new com.tedneward.Person("Ted", "Neward", 39);
                com.tedneward.Person charl = new com.tedneward.Person("Charlotte",
                    "Neward", 38);

                ted.setSpouse(charl); charl.setSpouse(ted);

                FileOutputStream fos = new FileOutputStream("tempdata.ser");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(ted);
                oos.close();
            }
            catch (Exception ex)
            {
                fail("Exception thrown during test: " + ex.toString());
            }
           
            try
            {
                FileInputStream fis = new FileInputStream("tempdata.ser");
                ObjectInputStream ois = new ObjectInputStream(fis);
                com.tedneward.Person ted = (com.tedneward.Person) ois.readObject();
                ois.close();
               
                assertEquals(ted.getFirstName(), "Ted");
                assertEquals(ted.getSpouse().getFirstName(), "Charlotte");

                // Clean up the file
                new File("tempdata.ser").delete();
            }
            catch (Exception ex)
            {
                fail("Exception thrown during test: " + ex.toString());
            }
        }
    }
     到現在為止,還沒有看到什么新鮮的或令人興奮的事情,但是這是一個很好的出發點。我們將使用 Person 來發現您可能不知道的關于 Java 對象序列化 的 5 件事。
    序列化允許重構
     序列化允許一定數量的類變種,甚至重構之后也是如此,ObjectInputStream 仍可以很好地將其讀出來。
     Java Object Serialization 規范可以自動管理的關鍵任務是:
    將新字段添加到類中
    將字段從 static 改為非 static
    將字段從 transient 改為非 transient
     取決于所需的向后兼容程度,轉換字段形式(從非 static 轉換為 static 或從非 transient 轉換為 transient)或者刪除字段需要額外的消息傳遞。
     重構序列化類
     既然已經知道序列化允許重構,我們來看看當把新字段添加到 Person 類中時,會發生什么事情。
     如清單 3 所示,PersonV2 在原先 Person 類的基礎上引入一個表示性別的新字段。
    清單 3. 將新字段添加到序列化的 Person 中
    enum Gender
    {
        MALE, FEMALE
    }

    public class Person
        implements java.io.Serializable
    {
        public Person(String fn, String ln, int a, Gender g)
        {
            this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g;
        }
     
        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public Gender getGender() { return gender; }
        public int getAge() { return age; }
        public Person getSpouse() { return spouse; }

        public void setFirstName(String value) { firstName = value; }
        public void setLastName(String value) { lastName = value; }
        public void setGender(Gender value) { gender = value; }
        public void setAge(int value) { age = value; }
        public void setSpouse(Person value) { spouse = value; }

        public String toString()
        {
            return "[Person: firstName=" + firstName +
                " lastName=" + lastName +
                " gender=" + gender +
                " age=" + age +
                " spouse=" + spouse.getFirstName() +
                "]";
        }   

        private String firstName;
        private String lastName;
        private int age;
        private Person spouse;
        private Gender gender;
    }
     序列化使用一個 hash,該 hash 是根據給定源文件中幾乎所有東西 — 方法名稱、字段名稱、字段類型、訪問修改方法等 — 計算出來的,序列化將該 hash 值與序列化流中的 hash 值相比較。
     為了使 Java 運行時相信兩種類型實際上是一樣的,第二版和隨后版本的 Person 必須與第一版有相同的序列化版本 hash(存儲為 private static final serialVersionUID 字段)。因此,我們需要 serialVersionUID 字段,它是通過對原始(或 V1)版本的 Person 類運行 JDK serialver 命令計算出的。
     一旦有了 Person 的 serialVersionUID,不僅可以從原始對象 Person 的序列化數據創建 PersonV2 對象(當出現新字段時,新字段被設為缺省值,常見的是“null”),還可以反過來做:即從 PersonV2 的數據通過反序列化得到 Person,這毫不奇怪。
    序列化并不安全
     讓 Java 開發人員詫異并感到不快的是,序列化二進制格式完全編寫在文檔中,并且完全可逆。實際上,只需將二進制序列化流的內容轉儲到控制臺,就足以看清類是什么樣子,以及它包含什么內容。
     這對于安全性有著不良影響。例如,當通過 RMI 進行遠程方法調用時,通過連接發送的對象中的任何 private 字段幾乎都是以明文的方式出現在套接字流中,這顯然容易招致哪怕簡單的安全問題。
     幸運的是,序列化允許 “hook” 序列化過程,并在序列化之前和反序列化之后保護(或模糊化)字段數據。可以通過在 Serializable 對象上提供一個 writeObject 方法來做到這一點。
     模糊化序列化數據
     假設 Person 類中的敏感數據是 age 字段。畢竟,女士忌談年齡。我們可以在序列化之前模糊化該數據,將數位循環左移一位,然后在反序列化之后復位。(您可以開發更安全的算法,當前這個算法只是作為一個例子。)
     為了 “hook” 序列化過程,我們將在 Person 上實現一個 writeObject 方法;為了 “hook” 反序列化過程,我們將在同一個類上實現一個 readObject 方法。重要的是這兩個方法的細節要正確 — 如果訪問修改方法、參數或名稱不同于清單 4 中的內容,那么代碼將不被察覺地失敗,Person 的 age 將暴露。
    清單 4. 模糊化序列化數據
    public class Person
        implements java.io.Serializable
    {
        public Person(String fn, String ln, int a)
        {
            this.firstName = fn; this.lastName = ln; this.age = a;
        }

        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public int getAge() { return age; }
        public Person getSpouse() { return spouse; }
       
        public void setFirstName(String value) { firstName = value; }
        public void setLastName(String value) { lastName = value; }
        public void setAge(int value) { age = value; }
        public void setSpouse(Person value) { spouse = value; }

        private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException
        {
            // "Encrypt"/obscure the sensitive data
            age = age << 2;
            stream.defaultWriteObject();
        }

        private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException
        {
            stream.defaultReadObject();

            // "Decrypt"/de-obscure the sensitive data
            age = age << 2;
        }
       
        public String toString()
        {
            return "[Person: firstName=" + firstName +
                " lastName=" + lastName +
                " age=" + age +
                " spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
                "]";
        }     

        private String firstName;
        private String lastName;
        private int age;
        private Person spouse;
    }
     如果需要查看被模糊化的數據,總是可以查看序列化數據流/文件。而且,由于該格式被完全文檔化,即使不能訪問類本身,也仍可以讀取序列化流中的內容。
    序列化的數據可以被簽名和密封
     上一個技巧假設您想模糊化序列化數據,而不是對其加密或者確保它不被修改。當然,通過使用 writeObject 和 readObject 可以實現密碼加密和簽名管理,但其實還有更好的方式。
     如果需要對整個對象進行加密和簽名,簡單的是將它放在一個 javax.crypto.SealedObject 和/或 java.security.SignedObject 包裝器中。兩者都是可序列化的,所以將對象包裝在 SealedObject 中可以圍繞原對象創建一種 “包裝盒”。必須有對稱密鑰才能解密,而且密鑰必須單獨管理。同樣,也可以將 SignedObject 用于數據驗證,并且對稱密鑰也必須單獨管理。
     結合使用這兩種對象,便可以輕松地對序列化數據進行密封和簽名,而不必強調關于數字簽名驗證或加密的細節。很簡潔,是吧?
    序列化允許將代理放在流中
     很多情況下,類中包含一個核心數據元素,通過它可以派生或找到類中的其他字段。在此情況下,沒有必要序列化整個對象。可以將字段標記為 transient,但是每當有方法訪問一個字段時,類仍然必須顯式地產生代碼來檢查它是否被初始化。
     如果首要問題是序列化,那么好指定一個 flyweight 或代理放在流中。為原始 Person 提供一個 writeReplace 方法,可以序列化不同類型的對象來代替它。類似地,如果反序列化期間發現一個 readResolve 方法,那么將調用該方法,將替代對象提供給調用者。
     打包和解包代理
     writeReplace 和 readResolve 方法使 Person 類可以將它的所有數據(或其中的核心數據)打包到一個 PersonProxy 中,將它放入到一個流中,然后在反序列化時再進行解包。
    清單 5. 你完整了我,我代替了你
    class PersonProxy
        implements java.io.Serializable
    {
        public PersonProxy(Person orig)
        {
            data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge();
            if (orig.getSpouse() != null)
            {
                Person spouse = orig.getSpouse();
                data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + "," 
                  + spouse.getAge();
            }
        }

        public String data;
        private Object readResolve()
            throws java.io.ObjectStreamException
        {
            String[] pieces = data.split(",");
            Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));
            if (pieces.length > 3)
            {
                result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt
                  (pieces[5])));
                result.getSpouse().setSpouse(result);
            }
            return result;
        }
    }

    public class Person
        implements java.io.Serializable
    {
        public Person(String fn, String ln, int a)
        {
            this.firstName = fn; this.lastName = ln; this.age = a;
        }

        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public int getAge() { return age; }
        public Person getSpouse() { return spouse; }

        private Object writeReplace()
            throws java.io.ObjectStreamException
        {
            return new PersonProxy(this);
        }
       
        public void setFirstName(String value) { firstName = value; }
        public void setLastName(String value) { lastName = value; }
        public void setAge(int value) { age = value; }
        public void setSpouse(Person value) { spouse = value; }  

        public String toString()
        {
            return "[Person: firstName=" + firstName +
                " lastName=" + lastName +
                " age=" + age +
                " spouse=" + spouse.getFirstName() +
                "]";
        }   
       
        private String firstName;
        private String lastName;
        private int age;
        private Person spouse;
    }
     注意,PersonProxy 必須跟蹤 Person 的所有數據。這通常意味著代理需要是 Person 的一個內部類,以便能訪問 private 字段。有時候,代理還需要追蹤其他對象引用并手動序列化它們,例如 Person 的 spouse。
     這種技巧是少數幾種不需要讀/寫平衡的技巧之一。例如,一個類被重構成另一種類型后的版本可以提供一個 readResolve 方法,以便靜默地將被序列化的對象轉換成新類型。類似地,它可以采用 writeReplace 方法將舊類序列化成新版本。
    信任,但要驗證
     認為序列化流中的數據總是與初寫到流中的數據一致,這沒有問題。但是,正如一位美國前總統所說的,“信任,但要驗證”。
     對于序列化的對象,這意味著驗證字段,以確保在反序列化之后它們仍具有正確的值,“以防萬一”。為此,可以實現 ObjectInputValidation 接口,并覆蓋 validateObject() 方法。如果調用該方法時發現某處有錯誤,則拋出一個 InvalidObjectException。


    分享到:

    相關閱讀:

    上一篇:Oracle事務講解

    下一篇:ibatis技術文檔

    近期文章

    搶試聽名額

    名額僅剩66名

    教育改變生活

    WE CHANGE LIVES

    主站蜘蛛池模板: heyzo专区无码综合| 狠狠色伊人亚洲综合网站色| 国产成人综合久久精品下载| 国产色产综合色产在线观看视频 | 久久久久久久综合日本| 五月丁香六月综合缴清无码| 思思91精品国产综合在线 | 久久综合伊人77777麻豆| 伊人久久大香线焦综合四虎| 自拍 偷拍 另类 综合图片| 99综合电影在线视频好看| 亚洲综合国产成人丁香五月激情| 一本丁香综合久久久久不卡网站| 久久综合亚洲色一区二区三区| 国产精品亚洲综合天堂夜夜| 狠狠色色综合网站| 国产综合激情在线亚洲第一页| 久久综合综合久久综合| 久久婷婷综合中文字幕| 日韩综合无码一区二区| 成人伊人青草久久综合网破解版| 亚洲妓女综合网99| 五月天激激婷婷大综合丁香| 久久综合久久伊人| 狠狠色综合久久久久尤物| 色偷偷狠狠色综合网| 国产香蕉久久精品综合网| 久久综合中文字幕| 婷婷久久香蕉五月综合加勒比| 久久综合精品国产一区二区三区| 亚洲综合区小说区激情区| 色婷婷久久综合中文久久一本| 狠狠色成人综合网图片区| 狠狠色丁香久久婷婷综合蜜芽五月| 97色伦图片97综合影院久久| 亚洲AV人无码综合在线观看 | 国内精品综合久久久40p| 五月丁香六月综合缴清无码| 伊人久久综合影院| 69国产成人综合久久精品91| 久久综合偷偷噜噜噜色|