四虎免费最新在线永久4HU,中文字幕无码AV激情不卡,国产精品成人免费视频一区,天天躁狠狠躁狠狠躁性色AV

南京北大青鳥

全國(guó)咨詢電話:15195455103

三分鐘了解北大青鳥
當(dāng)前位置:南京北大青鳥 > 學(xué)習(xí)園地 > 編程技巧

關(guān)于Java常用工具您不知道的5件事

來(lái)源:北大青鳥徐州校區(qū)? ? ? 作者:IT教育 ? ??

在千禧年左右,當(dāng) XML 第一次出現(xiàn)在很多 Java 開發(fā)人員面前時(shí),有兩種基本的解析 XML 文件的方法。SAX 解析器實(shí)際是由程序員對(duì)事件調(diào)用一系列回調(diào)方法的大型狀態(tài)機(jī)。DOM 解析器將整個(gè)

StAX – 解析XML文件
 在千禧年左右,當(dāng) XML 第一次出現(xiàn)在很多 Java 開發(fā)人員面前時(shí),有兩種基本的解析 XML 文件的方法。SAX 解析器實(shí)際是由程序員對(duì)事件調(diào)用一系列回調(diào)方法的大型狀態(tài)機(jī)。DOM 解析器將整個(gè) XML 文檔加入內(nèi)存,并切割成離散的對(duì)象,它們連接在一起形成一個(gè)樹。該樹描述了文檔的整個(gè) XML Infoset 表示法。這兩個(gè)解析器都有缺點(diǎn):SAX 太低級(jí),無(wú)法使用,DOM 代價(jià)太大,尤其對(duì)于大的 XML 文件 — 整個(gè)樹成了一個(gè)龐然大物。
 幸運(yùn)的是,Java 開發(fā)人員找到第三種方法來(lái)解析 XML 文件,通過(guò)對(duì)文檔建模成 “節(jié)點(diǎn)”,它們可以從文檔流中一次取出一個(gè),檢查,然后處理或丟棄。這些 “節(jié)點(diǎn)” 的 “流” 提供了 SAX 和 DOM 的中間地帶,名為 “Streaming API for XML”,或者叫做StAX。(此縮寫用于區(qū)分新的 API 與原來(lái)的 SAX 解析器,它與此同名。)StAX 解析器后來(lái)包裝到了 JDK 中,在 javax.xml.stream 包。
 使用 StAX 相當(dāng)簡(jiǎn)單:實(shí)例化 XMLEventReader,將它指向一個(gè)格式良好的 XML 文件,然后一次 “拉出” 一個(gè)節(jié)點(diǎn)(通常用 while 循環(huán)),查看。例如,在清單 1 中,列舉出了 Ant 構(gòu)造腳本中的所有目標(biāo):
清單 1. 只是讓 StAX 指向目標(biāo)
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 解析器不會(huì)替換所有的 SAX 和 DOM 代碼。但肯定會(huì)讓某些任務(wù)容易些。尤其對(duì)完成不需要知道 XML 文檔整個(gè)樹結(jié)構(gòu)的任務(wù)相當(dāng)方便。
 請(qǐng)注意,如果事件對(duì)象級(jí)別太高,無(wú)法使用,StAX 也有一個(gè)低級(jí) API 在 XMLStreamReader 中。盡管也許沒有閱讀器有用,StAX 還有一個(gè) XMLEventWriter,同樣,還有一個(gè) XMLStreamWriter 類用于 XML 輸出。
ServiceLoader – 加載服務(wù)(獲取接口的所有實(shí)現(xiàn))
 Java 開發(fā)人員經(jīng)常希望將使用和創(chuàng)建組件的內(nèi)容區(qū)分開來(lái)。這通常是通過(guò)創(chuàng)建一個(gè)描述組件動(dòng)作的接口,并使用某種中介創(chuàng)建組件實(shí)例來(lái)完成的。很多開發(fā)人員使用 Spring 框架來(lái)完成,但還有其他的方法,它比 Spring 容器更輕量級(jí)。
 java.util 的 ServiceLoader 類能讀取隱藏在 JAR 文件中的配置文件,并找到接口的實(shí)現(xiàn),然后使這些實(shí)現(xiàn)成為可選擇的列表。例如,如果您需要一個(gè)私仆(personal-servant)組件來(lái)完成任務(wù),您可以使用清單 2 中的代碼來(lái)實(shí)現(xiàn):
清單 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() 方法可讓您確定所提供的私仆實(shí)現(xiàn)是否滿足需求。清單 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));
        }
    }
}
 
 假設(shè)有此接口的實(shí)現(xiàn),如清單 4:
清單 4. Jeeves 實(shí)現(xiàn)了 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;
    }
}
 
 剩下的就是配置包含實(shí)現(xiàn)的 JAR 文件,讓 ServiceLoader 能識(shí)別 — 這可能會(huì)非常棘手。JDK 想要 JAR 文件有一個(gè) META-INF/services 目錄,它包含一個(gè)文本文件,其文件名與接口類名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口類名的內(nèi)容是實(shí)現(xiàn)的名稱,每行一個(gè),如清單 5:
清單 5. META-INF/services/IPersonalServant
Jeeves   # comments are OK
 
 幸運(yùn)的是,Ant 構(gòu)建系統(tǒng)(自 1.7.0 以來(lái))包含一個(gè)對(duì) jar 任務(wù)的服務(wù)標(biāo)簽,讓這相對(duì)容易,見清單 6:
清單 6. Ant 構(gòu)建的 IPersonalServant
    <target name="serviceloader" depends="build">
        <jar destfile="misc.jar" basedir="./classes">
            <service type="IPersonalServant">
                <provider classname="Jeeves" />
            </service>
        </jar>
    </target>
 
 這里,很容易調(diào)用 IPersonalServant,讓它執(zhí)行命令。然而,解析和執(zhí)行這些命令可能會(huì)非常棘手。這又是另一個(gè) “小線頭”。
Scanner
 有無(wú)數(shù) Java 工具能幫助您構(gòu)建解析器,很多函數(shù)語(yǔ)言已成功構(gòu)建解析器函數(shù)庫(kù)(解析器選擇器)。但如果要解析的是逗號(hào)分隔值文件,或空格分隔文本文件,又怎么辦呢?大多數(shù)工具用在此處就過(guò)于隆重了,而 String.split() 又不夠。(對(duì)于正則表達(dá)式,請(qǐng)記住一句老話:“ 您有一個(gè)問(wèn)題,用正則表達(dá)式解決。那您就有兩個(gè)問(wèn)題了。”)
 Java 平臺(tái)的 Scanner 類會(huì)是這些類中您好的選擇。以輕量級(jí)文本解析器為目標(biāo),Scanner 提供了一個(gè)相對(duì)簡(jiǎn)單的 API,用于提取結(jié)構(gòu)化文本,并放入強(qiáng)類型的部分。想象一下,如果您愿意,一組類似 DSL 的命令(源自 Terry Pratchett Discworld 小說(shuō))排列在文本文件中,如清單 7:
清單 7. Igor 的任務(wù)
fetch 1 head
fetch 3 eye
fetch 1 foot
attach foot to head
attach eye to head
admire
 您,或者是本例中稱為 Igor的私仆,能輕松使用 Scanner 解析這組違法命令,如清單 8 所示:
清單 8. Igor 的任務(wù),由 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>();
}
 假設(shè) Igor 已在 ServantLoader 中注冊(cè),可以很方便地將 can() 調(diào)用改得更實(shí)用,并重用前面的 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 實(shí)現(xiàn)顯然不會(huì)僅僅打印到標(biāo)準(zhǔn)輸出流。我把追蹤哪些部分、跟隨哪些部分的細(xì)節(jié)留待給您(當(dāng)然,還有忠誠(chéng)的 Igor)。
Timer
 java.util.Timer 和 TimerTask 類提供了方便、相對(duì)簡(jiǎn)單的方法可在定期或一次性延遲的基礎(chǔ)上執(zhí)行任務(wù):
清單 10. 稍后執(zhí)行
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() 重載,它們提示某一任務(wù)是一次性還是重復(fù)的,并且有一個(gè)啟動(dòng)的 TimerTask 實(shí)例。TimerTask 實(shí)際上是一個(gè) Runnable(事實(shí)上,它實(shí)現(xiàn)了它),但還有另外兩個(gè)方法:cancel() 用來(lái)取消任務(wù),scheduledExecutionTime() 用來(lái)返回任務(wù)何時(shí)啟動(dòng)的近似值。
 請(qǐng)注意 Timer 卻創(chuàng)建了一個(gè)非守護(hù)線程在后臺(tái)啟動(dòng)任務(wù),因此在清單 10 中我需要調(diào)用 System.exit() 來(lái)取消任務(wù)。在長(zhǎng)時(shí)間運(yùn)行的程序中,好創(chuàng)建一個(gè) Timer 守護(hù)線程(使用帶有指示守護(hù)線程狀態(tài)的參數(shù)的構(gòu)造函數(shù)),從而它不會(huì)讓 VM 活動(dòng)。
 這個(gè)類沒什么神奇的,但它確實(shí)能幫助我們對(duì)后臺(tái)啟動(dòng)的程序的目的了解得更清楚。它還能節(jié)省一些 Thread 代碼,并作為輕量級(jí) ScheduledExecutorService(對(duì)于還沒準(zhǔn)備好了解整個(gè) java.util.concurrent 包的人來(lái)說(shuō))。
JavaSound
 盡管在服務(wù)器端應(yīng)用程序中不常出現(xiàn),但 sound 對(duì)管理員有著有用的 “被動(dòng)” 意義 — 它是惡作劇的好材料。盡管它很晚才出現(xiàn)在 Java 平臺(tái)中,JavaSound API 終還是加入了核心運(yùn)行時(shí)庫(kù),封裝在 javax.sound * 包 — 其中一個(gè)包是 MIDI 文件,另一個(gè)是音頻文件示例(如普遍的 .WAV 文件格式)。
 JavaSound 的 “hello world” 是播放一個(gè)片段,如清單 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();
    }
}

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

 盡管這個(gè)示例不支持代理對(duì),但它提供了一個(gè)處理時(shí)間基準(zhǔn)來(lái)比較后續(xù)順序訪問(wèn)示例。
 示例 1-2:使用 isSurrogatePair()
 清單 2 使用 isSurrogatePair() 來(lái)計(jì)算代理對(duì)總數(shù)。計(jì)數(shù)之后,它分配足夠的內(nèi)存以便一個(gè)碼位數(shù)組存儲(chǔ)這個(gè)值。然后,它進(jìn)入一個(gè)順序訪問(wèn)循環(huán),使用 isHighSurrogate() 和 isLowSurrogate() 確定每個(gè)代理對(duì)字符是高代理還是低代理。當(dāng)它發(fā)現(xiàn)一個(gè)高代理后面帶一個(gè)低代理時(shí),它使用 toCodePoint() 將該代理對(duì)轉(zhuǎn)換為一個(gè)碼位值并將當(dāng)前索引值增加 2。否則,它將這個(gè) char 類型值直接分配給一個(gè)碼位值并將當(dāng)前索引值增加 1。這個(gè)示例的處理時(shí)間比 示例 1-1 長(zhǎng) 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 中更新軟件的方法很幼稚。它比較麻煩,需要大量修改,使得生成的軟件很脆弱且今后難以更改。具體而言,這些問(wèn)題是:
需要計(jì)算碼位的數(shù)量以分配足夠的內(nèi)存
很難獲得字符串中的指定索引的正確碼位值
很難為下一個(gè)處理步驟正確移動(dòng)當(dāng)前索引
 一個(gè)改進(jìn)后的算法出現(xiàn)在下一個(gè)示例中。
 示例:基本支持
 Java 1.5 提供了 codePointCount()、codePointAt() 和 offsetByCodePoints() 方法來(lái)分別處理 示例 1-2 的 3 個(gè)問(wèn)題。清單 3 使用這些方法來(lái)改善這個(gè)算法的可讀性:
清單 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 的處理時(shí)間比 清單 1 長(zhǎng) 2.8 倍。
 示例 1-4:使用 codePointBefore()
 當(dāng) offsetByCodePoints() 接收一個(gè)負(fù)數(shù)作為第二個(gè)參數(shù)時(shí),它就能計(jì)算一個(gè)距離字符串頭的絕對(duì)偏移值。接下來(lái),codePointBefore() 能夠返回一個(gè)指定索引前面的碼位值。這些方法用于清單 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;
}

 這個(gè)示例的處理時(shí)間 — 比 示例 1-1 長(zhǎng) 2.72 倍 — 比 示例 1-3 快一些。通常,當(dāng)您比較零而不是非零值時(shí),JVM 中的代碼大小要小一些,這有時(shí)會(huì)提高性能。但是,微小的改進(jìn)可能不值得犧牲可讀性。
 示例 1-5:使用 charCount()
 示例 1-3 和 1-4 提供基本的代理對(duì)支持。他們不需要任何臨時(shí)變量,是健壯的編碼方法。要獲取更短的處理時(shí)間,使用 charCount() 而不是 offsetByCodePoints() 是有效的,但需要一個(gè)臨時(shí)變量來(lái)存放碼位值,如清單 5 所示:
