【Java进阶篇】第四章 异常处理

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 【Java进阶篇】第四章 异常处理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

在这里插入图片描述

程序执行过程中发生了不正常的情况,JVM将异常信息打印到控制台,从而方便调试和增强程序的健壮性。

1、异常

异常在Java中以类的形式存在,如NullPointerException,每一个异常类都可以创建异常对象。

NumberFormatException nfe = new NumberFormatException("数字格式化异常");

NullPointerException npe = new NullPointerException("空指针异常");

举个例子:

int a = 10;
int b = 0;
int c = a/b;
//当JVM执行到这个地方时,会new异常对象,即new ArithmeticException("/by zero");
//并且JVM将new的异常对象抛出,打印输出信息到控制台

类比于生活:

火灾是一种异常(一种抽象出来的类),而异常对象则是xxx地方发生火灾了。

2、类Throwable

父类 java.lang.Object<——-java.lang.Throwable类——>子类Error和Exception

插播:
UML,即Unified Modeling Language,统一建模语言。

用来描述类个类之间的关系,程序执行的流程,对象的状态等。

对应的工具有:Rational Rose,startUML

Throwable类

3、运行时异常和编译时异常

定义

  • 所有Exception类的直接子类,称编译时异常(并不是字面意思上的编译阶段发生的)该类异常,必须在编写程序时,预先对这种异常进行处理,不处理,则编译时抛错
  • RuntimeException及其所有的子类,称为运行时异常,该类异常在编写程序时,可处理,也可不处理

在这里插入图片描述
异常的发生就是new对象,而只有运行阶段可以new对象,故运行时异常和编译时异常都是发生在运行阶段。

两种异常的区别

  • 编译时异常发生的概率较高,又称受检异常、受控异常。类比生活中的:出门前看到外面正在下大雨,可预料,不打伞出门大概率生病,生病这个”异常“发生的概率很大,所以出门前要做”处理“拿伞。
  • 运行时异常发生的概率比较低,又称非受检异常、非受控异常。类比生活中:走在大街上,被天上掉下来的陨石砸中了,概率极低,没必要提前做出预处理。若处理,你活着就很累,相对应的,代码就很本末倒置。

4、异常的处理

处理思路1:

在调用者方法声明的位置上,使用throws关键字,抛给上一级,即谁调用,抛给谁

public class Exception1 {
    public static void main(String[] args) throws ClassNotFoundException {
        //这里main方法调用doSome方法,必须对这个异常进行预处理
        doSome();
    }

    //doSome方法的声明位置有throws ClassNotFoundException
    //这表示这个方法执过程中很有可能会出现ClassNotFoundException异常
    //ClassNotFoundException异常的直接父类是Exception,即它是编译时异常
    public static void doSome() throws ClassNotFoundException{

    }
}

在这里插入图片描述
😉
在Java中,异常发生之后如果一直上抛,最终抛给main方法,main方法继续向上抛到JVM,到JVM,程序会被终止执行。

处理思路2:

使用try……catch语句进行异常的捕捉

public class Exception1 {
    public static void main(String[] args) {
        try {
            doSome();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void doSome() throws ClassNotFoundException{

    }
}

try{        //尝试
	m1();
}catch(FileNotFoundException e){   //e是一个引用,保存了new出来的那个异常对象的内存地址

	System.out.println("文件不存在");   //捕捉到异常以后执行的语句
}

对于运行时异常,如ArithmeticException,可以处理,也可以不处理

try{
	System.out.println(100/0);
catch(ArithmeticException e){
	System.out.println("算术异常");
}

总结:

  • throws上抛类似于推卸责任,继续吧异常传递给调用者,让调用者来决策怎么处理(继续抛还是捕捉)
  • try…catch捕捉等于把异常拦下解决了,调用者是不知道的
    异常处理图示

几点注意事项:

在这里插入图片描述

  • 在方法的定义中,+throws 异常类名,该异常类名可以是该异常类的父类,也可以是异常类名,异常类名1,异常类名2,异常类名3
  • 不建议在main方法上使用throws,因为异常若真的发生了,一定抛给JVM,则JVM会终止程序执行。而异常机制的作用和初衷就是增强程序的健壮性(即要做到异常发生了也不影响程序的执行)
  • main方法中用try…catch ,别上抛了。

5、异常导致某些代码不能被执行

异常导致某些代码块不能被执行

总结:

  • 只要异常没有被捕捉,即采用了上报的方式,则当某行代码出现异常的时候,此方法结束,方法中的后续代码不会被执行,相当于return。再往下分析,就只看其的上抛(调用者)就好。
  • try语句块中,某一行出现异常后,该行后面的代码不会执行,catch捕捉异常后,catch后的代码继续执行。

6、try…catch总结

😉 catch后,写确切的类型、写其父类型、父父类型也可以

try{
	FileInputStream fis = new FileInputStream("D:\\not_exist.java");
//这里写的是FileNotFoundException的父类IO Exception
}catch(IO Exception e){
	System.out.println("文件不存在");
}

😉 catch可以写多个,当这些异常父类一样时,直接写一个他们的父类也行,但建议精确catch,方便调试

try{
	...
//出现异常时,从上往下匹配catch
}catch(xx e){
	...
}catch(xxx e){
	...
}catch(xxxx e){
...
}

😉 catch写多个的时候,从上到下,必须遵循从小到大,若父类异常在上,则catch匹配时,父类下的精确子类一定轮不上执行,会报错。

😉 JDK8中,允许使用或”|”来连接多个异常

try{
	...
}catch(FileNotFoundException|ArithmeticException|NullPointerException e){
	...
}

上抛和捕捉如何选择?
——

如果希望调用者去处理,则用throws抛给调用者,反之,自己处理则try…catch

7、异常对象的常用方法

Exception exception = new Exception();

1)getMessage()方法

//作用:获取异常的简单描述信息
String msg = exception.getMessage();

2)printStackTrace()方法

//作用:打印异常追踪的堆栈信息
exception.printStackTrace();

举例:

public class Exception2 {
    public static void main (String[] args){
    	//异常的发生即new异常对象,那这行代码会导致程序终止运行吗?
    	//不会,这里只是new了一个异常对象,但并没有将异常对象throws抛出
    	//因此JVM会认为这是一个普通的Java对象
        NullPointerException e = new NullPointerException("发生空指针异常");
        
        String msg = e.getMessage();
        //getMessage方法的输出,其实是用有参构造方法传入的那个String
        System.out.println(msg);
        e.printStackTrace();
        System.out.println("Hello World!");
    }
}

运行结果:

多次运行可以看到,Hello World一会儿在堆栈信息之上,有时在堆栈信息之下,这是因为Java打印异常堆栈信息追踪信息的时候,采用的是异步线程的方式。

运行结果

Tip:

printStackTrace打印堆栈信息的结果也为红色,不是编译器出错了,如果是出错了,那下面的代码是不会执行的。错误分析时,从上往下看异常,下面的异常通常是上面的导致的。此外注意看包名,sun公司包后面的信息可以适当跳过。

8、try…catch和finally

  • 😉 在finally子句中的代码是最后执行的,并且一定会执行,即使try语句块中的代码出现了异常
  • 😉 finally子句必须和try一起出现
  • 😉 通常在finally语句块中完成资源的释放/关闭,以保障资源一定被关闭
    在这里插入图片描述
  • 😉 也可无catch,直接try+finally
    在这里插入图片描述
  • 😉 退出JVM时,finally语句不执行
将上面try语句块中的return;改为:

System.exit(0);
//再运行,则只输出try,finally中的语句不再执行

整理一个finally的坑,或许面试用得上:

public class Exception2 {
    public static void main (String[] args){
        int result = m();
        System.out.println(result);   //100
    }
    public static int m(){
        int i = 100;
        try{
            return i;
        }finally{
            i++;
        }
    }
}
-----
//按照刚学的执行顺序,应该是先i++,再return,但这里是特殊
//使用反编译工具可以看到m()方法其实是这样:
public static int m(){
	int i = 100;
	int j = i;
	i++;
	return j;
}

9、final、finally和finalize的整理区分

  • final是一个关键字,表示最终的,其修饰的类无法继承、修饰的方法无法覆盖、修饰的变量不能重新赋值
  • finally是一个关键字,和try一起出现,finally语句块中的代码必须执行(除非中途退出JVM)
  • finalize是Object类中的方法名(即标识符),该方法由GC机制调用

10、如何自定义异常

SUN提供的JDK内置异常不够用,实际的开发中有的异常和业务挂钩,这种异常需要我们自己定义,自定义异常的步骤是:

  • 编写一个类继承Exception(编译时异常)或者继承RuntimeException(运行时异常)
  • 提供两个构造方法,一个无参数的,一个带有String参数的
public class MyException extends Exception{
	public MyException(){
	}
	public MyException(String s){
		super(s);
	}
}

11、异常在实际开发中的作用

以下以数组模拟压栈弹栈为改良例子:

//之前的栈满是通过return+println来表达异常
public void push(Object obj){
        if(this.index >= this.elements.length-1){
            System.out.println("压栈失败,栈满");
            return;
        }
        this.index++;
        this.elements[index] = obj;
        System.out.println("压栈"+obj+"成功,栈帧指向"+ index);
    }

栈满是一种异常,我们用异常机制来改良:

/**
 * 改良后的压栈方法
 */
public void push2(Object obj) throws MyStackOperationException {
    if(index >= elements.length-1){
    	//创建异常对象,并将其抛出
        MyStackOperationException e = new MyStackOperationException("栈满");
        throw e;
        //抛出异常后,因为这个自定义异常我写的是编译时异常,故要预处理
        //这里选择throws,我定义异常的目的就是把栈满的异常信息传递出去
        //自己定义再自己捕捉,一切都没意义了,所以在方法名定义的末尾加上throws
    }
    this.index++;
    this.elements[index] = obj;
    System.out.println("压栈"+obj+"成功,栈帧指向"+ index);
}
throw new MyStackOperationException("栈满");

用这种抛异常来代替return + println

注意throw和throws的区别:

  • throws:在方法声明的位置上使用,表示上报异常信息给调用者
  • throw:手动抛出异常

12、异常与方法覆盖

覆盖那一章中提到:重写之后的方法,不能比重写之前的方法抛出更多或者更宽泛的异常,可以相等或者更少

class Animal{
    public void doSome(){
        
    }
    public void doOther() throws Exception{
        
    }
}
class Cat extends Animal{
    //抛的更多了,error
    public void doSome throws Exception{
        
    }
    //抛出更少,√
    public void doOther(){
        
    }
    //抛出一样的,√
    public void doOther2() throws Exception{
        
    }
    //抛出范围更小的,√
    public void doOther() throws NullPointerException{
        
    }
    //抛出运行时异常,√
    public void doOther() throws RuntimeException{
        
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/146101.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!