程序员

Java基础进阶 异常详解

2018-06-07  本文已影响50人  Villain丶Cc

1、异常

1.1、异常介绍

异常:不正常。
生活中的异常:
例:在上课时,突然间停电,造成上课终止。 处理:等待来电、使用备用发电机。

程序中的异常:
程序在运行的过程,出现了一些突发状况,造成程序无法继续运行。我们把上述的突发状况,无法正常运行的这些状态称为Java中的异常。

Java中的异常:就是程序中出现的错误(bug),或者不正常现象。而我们在开发程序的时候,就需要对这些问题进行预先的判断和处理。

学习Java中的异常,我们需要研究:
1、什么是异常;
2、异常问题怎么去解决和处理;
3、我们自己怎么把问题报告给程序的使用者;
我们编写程序的时候,在程序中肯定会有问题(bug)出现,而sun公司把开发中最最常见的一些问题,进行总结和抽取,形成了一个体系,这个体系就是我们要学习的异常体系。

1.2、异常发生过程

需求:根据用户指定的下标,输出该下标对应的数据。
1)定义一个类ExceptionDemo;
2)定义一个主方法,在主方法中定义一个int类型的数组,在这个数组中存放int类型的数据;
3)在这个类中自定义一个函数getValue,在这个函数中根据调用者传递过来的下标和数组返回给调用者
对应的数据;
4)在主方法中调用自定义函数getValue,接收自定义函数返回回来的数据,并将数据打印到屏幕上;

1.png 2.png

说明:上述代码发生异常的过程:
1)jvm先去调用main函数,在main函数中调用了getValue函数,然后jvm将getValue函数加载到内存中;
2)Jvm在执行getValue函数的时候,由于数组下标index的值3超过了数组的最大下标的范围,所以在这里发生了异常问题,ArrayIndexOutOfBoundsException,这样导致程序就不会向下执行,jvm会在发生异常的地方停止,jvm会将发生异常信息(发生异常的位置、异常内容、异常类型等)封装到一个类中(Java的异常类),然后把这个封装了异常信息类的对象(new 异常类)丢给了当前调用函数的地方。
3)如果发生异常,jvm会自动创建封装了异常信息的异常类的对象,然后将对象使用throw关键字抛给调用getValue函数的地方。
4)由于getValue函数把问题抛给了main函数,所以导致了main函数中也有了异常,而main函数中的异常是被迫接收的,此时main函数中并没有解决此异常的解决方案,但是main函数是jvm调用的,所以main函数又将异常抛给了jvm虚拟机,jvm已经是底层了,不能再将异常抛出,jvm需要对这个问题进行处理,即将这个问题显示到屏幕上,让程序的使用者看到。

1.3、异常简单应用举例

需求:代码和上述代码相同,我们要解决上述问题的发生。
1)由于在自定义函数中两个参数都是接收外界传递过来的,我们为了保证程序的健壮(合法)性,
所以我们要对传递过来的数据进行合法性的判断;
2)分别对下标和数组进行判断,如果不合法,将发生问题的异常抛出去给程序员看;

/*
    针对发生的异常进行简单的处理
*/
class ExceptionDemo1
{
    public static void main(String[] args) 
    {
        //定义数组
        int[] arr={1,2,5};
        //int value=getValue(arr,1);
        int value=getValue(arr,1);
        System.out.println(value);
    }
    //定义函数根据指定的下标返回对应的值
    public static int getValue(int[] arr,int index)
    {
        /*
            以后在开发中,定义函数的时候,对外界传递过来的参数一定要
            合法性的判断
            这里需要对错误数据进行判断,然后将错误信息报告给调用者
            在实际开发中我们一般会给固定的文件写错误信息(错误文档)
        */
        //System.out.println("haha");
        /*
         * 发生空指针异常的地方,一定是使用了某个引用变量,而这个引用变量又不指向任何的对象
         * 也就是说引用变量中保存的是null。这是使用空的引用调用属性或行为,而由于根本引用不指向
         * 任何的对象,那么就没有属性和行为而言,更无法去调用了。肯定就发生空指针异常。
         */
        if(arr==null)
        {
            throw new NullPointerException("对不起,数组引用变量的值不能为null");
        }
        if(index<0 || index>=arr.length)
        {
            throw new ArrayIndexOutOfBoundsException("下标越界了。。。");
        }
        return arr[index];
    }
}

