1、概述
异常就是“不正常”的含义,在 Java 语言中主要指程序执行中发生的不正常情况。
java.lang.Throwable
类是 Java 语言中错误(Error)和异常(Exception)的超类。java.lang.Error
类主要用于描述 Java 虚拟机无法解决的严重错误,通常无法编码解决,如:JVM挂掉了等。java.lang.Exception
类主要用于描述因编程错误或偶然外在因素导致的轻微错误,通常可以编码解决,如:0作为除数等。
2、异常
2.1、异常的分类
java.lang.Exception
类是所有异常的超类。异常主要分为两种,运行时异常(非检测性异常)、其他异常(检测性异常)。所谓检测性异常就是指在编译阶段都能被编译器检测出来的异常,而非检测性异常只会在程序运行过程中出现,在编译阶段无法检测出来(编写代码时 IDE 不标红)。
运行时异常,所对应的是
java.lang.RuntimeException
的子类,也就是说以java.lang.RuntimeException
为超类的异常类均是运行时异常。常见的有:
java.lang.ArithmeticException
- 算术异常
java.lang.ArrayIndexOutOfBoundsException
- 数组下标越界异常
java.lang.NullPointerException
- 空指针异常(简称:NPE)
java.lang.ClassCastException
- 类型转换异常
java.lang.NumberFormatException
- 数字格式异常
- 当程序执行过程中发生异常但又没有手动处理时,则由 Java 虚拟机采用默认方式处理异常,而默认处理方式就是:在控制台打印异常的名称、异常发生的原因、异常发生的位置以及终止程序。也就会有常说的报错。同时,发生异常处之后的代码不再执行!
2.2、异常的处理
- 可以采取像 if 等逻辑来避免异常的发生,注意是避免而不是处理,不属于 Java 自身的异常处理机制。
if (obj != null) {
obj.do(); // 避免发生 NPE
}
- Java 提供的异常处理机制。一个是捕获,另一个是抛出。
2.2.1、异常的捕获
这种异常处理机制,有些“防患于未然”的意思,如果没发生,就算了,如果发生了,就捕获处理。
异常的捕获,Java 中采取的是
try
、catch
、finally
这几个关键字。异常捕获的语法如下:
- 在
try
语句块中的代码,是有可能发生异常的代码,当一旦发生异常,根据异常的类型,立即转到对应的catch
语句块中执行catch
语句块中的语句,从发生异常到转到相应的catch
语句块中执行异常处理代码,这么的一个过程称为“捕获”(try 一词的中文含义是“尝试”,catch 一词的中文含义是“抓”,“尝试执行其中的代码,一旦出现问题,马上去抓”)。
/* 此段代码可以正常执行 */
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionTest {
public static void main(String[] args) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = dateFormat.parse("2020-10-13"); // 有可能发生异常的代码,此处会出现异常的原因是:无法基于给定的模式(yyyy-MM-dd)字符串转为 java.util.Date 类型的对象
System.out.println(date); // Tue Oct 13 00:00:00 CST 2020
} catch (ParseException e) {
e.printStackTrace(); // 此处处理异常的代码是:打印相应的异常信息
}
}
}
/* 此段代码会出现异常 */
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionTest {
public static void main(String[] args) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = dateFormat.parse("2020/10/13"); // 此处代码会出现异常
System.out.println(date); // 由于上一句代码发生异常,自异常处向后的 try 语句块中的代码将不再执行
} catch (ParseException e) {
e.printStackTrace(); // 执行此处代码
}
}
}
打印的异常信息:
java.text.ParseException: Unparseable date: "2020/10/13"
at java.base/java.text.DateFormat.parse(DateFormat.java:395)
at com.yscyber.lagou.chapter04.task01.ExceptionTest.main(ExceptionTest.java:13)
- 关于异常捕获中常见的语法、执行流程、注意事项:
1、try ... catch ...
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionTest {
public static void main(String[] args) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = dateFormat.parse("2020/10/13"); // 会发生异常
System.out.println("1"); // 不执行
} catch (ParseException e) {
e.printStackTrace(); // 执行
System.out.println("2"); // 执行
}
System.out.println("3"); // 执行
}
}
打印结果:
java.text.ParseException: Unparseable date: "2020/10/13"
at java.base/java.text.DateFormat.parse(DateFormat.java:395)
at com.yscyber.lagou.chapter04.task01.ExceptionTest.main(ExceptionTest.java:13)
2
3
2、try ... catch ... catch ...
在实际情况下,对于一个语句来说,有可能会出现多种类型的异常,所以说针对不同类型的异常,使用多个catch
的情况是存在的。
但是,有个问题要注意,由于异常本质上是类,在异常类之间会有继承的关系,在有多个catch
的情况下,切记小类型应该放在大类型的前面,像下面代码所示的那样,因为java.lang.Exception
是java.lang.ParseException
的超类。这个东西可以这么理解,如果发生异常,能够捕获该异常的catch
语句中参数都是当前异常的本类或超类,在这些catch
中,捕获会按照catch
的先后顺序进行。下面的代码如果Exception
放在ParseException
的前面,那么异常的捕获是由在前面的catch (Exception e)
进行,后面的catch (ParseException e)
就没有意义了。
另外,由于java.lang.Exception
是所有异常类的超类,所以编写1个catch (Exception e)
语句就能捕获所有的异常,这可以所作一种“懒人写法”。在实际的项目中,一般对于不同的异常有不同的处理逻辑,所以catch
分开写比较好。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionTest {
public static void main(String[] args) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = dateFormat.parse("2020/10/13");
} catch (ParseException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、try ... catch ... finally ...
在finally
语句块中的语句,不论异常是否发生,都要执行。在finally
中的代码通常会进行做一些“善后”的工作。
/* 程序正常执行 */
public class ExceptionFinallyTest {
public static void main(String[] args) {
try {
int i = 2;
int j = 1;
int k = i / j;
} catch (ArithmeticException e) {
// 运行时异常
e.printStackTrace(); // 程序没发生异常,不执行
} finally {
System.out.println("1"); // 执行
}
System.out.println("2"); // 执行
}
}
/* 程序发生异常 */
public class ExceptionFinallyTest {
public static void main(String[] args) {
try {
int i = 2;
int j = 0; // 除数为0
int k = i / j;
} catch (ArithmeticException e) {
// 运行时异常
e.printStackTrace(); // 程序发生异常,执行
} finally {
System.out.println("1"); // 执行
}
System.out.println("2"); // 执行
}
}
/* 特殊的例子,理解 finally */
public class ExceptionFinallyTest {
public static void main(String[] args) {
try {
int i = 2;
int j = 0; // 除数为0
int k = i / j; // 此处发生异常
} catch (ArithmeticException e) {
// 发生异常,执行 catch 中的语句
String x = null;
x.length(); // catch 语句块中的发生异常
} finally {
System.out.println("1"); // 仍然执行!!!虽然 catch 中又发生异常
}
System.out.println("2"); // 不执行,因为 catch 语句块中的异常没有处理机制
}
}
/* 特殊的例子,理解 finally */
public class ExceptionFinallyTest {
private static int method() {
try {
int i = 2;
int j = 0;
int k = i / j;
return 1; // 由于发生异常,这个语句肯定不会执行
} catch (ArithmeticException e) {
return 2;
} finally {
return 3; // finally 中的 return 会作为最终的 return
}
}
public static void main(String[] args) {
System.out.println(method()); // 3
}
}
4、常见的异常捕获的流程
2.2.2、异常的抛出
首先,要有个认识,上面所提到的“异常捕获”,之所以能够使用
try-catch-finally
来实现对异常的捕获,其最根本的原因是,有的地方抛出了异常。
如果将“捕获”比作是捡起在地上的东西,前提是要有东西掉在地上,这个“抛出”就是将东西扔在地上的行为。“抛出”相当于是一种将异常滞后处理的方案,等到合适的地方再通过捕获等再来处理。
方法中,某些特殊情况下有些异常不能处理或者不便于处理时,就可以将该异常转移给该方法的调用者,这种情形就叫异常的抛出。当方法执行时出现异常,则底层生成一个异常类对象抛出,此时异常代码后续的代码就不再执行。
(方法)抛出异常的语法
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionThrowTest {
// 使用 throws 将异常抛出
private static void show() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse("2020/10/15"); // 此处会发生异常
System.out.println(date);
System.out.println("1"); // 不执行
}
// 在 main 方法中直接捕获异常
// 一般 main 方法不建议再将异常抛出(main 方法可以抛出)
public static void main(String[] args) {
try {
show();
} catch (ParseException e) {
e.printStackTrace();
}
}
}
- 通过上面的一些阐述、举例,对于异常抛出有了一个大概的认识,但是有一个问题,就是“底层生成一个异常类对象抛出”这个怎么理解。
在异常抛出这个机制中,抛出是可以“连续”的,也就是说可以进行“甩锅”的,像下面的代码:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionThrowTest {
private static void show1() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse("2020/10/13");
System.out.println(date);
}
private static void show2() throws ParseException {
show1();
}
private static void show3() throws ParseException {
show2();
}
public static void main(String[] args) {
try {
show3();
} catch (ParseException e) {
e.printStackTrace();
}
}
}
show1
方法检测到异常(可能发生),不作处理,使用throws
将异常抛出;show2
方法调用show1
方法,检测到show1
抛出未处理的异常,show2
也不作处理,同样使用throws
将其抛出,······,一直到main
方法才对异常捕获处理。就像现实生活中的不断“甩锅”给其他人的现象。但是,并没有从上面的代码中看出异常最初的来源,当然这里的情况比较简单,来源的位置很好定位,是SimpleDateFormat.parse
方法,其源码如下:
public Date parse(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"", pos.errorIndex);
return result;
}
在方法体中,有一个throw new ParseException(······)
的语句,同时方法签名上也有throws ParseException
。异常最初的来源就是使用throw
关键字(注意,这里的关键字是throw
而不是用在方法签名中的throws
)来“抛出”一个异常类的对象,也印证了上面那句的“底层生成一个异常类对象抛出”,这也是异常抛出的本质。当然,如果异常的来源处抛出的是检测性异常也要按照“捕获”或“抛出”来处理异常,一般情况下直接使用“抛出”的语法,如下面的代码:
import java.text.ParseException;
public class ExceptionThrowTest {
// 在 throw 的地方进行 try-catch 从感觉上会有些别扭,本来是打算抛出的,却在这里给处理了
// 不建议这样编写
private static void show1() {
try {
throw new ParseException("格式错误", 1);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
show1();
}
}
import java.text.ParseException;
public class ExceptionThrowTest {
// 异常抛出来源处,在方法签名上使用 throws
// 这种写法相对来说比较容易接受,就把方法签名上的当成一个声明的东西
private static void show1() throws ParseException {
throw new ParseException("格式错误", 1);
}
public static void main(String[] args) {
try {
show1();
} catch (ParseException e) {
e.printStackTrace();
}
}
}
- 继承(方法重写)中异常抛出
“更大”的异常,就是指超类。子类重写方法时,可以选择不再抛出异常,如果抛出的话不得throws
其父类所throws
的超类。
2.3、自定义异常
前面所提到的异常类,都是 Java 自身提供的。为了方便使用,Java 也允许自定义异常类。
实现自定义异常类
a、自定义异常类继承java.lang.Exception
类或者其子类。一般异常类的命名规范是“xxxException”。
b、提供两个版本的构造方法,一个是无参构造方法,另外一个是字符串作为参数的构造方法。
public class AgeException extends Exception {
public AgeException() {
super();
}
public AgeException(String message) {
super(message);
}
}
- 自定义异常的使用
使用throw
关键字在适当的“抛出”异常。同时在方法签名上使用throws
或者就地使用try-catch
,建议使用前者。
package com.yscyber.lagou.chapter04.task01;
public class Student {
private String name;
private Integer age;
public Student() {
}
public Student(String name, Integer age) throws AgeException {
setName(name);
setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) throws AgeException {
if (age <= 0) {
throw new AgeException("Age Error!"); // 抛出异常,当然此处也可以选择就地 try-catch 处理
}
this.age = age;
}
}
public class StudentTest {
public static void main(String[] args) {
Student student = null;
try {
student = new Student("Jack", -2); // 年龄给一个错误的参数,使其发生异常
} catch (AgeException e) {
e.printStackTrace(); // 处理异常
}
System.out.println(student); // null
}
}