主要从这几个方面来说说Java中的异常:
1. 异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
异常的继承体系如下:
Throwable类是 Java 语言中所有错误或异常的父类。所以,如果要实现自定义异常,那么就要继承这个类或其子类。
Error:不做过多陈述,出现错误是非常严重的,因为Error是无法处理(虚拟机相关的问题:系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢)必须找到导致错误的根源,修改代码。
Exception:异常,也就是平时所说的“报错”,具体表现就是IDE控制台出现红色的信息,异常类以......Exception结尾,他有非常多的子类。
如图,我们都知道除数不能为0,当除数为0时,就会有一个算术异常(java.lang.ArithmeticException: / by zero):
异常的分类:如图2所示,在我们编写代码时,并没有出现任何错误提示,而当启动程序运行时,出现了这个异常。
我们把这个叫做:运行时异常(runtimeException) ;当写代码的过程中出现错误提示(红色波浪线),称为:编译错误。
常见的运行异常有1. NullPointerExceptin 空指针 2. IndexOutOfBoundsException 索引越界 3. ClassCastException 类转换 4. IllegalArgumentException 非法参数
问题:异常是怎么产生的(异常产生的过程)?
简单说一下,异常产生的过程。我们都知道,java程序都是经过编译后,运行在JVM(Java虚拟机)上,如图2,代码经过编译为.class文件,JVM加载.class文件开始运行:①JVM检测到程序做1除以0运算,但除数不能为0,于是,JVM创建了异常对象:exception = new ArithmeticException(); ②抛出异常对象(throw exception); ③把异常传递给方法调用者(这里为main),但main并没有处理这个异常,又传给了JVM;④JVM处理异常简单粗暴:打印异常信息,然后终止程序运行。
问题:以图2为例,JVM处理异常简单粗暴,那么我们能否在JVM处理前,自己把这个异常抛出?
当然可以,我们可以在做运算前检查一下参数,这里主要检查参数b。如果有问题自己在方法内抛出异常。代码如下:
public static void main(String[] args) {
int a = 1,b = 0;
getResult(a, b);
}
//计算a除以b的结果的方法
private static void getResult(int a, int b) {
//检查参数
if (b == 0){
//仿照JVM内,抛出(throw)一个(声明好的)算数异常
throw new ArithmeticException("b 不能为0");
}
int i = a/b;
System.out.println(i);
}
2. 那么,该如何在出现异常时优雅的处理异常,让程序不停止运行或者奔溃?
处理异常,离不开五个关键字:try、catch、finally、throw、throws。
①先说第一个关键字,throw(抛出):这个关键字,在上述示例中已经出现。有关throw关键字的使用:throw是使用在方法内,将这个异常传递(动态的抛出)到对象调用者处,并停止当前方法的执行(后面的方法也不会执行)。
格式:throw new 异常类名(参数)。 如:throw new NullPointerException("要访问的值不存在");
作用: 警告方法调用者,非法调用当前方法。
源码解读_Objects.requireNonNull()方法: 这是java.util.Objects工具类的一个查看指定引用对象不为空的静态方法,该方法主要用于验参数验证和构造函数。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
如果这个方法用于图2的参数验证和我们自己抛出异常效果是一样的,只不过直接使用,更方便。
②第二个关键字,throws(抛出):这里准确说为声明异常,即,将可能会出现的问题标识出来,报告给方法调用者,由调用者处理。【如果说异常是一个“流氓”,那么throws的处理方式为,我打不过你,我去叫人处理你;而稍后要说的try...catch,则一点“不怂”,直接踹你,把你KO了】
用法:运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
格式:方法名(参数)throws 异常类名1,异常类名2......{//方法体}
示例稍后展示;
③try...catch...(catch...)finally:
如果发生异常,不处理的话,程序就会立刻停止,这样带来的后果是很严重的,所以,我们要处理异常,即,用try..catch...
try中写的是可能会出现异常的代码,catch来捕获异常,当try中的代码出现问题时,catch就会立刻做出响应,捕获并处理异常;反之,try中的代码正常运行,也就没catch什么事了。
语法 (try和catch都不能单独使用,必须连用):
try{
//编写可能会出现异常的代码
}catch(异常类型 e){
//处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
public static void main(String[] args) {
method();
System.out.println("程序执行结束了");
}
private static void method() {
int[] array = {1,2,3};
try{
int e = array[3]; //JVM : throw new ArrayIndexOutOfBoundsException()
System.out.println(e);
}catch (Exception e){ // e = new ArrayIndexOutOfBoundsException()
// 发生异常并捕获了,执行这里
System.out.println("呵呵,异常发生了");
}
}
这样虽然捕获了异常,并且不影响后面的代码执行,但是,如果业务逻辑复杂,要想快速定位到发生异常的原因,位置,就要调用Throwable的
printStackTrace()方法,这个方法的作用是打印栈中追溯,简单说,就是打印异常信息(包含了异常的类型,原因和出现的位置)。只需要在catch中加入:
// 打印栈中追溯 (打印异常信息)
e.printStackTrace();
那么上面说的throws,将异常声明,由方法调用者处理,那么,方法调用者就需要try...catch来处理。
例:
public static void main(String[] args) {
String date = "2018-09-09"; //定义个时间,这里格式是正确的,可以试试错的
//调用方法,需要处理这个方法抛出的异常
try {
method(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
//将字符串类型时间转换成Date类型
private static void method(String date) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
//parse 方法可能有一个格式转换的问题,也就是参数格式个定义的转换格式不一致
//这里不处理,而是抛出
Date date1 = dateFormat.parse(date);
System.out.println(date1);
}
那么当,try中的代码可能发生多种异常时,那么可能就需要“分类讨论”,也就是分别列出可能发生异常情况;
语法:
try{
//编写可能会出现异常的代码
}catch(异常类型 e){
//处理异常的代码
//记录日志/打印异常信息/继续抛出异常
} catch (异常类型 e){
}
//以下代码只是示例,不具有任何实际意义
public static void main(String[] args) {
try {
method(1);
} catch (NullPointerException e) {
// 如果出现空指针异常
e.printStackTrace();
System.out.println(0);
}catch (IndexOutOfBoundsException e) {
//如果出现越界异常
e.printStackTrace();
System.out.println(1);
}
System.out.println("程序继续运行");
}
private static void method(int i) {
if(i == 0){
throw new NullPointerException("空指针异常");
}else{
throw new ArrayIndexOutOfBoundsException("越界异常了");
}
}
我们都知道Exception是所有异常的父类,可不可以直接用一个catch(Exception e)处理呢?当然是可以的。那么,这样“分类讨论”有什么意义呢?
当Java运行时,出现异常,就需要处理,而异常种类又是多种多样的,不同的异常,根据实际业务需求处理的方式可能不同,所以,需要不同的catch块来处理专门的异常。【多个catch块处理时,需要,注意异常等级需要从小到大,也就是第一个catch处理的异常一定不能是他的父类,因为父类异常一定能处理子类的异常,要是父类异常都处理完了,要子类异常来干嘛!】
最后来说说,finally代码块,finally意为最后的,在这里的意思就是,无论这个这个代码try了,还是catch了,finally都要执行。
public static void main(String[] args) {
try{
//可能出现异常的代码
int i = 1/0;
System.out.println(i);
}catch(NullPointerException e){
// 抓到异常,会执行这里
System.out.println("抓住了");
}finally {
// 无论如何都执行
System.out.println("无论如何都执行");
}
System.out.println("程序继续执行");
}
finally一般用在程序结束后释放资源之类的。
除了上面所说的,printStackTrace()方法输出异常信息之外,还有一些其他方法,访问异常信息:getMessage()方法返回该异常的详细描述字符串; printStackTrace(PrintStream s)方法将该异常的跟踪栈信息输出到指定输出流; getStackTrace()方法返回跟踪栈信息。
一般来说,使用printStackTrace()足够使用。
4. 最后是关于异常的注意点
①finally 与 return 之间的矛盾。
我们都知道return 标志着一个一个方法的结束,理论上return之后,任何代码不会执行。
但请看示例:
public static void main(String[] args) {
//调用定义的方法接收返回值
int result = method();
System.out.println("result:" + result);
}
private static int method() {
int i = 1;
try{
return i;
}catch (Exception e){
i = 2;
}finally {
i = 3;
System.out.println("finally:" + i);
return i;
}
}
在运行代码前?控制台会输出什么?按道理应该会是1.
事实却不是这样的:控制台会输出
finally:3
result:3
原因就在于:return 执行权 被finally 抢过去了,先记录了 i 的值 也就是数字1,而不是变量,等到finally 执行完, return接着执行 return i 而此时 i 被赋值为3了。
②方法重写和异常声明
问题:父类不抛出异常,子类可以抛出(throws)异常吗 ?
现有个父类Person,Person有个方法show,子类Student继承父类Person,并且重写了父类方法,那么,父类的方法没有抛出异常的情况下,子类重写的方法能抛出异常吗?
答案是:
父类方法如果没有抛出编译异常,那么子类重写的方法也不能抛出编译异常!!!!!!!
这里的IO异常属于编译异常。
这是为什么?
//父类引用指向子类对象
Person p = new Student();
p.show(); // 执行子类重写的方法
原因:
首先看上述代码示例。我们都知道异常分为两类,一个是编译时异常,一个是运行时异常。编译时异常在写代码的时候就要处理,而运行时期才会出现运行时异常。也就是说,这是一对矛盾,因为java中编译器编译时是只看 “=” 号左边,程序运行是看 “=” 号右边,而上述代码中“=”左边是父类引用,而父类方法没有抛出编译异常,所以不用处理,但是“=”右边new的是子类,子类重写的方法却去抛出编译异常。 矛盾点就在于编译异常的概念和运行看“=”右边出现矛盾,为了避免这种情况,就禁止这样去写。
5. 最后说一下自定义异常
什么是自定义异常?
在开发中根据自己业务的异常情况来定义异常类
为什么需要自定义异常?
Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。
如何自定义异常类?
自定义一个编译期异常: 自定义类 并继承于 java.lang.Exception 。
自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException 。
// 业务逻辑异常
public class LoginException extends Exception {
/**
* 空参构造
*/
public LoginException() {
} /
**
* *
@param message 表示异常提示
*/
public LoginException(String message) {
super(message);
}
}
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(LoginException e){
//处理异常
e.printStackTrace();
}
} /
/判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new LoginException("亲"+name+"已经被注册了!");
}
} r
eturn true;
}
}
推荐阅读: