StAX – 解析XML文件
在千禧年左右,當(dāng) XML 第一次出現(xiàn)在很多 Java 開發(fā)人員面前時,有兩種基本的解析 XML 文件的方法。SAX 解析器實際是由程序員對事件調(diào)用一系列回調(diào)方法的大型狀態(tài)機。DOM 解析器將整個 XML 文檔加入內(nèi)存,并切割成離散的對象,它們連接在一起形成一個樹。該樹描述了文檔的整個 XML Infoset 表示法。這兩個解析器都有缺點:SAX 太低級,無法使用,DOM 代價太大,尤其對于大的 XML 文件 — 整個樹成了一個龐然大物。
幸運的是,Java 開發(fā)人員找到第三種方法來解析 XML 文件,通過對文檔建模成 “節(jié)點”,它們可以從文檔流中一次取出一個,檢查,然后處理或丟棄。這些 “節(jié)點” 的 “流” 提供了 SAX 和 DOM 的中間地帶,名為 “Streaming API for XML”,或者叫做StAX。(此縮寫用于區(qū)分新的 API 與原來的 SAX 解析器,它與此同名。)StAX 解析器后來包裝到了 JDK 中,在 javax.xml.stream 包。
使用 StAX 相當(dāng)簡單:實例化 XMLEventReader,將它指向一個格式良好的 XML 文件,然后一次 “拉出” 一個節(jié)點(通常用 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 解析器不會替換所有的 SAX 和 DOM 代碼。但肯定會讓某些任務(wù)容易些。尤其對完成不需要知道 XML 文檔整個樹結(jié)構(gòu)的任務(wù)相當(dāng)方便。
請注意,如果事件對象級別太高,無法使用,StAX 也有一個低級 API 在 XMLStreamReader 中。盡管也許沒有閱讀器有用,StAX 還有一個 XMLEventWriter,同樣,還有一個 XMLStreamWriter 類用于 XML 輸出。
ServiceLoader – 加載服務(wù)(獲取接口的所有實現(xiàn))
Java 開發(fā)人員經(jīng)常希望將使用和創(chuàng)建組件的內(nèi)容區(qū)分開來。這通常是通過創(chuàng)建一個描述組件動作的接口,并使用某種中介創(chuàng)建組件實例來完成的。很多開發(fā)人員使用 Spring 框架來完成,但還有其他的方法,它比 Spring 容器更輕量級。
java.util 的 ServiceLoader 類能讀取隱藏在 JAR 文件中的配置文件,并找到接口的實現(xiàn),然后使這些實現(xiàn)成為可選擇的列表。例如,如果您需要一個私仆(personal-servant)組件來完成任務(wù),您可以使用清單 2 中的代碼來實現(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() 方法可讓您確定所提供的私仆實現(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è)有此接口的實現(xiàn),如清單 4:
清單 4. Jeeves 實現(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;
}
}
剩下的就是配置包含實現(xiàn)的 JAR 文件,讓 ServiceLoader 能識別 — 這可能會非常棘手。JDK 想要 JAR 文件有一個 META-INF/services 目錄,它包含一個文本文件,其文件名與接口類名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口類名的內(nèi)容是實現(xiàn)的名稱,每行一個,如清單 5:
清單 5. META-INF/services/IPersonalServant
Jeeves # comments are OK
幸運的是,Ant 構(gòu)建系統(tǒng)(自 1.7.0 以來)包含一個對 jar 任務(wù)的服務(wù)標(biāo)簽,讓這相對容易,見清單 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í)行這些命令可能會非常棘手。這又是另一個 “小線頭”。
Scanner
有無數(shù) Java 工具能幫助您構(gòu)建解析器,很多函數(shù)語言已成功構(gòu)建解析器函數(shù)庫(解析器選擇器)。但如果要解析的是逗號分隔值文件,或空格分隔文本文件,又怎么辦呢?大多數(shù)工具用在此處就過于隆重了,而 String.split() 又不夠。(對于正則表達式,請記住一句老話:“ 您有一個問題,用正則表達式解決。那您就有兩個問題了。”)
Java 平臺的 Scanner 類會是這些類中您好的選擇。以輕量級文本解析器為目標(biāo),Scanner 提供了一個相對簡單的 API,用于提取結(jié)構(gòu)化文本,并放入強類型的部分。想象一下,如果您愿意,一組類似 DSL 的命令(源自 Terry Pratchett Discworld 小說)排列在文本文件中,如清單 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 中注冊,可以很方便地將 can() 調(diào)用改得更實用,并重用前面的 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 實現(xiàn)顯然不會僅僅打印到標(biāo)準(zhǔn)輸出流。我把追蹤哪些部分、跟隨哪些部分的細節(jié)留待給您(當(dāng)然,還有忠誠的 Igor)。
Timer
java.util.Timer 和 TimerTask 類提供了方便、相對簡單的方法可在定期或一次性延遲的基礎(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ù)的,并且有一個啟動的 TimerTask 實例。TimerTask 實際上是一個 Runnable(事實上,它實現(xiàn)了它),但還有另外兩個方法:cancel() 用來取消任務(wù),scheduledExecutionTime() 用來返回任務(wù)何時啟動的近似值。
請注意 Timer 卻創(chuàng)建了一個非守護線程在后臺啟動任務(wù),因此在清單 10 中我需要調(diào)用 System.exit() 來取消任務(wù)。在長時間運行的程序中,好創(chuàng)建一個 Timer 守護線程(使用帶有指示守護線程狀態(tài)的參數(shù)的構(gòu)造函數(shù)),從而它不會讓 VM 活動。
這個類沒什么神奇的,但它確實能幫助我們對后臺啟動的程序的目的了解得更清楚。它還能節(jié)省一些 Thread 代碼,并作為輕量級 ScheduledExecutorService(對于還沒準(zhǔn)備好了解整個 java.util.concurrent 包的人來說)。
JavaSound
盡管在服務(wù)器端應(yīng)用程序中不常出現(xiàn),但 sound 對管理員有著有用的 “被動” 意義 — 它是惡作劇的好材料。盡管它很晚才出現(xiàn)在 Java 平臺中,JavaSound API 終還是加入了核心運行時庫,封裝在 javax.sound * 包 — 其中一個包是 MIDI 文件,另一個是音頻文件示例(如普遍的 .WAV 文件格式)。
JavaSound 的 “hello world” 是播放一個片段,如清單 11 所示:
清單 11. 再放一遍,Sam
public static void playClip(String audioFile)
{
try
{
AudioInputStream inputStream =
AudioSystem.getAudioInputStream(
this.getClass().getResourceAsStream(audioFile));
DataLine.Info info =
new DataLine.Info( Clip.class, audioInputStream.getFormat() );
Clip clip = (Clip) AudioSystem.getLine(info);
clip.addLineListener(new LineListener() {
public void update(LineEvent e) {
if (e.getType() == LineEvent.Type.STOP) {
synchronized(clip) {
clip.notify();
}
}
}
});
clip.open(audioInputStream);
clip.setFramePosition(0);
clip.start();
synchronized (clip) {
clip.wait();
}
clip.drain();
clip.close();
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
大多數(shù)還是相當(dāng)簡單(至少 JavaSound 一樣簡單)。第一步是創(chuàng)建一個文件的 AudioInputStream 來播放。為了讓此方法盡量與上下文無關(guān),我們從加載類的 ClassLoader 中抓取文件作為 InputStream。(AudioSystem 還需要一個 File 或 String,如果提前知道聲音文件的具體路徑。)一旦完成, DataLine.Info 對象就提供給 AudioSystem,得到一個 Clip,這是播放音頻片段簡單的方法。(其他方法提供了對片段更多的控制 — 例如獲取一個 SourceDataLine — 但對于 “播放” 來說,過于復(fù)雜)。
這里應(yīng)該和對 AudioInputStream 調(diào)用 open() 一樣簡單。(“應(yīng)該” 的意思是如果您沒遇到下節(jié)描述的錯誤。)調(diào)用 start() 開始播放,drain() 等待播放完成,close() 釋放音頻線路。播放是在單獨的線程進行,因此調(diào)用 stop() 將會停止播放,然后調(diào)用 start() 將會從播放暫停的地方重新開始;使用 setFramePosition(0) 重新定位到開始。
沒聲音?
JDK 5 發(fā)行版中有個討厭的小錯誤:在有些平臺上,對于一些短的音頻片段,代碼看上去運行正常,但就是 ... 沒聲音。顯然媒體播放器在應(yīng)該出現(xiàn)的位置之前觸發(fā)了 STOP 事件。
這個錯誤 “無法修復(fù)”,但解決方法相當(dāng)簡單:注冊一個 LineListener 來監(jiān)聽 STOP 事件,當(dāng)觸發(fā)時,調(diào)用片段對象的 notifyAll()。然后在 “調(diào)用者” 代碼中,通過調(diào)用 wait() 等待片段完成(還調(diào)用 notifyAll())。在沒出現(xiàn)錯誤的平臺上,這些錯誤是多余的,在 Windows® 及有些 Linux® 版本上,會讓程序員 “開心” 或 “憤怒”。
使用 Java 語言進行 Unicode 代理編程
原文地址:http://www.ibm.com/developerworks/cn/java/j-unicode/index.html#
早期 Java 版本使用 16 位 char 數(shù)據(jù)類型表示 Unicode 字符。這種設(shè)計方法有時比較合理,因為所有 Unicode 字符擁有的值都小于 65,535 (0xFFFF),可以通過 16 位表示。但是,Unicode 后來將大值增加到 1,114,111 (0x10FFFF)。由于 16 位太小,不能表示 Unicode version 3.1 中的所有 Unicode 字符,32 位值 — 稱為碼位(code point) — 被用于 UTF-32 編碼模式。
但與 32 位值相比,16 位值的內(nèi)存使用效率更高,因此 Unicode 引入了一個種新設(shè)計方法來允許繼續(xù)使用 16 位值。UTF-16 中采用的這種設(shè)計方法分配 1,024 值給 16 位高代理(high surrogate),將另外的 1,024 值分配給 16 位低代理(low surrogate)。它使用一個高代理加上一個低代理 — 一個代理對(surrogate pair) — 來表示 65,536 (0x10000) 和 1,114,111 (0x10FFFF) 之間的 1,048,576 (0x100000) 值(1,024 和 1,024 的乘積)。
Java 1.5 保留了 char 類型的行為來表示 UTF-16 值(以便兼容現(xiàn)有程序),它實現(xiàn)了碼位的概念來表示 UTF-32 值。這個擴展(根據(jù) JSR 204:Unicode Supplementary Character Support 實現(xiàn))不需要記住 Unicode 碼位或轉(zhuǎn)換算法的準(zhǔn)確值 — 但理解代理 API 的正確用法很重要。
東亞國家和地區(qū)近年來增加了它們的字符集中的字符數(shù)量,以滿足用戶需求。這些標(biāo)準(zhǔn)包括來自中國的國家標(biāo)準(zhǔn)組織的 GB 18030 和來自日本的 JIS X 0213。因此,尋求遵守這些標(biāo)準(zhǔn)的程序更有必要支持 Unicode 代理對。本文解釋相關(guān) Java API 和編碼選項,面向計劃重新設(shè)計他們的軟件,從只能使用 char 類型的字符轉(zhuǎn)換為能夠處理代理對的新版本的讀者。
順序訪問
順序訪問是在 Java 語言中處理字符串的一個基本操作。在這種方法下,輸入字符串中的每個字符從頭至尾按順序訪問,或者有時從尾至頭訪問。本小節(jié)討論使用順序訪問方法從一個字符串創(chuàng)建一個 32 位碼位數(shù)組的 7 個技術(shù)示例,并估計它們的處理時間。
示例 1-1:基準(zhǔn)測試(不支持代理對)
清單 1 將 16 位 char 類型值直接分配給 32 位碼位值,完全沒有考慮代理對:
清單 1. 不支持代理對
int[] toCodePointArray(String str) { // Example 1-1
int len = str.length(); // the length of str
int[] acp = new int[len]; // an array of code points
for (int i = 0, j = 0; i < len; i++) {
acp[j++] = str.charAt(i);
}
return acp;
}
盡管這個示例不支持代理對,但它提供了一個處理時間基準(zhǔn)來比較后續(xù)順序訪問示例。
示例 1-2:使用 isSurrogatePair()
清單 2 使用 isSurrogatePair() 來計算代理對總數(shù)。計數(shù)之后,它分配足夠的內(nèi)存以便一個碼位數(shù)組存儲這個值。然后,它進入一個順序訪問循環(huán),使用 isHighSurrogate() 和 isLowSurrogate() 確定每個代理對字符是高代理還是低代理。當(dāng)它發(fā)現(xiàn)一個高代理后面帶一個低代理時,它使用 toCodePoint() 將該代理對轉(zhuǎn)換為一個碼位值并將當(dāng)前索引值增加 2。否則,它將這個 char 類型值直接分配給一個碼位值并將當(dāng)前索引值增加 1。這個示例的處理時間比 示例 1-1 長 1.38 倍。
清單 2. 有限支持
int[] toCodePointArray(String str) { // Example 1-2
int len = str.length(); // the length of str
int[] acp; // an array of code points
int surrogatePairCount = 0; // the count of surrogate pairs
for (int i = 1; i < len; i++) {
if (Character.isSurrogatePair(str.charAt(i - 1), str.charAt(i))) {
surrogatePairCount++;
i++;
}
}
acp = new int[len - surrogatePairCount];
for (int i = 0, j = 0; i < len; i++) {
char ch0 = str.charAt(i); // the current char
if (Character.isHighSurrogate(ch0) && i + 1 < len) {
char ch1 = str.charAt(i + 1); // the next char
if (Character.isLowSurrogate(ch1)) {
acp[j++] = Character.toCodePoint(ch0, ch1);
i++;
continue;
}
}
acp[j++] = ch0;
}
return acp;
}
清單 2 中更新軟件的方法很幼稚。它比較麻煩,需要大量修改,使得生成的軟件很脆弱且今后難以更改。具體而言,這些問題是:
需要計算碼位的數(shù)量以分配足夠的內(nèi)存
很難獲得字符串中的指定索引的正確碼位值
很難為下一個處理步驟正確移動當(dāng)前索引
一個改進后的算法出現(xiàn)在下一個示例中。
示例:基本支持
Java 1.5 提供了 codePointCount()、codePointAt() 和 offsetByCodePoints() 方法來分別處理 示例 1-2 的 3 個問題。清單 3 使用這些方法來改善這個算法的可讀性:
清單 3. 基本支持
int[] toCodePointArray(String str) { // Example 1-3
int len = str.length(); // the length of str
int[] acp = new int[str.codePointCount(0, len)];
for (int i = 0, j = 0; i < len; i = str.offsetByCodePoints(i, 1)) {
acp[j++] = str.codePointAt(i);
}
return acp;
}
但是,清單 3 的處理時間比 清單 1 長 2.8 倍。
示例 1-4:使用 codePointBefore()
當(dāng) offsetByCodePoints() 接收一個負(fù)數(shù)作為第二個參數(shù)時,它就能計算一個距離字符串頭的絕對偏移值。接下來,codePointBefore() 能夠返回一個指定索引前面的碼位值。這些方法用于清單 4 中從尾至頭遍歷字符串:
清單 4. 使用 codePointBefore() 的基本支持
int[] toCodePointArray(String str) { // Example 1-4
int len = str.length(); // the length of str
int[] acp = new int[str.codePointCount(0, len)];
int j = acp.length; // an index for acp
for (int i = len; i > 0; i = str.offsetByCodePoints(i, -1)) {
acp[--j] = str.codePointBefore(i);
}
return acp;
}
這個示例的處理時間 — 比 示例 1-1 長 2.72 倍 — 比 示例 1-3 快一些。通常,當(dāng)您比較零而不是非零值時,JVM 中的代碼大小要小一些,這有時會提高性能。但是,微小的改進可能不值得犧牲可讀性。
示例 1-5:使用 charCount()
示例 1-3 和 1-4 提供基本的代理對支持。他們不需要任何臨時變量,是健壯的編碼方法。要獲取更短的處理時間,使用 charCount() 而不是 offsetByCodePoints() 是有效的,但需要一個臨時變量來存放碼位值,如清單 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 的處理時間降低到比 示例 1-1 長 1.68 倍。
示例 1-6:訪問一個 char 數(shù)組
清單 6 在使用 示例 1-5 中展示的優(yōu)化的同時直接訪問一個 char 類型數(shù)組:
清單 6. 使用一個 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ù)制而來的。性能得到改善,因為對數(shù)組的直接訪問比通過一個方法的間接訪問要快。處理時間比 示例 1-1 長 1.51 倍。但是,當(dāng)調(diào)用時,toCharArray() 需要一些開銷來創(chuàng)建一個新數(shù)組并將數(shù)據(jù)復(fù)制到數(shù)組中。String 類提供的那些方便的方法也不能被使用。但是,這個算法在處理大量數(shù)據(jù)時有用。
示例 1-7:一個面向?qū)ο蟮乃惴?br />
這個示例的面向?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 不需要一個索引來持有當(dāng)前位置以便進行順序訪問。相反,CharBuffer 在內(nèi)部跟蹤當(dāng)前位置。Character 類提供靜態(tài)方法 codePointCount() 和 codePointAt(),它們能通過 CharSequence 接口處理 CharBuffer。CharBuffer 總是將當(dāng)前位置設(shè)置為 CharSequence 的頭。因此,當(dāng) codePointAt() 被調(diào)用時,第二個參數(shù)總是設(shè)置為 0。處理時間比 示例 1-1 長 2.15 倍。
處理時間比較
這些順序訪問示例的計時測試使用了一個包含 10,000 個代理對和 10,000 個非代理對的樣例字符串。碼位數(shù)組從這個字符串創(chuàng)建 10,000 次。測試環(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 的絕對和相對處理時間以及關(guān)聯(lián)的 API:
表 1. 順序訪問示例的處理時間和 API
示例 說明 處理時間(毫秒) 與示例 1-1 的比率 API
1-1 不支持代理對 2031 1.00
1-2 有限支持 2797 1.38 Character 類:
static boolean isHighSurrogate(char ch)
static boolean isLowSurrogate(char ch)
static boolean isSurrogatePair(char high, char low)
static int toCodePoint(char high, char low)
1-3 基本支持 5687 2.80 String 類:
int codePointAt(int index)
int codePointCount(int begin, int end)
int offsetByCodePoints(int index, int cpOffset)
1-4 使用 codePointBefore() 的基本支持 5516 2.72 String 類:
int codePointBefore(int index)
1-5 使用 charCount() 的優(yōu)化支持 3406 1.68 Character 類:
static int charCount(int cp)
1-6 使用一個 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)
隨機訪問
隨機訪問是直接訪問一個字符串中的任意位置。當(dāng)字符串被訪問時,索引值基于 16 位 char 類型的單位。但是,如果一個字符串使用 32 位碼位,那么它不能使用一個基于 32 位碼位的單位的索引訪問。必須使用 offsetByCodePoints() 來將碼位的索引轉(zhuǎn)換為 char 類型的索引。如果算法設(shè)計很糟糕,這會導(dǎo)致很差的性能,因為 offsetByCodePoints() 總是通過使用第二個參數(shù)從第一個參數(shù)計算字符串的內(nèi)部。在這個小節(jié)中,我將比較三個示例,它們通過使用一個短單位來分割一個長字符串。
示例 2-1:基準(zhǔn)測試(不支持代理對)
清單 8 展示如何使用一個寬度單位來分割一個字符串。這個基準(zhǔn)測試留作后用,不支持代理對。
清單 8. 不支持代理對
String[] sliceString(String str, int width) { // Example 2-1
// It must be that "str != null && width > 0".
List<String> slices = new ArrayList<String>();
int len = str.length(); // (1) the length of str
int sliceLimit = len - width; // (2) Do not slice beyond here.
int pos = 0; // the current position per char type
while (pos < sliceLimit) {
int begin = pos; // (3)
int end = pos + width; // (4)
slices.add(str.substring(begin, end));
pos += width; // (5)
}
slices.add(str.substring(pos)); // (6)
return slices.toArray(new String[slices.size()]); }
sliceLimit 變量對分割位置有所限制,以避免在剩余的字符串不足以分割當(dāng)前寬度單位時拋出一個 IndexOutOfBoundsException 實例。這種算法在當(dāng)前位置超出 sliceLimit 時從 while 循環(huán)中跳出后再處理后的分割。
示例 2-2:使用一個碼位索引
清單 9 展示了如何使用一個碼位索引來隨機訪問一個字符串:
清單 9. 糟糕的性能
String[] sliceString(String str, int width) { // Example 2-2
// It must be that "str != null && width > 0".
List<String> slices = new ArrayList<String>();
int len = str.codePointCount(0, str.length()); // (1) code point count [Modified]
int sliceLimit = len - width; // (2) Do not slice beyond here.
int pos = 0; // the current position per code point
while (pos < sliceLimit) {
int begin = str.offsetByCodePoints(0, pos); // (3) [Modified]
int end = str.offsetByCodePoints(0, pos + width); // (4) [Modified]
slices.add(str.substring(begin, end));
pos += width; // (5)
}
slices.add(str.substring(str.offsetByCodePoints(0, pos))); // (6) [Modified]
return slices.toArray(new String[slices.size()]); }
清單 9 修改了 清單 8 中的幾行。首先,在 Line (1) 中,length() 被 codePointCount() 替代。其次,在 Lines (3)、(4) 和 (6) 中,char 類型的索引通過 offsetByCodePoints() 用碼位索引替代。
基本的算法流與 示例 2-1 中的看起來幾乎一樣。但處理時間根據(jù)字符串長度與示例 2-1 的比率同比增加,因為 offsetByCodePoints() 總是從字符串頭到指定索引計算字符串內(nèi)部。
示例 2-3:減少的處理時間
可以使用清單 10 中展示的方法來避免 示例 2-2 的性能問題:
清單 10. 改進的性能
String[] sliceString(String str, int width) { // Example 2-3
// It must be that "str != null && width > 0".
List<String> slices = new ArrayList<String>();
int len = str.length(); // (1) the length of str
int sliceLimit // (2) Do not slice beyond here. [Modified]
= (len >= width * 2 || str.codePointCount(0, len) > width)
? str.offsetByCodePoints(len, -width) : 0;
int pos = 0; // the current position per char type
while (pos < sliceLimit) {
int begin = pos; // (3)
int end = str.offsetByCodePoints(pos, width); // (4) [Modified]
slices.add(str.substring(begin, end));
pos = end; // (5) [Modified]
}
slices.add(str.substring(pos)); // (6)
return slices.toArray(new String[slices.size()]); }
首先,在 Line (2) 中,(清單 9 中的)表達式 len-width 被 offsetByCodePoints(len,-width) 替代。但是,當(dāng) width 的值大于碼位的數(shù)量時,這會拋出一個 IndexOutOfBoundsException 實例。必須考慮邊界條件以避免異常,使用一個帶有 try/catch 異常處理程序的子句將是另一個解決方案。如果表達式 len>width*2 為 true,則可以安全地調(diào)用 offsetByCodePoints(),因為即使所有碼位都被轉(zhuǎn)換為代理對,碼位的數(shù)量仍會超過 width 的值。或者,如果 codePointCount(0,len)>width 為 true,也可以安全地調(diào)用 offsetByCodePoints()。如果是其他情況,sliceLimit 必須設(shè)置為 0。
在 Line (4) 中,清單 9 中的表達式 pos + width 必須在 while 循環(huán)中使用 offsetByCodePoints(pos,width) 替換。需要計算的量位于 width 的值中,因為第一個參數(shù)指定當(dāng) width 的值。接下來,在 Line (5) 中,表達式 pos+=width 必須使用表達式 pos=end 替換。這避免兩次調(diào)用 offsetByCodePoints() 來計算相同的索引。源代碼可以被進一步修改以小化處理時間。
處理時間比較
圖 1 和圖 2 展示了示例 2-1、2-2 和 2-3 的處理時間。樣例字符串包含相同數(shù)量的代理對和非代理對。當(dāng)字符串的長度和 width 的值被更改時,樣例字符串被切割 10,000 次。
圖 1. 一個分段的常量寬度圖 2. 分段的常量計數(shù)
示例 2-1 和 2-3 按照長度比例增加了它們的處理時間,但 示例 2-2 按照長度的平方比例增加了處理時間。當(dāng)字符串長度和 width 的值增加而分段的數(shù)量固定時,示例 2-1 擁有一個常量處理時間,而示例 2-2 和 2-3 以 width 的值為比例增加了它們的處理時間。
信息 API
大多數(shù)處理代理的信息 API 擁有兩種名稱相同的方法。一種接收 16 位 char 類型參數(shù),另一種接收 32 為碼位參數(shù)。表 2 展示了每個 API 的返回值。第三列針對 U+53F1,第 4 列針對 U+20B9F,后一列針對 U+D842(即高代理),而 U+20B9F 被轉(zhuǎn)換為 U+D842 加上 U+DF9F 的代理對。如果程序不能處理代理對,則值 U+D842 而不是 U+20B9F 將導(dǎo)致意想不到的結(jié)果(在表 2 中以粗斜體表示)。
表 2. 用于代理的信息 API
類 方法/構(gòu)造函數(shù) 針對 U+53F1 的值 針對 U+20B9F 的值 針對 U+D842 的值
Character static byte getDirectionality(int cp) 0 0 0
static int getNumericValue(int cp) -1 -1 -1
static int getType(int cp) 5 5 19
static boolean isDefined(int cp) true true true
static boolean isDigit(int cp) false false false
static boolean isISOControl(int cp) false false false
static boolean isIdentifierIgnorable(int cp) false false false
static boolean isJavaIdentifierPart(int cp) true true false
static boolean isJavaIdentifierStart(int cp) true true false
static boolean isLetter(int cp) true true false
static boolean isLetterOrDigit(int cp) true true false
static boolean isLowerCase(int cp) false false false
static boolean isMirrored(int cp) false false false
static boolean isSpaceChar(int cp) false false false
static boolean isSupplementaryCodePoint(int cp) false true false
static boolean isTitleCase(int cp) false false false
static boolean isUnicodeIdentifierPart(int cp) true true false
static boolean isUnicodeIdentifierStart(int cp) true true false
static boolean isUpperCase(int cp) false false false
static boolean isValidCodePoint(int cp) true true true
static boolean isWhitespace(int cp) false false false
static int toLowerCase(int cp) (不可更改)
static int toTitleCase(int cp) (不可更改)
static int toUpperCase(int cp) (不可更改)
Character.UnicodeBlock Character.UnicodeBlock of(int cp) CJK_UNIFIED_IDEOGRAPHS CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B HIGH_SURROGATES
Font boolean canDisplay(int cp) (取決于 Font 實例)
FontMetrics int charWidth(int cp) (取決于 FontMetrics 實例)
String int indexOf(int cp) (取決于 String 實例)
int lastIndexOf(int cp) (取決于 String 實例)
其他 API
本小節(jié)介紹前面的小節(jié)中沒有討論的代理對相關(guān) API。表 3 展示所有這些剩余的 API。所有代理對 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 展示了從一個碼位創(chuàng)建一個字符串的 5 種方法。用于測試的碼位是 U+53F1 和 U+20B9F,它們在一個字符串中重復(fù)了 100 億次。清單 11 中的注釋部分顯示了處理時間:
清單 11. 從一個碼位創(chuàng)建一個字符串的 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 的處理時間沒有明顯不同。相反,創(chuàng)建 str5 花費的時間要長得多,因為它使用 String.format(),該方法支持基于本地和格式化信息的靈活輸出。str5 方法應(yīng)該只用于程序的末尾來輸出文本。
結(jié)束語
Unicode 的每個新版本都包含了通過代理對表示的新定義的字符。東亞字符集標(biāo)準(zhǔn)并不是這樣的字符的惟一來源。例如,移動電話中還需要支持 Emoji 字符(表情圖釋),還有各種古字符需要支持。您從本文收獲的技術(shù)和性能分析將有助于您在您的 Java 應(yīng)用程序中支持所有這些字符。
關(guān)于 JAR 您不知道的 5 件事
把它放在 JAR 中
通常,在源代碼被編譯之后,您需要構(gòu)建一個 JAR 文件,使用 jar 命令行實用工具,或者,更常用的是 Ant jar 任務(wù)將 Java 代碼(已經(jīng)被包分離)收集到一個單獨的集合中,過程簡潔易懂,我不想在這做過多的說明,稍后將繼續(xù)說明如何構(gòu)建 JAR。現(xiàn)在,我只需要存檔 Hello,這是一個獨立控制臺實用工具,對于執(zhí)行打印消息到控制臺這個任務(wù)十分有用。如清單 1 所示:
清單 1. 存檔控制臺實用工具
package com.tedneward.jars;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Howdy!");
}
}
Hello 實用工具內(nèi)容并不多,但是對于研究 JAR 文件卻是一個很有用的 “腳手架”,我們先從執(zhí)行此代碼開始。
JAR 是可執(zhí)行的
.NET 和 C++ 這類語言一直是 OS 友好的,只需要在命令行(helloWorld.exe)引用其名稱,或在 GUI shell 中雙擊它的圖標(biāo)就可以啟動應(yīng)用程序。然而在 Java 編程中,啟動器程序 — java — 將 JVM 引導(dǎo)入進程中,我們需要傳遞一個命令行參數(shù)(com.tedneward.Hello)指定想要啟動的 main() 方法的類。
這些附加步驟使使用 Java 創(chuàng)建界面友好的應(yīng)用程序更加困難。不僅終端用戶需要在命令行輸入所有參數(shù)(終端用戶寧愿避開),而且極有可能使他或她操作失誤以及返回一個難以理解的錯誤。
這個解決方案使 JAR 文件 “可執(zhí)行” ,以致 Java 啟動程序在執(zhí)行 JAR 文件時,自動識別哪個類將要啟動。我們所要做的是,將一個入口引入 JAR 文件清單文件(MANIFEST.MF 在 JAR 的 META-INF 子目錄下),像這樣:
清單 2. 展示入口點!
Main-Class: com.tedneward.jars.Hello
這個清單文件只是一個名值對。因為有時候清單文件很難處理回車和空格,然而在構(gòu)建 JAR 時,使用 Ant 來生成清單文件是很容易的。在清單 3 中,使用 Ant jar 任務(wù)的 manifest 元素來指定清單文件:
清單 3. 構(gòu)建我的入口點!
<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 文件時需要做的就是通過 java -jar outapp.jar 在命令行上指定其文件名。就 GUI shell 來說,雙擊 JAR 文件即可。
JAR 可以包括依賴關(guān)系信息
似乎 Hello 實用工具已經(jīng)展開,改變實現(xiàn)的需求已經(jīng)出現(xiàn)。Spring 或 Guice 這類依賴項注入(DI)容器可以為我們處理許多細節(jié),但是仍然有點小問題:修改代碼使其含有 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 的更多信息
這個技巧將幫助您熟悉依賴項注入和 Spring 框架。如果您需要溫習(xí)其他主題,見 參考資料。
由于啟動程序的 -jar 選項將覆蓋 -classpath 命令行選項中的所有內(nèi)容,因此運行這些代碼時,Spring 必須是在 CLASSPATH 和 環(huán)境變量中。幸運的是,JAR 允許在清單文件中出現(xiàn)其他的 JAR 依賴項聲明,這使得無需聲明就可以隱式創(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 屬性包含一個與應(yīng)用程序所依賴的 JAR 文件相關(guān)的引用。您可以將它寫成一個絕對引用或者完全沒有前綴。這種情況下,我們假設(shè) JAR 文件同應(yīng)用程序 JAR 在同一個目錄下。
不幸的是,value 屬性和 Ant Class-Path 屬性必須出現(xiàn)在同一行,因為 JAR 清單文件不能處理多個 Class-Path 屬性。因此,所有這些依賴項在清單文件中必須出現(xiàn)在一行。當(dāng)然,這很難看,但為了使 java -jar outapp.jar 可用,還是值得的!
JAR 可以被隱式引用
如果有幾個不同的命令行實用工具(或其他的應(yīng)用程序)在使用 Spring 框架,可能更容易將 Spring JAR 文件放在公共位置,使所有實用工具能夠引用。這樣就避免了文件系統(tǒng)中到處都有 JAR 副本。Java 運行時 JAR 的公共位置,眾所周知是 “擴展目錄” ,默認(rèn)位于 lib/ext 子目錄,在 JRE 的安裝位置之下。
JRE 是一個可定制的位置,但是在一個給定的 Java 環(huán)境中很少定制,以至于可以完全假設(shè) lib/ext 是存儲 JAR 的一個安全地方,以及它們將隱式地用于 Java 環(huán)境的 CLASSPATH 上。
Java 6 允許類路徑通配符
為了避免龐大的 CLASSPATH 環(huán)境變量(Java 開發(fā)人員幾年前就應(yīng)該拋棄的)和/或命令行 -classpath 參數(shù),Java 6 引入了類路徑通配符 的概念。與其不得不啟動參數(shù)中明確列出的每個 JAR 文件,還不如自己指定 lib/*,讓所有 JAR 文件列在該目錄下(不遞歸),在類路徑中。
不幸的是,類路徑通配符不適用于之前提到的 Class-Path 屬性清單入口。但是這使得它更容易啟動 Java 應(yīng)用程序(包括服務(wù)器)開發(fā)人員任務(wù),例如 code-gen 工具或分析工具。
JAR 有的不只是代碼
Spring,就像許多 Java 生態(tài)系統(tǒng)一樣,依賴于一個描述構(gòu)建環(huán)境的配置文件,前面提到過,Spring 依賴于一個 app.xml 文件,此文件同 JAR 文件位于同一目錄 — 但是開發(fā)人員在復(fù)制 JAR 文件的同時忘記復(fù)制配置文件,這太常見了!
一些配置文件可用 sysadmin 進行編輯,但是其中很大一部分(例如 Hibernate 映射)都位于 sysadmin 域之外,這將導(dǎo)致部署漏洞。一個合理的解決方案是將配置文件和代碼封裝在一起 — 這是可行的,因為 JAR 從根本上來說就是一個 “喬裝的” ZIP 文件。 當(dāng)構(gòu)建一個 JAR 時,只需要在 Ant 任務(wù)或 jar 命令行包括一個配置文件即可。
JAR 也可以包含其他類型的文件,不僅僅是配置文件。例如,如果我的 SpeakEnglish 部件要訪問一個屬性文件,我可以進行如下設(shè)置,如清單 6 所示:
清單 6. 隨機響應(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 文件時至少可以少考慮一個文件。這只需要在 JAR 步驟中包含 responses.properties 文件即可。
當(dāng)您在 JAR 中存儲屬性之后,您可能想知道如何將它取回。如果所需要的數(shù)據(jù)與 JAR 文件在同一位置,正如前面的例子中提到的那樣,不需要費心找出 JAR 文件的位置,使用 JarFile 對象就可將其打開。相反,可以使用類的 ClassLoader 找到它,像在 JAR 文件中尋找 “資源” 那樣,使用 ClassLoader getResourceAsStream() 方法,如清單 7 所示:
清單 7. ClassLoader 定位資源
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
// ...
public SpeakEnglish()
{
try
{
ClassLoader myCL = SpeakEnglish.class.getClassLoader();
responses.load(
myCL.getResourceAsStream(
"com/tedneward/jars/responses.properties"));
}
catch (Exception x)
{
x.printStackTrace();
}
}
// ...
}
您可以按照以上步驟尋找任何類型的資源:配置文件、審計文件、圖形文件,等等。幾乎任何文件類型都能被捆綁進 JAR 中,作為一個 InputStream 獲?。ㄍㄟ^ ClassLoader),并通過您喜歡的方式使用。
關(guān)于 Java 性能監(jiān)控您不知道的 5 件事,第 1 部分
許多開發(fā)人員沒有意識到從 Java 5 開始 JDK 中包含了一個分析器。JConsole(或者 Java 平臺新版本,VisualVM)是一個內(nèi)置分析器,它同 Java 編譯器一樣容易啟動。如果是從命令行啟動,使 JDK 在 PATH 上,運行 jconsole 即可。如果從 GUI shell 啟動,找到 JDK 安裝路徑,打開 bin 文件夾,雙擊 jconsole。
當(dāng)分析工具彈出時(取決于正在運行的 Java 版本以及正在運行的 Java 程序數(shù)量),可能會出現(xiàn)一個對話框,要求輸入一個進程的 URL 來連接,也可能列出許多不同的本地 Java 進程(有時包含 JConsole 進程本身)來連接。
JConsole 或 VisualVM?
JConsole 從 Java 5 開始就隨著 Java 平臺版本一起發(fā)布,而 VisualVM 是在 NetBeans 基礎(chǔ)上升級的一個分析器,在 Java 6 的更新版 12 中第一次發(fā)布。多數(shù)商店還沒有更新到 Java 6 ,因此這篇文章主要介紹 JConsole 。然而,多數(shù)技巧和這兩個分析器都有關(guān)。(注意:除了包含在 Java 6 中之外,VisualVM 還有一個獨立版下載。下載 VisualVM,參見 參考資料。)
使用 JConsole 進行工作
在 Java 5 中,Java 進程并不是被設(shè)置為默認(rèn)分析的,而是通過一個命令行參數(shù) — -Dcom.sun.management.jmxremote — 在啟動時告訴 Java 5 VM 打開連接,以便分析器可以找到它們;當(dāng)進程被 JConsole 撿起時,您只能雙擊它開始分析。
分析器有自己的開銷,因此好的辦法就是花點時間來弄清是什么開銷。發(fā)現(xiàn) JConsole 開銷簡單的辦法是,首先獨自運行一個應(yīng)用程序,然后在分析器下運行,并測量差異。(應(yīng)用程序不能太大或者太??;我喜歡使用 JDK 附帶的 SwingSet2 樣本。)因此,我使用 -verbose:gc 嘗試運行 SwingSet2 來查看垃圾收集清理,然后運行同一個應(yīng)用程序并將 JConsole 分析器連接到它。當(dāng) JConsole 連接好了之后,一個穩(wěn)定的 GC 清理流出現(xiàn),否則不會出現(xiàn)。這就是分析器的性能開銷。
遠程連接進程
因為 Web 應(yīng)用程序分析工具假設(shè)通過一個套接字進行連通性分析,您只需要進行少許配置來設(shè)置 JConsole(或者是基于 JVMTI 的分析器,就這點而言),監(jiān)控/分析遠程運行的應(yīng)用程序。
如果 Tomcat 運行在一個名為 “webserve” 的機器上,且 JVM 已經(jīng)啟動了 JMX 并監(jiān)聽端口 9004,從 JConsole(或者任何 JMX 客戶端)連接它需要一個 JMX URL “service:jmx:rmi:///jndi/rmi://webserver:9004/jmxrmi”。
基本上,要分析一個運行在遠程數(shù)據(jù)中心的應(yīng)用程序服務(wù)器,您所需要的僅僅是一個 JMX URL。更多關(guān)于使用 JMX 和 JConsole 遠程監(jiān)控和管理的信息,參見 參考資料。)
跟蹤統(tǒng)計
發(fā)現(xiàn)應(yīng)用程序代碼中性能問題的常用響應(yīng)多種多樣,但也是可預(yù)測的。早期的 Java 編程人員對舊的 IDE 可能十分生氣,并開始進行代碼庫中主要部分的代碼復(fù)查,在源代碼中尋找熟悉的 “紅色標(biāo)志”,像異步塊、對象配額等等。隨著編程經(jīng)驗的增加,開發(fā)人員可能會仔細研究 JVM 支持的 -X 標(biāo)志,尋找優(yōu)化垃圾收集器的方法。當(dāng)然,對于新手,直接去 Google 查詢,希望有其他人發(fā)現(xiàn)了 JVM 的神奇的 “make it go fast” 轉(zhuǎn)換,避免重寫代碼。
從本質(zhì)上來說,這些方法沒什么錯,但都是有風(fēng)險的。對于一個性能問題有效的響應(yīng)就是使用一個分析器 — 現(xiàn)在它們內(nèi)置在 Java 平臺 ,我們確實沒有理由不這樣做!
JConsole 有許多對收集統(tǒng)計數(shù)據(jù)有用的選項卡,包括:
Memory:在 JVM 垃圾收集器中針對各個堆跟蹤活動。
Threads:在目標(biāo) JVM 中檢查當(dāng)前線程活動。
Classes:觀察 VM 已加載類的總數(shù)。
這些選項卡(和相關(guān)的圖表)都是由每個 Java 5 及更高版本 VM 在 JMX 服務(wù)器上注冊的 JMX 對象提供的,是內(nèi)置到 JVM 的。一個給定 JVM 中可用 bean 的完整清單在 MBeans 選項卡上列出,包括一些元數(shù)據(jù)和一個有限的用戶界面來查看數(shù)據(jù)或執(zhí)行操作。(然而,注冊通知是在 JConsole 用戶界面之外。)
使用統(tǒng)計數(shù)據(jù)
假設(shè)一個 Tomcat 進程死于 OutOfMemoryError。如果您想要弄清楚發(fā)生了什么,打開 JConsole,單擊 Classes 選項卡,過一段時間查看一次類計數(shù)。如果數(shù)量穩(wěn)定上升,您可以假設(shè)應(yīng)用程序服務(wù)器或者您的代碼某個地方有一個 ClassLoader 漏洞,不久之后將耗盡 PermGen 空間。如果需要更進一步的確認(rèn)問題,請看 Memory 選項卡。
為離線分析創(chuàng)建一個堆轉(zhuǎn)儲
生產(chǎn)環(huán)境中一切都在快速地進行著,您可能沒有時間花費在您的應(yīng)用程序分析器上,相反地,您可以為 Java 環(huán)境中的每個事件照一個快照保存下來過后再看。在 JConsole 中您也可以這樣做,在 VisualVM 中甚至?xí)龅酶谩?br />
先找到 MBeans 選項卡,在其中打開 com.sun.management 節(jié)點,接著是 HotSpotDiagnostic 節(jié)點?,F(xiàn)在,選擇 Operations,注意右邊面板中的 “dumpHeap” 按鈕。如果您在第一個(“字符串”)輸入框中向 dumpHeap 傳遞一個文件名來轉(zhuǎn)儲,它將為整個 JVM 堆照一個快照,并將其轉(zhuǎn)儲到那個文件。
稍后,您可以使用各種不同的商業(yè)分析器來分析文件,或者使用 VisualVM 分析快照。(記住,VisualVM 是在 Java 6 中可用的,且是單獨下載的。)
作為一個分析器實用工具,JConsole 是極好的,但是還有更好的工具。一些分析插件附帶分析器或者靈巧的用戶界面,默認(rèn)情況下比 JConsole 跟蹤更多的數(shù)據(jù)。
JConsole 真正吸引人的是整個程序是用 “普通舊式 Java ” 編寫的,這意味著任何 Java 開發(fā)人員都可以編寫這樣一個實用工具。事實上,JDK 其中甚至包括如何通過創(chuàng)建一個插件來定制 JConsole 的示例(參見 參考資料)。建立在 NetBeans 頂部的 VisualVM 進一步延伸了插件概念。
如果 JConsole(或者 VisualVM,或者其他任何工具)不符合您的需求,或者不能跟蹤您想要跟蹤的,或者不能按照您的方式跟蹤,您可以編寫屬于自己的工具。如果您覺得 Java 代碼很麻煩,Groovy 或 JRuby 或很多其他 JVM 語言都可以幫助您更快完成。
您真正需要的是一個快速而粗糙(quick-and-dirty)的由 JVM 連接的命令行工具,可以以您想要的方式確切地跟蹤您感興趣的數(shù)據(jù)。
關(guān)于 Java 性能監(jiān)控您不知道的 5 件事,第 2 部分
全功能內(nèi)置分析器,如 JConsole 和 VisualVM 的成本有時比它們的性能費用還要高 — 尤其是在生產(chǎn)軟件上運行的系統(tǒng)中。因此,在聚焦 Java 性能監(jiān)控的第 2 篇文章中,我將介紹 5 個命令行分析工具,使開發(fā)人員僅關(guān)注運行的 Java 進程的一個方面。
JDK 包括很多命令行實用程序,可以用于監(jiān)控和管理 Java 應(yīng)用程序性能。雖然大多數(shù)這類應(yīng)用程序都被標(biāo)注為 “實驗型”,在技術(shù)上不受支持,但是它們很有用。有些甚至是特定用途工具的種子材料,可以使用 JVMTI 或 JDI(參見 參考資料)建立。
jps (sun.tools.jps)
很多命令行工具都要求您識別您希望監(jiān)控的 Java 進程。這與監(jiān)控本地操作系統(tǒng)進程、同樣需要一個程序識別器的同類工具沒有太大區(qū)別。
“VMID” 識別器與本地操作系統(tǒng)進程識別器(“pid”)并不總是相同的,這就是我們需要 JDK jps 實用程序的原因。
在 Java 進程中使用 jps
與配置 JDK 的大部分工具及本文中提及的所有工具一樣,可執(zhí)行 jps 通常是一個圍繞 Java 類或執(zhí)行大多數(shù)工作的類集的一個薄包裝。在 Windows® 環(huán)境下,這些工具是 .exe 文件,使用 JNI Invocation API 直接調(diào)用上面提及的類;在 UNIX® 環(huán)境下,大多數(shù)工具是一個 shell 腳本的符號鏈接,該腳本采用指定的正確類名稱開始一個普通啟動程序。
如果您希望在 Java 進程中使用 jps(或者任何其他工具)的功能 — Ant 腳本 — 僅在每個工具的 “主” 類上調(diào)用 main() 相對容易。為了簡化引用,類名稱出現(xiàn)在每個工具名稱之后的括號內(nèi)。
jps — 名稱反映了在大多數(shù) UNIX 系統(tǒng)上發(fā)現(xiàn)的 ps 實用程序 — 告訴我們運行 Java 應(yīng)用程序的 JVMID。顧名思義,jps 返回指定機器上運行的所有已發(fā)現(xiàn)的 Java 進程的 VMID。如果 jps 沒有發(fā)現(xiàn)進程,并不意味著無法附加或研究 Java 進程,而只是意味著它并未宣傳自己的可用性。
如果發(fā)現(xiàn) Java 進程,jps 將列出啟用它的命令行。這種區(qū)分 Java 進程的方法非常重要,因為只要涉及操作系統(tǒng),所有的 Java 進程都被統(tǒng)稱為 “java”。在大多數(shù)情況下,VMID 是值得注意的重要數(shù)字。
使用分析器開始
使用分析實用程序開始的簡單方法是使用一個如在 demo/jfc/SwingSet2 中發(fā)現(xiàn)的 SwingSet2 演示一樣的演示程序。這樣就可以避免程序作為背景/監(jiān)控程序運行時出現(xiàn)掛起的可能性。當(dāng)您了解工具及其費用后,就可以在實際程序中進行試用。
加載演示應(yīng)用程序后,運行 jps 并注意返回的 vmid。為了獲得更好的效果,采用 -Dcom.sun.management.jmxremote 屬性集啟動 Java 進程。如果沒有使用該設(shè)置,部分下列工具收集的部分?jǐn)?shù)據(jù)可能不可用。
jstat (sun.tools.jstat)
jstat 實用程序可以用于收集各種各樣不同的統(tǒng)計數(shù)據(jù)。jstat 統(tǒng)計數(shù)據(jù)被分類到 “選項” 中,這些選項在命令行中被指定作為第一參數(shù)。對于 JDK 1.6 來說,您可以通過采用命令 -options 運行 jstat 查看可用的選項清單。清單 1 中顯示了部分選項:
清單 1. jstat 選項
-class
-compiler
-gc
-gccapacity
-gccause
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcpermcapacity
-gcutil
-printcompilation
實用程序的 JDK 記錄(參見 參考資料)將告訴您清單 1 中每個選項返回的內(nèi)容,但是其中大多數(shù)用于收集垃圾的收集器或者其部件的性能信息。-class 選項顯示了加載及未加載的類(使其成為檢測應(yīng)用程序服務(wù)器或代碼中 ClassLoader 泄露的重要實用程序,且 -compiler 和 -printcompilation 都顯示了有關(guān) Hotspot JIT 編譯程序的信息。
默認(rèn)情況下,jstat 在您核對信息時顯示信息。如果您希望每隔一定時間拍攝快照,請在 -options 指令后以毫秒為單位指定間隔時間。jstat 將持續(xù)顯示監(jiān)控進程信息的快照。如果您希望 jstat 在終止前進行特定數(shù)量的快照,在間隔時間/時間值后指定該數(shù)字。
如果 5756 是幾分鐘前開始的運行 SwingSet2 程序的 VMID,那么下列命令將告訴 jstat 每 250 毫秒為 10 個佚代執(zhí)行一次 gc 快照轉(zhuǎn)儲,然后停止:
jstat -gc 5756 250 10
請注意 Sun(現(xiàn)在的 Oracle)保留了在不進行任何預(yù)先通知的情況下更改各種選項的輸出甚至是選項本身的權(quán)利。這是使用不受支持實用程序的缺點。請參看 Javadocs 了解 jstat 輸出中每一列的全部細節(jié)。
jstack (sun.tools.jstack)
了解 Java 進程及其對應(yīng)的執(zhí)行線程內(nèi)部發(fā)生的情況是一種常見的診斷挑戰(zhàn)。例如,當(dāng)一個應(yīng)用程序突然停止進程時,很明顯出現(xiàn)了資源耗盡,但是僅通過查看代碼無法明確知道何處出現(xiàn)資源耗盡,且為什么會發(fā)生。
jstack 是一個可以返回在應(yīng)用程序上運行的各種各樣線程的一個完整轉(zhuǎn)儲的實用程序,您可以使用它查明問題。
采用期望進程的 VMID 運行 jstack 會產(chǎn)生一個堆轉(zhuǎn)儲。就這一點而言,jstack 與在控制臺窗口內(nèi)按 Ctrl-Break 鍵起同樣的作用,在控制臺窗口中,Java 進程正在運行或調(diào)用 VM 內(nèi)每個 Thread 對象上的 Thread.getAllStackTraces() 或 Thread.dumpStack()。jstack 調(diào)用也轉(zhuǎn)儲關(guān)于在 VM 內(nèi)運行的非 Java 線程的信息,這些線程作為 Thread 對象并不總是可用的。
jstack 的 -l 參數(shù)提供了一個較長的轉(zhuǎn)儲,包括關(guān)于每個 Java 線程持有鎖的更多詳細信息,因此發(fā)現(xiàn)(和 squash)死鎖或可伸縮性 bug 是極其重要的。
jmap (sun.tools.jmap)
有時,您正在處理的問題是一個對象泄露,如一個 ArrayList (可能持有成千上萬個對象)該釋放時沒有釋放。另一個更普遍的問題是,看似從不會壓縮的擴展堆,卻有活躍的垃圾收集。
當(dāng)您努力尋找一個對象泄露時,在指定時刻對堆及時進行拍照,然后審查其中內(nèi)容非常有用。jmap 通過對堆拍攝快照來提供該功能的第一部分。然后您可以采用下一部分中描述的 jhat 實用程序分析堆數(shù)據(jù)。
與這里描述的其他所有實用程序一樣,使用 jmap 非常簡單。將 jmap 指向您希望拍快照的 Java 進程的 VMID,然后給予它部分參數(shù),用來描述產(chǎn)生的結(jié)果文件。您要傳遞給 jmap 的選項包括轉(zhuǎn)儲文件的名稱以及是否使用一個文本文件或二進制文件。二進制文件是有用的選項,但是只有當(dāng)與某一種索引工具 結(jié)合使用時 — 通過十六進制值的文本手動操作數(shù)百兆字節(jié)不是好的方法。
隨意看一下 Java 堆的更多信息,jmap 同樣支持 -histo 選項。-histo 產(chǎn)生一個對象文本柱狀圖,現(xiàn)在在堆中大量引用,由特定類型消耗的字節(jié)總數(shù)分類。它同樣給出了特定類型的總示例數(shù)量,支持部分原始計算,并猜測每個實例的相對成本。
不幸的是,jmap 沒有像 jstat 一樣的 period-and-max-count 選項,但是將 jmap(或 jmap.main())調(diào)用放入 shell 腳本或其他類的循環(huán),周期性地拍攝快照相對簡單。(事實上,這是加入 jmap 的一個好的擴展,不管是作為 OpenJDK 本身的源補丁,還是作為其他實用程序的擴展。)
jhat (com.sun.tools.hat.Main)
將堆轉(zhuǎn)儲至一個二進制文件后,您就可以使用 jhat 分析二進制堆轉(zhuǎn)儲文件。jhat 創(chuàng)建一個 HTTP/HTML 服務(wù)器,該服務(wù)器可以在瀏覽器中被瀏覽,提供一個關(guān)于堆的 object-by-object 視圖,及時凍結(jié)。根據(jù)對象引用草率處理堆可能會非??尚?,您可以通過對總體混亂進行某種自動分析而獲得更好的服務(wù)。幸運的是,jhat 支持 OQL 語法進行這樣的分析。
例如,對所有含有超過 100 個字符的 String 運行 OQL 查詢看起來如下:
select s from java.lang.String s where s.count >= 100
結(jié)果作為對象鏈接顯示,然后展示該對象的完整內(nèi)容,字段引用作為可以解除引用的其他鏈接的其他對象。OQL 查詢同樣可以調(diào)用對象的方法,將正則表達式作為查詢的一部分,并使用內(nèi)置查詢工具。一種查詢工具,referrers() 函數(shù),顯示了引用指定類型對象的所有引用。下面是尋找所有參考 File 對象的查詢:
select referrers(f) from java.io.File f
您可以查找 OQL 的完整語法及其在 jhat 瀏覽器環(huán)境內(nèi) “OQL Help” 頁面上的特性。將 jhat 與 OQL 相結(jié)合是對行為不當(dāng)?shù)亩堰M行對象調(diào)查的有效方法。
關(guān)于 Java Scripting API 您不知道的 5 件事
現(xiàn)在,許多 Java 開發(fā)人員都喜歡在 Java 平臺中使用腳本語言,但是使用編譯到 Java 字節(jié)碼中的動態(tài)語言有時是不可行的。在某些情況中,直接編寫一個 Java 應(yīng)用程序的腳本 部分 或者在一個腳本中調(diào)用特定的 Java 對象是更快捷、更高效的方法。
這就是 javax.script 產(chǎn)生的原因了。Java Scripting API 是從 Java 6 開始引入的,它填補了便捷的小腳本語言和健壯的 Java 生態(tài)系統(tǒng)之間的鴻溝。通過使用 Java Scripting API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語言,這使您能夠在解決一些很小的問題時有更多可選擇的方法。
使用 jrunscript 執(zhí)行 JavaScript
每一個新的 Java 平臺發(fā)布都會帶來新的命令行工具集,它們位于 JDK 的 bin 目錄。Java 6 也一樣,其中 jrunscript 便是 Java 平臺工具集中的一個不小的補充。
設(shè)想一個編寫命令行腳本進行性能監(jiān)控的簡單問題。這個工具將借用 jmap(見本系列文章 前一篇文章 中的介紹),每 5 秒鐘運行一個 Java 進程,從而了解進程的運行狀況。一般情況下,我們會使用命令行 shell 腳本來完成這樣的工作,但是這里的服務(wù)器應(yīng)用程序部署在一些差別很大的平臺上,包括 Windows® 和 Linux®。系統(tǒng)管理員將會發(fā)現(xiàn)編寫能夠同時運行在兩個平臺的 shell 腳本是很痛苦的。通常的做法是編寫一個 Windows 批處理文件和一個 UNIX® shell 腳本,同時保證這兩個文件同步更新。
但是,任何閱讀過 The Pragmatic Programmer 的人都知道,這嚴(yán)重違反了 DRY (Don't Repeat Yourself) 原則,而且會產(chǎn)生許多缺陷和問題。我們真正希望的是編寫一種與操作系統(tǒng)無關(guān)的腳本,它能夠在所有的平臺上運行。
當(dāng)然,Java 語言是平臺無關(guān)的,但是這里并不是需要使用 “系統(tǒng)” 語言的情況。我們需要的是一種腳本語言 — 如,JavaScript。
清單 1 顯示的是我們所需要的簡單 shell 腳本:
清單 1. periodic.js
while (true)
{
echo("Hello, world!");
}
由于經(jīng)常與 Web 瀏覽器打交道,許多 Java 開發(fā)人員已經(jīng)知道了 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 開發(fā)的一種 ECMAScript 語言)。問題是,系統(tǒng)管理員要如何運行這個腳本?
當(dāng)然,解決方法是 JDK 所帶的 jrunscript 實用程序,如清單 2 所示:
清單 2. jrunscript
C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
...
注意,您也可以使用 for 循環(huán)按照指定的次數(shù)來循環(huán)執(zhí)行這個腳本,然后才退出?;旧?,jrunscript 能夠讓您執(zhí)行 JavaScript 的所有操作。惟一不同的是它的運行環(huán)境不是瀏覽器,所以運行中不會有 DOM。因此,頂層的函數(shù)和對象稍微有些不同。
因為 Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript 可以執(zhí)行任何傳遞給它的 ECMAScript 代碼,不管是一個文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環(huán)境。運行 jrunscript 就可以訪問 REPL shell。
從腳本訪問 Java 對象
能夠編寫 JavaScript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們在 Java 語言中使用的所有代碼 — 這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪問整個 Java 生態(tài)系統(tǒng),因為本質(zhì)上一切代碼都還是 Java 字節(jié)碼。所以,回到我們之前的問題,我們可以在 Java 平臺上使用傳統(tǒng)的 Runtime.exec() 調(diào)用來啟動進程,如清單 3 所示:
清單 3. Runtime.exec() 啟動 jmap
var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
p.waitFor()
數(shù)組 arguments 是指向傳遞到這個函數(shù)參數(shù)的 ECMAScript 標(biāo)準(zhǔn)內(nèi)置引用。在頂層的腳本環(huán)境中,則是傳遞給腳本本身的的參數(shù)數(shù)組(命令行參數(shù))。所以,在清單 3 中,這個腳本預(yù)期接收一個參數(shù),該參數(shù)包含要映射的 Java 進程的 VMID。
除此之外,我們可以利用本身為一個 Java 類的 jmap,然后直接調(diào)用它的 main() 方法,如清單 4 所示。有了這個方法,我們不需要 “傳輸” Process 對象的 in/out/err 流。
清單 4. JMap.main()
var args = [ "-histo", arguments[0] ]
Packages.sun.tools.jmap.JMap.main(args)
Packages 語法是一個 Rhino ECMAScript 標(biāo)識,它指向已經(jīng) Rhino 內(nèi)創(chuàng)建的位于核心 java.* 包之外的 Java 包。
從 Java 代碼調(diào)用腳本
從腳本調(diào)用 Java 對象僅僅完成了一半的工作:Java 腳本環(huán)境也提供了從 Java 代碼調(diào)用腳本的功能。這只需要實例化一個 ScriptEngine 對象,然后加載和評估腳本,如清單 5 所示:
清單 5. Java 平臺的腳本調(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() 方法也可以直接操作一個 String,所以這個腳本不一定必須是文件系統(tǒng)的一個文件 — 它可以來自于數(shù)據(jù)庫、用戶輸入,或者甚至可以基于環(huán)境和用戶操作在應(yīng)用程序中生成。
將 Java 對象綁定到腳本空間
僅僅調(diào)用一個腳本還不夠:腳本通常會與 Java 環(huán)境中創(chuàng)建的對象進行交互。這時,Java 主機環(huán)境必須創(chuàng)建一些對象并將它們綁定,這樣腳本就可以很容易找到和使用這些對象。這個過程是 ScriptContext 對象的任務(wù),如清單 6 所示:
清單 6. 為腳本綁定對象
import java.io.*;
import javax.script.*;
public class App
{
public static void main(String[] args)
{
try
{
ScriptEngine engine =
new ScriptEngineManager().getEngineByName("javascript");
for (String arg : args)
{
Bindings bindings = new SimpleBindings();
bindings.put("author", new Person("Ted", "Neward", 39));
bindings.put("title", "5 Things You Didn't Know");
FileReader fr = new FileReader(arg);
engine.eval(fr, bindings);
}
}
catch(IOException ioEx)
{
ioEx.printStackTrace();
}
catch(ScriptException scrEx)
{
scrEx.printStackTrace();
}
}
}
訪問所綁定的對象很簡單 — 所綁定對象的名稱是作為全局命名空間引入到腳本的,所以在 Rhino 中使用 Person 很簡單,如清單 7 所示:
清單 7. 是誰撰寫了本文?
println("Hello from inside scripting!")
println("author.firstName = " + author.firstName)
您可以看到,JavaBeans 樣式的屬性被簡化為使用名稱直接訪問,這就好像它們是字段一樣。
編譯頻繁使用的腳本
腳本語言的缺點一直存在于性能方面。其中的原因是,大多數(shù)情況下腳本語言是 “即時” 解譯的,因而它在執(zhí)行時會損失一些解析和驗證文本的時間和 CPU 周期。運行在 JVM 的許多腳本語言終會將接收的代碼轉(zhuǎn)換為 Java 字節(jié)碼,至少在腳本被第一次解析和驗證時進行轉(zhuǎn)換;在 Java 程序關(guān)閉時,這些即時編譯的代碼會消失。將頻繁使用的腳本保持為字節(jié)碼形式可以幫助提升可觀的性能。
我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實現(xiàn)了 Compilable 接口,那么這個接口所編譯的方法可用于將腳本(以一個 String 或一個 Reader 傳遞過來的)編譯為一個 CompiledScript 實例,然后它可用于在 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 實例需要存儲在一個長時間存儲中(例如,servlet-context),這樣才能避免一次次地重復(fù)編譯相同的腳本。然而,如果腳本發(fā)生變化,您就需要創(chuàng)建一個新的 CompiledScript 來反映這個變化;一旦編譯完成,CompiledScript 就不再執(zhí)行原始的腳本文件內(nèi)容。
Java 異常處理及其應(yīng)用
Java 異常處理引出
假設(shè)您要編寫一個 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 對像連接到 InputStreamReader,而 InputStreamReader 又連接到標(biāo)準(zhǔn)輸入流 System.in;第 7 行讀入一行文本;第 8 行用標(biāo)準(zhǔn)輸出流 System.out 顯示出該文本。
表面看來上面的程序沒有問題,但實際上,EchoInput 類完全可能出現(xiàn)問題。要在調(diào)用第 7 行的 readLine 方法時正確讀取輸入,這幾種假設(shè)都必須成立:假定鍵盤有效,鍵盤能與計算機正常通信;假定鍵盤數(shù)據(jù)可從操作系統(tǒng)傳輸?shù)?Java 虛擬機,又從 Java 虛擬機傳輸 inputReader。
大多數(shù)情況下上述假設(shè)都成立,但不盡然。為此,Java 采用異常方法,以應(yīng)對可能出現(xià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 方法可能出錯:若果真如此,則產(chǎn)生 IOException 來記錄故障。編譯器錯誤是在告訴您,需要更改代碼來解決這個潛在的問題。在 JDK API 文檔中,可以看到同樣的信息。我們可以看到 readLine 方法,如圖 1 所示。
圖 1. BufferedReader 類的 readLine 方法的 JDK API 文檔
由圖 1 可知,readLine 方法有時產(chǎn)生 IOException。如何處理潛在的故障?編譯器需要“捕獲”或“聲明”IOException。
“捕獲 (catch)”指當(dāng) readLine 方法產(chǎn)生錯誤時截獲該錯誤,并處理和記錄該問題。而“聲明 (declare)”指錯誤可能引發(fā) IOException,并通知調(diào)用該方法的任何代碼:可能產(chǎn)生異常。
若要捕獲異常,必須添加一個特殊的“處理代碼塊”,來接收和處理 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 行),表示要讀取輸入。若成功,則正常運行。若讀取輸入時錯誤,則捕獲問題(由 IOException 對象表示),并采取相應(yīng)措施。在本例,采用的處理方式是輸出異常。
若不準(zhǔn)備捕獲 IOException,僅聲明異常,則要特別指定 main 方法可能出錯,而且特別說明可能產(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 }
從上面的這個簡單的例子中,我們可以看出異常處理在 Java 代碼開發(fā)中不能被忽視。
Java 異常以及異常處理
可將 Java 異??醋魇且活愊?,它傳送一些系統(tǒng)問題、故障及未按規(guī)定執(zhí)行的動作的相關(guān)信息。異常包含信息,以將信息從應(yīng)用程序的一部分發(fā)送到另一部分。
編譯語言為何要處理異常?為何不在異常出現(xiàn)位置隨時處理具體故障?因為有時候我們需要在系統(tǒng)中交流錯誤消息,以便按照統(tǒng)一的方式處理問題,有時是因為有若干處理問題的可能方式,但您不知道使用哪一種,此時,可將處理異常的任務(wù)委托給調(diào)用方法的代碼。調(diào)用者通常更能了解問題來源的上下文,能更好的確定恢復(fù)方式。
圖 2 是一個通用消息架構(gòu)。
圖 2. 通用消息架構(gòu)
從上圖可以看出,必定在運行的 Java 應(yīng)用程序的一些類或?qū)ο笾挟a(chǎn)生異常。出現(xiàn)故障時,“發(fā)送者”將產(chǎn)生異常對象。異常可能代表 Java 代碼出現(xiàn)的問題,也可能是 JVM 的相應(yīng)錯誤,或基礎(chǔ)硬件或操作系統(tǒng)的錯誤。
異常本身表示消息,指發(fā)送者傳給接收者的數(shù)據(jù)“負(fù)荷”。首先,異?;陬惖念愋蛠韨鬏斢杏眯畔?。很多情況下,基于異常的類既能識別故障本因并能更正問題。其次,異常還帶有可能有用的數(shù)據(jù)(如屬性)。
在處理異常時,消息必須有接收者;否則將無法處理產(chǎn)生異常的底層問題。
在上例中,異常“產(chǎn)生者”是讀取文本行的 BufferedReader。在故障出現(xiàn)時,將在 readLine 方法中構(gòu)建 IOException 對象。異常“接收者”是代碼本身。EchoInputHandle 應(yīng)用程序的 try-catch 結(jié)構(gòu)中的 catch 塊是異常的接收者,它以字符串形式輸出異常,將問題記錄下來。
Java 異常類的層次結(jié)構(gòu)
在我們從總體上了解異常后,我們應(yīng)該了解如何在 Java 應(yīng)用程序中使用異常,即需要了解 Java 類的層次結(jié)構(gòu)。圖 3 是 Java 類的層次結(jié)構(gòu)圖。
圖 3. Java 類的層次結(jié)構(gòu)
在 Java 中,所有的異常都有一個共同的祖先 Throwable(可拋出)。Throwable 指定代碼中可用異常傳播機制通過 Java 應(yīng)用程序傳輸?shù)娜魏螁栴}的共性。
Throwable 有兩個重要的子類:Exception(異常)和 Error(錯誤),二者都是 Java 異常處理的重要子類,各自都包含大量子類。
Exception(異常)是應(yīng)用程序中可能的可預(yù)測、可恢復(fù)問題。一般大多數(shù)異常表示中度到輕度的問題。異常一般是在特定環(huán)境下產(chǎn)生的,通常出現(xiàn)在代碼的特定方法和操作中。在 EchoInput 類中,當(dāng)試圖調(diào)用 readLine 方法時,可能出現(xiàn) IOException 異常。
Error(錯誤)表示運行應(yīng)用程序中較嚴(yán)重問題。大多數(shù)錯誤與代碼編寫者執(zhí)行的操作無關(guān),而表示代碼運行時 JVM(Java 虛擬機)出現(xiàn)的問題。例如,當(dāng) JVM 不再有繼續(xù)執(zhí)行操作所需的內(nèi)存資源時,將出現(xiàn) OutOfMemoryError。
Exception 類有一個重要的子類 RuntimeException。RuntimeException 類及其子類表示“JVM 常用操作”引發(fā)的錯誤。例如,若試圖使用空值對象引用、除數(shù)為零或數(shù)組越界,則分別引發(fā)運行時異常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
Java 異常的處理
在 Java 應(yīng)用程序中,對異常的處理有兩種方式:處理異常和聲明異常。
處理異常:try、catch 和 finally
若要捕獲異常,則必須在代碼中添加異常處理器塊。這種 Java 結(jié)構(gòu)可能包含 3 個部分,
都有 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 塊:將一個或者多個語句放入 try 時,則表示這些語句可能拋出異常。編譯器知道可能要發(fā)生異常,于是用一個特殊結(jié)構(gòu)評估塊內(nèi)所有語句。
catch 塊:當(dāng)問題出現(xiàn)時,一種選擇是定義代碼塊來處理問題,catch 塊的目的便在于此。catch 塊是 try 塊所產(chǎn)生異常的接收者。基本原理是:一旦生成異常,則 try 塊的執(zhí)行中止,JVM 將查找相應(yīng)的 JVM。
finally 塊:還可以定義 finally 塊,無論運行 try 塊代碼的結(jié)果如何,該塊里面的代碼一定運行。在常見的所有環(huán)境中,finally 塊都將運行。無論 try 塊是否運行完,無論是否產(chǎn)生異常,也無論是否在 catch 塊中得到處理,finally 塊都將執(zhí)行。
try-catch-finally 規(guī)則:
必須在 try 之后添加 catch 或 finally 塊。try 塊后可同時接 catch 和 finally 塊,但至少有一個塊。
必須遵循塊順序:若代碼同時使用 catch 和 finally 塊,則必須將 catch 塊放在 try 塊之后。
catch 塊與相應(yīng)的異常類的類型相關(guān)。
一個 try 塊可能有多個 catch 塊。若如此,則執(zhí)行第一個匹配塊。
可嵌套 try-catch-finally 結(jié)構(gòu)。
在 try-catch-finally 結(jié)構(gòu)中,可重新拋出異常。
除了下列情況,總將執(zhí)行 finally 做為結(jié)束:JVM 過早終止(調(diào)用 System.exit(int));在 finally 塊中拋出一個未處理的異常;計算機斷電、失火、或遭遇病毒攻擊。
聲明異常
若要聲明異常,則必須將其添加到方法簽名塊的結(jié)束位置。下面是一個實例:
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ī)則如下:
必須聲明方法可拋出的任何可檢測異常(checked exception)。
非檢測性異常(unchecked exception)不是必須的,可聲明,也可不聲明。
調(diào)用方法必須遵循任何可檢測異常的處理和聲明規(guī)則。若覆蓋一個方法,則不能聲明與覆蓋方法不同的異常。聲明的任何異常必須是被覆蓋方法所聲明異常的同類或子類。
Java 異常處理的分類
Java 異常可分為可檢測異常,非檢測異常和自定義異常。
可檢測異常
可檢測異常經(jīng)編譯器驗證,對于聲明拋出異常的任何方法,編譯器將強制執(zhí)行處理或聲明規(guī)則,例如:sqlExecption 這個異常就是一個檢測異常。你連接 JDBC 時,不捕捉這個異常,編譯器就通不過,不允許編譯。
非檢測異常
非檢測異常不遵循處理或聲明規(guī)則。在產(chǎn)生此類異常時,不一定非要采取任何適當(dāng)操作,編譯器不會檢查是否已解決了這樣一個異常。例如:一個數(shù)組為 3 個長度,當(dāng)你使用下標(biāo)為3時,就會產(chǎn)生數(shù)組下標(biāo)越界異常。這個異常 JVM 不會進行檢測,要靠程序員來判斷。有兩個主要類定義非檢測異常:RuntimeException 和 Error。
Error 子類屬于非檢測異常,因為無法預(yù)知它們的產(chǎn)生時間。若 Java 應(yīng)用程序內(nèi)存不足,則隨時可能出現(xiàn) OutOfMemoryError;起因一般不是應(yīng)用程序的特殊調(diào)用,而是 JVM 自身的問題。另外,Error 一般表示應(yīng)用程序無法解決的嚴(yán)重問題。
RuntimeException 類也屬于非檢測異常,因為普通 JVM 操作引發(fā)的運行時異常隨時可能發(fā)生,此類異常一般是由特定操作引發(fā)。但這些操作在 Java 應(yīng)用程序中會頻繁出現(xiàn)。因此,它們不受編譯器檢查與處理或聲明規(guī)則的限制。
自定義異常
自定義異常是為了表示應(yīng)用程序的一些錯誤類型,為代碼可能發(fā)生的一個或多個問題提供新含義??梢燥@示代碼多個位置之間的錯誤的相似性,也可以區(qū)分代碼運行時可能出現(xiàn)的相似問題的一個或者多個錯誤,或給出應(yīng)用程序中一組錯誤的特定含義。例如,對隊列進行操作時,有可能出現(xiàn)兩種情況:空隊列時試圖刪除一個元素;滿隊列時試圖添加一個元素。則需要自定義兩個異常來處理這兩種情況。
Java 異常處理的原則和忌諱
Java 異常處理的原則
盡可能的處理異常
要盡可能的處理異常,如果條件確實不允許,無法在自己的代碼中完成處理,就考慮聲明異常。如果人為避免在代碼中處理異常,僅作聲明,則是一種錯誤和依賴的實踐。
具體問題具體解決
異常的部分優(yōu)點在于能為不同類型的問題提供不同的處理操作。有效異常處理的關(guān)鍵是識別特定故障場景,并開發(fā)解決此場景的特定相應(yīng)行為。為了充分利用異常處理能力,需要為特定類型的問題構(gòu)建特定的處理器塊。
記錄可能影響應(yīng)用程序運行的異常
至少要采取一些永久的方式,記錄下可能影響應(yīng)用程序操作的異常。理想情況下,當(dāng)然是在第一時間解決引發(fā)異常的基本問題。不過,無論采用哪種處理操作,一般總應(yīng)記錄下潛在的關(guān)鍵問題。別看這個操作很簡單,但它可以幫助您用很少的時間來跟蹤應(yīng)用程序中復(fù)雜問題的起因。
根據(jù)情形將異常轉(zhuǎn)化為業(yè)務(wù)上下文
若要通知一個應(yīng)用程序特有的問題,有必要將應(yīng)用程序轉(zhuǎn)換為不同形式。若用業(yè)務(wù)特定狀態(tài)表示異常,則代碼更易維護。從某種意義上講,無論何時將異常傳到不同上下文(即另一技術(shù)層),都應(yīng)將異常轉(zhuǎn)換為對新上下文有意義的形式。
Java 異常處理的忌諱
一般不要忽略異常
在異常處理塊中,一項危險的舉動是“不加通告”地處理異常。如下例所示:
1 try{
2 Class.forName("business.domain.Customer");
3 }
4 catch (ClassNotFoundException exc){}
經(jīng)常能夠在代碼塊中看到類似的代碼塊。有人總喜歡在編寫代碼時簡單快速地編寫空處理器塊,并“自我安慰地”宣稱準(zhǔn)備在“后期”添加恢復(fù)代碼,但這個“后期”變成了“無期”。
這種做法有什么壞處?如果異常對應(yīng)用程序的其他部分確實沒有任何負(fù)面影響,這未嘗不可。但事實往往并非如此,異常會擾亂應(yīng)用程序的狀態(tài)。此時,這樣的代碼無異于掩耳盜鈴。
這種做法若影響較輕,則應(yīng)用程序可能出現(xiàn)怪異行為。例如,應(yīng)用程序設(shè)置的一個值不見了, 或 GUI 失效。若問題嚴(yán)重,則應(yīng)用程序可能會出現(xiàn)重大問題,因為異常未記錄原始故障點,難以處理,如重復(fù)的 NullPointerExceptions。
如果采取措施,記錄了捕獲的異常,則不可能遇到這個問題。實際上,除非確認(rèn)異常對代碼其余部分絕無影響,至少也要作記錄。進一步講,永遠不要忽略問題;否則,風(fēng)險很大,在后期會引發(fā)難以預(yù)料的后果。
不要使用覆蓋式異常處理塊
另一個危險的處理是覆蓋式處理器(blanket handler)。該代碼的基本結(jié)構(gòu)如下:
1 try{
2 // …
3 }
4 catch(Exception e){
5 // …
6 }
使用覆蓋式異常處理塊有兩個前提之一:
代碼中只有一類問題。
這可能正確,但即便如此,也不應(yīng)使用覆蓋式異常處理,捕獲更具體的異常形式有利物弊。
單個恢復(fù)操作始終適用。
這幾乎絕對錯誤。幾乎沒有哪個方法能放之四海而皆準(zhǔn),能應(yīng)對出現(xiàn)的任何問題。
分析下這樣編寫代碼將發(fā)生的情況。只要方法不斷拋出預(yù)期的異常集,則一切正常。但是,如果拋出了未預(yù)料到的異常,則無法看到要采取的操作。當(dāng)覆蓋式處理器對新異常類執(zhí)行千篇一律的任務(wù)時,只能間接看到異常的處理結(jié)果。如果代碼沒有打印或記錄語句,則根本看不到結(jié)果。
更糟糕的是,當(dāng)代碼發(fā)生變化時,覆蓋式處理器將繼續(xù)作用于所有新異常類型,并以相同方式處理所有類型。
一般不要把特定的異常轉(zhuǎn)化為更通用的異常
將特定的異常轉(zhuǎn)換為更通用異常時一種錯誤做法。一般而言,這將取消異常起初拋出時產(chǎn)生的上下文,在將異常傳到系統(tǒng)的其他位置時,將更難處理。見下例:
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 }
因為沒有原始異常的信息,所以處理器塊無法確定問題的起因,也不知道如何更正問題。
不要處理能夠避免的異常
對于有些異常類型,實際上根本不必處理。通常運行時異常屬于此類范疇。在處理空指針或者數(shù)據(jù)索引等問題時,不必求助于異常處理。
Java 異常處理的應(yīng)用實例
在定義銀行類時,若取錢數(shù)大于余額時需要做異常處理。
定義一個異常類 insufficientFundsException。取錢(withdrawal)方法中可能產(chǎn)生異常,條件是余額小于取額。
處理異常在調(diào)用 withdrawal 的時候,因此 withdrawal 方法要聲明拋出異常,由上一級方法調(diào)用。
異常類:
class InsufficientFundsExceptionextends Exception{
private Bank excepbank; // 銀行對象
private double excepAmount; // 要取的錢
InsufficientFundsException(Bank ba, double dAmount)
{ excepbank=ba;
excepAmount=dAmount;
}
public String excepMessage(){
String str="The balance is"+excepbank.balance
+ "\n"+"The withdrawal was"+excepAmount;
return str;
}
}// 異常類
銀行類:
class Bank{
double balance;// 存款數(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)用程序性能問題提供咨詢了,其實只要跨代碼快速運行 grep,就會發(fā)現(xiàn)清單 1 所示的問題 — 原始 java 性能反模式:
清單 1. System.gc();
// We just released a bunch of objects, so tell the stupid
// garbage collector to collect them already!
System.gc();
顯式垃圾收集是一個非常糟糕的主意 — 就像將您和一個瘋狂的斗牛犬鎖在一個電話亭里。盡管調(diào)用的語法是依賴實現(xiàn)的,但如果您的 JVM 正在運行一個分代的垃圾回收器(大多數(shù)是)System.gc(); 強迫 VM 執(zhí)行一個堆的 “全部清掃”,雖然有的沒有必要。全部清掃比一個常規(guī) GC 操作要昂貴好幾個數(shù)量級,這只是個簡單數(shù)學(xué)問題。
您可以不把我的話放在心上 — Sun 的工程師為這個特殊的人工錯誤提供一個 JVM 標(biāo)志; -XX:+DisableExplicitGC 標(biāo)志自動將 System.gc() 調(diào)用轉(zhuǎn)換成一個空操作,為您提供運行代碼的機會,您自己看看 System.gc() 對于整個 JVM 執(zhí)行有害還是有利。
HeapDumpOnOutOfMemoryError
您有沒有經(jīng)歷過這樣的情況:JVM 不能使用,不斷拋出 OutOfMemoryError,而您又不能為自己創(chuàng)建調(diào)試器來捕獲它或查看出現(xiàn)了什么問題?像這類偶發(fā)和/或不確定的問題,通常使開發(fā)人員發(fā)瘋。
買者自負(fù)
并不是任何 VM 都支持所有命令行標(biāo)志,Sun/Oracle 的 VM 除外。查明一個標(biāo)志是否被支持的好方法是試用它,看它是否正常工作。倘若這些標(biāo)志在技術(shù)上是不支持的,那么,使用它們您要承擔(dān)全部責(zé)任。如果這些標(biāo)志中的任何一個使您的代碼、您的數(shù)據(jù)、您的服務(wù)器或您的一切消失得無影無蹤,我、Sun/Oracle 和 IBM® 都將不負(fù)責(zé)任。為以防萬一,建議先在虛擬(非常生產(chǎn))環(huán)境中實驗。
在這個時刻您想要的是,在 JVM 消亡之際捕獲堆的一個快照 — 正好 -XX:+HeapDumpOnOutOfMemoryError 命令可以完成這一操作。
運行該命令通知 JVM 拍攝一個 “堆轉(zhuǎn)儲快照”,并將其保存在一個文件中以便處理,通常使用 jhat 實用工具(我在 上一篇文章 中介紹過)。您可以使用相應(yīng)的 -XX:HeapDumpPath 標(biāo)志指定到保存文件的實際路徑。(不管文件保存在哪,務(wù)必確保文件系統(tǒng)和/或 Java 流程必須要有權(quán)限配置,可以在其中寫入。)
bootclasspath
定期將一個類放入類路徑是很有幫助的,這類路徑與庫存 JRE 附帶的類路徑或者以某種方式擴展的 JRE 類路徑略有不同。(新 Java Crypto API 提供商就是一個例子)。如果您想要擴展 JRE ,那么您定制的實現(xiàn)必須可以使用引導(dǎo)程序 ClassLoader,該引導(dǎo)程序可以加載 rt.jar 中的 java.lang.Object 及其所有相關(guān)文件。
盡管您可以 非法打開 rt.jar 并將您的定制實現(xiàn)或新數(shù)據(jù)包移入其中,但從技術(shù)上您就違反了您下載 JDK 時同意的協(xié)議了。
相反,使用 JVM 自己的 -Xbootclasspath 選項,以及皮膚 -Xbootclasspath/p 和 -Xbootclasspath/a。
-Xbootclasspath 使您可以設(shè)置完整的引導(dǎo)類路徑(這通常包括一個對 rt.jar 的引用),以及一些其他 JDK 附帶的(不是 rt.jar 的一部分)JAR 文件。-Xbootclasspath/p 將值前置到現(xiàn)有 bootclasspath 中,并將 -Xbootclasspath/a 附加到其中。
例如,如果您修改了庫中的 java.lang.Integer,并將修改放在一個子路徑 mods 下,那么 -Xbootclasspath/a mods 參數(shù)將新 Integer 放在默認(rèn)的參數(shù)前面。
verbose
對于虛擬的或任何類型的 Java 應(yīng)用程序,-verbose 是一個很有用的一級診斷使用程序。該標(biāo)志有三個子標(biāo)志:gc、class 和 jni。
開發(fā)人員嘗試尋找是否 JVM 垃圾收集器發(fā)生故障或者導(dǎo)致性能低下,通常首先要做的就是執(zhí)行 gc。不幸的是,解釋 gc 輸出很麻煩 — 足夠?qū)懸槐緯?。更糟糕的是,在命令行中打印的輸出在不同?Java 版本中或者不在不同的 JVM 中會發(fā)生改變,這使得正確解釋變得更難。
一般來說,如果垃圾收集器是一個分代收集器(多數(shù) “企業(yè)級” VMs 都是)。某種虛擬標(biāo)志將會出現(xiàn),來指出一個全部清掃 GC 通路;在 Sun JVM 中,標(biāo)志在 GC 輸出行的開始以 “[Full GC ...]” 形式出現(xiàn)。
想要診斷 ClassLoader 和/或不匹配的類沖突,class 可以幫上大忙。它不僅報告類何時加載,還報告類從何處加載,包括到 JAR 的路徑(如果來自 JAR)。
jni 很少使用,除了使用 JNI 或本地庫時。打開時,它將報告各種 JNI 事件,比如,本地庫何時加載,方法何時彈回;再一次強調(diào),在不同 JVM 版本中,輸出會發(fā)生變化。
Command-line -X
我列出了 JVM 中提供的我喜歡的命令行選項,但是還有一些更多的需要您自己發(fā)現(xiàn),運行命令行參數(shù) -X,列出 JVM 提供的所有非標(biāo)準(zhǔn)(但大部分都是安全的)參數(shù) — 例如:
-Xint,在解釋模式下運行 JVM(對于測試 JIT 編譯器實際上是否對您的代碼起作用或者驗證是否 JIT 編譯器中有一個 bug,這都很有用)。
-Xloggc:,和 -verbose:gc 做同樣的事,但是記錄一個文件而不輸出到命令行窗口。
JVM 命令行選項時常發(fā)生變化,因此,定期查看是一個好主意。甚至,您深夜盯著監(jiān)控器和下午 5 點回家和妻子孩子吃頓晚飯,(或者在 Mass Effect 2 中消滅您的敵人,根據(jù)您的喜好),它們都是不一樣的。
關(guān)于 java.util.concurrent 您不知道的 5 件事,第 1 部分
Concurrent Collections 是 Java™ 5 的巨大附加產(chǎn)品,但是在關(guān)于注釋和泛型的爭執(zhí)中很多 Java 開發(fā)人員忽視了它們。此外(或者更老實地說),許多開發(fā)人員避免使用這個數(shù)據(jù)包,因為他們認(rèn)為它一定很復(fù)雜,就像它所要解決的問題一樣。
事實上,java.util.concurrent 包含許多類,能夠有效解決普通的并發(fā)問題,無需復(fù)雜工序。閱讀本文,了解 java.util.concurrent 類,比如 CopyOnWriteArrayList 和 BlockingQueue 如何幫助您解決多線程編程的棘手問題。
TimeUnit
盡管本質(zhì)上 不是 Collections 類,但 java.util.concurrent.TimeUnit 枚舉讓代碼更易讀懂。使用 TimeUnit 將使用您的方法或 API 的開發(fā)人員從毫秒的 “暴政” 中解放出來。
TimeUnit 包括所有時間單位,從 MILLISECONDS 和 MICROSECONDS 到 DAYS 和 HOURS,這就意味著它能夠處理一個開發(fā)人員所需的幾乎所有的時間范圍類型。同時,因為在列舉上聲明了轉(zhuǎn)換方法,在時間加快時,將 HOURS 轉(zhuǎn)換回 MILLISECONDS 甚至變得更容易。
CopyOnWriteArrayList
創(chuàng)建數(shù)組的全新副本是過于昂貴的操作,無論是從時間上,還是從內(nèi)存開銷上,因此在通常使用中很少考慮;開發(fā)人員往往求助于使用同步的 ArrayList。然而,這也是一個成本較高的選擇,因為每當(dāng)您跨集合內(nèi)容進行迭代時,您就不得不同步所有操作,包括讀和寫,以此保證一致性。
這又讓成本結(jié)構(gòu)回到這樣一個場景:需多讀者都在讀取 ArrayList,但是幾乎沒人會去修改它。
CopyOnWriteArrayList 是個巧妙的小寶貝,能解決這一問題。它的 Javadoc 將 CopyOnWriteArrayList 定義為一個 “ArrayList 的線程安全變體,在這個變體中所有易變操作(添加,設(shè)置等)可以通過復(fù)制全新的數(shù)組來實現(xiàn)”。
集合從內(nèi)部將它的內(nèi)容復(fù)制到一個沒有修改的新數(shù)組,這樣讀者訪問數(shù)組內(nèi)容時就不會產(chǎn)生同步成本(因為他們從來不是在易變數(shù)據(jù)上操作)。
本質(zhì)上講,CopyOnWriteArrayList 很適合處理 ArrayList 經(jīng)常讓我們失敗的這種場景:讀取頻繁,但很少有寫操作的集合,例如 JavaBean 事件的 Listeners。
BlockingQueue
BlockingQueue 接口表示它是一個 Queue,意思是它的項以先入先出(FIFO)順序存儲。在特定順序插入的項以相同的順序檢索 — 但是需要附加保證,從空隊列檢索一個項的任何嘗試都會阻塞調(diào)用線程,直到這個項準(zhǔn)備好被檢索。同理,想要將一個項插入到滿隊列的嘗試也會導(dǎo)致阻塞調(diào)用線程,直到隊列的存儲空間可用。
BlockingQueue 干凈利落地解決了如何將一個線程收集的項“傳遞”給另一線程用于處理的問題,無需考慮同步問題。Java Tutorial 的 Guarded Blocks 試用版就是一個很好的例子。它構(gòu)建一個單插槽綁定的緩存,當(dāng)新的項可用,而且插槽也準(zhǔn)備好接受新的項時,使用手動同步和 wait()/notifyAll() 在線程之間發(fā)信。(詳見 Guarded Blocks 實現(xiàn)。)
盡管 Guarded Blocks 教程中的代碼有效,但是它耗時久,混亂,而且也并非完全直觀。退回到 Java 平臺較早的時候,沒錯,Java 開發(fā)人員不得不糾纏于這種代碼;但現(xiàn)在是 2010 年 — 情況難道沒有改善?
清單 1 顯示了 Guarded Blocks 代碼的重寫版,其中我使用了一個 ArrayBlockingQueue,而不是手寫的 Drop。
清單 1. BlockingQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class ABQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
ArrayBlockingQueue 還體現(xiàn)了“公平” — 意思是它為讀取器和編寫器提供線程先入先出訪問。這種替代方法是一個更有效,但又冒窮盡部分線程風(fēng)險的政策。(即,允許一些讀取器在其他讀取器鎖定時運行效率更高,但是您可能會有讀取器線程的流持續(xù)不斷的風(fēng)險,導(dǎo)致編寫器無法進行工作。)
注意 Bug!
順便說一句,如果您注意到 Guarded Blocks 包含一個重大 bug,那么您是對的 — 如果開發(fā)人員在 main() 中的 Drop 實例上同步,會出現(xiàn)什么情況呢?
BlockingQueue 還支持接收時間參數(shù)的方法,時間參數(shù)表明線程在返回信號故障以插入或者檢索有關(guān)項之前需要阻塞的時間。這么做會避免非綁定的等待,這對一個生產(chǎn)系統(tǒng)是致命的,因為一個非綁定的等待會很容易導(dǎo)致需要重啟的系統(tǒng)掛起。
ConcurrentMap
Map 有一個微妙的并發(fā) bug,這個 bug 將許多不知情的 Java 開發(fā)人員引入歧途。ConcurrentMap 是容易的解決方案。
當(dāng)一個 Map 被從多個線程訪問時,通常使用 containsKey() 或者 get() 來查看給定鍵是否在存儲鍵/值對之前出現(xiàn)。但是即使有一個同步的 Map,線程還是可以在這個過程中潛入,然后奪取對 Map 的控制權(quán)。問題是,在對 put() 的調(diào)用中,鎖在 get() 開始時獲取,然后在可以再次獲取鎖之前釋放。它的結(jié)果是個競爭條件:這是兩個線程之間的競爭,結(jié)果也會因誰先運行而不同。
如果兩個線程幾乎同時調(diào)用一個方法,兩者都會進行測試,調(diào)用 put,在處理中丟失第一線程的值。幸運的是,ConcurrentMap 接口支持許多附加方法,它們設(shè)計用于在一個鎖下進行兩個任務(wù):putIfAbsent(),例如,首先進行測試,然后僅當(dāng)鍵沒有存儲在 Map 中時進行 put。
SynchronousQueues
根據(jù) Javadoc,SynchronousQueue 是個有趣的東西:
這是一個阻塞隊列,其中,每個插入操作必須等待另一個線程的對應(yīng)移除操作,反之亦然。一個同步隊列不具有任何內(nèi)部容量,甚至不具有 1 的容量。
本質(zhì)上講,SynchronousQueue 是之前提過的 BlockingQueue 的又一實現(xiàn)。它給我們提供了在線程之間交換單一元素的極輕量級方法,使用 ArrayBlockingQueue 使用的阻塞語義。在清單 2 中,我重寫了 清單 1 的代碼,使用 SynchronousQueue 替代 ArrayBlockingQueue:
清單 2. SynchronousQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class SynQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new SynchronousQueue<String>();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
實現(xiàn)代碼看起來幾乎相同,但是應(yīng)用程序有額外獲益:SynchronousQueue 允許在隊列進行一個插入,只要有一個線程等著使用它。
在實踐中,SynchronousQueue 類似于 Ada 和 CSP 等語言中可用的 “會合通道”。這些通道有時在其他環(huán)境中也稱為 “連接”,這樣的環(huán)境包括 .NET (見 參考資料)。
關(guān)于 java.util.concurrent 您不知道的 5 件事,第 2 部分
并發(fā) Collections 提供了線程安全、經(jīng)過良好調(diào)優(yōu)的數(shù)據(jù)結(jié)構(gòu),簡化了并發(fā)編程。然而,在一些情形下,開發(fā)人員需要更進一步,思考如何調(diào)節(jié)和/或限制線程執(zhí)行。由于 java.util.concurrent 的總體目標(biāo)是簡化多線程編程,您可能希望該包包含同步實用程序,而它確實包含。
本文是 第 1 部分 的延續(xù),將介紹幾個比核心語言原語(監(jiān)視器)更高級的同步結(jié)構(gòu),但它們還未包含在 Collection 類中。一旦您了解了這些鎖和門的用途,使用它們將非常直觀。
Semaphore
在一些企業(yè)系統(tǒng)中,開發(fā)人員經(jīng)常需要限制未處理的特定資源請求(線程/操作)數(shù)量,事實上,限制有時候能夠提高系統(tǒ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 個線程都在運行(您可以對運行 SemApp 的 Java 進程執(zhí)行 jstack 來驗證),但只有 3 個線程是活躍的。在一個信號計數(shù)器釋放之前,其他 7 個線程都處于空閑狀態(tài)。(實際上,Semaphore 類支持一次獲取和釋放多個 permit,但這不適用于本場景。)
CountDownLatch
如果 Semaphore 是允許一次進入一個(這可能會勾起一些流行夜總會的保安的記憶)線程的并發(fā)性類,那么 CountDownLatch 就像是賽馬場的起跑門柵。此類持有所有空閑線程,直到滿足特定條件,這時它將會一次釋放所有這些線程。
清單 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 有兩個用途:首先,它同時釋放所有線程,模擬馬賽的起點,但隨后會設(shè)置一個門閂模擬馬賽的終點。這樣,“主” 線程就可以輸出結(jié)果。 為了讓馬賽有更多的輸出注釋,可以在賽場的 “轉(zhuǎn)彎處” 和 “半程” 點,比如賽馬跨過跑道的四分之一、二分之一和四分之三線時,添加 CountDownLatch。
Executor
清單 1 和 清單 2 中的示例都存在一個重要的缺陷,它們要求您直接創(chuàng)建 Thread 對象。這可以解決一些問題,因為在一些 JVM 中,創(chuàng)建 Thread 是一項重量型的操作,重用現(xiàn)有 Thread 比創(chuàng)建新線程要容易得多。而在另一些 JVM 中,情況正好相反:Thread 是輕量型的,可以在需要時很容易地新建一個線程。當(dāng)然,如果 Murphy 擁有自己的解決辦法(他通常都會擁有),那么您無論使用哪種方法對于您終將部署的平臺都是不對的。
JSR-166 專家組(參見 參考資料)在一定程度上預(yù)測到了這一情形。Java 開發(fā)人員無需直接創(chuàng)建 Thread,他們引入了 Executor 接口,這是對創(chuàng)建新線程的一種抽象。如清單 3 所示,Executor 使您不必親自對 Thread 對象執(zhí)行 new 就能夠創(chuàng)建新線程:
清單 3. Executor
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });
使用 Executor 的主要缺陷與我們在所有工廠中遇到的一樣:工廠必須來自某個位置。不幸的是,與 CLR 不同,JVM 沒有附帶一個標(biāo)準(zhǔn)的 VM 級線程池。
Executor 類實際上 充當(dāng)著一個提供 Executor 實現(xiàn)實例的共同位置,但它只有 new 方法(例如用于創(chuàng)建新線程池);它沒有預(yù)先創(chuàng)建實例。所以您可以自行決定是否希望在代碼中創(chuàng)建和使用 Executor 實例。(或者在某些情況下,您將能夠使用所選的容器/平臺提供的實例。)
ExecutorService 隨時可以使用
盡管不必?fù)?dān)心 Thread 來自何處,但 Executor 接口缺乏 Java 開發(fā)人員可能期望的某種功能,比如結(jié)束一個用于生成結(jié)果的線程并以非阻塞方式等待結(jié)果可用。(這是桌面應(yīng)用程序的一個常見需求,用戶將執(zhí)行需要訪問數(shù)據(jù)庫的 UI 操作,然后如果該操作花費了很長時間,可能希望在它完成之前取消它。)
對于此問題,JSR-166 專家創(chuàng)建了一個更加有用的抽象(ExecutorService 接口),它將線程啟動工廠建模為一個可集中控制的服務(wù)。例如,無需每執(zhí)行一項任務(wù)就調(diào)用一次 execute(),ExecutorService 可以接受一組任務(wù)并返回一個表示每項任務(wù)的未來結(jié)果的未來列表。
ScheduledExecutorServices
盡管 ExecutorService 接口非常有用,但某些任務(wù)仍需要以計劃方式執(zhí)行,比如以確定的時間間隔或在特定時間執(zhí)行給定的任務(wù)。這就是 ScheduledExecutorService 的應(yīng)用范圍,它擴展了 ExecutorService。
如果您的目標(biāo)是創(chuàng)建一個每隔 5 秒跳一次的 “心跳” 命令,使用 ScheduledExecutorService 可以輕松實現(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);
}
}
這項功能怎么樣?不用過于擔(dān)心線程,不用過于擔(dān)心用戶希望取消心跳時會發(fā)生什么,也不用明確地將線程標(biāo)記為前臺或后臺;只需將所有的計劃細節(jié)留給 ScheduledExecutorService。
順便說一下,如果用戶希望取消心跳,scheduleAtFixedRate 調(diào)用將返回一個 ScheduledFuture 實例,它不僅封裝了結(jié)果(如果有),還擁有一個 cancel 方法來關(guān)閉計劃的操作。
Timeout 方法
為阻塞操作設(shè)置一個具體的超時值(以避免死鎖)的能力是 java.util.concurrent 庫相比起早期并發(fā)特性的一大進步,比如監(jiān)控鎖定。
這些方法幾乎總是包含一個 int/TimeUnit 對,指示這些方法應(yīng)該等待多長時間才釋放控制權(quán)并將其返回給程序。它需要開發(fā)人員執(zhí)行更多工作 — 如果沒有獲取鎖,您將如何重新獲?。?— 但結(jié)果幾乎總是正確的:更少的死鎖和更加適合生產(chǎn)的代碼。(關(guān)于編寫生產(chǎn)就緒代碼的更多信息,請參見 參考資料 中 Michael Nygard 編寫的 Release It!。)
結(jié)束語
java.util.concurrent 包還包含了其他許多好用的實用程序,它們很好地擴展到了 Collections 之外,尤其是在 .locks 和 .atomic 包中。深入研究,您還將發(fā)現(xiàn)一些有用的控制結(jié)構(gòu),比如 CyclicBarrier 等。
與 Java 平臺的許多其他方面一樣,您無需費勁地查找可能非常有用的基礎(chǔ)架構(gòu)代碼。在編寫多線程代碼時,請記住本文討論的實用程序和 上一篇文章 中討論的實用程序。
關(guān)于 Java Collections API 您不知道的 5 件事,第 1 部分
對于很多 Java 開發(fā)人員來說,Java Collections API 是標(biāo)準(zhǔn) Java 數(shù)組及其所有缺點的一個非常需要的替代品。將 Collections 主要與 ArrayList 聯(lián)系到一起本身沒有錯,但是對于那些有探索精神的人來說,這只是 Collections 的冰山一角。
Collections 比數(shù)組好
剛接觸 Java 技術(shù)的開發(fā)人員可能不知道,Java 語言初包括數(shù)組,是為了應(yīng)對上世紀(jì) 90 年代初期 C++ 開發(fā)人員對于性能方面的批評。從那時到現(xiàn)在,我們已經(jīng)走過一段很長的路,如今,與 Java Collections 庫相比,數(shù)組不再有性能優(yōu)勢。
例如,若要將數(shù)組的內(nèi)容轉(zhuǎn)儲到一個字符串,需要迭代整個數(shù)組,然后將內(nèi)容連接成一個 String;而 Collections 的實現(xiàn)都有一個可用的 toString() 實現(xiàn)。
除少數(shù)情況外,好的做法是盡快將遇到的任何數(shù)組轉(zhuǎn)換成集合。于是問題來了,完成這種轉(zhuǎn)換的容易的方式是什么?事實證明,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 是不可修改的,所以如果嘗試向其中添加新元素將拋出一個 UnsupportedOperationException。
而且,由于 Arrays.asList() 使用 varargs 參數(shù)表示添加到 List 的元素,所以還可以使用它輕松地用以 new 新建的對象創(chuàng)建 List。
迭代的效率較低
將一個集合(特別是由數(shù)組轉(zhuǎn)化而成的集合)的內(nèi)容轉(zhuǎn)移到另一個集合,或者從一個較大對象集合中移除一個較小對象集合,這些事情并不鮮見。
您也許很想對集合進行迭代,然后添加元素或移除找到的元素,但是不要這樣做。
在此情況下,迭代有很大的缺點:
每次添加或移除元素后重新調(diào)整集合將非常低效。
每次在獲取鎖、執(zhí)行操作和釋放鎖的過程中,都存在潛在的并發(fā)困境。
當(dāng)添加或移除元素時,存取集合的其他線程會引起競爭條件。
可以通過使用 addAll 或 removeAll,傳入包含要對其添加或移除元素的集合作為參數(shù),來避免所有這些問題。
用 for 循環(huán)遍歷任何 Iterable
Java 5 中加入 Java 語言的大的便利功能之一,增強的 for 循環(huán),消除了使用 Java 集合的后一道障礙。
以前,開發(fā)人員必須手動獲得一個 Iterator,使用 next() 獲得 Iterator 指向的對象,并通過 hasNext() 檢查是否還有更多可用對象。從 Java 5 開始,我們可以隨意使用 for 循環(huán)的變種,它可以在幕后處理上述所有工作。
實際上,這個增強適用于實現(xiàn) Iterable 接口的任何對象,而不僅僅是 Collections。
清單 2 顯示通過 Iterator 提供 Person 對象的孩子列表的一種方法。 這里不是提供內(nèi)部 List 的一個引用 (這使 Person 外的調(diào)用者可以為家庭增加孩子 — 而大多數(shù)父母并不希望如此),Person 類型實現(xiàn) Iterable。這種方法還使得 for 循環(huán)可以遍歷所有孩子。
清單 2. 增強的 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());
}
}
}
在域建模的時候,使用 Iterable 有一些明顯的缺陷,因為通過 iterator() 方法只能那么 “隱晦” 地支持一個那樣的對象集合。但是,如果孩子集合比較明顯,Iterable 可以使針對域類型的編程更容易,更直觀。
經(jīng)典算法和定制算法
您是否曾想過以倒序遍歷一個 Collection?對于這種情況,使用經(jīng)典的 Java Collections 算法非常方便。
在上面的 清單 2 中,Person 的孩子是按照傳入的順序排列的;但是,現(xiàn)在要以相反的順序列出他們。雖然可以編寫另一個 for 循環(huán),按相反順序?qū)⒚總€對象插入到一個新的 ArrayList 中,但是 3、4 次重復(fù)這樣做之后,就會覺得很麻煩。
在此情況下,清單 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 類有很多這樣的 “算法”,它們被實現(xiàn)為靜態(tài)方法,以 Collections 作為參數(shù),提供獨立于實現(xiàn)的針對整個集合的行為。
而且,由于很棒的 API 設(shè)計,我們不必完全受限于 Collections 類中提供的算法 — 例如,我喜歡不直接修改(傳入的 Collection 的)內(nèi)容的方法。所以,可以編寫定制算法是一件很棒的事情,例如清單 4 就是一個這樣的例子:
清單 4. ReverseIterator 使事情更簡單
class MyCollections
{
public static <T> List<T> reverse(List<T> src)
{
List<T> results = new ArrayList<T>(src);
Collections.reverse(results);
return results;
}
}
擴展 Collections API
以上定制算法闡釋了關(guān)于 Java Collections API 的一個終觀點:它總是適合加以擴展和修改,以滿足開發(fā)人員的特定目的。
例如,假設(shè)您需要 Person 類中的孩子總是按年齡排序。雖然可以編寫代碼一遍又一遍地對孩子排序(也許是使用 Collections.sort 方法),但是通過一個 Collection 類來自動排序要好得多。
實際上,您甚至可能不關(guān)心是否每次按固定的順序?qū)ο蟛迦氲?Collection 中(這正是 List 的基本原理)。您可能只是想讓它們按一定的順序排列。
java.util 中沒有 Collection 類能滿足這些需求,但是編寫一個這樣的類很簡單。只需創(chuàng)建一個接口,用它描述 Collection 應(yīng)該提供的抽象行為。對于 SortedCollection,它的作用完全是行為方面的。
清單 5. SortedCollection
public interface SortedCollection<E> extends Collection<E>
{
public Comparator<E> getComparator();
public void setComparator(Comparator<E> comp);
}
編寫這個新接口的實現(xià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);
}
}
這個實現(xiàn)非常簡陋,編寫時并沒有考慮優(yōu)化,顯然還需要進行重構(gòu)。但關(guān)鍵是 Java Collections API 從來無意將與集合相關(guān)的任何東西定死。它總是需要擴展,同時也鼓勵擴展。
當(dāng)然,有些擴展比較復(fù)雜,例如 java.util.concurrent 中引入的擴展。但是另一些則非常簡單,只需編寫一個定制算法,或者已有 Collection 類的簡單的擴展。
擴展 Java Collections API 看上去很難,但是一旦開始著手,您會發(fā)現(xiàn)遠不如想象的那樣難。
關(guān)于 Java Collections API 您不知道的 5 件事,第 2 部分
java.util 中的 Collections 類旨在通過取代數(shù)組提高 Java 性能。如您在 第 1 部分 中了解到的,它們也是多變的,能夠以各種方式定制和擴展,幫助實現(xiàn)優(yōu)質(zhì)、簡潔的代碼。
Collections 非常強大,但是很多變:使用它們要小心,濫用它們會帶來風(fēng)險。
List 不同于數(shù)組
Java 開發(fā)人員常常錯誤地認(rèn)為 ArrayList 就是 Java 數(shù)組的替代品。Collections 由數(shù)組支持,在集合內(nèi)隨機查找內(nèi)容時性能較好。與數(shù)組一樣,集合使用整序數(shù)獲取特定項。但集合不是數(shù)組的簡單替代。
要明白數(shù)組與集合的區(qū)別需要弄清楚順序 和位置 的不同。例如,List 是一個接口,它保存各個項被放入集合中的順序,如清單 1 所示:
清單 1. 可變鍵值
import java.util.*;
public class OrderAndPosition
{
public static <T> void dumpArray(T[] array)
{
System.out.println("=============");
for (int i=0; i<array.length; i++)
System.out.println("Position " + i + ": " + array[i]);
}
public static <T> void dumpList(List<T> list)
{
System.out.println("=============");
for (int i=0; i<list.size(); i++)
System.out.println("Ordinal " + i + ": " + list.get(i));
}
public static void main(String[] args)
{
List<String> argList = new ArrayList<String>(Arrays.asList(args));
dumpArray(args);
args[1] = null;
dumpArray(args);
dumpList(argList);
argList.remove(1);
dumpList(argList);
}
}
當(dāng)?shù)谌齻€元素從上面的 List 中被移除時,其 “后面” 的各項會上升填補空位。很顯然,此集合行為與數(shù)組的行為不同(事實上,從數(shù)組中移除項與從 List 中移除它也不完全是一回事兒 — 從數(shù)組中 “移除” 項意味著要用新引用或 null 覆蓋其索引槽)。
令人驚訝的 Iterator!
無疑 Java 開發(fā)人員很喜愛 Java 集合 Iterator,但是您后一次使用 Iterator 接口是什么時候的事情了?可以這么說,大部分時間我們只是將 Iterator 隨意放到 for() 循環(huán)或加強 for() 循環(huán)中,然后就繼續(xù)其他操作了。
但是進行深入研究后,您會發(fā)現(xiàn) Iterator 實際上有兩個十分有用的功能。
第一,Iterator 支持從源集合中安全地刪除對象,只需在 Iterator 上調(diào)用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException,這個異常顧名思意:當(dāng)打開 Iterator 迭代集合時,同時又在對集合進行修改。有些集合不允許在迭代時刪除或添加元素,但是調(diào)用 Iterator 的 remove() 方法是個安全的做法。
第二,Iterator 支持派生的(并且可能是更強大的)兄弟成員。ListIterator,只存在于 List 中,支持在迭代期間向 List 中添加或刪除元素,并且可以在 List 中雙向滾動。
雙向滾動特別有用,尤其是在無處不在的 “滑動結(jié)果集” 操作中,因為結(jié)果集中只能顯示從數(shù)據(jù)庫或其他集合中獲取的眾多結(jié)果中的 10 個。它還可以用于 “反向遍歷” 集合或列表,而無需每次都從前向后遍歷。插入 ListIterator 比使用向下計數(shù)整數(shù)參數(shù) List.get() “反向” 遍歷 List 容易得多。
并非所有 Iterable 都來自集合
Ruby 和 Groovy 開發(fā)人員喜歡炫耀他們?nèi)绾文艿麄€文本文件并通過一行代碼將其內(nèi)容輸出到控制臺。通常,他們會說在 Java 編程中完成同樣的操作需要很多行代碼:打開 FileReader,然后打開 BufferedReader,接著創(chuàng)建 while() 循環(huán)來調(diào)用 getLine(),直到它返回 null。當(dāng)然,在 try/catch/finally 塊中必須要完成這些操作,它要處理異常并在結(jié)束時關(guān)閉文件句柄。
這看起來像是一個沒有意義的學(xué)術(shù)上的爭論,但是它也有其自身的價值。
他們(包括相當(dāng)一部分 Java 開發(fā)人員)不知道并不是所有 Iterable 都來自集合。Iterable 可以創(chuàng)建 Iterator,該迭代器知道如何憑空制造下一個元素,而不是從預(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)勢是不會在內(nèi)存中保留整個內(nèi)容,但是有一個警告就是,它不能 close() 底層文件句柄(每當(dāng) readLine() 返回 null 時就關(guān)閉文件句柄,可以修正這一問題,但是在 Iterator 沒有結(jié)束時不能解決這個問題)。
注意可變的 hashCode()
Map 是很好的集合,為我們帶來了在其他語言(比如 Perl)中經(jīng)??梢姷暮糜玫逆I/值對集合。JDK 以 HashMap 的形式為我們提供了方便的 Map 實現(xiàn),它在內(nèi)部使用哈希表實現(xiàn)了對鍵的對應(yīng)值的快速查找。但是這里也有一個小問題:支持哈希碼的鍵依賴于可變字段的內(nèi)容,這樣容易產(chǎn)生 bug,即使耐心的 Java 開發(fā)人員也會被這些 bug 逼瘋。
假設(shè)清單 3 中的 Person 對象有一個常見的 hashCode() (它使用 firstName、lastName 和 age 字段 — 所有字段都不是 final 字段 — 計算 hashCode()),對 Map 的 get() 調(diào)用會失敗并返回 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));
}
}
很顯然,這種方法很糟糕,但是解決方法也很簡單:永遠不要將可變對象類型用作 HashMap 中的鍵。
equals() 與 Comparable
在瀏覽 Javadoc 時,Java 開發(fā)人員常常會遇到 SortedSet 類型(它在 JDK 中唯一的實現(xiàn)是 TreeSet)。因為 SortedSet 是 java.util 包中唯一提供某種排序行為的 Collection,所以開發(fā)人員通常直接使用它而不會仔細地研究它。清單 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);
}
}
使用上述代碼一段時間后,可能會發(fā)現(xiàn)這個 Set 的核心特性之一:它不允許重復(fù)。該特性在 Set Javadoc 中進行了介紹。Set 是不包含重復(fù)元素的集合。更準(zhǔn)確地說,set 不包含成對的 e1 和 e2 元素,因此如果 e1.equals(e2),那么多包含一個 null 元素。
但實際上似乎并非如此 — 盡管 清單 4 中沒有相等的 Person 對象(根據(jù) Person 的 equals() 實現(xiàn)),但在輸出時只有三個對象出現(xiàn)在 TreeSet 中。
與 set 的有狀態(tài)本質(zhì)相反,TreeSet 要求對象直接實現(xiàn) Comparable 或者在構(gòu)造時傳入 Comparator,它不使用 equals() 比較對象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。
因此存儲在 Set 中的對象有兩種方式確定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪種方法取決于上下文。
更糟的是,簡單的聲明兩者相等還不夠,因為以排序為目的的比較不同于以相等性為目的的比較:可以想象一下按姓排序時兩個 Person 相等,但是其內(nèi)容卻并不相同。
一定要明白 equals() 和 Comparable.compareTo() 兩者之間的不同 — 實現(xiàn) Set 時會返回 0。甚至在文檔中也要明確兩者的區(qū)別。
關(guān)于 Java 對象序列化您不知道的 5 件事
關(guān)于本系列
您覺得自己懂 Java 編程?事實上,大多數(shù)程序員對于 Java 平臺都是淺嘗則止,只學(xué)習(xí)了足以完成手頭上任務(wù)的知識而已。在本 系列 中,Ted Neward 深入挖掘 Java 平臺的核心功能,揭示一些鮮為人知的事實,幫助您解決棘手的編程挑戰(zhàn)。
大約一年前,一個負(fù)責(zé)管理應(yīng)用程序所有用戶設(shè)置的開發(fā)人員,決定將用戶設(shè)置存儲在一個 Hashtable 中,然后將這個 Hashtable 序列化到磁盤,以便持久化。當(dāng)用戶更改設(shè)置時,便重新將 Hashtable 寫到磁盤。
這是一個優(yōu)雅的、開放式的設(shè)置系統(tǒng),但是,當(dāng)團隊決定從 Hashtable 遷移到 Java Collections 庫中的 HashMap 時,這個系統(tǒng)便面臨崩潰。
Hashtable 和 HashMap 在磁盤上的格式是不相同、不兼容的。除非對每個持久化的用戶設(shè)置運行某種類型的數(shù)據(jù)轉(zhuǎn)換實用程序(極其龐大的任務(wù)),否則以后似乎只能一直用 Hashtable 作為應(yīng)用程序的存儲格式。
團隊感到陷入僵局,但這只是因為他們不知道關(guān)于 Java 序列化的一個重要事實:Java 序列化允許隨著時間的推移而改變類型。當(dāng)我向他們展示如何自動進行序列化替換后,他們終于按計劃完成了向 HashMap 的轉(zhuǎn)變。
本文是本系列的第一篇文章,這個系列專門揭示關(guān)于 Java 平臺的一些有用的小知識 — 這些小知識不易理解,但對于解決 Java 編程挑戰(zhàn)遲早有用。
將 Java 對象序列化 API 作為開端是一個不錯的選擇,因為它從一開始就存在于 JDK 1.1 中。本文介紹的關(guān)于序列化的 5 件事情將說服您重新審視那些標(biāo)準(zhǔn) Java API。
Java 序列化簡介
Java 對象序列化是 JDK 1.1 中引入的一組開創(chuàng)性特性之一,用于作為一種將 Java 對象的狀態(tài)轉(zhuǎn)換為字節(jié)數(shù)組,以便存儲或傳輸?shù)臋C制,以后,仍可以將字節(jié)數(shù)組轉(zhuǎn)換回 Java 對象原有的狀態(tài)。
實際上,序列化的思想是 “凍結(jié)” 對象狀態(tài),傳輸對象狀態(tài)(寫到磁盤、通過網(wǎng)絡(luò)傳輸?shù)鹊龋?,然?“解凍” 狀態(tài),重新獲得可用的 Java 對象。所有這些事情的發(fā)生有點像是魔術(shù),這要歸功于 ObjectInputStream/ObjectOutputStream 類、完全保真的元數(shù)據(jù)以及程序員愿意用 Serializable 標(biāo)識接口標(biāo)記他們的類,從而 “參與” 這個過程。
清單 1 顯示一個實現(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 序列化后,很容易將對象狀態(tài)寫到磁盤,然后重新讀出它,下面的 JUnit 4 單元測試對此做了演示。
清單 2. 對 Person 進行反序列化
public class SerTest
{
@Test public void serializeToDisk()
{
try
{
com.tedneward.Person ted = new com.tedneward.Person("Ted", "Neward", 39);
com.tedneward.Person charl = new com.tedneward.Person("Charlotte",
"Neward", 38);
ted.setSpouse(charl); charl.setSpouse(ted);
FileOutputStream fos = new FileOutputStream("tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(ted);
oos.close();
}
catch (Exception ex)
{
fail("Exception thrown during test: " + ex.toString());
}
try
{
FileInputStream fis = new FileInputStream("tempdata.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
com.tedneward.Person ted = (com.tedneward.Person) ois.readObject();
ois.close();
assertEquals(ted.getFirstName(), "Ted");
assertEquals(ted.getSpouse().getFirstName(), "Charlotte");
// Clean up the file
new File("tempdata.ser").delete();
}
catch (Exception ex)
{
fail("Exception thrown during test: " + ex.toString());
}
}
}
到現(xiàn)在為止,還沒有看到什么新鮮的或令人興奮的事情,但是這是一個很好的出發(fā)點。我們將使用 Person 來發(fā)現(xiàn)您可能不知道的關(guān)于 Java 對象序列化 的 5 件事。
序列化允許重構(gòu)
序列化允許一定數(shù)量的類變種,甚至重構(gòu)之后也是如此,ObjectInputStream 仍可以很好地將其讀出來。
Java Object Serialization 規(guī)范可以自動管理的關(guān)鍵任務(wù)是:
將新字段添加到類中
將字段從 static 改為非 static
將字段從 transient 改為非 transient
取決于所需的向后兼容程度,轉(zhuǎn)換字段形式(從非 static 轉(zhuǎn)換為 static 或從非 transient 轉(zhuǎn)換為 transient)或者刪除字段需要額外的消息傳遞。
重構(gòu)序列化類
既然已經(jīng)知道序列化允許重構(gòu),我們來看看當(dāng)把新字段添加到 Person 類中時,會發(fā)生什么事情。
如清單 3 所示,PersonV2 在原先 Person 類的基礎(chǔ)上引入一個表示性別的新字段。
清單 3. 將新字段添加到序列化的 Person 中
enum Gender
{
MALE, FEMALE
}
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a, Gender g)
{
this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public Gender getGender() { return gender; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setGender(Gender value) { gender = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" gender=" + gender +
" age=" + age +
" spouse=" + spouse.getFirstName() +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
private Gender gender;
}
序列化使用一個 hash,該 hash 是根據(jù)給定源文件中幾乎所有東西 — 方法名稱、字段名稱、字段類型、訪問修改方法等 — 計算出來的,序列化將該 hash 值與序列化流中的 hash 值相比較。
為了使 Java 運行時相信兩種類型實際上是一樣的,第二版和隨后版本的 Person 必須與第一版有相同的序列化版本 hash(存儲為 private static final serialVersionUID 字段)。因此,我們需要 serialVersionUID 字段,它是通過對原始(或 V1)版本的 Person 類運行 JDK serialver 命令計算出的。
一旦有了 Person 的 serialVersionUID,不僅可以從原始對象 Person 的序列化數(shù)據(jù)創(chuàng)建 PersonV2 對象(當(dāng)出現(xiàn)新字段時,新字段被設(shè)為缺省值,常見的是“null”),還可以反過來做:即從 PersonV2 的數(shù)據(jù)通過反序列化得到 Person,這毫不奇怪。
序列化并不安全
讓 Java 開發(fā)人員詫異并感到不快的是,序列化二進制格式完全編寫在文檔中,并且完全可逆。實際上,只需將二進制序列化流的內(nèi)容轉(zhuǎn)儲到控制臺,就足以看清類是什么樣子,以及它包含什么內(nèi)容。
這對于安全性有著不良影響。例如,當(dāng)通過 RMI 進行遠程方法調(diào)用時,通過連接發(fā)送的對象中的任何 private 字段幾乎都是以明文的方式出現(xiàn)在套接字流中,這顯然容易招致哪怕簡單的安全問題。
幸運的是,序列化允許 “hook” 序列化過程,并在序列化之前和反序列化之后保護(或模糊化)字段數(shù)據(jù)??梢酝ㄟ^在 Serializable 對象上提供一個 writeObject 方法來做到這一點。
模糊化序列化數(shù)據(jù)
假設(shè) Person 類中的敏感數(shù)據(jù)是 age 字段。畢竟,女士忌談年齡。我們可以在序列化之前模糊化該數(shù)據(jù),將數(shù)位循環(huán)左移一位,然后在反序列化之后復(fù)位。(您可以開發(fā)更安全的算法,當(dāng)前這個算法只是作為一個例子。)
為了 “hook” 序列化過程,我們將在 Person 上實現(xiàn)一個 writeObject 方法;為了 “hook” 反序列化過程,我們將在同一個類上實現(xiàn)一個 readObject 方法。重要的是這兩個方法的細節(jié)要正確 — 如果訪問修改方法、參數(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ù)流/文件。而且,由于該格式被完全文檔化,即使不能訪問類本身,也仍可以讀取序列化流中的內(nèi)容。
序列化的數(shù)據(jù)可以被簽名和密封
上一個技巧假設(shè)您想模糊化序列化數(shù)據(jù),而不是對其加密或者確保它不被修改。當(dāng)然,通過使用 writeObject 和 readObject 可以實現(xiàn)密碼加密和簽名管理,但其實還有更好的方式。
如果需要對整個對象進行加密和簽名,簡單的是將它放在一個 javax.crypto.SealedObject 和/或 java.security.SignedObject 包裝器中。兩者都是可序列化的,所以將對象包裝在 SealedObject 中可以圍繞原對象創(chuàng)建一種 “包裝盒”。必須有對稱密鑰才能解密,而且密鑰必須單獨管理。同樣,也可以將 SignedObject 用于數(shù)據(jù)驗證,并且對稱密鑰也必須單獨管理。
結(jié)合使用這兩種對象,便可以輕松地對序列化數(shù)據(jù)進行密封和簽名,而不必強調(diào)關(guān)于數(shù)字簽名驗證或加密的細節(jié)。很簡潔,是吧?
序列化允許將代理放在流中
很多情況下,類中包含一個核心數(shù)據(jù)元素,通過它可以派生或找到類中的其他字段。在此情況下,沒有必要序列化整個對象。可以將字段標(biāo)記為 transient,但是每當(dāng)有方法訪問一個字段時,類仍然必須顯式地產(chǎn)生代碼來檢查它是否被初始化。
如果首要問題是序列化,那么好指定一個 flyweight 或代理放在流中。為原始 Person 提供一個 writeReplace 方法,可以序列化不同類型的對象來代替它。類似地,如果反序列化期間發(fā)現(xiàn)一個 readResolve 方法,那么將調(diào)用該方法,將替代對象提供給調(diào)用者。
打包和解包代理
writeReplace 和 readResolve 方法使 Person 類可以將它的所有數(shù)據(jù)(或其中的核心數(shù)據(jù))打包到一個 PersonProxy 中,將它放入到一個流中,然后在反序列化時再進行解包。
清單 5. 你完整了我,我代替了你
class PersonProxy
implements java.io.Serializable
{
public PersonProxy(Person orig)
{
data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge();
if (orig.getSpouse() != null)
{
Person spouse = orig.getSpouse();
data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + ","
+ spouse.getAge();
}
}
public String data;
private Object readResolve()
throws java.io.ObjectStreamException
{
String[] pieces = data.split(",");
Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));
if (pieces.length > 3)
{
result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt
(pieces[5])));
result.getSpouse().setSpouse(result);
}
return result;
}
}
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
this.firstName = fn; this.lastName = ln; this.age = a;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
private Object writeReplace()
throws java.io.ObjectStreamException
{
return new PersonProxy(this);
}
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" age=" + age +
" spouse=" + spouse.getFirstName() +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
}
注意,PersonProxy 必須跟蹤 Person 的所有數(shù)據(jù)。這通常意味著代理需要是 Person 的一個內(nèi)部類,以便能訪問 private 字段。有時候,代理還需要追蹤其他對象引用并手動序列化它們,例如 Person 的 spouse。
這種技巧是少數(shù)幾種不需要讀/寫平衡的技巧之一。例如,一個類被重構(gòu)成另一種類型后的版本可以提供一個 readResolve 方法,以便靜默地將被序列化的對象轉(zhuǎn)換成新類型。類似地,它可以采用 writeReplace 方法將舊類序列化成新版本。
信任,但要驗證
認(rèn)為序列化流中的數(shù)據(jù)總是與初寫到流中的數(shù)據(jù)一致,這沒有問題。但是,正如一位美國前總統(tǒng)所說的,“信任,但要驗證”。
對于序列化的對象,這意味著驗證字段,以確保在反序列化之后它們?nèi)跃哂姓_的值,“以防萬一”。為此,可以實現(xiàn) ObjectInputValidation 接口,并覆蓋 validateObject() 方法。如果調(diào)用該方法時發(fā)現(xiàn)某處有錯誤,則拋出一個 InvalidObjectException。