关于JAVA 异常 基础知识/编码经验的一些总结

Posted 山河已无恙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于JAVA 异常 基础知识/编码经验的一些总结相关的知识,希望对你有一定的参考价值。

写在前面


  • 温习一下毕业以来学习的东西。准备做成一个系列。所以对于每一部分技术点进行一个笔记整理。更多详见 java面试的一些总结
  • 笔记主要是以网上开源的一本《Java核心面试知识整理》面试笔记为原型,结合工作中学习的知识。《Effective Java》《编写高质量代码(改善Java程序的151个建议)》这两本书为方向进行整理。
  • 笔记立足DevOps。开发+运维+测试三个方向 ,面向对JAVA有一定了解的小伙伴用于温习,因为理论较多一点在不断更新,博文内容理解不足之处请小伙伴留言指正

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波


一、JAVA 异常分类及处理

异常概述

程序中的错误可分为语法逻辑运行时错误。程序在运行时的错误被称为异常

白话讲解:如果已知某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。

Throwable

ThrowableJava语言中所有错误或异常的超类。下一层分为 ErrorException

如果面试被问到异常是如何定位到的异常位置的,你会怎样回答?:

JVM在创建一个Throwable类及其子类时会把当前线程的栈信息记录下来,以便在输出异常时准确定位异常原因,在出现异常时,JVM会通过fillInStackTrace(填写执行堆栈跟踪。)方法记录栈帧的信息,然后生成一个Throwable对象,可以知道类间的调用顺序方法名称当前行号


简单看一下源码

 ......
 * @since JDK1.0
 */
public class Throwable implements Serializable {
   /**
     * A shared value for an empty stack.
     */
    //出现异常的栈记录
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
    ......
    private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
    ......
     //构造函数记录栈帧
     public Throwable() {
        fillInStackTrace();
    }
    .....
    //调用本地方法,抓取执行时的栈信息。
    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }
   //本地方法.抓取执行时的栈信息。
    private native Throwable fillInStackTrace(int dummy);
    ......
}    

Error

Error 类是指java 运行时系统的内部错误资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。Error中面试常考OOM类型的问题, 内存溢出、泄露之类。

如果面试被问起来OOM,你会怎样简单描述下?:

所谓OOM,即 java.lang.OutOfMemoryError:翻译成中文就内存用完了,当JVM因为没有足够的内存来为对象分配空间,并且 垃圾回收器 也已经 没有空间可回收时,就会抛出这个error。我知道的,造成OOM的有两种,内存溢出GC占用大量时间释放很小空间(GC overhead limit exceeded)。

  • 内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出,可选的解决办法是调整JVM参数,造成溢出原因也可能是因为内存泄漏,所谓内存泄漏,即申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
  • GC占用大量时间释放很小空间JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存频繁的进行内存回收(最起码已经进行了5次连续的垃圾回收),JVM就会曝出java.lang.OutOfMemoryError: GC overhead limit exceeded错误。即超出了GC开销限制。是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存。

如果面试官问OOM的发生在JVM那些内存区域:要怎么回答?:

OOM会发生在java虚拟机栈本地方法栈java 堆,以及运行时常量池中。

  • 对于java虚拟机栈来讲,当线程的请求栈深度大与虚拟机所允许的深度的时候。将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常.
  • 本地方法栈与虚拟机栈类似,区别是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。也会抛出StackOverflowError异常,OutOfMemoryError异常。
  • 故所周知,java堆是虚拟机所管理的内存最大的一块,是被所有线程共享的一块内存区域。几乎所有的实例以及数组都要在堆上分配空间。同时,java堆也是GC的主要区域,也被称为GC堆,如果在堆中没有内存完成实例分配,并且堆中也无法扩展,会抛出OutOfMenory异常。
  • 运行时常量池,是方法区的一部分,当常量池无法在申请内存时会抛出OutOfMethodError异常。java8之后改变了永久代的位置,存放到了直接内存,所以这部分的溢出算到直接内存溢出。

