Java 异常处理

是的,别人的代码可能有问题,但是我的代码不会。程序员往往过于自信。

什么是异常

Java Exception 是 Java 编程语言中的一个重要概念,可以帮助开发人员捕捉和处理不可预料的错误或异常,应该熟练掌握。在编写 Java 程序时,应该注意捕捉和处理可能出现的异常,以确保程序的稳定性和可靠性。

Java 异常

在 Java 中,Exception 是一个类,用于表示程序运行时发生的错误或异常情况。当程序执行过程中出现错误或异常时,程序会抛出一个 Exception 对象,开发人员可以通过捕捉这个对象并对其进行处理来避免程序崩溃或出现不可预料的结果。

Java Exception 的处理方式有两种,分别是 try-catchthrowtry-catch 语句用于捕捉并处理 Exception 对象,语法如下:

try {
    // 可能会抛出异常的代码块
} catch (Exception e) {
    // 异常处理代码块
}

throw 语句用于手动抛出异常对象,期望由上次调用者进行处理。语法如下:

throw new Exception("Error message");

Java 异常示例 - 空指针

定义一个字符串,不进行任何赋值。这时打印这个字符串的长度,会触发空指针异常

空指针异常 java.lang.NullPointerException 几乎是 Java 中最常见的异常。

package com.wdbyte.exception;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException1 {

    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length());
    }
}

运行后抛出了 java.lang.NullPointerException 异常,在异常信息中,异常的类名和行号至关重要。是定位异常问题的重要依据。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
	at com.wdbyte.exception.JavaException1.main(JavaException1.java:10)

在这个示例中,对于空指针有额外的提示 because "str" is null,是因为我使用了 Java 17,从Java 14 开始,针对空指针异常提示进行了优化。

相关文章:JEP 358:更有用的 NullPointerExceptions

Java 异常示例 - 数组越界

ArrayIndexOutOfBoundsException 也是常见异常的一种,如果试图访问超过了数组大小的元素,就会报错。

package com.wdbyte.exception;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException2 {

    public static void main(String[] args) {
        String[] strArr = {"www", "wdbyte", "com"};
        System.out.println(strArr[0]);
        System.out.println(strArr[3]);
    }
}

运行:

www
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at com.wdbyte.exception.JavaException2.main(JavaException2.java:11)

Java 异常分类

Java Exception 分为两类,分别是受检异常 checked Exception非受检异常 unchecked Exception。受检异常是指必须在程序中进行捕捉和处理的异常,如果不进行处理,程序将无法通过编译。而非受检异常则是指可以不在程序中进行捕捉和处理的异常,但是如果发生了这类异常而没有捕捉处理,程序会立即崩溃停止运行。

通过一个Java 异常继承关系图区分受检异常和非受检异常。

从图中我们发现 Java 异常的一个特点是,所有异常都是 Throwable 的子类。Throwable 的子类 Error 异常通常是不需要我们去处理的,所以重点就是 Exception 异常类了,它是 Java 程序常见的异常的基类。

非受检异常

所有的非受检异常都是 RuntimeException 的子类或者 Error 子类,所以上面的几个例子中空指针异常和数组越界异常,都是非受检异常,如果不主动捕获异常,程序也是可以编译通过的,只是在运行时遇到异常会表现出来。

受检异常

其他的 Exception 的子类且非 RuntimeException 子类的异常,都是受检异常,都需要进行手动捕捉处理,否则程序编译不通过。

举例:读取文件可能会发生异常,且是 IOException 异常,是一个受检异常,所以必须在程序中处理。

package com.wdbyte.exception;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException6 {
    public static void main(String[] args) {
        Files.readAllLines(Paths.get("c:/abc.txt"));
    }
}
// 不处理编译直接报错
// JavaException6.java:13:27
// java: 未报告的异常错误java.io.IOException; 必须对其进行捕获或声明以便抛出

必须捕获异常才能编译通过,这种机制其实是在帮助你编写更稳定的代码:

try {
    Files.readAllLines(Paths.get("c:/abc.txt"));
} catch (IOException e) {
    throw new RuntimeException(e);
}

Java 捕捉异常

上面的示例中,运行时都发生了异常行为,这种异常在编译时并不会提示。如果我们知道某处可能要发生异常,可以使用 try-catch 捕获异常。

捕获指定异常

