java基础之异常

异常基础知识

  1. 异常机制的五个关键字try, catch,finally,throw,throws

  2. java中异常的思想:用类的形式对不正常的情况进行了描述和封装对象,这种类称为异常类。不同的问题使用不同的类进行描述,实现流程代码与问题处理代码分离。

  3. 抛出异常:不管程序代码块中是否处于try中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象,该异常对象被提交给java运行时环境,这个过程被称为抛出异常(throw Exception)。当异常被抛出后,JVM会在抛出异常的方法中寻找最近的匹配的catch 语句,如果没有则在调用方法中找,直至遍历调用栈中所有方法位置。如果没有找到任何匹配的catch语句,则会调用ThreadGroup.uncaughtException()方法

  4. throw自行抛出异常,可单独使用,throw语句抛出的不是异常类,而是一个异常类型,而且每次只能抛出一个异常实例。

  5. throws主要在方法签名中使用,用于声明该方法可能抛出的异常(ps:加s表示抛一堆),throw用于抛出一个实际的异常,可单独作为语句使用,抛出一个具体的异常对象。子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

  6. try块中声明的变量是代码块内局部变量,外界不可访问。通常情况下try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块执行。除非在循环中使用continue开始下一次循环,下一次循环又重新运行try块,这才可能导致多个catch块被执行。父类异常的catch都应该在子类异常catch块的后面。

  7. Error:一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接库失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断

  8. finally:不管try中的代码是否出现异常,也不管哪一个catch块被执行,甚至try或catch中执行了return语句,finally块总会被执行(一些特殊情况除外)。异常处理结果中try是必须的,但catch和finally块至少出现其中之一。当要把内存之外的资源恢复到他们初始状态时,就要用到finally子句。包括已经打开的文件或网络链接,在屏幕上画出的图形。

    public class FinallyTest {
        static int count = 0;
        /**
         * 发生了异常
         * 在finally中发生
         * 没有异常
         * 在finally中发生
         */
        public static void main(String[] args) {
            while (true) {
                try {
                    if (count++ == 0) {
                        throw new Exception("发生了异常");
                    }
                    System.out.println("没有异常");
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                } finally {
                    System.out.println("在finally中发生");
                    if (count == 2) {
                        break;
                    }
                }
            }
        }
    }
    
  9. 除非在try或者catch块中调用了退出虚拟机的方法(System.exit(0)),否则不管try-catch中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。通常情况下不要在finally块中使用如return或throw等导致方法终止的语句,一旦finally块中使用可return或throw语句,将导致try块catch块中的return、throw语句失效。另一方面,如果在 try 块执行期间拨掉电源,finally 也不会执行。

  10. finally不会执行的几种情况

    • JVM过早中止(调用System.exit(0));

    • 在finally块中抛出一个未处理的异常

    • 计算机断电失火或遭遇病毒攻击

      public class Finally2Test {
          //什么也不会输出
          public static void testFinally2() {
              try {
                  System.exit(0);
              } finally {
                  System.out.println("执行finally");
              }
          }
      
          public static void main(String[] args) {
              testFinally2();
          }
      }
      public class Finally3Test {
          /**
           * 输出结果:执行finally
           */
          public static void testFinally() {
              //ThreadDeath 是一个Error,以上代码表示当JVM退出的时候抛出一个Error。所以finally执行了
              System.setSecurityManager(new SecurityManager() {
                  @Override
                  public void checkExit(int status) {
                      throw new ThreadDeath();
                  }
              });
              try {
                  System.exit(0);
              } finally {
                  System.out.println("执行finally");
              }
          }
      
          public static void main(String[] args) {
              testFinally();
          }
      }
      
      
  11. Checked Exception(未检查异常):没有完善错误处理的代码根本就不会被执行!checked降低了程序开发的生产效率和代码的执行效率。在java语言规范中,将任何Error的子类以及RuntimeException的子类都称为未检查异常,而其他的异常都被称为检查异常

  12. 自定义异常

    • 提供一个无参数的构造器
    • 提供一个带字符串参数的构造器,这个字符串将作为该异常对象的描述信息
    1. Exception没有定义任何方法,但它继承了Throwable提供的方法。
    public class MyException extends Exception{
    	public MyException(){}
    	public MyException(String name){
        //调用父类构造器
    	  super(msg);
      }
    }
    
  13. 重抛异常

    • 重新抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。此外异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
    • 如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。
    • 如果是在捕获异常之后抛出另一个异常,这么做类似于使用fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。他们都是用new在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。
    • 异常链(异常转译):常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这称为异常链(异常转译)。
  14. 异常处理规则:异常处理的一个重要原则是只有在你知道如何处理的情况下才捕获异常,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相隔离,这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。

    • 不要过度的使用异常,会减慢程序的运行速度
    • 不要用异常代替流程控制,代价高昂
    • 不要将过大的内容包括在try块中
    • 不要忽略捕获到的异常

