系列文章目录
第一部分——编程基础与二进制 1
第一部分——编程基础与二进制 2
第二部分——面向对象.类的基础
第二部分——面向对象.类的继承
第二部分——面向对象.类的扩展
文章目录
第二部分——面向对象
6.异常
6.1初识异常
这一节,我们通过两个具体的异常来引入异常的概念
6.1.1NullPointerException——空指针异常
public static void main(String[] args) {
String s = null;
System.out.println(s.indexOf("a"));
System.out.println("end");
}
这里我们调用一个值为null的变量s的方法,运行
Exception in thread "main" java.lang.NullPointerException
at com.moozlee.core.exception.TestException.main(TestException.java:10)
从字面意思可以知道,这是一个在main线程里出现的java.lang包下定义的NullPointerException空指针异常,并且后续的打印语句也没有被执行
而运行到第三行具体发生的事情可以概括为以下几步:
- JVM发现s为null,无法调用其方法,启用异常处理的机制
- 创建一个NullPointerException对象
- 查找是否有人处理异常,发现没有能够处理异常
- 采用默认处理机制,打印异常栈信息到控制台,提前结束程序
这里提到的异常栈信息包含了从异常发生的位置到最上层调用者的调用线路以及出错的位置,有时候因为调用线路过深可能会出现省略异常堆栈信息的情况
6.1.2NumberFormatException——数字格式化异常
public static void main(String[] args) {
String s = "abc";
int num = Integer.parseInt(s);
System.out.println(num);
}
这里我们试图将abc转成整数,肯定是有问题的,于是就产生了下列的异常信息
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at com.moozlee.core.exception.TestException.main(TestException.java:10)
这一次,我们跟着堆栈信息的调用线路走一次,从最底下的也就是我们自己的代码报错的地方跟进,在我的编译器里是第10行,也就是上面的第3行,这里我们试图调用Integer.parseInt的方法,出现问题
再跟进一层,到Integer的615行
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
发现这是一个静态方法,我们传入的字符串被传给了另一个重载的函数
再跟进一层,到Integer的580行
public static int parseInt(String s, int radix)
throws NumberFormatException {
//省略了很多代码
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit(s.charAt(i++),radix);
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
result -= digit;
}
//省略了很多代码
}
我们跟踪到了上面调用的这个方法,参数一是我们传入的字符串,参数二是转换成数据的进制,然后定位到580行也就是上述的第8行,整个数据处理的过程不详细叙述,这里是将一个一个数字的遍历字符串,然后再转换成整数,而这之中当遍历到字符a时,Character.digit方法发现无法转换成数字于是返回了一个小于0的值,于是进入了第一个if语句块出现了关键的一行代码
throw NumberFormatException(...)
这里又引入一个关键字throw,意思是抛出一个异常,通过抛出异常就会除法java的异常处理机制,而在谈函数实现原理的时候我们提到过退出程序或称方法执行的方式有两种一种是return结束,而这里就是第二种异常退出,至此会一直像调用的上层寻求处理异常的逻辑,如果到了main方法这一层还没有处理逻辑,那么就启用默认的机制,打印异常栈信息并结束程序
6.2异常类的框架
上一节,我们具体的感受了两个异常的产生和处理过程,引入了异常的概念和throw这个关键字,这一节我们就具体了解以下Java语言下异常的整体框架
6.2.1Throwable
public class Throwable implements Serializable
Throwable是Java中所有错误类Error和异常类Exception的超类,根据字面意思也知道这是一个“可抛出的类”,也即只有Throwable类型的实例可以被关键字throw抛出以及后面会提到的catch关键字捕获
我们继续看这个类的重要方法
- fillInStackTrace这个方法会将记录当前线程的执行的栈帧信息
- getMessage这个方法会将在构造函数部分初始化好异常的详细信息返回
- getCause返回一个该对象代表的异常原因,如果不存在或者未知就返回null
- getStackTrace返回一个堆栈层次的数组,0位置的是栈顶,最后一个位置是方法调用堆栈的栈底
- printStackTrace打印toString后栈帧信息的结果到错误输出流System.err
- toString获取当前异常类的类名和Message信息
6.2.2Error
public class Error extends Throwable
Error是Throwable的子类,表示出乎意料的错误,这个类型代表的问题全部都不是正常情况下应该出现的问题,因此在Error里而不是Exception,对于这一类问题,不方便也不需要捕获,一般会直接报错,如
- AssertionError 断言错误
- VirtualMachineError 虚拟机错误
- UnsupportedClassVersionError Java类版本错误(刚打开陈年老项目基本上必有)
- StackOverflowError 栈内存溢出错误(无限递归)
- OutOfMemoryError OOM内存溢出错误(正常测试难以测试的出,需要配合虚拟机参数,深入Java虚拟机有实验案例)
- …
6.2.3Exception
同样也是Throwable的子类,不同于Error,Exception都是预料中会发生的意外情况,需要被提前设计捕获处理,因此有一套完整的处理流程,后续会讲
其中Exception可以分为可检查异常与不检查异常,可检查异常必须显示的进行处理,要么抛出,要么try/catch,否则编译不通过,现代编译环境都有提示
常见的Exception:
- ClassNotFoundException 准备加载类却发现找不到(检查想要用的类是不是这个包下的;检查依赖是否成功加载;SpringBoot项目中检查是否配置好相关参数)
- CloneNotSupportException 调用Object的clone方法,却发现该类没有实现Cloneable接口(建议非必要不用clone方法)
- IllegalAccessException 拒绝访问一个类(最近在学大数据框架,经常发生在数据库或者其他框架设置了权限)
- …
6.2.4RuntimeException
运行时异常,我们日常最常见的异常几乎都囊括在这个类别下
常见的RuntimeException:
- ArrayIndexOutOfBoundsException 数组越界(检查边界条件处理)
- ClassCastException 强制转换错误(加instanceof判断)
- IllegalArgumentException 不合法参数(打印异常附近可能涉及到的参数)
- NullPointerException 空指针异常(爹,断电debug,看谁是空值)
- …
6.3自定义异常
一个完整的框架或者系统,基本都需要自定义异常,能用Java库中定义好的异常表达诉求就用,不能再自定义
一般需要自定义异常的场景,都是符合程序运行,但不符合系统运行的逻辑上的bug
public class MyException extends RuntimeException {
public MyException(String message) {
super(message);
}
}
定义一个类继承Exception或者RuntimeException,根据需求选择性覆写方法(一般不用),简单的像上面这样处理mesasge就行
6.4处理异常
6.4.1抛出异常
这个部分常用到两个关键字 throw和throws
throw之前看到过很多次了,在发现异常的地方新建一个定义好的异常并使用throw关键字抛出
public void test(String s) {
if(s == null || s.equals("")) {
throw new MyException("字符串为空");//当然这里最好是用IlleagalArgumentException
}
}
如果一个方法没有捕获一个检查性异常,那么该方法必须使用throws关键字来声明
public class ThrowsDemo {
public static void f1() throws NoSuchMethodException, NoSuchFieldException {
Field field = Integer.class.getDeclaredField("digits");
if (field != null) {
System.out.println("反射获取 digits 方法成功");
}
Method method = String.class.getMethod("toString", int.class);
if (method != null) {
System.out.println("反射获取 toString 方法成功");
}
}
public static void f2() {
try {
// 调用 f1 处,如果不用 try catch ,编译时会报错,除非你也throws出去
f1();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
f2();//如果f2处没有处理,那么这里就一定要被处理
}
};
6.4.2捕获异常
try-catich-finally是捕获异常的一套规范的语法形式
try {
//可能发生异常的部分
} catch (Exception1 e1) {
//捕获并处理与这个异常类型匹配的异常
} catch (Exception2 e2) {
//捕获并处理与这个异常类型匹配的异常
} finally {
//无论是否异常,都将执行的代码
}
在JDK7以后,可以合并多个异常
try {
//可能发生异常的部分
} catch (Exception1 | Exception2 e) {
//捕获并处理与这个异常类型匹配的异常
} finally {
//无论是否异常,都将执行的代码
}
- 使用捕获的方式的能够避免程序因为异常崩溃,提高了程序鲁棒性,并且一般建议同抛出异常合理搭配,碰见异常就捕获和一直抛出最后捕获都是不可取的做法,需要根据业务分层的需求合理的安排,保证各个业务层级之间不会互相影响
- finally中语句无论是否有异常都将执行,因此可以将一些“善后”工作交给finally做,如关闭释放资源,不过对于关闭释放资源还有一套更简单的try-with-resources的语法,下面讲
- try-catch-finally三个代码块中的局部变量不共享
- catch的匹配机制是短路匹配,因此子异常放前面捕获,父异常放后面捕获
try-with-resources
Java7之后开始支持的语法,这种语法针对实现了java.lang.AutoCloseable的对象
public interface AutoCloseable {
void close() throws Exception;
}
不使用try-with-resources时
AutoCloseable r = new FileInputStream("file");
try {
//使用资源
} catch(Exception e) {
//处理异常
} finally {
r.close();
}
使用try-with-resources时
try (AutoCloseable r = new FileInputStream("hello")) {
//使用资源
} catch(Exception e) {
//处理异常
}
它可以在执行结束后自动调用close方法,如果定义多个资源用分号分隔
6.5异常注意事项
6.5.1finally覆盖异常
finally语句中的return会覆盖catch模块的return和throw,因此不建议在finally中使用return
public class FinallyOverrideExceptionDemo {
static void f() throws Exception {
try {
throw new Exception("A");
} catch (Exception e) {
throw new Exception("B");
} finally {
throw new Exception("C");
}
}
public static void main(String[] args) {
try {
f();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
输出异常C
6.5.2覆盖抛出异常的方法
当子类重写父类带有 throws声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内——用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法 。
public class ExceptionOverrideDemo {
static class Father {
public void start() throws IOException {
throw new IOException();
}
}
static class Son extends Father {
@Override
public void start() throws SQLException {
throw new SQLException();
}
}
public static void main(String[] args) {
Father obj1 = new Father();
Father obj2 = new Son();
try {
obj1.start();
obj2.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的代码编译就无法通过,因为IOException无法处理SQLException