犬豪

V1

2023/04/13阅读:29主题:Pornhub黄

Java中的异常体系

Java语言在设计之初就提供了相对完善的异常处理机制,在日常编程中,如何处理好异常是比较考验功底的,掌握好需要两个方面,面试官也常从这两个方面作为抓手出题:

  1. 理解Throwable,Exception,Error的设计和分类
  2. 理解java语言中操作Throwable的元素和实践

今天就来谈谈第一点,除了掌握Exception和Error的设计理念外,还有比如:

  • 掌握那些应用最为广泛的子类
  • 以及如何自定义异常等

一、掌握常见的异常类

很多面试官会进一步追问一些细节,比如,你了解哪些Error,Exception或者RuntimeException?面试官并不想听到你说NullPointerException、ClassCastException这些运行时异常,如果你只了解这些说明你掌握的还是太少了。

今天我导了一个类图,并列出来典型例子,可以给你作为参考,至少做到基本心里有数:

这些异常都是比较常见的异常,你是否都遇到过呢?接下来我们一个一个看:

ExceptionInInitializerError

ExceptionInInitializerError:当在静态初始化块中出现异常的时候会抛出该异常。

Demo:

public class QuanHaoTest {

    public static void main(String[] args) {
        Person.hello();
    }

}

class Person {
    static String name = "小豪";
    static {
        int num = 5 / 0;
    }

    public static void hello() {
        System.out.println("hello");
    }
}

输出结果:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.ArithmeticException: / by zero
    at com.qhao.java.Person.<clinit>(QuanHaoTest.java:19)
    ... 1 more

几个点说明一下:

1."Exception in thread "main" java.lang.ExceptionInInitializerError"意味着异常出现在主线程,并且是LinkageError的一个子类java.lang.ExceptionInInitializerError,这是JVM类加载失败时才抛出的,原因是静态初始化代码中出现了诸如IndexOutOfBoundsException这样的RuntimeException

2.记住JVM会将所有的静态变量的初始化按它们在源文件中的出现顺序放到一个静态初始化块中。因此,不要觉得没有看到静态初始块就认为不会出现这个异常。事实上,你得确保静态变量的正确顺序,比如说,如果 一个变量初始化的时候用到了另一个变量,你得确保这个变量在前面已经初始化过了。

比如看下面这段代码,这段编码编译可以通过,但是运行会报错:

public class StaticParams {
    //1.静态初始化,先执行这里
    private static StaticParams sp = buildStaticParams();
    //3.如果没有报错,第三步走这
    private static List<String> LIST_A = getListA();

    private static StaticParams buildStaticParams() {
        //2.走到这静态变量LIST_A还没有初始化报空指针
        LIST_A.add("abc");
        return new StaticParams();
    }

    private static List<String> getListA() {
        System.out.println("初始化List");
        return new ArrayList<String>();
    }
}

public class QuanHaoTest {

    public static void main(String[] args) {
        StaticParams staticParams = new StaticParams();
    }

}

输出:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.NullPointerException
    at com.qhao.java.StaticParams.buildStaticParams(StaticParams.java:16)
    at com.qhao.java.StaticParams.<clinit>(StaticParams.java:12)
    ... 1 more

3.对于静态初始化块中出现异常的类,第一次加载会抛出ExceptionInInitializerError异常,以后任何使用到这个类的地方,都会抛出NoClassDefFoundError异常。这也是生产环境中经常遇到的错误,如果你在遇到NoClassDefFoundError类不存在的异常时,不妨先看看你的日志文件中有没有ExceptionInInitializerError这个异常。

Demo:

public class QuanHaoTestError {

    public static void main(String[] args) {
        try {
            // 第一次使用,加载类
            Person.hello();
        } catch (Throwable e) {
            // 捕获之后再次使用,还会报错
            Person.hello();
        }
    }
}

class Person {
    static String name = "小豪";
    static {
        int num = 5 / 0;
    }

    public static void hello() {
        System.out.println("hello");
    }
}

输出:

Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class com.qhao.java.Person
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:16)

4.记住静态初始化代码块会抛出RuntimeException而不是已检查异常,而后者需要有对应的catch块来进行处理。

需要谨记的是这个异常的一个副作用是NoClassDefFoundError,而Java程序抛出这个异常的位置可能会离java.lang.ExceptionInInitializerError很远,这取决于你的客户端代码何时引用到这个类。因此,在查看类路径解决NoClassDefFoundError异常之前,最好先看看日志有没有出现ExceptionInInitializerError。

UnsatisfiedLinkError

这个异常和JNI有关,先简单了解一下JNI:

JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。

关于JNI先了解这么多,我们只要知道Java中提供了API:System.loadLibrary("") 可以加载本地的动态链接库,将Java中的native方法和本地代码关联上,其方法参数为不包含扩展名的动态链接库库文件名。

