5 - 异常处理

目录

1. 总览

1.1 Exception 与 Error

1.2 checked unchecked 异常

1)使用 try-catch 进行捕获

2)使用 throws 关键字抛出

1.3 throw 与 throws

1)throw

2)throws

3)区别

1.4 try-catch-finally

2. try with resources

2.1 try–catch-finally 不足

2.2 解决问题

 3. 异常处理实践

3.1 尽量不要捕获 RuntimeException

3.2 尽量使用 try-with-resource 来关闭资源

3.3 不要捕获  Throwable

3.4 不要省略异常信息的记录

3.5 不要记录了异常又抛出异常

3.6 不要在 finally 块中使用 return

3.7 抛出具体定义的检查性异常而不是 Exception

3.8 捕获具体的子类而不是捕获 Exception 类

3.9 自定义异常时不要丢失堆栈跟踪

3.10 finally 块中不要抛出任何异常

3.11 不要在生产环境中使用 printStackTrace()

3.12 对于不打算处理的异常,直接使用 try-finally,不用 catch

3.13 记住早 throw 晚 catch 原则

3.14 只抛出和方法相关的异常

3.15 切勿在代码中使用异常来进行流程控制

3.16 尽早验证用户输入以在请求处理的早期捕获异常

3.17 一个异常只能包含在一个日志中

3.18 将所有相关信息尽可能地传递给异常

3.19 终止掉被中断线程

3.20 对于重复的 try-catch,使用模板方法


1. 总览

异常是指中断程序正常执行的一个不确定的事件

有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程

1.1 Exception 与 Error

Exception 和 Error 都继承了 Throwable 类

NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样

  • NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件;原因:可能是 jar 包缺失或者调用了初始化失败的类
  • ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因:要加载的类不存在或者类名写错了

Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉

Exception 的出现,意味着程序出现了一些在可控范围内的问题,应当采取措施进行挽救

1.2 checked unchecked 异常

checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作

unchecked 异常(非检查型异常)就是运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出

1)使用 try-catch 进行捕获

try {
    Class clz = Class.forName("com.itwanger.s41.Demo1");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

注:该方法会将异常的堆栈信息打印到标准的控制台下;如果是生产环境,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪

2)使用 throws 关键字抛出

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clz = Class.forName("com.itwanger.s41.Demo1");
    }
}

这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可

坏处就是没法针对这种情况做相应的处理

1.3 throw 与 throws

1)throw

throw 关键字,用于主动地抛出异常

throw new exception_class("error message");

2)throws

throws 与 try-catch 对比

public static void main(String args[]){
    try {
        myMethod1();
    } catch (ArithmeticException e) {
        // 算术异常
    } catch (NullPointerException e) {
        // 空指针异常
    }
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
    // 方法签名上声明异常
}

3)区别

  1. throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。
  2. throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象
  3. throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里
  4. throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常

1.4 try-catch-finally

try {
    // 可能发生异常的代码
}catch {
   // 异常处理
}finally {
   // 必须执行的代码
}

finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等

OutputStream osf = new FileOutputStream( "filename" );
OutputStream osb = new BufferedOutputStream(opf);
ObjectOutput op = new ObjectOutputStream(osb);
try{
    output.writeObject(writableObject);
} finally{
    op.close();
}

注:即使 try 块有 return,finally 块也会执行

 当然也存在 不执行 finally 的情况:

  • 死循环
  • 执行了 System.exit() 

System.exit() 与 return 不同,前者是用来退出程序的,后者只是回到了上一级方法调用


2. try with resources

2.1 try–catch-finally 不足

在 Java 7 之前,try–catch-finally 是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常

public class TrycatchfinallyDecoder {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            String path = TrycatchfinallyDecoder.class.getResource("/a.txt").getFile();
            String decodePath = URLDecoder.decode(path,"utf-8");
            br = new BufferedReader(new FileReader(decodePath));