清單 5. 使用 charCount() 的優(yōu)化支持
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 的處理時(shí)間降低到比 示例 1-1 長(zhǎng) 1.68 倍。
 示例 1-6:訪問(wèn)一個(gè) char 數(shù)組
 清單 6 在使用 示例 1-5 中展示的優(yōu)化的同時(shí)直接訪問(wèn)一個(gè) char 類型數(shù)組:
清單 6. 使用一個(gè) char 數(shù)組的優(yōu)化支持
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 數(shù)組是使用 toCharArray() 從字符串復(fù)制而來(lái)的。性能得到改善,因?yàn)閷?duì)數(shù)組的直接訪問(wèn)比通過(guò)一個(gè)方法的間接訪問(wèn)要快。處理時(shí)間比 示例 1-1 長(zhǎng) 1.51 倍。但是,當(dāng)調(diào)用時(shí),toCharArray() 需要一些開銷來(lái)創(chuàng)建一個(gè)新數(shù)組并將數(shù)據(jù)復(fù)制到數(shù)組中。String 類提供的那些方便的方法也不能被使用。但是,這個(gè)算法在處理大量數(shù)據(jù)時(shí)有用。
 示例 1-7:一個(gè)面向?qū)ο蟮乃惴?br />  這個(gè)示例的面向?qū)ο笏惴ㄊ褂?CharBuffer 類,如清單 7 所示:
清單 7. 使用 CharSequence 的面向?qū)ο笏惴?br /> 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 不需要一個(gè)索引來(lái)持有當(dāng)前位置以便進(jìn)行順序訪問(wèn)。相反,CharBuffer 在內(nèi)部跟蹤當(dāng)前位置。Character 類提供靜態(tài)方法 codePointCount() 和 codePointAt(),它們能通過(guò) CharSequence 接口處理 CharBuffer。CharBuffer 總是將當(dāng)前位置設(shè)置為 CharSequence 的頭。因此,當(dāng) codePointAt() 被調(diào)用時(shí),第二個(gè)參數(shù)總是設(shè)置為 0。處理時(shí)間比 示例 1-1 長(zhǎng) 2.15 倍。
 處理時(shí)間比較
 這些順序訪問(wèn)示例的計(jì)時(shí)測(cè)試使用了一個(gè)包含 10,000 個(gè)代理對(duì)和 10,000 個(gè)非代理對(duì)的樣例字符串。碼位數(shù)組從這個(gè)字符串創(chuàng)建 10,000 次。測(cè)試環(huán)境包括:
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 的絕對(duì)和相對(duì)處理時(shí)間以及關(guān)聯(lián)的 API:
表 1. 順序訪問(wèn)示例的處理時(shí)間和 API
示例 說(shuō)明 處理時(shí)間(毫秒) 與示例 1-1 的比率 API
1-1  不支持代理對(duì) 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() 的優(yōu)化支持 3406 1.68 Character 類:
static int charCount(int cp)
1-6  使用一個(gè) char 數(shù)組的優(yōu)化支持 3062 1.51 Character 類:
static int codePointAt(char[] ach, int index)
static int codePointCount(char[] ach, int offset, int count)
1-7  使用 CharSequence 的面向?qū)ο蠓椒?nbsp;4360 2.15  Character 類:
static int codePointAt(CharSequence seq, int index)
static int codePointCount(CharSequence seq, int begin, int end)
隨機(jī)訪問(wèn)
 隨機(jī)訪問(wèn)是直接訪問(wèn)一個(gè)字符串中的任意位置。當(dāng)字符串被訪問(wèn)時(shí),索引值基于 16 位 char 類型的單位。但是,如果一個(gè)字符串使用 32 位碼位,那么它不能使用一個(gè)基于 32 位碼位的單位的索引訪問(wèn)。必須使用 offsetByCodePoints() 來(lái)將碼位的索引轉(zhuǎn)換為 char 類型的索引。如果算法設(shè)計(jì)很糟糕,這會(huì)導(dǎo)致很差的性能,因?yàn)?offsetByCodePoints() 總是通過(guò)使用第二個(gè)參數(shù)從第一個(gè)參數(shù)計(jì)算字符串的內(nèi)部。在這個(gè)小節(jié)中,我將比較三個(gè)示例,它們通過(guò)使用一個(gè)短單位來(lái)分割一個(gè)長(zhǎng)字符串。
 示例 2-1:基準(zhǔn)測(cè)試(不支持代理對(duì))
 清單 8 展示如何使用一個(gè)寬度單位來(lái)分割一個(gè)字符串。這個(gè)基準(zhǔn)測(cè)試留作后用,不支持代理對(duì)。
清單 8. 不支持代理對(duì)
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 變量對(duì)分割位置有所限制,以避免在剩余的字符串不足以分割當(dāng)前寬度單位時(shí)拋出一個(gè) IndexOutOfBoundsException 實(shí)例。這種算法在當(dāng)前位置超出 sliceLimit 時(shí)從 while 循環(huán)中跳出后再處理后的分割。
 示例 2-2:使用一個(gè)碼位索引
 清單 9 展示了如何使用一個(gè)碼位索引來(lái)隨機(jī)訪問(wèn)一個(gè)字符串:
清單 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 類型的索引通過(guò) offsetByCodePoints() 用碼位索引替代。
 基本的算法流與 示例 2-1 中的看起來(lái)幾乎一樣。但處理時(shí)間根據(jù)字符串長(zhǎng)度與示例 2-1 的比率同比增加,因?yàn)?offsetByCodePoints() 總是從字符串頭到指定索引計(jì)算字符串內(nèi)部。
 示例 2-3:減少的處理時(shí)間
 可以使用清單 10 中展示的方法來(lái)避免 示例 2-2 的性能問(wèn)題:
清單 10. 改進(jìn)的性能
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 中的)表達(dá)式 len-width 被 offsetByCodePoints(len,-width) 替代。但是,當(dāng) width 的值大于碼位的數(shù)量時(shí),這會(huì)拋出一個(gè) IndexOutOfBoundsException 實(shí)例。必須考慮邊界條件以避免異常,使用一個(gè)帶有 try/catch 異常處理程序的子句將是另一個(gè)解決方案。如果表達(dá)式 len>width*2 為 true,則可以安全地調(diào)用 offsetByCodePoints(),因?yàn)榧词顾写a位都被轉(zhuǎn)換為代理對(duì),碼位的數(shù)量仍會(huì)超過(guò) width 的值?;蛘?,如果 codePointCount(0,len)>width 為 true,也可以安全地調(diào)用 offsetByCodePoints()。如果是其他情況,sliceLimit 必須設(shè)置為 0。
 在 Line (4) 中,清單 9 中的表達(dá)式 pos + width 必須在 while 循環(huán)中使用 offsetByCodePoints(pos,width) 替換。需要計(jì)算的量位于 width 的值中,因?yàn)榈谝粋€(gè)參數(shù)指定當(dāng) width 的值。接下來(lái),在 Line (5) 中,表達(dá)式 pos+=width 必須使用表達(dá)式 pos=end 替換。這避免兩次調(diào)用 offsetByCodePoints() 來(lái)計(jì)算相同的索引。源代碼可以被進(jìn)一步修改以小化處理時(shí)間。
 處理時(shí)間比較
 圖 1 和圖 2 展示了示例 2-1、2-2 和 2-3 的處理時(shí)間。樣例字符串包含相同數(shù)量的代理對(duì)和非代理對(duì)。當(dāng)字符串的長(zhǎng)度和 width 的值被更改時(shí),樣例字符串被切割 10,000 次。
圖 1. 一個(gè)分段的常量寬度圖 2. 分段的常量計(jì)數(shù)
 示例 2-1 和 2-3 按照長(zhǎng)度比例增加了它們的處理時(shí)間,但 示例 2-2 按照長(zhǎng)度的平方比例增加了處理時(shí)間。當(dāng)字符串長(zhǎng)度和 width 的值增加而分段的數(shù)量固定時(shí),示例 2-1 擁有一個(gè)常量處理時(shí)間,而示例 2-2 和 2-3 以 width 的值為比例增加了它們的處理時(shí)間。
信息 API
 大多數(shù)處理代理的信息 API 擁有兩種名稱相同的方法。一種接收 16 位 char 類型參數(shù),另一種接收 32 為碼位參數(shù)。表 2 展示了每個(gè) API 的返回值。第三列針對(duì) U+53F1,第 4 列針對(duì) U+20B9F,后一列針對(duì) U+D842(即高代理),而 U+20B9F 被轉(zhuǎn)換為 U+D842 加上 U+DF9F 的代理對(duì)。如果程序不能處理代理對(duì),則值 U+D842 而不是 U+20B9F 將導(dǎo)致意想不到的結(jié)果(在表 2 中以粗斜體表示)。
表 2. 用于代理的信息 API
類 方法/構(gòu)造函數(shù) 針對(duì) U+53F1 的值 針對(duì) U+20B9F 的值 針對(duì) 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 實(shí)例)
FontMetrics  int charWidth(int cp)  (取決于 FontMetrics 實(shí)例)
String  int indexOf(int cp)  (取決于 String 實(shí)例)
 int lastIndexOf(int cp)  (取決于 String 實(shí)例)
其他 API
 本小節(jié)介紹前面的小節(jié)中沒有討論的代理對(duì)相關(guān) API。表 3 展示所有這些剩余的 API。所有代理對(duì) API 都包含在表 1、2 和 3 中。
表 3. 其他代理 API
類 方法/構(gòu)造函數(shù)
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 展示了從一個(gè)碼位創(chuàng)建一個(gè)字符串的 5 種方法。用于測(cè)試的碼位是 U+53F1 和 U+20B9F,它們?cè)谝粋€(gè)字符串中重復(fù)了 100 億次。清單 11 中的注釋部分顯示了處理時(shí)間:
清單 11. 從一個(gè)碼位創(chuàng)建一個(gè)字符串的 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 的處理時(shí)間沒有明顯不同。相反,創(chuàng)建 str5 花費(fèi)的時(shí)間要長(zhǎng)得多,因?yàn)樗褂?String.format(),該方法支持基于本地和格式化信息的靈活輸出。str5 方法應(yīng)該只用于程序的末尾來(lái)輸出文本。
結(jié)束語(yǔ)
 Unicode 的每個(gè)新版本都包含了通過(guò)代理對(duì)表示的新定義的字符。東亞字符集標(biāo)準(zhǔn)并不是這樣的字符的惟一來(lái)源。例如,移動(dòng)電話中還需要支持 Emoji 字符(表情圖釋),還有各種古字符需要支持。您從本文收獲的技術(shù)和性能分析將有助于您在您的 Java 應(yīng)用程序中支持所有這些字符。
關(guān)于 JAR 您不知道的 5 件事
 把它放在 JAR 中
 通常,在源代碼被編譯之后,您需要構(gòu)建一個(gè) JAR 文件,使用 jar 命令行實(shí)用工具,或者,更常用的是 Ant jar 任務(wù)將 Java 代碼(已經(jīng)被包分離)收集到一個(gè)單獨(dú)的集合中,過(guò)程簡(jiǎn)潔易懂,我不想在這做過(guò)多的說(shuō)明,稍后將繼續(xù)說(shuō)明如何構(gòu)建 JAR?,F(xiàn)在,我只需要存檔 Hello,這是一個(gè)獨(dú)立控制臺(tái)實(shí)用工具,對(duì)于執(zhí)行打印消息到控制臺(tái)這個(gè)任務(wù)十分有用。如清單 1 所示:
清單 1. 存檔控制臺(tái)實(shí)用工具
package com.tedneward.jars;

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

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

println("author.firstName = " + author.firstName)
 您可以看到,JavaBeans 樣式的屬性被簡(jiǎn)化為使用名稱直接訪問(wèn),這就好像它們是字段一樣。
編譯頻繁使用的腳本
 腳本語(yǔ)言的缺點(diǎn)一直存在于性能方面。其中的原因是,大多數(shù)情況下腳本語(yǔ)言是 “即時(shí)” 解譯的,因而它在執(zhí)行時(shí)會(huì)損失一些解析和驗(yàn)證文本的時(shí)間和 CPU 周期。運(yùn)行在 JVM 的許多腳本語(yǔ)言終會(huì)將接收的代碼轉(zhuǎn)換為 Java 字節(jié)碼,至少在腳本被第一次解析和驗(yàn)證時(shí)進(jìn)行轉(zhuǎn)換;在 Java 程序關(guān)閉時(shí),這些即時(shí)編譯的代碼會(huì)消失。將頻繁使用的腳本保持為字節(jié)碼形式可以幫助提升可觀的性能。
 我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實(shí)現(xiàn)了 Compilable 接口,那么這個(gè)接口所編譯的方法可用于將腳本(以一個(gè) String 或一個(gè) Reader 傳遞過(guò)來(lái)的)編譯為一個(gè) CompiledScript 實(shí)例,然后它可用于在 eval() 方法中使用不同的綁定重復(fù)地處理編譯后的代碼,如清單 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();
        }
    }
}
 在大多數(shù)情況中,CompiledScript 實(shí)例需要存儲(chǔ)在一個(gè)長(zhǎng)時(shí)間存儲(chǔ)中(例如,servlet-context),這樣才能避免一次次地重復(fù)編譯相同的腳本。然而,如果腳本發(fā)生變化,您就需要?jiǎng)?chuàng)建一個(gè)新的 CompiledScript 來(lái)反映這個(gè)變化;一旦編譯完成,CompiledScript 就不再執(zhí)行原始的腳本文件內(nèi)容。
Java 異常處理及其應(yīng)用
Java 異常處理引出
 假設(shè)您要編寫一個(gè) 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 行設(shè)置 BufferedReader 對(duì)像連接到 InputStreamReader,而 InputStreamReader 又連接到標(biāo)準(zhǔn)輸入流 System.in;第 7 行讀入一行文本;第 8 行用標(biāo)準(zhǔn)輸出流 System.out 顯示出該文本。
 表面看來(lái)上面的程序沒有問(wèn)題,但實(shí)際上,EchoInput 類完全可能出現(xiàn)問(wèn)題。要在調(diào)用第 7 行的 readLine 方法時(shí)正確讀取輸入,這幾種假設(shè)都必須成立:假定鍵盤有效,鍵盤能與計(jì)算機(jī)正常通信;假定鍵盤數(shù)據(jù)可從操作系統(tǒng)傳輸?shù)?Java 虛擬機(jī),又從 Java 虛擬機(jī)傳輸 inputReader。
 大多數(shù)情況下上述假設(shè)都成立,但不盡然。為此,Java 采用異常方法,以應(yīng)對(duì)可能出現(xiàn)的錯(cuò)誤,并采取步驟進(jìn)行更正。在本例中,若試圖編譯以上代碼,將看到以下信息:
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
    Unhandled exception type IOException
    at EchoInput.main(EchoInput.java:7)
 從中可以看到,第 7 行調(diào)用 readLine 方法可能出錯(cuò):若果真如此,則產(chǎn)生 IOException 來(lái)記錄故障。編譯器錯(cuò)誤是在告訴您,需要更改代碼來(lái)解決這個(gè)潛在的問(wèn)題。在 JDK API 文檔中,可以看到同樣的信息。我們可以看到 readLine 方法,如圖 1 所示。
圖 1. BufferedReader 類的 readLine 方法的 JDK API 文檔
 由圖 1 可知,readLine 方法有時(shí)產(chǎn)生 IOException。如何處理潛在的故障?編譯器需要“捕獲”或“聲明”IOException。
 “捕獲 (catch)”指當(dāng) readLine 方法產(chǎn)生錯(cuò)誤時(shí)截獲該錯(cuò)誤,并處理和記錄該問(wèn)題。而“聲明 (declare)”指錯(cuò)誤可能引發(fā) IOException,并通知調(diào)用該方法的任何代碼:可能產(chǎn)生異常。
 若要捕獲異常,必須添加一個(gè)特殊的“處理代碼塊”,來(lái)接收和處理 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 }
 新添的代碼塊包含關(guān)鍵字 try 和 catch(第 7,10,11,13 行),表示要讀取輸入。若成功,則正常運(yùn)行。若讀取輸入時(shí)錯(cuò)誤,則捕獲問(wèn)題(由 IOException 對(duì)象表示),并采取相應(yīng)措施。在本例,采用的處理方式是輸出異常。
 若不準(zhǔn)備捕獲 IOException,僅聲明異常,則要特別指定 main 方法可能出錯(cuò),而且特別說(shuō)明可能產(chǎn)生 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 }

 從上面的這個(gè)簡(jiǎn)單的例子中,我們可以看出異常處理在 Java 代碼開發(fā)中不能被忽視。
 Java 異常以及異常處理
 可將 Java 異常看作是一類消息,它傳送一些系統(tǒng)問(wèn)題、故障及未按規(guī)定執(zhí)行的動(dòng)作的相關(guān)信息。異常包含信息,以將信息從應(yīng)用程序的一部分發(fā)送到另一部分。
 編譯語(yǔ)言為何要處理異常?為何不在異常出現(xiàn)位置隨時(shí)處理具體故障?因?yàn)橛袝r(shí)候我們需要在系統(tǒng)中交流錯(cuò)誤消息,以便按照統(tǒng)一的方式處理問(wèn)題,有時(shí)是因?yàn)橛腥舾商幚韱?wèn)題的可能方式,但您不知道使用哪一種,此時(shí),可將處理異常的任務(wù)委托給調(diào)用方法的代碼。調(diào)用者通常更能了解問(wèn)題來(lái)源的上下文,能更好的確定恢復(fù)方式。
 圖 2 是一個(gè)通用消息架構(gòu)。
圖 2. 通用消息架構(gòu)
 從上圖可以看出,必定在運(yùn)行的 Java 應(yīng)用程序的一些類或?qū)ο笾挟a(chǎn)生異常。出現(xiàn)故障時(shí),“發(fā)送者”將產(chǎn)生異常對(duì)象。異??赡艽?Java 代碼出現(xiàn)的問(wèn)題,也可能是 JVM 的相應(yīng)錯(cuò)誤,或基礎(chǔ)硬件或操作系統(tǒng)的錯(cuò)誤。
 異常本身表示消息,指發(fā)送者傳給接收者的數(shù)據(jù)“負(fù)荷”。首先,異?;陬惖念愋蛠?lái)傳輸有用信息。很多情況下,基于異常的類既能識(shí)別故障本因并能更正問(wèn)題。其次,異常還帶有可能有用的數(shù)據(jù)(如屬性)。
 在處理異常時(shí),消息必須有接收者;否則將無(wú)法處理產(chǎn)生異常的底層問(wèn)題。
 在上例中,異常“產(chǎn)生者”是讀取文本行的 BufferedReader。在故障出現(xiàn)時(shí),將在 readLine 方法中構(gòu)建 IOException 對(duì)象。異常“接收者”是代碼本身。EchoInputHandle 應(yīng)用程序的 try-catch 結(jié)構(gòu)中的 catch 塊是異常的接收者,它以字符串形式輸出異常,將問(wèn)題記錄下來(lái)。
Java 異常類的層次結(jié)構(gòu)
 在我們從總體上了解異常后,我們應(yīng)該了解如何在 Java 應(yīng)用程序中使用異常,即需要了解 Java 類的層次結(jié)構(gòu)。圖 3 是 Java 類的層次結(jié)構(gòu)圖。
圖 3. Java 類的層次結(jié)構(gòu)
 在 Java 中,所有的異常都有一個(gè)共同的祖先 Throwable(可拋出)。Throwable 指定代碼中可用異常傳播機(jī)制通過(guò) Java 應(yīng)用程序傳輸?shù)娜魏螁?wèn)題的共性。
 Throwable 有兩個(gè)重要的子類:Exception(異常)和 Error(錯(cuò)誤),二者都是 Java 異常處理的重要子類,各自都包含大量子類。
 Exception(異常)是應(yīng)用程序中可能的可預(yù)測(cè)、可恢復(fù)問(wèn)題。一般大多數(shù)異常表示中度到輕度的問(wèn)題。異常一般是在特定環(huán)境下產(chǎn)生的,通常出現(xiàn)在代碼的特定方法和操作中。在 EchoInput 類中,當(dāng)試圖調(diào)用 readLine 方法時(shí),可能出現(xiàn) IOException 異常。
 Error(錯(cuò)誤)表示運(yùn)行應(yīng)用程序中較嚴(yán)重問(wèn)題。大多數(shù)錯(cuò)誤與代碼編寫者執(zhí)行的操作無(wú)關(guān),而表示代碼運(yùn)行時(shí) JVM(Java 虛擬機(jī))出現(xiàn)的問(wèn)題。例如,當(dāng) JVM 不再有繼續(xù)執(zhí)行操作所需的內(nèi)存資源時(shí),將出現(xiàn) OutOfMemoryError。
 Exception 類有一個(gè)重要的子類 RuntimeException。RuntimeException 類及其子類表示“JVM 常用操作”引發(fā)的錯(cuò)誤。例如,若試圖使用空值對(duì)象引用、除數(shù)為零或數(shù)組越界,則分別引發(fā)運(yùn)行時(shí)異常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
Java 異常的處理
 在 Java 應(yīng)用程序中,對(duì)異常的處理有兩種方式:處理異常和聲明異常。
 處理異常:try、catch 和 finally
 若要捕獲異常,則必須在代碼中添加異常處理器塊。這種 Java 結(jié)構(gòu)可能包含 3 個(gè)部分,
 都有 Java 關(guān)鍵字。下面的例子中使用了 try-catch-finally 代碼結(jié)構(gòu)。
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 塊:將一個(gè)或者多個(gè)語(yǔ)句放入 try 時(shí),則表示這些語(yǔ)句可能拋出異常。編譯器知道可能要發(fā)生異常,于是用一個(gè)特殊結(jié)構(gòu)評(píng)估塊內(nèi)所有語(yǔ)句。
catch 塊:當(dāng)問(wèn)題出現(xiàn)時(shí),一種選擇是定義代碼塊來(lái)處理問(wèn)題,catch 塊的目的便在于此。catch 塊是 try 塊所產(chǎn)生異常的接收者?;驹硎牵阂坏┥僧惓?,則 try 塊的執(zhí)行中止,JVM 將查找相應(yīng)的 JVM。
finally 塊:還可以定義 finally 塊,無(wú)論運(yùn)行 try 塊代碼的結(jié)果如何,該塊里面的代碼一定運(yùn)行。在常見的所有環(huán)境中,finally 塊都將運(yùn)行。無(wú)論 try 塊是否運(yùn)行完,無(wú)論是否產(chǎn)生異常,也無(wú)論是否在 catch 塊中得到處理,finally 塊都將執(zhí)行。
 try-catch-finally 規(guī)則:
必須在 try 之后添加 catch 或 finally 塊。try 塊后可同時(shí)接 catch 和 finally 塊,但至少有一個(gè)塊。
必須遵循塊順序:若代碼同時(shí)使用 catch 和 finally 塊,則必須將 catch 塊放在 try 塊之后。
catch 塊與相應(yīng)的異常類的類型相關(guān)。
一個(gè) try 塊可能有多個(gè) catch 塊。若如此,則執(zhí)行第一個(gè)匹配塊。
可嵌套 try-catch-finally 結(jié)構(gòu)。
在 try-catch-finally 結(jié)構(gòu)中,可重新拋出異常。
除了下列情況,總將執(zhí)行 finally 做為結(jié)束:JVM 過(guò)早終止(調(diào)用 System.exit(int));在 finally 塊中拋出一個(gè)未處理的異常;計(jì)算機(jī)斷電、失火、或遭遇病毒攻擊。
 聲明異常
 若要聲明異常,則必須將其添加到方法簽名塊的結(jié)束位置。下面是一個(gè)實(shí)例:
public void errorProneMethod(int input) throws java.io.IOException {
    //Code for the method,including one or more method
    //calls that may produce an IOException
}
   
 這樣,聲明的異常將傳給方法調(diào)用者,而且也通知了編譯器:該方法的任何調(diào)用者必須遵守處理或聲明規(guī)則。聲明異常的規(guī)則如下:
