Java - 异常

13

异常(类)(Exception):

指程序运行过程中, 因为用户的误操作, 代码的 bug 等等一系列原因引起程序的崩溃

不是 Error, 异常是可以挽救的错误, 而 Error 是不可挽救的.

异常分类:

  • 编译型异常: 在源码编译阶段时抛出, 这种异常必须处理, 不处理就无法编译成功
  • 运行时异常: 编译阶段看不出有错误, 在运行时有可能出现的异常

异常类继承树:

异常类继承树.drawio

从继承关系可知: Throwable 是异常体系的根, 它继承自 Object.

Throwable 有两个体 系: Error 和, 表示严重的错误, 程序对此一般无能为力, 例如:

  • OutOfMemoryError: 内存耗尽

  • NoClassDefFoundError: 无法加载某个 Class

  • StackOverflowError: 栈溢出

Exception 则是运行时的错误, 它可以被捕获并处理.

某些异常是应用程序逻辑处理的一部分, 应该捕获并处理. 例如:

  • NumberFormatException: 数值类型的格式错误

  • FileNotFoundException: 未找到文件

  • SocketException: 读取网络失败

还有一些异常是程序逻辑编写不对造成的, 应该修复程序本身. 例如:

  • NullPointerException: 对某个 null 的对象调用方法或字段

  • IndexOutOfBoundException: 数组索引越界

Exception 又分为两大类:

  1. RuntimeException 以及它的子类;
  2. RuntimeException (包括 IOException, ReflectiveOperationException 等等)

Java规定:

必须捕获的异常, 包括 Exception 及其子类, 但不包括 RuntimeException 及其子类, 这 种类型的异常为 Checked Exception.

不需要捕获的异常, 包括 Error 及其子类, RuntimeException 及其子类.

异常处理:

解决掉异常的现象, 让程序继续运行

提高程序的容错能力, 也就提高了程序的稳定性

java 有一个 Exception 的对象处理异常

尽量不要直接用 Exception 类来处理, 应该先用 Exception 的子类, 最后再写一个 Exception

java进行异常处理, 有两种解决方案:

  1. 抓捕异常(重点)
  2. 抛出异常

throw和throws

throw 用在方法里, throws 用在方法上(处理编译阶段的异常)

class Test {
    public static void test() {
        throw new MyException("异常");
    }

    public static void test2() throws Exception {

    }
}

抓捕异常:

针对于可能出现异常的代码, 进行抓捕

class Test {
    public static void main(String[] args) {
        try {

        } catch (Exception e) {

            // 如果出现异常, 代码会立刻进入catch中
            // 出现异常的语句后面的代码不会执行, 会立即进入catch
            // 在这里解决抓捕到的异常
            // 只有出现了异常catch中的代码才会执行
        } finally {
            // 必须执行的代码
        }
    }
}

如果使用抓捕异常, 通过这种处理, 程序即便是遇到了, 也不崩溃

class Test {
    public static void main(String[] args) {
        int result = 0;
        int num1;
        int num2;
        Scanner scan = new Scanner(System.in);
        num1 = scan.nextInt();
        num2 = scan.nextInt();
        try {
            System.out.println(1);
            result = num1 / num2;
            System.out.println(2);
        } catch (Exception e) {
            e.printStackTrace(); // 可以用此方法打印出异常
            System.out.println(3);
        }
        System.out.println(4);
        System.out.println(result);
    }
}

捕获异常使用try...catch语句, 把可能发生异常的代码放到try {...}中, 然后使用 catch 捕获对应的 Exception 及其子类:

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {
            // 用指定编码转换String为byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 如果系统不支持GBK编码, 会捕获到UnsupportedEncodingException:
            System.out.println(e); // 打印异常信息
            return s.getBytes(); // 尝试使用用默认编码
        }
    }
}

如果我们不捕获 UnsupportedEncodingException , 会出现编译失败的问题.

编译器会报错, 错误信息类似: unreported exception UnsupportedEncodingException; must be caught or declared to be thrown, 并且准确地指出需要捕获的语句是 return s.getBytes("GBK"); .

意思是说, 像 UnsupportedEncodingException 这样的 Checked Exception, 必须被捕获.

这是因为 String.getBytes(String) 方法定义是:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {
    public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
        //...
    }
}

在方法定义的时候, 使用 throws xxx 表示该方法可能抛出的异常类型. 调用方在调用的时候, 必须强制捕获这些异常, 否则编译器会报错.

