在《Effective Java》中,Joshua Bloch 写了 9 个关于如何在 Java 中处理异常的技巧。这些技巧已经成为 Java 异常处理的事实上的标准。在这篇文章中,我列出了一些开源项目中Java异常处理的一些例子,并按照异常处理的9个技巧来评论用法。
Java异常处理的9个技巧是:
1. 仅在异常情况下使用异常 2. 对可恢复条件使用检查异常,对编程错误使用运行时异常 3. 避免不必要的使用受检异常 4. 赞成使用标准异常 5. 抛出适合抽象的异常 6. 记录每个方法抛出的所有异常 7. 在详细消息中包含故障捕获信息 8. 力求故障原子性 9. 不要忽视异常
1. 仅在异常情况下使用异常
此项主要是避免对普通控制流使用异常。
例如,不是使用异常来终止循环控制流:
try{
Iterator<Foo> iter = ...;
while(true) {
Foo foo = i.next();
...
}
} catch (NoSuchElementException e){
}
应该使用对集合的常规迭代:
for(Iterator<Foo> iter = ...; i.hasNext();){
Foo foo = i.next();
...
}
我没有找到任何使用常规控制流异常的示例。
2. 对可恢复条件使用检查异常,对编程错误使用运行时异常
大多数情况下,如果调用者可以恢复异常,则应使用已检查的异常。如果不是,则应使用运行时异常。运行时异常表示可以通过检查某些先决条件(例如数组边界和空性检查)来防止的编程错误。
在下面的方法中,IllegalArgumentException
是一个 RuntimeException
,它的用法表示编程错误。通常可以通过检查前提条件来避免编程错误。所以这是基于这个技巧的一个不好的例子。可以通过检查先决条件来避免异常,即这里的“hasNext()
”方法。
/**
* Convert a tag string into a tag map.
*
* @param tagString a space-delimited string of key-value pairs. For example, {@code "key1=value1 key_n=value_n"}
* @return a tag {@link Map}
* @throws IllegalArgumentException if the tag string is corrupted.
*/
public static Map<String, String> parseTags(final String tagString) throws IllegalArgumentException {
// delimit by whitespace or '='
Scanner scanner = new Scanner(tagString).useDelimiter("\\s+|=");
Map<String, String> tagMap = new HashMap<String, String>();
try {
while (scanner.hasNext()) {
String tagName = scanner.next();
String tagValue = scanner.next();
tagMap.put(tagName, tagValue);
}
} catch (NoSuchElementException e) {
// The tag string is corrupted.
throw new IllegalArgumentException("Invalid tag string '" + tagString + "'");
} finally {
scanner.close();
}
return tagMap;
}
3. 避免不必要的使用受检异常
检查异常强制调用者处理异常情况,因为如果没有,编译器会抱怨。过度使用检查异常会给调用者带来处理异常情况的负担。所以必要时应该使用受检异常。使用受检异常的经验法则是,当无法通过检查前提条件避免异常时,调用者可以采取一些有用的操作来处理异常。
常用的运行时异常本身就是不要过度使用检查异常的例子。在常见的运行时异常有:ArithmeticException
,ClassCastException
异常,抛出:IllegalArgumentException
,IllegalStateException
异常,IndexOutOfBoundExceptions
,NoSuchElementException
异常,和NullPointerException
异常。
在下面的方法中,当propertyName
不是目标情况之一时,调用者可以做的事情不多,因此抛出运行时异常。
@Override
public Object get(String propertyName) {
switch (propertyName.hashCode()) {
case 842855857: // marketDataName
return marketDataName;
case -1169106440: // parameterMetadata
return parameterMetadata;
case 106006350: // order
return order;
case 575402001: // currency
return currency;
case 564403871: // sensitivity
return sensitivity;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
}
4. 赞成使用标准异常
最常重用的 Java 异常类如下:
1.java.io.IO异常 2.java.io.FileNotFoundException 3.java.io.UnsupportedEncodingException 4. java.lang.reflect.InvocationTargetException 5.java.security.NoSuchAlgorithmException 6.java.net.MalformedURLException 7.java.text.ParseException 8. java.net.URISyntaxException 9. java.util.concurrent.ExecutionException 10. java.net.UnknownHostException
前 10 名中没有一个是书中显示的最常用的。但是要注意,这些是按项目计算的,即如果一个类在一个项目中使用,无论项目中有多少方法在使用它,它都只计算一次。所以这是按项目数计算,但按代码中出现的次数计算。
5. 抛出适合抽象的异常
抛出的异常应该与调用者执行的任务有联系。此项介绍异常转换(捕获异常并抛出另一个)和异常链(将异常包装在新的异常中以保留异常的因果链)。
private void serializeBillingDetails(BillingResult billingResult,
BillingDetailsType billingDetails) {
try {
final JAXBContext context = JAXBContext
.newInstance(BillingdataType.class);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final Marshaller marshaller = context.createMarshaller();
marshaller.setProperty("jaxb.formatted.output", Boolean.FALSE);
final BillingdataType billingdataType = new BillingdataType();
billingdataType.getBillingDetails().add(billingDetails);
marshaller.marshal(factory.createBillingdata(billingdataType), out);
final String xml = new String(out.toByteArray(), "UTF-8");
billingResult.setResultXML(xml.substring(
xml.indexOf("<Billingdata>") + 13,
xml.indexOf("</Billingdata>")).trim());
billingResult.setGrossAmount(billingDetails.getOverallCosts()
.getGrossAmount());
billingResult.setNetAmount(billingDetails.getOverallCosts()
.getNetAmount());
} catch (JAXBException | UnsupportedEncodingException ex) {
throw new BillingRunFailed(ex);
}
}
上述方法捕获 JAXBException
和 UnsupportedEncodingException
,并重新抛出一个适合方法抽象级别的新异常。新的 BillingRunFailed
异常包装了原始异常。所以这是异常链的一个很好的例子。异常链的好处是保留有助于调试问题的低级异常。
6. 记录每个方法抛出的所有异常
这是严重使用不足。大多数公共 API 都没有 @throws Java 文档来解释抛出的异常。
这是一个很好的例子。
...
*
* @throws MalformedURLException The formal system identifier of a
* subordinate catalog cannot be turned into a valid URL.
* @throws IOException Error reading subordinate catalog file.
*/
public String resolveSystem(String systemId)
throws MalformedURLException, IOException {
...
这是一个缺乏有关在什么情况下抛出异常的信息的坏例子。
* @throws Exception exception
*/
public void startServer() throws Exception {
if (!externalDatabaseHost) {
7. 在详细消息中包含故障捕获信息
private OutputStream openOutputStream(File file) throws IOException {
if (file.exists()) {
if (file.isDirectory()) {
throw new IOException("File '" + file + "' exists but is a directory");
}
if (!file.canWrite()) {
throw new IOException("File '" + file + "' cannot be written to");
}
} else {
final File parent = file.getParentFile();
if (parent != null) {
if (!parent.mkdirs() && !parent.isDirectory()) {
throw new IOException("Directory '" + parent + "' could not be created");
}
}
}
return new FileOutputStream(file, false);
}
在该方法中,IOException
使用不同的字符串来传递不同的故障捕获信息。
8.力求故障原子性
第 8 项是关于失败的。一般规则是失败的方法不应该改变方法中对象的状态。为了尽早失败,一种方法是在执行操作之前检查参数的有效性。以下是遵循此提示的一个很好的示例。
/**
* Assigns a new int value to location index of the buffer instance.
* @param index int
* @param newValue int
*/
public void modifyEntry(int index, int newValue) {
if (index < 0 || index > size - 1) {
throw new IndexOutOfBoundsException();
}
// ((int[]) bufferArrayList.get((int) (index / pageSize)))[index % pageSize] =
((int[]) bufferArrayList.get((index >> exp)))[index & r] =
newValue;
}
9. 不要忽视异常
public static Bundle decodeUrl(String s) {
Bundle params = new Bundle();
if (s != null) {
String array[] = s.split("&");
for (String parameter : array) {
String v[] = parameter.split("=");
try {
params.putString(URLDecoder.decode(v[0], "UTF-8"), URLDecoder.decode(v[1], "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
return params;
}
在生产代码中几乎总是应该避免打印堆栈跟踪。这与忽略异常一样糟糕。这将写入标准错误流,这不是日志使用日志记录框架的地方。