必須聲明方法可拋出的任何可檢測(cè)異常(checked exception)。
非檢測(cè)性異常(unchecked exception)不是必須的,可聲明,也可不聲明。
調(diào)用方法必須遵循任何可檢測(cè)異常的處理和聲明規(guī)則。若覆蓋一個(gè)方法,則不能聲明與覆蓋方法不同的異常。聲明的任何異常必須是被覆蓋方法所聲明異常的同類或子類。
Java 異常處理的分類
 Java 異??煞譃榭蓹z測(cè)異常,非檢測(cè)異常和自定義異常。
 可檢測(cè)異常
 可檢測(cè)異常經(jīng)編譯器驗(yàn)證,對(duì)于聲明拋出異常的任何方法,編譯器將強(qiáng)制執(zhí)行處理或聲明規(guī)則,例如:sqlExecption 這個(gè)異常就是一個(gè)檢測(cè)異常。你連接 JDBC 時(shí),不捕捉這個(gè)異常,編譯器就通不過(guò),不允許編譯。
 非檢測(cè)異常
 非檢測(cè)異常不遵循處理或聲明規(guī)則。在產(chǎn)生此類異常時(shí),不一定非要采取任何適當(dāng)操作,編譯器不會(huì)檢查是否已解決了這樣一個(gè)異常。例如:一個(gè)數(shù)組為 3 個(gè)長(zhǎng)度,當(dāng)你使用下標(biāo)為3時(shí),就會(huì)產(chǎn)生數(shù)組下標(biāo)越界異常。這個(gè)異常 JVM 不會(huì)進(jìn)行檢測(cè),要靠程序員來(lái)判斷。有兩個(gè)主要類定義非檢測(cè)異常:RuntimeException 和 Error。
 Error 子類屬于非檢測(cè)異常,因?yàn)闊o(wú)法預(yù)知它們的產(chǎn)生時(shí)間。若 Java 應(yīng)用程序內(nèi)存不足,則隨時(shí)可能出現(xiàn) OutOfMemoryError;起因一般不是應(yīng)用程序的特殊調(diào)用,而是 JVM 自身的問(wèn)題。另外,Error 一般表示應(yīng)用程序無(wú)法解決的嚴(yán)重問(wèn)題。
 RuntimeException 類也屬于非檢測(cè)異常,因?yàn)槠胀?JVM 操作引發(fā)的運(yùn)行時(shí)異常隨時(shí)可能發(fā)生,此類異常一般是由特定操作引發(fā)。但這些操作在 Java 應(yīng)用程序中會(huì)頻繁出現(xiàn)。因此,它們不受編譯器檢查與處理或聲明規(guī)則的限制。
 自定義異常
 自定義異常是為了表示應(yīng)用程序的一些錯(cuò)誤類型,為代碼可能發(fā)生的一個(gè)或多個(gè)問(wèn)題提供新含義??梢燥@示代碼多個(gè)位置之間的錯(cuò)誤的相似性,也可以區(qū)分代碼運(yùn)行時(shí)可能出現(xiàn)的相似問(wèn)題的一個(gè)或者多個(gè)錯(cuò)誤,或給出應(yīng)用程序中一組錯(cuò)誤的特定含義。例如,對(duì)隊(duì)列進(jìn)行操作時(shí),有可能出現(xiàn)兩種情況:空隊(duì)列時(shí)試圖刪除一個(gè)元素;滿隊(duì)列時(shí)試圖添加一個(gè)元素。則需要自定義兩個(gè)異常來(lái)處理這兩種情況。
Java 異常處理的原則和忌諱
Java 異常處理的原則
盡可能的處理異常
 要盡可能的處理異常,如果條件確實(shí)不允許,無(wú)法在自己的代碼中完成處理,就考慮聲明異常。如果人為避免在代碼中處理異常,僅作聲明,則是一種錯(cuò)誤和依賴的實(shí)踐。
具體問(wèn)題具體解決
 異常的部分優(yōu)點(diǎn)在于能為不同類型的問(wèn)題提供不同的處理操作。有效異常處理的關(guān)鍵是識(shí)別特定故障場(chǎng)景,并開發(fā)解決此場(chǎng)景的特定相應(yīng)行為。為了充分利用異常處理能力,需要為特定類型的問(wèn)題構(gòu)建特定的處理器塊。
記錄可能影響應(yīng)用程序運(yùn)行的異常
 至少要采取一些永久的方式,記錄下可能影響應(yīng)用程序操作的異常。理想情況下,當(dāng)然是在第一時(shí)間解決引發(fā)異常的基本問(wèn)題。不過(guò),無(wú)論采用哪種處理操作,一般總應(yīng)記錄下潛在的關(guān)鍵問(wèn)題。別看這個(gè)操作很簡(jiǎn)單,但它可以幫助您用很少的時(shí)間來(lái)跟蹤應(yīng)用程序中復(fù)雜問(wèn)題的起因。
根據(jù)情形將異常轉(zhuǎn)化為業(yè)務(wù)上下文
 若要通知一個(gè)應(yīng)用程序特有的問(wèn)題,有必要將應(yīng)用程序轉(zhuǎn)換為不同形式。若用業(yè)務(wù)特定狀態(tài)表示異常,則代碼更易維護(hù)。從某種意義上講,無(wú)論何時(shí)將異常傳到不同上下文(即另一技術(shù)層),都應(yīng)將異常轉(zhuǎn)換為對(duì)新上下文有意義的形式。
Java 異常處理的忌諱
一般不要忽略異常
 在異常處理塊中,一項(xiàng)危險(xiǎn)的舉動(dòng)是“不加通告”地處理異常。如下例所示:
1   try{
2       Class.forName("business.domain.Customer");
3   }
4   catch (ClassNotFoundException exc){}
 經(jīng)常能夠在代碼塊中看到類似的代碼塊。有人總喜歡在編寫代碼時(shí)簡(jiǎn)單快速地編寫空處理器塊,并“自我安慰地”宣稱準(zhǔn)備在“后期”添加恢復(fù)代碼,但這個(gè)“后期”變成了“無(wú)期”。
 這種做法有什么壞處?如果異常對(duì)應(yīng)用程序的其他部分確實(shí)沒有任何負(fù)面影響,這未嘗不可。但事實(shí)往往并非如此,異常會(huì)擾亂應(yīng)用程序的狀態(tài)。此時(shí),這樣的代碼無(wú)異于掩耳盜鈴。
 這種做法若影響較輕,則應(yīng)用程序可能出現(xiàn)怪異行為。例如,應(yīng)用程序設(shè)置的一個(gè)值不見了, 或 GUI 失效。若問(wèn)題嚴(yán)重,則應(yīng)用程序可能會(huì)出現(xiàn)重大問(wèn)題,因?yàn)楫惓N从涗浽脊收宵c(diǎn),難以處理,如重復(fù)的 NullPointerExceptions。
 如果采取措施,記錄了捕獲的異常,則不可能遇到這個(gè)問(wèn)題。實(shí)際上,除非確認(rèn)異常對(duì)代碼其余部分絕無(wú)影響,至少也要作記錄。進(jìn)一步講,永遠(yuǎn)不要忽略問(wèn)題;否則,風(fēng)險(xiǎn)很大,在后期會(huì)引發(fā)難以預(yù)料的后果。
不要使用覆蓋式異常處理塊
 另一個(gè)危險(xiǎn)的處理是覆蓋式處理器(blanket handler)。該代碼的基本結(jié)構(gòu)如下:
1   try{
2     // …
3   }
4   catch(Exception e){
5     // …
6   }
使用覆蓋式異常處理塊有兩個(gè)前提之一:
代碼中只有一類問(wèn)題。
 這可能正確,但即便如此,也不應(yīng)使用覆蓋式異常處理,捕獲更具體的異常形式有利物弊。
單個(gè)恢復(fù)操作始終適用。
 這幾乎絕對(duì)錯(cuò)誤。幾乎沒有哪個(gè)方法能放之四海而皆準(zhǔn),能應(yīng)對(duì)出現(xiàn)的任何問(wèn)題。
 分析下這樣編寫代碼將發(fā)生的情況。只要方法不斷拋出預(yù)期的異常集,則一切正常。但是,如果拋出了未預(yù)料到的異常,則無(wú)法看到要采取的操作。當(dāng)覆蓋式處理器對(duì)新異常類執(zhí)行千篇一律的任務(wù)時(shí),只能間接看到異常的處理結(jié)果。如果代碼沒有打印或記錄語(yǔ)句,則根本看不到結(jié)果。
 更糟糕的是,當(dāng)代碼發(fā)生變化時(shí),覆蓋式處理器將繼續(xù)作用于所有新異常類型,并以相同方式處理所有類型。
 一般不要把特定的異常轉(zhuǎn)化為更通用的異常
 將特定的異常轉(zhuǎn)換為更通用異常時(shí)一種錯(cuò)誤做法。一般而言,這將取消異常起初拋出時(shí)產(chǎn)生的上下文,在將異常傳到系統(tǒng)的其他位置時(shí),將更難處理。見下例:
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   }
 因?yàn)闆]有原始異常的信息,所以處理器塊無(wú)法確定問(wèn)題的起因,也不知道如何更正問(wèn)題。
 不要處理能夠避免的異常
 對(duì)于有些異常類型,實(shí)際上根本不必處理。通常運(yùn)行時(shí)異常屬于此類范疇。在處理空指針或者數(shù)據(jù)索引等問(wèn)題時(shí),不必求助于異常處理。
Java 異常處理的應(yīng)用實(shí)例
 在定義銀行類時(shí),若取錢數(shù)大于余額時(shí)需要做異常處理。
 定義一個(gè)異常類 insufficientFundsException。取錢(withdrawal)方法中可能產(chǎn)生異常,條件是余額小于取額。
 處理異常在調(diào)用 withdrawal 的時(shí)候,因此 withdrawal 方法要聲明拋出異常,由上一級(jí)方法調(diào)用。
異常類:
class InsufficientFundsExceptionextends Exception{
   private Bank  excepbank;      // 銀行對(duì)象
   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;// 存款數(shù)
   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);
   }
}
前端調(diào)用:
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());
      }
   }
}
關(guān)于 JVM 命令行標(biāo)志您不知道的 5 件事
DisableExplicitGC
 我已記不清有多少次用戶要求我就應(yīng)用程序性能問(wèn)題提供咨詢了,其實(shí)只要跨代碼快速運(yùn)行 grep,就會(huì)發(fā)現(xiàn)清單 1 所示的問(wèn)題 — 原始 java 性能反模式:
清單 1. System.gc();
// We just released a bunch of objects, so tell the stupid
// garbage collector to collect them already!
System.gc();
 顯式垃圾收集是一個(gè)非常糟糕的主意 — 就像將您和一個(gè)瘋狂的斗牛犬鎖在一個(gè)電話亭里。盡管調(diào)用的語(yǔ)法是依賴實(shí)現(xiàn)的,但如果您的 JVM 正在運(yùn)行一個(gè)分代的垃圾回收器(大多數(shù)是)System.gc(); 強(qiáng)迫 VM 執(zhí)行一個(gè)堆的 “全部清掃”,雖然有的沒有必要。全部清掃比一個(gè)常規(guī) GC 操作要昂貴好幾個(gè)數(shù)量級(jí),這只是個(gè)簡(jiǎn)單數(shù)學(xué)問(wèn)題。
 您可以不把我的話放在心上 — Sun 的工程師為這個(gè)特殊的人工錯(cuò)誤提供一個(gè) JVM 標(biāo)志; -XX:+DisableExplicitGC 標(biāo)志自動(dòng)將 System.gc() 調(diào)用轉(zhuǎn)換成一個(gè)空操作,為您提供運(yùn)行代碼的機(jī)會(huì),您自己看看 System.gc() 對(duì)于整個(gè) JVM 執(zhí)行有害還是有利。
HeapDumpOnOutOfMemoryError
 您有沒有經(jīng)歷過(guò)這樣的情況:JVM 不能使用,不斷拋出 OutOfMemoryError,而您又不能為自己創(chuàng)建調(diào)試器來(lái)捕獲它或查看出現(xiàn)了什么問(wèn)題?像這類偶發(fā)和/或不確定的問(wèn)題,通常使開發(fā)人員發(fā)瘋。
 買者自負(fù)
 并不是任何 VM 都支持所有命令行標(biāo)志,Sun/Oracle 的 VM 除外。查明一個(gè)標(biāo)志是否被支持的好方法是試用它,看它是否正常工作。倘若這些標(biāo)志在技術(shù)上是不支持的,那么,使用它們您要承擔(dān)全部責(zé)任。如果這些標(biāo)志中的任何一個(gè)使您的代碼、您的數(shù)據(jù)、您的服務(wù)器或您的一切消失得無(wú)影無(wú)蹤,我、Sun/Oracle 和 IBM® 都將不負(fù)責(zé)任。為以防萬(wàn)一,建議先在虛擬(非常生產(chǎn))環(huán)境中實(shí)驗(yàn)。
 在這個(gè)時(shí)刻您想要的是,在 JVM 消亡之際捕獲堆的一個(gè)快照 — 正好 -XX:+HeapDumpOnOutOfMemoryError 命令可以完成這一操作。
 運(yùn)行該命令通知 JVM 拍攝一個(gè) “堆轉(zhuǎn)儲(chǔ)快照”,并將其保存在一個(gè)文件中以便處理,通常使用 jhat 實(shí)用工具(我在 上一篇文章 中介紹過(guò))。您可以使用相應(yīng)的 -XX:HeapDumpPath 標(biāo)志指定到保存文件的實(shí)際路徑。(不管文件保存在哪,務(wù)必確保文件系統(tǒng)和/或 Java 流程必須要有權(quán)限配置,可以在其中寫入。)
