{% raw %}

Java 中的异常

原文:http://zetcode.com/lang/java/exceptions/

在 Java 教程的这一章中,我们将处理异常。 Java 使用异常来处理错误。

在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存数据。 当我们的应用尝试连接到站点时,互联网连接可能会断开。 用户将无效数据填充到表单。 这些错误可能会使应用崩溃,使其无法响应,并且在某些情况下甚至会损害系统的安全性。 程序员有责任处理可以预期的错误。

在 Java 中,我们可以识别三种异常:受检的异常,非受检的异常和错误。

Java 受检的异常

受检的异常是可以预期并从中恢复的错误情况(无效的用户输入,数据库问题,网络中断,文件缺失)。 除RuntimeException及其子类之外的Exception的所有子类都是受检的异常。 IOExceptionSQLExceptionPrinterException是受检的异常的示例。 Java 编译器强制将受检的异常捕获或在方法签名中声明(使用throws关键字)。

Java 非受检的异常

非受检的异常是无法预期和无法恢复的错误条件。 它们通常是编程错误,无法在运行时处理。 非受检的异常是java.lang.RuntimeException的子类。 ArithmeticExceptionNullPointerExceptionBufferOverflowException属于这组异常。 Java 编译器不强制执行非受检的异常。

Java 错误

错误是程序员无法解决的严重问题。 例如,应用无法处理硬件或系统故障。 错误是java.lang.Error类的实例。 错误的示例包括InternalErrorOutOfMemoryErrorStackOverflowErrorAssertionError

错误和运行时异常通常称为非受检的异常。

Java trycatchfinally

trycatchfinally关键字用于处理异常。 throws关键字在方法声明中用于指定哪些异常不在方法内处理,而是传递给程序的下一个更高级别。

throw关键字导致抛出已声明的异常实例。 引发异常后,运行时系统将尝试查找适当的异常处理器。 调用栈是为处理器搜索的方法的层次结构。

Java 非受检的异常示例

Java 受检的异常包括ArrayIndexOutOfBoundsExceptionUnsupportedOperationExceptionNullPointerExceptionInputMismatchException

ArrayIndexOutOfBoundsException

抛出ArrayIndexOutOfBoundsException表示已使用非法索引访问了数组。 索引为负或大于或等于数组的大小。

com/zetcode/ArrayIndexOutOfBoundsEx.java

  1. package com.zetcode;
  2. public class ArrayIndexOutOfBoundsEx {
  3. public static void main(String[] args) {
  4. int[] n = { 5, 2, 4, 5, 6, 7, 2 };
  5. System.out.format("The last element in the array is %d%n", n[n.length]);
  6. }
  7. }

上面的程序中有一个错误。 我们尝试访问一个不存在的元素。 这是编程错误。 没有理由要处理此错误:必须固定代码。

  1. System.out.format("The last element in the array is %d%n", n[n.length]);

数组索引从零开始。 因此,最后一个索引是n.length - 1

  1. $ java ArrayIndexOutOfBoundsEx.java
  2. Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 7
  3. at com.zetcode.ArrayIndexOutOfBoundsEx.main(ArrayIndexOutOfBoundsEx.java:9)

运行系统将抛出java.lang.ArrayIndexOutOfBoundsException。 这是非受检的异常的示例。

UnsupportedOperationException

抛出UnsupportedOperationException,表明不支持所请求的操作。

com/zetcode/UnsupportedOperationEx.java

  1. package com.zetcode;
  2. import java.util.List;
  3. public class UnsupportedOperationEx {
  4. public static void main(String[] args) {
  5. var words = List.of("sky", "blue", "forest", "lake", "river");
  6. words.add("ocean");
  7. System.out.println(words);
  8. }
  9. }

List.of()工厂方法创建一个不可变列表。 不可变列表不支持add()方法; 因此,我们在运行示例时抛出了UnsupportedOperationException

NullPointerException

当应用尝试使用具有null值的对象引用时,将引发NullPointerException。 例如,我们在null引用所引用的对象上调用实例方法。

com/zetcode/NullPointerEx.java

  1. package com.zetcode;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class NullPointerEx {
  5. public static void main(String[] args) {
  6. List<String> words = new ArrayList<>() {{
  7. add("sky");
  8. add("blue");
  9. add("cloud");
  10. add(null);
  11. add("ocean");
  12. }};
  13. words.forEach(word -> {
  14. System.out.printf("The %s word has %d letters%n", word, word.length());
  15. });
  16. }
  17. }

