Java异常处理最佳实践

时间:2020-01-09 10:34:58  来源:igfitidea点击:

在这篇文章中,列出了一些Java中的异常处理最佳实践。在Java代码中遵循这些最佳实践将有助于我们编写健壮的代码。

Java异常处理最佳实践

1.不要忽略异常-异常处理(尤其是已检查的异常)使我们有机会从引发的异常中恢复。因此,使捕获块为空将无法实现异常处理的目的。

我们需要避免这样的代码

try {
  ...
  ...
} catch( IOException e ) {

}

即使我们非常确定代码块中不会有任何异常,也至少应记录该错误。在极少数情况下,至少会在该块中引发异常,我们会收到一些日志消息以找出问题所在。

try {
  ...
  ...
} catch( IOException e ) {
  logger.error(“Exception caught ” + e.getMessage());
}

2.始终在finally块中清理资源–如果在代码中使用I / O流,DB连接,套接字连接等资源,请确保在finally块中将其关闭。
如果没有异常,则在try块中将它们关闭可能会很好。如果try块中引发了任何异常并且正常流程被中断,则关闭资源的代码可能永远不会执行。为了避免总是关闭finally块中的资源,因为无论是否抛出错误,总是执行finally块。

从Java 7开始,我们还可以使用try-with-resource语句来确保清理资源。使用try-with-resource也将使代码更短。

3.不要将父类用作"全部捕获"解决方案-将Throawble,Exception或者RunTimeException等父类用作通用异常处理程序不是一个好习惯。
我们应该始终尝试抛出可以从代码块中抛出的特定异常类。这使代码更具可读性。
我们应该抛出特定的异常

public void method1 throws ParseException {
 ..
 ..
}

不是通用的"全部捕获"异常。

public void method1 throws Exception {
 ..
 ..
}

我们不应该这样捕获Throwable。

try {
} catch(Throwable t) {
  t.printStackTrace();//Should not do this
}

Throwable是Java中所有错误和异常的超类。捕获Throwable意味着我们也正在捕获无法从中恢复的错误,例如OutOfMemoryError,StackOverFlowError。这与应用程序不应尝试从此类错误中恢复的建议方法背道而驰。

4.提早抛出或者快速失败-Java中处理异常的最佳实践之一是提早抛出。通过尽早抛出异常(也称为"快速失败"),异常变得更加具体和准确。堆栈跟踪立即显示出了什么问题。

异常堆栈跟踪通过向我们显示导致异常的方法调用的确切顺序,以及每个方法调用的类名,方法名,源代码文件名和行号,可以帮助查明发生异常的位置。

现在来看一个例子

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

public class ReadPreference {
  public static void main(String[] args) {
    File fileName = new File("");
    try{
      readFile(fileName);
    }catch (FileNotFoundException e){
      e.printStackTrace();
    }catch (EOFException e){
      e.printStackTrace();
    }
  }
	
  private static void readFile(File fileName) throws 
    FileNotFoundException, EOFException{
    InputStream in = new FileInputStream(fileName);        
  }
}

输出:

java.io.FileNotFoundException: 
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(Unknown Source)
	at java.io.FileInputStream.<init>(Unknown Source)
	at com.theitroad.ReadPreference.readFile(ReadPreference.java:24)
	at com.theitroad.ReadPreference.main(ReadPreference.java:14)

如果我们扫描堆栈跟踪,则FileInputStream类的open()方法似乎存在问题。在代码中,我们可以看到真正的问题是传递空格作为文件名。因此,在方法中立即检查该条件,可以尽早引发异常。

更改了Java程序的文件名检查条件

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

public class ReadPreference {
  public static void main(String[] args) {
    File fileName = new File("");
    try{
      readFile(fileName);
    }catch (FileNotFoundException e){
      e.printStackTrace();
    }catch (EOFException e){
      e.printStackTrace();
    }
  }
	
  private static void readFile(File fileName) throws FileNotFoundException, EOFException, IllegalArgumentException{
    if(fileName == null || fileName.getPath().equals("")){
      throw new IllegalArgumentException("File Name not present");
    }
    InputStream in = new FileInputStream(fileName);        
  }
}

输出:

Exception in thread "main" java.lang.IllegalArgumentException: File Name not present
	at com.theitroad.ReadPreference.readFile(ReadPreference.java:25)
	at com.theitroad.ReadPreference.main(ReadPreference.java:14)

现在,异常消息更加精确。

5.延迟捕获-一个常见的错误是在程序可以适当的方式处理异常之前捕获异常。对于检查的异常,Java编译器会强制捕获或者声明该异常。自然的趋势是立即将代码包装在try块中,并捕获异常以停止编译时错误。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class ReadPreference {
  public static void main(String[] args) {
    File fileName = new File("");
    readFile(fileName);  
  }
	
  private static void readFile(File fileName){
    InputStream in = null;
    try {
      in = new FileInputStream(fileName);
    } catch (FileNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }     
    try {
      in.read();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } 
  }
}