bootclasspath
 定期將一個(gè)類放入類路徑是很有幫助的,這類路徑與庫(kù)存 JRE 附帶的類路徑或者以某種方式擴(kuò)展的 JRE 類路徑略有不同。(新 Java Crypto API 提供商就是一個(gè)例子)。如果您想要擴(kuò)展 JRE ,那么您定制的實(shí)現(xiàn)必須可以使用引導(dǎo)程序 ClassLoader,該引導(dǎo)程序可以加載 rt.jar 中的 java.lang.Object 及其所有相關(guān)文件。
 盡管您可以 非法打開 rt.jar 并將您的定制實(shí)現(xiàn)或新數(shù)據(jù)包移入其中,但從技術(shù)上您就違反了您下載 JDK 時(shí)同意的協(xié)議了。
 相反,使用 JVM 自己的 -Xbootclasspath 選項(xiàng),以及皮膚 -Xbootclasspath/p 和 -Xbootclasspath/a。
 -Xbootclasspath 使您可以設(shè)置完整的引導(dǎo)類路徑(這通常包括一個(gè)對(duì) rt.jar 的引用),以及一些其他 JDK 附帶的(不是 rt.jar 的一部分)JAR 文件。-Xbootclasspath/p 將值前置到現(xiàn)有 bootclasspath 中,并將 -Xbootclasspath/a 附加到其中。
 例如,如果您修改了庫(kù)中的 java.lang.Integer,并將修改放在一個(gè)子路徑 mods 下,那么 -Xbootclasspath/a mods 參數(shù)將新 Integer 放在默認(rèn)的參數(shù)前面。
verbose
 對(duì)于虛擬的或任何類型的 Java 應(yīng)用程序,-verbose 是一個(gè)很有用的一級(jí)診斷使用程序。該標(biāo)志有三個(gè)子標(biāo)志:gc、class 和 jni。
 開發(fā)人員嘗試尋找是否 JVM 垃圾收集器發(fā)生故障或者導(dǎo)致性能低下,通常首先要做的就是執(zhí)行 gc。不幸的是,解釋 gc 輸出很麻煩 — 足夠?qū)懸槐緯?。更糟糕的是,在命令行中打印的輸出在不同?Java 版本中或者不在不同的 JVM 中會(huì)發(fā)生改變,這使得正確解釋變得更難。
 一般來(lái)說(shuō),如果垃圾收集器是一個(gè)分代收集器(多數(shù) “企業(yè)級(jí)” VMs 都是)。某種虛擬標(biāo)志將會(huì)出現(xiàn),來(lái)指出一個(gè)全部清掃 GC 通路;在 Sun JVM 中,標(biāo)志在 GC 輸出行的開始以 “[Full GC ...]” 形式出現(xiàn)。
 想要診斷 ClassLoader 和/或不匹配的類沖突,class 可以幫上大忙。它不僅報(bào)告類何時(shí)加載,還報(bào)告類從何處加載,包括到 JAR 的路徑(如果來(lái)自 JAR)。
 jni 很少使用,除了使用 JNI 或本地庫(kù)時(shí)。打開時(shí),它將報(bào)告各種 JNI 事件,比如,本地庫(kù)何時(shí)加載,方法何時(shí)彈回;再一次強(qiáng)調(diào),在不同 JVM 版本中,輸出會(huì)發(fā)生變化。
Command-line -X
 我列出了 JVM 中提供的我喜歡的命令行選項(xiàng),但是還有一些更多的需要您自己發(fā)現(xiàn),運(yùn)行命令行參數(shù) -X,列出 JVM 提供的所有非標(biāo)準(zhǔn)(但大部分都是安全的)參數(shù) — 例如:
-Xint,在解釋模式下運(yùn)行 JVM(對(duì)于測(cè)試 JIT 編譯器實(shí)際上是否對(duì)您的代碼起作用或者驗(yàn)證是否 JIT 編譯器中有一個(gè) bug,這都很有用)。
-Xloggc:,和 -verbose:gc 做同樣的事,但是記錄一個(gè)文件而不輸出到命令行窗口。
 JVM 命令行選項(xiàng)時(shí)常發(fā)生變化,因此,定期查看是一個(gè)好主意。甚至,您深夜盯著監(jiān)控器和下午 5 點(diǎn)回家和妻子孩子吃頓晚飯,(或者在 Mass Effect 2 中消滅您的敵人,根據(jù)您的喜好),它們都是不一樣的。
關(guān)于 java.util.concurrent 您不知道的 5 件事,第 1 部分
 Concurrent Collections 是 Java™ 5 的巨大附加產(chǎn)品,但是在關(guān)于注釋和泛型的爭(zhēng)執(zhí)中很多 Java 開發(fā)人員忽視了它們。此外(或者更老實(shí)地說(shuō)),許多開發(fā)人員避免使用這個(gè)數(shù)據(jù)包,因?yàn)樗麄冋J(rèn)為它一定很復(fù)雜,就像它所要解決的問(wèn)題一樣。
 事實(shí)上,java.util.concurrent 包含許多類,能夠有效解決普通的并發(fā)問(wèn)題,無(wú)需復(fù)雜工序。閱讀本文,了解 java.util.concurrent 類,比如 CopyOnWriteArrayList 和 BlockingQueue 如何幫助您解決多線程編程的棘手問(wèn)題。
TimeUnit
 盡管本質(zhì)上 不是 Collections 類,但 java.util.concurrent.TimeUnit 枚舉讓代碼更易讀懂。使用 TimeUnit 將使用您的方法或 API 的開發(fā)人員從毫秒的 “暴政” 中解放出來(lái)。
 TimeUnit 包括所有時(shí)間單位,從 MILLISECONDS 和 MICROSECONDS 到 DAYS 和 HOURS,這就意味著它能夠處理一個(gè)開發(fā)人員所需的幾乎所有的時(shí)間范圍類型。同時(shí),因?yàn)樵诹信e上聲明了轉(zhuǎn)換方法,在時(shí)間加快時(shí),將 HOURS 轉(zhuǎn)換回 MILLISECONDS 甚至變得更容易。
CopyOnWriteArrayList
 創(chuàng)建數(shù)組的全新副本是過(guò)于昂貴的操作,無(wú)論是從時(shí)間上,還是從內(nèi)存開銷上,因此在通常使用中很少考慮;開發(fā)人員往往求助于使用同步的 ArrayList。然而,這也是一個(gè)成本較高的選擇,因?yàn)槊慨?dāng)您跨集合內(nèi)容進(jìn)行迭代時(shí),您就不得不同步所有操作,包括讀和寫,以此保證一致性。
 這又讓成本結(jié)構(gòu)回到這樣一個(gè)場(chǎng)景:需多讀者都在讀取 ArrayList,但是幾乎沒人會(huì)去修改它。
 CopyOnWriteArrayList 是個(gè)巧妙的小寶貝,能解決這一問(wèn)題。它的 Javadoc 將 CopyOnWriteArrayList 定義為一個(gè) “ArrayList 的線程安全變體,在這個(gè)變體中所有易變操作(添加,設(shè)置等)可以通過(guò)復(fù)制全新的數(shù)組來(lái)實(shí)現(xiàn)”。
 集合從內(nèi)部將它的內(nèi)容復(fù)制到一個(gè)沒有修改的新數(shù)組,這樣讀者訪問(wèn)數(shù)組內(nèi)容時(shí)就不會(huì)產(chǎn)生同步成本(因?yàn)樗麄儚膩?lái)不是在易變數(shù)據(jù)上操作)。
 本質(zhì)上講,CopyOnWriteArrayList 很適合處理 ArrayList 經(jīng)常讓我們失敗的這種場(chǎng)景:讀取頻繁,但很少有寫操作的集合,例如 JavaBean 事件的 Listeners。
BlockingQueue
 BlockingQueue 接口表示它是一個(gè) Queue,意思是它的項(xiàng)以先入先出(FIFO)順序存儲(chǔ)。在特定順序插入的項(xiàng)以相同的順序檢索 — 但是需要附加保證,從空隊(duì)列檢索一個(gè)項(xiàng)的任何嘗試都會(huì)阻塞調(diào)用線程,直到這個(gè)項(xiàng)準(zhǔn)備好被檢索。同理,想要將一個(gè)項(xiàng)插入到滿隊(duì)列的嘗試也會(huì)導(dǎo)致阻塞調(diào)用線程,直到隊(duì)列的存儲(chǔ)空間可用。
 BlockingQueue 干凈利落地解決了如何將一個(gè)線程收集的項(xiàng)“傳遞”給另一線程用于處理的問(wèn)題,無(wú)需考慮同步問(wèn)題。Java Tutorial 的 Guarded Blocks 試用版就是一個(gè)很好的例子。它構(gòu)建一個(gè)單插槽綁定的緩存,當(dāng)新的項(xiàng)可用,而且插槽也準(zhǔn)備好接受新的項(xiàng)時(shí),使用手動(dòng)同步和 wait()/notifyAll() 在線程之間發(fā)信。(詳見 Guarded Blocks 實(shí)現(xiàn)。)
 盡管 Guarded Blocks 教程中的代碼有效,但是它耗時(shí)久,混亂,而且也并非完全直觀。退回到 Java 平臺(tái)較早的時(shí)候,沒錯(cuò),Java 開發(fā)人員不得不糾纏于這種代碼;但現(xiàn)在是 2010 年 — 情況難道沒有改善?
 清單 1 顯示了 Guarded Blocks 代碼的重寫版,其中我使用了一個(gè) 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 還體現(xiàn)了“公平” — 意思是它為讀取器和編寫器提供線程先入先出訪問(wèn)。這種替代方法是一個(gè)更有效,但又冒窮盡部分線程風(fēng)險(xiǎn)的政策。(即,允許一些讀取器在其他讀取器鎖定時(shí)運(yùn)行效率更高,但是您可能會(huì)有讀取器線程的流持續(xù)不斷的風(fēng)險(xiǎn),導(dǎo)致編寫器無(wú)法進(jìn)行工作。)
 注意 Bug!
 順便說(shuō)一句,如果您注意到 Guarded Blocks 包含一個(gè)重大 bug,那么您是對(duì)的 — 如果開發(fā)人員在 main() 中的 Drop 實(shí)例上同步,會(huì)出現(xiàn)什么情況呢?
 BlockingQueue 還支持接收時(shí)間參數(shù)的方法,時(shí)間參數(shù)表明線程在返回信號(hào)故障以插入或者檢索有關(guān)項(xiàng)之前需要阻塞的時(shí)間。這么做會(huì)避免非綁定的等待,這對(duì)一個(gè)生產(chǎn)系統(tǒng)是致命的,因?yàn)橐粋€(gè)非綁定的等待會(huì)很容易導(dǎo)致需要重啟的系統(tǒng)掛起。
ConcurrentMap
 Map 有一個(gè)微妙的并發(fā) bug,這個(gè) bug 將許多不知情的 Java 開發(fā)人員引入歧途。ConcurrentMap 是容易的解決方案。
 當(dāng)一個(gè) Map 被從多個(gè)線程訪問(wèn)時(shí),通常使用 containsKey() 或者 get() 來(lái)查看給定鍵是否在存儲(chǔ)鍵/值對(duì)之前出現(xiàn)。但是即使有一個(gè)同步的 Map,線程還是可以在這個(gè)過(guò)程中潛入,然后奪取對(duì) Map 的控制權(quán)。問(wèn)題是,在對(duì) put() 的調(diào)用中,鎖在 get() 開始時(shí)獲取,然后在可以再次獲取鎖之前釋放。它的結(jié)果是個(gè)競(jìng)爭(zhēng)條件:這是兩個(gè)線程之間的競(jìng)爭(zhēng),結(jié)果也會(huì)因誰(shuí)先運(yùn)行而不同。
 如果兩個(gè)線程幾乎同時(shí)調(diào)用一個(gè)方法,兩者都會(huì)進(jìn)行測(cè)試,調(diào)用 put,在處理中丟失第一線程的值。幸運(yùn)的是,ConcurrentMap 接口支持許多附加方法,它們?cè)O(shè)計(jì)用于在一個(gè)鎖下進(jìn)行兩個(gè)任務(wù):putIfAbsent(),例如,首先進(jìn)行測(cè)試,然后僅當(dāng)鍵沒有存儲(chǔ)在 Map 中時(shí)進(jìn)行 put。
SynchronousQueues
 根據(jù) Javadoc,SynchronousQueue 是個(gè)有趣的東西:
 這是一個(gè)阻塞隊(duì)列,其中,每個(gè)插入操作必須等待另一個(gè)線程的對(duì)應(yīng)移除操作,反之亦然。一個(gè)同步隊(duì)列不具有任何內(nèi)部容量,甚至不具有 1 的容量。
 本質(zhì)上講,SynchronousQueue 是之前提過(guò)的 BlockingQueue 的又一實(shí)現(xiàn)。它給我們提供了在線程之間交換單一元素的極輕量級(jí)方法,使用 ArrayBlockingQueue 使用的阻塞語(yǔ)義。在清單 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();
    }
}
 實(shí)現(xiàn)代碼看起來(lái)幾乎相同,但是應(yīng)用程序有額外獲益:SynchronousQueue 允許在隊(duì)列進(jìn)行一個(gè)插入,只要有一個(gè)線程等著使用它。
 在實(shí)踐中,SynchronousQueue 類似于 Ada 和 CSP 等語(yǔ)言中可用的 “會(huì)合通道”。這些通道有時(shí)在其他環(huán)境中也稱為 “連接”,這樣的環(huán)境包括 .NET (見 參考資料)。
