JAVA学习笔记17-(异常)

本文深入探讨Java异常处理机制,包括异常的概念、分类、处理方式及自定义异常,旨在增强程序健壮性和错误处理能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


异常

异常是java中一个非常有用的机制,能够让程序员快速定位问题所在并对其进行修改,也能去提醒使用者所出现的错误等等。


异常概述

什么是异常,java提供异常处理机制有什么用?
	以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
    java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况,
    java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对
    程序进行修改,让程序更加的健壮。

    什么是异常:程序执行过程中的不正常情况。
    异常的作用:增强程序的健壮性。

例如在下面代码中,我们尝试去用10除以0,会触发一个异常:

int a = 10;
        int b = 0;
        
        实际上JVM在执行到此处的时候,会new异常对象:new ArithmeticException("/ by zero");
        并且JVM将new的异常对象抛出,打印输出信息到控制台了。
        int c = a / b;
        System.out.println(a + "/" + b + "=" + c);

这段代码触发异常后会在控制台输出如下:

Exception in thread "main" java.lang.ArithmeticException: / by zeroat com.bjpowernode.javase.exception.ExceptionTest01.main(ExceptionTest01.java:14)
这个信息被我们称为:异常信息。这个信息是JVM打印的。

这样,当程序员观察到异常之后,便可以对代码进行维护了:

int a = 10;
int b = 2;
if(b == 0) {
	System.out.println("除数不能为0");
	return;
}
程序执行到此处表示除数一定不是0
int c = a / b;
System.out.println(a + "/" + b + "=" + c);

java语言中异常是以什么形式存在的呢?

异常在java中以类的形式存在,每一个异常类都可以创建异常对象。
通过“异常类”实例化“异常对象”
NumberFormatException nfe = new NumberFormatException("数字格式化异常!");

System.out.println(nfe);
java.lang.NumberFormatException: 数字格式化异常!

异常的继承结构

通过翻源代码或者API可以得知,异常的老祖宗也是Object

往下则是Object下的一个Throwable(可抛出的)

Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)

Exception下有两个分支:
	Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)。
	RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也可以不管,都行。)

在这里插入图片描述
该图仅是写出了大体的关系,当然JAVA中存在着很多的异常,具体详询API文档。
API链接: JDK11版本.
API链接: JDK8版本.


编译时异常和运行时异常

异常在Exception中又有Exception的直接子类和RuntimeException
分别为编译时异常和运行时异常。
编译时异常和运行时异常,都是发生在运行阶段。 编译阶段异常是不会发生的,因为异常首先就是要创建异常对象!编译阶段是不会创建对象的!

编译时异常因为什么而得名?
	因为编译时异常必须在编译(编写)阶段预先处理(通过throws或者try catch),如果不处理编译器报错,因此得名。
	所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象。
	因为异常的发生就是new异常对象。

编译时异常和运行时异常的区别?
编译时异常一般发生的概率比较高。
对于一些发生概率较高的异常,需要在运行之前对其进行预处理。
(例如你出门前看到要下雨了,带把伞,这就是对发生概率较高的异常的预处理。)

运行时异常一般发生的概率比较低。
所以一般不需要对其进行预处理,否则会太累
(就像你出门前做好万全准备,防护到头发去预防全部可能出现的危险一样,
但是有些危险很明显是可能性很小的,例如你出门被飞机砸到一样
这些危险可以预防,但没必要。)

编译时异常还有其他名字:

受检异常:CheckedException
受控异常

运行时异常还有其它名字:

未受检异常:UnCheckedException
非受控异常


异常的处理方式

在JAVA中对异常有两种处理方式:

1、通过throws向上抛出异常
2、通过try···catch去抓取并处理异常

关于此有个很形象的描述:
A在工作时由于异常操作导致公司亏损
他有两种方法去补救:
1、上报上级,交给他们解决。
2、自己掏腰包补偿亏损,这样只有他自己知道。

当然如果上级上报上级,再上报上报···到最后就是上报到CEO处也无法解决时(throws到main方法还未解决异常),公司就倒闭了(程序停止运行)

