Java异常详解

1 异常简介

程序运行时,发生的不被期望的事件,它阻止了程序按照预期的逻辑正常执行,这就是异常。

异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?

Java提供了更加优秀的解决办法:异常处理机制

异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

Java中的异常可以是函数中的语句执行时引发的,也可以是通过 throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。

2 异常的分类

Java语言中,Throwable类是异常的顶层父类,所有的异常类都是它的子类。

2.1 异常结构图

Java中的异常结构图如下:
在这里插入图片描述
Throwable下层被分为ErrorException两类。

2.2 ErrorException

(1)Error类及其子类,代表了JVM系统的内部错误和资源耗尽错误。

  • 应用程序不应该抛出这种错误,因为一旦出现了这样的内部错误,除了通知用户,并尽力使程序安全地终止之外,别无他法。
  • Error这种情况出现的很少。

(2)Exception及其子类是异常处理的核心,需要重点关注。

  • Exception又被划分为两个分支,一个分支派生于RuntimeEception,另一个分支包含了其他异常。
  • 划分两个分支的规则是:由程序本身逻辑错误导致的异常属于RuntimeException;而程序逻辑本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

派生于RuntimeException的异常包含下面几种情况:

  • 错误的类型转换

  • 数组访问越界

  • 访问null指针

不派生于RuntimeException的异常包括:

  • 试图在文件尾部后面读取数据
  • 试图打开一个不存在的文件
  • 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在

2.3 非受查(unchecked)异常、受查(checked)异常

需要明确的是:受查、非受查是对于javac来说的,这样就很好理解和区分了。

(1)非受查异常(unchecked exception)

  • 派生于Error类或者派生于RuntimeException类的所有异常称为非受查异常。
  • javac在编译的时候,不会提示和发现这样的异常。
  • 如果愿意,可以编写代码(如使用try...catch...finally)处理这样的异常,也可以不处理。

(2)受查异常(checked exception)

  • 除了派生于Error类或者派生于RuntimeException类的所有异常称为受查异常。
  • 编译器将核查是否为所有的受查异常提供了异常处理器。
  • 强制要求为这样的异常做预备处理工作(使用try...catch...finally或者throws)。

注意:一个方法必须在方法签名上面通过throws声明所有可能抛出的受查异常,对于ErrorRuntimeException异常不要通过throws声明。

3 如何抛出异常

在程序中,我们通过使用throw new来生成并抛出一个异常对象。

3.1 抛出Java中定义的异常

Java语言中已经定义了很多有用的标准异常类,我们在抛出这样的异常时候,可以:

  • 找到一个合适的异常类

  • 创建这个异常类的一个对象

  • 将创建的对象抛出

注意:一旦方法抛出了异常,这个方法就不可能返回到调用它的位置。也就是说,不必为了返回的默认值或错误代码担忧。

例如,在我们编写一个读取文件内容的方法时候,还没读完文件就结束了,我们应该抛出EOFException异常:

public String readData(Scanner in) throws EOFException {
    ...
    while (...) {
        if (!in.hasNext()) {
            if (n < len) {
                throw new EOFException();
            }
        }
        ...
    }
    return s;
}

3.2 抛出自定义异常

在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的异常问题。在这种情况下,我们可以创建自定义的异常类来描述。我们只需要:

  • 定义一个派生于Exception的类,或者派生于Exception子类的类。
  • 在程序中创建并抛出自定义的这个类的实例。

例如,自定义一个FileFormatException

class FileFormatException extends IOException {
    
    public FileFormatException() {}
    
    public FileFormatException(String message) {
        super(message);
    }
}

现在,我们可以在程序中抛出自定义的异常了:

public String readData(Scanner in) throws EOFException {
    ...
    while (...) {
        if (!in.hasNext()) {
            if (n < len) {
                throw new FileFormatException("文件未读完,异常结束...");
            }
        }
        ...
    }
    return s;
}

按照国际惯例,自定义的异常应该总是包含如下的构造函数

  • 一个无参构造函数
  • 一个带有 String 参数的构造函数,并传递给父类的构造函数
  • 一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数
  • 一个带有 Throwable 参数的构造函数,并传递给父类的构造函数

4 如何捕获异常

如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈信息。

4.1 使用try...catch...finally语句块捕获异常

要想捕获异常,必须设置try/catch语句块。形式如下:

try {
    // 处理业务逻辑...
} catch (SomeException e) {
    // 处理异常
} finally {
    // 无论如何都会执行的代码
}