關(guān)于 java.util.concurrent 您不知道的 5 件事,第 2 部分
 并發(fā) Collections 提供了線程安全、經(jīng)過(guò)良好調(diào)優(yōu)的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)化了并發(fā)編程。然而,在一些情形下,開發(fā)人員需要更進(jìn)一步,思考如何調(diào)節(jié)和/或限制線程執(zhí)行。由于 java.util.concurrent 的總體目標(biāo)是簡(jiǎn)化多線程編程,您可能希望該包包含同步實(shí)用程序,而它確實(shí)包含。
 本文是 第 1 部分 的延續(xù),將介紹幾個(gè)比核心語(yǔ)言原語(yǔ)(監(jiān)視器)更高級(jí)的同步結(jié)構(gòu),但它們還未包含在 Collection 類中。一旦您了解了這些鎖和門的用途,使用它們將非常直觀。
Semaphore
 在一些企業(yè)系統(tǒng)中,開發(fā)人員經(jīng)常需要限制未處理的特定資源請(qǐng)求(線程/操作)數(shù)量,事實(shí)上,限制有時(shí)候能夠提高系統(tǒng)的吞吐量,因?yàn)樗鼈儨p少了對(duì)特定資源的爭(zhēng)用。盡管完全可以手動(dòng)編寫限制代碼,但使用 Semaphore 類可以更輕松地完成此任務(wù),它將幫您執(zhí)行限制,如清單 1 所示:
清單 1. 使用 Semaphore 執(zhí)行限制
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 個(gè)線程都在運(yùn)行(您可以對(duì)運(yùn)行 SemApp 的 Java 進(jìn)程執(zhí)行 jstack 來(lái)驗(yàn)證),但只有 3 個(gè)線程是活躍的。在一個(gè)信號(hào)計(jì)數(shù)器釋放之前,其他 7 個(gè)線程都處于空閑狀態(tài)。(實(shí)際上,Semaphore 類支持一次獲取和釋放多個(gè) permit,但這不適用于本場(chǎng)景。)
CountDownLatch
 如果 Semaphore 是允許一次進(jìn)入一個(gè)(這可能會(huì)勾起一些流行夜總會(huì)的保安的記憶)線程的并發(fā)性類,那么 CountDownLatch 就像是賽馬場(chǎng)的起跑門柵。此類持有所有空閑線程,直到滿足特定條件,這時(shí)它將會(huì)一次釋放所有這些線程。
清單 2. CountDownLatch:讓我們?nèi)ベ愸R吧!
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 有兩個(gè)用途:首先,它同時(shí)釋放所有線程,模擬馬賽的起點(diǎn),但隨后會(huì)設(shè)置一個(gè)門閂模擬馬賽的終點(diǎn)。這樣,“主” 線程就可以輸出結(jié)果。 為了讓馬賽有更多的輸出注釋,可以在賽場(chǎng)的 “轉(zhuǎn)彎處” 和 “半程” 點(diǎn),比如賽馬跨過(guò)跑道的四分之一、二分之一和四分之三線時(shí),添加 CountDownLatch。
Executor
 清單 1 和 清單 2 中的示例都存在一個(gè)重要的缺陷,它們要求您直接創(chuàng)建 Thread 對(duì)象。這可以解決一些問(wèn)題,因?yàn)樵谝恍?JVM 中,創(chuàng)建 Thread 是一項(xiàng)重量型的操作,重用現(xiàn)有 Thread 比創(chuàng)建新線程要容易得多。而在另一些 JVM 中,情況正好相反:Thread 是輕量型的,可以在需要時(shí)很容易地新建一個(gè)線程。當(dāng)然,如果 Murphy 擁有自己的解決辦法(他通常都會(huì)擁有),那么您無(wú)論使用哪種方法對(duì)于您終將部署的平臺(tái)都是不對(duì)的。
 JSR-166 專家組(參見 參考資料)在一定程度上預(yù)測(cè)到了這一情形。Java 開發(fā)人員無(wú)需直接創(chuàng)建 Thread,他們引入了 Executor 接口,這是對(duì)創(chuàng)建新線程的一種抽象。如清單 3 所示,Executor 使您不必親自對(duì) Thread 對(duì)象執(zhí)行 new 就能夠創(chuàng)建新線程:
清單 3. Executor
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });
 使用 Executor 的主要缺陷與我們?cè)谒泄S中遇到的一樣:工廠必須來(lái)自某個(gè)位置。不幸的是,與 CLR 不同,JVM 沒有附帶一個(gè)標(biāo)準(zhǔn)的 VM 級(jí)線程池。
 Executor 類實(shí)際上 充當(dāng)著一個(gè)提供 Executor 實(shí)現(xiàn)實(shí)例的共同位置,但它只有 new 方法(例如用于創(chuàng)建新線程池);它沒有預(yù)先創(chuàng)建實(shí)例。所以您可以自行決定是否希望在代碼中創(chuàng)建和使用 Executor 實(shí)例。(或者在某些情況下,您將能夠使用所選的容器/平臺(tái)提供的實(shí)例。)
 ExecutorService 隨時(shí)可以使用
 盡管不必?fù)?dān)心 Thread 來(lái)自何處,但 Executor 接口缺乏 Java 開發(fā)人員可能期望的某種功能,比如結(jié)束一個(gè)用于生成結(jié)果的線程并以非阻塞方式等待結(jié)果可用。(這是桌面應(yīng)用程序的一個(gè)常見需求,用戶將執(zhí)行需要訪問(wèn)數(shù)據(jù)庫(kù)的 UI 操作,然后如果該操作花費(fèi)了很長(zhǎng)時(shí)間,可能希望在它完成之前取消它。)
 對(duì)于此問(wèn)題,JSR-166 專家創(chuàng)建了一個(gè)更加有用的抽象(ExecutorService 接口),它將線程啟動(dòng)工廠建模為一個(gè)可集中控制的服務(wù)。例如,無(wú)需每執(zhí)行一項(xiàng)任務(wù)就調(diào)用一次 execute(),ExecutorService 可以接受一組任務(wù)并返回一個(gè)表示每項(xiàng)任務(wù)的未來(lái)結(jié)果的未來(lái)列表。
ScheduledExecutorServices
 盡管 ExecutorService 接口非常有用,但某些任務(wù)仍需要以計(jì)劃方式執(zhí)行,比如以確定的時(shí)間間隔或在特定時(shí)間執(zhí)行給定的任務(wù)。這就是 ScheduledExecutorService 的應(yīng)用范圍,它擴(kuò)展了 ExecutorService。
 如果您的目標(biāo)是創(chuàng)建一個(gè)每隔 5 秒跳一次的 “心跳” 命令,使用 ScheduledExecutorService 可以輕松實(shí)現(xiàn),如清單 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);
    }
}
 這項(xiàng)功能怎么樣?不用過(guò)于擔(dān)心線程,不用過(guò)于擔(dān)心用戶希望取消心跳時(shí)會(huì)發(fā)生什么,也不用明確地將線程標(biāo)記為前臺(tái)或后臺(tái);只需將所有的計(jì)劃細(xì)節(jié)留給 ScheduledExecutorService。
 順便說(shuō)一下,如果用戶希望取消心跳,scheduleAtFixedRate 調(diào)用將返回一個(gè) ScheduledFuture 實(shí)例,它不僅封裝了結(jié)果(如果有),還擁有一個(gè) cancel 方法來(lái)關(guān)閉計(jì)劃的操作。
Timeout 方法
 為阻塞操作設(shè)置一個(gè)具體的超時(shí)值(以避免死鎖)的能力是 java.util.concurrent 庫(kù)相比起早期并發(fā)特性的一大進(jìn)步,比如監(jiān)控鎖定。
 這些方法幾乎總是包含一個(gè) int/TimeUnit 對(duì),指示這些方法應(yīng)該等待多長(zhǎng)時(shí)間才釋放控制權(quán)并將其返回給程序。它需要開發(fā)人員執(zhí)行更多工作 — 如果沒有獲取鎖,您將如何重新獲取? — 但結(jié)果幾乎總是正確的:更少的死鎖和更加適合生產(chǎn)的代碼。(關(guān)于編寫生產(chǎn)就緒代碼的更多信息,請(qǐng)參見 參考資料 中 Michael Nygard 編寫的 Release It!。)
結(jié)束語(yǔ)
 java.util.concurrent 包還包含了其他許多好用的實(shí)用程序,它們很好地?cái)U(kuò)展到了 Collections 之外,尤其是在 .locks 和 .atomic 包中。深入研究,您還將發(fā)現(xiàn)一些有用的控制結(jié)構(gòu),比如 CyclicBarrier 等。
 與 Java 平臺(tái)的許多其他方面一樣,您無(wú)需費(fèi)勁地查找可能非常有用的基礎(chǔ)架構(gòu)代碼。在編寫多線程代碼時(shí),請(qǐng)記住本文討論的實(shí)用程序和 上一篇文章 中討論的實(shí)用程序。
關(guān)于 Java Collections API 您不知道的 5 件事,第 1 部分
 對(duì)于很多 Java 開發(fā)人員來(lái)說(shuō),Java Collections API 是標(biāo)準(zhǔn) Java 數(shù)組及其所有缺點(diǎn)的一個(gè)非常需要的替代品。將 Collections 主要與 ArrayList 聯(lián)系到一起本身沒有錯(cuò),但是對(duì)于那些有探索精神的人來(lái)說(shuō),這只是 Collections 的冰山一角。
Collections 比數(shù)組好
 剛接觸 Java 技術(shù)的開發(fā)人員可能不知道,Java 語(yǔ)言初包括數(shù)組,是為了應(yīng)對(duì)上世紀(jì) 90 年代初期 C++ 開發(fā)人員對(duì)于性能方面的批評(píng)。從那時(shí)到現(xiàn)在,我們已經(jīng)走過(guò)一段很長(zhǎng)的路,如今,與 Java Collections 庫(kù)相比,數(shù)組不再有性能優(yōu)勢(shì)。
 例如,若要將數(shù)組的內(nèi)容轉(zhuǎn)儲(chǔ)到一個(gè)字符串,需要迭代整個(gè)數(shù)組,然后將內(nèi)容連接成一個(gè) String;而 Collections 的實(shí)現(xiàn)都有一個(gè)可用的 toString() 實(shí)現(xiàn)。
 除少數(shù)情況外,好的做法是盡快將遇到的任何數(shù)組轉(zhuǎn)換成集合。于是問(wèn)題來(lái)了,完成這種轉(zhuǎn)換的容易的方式是什么?事實(shí)證明,Java Collections API 使這種轉(zhuǎn)換變得容易,如清單 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 是不可修改的,所以如果嘗試向其中添加新元素將拋出一個(gè) UnsupportedOperationException。
 而且,由于 Arrays.asList() 使用 varargs 參數(shù)表示添加到 List 的元素,所以還可以使用它輕松地用以 new 新建的對(duì)象創(chuàng)建 List。
迭代的效率較低
 將一個(gè)集合(特別是由數(shù)組轉(zhuǎn)化而成的集合)的內(nèi)容轉(zhuǎn)移到另一個(gè)集合,或者從一個(gè)較大對(duì)象集合中移除一個(gè)較小對(duì)象集合,這些事情并不鮮見。
 您也許很想對(duì)集合進(jìn)行迭代,然后添加元素或移除找到的元素,但是不要這樣做。
 在此情況下,迭代有很大的缺點(diǎn):
每次添加或移除元素后重新調(diào)整集合將非常低效。
每次在獲取鎖、執(zhí)行操作和釋放鎖的過(guò)程中,都存在潛在的并發(fā)困境。
當(dāng)添加或移除元素時(shí),存取集合的其他線程會(huì)引起競(jìng)爭(zhēng)條件。
 可以通過(guò)使用 addAll 或 removeAll,傳入包含要對(duì)其添加或移除元素的集合作為參數(shù),來(lái)避免所有這些問(wèn)題。
用 for 循環(huán)遍歷任何 Iterable
 Java 5 中加入 Java 語(yǔ)言的大的便利功能之一,增強(qiáng)的 for 循環(huán),消除了使用 Java 集合的后一道障礙。
 以前,開發(fā)人員必須手動(dòng)獲得一個(gè) Iterator,使用 next() 獲得 Iterator 指向的對(duì)象,并通過(guò) hasNext() 檢查是否還有更多可用對(duì)象。從 Java 5 開始,我們可以隨意使用 for 循環(huán)的變種,它可以在幕后處理上述所有工作。
 實(shí)際上,這個(gè)增強(qiáng)適用于實(shí)現(xiàn) Iterable 接口的任何對(duì)象,而不僅僅是 Collections。
 清單 2 顯示通過(guò) Iterator 提供 Person 對(duì)象的孩子列表的一種方法。 這里不是提供內(nèi)部 List 的一個(gè)引用 (這使 Person 外的調(diào)用者可以為家庭增加孩子 — 而大多數(shù)父母并不希望如此),Person 類型實(shí)現(xiàn) Iterable。這種方法還使得 for 循環(huán)可以遍歷所有孩子。