该示例遍历字符串列表,并确定每个字符串的长度。 在null值上调用length()会导致NullPointerException。 为了解决这个问题,我们可以在调用length()之前从检查列表中删除null值的所有null值。

InputMismatchException

Scanner类抛出InputMismatchException,以指示检索到的令牌与预期类型的模式不匹配。 此异常是非受检的异常的示例。 编译器不强制我们处理此异常。

com/zetcode/InputMismatchEx.java

  1. package com.zetcode;
  2. import java.util.InputMismatchException;
  3. import java.util.Scanner;
  4. import java.util.logging.Level;
  5. import java.util.logging.Logger;
  6. public class InputMismatchEx {
  7. public static void main(String[] args) {
  8. System.out.print("Enter an integer: ");
  9. try {
  10. Scanner sc = new Scanner(System.in);
  11. int x = sc.nextInt();
  12. System.out.println(x);
  13. } catch (InputMismatchException e) {
  14. Logger.getLogger(InputMismatchEx.class.getName()).log(Level.SEVERE,
  15. e.getMessage(), e);
  16. }
  17. }

容易出错的代码位于try块中。 如果引发异常,则代码跳至catch块。 引发的异常类必须与catch关键字后面的异常匹配。

  1. try {
  2. Scanner sc = new Scanner(System.in);
  3. int x = sc.nextInt();
  4. System.out.println(x);
  5. }

try关键字定义了可能引发异常的语句块。

  1. } catch (InputMismatchException e) {
  2. Logger.getLogger(InputMismatchEx.class.getName()).log(Level.SEVERE,
  3. e.getMessage(), e);
  4. }

异常在catch块中处理。 我们使用Logger类记录错误。

受检的异常

Java 受检的异常包括SQLExceptionIOExceptionParseException

SQLException

使用数据库时发生SQLException

com/zetcode/MySqlVersionEx.java

  1. package com.zetcode;
  2. import java.sql.Connection;
  3. import java.sql.DriverManager;
  4. import java.sql.ResultSet;
  5. import java.sql.SQLException;
  6. import java.sql.Statement;
  7. import java.util.logging.Level;
  8. import java.util.logging.Logger;
  9. public class MySqlVersionEx {
  10. public static void main(String[] args) {
  11. Connection con = null;
  12. Statement st = null;
  13. ResultSet rs = null;
  14. String url = "jdbc:mysql://localhost:3306/testdb?useSsl=false";
  15. String user = "testuser";
  16. String password = "test623";
  17. try {
  18. con = DriverManager.getConnection(url, user, password);
  19. st = con.createStatement();
  20. rs = st.executeQuery("SELECT VERSION()");
  21. if (rs.next()) {
  22. System.out.println(rs.getString(1));
  23. }
  24. } catch (SQLException ex) {
  25. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  26. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  27. } finally {
  28. if (rs != null) {
  29. try {
  30. rs.close();
  31. } catch (SQLException ex) {
  32. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  33. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  34. }
  35. }
  36. if (st != null) {
  37. try {
  38. st.close();
  39. } catch (SQLException ex) {
  40. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  41. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  42. }
  43. }
  44. if (con != null) {
  45. try {
  46. con.close();
  47. } catch (SQLException ex) {
  48. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  49. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  50. }
  51. }
  52. }
  53. }
  54. }

该示例连接到 MySQL 数据库并找出数据库系统的版本。 连接数据库很容易出错。

  1. try {
  2. con = DriverManager.getConnection(url, user, password);
  3. st = con.createStatement();
  4. rs = st.executeQuery("SELECT VERSION()");
  5. if (rs.next()) {
  6. System.out.println(rs.getString(1));
  7. }
  8. }

可能导致错误的代码位于try块中。

  1. } catch (SQLException ex) {
  2. Logger lgr = Logger.getLogger(Version.class.getName());
  3. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  4. }

发生异常时,我们跳至catch块。 我们通过记录发生的情况来处理异常。

  1. } finally {
  2. if (rs != null) {
  3. try {
  4. rs.close();
  5. } catch (SQLException ex) {
  6. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  7. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  8. }
  9. }
  10. if (st != null) {
  11. try {
  12. st.close();
  13. } catch (SQLException ex) {
  14. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  15. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  16. }
  17. }
  18. if (con != null) {
  19. try {
  20. con.close();
  21. } catch (SQLException ex) {
  22. Logger lgr = Logger.getLogger(MySqlVersionEx.class.getName());
  23. lgr.log(Level.SEVERE, ex.getMessage(), ex);
  24. }
  25. }
  26. }

