异常是程序执行期间发生的意外事件。它影响程序指令的流程,这可能导致程序异常终止。

出于多种原因可能会发生异常。他们之中有一些是:

  • 无效的用户输入
  • 设备故障
  • 网络连接丢失
  • 物理限制(磁盘内存不足)
  • 代码错误
  • 打开一个不可用的文件

Java异常层次结构

这是Java中异常层次结构的简化图:

Throwable类是层次结构中的根类。请注意,层次结构分为两个分支:ErrorException

Errors

错误表示不可恢复的情况,例如Java虚拟机(JVM)内存不足,内存泄漏,堆栈溢出错误,库不兼容,无限递归等.

错误通常是程序员无法控制的,我们不应该尝试处理错误。

Exceptions

程序可以捕获并处理异常。

当方法中发生异常时,它将创建一个对象。该对象称为异常对象。

它包含有关异常的信息,例如,异常的名称和说明以及发生异常时程序的状态。

Java异常类型

异常层次结构还具有两个分支:RuntimeExceptionIOException

RuntimeException

由于编程错误而发生运行时异常,它们也称为非检查异常。

这些异常不在编译时检查,而是在运行时检查.一些常见的运行时异常是:

  • API使用不当-IllegalArgumentException
  • 空指针访问(缺少变量的初始化)- NullPointerException
  • 越界数组访问 - ArrayIndexOutOfBoundsException
  • 将数字除以0 - ArithmeticException

你可以这么理解: “如果是运行时异常,那是您的错”。如果在使用变量之前检查了变量是否已初始化,则不会发生NullPointerException

如果针对数组范围测试了数组索引,则不会发生ArrayIndexOutOfBoundsException

IOException

IOException也称为检查异常。它们由编译器在编译时检查,并提示程序员处理这些异常。

受检查的异常的一些示例是:

  • 尝试打开不存在的文件会导致FileNotFoundException
  • 尝试读取文件末尾

Java异常处理

异常是程序执行期间发生的意外事件

捕捉和处理异常

在Java中,我们使用异常处理程序组件try, catchfinally块来处理异常

为了捕获和处理异常,我们在可能会生成异常的代码周围放置try ... catch ... finally

,finally块是可选的.

1
2
3
4
5
6
7
try {
// code
} catch (ExceptionType e) {
// catch block
} finally {
// finally block
}

try…catch block

可能产生异常的代码放在try块中

每个try块应紧随catchfinally块,发生异常时,它会被紧随其后的catch块捕获。

catch块不能单独使用,并且必须始终在try块之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Main {
public static void main(String[] args) {

try {
int divideByZero = 5 / 0;
System.out.println("Rest of code in try block");
} catch (ArithmeticException e) {
System.out.println("ArithmeticException => " + e.getMessage());
}

}
}
// 输出
// ArithmeticException => / by zero
  • 我们在try块中将数字除以0。这将产生ArithmeticException
  • 发生异常时,程序将跳过try块中的其余代码
  • 在这里,我们创建了一个catch块来处理ArithmeticException,因此,执行catch块内的语句。

如果try块中的所有语句均未生成异常,则将跳过catch块.

多个catch 模块

对于每个try块,可以有零个或多个catch块.

每个catch块的参数类型指示其可以处理的异常类型,多个catch块使我们能够以不同方式处理每个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ListOfNumbers {
public int[] arrayOfNumbers = new int[10];

public void writeList() {

try {
arrayOfNumbers[10] = 11;
} catch (NumberFormatException e1) {
System.out.println("NumberFormatException => " + e1.getMessage());
} catch (IndexOutOfBoundsException e2) {
System.out.println("IndexOutOfBoundsException => " + e2.getMessage());
}

}
}

class Main {
public static void main(String[] args) {
ListOfNumbers list = new ListOfNumbers();
list.writeList();
}
}
// 输出
// IndexOutOfBoundsException => Index 10 out of bounds for length 10

在此示例中,声明了一个大小为10的整数arrayOfNumbers数组,我们知道数组索引总是从0开始。因此,当我们尝试为索引10分配一个值时,就会发生IndexOutOfBoundsException,因为arrayOfNumbers的数组范围是0到9。

当try块中发生异常时:

  • 异常被引发到第一个catch块。第一个catch块不处理IndexOutOfBoundsException,因此将其传递到下一个catch块。
  • 上面示例中的第二个catch块是适当的异常处理程序,因为它处理IndexOutOfBoundsException

Java finally

对于每个try块,只能有一个finally块,finally块是可选的。但是,如果已定义,它将始终执行(即使不会发生异常)。

如果发生异常,则在try ... catch块之后执行该异常。如果没有异常发生,则在try块之后执行。

1
2
3
4
5
6
7
8
9
try {
//code
} catch (ExceptionType1 e1) {
// catch block
} catch (ExceptionType1 e2) {
// catch block
} finally {
// finally block always executes
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Main {
public static void main(String[] args) {
try {
int divideByZero = 5 / 0;
} catch (ArithmeticException e) {
System.out.println("ArithmeticException => " + e.getMessage());
} finally {
System.out.println("Finally block is always executed");
}
}
}
// 输出
// ArithmeticException => / by zero
// Finally block is always executed

在此示例中,数字除以0。这引发了一个由catch块捕获的ArithmeticException,finally块始终执行。

进行最后finally被认为是一种好习惯。这是因为在此可以进行代码清理的动作,例如:

  • 返回,继续或中断语句可能意外跳过的代码
  • 关闭文件或连接

我们已经提到,finally总是执行,通常就是这种情况。但是,在某些情况下finally块不执行:

  • 使用System.exit()方法
  • finally块中发生异常
  • 线程挂掉

举一个例子,我们尝试使用FileWriter创建一个新文件,然后使用PrintWriter将数据写入其中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.io.*;

class ListOfNumbers {
private int[] list = new int[10];

public ListOfNumbers() {
// storing integer values in the list array
for (int i = 0; i < 10; i++) {
list[i] = i;
}
}

}

public void writeList() {
PrintWriter out = null;

try {
System.out.println("Entering try statement");

// creating a new file OutputFile.txt
out = new PrintWriter(new FileWriter("OutputFile.txt"));

// writing values from list array to the new created file
for (int i = 0; i < 10; i++) {
out.println("Value at: " + i + " = " + list[i]);
}
} catch (IndexOutOfBoundsException e1) {
System.out.println("IndexOutOfBoundsException => " + e1.getMessage());
} catch (IOException e2) {
System.out.println("IOException => " + e2.getMessage());
} finally {
// checking if PrintWriter has been opened
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
} else {
System.out.println("PrintWriter not open");
}
}

}
}

class Main {
public static void main(String[] args) {
ListOfNumbers list = new ListOfNumbers();
list.writeList();
}
}

当您运行此程序时,可能会发生两种可能性:

  • try块中发生异常
  • try块正常执行

创建新的FileWriter时可能会发生异常。如果无法创建或写入指定的文件,则抛出IOException

当发生异常时,将获得以下输出:

1
2
3
Entering try statement
IOException => OutputFile.txt
PrintWriter not open

当未发生异常且try块正常执行时,将获得以下输出:

1
2
Entering try statement
Closing PrintWriter

创建一个OutputFile.txt,它将具有以下内容:

1
2
3
4
5
6
7
8
9
10
Value at: 0 = 0
Value at: 1 = 1
Value at: 2 = 2
Value at: 3 = 3
Value at: 4 = 4
Value at: 5 = 5
Value at: 6 = 6
Value at: 7 = 7
Value at: 8 = 8
Value at: 9 = 9

try … catch详细细节

上述示例的帮助下详细了解异常处理的流程。

上图描述了在创建新FileWriter时发生异常时的程序执行流程。

  • 为了进入发生异常的方法,main方法中调用writeList()方法,然后再调用FileWriter()方法来创建新的OutputFile.txt文件。
  • 发生异常时,运行时系统将跳过try块中的其余代码。
  • 它开始以相反的顺序搜索调用堆栈,以找到合适的异常处理程序。
  • 在这里,FileWriter没有异常处理程序,因此运行时系统检查调用堆栈中的下一个方法,即writeList
  • writeList方法有两个异常处理程序:一个处理IndexOutOfBoundsException,另一个处理IOException
  • 然后,系统按顺序处理这些处理程序。
  • 此示例中的第一个处理程序处理IndexOutOfBoundsException这与try块引发的IOException不匹配。
  • 因此,将检查下一个处理程序,即IOException处理程序这与抛出的异常类型匹配,因此执行catch块中的代码。
  • 执行异常处理程序后,将执行finally块。
  • 在这种情况下,由于FileWriter中发生异常,因此PrintWriter对象out永远不会打开,因此不需要关闭。