            String str = null;
            while ((str =br.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

存在一个严重的隐患:try 与 finally 都有可能抛出 IOException,那么程序的调试任务就变得复杂了起来,因为不确定到底是哪一处出了错误

2.2 解决问题

try-with-resources 可以解决该问题,前提是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口,并提供 close() 方法即可

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) {
    String str = null;
    while ((str =br.readLine()) != null) {
        System.out.println(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

把要释放的资源写在 try 后的 () 里,如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在 () 中添加(就像写成员属性定义的语句一样)

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));
     PrintWriter writer = new PrintWriter(new File(writePath))) {
    String str = null;
    while ((str =br.readLine()) != null) {
        writer.print(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

如果想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 close() 方法即可

public class TrywithresourcesCustom {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource();) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}

本质:编译器主动为 try-with-resources 进行了变身,在 try 中调用了 close() 方法

好处:是不会丢失任何异常


 3. 异常处理实践

异常处理实践经验

3.1 尽量不要捕获 RuntimeException

正例:

if (obj != null) {
  //...
}

反例:

try { 
  obj.method(); 
} catch (NullPointerException e) {
  //...
}

3.2 尽量使用 try-with-resource 来关闭资源

当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源

反例:

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

原因:一旦 close()之前发生异常,那么资源就无法关闭

正例:直接使用 try-with-resource

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

如果资源没有实现 AutoCloseable 接口,就在 finally 块关闭流

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}

3.3 不要捕获  Throwable

Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了

反例:

public void doNotCatchThrowable() {
    try {
    } catch (Throwable t) {
        // 不要这样做
    }
}

3.4 不要省略异常信息的记录

需要记录错误信息

public void logAnException() {
    try {
    } catch (NumberFormatException e) {
        log.error("错误发生了: " + e);
    }
}

3.5 不要记录了异常又抛出异常

反例:

public void wrapException(String input) throws MyBusinessException {
    try {
    } catch (NumberFormatException e) {
        throw new MyBusinessException("错误信息描述:", e);
    }
}

3.6 不要在 finally 块中使用 return

try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。

反例:

private int x = 0;
public int checkReturn() {
    try {
        return ++x;
    } finally {
        return ++x;
    }
}

3.7 抛出具体定义的检查性异常而不是 Exception

反例:

public void foo() throws Exception { //错误方式
}

声明的方法应该尽可能抛出具体的检查性异常

3.8 捕获具体的子类而不是捕获 Exception 类

反例:

try {
   someMethod();
} catch (Exception e) { //错误方式
   LOGGER.error("method has failed", e);
}

如果捕获 Exception 类型的异常,可能会导致以下问题:

  • 难以识别和定位异常:如果捕获 Exception 类型的异常,可能会捕获到一些不应该被处理的异常,从而导致程序难以识别和定位异常
  • 难以调试和排错:如果捕获 Exception 类型的异常,可能会使得调试和排错变得更加困难,因为无法确定具体的异常类型和异常发生的原因

正例:应该尽可能地捕获具体的子类

try {
    // 读取数据的代码
} catch (FileNotFoundException e) {
    // 处理文件未找到异常的代码
} catch (IOException e) {
    // 处理输入输出异常的代码
}

3.9 自定义异常时不要丢失堆栈跟踪

不要破坏原始异常的堆栈跟踪

public class MyException extends Exception {
    public MyException(String message, Throwable cause) {
        super(message, cause);
    }

    @Override
    public void printStackTrace() {
        System.err.println("MyException:");
        super.printStackTrace();
    }
}

3.10 finally 块中不要抛出任何异常

try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
}

如果在 finally 块中抛出异常,可能会导致原始异常被掩盖

一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖

3.11 不要在生产环境中使用 printStackTrace()

它可能会导致以下问题:

  • printStackTrace() 方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。
  • printStackTrace() 方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。
  • 由于生产环境中往往是多线程、分布式的复杂系统,printStackTrace() 方法输出的堆栈跟踪信息可能并不完整或准确。

在生产环境中,应该使用日志系统来记录异常信息,日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性

// 可以使用 logback 记录异常信息,如下所示:
try {
    // some code
} catch (Exception e) {
    logger.error("An error occurred: ", e);
}

3.12 对于不打算处理的异常,直接使用 try-finally,不用 catch

try {
  method1();  // 会调用 Method 2
} finally {
  cleanUp();    //do cleanup here
}

3.13 记住早 throw 晚 catch 原则

在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常

在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常

3.14 只抛出和方法相关的异常

public class Demo {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("The result is: " + result);
        } catch (ArithmeticException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

只抛出了和方法相关的异常 ArithmeticException,这可以使代码更加清晰和易于维护

3.15 切勿在代码中使用异常来进行流程控制

使用异常来进行流程控制会导致代码的可读性、可维护性和性能出现问题

应该使用其他合适的控制结构来管理程序的流程

虽然是可以实现逻辑的,但是要避免这样使用

public class Demo {
    public static void main(String[] args) {
        String input = "1,2,3,a,5";
        String[] values = input.split(",");
        for (String value : values) {
            try {
                int num = Integer.parseInt(value);
                System.out.println(num);
            } catch (NumberFormatException e) {
                System.err.println(value + " is not a valid number");
            }
        }
    }
}

3.16 尽早验证用户输入以在请求处理的早期捕获异常

用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert

3.17 一个异常只能包含在一个日志中

反例:

log.debug("Using cache sector A");
log.debug("Using retry sector B");

在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难

正例:

LOGGER.debug("Using cache sector A, using retry sector B");

3.18 将所有相关信息尽可能地传递给异常

有用的异常消息和堆栈跟踪非常重要

应该尽量把 String message, Throwable cause 异常信息和堆栈都输出

3.19 终止掉被中断线程

正例:

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();

3.20 对于重复的 try-catch,使用模板方法

在尝试关闭数据库连接时的异常处理时使用模板方法:

class DBUtil{
    public static void closeConnection(Connection conn){
        try{
            conn.close();
        } catch(Exception ex){
            //Log Exception - Cannot close connection
        }
    }
}

public void dataAccessCode() {
    Connection conn = null;
    try{
        conn = getConnection();
        ....
    } finally{
        DBUtil.closeConnection(conn);
    }
}

  • 44
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值