输出:

java.io.FileNotFoundException: 
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(Unknown Source)
	at java.io.FileInputStream.<init>(Unknown Source)
	at com.theitroad.ReadPreference.readFile(ReadPreference.java:22)
	at com.theitroad.ReadPreference.main(ReadPreference.java:15)
Exception in thread "main" java.lang.NullPointerException
	at com.theitroad.ReadPreference.readFile(ReadPreference.java:28)
	at com.theitroad.ReadPreference.main(ReadPreference.java:15)

上面的代码确实无法采取任何措施从错误中恢复时,将捕获FileNotFoundException。如果找不到该文件,则该方法的其余部分肯定无法从该文件中读取。
调用不存在的文件的代码将导致记录FileNotFoundException,然后程序尝试从文件中读取数据。
由于该文件不存在,因此in为null,并且会引发NullPointerException。

在调用链中进一步传递处理异常的责任的方法是在方法的throws子句中声明异常。在声明可能抛出哪些异常时,请记住要尽可能具体。
经过这些更改,代码如下所示。

public class ReadPreference {
  public static void main(String[] args) {
    File fileName = new File("");
    try{
      readFile(fileName);
    }catch (FileNotFoundException e){
      e.printStackTrace();
    }catch (IOException e){
      e.printStackTrace();
    }   
  }
	
  private static void readFile(File fileName) throws IllegalArgumentException, 
    FileNotFoundException, IOException{
    if(fileName == null || fileName.getPath().equals("")){
      throw new IllegalArgumentException("File Name not present");
    }                 
    InputStream in = new FileInputStream(fileName);
    in.read();  
  }
}

6.记录引发的异常-记录在Javadoc中方法签名中声明的异常。为此,请在Javadoc中使用@throws指定异常以及可以引发该异常的可能原因。

/**
* 
* @param fileName
* @throws IllegalArgumentException --if filename is not passed
* @throws FileNotFoundException - if passed file doesn't exist
* @throws IOException - For other I/O errors
*/
private static void readFile(File fileName) throws IllegalArgumentException, 
   FileNotFoundException, IOException{
  ...
  ...
}

7.不要将异常用于流程控制–在引发异常的整个过程中,创建异常对象的过程将遍历方法堆栈以查找可以处理引发异常的异常处理程序。因此,请尝试仅将此异常处理机制用于特殊情况。
使用异常处理作为流控制工具意味着降低了可以通过条件语句轻松检查的简单事情的应用程序性能。
如果条件检查,请使用

int i = 7;
int value;
int[] numArr = {4,5,6};
if(i < numArr.length){
  value = numArr[i];
}

而不是这个

int i = 7;
int value;
int[] numArr = {4,5,6};
try{
  value = numArr[i];
}catch (ArrayIndexOutOfBoundsException ex) {
  ex.printStackTrace();
}

8.不要记录和抛出-进行记录以及重新抛出异常都是一种反模式,不是一种好习惯。对于相同的异常,它将在日志中添加多个错误消息。

try{
  value = numArr[i];
}catch (ArrayIndexOutOfBoundsException ex) {
  logger.info("exception caught " + ex);
  throw ex;
}

输出:

INFO: exception caught java.lang.ArrayIndexOutOfBoundsException: 7
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 7
	at com.theitroad.ReadPreference.main(ReadPreference.java:18)

如我们在输出中看到的,对于同一异常,我们有多个错误消息。

  1. Java异常处理会降低整体性能–在代码中引发异常时,将创建一个异常对象,并在方法堆栈中搜索适当的异常处理程序。这会降低应用程序的整体性能,因此请在异常情况下使用异常处理机制。
    我们可以通过执行一些条件检查来避免异常处理,而不必使用try-catch块。其中一些已经在第7点"不要将异常用于流量控制"中进行讨论。

10.包含原始异常-如果捕获了原始异常后引发了另一个异常,则按照Java中处理异常的最佳实践,应确保不会丢失原始异常。
将构造函数与cause参数一起使用可保留原始异常。

catch (IllegalArgumentException e) {
   throw new MyException ("Exception caught: ", e);  
}

11.转换特定于层的异常-如果在应用程序的任何层中抛出了特定于该层的异常,请确保将其包装在其他异常中。这种实践有助于松散耦合,其中任何特定层的实现都保持与另一层的抽象。
例如,在DAOLayer中,我们可能必须捕获SQLException,但不应将其传播到另一层。我们可以将其包装到另一个异常中并抛出它。

catch(SQLException ex){
  throw new MyException("DB error", ex);
}