初识 Java 异常 - Exception

304 阅读9分钟

程序中,代码出现错误,程序就会终止运行。

异常指的并不是语法错误,语法错误,编译不通过,不会产生字节码文件根本不能运行。
异常处理是衡量一门语言是否成熟的标准之一,主流的语言 JavaC++C# 等支持异常处理机制。
异常处理可以让程序有更好的容错性,使我们的代码更健壮。
如果没有异常会带来什么问题?
1、使用方法的返回值来表示异常情况优先,无法穷举所有的异常情况;
2、异常流程代码和正常流程代码混合一起,增大了程序的复杂性,可读性也不好;
3、随着系统规模的不断扩大,程序的可维护性极低。

所以可以通过以下方案解决上面没有异常机制的问题:
1、把不同类型的异常情况描述成不同类(称之为异常类);
2、分离异常流程代码和正常流程代码;
3、灵活处理异常,如果当前方法处理不了,应该交给调用者来处理。

非正常情况出现后,程序会中断。
可分为以下两种非正常情况:
1)Error:表示错误,一般指 JVM 相关的不可修复的错误,如系统崩溃、内存溢出、JVM 错误等,由 JVM 抛出,我们不需要处理;
2)Exception:表示异常,程序中出现不正常的情况,该为题可以修复 - 处理异常。 几乎所有的子类都是以 Exception 作为类名的后缀。

常见的 Error
StackOverflowError

public class  Main {
    public static void main(String[] args) {
        main(args);
    }
}

上面循环调用 main 方法导致栈溢出:

常见的 Exception
NumberFormatException:数字格式化异常,一般之把非数字的字符串转化成整数时抛出:

public class  Main {
    public static void main(String[] args) {
        Integer age = Integer.valueOf("18s");
    }
}

等等... 当程序出现异常后,可以在 jdk api 帮助文档中查找相关异常。

出现异常,程序会中断,因此需要处理异常:
1)该方法不处理,而是声明抛出,由该方法的调用者来处理(throws);
2)在方法中使用 try-catch 的语句块来处理异常。

使用 try-catch 捕获单个异常
语法:

try {
    // 编写可能会出现异常的代码         
} catch (异常类型 异常变量名) {
	// 处理异常的代码
}

示例:

public class  Main {
    public static void main(String[] args) {
        System.out.println("start。。。");
        try {
            int result = 10 / 0;
            System.out.println("运算结果:" + result);
            
        } catch (ArithmeticException e) {
            System.out.println("运算异常:" + e);
        }

        System.out.println("end。。。");
    }
}

上述示例,在执行 10 / 0这行代码的时候,出现算数异常,此时底层会创建一个算数异常对象: new ArithmeticException("by zero"),然后去找到能接收异常类型的 catch,直接跳转到 catch 中:即 ArithmeticException e = new ArithmeticException("by zero") 注意:算数异常这里相当于会判断实例对象

if (e instanceof ArithmeticException) {
                
}

如何获取异常信息,Throwable 类的几个方法:
1)String getMessage(): 获取异常的描述信息 - 原因(提示给用户的时候,就提示错误原因);
2)Stirng toString(): 获取异常的类型和异常描述信息(一般不用);
3)void printStackTrace:打印异常的跟踪栈信息并输出到控制台 - 包含了异常的类型,异常的原因,异常出现的位置,在开发和调试阶段都会使用。 此时打印异常信息不需要使用 Systeme.out.println

示例:

public class  Main {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
            System.out.println("运算结果:" + result);

        } catch (ArithmeticException e) {
            System.out.println("运算出现异常...");
            System.out.println("异常getMessage:" + e.getMessage());
            System.out.println("异常toString:" + e.toString());
            e.printStackTrace();
        }
    }
}

打印信息:

运算异常:java.lang.ArithmeticException: / by zero
异常getMessage:/ by zero
异常toString:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
	at com.cw.Main.main(Main.java:7)

使用 try-catch 捕获多个异常
一个 catch 语句只能捕获一种类型的异常,如果需要捕获多中异常类型就需要使用多个 catch 语句。

try {
    // 编写可能会出现异常的代码         
} catch (异常类型A 异常变量名) { //当 try 中出现A类型异常,就会使用该catch 来捕获
	// 处理异常的代码
} catch (异常类型B 异常变量名) { //当 try 中出现B类型异常,就会使用该catch 来捕获
	// 处理异常的代码
}

注意:代码在一瞬间只能出现一种类型的异常。

示例:

public static void main(String[] args) {
    String sNum1 = "10";
    String sNum2 = "0";

    try {
        int num1 = Integer.parseInt(sNum1);
        int num2 = Integer.parseInt(sNum2);
        int result = num1 / num2;

    } catch (ArithmeticException e) {
        e.printStackTrace();
    } catch (NumberFormatException e) {
        e.printStackTrace();
    } catch (Exception e) { //上面两种异常的父类,智能放在其他子类的catch之后,当都不属于上面异常类型的时候,会执行这里
        e.printStackTrace();
    }
}

当被除数字符串为 "0" 时,控制台打印:

java.lang.ArithmeticException: / by zero
	at com.cw.Main.main(Main.java:12)

当被除数字符串为 "1s" 时,控制台打印:

java.lang.NumberFormatException: For input string: "1s"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.parseInt(Integer.java:615)
	at com.cw.Main.main(Main.java:11)

finally

finally 语句块表示最终都会执行的代码,无论是否有异常.

当我们在 try 语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),都需要在使用完了之后,关闭所打开的资源。

语法:

try {
} finally {
}

示例:

