初识异常
例如:除以0
public class Test {
public static void main(String[] args) {
System.out.println(10 / 0);
}
}

数组下标越界:
public class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
}
}

访问null对象
public class Test {
public int num = 10;
public static void main(String[] args) {
Test t = null;
System.out.println(t.num);
}
}

异常就是指程序运行时,出现错误时通知程序调用者的一种机制。
- 注意异常时运行时的一种机制,不是编译期。
- 我们可以举个例子来理解一下什么是编译期的错误,比如我们写代码时出现关键字的拼写错误,此时出现的错误就是编译期的错误。
防御式编程
- 防御式编程时提高软件质量技术的有益辅助手段;
- 防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据;
- 这种思想将可能出现的错误造成的影响控制在有限的范围内。
错误在代码中是客观存在的。因此我们要让程序出现问题的时候及时通知程序猿。主要有两种方式:
- LBYL:Look Before You Leap,在操作之前就做充分的检查;
-
EAFP:It’s Easier to Ask Forgiveness than Permission,先操作,遇到问题再处理。
异常的核心思想就是EAFP(先操作,遇到问题再处理)
异常的优点
- LBYL:
boolean ret = 创建套接字();
if(!ret) {
创建套接字失败;
return;
}
ret = 绑定地址信息();
if(!ret) {
绑定地址信息失败;
return;
}
...
- EAFP:
try {
创建套接字();
绑定地址信息();
...
} catch (创建套接字异常) {
处理创建套接字异常;
} catch (绑定地址信息异常) {
处理绑定地址信息异常;
}
...
对比两种风格的代码,我们可以发现:
- LBLY处理方式会将正常流程和错误处理流程代码混在一起,代码整体显得比较混乱;
- EAFP处理方式的正常流程和错误处理流程是分开的,更容易理解代码。
异常的基本用法
异常捕捉
- 基本语法
try {
//可能出现的异常
} [catch (异常类型 异常对象) {
//异常处理
} ...]
[finally {
//异常出口
}]
- try代码块中放的是可能出现异常的代码;
- catch代码块中放的是出现异常后的处理行为;
- finally代码块中的代码用于处理善后工作,会在最后执行;
- 其中catch和finally都可以根据情况选择加或者不加。
下面我们看几种常见的异常处理方式:
- 不处理异常
public class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
System.out.println("hello");
}
}

可以看到,如果对于异常不进行处理,程序就会在出现异常处终止,后序的代码将不再执行。其实这里异常并不是没有被处理,而是被JVM处理,JVM处理异常的方式就是打印出现异常的调用栈信息并终止程序。
- 使用try catch后的程序执行过程
public class Test {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 6};
try {
System.out.println(arr[100]);
} catch(ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

从运行结果可以看出,try中一旦有了异常,就会跳到对应的catch中,不再执行try中剩余的逻辑。
- catch只能处理对应种类的异常
public class Test {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 6};
try {
System.out.println(arr[100]);
} catch(NullPointerException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

可以看到,这里的catch语句并没有捕获到数组访问越界的异常,该异常最终被JVM处理。
catch可以有多个
public class Test {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 6};
try {
System.out.println(arr[100]);
} catch(NullPointerException e) {
e.printStackTrace();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

- 也可以使用一个catch捕获所有的异常。Exception类是所有异常类的父类。因此可以用这个类型表示捕获所有异常。catch进行匹配的时候,不仅可以捕捉到相同类型的异常,还可以捕捉到目标类型异常的子类对象。不推荐使用这种方式。
public class Test {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 6};
try {
System.out.println(arr[100]);
} catch(Exception e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

- finally表示最后的善后工作,如释放资源
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
System.out.println("Before scanner");
int num = scanner.nextInt();
System.out.println("After scanner");
} catch(InputMismatchException e) {
e.printStackTrace();
} finally {
System.out.println("scanner.close()");
scanner.close();
}
System.out.println("hello");
}
}

无论try中是否发生异常,finally中的代码一定会执行。
- 可以使用try回收资源
public class Test {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Before scanner");
int num = scanner.nextInt();
System.out.println("After scanner");
} catch(InputMismatchException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}

和前一个代码的写法等价,将Scanner对象在try的()中创建,能够保证在try执行完毕后自动调用Scanner的close方法。
- 如果当前方法中没有合适的异常处理方式,异常就会沿着调用栈向上传递,直到最后交给JVM处理
public class Test {
private static void func() {
int[] arr = {1, 2, 3, 5};
System.out.println(arr[100]);
}
public static void main(String[] args) {
try {
func();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("hhh");
}
}

异常处理流程
- 程序先执行try中的代码;
- 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配;
- 如果找到匹配的异常类型,就会执行catch中的代码;
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者;
- 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行);
- 如果上层调用者也无法处理异常,异常就会继续向上传递;
- 一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止。
异常抛出
除了Java内置的类会抛出一些异常之外,程序猿也可以手动抛出某个异常。使用throw关键字来完成这个操作。
下面来看一个具体的例子:
public class Test {
private static int div(int a, int b) {
if(b == 0) {
throw new ArithmeticException("这是我自己抛出的异常");
}
return a / b;
}
public static void main(String[] args) {
int ret = div(10, 0);
System.out.println(ret);
}
}

异常说明
我们在处理异常时,通常希望知道这段代码中究竟会出现哪些可能的异常。我们可以使用throws关键字,把可能抛出的异常显式的标注在方法定义的位置。从而提醒调用者要注意捕获。
public class Test {
private static int div(int a, int b) throws ArithmeticException {
if(b == 0) {
throw new ArithmeticException("这是我自己抛出的异常");
}
return a / b;
}
public static void main(String[] args) {
int ret = div(10, 0);
System.out.println(ret);
}
}

finally的注意事项
public class Test {
private static int func() {
try {
return 10;
} finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(func());
}
}

注意
- finally执行的时机是在方法返回之前(try或者catch中如果有return会在这个return之前执行finally);
- 但是如果finally中也存在return语句,那么就会执行finally中的return,从而不会执行到try中原有的return;
- 不建议在finally中写return语句,编译器会有警告。
java异常体系
- 顶层类Throwable派生出两个重要的子类,Error和Exception;
- 其中Error指的是Java运行时内部错误和资源耗尽错误。应用程序不抛出此类异常。这种内部错误一旦出现,除了告知用户并使程序终止之外,没有别的办法,这种情况很少出现;
- Exception是程序猿使用的异常类的父类;
- 其中Exception有一个子类称为RuntimeException,这里面又派生出很多我们常见的异常类NullPointerException等。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查异常,所有的其他异常称为受查异常。
如果一段代码可能抛出受查异常,那么必须显式进行处理。
import java.io.File;
import java.util.InputMismatchException;
import java.util.Scanner;
import java.util.Stack;
public class Test {
public static void main(String[] args) {
File file = new File("d:/test.txt");
Scanner sc = new Scanner(file);
String str = sc.next();
}
}

从报错信息可以看出,我们必须对受查异常进行处理。
这里有两种处理方式:
- 方法一:使用try catch包裹起来进来
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.FileAlreadyExistsException;
import java.util.InputMismatchException;
import java.util.Scanner;
import java.util.Stack;
public class Test {
public static void main(String[] args) {
File file = new File("e:/test.txt");
Scanner sc = null;
try {
sc = new Scanner(file);
String str = sc.nextLine();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("After try catch");
}
}

- 方法二:在方法上加上异常说明,相当于将处理动作交给上级调用者
public class Test {
public static void main(String[] args) throws FileNotFoundException {
File file = new File("e:/test.txt");
Scanner sc = new Scanner(file);
String str = sc.nextLine();
}
}

自定义异常类
中虽然已经内置了丰富的异常类,但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展,创建符合我们实际情况的异常。
这里,我们模拟一个用户登录的场景:
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.FileAlreadyExistsException;
import java.util.InputMismatchException;
import java.util.Scanner;
import java.util.Stack;
public class Test {
public static void main(String[] args) throws FileNotFoundException {
try (Scanner sc = new Scanner(System.in)) {
System.out.println("请输入用户名:");
String name = sc.next();
System.out.println("请输入登录密码:");
String password = sc.next();
login(name, password);
} catch (PasswordException e) {
e.printStackTrace();
} catch (UserException e) {
e.printStackTrace();
}
}
private static void login(String name, String password) throws UserException, PasswordException {
if(!"admin".equals(name)) {
throw new UserException("用户名错误");
}
if(!"123456".equals(password)) {
throw new PasswordException("密码错误");
}
System.out.println("登录成功");
}
}
class UserException extends Exception {
public UserException(String msg) {
super(msg);
}
}
class PasswordException extends Exception {
public PasswordException(String msg) {
super(msg);
}
}



注意:
- 自定义异常通常会继承自Exception或者RuntimeException;
- 继承自Exception的异常默认是受查异常;
- 继承自RuntimeException的异常默认是非受查异常;
- 自定义异常类往往不是创建一个类,而是创建一个系列。