【自学】Java核心技术卷1_7.1-7.3异常

参考博客1博客2

7 异常、断言、日志

Java中的三种处理系统错误的机制:

  • 异常处理:捕获异常情况并处理(将控制权从错误产生的地方转移给能够处理这种情况的错误处理器)
  • 断言:有选择的弃用检测(测试期间用检测验证程序操作的正确性)
  • 日志:记录出现的问题,以备日后分析

7.1 处理错误

  • 如果由于出现错误而使得某些操作没有完成, 程序应该返回到一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序。
  • 如果某方法不能采用正常途径完成它的任务,可以抛出一个封装了错误信息的对象,且这个方法立即退出不返回任何值
  • 程序中可能出现的问题:用户输入错误、设备错误、物理限制、代码错误

7.1.1 异常分类

  • 异常指不期而至的各种状况,是发生在程序运行期间的事件,干扰正常的指令流程。
  • Java通过API中的Throwable类的众多子类描述不同的异常,即异常都是对象,都是Throwable子类的实例。异常描述了出现在一段编码中的错误条件,当条件生成时,错误将引发异常。
  • 异常具有自己的语法和特定的继承结构

https://i-blog.csdnimg.cn/blog_migrate/ee5214d21eef6272ca94fb4a54357857.jpeg

  • Error程序无法处理的错误。表示应用程序中较严重的问题,这些错误表示故障发生于虚拟机自身,或者发生在虚拟机试图执行应用时(即代码运行时JVM出现的问题),如系统内部错误和资源耗尽错误。这些异常发生时,JVM一般会选择的线程终止。
  • Exception:程序本身可以处理的异常。
  • RuntimeException运行时异常:“JVM常用操作”引发的错误,即由程序错误导致的异常(错误的类型转换、数组访问越界、访问null指针…),一般是由程序逻辑错误引起的,应该从逻辑角度尽可能避免这类异常。这些异常是不可查异常,可以选择捕获处理,也可以不处理。
  • 非运行时异常:RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常
  • IOException:程序本身没问题,但由于像I/O错误这类问题导致的异常(试图在文件尾部后面读取数据、试图打开不存在的文件、根据给定的字符串查找不存在的类的对象…)
  • Unchecked不可查异常:派生于Error类或RuntimeException类的所有异常
  • Checked可查异常:正确的程序中运行中很容易出现的、情理可容的所有其他的异常。编译器将核查是否为所有的checked异常提供了异常处理器,即要么try-catch语句捕获它,要么throws字句声明抛出它,否则编译不通过。

Java异常处理机制:抛出异常,捕获异常

  • 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象包含了异常类型和异常出现时程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行
  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。潜在的异常处理器是异常发生时依次存留在调用栈中的方法集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止,即Java程序终止。

7.1.2 throws声明checked异常

  • 一个方法所能捕获的异常,一定是Java代码在某处所抛出的异常,即异常总是先被抛出,后被捕获。
  • 任何Java代码都可以抛出异常(自己编写的代码、来自Java开发环境包中的代码、Java运行时系统)
  • 如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句声明抛出异常(抛给调用者处理):methodname throws Exception1,Exception2,…{ }
  • throws语句用于在方法定义时声明该方法要抛出的异常类型,多个异常可用逗号分隔,如果抛出的是Exception异常类型,则该方法被声明为抛出所有异常。(用throws抛出可查异常,不可查异常要么不可控制,要么就如RuntimeException应该避免发生)

遇到下面的情况时应声明抛出异常:

  • 调用一个抛出checked异常的方法(也可以捕获处理)
  • 程序运行过程中发现错误,并且利用throw语句抛出一个checked异常
  • 程序出现错误
  • Java虚拟机和运行时库出现的内部错误

对于子类覆盖超类方法:

  • 1)如果超类方法没有抛出任何checked异常,子类方法也不能抛出任何checked异常
  • 2)子类方法中声明的checked异常不能比超类方法中声明的异常更通用,即子类可以抛出更特定的异常,或者声明不抛出异常,不能声明与被覆盖方法不同的异常

