• Java的异常
  1. 计算机程序运行的过程中,总是会出现各种各样的错误。有一些错误是用户造成的,比如,希望用户输入一个int类型的年龄,但是用户的输入是abc。程序想要读写某个文件的内容,但是用户已经把它删除了。还有一些错误是随机出现,并且永远不可能避免的。比如:

    • 网络突然断了,连接不到远程服务器;
    • 内存耗尽,程序崩溃了;
    • 用户点“打印”,但根本没有打印机;
    • ……
  2. Java内置了一套异常处理机制,总是使用异常来表示错误。异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了。

     1 try {
    2 String s = processFile(“C:\\test.txt”);
    3 // ok:
    4 } catch (FileNotFoundException e) {
    5 // file not found:
    6 } catch (SecurityException e) {
    7 // no read permission:
    8 } catch (IOException e) {
    9 // io error:
    10 } catch (Exception e) {
    11 // other error:
    12 }
  3. Java的异常是class,它的继承关系如下:
  4. 从继承关系可知:Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorExceptionError表示严重的错误,程序对此一般无能为力。
  5. Exception则是运行时的错误,它可以被捕获并处理。
  6. Exception又分为两大类:

    1. RuntimeException以及它的子类;
    2. RuntimeException(包括IOExceptionReflectiveOperationException等等)
  7. Java规定:
    • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。

    • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。

  8. 编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。
  9. 捕获异常

    • 捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类。

       1 import java.io.UnsupportedEncodingException;
      2 import java.util.Arrays;
      3
      4 public class Main {
      5 public static void main(String[] args) {
      6 byte[] bs = toGBK("中文");
      7 System.out.println(Arrays.toString(bs));
      8 }
      9
      10 static byte[] toGBK(String s) {
      11 try {
      12 // 用指定编码转换String为byte[]:
      13 return s.getBytes("GBK");
      14 } catch (UnsupportedEncodingException e) {
      15 // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
      16 System.out.println(e); // 打印异常信息
      17 return s.getBytes(); // 尝试使用用默认编码
      18 }
      19 }
      20 }
    • 如果我们不捕获UnsupportedEncodingException(去掉try catch),会出现编译失败的问题。编译器会报错,错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException这样的Checked Exception,必须被捕获。
    • 这是因为String.getBytes(String)方法定义是(如下),在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型(Runtime及其子类除外)。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。
      public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
      ...
      }
    • toGBK()方法中,因为调用了String.getBytes(String)方法,就必须捕获UnsupportedEncodingException。我们也可以不捕获它,而是在方法定义处用throws表示toGBK()方法可能会抛出UnsupportedEncodingException,就可以让toGBK()方法通过编译器检查。
       1 import java.io.UnsupportedEncodingException;
      2 import java.util.Arrays;
      3
      4 public class Main {
      5 public static void main(String[] args) {
      6 byte[] bs = toGBK("中文");
      7 System.out.println(Arrays.toString(bs));
      8 }
      9
      10 static byte[] toGBK(String s) throws UnsupportedEncodingException {
      11 return s.getBytes("GBK");
      12 }
      13 }
    • 只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()方法中捕获,不会出现漏写try的情况。这是由编译器保证的。main()方法也是最后捕获Exception的机会。
    • 如果是测试代码,上面的写法就略显麻烦。如果不想写任何try代码,可以直接把main()方法定义为throws Exception。因为main()方法声明了可能抛出Exception,也就声明了可能抛出所有的Exception,因此在内部就无需捕获了。代价就是一旦发生异常,程序会立刻退出。
    • toGBK()内部“消化”异常
      static byte[] toGBK(String s) {
      try {
      return s.getBytes("GBK");
      } catch (UnsupportedEncodingException e) {
      // 什么也不干
      }
      return null;
    • 这种捕获后不处理的方式是非常不好的,即使真的什么也做不了,也要先把异常记录下来:
      static byte[] toGBK(String s) {
      try {
      return s.getBytes("GBK");
      } catch (UnsupportedEncodingException e) {
      // 先记下来再说:
      e.printStackTrace();
      }
      return null;
    • 所有异常都可以调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。
  • 捕获异常
    1. 凡是可能抛出异常的语句,都可以用try ... catch捕获。把可能发生异常的语句放在try { ... }中,然后使用catch捕获对应的Exception及其子类。  
    2. 可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

      简单地说就是:多个catch语句只有一个能被执行。

    3. 存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。
    4. 无论是否有异常发生,都希望执行一些语句,例如清理工作,可以把执行语句写若干遍:正常执行的放到try中,每个catch再写一遍。

       1 public static void main(String[] args) {
      2 try {
      3 process1();
      4 process2();
      5 process3();
      6 System.out.println("END");
      7 } catch (UnsupportedEncodingException e) {
      8 System.out.println("Bad encoding");
      9 System.out.println("END");
      10 } catch (IOException e) {
      11 System.out.println("IO error");
      12 System.out.println("END");
      13 }
      14 }
    5. Java的try ... catch机制还提供了finally语句,finally语句块保证有无错误都会执行。上述代码可以改写如下。
      public static void main(String[] args) {
      try {
      process1();
      process2();
      process3();
      } catch (UnsupportedEncodingException e) {
      System.out.println("Bad encoding");
      } catch (IOException e) {
      System.out.println("IO error");
      } finally {
      System.out.println("END");
      }
      }
    6. finally有几个特点:

      1. finally语句不是必须的,可写可不写;
      2. finally总是最后执行。
    7. 某些情况下,可以没有catch,只使用try ... finally结构。
      void process(String file) throws IOException {
      try {
      ...
      } finally {
      System.out.println("END");
      }
      }

      因为方法声明了可能抛出的异常,所以可以不写catch

  • 抛出异常
    1. 查看Integer.java源码可知,抛出异常的方法代码如下。

      public static int parseInt(String s, int radix) throws NumberFormatException {
      if (s == null) {
      throw new NumberFormatException("null");
      }
      ...
      }
    2. 查看Integer.java源码可知,抛出异常的方法代码如下当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:

      1. 创建某个Exception的实例;
      2. throw语句抛出。
        void process2(String s) {
        if (s==null) {
        NullPointerException e = new NullPointerException();
        throw e;
        }
        } 或: void process2(String s) {
        if (s==null) {
        throw new NullPointerException();
        }
        }
      3. 在代码中获取原始异常可以使用Throwable.getCause()方法。如果返回null,说明已经是“根异常”了。
      4. 捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!
    3. try或者catch语句块中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。
    4. 如果在执行finally语句时抛出异常,那么,finally抛出异常后,原来在catch中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
      public class Main {
      public static void main(String[] args) {
      try {
      Integer.parseInt("abc");
      } catch (Exception e) {
      System.out.println("catched");
      throw new RuntimeException(e);
      } finally {
      System.out.println("finally");
      throw new IllegalArgumentException();
      }
      }
      } 输出: catched
      finally
      Exception in thread "main" java.lang.IllegalArgumentException
      at Main.main(Main.java:11)
    5. 在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用origin变量保存原始异常,然后调用Throwable.addSuppressed(),把原始异常添加进来,最后在finally抛出。(通过Throwable.getSuppressed()可以获取所有的Suppressed Exception。绝大多数情况下,在finally中不要抛出异常。因此,通常不需要关心Suppressed Exception。)
       1 public class Main {
      2 public static void main(String[] args) throws Exception {
      3 Exception origin = null;
      4 try {
      5 System.out.println(Integer.parseInt("abc"));
      6 } catch (Exception e) {
      7 origin = e;
      8 throw e;
      9 } finally {
      10 Exception e = new IllegalArgumentException();
      11 if (origin != null) {
      12 e.addSuppressed(origin);
      13 }
      14 throw e;
      15 }
      16 }
      17 }
  • 自定义异常
    1. 在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生。

      public class BaseException extends RuntimeException {
      }

      //其他业务类型的异常就可以从BaseException派生:
      public class UserNotFoundException extends BaseException {
      } public class LoginFailedException extends BaseException {
      }

      ...

      //自定义的BaseException应该提供多个构造方法
      public class BaseException extends RuntimeException {
      public BaseException() {
      super();
      } public BaseException(String message, Throwable cause) {
      super(message, cause);
      } public BaseException(String message) {
      super(message);
      } public BaseException(Throwable cause) {
      super(cause);
      }
      }
    2. 上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。

  • NullPointerException

    • NullPointerException即空指针异常,俗称NPE。如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的。
    • 指针这个概念实际上源自C语言,Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference。
    • NullPointerException是一种代码逻辑错误,遇到NullPointerException,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误。  
    • 成员变量在定义时初始化使用空字符串""而不是默认的null可避免很多NullPointerException,编写业务逻辑时,用空字符串""表示未填写比null安全得多。
    • 如果调用方一定要根据null判断,比如返回null表示文件不存在,那么考虑返回Optional<T>
      public Optional<String> readFromFile(String file) {
      if (!fileExist(file)) {
      return Optional.empty();
      }
      ...
      }

      这样调用方必须通过Optional.isPresent()判断是否有结果。

    • 定位NullPointerException

      •   从Java 14开始,如果产生了NullPointerException,JVM可以给出详细的信息告诉我们null对象到底是谁。
      • 这种增强的NullPointerException详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它:

        java -XX:+ShowCodeDetailsInExceptionMessages Main.java
  • 使用断言

    • 断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。 

      public static void main(String[] args) {
      double x = Math.abs(-123.45);
      assert x >= 0;
      System.out.println(x);
      }

      语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError

    • 使用assert语句时,还可以添加一个可选的断言消息。 
      assert x >= 0 : "x must >= 0";

      断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。

  • 使用JDK Logging

    • Java标准库内置了日志包java.util.logging,可以直接用。

      import java.util.logging.Level;
      import java.util.logging.Logger; public class Hello {
      public static void main(String[] args) {
      Logger logger = Logger.getGlobal();
      logger.info("start process...");
      logger.warning("memory is running out...");
      logger.fine("ignored.");
      logger.severe("process will be terminated...");
      }
      }
    • 日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:

      • SEVERE
      • WARNING
      • INFO
      • CONFIG
      • FINE
      • FINER
      • FINEST
    • 因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。

  • Commons Logging(理解)

    • Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。

    • 使用Commons Logging只需要和两个类打交道,并且只有两步:第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。

      import org.apache.commons.logging.Log;
      import org.apache.commons.logging.LogFactory; public class Main {
      public static void main(String[] args) {
      Log log = LogFactory.getLog(Main.class);
      log.info("start...");
      log.warn("end.");
      }
      }
  • 使用Log4j

  • 使用SLF4J和Logback

最新文章

  1. enum 与 enum class
  2. Python之路Day14--html
  3. hive的内部表与外部表创建
  4. (五)stm32工程代码HardFault异常查错调试方法
  5. java web目录结构
  6. LibreOffice源码编译以及生成VS项目
  7. [转载]mongoDB学习笔记——存取图片(C#)
  8. Android图片裁剪之自由裁剪
  9. MINA、Netty、Twisted一起学(十二):HTTPS
  10. 从 HTTP 到 HTTPS 再到 HSTS
  11. git 菜鸟入门
  12. WPF项目学习.四
  13. 实现自动SSH连接
  14. &ldquo;.Net 社区大会&rdquo;(dotnetConf) 2018 Day 1 主题演讲
  15. leetcode46. Permutations 、47. Permutations II、 剑指offer字符串的排列
  16. Spring Boot 集成 thymeleaf 模版引擎
  17. wpf mediakit 摄像头截图
  18. xen虚拟机管理命令
  19. C#文件系统管理【转】
  20. POJ3045 Cow Acrobats

热门文章

  1. Java基础教程——打印流
  2. Java数据结构(十)—— 树
  3. ModelViewSet + ModelSerializer
  4. select监听服务端
  5. InnoDB 中的缓冲池(Buffer Pool)
  6. 「考试」CSP-S 2020
  7. Eclipse的新建工作空间如何用以前工作空间的配置
  8. iNeuOS工业互联平台,WEB组态(iNeuView)图元和数据点组合及生成新图元复用,实现拖业务
  9. 第14.4节 使用IE浏览器获取网站访问的http信息
  10. Mac下安装Mesa