Java 基础之 异常

发布于 2024-08-08 21:07:28 字数 12369 浏览 16 评论 0

异常

异常简介

何为异常?就是程序因为某些非语法错误原因导致的意外情况,可能是没有控制变量范围,可能是没有权限读取等

举个例子

public void fun(int a,int b){
    int c = a/b
}

这个例子我没有控制 b 的范围,也没有任何语法错误,试想如果 b == 0 时,除数为 0 是不是就是一种异常情况?

(实际上运行时会抛出一个异常)

异常分类

只展示实际用途中最广泛使用的几种

1604643102895

Throwable

所有异常的顶级接口

其接口描述为

The Throwable class is the superclass of all errors and
exceptions in the Java language. Only objects that are instances of this
class (or one of its subclasses) are thrown by the Java Virtual Machine or
can be thrown by the Java throw statement. Similarly, only
this class or one of its subclasses can be the argument type in a
catch clause.

翻译一下

Throwable 类是所有错误和错误的超类。只有这个类(或它的一个子类) 的实例的对象可以由 Java 虚拟机抛出,或者可以由 Java 的 throw 语句抛出。同样的,只这个类或它的一个子类可以是 catch 子句中的参数类型。

这就是规范了 java 的异常机制,如果是异常就必须是这个类的子类

Exception

程序员自定义异常的父类

The class {@code Exception} and its subclasses are a form of{@code Throwable} that indicates conditions that a reasonablea pplication might want to catch.

简单来说就是这个异常是明确知道原因且希望被捕获

这个类本身和其大部分子类抛出时要在方法签名上注明,方法签名上有这个异常声明的一定要捕获或者向上抛出,专有名词被称为 checked exception (受检异常)

Error

An {@code Error} is a subclass of {@code Throwable}
that indicates serious problems that a reasonable application
should not try to catch. Most such errors are abnormal conditions.
The {@code ThreadDeath} error, though a "normal" condition,
is also a subclass of {@code Error} because most applications
should not try to catch it.

简单来说就是一个不希望被捕获的异常,一般是触发了重大异常程序无法正常继续运行

let it crash 但你说能不能 catch 呢?其实是可以的 但是不推荐

1650594425341

RuntimeException

这个是 Exception 子类

<p>{@code RuntimeException} and its subclasses are <em>uncheckedexceptions</em>.  Unchecked exceptions do <em>not</em> need to bedeclared in a method or constructor's {@code throws} clause if theycan be thrown by the execution of the method or constructor andpropagate outside the method or constructor boundary.

简单来说这个类及其子类抛出时不需要标识在方法签名中

专有名词为 unchecked exception

异常处理和抛出

处理

在开发中我们调用第三方类库或者 jdk 类库往往能看到形如这样的方法

public void run() throws Exception{
//todo
}

当我们调用时一般有两者方法处理

1,向上抛出 2,自行捕获处理

向上抛出

请注意这个方法治标不治本 ,本质不处理,处理交给方法调用者处理,如果一直不处理传递到 main 就会终止程序

public static void exceptionMethod() throws Exception {

    }
public static void handleException1()throws Exception{
        exceptionMethod();
    }
自行捕获处理

注意这里我们会提到一个新的语法 try..catch

checked exception 必须捕获,unchecked exception 可以捕获也可以不捕获

这样异常就不会继续传播了

