Java基础(4)—异常与内部类

作者: 危君子频道 | 来源:发表于2020-05-18 20:52 被阅读0次
    image

    一、异常

    简介:

    1.程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
    2.Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw语句手动抛出的,只要Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
    3.Throwable类是Java异常类型的顶层父类,一个对象只有是Throwable类的(直接或间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。

    异常的分类和类结构图:

                                              |--StackOverFlowError
                        |--VirtuMachineError--|--OutOfMemoryError
              |--Error--|--AWTError                     
              |                                          |--EOFException
              |             |--IOException---------------|--FileNotFoundException
    Throwable-|             |                            |--MalformedURLException
              |             |--ClassNotFoundException    |--UnknownHostException
              |--Exception--|
                            |--CloneNotSupportedException |--ArithmeticException
                            |                             |--ClassCastException
                            |--RuntimeException-----------|--IllegalArgumentException
                                                          |--IllegalStateException
                                                          |--IndexOutOfBoundsException
                                                          |--NoSuchElementException
                                                          |--NullPointerException
    

    以Throwable为顶层父类。Throwable又派生出Error类和Exception类。

    错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

    异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

    总体上我们根据Javac对异常的处理要求,将异常类分为2类。

    非检查异常(unckecked exception):Error和RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理。这样的异常发生的原因多半是代码写的有问题。

    检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException 、IOException、ClassNotFoundException等。

    需要明确的是:检查和非检查是对于javac来说的。

    异常处理的基本语法:

    1.try块:负责捕获异常,一旦try中发现异常,程序的控制权将被移交给catch块中的异常处理程序。
    注意:try语句块不可以独立存在,必须与 catch 或者 finally 块同存

    2.catch块:异常处理,比如发出警告:提示、检查配置、网络连接,记录错误等。执行完catch块之后程序跳出catch块,继续执行后面的代码。
    注意:编写catch块的注意事项:多个catch块处理的异常类,要按照先catch子类后catch父类的处理方式,因为会就近处理异常(从上到下)。

    3.finally块:最终执行的代码,用于关闭和释放资源。

    try{
     //一些会抛出的异常
    }catch(Exception e){
     //第一个catch
     //处理该异常的代码块
    }catch(Exception e){
     //第二个catch,可以有多个catch
     //处理该异常的代码块
    }finally{
     //最终要执行的代码
    } 
    

    finally块和return:

    执行顺序:

    1.执行:expression,计算该表达式,结果保存在操作数栈顶;
    2.执行:操作数栈顶值(expression的结果)复制到局部变量区作为返回值;
    3.执行:finally语句块中的代码;
    4.执行:将第2步复制到局部变量区的返回值又复制回操作数栈顶;
    5.执行:return指令,返回操作数栈顶的值;

    分析:

    在第一步执行完毕后,整个方法的返回值就已经确定了,由于还要执行finally代码块,因此程序会将返回值暂存在局部变量区,腾出操作数栈用来执行finally语句块中代码,等finally执行完毕,再将暂存的返回值又复制回操作数栈顶。所以无论finally语句块中执行了什么操作,都无法影响返回值,所以试图在finally语句块中修改返回值是徒劳的。因此,finally语句块设计出来的目的只是为了让方法执行一些重要的收尾工作,而不是用来计算返回值的。所以在finally中更改返回值是无效的,因为它只是更改了操作数栈顶端复制到局部变量区的快照,并不能真正的更改返回值,但是如果在finally中使用return的话则是会将新的操作数栈的顶端数据返回,而不是之前复制到局部变量区用作返回内容快照的值返回,所以这样是可以返回的,同样的在catch语句块里也是这样,只有重新出现了return才有可能更改返回值

    小结:

    1.不管有无出现异常或者try和catch中有返回值return,finally块中代码都会执行。
    2.finally中最好不要包含return,否则程序会提前退出,返回会覆盖try或catch中保存的返回值。
    3.e.printStackTrace()可以输出异常信息。
    4.return值为-1为抛出异常的习惯写法。
    5.如果方法中try、catch、finally中没有返回语句,则会调用这三个语句块之外的return结果。
    6.finally在try中的return之后,在返回主调函数之前执行。
    7.将尽量将所有return写在函数的最后面,而不是在try...catch…finally中。

    throw和throws异常抛出:

    java中的异常抛出通常使用throw和throws关键字来实现。
    throw:

    throw--将产生的异常抛出,是抛出异常的一个动作。一般会用于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。
    语法:throw (异常对象)
    代码例子:

    public static void main(String[] args) { 
        String s = "abc"; 
        if(s.equals("abc")) { 
          throw new NumberFormatException(); 
        } else { 
          System.out.println(s); 
        } 
    } 
    

    运行结果:

    Exception in thread "main" java.lang.NumberFormatException at test.ExceptionTest.main(ExceptionTest.java:13)
    

    throws:

    throws--声明将要抛出何种类型的异常(声明)。当某个方法可能会抛出某种异常时用于throws 声明可能抛出的异常,然后交给上层调用它的方法程序处理。

    语法格式:

    public void 方法名(参数列表) throws 异常列表{
     //调用会抛出异常的方法或者:
     throw new Exception();
     }
    

    代码例子:

    public static void function() throws NumberFormatException{ 
          String s = "abc"; 
          System.out.println(Double.parseDouble(s)); 
        } 
          
        public static void main(String[] args) { 
          try { 
            function(); 
          } catch (NumberFormatException e) { 
           System.err.println("非数据类型不能转换。"); 
         } 
     } 
    

    运行结果:

    非数据类型不能转换。
    

    两者比较:

    1.throws出现在方法函数头;而throw出现在函数体。
    2.throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
    3.两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
    4.throws的异常列表可以是抛出一条异常,也可以是抛出多条异常,每个类型的异常中间用逗号隔开。
    5.如果某个方法调用了抛出异常的方法,那么必须添加try catch语句去尝试捕获这种异常, 或者添加声明,将异常抛出给更上一层的调用者进行处理。

    异常的链化:

    在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

    异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

    查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

    public class Throwable implements Serializable {
       private Throwable cause = this;
    
       public Throwable(String message, Throwable cause) {
           fillInStackTrace();
           detailMessage = message;
           this.cause = cause;
       }
        public Throwable(Throwable cause) {
           fillInStackTrace();
           detailMessage = (cause==null ? null : cause.toString());
           this.cause = cause;
       }
       //........
    }
    

    代码例子:

    从命令行输入2个int,将他们相加,输出。输入的数非int,则导致getInputNumbers异常,从而导致add函数异常,则可以在add函数中抛出一个链化的异常。

    public static void main(String[] args){
       System.out.println("请输入2个加数");
       int result;
       try{
           result = add();
           System.out.println("结果:"+result);
       } catch (Exception e){
           e.printStackTrace();
       }
    }
    //获取输入的2个整数返回
    private static List<Integer> getInputNumbers(){
       List<Integer> nums = new ArrayList<>();
       Scanner scan = new Scanner(System.in);
       try {
           int num1 = scan.nextInt();
           int num2 = scan.nextInt();
           nums.add(new Integer(num1));
           nums.add(new Integer(num2));
       }catch(InputMismatchException immExp){
           throw immExp;
       }finally {
           scan.close();
       }
       return nums;
    }
    
    //执行加法计算
    private static int add() throws Exception{
       int result;
       try {
           List<Integer> nums = getInputNumbers();
           result = nums.get(0)  + nums.get(1);
       }catch(InputMismatchException immExp){
           throw new Exception("计算失败",immExp);//链化:以一个异常对象为参数构造新的异常对象。
       }
       return  result;
    }
    

    输出结果:

    请输入2个加数
    a s
    java.lang.Exception: 计算失败
       at practise.ExceptionTest.add(ExceptionTest.java:53)
       at practise.ExceptionTest.main(ExceptionTest.java:18)
    Caused by: java.util.InputMismatchException
       at java.util.Scanner.throwFor(Scanner.java:864)
       at java.util.Scanner.next(Scanner.java:1485)
       at java.util.Scanner.nextInt(Scanner.java:2117)
       at java.util.Scanner.nextInt(Scanner.java:2076)
       at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
       at practise.ExceptionTest.add(ExceptionTest.java:48)
       ... 1 more
    

    自定义异常:

    如果要自定义异常类,则扩展Exception类即可,此时自定义异常属于检查异常(checked exception)。如果要自定义非检查异常,则扩展RuntimeException类。

    按照国际惯例,自定义的异常应该总是包含如下的构造函数:

    1.一个无参构造函数。
    2.一个带有String参数的构造函数,并传递给父类的构造函数。
    3.一个带有String参数和Throwable参数,并都传递给父类构造函数。
    4.一个带有Throwable参数的构造函数,并传递给父类的构造函数。

    以下为IOException类的完成源代码,可参考:

    public class IOException extends Exception{
        static final long serialVersionUID = 7818375828146090155L;
     
        public IOException(){
            super();
        }
        
        public IOException(String message){
            super(message);
        } 
        
        public IOException(String message, Throwable cause){
            super(message, cause);
        }
     
        public IOException(Throwable cause){
            super(cause);
        }
    }
    

    异常的使用场景:

    1.大概率发生时使用运行时异常。
    2.异常无法恢复时使用运行时异常。
    3.可恢复时优先使用受检异常。
    4.使用受检异常做流程控制。
    5.尽量集中处理异常。

    异常的实现原理(字节码级别):

    在jvm中对于执行的方法都会有一个执行方法栈区,每次执行的时候jvm会生成一张栈表用于记录执行的每一行代码的执行,但代码发生异常的时候,会回溯代码的执行栈以及抛出相对应的代码位置,代码嵌套的越多回溯的时间越长,性能影响较大。

    示例代码:

    public class ExceptionClassCode {
       public int demo() {
           int x;
           try {
               x = 1;
               return x;
           } catch (Exception e) {
               x = 2;
               return x;
           } finally {
               x = 3
           }
       }
    }
    

    对应的字节码:

    public int demo();
       descriptor: ()I
       flags: ACC_PUBLIC
       Code:
         stack=1, locals=5, args_size=1
            0: iconst_1 // 生成整数1
            1: istore_1 // 将生成的整数1赋予第1号局部变量(x=1)
            2: iload_1 // 将x(=1)的值入栈
            3: istore_2 // 将栈顶的值(=1)赋予第2号变量(returnValue)
            4: iconst_3 // 生成整数3
            5: istore_1 // x=3
            6: iload_2 // returnValue=当前栈顶值(=1)
            7: ireturn // 返回returnValue(=1)
            8: astore_2 // 将Exception对象引用值赋予第2号局部变量
            9: iconst_2 // 生成整数2
           10: istore_1 // x=2
           11: iload_1 // x(=2)压入栈顶
           12: istore_3 // 将栈顶的值(=2)赋予第3号变量(returnValue)
           13: iconst_3 // 生成整数3
           14: istore_1 // x=3
           15: iload_3  // returnValue(=2)压入栈顶
           16: ireturn  // 返回returnValue(=2)
           17: astore        4 // 将异常信息保存到第4号局部变量
           19: iconst_3 // 生成整数3
           20: istore_1 // x=3
           21: aload         4 // 将异常引用值压入栈
           23: athrow // 抛出栈顶所引用的异常
         Exception table:
            from    to  target type
                0     4     8   Class java/lang/Exception # 如果0~4行字节码(try代码块)中出现Exception及其子类异常,则执行第8行(catch代码行)
                0     4    17   any # 无论0~4行字节码(try代码块)是否抛出异常,都执行第17行(finally代码行)
                8    13    17   any # 无论8~13行字节码(catch代码块)是否抛出异常,都执行第17行(finally代码行)
               17    19    17   any 
    

    看到字节码中有一个Exception table(异常表)区域,这个就是与异常相关的字节码内容。它表示在fromto所指示的字节码行中,如果抛出type所对应的异常(及其子类),那么就跳到target指定的字节码行开始执行。

    注意事项:

    1.当子类重写父类的带有throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法。这是为了支持多态。

    2.Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

    3.在多重catch块后面,可以加一个catch(Exception e)来处理可能会被遗漏的异常。

    二、内部类

    内部类基础:

    在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

    成员内部类:

    成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所有成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。在成员内部类中要注意两点:

    第一:成员内部类中不能存在任何static的变量和方法。
    第二:成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。

    代码例子:

    public class OuterClass {
       private String str;
       
       public void outerDisplay(){
           System.out.println("outerClass");
       }
       
       public class InnerClass{
           public void innerDisplay(){
               //使用外围内的属性
               str = "Hello,jimyoungwei";
               System.out.println(str);
               //使用外围内的方法
               outerDisplay();
           }
       }
       
       /*推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时 */
       public InnerClass getInnerClass(){
           return new InnerClass();
       }
       
       public static void main(String[] args) {
           OuterClass outer = new OuterClass();
           OuterClass.InnerClass inner = outer.getInnerClass();
           inner.innerDisplay();
       }
    }
    /*
    输出结果:
    Hello,jimyoungwei
    outerClass
    */
    

    局部内部类:

    局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

    注意:局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
    代码例子:

    class People{
       public People(){}
    }
    
    class Man{
       public Man(){}     
       public People getWoman(){
           class Woman extends People{   //局部内部类
               int age =0;
           }
           return new Woman();
       }
    }
    

    匿名内部类:

    匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

    代码例子:

    scan_bt.setOnClickListener(new OnClickListener() {             
               @Override
               public void onClick(View v) {  
               //匿名内部类,此处编写按钮点击事件代码
               }
           });        
    

    静态内部类:

    静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。

    注意:静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。(在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。)

    代码例子:

    class Outter{
       int a = 10;
       static int b= 5;
       public Outter(){}
       
       static class Inner{
           public Inner(){
               System.out.println(a); //代码出错,静态内部类无法访问非static成员变量
               System.out.println(b); //代码正确
           }
       }
    }
    

    深入理解内部类:

    1.为什么成员内部类可以无条件访问外部类的成员?

    使用成员内部类,编译时系统会自动生成一个名为Outter$成员内部类类名.class文件。当我们去反编译该文件时可以找到里面存在一个指向外部类对象的指针,也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用。如果没有创建外部类的对象,则无法让内部类构造器对外部类对象的引用进行初始化赋值,也就无法创建成员内部类的对象了。

    2.为什么局部内部类和匿名内部类只能访问局部final变量?(JAVA jdk 1.8之前)

    public class Test {
        public static void main(String[] args)  {}
         
        public void test(final int b) {
            final int a = 10;
            new Thread(){
                public void run() {
                    System.out.println(a);
                    System.out.println(b);
                };
            }.start();
        }
    }
    

    上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。

    复制:如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

    Java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

    注意:在jdk1.8中,一定程度上解决了这个final限制问题。不用显式地将要被局部内部类或者匿名内部类访问的成员变量声明为final,但是在编译阶段,编译器仍然会给这些变量加上final限制。这些变量如果没有显式的声明为final时,如果在成员内部类或者匿名内部类中重新为其赋值,编译器也会报错的。

    3.静态内部类有特殊的地方吗?

    静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的。

    4.内部类对象的创建形式?

    创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()。
    创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()。

    内部类的好处:

    1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
    2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
    3.方便编写事件驱动程序。
    4.方便编写线程代码。
    5.内部类提供了更好的封装,除了该外部类,其他类都不能访问。


    上一篇:Java基础(3)—static关键字与继承
    下一篇:Java基础(5)—垃圾回收机制 GC
    精彩内容不够看?更多精彩内容,请到微信搜索 “危君子频道” 订阅号,每天更新,欢迎大家关注订阅!

    image

    相关文章

      网友评论

        本文标题:Java基础(4)—异常与内部类

        本文链接:https://www.haomeiwen.com/subject/aygpohtx.html