目录

异常处理(exceptions handing)

在程序编译或运行过程中出现问题,这就是异常。在编译阶段出现异常叫编译异常,运行阶段异常叫运行异常,前者出现时不做处理代码阶段无法通过,后者在程序运行中违反语法会直接停止运行。特别是在代码量大的程序中,很难避免运行异常,所以提前做好异常处理有利于提高程序健壮性。

Java 把常见异常给做成一个个类来使用:

Java 异常类.svg

代码出现问题时生成异常类对象,直接抛出,只有 Exception 异常是需要主动处理的,而 Error 则是系统问题,比如堆栈溢出,爆出来就爆了,除了问题你只能回去改代码。这俩类下面还有一堆子类,都是具体实现,我们只需要关注 Exception 类即可。

Error 和 Exception 下 RuntimeException 子类是 Unchecked Exception,表明在运行时代码逻辑出问题抛出异常,除此之外 Exception 和其子类都是 Checked Exception,需要必须编译前手动处理否则编译时异常。

常见运行时异常:

  • NullPointerException,空指针异常
  • ArrayIndexOutOfBoundsException,数组索引越界异常
  • ArithmeticException,数学操作异常
  • ClassCastException,类型转换异常
  • NumberFormatException,数学格式异常
  • InputMismatchException,输入格式匹配异常

常见编译时异常:

  • ClassNotFoundException,类没找到异常

创建 ExceptionCatch.java:

import java.util.Scanner;

public class ExceptionCatch {
    public static void main(String[] args) {
        // ArrayIndexOutOfBoundsException,数组索引越界异常
        String[] a = {"1"};
        System.out.println(a[1]);

        // ArithmeticException,数学操作异常
        System.out.println(10 / 0);

        // ClassCastException,类型转换异常
        Object str = "12a";
        int numStr = (int)str;

        // NumberFormatException,数学格式异常
        Integer.parseInt("1A");

        // NullPointerException,空指针异常
        String str2 = null;
        str2.length();

        // InputMismatchException,输入格式匹配异常。
        Scanner scanner = new Scanner(System.in);
        scanner.nextInt();

        // ClassNotFoundException,类没找到异常。
        Class.forName("java.lang.String").getName();
    }
}

try-catch-finally

代码生成异常对象后会在 catch 匹配对应异常类型,匹配成功就执行catch 代码块中内容,执行完如果有 finally 代码块就去执行,没有就往下执行代码。因为处理了异常下面代码不会结束运行。

try {
    // 可能会出现异常的代码。
} catch(ExceptionType name) {
    // 异常处理代码。
} catch (ExceptionType name) {
    // 异常处理代码。
} finally {
    // 不管有没异常都会执行。
}

Java 7 以后 catch 支持捕获多个异常,再需要一起处理时很方便。

try {
    // 可能会出现异常的代码。
} catch(ExceptionType | ExceptionType name) {
    // 异常处理代码。
} finally {
    // 不管有没异常都会执行。finally 是可选的,不是异常处理时必须写上。
}

异常对象有以下方法:

  • getMessage,获取错误信息
  • printStackTrace() ,打印出错误堆栈信息

将 ExceptionCatch.java 用 try-catch 改造下:

import java.util.Scanner;

public class ExceptionCatch {
    public static void main(String[] args) {
        try {
            // ArrayIndexOutOfBoundsException,数组索引越界异常
            String[] a = {"1"};
            System.out.println(a[1]);

            // ArithmeticException,数学操作异常
            System.out.println(10 / 0);

            // ClassCastException,类型转换异常
            Object str = "12a";
            int numStr = (int)str;

            // NumberFormatException,数学格式异常
            Integer.parseInt("1A");

            // NullPointerException,空指针异常
            String str2 = null;
            str2.length();

            // InputMismatchException,输入格式匹配异常。
            Scanner scanner = new Scanner(System.in);
            scanner.nextInt();

            // ClassNotFoundException,类没找到异常。
            Class.forName("java.lang.String").getName();
        } catch(Exception e) {
            e.printStackTrace();
        }
        System.out.println("处理完异常下面代码正常运行");
    }
}

输出:

java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
    at exceptionTest.ExceptionCatch.main(ExceptionCatch.java:12)
处理完异常下面代码正常运行

