Java异常体系简析
最近在阅读《Java编程思想》的时候看到了书中对异常的描述,结合自己阅读源码经历,谈谈自己对异常的理解。首先记住下面两句话:
除非你能解决(或必须要处理)这个异常,否则不要捕获它,如果打算记录错误消息,那么别忘了把它再抛出去。
异常既代表一种错误,又可以代表一个消息。
一、为什么会有异常
这个问题其实不难理解,如果一切都按我们设计好的进行,那么一般(不一般的情况是我们设计的就是有缺陷的)是不会出现异常的,比如说一个除法操作:
public int div(int x,int y){
return x/y;
}
当然我们设计的是除数不能为0,我们也在方法名上添加了注释,输出不能为0,如果用户按照我们的要求使用这个方法,当然不会有异常产生。可是很多时候,用户不一定阅读我们的注释,或者说,输入的数据不是用户主动指定的,而是程序计算的中间结果,这个时候就会导致除数为0的情况出现。
现在异常情况出现了,程序应该怎么办呢,直接挂掉肯定是不行的,但是程序确实不能自己处理这种突发情况,所以得想办法把这种情况告诉用户,让用户自己来决定,也就是说程序需要把遇到的这种异常情况包装一下发送出去,由用户来决定如何处理。
异常表示着一种信息。熟悉EOFException的程序员一般都会了解,这个异常,表示信息的成分大于表示出现了异常,不熟悉的参照我之前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107084.html。当这种情形下的异常(包括用户自定义的大部分异常都属于此类)出现时,是不需要解决的。
二、Java异常的分类
在继续讲解下面部分之前,还是有必要了解下Java的异常分类的,通过Java API可以看到如下继承关系:
简单介绍一点:
- Throwable是所有异常的父类
- Error表示很严重的问题发生了,可以捕获但是不要捕获,因为捕获了也解决不了,这个不是由程序产出的,底层出现问题就让他它挂了吧。
三、异常的处理的理解
再把一开始说的那句话重复一遍,除非你能解决这个异常,否则不要捕获它,如果打算记录错误消息,那么别忘了把它再抛出去。不过说真的,一个异常既然产生了,基本都是不能解决的,因为我们的程序不能倒退到出现异常的代码,更不能在相同输入(不能改变输入,不然结果还有什么用),相同代码(不能动态改变原有代码)的情况下来来让它不再出现异常,不然同一段代码,在同一个输入的情况下有两种不同的结果,谁还敢用呢?
除非我们的程序需要依赖外部条件,而由外部条件导致的异常,我们可以改变外部条件使之满足程序要求,不过这种情况基本都可以在程序执行前检测出来。
3.1 怎么才算解决异常
举两个简单的例子方便理解下,第一个是关于Socket的,具体Socket的知识可以参考我之前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107785.html。
3.1.1 重复尝试解决偶发问题
在Socket建立连接以后,我们可以通过Socket发送消息,高效的Socket利用方法是建立一个连接来持续使用,可是在这种情况下,有一个需要注意的问题,那就是我在每次发送消息的时候,要不要检测Socket是否还在连接中,我的在上面博客中介绍了,不需要。伪代码如下:
//有一个连接中的socket
Socket socket=...
//要发送的数据
String data="";
try{
socket.write(data);
}catch (Excetption e){
//打印日志,并重连Socket
socket=new Socket(host,port);
socket.write(data);
}
可以看到,假如当前连接不可用(长时间不用被服务器主动断开,或者网络抖动导致的断开),那么我们捕获这个异常,然后重新建立一个连接来发送。这是最基本的解决方法,再高级一点的就是设置一个重复次数,当出现异常的时候重复发送指定的次数。
如果我们仔细想想,这个连接异常我们没有真正的解决它,而是通过又新建了一个连接来处理的,我们解决的不是这个异常,而是发送数据出现了问题,我们解决的是发送数据没有成功这个问题。
同样的,重复尝试解决的偶发问题,这个偶发也是外部的条件导致的偶发,而不是程序自身问题。
3.1.2 不想看到错误堆栈
一般的Web三层架构,action,server,Dao,如果出现异常后,再不满足上面解决条件的情况下,如果都不捕获异常,那么用户将会看到一个500页面,附带着堆栈信息,这种事不友好的表现方式,这种情况下,我们就需要在action层,用一个最大的try catch包住一个个方法,当出现异常的时候跳转到错误页面。
public String method(String param){
try {
//逻辑处理
} catch (Exception e) {
e.printStackTrace();
//跳转到错误页面
}
}
实际上,我们没有解决异常,我们只是解决了异常导致的问题,异常本身还在那,真正的解决方法就是程序员解决bug然后重新上线。
这种也算另类的解决,迫不得已不得不这么做,实际上异常是被吞掉了,吞掉前留下了一点点信息。
3.2 我们应该怎么做
首要条件还是那句话,如果不能解决到出现异常的情况,那就不要捕获它,更不要吞掉他。
当然有的时候你会打算记录异常的日志,但是最开始也说过,异常也代表一个消息,就像IndexOutOfBoundsException、IOException本身的名字已经可以表明异常的大部分信息,也就是说通过异常堆栈基本就能得到关于异常部分的信息,但是有些异常堆栈没有的是什么呢,那就是发生异常条件时的外部信息。
当然在抛出异常的时候,虚拟机本身会尽可能的打印出直接导致异常产生的输入,可是当我们还想获取额外的环境信息的时候,我们就需要捕获异常,然后打印出来。
就像简单的除0异常,以及字符串转数字异常,本身异常堆栈就会提供基本的信息,但是如果我们在一个用户交互的环境下,假如我们想要知道是哪个用户的输入导致了异常的产生,这个时候系统产生的异常堆栈信息就不能满足我们的要求了,而这个信息在当前类的一个字段中,这时候我们就要主动捕获然后打印出我们想要的。
四、异常的处理
现在就这各种实例来说明异常怎么处理。
4.1 对认为一定不会出现的异常
假如说你写了一个工具类,用于字符串和字节数组的UTF-8的转换,假如如下:
package yiwangzhibujian.util; import java.io.UnsupportedEncodingException; public class Utils {
public static String utf8(byte[] bytes) throws UnsupportedEncodingException{
return new String(bytes,"UTF-8");
} public static byte[] utf8(String str) throws UnsupportedEncodingException{
return str.getBytes("UTF-8");
}
}
那么用你工具类的人会头疼死,明明不会有错误的,要么抛出这个异常,要么捕获,实际上使用者根本不能解决这个异常。
所以有的人可能这么做,他想既然这个异常一定不可能出现(本质上jvm一定能解析UTF-8的编码,如果不能解析jvm也就不需要继续运行了),那么我就吞了它,什么都不做:
package yiwangzhibujian.util; import java.io.UnsupportedEncodingException; public class Utils {
public static String utf8(byte[] bytes){
try {
return new String(bytes,"UTF-8");
} catch (UnsupportedEncodingException e) {
}
return null;
} public static byte[] utf8(String str){
try {
return str.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
}
这么做的人也有,不过这么做的人也分为两种,一种是catch内什么都不做,还有一种是catch内把异常信息打印出来,这两种做法我比较倾向于后面那种,因为要考虑以下条件。
你认为jvm一定能解析UTF-8,我不反对,可是你能保证你没有拼错UTF-8吗,假如你写成UFO-8呢?
public static String utf8(byte[] bytes){
try {
return new String(bytes,"UFO-8");
} catch (UnsupportedEncodingException e) {
}
return null;
}
那么调用你的方法不仅没有错误提示,还导致返回了错误的结果,并导致后续一系列问题的产生,最致命的是 ,我们根部不知道错误的根源在哪。
再举一个对象克隆的例子。
package yiwangzhibujian.util; public class CloneTest {
public static void main(String[] args) {
Dog d1=new Dog("zhuzhuxia",26);
Dog d2=d1.clone();//此处要么捕获要么抛出
System.out.println(d1);
System.out.println(d2);
}
}
class Dog{
public String name;
public int age;
public Dog(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
可以看到用户调用你的对象的克隆方法是不是很痛苦,你既然提供给我克隆方法,就一定要能用,如果不能用,那么拿回去重写吧,我不会给你擦屁股的。所以我们就会这么做:
package yiwangzhibujian.util; public class CloneTest {
public static void main(String[] args) {
Dog d1=new Dog("zhuzhuxia",26);
Dog d2=d1.clone();//此处要么捕获要么抛出
System.out.println(d1);
System.out.println(d2);
}
}
class Dog{
public String name;
public int age;
public Dog(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
protected Dog clone() {
try {
return (Dog) super.clone();
} catch (Exception e) {
e.printStackTrace();//不要省
}
return null;
}
}
如果你运行上面的代码的话,那么就会抛出异常,因为我们的类没有实现Cloneable接口,原因就是忘了写,这在测试运行的首次就会发现并纠正。
java.lang.CloneNotSupportedException: yiwangzhibujian.util.Dog
at java.lang.Object.clone(Native Method)
at yiwangzhibujian.util.Dog.clone(CloneTest.java:22)
at yiwangzhibujian.util.CloneTest.main(CloneTest.java:6)
yiwangzhibujian.util.Dog@2a139a55
null
所以,我可以假定这种情况下不会出异常,但是我们不能保证我们没有犯最基本的错误,所以错误堆栈还是不能省的。
我们来看一下jdk8中的HashMap关于克隆的处理:
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
是不是不会抛必须捕获的异常,它还做了更高级的事,那就是我抛一个ERROR,一般我们的程序都是捕获Exception,不会捕捉这个异常,这个异常会一直向上传播。
那么打印异常堆栈和抛出ERROR哪种更好呢,我的建议是抛出ERROR:
- 能出现这种情况也就代表jvm出现了问题,或许其他基本功能也出现了问题,应该立即停掉重启并解决问题,不然数据都有可能出现错误。
- 如果打印堆栈信息,那么下次调用还是会出错,不如直接抛ERROR,如果上层没有做具体的应对jvm应该会停止。
4.2 对假定不应该出现的异常
我们再拿上面的字符串,字节数组例子来说明,我们对它进行了升级,下面是不完整代码:
public static String byteToStr(byte[] bytes, String charsetName) {
return new String(bytes, charsetName);
}
应该怎么做,抛异常?捕获异常打印日志?两种做法都不好:
- 如果抛异常:那么使用你工具类的人依然很头疼,他必须在每次调用你方法的时候做处理,要么抛要么捕获,而他在想我明明传入一个UTF-8,非得给我抛异常,难用死了。
- 如果捕获打印日志:这个更不可取,如果用户输错了编码类型,那么你将不能给出任何信息给调用者(打印日志只能事后找错),用户认为写的没错而你也给出了返回值,这也会导致一系列错误的产生。
这种情况下应该怎么做呢,比较推荐的做法就是包装成运行时异常抛出:
public static String byteToStr(byte[] bytes, String charsetName) {
try {
return new String(bytes, charsetName);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
这么做就解决了上面的两个问题。
4.3 对假定一定出异常的情况
你的代码一定会出异常,那你还是拿回去重写吧。除非你不想让别人调用你的方法,比如说不可变容器的操作类方法都将抛出异常。
五、异常的一些特殊情况
5.1 防止异常丢失
在你不主动吞并异常的情况下,异常是不会丢失的,但是有一种特殊情形需要注意,那就是finally中有return的情况(代码参照Java编程思想):
public static void ExceptionSilencer(){
try {
throw new RuntimeException();
} finally {
return;
}
}
这种情况下,异常就会丢了,完完全全消失不见了,所以要避免这么使用,避免finally中使用return。
5.2 线程中ThreadDeath异常
这个异常是归于ERROR级别的,Java api也对此有相应介绍:
The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it.
就是说ThreadDeath本身是一个普通的异常,这个异常出现应该导致线程死亡,但是不把它归于Exception的原因就是,jdk的开发者也料到Java程序员最喜欢try catch异常然后吞掉了,这样将会导致本该死亡的线程继续运行下去,这是不应该的。而且当这个异常出现时会终结线程,但是不会打印出任何异常堆栈信息。
这个异常比较少见,Thread的stop方法,会产生这个异常。
如果你的线程经常莫名其妙的消失,而没有任何相关日志,你可以尝试捕获这个异常,但是记住,打印完相关日志再把它重新抛出去。
六、Java编程思想中关于总结的解读
下面摘自Java编程思想的异常使用指南,特别好,一定要深入理解一下:
- 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层
- 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层
- 终止程序
- 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
- 让类库和程序更安全。
下面依次说下我的想法。
- 第1条:上面章节已经介绍了,此处不再说明
- 第2条:上面也介绍过,就是外部条件导致的,可以重复执行可能正常的代码
- 第3条:这种情况实质上也是吞并异常,比如说网络爬虫,当遇到死链接的时候,可能会抛出连接异常等,此时抛弃这个连接也是可以的,这个误差可以接收
- 第4条:有的程序员会这么设计,当出现用户输出错误数据导致异常的时候,就用一个默认的值来代替,我不喜欢这么做,我会直接抛异常让使用者去更改,如果非要这么做一定要打印好相关日志
- 第5条:这种情况看需求,如果要求要么全部成功,要么都不做,那么就不适合这种情况
- 第6条:同上,但是我不太理解这个
- 第7条:这个就不要了吧,出现一个异常程序就挂了,那也太脆了,不过当程序在正常启动过程中,如果出现异常就直接挂掉还是合理的,让用户修改外部条件保障启动没有问题,比如说用户指定的配置文件不存在(或许他写错了路径),那么不要使用默认配置,程序直接挂掉就可以了,不然会给用户一种按照他的配置成功启动的错觉。
- 第8条:这个和上面说到的用运行时异常来包裹捕获异常一个性质。
- 第9条:这个是终极目标,考虑所有的情况,把异常消灭在萌芽中,过于理想了。一般越安全越健壮的程序考虑的异常条件就越多。一般都会在使用前做各种判断,条件是否满足,输入是否正确等。
以上就是我对异常的理解,希望可以帮助到有需要的人,如果你能认真看完我相信你会有收获的,如果错误请指出,禁止转载。
Java异常体系简析的更多相关文章
- Java 异常体系(美团面试)
Java把异常作为一种类,当做对象来处理.所有异常类的基类是Throwable类,两大子类分别是Error和Exception. 系统错误由Java虚拟机抛出,用Error类表示.Error类描述的是 ...
- Unity5中新的Shader体系简析
一.Unity5中新的Shader体系简析 Unity5和之前的书写模式有了一定的改变.Unity5时代的Shader Reference官方文档也进一步地变得丰满. 主要需要了解到的是,在原来的Un ...
- java(异常体系及权限修饰符)
java异常体系 异常的体系: 异常体系: --------| Throwable 所有错误或者异常的父类 --------------| Error(错误) --------------| Exce ...
- Java异常体系概述
Java的异常体系结构 Java异常体系的根类是 Throwable, 所以当写在java代码中写throw抛出异常时,后面跟的对象必然是Throwable或其子类的对象. 其中Exception异常 ...
- JAVA异常体系
1.异常体系 ----|Throwable 所有错误或异常的父类 --------|Error(错误) --------|Exception(异常)一般能通过代码处理 ------------|运行时 ...
- Java异常处理-----java异常体系
再三思考后还是决定贴图,csdn的格式,我是真玩不转,对不起了各位,继续将就吧. 错误原因:内存溢出.需要的内存已经超出了java虚拟机管理的内存范围. 错误原因:找不到类文件. 错误(Error): ...
- Java基础系列5:深入理解Java异常体系
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 前言: Java的基 ...
- Java——深入理解Java异常体系
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 前言: Java的基 ...
- Java异常体系及分类
上图是基本的java异常体系结构. 主要分为2大类:Error和Exception 1.Error:描述了Java运行系统中的内部错误以及资源耗尽的情形.应用程序不应该抛出这种类型的对象,一般是由虚拟 ...
随机推荐
- html学习笔记 - 列表
<!-- 无序列表 --> <ul type = square> <li><a href="https://www.baidu.com"& ...
- 020 <one-to-one>、<many-to-one>单端关联上的lazy(懒加载)属性
<one-to-one>.<many-to-one>单端关联上,可以取值:false/proxy/noproxy(false/代理/不代理) 实例一:所有lazy属性默认(支持 ...
- 5.Lock接口及其实现ReentrantLock
jdk1.7.0_79 在java.util.concurrent.locks这个包中定义了和synchronized不一样的锁,重入锁——ReentrantLock,读写锁——ReadWriteLo ...
- 利用R语言进行交互数据可视化(转)
上周在中国R语言大会北京会场上,给大家分享了如何利用R语言交互数据可视化.现场同学对这块内容颇有兴趣,故今天把一些常用的交互可视化的R包搬出来与大家分享. rCharts包 说起R语言的交互包,第一个 ...
- C#基础篇--静态成员、抽象成员、接口
1.静态成员: 静态成员(static).静态类与实例成员.类: 静态成员属于类所有,非静态成员属于类的实例所有. 静态成员不能标记为 Virtual,Abstract,Override,也就是说静态 ...
- nested exception is java.sql.SQLException: Cannot convert value '0000-00-00 00:00:00' from column 14 to TIMESTAMP.
无法将"0000-00-00 00:00:00"转换为TIMESTAMP 2017-05-08 00:56:59 [ERROR] - cn.kee.core.dao.impl.Ge ...
- 乐视开放平台技术架构-servlet和spring mvc篇
在乐视风口浪尖的时候,敢于站出来说我是乐视的而不怕被打脸的,也就是我了.就算我以后不在乐视了,提起来在乐视工作过,我也还是挺骄傲的.因为这是一个有理想,敢拼敢干的公司.想起复仇者联盟里Fury指挥官的 ...
- 如何动态加载js文件,$.getScript()方法的使用
有时候我们需要动态在页面中加载js文件,jquery封装了getScript()方法,不用自己再创建标签了. 写法: $.getScript("name.js",function( ...
- socket获取百度页面
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import jav ...
- 刨根究底字符编码之十三——UTF-16编码方式
UTF-16编码方式 1. UTF-16编码方式源于UCS-2(Universal Character Set coded in 2 octets.2-byte Universal Character ...