准备校招,关于JAVA 异常,面试看这篇就够了

网友投稿 725 2022-05-29

写在前面

温习一下毕业以来学习的东西。准备做成一个系列。所以对于每一部分技术点进行一个笔记整理。

笔记主要是以《Effective Java》、 《编写高质量代码(改善Java程序的151个建议)》这两本书为方向进行整理。

在不断更新,博文内容理解不足之处请小伙伴留言指正

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

一、JAVA 异常分类及处理

异常概述

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

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

Throwable

Throwable 是 Java语言中所有错误或异常的超类。下一层分为 Error 和 Exception

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

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

准备校招,关于JAVA 异常,面试看这篇就够了

简单看一下源码

...... * @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 错误导致的 IOException、SQLException。

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

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

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

试图在文件尾部读取数据

试图打开一个错误格式的 URL

试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

异常的处理方式

捕获异常

将可能出现的异常的代码放到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中出现异常无法回收物理资源,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{ //资源回收语句 }

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

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

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

如果面试被问到try-with-resources可以有catch和finally 吗,可以只有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块则检查外部的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有啥区别?要怎么回答?

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

throw new 对象名();

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

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

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

throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。

throws表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行throw则一定抛出了某种异常对象。

两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

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

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

[x] 一个方法被覆盖时,覆盖它的方法必须抛出相同的异常或异常的子类。如果父类抛出多个异常,则覆盖方法必须抛出那些异常的一个子集,不能抛出新异常。

[x] 提倡异常的封装,可以提供系统的友好性,提高系统的可维护性,对异常进行分类,解决Java异常机制缺陷。进行异常封装 。

class MyException extends Exception{ private List causes = new ArrayList<>(); public MyException(List causes){ this.causes.add((Throwable) causes); } public List getException(){ return causes; } public static void doStuff() throws MyException { List 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); } }

[x] 异常只为异常服务,只针对异常的情况才使用异常,异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。即异常块里不添加逻辑,添加逻辑会造成异常判断降低了系统性能,降低了代码可读性,隐藏了运行期可能产生的异常.

[x] 多使用异常,把性能放一边,异常是主逻辑的例外逻辑,比如 在马路上走路(主逻辑),突然开过一辆车,我要避让(受检异常,必须处理),继续走路,突然一架飞机从我头顶飞过(非受检异常),我可以选择走路(不捕捉),也可以选择职责其噪音污染(捕捉,主逻辑的补充处理),在继续走下去,突然一棵流星砸下类,这是没有选择,属于错误,不能做任何处理。

[x] 不要在构造函数中抛出异常,一个对象的创建要经过内存分配,静态代码初始化,构造函数执行等过程,一般不在构造函数中抛出异常,是程序员无法处理的,会加重上层代码的编写者的负担,后续代码不会执行,违背了里氏替换原则,父类能出现的地方子类就可以出现,而且将父类替换为子类也不会产生任何异常, java的构造函数允许子类的构造函数抛出更广泛的异常类(正好与类方法的异常机制相反:子类方法的异常类型必须为父类方法的子类型,覆写要求),当替换时,需要增加catch块,构造函数没有覆写的概念,只有构造函数之间的引用调用而已,同时子类构造函数扩展受限。

[x] 使用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()); } }

[x] API设计中,对可恢复的情况使用受检异常,对编程错误使用运行时异常,如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。用运行时异常来表明编程错误。你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接的或者间接的),

[x] 避免不必要地使用受检异常,受检异常可以提升程序的可读性;如果过度使用,将会使API使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个optional值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。

[x] 优先使用标准的异常,重用标准的异常有多个好处:

[x] 它使API更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致。最主要的好处。

[x] 对于用到这些API的程序而言,·它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。

[x] 异常类越少,意味着内存占用(footprint)就越小,装载这些类的时间开销也越少。最不重要的一点。

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

[x] 抛出与抽象对应的异常

[x] 每个方法抛出的所有异常都要建立文档,使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档。

[x] 努力使失败保持原子性,作为方法规范的一部分,产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API文档就应该清楚地指明对象将会处于什么样的状态.

[x] 不要忽略异常忽略一个异常很容易,空的catch块会使异常达不到应有的目的.如果选择忽略异常, catch块中应该包含注释,说明为什么可以这么做,并且变量应该命名为ignored:

Java JVM

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:maven基本使用-个人手稿版
下一篇:碰到Cannot find module了吗? 来看看require函数与NodeJS模块加载
相关文章