# 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`

```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]);
    }
}

```

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

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

```

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

```java
$ 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`

```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`

```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`

```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`关键字后面的异常匹配。

```java
try {

    Scanner sc = new Scanner(System.in);
    int x = sc.nextInt();

    System.out.println(x);
}

```

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

```java
} 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`

```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 数据库并找出数据库系统的版本。 连接数据库很容易出错。

```java
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`块中。

```java
} catch (SQLException ex) {

    Logger lgr = Logger.getLogger(Version.class.getName());
    lgr.log(Level.SEVERE, ex.getMessage(), ex);
}

```

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

```java
} 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`

```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`

```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`

```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);
        }
    }
}

```

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

```java
if (age <= 0 || age > 130) {

    throw new IllegalArgumentException("Incorrect age");
}

```

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

```java
} catch (IllegalArgumentException | InputMismatchException e) {

        Logger.getLogger(ThrowingExceptions.class.getName()).log(Level.SEVERE,
                e.getMessage(), e);
}

```

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

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

`thermopylae.txt`

```java
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`

```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 处理它。

```java
public static void readFileContents(String fname) throws IOException {

```

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

```java
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`

```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`语句。

```java
try (BufferedReader br = Files.newBufferedReader(myPath,
        StandardCharsets.UTF_8)) {
...

```

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

## Java 自定义异常

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

`com/zetcode/JavaCustomException.java`

```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());
        }
    }
}

```

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

```java
class BigValueException extends Exception {

  public BigValueException(String message) {

        super(message);
    }
}

```

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

```java
final int LIMIT = 333;

```

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

```java
if (x > LIMIT) {

    throw new BigValueException("Exceeded the maximum value");
}

```

如果该值大于限制，则抛出自定义异常。 我们给异常消息“超出最大值”。

```java
} catch (BigValueException e) {

    System.out.println(e.getMessage());
}

```

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

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