对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同:

  • 1)由于RuntimeException的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将Java运行时系统自动抛出,允许应用程序忽略运行时异常。
  • 2) 对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
  • 3)对于所有的可查异常,Java规定:一个方法必须捕捉或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

总的来说Java规定:对不可查的RuntimeException和Error允许忽略;对可查异常必须捕获或者抛出

7.1.3 throw抛出异常

对已存在的异常类,抛出异常的过程:

  • 找到合适的异常类
  • 创建这个类的一个对象
  • 将对象抛出

// Java异常语法规则例程:

void method1() throws IOException{}  //合法 

//编译错误,必须捕获或声明抛出IOException 
void method2(){ 
  method1(); 
} 

//合法,声明抛出IOException 
void method3()throws IOException { 
  method1(); 
} 

//合法,声明抛出Exception,IOException是Exception的子类 
void method4()throws Exception { 
  method1(); 
} 

//合法,捕获IOException 
void method5(){ 
 try{ 
    method1(); 
 }catch(IOException e){…} 
} 

//编译错误,必须捕获或声明抛出Exception 
void method6(){ 
  try{ 
    method1(); 
  }catch(IOException e){throw new Exception();} 
} 

//合法,声明抛出Exception 
void method7()throws Exception{ 
 try{ 
  method1(); 
 }catch(IOException e){throw new Exception();} 
}

7.1.4 创建异常类

  • 创建自定义异常类:定义一个派生于Exception或其子类的类
  • 习惯上自定义的类应该包含两个构造器:默认构造器、带有详细描述信息的构造器(超类Throwable的toString方法将会打印这些详细信息,在调试中很有用)

在程序中使用自定义异常类,大体可分为以下几个步骤:

  • 1)创建自定义异常类。
  • 2)在方法中通过throw关键字抛出异常对象。
  • 3)如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
  • 4)在出现异常方法的调用者中捕获并处理异常。

7.2 捕获异常

  • 方法应该捕获并处理知道如何处理的异常,也可以将不知道如何处理的异常传递给调用者(用throws说明符声明抛出异常),如果调用了一个抛出checked异常的方法,就必须对它进行处理,或者继续传递
  • 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容

几种异常处理情况:

  • 1)try语句块中的代码抛出在catch子句中说明的异常类(或其子类)时,程序跳过try语句块其余代码并执行catch子句中的处理器代码,一经处理结束就意味着整个try-catch语句结束,其他的catch子句不再有匹配和捕获异常类型的机会
  • 2)try语句块中的代码没有抛出任何异常,程序就跳过catch子句
  • 3)如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,这个方法就立刻退出

7.2.2 捕获多个异常

  • 在一个try语句块中可以捕获多个异常类型,并对不同类型的异常作出不同的处理(为每个异常类型使用一个单独的catch子句);对异常的处理方式相同时,也可以用同一个catch子句捕获多个异常类型(捕获的异常类型彼此之间不存在子类关系)
  • 捕获多个异常时,异常变量隐含为final变量
  • 对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层(Exception最高)的异常类的catch子句放在后面,否则捕获高层异常类的catch子句可能会屏蔽捕获底层异常类的catch子句

7.2.3 再次抛出异常与异常链

在catch子句中可以抛出一个异常,目的是改变异常的类型:

  • 抛出的异常可用带有异常信息文本的构造器来构造
  • 也可以将原始异常设置为新异常的“原因”

 

7.2.4 finally子句

  • 不管是否有异常被捕获,finally子句中的代码都被执行
  • 需要finally子句的情景:当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题

执行finally子句的情况

  • 代码没有抛出异常:程序首先执行try语句块中的全部代码,然后执行finally子句中的代码
  • 代码抛出一个在catch子句中捕获的异常:程序将执行try语句块中的代码,直到发生异常,然后跳过try中剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码
  • 代码抛出了一个异常,但这个异常不是由catch子句捕获的:程序将执行try中代码直到出现异常,然后跳过try剩余代码,然后执行finally子句的的代码,并将异常抛给JVM处理,但finally块后的语句不会被执行