1.4、Java异常体系介绍

我们书写程序,肯定会有问题的发生,这些问题统称为异常。而sun公司把最常见的一些异常进行类的描述和封装。然后我们如果在程序中遇到了这些问题,就可以直接通过这些描述异常的类进行错误信息的封装。然后把这些信息丢给程序的调用者。

研究Java中众多的异常类:
Throwable:它在java.lang包中。
Throwable这个类描述的是Java中所有异常和错误的共性内容。


3.png

1.5、异常的分类

Java中异常体系:
Throwable类(顶层的父类):Java中异常的顶层类。
Error(错误类)(了解):在程序运行时,会产生一些错误信息。java把这些错误信息使用Error或其子类进行描述。
错误属于系统级别的,是由于JVM在操作内存时(JVM需要借助操作系统来实现内存的操作),出现了一些不正常的操作,造成内存错误,出现错误后操作系统就会把这个错误返回给JVM。
在程序中,遇到错误时,java没有针对性的解决方案,只能通过修改源代码的方式来解决程序中的错误问题。

Exception(异常类)(掌握):在程序运行时,也会出现一些异常状况。表示Java程序中存在的 异常问题,而不是错误问题。这些异常问题,在程序中通过判断等形式是可以检测并且预 防的。针对这些异常问题,程序员在写代码的时候一旦发生,必须给出有效的解决方案。
java对于异常状况是有针对性的解决方案(异常处理),例:角标越界、空指针异常等。

异常状况的发生,通常是JVM在操作一些数据时,出现的问题,java对于异常的发生, 是可以通过一些手段(捕获)避免程序终止运行,保证让程序继续向下正常执行。

异常的分类:
1)编译时异常:Exception
在程序中如果书写了编译时异常,那么编译时期,编译器会对所书写的异常进行检测,检测该异常是否有针对性的处理方案(声明或捕获,要么让调用者知道,要么自己处理掉),如果没有针对编译时异常的处理方案,编译时会报错。
Exception:如果在程序中我们手动的使用throw关键字 抛出 Exception对象,这时在进行源代码的编译 时,编译器会对抛出的这个异常进行检测。要求在抛出这个异常的函数中对这个抛出的异常进行处理。处理的方案是捕获或者声明。如果我们函数中抛出了Exception异常对象,这时需要把这个异常 告诉调用者的话,需要在函数上使用 throws来显示声明函数中被抛出的Exception。
这样当调用者调用我们这个函数的时候,调用者就在调用之前已经知道这个函数中有异常发生,如果调用者依然调用,这时程序要求调用者应该给出异常的处理方案。
总结:声明异常的目的是为了让调用者进行处理。

需求:
1)定义一个ExceptionDemo2类;
2)在这个类中定义一个div函数,接收调用者传递过来的两个int类型的参数,保存到a和b中;
3)使用判断结构判断除数b等于0,使用throw抛出Exception异常,如果除数不是0,则使用return关键字返回a/b的结果;
4)在主函数中接收并打印最终返回来的结果;


4.png

Exception:它表示的是Java中所有的异常的统称。在Exception的下面,有一个特殊的子类RuntimeException。

2)运行时异常:RuntimeException
在程序中书写了运行时异常,在编译时期,不会对运行时异常进行检测。但是,在程序运行时如果发生了异常,并且也没有针对发生异常的处理方案,此时程序会终止运行。
RuntimeException:如果在函数中抛出的RuntimeException,这时这种异常在编译的时候,编译器不会 处理。只有运行代码的时候,如果真的发生了,这时程序就会因为这个异常而停止运行。


5.png

总结:对于RuntimeException运行时异常,不需要捕获和声明。
为什么抛出RuntimeException异常,不需要捕获和声明呢?
不声明不捕获的目的是不让调用者进行处理,就是为了让程序停止,让调用者看到现象,并进行代码的修正。