关于更多这方面的知识推荐小伙伴去看《深入理解Java虚拟机》这本书。关于上面讲的溢出实例代码,小伙伴可移步到博客java中OOM错误浅析(没有深入太多,一些粗浅的知识,可以温习用)

Exception

Exception又有两个分支,一个是运行时异常RuntimeException ,一个是检查异常CheckedException,如 I/O 错误导致的 IOExceptionSQLException

如果面试官问受检异常和非受检异常的区别的时候,要怎么回答?

  • 非检查型异常RuntimeException 如 :NullPointerExceptionClassCastExceptionRuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现RuntimeException,那么一定是程序员的错误.

  • 检查异常 CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

    1. 试图在文件尾部读取数据
    2. 试图打开一个错误格式的 URL
    3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

异常的处理方式

捕获异常

使用try……catch捕获异常。

将可能出现的异常的代码放到try语句中进行相应的隔离,如果遇到异常,程序会停止执行try块的代码,自动生成一个异常对象,该异常对象被提交给Java运行时环境(抛出异常),Java运行时环境收到异常对象时,跳到catch块中进行处理(即与catch中的异常对象一一匹配),匹配成功执行相应的catch块语句(捕获异常)。

如果面试官问异常捕获有几种方式,要怎么回答?

多异常单独捕获

try{
    //可能出现异常的代码
	}catch(异常类 异常对象){
	//该异常类的处理代码
	}catch(异常类 异常对象){
	}
	// 可以有多个catch语句。
	// 在执行多catch语句时,当异常对象被其中的一个catch语句捕获时, 剩下的catch语句将不会进行匹配。
	// 在安排顺序时,首先应该捕获一些子类异常,然后在捕获父类异常。即父类异常必须放到子类的后面,同一级不影响。

多异常统一捕获

try{
	//业务实现代码(可能发生异常)
	}catch(异常类1[|异常类2|异常类3……] 异常对象){
	//多异常捕获处理代码
	}
	//多异常捕获时,异常变量默认是常量,因此不能对该异常变量重新赋值。

所有异常对象都包含一下几个常用的方法用于访问异常信息

  • getMessage() 返回该异常的详细描述字符串(类型,性质)
  • printStackTrace() 将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s) 将该异常的跟踪栈信息出现在程序中的位置输出到指定的输出流
  • getStackTrace() 返回该异常的跟踪栈信息
  • toString() 返回异常发的类型与性质
使用try……catch……finally捕获异常。

防止try中出现异常无法回收物理资源finally块中放回收代码显示回收在try中打开的物理资源。不管try块中是否出现异常,也不管catch块中是否执行return语句,finally总会被执行(不被执行的情况:finally语句发生异常,使用了System.exit(0)语句,线程死亡,关闭cpu)。

如果面试官问异常捕获中 只有try和finally没有catch可以不,是语法错误吗?要怎么回答?

有catch的情况

try{
	//可能发生异常的代码
	}catch(异常类 异常类对象){
	//异常类的处理代码
	}finally{
	//资源回收语句
	}//try块时必须的,catch块与finally块时可选的,即try……finally或try……catch……finally(顺序不能颠倒);

没有catch的情况
try块时必须的,catch块与finally块时可选的,即try……finally或try……catch……finally(顺序不能颠倒);

try{
	//可能发生异常的代码
	}finally{
	//资源回收语句
	}
使用自动关闭资源的try-with-resources语句:

1.7之后可以使用自动关闭的资源的try语句,但是被释放的资源必须实现AutoCloseable接口, AutoCloseable对象的close()方法在退出已在资源规范头中声明对象的try -with-resources块时自动调用。 这种结构确保迅速释放,避免资源耗尽异常和可能发生的错误

如果面试被问到try-with-resources的使用有什么限制么?怎么回答?

前提是必须实现了AutoCloseable接口才可以


如果面试被问到try-with-resources可以有catchfinally 吗,可以只有try吗?怎么回答?

有catch和finally 的情况

 try (Connection connection = DriverManager.getConnection("")) {
            
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {

        }
 	

没有catch块,受检异常需要显示声明

  static String readFirstLineFromFile(String path) throws IOException {

        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        }
    }