性能问题

  1. 异常应该仅仅发生在异常情况下。当设计方法时,抛出异常不应该是方法返回结果的标准方式。例如,如果文件没有被发现,那么编写检查文件存在的方法时可以返回一个异常。但是如果文件总是不存在,那么此方法最好返回布尔值

  2. 案例

    • method1()大部分时间都是抛出异常,并且没有覆写fillInStackTrace()
    • method2()虽然抛出异常但是覆写fillInStackTrace()来提高性能
    • method3()从来都不抛出异常。
    • MacBook Pro 16G内存 i7 4核 jdk8下多次调用100w次
      • method1()执行的时间大概在650ms-750ms
      • method2()执行的时间大概在60ms-90ms
      • method3()执行的时间大概在3ms~4ms
      • 这样巨大的差别表明java中的异常对性能还是有很大影响的。如果必须使用异常,最好覆写Throwable.fillInStackTrace(如果不关心异常栈的情况下,比如参数校验场景抛异常,完全不需要关心异常栈信息)可以提高性能
    public class ExceptionTest {
    
    
        public static void main(String[] args) {
            long startTime1 = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                try {
                    method1(i);
                } catch (Exception e) {
                    //
                }
            }
            long endTime1 = System.currentTimeMillis();
            //672ms 733ms 658ms
            System.out.println(endTime1 - startTime1);
    
    
            long startTime2 = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                try {
                    method2(i);
                } catch (Exception e) {
                    //
                }
            }
            long endTime2 = System.currentTimeMillis();
    
            //72ms 80ms 91ms
            System.out.println(endTime2 - startTime2);
    
    
            long startTime3 = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                method3(i);
            }
            long endTime3 = System.currentTimeMillis();
            //3ms 3ms 3ms
            System.out.println(endTime3 - startTime3);
        }
    
    
        public static boolean method1(int param1) {
            if (param1 > 1000000)
                return false;
            throw new BusinessException0(param1 + "抛出异常");
        }
    
        public static boolean method2(int param2) {
            if (param2 > 1000000)
                return false;
            throw new BusinessException1(param2 + "抛出异常");
        }
    
    
        public static boolean method3(int param3) {
            if (param3 > 1000000)
                return false;
            return true;
        }
    
    
        static class BusinessException0 extends RuntimeException {
            public BusinessException0(String message) {
                super(message);
            }
    
    
        }
    
        static class BusinessException1 extends RuntimeException {
            public BusinessException1(String message) {
                super(message);
            }
    
            @Override
            public synchronized Throwable fillInStackTrace() {
                //返回null或者this
                return this;
            }
        }
    
    }
    
    
  3. jdk7里Throwable类新增了一个构造方法,可以动态决定是否需要异常栈。代码如下,writableStackTrace参数的动态设置可以决定是否需要执行fillInStackTrace()

    protected Throwable(String message, Throwable cause,
                            boolean enableSuppression,
                            boolean writableStackTrace) {
            if (writableStackTrace) {
                fillInStackTrace();
            } else {
                stackTrace = null;
            }
            detailMessage = message;
            this.cause = cause;
            if (!enableSuppression)
                suppressedExceptions = null;
    }
    