清單 2. 增強(qiáng)的 for 循環(huán):顯示孩子
// 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());
        }
    }
}
 在域建模的時(shí)候,使用 Iterable 有一些明顯的缺陷,因?yàn)橥ㄟ^(guò) iterator() 方法只能那么 “隱晦” 地支持一個(gè)那樣的對(duì)象集合。但是,如果孩子集合比較明顯,Iterable 可以使針對(duì)域類型的編程更容易,更直觀。
經(jīng)典算法和定制算法
 您是否曾想過(guò)以倒序遍歷一個(gè) Collection?對(duì)于這種情況,使用經(jīng)典的 Java Collections 算法非常方便。
 在上面的 清單 2 中,Person 的孩子是按照傳入的順序排列的;但是,現(xiàn)在要以相反的順序列出他們。雖然可以編寫另一個(gè) for 循環(huán),按相反順序?qū)⒚總€(gè)對(duì)象插入到一個(gè)新的 ArrayList 中,但是 3、4 次重復(fù)這樣做之后,就會(huì)覺得很麻煩。
 在此情況下,清單 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 類有很多這樣的 “算法”,它們被實(shí)現(xiàn)為靜態(tài)方法,以 Collections 作為參數(shù),提供獨(dú)立于實(shí)現(xiàn)的針對(duì)整個(gè)集合的行為。
 而且,由于很棒的 API 設(shè)計(jì),我們不必完全受限于 Collections 類中提供的算法 — 例如,我喜歡不直接修改(傳入的 Collection 的)內(nèi)容的方法。所以,可以編寫定制算法是一件很棒的事情,例如清單 4 就是一個(gè)這樣的例子:
清單 4. ReverseIterator 使事情更簡(jiǎn)單
class MyCollections
{
    public static <T> List<T> reverse(List<T> src)
    {
        List<T> results = new ArrayList<T>(src);
        Collections.reverse(results);
        return results;
    }
}
擴(kuò)展 Collections API
 以上定制算法闡釋了關(guān)于 Java Collections API 的一個(gè)終觀點(diǎn):它總是適合加以擴(kuò)展和修改,以滿足開發(fā)人員的特定目的。
 例如,假設(shè)您需要 Person 類中的孩子總是按年齡排序。雖然可以編寫代碼一遍又一遍地對(duì)孩子排序(也許是使用 Collections.sort 方法),但是通過(guò)一個(gè) Collection 類來(lái)自動(dòng)排序要好得多。
 實(shí)際上,您甚至可能不關(guān)心是否每次按固定的順序?qū)?duì)象插入到 Collection 中(這正是 List 的基本原理)。您可能只是想讓它們按一定的順序排列。
 java.util 中沒有 Collection 類能滿足這些需求,但是編寫一個(gè)這樣的類很簡(jiǎn)單。只需創(chuàng)建一個(gè)接口,用它描述 Collection 應(yīng)該提供的抽象行為。對(duì)于 SortedCollection,它的作用完全是行為方面的。
清單 5. SortedCollection
public interface SortedCollection<E> extends Collection<E>
{
    public Comparator<E> getComparator();
    public void setComparator(Comparator<E> comp);
}
 編寫這個(gè)新接口的實(shí)現(xiàn)簡(jiǎn)直不值一提:
清單 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);
    }
}
 這個(gè)實(shí)現(xiàn)非常簡(jiǎn)陋,編寫時(shí)并沒有考慮優(yōu)化,顯然還需要進(jìn)行重構(gòu)。但關(guān)鍵是 Java Collections API 從來(lái)無(wú)意將與集合相關(guān)的任何東西定死。它總是需要擴(kuò)展,同時(shí)也鼓勵(lì)擴(kuò)展。
 當(dāng)然,有些擴(kuò)展比較復(fù)雜,例如 java.util.concurrent 中引入的擴(kuò)展。但是另一些則非常簡(jiǎn)單,只需編寫一個(gè)定制算法,或者已有 Collection 類的簡(jiǎn)單的擴(kuò)展。
 擴(kuò)展 Java Collections API 看上去很難,但是一旦開始著手,您會(huì)發(fā)現(xiàn)遠(yuǎn)不如想象的那樣難。
關(guān)于 Java Collections API 您不知道的 5 件事,第 2 部分
 java.util 中的 Collections 類旨在通過(guò)取代數(shù)組提高 Java 性能。如您在 第 1 部分 中了解到的,它們也是多變的,能夠以各種方式定制和擴(kuò)展,幫助實(shí)現(xiàn)優(yōu)質(zhì)、簡(jiǎn)潔的代碼。
 Collections 非常強(qiáng)大,但是很多變:使用它們要小心,濫用它們會(huì)帶來(lái)風(fēng)險(xiǎn)。
List 不同于數(shù)組
 Java 開發(fā)人員常常錯(cuò)誤地認(rèn)為 ArrayList 就是 Java 數(shù)組的替代品。Collections 由數(shù)組支持,在集合內(nèi)隨機(jī)查找內(nèi)容時(shí)性能較好。與數(shù)組一樣,集合使用整序數(shù)獲取特定項(xiàng)。但集合不是數(shù)組的簡(jiǎn)單替代。
 要明白數(shù)組與集合的區(qū)別需要弄清楚順序 和位置 的不同。例如,List 是一個(gè)接口,它保存各個(gè)項(xiàng)被放入集合中的順序,如清單 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);
    }
}
 當(dāng)?shù)谌齻€(gè)元素從上面的 List 中被移除時(shí),其 “后面” 的各項(xiàng)會(huì)上升填補(bǔ)空位。很顯然,此集合行為與數(shù)組的行為不同(事實(shí)上,從數(shù)組中移除項(xiàng)與從 List 中移除它也不完全是一回事兒 — 從數(shù)組中 “移除” 項(xiàng)意味著要用新引用或 null 覆蓋其索引槽)。
令人驚訝的 Iterator!
 無(wú)疑 Java 開發(fā)人員很喜愛 Java 集合 Iterator,但是您后一次使用 Iterator 接口是什么時(shí)候的事情了?可以這么說(shuō),大部分時(shí)間我們只是將 Iterator 隨意放到 for() 循環(huán)或加強(qiáng) for() 循環(huán)中,然后就繼續(xù)其他操作了。
 但是進(jìn)行深入研究后,您會(huì)發(fā)現(xiàn) Iterator 實(shí)際上有兩個(gè)十分有用的功能。
 第一,Iterator 支持從源集合中安全地刪除對(duì)象,只需在 Iterator 上調(diào)用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException,這個(gè)異常顧名思意:當(dāng)打開 Iterator 迭代集合時(shí),同時(shí)又在對(duì)集合進(jìn)行修改。有些集合不允許在迭代時(shí)刪除或添加元素,但是調(diào)用 Iterator 的 remove() 方法是個(gè)安全的做法。
 第二,Iterator 支持派生的(并且可能是更強(qiáng)大的)兄弟成員。ListIterator,只存在于 List 中,支持在迭代期間向 List 中添加或刪除元素,并且可以在 List 中雙向滾動(dòng)。
 雙向滾動(dòng)特別有用,尤其是在無(wú)處不在的 “滑動(dòng)結(jié)果集” 操作中,因?yàn)榻Y(jié)果集中只能顯示從數(shù)據(jù)庫(kù)或其他集合中獲取的眾多結(jié)果中的 10 個(gè)。它還可以用于 “反向遍歷” 集合或列表,而無(wú)需每次都從前向后遍歷。插入 ListIterator 比使用向下計(jì)數(shù)整數(shù)參數(shù) List.get() “反向” 遍歷 List 容易得多。
并非所有 Iterable 都來(lái)自集合
 Ruby 和 Groovy 開發(fā)人員喜歡炫耀他們?nèi)绾文艿麄€(gè)文本文件并通過(guò)一行代碼將其內(nèi)容輸出到控制臺(tái)。通常,他們會(huì)說(shuō)在 Java 編程中完成同樣的操作需要很多行代碼:打開 FileReader,然后打開 BufferedReader,接著創(chuàng)建 while() 循環(huán)來(lái)調(diào)用 getLine(),直到它返回 null。當(dāng)然,在 try/catch/finally 塊中必須要完成這些操作,它要處理異常并在結(jié)束時(shí)關(guān)閉文件句柄。
 這看起來(lái)像是一個(gè)沒有意義的學(xué)術(shù)上的爭(zhēng)論,但是它也有其自身的價(jià)值。
 他們(包括相當(dāng)一部分 Java 開發(fā)人員)不知道并不是所有 Iterable 都來(lái)自集合。Iterable 可以創(chuàng)建 Iterator,該迭代器知道如何憑空制造下一個(gè)元素,而不是從預(yù)先存在的 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);
    }
}
 此方法的優(yōu)勢(shì)是不會(huì)在內(nèi)存中保留整個(gè)內(nèi)容,但是有一個(gè)警告就是,它不能 close() 底層文件句柄(每當(dāng) readLine() 返回 null 時(shí)就關(guān)閉文件句柄,可以修正這一問(wèn)題,但是在 Iterator 沒有結(jié)束時(shí)不能解決這個(gè)問(wèn)題)。
注意可變的 hashCode()
 Map 是很好的集合,為我們帶來(lái)了在其他語(yǔ)言(比如 Perl)中經(jīng)常可見的好用的鍵/值對(duì)集合。JDK 以 HashMap 的形式為我們提供了方便的 Map 實(shí)現(xiàn),它在內(nèi)部使用哈希表實(shí)現(xiàn)了對(duì)鍵的對(duì)應(yīng)值的快速查找。但是這里也有一個(gè)小問(wèn)題:支持哈希碼的鍵依賴于可變字段的內(nèi)容,這樣容易產(chǎn)生 bug,即使耐心的 Java 開發(fā)人員也會(huì)被這些 bug 逼瘋。
 假設(shè)清單 3 中的 Person 對(duì)象有一個(gè)常見的 hashCode() (它使用 firstName、lastName 和 age 字段 — 所有字段都不是 final 字段 — 計(jì)算 hashCode()),對(duì) Map 的 get() 調(diào)用會(huì)失敗并返回 null:
清單 3. 可變 hashCode() 容易出現(xiàn) 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));
    }
}

 很顯然,這種方法很糟糕,但是解決方法也很簡(jiǎn)單:永遠(yuǎn)不要將可變對(duì)象類型用作 HashMap 中的鍵。
equals() 與 Comparable
 在瀏覽 Javadoc 時(shí),Java 開發(fā)人員常常會(huì)遇到 SortedSet 類型(它在 JDK 中唯一的實(shí)現(xiàn)是 TreeSet)。因?yàn)?SortedSet 是 java.util 包中唯一提供某種排序行為的 Collection,所以開發(fā)人員通常直接使用它而不會(huì)仔細(xì)地研究它。清單 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);
    }
}
 使用上述代碼一段時(shí)間后,可能會(huì)發(fā)現(xiàn)這個(gè) Set 的核心特性之一:它不允許重復(fù)。該特性在 Set Javadoc 中進(jìn)行了介紹。Set 是不包含重復(fù)元素的集合。更準(zhǔn)確地說(shuō),set 不包含成對(duì)的 e1 和 e2 元素,因此如果 e1.equals(e2),那么多包含一個(gè) null 元素。
 但實(shí)際上似乎并非如此 — 盡管 清單 4 中沒有相等的 Person 對(duì)象(根據(jù) Person 的 equals() 實(shí)現(xiàn)),但在輸出時(shí)只有三個(gè)對(duì)象出現(xiàn)在 TreeSet 中。
 與 set 的有狀態(tài)本質(zhì)相反,TreeSet 要求對(duì)象直接實(shí)現(xiàn) Comparable 或者在構(gòu)造時(shí)傳入 Comparator,它不使用 equals() 比較對(duì)象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。
 因此存儲(chǔ)在 Set 中的對(duì)象有兩種方式確定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪種方法取決于上下文。
 更糟的是,簡(jiǎn)單的聲明兩者相等還不夠,因?yàn)橐耘判驗(yàn)槟康牡谋容^不同于以相等性為目的的比較:可以想象一下按姓排序時(shí)兩個(gè) Person 相等,但是其內(nèi)容卻并不相同。
 一定要明白 equals() 和 Comparable.compareTo() 兩者之間的不同 — 實(shí)現(xiàn) Set 時(shí)會(huì)返回 0。甚至在文檔中也要明確兩者的區(qū)別。