在不同操作系统下,c/c++或其他代码生成的动态链接库也会有差异,在window平台下会编译为dll文件,linux平台下会编译为so文件,在mac os下会编译为dylib文件,关于这一点,你可以在JRE_HOME/jre/lib下查看,这也是为什么JVM跨平台的原因:

再回来看这个异常定义

UnsatisfiedLinkError:对于声明为 native 的方法,如果 Java 虚拟机找不到和它对应的本机语言定义,就会抛出该异常。

当调用本地方法时,类加载器会尝试加载定义了该方法的本地库。如果找不到这个库,就会抛出这个错误。 下面是抛出 UnsatisfiedLinkError 的测试用例 :

public class QuanHaoTestError {

    public native void call_A_Native_Method();

    static {
        System.loadLibrary("myNativeLibrary");
    }

    public static void main(String[] args) {
        new QuanHaoTestError().call_A_Native_Method();
    }
}

输出:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no myNativeLibrary in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1122)
    at com.qhao.java.QuanHaoTest.<clinit>(QuanHaoTest.java:13)

NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

这里两个类一起说,因为NoClassDefFoundError和ClassNotFoundException有什么区别,这也是一个经典的入门题目。

当一个类找不到的时候,JVM有时候会抛出ClassNotFoundException异常,而有时候又会抛出NoClassDefFoundError。看两个异常的字面意思,好像都是类找不到,但是JVM为什么要用两个异常去区分类找不到的情况呢?这个两个异常有什么不同的地方呢?

ClassNotFoundException:

ClassNotFoundException是一个运行时异常。从类继承层次上来看,ClassNotFoundException是从Exception继承的,所以ClassNotFoundException是一个检查异常。

当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。

最简单的,当我们使用JDBC去连接数据库的时候,我们一般会使用Class.forName()的方式去加载JDBC的驱动,如果我们没有将驱动放到应用的classpath下,那么会导致运行时找不到类,所以运行Class.forName()会抛出ClassNotFoundException。

public class QuanHaoTest {
    public static void main(String[] args) {
        try {
            Class.forName("oracle.jdbc.driver.OracleDriver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)

NoClassDefFoundError:

NoClassDefFoundError异常,看命名后缀是一个Error。从类继承层次上看,NoClassDefFoundError是从Error继承的。和ClassNotFoundException相比,明显的一个区别是,NoClassDefFoundError并不需要应用程序去关心catch的问题。

当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行时类找不到,则会抛出一个NoClassDefFoundError的错误。

public class QuanHaoTest {

    public static void main(String[] args) {
        TempClass t = new TempClass();
    }
}

class TempClass {}

首先这里我们先创建一个TempClass,然后编译以后,将TempClass生产的TempClass.class文件删除,然后执行程序,输出:

Exception in thread "main" java.lang.NoClassDefFoundError: com/qhao/java/TempClass
	at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.ClassNotFoundException: com.qhao.java.TempClass
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 1 more

总结就是:

ClassNotFoundException NoClassDefFoundError
从java.lang.Exception继承,是一个Exception类型 从Java.lang.Error继承,是一个Error类型
当动态加载Class的时候找不到类会抛出该异常 当编译成功以后执行过程中Class找不到导致抛出该错误
一般在执行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的时候抛出 由JVM的运行时系统抛出

OutOfMemoryError

内存溢出也是平时开发经常遇到的一个错误,直接演示:

堆溢出,设置堆大小 -Xmx10m -Xms10m :

public class QuanHaoTest {
    byte[] buffer = new byte[new Random().nextInt(1024 * 200)];

    /**
     * -Xmx10m -Xms10m
     * 
     * @param args
     */

    public static void main(String[] args) {
        List<QuanHaoTest> list = new ArrayList<QuanHaoTest>();
        while (true) {
            list.add(new QuanHaoTest());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at com.qhao.java.QuanHaoTest.<init>(QuanHaoTest.java:13)
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:23)

方法区溢出,我这里是jdk1.8,所以用的元空间大小参数 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m:

/**
 * Created by 犬豪
 *
 * @Homepage www.yuque.com/qhao
 */

public class QuanHaoTest {

    /**
     * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     * 
     * @param args
     */

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
                    throws Throwable 
{
                    return methodProxy.invoke(o, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {}
}
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:557)
    at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
    at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
    at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
    at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:387)
    at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:35)

StackOverflowError

栈溢出也是我们经常遇到的一个异常,在写递归的时候经常会出现,这里补充一个小知识点:你知道Java最大支持栈深度有多大吗? 其实这里是没有一个固定数值的,主要受两个因素影响:

  1. 我们可以通过 -Xss 设置 Java 线程堆栈大小,堆栈大小越大,能够支持越多的方法调用(能够存储更多的栈帧)
  2. 我们知道JVM中每个栈帧包含五部分,分别包括局部变量表、操作数栈、动态链表(指向运行时常量池的方法引用)、方法返回地址和一些附加信息,其中影响栈帧大小最关键的因素就是局部变量表了。所以局部变量表内容越多(注意是个数),那么栈帧就越大,栈深度就越小

堆栈大小设置-Xss 5m:

public class QuanHaoTest {
    public int count = 0;

    public void stackOverTest(String s1) {
        count++;
        stackOverTest(s1);
    }

    /**
     * -Xss 5m
     */

    public static void main(String[] args) {
        QuanHaoTest quanHaoTest = new QuanHaoTest();
        try {
            quanHaoTest.stackOverTest("s1");
        } catch (Throwable e) {
            System.out.println("stack depth: " + quanHaoTest.count);
            e.printStackTrace();
        }
    }

}
stack depth: 1732
java.lang.StackOverflowError
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:12)
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)

添加成两个参数后:

public class QuanHaoTest {
    public int count = 0;

    public void stackOverTest(String s1, String s2) {
        count++;
        stackOverTest(s1, s2);
    }

    /**
     * -Xss 5m
     */

    public static void main(String[] args) {
        QuanHaoTest quanHaoTest = new QuanHaoTest();
        try {
            quanHaoTest.stackOverTest("s1""s2");
        } catch (Throwable e) {
            System.out.println("stack depth: " + quanHaoTest.count);
            e.printStackTrace();
        }
    }

}
stack depth: 1599
java.lang.StackOverflowError
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:12)
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
    at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)

SecurityException

这个异常通常都是由安全管理器SecurityManager抛出的,以指示安全违规的,除了SecurityManager外其他任何有安全违规的场景都有在用,比如当你自己定义了一个包路径以"java."开头的类的时候:

package java;

public class QuanHaoTest {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

其他常见的运行时异常