无论是否接收到异常,都将执行finally块。 我们正在尝试关闭资源。 即使在此过程中,也可能会有异常。 因此,我们还有其他try/catch块。

IOException

输入/输出操作失败时,抛出IOException。 这可能是由于权限不足或文件名错误造成的。

com/zetcode/IOExceptionEx.java

  1. package com.zetcode;
  2. import java.io.FileReader;
  3. import java.io.IOException;
  4. import java.nio.charset.StandardCharsets;
  5. public class IOExceptionEx {
  6. private static FileReader fr;
  7. public static void main(String[] args) {
  8. try {
  9. char[] buf = new char[1024];
  10. fr = new FileReader("src/resources/data.txt", StandardCharsets.UTF_8);
  11. while (fr.read(buf) != -1) {
  12. System.out.println(buf);
  13. }
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. } finally {
  17. if (fr != null) {
  18. try {
  19. fr.close();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
  25. }
  26. }

当我们从文件中读取数据时,我们需要处理IOException。 当我们尝试使用read()读取数据并使用close()关闭读取器时,可能会引发异常。

ParseException

解析操作失败时,将引发ParseException

com/zetcode/ParseExceptionEx.java

  1. package com.zetcode;
  2. import java.text.NumberFormat;
  3. import java.text.ParseException;
  4. import java.util.Locale;
  5. public class ParseExceptionEx {
  6. public static void main(String[] args) {
  7. NumberFormat nf = NumberFormat.getInstance(new Locale("sk", "SK"));
  8. nf.setMaximumFractionDigits(3);
  9. try {
  10. Number num = nf.parse("150000,456");
  11. System.out.println(num.doubleValue());
  12. } catch (ParseException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }

在示例中,我们将本地化的数字值解析为 Java Number。 我们使用try/catch语句处理ParseException

Java 抛出异常

Throwable类是 Java 语言中所有错误和异常的超类。 Java 虚拟机仅抛出属于此类(或其子类之一)的实例的对象,或者 Java throw语句可以抛出该对象。 同样,在catch子句中,只有此类或其子类之一可以作为参数类型。

程序员可以使用throw关键字引发异常。 异常通常在引发异常的地方进行处理。 方法可以通过在方法定义的末尾使用throws关键字来摆脱处理异常的责任。 关键字后是该方法引发的所有异常的逗号分隔列表。 被抛出的异常在调用栈中传播,并寻找最接近的匹配项。

com/zetcode/ThrowingExceptions.java

  1. package com.zetcode;
  2. import java.util.InputMismatchException;
  3. import java.util.Scanner;
  4. import java.util.logging.Level;
  5. import java.util.logging.Logger;
  6. public class ThrowingExceptions {
  7. public static void main(String[] args) {
  8. System.out.println("Enter your age: ");
  9. try {
  10. Scanner sc = new Scanner(System.in);
  11. short age = sc.nextShort();
  12. if (age <= 0 || age > 130) {
  13. throw new IllegalArgumentException("Incorrect age");
  14. }
  15. System.out.format("Your age is: %d %n", age);
  16. } catch (IllegalArgumentException | InputMismatchException e) {
  17. Logger.getLogger(ThrowingExceptions.class.getName()).log(Level.SEVERE,
  18. e.getMessage(), e);
  19. }
  20. }
  21. }

在示例中,我们要求用户输入他的年龄。 我们读取该值,如果该值超出预期的人类年龄范围,则会引发异常。

  1. if (age <= 0 || age > 130) {
  2. throw new IllegalArgumentException("Incorrect age");
  3. }

年龄不能为负值,也没有年龄超过 130 岁的记录。 如果该值超出此范围,则抛出内置IllegalArgumentException。 抛出此异常表示方法已传递了非法或不适当的参数。

  1. } catch (IllegalArgumentException | InputMismatchException e) {
  2. Logger.getLogger(ThrowingExceptions.class.getName()).log(Level.SEVERE,
  3. e.getMessage(), e);
  4. }

从 Java 7 开始,可以在一个catch子句中捕获多个异常。 但是,这些异常不能是彼此的子类。 例如,IOExceptionFileNotFoundException不能在一个catch语句中使用。

下面的示例将说明如何将处理异常的责任传递给其他方法。

thermopylae.txt

  1. The Battle of Thermopylae was fought between an alliance of Greek city-states,
  2. led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
  3. course of three days, during the second Persian invasion of Greece.

我们使用此文本文件。

com/zetcode/ThrowingExceptions2.java

  1. package com.zetcode;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.nio.charset.StandardCharsets;
  5. import java.nio.file.Files;
  6. import java.nio.file.Path;
  7. import java.nio.file.Paths;
  8. public class ThrowingExceptions2 {
  9. public static void readFileContents(String fname) throws IOException {
  10. BufferedReader br = null;
  11. Path myPath = Paths.get(fname);
  12. try {
  13. br = Files.newBufferedReader(myPath, StandardCharsets.UTF_8);
  14. String line;
  15. while ((line = br.readLine()) != null) {
  16. System.out.println(line);
  17. }
  18. } finally {
  19. if (br != null) {
  20. br.close();
  21. }
  22. }
  23. }
  24. public static void main(String[] args) throws IOException {
  25. String fileName = "src/main/resources/thermopylae.txt";
  26. readFileContents(fileName);
  27. }
  28. }

本示例读取文本文件的内容。 readFileContents()main()方法都不处理潜在的IOException; 我们让 JVM 处理它。

  1. public static void readFileContents(String fname) throws IOException {

当我们从文件读取时,会抛出IOExceptionreadFileContents()方法引发异常。 处理这些异常的任务委托给调用者。

  1. public static void main(String[] args) throws IOException {
  2. String fileName = "src/main/resources/thermopylae.txt";
  3. readFileContents(fileName);
  4. }

main()方法也会抛出IOException。 如果有这样的异常,它将由 JVM 处理。

Java try-with-resources语句

try-with-resources语句是一种特殊的try语句。 它是 Java 7 中引入的。在括号中,我们放置了一个或多个资源。 这些资源将在语句末尾自动关闭。 我们不必手动关闭资源。

com/zetcode/TryWithResources.java

  1. package com.zetcode;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.nio.charset.StandardCharsets;
  5. import java.nio.file.Files;
  6. import java.nio.file.Path;
  7. import java.nio.file.Paths;
  8. import java.util.logging.Level;
  9. import java.util.logging.Logger;
  10. public class TryWithResources {
  11. public static void main(String[] args) {
  12. String fileName = "src/main/resources/thermopylae.txt";
  13. Path myPath = Paths.get(fileName);
  14. try (BufferedReader br = Files.newBufferedReader(myPath,
  15. StandardCharsets.UTF_8)) {
  16. String line;
  17. while ((line = br.readLine()) != null) {
  18. System.out.println(line);
  19. }
  20. } catch (IOException ex) {
  21. Logger.getLogger(TryWithResources.class.getName()).log(Level.SEVERE,
  22. ex.getMessage(), ex);
  23. }
  24. }
  25. }

在示例中,我们读取文件的内容并使用try-with-resources语句。

  1. try (BufferedReader br = Files.newBufferedReader(myPath,
  2. StandardCharsets.UTF_8)) {
  3. ...

打开的文件是必须关闭的资源。 资源放置在try语句的方括号之间。 无论try语句是正常完成还是突然完成,输入流都将关闭。

Java 自定义异常

自定义异常是用户定义的异常类,它们扩展了Exception类或RuntimeException类。 使用throw关键字可以消除自定义异常。

com/zetcode/JavaCustomException.java

  1. package com.zetcode;
  2. class BigValueException extends Exception {
  3. public BigValueException(String message) {
  4. super(message);
  5. }
  6. }
  7. public class JavaCustomException {
  8. public static void main(String[] args) {
  9. int x = 340004;
  10. final int LIMIT = 333;
  11. try {
  12. if (x > LIMIT) {
  13. throw new BigValueException("Exceeded the maximum value");
  14. }
  15. } catch (BigValueException e) {
  16. System.out.println(e.getMessage());
  17. }
  18. }
  19. }

我们假定存在无法处理大量数字的情况。

  1. class BigValueException extends Exception {
  2. public BigValueException(String message) {
  3. super(message);
  4. }
  5. }

我们有一个BigValueException类。 该类派生自内置的Exception类。 它使用super关键字将错误消息传递给父类。

  1. final int LIMIT = 333;

大于此常数的数字在我们的程序中被视为big

  1. if (x > LIMIT) {
  2. throw new BigValueException("Exceeded the maximum value");
  3. }

如果该值大于限制,则抛出自定义异常。 我们给异常消息"Exceeded the maximum value"

  1. } catch (BigValueException e) {
  2. System.out.println(e.getMessage());
  3. }

我们捕获到异常并将其消息打印到控制台。

在 Java 教程的这一部分中,我们讨论了 Java 中的异常。

{% endraw %}