注意:
编译时异常是必须去处理的,无论通过方式1或者2
但是运行时异常可以不用去写代码预处理,出了问题提示了再去处理也可

Throws向上抛出异常

处理异常的第一种方式:
在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁。抛给调用者来处理。
这种处理异常的态度:上报。
例如:

public static void doSome() throws ClassNotFoundException{
	System.out.println("doSome!!!!");
}
这个代码表示doSome()方法在执行过程中,有可能会出现ClassNotFoundException异常。
在方法声明后面使用throws关键字抛出,即可将异常抛给这个方法的调用者。

下面在main方法中调用这个方法

public static void main(String[] args){
	doSome();
	
	这里要注意!doSome方法抛给你了一个异常,你就得对他处理!
	同样,调用者有两种方式。throws或者try catch
	也就是要么继续上报要么尝试解决。
	但是main方法中不要上报!
	main方法上报给了JVM,JVM只有终止程序的执行。
	这与我们异常处理的初衷:增强程序的健壮性相违背。
	我们希望的是在出现异常时能够解决并且不去影响程序的运行。
	所以一般在main方法中就会用try catch去处理异常!

	try {
		doSome();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
		这行话意思是去打印堆错误信息,也就是提醒程序员错误。
	}
}

注意:只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。


通过try···catch去抓取,处理异常

处理异常的第二种方式:
使用try…catch语句对异常进行捕捉。
这个异常不会上报,自己把这个事儿处理了。
异常抛到此处为止,不再上抛了。
使用方法:

	try 
	{
		doSome();
		这里是先尝试去执行的语句
	} 
	
	catch(异常类型 自变量){}
	这里是当抓取到异常时,注意抓取的异常类型必须是人家抛出的异常类型,
	不能人抛出了异常A,你却去catch异常B
	当然你可以去catch这个异常的父类,这是允许的,其实就是多态。
	
	catch (ClassNotFoundException e) 
	{
		e.printStackTrace();
		这里调用了一个异常处理的方法,下面说。
	}

值得注意的是:
1、try语句块中的某一行出现异常,该行后面的代码不会执行。
2、try…catch捕捉异常之后,后续代码可以执行。
什么意思呢?

	try {
		doSome();
		
		当执行到doSome() 他抛出了一个异常ClassNotFoundException
		这个时候就会转到catch里面了,这下面的语句不会被执行!
		
		System.out.println("123");这句话不会执行!
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}
	
	但是catch完之后,这下面的会继续正常执行!这就保证了没影响程序的健壮性!即发生了异常也不影响我们程序的整体运行!
	
	System.out.println("456");这句话会执行!

深入try…catch
1catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3catch写多个的时候,从上到下,必须遵守从小到大。也就是得先子类型的异常,再父类型的异常
4、JDK8之后,catch也可以这样写!
catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
	System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}

try…catch中的finally子句
1、在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。
finally子句必须和try一起出现,不能单独编写。

2finally语句通常使用在哪些情况下呢?
通常在finally语句块中完成资源的释放/关闭。
因为finally中的代码比较有保障。
即使try语句块中的代码出现异常,finally中代码也会正常执行。

具体的语法如下:

try{
	···
}catch(){
	···
}finally{
	···
}

注意点:

放在finally语句块中的代码是一定会执行的【再次强调!!!】
tryfinally,没有catch可以吗?可以。
try不能单独使用。
try finally可以联合使用。

如下代码:

try {
	System.out.println("try...");
	return;
} finally {
	finally中的语句会执行。能执行到。
	System.out.println("finally...");
}

以上代码的执行顺序:
先执行try...
再执行finally...
最后执行 returnreturn语句只要执行方法必然结束。)

当然也有例外情况(也不算啦):

try {
	System.out.println("try...");
	
	退出JVM语句:
	System.exit(0);
	退出JVM之后,finally语句中的代码就不执行了!
	
} finally {
	System.out.println("finally...");
}

final finally finalize
final finally finalize有什么区别?
    final 关键字
        final修饰的类无法继承
        final修饰的方法无法覆盖
        final修饰的变量不能重新赋值。

    finally 关键字
        和try一起联合使用。
        finally语句块中的代码是必须执行的。

    finalize 标识符
        是一个Object类中的方法名。
        这个方法是由垃圾回收器GC负责调用的。

