Java异常梳理

定义

java中的异常提供了一种识别及响应错误情况的一致性机制,若有效地处理异常能使程序更加的健壮,且更易于调试。异常之所以是一种强大的调试手段,在于其回答了三个问题:什么出了错?在哪出的错?为什么出错?
异常的类型回答了“什么”被抛出(什么出了错),异常的堆栈信息回答了“在哪”抛出(在哪出的错),异常的函数调用信息回答了“为什么”会抛出(为什么出错),如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们。

异常分类

异常整体上可以分为以下3种类型:

  • 运行时异常:是RuntimeException类及其子类标识的异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,我们需要从程序逻辑的角度尽可能避免这类异常的发生。(运行时异常的特点是Java编译器不会检查它)
  • 检查性异常:正确的程序在运行中,很容易出现的、情理可容的异常状况。检查性异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,需要采取某种方式进行处理(捕获处理或者继续抛出)。(这些异常在编译时不能被简单地忽略,必须捕获处理或继续抛出)
  • 错误Error类型及子类是程序所无法处理的错误,表示运行的应用程序中的较严重问题。大多数错误与代码编写者执行的操作无关,而是表示代码运行时JVM(Java 虚拟机)出现的问题。如:当JVM不再有继续执行操作所需的内存资源时将出现OutOfMemoryError等。

具体使用

略。(本篇文章不打算介绍基本使用)

异常处理三原则

使用以下三个原则可以帮助我们在调试过程中最大限度地使用好异常机制。

  1. 具体明确
    当我们需要抛出一个异常时,应尽可能的使用能描述具体问题的异常子类,而不是new一个通用的基类(如错误写法:throw new Exception("test exception"); )。其目的是为了在捕获异常的时候,我们能根据异常的类型一眼就能分辨什么出了错,另外更重要的是捕获异常处理时,能将异常类型对应到不同的catch块,以便针对不同的异常做出不同的处理。
    比如,异常IOException更加特化的异常FileNotFoundExceptionEOFExceptionObjectStreamException,这些都是IOException的子类,每一种都描述了一类特定的I/O错误:分别是文件不存在,异常文件结尾和错误的序列化对象流。

  2. 提早抛出
    提早抛出异常的意思是,在可预见的异常前通过程序代码检查可能的异常(特别是运行时异常),要么通过if条件过滤即将抛出的异常使整个函数调用立即返回,要么构造一个更加特例的异常提前抛出。其目的是为了能更清晰的定义一些可能预知的异常、避免不必要的对象构造或资源占用,比如文件或网络连接,还能避开了资源操作所带来的清理动作。
    比如:

testException.readValueFromFile(null); //测试传入一个为null的文件名

public int readValueFromFile(String filename) {
    int size = 0;
    InputStream in = null;
    try {
        in = new FileInputStream(filename); //FileInputStream会抛出异常
    } catch (Exception e) {
        
    } finally {
        in.close();
    }
    return 0;
}

将会抛出一下异常:

java.lang.NullPointerException: Attempt to invoke virtual method 'char[] java.lang.String.toCharArray()' on a null object reference
at java.io.File.fixSlashes(File.java:183)
at java.io.File.<init>(File.java:130)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at com.android.test.demo.exception.TestException.readValueFromFile(TestException.java:22)
at com.android.test.demo.MainActivity.testException(MainActivity.java:56)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:41)

从异常堆栈信息看:是FileInputStream的构造函数中抛出了NullPointerException异常,而JDK API一般不会出错的,很可能是我们的调用逻辑有问题,但异常堆栈却不能清晰的看出到底是什么为空导致的空指针,假设我们回退堆栈和检查程序最终还是会发现是filename参数传了空导致的。
  如果我们在实例化FileInputStream之前做一次参数检查,若为空时提早抛出IllegalArgumentException,如下所示:

    if (filename == null){
         throw new IllegalArgumentException("filename is null");
    }

则会抛出这样的堆栈信息:

