JAVA 技术文章收集

更新时间:2024-01-28 07:55:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

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

1、 StAX – 解析XML文件

在千禧年左右,当 XML 第一次出现在很多 Java 开发人员面前时,有两种基本的解析 XML 文件的方法。SAX 解析器实际是由程序员对事件调用一系列回调方法的大型状态机。DOM 解析器将整个 XML 文档加入内存,并切割成离散的对象,它们连接在一起形成一个树。该树描述了文档的整个 XML Infoset 表示法。这两个解析器都有缺点:SAX 太低级,无法使用,DOM 代价太大,尤其对于大的 XML 文件 — 整个树成了一个庞然大物。

幸运的是,Java 开发人员找到第三种方法来解析 XML 文件,通过对文档建模成 “节点”,它们可以从文档流中一次取出一个,检查,然后处理或丢弃。这些 “节点” 的 “流” 提供了 SAX 和 DOM 的中间地带,名为 “Streaming API for XML”,或者叫做StAX。(此缩写用于区分新的 API 与原来的 SAX 解析器,它与此同名。)StAX 解析器后来包装到了 JDK 中,在 javax.xml.stream 包。

使用 StAX 相当简单:实例化 XMLEventReader,将它指向一个格式良好的 XML 文件,然后一次 “拉出” 一个节点(通常用 while 循环),查看。例如,在清单 1 中,列举出了 Ant 构造脚本中的所有目标: 清单 1. 只是让 StAX 指向目标 import java.io.*; import javax.xml.namespace.QName; import javax.xml.stream.*; import javax.xml.stream.events.*; import javax.xml.stream.util.*; public class Targets { public static void main(String[] args) throws Exception { for (String arg : args) { XMLEventReader xsr = XMLInputFactory.newInstance() .createXMLEventReader(new FileReader(arg)); while (xsr.hasNext()) { XMLEvent evt = xsr.nextEvent(); switch (evt.getEventType()) { case XMLEvent.START_ELEMENT: { StartElement se = evt.asStartElement(); if (se.getName().getLocalPart().equals(\ { Attribute targetName = se.getAttributeByName(new QName(\ // Found a target! System.out.println(targetName.getValue()); } break; } // Ignore everything else } } } } }

StAX 解析器不会替换所有的 SAX 和 DOM 代码。但肯定会让某些任务容易些。尤其对完成不需要知道 XML 文档整个树结构的任务相当方便。

请注意,如果事件对象级别太高,无法使用,StAX 也有一个低级 API 在 XMLStreamReader 中。尽管也许没有阅读器有用,StAX 还有一个 XMLEventWriter,同样,还有一个 XMLStreamWriter 类用于 XML 输出。

2、 ServiceLoader – 加载服务(获取接口的所有实现)

Java 开发人员经常希望将使用和创建组件的内容区分开来。这通常是通过创建一个描述组件动作的接口,并使用某种中介创建组件实例来完成的。很多开发人员使用 Spring 框架来完成,但还有其他的方法,它比 Spring 容器更轻量级。

java.util 的 ServiceLoader 类能读取隐藏在 JAR 文件中的配置文件,并找到接口的实现,然后使这些实现成为可选择的列表。例如,如果您需要一个私仆(personal-servant)组件来完成任务,您可以使用清单 2 中的代码来实现: 清单 2. IPersonalServant public interface IPersonalServant { // Process a file of commands to the servant public void process(java.io.File f) throws java.io.IOException; public boolean can(String command); }

can() 方法可让您确定所提供的私仆实现是否满足需求。清单 3 中的 ServiceLoader 的 IPersonalServant 列表基本上满足需求: 清单 3. IPersonalServant 行吗? import java.io.*; import java.util.*; public class Servant { public static void main(String[] args) throws IOException { ServiceLoader servantLoader = ServiceLoader.load(IPersonalServant.class); IPersonalServant i = null; for (IPersonalServant ii : servantLoader) if (ii.can(\ i = ii; if (i == null) throw new IllegalArgumentException(\ for (String arg : args) { i.process(new File(arg)); } } }

假设有此接口的实现,如清单 4: 清单 4. Jeeves 实现了 IPersonalServant import java.io.*; public class Jeeves implements IPersonalServant { public void process(File f) { System.out.println(\ } public boolean can(String cmd) { if (cmd.equals(\ return true; else return false; } }

剩下的就是配置包含实现的 JAR 文件,让 ServiceLoader 能识别 — 这可能会非常棘手。JDK 想要 JAR 文件有一个 META-INF/services 目录,它包含一个文本文件,其文件名与接口类名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口类名的内容是实现的名称,每行一个,如清单 5: 清单 5. META-INF/services/IPersonalServant

Jeeves # comments are OK

幸运的是,Ant 构建系统(自 1.7.0 以来)包含一个对 jar 任务的服务标签,让这相对容易,见清单 6:

清单 6. Ant 构建的 IPersonalServant

这里,很容易调用 IPersonalServant,让它执行命令。然而,解析和执行这些命令可能会非常棘手。这又是另一个 “小线头”。

3、 Scanner

有无数 Java 工具能帮助您构建解析器,很多函数语言已成功构建解析器函数库(解析器选择器)。但如果要解析的是逗号分隔值文件,或空格分隔文本文件,又怎么办呢?大多数工具用在此处就过于隆重了,而 String.split() 又不够。(对于正则表达式,请记住一句老话:“ 您有一个问题,用正则表达式解决。那您就有两个问题了。”)

Java 平台的 Scanner 类会是这些类中您最好的选择。以轻量级文本解析器为目标,Scanner 提供了一个相对简单的 API,用于提取结构化文本,并放入强类型的部分。想象一下,如果您愿意,一组类似 DSL 的命令(源自 Terry Pratchett Discworld 小说)排列在文本文件中,如清单 7: 清单 7. Igor 的任务 fetch 1 head fetch 3 eye fetch 1 foot attach foot to head attach eye to head admire 您,或者是本例中称为 Igor的私仆,能轻松使用 Scanner 解析这组违法命令,如清单 8 所示:

清单 8. Igor 的任务,由 Scanner 解析 import java.io.*; import java.util.*; public class Igor implements IPersonalServant { public boolean can(String cmd)

{ if (cmd.equals(\ return true; if (cmd.equals(\ 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(\ { int num = scanner.nextInt(); String type = scanner.next(); fetch (num, type); } else if (verb.equals(\ { String item = scanner.next(); String to = scanner.next(); String target = scanner.next(); attach(item, target); } else if (verb.equals(\ { admire(); } else { System.out.println(\ + verb + \ } } } public void fetch(int number, String type) { if (parts.get(type) == null) { System.out.println(\ + type + (number > 1 ? \ parts.put(type, number); } else { System.out.println(\ + type + (number > 1 ? \ Integer currentTotal = parts.get(type); parts.put(type, currentTotal + number); } System.out.println(\ } public void attach(String item, String target) { System.out.println(\ target + \ } public void admire() { System.out.println(\ } private Map parts = new HashMap(); } 假设 Igor 已在 ServantLoader 中注册,可以很方便地将 can() 调用改得更实用,并重用前面的 Servant 代码,如清单 9 所示: 清单 9. Igor 做了什么 import java.io.*; import java.util.*; public class Servant { public static void main(String[] args) throws IOException { ServiceLoader servantLoader = ServiceLoader.load(IPersonalServant.class); IPersonalServant i = null; for (IPersonalServant ii : servantLoader) if (ii.can(\ i = ii; if (i == null) throw new IllegalArgumentException(\ for (String arg : args) { i.process(new File(arg)); } } } 真正 DSL 实现显然不会仅仅打印到标准输出流。我把追踪哪些部分、跟随哪些部分的细节留待给您(当然,还有忠诚的 Igor)。

4、 Timer

java.util.Timer 和 TimerTask 类提供了方便、相对简单的方法可在定期或一次性延迟的基础上执行任务: 清单 10. 稍后执行

import java.util.*; public class Later { public static void main(String[] args) { Timer t = new Timer(\ t.schedule(new TimerTask() { public void run() { System.out.println(\ System.exit(0); } }, 1 * 1000); System.out.println(\ } } Timer 有许多 schedule() 重载,它们提示某一任务是一次性还是重复的,并且有一个启动的 TimerTask 实例。TimerTask 实际上是一个 Runnable(事实上,它实现了它),但还有另外两个方法:cancel() 用来取消任务,scheduledExecutionTime() 用来返回任务何时启动的近似值。

请注意 Timer 却创建了一个非守护线程在后台启动任务,因此在清单 10 中我需要调

用 System.exit() 来取消任务。在长时间运行的程序中,最好创建一个 Timer 守护线程(使用带有指示守护线程状态的参数的构造函数),从而它不会让 VM 活动。

这个类没什么神奇的,但它确实能帮助我们对后台启动的程序的目的了解得更清楚。它还能节省一些 Thread 代码,并作为轻量级 ScheduledExecutorService(对于还没准备好了解整个 java.util.concurrent 包的人来说)。

5、 JavaSound

尽管在服务器端应用程序中不常出现,但 sound 对管理员有着有用的 “被动” 意义 — 它是恶作剧的好材料。尽管它很晚才出现在 Java 平台中,JavaSound API 最终还是加入了核心运行时库,封装在 javax.sound * 包 — 其中一个包是 MIDI 文件,另一个是音频文件示例(如普遍的 .WAV 文件格式)。

JavaSound 的 “hello world” 是播放一个片段,如清单 11 所示: 清单 11. 再放一遍,Sam public static void playClip(String audioFile) { try { AudioInputStream inputStream = AudioSystem.getAudioInputStream( this.getClass().getResourceAsStream(audioFile)); DataLine.Info info = new DataLine.Info( Clip.class, audioInputStream.getFormat() ); Clip clip = (Clip) AudioSystem.getLine(info); clip.addLineListener(new LineListener() { public void update(LineEvent e) { if (e.getType() == LineEvent.Type.STOP) { synchronized(clip) { clip.notify(); } } } }); clip.open(audioInputStream); clip.setFramePosition(0); clip.start(); synchronized (clip) { clip.wait(); } clip.drain(); clip.close(); } catch (Exception ex) { ex.printStackTrace(); } } 大多数还是相当简单(至少 JavaSound 一样简单)。第一步是创建一个文件的 AudioInputStream 来播放。为了让此方法尽量与上下文无关,我们从加载类的 ClassLoader 中抓取文件作为 InputStream。(AudioSystem 还需要一个 File 或 String,如果提前知道声音文件的具体路径。)一旦完成, DataLine.Info 对象就提供给 AudioSystem,得到一个 Clip,这是播放音频片段最简单的方法。(其他方法提供了对片段更多的控制 — 例如获取一个 SourceDataLine — 但对于 “播放” 来说,过于复杂)。

这里应该和对 AudioInputStream 调用 open() 一样简单。(“应该” 的意思是如果您没遇到下节描述的错误。)调用 start() 开始播放,drain() 等待播放完成,close() 释放音频线路。播放是在单独的线程进行,因此调用 stop() 将会停止播放,然后调用 start() 将会从播放暂停的地方重新开始;使用 setFramePosition(0) 重新定位到开始。

没声音?

JDK 5 发行版中有个讨厌的小错误:在有些平台上,对于一些短的音频片段,代码看上去运行正常,但就是 ... 没声音。显然媒体播放器在应该出现的位置之前触发了 STOP 事件。

这个错误 “无法修复”,但解决方法相当简单:注册一个 LineListener 来监听 STOP 事件,当触发时,调用片段对象的 notifyAll()。然后在 “调用者” 代码中,通过调用 wait() 等待片段完成(还调用 notifyAll())。在没出现错误的平台上,这些错误是多余的,在 Windows? 及有些 Linux? 版本上,会让程序员 “开心” 或 “愤怒”。

使用 Java 语言进行 Unicode 代理编程

原文地址:http://www.ibm.com/developerworks/cn/java/j-unicode/index.html#

早期 Java 版本使用 16 位 char 数据类型表示 Unicode 字符。这种设计方法有时比较合理,因为所有 Unicode 字符拥有的值都小于 65,535 (0xFFFF),可以通过 16 位表示。但是,Unicode 后来将最大值增加到 1,114,111 (0x10FFFF)。由于 16 位太小,不能表示 Unicode version 3.1 中的所有 Unicode 字符,32 位值 — 称为码位(code point) — 被用于 UTF-32

编码模式。

但与 32 位值相比,16 位值的内存使用效率更高,因此 Unicode 引入了一个种新设计方法来允许继续使用 16 位值。UTF-16 中采用的这种设计方法分配 1,024 值给 16 位高代理(high surrogate),将另外的 1,024 值分配给 16 位低代理(low surrogate)。它使用一个高代理加上一个低代理 — 一个代理对(surrogate pair) — 来表示 65,536 (0x10000) 和 1,114,111 (0x10FFFF) 之间的 1,048,576 (0x100000) 值(1,024 和 1,024 的乘积)。

Java 1.5 保留了 char 类型的行为来表示 UTF-16 值(以便兼容现有程序),它实现了码位的概念来表示 UTF-32 值。这个扩展(根据 JSR 204:Unicode Supplementary Character Support 实现)不需要记住 Unicode 码位或转换算法的准确值 — 但理解代理 API 的正确用法很重要。

东亚国家和地区近年来增加了它们的字符集中的字符数量,以满足用户需求。这些标准包括来自中国的国家标准组织的 GB 18030 和来自日本的 JIS X 0213。因此,寻求遵守这些标准的程序更有必要支持 Unicode 代理对。本文解释相关 Java API 和编码选项,面向计划重新设计他们的软件,从只能使用 char 类型的字符转换为能够处理代理对的新版本的读者。

1、 顺序访问

顺序访问是在 Java 语言中处理字符串的一个基本操作。在这种方法下,输入字符串中的每个字符从头至尾按顺序访问,或者有时从尾至头访问。本小节讨论使用顺序访问方法从一个字符串创建一个 32 位码位数组的 7 个技术示例,并估计它们的处理时间。

示例 1-1:基准测试(不支持代理对)

清单 1 将 16 位 char 类型值直接分配给 32 位码位值,完全没有考虑代理对: 清单 1. 不支持代理对 int[] toCodePointArray(String str) { // Example 1-1 int len = str.length(); // the length of str int[] acp = new int[len]; // an array of code points for (int i = 0, j = 0; i < len; i++) { acp[j++] = str.charAt(i); } return acp; } 尽管这个示例不支持代理对,但它提供了一个处理时间基准来比较后续顺序访问示例。

示例 1-2:使用 isSurrogatePair()

清单 2 使用 isSurrogatePair() 来计算代理对总数。计数之后,它分配足够的内存以便一个码位数组存储这个值。然后,它进入一个顺序访问循环,使用 isHighSurrogate() 和 isLowSurrogate() 确定每个代理对字符是高代理还是低代理。当它发现一个高代理后面带一个低代理时,它使用 toCodePoint() 将该代理对转换为一个码位值并将当前索引值增加 2。否则,它将这个 char 类型值直接分配给一个码位值并将当前索引值增加 1。这个示例的处理时间比 示例 1-1 长 1.38 倍。 清单 2. 有限支持 int[] toCodePointArray(String str) { // Example 1-2 int len = str.length(); // the length of str int[] acp; // an array of code points int surrogatePairCount = 0; // the count of surrogate pairs for (int i = 1; i < len; i++) { if (Character.isSurrogatePair(str.charAt(i - 1), str.charAt(i))) { surrogatePairCount++; i++; } } acp = new int[len - surrogatePairCount]; for (int i = 0, j = 0; i < len; i++) { char ch0 = str.charAt(i); // the current char if (Character.isHighSurrogate(ch0) && i + 1 < len) { char ch1 = str.charAt(i + 1); // the next char if (Character.isLowSurrogate(ch1)) { acp[j++] = Character.toCodePoint(ch0, ch1); i++; continue; } } acp[j++] = ch0; } return acp; }

清单 2 中更新软件的方法很幼稚。它比较麻烦,需要大量修改,使得生成的软件很脆弱且今后难以更改。具体而言,这些问题是:

? 需要计算码位的数量以分配足够的内存

? 很难获得字符串中的指定索引的正确码位值 ? 很难为下一个处理步骤正确移动当前索引 一个改进后的算法出现在下一个示例中。 示例:基本支持

Java 1.5 提供了 codePointCount()、codePointAt() 和 offsetByCodePoints() 方法来分别处理 示例 1-2 的 3 个问题。清单 3 使用这些方法来改善这个算法的可读性: 清单 3. 基本支持

int[] toCodePointArray(String str) { // Example 1-3 int len = str.length(); // the length of str int[] acp = new int[str.codePointCount(0, len)]; for (int i = 0, j = 0; i < len; i = str.offsetByCodePoints(i, 1)) { acp[j++] = str.codePointAt(i); } return acp; } 但是,清单 3 的处理时间比 清单 1 长 2.8 倍。 示例 1-4:使用 codePointBefore()

当 offsetByCodePoints() 接收一个负数作为第二个参数时,它就能计算一个距离字符串头的绝对偏移值。接下来,codePointBefore() 能够返回一个指定索引前面的码位值。这些方法用于清单 4 中从尾至头遍历字符串: 清单 4. 使用 codePointBefore() 的基本支持

int[] toCodePointArray(String str) { // Example 1-4 int len = str.length(); // the length of str int[] acp = new int[str.codePointCount(0, len)]; int j = acp.length; // an index for acp for (int i = len; i > 0; i = str.offsetByCodePoints(i, -1)) { acp[--j] = str.codePointBefore(i); } return acp; } 这个示例的处理时间 — 比 示例 1-1 长 2.72 倍 — 比 示例 1-3 快一些。通常,当您比较零而不是非零值时,JVM 中的代码大小要小一些,这有时会提高性能。但是,微小

的改进可能不值得牺牲可读性。

示例 1-5:使用 charCount()

示例 1-3 和 1-4 提供基本的代理对支持。他们不需要任何临时变量,是健壮的编码方法。要获取更短的处理时间,使用 charCount() 而不是 offsetByCodePoints() 是有效的,但需要一个临时变量来存放码位值,如清单 5 所示: 清单 5. 使用 charCount() 的优化支持 int[] toCodePointArray(String str) { // Example 1-5 int len = str.length(); // the length of str int[] acp = new int[str.codePointCount(0, len)]; int j = 0; // an index for acp for (int i = 0, cp; i < len; i += Character.charCount(cp)) { cp = str.codePointAt(i); acp[j++] = cp; } return acp; }

清单 5 的处理时间降低到比 示例 1-1 长 1.68 倍。 示例 1-6:访问一个 char 数组

清单 6 在使用 示例 1-5 中展示的优化的同时直接访问一个 char 类型数组: 清单 6. 使用一个 char 数组的优化支持

int[] toCodePointArray(String str) { // Example 1-6 char[] ach = str.toCharArray(); // a char array copied from str int len = ach.length; // the length of ach int[] acp = new int[Character.codePointCount(ach, 0, len)]; int j = 0; // an index for acp for (int i = 0, cp; i < len; i += Character.charCount(cp)) { cp = Character.codePointAt(ach, i); acp[j++] = cp; } return acp; } char 数组是使用 toCharArray() 从字符串复制而来的。性能得到改善,因为对数组的直接访问比通过一个方法的间接访问要快。处理时间比 示例 1-1 长 1.51 倍。但是,当调用时,toCharArray() 需要一些开销来创建一个新数组并将数据复制到数组中。String 类提供的

那些方便的方法也不能被使用。但是,这个算法在处理大量数据时有用。

示例 1-7:一个面向对象的算法

这个示例的面向对象算法使用 CharBuffer 类,如清单 7 所示: 清单 7. 使用 CharSequence 的面向对象算法

int[] toCodePointArray(String str) { // Example 1-7 CharBuffer cBuf = CharBuffer.wrap(str); // Buffer to wrap str IntBuffer iBuf = IntBuffer.allocate( // Buffer to store code points Character.codePointCount(cBuf, 0, cBuf.capacity())); while (cBuf.remaining() > 0) { int cp = Character.codePointAt(cBuf, 0); // the current code point iBuf.put(cp); cBuf.position(cBuf.position() + Character.charCount(cp)); } return iBuf.array(); }

与前面的示例不同,清单 7 不需要一个索引来持有当前位置以便进行顺序访问。相反,CharBuffer 在内部跟踪当前位置。Character 类提供静态方法 codePointCount() 和 codePointAt(),它们能通过 CharSequence 接口处理 CharBuffer。CharBuffer 总是将当前位置设置为 CharSequence 的头。因此,当 codePointAt() 被调用时,第二个参数总是设置为 0。处理时间比 示例 1-1 长 2.15 倍。

处理时间比较

这些顺序访问示例的计时测试使用了一个包含 10,000 个代理对和 10,000 个非代理对的样例字符串。码位数组从这个字符串创建 10,000 次。测试环境包括: ? ? ? ?

OS:Microsoft Windows? XP Professional SP2 Java:IBM Java 1.5 SR7

CPU:Intel? Core 2 Duo CPU T8300 @ 2.40GHz Memory:2.97GB RAM

表 1 展示了示例 1-1 到 1-7 的绝对和相对处理时间以及关联的 API: 表 1. 顺序访问示例的处理时间和 API 示例 处理 与 时间 示例 (毫1-1 的秒) 比率 2031 1.00 2797 1.38 Character 类: 说明 API 1-1 不支持代理对 1-2 有限支持

? ? ? ? static boolean isHighSurrogate(char ch) static boolean isLowSurrogate(char ch) static boolean isSurrogatePair(char high, char low) static int toCodePoint(char high, char low) String 类: ? 1-3 基本支持 5687 2.80 ? ? int codePointAt(int index) int codePointCount(int begin, int end) int offsetByCodePoints(int index, int cpOffset) 使用 1-4 codePointBefore() 的5516 2.72 基本支持 使用 charCount() 的优1-5 3406 1.68 化支持 String 类: ? int codePointBefore(int index) Character 类: ? static int charCount(int cp) Character 类: ? ? 使用一个 char 数组的1-6 3062 1.51 优化支持 static int codePointAt(char[] ach, int index) static int codePointCount(char[] ach, int offset, int count) Character 类: ? 使用 CharSequence 的1-7 4360 2.15 面向对象方法 ? static int codePointAt(CharSequence seq, int index) static int codePointCount(CharSequence seq, int begin, int end) 2、 随机访问 随机访问是直接访问一个字符串中的任意位置。当字符串被访问时,索引值基于 16 位 char 类型的单位。但是,如果一个字符串使用 32 位码位,那么它不能使用一个基于 32 位

码位的单位的索引访问。必须使用 offsetByCodePoints() 来将码位的索引转换为 char 类型的索引。如果算法设计很糟糕,这会导致很差的性能,因为 offsetByCodePoints() 总是通过使用第二个参数从第一个参数计算字符串的内部。在这个小节中,我将比较三个示例,它们通过使用一个短单位来分割一个长字符串。

示例 2-1:基准测试(不支持代理对)

清单 8 展示如何使用一个宽度单位来分割一个字符串。这个基准测试留作后用,不支持代理对。

清单 8. 不支持代理对

String[] sliceString(String str, int width) { // Example 2-1 // It must be that \ List slices = new ArrayList(); int len = str.length(); // (1) the length of str int sliceLimit = len - width; // (2) Do not slice beyond here. int pos = 0; // the current position per char type while (pos < sliceLimit) { int begin = pos; // (3) int end = pos + width; // (4) slices.add(str.substring(begin, end)); pos += width; // (5) } slices.add(str.substring(pos)); // (6) return slices.toArray(new String[slices.size()]); }

sliceLimit 变量对分割位置有所限制,以避免在剩余的字符串不足以分割当前宽度单位时抛出一个 IndexOutOfBoundsException 实例。这种算法在当前位置超出 sliceLimit 时从 while 循环中跳出后再处理最后的分割。

示例 2-2:使用一个码位索引

清单 9 展示了如何使用一个码位索引来随机访问一个字符串: 清单 9. 糟糕的性能 String[] sliceString(String str, int width) { // Example 2-2 // It must be that \ List slices = new ArrayList(); int len = str.codePointCount(0, str.length()); // (1) code point count [Modified] int sliceLimit = len - width; // (2) Do not slice beyond here. int pos = 0; // the current position per code point while (pos < sliceLimit) { int begin = str.offsetByCodePoints(0, pos); // (3) [Modified] int end = str.offsetByCodePoints(0, pos + width); // (4) [Modified] slices.add(str.substring(begin, end)); pos += width; // (5) } slices.add(str.substring(str.offsetByCodePoints(0, pos))); // (6) [Modified] return slices.toArray(new String[slices.size()]); } 清单 9 修改了 清单 8 中的几行。首先,在 Line (1) 中,length() 被 codePointCount() 替代。其次,在 Lines (3)、(4) 和 (6) 中,char 类型的索引通过 offsetByCodePoints() 用码位索引替代。

基本的算法流与 示例 2-1 中的看起来几乎一样。但处理时间根据字符串长度与示例 2-1 的比率同比增加,因为 offsetByCodePoints() 总是从字符串头到指定索引计算字符串内部。

示例 2-3:减少的处理时间

可以使用清单 10 中展示的方法来避免 示例 2-2 的性能问题: 清单 10. 改进的性能

String[] sliceString(String str, int width) { // Example 2-3 // It must be that \ List slices = new ArrayList(); int len = str.length(); // (1) the length of str int sliceLimit // (2) Do not slice beyond here. [Modified] = (len >= width * 2 || str.codePointCount(0, len) > width) ? str.offsetByCodePoints(len, -width) : 0; int pos = 0; // the current position per char type while (pos < sliceLimit) { int begin = pos; // (3) int end = str.offsetByCodePoints(pos, width); // (4) [Modified] slices.add(str.substring(begin, end)); pos = end; // (5) [Modified] } slices.add(str.substring(pos)); // (6) return slices.toArray(new String[slices.size()]); } 首先,在 Line (2) 中,(清单 9 中的)表达式 len-width 被 offsetByCodePoints(len,-width) 替代。但是,当 width 的值大于码位的数量时,这会抛出一个 IndexOutOfBoundsException 实

例。必须考虑边界条件以避免异常,使用一个带有 try/catch 异常处理程序的子句将是另一个解决方案。如果表达式 len>width*2 为 true,则可以安全地调用 offsetByCodePoints(),因为即使所有码位都被转换为代理对,码位的数量仍会超过 width 的值。或者,如果 codePointCount(0,len)>width 为 true,也可以安全地调用 offsetByCodePoints()。如果是其他情况,sliceLimit 必须设置为 0。

在 Line (4) 中,清单 9 中的表达式 pos + width 必须在 while 循环中使用 offsetByCodePoints(pos,width) 替换。需要计算的量位于 width 的值中,因为第一个参数指定当 width 的值。接下来,在 Line (5) 中,表达式 pos+=width 必须使用表达式 pos=end 替换。这避免两次调用 offsetByCodePoints() 来计算相同的索引。源代码可以被进一步修改以最小化处理时间。

处理时间比较

图 1 和图 2 展示了示例 2-1、2-2 和 2-3 的处理时间。样例字符串包含相同数量的代理对和非代理对。当字符串的长度和 width 的值被更改时,样例字符串被切割 10,000 次。 图 1. 一个分段的常量宽度

图 2. 分段的常量计数

示例 2-1 和 2-3 按照长度比例增加了它们的处理时间,但 示例 2-2 按照长度的平方比例增加了处理时间。当字符串长度和 width 的值增加而分段的数量固定时,示例 2-1 拥有一个常量处理时间,而示例 2-2 和 2-3 以 width 的值为比例增加了它们的处理时间。

3、 信息 API

大多数处理代理的信息 API 拥有两种名称相同的方法。一种接收 16 位 char 类型参数,另一种接收 32 为码位参数。表 2 展示了每个 API 的返回值。第三列针对 U+53F1,第 4 列针对 U+20B9F,最后一列针对 U+D842(即高代理),而 U+20B9F 被转换为 U+D842 加上 U+DF9F 的代理对。如果程序不能处理代理对,则值 U+D842 而不是 U+20B9F 将导致意想不到的结果(在表 2 中以粗斜体表示)。 表 2. 用于代理的信息 API 类 方法/构造函数 针对 U+53F1 的值 针对 U+20B9F 的值 针对 U+D842 的值 0 static byte CharactegetDirectionality(i0 r nt cp) static int getNumericValue(int -1 cp) static int getType(int cp) 5 0 -1 -1 5 true false false 19 true false false static boolean true isDefined(int cp) static boolean isDigit(int cp) false static boolean false isISOControl(int cp)

static boolean isIdentifierIgnorabfalse le(int cp) static boolean isJavaIdentifierPartrue t(int cp) static boolean isJavaIdentifierStatrue rt(int cp) static boolean true isLetter(int cp) static boolean isLetterOrDigit(int true cp) static boolean false isLowerCase(int cp) static boolean false isMirrored(int cp) static boolean false isSpaceChar(int cp) static boolean isSupplementaryCodefalse Point(int cp) static boolean false isTitleCase(int cp) static boolean isUnicodeIdentifiertrue Part(int cp) static boolean isUnicodeIdentifiertrue Start(int cp) static boolean false isUpperCase(int cp) static boolean isValidCodePoint(intrue t cp) static boolean false isWhitespace(int cp) static int (不可更改) toLowerCase(int cp) static int (不可更改) toTitleCase(int cp) false false true false true false true false true false false false false false false false true false false false true false true false false false true true false false static int (不可更改) toUpperCase(int cp) Character. Character.UnicodeBlCJK_UNIFIED_IDECJK_UNIFIED_IDEOGRAPHS_HIGH_SURROUnicodeBock of(int cp) OGRAPHS EXTENSION_B GATES lock Font boolean (取决于 Font 实例) canDisplay(int cp) FontMetrint charWidth(int (取决于 FontMetrics 实例) ics cp) String int indexOf(int cp) (取决于 String 实例) int lastIndexOf(int (取决于 String 实例) cp) 本小节介绍前面的小节中没有讨论的代理对相关 API。表 3 展示所有这些剩余的 API。所有代理对 API 都包含在表 1、2 和 3 中。 表 3. 其他代理 API

类 Character String StringBuffer StringBuilder 方法/构造函数 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(int[] acp, int offset, int count) int indexOf(int cp, int fromIndex) int lastIndexOf(int cp, int fromIndex) 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 appendCodePoint(int cp) int codePointAt(int index) int codePointBefore(int index) 4、 其他 API int codePointCount(int beginIndex, int endIndex) int offsetByCodePoints(int index, int cpOffset) IllegalFormat IllegalFormatCodePointException(int cp) CodePointException

清单 11 展示了从一个码位创建一个字符串的 5 种方法。用于测试的码位是 U+53F1 和 U+20B9F,它们在一个字符串中重复了 100 亿次。清单 11 中的注释部分显示了处理时间:

清单 11. 从一个码位创建一个字符串的 5 种方法 int cp = 0x20b9f; // CJK Ideograph Extension B String str1 = new String(new int[]{cp}, 0, 1); // processing time: 206ms String str2 = new String(Character.toChars(cp)); // 187ms String str3 = String.valueOf(Character.toChars(cp)); // 195ms String str4 = new StringBuilder().appendCodePoint(cp).toString(); // 269ms String str5 = String.format(\int getCodePoint() str1、str2、str3 和 str4 的处理时间没有明显不同。相反,创建 str5 花费的时间要长得多,因为它使用 String.format(),该方法支持基于本地和格式化信息的灵活输出。str5 方法应该只用于程序的末尾来输出文本。

5、 结束语

Unicode 的每个新版本都包含了通过代理对表示的新定义的字符。东亚字符集标准并不是这样的字符的惟一来源。例如,移动电话中还需要支持 Emoji 字符(表情图释),还有各种古字符需要支持。您从本文收获的技术和性能分析将有助于您在您的 Java 应用程序中支持所有这些字符。

关于 JAR 您不知道的 5 件事

把它放在 JAR 中

通常,在源代码被编译之后,您需要构建一个 JAR 文件,使用 jar 命令行实用工具,或者,更常用的是 Ant jar 任务将 Java 代码(已经被包分离)收集到一个单独的集合中,过程简洁易懂,我不想在这做过多的说明,稍后将继续说明如何构建 JAR。现在,我只需要存档 Hello,这是一个独立控制台实用工具,对于执行打印消息到控制台这个任务十分有用。如清单 1 所示:

清单 1. 存档控制台实用工具

package com.tedneward.jars; public class Hello { public static void main(String[] args) { System.out.println(\ } } Hello 实用工具内容并不多,但是对于研究 JAR 文件却是一个很有用的 “脚手架”,我们先从执行此代码开始。

1、 JAR 是可执行的

.NET 和 C++ 这类语言一直是 OS 友好的,只需要在命令行(helloWorld.exe)引用其名称,或在 GUI shell 中双击它的图标就可以启动应用程序。然而在 Java 编程中,启动器程序 — java — 将 JVM 引导入进程中,我们需要传递一个命令行参数(com.tedneward.Hello)指定想要启动的 main() 方法的类。

这些附加步骤使使用 Java 创建界面友好的应用程序更加困难。不仅终端用户需要在命令行输入所有参数(终端用户宁愿避开),而且极有可能使他或她操作失误以及返回一个难以理解的错误。

这个解决方案使 JAR 文件 “可执行” ,以致 Java 启动程序在执行 JAR 文件时,自动识别哪个类将要启动。我们所要做的是,将一个入口引入 JAR 文件清单文件(MANIFEST.MF 在 JAR 的 META-INF 子目录下),像这样: 清单 2. 展示入口点!

Main-Class: com.tedneward.jars.Hello 这个清单文件只是一个名值对。因为有时候清单文件很难处理回车和空格,然而在构建 JAR 时,使用 Ant 来生成清单文件是很容易的。在清单 3 中,使用 Ant jar 任务的 manifest 元素来指定清单文件: 清单 3. 构建我的入口点!

现在用户在执行 JAR 文件时需要做的就是通过 java -jar outapp.jar 在命令行上指定其文件名。就 GUI shell 来说,双击 JAR 文件即可。

2、 JAR 可以包括依赖关系信息

似乎 Hello 实用工具已经展开,改变实现的需求已经出现。Spring 或 Guice 这类依赖项注入(DI)容器可以为我们处理许多细节,但是仍然有点小问题:修改代码使其含有 DI 容器的用法可能导致清单 4 所示的结果,如: 清单 4. Hello、Spring world!

package com.tedneward.jars; import org.springframework.context.*; import org.springframework.context.support.*; public class Hello { public static void main(String[] args) { ApplicationContext appContext = new FileSystemXmlApplicationContext(\ ISpeak speaker = (ISpeak) appContext.getBean(\ System.out.println(speaker.sayHello()); } } 3、 关于 Spring 的更多信息

这个技巧将帮助您熟悉依赖项注入和 Spring 框架。如果您需要温习其他主题,见 参考资料。

由于启动程序的 -jar 选项将覆盖 -classpath 命令行选项中的所有内容,因此运行这些代码时,Spring 必须是在 CLASSPATH 和 环境变量中。幸运的是,JAR 允许在清单文件中出现其他的 JAR 依赖项声明,这使得无需声明就可以隐式创建 CLASSPATH,如清单 5 所示: 清单 5. Hello、Spring CLASSPATH!

./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\ 注意 Class-Path 属性包含一个与应用程序所依赖的 JAR 文件相关的引用。您可以将它写成一个绝对引用或者完全没有前缀。这种情况下,我们假设 JAR 文件同应用程序 JAR 在同一个目录下。

不幸的是,value 属性和 Ant Class-Path 属性必须出现在同一行,因为 JAR 清单文件不能处理多个 Class-Path 属性。因此,所有这些依赖项在清单文件中必须出现在一行。当然,这很难看,但为了使 java -jar outapp.jar 可用,还是值得的!

4、 JAR 可以被隐式引用

如果有几个不同的命令行实用工具(或其他的应用程序)在使用 Spring 框架,可能更容易将 Spring JAR 文件放在公共位置,使所有实用工具能够引用。这样就避免了文件系统中到处都有 JAR 副本。Java 运行时 JAR 的公共位置,众所周知是 “扩展目录” ,默认位于 lib/ext 子目录,在 JRE 的安装位置之下。

JRE 是一个可定制的位置,但是在一个给定的 Java 环境中很少定制,以至于可以完全假设 lib/ext 是存储 JAR 的一个安全地方,以及它们将隐式地用于 Java 环境的 CLASSPATH 上。

5、 Java 6 允许类路径通配符

为了避免庞大的 CLASSPATH 环境变量(Java 开发人员几年前就应该抛弃的)和/或命令行 -classpath 参数,Java 6 引入了类路径通配符 的概念。与其不得不启动参数中明确列出的每个 JAR 文件,还不如自己指定 lib/*,让所有 JAR 文件列在该目录下(不递归),在类路径中。

不幸的是,类路径通配符不适用于之前提到的 Class-Path 属性清单入口。但是这使得它更容易启动 Java 应用程序(包括服务器)开发人员任务,例如 code-gen 工具或分析工具。

6、 JAR 有的不只是代码

Spring,就像许多 Java 生态系统一样,依赖于一个描述构建环境的配置文件,前面提到过,Spring 依赖于一个 app.xml 文件,此文件同 JAR 文件位于同一目录 — 但是开发人员在复制 JAR 文件的同时忘记复制配置文件,这太常见了!

一些配置文件可用 sysadmin 进行编辑,但是其中很大一部分(例如 Hibernate 映射)都位于 sysadmin 域之外,这将导致部署漏洞。一个合理的解决方案是将配置文件和代码封装在一起 — 这是可行的,因为 JAR 从根本上来说就是一个 “乔装的” ZIP 文件。 当构建一个 JAR 时,只需要在 Ant 任务或 jar 命令行包括一个配置文件即可。

JAR 也可以包含其他类型的文件,不仅仅是配置文件。例如,如果我的 SpeakEnglish 部件要访问一个属性文件,我可以进行如下设置,如清单 6 所示: 清单 6. 随机响应

package com.tedneward.jars; import java.util.*; public class SpeakEnglish implements ISpeak { Properties responses = new Properties(); Random random = new Random(); public String sayHello() { // Pick a response at random int which = random.nextInt(5); return responses.getProperty(\ } } 可以将 responses.properties 放入 JAR 文件,这意味着部署 JAR 文件时至少可以少考虑一个文件。这只需要在 JAR 步骤中包含 responses.properties 文件即可。

当您在 JAR 中存储属性之后,您可能想知道如何将它取回。如果所需要的数据与 JAR 文件在同一位置,正如前面的例子中提到的那样,不需要费心找出 JAR 文件的位置,使用 JarFile 对象就可将其打开。相反,可以使用类的 ClassLoader 找到它,像在 JAR 文件中寻找 “资源” 那样,使用 ClassLoader getResourceAsStream() 方法,如清单 7 所示: 清单 7. ClassLoader 定位资源 package com.tedneward.jars; import java.util.*; public class SpeakEnglish implements ISpeak { Properties responses = new Properties(); // ... public SpeakEnglish() { try { ClassLoader myCL = SpeakEnglish.class.getClassLoader(); responses.load( myCL.getResourceAsStream( \ } catch (Exception x) { x.printStackTrace(); } } // ... } 您可以按照以上步骤寻找任何类型的资源:配置文件、审计文件、图形文件,等等。几乎任何文件类型都能被捆绑进 JAR 中,作为一个 InputStream 获取(通过 ClassLoader),并通过您喜欢的方式使用。

关于 Java 性能监控您不知道的 5 件事,第 1 部分

许多开发人员没有意识到从 Java 5 开始 JDK 中包含了一个分析器。JConsole(或者 Java 平台最新版本,VisualVM)是一个内置分析器,它同 Java 编译器一样容易启动。如果是从命令行启动,使 JDK 在 PATH 上,运行 jconsole 即可。如果从 GUI shell 启动,找到 JDK 安装路径,打开 bin 文件夹,双击 jconsole。

当分析工具弹出时(取决于正在运行的 Java 版本以及正在运行的 Java 程序数量),可能会出现一个对话框,要求输入一个进程的 URL 来连接,也可能列出许多不同的本地 Java 进程(有时包含 JConsole 进程本身)来连接。

1、 JConsole 或 VisualVM?

JConsole 从 Java 5 开始就随着 Java 平台版本一起发布,而 VisualVM 是在 NetBeans 基础上升级的一个分析器,在 Java 6 的更新版 12 中第一次发布。多数商店还没有更新到 Java 6 ,因此这篇文章主要介绍 JConsole 。然而,多数技巧和这两个分析器都有关。(注意:

除了包含在 Java 6 中之外,VisualVM 还有一个独立版下载。下载 VisualVM,参见 参考资料。)

2、 使用 JConsole 进行工作

在 Java 5 中,Java 进程并不是被设置为默认分析的,而是通过一个命令行参数 — -Dcom.sun.management.jmxremote — 在启动时告诉 Java 5 VM 打开连接,以便分析器可以找到它们;当进程被 JConsole 捡起时,您只能双击它开始分析。

分析器有自己的开销,因此最好的办法就是花点时间来弄清是什么开销。发现 JConsole 开销最简单的办法是,首先独自运行一个应用程序,然后在分析器下运行,并测量差异。(应用程序不能太大或者太小;我最喜欢使用 JDK 附带的 SwingSet2 样本。)因此,我使用 -verbose:gc 尝试运行 SwingSet2 来查看垃圾收集清理,然后运行同一个应用程序并将 JConsole 分析器连接到它。当 JConsole 连接好了之后,一个稳定的 GC 清理流出现,否则不会出现。这就是分析器的性能开销。

3、 远程连接进程

因为 Web 应用程序分析工具假设通过一个套接字进行连通性分析,您只需要进行少许配置来设置 JConsole(或者是基于 JVMTI 的分析器,就这点而言),监控/分析远程运行的应用程序。

如果 Tomcat 运行在一个名为 “webserve” 的机器上,且 JVM 已经启动了 JMX 并监听端口 9004,从 JConsole(或者任何 JMX 客户端)连接它需要一个 JMX URL “service:jmx:rmi:///jndi/rmi://webserver:9004/jmxrmi”。

基本上,要分析一个运行在远程数据中心的应用程序服务器,您所需要的仅仅是一个 JMX URL。更多关于使用 JMX 和 JConsole 远程监控和管理的信息,参见 参考资料。)

4、 跟踪统计

发现应用程序代码中性能问题的常用响应多种多样,但也是可预测的。早期的 Java 编程人员对旧的 IDE 可能十分生气,并开始进行代码库中主要部分的代码复查,在源代码中寻找熟悉的 “红色标志”,像异步块、对象配额等等。随着编程经验的增加,开发人员可能会仔细研究 JVM 支持的 -X 标志,寻找优化垃圾收集器的方法。当然,对于新手,直接去 Google 查询,希望有其他人发现了 JVM 的神奇的 “make it go fast” 转换,避免重写代码。

从本质上来说,这些方法没什么错,但都是有风险的。对于一个性能问题最有效的响应就是使用一个分析器 — 现在它们内置在 Java 平台 ,我们确实没有理由不这样做!

JConsole 有许多对收集统计数据有用的选项卡,包括: ? Memory:在 JVM 垃圾收集器中针对各个堆跟踪活动。

? Threads:在目标 JVM 中检查当前线程活动。 ? Classes:观察 VM 已加载类的总数。

这些选项卡(和相关的图表)都是由每个 Java 5 及更高版本 VM 在 JMX 服务器上注册的 JMX 对象提供的,是内置到 JVM 的。一个给定 JVM 中可用 bean 的完整清单在 MBeans 选项卡上列出,包括一些元数据和一个有限的用户界面来查看数据或执行操作。(然而,注册通知是在 JConsole 用户界面之外。)

使用统计数据

假设一个 Tomcat 进程死于 OutOfMemoryError。如果您想要弄清楚发生了什么,打开 JConsole,单击 Classes 选项卡,过一段时间查看一次类计数。如果数量稳定上升,您可以假设应用程序服务器或者您的代码某个地方有一个 ClassLoader 漏洞,不久之后将耗尽 PermGen 空间。如果需要更进一步的确认问题,请看 Memory 选项卡。

5、 为离线分析创建一个堆转储

生产环境中一切都在快速地进行着,您可能没有时间花费在您的应用程序分析器上,相反地,您可以为 Java 环境中的每个事件照一个快照保存下来过后再看。在 JConsole 中您也可以这样做,在 VisualVM 中甚至会做得更好。

先找到 MBeans 选项卡,在其中打开 com.sun.management 节点,接着是 HotSpotDiagnostic 节点。现在,选择 Operations,注意右边面板中的 “dumpHeap” 按钮。如果您在第一个(“字符串”)输入框中向 dumpHeap 传递一个文件名来转储,它将为整个 JVM 堆照一个快照,并将其转储到那个文件。

稍后,您可以使用各种不同的商业分析器来分析文件,或者使用 VisualVM 分析快照。(记住,VisualVM 是在 Java 6 中可用的,且是单独下载的。)

作为一个分析器实用工具,JConsole 是极好的,但是还有更好的工具。一些分析插件附带分析器或者灵巧的用户界面,默认情况下比 JConsole 跟踪更多的数据。

JConsole 真正吸引人的是整个程序是用 “普通旧式 Java ” 编写的,这意味着任何 Java 开发人员都可以编写这样一个实用工具。事实上,JDK 其中甚至包括如何通过创建一个插件来定制 JConsole 的示例(参见 参考资料)。建立在 NetBeans 顶部的 VisualVM 进一步延伸了插件概念。

如果 JConsole(或者 VisualVM,或者其他任何工具)不符合您的需求,或者不能跟踪您想要跟踪的,或者不能按照您的方式跟踪,您可以编写属于自己的工具。如果您觉得 Java 代码很麻烦,Groovy 或 JRuby 或很多其他 JVM 语言都可以帮助您更快完成。

您真正需要的是一个快速而粗糙(quick-and-dirty)的由 JVM 连接的命令行工具,可以以您想要的方式确切地跟踪您感兴趣的数据。

关于 Java 性能监控您不知道的 5 件事,第 2 部分

全功能内置分析器,如 JConsole 和 VisualVM 的成本有时比它们的性能费用还要高 — 尤其是在生产软件上运行的系统中。因此,在聚焦 Java 性能监控的第 2 篇文章中,我将介绍 5 个命令行分析工具,使开发人员仅关注运行的 Java 进程的一个方面。

JDK 包括很多命令行实用程序,可以用于监控和管理 Java 应用程序性能。虽然大多数这类应用程序都被标注为 “实验型”,在技术上不受支持,但是它们很有用。有些甚至是特定用途工具的种子材料,可以使用 JVMTI 或 JDI(参见 参考资料)建立。

1、 jps (sun.tools.jps)

很多命令行工具都要求您识别您希望监控的 Java 进程。这与监控本地操作系统进程、同样需要一个程序识别器的同类工具没有太大区别。

“VMID” 识别器与本地操作系统进程识别器(“pid”)并不总是相同的,这就是我们需要 JDK jps 实用程序的原因。

在 Java 进程中使用 jps

与配置 JDK 的大部分工具及本文中提及的所有工具一样,可执行 jps 通常是一个围绕 Java 类或执行大多数工作的类集的一个薄包装。在 Windows? 环境下,这些工具是 .exe 文件,使用 JNI Invocation API 直接调用上面提及的类;在 UNIX? 环境下,大多数工具是一个 shell 脚本的符号链接,该脚本采用指定的正确类名称开始一个普通启动程序。

如果您希望在 Java 进程中使用 jps(或者任何其他工具)的功能 — Ant 脚本 — 仅在每个工具的 “主” 类上调用 main() 相对容易。为了简化引用,类名称出现在每个工具名称之后的括号内。

jps — 名称反映了在大多数 UNIX 系统上发现的 ps 实用程序 — 告诉我们运行 Java 应用程序的 JVMID。顾名思义,jps 返回指定机器上运行的所有已发现的 Java 进程的 VMID。如果 jps 没有发现进程,并不意味着无法附加或研究 Java 进程,而只是意味着它并未宣传自己的可用性。

如果发现 Java 进程,jps 将列出启用它的命令行。这种区分 Java 进程的方法非常重要,因为只要涉及操作系统,所有的 Java 进程都被统称为 “java”。在大多数情况下,VMID 是值得注意的重要数字。

使用分析器开始

使用分析实用程序开始的最简单方法是使用一个如在 demo/jfc/SwingSet2 中发现的 SwingSet2 演示一样的演示程序。这样就可以避免程序作为背景/监控程序运行时出现挂起的可能性。当您了解工具及其费用后,就可以在实际程序中进行试用。

加载演示应用程序后,运行 jps 并注意返回的 vmid。为了获得更好的效果,采用 -Dcom.sun.management.jmxremote 属性集启动 Java 进程。如果没有使用该设置,部分下列工具收集的部分数据可能不可用。

2、 jstat (sun.tools.jstat)

jstat 实用程序可以用于收集各种各样不同的统计数据。jstat 统计数据被分类到 “选项” 中,这些选项在命令行中被指定作为第一参数。对于 JDK 1.6 来说,您可以通过采用命令 -options 运行 jstat 查看可用的选项清单。清单 1 中显示了部分选项: 清单 1. jstat 选项 -class -compiler -gc -gccapacity -gccause -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -printcompilation 实用程序的 JDK 记录(参见 参考资料)将告诉您清单 1 中每个选项返回的内容,但是其中大多数用于收集垃圾的收集器或者其部件的性能信息。-class 选项显示了加载及未加载的类(使其成为检测应用程序服务器或代码中 ClassLoader 泄露的重要实用程序,且 -compiler 和 -printcompilation 都显示了有关 Hotspot JIT 编译程序的信息。

默认情况下,jstat 在您核对信息时显示信息。如果您希望每隔一定时间拍摄快照,请在 -options 指令后以毫秒为单位指定间隔时间。jstat 将持续显示监控进程信息的快照。如果您希望 jstat 在终止前进行特定数量的快照,在间隔时间/时间值后指定该数字。

如果 5756 是几分钟前开始的运行 SwingSet2 程序的 VMID,那么下列命令将告诉 jstat 每 250 毫秒为 10 个佚代执行一次 gc 快照转储,然后停止:

jstat -gc 5756 250 10

请注意 Sun(现在的 Oracle)保留了在不进行任何预先通知的情况下更改各种选项的

输出甚至是选项本身的权利。这是使用不受支持实用程序的缺点。请参看 Javadocs 了解 jstat 输出中每一列的全部细节。

3、 jstack (sun.tools.jstack)

了解 Java 进程及其对应的执行线程内部发生的情况是一种常见的诊断挑战。例如,当一个应用程序突然停止进程时,很明显出现了资源耗尽,但是仅通过查看代码无法明确知道何处出现资源耗尽,且为什么会发生。

jstack 是一个可以返回在应用程序上运行的各种各样线程的一个完整转储的实用程序,您可以使用它查明问题。

采用期望进程的 VMID 运行 jstack 会产生一个堆转储。就这一点而言,jstack 与在控制台窗口内按 Ctrl-Break 键起同样的作用,在控制台窗口中,Java 进程正在运行或调用 VM 内每个 Thread 对象上的 Thread.getAllStackTraces() 或 Thread.dumpStack()。jstack 调用也转储关于在 VM 内运行的非 Java 线程的信息,这些线程作为 Thread 对象并不总是可用的。

jstack 的 -l 参数提供了一个较长的转储,包括关于每个 Java 线程持有锁的更多详细信息,因此发现(和 squash)死锁或可伸缩性 bug 是极其重要的。

4、 jmap (sun.tools.jmap)

有时,您正在处理的问题是一个对象泄露,如一个 ArrayList (可能持有成千上万个对象)该释放时没有释放。另一个更普遍的问题是,看似从不会压缩的扩展堆,却有活跃的垃圾收集。

当您努力寻找一个对象泄露时,在指定时刻对堆及时进行拍照,然后审查其中内容非常有用。jmap 通过对堆拍摄快照来提供该功能的第一部分。然后您可以采用下一部分中描述的 jhat 实用程序分析堆数据。

与这里描述的其他所有实用程序一样,使用 jmap 非常简单。将 jmap 指向您希望拍快照的 Java 进程的 VMID,然后给予它部分参数,用来描述产生的结果文件。您要传递给 jmap 的选项包括转储文件的名称以及是否使用一个文本文件或二进制文件。二进制文件是最有用的选项,但是只有当与某一种索引工具 结合使用时 — 通过十六进制值的文本手动操作数百兆字节不是最好的方法。

随意看一下 Java 堆的更多信息,jmap 同样支持 -histo 选项。-histo 产生一个对象文本柱状图,现在在堆中大量引用,由特定类型消耗的字节总数分类。它同样给出了特定类型的总示例数量,支持部分原始计算,并猜测每个实例的相对成本。

不幸的是,jmap 没有像 jstat 一样的 period-and-max-count 选项,但是将 jmap(或

jmap.main())调用放入 shell 脚本或其他类的循环,周期性地拍摄快照相对简单。(事实上,这是加入 jmap 的一个好的扩展,不管是作为 OpenJDK 本身的源补丁,还是作为其他实用程序的扩展。)

5、 jhat (com.sun.tools.hat.Main)

将堆转储至一个二进制文件后,您就可以使用 jhat 分析二进制堆转储文件。jhat 创建一个 HTTP/HTML 服务器,该服务器可以在浏览器中被浏览,提供一个关于堆的 object-by-object 视图,及时冻结。根据对象引用草率处理堆可能会非常可笑,您可以通过对总体混乱进行某种自动分析而获得更好的服务。幸运的是,jhat 支持 OQL 语法进行这样的分析。

例如,对所有含有超过 100 个字符的 String 运行 OQL 查询看起来如下: select s from java.lang.String s where s.count >= 100

结果作为对象链接显示,然后展示该对象的完整内容,字段引用作为可以解除引用的其他链接的其他对象。OQL 查询同样可以调用对象的方法,将正则表达式作为查询的一部分,并使用内置查询工具。一种查询工具,referrers() 函数,显示了引用指定类型对象的所有引用。下面是寻找所有参考 File 对象的查询:

select referrers(f) from java.io.File f

您可以查找 OQL 的完整语法及其在 jhat 浏览器环境内 “OQL Help” 页面上的特性。将 jhat 与 OQL 相结合是对行为不当的堆进行对象调查的有效方法。

关于 Java Scripting API 您不知道的 5 件事

现在,许多 Java 开发人员都喜欢在 Java 平台中使用脚本语言,但是使用编译到 Java 字节码中的动态语言有时是不可行的。在某些情况中,直接编写一个 Java 应用程序的脚本 部分 或者在一个脚本中调用特定的 Java 对象是更快捷、更高效的方法。

这就是 javax.script 产生的原因了。Java Scripting API 是从 Java 6 开始引入的,它填补了便捷的小脚本语言和健壮的 Java 生态系统之间的鸿沟。通过使用 Java Scripting API,您就可以在您的 Java 代码中快速整合几乎所有的脚本语言,这使您能够在解决一些很小的问题时有更多可选择的方法。

1、 使用 jrunscript 执行 JavaScript

每一个新的 Java 平台发布都会带来新的命令行工具集,它们位于 JDK 的 bin 目录。Java 6 也一样,其中 jrunscript 便是 Java 平台工具集中的一个不小的补充。

设想一个编写命令行脚本进行性能监控的简单问题。这个工具将借用 jmap(见本系列

文章 前一篇文章 中的介绍),每 5 秒钟运行一个 Java 进程,从而了解进程的运行状况。一般情况下,我们会使用命令行 shell 脚本来完成这样的工作,但是这里的服务器应用程序部署在一些差别很大的平台上,包括 Windows? 和 Linux?。系统管理员将会发现编写能够同时运行在两个平台的 shell 脚本是很痛苦的。通常的做法是编写一个 Windows 批处理文件和一个 UNIX? shell 脚本,同时保证这两个文件同步更新。

但是,任何阅读过 The Pragmatic Programmer 的人都知道,这严重违反了 DRY (Don't Repeat Yourself) 原则,而且会产生许多缺陷和问题。我们真正希望的是编写一种与操作系统无关的脚本,它能够在所有的平台上运行。

当然,Java 语言是平台无关的,但是这里并不是需要使用 “系统” 语言的情况。我们需要的是一种脚本语言 — 如,JavaScript。

清单 1 显示的是我们所需要的简单 shell 脚本: 清单 1. periodic.js while (true) { echo(\} 由于经常与 Web 浏览器打交道,许多 Java 开发人员已经知道了 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 开发的一种 ECMAScript 语言)。问题是,系统管理员要如何运行这个脚本?

当然,解决方法是 JDK 所带的 jrunscript 实用程序,如清单 2 所示: 清单 2. jrunscript C:\\developerWorks\\5things-scripting\\code\\jssrc>jrunscript periodic.js Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! ... 注意,您也可以使用 for 循环按照指定的次数来循环执行这个脚本,然后才退出。基本上,jrunscript 能够让您执行 JavaScript 的所有操作。惟一不同的是它的运行环境不是浏览器,所以运行中不会有 DOM。因此,最顶层的函数和对象稍微有些不同。

因为 Java 6 将 Rhino ECMAScript 引擎作为 JDK 的一部分,jrunscript 可以执行任何传

递给它的 ECMAScript 代码,不管是一个文件(如此处所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 环境。运行 jrunscript 就可以访问 REPL shell。

2、 从脚本访问 Java 对象

能够编写 JavaScript/ECMAScript 代码是非常好的,但是我们不希望被迫重新编译我们在 Java 语言中使用的所有代码 — 这是违背我们初衷的。幸好,所有使用 Java Scripting API 引擎的代码都完全能够访问整个 Java 生态系统,因为本质上一切代码都还是 Java 字节码。所以,回到我们之前的问题,我们可以在 Java 平台上使用传统的 Runtime.exec() 调用来启动进程,如清单 3 所示:

清单 3. Runtime.exec() 启动 jmap

var p = java.lang.Runtime.getRuntime().exec(\p.waitFor() 数组 arguments 是指向传递到这个函数参数的 ECMAScript 标准内置引用。在最顶层的脚本环境中,则是传递给脚本本身的的参数数组(命令行参数)。所以,在清单 3 中,这个脚本预期接收一个参数,该参数包含要映射的 Java 进程的 VMID。

除此之外,我们可以利用本身为一个 Java 类的 jmap,然后直接调用它的 main() 方法,如清单 4 所示。有了这个方法,我们不需要 “传输” Process 对象的 in/out/err 流。 清单 4. JMap.main()

var args = [ \Packages.sun.tools.jmap.JMap.main(args) Packages 语法是一个 Rhino ECMAScript 标识,它指向已经 Rhino 内创建的位于核心 java.* 包之外的 Java 包。

3、 从 Java 代码调用脚本

从脚本调用 Java 对象仅仅完成了一半的工作:Java 脚本环境也提供了从 Java 代码调用脚本的功能。这只需要实例化一个 ScriptEngine 对象,然后加载和评估脚本,如清单 5 所示:

清单 5. Java 平台的脚本调用

import java.io.*; import javax.script.*; public class App { public static void main(String[] args) { try { ScriptEngine engine = new ScriptEngineManager().getEngineByName(\ for (String arg : args) { FileReader fr = new FileReader(arg); engine.eval(fr); } } catch(IOException ioEx) { ioEx.printStackTrace(); } catch(ScriptException scrEx) { scrEx.printStackTrace(); } } } eval() 方法也可以直接操作一个 String,所以这个脚本不一定必须是文件系统的一个文件 — 它可以来自于数据库、用户输入,或者甚至可以基于环境和用户操作在应用程序中生成。

4、 将 Java 对象绑定到脚本空间

仅仅调用一个脚本还不够:脚本通常会与 Java 环境中创建的对象进行交互。这时,Java 主机环境必须创建一些对象并将它们绑定,这样脚本就可以很容易找到和使用这些对象。这个过程是 ScriptContext 对象的任务,如清单 6 所示: 清单 6. 为脚本绑定对象 import java.io.*; import javax.script.*; public class App { public static void main(String[] args) { try { ScriptEngine engine = new ScriptEngineManager().getEngineByName(\ for (String arg : args) { Bindings bindings = new SimpleBindings(); bindings.put(\ bindings.put(\ FileReader fr = new FileReader(arg); engine.eval(fr, bindings); } } catch(IOException ioEx) { ioEx.printStackTrace(); } catch(ScriptException scrEx) { scrEx.printStackTrace(); } } } 访问所绑定的对象很简单 — 所绑定对象的名称是作为全局命名空间引入到脚本的,所以在 Rhino 中使用 Person 很简单,如清单 7 所示: 清单 7. 是谁撰写了本文?

println(\ println(\您可以看到,JavaBeans 样式的属性被简化为使用名称直接访问,这就好像它们是字段一样。

5、 编译频繁使用的脚本

脚本语言的缺点一直存在于性能方面。其中的原因是,大多数情况下脚本语言是 “即时” 解译的,因而它在执行时会损失一些解析和验证文本的时间和 CPU 周期。运行在 JVM 的许多脚本语言最终会将接收的代码转换为 Java 字节码,至少在脚本被第一次解析和验证时进行转换;在 Java 程序关闭时,这些即时编译的代码会消失。将频繁使用的脚本保持为字节码形式可以帮助提升可观的性能。

我们可以以一种很自然和有意义的方法使用 Java Scripting API。如果返回的 ScriptEngine 实现了 Compilable 接口,那么这个接口所编译的方法可用于将脚本(以一个 String 或一个 Reader 传递过来的)编译为一个 CompiledScript 实例,然后它可用于在 eval() 方法中使用不同的绑定重复地处理编译后的代码,如清单 8 所示: 清单 8. 编译解译后的代码 import java.io.*; import javax.script.*; public class App { public static void main(String[] args) { try { ScriptEngine engine = new ScriptEngineManager().getEngineByName(\ for (String arg : args) { Bindings bindings = new SimpleBindings(); bindings.put(\ bindings.put(\ FileReader fr = new FileReader(arg); if (engine instanceof Compilable) { System.out.println(\ Compilable compEngine = (Compilable)engine; CompiledScript cs = compEngine.compile(fr); cs.eval(bindings); } else engine.eval(fr, bindings); } } catch(IOException ioEx) { ioEx.printStackTrace(); } catch(ScriptException scrEx) { scrEx.printStackTrace(); } } } 在大多数情况中,CompiledScript 实例需要存储在一个长时间存储中(例如,servlet-context),这样才能避免一次次地重复编译相同的脚本。然而,如果脚本发生变化,您就需要创建一个新的 CompiledScript 来反映这个变化;一旦编译完成,CompiledScript 就

不再执行原始的脚本文件内容。

Java 异常处理及其应用

1、 Java 异常处理引出

假设您要编写一个 Java 程序,该程序读入用户输入的一行文本,并在终端显示该文本。 程序如下:

1 import java.io.*; 2 public class EchoInput { 3 public static void main(String args[]){ 4 System.out.println(\5 InputStreamReader isr = new InputStreamReader(System.in); 6 BufferedReader inputReader = new BufferedReader(isr); 7 String inputLine = inputReader.readLine(); 8 System.out.println(\9 } 10 } 分析上面的代码,在 EchoInput 类中,第 3 行声明了 main 方法;第 4 行提示用户输入文本;第 5、6 行设置 BufferedReader 对像连接到 InputStreamReader,而 InputStreamReader 又连接到标准输入流 System.in;第 7 行读入一行文本;第 8 行用标准输出流 System.out 显示出该文本。

表面看来上面的程序没有问题,但实际上,EchoInput 类完全可能出现问题。要在调用第 7 行的 readLine 方法时正确读取输入,这几种假设都必须成立:假定键盘有效,键盘能与计算机正常通信;假定键盘数据可从操作系统传输到 Java 虚拟机,又从 Java 虚拟机传输 inputReader。

大多数情况下上述假设都成立,但不尽然。为此,Java 采用异常方法,以应对可能出现的错误,并采取步骤进行更正。在本例中,若试图编译以上代码,将看到以下信息: Exception in thread \ Unhandled exception type IOException at EchoInput.main(EchoInput.java:7) 从中可以看到,第 7 行调用 readLine 方法可能出错:若果真如此,则产生 IOException 来记录故障。编译器错误是在告诉您,需要更改代码来解决这个潜在的问题。在 JDK API 文档中,可以看到同样的信息。我们可以看到 readLine 方法,如图 1 所示。

图 1. BufferedReader 类的 readLine 方法的 JDK API 文档

由图 1 可知,readLine 方法有时产生 IOException。如何处理潜在的故障?编译器需要“捕获”或“声明”IOException。

“捕获 (catch)”指当 readLine 方法产生错误时截获该错误,并处理和记录该问题。而“声明 (declare)”指错误可能引发 IOException,并通知调用该方法的任何代码:可能产生异常。

若要捕获异常,必须添加一个特殊的“处理代码块”,来接收和处理 IOException。于是程序改为如下:

1 import java.io.*; 2 public class EchoInputHandle { 3 public static void main(String args[]){ 4 System.out.println(\5 InputStreamReader isr = new InputStreamReader(System.in); 6 BufferedReader inputReader = new BufferedReader(isr); 7 try{ 8 String inputLine = inputReader.readLine(); 9 System.out.println(\10 } 11 catch(IOException exc){ 12 System.out.println(“Exception encountered: ” + exc); 13 } 14 } 15 } 新添的代码块包含关键字 try 和 catch(第 7,10,11,13 行),表示要读取输入。若成功,则正常运行。若读取输入时错误,则捕获问题(由 IOException 对象表示),并采取相应措施。在本例,采用的处理方式是输出异常。

若不准备捕获 IOException,仅声明异常,则要特别指定 main 方法可能出错,而且特别说明可能产生 IOException。于是程序改为如下: 1 import java.io.*; 2 public class EchoInputDeclare { 3 public static void main(String args[]) throws IOException{ 4 System.out.println(\5 InputStreamReader isr = new InputStreamReader(System.in); 6 BufferedReader inputReader = new BufferedReader(isr); 7 String inputLine = inputReader.readLine(); 8 System.out.println(\9 } 10 } 从上面的这个简单的例子中,我们可以看出异常处理在 Java 代码开发中不能被忽视。 Java 异常以及异常处理

可将 Java 异常看作是一类消息,它传送一些系统问题、故障及未按规定执行的动作的相关信息。异常包含信息,以将信息从应用程序的一部分发送到另一部分。

编译语言为何要处理异常?为何不在异常出现位置随时处理具体故障?因为有时候我们需要在系统中交流错误消息,以便按照统一的方式处理问题,有时是因为有若干处理问题的可能方式,但您不知道使用哪一种,此时,可将处理异常的任务委托给调用方法的代码。调用者通常更能了解问题来源的上下文,能更好的确定恢复方式。

图 2 是一个通用消息架构。 图 2. 通用消息架构

从上图可以看出,必定在运行的 Java 应用程序的一些类或对象中产生异常。出现故障时,“发送者”将产生异常对象。异常可能代表 Java 代码出现的问题,也可能是 JVM 的相应错误,或基础硬件或操作系统的错误。

异常本身表示消息,指发送者传给接收者的数据“负荷”。首先,异常基于类的类型来

传输有用信息。很多情况下,基于异常的类既能识别故障本因并能更正问题。其次,异常还带有可能有用的数据(如属性)。

在处理异常时,消息必须有接收者;否则将无法处理产生异常的底层问题。

在上例中,异常“产生者”是读取文本行的 BufferedReader。在故障出现时,将在 readLine 方法中构建 IOException 对象。异常“接收者”是代码本身。EchoInputHandle 应用程序的 try-catch 结构中的 catch 块是异常的接收者,它以字符串形式输出异常,将问题记录下来。

2、 Java 异常类的层次结构

在我们从总体上了解异常后,我们应该了解如何在 Java 应用程序中使用异常,即需要了解 Java 类的层次结构。图 3 是 Java 类的层次结构图。 图 3. Java 类的层次结构

在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。

Throwable 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。

Exception(异常)是应用程序中可能的可预测、可恢复问题。一般大多数异常表示中度到轻度的问题。异常一般是在特定环境下产生的,通常出现在代码的特定方法和操作中。在 EchoInput 类中,当试图调用 readLine 方法时,可能出现 IOException 异常。

Error(错误)表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。

Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。

3、 Java 异常的处理

在 Java 应用程序中,对异常的处理有两种方式:处理异常和声明异常。 处理异常:try、catch 和 finally

若要捕获异常,则必须在代码中添加异常处理器块。这种 Java 结构可能包含 3 个部分,

都有 Java 关键字。下面的例子中使用了 try-catch-finally 代码结构。 1 import java.io.*; 2 public class EchoInputTryCatchFinally { 3 public static void main(String args[]){ 4 System.out.println(\5 InputStreamReader isr = new InputStreamReader(System.in); 6 BufferedReader inputReader = new BufferedReader(isr); 7 try{ 8 String inputLine = inputReader.readLine(); 9 System.out.println(\10 } 11 catch(IOException exc){ 12 System.out.println(\13 } 14 finally{ 15 System.out.println(\16 } 17 } 18} 其中:

? try 块:将一个或者多个语句放入 try 时,则表示这些语句可能抛出异常。编译器

知道可能要发生异常,于是用一个特殊结构评估块内所有语句。

? catch 块:当问题出现时,一种选择是定义代码块来处理问题,catch 块的目的便

在于此。catch 块是 try 块所产生异常的接收者。基本原理是:一旦生成异常,则 try 块的执行中止,JVM 将查找相应的 JVM。

? finally 块:还可以定义 finally 块,无论运行 try 块代码的结果如何,该块里面的

代码一定运行。在常见的所有环境中,finally 块都将运行。无论 try 块是否运行

完,无论是否产生异常,也无论是否在 catch 块中得到处理,finally 块都将执行。 try-catch-finally 规则:

? 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,

但至少有一个块。

? 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try

块之后。

? catch 块与相应的异常类的类型相关。

? 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。 ? 可嵌套 try-catch-finally 结构。

? 在 try-catch-finally 结构中,可重新抛出异常。

? 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用 System.exit(int));

在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。 声明异常

若要声明异常,则必须将其添加到方法签名块的结束位置。下面是一个实例: public void errorProneMethod(int input) throws java.io.IOException { //Code for the method,including one or more method //calls that may produce an IOException } 这样,声明的异常将传给方法调用者,而且也通知了编译器:该方法的任何调用者必须遵守处理或声明规则。声明异常的规则如下:

? 必须声明方法可抛出的任何可检测异常(checked exception)。

? 非检测性异常(unchecked exception)不是必须的,可声明,也可不声明。 ? 调用方法必须遵循任何可检测异常的处理和声明规则。若覆盖一个方法,则不能声

明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。

4、 Java 异常处理的分类

Java 异常可分为可检测异常,非检测异常和自定义异常。 可检测异常

可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,例如:sqlExecption 这个异常就是一个检测异常。你连接 JDBC 时,不捕捉这个异

常,编译器就通不过,不允许编译。

非检测异常

非检测异常不遵循处理或声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已解决了这样一个异常。例如:一个数组为 3 个长度,当你使用下标为3时,就会产生数组下标越界异常。这个异常 JVM 不会进行检测,要靠程序员来判断。有两个主要类定义非检测异常:RuntimeException 和 Error。

Error 子类属于非检测异常,因为无法预知它们的产生时间。若 Java 应用程序内存不足,则随时可能出现 OutOfMemoryError;起因一般不是应用程序的特殊调用,而是 JVM 自身的问题。另外,Error 一般表示应用程序无法解决的严重问题。

RuntimeException 类也属于非检测异常,因为普通 JVM 操作引发的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在 Java 应用程序中会频繁出现。因此,它们不受编译器检查与处理或声明规则的限制。

自定义异常

自定义异常是为了表示应用程序的一些错误类型,为代码可能发生的一个或多个问题提供新含义。可以显示代码多个位置之间的错误的相似性,也可以区分代码运行时可能出现的相似问题的一个或者多个错误,或给出应用程序中一组错误的特定含义。例如,对队列进行操作时,有可能出现两种情况:空队列时试图删除一个元素;满队列时试图添加一个元素。则需要自定义两个异常来处理这两种情况。

5、 Java 异常处理的原则和忌讳

Java 异常处理的原则

1 尽可能的处理异常

要尽可能的处理异常,如果条件确实不允许,无法在自己的代码中完成处理,就考虑声明异常。如果人为避免在代码中处理异常,仅作声明,则是一种错误和依赖的实践。

2 具体问题具体解决

异常的部分优点在于能为不同类型的问题提供不同的处理操作。有效异常处理的关键是识别特定故障场景,并开发解决此场景的特定相应行为。为了充分利用异常处理能力,需要为特定类型的问题构建特定的处理器块。

3 记录可能影响应用程序运行的异常

至少要采取一些永久的方式,记录下可能影响应用程序操作的异常。理想情况下,当然是在第一时间解决引发异常的基本问题。不过,无论采用哪种处理操作,一般总应记录下潜

在的关键问题。别看这个操作很简单,但它可以帮助您用很少的时间来跟踪应用程序中复杂问题的起因。

4 根据情形将异常转化为业务上下文

若要通知一个应用程序特有的问题,有必要将应用程序转换为不同形式。若用业务特定状态表示异常,则代码更易维护。从某种意义上讲,无论何时将异常传到不同上下文(即另一技术层),都应将异常转换为对新上下文有意义的形式。 Java 异常处理的忌讳

1 一般不要忽略异常

在异常处理块中,一项最危险的举动是“不加通告”地处理异常。如下例所示: 1 try{ 2 Class.forName(\3 } 4 catch (ClassNotFoundException exc){} 经常能够在代码块中看到类似的代码块。有人总喜欢在编写代码时简单快速地编写空处理器块,并“自我安慰地”宣称准备在“后期”添加恢复代码,但这个“后期”变成了“无期”。

这种做法有什么坏处?如果异常对应用程序的其他部分确实没有任何负面影响,这未尝不可。但事实往往并非如此,异常会扰乱应用程序的状态。此时,这样的代码无异于掩耳盗铃。

这种做法若影响较轻,则应用程序可能出现怪异行为。例如,应用程序设置的一个值不见了, 或 GUI 失效。若问题严重,则应用程序可能会出现重大问题,因为异常未记录原始故障点,难以处理,如重复的 NullPointerExceptions。

如果采取措施,记录了捕获的异常,则不可能遇到这个问题。实际上,除非确认异常对代码其余部分绝无影响,至少也要作记录。进一步讲,永远不要忽略问题;否则,风险很大,在后期会引发难以预料的后果。

2 不要使用覆盖式异常处理块

另一个危险的处理是覆盖式处理器(blanket handler)。该代码的基本结构如下: 1 try{ 2 // ? 3 } 4 catch(Exception e){ 5 // ? 6 } 使用覆盖式异常处理块有两个前提之一:

1 代码中只有一类问题。

这可能正确,但即便如此,也不应使用覆盖式异常处理,捕获更具体的异常形式有利物弊。

2 单个恢复操作始终适用。

这几乎绝对错误。几乎没有哪个方法能放之四海而皆准,能应对出现的任何问题。 分析下这样编写代码将发生的情况。只要方法不断抛出预期的异常集,则一切正常。但是,如果抛出了未预料到的异常,则无法看到要采取的操作。当覆盖式处理器对新异常类执行千篇一律的任务时,只能间接看到异常的处理结果。如果代码没有打印或记录语句,则根本看不到结果。

更糟糕的是,当代码发生变化时,覆盖式处理器将继续作用于所有新异常类型,并以相同方式处理所有类型。

一般不要把特定的异常转化为更通用的异常

将特定的异常转换为更通用异常时一种错误做法。一般而言,这将取消异常起初抛出时产生的上下文,在将异常传到系统的其他位置时,将更难处理。见下例:

1 try{ 2 // Error-prone code 3 } 4 catch(IOException e){ 5 String msg = \ 6 throw new Exception(msg); 7 } 因为没有原始异常的信息,所以处理器块无法确定问题的起因,也不知道如何更正问题。 不要处理能够避免的异常

对于有些异常类型,实际上根本不必处理。通常运行时异常属于此类范畴。在处理空指针或者数据索引等问题时,不必求助于异常处理。

6、 Java 异常处理的应用实例

在定义银行类时,若取钱数大于余额时需要做异常处理。

定义一个异常类 insufficientFundsException。取钱(withdrawal)方法中可能产生异常,条件是余额小于取额。

处理异常在调用 withdrawal 的时候,因此 withdrawal 方法要声明抛出异常,由上一

级方法调用。 异常类:

class InsufficientFundsExceptionextends Exception{ private Bank excepbank; // 银行对象 private double excepAmount; // 要取的钱 InsufficientFundsException(Bank ba, double dAmount) { excepbank=ba; excepAmount=dAmount; } public String excepMessage(){ String str=\ + \ return str; } }// 异常类 银行类:

class Bank{ double balance;// 存款数 Bank(double balance){this.balance=balance;} public void deposite(double dAmount){ if(dAmount>0.0) balance+=dAmount; } public void withdrawal(double dAmount) throws InsufficientFundsException{ if (balance

public class ExceptionDemo{ public static void main(String args[]){ try{ Bank ba=new Bank(50); ba.withdrawal(100); System.out.println(\ }catch(InsufficientFundsException e) { System.out.println(e.toString()); System.out.println(e.excepMessage()); } } } 关于 JVM 命令行标志您不知道的 5 件事

1、 DisableExplicitGC

我已记不清有多少次用户要求我就应用程序性能问题提供咨询了,其实只要跨代码快速运行 grep,就会发现清单 1 所示的问题 — 原始 java 性能反模式: 清单 1. System.gc(); // We just released a bunch of objects, so tell the stupid // garbage collector to collect them already! System.gc(); 显式垃圾收集是一个非常糟糕的主意 — 就像将您和一个疯狂的斗牛犬锁在一个电话亭里。尽管调用的语法是依赖实现的,但如果您的 JVM 正在运行一个分代的垃圾回收器(大多数是)System.gc(); 强迫 VM 执行一个堆的 “全部清扫”,虽然有的没有必要。全部清扫比一个常规 GC 操作要昂贵好几个数量级,这只是个简单数学问题。

您可以不把我的话放在心上 — Sun 的工程师为这个特殊的人工错误提供一个 JVM 标志; -XX:+DisableExplicitGC 标志自动将 System.gc() 调用转换成一个空操作,为您提供运行代码的机会,您自己看看 System.gc() 对于整个 JVM 执行有害还是有利。

2、 HeapDumpOnOutOfMemoryError

您有没有经历过这样的情况:JVM 不能使用,不断抛出 OutOfMemoryError,而您又不能为自己创建调试器来捕获它或查看出现了什么问题?像这类偶发和/或不确定的问题,通常使开发人员发疯。

买者自负

并不是任何 VM 都支持所有命令行标志,Sun/Oracle 的 VM 除外。查明一个标志是否被支持的最好方法是试用它,看它是否正常工作。倘若这些标志在技术上是不支持的,那么,使用它们您要承担全部责任。如果这些标志中的任何一个使您的代码、您的数据、您的服务器或您的一切消失得无影无踪,我、Sun/Oracle 和 IBM? 都将不负责任。为以防万一,建议先在虚拟(非常生产)环境中实验。

在这个时刻您想要的是,在 JVM 消亡之际捕获堆的一个快照 — 正好 -XX:+HeapDumpOnOutOfMemoryError 命令可以完成这一操作。

本文来源:https://www.bwwdw.com/article/6slw.html

Top