  • IllegalArgumentException:非法参数异常
  • NullPointerException:空指针
  • ClassCastException:类型转换异常
  • ArrayIndexOutOfBoundsException:索引越界
  • ArithmeticException:运算异常

这些非常常见,直接上Demo:

public class QuanHaoTest {

    public static void main(String[] args) {
        QuanHaoTest nullPointerException = null;
        nullPointerException.hello();// 空指针

        Object str = "QuanHaoTest";
        QuanHaoTest classCastException = (QuanHaoTest)str;//类型转换异常

        int[] arrayIndexOutOfBoundsException = new int[3];
        int value = arrayIndexOutOfBoundsException[5];//索引越界

        int arithmeticException=10/0;//运算异常
    }

    void hello() {
    }
}

二、自定义异常

有的时候我们会根据需要自定义异常:

首先从Exception和Error的设计理念来看,自定义异常一般是不会继承Error的,而是继承Exception,因为自定义的异常肯定是可预料到的,可处理的,一般也不会是虚拟机层面的错误。

另外自定义异常除了保证提供足够的信息外,还有两个点需要考虑:

  • 是否需要定义成Checked Exception,因为这种类型设计的初衷就是为了从异常情况下恢复,作为异常设计者,我们往往有充足信息进行分类。所以如果是"可恢复"的异常,可以定义成它
  • 为保证诊断信息足够的同时,要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。如果我们看java的标准库,你可能注意到类似java.net.ConnectException,出错信息是类似"connection refused(connection refused)",而不包含具体的机器名,ip,端口等,一个重要考虑就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。

业界有一种争论(甚至可以算上是某种程度的共识),java语言的Checked Exception也许是个设计错误,反对者列举几点:

  • Checked Exception的假设是我们捕获了异常,然后恢复程序。但是,其实我们大多数情况下,根本不可能恢复。Checked exception的使用,已经大大偏离了最初的设计目的。(一般产品业务代码看到最多的自定义异常都是继承RuntimeException)
  • Checked Exception不兼容functional编程,如果你写过lambda/stream代码,相信深有体会。

比如,下面这段代码编译是通过的:

public class QuanHaoTest {

    public static void main(String[] args) {
        print(QuanHaoTest::out);
    }

    private static String out(String o) {
        System.out.println(o);
        return "execute success";
    }

    public static void print(Function<String, String> func) {
        String result = func.apply("ok");
        System.out.println(result);
    }

}

但如果out方法申明了检查异常:

很多开源项目,已经采纳了这种实践,比如Spring,Hibernate等,甚至反映在新的编程语言的设计中,比如Scala等。 当然,很多人觉得没有必要矫枉过正,因为确实有一些异常,比如和环境相关的IO,网络等,其实是存在可回复性的,而且Java已经通过业界的海量实践,证明了其结构高质量软件的能力。我们就不再进一步解读了。

分类:

后端

标签:

后端

作者介绍

犬豪
V1