Java——异常处理

二、异常处理

1、Java的异常

在计算机程序运行的过程中,总是会出现各种各样的错误。

有些错误是用户造成的,比如,希望用户输入一个int类型的年龄,但是用户的输入是abc

// 假设用户输入了abc:
String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!

还有一些错误是随机出现,并且永远不可能避免的。比如:

  • 网络突然断了,连接不到远程服务器;
  • 内存耗尽,程序崩溃了;

所以,一个健壮的程序必须处理各种各样的错误。

Java对于错误的处理是内置了一套异常处理机制,总是使用异常来表示错误。

异常是一种class,因此它本身就带有类型信息。异常可以在任何地方抛出,但是需要在上层捕获,这样就和方法调用分离了:

try {
    String s = processFile(“C:\\test.txt”);
    // ok:
} catch (FileNotFoundException e) {
    // file not found:
} catch (SecurityException e) {
    // no read permission:
} catch (IOException e) {
    // io error:
} catch (Exception e) {
    // other error:
}

Java的异常是class,它的继承关系如下:

在这里插入图片描述

从继承关系可知:Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorException

Error表示严重的错误,程序对此一般无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception则是运行时的错误,它可以被捕获并处理。

某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

总结:

  • Error是十分严重的错误,程序对此无能为力,不需要捕获。
  • RuntimeException是运行时的错误,应该修改代码本身,不强制捕获。
  • RuntimeException(Checked Exception)是逻辑处理的错误,必须要捕获处理。

不推荐捕获了异常但不进行任何处理。

2、捕获异常

在Java中,凡是可能抛出异常的语句,都可以用try ... catch捕获。

2.1 多catch语句

可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。

JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println(e);
    } catch (NumberFormatException e) {
        System.out.println(e);
    }
}

注意:因为JVM在配置到某个 catch 后,就不再继续匹配。因此,存在多个 catch 时,catch 的顺序很重要:子类必须写在前面。否则会永远捕获不到。例如:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { 
      // UnsupportedEncodingException是IOException的子类,所以永远捕获不到
        System.out.println("Bad encoding");
    }
}

2.2 finally语句

我们我们希望无论是否有异常发生,都执行一些语句,例如关闭数据库等,可以使用 finally 语句。

finally语句块保证有无错误都会执行。

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (UnsupportedEncodingException e) {
        System.out.println("Bad encoding");
    } catch (IOException e) {
        System.out.println("IO error");
    } finally {
        System.out.println("END");
    }
}

finally有几个特点:

  1. finally语句不是必须的,可写可不写;
  2. finally总是最后执行。

某些情况下,可以没有catch,只使用try ... finally结构

2.3 捕获多种异常

如果某些异常的处理代码相同,但是异常本身不存在继承关系,可以把它两用|合并到一起:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
        System.out.println("Bad input");
    } catch (Exception e) {
        System.out.println("Unknown error");
    }
}

3、抛出异常

3.1 异常的传播

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch被捕获为止。

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        process2();
    }

    static void process2() {
        Integer.parseInt(null); // 会抛出NumberFormatException
    }
}

通过printStackTrace()可以打印出方法的调用栈,类似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

上述信息表示:

  • NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的;
  • 调用层次从上到下依次是:
    1. main()调用process1()
    2. process1()调用process2()
    3. process2()调用Integer.parseInt(String)
    4. Integer.parseInt(String)调用Integer.parseInt(String, int)

查看Integer.java源码可知,抛出异常的方法代码如下:

public static int parseInt(String s, int radix) throws NumberFormatException {
    if (s == null) {
        throw new NumberFormatException("null");
    }
    ...
}

3.2 抛出异常

当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。

如何抛出异常?参考Integer.parseInt()抛出异常的方法,抛出异常分两步:

  1. 创建某个Exception的实例;
  2. throw语句抛出。

例子:

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
            throw new IllegalArgumentException();
        }
    }

    static void process2() {
        throw new NullPointerException();
    }
}

在上面的代码中,当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException()。最后,在main()中捕获IllegalArgumentException并打印异常栈,如下:

java.lang.IllegalArgumentException
	at Main.process1(Main.java:19)
	at Main.main(Main.java:9)

这说明新的异常丢失了原始异常信息,我们已经看不到原始异常NullPointerException的信息了。

为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息。如下代码:

public class Main {
    //...

    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
            throw new IllegalArgumentException(e);
        }
    }

    //...
}

这次,打印出来的异常栈类似:

java.lang.IllegalArgumentException: java.lang.NullPointerException
	at Main.process1(Main.java:19)
	at Main.main(Main.java:9)
Caused by: java.lang.NullPointerException
	at Main.process2(Main.java:24)
	at Main.process1(Main.java:17)
	... 1 more

由输出可以知道:

  • Caused by: Xxx知道造成问题的根源是 NullPointerException,是在Main.process2()方法抛出的。

4、自定义异常

Java标准库定义的常用异常包括:

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException

在需要排除异常时,尽量使用已定义的异常类型。

在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。

一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。

BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:

public class BaseException extends RuntimeException {
}

其他业务类型的异常就可以从BaseException派生:

public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

//...

自定义的BaseException应该提供多个构造方法:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

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

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

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

上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。

5、日志输出

SLF4J和Logback的使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值