java.lang.IllegalArgumentException: filename is null
at com.android.test.demo.exception.TestException.readValueFromFile(TestException.java:23)
at com.android.test.demo.MainActivity.testException(MainActivity.java:56)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:41)

此时堆栈信息不会深入到FileInputStream中去了(一般情况不会是JDK的API出现异常),而是明确的告诉了我们三个问题:出了什么错(提供了非法参数值),为什么出错(文件名不能为空值),以及哪里出的错(readValueFromFile()函数)。

  1. 延迟捕获
    延迟捕获的意思是,当函数出现异常且在当前函数无法做出有效处理时,应当将其抛给函数的调用者去处理,以便调用者有机会通过不同的参数从异常中恢复出来。比如,在调用者第一次传入错误的参数导致异常后,调用者仍有机会(在catch{}中)再一次传入某个默认参数来使该函数调用成功。
      大多数人可能都会犯的一个错是,在程序有能力处理异常之前就捕获了它。Java编译器要求检查出的异常必须被捕获或抛出间接助长了这种行为,大家自然而然的做法就是立即将代码用try块包装起来,并使用catch捕获异常,以免编译器报错。如果当前函数有能力处理异常还好,不然只能仅仅打印一下异常堆栈信息,无法通过异常机制对函数的执行提供有效的帮助。

finally{}语句块的执行情况

一般的描述是:try{}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行,什么时候被执行,在return前还是后。会不会影响返回值?
先给结论:finally块的语句在trycatch中的return语句执行之后返回之前执行,且finally里的修改语句可能影响也可能不影响trycatchreturn已经确定的返回值(由返回值的传递类型决定:传值还是传地址),若finally里也有return语句则覆盖trycatch中的return语句直接返回。
测试1:

//测试调用
final int value = testException.testFinally1();
Log.d(TAG, "testFinally1 return size: " + value);

public int testFinally1() {
    int x = 1;
    try {
        ++x;
        Log.d(TAG, "try{} x: " + x);
        return returnSize(x);
    } finally { //finally块在retrun语句执行执行之后,返回之前执行
        ++x;
        Log.d(TAG, "finally{} x: " + x);
    }
}

private int returnSize(int size) {
    Log.d(TAG, "enter returnSize()");
    return size;
}

执行结果:

TestException( 1468): try{} x: 2
TestException( 1468): enter returnSize()
TestException( 1468): finally{} x: 3
TestException( 1468): testFinally1 return size: 2

可以看到函数returnSizefinally{}块之前执行了(finally{}return语句执行之后,返回之前执行),且finally{}++x没有生效(return的时候是复制了一份变量然后返回,所以之后finally操作的变量如果是基本类型的话不会影响返回值)

测试2:

//测试调用
Map<String, String> map = testException.testFinally2();
Log.d(TAG, "testFinally2 map: " + (map != null ? map.get("key") : "null"));

public Map<String, String> testFinally2() {
    Map<String, String> result = new HashMap<>();
    result.put("key", "start");
    try {
        result.put("key", "try");
        return result;
    } catch (Exception e) {
        result.put("key", "catch");
    } finally {
        result.put("key", "finally"); //此处生效
        result = null; //此处不生效
    }
    return result;
}

执行结果:

TestException( 1468): testFinally2 map: finally

从测试结果我们可以看到,finally{}的code生效了,这是因为返回值为引用类型,虽然在return的时候是复制了一份变量然后返回,但该变量指向的是同一个对象,因此这里的操作会反映到返回结果中;result = null;这句代码不生效的原因同测试1。

另外,finally里也有return语句时,则覆盖trycatch中的return语句直接返回。----原因也可以从上面两个测试例子中得出结论。

自定义异常