使用 Exception 捕获后,在处理编译时异常时,确实不报错了,在实际运行过程中任然有可能再次出现异常。拿 Class.forName() 举例,你处理了,在实际中要是找不到这个类呢?还是一样爆运行异常。

使用 try-catch 处理异常时有几点注意事项:

  1. try 代码块中的变量是局部变量,不能在外面使用。非要使用就在外面创建变量并初始化好值。

     public class ExceptionCatch {
         public static void main(String[] args) {
     //        Object str; 不赋值就是 null
             String[] str = null;
             try {
                 str = new String[]{"str1", "str2"};
             } catch(Exception e) {
                 e.printStackTrace();
             }
     //        System.out.println(str); // 直接使用可能会没有赋值,所有产生未初始化的异常
             System.out.println(str.length);
         }
     }
  2. catch 异常类父类时,下面不能在 catch 异常子类,否则报错。再说也没意义,你都捕获异常父类了,当产生异常子类时肯定会捕获到。

     public class ExceptionCatch {
         public static void main(String[] args) {
     //        Object str; 不赋值就是 null
             String[] str = null;
             try {
                 str = new String[]{"str1", "str2"};
             } catch(Exception e) {
                 e.printStackTrace();
             } catch(RuntimeException e) { // 捕获异常父类后再捕获异常子类则报错。java: 已捕获到异常错误java.lang.RuntimeException
                 e.printStackTrace();
             }
     //        System.out.println(str); // 直接使用可能会没有赋值,所有产生未初始化的异常
             System.out.println(str.length);
         }
     }

finally

finally 不管什么情况,就算 catch 中再次出现异常情况,一定会在 catch 最后一行代码运行前执行最后才结束 try-catch。而 finally 一般用在释放 IO流、数据库连接等资源。

创建 FinallyTest.java:

try {
    System.out.println(10 / 0);
} catch (ArithmeticException e) {
    System.out.println(1);
    return;
} finally {
    System.out.println("finally 代码块一定会执行");
}

运行返回

1
finally 代码块一定会执行

try-catch-finally,catch 里 return 一个变量,在 finally 里修改 return 变量的值会失败。可以理解为要 return 的变量的值已经取出来,等 finally 执行完就返回,因此 finally 里去改已经没用了。

创建 FinallyTest2.java:

public static int finallyReturnVarTest() {
    int tmpInt = 0;
    try {
        throw new RuntimeException();
    } catch (RuntimeException e) {
        return tmpInt;
    } finally {
        tmpInt = 2;
    }
}

运行输出:

0

真实开发中运行时异常很少处理,遇到回去改代码就行,都只会处理编译时异常,不然代码都没法运行呐。

throws

异常另一种处理办法是自己不想处理甩出去让别人处理。throws 是不去 catch 捕获,有任何异常直接往调用方抛,由调用方 try-catch 处理,或者调用方不处理就继续 throws 往上抛。

语法是 throws Expression,写在代码块左花括号左边:

public class ExceptionCatch {
    public static void main(String[] args) {
        try {
            ExceptionCatch exceptionCatch = new ExceptionCatch();
            exceptionCatch.func();
        } catch (ClassNotFoundException e) {
            System.out.println("类名未找到");
        }
    }

    public void func() throws ArithmeticException, ClassNotFoundException {
        Class.forName("java.lang.String1");
    }
}

方法 func() 不会再报编译时错误,因为抛给调用者了。只要记住谁调用谁处理就行。

运行输出:

类名未找到

那运行时异常呢?也是一样往上抛不用在当前处理,交给调用者处理即可:

public class ExceptionCatch {
    public static void main(String[] args) {
        try {
            ExceptionCatch exceptionCatch = new ExceptionCatch();
            System.out.println(exceptionCatch.func1());
        } catch (ArithmeticException e) {
            System.out.println("除 0 错误");
        }
    }

    public int func1() throws ArithmeticException {
        return 10 / 0;
    }
}

运行输出:

除 0 错误

抛父类和子类两个异常接的时候只 catch 父类异常可以吗?

public class ExceptionCatch {
    public static void main(String[] args) {
        try {
            ExceptionCatch exceptionCatch = new ExceptionCatch();
            System.out.println(exceptionCatch.func1());
        } catch (Exception e) {
            System.out.println("除 0 错误");
        }
    }