try() 中()多条语句

 @Override
    public void run() {
        try(
         // 拿到输入流
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 拿到输出流
        PrintWriter out = new PrintWriter(socket.getOutputStream())
        ){
            while (true){
                String str = in.readLine();
                if (str == null){
                    continue;
                }
                System.out.println("接受原始消息"+str);
                // CONSUME表示消费一条消息
                if ("CONSUME".equals(str)){
                    String s = Broker.consume();
                    out.println(s);
                    out.flush();
                }else {
                    // 其他情况表示生产消息放到消息队列里面
                    Broker.produce(str);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
嵌套的try...catch语句:

如果面试被问到异常块可以嵌套么?怎么回答?

使用嵌套的try...catch语句,当内部的try块没有遇到匹配的catch块则检查外部的catch块。

	try {
            try {
                
            }catch (ArrayIndexOutOfBoundsException e){
                e.printStackTrace();
            }
            
        }catch (Exception e ){
            e.printStackTrace();
        }
    public static String fileToBufferedReader(InputStreamPeocess inputStreamPeocess, File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            try (InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream)) {
                try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
                    return inputStreamPeocess.peocess(bufferedReader);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }

抛出异常:

如果面试官问throw和throws有啥区别?要怎么回答?

throw抛出一个异常对象

thorw:使用throw抛出一个异常对象,当程序出现异常时,系统会自动抛出异常,也可以使用throw语句自动抛出一个异常。throw语句抛出的不是异常类,而是一个异常实例对象,且每次只能抛出一个异常实例对象。

throw new  对象名()
throws声明抛出的异常类

thorws:使用throws声明抛出异常。使用throws声明一个异常序列:当前方法不知道如何处理异常时,该异常应有上一级调用者进行处理,可在定义该方法时使用throws声明抛出异常。语法:

[访问符] <返回类型> 方法名([参数表列]throws 异常类A [,异常类B,异常类C,……]{
}.

throws只是抛出异常,还需要进行捕获。如果是Error或者RuntimeException或他们的子类,可以不用throws关键字来声明要抛出的异常,编译仍能通过,运行时系统抛出。

  • throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
  • throws表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行throw一定抛出了某种异常对象
  • 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

关于异常的一些其他编码经验:

如果面试问关于异常,平常开发中有哪些经验,要怎么回答?

  • 一个方法被覆盖时,覆盖它的方法必须抛出相同异常或异常的子类。如果父类抛出多个异常,则覆盖方法必须抛出那些异常的一个子集,不能抛出新异常。
  • 提倡异常的封装,可以提供系统的友好性,提高系统的可维护性,对异常进行分类,解决Java异常机制缺陷。进行异常封装 。
class MyException extends Exception{
    private List<Throwable> causes = new ArrayList<>();
    public MyException(List<? extends Throwable> causes){
        this.causes.add((Throwable) causes);
    }
    public List<Throwable> getException(){
        return causes;
    }
    public static void doStuff() throws MyException {
        List<Throwable> list = new ArrayList<>();
        try{
            //异常代码
        }catch(Exception e){
            list.add(e);
        }
        try{
            //异常代码
        }catch (Exception e){
            list.add(e);
        }
        if (list.size() > 0)
            throw new MyException(list);
    }
}

  • 异常只为异常服务,只针对异常的情况才使用异常,异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。即异常块不添加逻辑,添加逻辑会造成异常判断降低了系统性能,降低了代码可读性,隐藏了运行期可能产生的异常.
  • 多使用异常,把性能放一边,异常是主逻辑的例外逻辑,比如 在马路上走路(主逻辑),突然开过一辆车,我要避让(受检异常,必须处理),继续走路,突然一架飞机从我头顶飞过(非受检异常),我可以选择走路(不捕捉),也可以选择职责其噪音污染(捕捉,主逻辑的补充处理),在继续走下去,突然一棵流星砸下类,这是没有选择,属于错误,不能做任何处理。
  • 不要在构造函数中抛出异常,一个对象的创建要经过内存分配,静态代码初始化,构造函数执行等过程,一般不在构造函数中抛出异常,是程序员无法处理的,会加重上层代码的编写者的负担,后续代码不会执行,违背了里氏替换原则,父类能出现的地方子类就可以出现,而且将父类替换为子类也不会产生任何异常, java的构造函数允许子类的构造函数抛出更广泛异常类(正好与类方法的异常机制相反:子类方法的异常类型必须为父类方法的子类型,覆写要求),当替换时,需要增加catch块,构造函数没有覆写的概念,只有构造函数之间的引用调用而已,同时子类构造函数扩展受限
  • 使用Throwsable获得栈信息AOP编程可以亲松的控制一个方法调用那些类,也能控制那些方法允许别调用,一般来讲切面编程只能控制到方法级别,不能实现代码级别的植入(Weave),即不同类的不同方法参数相同调用相同方法返回不同的值。即要求被调用者具有识别调用者的能力,可以使用Throwable获得栈信息,然后鉴别调用者信息。
package com.liruilong.throwable_demo;

/**
 * @Classname Main
 * @Description TODO
 * @Date 2021/6/22 2:31
 * @Created Li Ruilong
 */
public class Main {
    public static void main(String[] args) {
      Invoker.m1();
      Invoker.m2();
    }
}
class Foo{
    public  static boolean m(){
        // 堆栈跟踪中的一个元素,由Throwable.getStackTrace()返回。 每个元素表示单个堆栈帧。
        // 堆栈顶部除堆栈之外的所有堆栈都表示方法调用。 堆栈顶部的帧表示生成堆栈跟踪的执行点。
        // 通常,这是创建与堆栈跟踪相对应的throwable的点。
        //取得当前栈信息
        StackTraceElement []  stackTraceElements = new Throwable().getStackTrace();
        //检查是否是m1方法调用
        for(StackTraceElement st: stackTraceElements){
            System.out.println(st);
            if(st.getMethodName().equals("m1"))
                return true;
        }
        return false;
        //throw new RuntimeException("除m1方法外,该方法不允许其他方法调用");
    }
}
class  Invoker{
    //该方法打印true
    public static void m1(){
        System.out.println(Foo.m());
    }
    //该方法打印false
    public  static void m2(){
        System.out.println(Foo.m());
    }
}

  • API设计中,对可恢复的情况使用受检异常,对编程错误使用运行时异常,如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。用运行时异常来表明编程错误。你实现的所有未受检抛出结构都应该是RuntimeException的子类(直接的或者间接的),
  • 避免不必要地使用受检异常,受检异常可以提升程序的可读性;如果过度使用,将会使API使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个optional值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
  • 优先使用标准的异常,重用标准的异常有多个好处:
    • 它使API更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致。最主要的好处。
    • 对于用到这些API的程序而言,·它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。
    • 异常类越少,意味着内存占用(footprint)就越小,装载这些类的时间开销也越少。最不重要的一点。

不要直接重用ExceptionRuntimeExceptionThrowable或者Error。对待"这些类要像对待抽象类一样。你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常超类

  • 抛出与抽象对应的异常
  • 每个方法抛出的所有异常都要建立文档,使用Javadoc@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档。
  • 努力使失败保持原子性,作为方法规范的一部分,产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API文档就应该清楚地指明对象将会处于什么样的状态.
  • 不要忽略异常忽略一个异常很容易,空的catch块会使异常达不到应有的目的.如果选择忽略异常, catch块中应该包含注释,说明为什么可以这么做,并且变量应该命名为ignored:

以上是关于关于JAVA 异常 基础知识/编码经验的一些总结的主要内容,如果未能解决你的问题,请参考以下文章

关于JAVA 反射 基础知识/编码经验的一些总结

关于JAVA 反射 基础知识/编码经验的一些总结

Java 异常处理的误区和经验总结

Java 异常处理的误区和经验总结

关于JVM内存垃圾回收性能调优总结篇

JAVA异常知识点总结