什么时候使用运行异常呢?
如果通过参数传递的方式导致功能(函数)失败的话,调用者程序不需要再运行的话,我们就可以使用RuntimeException。也就是说我们不想让程序继续运行,想让程序停止的时候,如果报异常我们需要修改代码的时候,这时就可以使用RuntimeException运行异常。

小结:

 RuntimeException和Exception有什么区别:
 Exception属于编译时异常,编译器在编译时会检测该异常是否异常的处理方案
 RuntimeException属于运行时异常,编译器不会检测该异常是否有异常的处理方案,不需要声明。

说明:在Exception的所有子类异常中,只有RuntimeException不是编译异常,是运行时异常,其他子类都是编译异常。

关于异常体系和异常分类如下图所示:


6.png

2、异常应用

2.1、自定义异常(掌握)

我们知道sun公司把最常见的一些问题和程序运行中的不正常现象抽取成不同的类,描述成异常类。可是在实际开发中我们所写的程序会遇到一些其他问题,针对这些问题sun公司并没有提前给出对应的异常类。
可以使用自定义异常解决以上问题,需要开发人员在程序中自己手动的定义异常类。

自定义异常:就是由开发人员书写属于自己的异常类。

注意:自定义异常类是建立在java提供的异常类基础之上(必须继承java中的异常类)。也就是说我们自己定义的类要是异常类的话,要求我们的自己定义的类也必须是Throwable下的一个子类。

我们自己定义的异常类,可以继承Throwable,但是在开发中一般不会去继承Throwable。如果描述的是错误应该继承Error类,如果是普通的异常应该继承Exception类。

一般我们不会描述Error,而主要在项目中描述异常信息:

如果自己定义的异常,需要编译器在编译的时候检测,这时就继承Exception,如果编译的时候不需要检测,这时就继承RuntimeException。

异常也是一个类,那么就和我们学习面向对象中定义类没有区别:

格式:

public class 异常类的名字  extends Exception / RuntimeException
{
//不需要任何的属性和行为,仅仅只需要提供构造函数即可。
public 异常类的名字(){}
public 异常类的名字( String message ){
super(message);
}
}

注意:不需要任何的属性和行为,仅仅只需要提供构造函数即可。

2.2、异常的声明和捕获介绍

不管是什么异常,作为程序员,我们在开发中,最后一定要对这个异常进行处理。
我们要对异常进行处理:
首先自己要清楚自己当前写的代码是因为调用别人的程序被动接受的异常,还是自己的程序中主动抛出的异常。
如果自己是异常问题的抛出者(在自己的函数中使用了throw 关键字抛出了异常对象),这时如果抛出的这个异常是编译器时期异常,那么在自己的函数上需要使用throws 对抛出函数进行声明。
如果自己是程序的调用者,调用了别人的程序,而别人的程序中有异常发生,导致自己的程序有异常了,这时自己就要考虑,当前这个异常自己要不要继续抛出,如果自己还需要抛出,可以继续在自己的函数上使用throws 继续声明,如果在自己的函数中这个异常不能再往出抛,这时只能捕获。

编译异常的处理方案有2种:

1、声明:
就是遇到异常时,自己不处理,把异常交给别人处理。
在自己的函数中如果有异常,但这个异常需要告诉给调用者,这时直接在自己的函数上使用throws关键字声明这个异常。
声明的格式:

   修饰符 返回值类型  函数名(参数列表)  throws 异常类 <,异常类2>  {//(<…>表示可选)

      //代码

      异常

}

注意:
1)throws后面可以跟多个异常类,使用逗号分隔;
2)在函数中如果发生了异常,jvm会拿发生的异常和声明的异常类进行匹配,如果匹配才会把发生的异常交给别人处理;

需求:声明的简单使用
1)定义一个ThrowDemo类;
2)在定义一个Demo类,在这个类中定义一个函数show,在show函数中对传入的参数x进行判断,如果x等于0,则使用throw关键字抛出Exception异常;
3)由于抛出的是编译时异常所以需要在show函数上使用throws关键字声明这个异常,告诉调用者,有异常;
4)在show函数中打印x的值;
5)在ThrowDemo类中创建Demo类的对象,使用对象调用函数;