不执行finally块的特殊情况:

  • 在finally块中发生异常
  • 在finally前面的代码中用了System.exit()退出程序
  • 程序所在的线程死亡
  • 关闭CPU(计算机断电、失火、遭遇病毒攻击)

try-catch-finally规则:

  • 必须在try之后添加catch或finally块,try后可同时接catch和finally块,但至少一个块
  • Catch块应与相应的异常类的类型相关
  • 含有多个catch块时,只执行第一个匹配块
  • try-catch-finally结构可嵌套,还可重新抛出异常
  • 除了不执行finally的特殊情况外,总将执行finally作为结束

  • finally子句若包含return语句,而try语句块也包含return语句的话,finally中的返回值将覆盖try中的返回值
  • finally子句也可能抛出异常,如果try和finally都有异常抛出,则try中的原始异常将会丢失转而抛出finally中的异常——可以通过适当处理,重新抛出原来的异常(如在finally中要求try没有抛出异常时,finally才能抛出异常)

7.2.5 带资源的try语句

  • 带资源的try语句最简单的形式如下:
  • AutoCloseable和Closeable接口提供了close方法,如果资源属于实现了AutoCloseable或Closeable接口的类,则在try块退出时(不论是正常退出还是存在异常退出,都会调用close方法
  • 也可以指定多个资源,不论这个try块如何退出,in和out都会关闭

  • 当try块抛出一个异常,close方法也抛出一个异常时,try块的异常会重新抛出,close的异常会“被抑制”,close的异常将自动捕获,并由addSuppressed方法增加到原来的异常(用getSuppressed方法可以得到close抛出并被抑制的异常列表)
  • 带资源的try语句自身也可以有catch子句和finally子句,这些子句会在close关闭资源后执行(不过try语句中不建议加入太多内容)

7.2.6 分析堆栈轨迹元素

  • 堆栈轨迹(stack trace):方法调用过程的列表,包含程序执行过程中方法调用的特定位置
  • 当程序正常终止,没有捕获异常时,堆栈轨迹就会显示
  • printStackTrace():访问堆栈轨迹的文本描述信息,printStackTrace(PrintWriter p)
  • getStackTrace():得到StackTraceElement对象的一个数组,StackTraceElement[] getStackTrace()
  • StackTraceElement类:含有能够获得文件名和当前执行代码行号的方法,还有能够获得类名和方法名的方法
  • Treasd.getAllStackTrace():产生所有线程的堆栈轨迹

7.3 异常机制的使用技巧

  • 1)异常处理不能代替简单的测试。即尽量避免使用异常,将异常情况提前检测出来。捕获异常花费的时间超过简单测试,应该只在异常情况使用异常机制。
  • 2)不要过分地细化异常。不要为每个可能会出现遗产更多语句都设置try-catch,即不要把每条语句都分装在一个独立的try语句块中,应将整个任务包装在一个try语句块中,这样当任何一个操作出现问题时,整个任务都可以取消,从而将正常处理与错误处理分开
  •  
  • 3)利用异常层次结构。避免总是catch Exception或Throwable,而要catch更具体的异常类或创建自己的异常类,这样可以根据不同的异常做不同的处理,使程序更加清晰。
  • 4)不要压制异常。将不能处理的异常往外抛,而不是捕获之后随便处理。
  • 5)在检测错误时,“苛刻”比放任更好。比如对空栈pop时,抛出EmptyStackException异常比返回一个null,等以后抛出NullPointerException异常好——早抛出
  • 6)不要羞于传递异常。当异常发生时,不应立即捕获,而是应该考虑当前作用域是否有能力处理这一异常,如果没有则应该将异常继续向上抛出,交由更上层作用域来处理(有时候让高层次的方法通知用户发生了错误,或者放弃不成功的命令比捕获异常更好)——晚捕获
  • 7)不要在循环中使用try-catch,尽量将其放在循环外或者避免使用
       // 异常使用技巧例程:

        //1、尽量避免使用异常,将异常情况提前检测出来
        Stack<Object> stack=new Stack();
        String str="123";
        stack.push(str);
        try{
            stack.pop();
        }catch(EmptyStackException e){
            stack.pop();
        }
        //应该用下面的方式,避免使用异常
        if(!stack.isEmpty()){
            stack.pop();
        }

        //2、不要为每个可能会出现异常的语句都设置try-catch
        try{
            stack.pop();
        }catch(EmptyStackException e){
            //...
        }
        try{
            Double.parseDouble(str);
        }catch(NumberFormatException e){
            //...
        }
        //应该用下面的方式,将两个语句放在一个try中
        try{
            stack.pop();
            Double.parseDouble(str);
        }catch(EmptyStackException e){
            //...
        }catch(NumberFormatException e){
            //...
        }

        //3、避免在方法中抛出或捕获RuntimeException和Error
        String[] array;
        try{
            array=new String[100];
            //array = new String[1000000];此时会出现OutOfMemoryError异常
        }catch(OutOfMemoryError e){
            throw e;
        }
        //应直接用下面的代码
        array=new String[100];

        //4、避免总是catch Exception或Throwable,应捕获具体的异常
        try{
            stack.pop();
        }catch(Exception e){
            //应catch具体的EmptyStackException
        }

        //5、不要压制、隐瞒异常,将不能处理的异常往外抛,而不是捕获后随便处理
        try{
            Double.parseDouble(str);
        }catch(NumberFormatException e){
            //...假设此处为随便处理
            throw e;  //抛出不能处理的异常
        }