public static void handlerException2(){
        try {
            exceptionMethod();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

抛出

对于异常我们通过一个关键字 throw 抛出

请记住 throw 一个 checked exception 需要在方法签名上标识

public static void throwException() throws Exception {
        throw new Exception("发生异常");
    }

而对于一个 unchecked exception 则不需要

 public static void throwRuntimeException(){
        throw new RuntimeException();
    }

try .. catch .. finally

对于 try 语句块后面可以跟随 catch 块或者 finally 块,或者一起

对于 finally 块的意义在于无论是否发生异常这个块必定的会执行

try .. catch

try {
public static void tryCatch1(){
        try { int i = 1 / 0;
    }catch (ArithmeticException e){
        System.out.println("arithmetic");
    }catch (RuntimeException e){
        System.out.println("runtime");
    }
}
public static void tryCatch2(){
    try {
        int i = 1 / 0;
    }catch (RuntimeException e){
        System.out.println("arithmetic");
    }catch (ArithmeticException e){
        System.out.println("runtime");
    }
    //正确写法
    // try {
    // int i = 1 / 0;
    // }catch (ArithmeticException e){
    // System.out.println("arithmetic");
    // }catch (RuntimeException e){
    // System.out.println("runtime");
    // }
}

对比这两个以及观察 ide 的提示

Error:(39, 10) java: 已捕获到异常错误 java.lang.ArithmeticException

也就是说 catch 的上下顺序即子类到父类的顺序,必须先捕获子类,如果 catch 语句中无对应子类则取寻找其 catch 的父类异常,而且符合短路原则,优先被先声明 catch 的捕获,捕获后不再执行下面的 catch

try .. finally

public static void tryFinally(){
    try {
        System.out.println("抛出前");
        if (1 == 1){
            throw new RuntimeException();
        }
        System.out.println("我不会执行");
    }finally {
        System.out.println("我一定执行");
    }
}

控制台输出

抛出前
我一定执行

涉及到返回值的复合 try…catch..finally ​ 语句

这一块更是重量级 没兴趣就别看了 没什么意义

  1. 如果三个块均有 return 则返回 finally 里面的
  2. 如果 finally 没有 return 而在 try return 前捕获了异常,则返回 catch 的 return
 public static String complexTryCatchFinally(boolean hasException){
        try {
            System.out.println("执行 try");
            throwExceptionByParam(hasException);
            return "try";
        }catch (Exception e){
            System.out.println("执行 catch");
            return "catch";
        }finally {
            System.out.println("执行 finally");
            return "finally";
        }
    }
    private static void throwExceptionByParam(boolean hasException) throws Exception{
        if (hasException){
            throw new Exception();
        }
    }

关于执行

    System.out.println(LearnException.complexTryCatchFinally(false));
        System.out.println("---------------------");
        System.out.println(LearnException.complexTryCatchFinally(true));

控制台

执行 try
执行 finally
finally
---------------------
执行 try
执行 catch
执行 finally
finally

当注释掉 finally 块时

控制台输出

执行 try
try
---------------------
执行 try
执行 catch
catch

总结一下 return 实际看优先级顺序,其不代表着对应优先级低的块不执行,详见 finally 和 return 的执行顺序

try-with-resource*(建议学完之后的流再看)

// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(
           new FileReader(path))) {
       return br.readLine();
    }
}

即 try 允许传入一个资源(实际上就是实现 autoclose 接口),会最后自动关闭

详见:

利用 twr 块级作用域 RAII 的技巧

有一些资源允许你手动释放 比如说连接池的链接,native 内存的释放,文件描述符等

虽然 jdk 内部利用 finalize(jdk18 确定移除这个 api 了)或 java.lang.ref.Cleaner 等可以让 gc 帮我们辅助清除这些资源,但是存在资源释放不及时影响吞吐或者存在一定的资源泄露风险,所以有时候我们需要确定性析构,控制资源超过他的逻辑生命周期的时候释放掉

以 java 的 project Panama 引入的更友好的 native memory api 举例子:

s1,s2 都会在 twr 块结束后直接释放掉,这样看起来更加直观且放心

try (MemorySession session = MemorySession.openConfined()) {
    MemorySegment s1 = MemorySegment.map(Path.of("someFile"),
                                         0, 100000,
                                         MapMode.READ_WRITE, session);

    MemorySegment s2 = MemorySegment.allocateNative(100, session);
    ...
}
利用 twr 块级作用域进行结构化并发的技巧*

注意 :截止到本文 commit(2022 年 4 月 22 日)时,这个仍没有合并到主线中,使用的 jdk 为 19-loom+5-429