8.png

注意:在开发中,main函数中不会出现声明,在main函数通常是使用捕获。

2、捕获:

就是遇到异常时,不再把异常交给他人处理,自己处理。

在程序中有异常,但这个异常我们不能继续使用throws声明,这时不处理,程序无法编译通过,那么在程序中只能使用捕获方式解决问题。

格式:

try{

//书写有可能发生问题的代码

}catch( 需要捕获的异常类名  变量名 ){

//具体的异常处理代码

}
public class ThrowDemo {
    /*
     * 一般在开发中我们不会在主函数上面抛异常,主函数一般是最后处理者,
     * 我们需要在主函数中对异常进行处理------》捕获
     */
    public static void main(String[] args){
        //创建Demo类的对象
        Demo d=new Demo();
        try
        {
            d.show(0);//异常代码
        }catch(Exception e)//Exception e=new Exception("x不能等于0")
        {
            //处理异常的代码
            //System.out.println("hahhahaha");
//          System.out.println(e.getMessage());
//          System.out.println(e);
            e.printStackTrace();
        }
    }
}

总结:不管自己是函数的定义者,还是函数的调用者,只要是在自己的函数中有异常发生,那么自己都可以使用上述的两种方案对异常进行处理。

2.3、声明和捕获的使用场景

声明和捕获的使用场景:

声明:
当在自己的函数中有异常,而这个异常自己内部是不能直接消化掉。就需要在函数上使用throws进行内部的异常声明。
只要在函数中把这个异常声明出去了,这时针对这个函数就相当于把异常处理掉了。

捕获:
在自己的函数中有异常了,这时自己不能进行异常的声明,这时只能内部把这个异常捕获掉。
只能在函数中使用

try{

// 可能发生异常的代码

}catch( 捕获的异常的类名  变量名 ){

//处理异常

}

2.4、声明和捕获的举例

需求:定义一个类,描述矩形,提供计算面积的功能。要求对长和宽进行判断, 如果非法直接抛出长或宽非法异常。

分析和步骤:
1)定义一个类Rectangle描述矩形,在这个类中分别定义长length和宽width两个私有属性,并对外提供get和set方法;
2)在Rectangle类中定义一个构造函数分别给长length和宽width初始化值;
3)在这个类的构造函数中分别对length和width两个属性进行判断,如果长和宽不合法,分别对长和宽进行抛异常,同时构造函数要声明异常;
4)定义一个类IllegalWidthException对非法的宽进行异常处理,在这个类中分别定义无参构造函数和有一个参数的构造函数,同时这个类需要继承Exception类,这样在编译的时候就可以检测异常;
5)定义一个类IllegalLengthException对非法的长进行异常处理,在这个类中分别定义无参构造函数和有一个参数的构造函数,同时这个类需要继承Exception类,这样在编译的时候就可以检测异常;
6)在Rectangle类中定义一个计算矩形面积的函数getArea,将最终的面积值返回给调用者;
7)定义一个异常测试类ThrowTest,在这个测试类中创建矩形类Rectangle的对象,并通过对象调用计算圆的面积的函数getArea并打印,同时并对异常进行捕获处理;

创建的类在eclipse的结构图如下所示:


9.png
/*
 * 需求:定义一个类,描述矩形,提供计算面积的功能。
 * 要求对长和宽进行判断, 如果非法直接抛出长或宽非法异常。
 */
//自己定义长和宽非法的异常
class IllegalWidthException extends Exception{

    public IllegalWidthException() {
        super();
    }

    public IllegalWidthException(String message) {
        super(message);
    }
}

//非法的长度异常
class IllegalLengthException extends Exception{

    public IllegalLengthException() {
        super();
    }
    public IllegalLengthException(String message) {
        super(message);
    }
}

//描述矩形
class Rectangle{
    //长和宽
    private double length;
    private double width;
    
