更好的阅读体验:点这里 ( www.doubibiji.com
)
10 异常
10.1 异常的概念
什么是异常?
异常(Exceptions)是指在程序执行过程中出现的错误或异常情况,导致程序无法继续正常执行的事件。
但是程序语法错误和逻辑错误不属于异常。在 Java 中,异常分为 Error 和 Exception,但是 Error 是 Java 虚拟机无法解决的严重问题,例如递归调用没有跳出条件,导致无限递归产生的 StackOverflowError,或内存不足造成的 OutOfMemeryError,Error 异常一般不编写针对性的代码来处理,只能改代码来处理。我们在这里要针对性处理的是 Exception 异常。
Exception 又分为 编译时异常 和 运行时异常。
- 编译时异常:在编译的时候就需要对可能存在的异常进行处理,也叫受检异常。
- 运行时异常:在编译的时候不强制要求处理,也叫非受检异常,
java.lang.RuntimeException
类及它的子类都是运行时异常。
异常类的继承关系及常见的异常类:
1 运行时异常举例
例如:整数除数是0会抛出异常。
public static void main(String[] args) {int i = 5 / 0;System.out.println(i);
}
执行的时候就会发生错误:
Exception in thread "main" java.lang.ArithmeticException: / by zeroat com.doubibiji.Test.main(Test.java:8)
当程序出现错误的时候,我们通常称之为抛出异常,抛出异常后,程序终止执行。
2 编译时异常举例
以 IO 异常为例:
public static void main(String[] args) {FileInputStream fis = new FileInputStream("xxx.txt");int x = fis.read();System.out.println(x);fis.close();
}
上面的代码无法通过编译,因为没有对编译时异常进行处理。
上面的 FileInputStream 是用来读取文件内容的,在后面的文件与IO章节会讲到。
10.2 捕获异常
程序抛出异常就无法继续执行了,但是任何程序都不可能是完美的没有bug的,只能尽可能的对错误进行预防和处理。
对异常进行预防和提前处理,这种行为通常称之为异常捕获。
一个程序出现任何错误,就停止运行肯定不是我们希望看到的,即使程序出现错误,哪怕给与用户一个错误提示,也是一种积极的处理方式。
1 异常捕获语法
最简单的异常捕获语法:
try {// 可能会抛出异常的代码
} catch (Exception e) {// 捕获并处理异常
}
举个栗子,捕获前面例子中运行时异常:
public static void main(String[] args) {try {int i = 5 / 0;System.out.println(i);} catch (Exception e) {// 捕获并处理异常System.out.println("除数不能为0");}System.out.println("异常后的代码");
}
将可能抛出异常的代码,放在 try
代码块中,上面的程序执行到 int i = 5 / 0;
会抛出异常,然后执行 catch
块中的语句。
出现异常后,在 try 代码块中,抛出异常语句之后的代码是无法被继续执行的,但是在 try-catch 后面的代码可以继续执行。
执行结果:
除数不能为0
异常后的代码
这样在发生错误的时候,可以给用户一个提示。当然具体的处理方式需要根据业务需求的处理,这里只是举个例子。
另外如果方法有返回值,还需要在 catch 块中,根据需要进行返回:
public static int test() {try {int[] nums = new int[]{1, 2, 3};return nums[3]; // 会抛出异常} catch (Exception e) {e.printStackTrace();return 0; // 需要返回值,否则方法没有返回值}
}
2 打印异常详细信息
捕获异常后,在 catch 块中,可以通过异常对象和 stacktrace 对象打印异常的详细信息。
public static void main(String[] args) {try {int i = 5 / 0;System.out.println(i);} catch (Exception e) {// 捕获并处理异常System.out.println("除数不能为0");System.out.println(e.getMessage()); // 打印异常信息:/ by zeroe.printStackTrace(); // 打印异常堆栈跟踪信息}System.out.println("异常后的代码");
}
执行结果:
3 捕获指定类型的异常
在程序运行时,可能会出现不同类型的异常,有时候可能只想捕获和处理指定类型的异常,这个时候就需要根据类型来捕获异常了。因为 Exception e
会捕获所有类型的异常。
语法:
public static void main(String[] args) {try {int i = 5 / 0;System.out.println(i);} catch (ArithmeticException e) {// 捕获并处理异常System.out.println("除数不能为0");e.printStackTrace();}System.out.println("异常后的代码");
}
上面捕获了 ArithmeticException
类型的异常,在捕获异常后,打印了异常信息和提示信息。
需要注意,上面只是捕获了 ArithmeticException 类型的异常,如果有代码抛出了其他类型的异常,是无法捕获的。
举个栗子,我们修改代码如下:
public static void main(String[] args) {try {int[] nums = new int[3];System.out.println(nums[3]);} catch (ArithmeticException e) {// 捕获并处理异常System.out.println("除数不能为0");e.printStackTrace();}System.out.println("异常后的代码");
}
在上面的代码中,nums
的长度为 3,所以 nums[3]
会报数组越界异常(ArrayIndexOutOfBoundsException) ,但是我们没有捕获这个异常,导致程序终止。
执行信息:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3at com.doubibiji.Test.main(Test.java:11)
如果想单独针对某个异常做特殊处理,并其他异常使用 Exception 进行兜底,这种情况需要捕获多个异常。
4 捕获多个异常
我们可以同时捕获多个异常,并对每种异常可以采用不同的处理。
举个栗子:
public static void main(String[] args) {try {int[] nums = new int[3];System.out.println(nums[3]);} catch (ArithmeticException e) {// 捕获并处理异常System.out.println("除数不能为0");e.printStackTrace();} catch (ArrayIndexOutOfBoundsException e) {// 捕获并处理异常System.out.println("数组越界");e.printStackTrace();} catch (Exception e) {e.printStackTrace();}System.out.println("异常后的代码");
}
如果我们不能判断是否还会抛出其他类型的错误,可以在最后添加 catch (Exception e)
用来捕获其他类型的错误。
5 对多个异常使用相同的处理
刚才针对指定的异常进行捕获,然后针对性的处理。
如果相对多个异常采用相同的处理,那么可以使用 |
操作符。
举个例子:
try {int[] nums = new int[3];System.out.println(nums[3]);
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) { // 统一处理两个类型的异常// 捕获并处理异常System.out.println("统一处理多个异常");e.printStackTrace();
} catch (Exception e) {e.printStackTrace();
}
6 异常finally
finally
表示的是无论是否发生异常都要执行的代码。
举个栗子:
public static void main(String[] args) {try {int[] nums = new int[3];System.out.println(nums[3]);} catch (Exception e) {System.out.println("出现异常");return ;} finally {System.out.println("出现异常也会被执行");}System.out.println("异常后的代码");
}
执行结果:
出现异常
出现异常也会被执行
哪怕出现异常,使用 return 返回,终止了方法的执行,但是 finally 代码块还是在返回之前被执行。
finally 一般在文件读写操作的时候用的比较多,就是不管是否发生错误都要关闭文件,所以可以将文件的关闭操作放在 finally 块中。
举个例子,捕获前面例子中的编译时异常:
public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("xxx.txt");int x = fis.read();System.out.println(x);} catch (IOException e) {e.printStackTrace();} finally {try {fis.close(); // 关闭也会抛出异常} catch(IOException e) {e.printStackTrace();}}
}
在上面的代码中,对文件进行读取,最终在 finally 块中关闭流,关于 IO 后面的章节再讲,这里可以看出,try-catch-finally
是可以嵌套的。
再看一段代码:
public class ExceptionTest {public static void main(String[] args) {int i = test();System.out.println(i); // 3}public static int test() {try {int[] nums = new int[]{1, 2, 3};return nums[3]; // 抛出异常} catch (Exception e) {e.printStackTrace();return 2;} finally {System.out.println("一定会执行");return 3;}}
}
在 test() 方法中,try 代码块中会抛出异常,进入到 catch 代码块中,但是在 return 2
之前会执行 finally 代码块中的内容,所以会先执行 return 3
,但是 return 后方法就结束了,所以返回值是 3
。
10.3 throws异常
前面使用 try-catch-finally
捕获异常并进行处理,这是一种异常的处理方式。
另一种异常的处理方式就是使用 throws 进行处理,其实这种处理方式就是不处理,而是将异常往上抛,抛给调用的方法,并没有真正处理异常。
例如之前的编译时异常,我们使用了 try-catch-finally 进行捕获处理,也可以使用 throws 抛出异常。
public class ExceptionTest {public static void main(String[] args) {try {test();} catch(IOException e) {e.printStackTrace();}}// 将IOException抛出,交由调用者处理public static void test() throws IOException {FileInputStream fis = new FileInputStream("xxx.txt");int x = fis.read();System.out.println(x);fis.close();}
}
在上面的代码中,test() 中的代码是会抛出编译时异常的,但是没有使用 try-catch-finally 进行捕获处理,而是直接使用 throws 指明此方法会抛出的异常,那么此方法就不用对异进行处理了,而是由调用者进行处理,调用者可以使用 try-catch-finally 进行捕获处理,或者也可以使用 throws 进行处理,继续向上抛出。
在子类重写父类方法的时候,父类方法使用throws指明了抛出的异常类型,那么子类方法throws的异常类型不能大于父类抛出的异常类型,只能是父类抛出异常的类型或其子类型。如果父类方法没有throws异常,重写的时候也不能throws异常。
10.4 异常的传递
什么是异常的传递?
异常的传递,就是当方法执行的时候出现异常,如果没有进行捕获,就会将异常传递给该方法的调用者,如果调用者仍未处理异常,则继续向上传递,直到传递给主程序,如果主程序仍然没有进行异常处理,则程序将被终止。
举个栗子:
public class Test{public static void main(String[] args) {try {testException();} catch (Exception e) {e.printStackTrace();}}public static void testException() {testNullPoint();}public static void testNullPoint() {String str = null;str.length(); // 会抛出空指针异常}
}
上面代码,执行 testNullPoint()
会抛出空指针异常,因为 testNullPoint()
函数中没有进行异常处理,则异常会抛给 testException()
函数,testException()
没有进行异常处理,则抛给 main()
函数,main()
函数中则对异常进行了处理,程序不会崩溃。
利用异常的传递性,如果我们在 main()
函数中进行了异常捕获,无论程序哪里发生了错误,最终都会被传递到 main()
函数中,保证所有的异常都会被捕获。但是不要所有的异常都在 main()
函数中处理,main()
函数中的异常处理只是兜底处理。
10.5 主动抛出异常
除了代码执行的时候出错,系统会主动抛出异常,我们还可以根据实际的业务需要,主动抛出异常。
首先创建一个异常对象,然后使用 throw 关键字抛出异常对象(不是 throws)。
举个栗子:
public class Test{public static void main(String[] args) {try {String name = inputUsername();System.out.println("输入的用户名:" + name);} catch (Exception e) {System.out.println("用户名格式错误,请重新输入");}}/*** 从键盘读取用户名*/public static String inputUsername() {System.out.println("请输入用户名:");Scanner scanner = new Scanner(System.in);// 读取键盘输入String name = scanner.nextLine();if (null == name || name.length() > 16 || name.length() < 8) {throw new RuntimeException("用户名格式错误"); // 主动抛出异常}return name;}
}
在上面的代码中,如果输入用户名不正确,主动创建一个 RuntimeException 异常对象,并使用 throw 抛出。
当输入abc的时候,执行结果:
请输入用户名:
abc
用户名格式错误,请重新输入
10.6 自定义异常类
除了使用 Java 内置的异常类型外,您还可以自定义异常类,以便更好地描述特定的异常情况。
自定义异常类一般情况下都是继承 Exception 或 RuntimeException 类,如果继承 Exception,属于编译时异常,需要在编译的时候就要处理,如果继承 RuntimeException,那么是运行时异常。
异常类建议以 Exception 结尾,见名知意。
举个栗子:
下面我们首先自定义了两个异常类,然后根据需要抛出这两个异常类的对象,在捕获异常的时候,根据异常类型进行不同的处理。
并在自定义异常类中可以封装自定义的参数。
import java.util.Scanner;// 定义异常类一:一般自定义异常类,定义两个构造方法,并调用父类的构造方法
class UsernameEmptyException extends RuntimeException {public UsernameEmptyException() {}public UsernameEmptyException(String message) {super(message);}
}// 定义异常类二:可以在自己的异常类中定义各种参数
class UsernameLengthException extends RuntimeException {int minLength;int maxLength;String message;// 自定义异常类public UsernameLengthException(String message, int minLength, int maxLength) {super(message); // 调用父类构造,并传递异常信息this.message = message;this.minLength = minLength;this.maxLength = maxLength;}@Overridepublic String toString() {return this.message + ", minLength:" + this.minLength + ", maxLength:" + this.maxLength;}
}/*** 测试类*/
public class ExceptionTest {public static void main(String[] args) {try {String username = inputUsername();System.out.println("输入的用户名:$password");} catch(UsernameEmptyException e) {System.out.println("用户名不能为空");} catch (UsernameLengthException e) {System.out.println(e.getMessage());}}public static String inputUsername() {System.out.println("请输入用户名:");Scanner scanner = new Scanner(System.in);// 读取键盘输入String name = scanner.nextLine();if (null == name || name.length() < 1) {throw new UsernameEmptyException("用户名为空"); // 抛出异常}if (name.length() > 16 || name.length() < 8) {throw new UsernameLengthException("用户名格式错误", 8, 16); // 抛出异常}return name;}
}