在JAVA中,将程序执行过程中发生的不正常行为称为异常。简单来说就是我们在运行或编译一段代码时所报的错误。
一,异常的体系结构
在JAVA中不同类型的异常,都有与其对应的类来进行描述。
1. Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
2. Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生回力乏术。
3. Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception
二,异常的分类
2.1 编译时异常/受查异常
编译时异常:在编译过程中发生的异常就叫编译时异常,上图所示的Exception子类中除了RuntimeException类,其他都属于编译时异常,比如不支持克隆异常。
2.2 运行时异常/非受查异常
运行时异常:在运行时产生的异常就叫运行时异常,上图所示的RuntimeException类及其子类都是运行时异常,比如:越界,空指针,算数异常。
三,JAVA处理异常的方式
EAFP:It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易". 也就是先操作, 遇到问题再处理. 即:事后认错型
try {
可能出现异常的代码....
}catch (异常1){
处理异常1
}catch (异常2){
处理异常2
}catch (异常3){
处理异常3
}....
这样做的优点就是,正常流程与异常处理流程是分开来写的,程序员更关注正常流程,代码更清晰。
四,处理异常的五个关键字
JAVA中,异常处理主要的5个关键字:throw、try、catch、final、throws。
4.1 throw 和 throws 关键字
在Java中,如果出现了异常,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者,例如:
public class Test {
public static void func(){
int a = 0;
if(a == 0){
throw new ArithmeticException("a == 0");//运行时异常
}
}
public static void main(String[] args) {
func();
}
}
throw的使用规范:
1. throw必须写在方法体内部 2. 异常一旦抛出,其后的代码就不会执行
3. 抛出的对象必须是Exception 或者 Exception 的子类对象
4. 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
5. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
public class Test {
public static void func(){
int a = 0;
if(a == 0){
throw new CloneNotSupportedException("a == 0");//编译时异常
}
}
public static void main(String[] args) {
func();
}
}
那么我们怎么来处理这个异常呢?最简单的方法就是通过throws关键字来处理,实际上使用throws并没用处理该异常,只是将该异常告诉了调用此方法的人,要这个人去解决,也就是说,异常并没用消失,它只是转移到了调用者身上。例如:
public class Test {
public static void func() throws CloneNotSupportedException {
int a = 0;
if(a == 0){
throw new CloneNotSupportedException("a == 0");
}
}
public static void main(String[] args) {
func();
}
}
因为main 方法调用了该方法,而main方法也没有处理该异常,所以依然会报错。
throws的使用规范:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
1. throws必须跟在方法的参数列表之后
2. 声明的异常必须是 Exception 或者 Exception 的子类
3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可
4. 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出,例如:
public class Test {
public static void func() throws CloneNotSupportedException {
int a = 0;
if(a == 0){
throw new CloneNotSupportedException("a == 0");
}
}
public static void main(String[] args) throws CloneNotSupportedException {
func();
}
}
main方法继续抛出该异常,最后就会由JVM来处理该异常。
4.2 try - catch - finally
那么我们究竟要怎么处理异常呢?这就要提到我们上面提到的try - catch - finally了,语法格式:
try{
可能出现异常的代码
}catch(要捕获的异常类型 e){
处理该异常
}catch(要捕获的异常类型 e){
处理该异常
}finally{
一定会执行此处的代码
}//finally可加可不加,catch可以有多个
举一个例子:
public class Test {
public static void main(String[] args) {
try{
System.out.println(1/0);
}catch (ArithmeticException e){
System.out.println("被除数不可以为0!");
}
System.out.println("11111");
}
}
1. try块内抛出异常位置之后的代码将不会被执行,例如:
public class Test {
public static void main(String[] args) {
try{
System.out.println(1/0);
System.out.println("2222");//这段代码不会执行
}catch (ArithmeticException e){
System.out.println("被除数不可以为0!");
}
System.out.println("11111");
}
}
2. 如果抛出异常类型与所有catch的异常类型不匹配,即异常不会被成功捕获,也就不会被处理,它会继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的
3. 当try块中可能抛出多个不同的异常时,需要用多个catch来抓捕异常,但是一旦抓捕到了第一个异常,try块出现异常后的代码就不会别执行,就是说即使后面的代码也出现了异常,也不会去处理了,例如:
public class Test {
public static void main(String[] args) {
try{
//算数异常
System.out.println(1/0);
//下面是空指针异常,因捕获算数异常,以下代码不会执行
int[] a = null;
System.out.println(a.length);
System.out.println("2222");
}catch (ArithmeticException e){
System.out.println("被除数不可以为0!");
}catch (NullPointerException e){
System.out.println("空指针异常!");
}
System.out.println("11111");
}
}
如果要在一个catch中捕获多个异常,可以使用 | ,但是不建议这么写,因为捕获的异常太模糊了,例如:
public class Test {
public static void main(String[] args) {
try{
System.out.println(1/0);
int[] a = null;
System.out.println(a.length);
System.out.println("2222");
}catch (ArithmeticException | NullPointerException e){
System.out.println("被除数不可以为0!");
}
}
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch
4. 可以通过一个catch捕获所有的异常,catch(Exception e),一次捕获(不推荐),因为捕获的异常太模糊了,不能针对性的解决问题。
有人可能发现了,我们处理异常到现在也没有用过finally ,他到底有什么用?实际上它主要的作用就是用来回收资源的,比如:Scanner,每当我们使用完Scanner时,我们都要通过 .close 来把它关闭,例如:
public class Test {
public static void func() throws CloneNotSupportedException {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
try{
System.out.println(1/a);
}catch (ArithmeticException e){
System.out.println("被除数不可以为0!");
}finally {
sc.close();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
func();
}
}
这时候又会有人问了,既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?那我将上面的代码改一改你就知道了:
public class Test {
public static int func() throws CloneNotSupportedException {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
try{
System.out.println(1/a);
return a;//即使没报错,返回去了,finally中的代码也一定会执行
}catch (ArithmeticException | NullPointerException e){
System.out.println("被除数不可以为0!");
}finally {
sc.close();
return 10;
}
}
public static void main(String[] args) throws CloneNotSupportedException {
int ret = func();//所以此时 ret = 10
}
}
总结一下异常处理流程:
1. 程序先执行 try 中的代码
2. 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
3. 如果找到匹配的异常类型, 就会执行 catch 中的代码
4. 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
5. 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
6. 如果上层调用者也没有处理的了异常, 就继续向上传递,一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止
五,自定义异常类
要想自定义一个异常类,我们先看看JAVA是怎么定义异常类的:
在这里我定义一个名称异常类,我们依葫芦画瓢:
public class NameException extends RuntimeException{
public NameException() {
}
public NameException(String message) {
super(message);
}
}
我们调用一下这个类,看看行不行:
class F{
public String name = "zhangsan";
public void loginInfo(String userName) throws NameException{
if (!this.name.equals(userName)) {
throw new NameException("用户名错误!");
}
System.out.println("登陆成功");
}
public static void main(String[] args) {
F f = new F();
try {
f.loginInfo("lisi");
} catch (NameException e) {
e.printStackTrace();//能打印哪一行出现异常
}
}
}