Skip to content

异常捕获 #2

@wuyuedefeng

Description

@wuyuedefeng

Java异常是class,它的继承关系如下:

注意:编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。

定义需要用户捕获的方法

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
   //  ...
}

捕获异常

// eg: 
import java.io.UnsupportedEncodingException;
import java.io.IOException;

public class Main {
    // demo1
    public static void main(String[] args) {
        try {
            // ...
        } catch (UnsupportedEncodingException e) {
            System.out.println("Bad encoding");
        } catch (IOException | NumberFormatException e) { // 多个不同异常但是逻辑相同可以使用 | 运算符
            System.out.println("IO error");
        } finally { // 是否抛出异常都会执行
            System.out.println("END");
        }
    }
    // demo2
    void process(String file) throws IOException {
        try {
            // ...
        } finally {
            System.out.println("END");
        }
    }
}

抛出异常

  • 创建某个Exception的实例;
  • 用throw语句抛出。
// eg:
public class Main {
    public static void main(String[] args) {
        try {
            Main.process1(null);
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常栈信息
        }
    }

    static void process1(String s) {
        try {
            Main.process2(s);
        } catch (NullPointerException e) {
            // 捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!(所以这里的e要作为参数传递出去)
            throw new IllegalArgumentException(e);
        } finally {
            System.out.println("finally");
            // 如果在finally中也抛出异常,则cache中的异常将会被屏蔽; 解决:将cache中的e放在临时变量(eg: origin)中,在下面传递给新的错误
            // 通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。
            // Exception e = new IllegalArgumentException();
            // if (origin != null) {
            //    e.addSuppressed(origin); // 把原始异常添加进来
            // }
            // throw e;
        }
    }

    static void process2(String s) {
        if (s==null) {
            throw new NullPointerException();
        }
    }
}

自定义异常

当我们在代码中需要抛出异常时,尽量使用JDK已定义的异常类型。例如,参数检查不合法,应该抛出IllegalArgumentException。

# Java标准库定义的常用异常包括:
Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException

如果定义业务异常,需要从一个适合的Exception派生,通常建议从RuntimeException派生:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}
public class UserNotFoundException extends BaseException {
    // ...
}
public class LoginFailedException extends BaseException {
    // ...
}

NullPointerException

在所有的RuntimeException异常中,Java程序员最熟悉的恐怕就是NullPointerException了。NullPointerException即空指针异常,俗称NPE。

  • 尝试解决方法(考虑返回Optional<T>):
public Optional<String> readFromFile(String file) {
    if (!fileExist(file)) {
        return Optional.empty();
    }
    // ...
}

这样调用方必须通过Optional.isPresent()判断是否有结果。

  • 定位NullPointerException(我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它:)
# 启用Java 14的增强异常信息来查看NullPointerException的详细错误信息
java -XX:+ShowCodeDetailsInExceptionMessages Main.java

这样我们就能在报错信息中查看到具体哪个值为null导致的错误。

断言

断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
断言很少被使用,更好的方法是编写单元测试。

要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)

  • 参数启用断言。eg: java -ea Main.java
  • 还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main
  • 或者对特定地包启用断言,命令行参数是:-ea:com.itranswarp.sample...(注意结尾有3个.)
public class Main {
    public static void main(String[] args) {
        double x = Math.abs(-123.45);

        // 如果计算结果为false,则断言失败,抛出AssertionError
        assert x >= 0;
        assert x >= 0 : "x must >= 0"; // 断言并自定义错误信息

        System.out.println(x);
    }
}

使用JDK Logging

import java.util.logging.Level;
import java.util.logging.Logger;

public class Hello {
    public static void main(String[] args) {
        Logger logger = Logger.getGlobal();
        logger.info("start process...");
        logger.warning("memory is running out...");
        logger.fine("ignored.");
        logger.severe("process will be terminated...");
    }
}

JDK的Logging定义了7个日志级别,从严重到普通:SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST;
默认级别是INFO。

Java标准库内置的Logging有以下局限:

  • Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改配置;
  • 配置不太方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=。

因此,Java标准库内置的Logging使用并不是非常广泛。

使用Commons Logging

使用Log4j

使用SLF4J和Logback

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions