从浅入深理解JAVA异常
- 一、什么是异常以及异常的分类
- 二、异常的分类
- 1、常见的系统错误
- 2、常见的编译时异常
- 3、常见的运行时异常
- 三、创建异常
- 1、创建JAVA中已经存在的异常 -- throw关键字
- 1.1 语法
- 1.2 使用
- 2、自定义异常
- (1)区分你要创建哪种异常
- (2)创建一个异常类,并继承对应的父类
- 四、异常的处理
- 1、非受查异常
- 1.1 处理
- 1.2 抛出
- 2、受查异常
一、什么是异常以及异常的分类
异常其实就是错误的意思,那么在编程中,我们会遇到哪些错误呢?
最常见的错误当然是语法错误,在我们写代码的时候,可能会因为粗心导致出现一个语法错误,比如关键字写错了,变量没有初始化等等。那么此时,编辑器就会在这一行代码的下面画出红色的波浪线,来提示我们,这里出现了一个语法错误。如果我们不修改错误语法的话,我们的代码是无法进行编译的,因为编译器不认识你写的代码。这种编译时出现的错误,我们叫做“编译时异常”,也叫做“受查异常”。就是说,我会在你写代码的时候,检查出这种错误,并且让你修改。
那么还有一种错误,这种错误在语法上是通过的,但是当跑起来以后,在某些情况下却出现了错误。比如说我们定义了一个函数,这个函数是实现两个数字相除。然后我们调用这个方法,在这个方法的形参中,我们传入两个数字。单看这两个步骤并没有什么问题,但是,如果我们在传入的时候,传入一个0作为除数。此时在逻辑上就发生了错误,但是语法上并没有错误。因为0也是数字,当然可以作为参数。
但这种错误,只有我们执行代码的时候,真正将传入的参数带入函数运算的情况下,我们才会发现这里出现了一个除数为0的错误。这种错误就叫做“运行时异常”,同时,因为你的语法正确,所以你可以通过编译,也就是说编辑器并没有查出你的错误,所以这种异常也叫做“非受查异常”。
这是常见的两种异常,而出现这两种异常的时候,我们可以在代码上做一些处理,使得程序依旧能完成其原有的任务。比如,出现编译时异常的时候,我们就可以修改一下代码,然后使其通过编译,并且使程序完成其原有的任务。出现运行时异常的时候,按照之前的逻辑,我们可以加一个if
语句,特判掉你出现错误的特殊情况,以除数为0距离,我们可以特判,如果除数为0,那就返回一个非常大的数字,而不做除法运算。这样就能保证,我们做完这个操作后,该程序依旧能完成“计算两数”相除这个任务。
像上述这种,我们能够通过代码上的处理,使得程序依旧能完成原定任务的错误,统称为异常。
但是还有另外一种,就是你原定的任务本身就不可能实现出来,比如你想实现一个无限递归的功能,那么很明显,必定导致内存中的堆栈溢出。又或者你想开一段非常非常大的空间,此时也必定会导致内存不够的问题。这种问题,你并不能通过对代码的修改而使其顺利完成原定任务。这种问题称为系统错误,系统错误在Java中也是一个非受查异常。
当然,有人会说,可以呀,我不让他无限递归不就好了。但是此时你已经改变了你原定的任务或者需求,你原本就是想实现一个无限递归的需求,可是你现在更改了这个需求。而刚刚提到的两数相除的需求,我们仅仅是加了一个特判,但这个程序依旧能满足我们计算除法结果的这一需求。
二、异常的分类
下图为JAVA中异常类的继承情况:
Throwable是一个超类,所有的异常或者错误都要继承这个超类,只有继承了这个超类,Java虚拟机(JVM)才会将这个类当作异常或错误类。从而进行后续的抛出、捕获等操作。
Error就是刚刚提到的系统错误,Exception就是刚刚提到的两种异常的父类,在它的子类中可以划分为两类,一类是RuntimeException,另一类就是除了RuntimeException之外的子类。前者就是运行时异常,后者就是编译时异常。
那么常见的异常都有哪些呢?
1、常见的系统错误
在Java中,Error是指在应用程序中不太可能处理或恢复的严重问题。这些问题通常是由于系统错误、资源耗尽或虚拟机出现问题等原因而引起的。以下是Java中常见的Error类型:
-
OutOfMemoryError:当虚拟机无法分配足够的内存空间时,会抛出此错误。例如,当程序尝试创建大量对象或递归调用太深时,可能导致内存溢出。
-
StackOverflowError:当方法调用的堆栈溢出时,会抛出此错误。这通常是由于无限递归调用或方法嵌套过深导致的。
-
NoClassDefFoundError:当虚拟机无法找到某个类的定义时,会抛出此错误。可能是由于类文件未正确加载或类文件路径配置错误等原因导致的。
-
AssertionError:当断言语句失败时,会抛出此错误。断言通常用于在代码中验证预期结果,如果断言失败,则说明代码逻辑有误。
-
LinkageError:当类库或模块之间的链接失败时,会抛出此错误。可能是由于类库版本不兼容或类文件依赖关系错误导致的。
-
VirtualMachineError:当Java虚拟机出现内部错误时,会抛出此错误。这些错误通常是由于虚拟机运行时出现问题导致的,例如栈溢出、垃圾回收失败等。
2、常见的编译时异常
Java中常见的编译时异常包括:
- FileNotFoundException:文件未找到异常,当尝试打开一个不存在的文件时抛出。
- IOException:输入输出异常,当发生输入输出操作错误时抛出。
- ClassNotFoundException:类未找到异常,当尝试加载不存在的类时抛出。
- SQLException:SQL异常,当操作数据库发生错误时抛出。
- InterruptedException:线程中断异常,当线程被中断时抛出。
- NoSuchMethodException:方法未找到异常,当尝试调用不存在的方法时抛出。
- NoSuchFieldException:字段未找到异常,当尝试访问不存在的字段时抛出。
- IllegalAccessException:非法访问异常,当通过反射访问私有方法或字段时抛出。
- IllegalArgumentException:非法参数异常,当传入的参数不符合方法要求时抛出。
- ArrayIndexOutOfBoundsException:数组索引越界异常,当访问数组超出索引范围时抛出。
3、常见的运行时异常
在Java中,常见的运行时异常包括:
- NullPointerException(空指针异常):当一个对象引用为null,但在代码中尝试调用该对象的方法或访问其属性时,就会抛出该异常。
- ArrayIndexOutOfBoundsException(数组越界异常):当尝试访问数组中不存在的索引位置时,就会抛出该异常。
- ClassCastException(类转换异常):当尝试将一个对象强制转换为不兼容的类型时,就会抛出该异常。
- IllegalArgumentException(非法参数异常):当方法接收到一个非法或不合适的参数时,就会抛出该异常。
- ArithmeticException(算术异常):当在运算中出现了算术错误,比如除以0,就会抛出该异常。
- IndexOutOfBoundsException(索引越界异常):当使用一个不存在的索引操作容器类(如List或字符串)时,就会抛出该异常。
- UnsupportedOperationException(不支持的操作异常):当尝试执行一个不支持的操作时,就会抛出该异常。
- NullPointerException(空指针异常):一般是因为调用的对象为空,即null。
- NumberFormatException(数字格式异常):当字符串无法转换为数字时,就会抛出该异常。
- ConcurrentModificationException(并发修改异常):当在迭代容器时,发生了并发修改操作,比如在使用Iterator遍历时,又对容器进行了增删操作,就会抛出该异常。
三、创建异常
1、创建JAVA中已经存在的异常 – throw关键字
在讲解之前我们先看下面的代码。
public class Exception01 {
public static void main(String[] args) {
int a = 10, b = 0;
System.out.println(a / b);
}
}
如果我们运行上述的代码,我们会发现下面这个结果。
这个结果就是告诉我们,这段代码中出现了一个异常,这个异常的类型是:ArithmeticException
。那么我是怎么知道的呢?这就需要我们来学习一下如何看终端中打印的红色信息。
那么这个终端中红色字到底记录了哪些信息呢?
那这个异常是哪里来的呢?
其实就是我们计算a/b
的时候,java内部创建出来的。
那么我们能不能在计算a/b之前就创建出这个异常呢?
答案是可以的,这里要用到的就是throw
关键字。
1.1 语法
throw new 异常类名(参数);
这里的参数要特殊解释一下,不同的异常规定的构造方法的参数也不一样。常见的参数有:无参数、字符串等。如果是无参数,那就是直接创建一个异常出来,如果是字符串做为参数,那么就是将详细的错误信息传进去。
1.2 使用
比如:
public static void main(String[] args) {
int a = 10, b = 0;
if(b == 0){
throw new ArithmeticException("除数不能为0");
}
System.out.println(a / b);
}
此时运行代码,再次观察终端信息。
此时显示的就是我们自己创建的异常,同时还将我们传入的错误信息打印了出来。
2、自定义异常
在日常的开发过程中,我们会发现某些特殊情况下的异常,这些异常是Java库中不存在的,此时就需要我们自己来定义了。那么如何定义呢?主要分为两步,首先你要区分你想定义刚刚所说的哪种异常(系统错误、编译时异常、运行时异常)。
(1)区分你要创建哪种异常
常见的为后两者,如果你这个错误非常严重,不处理不行,那么这种情况下,你可以创建编译时异常,为什么呢?
因为刚刚说了,编译时异常会在你写代码的时候,就用红色波浪线标识出来,提示你进行处理。所以这种情况下,就很适合那些非常严重的异常。此时你在自定义异常的时候,你的自定义异常类就要继承一个编译时异常作为父类,常见的比如说继承Exception,有人会说,不对呀,运行时异常(RuntimeException)也继承了这个类啊。是的,但这个是一个特殊情况。
如果你的自定义异常继承了Exception,那么就默认你的自定义异常是一个编译时异常。
如果说,你想定义一个运行时异常,那么你的自定义异常就要继承一个运行时异常作为父类,比如你直接继承RuntimeException。
如果你想定义的是一个系统错误,那么你就继承Error。这里要注意一个地方,如果你的异常已经到了Error的级别,说明你的异常原因是与JVM相关的,此时你即使进行处理了,作用也不大。
(2)创建一个异常类,并继承对应的父类
直接看例子:
比如我们创建一个编译时异常:
public class IndexException extends Exception {
public IndexException(){
super();
};
public IndexException(String str){
super(str);
}
}
我们也可以创建一个运行时异常:
public class IndexException extends RuntimeException {
public IndexException(){
super();
};
public IndexException(String str){
super(str);
}
}
同理也可以创建一个系统错误。
public class IndexException extends Error {
public IndexException(){
super();
};
public IndexException(String str){
super(str);
}
}
四、异常的处理
异常的处理大体上主要分为两部分,第一种就是自己处理,第二种就是交给调用者处理,即将异常抛出去。第一种需要使用,try-catch语句,第二种需要用到throws关键字。
通过刚刚的介绍,我们知道异常的分类可以分为两大类,第一类:非受查异常(Error,RuntimeException),第二类:受查异常(Exception)。
1、非受查异常
1.1 处理
这里要用到的是try-catch语句,语法如下:
try{
//可能会出错的代码
}catch(异常类 异常类名){
//处理方式
}finally{
//一般用来释放资源
}
如果不是必须的,finally部分可以不写。
当然有的时候,你try中可能产生的异常不止一个,此时你需要catch多种可能的异常,那么你可以写多个catch。如下:
try{
//可能会出错的代码
}catch(异常类1 异常类名1){
//处理方式
}catch(异常类2 异常类名2){
//处理方式
}catch(异常类3 异常类名3){
//处理方式
}finally{
//一般用来释放资源
}
这里要注意的是,这些异常最终只会被成功捕获一个,因为你的try语句中,只要抛出一个异常后,就会直接跳转到对应的catch语句中进行捕获,所以try中后续代码是不会再执行了,那么在这种情况下,try中后续代码既然不执行了,自然就不会再抛出其他异常。
那么,到底是哪个catch来捕获抛出的这个异常呢?此时会从第一个catch开始往下遍历,如果说这个catch中的异常类型是所抛出异常的类或者是其父类,那么就会又该catch进行捕获。
这里就会出现另外一个注意事项。catch的顺序问题,如果说你的第一个catch中的异常类是Exception
。这个类是所有编译时异常和运行时异常的父类,所以说,不管你抛出哪个异常,只要这个异常是Exception的子类,都会被这个catch所捕获,此时你后面的catch就没有用了。在这种情况下,我们就要把这种父类异常尽量往后写,前面的catch优先写细分的子类异常。
比如下面的例子:
public class Exception01 {
public static void main(String[] args) {
System.out.println(cal(1, 0));
return;
}
public static int cal(int a, int b) {
int res = 0;
try{
res = a / b;
}catch (ArithmeticException e){
res = Integer.MAX_VALUE;
}
return res;
}
}
当出现异常的时候,我让这个返回值的值为正无穷。
此时,我们运行结果:
它就没有打印错误信息,而是按照我们的方式去处理的这个异常。
当然我们也可以打印异常信息,此时需要调用这个方法e.printStackTrace()
:
public class Exception01 {
public static void main(String[] args) {
System.out.println(cal(1, 0));
return;
}
public static int cal(int a, int b) {
int res = 0;
try{
res = a / b;
}catch (ArithmeticException e){
e.printStackTrace();
}
return res;
}
}
我们会发现,打印完异常以后,并没有终止程序,而是继续执行剩下代码,因为最后打印了res的初始值0。这是因为,如果异常我们不手动处理的话,JVM会帮我们处理,比如之前的代码,我们没有try-catch,所以最终是JVM处理的。而JVM的处理方式非常果断,它会打印出异常信息,然后终止程序。
1.2 抛出
当我们遇到一个异常的时候,按照刚刚的说法,我们要写一个try-catch语句来处理该异常,但是如果我们不想处理呢?比如刚刚的例子,我们写了一个实现计算两数相除的函数,你传入了一个0进来,导致出现了异常,那既然是你导致的,能不能由调用者处理呢?答案是可以的。
此时,对于非受查异常而言,我们可以不管。
public class Exception01 {
public static void main(String[] args) {
System.out.println(cal(1, 0));
return;
}
public static int cal(int a, int b) {
int res = 0;
res = a / b;
return res;
}
}
此时,最终就会交给JVM处理。所以我们运行结果后,并没有打印结果,因为JVM直接终止程序了。
但这种不太负责任,因为相当于我们没有告诉调用者,可能会出现哪些异常,因此,我们引入了一个新的关键字:throws
关键字,这个关键字后面可以罗列出该函数可能出现的异常。不同的异常之间用,
隔开。
如下:
public class Exception01 {
public static void main(String[] args) {
System.out.println(cal(1, 0));
return;
}
public static int cal(int a, int b) throws ArithmeticException{
int res = 0;
res = a / b;
return res;
}
}
这样写完后,就相当于,我的cal函数没有处理这个异常,我选择交给调用者来处理,但是我告诉你了,我可能会产生哪些异常,处理或者不处理是你的事情。如果都不处理,那最后只能是由JVM处理了。
2、受查异常
受查异常的处理也是分为两种,要么try-catch,要么抛出去。
但是对于非受查异常而言,你写不写throws都可以,但是受查异常要么用try-catch,要么必须写throws声明要抛出的异常。
如果你什么都不写,编译会出错。
所以你有两种办法:
第一种:
public static int cal(int a, int b) {
int res = 0;
if(b == 0){
try {
throw new Exception();
} catch (Exception e) {
//处理办法
}
}
res = a / b;
return res;
}
第二种:
public static int cal(int a, int b)throws Exception {
int res = 0;
if(b == 0){
throw new Exception();
}
res = a / b;
return res;
}
这里有一个特殊情况,只能用第一种处理,就是如果你在重写某个方法,既然是重写,你就不能改函数签名,而你写throws就是在更改函数签名,所以如果该方法原本的函数签名中,没有声明抛出这个异常。你也不能在后面声明抛出。只能try-catch处理。