    public Rectangle(double length, double width) throws IllegalLengthException, IllegalWidthException {
        //对长和宽进行合法的验证
        if( length <= 0 ){
            throw new IllegalLengthException("非法的矩形长度");
        }
        
        if( width <= 0 ){
            throw new IllegalWidthException("非法的矩形宽度");
        }
        this.length = length;
        this.width = width;
    }
    
    public double getLength() {
        return length;
    }
    public void setLength(double length) {
        this.length = length;
    }
    public double getWidth() {
        return width;
    }
    public void setWidth(double width) {
        this.width = width;
    }
    
    //计算面积
    public double getArea(){
        return this.length * this.width;
    }
    
}

public class ThrowTest {
    public static void main(String[] args) {
        
        //创建矩形对象
        try {
            Rectangle r = new Rectangle(-3,4);
            
            System.out.println(r.getArea());
            
        } catch ( IllegalWidthException e) {
            e.printStackTrace();
        } catch ( IllegalLengthException e) {
            e.printStackTrace();
        }
        
    }
}

2.5、throw和throws区别

throw:
throw书写在函数内,当在函数中有异常的时候,这个异常需要抛出,这时我们可以在函数中使用throw关键字把这个异常抛出。
throws:
throws是书写在函数上,当函数中有编译时期的异常时,我们同时又需要把这个异常告诉给函数的调用者,这时可以在函数上使用throws关键字声明函数中的异常。

3、异常的细节

3.1、异常代码块使用

异常的代码块:

1)try-catch代码块(掌握)

try{

可能发生异常的代码

}catch( 异常类名  变量名 ){

处理异常的代码。

}

2)try-catch-catch代码块

当try代码块中可能有多个异常的时候,针对每个异常,需要分别进行捕获

try{

可能发生异常的代码

}catch( 异常类名  变量名 ){

}catch( 异常类名  变量名 ){

}

......

多catch分别捕获异常的时候,最上面的catch应该捕获的是子类的那个异常,最后的一个catch,一定捕获的需要捕获的异常的最后那个父类。

需求:
1)定义一个Demo类,在这个类中分别定义主函数和一个show函数;
2)在分别定义两个异常类A和B;
3)A类继承Exception类,并创建两个构造函数;
4)B类继承A类,并创建两个构造函数;
5)在Demo类中的主函数中调用show函数,并传参0;
6)在Demo类中的show函数中分别写三个判断语句,使用throw关键字分别抛出B、A和Exception类三个异常类的对象,同时在show函数声明三个异常;
7)这样在main函数中就得对异常语句(调用show函数语句)进行捕获异常处理;

package cn.xuexi.Dome;
//自定义异常类
class A extends Exception
{
    public A(){
       super();
    }
    public A(String message) {
       super(message);
    }
}
class B extends A
{
    public B(){
       super();
    }
    public B(String message) {
       super(message);
    }
}
public class Demo {
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       try
       {
           show(0);//new Exception() new A() new B()
           //这里可能发生A B Exception异常中的某一个异常
           /*
            * Exception e =new Exception()
            * Exception e=new A()
            * Exception e=new B()
            */
       }catch(B b)
       {
          
       }catch(A a)
       {
          
       }catch(Exceptione)//
       {
          
       }
    }
    public static void show(int x) throwsB,A,Exception
    {
       if(x==0)
       {
            throw new B();
       }
       if(x==1)
       {
           throw new A();
       }
       if(x==2)
       {
           throw new Exception();
       }
    }
}

3)try-catch-finally代码块(掌握):这种组合一般用在某些代码要求不管怎么样都必须执行的时候,可以使用。

try{
可能发生异常的代码
}catch( 异常类名  变量名 ){
处理异常的代码。
}finally{
程序中永远都能执行到的代码
}

在程序中随时都有可能发生异常,一旦程序中发生了异常,这样就会导致在发生异常的地方之后的所有代码都不会运行。可是在程序中,有时有些代码不管程序中有没有问题都必须执行。这时这部分代码必须写在finally代码块中。

