合理运用异常机制,能够显著提升代码的健壮性,确保程序在面对各种意外情况时仍能保持稳定运行。
我们一起来看下这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();
...
}
换句话说,不要故意写异常,该检查的时候先检查,比如必要的空值检查,不要出现NullPointerException。
2. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
在大多数情况下,如果调用者能够恢复异常,则应使用受检异常。否则,应使用运行时异常。
运行时异常表示可通过检查某些前置条件(如数组边界和空值检查)来避免的编程错误。
在以下方法中,IllegalArgumentException是一个运行时异常,其用法表明存在编程错误。
通常可以通过检查前置条件来避免此类错误,例如在此处检查hasNext()方法。
/**
* 将标签字符串转换为标签映射。
*
* @param tagString 以空格分隔的键值对字符串。例如,{@code "key1=value1 key_n=value_n"}
* @return 标签{@link Map}
* @throws IllegalArgumentException 如果标签字符串已损坏。
*/
public static Map<String, String> parseTags(final String tagString) throws IllegalArgumentException {
// 按空格或'='分隔
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) {
// 标签字符串已损坏。
throw new IllegalArgumentException("无效的标签字符串 '" + 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 1:
return marketDataName;
case 2:
return parameterMetadata;
case 3:
return order;
case 4:
return currency;
case 5:
return sensitivity;
default:
throw new NoSuchElementException("未知属性: " + propertyName);
}
}
4. 优先使用标准异常
常用的异常包括:
- java.io.IOException
- java.io.FileNotFoundException
- java.io.UnsupportedEncodingException
- java.lang.reflect.InvocationTargetException
- java.security.NoSuchAlgorithmException
- java.net.MalformedURLException
- java.text.ParseException
- java.net.URISyntaxException
- java.util.concurrent.ExecutionException
- java.net.UnknownHostException
标准异常是JDK提供给我们的小宝藏,根据名字我们就能够知道异常原因,而且,大家共用一套异常,也便于沟通。
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异常包装了原始异常。异常链接的好处是保留了有助于调试问题的低级异常。
建议很多新手、老手听一下这条建议。异常转换是为了返回的异常更容易理解,明确异常本质;但是转换后不要丢弃了原始异常,在Debug或排错的时候,如果丢失了原始异常,很容易懵~~
6. 为每个方法抛出的所有异常编写文档
这一点被严重忽视。大多数公共API都缺少@throws Java文档来解释所抛出的异常。
...
*
* @throws MalformedURLException 下级目录的正式系统标识符无法转换为有效URL。
* @throws IOException 读取下级目录文件时出错。
*/
public String resolveSystem(String systemId)
throws MalformedURLException, IOException {
...
下面这个是缺少关于在何种情况下抛出异常信息的坏示例。
* @throws 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 + "' 已存在但为目录");
}
if (!file.canWrite()) {
throw new IOException("文件 '" + file + "' 不可写");
}
} else {
final File parent = file.getParentFile();
if (parent!= null) {
if (!parent.mkdirs() &&!parent.isDirectory()) {
throw new IOException("目录 '" + parent + "' 无法创建");
}
}
}
return new FileOutputStream(file, false);
}
在此方法中,IOException使用不同的字符串来传递不同的故障捕获信息。
这条建议同样适用于日志或接口异常信息,看过很多接口返回的是“服务异常,请稍后再试”,返回了一句没有太多帮助的信息。
理性的说,如果返回错误,那就是有异常了。很多时候,应该包含一些有用的信息,比如,缺少必填参数xxx。
8. 力求故障原子性
这条建议关于失败的。
一般来说,失败的方法不应更改方法中对象的状态。
为了尽早失败,一种方法是在执行操作之前检查参数的有效性,若无效则立即抛出异常,避免执行可能导致状态改变的操作。。比如:
/**
* 将新的整数值分配给缓冲区实例的位置索引。
* @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. 不要忽略异常
不要空 catch 异常块,应根据异常的性质进行适当处理,如记录日志、提供友好的错误提示给用户、进行错误恢复操作或重新抛出更合适的异常等。
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;
}
该说不说,printStackTrace方法和空catch一样差劲。