在开发过程中,自我认知范围内需要使用自定义异常的情况总结为以下两种:

  1. 自定义异常继承自某个相关异常,抛出自定义异常时,信息可以根据情况自定义,从而使得异常可以隐藏底层的异常中,使得信息更安全、也更加直观。因为自定义异常可以抛出我们自己想要抛出的信息,也可以通过抛出的信息区分异常发生的位置、根据异常名我们就可以知道哪里有异常,根据异常提示信息进行程序修改。比如空指针异常NullPointException,我们可以抛出信息为“xxx为空”定位异常位置,而不用输出堆栈信息。
    测试:
//自定义异常类
public class TestGHException extends RuntimeException {

    public TestGHException(Throwable cause) {
        super(cause);
    }

    public TestGHException(String message, Throwable cause, boolean enableSuppression,
        boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }


    public TestGHException(String message) {
        super(message);
    }

    public TestGHException(String message, Throwable cause) {
        super(message, cause);
    }
}

测试1(不使用自定义异常):

public void testGHException() {
    String value = "test";
    value = null;
    final String sub = value.substring(0, 1);
}

输出异常信息:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.substring(int, int)' on a null object reference
at com.android.test.demo.exception.TestException.testGHException(TestException.java:112)
at com.android.test.demo.MainActivity.testException(MainActivity.java:64)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:40)
at android.app.Activity.performCreate(Activity.java:6362)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1122)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2656)

测试2(使用自定义异常):

public void testGHException() {
    try {
        String value = "test";
        value = null;
        final String sub = value.substring(0, 1);
    } catch (NullPointerException e) {
        throw new TestGHException("string value == null", e);
    }
}

输出异常信息:

Caused by: com.android.test.demo.exception.TestGHException: string value == null
at com.android.test.demo.exception.TestException.testGHException(TestException.java:105)
at com.android.test.demo.MainActivity.testException(MainActivity.java:64)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:40)
at android.app.Activity.performCreate(Activity.java:6362)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1122)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2656)

结论:通过比较上面两个测试例子可以看出,自定义异常使得异常堆栈信息更加直观,一样就能看到出现问题的地方。

  1. 可以实现异常在受检异常和运行时异常之间互相转换。有时候引用的某些API抛出了运行时异常(由于不需要编译器检查,却又有可能异常),为了具体业务的需要,可以在该API上再包一层将这个运行时异常转换成受检异常,以便提醒调用者注意该异常,或者反之,简化繁琐的受检异常,典型的用例是jOOR中对反射接口的受检异常(ClassNotFoundException、NoSuchMethodException、NoSuchFieldException等)统一转化成ReflectException定义的运行时异常。

jOOR代码:

//ReflectException自定义异常
public class ReflectException extends RuntimeException {
}

//函数执行接口,
public Reflect call(String name, Object... args) throws ReflectException {
    Class<?>[] types = types(args);

    // Try invoking the "canonical" method, i.e. the one with exact
    // matching argument types
    try {
        Method method = exactMethod(name, types);
        return on(method, object, args);
    }

    // If there is no exact match, try to find a method that has a "similar"
    // signature if primitive argument types are converted to their wrappers
    catch (NoSuchMethodException e) {
        try {
            Method method = similarMethod(name, types);
            return on(method, object, args);
        } catch (NoSuchMethodException e1) {
            throw new ReflectException(e1); //此处将受检异常转换成了运行时异常
        }
    }
}

//调用时 ,省去了检查操作
public static boolean isSplitMode(Context context) {
    return Reflect.on("meizu.splitmode.FlymeSplitModeManager")
        .call("getInstance", context)
        .call("isSplitMode").get();
}

常见影响app的崩溃率指标的异常分析

准备在单独章节中持续更新。

总结

有经验的的开发人员都知道,调试程序的最大难点不在于修复bug,而在于从海量的代码中找出bug的藏身之处。因此,我们要做的就是在写代码过程中尽可能的暴露bug的可能出处。

在《Effective Java》中对异常的使用给出了以下指导原则:

  • 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
  • 优先使用标准的异常
  • 每个方法抛出的异常都要有文档
  • 保持异常的原子性
  • 不要在catch中忽略掉捕获到的异常*

参考文档

http://www.importnew.com/1701.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容