其中每一次 fork 都会启动一个 java 虚拟线程(有栈协程)

你可以通过 join 等待全部的控制流分支完成 通过 scope 的 close 来确保结束后不再有控制流分叉

这种就可以帮助你写出明确控制流的并发程序且便利地统一控制资源的释放

try (StructuredTaskScope<String> scope = new StructuredTaskScope<String>()) {
            Future<String> f1= scope.<String>fork(() -> {
                Thread.sleep(100);
                return "fork2";
            });
            Future<String> f2 = scope.<String>fork(() -> {
                Thread.sleep(200);
                return "fork2";
            });
            scope.join();
            System.out.println(f1.get()+" "+f2.get());
        }catch (Exception e){
            e.printStackTrace();
        }

自定义异常

直接继承对应父类即可,这样就可以在 throw 中抛出

checked exception

public class CustomerCheckException extends Exception{}

unchecked exception

public class CustomerUncheckedException extends RuntimeException{

}

异常的性能开销

太长不看版:只是个跳转 想用就用,性能开销不在抛出异常 catch 异常上面

写个伪代码来讲解 catch 原理,本质上就是一个跳转表

if error occur goto catch_method
else next

问题性能的关键在于填充堆栈,就是你的 Throwable::printStackTrace 的打印数据

我们先来看看如何不进行堆栈填充

1650600140500

找到 Throwable 源码查看 javadoc 可知 只需要 writeStckTrace 为 false 即可

即这样就行了 这样就可以把它作为低开销的控制流方法了

public class NoStackTraceThrowable extends Throwable {

  public NoStackTraceThrowable(String message) {
    super(message, null, false, false);
  }
}

那么代价是什么?

代价就是你不再有 任何的堆栈信息 ,比如说调用栈情况,代码行数等信息

但是有时候确实是有利的

微服务中线程堆栈会很深(150 左右) ,主要是因为 servlet 与 filter 的设计是责任链模式,各个 filter 会不断加入堆栈,如果是还存在反射,代理等会导致有大量的堆栈噪音,其实有效信息很少了。

对于填充堆栈需要访问 StringTable 和 SymbolTable 这两个的访问都需要 String::intern 方法,因为我们要看到的是具体的类名方法名,而不是类的地址以及方法的地址,更不是类名的地址以及方法名的地址

需要有大量的字符串拼接操作,而有效信息只有栈顶一点点,实际上有很多算力就这么浪费掉了。即有些时候我们只是单纯记录异常发生原因或者为了逻辑清晰的跳转 就可以不填充堆栈,只要传入的 Message 足够清晰即可

而且

HotSpot VM 有个许多人觉得“匪夷所思”的优化,叫做 fast throw:有些特定的隐式异常类型(NullPointerException、ArithmeticException( / 0)之类)如果在代码里某个特定位置被抛出过多次的话,HotSpot Server Compiler(C2)会透明的决定用 fast throw 来优化这个抛出异常的地方——直接抛出一个事先分配好的、类型匹配的异常对象。这个对象的 message 和 stack trace 都被清空。抛出这个异常的速度是非常快,不但不用额外分配内存,而且也不用爬栈;但反面就是可能正好是需要知道哪里出问题的时候看不到 stack trace 了。

拓展资料

奇技淫巧

如何既抛出一个 checked 异常又不需要调用者注明在方法上

public class TricksOnException {


    //调用这个方法就行
    public static<E extends Throwable> void throwUncheckByGeneric(Throwable e) throws E{
        throw (E)e;
    }

    //调用这个方法就行
    public static void throwUncheckedByUnsafe(Throwable throwable){
        try {
            Field field = Unsafe.class.getField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            unsafe.throwException(throwable);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }



}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

linfzu01

文章 0 评论 0

可遇━不可求

文章 0 评论 0

枕梦

文章 0 评论 0

qq_3LFa8Q

文章 0 评论 0

JP

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文