说明:

  • 如果在try语句块中的任何代码抛出了一个catch语句块中说明的异常,那么:

    • 程序将跳过try语句块中剩余的代码
    • 程序将会跳转到catch语句中执行这个异常的处理器代码
  • 如果在try语句块中的代码没有抛出任何异常,那么程序将会跳过所有catch语句块。

  • 如果try语句块中抛出了任何一个在catch语句块中没有说明的异常,那么这个方法将会立刻退出。

  • finally语句块是可选的,无论异常是否发生都会执行,通常用来做一些清理工作,比如流的关闭,数据库连接的关闭等。

  • catch语句块处理完异常之后,程序不会回到抛出异常的那行代码,而是接着执行catch语句块后边的程序代码。

4.2 使用throws将异常传递给调用者

对于受查异常(checked exception),通常最好的方法就是什么也不做,将异常传递给方法的调用者,让调用者来操心如何处理。

例如,读取文件可能抛出IOException异常,这是一个受查异常,可以通过在方法签名上使用throws将异常传递给调用者,而不是在内部捕获:

public void read(String filename) throws IOException {
    InputStream in = new FileInputStream(filename);
    int b;
    while ((b = in.read()) != -1) {
        // 处理读取的内容...
    }
}

4.3 捕获多个异常

在一个try/catch语句块中可以捕获多个异常类型,并针对不同类型的异常做出不同的处理。可以按照下面的方式为每个异常类型使用一个单独的catch子句:

try {
    // 可能会抛出不同类型的异常
} catch (FileNotFoundException e) {
    // 处理FileNotFoundException异常
} catch (UnknownHostException e) {
    // 处理UnknownHostException异常
} catch (IOException e) {
    // 处理IOException异常
}

如果多个异常的处理逻辑是一样的,那么可以合并在一个catch语句中:

try {
    // 可能会抛出不同类型的异常
} catch (FileNotFoundException | UnknownHostException e) {
    // 处理FileNotFoundException与UnknownHostException异常
} catch (IOException e) {
    // 处理IOException异常
}

捕获多个异常时,异常变量e隐含为final变量,不可改变。

4.4 通过throw再次抛出异常与异常链

catch语句中,还可以通过throw再次抛出异常,这样做的目的是改变异常的类型。

(1)可以将原始异常包装,并抛出新的异常:

try {
    // 连接数据库
} catch (SQLException e) {
    Throwable se = new ServletException("database error.");
    se.initCause(e);
    throw se;
}

当捕获ServletException的时候,就可以通过下边的语句获得原始异常信息:

Throwable s = se.getCause();

强烈建议使用这种包装技术,这样可以让用户抛出子系统中的高级异常,但又不会丢失原始异常细节。

(2)如果在一个方法中发生了受查异常(checked exception),而不允许抛出它,那么使用包装技术就十分合适。我们可以捕获这个异常,并将它包装成一个运行时异常。

(3)有时候我们可能只是想记录一下异常信息(打日志),也可以在catch语句中捕获,打日志,然后再抛出这个异常:

try {
    // 连接数据库
} catch (SQLException e) {
    log.error("database error.", e);
    throw e;
}

5 带资源的try语句

对于以下的代码模式:

// 打开资源...
try {
    // 处理业务逻辑
} finally {
    // 关闭资源
}

Java SE 7之后,只要资源实现了AutoCloseable接口,就可以通过带资源的try语句(try-with-resources)简化代码编写:

try (Resource res = ...) {
    // 处理业务逻辑...
}

try语句块推出的时候,会自动调用res.close()方法。

下面给出一个典型的例子,读取文件中所有的单词:

try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8")) {
    
    while (in.hasNext()) {
        System.out.println(in.next());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

这块代码正常退出时,或者抛出异常时,最终都会调用in.close()方法,就像使用了finally语句块一样。

还可以一次指定多个资源,如下:

try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
     PrintWriter out = new PrintWriter("out.txt")) {
    
    while (in.hasNext()) {
        out.println(in.next().toUpperCase());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

不论这个块如何退出,inout都会被关闭。如果通过finally语句块变成,那么就需要编写两个嵌套的try/finally语句。

6 使用异常机制的一些技巧

  1. 异常处理不能代替简单的测试判断

    例如,对于执行一个退栈操作来说,我们应该首先对栈进行判空处理:

    if (!stack.empty()) {
        stack.pop();
    }
    

    不能因为有异常机制,而省略掉这层判断。下面的代码不是一个好的写法:

    try {
        stack.pop();
    } catch (EmptyStackException e) {
        
    }
    
  2. 不要过分地细化异常

    很多程序员喜欢将每一条语句封装在一个独立的try/catch语句中,例如:

    PrintStream out;
    Stack s;
    for (i = 0; i < 100; i++) {
        try {
            n = s.pop();
        } catch (EmptyStackException e) {
            // 栈空
        }
        
        try {
            out.writeInt(n);
        } catch (IOException e) {
            // 写文件发生错误
        }
    }
    

    对于这段代码逻辑来说,从栈中取出元素和写入流中,从业务上来说是一套操作,如果从栈中取元素发生了异常,那就没必要写流了,但是这样分开try/catch导致两部分操作不能互相感知。

    我们可以将它们合并到一个try/catch中,这样只要任何一步发生异常,就退出就好了:

    PrintStream out;
    Stack s;
    for (i = 0; i < 100; i++) {
        try {
            n = s.pop();
            out.writeInt(n);
        } catch (EmptyStackException e) {
            // 栈空
        } catch (IOException e) {
            // 写文件发生错误
        }
    }
    
  3. 合理利用异常的层次结构

    • 不要一味的只抛出RuntimeException异常。应该寻找更加适当的子类或者自定义异常。

    • 不要只捕获Throwable异常,否则,程序代码更难读和维护。

  4. 子类重写带有throws声明的方法时,其throws声明的异常必须在父类异常的可控范围内

    • 用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法,这是为了支持多态。

    • 父类方法throws的是2个异常,子类就不能throws 3个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。

  5. 多线程中,每个线程中的异常是相互独立的

    • Java支持多线程编程。

    • 每一个线程都是一个独立的执行流,独立的函数调用栈。

    • 如果程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止;如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

    • Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

7 finally块和return

注意:在try块中即便有returnbreakcontinue等改变执行流的语句,finally块也会执行。

public static void main(String[] args) {
    int re = bar();
    System.out.println(re);
}

private static int bar() {
    try{
        return 5;
    } finally {
        System.out.println("finally语句块");
    }
}

/* 输出:
finally语句块
5
*/

因此,可能会出现一下几个问题:

7.1 finally中的return会覆盖try或者catch中的return返回值

	public static void main(String[] args) {
        int result;

        result  =  foo();
        System.out.println(result);     // 2

        result = bar();
        System.out.println(result);    // 4
    }


    private static int foo() {
        try {
            int a = 5 / 0;
        } catch (Exception e){
            return 1;
        } finally {
            return 2;
        }

    }

    private static int bar() {
        try {
            return 3;
        } finally {
            return 4;
        }
    }

7.2 finally中的return会抑制(消灭)前面try或者catch块中的异常

	public static void main(String[] args) {
        int result;
        
        try {
            result = foo();
            System.out.println(result);            // 输出100
        } catch (Exception e) {
            System.out.println(e.getMessage());    // 没有捕获到异常
        }


        try {
            result  = bar();
            System.out.println(result);            // 输出100
        } catch (Exception e) {
            System.out.println(e.getMessage());    // 没有捕获到异常
        }
    }

    // catch中的异常被抑制
    private static int foo() {
        try {
            int a = 5 / 0;
            return 1;
        } catch (ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中使用了return");
        } finally {
            return 100;
        }
    }

    // try中的异常被抑制
    private static int bar() {
        try {
            int a = 5 / 0;
            return 1;
        } finally {
            return 100;
        }
    }

7.3 finally中的异常会覆盖(消灭)前面try或者catch中的异常

	public static void main(String[] args) {
        int result;

        try {
            result = foo();
            System.out.println(result);
        } catch (Exception e) {
            System.out.println(e.getMessage());    // 输出:我是finaly中的Exception
        }


        try {
            result  = bar();
            System.out.println(result);
        } catch (Exception e) {
            System.out.println(e.getMessage());    // 输出:我是finaly中的Exception
        }
    }

    //catch中的异常被抑制
    public static int foo() throws Exception {
        try {
            int a = 5 / 0;
            return 1;
        } catch (ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
        } finally {
            throw new Exception("我是finaly中的Exception");
        }
    }

    //try中的异常被抑制
    private static int bar() throws Exception {
        try {
            int a = 5 / 0;
            return 1;
        } finally {
            throw new Exception("我是finaly中的Exception");
        }

    }

上面的三个例子都异于常人的思维,一不小心就掉坑里了,因此建议:

  • 不要在fianlly中使用return
  • 不要在finally中抛出异常
  • 减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的
  • 尽量将所有的return写在函数的最后面,而不是try ... catch ... finally
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值