代码练习:

public class ExceptionTest {
    public static int division(int x,int y) throws MyException{  //声明方法抛出异常
        if(y<0){
            throw new MyException("异常:除数不能为负数");  //抛出异常
        }
        return x/y;
    }
    public static void main(String args[]){
        int x=1,y=-1;
//        int x=1,y=0;
//        int x=1,y=1;
        try{
           int result=division(x,y);  //调用会抛出异常的方法
            System.out.println(x+"/"+y+"="+result);
        }catch(MyException e){    //捕获并处理异常
            System.out.println(e.getMessage());
        }catch(ArithmeticException e){  //ArithmeticException异常属于运行时异常,division没有捕获,将此异常上传给其调用者main方法
            System.out.println("异常:除数不能为0");
        }catch(Exception e){
            System.out.println("发生了其他异常");
        }finally{
            System.out.println("code end");
        }
    }
}

class MyException extends Exception{
    String message;
    public MyException(String message){
        this.message=message;
    }
    public String getMessage(){
        return message;
    }
}

最后,对于参考博客1中的引子问题的思考:main中调用testEx,testEx调用testEx1,testEx1调用testEx2,testEx2中 i==0时发生异常,catch捕获并处理后执行testEx2的finally,然后返回testEx1时,异常已被处理,testEx1的try块正常执行完后执行finally,然后返回testEx中正常执行完try和finally,再返回main执行完try,跳过catch,程序结束

package Test;
 
public class TestException {
	public TestException() {
	}
 
	boolean testEx() throws Exception {
		boolean ret = true;
		try {
			ret = testEx1();
		} catch (Exception e) {
			System.out.println("testEx, catch exception");
			ret = false;
			throw e;
		} finally {
			System.out.println("testEx, finally; return value=" + ret);
			return ret;
		}
	}
 
	boolean testEx1() throws Exception {
		boolean ret = true;
		try {
			ret = testEx2();
			if (!ret) {
				return false;
			}
			System.out.println("testEx1, at the end of try");
			return ret;
		} catch (Exception e) {
			System.out.println("testEx1, catch exception");
			ret = false;
			throw e;
		} finally {
			System.out.println("testEx1, finally; return value=" + ret);
			return ret;
		}
	}
 
	boolean testEx2() throws Exception {
		boolean ret = true;
		try {
			int b = 12;
			int c;
			for (int i = 2; i >= -2; i--) {
				c = b / i;
				System.out.println("i=" + i);
			}
			return true;
		} catch (Exception e) {
			System.out.println("testEx2, catch exception");
			ret = false;
			throw e;
		} finally {
			System.out.println("testEx2, finally; return value=" + ret);
			return ret;
		}
	}
 
	public static void main(String[] args) {
		TestException testException1 = new TestException();
		try {
			testException1.testEx();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值