toGBK() 方法中, 因为调用了 String.getBytes(String) 方法, 就必须捕获 .

我们也可以不捕获它, 而是在方法定义处用 throws 表示 toGBK() 方法可能会抛出 UnsupportedEncodingException, 就可以让 toGBK() 方法通过编译器检查:

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) throws UnsupportedEncodingException {
        return s.getBytes("GBK");
    }
}

上述代码仍然会得到编译错误, 但这一次, 编译器提示的不是调用return s.getBytes("GBK") 的问题, 而是 byte[] bs = toGBK("中文").

因为在 main() 方法中调用 toGBK(), 没有捕获它声明的可能抛出的 UnsupportedEncodingException.

修饰方法是在main()方法中捕获异常并处理:

public class Main {
    public static void main(String[] args) {
        try {
            byte[] bs = toGBK("中文");
            System.out.println(Arrays.toString(bs));
        } catch (UnsupportedEncodingException e) {
            System.out.println(e);
        }
    }

    static byte[] toGBK(String s) throws UnsupportedEncodingException {
        // 用指定编码转换String为byte[]:
        return s.getBytes("GBK");
    }
}

可见, 只要是方法声明的 Checked Exception, 不在调用层捕获, 也必须在更高的调用层捕获. 所有 未捕获的异常, 最终也必须在main() 方法中捕获, 不会出现漏写 try 的情况. 这是由编译器保证 的. main() 方法也是最后捕获 Exception 的机会.

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) throws UnsupportedEncodingException {
        // 用指定编码转换String为byte[]:
        return s.getBytes("GBK");
    }
}

因为 main() 方法声明了可能抛出 Exception, 也就声明了可能抛出所有的 Exception, 因此 在内部就无需捕获了. 代价就是一旦发生异常, 程序会立刻退出.

也可在内部消化

class Test {
    static byte[] toGBK(String s) {
        try {
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 什么也不干 
        }
        return null;
    }
}

这种捕获后不处理的方式是非常不好的, 即使真的什么也做不了, 也要先把异常记录下来:

class Test {
    static byte[] toGBK(String s) {
        try {
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 先记下来再说:
            e.printStackTrace();
        }
        return null;
    }
}

所有异常都可以调用 printStackTrace() 方法打印异常栈, 这是一个简单有用的快速打印异常的方法.

抛出异常:

一种消极处理

方法后面用 throws 关键字加上异常类, 把异常抛给调用者, 让调用者处理, 如果抛到main函数不解决再继续抛, 最后会抛给JVM处理

子类不能抛出比父类更多的异常

class Test {
    public static void main() {
        try {
            createFile("Users/usus/a.txt");
        } catch (IoException e) {

            throw new IoExcption();
        } catch (Exception e) {

            throw new Exception();
        }
    }

    public static void createFile(String path) throws IoException, Exception {
        File file = new file(path);
        file.createNewFile();
    }
}

finally关键字:

finally 中的代码一定会执行, 即使 finally 之前有 return 也会执行 finally 中的代码, 因为 finallyjvm级别而不是源码级别

class Test {
    public static void main(String[] args) {
        try {
            // 可能存在异常的代码
        } catch (Exception e) {
            // 处理的方式
        } finally {
            // 这里的代码可以不用写
            // 一旦这里写了代码, 不管try中有没有异常这里必须执行
        }
    }
}

finally中写什么:

  • 回收垃圾
  • 关闭IO流
  • 关闭数据库链接
  • ..........
  • 类似的核心代码

finally特殊用法:

在开发中有一些代码必须执行但不知道会发生什么异常, 为了保证执行, 可以这么写:

class Test {
    void test() {
        try {
            // 这里不写
        } catch (Exception e) {
            // 这里不写
        } finally {
            // 这里写重要代码
        }
    }
}

以下返回值是多少?

class Test {
    public static int test() {
        int a = 10;
        int b = 20;
        try {
            b += a++;
            return b;
        } catch (Exception e) {

        } finally {
            b += 10;
            a += 10;
        }
        return a;
    }
}
// 这个函数的返回值是30

自定义异常类:

继承异常类, 再自定义

自定义异常可以向调用者传递信息

class MyException extends RuntimeException {
    MyException(String s) {
        super(s);
    }

    MyException() {
        super();
    }
}