关于JAVA 异常 基础知识/编码经验的一些总结
Posted 山河已无恙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于JAVA 异常 基础知识/编码经验的一些总结相关的知识,希望对你有一定的参考价值。
写在前面
- 温习一下毕业以来学习的东西。准备做成一个系列。所以对于每一部分技术点进行一个笔记整理。更多详见 java面试的一些总结
- 笔记主要是以网上开源的一本
《Java核心面试知识整理》
面试笔记为原型,结合工作中学习
的知识。《Effective Java》
、《编写高质量代码(改善Java程序的151个建议)》
这两本书为方向进行整理。 - 笔记立足
DevOps
。开发+运维+测试三个方向 ,面向对JAVA有一定了解
的小伙伴用于温习
,因为理论较多一点
。在不断更新,博文内容理解不足之处请小伙伴留言指正。
傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺
了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事
。--------王小波
一、JAVA 异常分类及处理
异常概述
程序中的错误
可分为语法
,逻辑
,运行时错误
。程序在运行时的错误
被称为异常
。
白话讲解
:如果已知某个方法不能按照正常的途径
完成任务,就可以通过另一种路径退出
方法。在这种情况下会抛出一个封装了错误信息
的对象。此时,这个方法会立刻退出同时不返回任何值
。另外,调用这个方法的其他代码也无法继续执行
,异常处理机制会将代码执行交给异常处理器。
Throwable
Throwable
是 Java
语言中所有错误或异常的超类。下一层分为 Error
和 Exception
如果面试被问到异常是如何定位到的异常位置的,你会怎样回答?:
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 错误导致的 IOException
、SQLException
。
如果面试官问受检异常和非受检异常的区别的时候,要怎么回答?
-
非检查型异常
RuntimeException
如 :NullPointerException
、ClassCastException
;RuntimeException
是那些可能在 Java 虚拟机正常运行期间
抛出的异常的超类。 如果出现RuntimeException
,那么一定是程序员的错误. -
检查异常
CheckedException
:一般是外部错误,这种异常都发生在编译阶段
,Java 编译器会强制程序去捕获此类异常,即会出现要求
你把这段可能出现异常的程序进行 try catch
,该类异常一般包括几个方面:- 试图在文件尾部读取数据
- 试图打开一个错误格式的 URL
- 试图根据给定的字符串查找 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
可以有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
语句,当内部的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)就越小,装载
这些类的时间开销也越少。最不重要的一点。
- 它使API更
不要直接重用
:Exception
、 RuntimeException
、 Throwable
或者Error
。对待"这些类要像对待抽象类
一样。你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常
的超类
。
- 抛出与
抽象对应的异常
- 每个方法抛出的所有异常都要建立
文档
,使用Javadoc
的@throws
标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws
关键字将未受检的异常包含在方法的声明中。如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档。 -
努力使失败保持原子性
,作为方法规范的一部分,产生的任何异常
都应该让对象保持在调用该方法之前的状态
。如果违反
这条规则, API文档就应该清楚地指明
对象将会处于什么样的状态. -
不要忽略异常
忽略一个异常很容易,空的catch块会
使异常达不到应有的目的.如果选择忽略异常, catch块中应该包含注释
,说明为什么可以这么做,并且变量
应该命名为ignored
:
以上是关于关于JAVA 异常 基础知识/编码经验的一些总结的主要内容,如果未能解决你的问题,请参考以下文章