java中的异常与断言

异常与断言

异常机制用来提高程序健壮性(鲁棒性),可以对不在规约中的情况包容处理;断言用来提高保证程序的正确性

异常

  1. 异常的分类:
  • Java语言中异常对象都是都是派生于java.lang.Throwable类的一个类的实例,但是如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类
    image

  • Error类

    • 描述了java运行时系统的内部错误和资源耗尽错误
    • 程序不应该抛出这种类型的对象,在大多数情况下程序员不需要实例化Error类
    • 如果出现了这样的内部错误除了通知用户并尽力妥善终止程序外几乎无能为力,所以我们不会太关注Error类的处理而是更关心下面介绍的Exception类
    • 但是这种情况很少见
    • 子类举例1:VirtualMachineError子类描述JVM(JAVA虚拟机)被损坏或者运行超范围
      • OutOfMemoryError
      • StackOverflowError
      • InternalError
    • 子类举例2:LinkageError子类描述A类对B类有依赖但是B类在A类编译之后发生了不兼容的更改
      • NoClassDefFoundError
  • Exception类

    • RuntimeException类:
      • 描述由编程错误导致的异常(包括错误的强制类型转换、数组访问越界ArrayIndexOutOfBoundsException、访问空指针NullPointerException等)
      • 如果触发了RuntimeException那一定是你的问题
      • 可以发现RuntimeException是完全可以被避免的,比如应该提前检测数组下标是否越界、使用变量前通过检测指针是否为空指针再考虑引用等检测手段避免出现异常
    • 其他类:
      • 描述程序本身无问题的异常(包括试图超越文件末尾继续读取数据、试图打开一个不存在的文件、试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在)
      • 此类异常区别于RuntimeException类的一个很大不同就是无法通过检验避免异常的产生,比如即使先检验了文件确实存在后就立即被删除了,这样我们就对此无能为力,因为“是否存在”取决于环境而并非取决于你的代码
      • 举例:IOException类(包含EOFException子类,FileNotFoundException子类等多个子类),ClassNotFoundException类
  • Java语言将派生于Error类或RuntimeException类的所有异常成为非检查型异常(unchecked),所有其他异常成为检查型异常(checked),编译器会检查你是否为所有的检查型异常提供了异常处理器(异常处理器可以理解为是处理该种异常的一段代码)(区分检查型异常和非检查型异常的一点技巧就是看客户端对产生的异常是否可以进行补救,如果可以通过其它程序补救,那么我们将这种异常归结为检查型异常;如果不可以补救那么就归结于非检查型异常,关键在于是否可恢复

注意:虽然符合特定特征的异常才属于RuntimeException,但是我们所涉及到的所有异常都发生在运行时

  1. 异常处理第一步:声明异常
  • 声明异常的范围:
    • 必须声明检查型异常:(从编程角度来说,有些情况下如果你的某些方法不声明异常那么编译器就会将其视为一个错误,所以我们首先需要明确的就是什么情况下我们必须要声明异常)根据之前介绍的“编译器会检查你是否为所有的检查型异常提供了异常处理器”,所以一个方法必须声明所有可能抛出的检查型异常否则编译器就会给你的程序报错。也可以理解为任何一个抛出检查型异常的方法都有可能是一个死亡陷阱,如果没有相应的处理器捕获这个检查型异常那么当前的线程就会被终止。而声明检查型异常之后当程序真的抛出了检查型异常,由于我们已经提前通知了编译器如何处理,那么编译器就会调用异常处理器去执行其内部代码(由程序员自定义),而不会终止程序。这一步的声明就好比C语言中子函数需要在头部声明一样,告诉编译器遇到异常不要慌,我已经写好了处理程序
    • 不应该声明非检查型异常:对于任何程序代码都可能抛出的我们完全控制不了的从Error类继承的异常我们不需要声明;同样,我们也不应该声明从RuntimeException类继承的异常,这类异常完全在我们控制之下,应该通过代码检查来避免。
  • 在规约中声明单个或多个检查型异常:
    • 格式:使用@throws + 异常名 + 异常描述
    • 举例:@throws NotPerfectSquareException if x is not a perfect square
    • 注意:规约中必须写明全部可能抛出的检查型异常,而无需提及非检查型异常
  • 在程序中声明单个检查型异常:
    • 格式:修改方法首部
    • 举例:public FileInputStream(String name) throws FileNotFoundException
    • 分析:这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也可能出错而抛出一个FileNotFoundException异常。如果发生了这种糟糕的情况那么构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException对象。如果这个方法真的抛出了一个FileNotFoundException对象那么运行时系统就会开始搜索知道如何处理FileNotFoundException对象的异常处理器
  • 在程序中声明多个检查型异常:
    • 格式:修改方法首部,多个检查型异常之间使用“,”分隔
    • 举例:public Image loadImage(String s) throws FileNotFoundException, EOFException

注意:根据里氏替换原则(LSP原则)(即目标是子类的多态,客户端可以用统一的方式处理不同类型的对象,子类型可以替代父类型)所以如果在子类中覆盖了(override)超类的一个方法,那么子类方法中声明的检查型异常不能比超类中声明的异常更通用(如果超类方法没有抛出任何检查型异常,那么子类也不允许抛出任何检查型异常;如果超类方法抛出了检查型异常,那么子类可以不抛出任何检查型异常也可以抛出对应的更具体的检查型异常)

注意:如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类也可能属于这个类的任意一个子类,例如FileInputStream构造器声明有可能抛出一个IOException异常,而我们很清楚IOException类下还有诸多子类,在这种情况下我们并不知道是哪种IOException异常,既可能是IOException异常,也可能是其某个子类的对象,比如FileNotFoundException异常

  1. 异常处理第二步:抛出异常
  • 抛出异常的流程:

    • 在Throwable类中的Exception类中找到相应的标准异常类或自己创建异常类(如果JDK提供的Exception类无法充分描述你的程序发生的错误的话)
    • 创建这个类的一个对象
    • 将对象抛出
  • 抛出的异常的来源

    • 从标准异常类中找现成的,即从java.lang.Throwable类中找
    • 创建自定义异常类
  • 抛出异常的来源——创建自定义异常类

    • 继承关系:我们自定义的异常类应派生于Exception类或Exception类的某个子类

    • 通常要求:我们自定义的异常类应包含两个构造器,其中一个是默认的构造器,另一个是包含详细描述信息的构造器

    • 举例:自定义一个FileFormatException异常

      class FileFormatException extends IOException
      {
          public FileFormatException(){}	// 默认构造器
          public FileFormatException(String gripe)	// 包含详细信息的构造器
          {
              super(gripe);
          }
      }
      
  • 抛出异常的格式举例1:

String readData(Scanner in) throws EOFException
{
    ...
        
    if(n < len)
        throw new EOFException();
    
    ...
}
  • 抛出异常的格式举例2:
String readData(Scanner in) throws EOFException
{
    ...
        
    if(n < len)
    {
        var e = new EOFException();
        throw e;
    }
    
    ...
}
  1. 异常处理第三步:捕获异常

抛出异常的过程非常容易,有一部分程序将异常抛出就不用再管了,但是在某些情况下你不得不在抛出异常之后进行捕获异常(比如超类方法没有抛出异常如果子类方法中存在检查型异常,根据之前的规则此时你不被允许使用抛出异常,但是编译器又要求你对该检查型异常设计了异常处理器,在这种情况下你唯一的途径就是必须捕获子类中出现的每一个检查型异常)

  • 捕获异常的格式:try/catch语句块,其中将可能抛出异常的代码块放到try语句块中,将异常处理程序放到catch语句块中
  • 捕获异常举例——捕获一个异常:(一个典型的读取数据的代码段)
public void read(String filename)
{
    try
    {
        var in = new FileInputStream(filename);
        int b;
        while((b = in.read()) != -1)
        {
            ...
        }
    }
    catch(IOException exception)
    {
        exception.printStackTrace();
    }
}
  • 捕获异常举例分析:

    • 如果try语句块中的任何代码抛出了catch中的异常,那么直接goto到catch语句块执行
    • 如果try语句块中的代码没有抛出异常,则程序顺序执行并忽略catch语句块向下执行
    • 如果try语句块中的代码抛出了catch中没有的异常,那么程序立即退出,并在控制台上打印一个消息,其中包括这个异常的类型和一个堆栈轨迹。GUI(图形用户界面)程序就会捕获异常并打印堆栈轨迹信息,然后返回用户界面处理循环
  • 捕获异常举例——捕获多个异常

    • 多个异常处理方式互不相同
    try{
       ... 
    }
    catch(FileNotFoundException e)
    {
        ...
    }
    catch(UnknownHostException e)
    {
        ...
    }
    catch(IOException e)
    {
        ...
    }
    
    • 多个异常处理方式中A和B方式相同,且A与B之间不存在子类关系
    try{
       ... 
    }
    catch(FileNotFoundException | UnknownHostException e)
    {
        ...
    }
    catch(IOException e)
    {
        ...
    }
    

    注意:在这种情况下catch(FileNotFoundException | UnknownHostException e)中由于java规定捕获多个变量时,异常变量隐含为final变量,所以接下来的处理程序代码块中不可以为e赋不同的值

    • 多个异常处理方式中A和B方式相同,且B是A的子类,那么直接用B代替A即可
  • 从异常中获取更多信息:

    • e.getMessage():得到详细的错误信息(如果有的话)
    • e.getClass().getName():得到异常对象的实际类型
  • 在catch子块中抛出异常:

    • 好处:在很深的子系统中抛出更高层次的异常,不会丢失异常的细节(有时外部用户并不想知道内部的具体异常错误,只想要知道是哪个模块有错误)
    • 举例——允许抛出异常的模块(给出异常的消息文本):
    try{
        (access the database)
    }
    catch(SQLException e){
        throw new ServletException("database error: " + e.getMessage());
    }
    
    • 举例——允许抛出异常的模块(给出异常的根原因)(更好):
    try{
        (access the database)
    }
    catch(SQLException original){
        var e = new ServletException("database error");
        e.initCause(original);
        throw e;
    }
    
    • 举例——不允许抛出异常的模块:捕获检查型异常并将其包装成一个运行时异常
  • finally子句

    • 目的:代码抛出一个异常时无论该异常是否被捕获都会停止处理该方法中的剩余代码并退出该方法,如果该方法中已经获得了只有它自己知道的一些本地资源而且这些资源必须清理就会出现问题。最直接的解决方案是捕获所有的异常完成资源的清理,再重新抛出异常,但是这种方法需要在正常代码中和异常代码中分别进行代码清理,比较繁琐,而使用finally子句就可以解决这个问题。
    • 无论是否有异常被捕获,finally子句中的代码都会被执行,下面的实例中无论何种情况程序都将关闭输入流in
    var in = new FileInputStream(...);
    try
    {
        ...
    }
    catch(IOException e)
    {
        ...
    }
    finally
    {
        in.close();
    }
    
    • try语句可以只有finally子句而没有catch子句
    • 更推荐的嵌套方式:此种方式不仅更加清楚,而且功能更强(能报告finally子句中出现的错误)
    InputStream in = ...;
    try
    {
    	try
    	{
    		...
    	}
    	finally
    	{
    		in.close();
    	}
    }
    catch(IOException e)
    {
    	...
    }
    
    • 注意:finally子句的体的功能就是用来清理资源,现有的控制流已经很复杂了,不要把改变控制流的语句(return, throw, break, continue)放入finally子句中,否则很容易出现不易察觉的严重错误(可能会遮蔽返回值,可能会吞掉异常…),总之就是不要这么做
    • (JAVA 7后,我们还有一种更巧妙的解决方案,即try-with-resources语句,实际中,后者更为常用)

断言

  1. 使用断言的目的:断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查,在保证正确性的同时不给程序增加运行负担
  2. 断言的格式:
  • assert condition:报错时抛出异常调用默认构造器
  • assert condition : message :报错时抛出异常调用包含消息的构造器,显示message.toString()
  1. 注意:由于断言的“测试时执行,程序运行时被跳过”的特性,所以一定不要出现assert list.remove(x)因为在执行时这句话会被忽略,导致list.remove(x)不会被执行,可以改为boolean found = list.remove(x); assert found;

  2. 断言只是用来检查程序内部状态是否符合规约,不要管程序之外的事情,因为程序之外的事情不受你控制,而一旦断言false整个程序就会发生终止;因此外部错误使用异常机制处理,内部可以使用断言机制

  3. 启用和禁用断言:

  • 在默认情况下,断言是禁用的

  • 启用断言:在运行程序时用-enableassertions或-ea选项启用断言

    java -enableassertions MyApp

  • 在某个类或整个包中启用断言:(将MyClass类以及mycompany.mylib包和它的子包中的所有类打开断言,其中选项-ea将打开无名包中所有类的断言)

java -ea:MyClass -ea:com.mycompany.mylib MyApp

  • 用-disableassertions或-da选项在某个类或整个包中禁用断言

java -ea:MyClass -ea:com.mycompany.mylib -da:MyClass MyApp

  • 启用和禁用所有断言的-ea和-da开关不能应用到那些没有类加载器的“系统类”上,需要换用-esa或-enablesystemassertions开关启用断言和相应格式关闭断言
posted @ 2022-06-09 03:40  dcyyyyyy  阅读(471)  评论(0编辑  收藏  举报