异常声明

  1. 如果方法里的代码产生了异常(调用其他声明异常的方法)却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么在本方法声明将抛出的异常(比如throws Exception)。不过有一个能作弊的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

    // 声明异常但是方法中没有异常代码
      public static boolean method4(int param4) throws Exception {
            if (param4 > 1000000)
                return false;
            return true;
        }
        
      public static void main(String[] args) {
        try {
            
            method4(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    
    

异常问题

  1. 案例解析:Java语言规范规定,如果一个catch子句要捕获一个类型为E的受检查异常,而其对应的try子句不能抛出E的某种子类型的异常,那么这就是一个编译期错误。但是捕获Exception或Throwable的catch子句是合法的,不管与其对应的try子句的内容是什么

    1. 无法编译通过
    public class ExceptionTest2 {
        public static void main(String[] args) {
            try {
                System.out.println("hello world!");
            } catch (IOException e) {
                System.out.println(e);
            }
        }
    }
    
    2. 可以编译通过,运行时不会输出异常信息
    public class ExceptionTest2 {
        public static void main(String[] args) {
            try{
                System.out.println("sdf");
            }catch(Exception e){
                System.out.println(e);
            }
        }
    }
    
  2. 案例分析:每一个接口都限制了方法f可以抛出的受检查异常集合。一个方法可以抛出的受检查异常集合是它所适用的所有类型声明要抛出的受检查异常集合的交集,而不是并集。因此Type3的对象类型上f方法根本不能抛出任何受检查异常

    interface Type1 {
        void f() throws CloneNotSupportedException;
    }
    
    interface Type2 {
        void f() throws InterruptedException;
    }
    
    interface Type3 extends Type1, Type2 { }
    
    public class ExceptionType implements Type3 {
        public static void main(String[] args) {
            ExceptionType exceptionTest = new ExceptionType();
           //可以编译通过,并打印hello world
            exceptionTest.f();
        }
    		
        @Override
        public void f() {
            System.out.println("hello world");
        }
    }
    
  3. 构造器中的异常(陷阱)

    • 有一点很重要,即你要时刻询问自己"如果异常发生了,所有东西都被正确的清理吗?"尽管大多是情况下是非常安全的,但涉及到构造器时,问题就出现了。如果在构造器内使用了异常 ,这些清理行为也许就不能正常工作了。这意味着在编写构造器的时候要格外小心。也许你认为使用finally就可以解决问题了,但问题并非如此简单,因为finally会每次都执行清理代码,如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被创建成功,而这些部分在finally子句中却是要清理的。
    • 对于构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的try子句。
  4. 在释放多个IO资源时,都会抛出IOException ,于是可能为了省事如此写

    public static void inputToOutput(InputStream is, OutputStream os,boolean isClose) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(is, 1024);
        BufferedOutputStream bos = new BufferedOutputStream(os, 1024);  
      
        if (isClose) {
           bos.close();
           bis.close();
        }
    }
    假设bos关闭失败,bis还能关闭吗?当然不能!
    解决办法:虽然抛出的是同一个异常,但是还是各自捕获各的为好。否则第一个失败,后一个面就没有机会去释放资源了.
    

异常的缺陷

  1. 异常作为程序出错的标志,绝不应该被忽略,但它还是有可能被轻易地忽略。在用某些特殊的方式使用finally子句,就会发生这种情况。

    class VeryImportantException extends Exception {
        @Override
        public String toString() {
            return "A very important exception!";
        }
    }
    
    //hohum令人厌烦的
    class HoHumException extends Exception {
        @Override
        public String toString() {
            //trivial 没有价值的
            return "A trivial exception";
        }
    }
    
    public class LostMessage {
        void veryImportant() throws VeryImportantException {
            throw new VeryImportantException();
        }
    
        void dispose() throws HoHumException {
            throw new HoHumException();
        }
    
        /*
         * Output: A trivial exception
         * VeryImportantException不见了,它被finally子句里的HoHumException所取代
         */
        public static void main(String[] args) {
            try {
                LostMessage lm = new LostMessage();
                try {
                    lm.veryImportant();
                } finally {
                    lm.dispose();
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
    
  2. 从上面输出中可以看到VeryImportantException不见了,它被finally子句里的HoHumException所取代。这是相当严重的缺陷

  3. 最简单的异常丢失

    1. 如果运行这个程序,就会看到即使抛出了异常,它也不会产生任何的输出.
    public class LostException {
        //什么都不会输出
        public static void main(String[] args) {
            try {
                throw new RuntimeException();
            } finally {
                return;
            }
        }
    }
    
    

JDK7异常新特性

  1. JDK7新增

    • 捕获多种异常类型
    • 重新抛出异常
    • 简化资源清理
  2. 当在catch中声明多种异常时,被声明的异常默认为final的,也就是说不能再修改异常的引用。用一个catch处理多个异常,比用多个catch每个处理一个异常生成的字节码要更小更高效。

    try {   
    	BufferedReader reader = new BufferedReader(new FileReader(""));   
      Connection con = null;   
      Statement stmt = con.createStatement();   
    } catch (IOException | SQLException e) {   
       //捕获多个异常,e就是final类型的   
       e.printStackTrace();   
    }   
    
  3. try后面的圆括号里只能声明并创建可自动关闭的资源,自动关闭资源时资源必须在try后的()中声明,不能在try外声明如下是错误的

    BufferedReader reader = null;
    try(//关闭资源的代码)
    {
    }catch{}
    
  4. 在Java SE 7及以后版本中,当你在catch语句里声明了一个或多个异常类型,并且在catch块里重新抛出了这些异常,编译器根据下面几个条件来去核实异常的类型。

    • Try块里抛出它

    • 前面没有catch块处理它

    • 它是catch里一个异常类型的父类或子类

  5. Closeable是AutoCloseable的子接口

    • 可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。java7几乎把所有的资源类都实现了AutoCloseable或Closeable接口
  • Closeable接口里的close()方法声明抛出了IOException异常,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类
    • AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现在实现close()方法时可以声明抛出任何异常

抛出多个异常

  1. 一个方法抛出多个异常的案例:web界面注册时,展现层依次把User对象传递给逻辑层,注册方法需要对各个Field进行校检并注册,例如用户名不能重复、密码必须符合某种策略等,不要出现用户第一次提交时系统提示"用户名不能重复",在用户修改用户名后再次提交后,系统有提示密码长度不能少于6位的情况,这个操作模式下的用户体验非常糟糕,最好的解决办法就是封装异常,建立异常容器,一次性地针对User对象进行校检,然后返回所有的异常。

    public class ThrowMulitExceptionTest {
        public static void calc() throws ExceptionCollection {
            List<Throwable> list = new ArrayList<Throwable>();
            try {
                int i = 1 / 0;
                System.out.println(i);
            } catch (Exception e) {
                list.add(e);
            }
            try {
                Integer j = null;
                j.intValue();
            } catch (Exception e) {
                list.add(e);
            }
            //检查是否有必要抛出异常
            if (!list.isEmpty()) {
                throw new ExceptionCollection(list);
            }
        }
    }
    
    /**
     *MyExceptionCollection只是一个异常容器,
     *可以容纳多个异常,它本身并不代表任何异常含义
     *所解决的是一次抛出多个异常
     * */
    class ExceptionCollection extends Exception {
        private List<Throwable> causes = new ArrayList<>();
    
        public ExceptionCollection(List<? extends Throwable> e) {
            causes.addAll(e);
        }
    
        public List<Throwable> getException() {
            return causes;
        }
    }
    

无法捕获的异常

  1. 是否可以写一段Java代码让一个假设的java.lang.ChuckNorrisException无法被捕获?

    • 你可以编译一段代码抛出一个ChuckNorrisException,但是在运行时动态生成一个并不继承于Throwable接口的ChuckNorrisException类。当然,为了让这个过程可以进行,你需要关闭掉字节码验证(-Xverify:none)
    • 解决方案
  2. 创建类

    package cn.jannal.java.exception.notry;
    public class ChuckNorrisException
            extends RuntimeException  //在第二次编译时注释此行
    {
        public ChuckNorrisException() {
        }
    }
    
    package cn.jannal.java.exception.notry;
    public class TestVillain {
        public static void main(String[] args) {
            try {
                throw new ChuckNorrisException();
            } catch (Throwable t) {
                System.out.println("Exception!");
            } finally {
                System.out.println("Finally!");
            }
        }
    }
    
  3. 第一步编译

    javac -cp . TestVillain.java ChuckNorrisException.java 
    javac -cp cn.jannal.java.exception.notry  TestVillain.java ChuckNorrisException.java
      
    javac  -d . TestVillain.java ChuckNorrisException.java   
    
  4. 第二步运行

    $ java  cn.jannal.java.exception.notry.TestVillain 
    
    Exception!
    Finally!
    
  5. 第三步:注释代码并仅仅重新编译ChuckNorrisException.java文件

    public class ChuckNorrisException
           // extends RuntimeException  //在第二次编译时注释此行
    {
        public ChuckNorrisException() {
        }
    }
    
    $  javac  -d .  ChuckNorrisException.java 
    
  6. 第四步:再次运行

    $ java   -Xverify:none cn.jannal.java.exception.notry.TestVillain 
    Finally!
    Exception in thread "main" 
    Exception: java.lang.AbstractMethodError thrown from the UncaughtExceptionHandler in thread "main"
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值