目录
1.什么是异常
在日常编写程序的过程中,由于疏忽或者逻辑错误时常会导致程序运行时出现这样或那样的错误导致程序非正常终止。其实这就是程序发生了异常,我们举几个常见的例子:
(1)数组越界异常
public static void main(String[] args) {
//定义数组长度为3,索引[0,2]
Integer[] arrays = new Integer[3];
//在索引4处添加元素
arrays[4] = 45;
}
控制台报错:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at com.java.day01.Demo1.main(Demo1.java:10)
(2)格式异常
public static void main(String[] args) {
//把一个含有字母的字符串转换为整数
Integer integer = Integer.valueOf("123qwe");
}
控制台报错:
Exception in thread "main" java.lang.NumberFormatException: For input string: "123qwe"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.valueOf(Integer.java:766)
at com.java.day01.Demo1.main(Demo1.java:10)
(3)类型装换异常
public static void main(String[] args) {
//任意类数组
Object[] objects = new Object[]{"123",34,56.7,true};
//把Double转为String
String str = (String) objects[2];
}
控制台报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be
cast to java.lang.String
at com.java.day01.Demo1.main(Demo1.java:12)
2. Java 的异常类继承关系及分类
Java 异常是程序运行过程中出现的错误。Java 把异常当作对象来处理,并定义一个基类 java.lang.Throwable 作为所有异常的超类。在 Java API 中定义了许多异常类,分为两大类,错误 Error 和异常 Exception 。Error 是程序无法处理的严重错误,这种错误一般来说与操作者无关,并且开发者与应用程序没有能力去解决这一问题,而是由 JVM 终止线程,因此对于 Error 一般不作处理。而 Exception 通常是程序编写时由于一些逻辑错误产生的,因此是可以被避免的。
其中异常类 Exception 又分为运行时异常 (RuntimeException) 和非运行时异常 (非runtimeException) ;也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。上边所列举的三个异常类均属于运行时异常,而对于非运行时异常,多出现于流的传输之中,这些异常从程序语法角度讲是必须进行处理的异常,如若不处理,程序就不能编译通过。
下图展示了异常的继承关系以及常见的异常分类:
3.异常的产生过程
对于异常的产生过程,我们拿下面例子中的数组越界异常 ArrayIndexOutOfBoundsException 来做具体分析。
public class Demo1 {
public static void main(String[] args) {
int[] arrays = new int[3];
int result = getElement(arrays, 4);//数组越界异常
}
public static int getElement(int[] arrays, int index) {
//此时我们不对传入参数的合法性进行检查
return arrays[index];
}
}
①在调用 getElement ( ) 方法时,由于 arrays 数组没有 3 这个索引,因此 JVM 就会检测到程序异常。
② JVM 创建一个包含异常原因、内容、位置的异常对象 —— new ArrayIndexOutOfBoundsException ( )。
③ JVM 检测到 getElement ( ) 方法中没有异常发生后的处理逻辑(try - catch),就把异常对象抛给方法的调用者 main ( ) 方法。
④ main ( ) 方法接收到异常对象,但是它也没有异常的处理逻辑,因此把该异常对象抛给它的调用者 JVM 。
⑤ JVM 首先把异常对象包含的信息打印在控制台,之后执行中断处理。
4.异常的处理
(1)throw关键字
throw 关键字的作用是用来创建一个包含自定义提示信息的异常对象,通常我们结合方法参数合法性的检验来使用。拿上面的 getElement ( ) 方法举例:
public static int getElement(int[] arrays, int index) {
//做参数合法性检验
//数组不能为空
if (arrays == null) {
throw new NullPointerException("数组不能为null");//空指针异常
}
//索引值不能超出数组索引值范围
if (index >= arrays.length) {
throw new ArrayIndexOutOfBoundsException("索引值不能超出数组范围");//数组越界异常
}
return arrays[index];
}
此时控制台打印的异常信息就包含了我们自定义的内容。
public static void main(String[] args) {
int[] arrays = new int[3];
int result = getElement(arrays, 4);//数组越界
}
控制台报错:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 索引值不能超出数组范围
at com.java.day01.Demo1.getElement(Demo1.java:23)
at com.java.day01.Demo1.main(Demo1.java:11)
注意:
在不使用 throw 关键字创建异常对象时,系统会自动调用无参构造创建默认异常对象。
对于运行期异常,我们可以不处理,默认交给 JVM 处理;但是对于编译异常,我们必须处理完这个异常程序才能运行。
(2)throws 关键字
throws 关键字并不能够处理异常,它所做的只是把异常对象向上抛给此方法的调用者,让调用者去处理。
对于运行期异常,在程序运行时异常对象会默认抛给调用者,因此 throws 关键字多用于声明把不希望在此方法中处理的编译异常抛给调用者。
public static int getElement (int[] arrays, int index) throws Exception{
(3)try - catch 处理异常
try - catch 是真正意义上用来处理异常的方式,其中 try 中包含了可能发生异常的代码段,catch 包含了异常对象和发生异常后的处理逻辑。
public static void testException() {
Scanner scanner = new Scanner(System.in);
System.out.print("输入一个整数:");
try {
Integer.valueOf(scanner.next());
System.out.println("程序没有出现异常。。。");
} catch (NumberFormatException ne) {
//此代码不添加默认调用无参构造
ne = new NumberFormatException("数字格式异常");//添加自定义提示信息
ne.printStackTrace();//打印完整详细的异常信息
//处理异常的代码。。。
System.out.println("处理了异常。。。");
} finally {
System.out.println("一定会被执行。。。");
}
}
通常情况下,finally 块与 try - catch 配合使用且不能单独存在。finally 表示无论异常是否发生都必须要执行的代码,多用于资源的释放与回收。
下面我们调用这个方法:
public static void main(String[] args) {
testException("122");//正确格式
System.out.println("====分割线====");
testException("qw2343eree");//错误格式
}
结果:
程序没有出现异常。。。
一定会被执行。。。
====分割线====
处理了异常。。。
一定会被执行。。。
java.lang.NumberFormatException: 数字格式异常
at com.java.day01.Demo1.testException(Demo1.java:44)
at com.java.day01.Demo1.main(Demo1.java:11)
可以看到,即使异常发生,finally 块中的代码还是被执行了。
注意:
return 语句表示结束当前函数的调用,任何语句包括 finally 的执行都是在 return 语句前。若 try - finally / catch - finally 中都包含有 return 语句,则 finally 块的中 return 会覆盖 try 或 catch 的中的 return 语句,致使返回结果出错。因此 return 不应该写在 finally 块中。
public static int testException() {
try {
return 1;
} finally {
return 2;
}
}
调用函数
public static void main(String[] args) {
int result = testException();
System.out.println(result);
}
执行结果
2
5.关于 printStackTrace ( ) 方法
在我们处理异常时,常常会想到使用 printStackTrace ( ) 方法在控制台打印异常信息。这虽然并没有什么错误,但是应该尽量避免使用这种方式打印日志,为什么呢?主要由以下几点原因:
(1)占用内存过多,容易造成锁死
要打印字符串输出到控制台上,需要字符串常量池所在的内存块有足够的空间。然而,printStackTrace ( ) 语句要产生的字符串记录的是堆栈信息,要按先进后出的顺序记录完所有信息再打印出来,这就使得信息量十分庞大,内存很容易被填满。大量线程产出字符串产出到一半,而此时内存已经被占满,必须等待内存释放,导致锁死。
(2)日志交错混合,不易读
printStackTrace ( ) 默认使用了 System.err 输出流进行输出,与 System.out 是两个不同的输出流,那么在打印时自然就形成了交叉;再者由于输出流是有缓冲区的,导致了输出时间的随机性。