Exception最佳实践

Exception最佳实践

异常实现及分类

在这里插入图片描述
上图可以简单展示一下异常类实现结构图,当然上图不是所有的异常,用户自己也可以自定义异常实现。上图已经足够帮我们解释和理解异常实现了:

1.所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。

2.Throwable有两个子类,Error和Exception。
其中Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。

3.Exception,是另外一个非常重要的异常子类。它规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。

4.Checked Exception

可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的。它们的发生是可以预测的,正常的一种情况,可以合理的处理。比如IOException,或者一些自定义的异常。除了RuntimeException及其子类以外,都是checked exception。

5.Unchecked Exception

RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是运行时发生,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。

上面参考与:https://blog.youkuaiyun.com/michaelgo/article/details/82790253

如何去处理异常

当我们写Java代码进行异常处理的时候往往会遇到很多一些小的问题,比如我们应该怎么处理异常,处理哪些异常,很多团队可能对异常的处理都有一些规范,但是不用的团队的规范都不一样,下面我就说一些处理异常的最佳实践。

  1. 不要直接抛出Exception

    看下面这个代码

    public class Example1 {
       public void test() throws Exception{
           Integer number = Integer.parseInt("heloworld");
       }
    }
    

    这里是直接把Exception抛出,但是Exception还有很多子异常,当发生错误的时候,调用者并不能知道发生了什么错误,所以应把异常具体化,就比如上面这个例子改成下面这个样子

    public class Example1 {
       public void test() throws NumberFormatException{
           Integer number = Integer.parseInt("heloworld");
       }
    }
    

    点进源码可以看到, Integer.parseInt()方法抛出的是NumberFormatException异常,所以我们写的具体点,当发生异常的时候,调用者很容易就能从NumberFormatException这个单词上知道,哦,这是个数字格式化异常。

  2. 给异常加说明

    看下面这个代码

    public class Example2 {
        class NotFindException extends Exception {
            public NotFindException(String message) {
                super(message);
            }
        }
    
        /**
         * @throws NotFindException 如果发生文件找不到错误,就抛出此错误
         */
        public void doThings() throws NotFindException {
    
        }
    }
    

    这样的话,当调用者调用doThings()方法的时候,就能知道,什么时候会抛出这个错误,出现之后如果去处理。

  3. 使用描述性信息抛出异常

    看下面这个代码

    public class Example3 {
        private static final Logger LOGGER = Logger.getLogger(Example3.class);
    
        public void doSomeThings() {
            try {
                new Long("helloworld");
            } catch (NumberFormatException e) {
                LOGGER.error("数据格式化错误", e);
            }
        }
    }
    

    这样,进行打印日志的时候,就能知道出现错误的原因,从而进行错误处理。而且,始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失,也就是说,上面的catch块不要这样写

    LOGGER.error("数据格式化错误", e.getMessage());
    
  4. 优先捕获具体异常

    我们大家都知道,在进行异常处理的时候,是找到第一个符合的异常catch块进行处理,如果把具体的异常放在后面的话,就可能出现一种不可达的情况,Java也不允许我们这样做。

    如果是下面这段代码

    public class Example4 {
        private static final Logger LOGGER = Logger.getLogger(Example4.class);
        
        public void catchMostSpecificExceptionFirst(){
            try {
                doSomeThings();
            } catch (IllegalArgumentException e){
                LOGGER.error("非法字符串", e);
            } catch (NumberFormatException e){
                LOGGER.error("字符串转换失败", e);
            }
        }
    }
    

    NumberFormatException 继承了 IllegalArgumentException 类,当执行doSomeThings()方法发生了NumberFormatException 异常的时候,就会在 IllegalArgumentException异常catch块进行异常处理,从而不能获取准确的异常信息。

    正确的写法是先写具体异常的catch块。

  5. 永远不要捕获Throwable类

    看这段代码

    public class Example5 {
        private static final Logger LOGGER = Logger.getLogger(Example4.class);
    
        public void doNotCatchThrowable() {
            try {
                doSomeThings();
            } catch (Throwable e) {
                //永远不要这么干
            }
        }
    }
    
    

    这是一个更严重的麻烦。 因为这样的话,catch不光会捕获exception,还会捕获error。 error是JVM本身无法处理的不可逆转的条件。 对于某些JVM的实现,JVM可能实际上甚至不会在错误上调用catch子句。

  6. 不要在try块中进行资源关闭

    看下面这段代码

    public class Example6 {
        private static final Logger LOGGER = Logger.getLogger(Example6.class);
        public static void main(String[] args) {
            File file = new File("d:/1.json");
            InputStream inputStream = null;
            try {
                inputStream = new FileInputStream(file);
                //事件1
                //事件2
                inputStream.close();
            } catch (FileNotFoundException e) {
                LOGGER.error(e.toString(), e);
            } catch (IOException e) {
                LOGGER.error(e.toString(), e);
            }
        }
    }
    

    假想一下,如果在try块中关闭资源,当进行事件1或者事件2操作时,抛出了异常,资源关闭方法close就不会被执行了,达不到我们想要的效果,所以正确的关闭方式应该是这样的

    public class Example6 {
        private static final Logger LOGGER = Logger.getLogger(Example6.class);
        public static void main(String[] args) {
            File file = new File("d:/1.json");
            InputStream inputStream = null;
            try {
                inputStream = new FileInputStream(file);
                //事件1
                //事件2
            } catch (FileNotFoundException e) {
                LOGGER.error(e.toString(), e);
            }  finally {
                if (inputStream != null){
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        LOGGER.error(e.toString(), e);
                    }
                }
            }
        }
    }
    

    首先对inputStream进行非空判断,因为可能在try块中还没new一个inputstream就发生了错误,所以如果不进行非空判断就进行close操作可能就会发生一个空指针异常。

    但是这样写还有一个问题,当出现多个需要关闭的资源时应该怎么写?正确的写法是在第一个资源关闭的finally块中进行第二个资源的关闭,依此类推,这样写的话,就会造成循环的嵌套,所以jdk1.7给了我们一种优雅关闭资源的一个方法,也就是try-with-resource方法。

    public class Example6 {
        private static final Logger LOGGER = Logger.getLogger(Example6.class);
        public static void main(String[] args) {
            File file = new File("d:/1.json");
            try (InputStream inputStream = new FileInputStream(file);){
                //事件1
                //事件2
            } catch (FileNotFoundException e) {
                LOGGER.error(e.toString(), e);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    这样程序就会自动帮我们运行close()方法。

  7. 不要忽略异常

    要对异常信息进行记录,哪怕你觉得不会出现这种错误。也就是说,不要在catch块中什么都不做,

  8. 要么记录异常要么抛出异常,但不要一起执行

    public class Example7 {
        private static final Logger LOGGER = Logger.getLogger(Example4.class);
        public void fo(){
            try {
                new Long("hello");
            } catch (NumberFormatException e){
                LOGGER.error(e.toString(), e);
                throw  e;
            }
        }
    
        public static void main(String[] args) {
            new Example7().fo();
        }
    }
    

    正如在上面的示例代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且让尝试挖掘日志的工程师生活变得很糟糕。

  9. 不要使用printStackTrace()语句或类似的方法

    完成代码后,切勿忽略printStackTrace()。 你的同事可能会最终得到这些堆栈,并且对于如何处理它完全没有任何知识,因为它不会附加任何上下文信息。

  10. 如果你不打算处理异常,请使用finally块而不是catch块

    try {
      someMethod();  //Method 2
    } finally {
      cleanUp();    //do cleanup here
    }
    

    这也是一个很好的做法。 如果在你的方法中你正在访问Method 2,而Method 2抛出一些你不想在method 1中处理的异常,但是仍然希望在发生异常时进行一些清理,然后在finally块中进行清理。 不要使用catch块。

  11. 包装异常不要丢弃原始异常

    看下面一段代码

    public class Example8 {
    
        class MyBusinessException extends Exception {
            public MyBusinessException(String message) {
                super(message);
            }
    
            public MyBusinessException(String message, Throwable cause) {
                super(message, cause);
            }
        }
    
        public void warpException(String id) throws MyBusinessException {
            try {
                long userId = Long.parseLong(id);
                System.out.println("userId  " + userId);
            } catch (NumberFormatException e) {
                throw new MyBusinessException("这里是具体的描述");
            }
        }
    
        public static void main(String[] args) throws MyBusinessException {
            new Example8().warpException("hello");
        }
    }
    
    

    下面看运行结果

    Exception in thread "main" exception.Example8$MyBusinessException: 这里是具体的描述
    	at exception.Example8.warpException(Example8.java:23)
    	at exception.Example8.main(Example8.java:28)
    

    我们在运行结果中,并没有看到我们应该怎么快速处理这个异常信息,原因是,我们在包装异常的时候,并没有把异常栈传递进去,所以我们正常的做法是,在catch块中把栈信息也传递进去,即

    throw new MyBusinessException("这里是具体的描述", e);
    

    此时,结果如下

    Exception in thread "main" exception.Example8$MyBusinessException: 这里是具体的描述
    	at exception.Example8.warpException(Example8.java:25)
    	at exception.Example8.main(Example8.java:30)
    Caused by: java.lang.NumberFormatException: For input string: "hello"
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    	at java.lang.Long.parseLong(Long.java:589)
    	at java.lang.Long.parseLong(Long.java:631)
    	at exception.Example8.warpException(Example8.java:22)
    	... 1 more
    
  12. **不要在finally中执行return操作

public class ReturnTest {
    static int x = 1;
    static int y = 10;
    static int z = 100;

    public static void main(String[] args) {
        int value = finallyReturn();
        System.out.println("value= " + value);
        System.out.println("x= " + x);
        System.out.println("y= " + y);
        System.out.println("z= " + z);
    }
    public static int finallyReturn(){
        try {
            // ...
            return ++x;
        } catch (Exception e){
            return ++y;
        } finally {
            return ++z;
        }
    }
}

运行结果如下
value= 101
x= 2
y= 10
z=101
这样返回值就会变的不可控,所以我们不要再finally中使用return语句。

此时,我们知道怎么发生的异常,从而可以快速的去处理异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值