關(guān)于 Java 對(duì)象序列化您不知道的 5 件事
 關(guān)于本系列
 您覺得自己懂 Java 編程?事實(shí)上,大多數(shù)程序員對(duì)于 Java 平臺(tái)都是淺嘗則止,只學(xué)習(xí)了足以完成手頭上任務(wù)的知識(shí)而已。在本 系列 中,Ted Neward 深入挖掘 Java 平臺(tái)的核心功能,揭示一些鮮為人知的事實(shí),幫助您解決棘手的編程挑戰(zhàn)。
 大約一年前,一個(gè)負(fù)責(zé)管理應(yīng)用程序所有用戶設(shè)置的開發(fā)人員,決定將用戶設(shè)置存儲(chǔ)在一個(gè) Hashtable 中,然后將這個(gè) Hashtable 序列化到磁盤,以便持久化。當(dāng)用戶更改設(shè)置時(shí),便重新將 Hashtable 寫到磁盤。
 這是一個(gè)優(yōu)雅的、開放式的設(shè)置系統(tǒng),但是,當(dāng)團(tuán)隊(duì)決定從 Hashtable 遷移到 Java Collections 庫(kù)中的 HashMap 時(shí),這個(gè)系統(tǒng)便面臨崩潰。
 Hashtable 和 HashMap 在磁盤上的格式是不相同、不兼容的。除非對(duì)每個(gè)持久化的用戶設(shè)置運(yùn)行某種類型的數(shù)據(jù)轉(zhuǎn)換實(shí)用程序(極其龐大的任務(wù)),否則以后似乎只能一直用 Hashtable 作為應(yīng)用程序的存儲(chǔ)格式。
 團(tuán)隊(duì)感到陷入僵局,但這只是因?yàn)樗麄儾恢狸P(guān)于 Java 序列化的一個(gè)重要事實(shí):Java 序列化允許隨著時(shí)間的推移而改變類型。當(dāng)我向他們展示如何自動(dòng)進(jìn)行序列化替換后,他們終于按計(jì)劃完成了向 HashMap 的轉(zhuǎn)變。
 本文是本系列的第一篇文章,這個(gè)系列專門揭示關(guān)于 Java 平臺(tái)的一些有用的小知識(shí) — 這些小知識(shí)不易理解,但對(duì)于解決 Java 編程挑戰(zhàn)遲早有用。
 將 Java 對(duì)象序列化 API 作為開端是一個(gè)不錯(cuò)的選擇,因?yàn)樗鼜囊婚_始就存在于 JDK 1.1 中。本文介紹的關(guān)于序列化的 5 件事情將說(shuō)服您重新審視那些標(biāo)準(zhǔn) Java API。
 Java 序列化簡(jiǎn)介
 Java 對(duì)象序列化是 JDK 1.1 中引入的一組開創(chuàng)性特性之一,用于作為一種將 Java 對(duì)象的狀態(tài)轉(zhuǎn)換為字節(jié)數(shù)組,以便存儲(chǔ)或傳輸?shù)臋C(jī)制,以后,仍可以將字節(jié)數(shù)組轉(zhuǎn)換回 Java 對(duì)象原有的狀態(tài)。
 實(shí)際上,序列化的思想是 “凍結(jié)” 對(duì)象狀態(tài),傳輸對(duì)象狀態(tài)(寫到磁盤、通過(guò)網(wǎng)絡(luò)傳輸?shù)鹊龋?,然?“解凍” 狀態(tài),重新獲得可用的 Java 對(duì)象。所有這些事情的發(fā)生有點(diǎn)像是魔術(shù),這要?dú)w功于 ObjectInputStream/ObjectOutputStream 類、完全保真的元數(shù)據(jù)以及程序員愿意用 Serializable 標(biāo)識(shí)接口標(biāo)記他們的類,從而 “參與” 這個(gè)過(guò)程。
 清單 1 顯示一個(gè)實(shí)現(xiàn) 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 序列化后,很容易將對(duì)象狀態(tài)寫到磁盤,然后重新讀出它,下面的 JUnit 4 單元測(cè)試對(duì)此做了演示。
清單 2. 對(duì) Person 進(jìn)行反序列化
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());
        }
    }
}
 到現(xiàn)在為止,還沒有看到什么新鮮的或令人興奮的事情,但是這是一個(gè)很好的出發(fā)點(diǎn)。我們將使用 Person 來(lái)發(fā)現(xiàn)您可能不知道的關(guān)于 Java 對(duì)象序列化 的 5 件事。
序列化允許重構(gòu)
 序列化允許一定數(shù)量的類變種,甚至重構(gòu)之后也是如此,ObjectInputStream 仍可以很好地將其讀出來(lái)。
 Java Object Serialization 規(guī)范可以自動(dòng)管理的關(guān)鍵任務(wù)是:
將新字段添加到類中
將字段從 static 改為非 static
將字段從 transient 改為非 transient
 取決于所需的向后兼容程度,轉(zhuǎn)換字段形式(從非 static 轉(zhuǎn)換為 static 或從非 transient 轉(zhuǎn)換為 transient)或者刪除字段需要額外的消息傳遞。
 重構(gòu)序列化類
 既然已經(jīng)知道序列化允許重構(gòu),我們來(lái)看看當(dāng)把新字段添加到 Person 類中時(shí),會(huì)發(fā)生什么事情。
 如清單 3 所示,PersonV2 在原先 Person 類的基礎(chǔ)上引入一個(gè)表示性別的新字段。
清單 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;
}
 序列化使用一個(gè) hash,該 hash 是根據(jù)給定源文件中幾乎所有東西 — 方法名稱、字段名稱、字段類型、訪問(wèn)修改方法等 — 計(jì)算出來(lái)的,序列化將該 hash 值與序列化流中的 hash 值相比較。
 為了使 Java 運(yùn)行時(shí)相信兩種類型實(shí)際上是一樣的,第二版和隨后版本的 Person 必須與第一版有相同的序列化版本 hash(存儲(chǔ)為 private static final serialVersionUID 字段)。因此,我們需要 serialVersionUID 字段,它是通過(guò)對(duì)原始(或 V1)版本的 Person 類運(yùn)行 JDK serialver 命令計(jì)算出的。
 一旦有了 Person 的 serialVersionUID,不僅可以從原始對(duì)象 Person 的序列化數(shù)據(jù)創(chuàng)建 PersonV2 對(duì)象(當(dāng)出現(xiàn)新字段時(shí),新字段被設(shè)為缺省值,常見的是“null”),還可以反過(guò)來(lái)做:即從 PersonV2 的數(shù)據(jù)通過(guò)反序列化得到 Person,這毫不奇怪。
序列化并不安全
 讓 Java 開發(fā)人員詫異并感到不快的是,序列化二進(jìn)制格式完全編寫在文檔中,并且完全可逆。實(shí)際上,只需將二進(jìn)制序列化流的內(nèi)容轉(zhuǎn)儲(chǔ)到控制臺(tái),就足以看清類是什么樣子,以及它包含什么內(nèi)容。
 這對(duì)于安全性有著不良影響。例如,當(dāng)通過(guò) RMI 進(jìn)行遠(yuǎn)程方法調(diào)用時(shí),通過(guò)連接發(fā)送的對(duì)象中的任何 private 字段幾乎都是以明文的方式出現(xiàn)在套接字流中,這顯然容易招致哪怕簡(jiǎn)單的安全問(wèn)題。
 幸運(yùn)的是,序列化允許 “hook” 序列化過(guò)程,并在序列化之前和反序列化之后保護(hù)(或模糊化)字段數(shù)據(jù)。可以通過(guò)在 Serializable 對(duì)象上提供一個(gè) writeObject 方法來(lái)做到這一點(diǎn)。
 模糊化序列化數(shù)據(jù)
 假設(shè) Person 類中的敏感數(shù)據(jù)是 age 字段。畢竟,女士忌談年齡。我們可以在序列化之前模糊化該數(shù)據(jù),將數(shù)位循環(huán)左移一位,然后在反序列化之后復(fù)位。(您可以開發(fā)更安全的算法,當(dāng)前這個(gè)算法只是作為一個(gè)例子。)
 為了 “hook” 序列化過(guò)程,我們將在 Person 上實(shí)現(xiàn)一個(gè) writeObject 方法;為了 “hook” 反序列化過(guò)程,我們將在同一個(gè)類上實(shí)現(xiàn)一個(gè) readObject 方法。重要的是這兩個(gè)方法的細(xì)節(jié)要正確 — 如果訪問(wèn)修改方法、參數(shù)或名稱不同于清單 4 中的內(nèi)容,那么代碼將不被察覺地失敗,Person 的 age 將暴露。
清單 4. 模糊化序列化數(shù)據(jù)
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;
}
 如果需要查看被模糊化的數(shù)據(jù),總是可以查看序列化數(shù)據(jù)流/文件。而且,由于該格式被完全文檔化,即使不能訪問(wèn)類本身,也仍可以讀取序列化流中的內(nèi)容。
序列化的數(shù)據(jù)可以被簽名和密封
 上一個(gè)技巧假設(shè)您想模糊化序列化數(shù)據(jù),而不是對(duì)其加密或者確保它不被修改。當(dāng)然,通過(guò)使用 writeObject 和 readObject 可以實(shí)現(xiàn)密碼加密和簽名管理,但其實(shí)還有更好的方式。
 如果需要對(duì)整個(gè)對(duì)象進(jìn)行加密和簽名,簡(jiǎn)單的是將它放在一個(gè) javax.crypto.SealedObject 和/或 java.security.SignedObject 包裝器中。兩者都是可序列化的,所以將對(duì)象包裝在 SealedObject 中可以圍繞原對(duì)象創(chuàng)建一種 “包裝盒”。必須有對(duì)稱密鑰才能解密,而且密鑰必須單獨(dú)管理。同樣,也可以將 SignedObject 用于數(shù)據(jù)驗(yàn)證,并且對(duì)稱密鑰也必須單獨(dú)管理。
 結(jié)合使用這兩種對(duì)象,便可以輕松地對(duì)序列化數(shù)據(jù)進(jìn)行密封和簽名,而不必強(qiáng)調(diào)關(guān)于數(shù)字簽名驗(yàn)證或加密的細(xì)節(jié)。很簡(jiǎn)潔,是吧?
序列化允許將代理放在流中
 很多情況下,類中包含一個(gè)核心數(shù)據(jù)元素,通過(guò)它可以派生或找到類中的其他字段。在此情況下,沒有必要序列化整個(gè)對(duì)象。可以將字段標(biāo)記為 transient,但是每當(dāng)有方法訪問(wèn)一個(gè)字段時(shí),類仍然必須顯式地產(chǎn)生代碼來(lái)檢查它是否被初始化。
 如果首要問(wèn)題是序列化,那么好指定一個(gè) flyweight 或代理放在流中。為原始 Person 提供一個(gè) writeReplace 方法,可以序列化不同類型的對(duì)象來(lái)代替它。類似地,如果反序列化期間發(fā)現(xiàn)一個(gè) readResolve 方法,那么將調(diào)用該方法,將替代對(duì)象提供給調(diào)用者。
 打包和解包代理
 writeReplace 和 readResolve 方法使 Person 類可以將它的所有數(shù)據(jù)(或其中的核心數(shù)據(jù))打包到一個(gè) PersonProxy 中,將它放入到一個(gè)流中,然后在反序列化時(shí)再進(jìn)行解包。
清單 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 的所有數(shù)據(jù)。這通常意味著代理需要是 Person 的一個(gè)內(nèi)部類,以便能訪問(wèn) private 字段。有時(shí)候,代理還需要追蹤其他對(duì)象引用并手動(dòng)序列化它們,例如 Person 的 spouse。
 這種技巧是少數(shù)幾種不需要讀/寫平衡的技巧之一。例如,一個(gè)類被重構(gòu)成另一種類型后的版本可以提供一個(gè) readResolve 方法,以便靜默地將被序列化的對(duì)象轉(zhuǎn)換成新類型。類似地,它可以采用 writeReplace 方法將舊類序列化成新版本。
信任,但要驗(yàn)證
 認(rèn)為序列化流中的數(shù)據(jù)總是與初寫到流中的數(shù)據(jù)一致,這沒有問(wèn)題。但是,正如一位美國(guó)前總統(tǒng)所說(shuō)的,“信任,但要驗(yàn)證”。
 對(duì)于序列化的對(duì)象,這意味著驗(yàn)證字段,以確保在反序列化之后它們?nèi)跃哂姓_的值,“以防萬(wàn)一”。為此,可以實(shí)現(xiàn) ObjectInputValidation 接口,并覆蓋 validateObject() 方法。如果調(diào)用該方法時(shí)發(fā)現(xiàn)某處有錯(cuò)誤,則拋出一個(gè) InvalidObjectException。


分享到:

相關(guān)閱讀:

近期文章

搶試聽名額

名額僅剩66名

教育改變生活

WE CHANGE LIVES