推荐一个网站给想要了解或者学习人工智能知识的读者,这个网站里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏网站,请点击下方链接查看。 https://www.captainbed.cn/f1
Java异常是在程序执行过程中发生的一些错误或异常情况。当程序运行时,如果出现了一些不可预料的情况,如除零错误、空指针引用等,程序会抛出一个异常对象。异常可以被捕获并进行相应的处理,以防止程序崩溃或产生错误的结果。Java中提供了一些异常类和异常处理机制,用于处理不同类型的异常。
Java 中的异常是一种特殊的错误运行对象,又称例外,是Throwable
子类的实例,描述了出现在一段编码中的错误条件,当条件生成时,错误将引发异常。
为了能够及时有效地处理程序中的运行错误,Java 引入了异常和异常类,异常是异常类的对象。一旦一个异常对象产生了,系统中就一定有相应的机制来处理它,确保不会产生死机、死循环或其他对操作系统的损害,保证整个程序运行的安全性。
Java 异常处理中的关键字有 try
、catch
、finally
、throw
、throws
、return
。
Java通过 API中 Throvable
类的众多子类描述各种不同的异常。在Java 中,所有的异常都有一个共同的祖先 Throwable
(可抛出)。Throwable
位于 java.lang
包中,有两个重要的子类:Exception
(异常)和 Error
(错误)。
Error
是程序无法处理的错误,表示运行应用程序中较严重的问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM 出现的问题。
Exception
是程序本身可以处理的异常。每一种异常类对应一种特定的错误,异常类都是系统 Exception
类的子类。Exception
类有一个重要的子类 RuntimeException
。该类及其子类表示“JVM 常用操作”引发的错误。
Java 中常见的系统定义的运行异常与所对应的系统运行错误见表3-5。
表3-5 系统定义的常见运行异常一览表
系统定义的运行异常 | 异常对应的系统运行错误 |
---|---|
ClassNotFoundException | 未找到欲装载使用的类 |
ArrayIndexOutOfBoundsException | 数组越界使用 |
FileNotFoundException | 未找到指定的文件或目录 |
IOException | 输入、输出错误 |
ArithmeticException | 算术错误,如除数为零 |
NullPointerException | 引用了未被初始化的对象 |
我们曾经的代码中已经接触了一些 “异常” 了.
例如:
System.out.println(10 / 0);
// 执行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
// 执行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
public class Test {
public int num = 10;
public static void main(String[] args) {
Test t = null;
System.out.println(t.num);
}
}
// 执行结果
Exception in thread "main" java.lang.NullPointerException
所谓异常指的就是程序在 运行时 出现错误时通知调用者的一种机制.
关键字 “运行时”
有些错误是这样的, 例如将 System.out.println
拼写错了, 写成了 system.out.println
. 此时编译过程中就会出错, 这是 “编译期” 出错.
而运行时指的是程序已经编译通过得到 class
文件了, 再由 JVM 执行过程中出现的错误.
异常的种类有很多, 不同种类的异常具有不同的含义, 也有不同的处理方式.
错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿.
我们有两种主要的方式
异常的核心思想就是 EAFP.
例如, 我们用伪代码演示一下开始一局王者荣耀的过程.
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
}
ret = 载入游戏画面();
if (!ret) {
处理载入游戏错误;
return;
}
......
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
对比两种不同风格的代码, 我们可以发现, 使用第一种方式, 正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱. 而第二种方式正常流程和错误流程是分离开的, 更容易理解代码.
try{
有可能出现异常的语句 ;
}[catch (异常类型 异常对象) {
} ... ]
[finally {
异常的出口
}]
try
代码块中放的是可能出现异常的代码.catch
代码块中放的是出现异常后的处理行为.finally
代码块中的代码用于处理善后工作, 会在最后执行.catch
和 finally
都可以根据情况选择加或者不加.int[] arr = {1, 2, 3};
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
// 执行结果
before
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
我们发现一旦出现异常, 程序就终止了. after
没有正确输出.
int[] arr = {1, 2, 3};
try {
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
// 打印出现异常的调用栈
e.printStackTrace();
}
System.out.println("after try catch");
// 执行结果
before
java.lang.ArrayIndexOutOfBoundsException: 100
at demo02.Test.main(Test.java:10)
after try catch
我们发现, 一旦 try
中出现异常, 那么 try
代码块中的程序就不会继续执行, 而是交给 catch
中的代码来执行. catch
执行完毕会继续往下执行.
我们修改了代码, 让代码抛出的是空指针异常.
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("after try catch");
// 执行结果
before
Exception in thread "main" java.lang.NullPointerException
at demo02.Test.main(Test.java:11)
此时, catch
语句不能捕获到刚才的空指针异常. 因为异常类型不匹配.
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
} catch (NullPointerException e) {
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
// 执行结果
before
这是个空指针异常
java.lang.NullPointerException
at demo02.Test.main(Test.java:12)
after try catch
一段代码可能会抛出多种不同的异常, 不同的异常有不同的处理方式. 因此可以搭配多个 catch
代码块.
如果多个异常的处理方式是完全相同, 也可以写成这样
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
...
}
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("after try catch");
// 执行结果
before
java.lang.NullPointerException
at demo02.Test.main(Test.java:12)
after try catch
由于 Exception
类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常.
ps:catch
进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象.如刚才的代码, NullPointerException
和 ArrayIndexOutOfBoundsException
都是 Exception
的子类, 因此都能被捕获到.
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally code");
}
// 执行结果
before
java.lang.NullPointerException
at demo02.Test.main(Test.java:12)
finally code
无论是否存在异常, finally
中的代码一定都会执行到. 保证最终一定会执行到 Scanner
的 close
方法.
刚才的代码可以有一种等价写法, 将 Scanner
对象在 try
的 ( )
中创建, 就能保证在 try
执行完毕后自动调用 Scanner
的 close
方法.
try (Scanner sc = new Scanner(System.in)) {
int num = sc.nextInt();
System.out.println("num = " + num);
} catch (Exception e) {
e.printStackTrace();
}
public static void main(String[] args) {
try {
func();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
public static void func() {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
}
// 直接结果
java.lang.ArrayIndexOutOfBoundsException: 100
at demo02.Test.func(Test.java:18)
at demo02.Test.main(Test.java:9)
after try catch
public static void main(String[] args) {
func();
System.out.println("after try catch");
}
public static void func() {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
}
// 执行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at demo02.Test.func(Test.java:14)
at demo02.Test.main(Test.java:8)
可以看到, 程序已经异常终止了, 没有执行到 System.out.println("after try catch");
这一行.
IDEA 能自动检查我们的代码风格, 并给出一些更好的建议.
如我们之前写的代码, 在 try
上有一个 “加深底色” , 这时 IDEA 针对我们的代码提出了一些更好的建议.
此时把光标放在 try
上悬停, 会给出原因. 按下 alt + enter
, 会弹出一个改进方案的弹窗. 我们选择其中的
此时我们的代码就自动被 IDEA 调整成上面的 代码的模样
异常的种类有很多, 我们要根据不同的业务场景来决定.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.
方法之间是存在相互调用关系的, 这种调用关系我们可以用 “调用栈” 来描述.
在 JVM 中有一块内存空间称为 “虚拟机栈” 专门存储方法之间的调用关系. 当代码中出现异常的时候, 我们就可以使用e.printStackTrace();
的方式查看出现异常代码的调用栈.
try
中的代码try
中的代码出现异常, 就会结束 try
中的代码, 看和 catch
中的异常类型是否匹配.catch
中的代码finally
中的代码都会被执行到(在该方法结束之前执行).main
方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.除了 Java 内置的类会抛出一些异常之外, 程序猿也可以手动抛出某个异常. 使用 throw
关键字完成这个操作.
public static void main(String[] args) {
System.out.println(divide(10, 0));
}
public static int divide(int x, int y) {
if (y == 0) {
throw new ArithmeticException("抛出除 0 异常");
}
return x / y;
}
// 执行结果
Exception in thread "main" java.lang.ArithmeticException: 抛出除 0 异常
at demo02.Test.divide(Test.java:14)
at demo02.Test.main(Test.java:9)
在这个代码中, 我们可以根据实际情况来抛出需要的异常. 在构造异常对象同时可以指定一些描述性信息.
我们在处理异常的时候, 通常希望知道这段代码中究竟会出现哪些可能的异常.
我们可以使用 throws
关键字, 把可能抛出的异常显式的标注在方法定义的位置. 从而提醒调用者要注意捕获这些异常.
public static int divide(int x, int y) throws ArithmeticException {
if (y == 0) {
throw new ArithmeticException("抛出除 0 异常");
}
return x / y;
}
finally
中的代码保证一定会执行到. 这也会带来一些麻烦.
public static void main(String[] args) {
System.out.println(func());
}
public static int func() {
try {
return 10;
} finally {
return 20;
}
}
// 执行结果
20
finally
执行的时机是在方法返回之前(try
或者 catch
中如果有 return
会在这个 return
之前执行 finally
). 但是如果finally
中也存在 return
语句, 那么就会执行 finally
中的 return
, 从而不会执行到 try
中原有的 return
.
一般我们不建议在 finally
中写 return
(被编译器当做一个警告).
Java 内置了丰富的异常体系, 用来表示不同情况下的异常.
下图表示 Java 内置的异常类之间的继承关系:
Throwable
派生出两个重要的子类, Error
和 Exception
Error
指的是 Java 运行时内部错误和资源耗尽错误. 应用程序不抛出此类异常. 这种内部错误一旦出现,除了告知用户并使程序终止之外, 再无能无力. 这种情况很少出现.Exception
是我们程序猿所使用的异常类的父类.Exception
有一个子类称为 RuntimeException
, 这里面又派生出很多我们常见的异常类NullPointerException
, IndexOutOfBoundsException
等.Java语言规范将派生于 Error
类或 RuntimeException
类的所有异常称为 非受查异常, 所有的其他异常称为 受查异常.
如果一段代码可能抛出 受查异常, 那么必须显式进行处理.
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
// 尝试打开文件, 并读其中的一行.
File file = new File("d:/test.txt");
// 使用文件对象构造 Scanner 对象.
Scanner sc = new Scanner(file);
return sc.nextLine();
}
// 编译出错
Error:(13, 22) java: 未报告的异常错误java.io.FileNotFoundException; 必须对其进行捕获或声明以便抛出
查看 Scanner
的构造方法可以发现, 存在 FileNotFoundException
这样的异常说明.
public Scanner(File source) throws FileNotFoundException {
...
}
如 FileNotFoundException
这样的异常就是受查异常. 如果不显式处理, 编译无法通过.
显式处理的方式有两种
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
File file = new File("d:/test.txt");
Scanner sc = null;
try {
sc = new Scanner(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return sc.nextLine();
}
public static void main(String[] args) {
try {
System.out.println(readFile());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static String readFile() throws FileNotFoundException {
File file = new File("d:/test.txt");
Scanner sc = new Scanner(file);
return sc.nextLine();
}
Java 中虽然已经内置了丰富的异常类, 但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展, 创建符合我们实际情况的异常.
例如, 我们实现一个用户登陆功能.
public class Test {
private static String userName = "admin";
private static String password = "123456";
public static void main(String[] args) {
login("admin", "123456");
}
public static void login(String userName, String password) {
if (!Test.userName.equals(userName)) {
// TODO 处理用户名错误
}
if (!Test.password.equals(password)) {
// TODO 处理密码错误
}
System.out.println("登陆成功");
}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常.
我们可以基于已有的异常类进行扩展(继承), 创建和我们业务相关的异常类.
class UserError extends Exception {
public UserError(String message) {
super(message);
}
}
class PasswordError extends Exception {
public PasswordError(String message) {
super(message);
}
}
此时我们的 login
代码可以改成
public static void main(String[] args) {
try {
login("admin", "123456");
} catch (UserError userError) {
userError.printStackTrace();
} catch (PasswordError passwordError) {
passwordError.printStackTrace();
}
}
public static void login(String userName, String password) throws UserError,
PasswordError {
if (!Test.userName.equals(userName)) {
throw new UserError("用户名错误");
}
if (!Test.password.equals(password)) {
throw new PasswordError("密码错误");
}
System.out.println("登陆成功");
}
Exception
或者 RuntimeException
Exception
的异常默认是受查异常RuntimeException
的异常默认是非受查异常