package com.wdbyte.exception;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException3 {

    public static void main(String[] args) {
        String[] strArr = {"www", "wdbyte", "com"};
        try {
            System.out.println(strArr[0]);
            System.out.println(strArr[3]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("发生数组访问越界异常,不能访问超过数组长度的元素,数组长度为:" + strArr.length);
        }
    }
}

这个例子中捕捉了数组越界异常,这会输出:

www
发生数组访问越界异常,不能访问超过数组长度的元素,数组长度为:3

不过通常情况下,不建议在捕获异常时只输出一段内容,为了异常的情况定位,最好输出异常的详细内容。

String[] strArr = {"www", "wdbyte", "com"};
try {
    System.out.println(strArr[0]);
    System.out.println(strArr[3]);
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("发生数组访问越界异常,不能访问超过数组长度的元素,数组长度为:" + strArr.length);
    System.out.println("异常描述:" + e.getMessage());
    e.printStackTrace();
}

这会输出详细的报错信息,方便后续异常定位:

www
发生数组访问越界异常,不能访问超过数组长度的元素,数组长度为:3
异常描述:Index 3 out of bounds for length 3
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at com.wdbyte.exception.JavaException3.main(JavaException3.java:12)

打印看懂异常信息

使用 e.printStackTrace() 方法来输出异常的详细信息。

package com.wdbyte.exception;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author https://www.wdbyte.com
 * @date 2023/04/27
 */
public class JavaException6 {

    public static void main(String[] args) {
        try {
            Files.readAllLines(Paths.get("c:/abc.txt"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

输出:

java.nio.file.NoSuchFileException: c:/abc.txt
	at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
	at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
	at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
	at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)
	at java.base/java.nio.file.Files.newByteChannel(Files.java:380)
	at java.base/java.nio.file.Files.newByteChannel(Files.java:432)
	at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:422)
	at java.base/java.nio.file.Files.newInputStream(Files.java:160)
	at java.base/java.nio.file.Files.newBufferedReader(Files.java:2922)
	at java.base/java.nio.file.Files.readAllLines(Files.java:3412)
	at java.base/java.nio.file.Files.readAllLines(Files.java:3453)
	at com.wdbyte.exception.JavaException6.main(JavaException6.java:15)

如何看懂这个异常信息呢?通常异常的类名和异常发生的类的行号至关重要,其次是错误描述信息,通过这三个信息就可以看懂绝大多数异常了。特别是 JDK 中的异常类名,基本都是见名知意。在这个异常堆栈中先找到自己编写类,可以看到 JavaException6.java:15 ,也就是错误发生在类 JavaException6 的第 15 行,异常类名为 NoSuchFileException,可见是找不到文件。描述是 c:/abc.txt ,可见是找不到这个文件。

代码的第 15 行内容为:

 Files.readAllLines(Paths.get("c:/abc.txt"));

捕获多个异常

使用多个 catch 捕获不同的异常,

package com.wdbyte.exception;

import org.apache.commons.lang3.ObjectUtils.Null;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException5 {

    public static void main(String[] args) {
        String[] strArr = {"www", null, "com"};
        try {
            System.out.println(strArr[0].length());
            // 空指针异常
            System.out.println(strArr[1].length());
             // 越界异常
            System.out.println(strArr[3].length());
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("发生数组访问越界异常,不能访问超过数组长度的元素,数组长度为:" + strArr.length);
        } catch (NullPointerException e) {
            System.out.println("发现空指针异常");
        } 
        System.out.println("执行完成。");
    }
}

输出:

3
发现空指针异常
执行完成。

捕获所有异常

直接捕获异常的底层父类 Exception ,可以捕获到所有 Exception 子类异常。

try {
    System.out.println(strArr[0].length());
    // 空指针异常
    System.out.println(strArr[1].length());
    // 越界异常
    System.out.println(strArr[3].length());
} catch (Exception e) {
    System.out.println("发生异常:" + e.getMessage());
    e.printStackTrace();
}

Java 处理异常

上面的示例中,使用 try-catch 捕捉了异常情况,并且输出了异常的详细信息,这已经是处理异常的一种方式了,还有一种情况是自身不能处理异常行为,这时可以把异常抛出,让上层调用者处理异常。

package com.wdbyte.exception;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException4 {

    public static void main(String[] args) {
        String[] strArr = {"www", "wdbyte", "com"};
        try {
            System.out.println(strArr[0]);
            System.out.println(strArr[3]);
        } catch (ArrayIndexOutOfBoundsException e) {
            throw e;
        }
        System.out.println("执行完成。");
    }
}

因为在 14 行对异常进行了抛出,所以程序运行到此处会中断运行同时抛出异常,交给上层调用处理,上次调用已经是 main 函数,没有针对处理,程序结束。执行完成 不会被输出。

运行日志:

www
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at com.wdbyte.exception.JavaException4.main(JavaException4.java:12)

Finally 处理

通过上面的例子发现,如果发生了异常,程序运行会直接中断,运行跳到异常捕捉部分,如果继续抛出异常,则跳到上层异常捕捉部分。这时如果我们有些非常必要的代码写在了异常发生行数之后,程序运行的稳定性就无法保证了,可能会出现意外情况。

比如读取一个文件,因为一个进程同时打开的文件数量有限,所以读取后需要关闭文件流信息。如果发生异常可能会导致关闭代码执行不到,这时候可以使用 finally 关键词,它可以保证即使在异常情况下,也会执行某段代码。

示例:读取一个不存在的文件,抛出异常后执行到 finally 代码。

package com.wdbyte.exception;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class ReadFile {

    public static void main(String[] args) {

        File file = new File("pom2.xml");
        FileReader fileReader = null;
        BufferedReader bufferedReader = null;
        try {
            fileReader = new FileReader(file);
            bufferedReader = new BufferedReader(fileReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
            bufferedReader.close();
            fileReader.close();
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在");
        } catch (IOException e) {
            System.out.println("读取文件失败");
        } finally {
            try {
                System.out.println("开始关闭资源");
                if (bufferedReader != null) {
                    bufferedReader.close();
                    System.out.println("关闭 bufferedReader");
                }
                if (fileReader != null) {
                    fileReader.close();
                    System.out.println("关闭 fileReader");
                }
            } catch (IOException e) {
                System.out.println("关闭文件失败");
            }
        }
    }
}

输出:

文件不存在
开始关闭资源

示例:正常读取一个文件,也会运行到 finally 代码。

package com.wdbyte.exception;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class ReadFile {

    public static void main(String[] args) {

        File file = new File("pom.xml");
        FileReader fileReader = null;
        BufferedReader bufferedReader = null;
        try {
            fileReader = new FileReader(file);
            bufferedReader = new BufferedReader(fileReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
            bufferedReader.close();
            fileReader.close();
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在");
        } catch (IOException e) {
            System.out.println("读取文件失败");
        } finally {
            try {
                if (bufferedReader != null) {
                    bufferedReader.close();
                    System.out.println("关闭 bufferedReader");
                }
                if (fileReader != null) {
                    fileReader.close();
                    System.out.println("关闭 fileReader");
                }
            } catch (IOException e) {
                System.out.println("关闭文件失败");
            }
        }
    }
}

运行输出:

// 输出内容
关闭 bufferedReader
关闭 fileReader

资源初始化与关闭

上面的读取文件关闭文件的 try-catch-finally 方式是在太繁琐了, Java 7 后可以直接通过 try-with-resource 的方式关闭资源。被 try 括号包围的资源会自动调用 close 方法,不管是否发生异常,所以可以简化为。

package com.wdbyte.exception;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class ReadFile2 {

    public static void main(String[] args) {
        File file = new File("pom.xml");
        try (FileReader fileReader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(
            fileReader);) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在");
        } catch (IOException e) {
            System.out.println("读取文件失败");
        }
    }
}

相关文档:try-with-resource 介绍

自定义异常

Java 中的自定义异常可以通过继承 Exception 或 RuntimeException 类来实现。通过自定义异常,我们可以根据自己的需求定义一些特定的异常类型,从而更好地处理程序中可能出现的错误情况。

具体步骤如下:

  1. 创建一个新的Java 类,命名为自定义异常类的名称。
  2. 继承 Exception 或 RuntimeException 类。

例如:

public class MyException extends Exception {
}

也可以在自定义异常类中添加构造方法,以便在捕获异常时能够提供有用的信息。

例如:

public class MyException extends Exception {
	public MyException(String message) { 
    super(message); 
  }
}

当然,你也可以在异常类中定义自己需要的属性和方法。继承自 Exception 的非 RuntimeException 子类的异常,是受检异常,调用者调用的方法如果抛出了这个异常,那么调用者必须处理此异常。

package com.wdbyte.exception;

/**
 * @author https://www.wdbyte.com
 */
public class JavaException8 {

    public static void main(String[] args) {
        int result = 0;
        try {
            result = calc(4, 2);
            System.out.println("4 / 2 = " + result);
        } catch (MyException e) { // 不处理异常会报错
            e.printStackTrace();
        }

        try {
            result = calc(4, 0);
            System.out.println("4 / 0 = " + result);
        } catch (MyException e) { // 不处理异常会报错
            e.printStackTrace();
        }
    }

    public static int calc(int a, int b) throws MyException {
        if (b == 0) {
            throw new MyException("除数不能为0");
        }
        return a / b;
    }
}

执行输出:

4 / 2 = 2
com.wdbyte.exception.MyException: 除数不能为0
	at com.wdbyte.exception.JavaException8.calc(JavaException8.java:27)
	at com.wdbyte.exception.JavaException8.main(JavaException8.java:18)

附录:常见 Java 异常

异常 描述
NullPointerException 当试图在需要对象的地方使用 null 时,抛出该异常。
ArrayIndexOutOfBoundsException 当试图访问数组的索引超出范围时,抛出该异常。
ClassNotFoundException 程序试图加载类时,找不到相应的类,抛出该异常。
DivideByZeroException 当除数为零时,抛出该异常。
FileNotFoundException 当程序试图打开一个不存在的文时,抛出该异常。
ClassCastException 当试图将对象强制转换为不能转换的类型时,抛出该异常。
IOException 当发生某种 I/O 异常时,抛出该异常。
NoSuchMethodException 当试图访问一个不存在的方法时,抛出该异常。
NumberFormatException 当应用程序试图将字符串转换为数字,但该字符串不能转换为数字时,抛出该异常。
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
SQLException 当访问数据库时出现问题时,抛出该异常。
InterruptedException 当一个线程处于等待状态,另一个线程中断该线程时,抛出该异常。
IllegalArgumentException 当向方法传递一个不合法或不正确的参数时,抛出该异常。
ConcurrentModificationException 遍历集合时修改集合中的对象时报错,往往是使用的非线程安全集合。

一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.