具体见前面的笔记。


一个关于try···catch的奇怪题目
先定义这么一个方法m
public static int m(){
	int i = 100;
	try {
		return i;
	} finally {
		i++;
	}
}

然后在主方法去调用这个方法
public static void main(String[] args) {
	int result = m();
	System.out.println(result);
}

是不是会觉得打印出101?因为finally语句不是无论如何都会执行,且return在最后执行吗?
错了!打印答案是100!!!


						我  也  傻  了

已知:
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):

java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)

java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)

再回过去看m方法
public static int m(){
	int i = 100;
	try {
		return i;
		这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
		return语句还必须保证是最后执行的。一旦执行,整个方法结束。
	} finally {
		i++;
	}
}

也就是说,finall的确也执行了
但是由于return i 这句的确就是在人机int i = 100正下方
所以就返回了100!

可以通过反编译其class文件看到深处:
反编译之后的效果
public static int m(){
    int i = 100;
    int j = i;
    i++;的确他进行了finally的语句,且在return之前执行的
    return j;的确他也是按顺序直接返回了100
    人家没毛病!
}
所以这样JAVA就做到了不打自己脸()
就难为我呗

异常对象的方法

在try···catch语句中,我们在catch后括号中生命了个对象

	try {
		doSome();
	} catch (ClassNotFoundException e) {
		这个e就是
		e.printStackTrace();
	}

我们自然可以通过他去调用方法!
异常对象有两个非常重要的方法:

获取异常简单的描述信息:
String msg = exception.getMessage();
System.out.println(msg);

打印异常追踪的堆栈信息:
exception.printStackTrace();

例如如下用法:

NullPointerException e = new NullPointerException("空指针异常12345");

获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。
String msg = e.getMessage(); 
System.out.println(msg);
空指针异常12345

打印异常堆栈信息
java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。
e.printStackTrace();
java.lang.NullPointerException: 空指针异常12345
	at com.bjpowernode.javase.exception.ExceptionTest08.main(ExceptionTest08.java:15)

这两个方法都能帮助我们去快速的定位到异常所在,然后对其进行修改和维护。

如何查看异常信息呢?

异常信息追踪信息,从上往下一行一行看。
但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的。)。
主要的问题是出现在自己编写的代码上。

创建自己的异常对象

SUN提供的JDK内置的异常肯定是不够的用的。在实际的开发中,有很多业务,这些业务出现异常之后,JDK中都是没有的。和业务挂钩的。那么异常类我们程序员可以自己定义吗?

可以。

语法很简单,参照源代码的异常创建方式即可:

第一步:编写一个类继承Exception或者RuntimeException.
第二步:提供两个构造方法,一个无参数的,一个带有String参数的。

编译时异常:
public class MyException extends Exception{
    public MyException(){

    }
    public MyException(String s){
        super(s);
    }
}

运行时异常
public class MyException extends RuntimeException{

}

利用自己创建的异常

在前面的笔记中我们写了一个作业:
运用数组去模拟栈结构,并完成压栈和弹栈操作
当栈满时再压栈便会触发异常,这个时候我们可以自己定义一个异常并在这个时候抛出异常!从而完成提醒异常的目的!

public void push(Object obj) throws MyStackOperationException {
	if(index >= elements.length - 1){
		改良之前
		System.out.println("压栈失败,栈已满!");
        return;
		
		改良之后
        创建异常对象
        MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
        手动将异常抛出去!
        throw e;
        这里捕捉没有意义,自己new一个异常,自己捉,没有意义。栈已满这个信息你需要传递出去。

        合并(手动抛出异常!)
        throw new MyStackOperationException("压栈失败,栈已满!");
        
        }
        程序能够走到这里,说明栈没满
        向栈中加1个元素,栈帧向上移动一个位置。
        index++;
        elements[index] = obj;
        在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
        System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}

方法覆盖与异常

之前在讲解方法覆盖的时候,当时遗留了一个问题?
重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少。
一般来说覆盖直接复制粘贴就可!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值