public static void main(String[] args) {
    try {
        int result = 10 / 0 ;
        System.out.println("运算结果:" + result);

    } catch (ArithmeticException e) {
        e.printStackTrace();
    }  finally {
        System.out.println("最终必须执行的代码 - 关闭资源");
    }
}

打印信息,可以看到最终执行了 finally 代码块中的语句:

java.lang.ArithmeticException: / by zero
	at com.cw.Main.main(Main.java:8)
最终必须执行的代码 - 关闭资源

补充说明:
1、finally 不能单独使用;
2、当只有在 try 或者 catch 中调用 JVM 的相关方法(如 System.exit(0);),此时 finally 才不会执行,否则 finally 永远会执行;
3、如果 finallyreturn 语句,则永远会返回 finally 中的结果,应该避免这种情况的发生。如下面的示例:

public static void main(String[] args) {
    System.out.println("start。。。");

    int result = finallyTest();
    System.out.println(result);

    System.out.println("end。。。");
}

private  static  int finallyTest (){
    int a = 10;
    try {
        return a;
    } finally {
        a += 1;
        return  a;
    }
}

打印信息,可以看到 return 的值其实是 finally 代码块中的终值:

start。。。
11
end。。。

异常的分类

根据在编译时期还是运行时期去检查异常可分为:
1)编译时期异常(受检异常):checked 异常,在编译时期就会检查异常,如果没有处理异常,则编译失败。
2)运行时期异常(不受检异常):runtime 异常,在运行时期检查异常。

抛出异常

throw

运用于方法内部,抛出一个具体的异常对象。

语法格式:

throw new 异常类("异常信息"); //相当于一个终止,不会继续执行该代码块中后面的代码

当一个方法出现不正常的情况时,我们不知道该方法应该返回什么,此时就返回一个异常。
catch 语句块中向上层(调用者)抛出异常。
注意:return 是返回一个值,而 throw 是返回一个错误,返回给该方法的调用者。

示例:

public static void main(String[] args) {
    try {
        int result = divide(10, 0);
    } catch (ArithmeticException e) { //处理调用方法时抛出的异常
        e.printStackTrace();
    }
}

// 本方法不处理异常,而是直接抛出返回给方法调用者处理异常
private static int divide (int v1, int v2) {
    if (v2 == 0) {
        // 返回异常,由方法调用者处理异常
        throw new ArithmeticException("除数不能为0");
    } else {
        // 返回正常值
        return  v1 / v2;
    }
}

运行查看打印信息:

java.lang.ArithmeticException: 除数不能为0
	at com.cw.Main.divide(Main.java:14)
	at com.cw.Main.main(Main.java:6)

throws

throws 关键字,定义在方法申明的地方,表示当前方法不处理异常,提醒该方法的调用者来处理异常(抛出异常)。
如果每一个方法都不处理该异常,而直接通过 throws 向上层抛出,最后会抛到 main 方法,如果此时 main 方法不处理,继续抛出给 JVM,底层的处理机制是直接打印该异常的跟踪栈信息。runtime 异常就是这种情况。

示例代码:

public static void main(String[] args) throws Exception {
    int result = divide(10, 0);
}

// 在本方法中不处理某种类型的异常,提醒调用者需要来处理该异常
private static int divide (int v1, int v2) throws Exception {
    if (v2 == 0) {
        // 抛出Exception(受检异常)对象
        throw new Exception("除数不能为0");
    } else {
        // 返回正常值
        return  v1 / v2;
    }
}

打印信息:

Exception in thread "main" java.lang.Exception: 除数不能为0
	at com.cw.Main.divide(Main.java:12)
	at com.cw.Main.main(Main.java:5)

自定义异常类

开发中有些异常是 SUN 公司没有定义好的,我们可以根据业务的实际情况来自定义异常类。
自定义异常类:
方式1:自定义一个受检异常类,自定义类并继承 java.lang.Exception
方式2:自定义一个非受检异常类,自定义类并继承 java.lang.RuntimeException

示例:自定义非受检异常类 - LogicException

public class LogicException extends RuntimeException {
    public LogicException() {
    }

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

    /**
     *
     * @param message 当前异常的原因/信息
     * @param cause 当前异常的根被原因
     */
    public LogicException(String message, Throwable cause) {
        super(message, cause);
    }
}

使用 LogicException

public static void main(String[] args) {
    try {
        checkPhoneNumber("1552784110");
    } catch (LogicException e) {
        System.out.println(e.getMessage());
    }
}

private static boolean checkPhoneNumber(String phoneNumber) {
    if (phoneNumber.length() != 11) {
        throw new LogicException("手机号码长度异常");
    }
    return true;
}

打印信息:

手机号码长度异常

异常转译和异常链

异常转译:当位于最上层的子系统不需要关心底层的异常细节是,常见的做法是捕获原始的异常,把他们转换为一个新的不同类型的异常,再抛出新的异常。

异常链:把原始的异常包装为新的异常类,从而形成多个异常的有序排列,有助于查找异常的根本原因。

处理异常的原则

1、异常只能用于非正常情况下,try-catch 的存在会影响性能;
2、需要为异常提供说明文档,比如 java doc,如果定义了异常或某一个方法抛出了异常,我们应该记录在文档注释中;
3、尽可能避免异常;
4、异常的颗粒度很重要,应该为一个基本操作顶一个一个 try-catch 块,不要为了简便,将大量代码全放在一个 try-catch 中;
5、不建议在循环中进行异常处理,应该在循环外对异常进行捕获处理(在循环之外使用 try-catch);
6、自定义异常尽量使用 RuntimeException 类型的。