需求:
1)创建一个Test类;
2)定义一个Demo1类,在这个类中定义一个show函数,根据传递进来的参数x进行判断,如果x等于0,则使用throw关键字抛异常Exception;
3)对这个异常我们不使用声明处理,我们使用捕获进行处理,即使用try-catch-finally进行处理;
4)并分别在try-catch-finally的代码中使用return关键字返回给调用者1,2,3;
5)在Test类中创建Demo1对象,并使用对象调用类中的show函数,并打印返回来的值;

package cn.xuexi.Dome;
class Demo1
{
    public int show(int x)
    {
       try
       {
           if(x==0){
              throw new Exception("x是零");
           }
           System.out.println("try.....");
           return 1;
       }catch(Exceptione)
       {
           System.out.println("捕获异常");
           return 2;
       }finally
       {
           System.out.println("必须执行");
           return 3;
       }
    }
}
public class Test {
    public static void main(String[] args) {
       Demo1 d = new Demo1();
       int value=d.show(0);
       System.out.println(value);
    }
}

说明:在上述代码中,因为传入show函数中的值是0,所以发生异常,那么在jvm执行catch中的return 2代码的时候,jvm发现此时下面有finally关键字,那么jvm先不会执行return 2语句,jvm会先去执行finally中的代码,因为此时finally中有return 3语句,在函数中,遇见return关键字就会结束整个函数,此时jvm不会回来执行return2语句,如果在finally代码块中没有return语句,那么jvm在执行finally代码块里面的代码之后就又会回到catch中继续执行return 2语句。

   finally代码块是永远都会被执行的代码,不管程序发生什么问题,最后JVM一定会把finally中的代码执行一遍。

4)try-finally:这种组合一般用在某些代码要求不管怎么样都必须执行的时候,可以使用。

try{
可能发生异常的代码
//throw new Exception();
}
finally{
}

说明:以上组合表示对代码进行异常检测,检测到异常后因为没有catch,所以一样会被jvm默认抛出。异常是没有捕获处理的,但是功能所开启的资源需要关闭,所以有finally,只为关闭资源。

注意:finally的使用场景

finally:程序中的代码不管发生什么异常,如果都需要执行,就可以放在finally代码块中。

try{
操作数据库
}
finally{
关闭和数据的资源
}

注意:catch和finally必须和try一起使用,catch和finally不能单独使用。

3.2、继承中异常的使用

1、如果在函数中使用throw关键字抛出了异常,在throw语句的下面不能再有其他的代码,否则编译报错。


10.png

2、继承中的函数复写异常细节:
1、子类复写父类的函数,要求函数的返回值类型,函数名,参数列表必须一致。
2、子类复写父类的函数,子类中函数访问权限大于等于父类中函数的访问权限。
3、不能修改访问方式

4、子类复写父类的函数时,如果父类的函数上没有声明异常,子类复写的时候,也不能声明异常.(重点,开发中会经常犯错误)

实际开发中,如果子类复写了父类中的函数,而在子类复写的函数中,即使出现异常也不能声明异常,只能使用try-catch代码块进行捕获处理。

需求:
1)创建类Test2;
2)定义一个Fu类和Zi类,在Fu类中定义一个method函数;
3)在Zi类中复写method函数,在这个函数中抛Exception异常;
4)在类Test2中创建Zi类对象,通过子类对象调用子类中的method函数;
5)在Zi类中的method中捕获异常;

package cn.xuexi.sh.Dome;
class Fu
{
    public void method()
    {
       System.out.println("父类中的method");
    }
}
class Zi extends Fu
{
    public void method() //throwsException
    {
       System.out.println("子类重写方法method");
       try {
           throw new Exception("异常");
       } catch(Exception e) {
           e.printStackTrace();
       }
    }
}
public class Test2 {
    public static void main(String[] args) {
       Zi z=new Zi();
       z.method();
    }
}

5、子类复写父类的函数时,如果父类函数上有异常声明,子类复写父类的函数时,可以声明父类异常的子异常。

异常需要掌握
1)知道什么是异常。
2)知道遇到了异常在函数中2种处理方案:声明和捕获
3)理解Exception和RuntimeException区别。
4)自定义异常。

上一篇下一篇

猜你喜欢

热点阅读