    public int func1() throws ArithmeticException, Exception {
        return 10 / 0;
    }
}

运行输出:

除 0 错误

结果证明是没问题的。

throws 使用上有个注意事项,子类重写父类方法时,throws 的异常不能大于父类,这点也适用于抽象类和接口上。

public class ExceptionCatch {
    public static void main(String[] args) {
        new ExceptionCatch().func1();
    }

    public void func1() throws RuntimeException {
    }
}

class SubClass extends ExceptionCatch {
    // java: exceptionTest.SubClass中的func1()无法覆盖exceptionTest.ExceptionCatch中的func1()
    // 被覆盖的方法未抛出 java.lang.Exception
    public void func1() throws Exception {
        super.func1();
    }
}

这里父类 func1() 方法 throws RuntimeException,子类重写后则直接 throws Exception,而 Exception 是 RuntimeException 父类,这关系不能颠倒。当然子类 func1() 方法也可以不去抛出也是没问题的,不是说父类抛异常,子类继承/重写后就必须也要抛。

那父类这个异常什么时候抛呢?还以一样,谁通过这个类的实例调用它的 func1() 方法,谁就必须要处理,比如 new ExceptionCatch().func1(),调用这个方法的地方就需要处理异常。

编译输出:

java: exceptionTest.SubClass中的func1()无法覆盖exceptionTest.ExceptionCatch中的func1()
  被覆盖的方法未抛出java.lang.Exception

throw

throw 是手动抛异常,和 throws 不同,后者是往上报送异常,前者是直接手动生成一个异常对象抛出。但是抛你也不能瞎选异常,比如明明是索引超出范围那就应该抛 ArrayIndexOutOfBoundsException,而不是 IOException。

public class ExceptionCatch {
    public static void main(String[] args) {
        new ExceptionCatch().func1();
    }

    public void func1() {
        throw new RuntimeException("手动生成异常信息");
    }
}

运行输出:

Exception in thread "main" java.lang.RuntimeException: 手动生成异常信息
    at exceptionTest.ExceptionCatch.func1(ExceptionCatch.java:9)
    at exceptionTest.ExceptionCatch.main(ExceptionCatch.java:5)

可以发现错误信息是自定义的。

处理异常一方可以通过异常对象的 getMessage() 方法获得异常信息:

public class ExceptionCatch {
    public static void main(String[] args) {
        try {
            new ExceptionCatch().func1();
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }

    public void func1() throws RuntimeException {
        throw new RuntimeException("手动生成异常信息");
    }
}

运行输出:

手动生成异常信息

custom exception

现有的异常类不能描述业务错误信息时,就需要自己创建个异常类。现有异常类是通过继承得来的,那么自己也能创建个类去继承,然后就成了自定义异常类了。继承时需要分清是 Exception 编译时异常还是运行时异常,类命名上也需要明确指明是什么异常。

创建 PrintStringCharException.java:

public class PrintStringCharException extends RuntimeException {
    PrintStringCharException() {
        super();
    }

    PrintStringCharException(String errorMsg) {
        super(errorMsg);
    }

    //public PrintStringCharException(String message, Throwable cause) {
    //    cause 参数是说当前这个异常是哪个异常引起的,直接传入就行。
    //    super(message, cause);
    //}
}

创建 ExceptionCatch.java:

public class ExceptionCatch {
    public void func() throws PrintStringCharException {
        throw new PrintStringCharException("字符串输出异常");
    }

    public static void main(String[] args) {
        try {
            new ExceptionCatch().func();
        } catch (PrintStringCharException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("看起来一切都好");
    }
}

运行输出:

字符串输出异常
看起来一切都好

这里使用以下方法创建异常:

  1. 使用 Alt+ Insert 生成 Constructor。
  2. 照着 Java 官方异常类抄。抄的时候要注意 static final long serialVersionUID 改个其他值,不能和其他异常类重复。此知识点见序列化一节。

这里创建了个运行时异常,具体使用什么异常自己决定。再真正使用是手动 throw 异常自己不处理最后抛出去,交给 main 方法 catch,最终运行没有爆错误。

最近更新:

发布时间:

摆哈儿龙门阵