现在,让我们假设在运行该程序时没有发生异常,并且try块正常执行。在这种情况下,将创建并写入一个OutputFile.txt

众所周知,无论异常处理如何,都将执行finally块。由于没有发生异常,因此PrintWriter是打开的,则需要关闭。这是通过finally块中的out.close()语句完成的。

捕获多个异常

从Java SE 7和更高版本开始,我们现在可以通过一个catch块捕获不止一种类型的异常。

这样可以减少代码重复并提高代码的简单性和效率。

catch块可以处理的每种异常类型都使用竖线|分隔。

1
2
3
4
5
try {
// code
} catch (ExceptionType1 | Exceptiontype2 ex) {
// catch block
}

try-width-resources 语句

try-with-resources语句是一种try语句,具有一个或多个资源声明。

1
2
3
4
5
try (resource declaration) {
// use of the resource
} catch (ExceptionType e1) {
// catch block
}

资源是在程序结束时要关闭的对象。必须在try语句中声明和初始化它。

1
2
3
try (PrintWriter out = new PrintWriter(new FileWriter(“OutputFile.txt”)) {
// use of the resource
}

try-with-resources 语句也称为自动资源管理。该语句在语句末尾自动关闭所有资源。

throw 和 throws

在Java中,异常可以分为两种类型:

  • Unchecked Exceptions: 它们不是在编译时检查的,而是在运行时检查的。例子:

    ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException,Error类下的异常等

  • Checked Exceptions: 它们在编译时检查.例子: IOException, InterruptedException.

通常,我们不需要处理Unchecked Exceptions的异常。因为Unchecked Exceptions是代码逻辑有问题.最好的习惯是,解决它而不是捕获它.

throws 关键字

我们在方法声明中使用throws关键字来声明其中可能发生的异常的类型。

1
2
3
accessModifier returnType methodName() throws ExceptionType1, ExceptionType2 … {
// code
}

从上面的语法可以看到:使用throws声明多个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
class Main {
public static void findFile() throws IOException {
// code that may produce IOException
File newFile=new File("test.txt");
FileInputStream stream=new FileInputStream(newFile);
}

public static void main(String[] args) {
try{
findFile();
} catch(IOException e){
System.out.println(e);
}
}
}

// 输出
// java.io.FileNotFoundException: test.txt (No such file or directory)

当我们运行该程序时,如果文件test.txt不存在,则FileInputStream抛出一个FileNotFoundException,它扩展了IOException类。

如果方法不处理异常,必须在throws子句中指定其中可能发生的异常的类型,以便调用堆栈中更远的方法可以处理它们。

findFile()方法指定可以抛出IOExceptionmain()方法调用此方法并处理抛出的异常。

引发多个异常

这是我们可以使用throws关键字引发多个异常的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.*;
class Main {
public static void findFile() throws NullPointerException, IOException, InvalidClassException {

// code that may produce NullPointerException
… … …

// code that may produce IOException
… … …

// code that may produce InvalidClassException
… … …
}

public static void main(String[] args) {
try{
findFile();
} catch(IOException e1){
System.out.println(e1.getMessage());
} catch(InvalidClassException e2){
System.out.println(e2.getMessage());
}
}
}

findFile()方法指定它可以在其throws子句中引发NullPointerExceptionIOExceptionInvalidClassException

请注意我们尚未处理NullPointerException.因为它是unchecked exception.不必在throws子句中指定它并进行处理。

throws VS try…catch…finally

可能有几种方法可能导致异常。为每种方法编写try ... catch会很乏味,并且代码会变得很长且可读性较差。

当你在当前方法并不想处理异常, 但你检测了有可能的异常,那么throws是个很有用.

throw 关键字

throw关键字用于显式引发单个异常

引发异常时,程序执行流程从try块转移到catch

1
throw throwableObject;

Throwable对象是Throwable类或Throwable类的子类的实例。

1
2
3
4
5
6
7
8
9
class Main {
public static void divideByZero() {
throw new ArithmeticException("Trying to divide by 0");
}

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

输出:

1
2
3
4
Exception in thread "main" java.lang.ArithmeticException: Trying to divide by 0
at Main.divideByZero(Main.java:3)
at Main.main(Main.java:7)
exit status 1

在此示例中,我们显式抛出ArithmeticException

注意: ArithmeticExceptionunchecked exception,通常没有必要处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.*;
class Main {
public static void findFile() throws IOException {
throw new IOException("File not found");
}

public static void main(String[] args) {
try {
findFile();
System.out.println("Rest of code in try block");
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}

输出:

1
File not found

findFile()方法会引发IOException以及我们传递给其构造函数的消息

请注意由于它是一个checked exception,因此必须在throws子句中指定它

调用此findFile()方法的方法需要处理此异常,或者自己使用throws关键字指定它

我们已经在main()方法中处理了这个异常。引发异常时程序执行流程从try块转移到catch块。因此将跳过try块中的其余代码,并执行catch块中的语句

捕获多个异常

在Java 7之前即使存在代码冗余,我们也必须针对不同类型的异常编写多个异常处理代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Main {
public static void main(String[] args) {
try {
int array[] = new int[10];
array[10] = 30 / 0;
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
}
}
}
// 输出
// / by zero

在此的示例可能会发生两个异常:

  • ArithmeticException因为我们试图将数字除以0.
  • ArrayIndexOutOfBoundsException因为我们已经声明了一个新的整数数组,其数组边界为0到9,并且我们试图为索引10分配一个值

我们正在两个catch块中打印出异常消息,即重复代码

一个catch 捕获多个异常

在Java SE 7和更高版本中,我们现在可以在单个catch块中捕获不止一种类型的异常

catch块可以处理的每种异常类型都使用竖线或竖线|分隔

1
2
3
4
5
try {
// code
} catch (ExceptionType1 | Exceptiontype2 ex) {
// catch block
}
1
2
3
4
5
6
7
8
9
10
11
12
class Main {
public static void main(String[] args) {
try {
int array[] = new int[10];
array[10] = 30 / 0;
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
}
}
}
// 输出
// / by zero

在单·catch块中捕获多个异常可以减少代码重复并提高效率。

编译该程序时生成的字节码将比具有多个捕获块的程序小,因为没有代码冗余。

注意: 如果catch块处理多个异常,则catch参数隐式为final。这意味着我们无法分配任何值来捕获参数

捕获基础异常

当在单个catch块中捕获多个异常时,该规则一般化为专门化。

这意味着,如果catch块中存在异常的层次结构,我们只能捕获基本异常,而不是捕获多个专门的异常。

1
2
3
4
5
6
7
8
9
10
11
12
class Main {
public static void main(String[] args) {
try {
int array[] = new int[10];
array[10] = 30 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
// 输出
// / by zero

我们知道所有异常类都是Exception类的子类。因此我们不必捕获多个专门的异常,而只需捕获Exception类。

如果在catch块中已经指定了基本异常类,则不要在同一catch块中使用子异常类。否则我们会得到一个编译错误。

1
2
3
4
5
6
7
8
9
10
class Main {
public static void main(String[] args) {
try {
int array[] = new int[10];
array[10] = 30 / 0;
} catch (Exception | ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
}
}
}

输出:

1
Main.java:6: error: Alternatives in a multi-catch statement cannot be related by subclassing

在此示例中,ArithmeticException和`ArrayIndexOutOfBoundsException都是Exception类的子类,所以编译错误.

try-with-resources

try-with-resources语句末尾自动关闭所有资源.资源是程序结束时要关闭的对象。

1
2
3
4
5
try (resource declaration) {
// use of the resource
} catch (ExceptionType e1) {
// catch block
}

从上面的语法可以看出,我们通过以下方式声明try-with-resources语句:

  • try子句中声明和实例化资源。
  • 指定并处理关闭资源时可能引发的所有异常。

注意: try-with-resources语句关闭实现AutoCloseable接口的所有资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;

class Main {
public static void main(String[] args) {
String line;
try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
while ((line = br.readLine()) != null) {
System.out.println("Line =>"+line);
}
} catch (IOException e) {
System.out.println("IOException in try block =>" + e.getMessage());
}
}
}

如果test.txt文件没找到:

1
IOException in try-with-resources block =>test.txt (No such file or directory)

如果test.text文件找到

1
2
Entering try-with-resources block
Line =>test line

在此示例中,使用BufferedReader的实例从test.txt文件读取数据

try-with-resources语句中声明并实例化BufferedReader可以确保关闭其实例,而不管try语句是正常完成还是引发异常。

如果发生异常,则可以使用异常处理块或throws关键字对其进行处理。

被抑制异常

在上面的示例中,在以下情况下,可以从try-with-resources语句引发异常:

  • 找不到文件test.txt
  • 关闭BufferedReader对象

也可以从try块中引发异常,因为文件读取可能随时因多种原因而失败。

如果try块和try-with-resources语句都抛出了异常,将抛出try块中的异常,并抑制try-with-resources语句中的异常。

检查被抑制的异常

在Java 7和更高版本中,可以通过从try块引发的异常中调用Throwable.getSuppressed()方法来检索受抑制的异常。

此方法返回所有抑制的异常的数组。我们在catch块中得到了抑制的异常。

1
2
3
4
5
6
7
catch(IOException e) {
System.out.println("Thrown exception=>" + e.getMessage());
Throwable[] suppressedExceptions = e.getSuppressed();
for (int i=0; i<suppressedExceptions.length; i++) {
System.out.println("Suppressed exception=>" + suppressedExceptions[i]);
}
}

try-with-resources的优势

finally块不需要关闭资源

在Java 7引入此功能之前,我们必须使用finally块来确保关闭资源以避免资源泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;

class Main {
public static void main(String[] args) {
BufferedReader br = null;
String line;

try {
System.out.println("Entering try block");
br = new BufferedReader(new FileReader("test.txt"));
while ((line = br.readLine()) != null) {
System.out.println("Line =>"+line);
}
} catch (IOException e) {
System.out.println("IOException in try block =>" + e.getMessage());
} finally {
System.out.println("Entering finally block");
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
System.out.println("IOException in finally block =>"+e.getMessage());
}

}
}
}

输出:

1
2
3
Entering try block
Line =>line from test.txt file
Entering finally block

从上面的示例可以看出,使用finally块来清理资源使代码更加复杂。

还要注意finally块中的try ... catch块吗?这是因为在关闭此finally块内的BufferedReader实例时也可能发生IOException,因此也将其捕获并处理。

try-with-resources语句执行自动资源管理,我们不需要显式关闭资源,因为JVM会自动关闭它们。这使代码更具可读性,更易于编写。

try-with-resources获取多资源

我们可以在try-with-resources语句中声明多个资源,方法是用;将它们分开.

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.*;
import java.util.*;
class Main {
public static void main(String[] args) throws IOException{
try (Scanner scanner = new Scanner(new File("testRead.txt"));
PrintWriter writer = new PrintWriter(new File("testWrite.txt"))) {
while (scanner.hasNext()) {
writer.print(scanner.nextLine());
}
}
}
}

如果该程序执行时未生成任何异常,Scanner对象从testRead.txt文件中读取一行并将其写入新的testWrite.txt文件中。

进行多个声明时,try-with-resources语句以相反的顺序关闭这些资源。在此示例中,首先关闭PrintWriter对象,然后关闭Scanner对象。

Java 9 增强 try-with-resources

在Java 7中,try-with-resources语句受到限制,该资源需要在其块内本地声明。

1
2
3
try (Scanner scanner = new Scanner(new File("testRead.txt"))) {
// code
}

如果我们在Java 7中在块外声明资源,它会生成一条错误消息。

1
2
3
4
Scanner scanner = new Scanner(new File("testRead.txt"));
try (scanner) {
// code
}

处理此错误,Java 9改进了try-with-resources语句,因此即使未在本地声明资源的引用,也可以使用。上面的代码现在将执行,没有任何编译错误。