{% raw %}
Java 中的异常
原文:http://zetcode.com/lang/java/exceptions/
在 Java 教程的这一章中,我们将处理异常。 Java 使用异常来处理错误。
在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存数据。 当我们的应用尝试连接到站点时,互联网连接可能会断开。 用户将无效数据填充到表单。 这些错误可能会使应用崩溃,使其无法响应,并且在某些情况下甚至会损害系统的安全性。 程序员有责任处理可以预期的错误。
在 Java 中,我们可以识别三种异常:受检的异常,非受检的异常和错误。
Java 受检的异常
受检的异常是可以预期并从中恢复的错误情况(无效的用户输入,数据库问题,网络中断,文件缺失)。 除RuntimeException
及其子类之外的Exception
的所有子类都是受检的异常。 IOException
,SQLException
或PrinterException
是受检的异常的示例。 Java 编译器强制将受检的异常捕获或在方法签名中声明(使用throws
关键字)。
Java 非受检的异常
非受检的异常是无法预期和无法恢复的错误条件。 它们通常是编程错误,无法在运行时处理。 非受检的异常是java.lang.RuntimeException
的子类。 ArithmeticException
,NullPointerException
或BufferOverflowException
属于这组异常。 Java 编译器不强制执行非受检的异常。
Java 错误
错误是程序员无法解决的严重问题。 例如,应用无法处理硬件或系统故障。 错误是java.lang.Error
类的实例。 错误的示例包括InternalError
,OutOfMemoryError
,StackOverflowError
或AssertionError
。
错误和运行时异常通常称为非受检的异常。
Java try
,catch
和finally
try
,catch
和finally
关键字用于处理异常。 throws
关键字在方法声明中用于指定哪些异常不在方法内处理,而是传递给程序的下一个更高级别。
throw
关键字导致抛出已声明的异常实例。 引发异常后,运行时系统将尝试查找适当的异常处理器。 调用栈是为处理器搜索的方法的层次结构。
Java 非受检的异常示例
Java 受检的异常包括ArrayIndexOutOfBoundsException
,UnsupportedOperationException
,NullPointerException
和InputMismatchException
。
ArrayIndexOutOfBoundsException
抛出ArrayIndexOutOfBoundsException
表示已使用非法索引访问了数组。 索引为负或大于或等于数组的大小。
com/zetcode/ArrayIndexOutOfBoundsEx.java
package com.zetcode;
public class ArrayIndexOutOfBoundsEx {
public static void main(String[] args) {
int[] n = { 5, 2, 4, 5, 6, 7, 2 };
System.out.format("The last element in the array is %d%n", n[n.length]);
}
}
上面的程序中有一个错误。 我们尝试访问一个不存在的元素。 这是编程错误。 没有理由要处理此错误:必须固定代码。
System.out.format("The last element in the array is %d%n", n[n.length]);
数组索引从零开始。 因此,最后一个索引是n.length - 1
。
$ java ArrayIndexOutOfBoundsEx.java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 7
at com.zetcode.ArrayIndexOutOfBoundsEx.main(ArrayIndexOutOfBoundsEx.java:9)
运行系统将抛出java.lang.ArrayIndexOutOfBoundsException
。 这是非受检的异常的示例。
UnsupportedOperationException
抛出UnsupportedOperationException
,表明不支持所请求的操作。
com/zetcode/UnsupportedOperationEx.java
package com.zetcode;
import java.util.List;
public class UnsupportedOperationEx {
public static void main(String[] args) {
var words = List.of("sky", "blue", "forest", "lake", "river");
words.add("ocean");
System.out.println(words);
}
}
用List.of()
工厂方法创建一个不可变列表。 不可变列表不支持add()
方法; 因此,我们在运行示例时抛出了UnsupportedOperationException
。
NullPointerException
当应用尝试使用具有null
值的对象引用时,将引发NullPointerException
。 例如,我们在null
引用所引用的对象上调用实例方法。
com/zetcode/NullPointerEx.java
package com.zetcode;
import java.util.ArrayList;
import java.util.List;
public class NullPointerEx {
public static void main(String[] args) {
List<String> words = new ArrayList<>() {{
add("sky");
add("blue");
add("cloud");
add(null);
add("ocean");
}};
words.forEach(word -> {
System.out.printf("The %s word has %d letters%n", word, word.length());
});
}
}
该示例遍历字符串列表,并确定每个字符串的长度。 在null
值上调用length()
会导致NullPointerException
。 为了解决这个问题,我们可以在调用length()
之前从检查列表中删除null
值的所有null
值。
InputMismatchException
Scanner
类抛出InputMismatchException
,以指示检索到的令牌与预期类型的模式不匹配。 此异常是非受检的异常的示例。 编译器不强制我们处理此异常。
com/zetcode/InputMismatchEx.java
package com.zetcode;
import java.util.InputMismatchException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
public class InputMismatchEx {
public static void main(String[] args) {
System.out.print("Enter an integer: ");
try {
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
System.out.println(x);
} catch (InputMismatchException e) {
Logger.getLogger(InputMismatchEx.class.getName()).log(Level.SEVERE,
e.getMessage(), e);
}
}
容易出错的代码位于try
块中。 如果引发异常,则代码跳至catch
块。 引发的异常类必须与catch
关键字后面的异常匹配。
try {
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
System.out.println(x);
}
try
关键字定义了可能引发异常的语句块。
} catch (InputMismatchException e) {
Logger.getLogger(InputMismatchEx.class.getName()).log(Level.SEVERE,
e.getMessage(), e);
}
异常在catch
块中处理。 我们使用Logger
类记录错误。
受检的异常
Java 受检的异常包括SQLException
,IOException
或ParseException
。
SQLException
使用数据库时发生SQLException
。
com/zetcode/MySqlVersionEx.java
package com.zetcode;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
public class MySqlVersionEx {
public static void main(String[] args) {
Connection con = null;
Statement st = null;
ResultSet rs = null;
String url = "jdbc:mysql://localhost:3306/testdb?useSsl=false";
String user = "testuser";
String password = "test623";
try {
con = DriverManager.getConnection(url, user, password);
st = con.createStatement();
rs = st.executeQuery("SELECT VERSION()");
if (rs.next()) {
System.out.println(rs.getString(1));
}
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
if (st != null) {
try {
st.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
}
}
}
该示例连接到 MySQL 数据库并找出数据库系统的版本。 连接数据库很容易出错。
try {
con = DriverManager.getConnection(url, user, password);
st = con.createStatement();
rs = st.executeQuery("SELECT VERSION()");
if (rs.next()) {
System.out.println(rs.getString(1));
}
}
可能导致错误的代码位于try
块中。
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(Version.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
发生异常时,我们跳至catch
块。 我们通过记录发生的情况来处理异常。
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
if (st != null) {
try {
st.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
lgr.log(Level.SEVERE, ex.getMessage(), ex);
}
}
}
无论是否接收到异常,都将执行finally
块。 我们正在尝试关闭资源。 即使在此过程中,也可能会有异常。 因此,我们还有其他try/catch
块。
IOException
输入/输出操作失败时,抛出IOException
。 这可能是由于权限不足或文件名错误造成的。
com/zetcode/IOExceptionEx.java
package com.zetcode;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class IOExceptionEx {
private static FileReader fr;
public static void main(String[] args) {
try {
char[] buf = new char[1024];
fr = new FileReader("src/resources/data.txt", StandardCharsets.UTF_8);
while (fr.read(buf) != -1) {
System.out.println(buf);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
当我们从文件中读取数据时,我们需要处理IOException
。 当我们尝试使用read()
读取数据并使用close()
关闭读取器时,可能会引发异常。
ParseException
解析操作失败时,将引发ParseException
。
com/zetcode/ParseExceptionEx.java
package com.zetcode;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
public class ParseExceptionEx {
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getInstance(new Locale("sk", "SK"));
nf.setMaximumFractionDigits(3);
try {
Number num = nf.parse("150000,456");
System.out.println(num.doubleValue());
} catch (ParseException e) {
e.printStackTrace();
}
}
}
在示例中,我们将本地化的数字值解析为 Java Number
。 我们使用try/catch
语句处理ParseException
。
Java 抛出异常
Throwable
类是 Java 语言中所有错误和异常的超类。 Java 虚拟机仅抛出属于此类(或其子类之一)的实例的对象,或者 Java throw
语句可以抛出该对象。 同样,在catch
子句中,只有此类或其子类之一可以作为参数类型。
程序员可以使用throw
关键字引发异常。 异常通常在引发异常的地方进行处理。 方法可以通过在方法定义的末尾使用throws
关键字来摆脱处理异常的责任。 关键字后是该方法引发的所有异常的逗号分隔列表。 被抛出的异常在调用栈中传播,并寻找最接近的匹配项。
com/zetcode/ThrowingExceptions.java
package com.zetcode;
import java.util.InputMismatchException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ThrowingExceptions {
public static void main(String[] args) {
System.out.println("Enter your age: ");
try {
Scanner sc = new Scanner(System.in);
short age = sc.nextShort();
if (age <= 0 || age > 130) {
throw new IllegalArgumentException("Incorrect age");
}
System.out.format("Your age is: %d %n", age);
} catch (IllegalArgumentException | InputMismatchException e) {
Logger.getLogger(ThrowingExceptions.class.getName()).log(Level.SEVERE,
e.getMessage(), e);
}
}
}
在示例中,我们要求用户输入他的年龄。 我们读取该值,如果该值超出预期的人类年龄范围,则会引发异常。
if (age <= 0 || age > 130) {
throw new IllegalArgumentException("Incorrect age");
}
年龄不能为负值,也没有年龄超过 130 岁的记录。 如果该值超出此范围,则抛出内置IllegalArgumentException
。 抛出此异常表示方法已传递了非法或不适当的参数。
} catch (IllegalArgumentException | InputMismatchException e) {
Logger.getLogger(ThrowingExceptions.class.getName()).log(Level.SEVERE,
e.getMessage(), e);
}
从 Java 7 开始,可以在一个catch
子句中捕获多个异常。 但是,这些异常不能是彼此的子类。 例如,IOException
和FileNotFoundException
不能在一个catch
语句中使用。
下面的示例将说明如何将处理异常的责任传递给其他方法。
thermopylae.txt
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.
我们使用此文本文件。
com/zetcode/ThrowingExceptions2.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ThrowingExceptions2 {
public static void readFileContents(String fname) throws IOException {
BufferedReader br = null;
Path myPath = Paths.get(fname);
try {
br = Files.newBufferedReader(myPath, StandardCharsets.UTF_8);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} finally {
if (br != null) {
br.close();
}
}
}
public static void main(String[] args) throws IOException {
String fileName = "src/main/resources/thermopylae.txt";
readFileContents(fileName);
}
}
本示例读取文本文件的内容。 readFileContents()
和main()
方法都不处理潜在的IOException
; 我们让 JVM 处理它。
public static void readFileContents(String fname) throws IOException {
当我们从文件读取时,会抛出IOException
。 readFileContents()
方法引发异常。 处理这些异常的任务委托给调用者。
public static void main(String[] args) throws IOException {
String fileName = "src/main/resources/thermopylae.txt";
readFileContents(fileName);
}
main()
方法也会抛出IOException
。 如果有这样的异常,它将由 JVM 处理。
Java try-with-resources
语句
try-with-resources
语句是一种特殊的try
语句。 它是 Java 7 中引入的。在括号中,我们放置了一个或多个资源。 这些资源将在语句末尾自动关闭。 我们不必手动关闭资源。
com/zetcode/TryWithResources.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import java.util.logging.Logger;
public class TryWithResources {
public static void main(String[] args) {
String fileName = "src/main/resources/thermopylae.txt";
Path myPath = Paths.get(fileName);
try (BufferedReader br = Files.newBufferedReader(myPath,
StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException ex) {
Logger.getLogger(TryWithResources.class.getName()).log(Level.SEVERE,
ex.getMessage(), ex);
}
}
}
在示例中,我们读取文件的内容并使用try-with-resources
语句。
try (BufferedReader br = Files.newBufferedReader(myPath,
StandardCharsets.UTF_8)) {
...
打开的文件是必须关闭的资源。 资源放置在try
语句的方括号之间。 无论try
语句是正常完成还是突然完成,输入流都将关闭。
Java 自定义异常
自定义异常是用户定义的异常类,它们扩展了Exception
类或RuntimeException
类。 使用throw
关键字可以消除自定义异常。
com/zetcode/JavaCustomException.java
package com.zetcode;
class BigValueException extends Exception {
public BigValueException(String message) {
super(message);
}
}
public class JavaCustomException {
public static void main(String[] args) {
int x = 340004;
final int LIMIT = 333;
try {
if (x > LIMIT) {
throw new BigValueException("Exceeded the maximum value");
}
} catch (BigValueException e) {
System.out.println(e.getMessage());
}
}
}
我们假定存在无法处理大量数字的情况。
class BigValueException extends Exception {
public BigValueException(String message) {
super(message);
}
}
我们有一个BigValueException
类。 该类派生自内置的Exception
类。 它使用super
关键字将错误消息传递给父类。
final int LIMIT = 333;
大于此常数的数字在我们的程序中被视为big
。
if (x > LIMIT) {
throw new BigValueException("Exceeded the maximum value");
}
如果该值大于限制,则抛出自定义异常。 我们给异常消息"Exceeded the maximum value"
。
} catch (BigValueException e) {
System.out.println(e.getMessage());
}
我们捕获到异常并将其消息打印到控制台。
在 Java 教程的这一部分中,我们讨论了 Java 中的异常。
{% endraw %}