Java基础——异常(Exception)

本文详细介绍了Java中的异常处理,包括异常概述、异常体系结构、异常处理模型和方式,强调了try-catch-finally和throws的使用场景。讲解了编译时异常与运行时异常的区别,并探讨了自定义异常的实现。最后总结了throw和throws的关键区别。

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

【后续内容:异常、集合、常用类,其实都是在面向对象的基础上,(即:我们需要解决一个实际的问题,Java为我们提供了一套API,API中可能有很多类、接口,它们之间有复杂的继承、实现关系),我们要做的就是学习这些类、接口,他们之间的关系,以及他们当中的一些方法】


一、异常概述:

  • 异常:程序执行过程中的"不正常"情况,(开发过程中的语法错误、逻辑错误,不算异常)
    【编写代码的过程中,不可能一点bug不出,很多问题并不是靠编码就能解决的,比如:用户输入的格式不对(整型故意或无意输错成String型),读取的文件不存在(误删了),网络是否保持畅通(断网了)】

  • 错误分两类:

    1. Error:Java虚拟机也无法解决的严重问题,如:JVM系统内部错误(虚拟机自己都崩了)、资源耗尽等严重情况,这种问题一般不专门编写新代码来处理;
      【解决不了,必须回头重写对应部分的源代码】
      【例如:栈溢出 StackOverflowError,堆溢出 OutOfMemoryError(OOM)】
    2. Exception:其他因编程错误或偶然的外因造成的一般性问题,这种问题可以通过编写针对性的代码来处理;
      【例如:空指针异常、试图读取的文件不存在、网络连接中断、数组下标越界】

【平时所说的异常都是Exception,因为Error是无法处理的严重错误,不考虑去专门处理,也处理不了,只能去重写错误源码】

public static void main(String[] args) {
	//栈溢出:java.lang.StackOverflowError
	main(args); //递归调用,没有出口
	
	//堆溢出:java.lang.OutOfMemoryError
	int[] arr = new int[1024 * 1024 * 1024];
	
	//空指针异常:java.lang.NullPointerException
	String str = null;
	System.out.println(str.charAt(0));
}
  • 两种解决方案:
    1. 遇到错误,直接终止程序(即:啥也不做);
    2. 提前考虑到错误的检测、错误消息的提示,以及解决方案

【就像:出去旅游,途中生病了,①直接躺了,啥也干不了了;②或者提前考虑到可能感冒、肚子疼,带好了药,如果途中没生病,更好,如果生病了,该吃药吃药】

  • 异常(Exception)的分类:
    1. 编译时异常:
    2. 运行时异常:

【处理错误的最佳时期是编译时,但有些错误只有在运行时才会发生(比如:除数为0,数组下标越界)】


二、异常体系结构

【图中Exception的子类中:红色为编译时异常,蓝色为运行时异常

补充:面试题:常见异常都有哪些?举例说明

 * java.lang.Throwable
 * 		|-----java.lang.Error:(一般不处理)
 * 		|-----java.lang.Exception:(需要处理)
 * 			|-----编译时异常(checked)【编译就不通过,提示"未处理xxx异常"(比如使用FileInputStream时,不try-catch,就会报错,提示没处理FileNotFoundException),单纯的语法写错了不叫编译时异常】
 * 				|-----IOException
 * 					|-----FileNotFoundException
 * 				|-----ClassNotFoundException
 * 			|-----运行时异常(unchecked,RuntimeException)
 * 				|-----NullPointerException
 * 				|-----ArrayIndexOutOfBoundsException
 * 				|-----ClassCastException
 * 				|-----NumberFormatException
 * 				|-----InputMismathException
 * 				|-----ArithmeticException

三、异常的处理模型:抓抛模型

3.1 过程一:“抛”

  • "抛":程序在正常执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出【抛给程序调用者】
    (一旦抛出异常对象后,其后的代码就不再执行了)

3.2 过程二:“抓”

  • "抓":即抓住这个被抛出的异常,可以理解为异常的处理方式(抓住之后怎么做):① try-catch-finally;② throws

四、异常的处理方式

4.1 为什么要有专门的异常处理?

程序运行过程中,可能出现很多种异常(除数为0、数据为空…),如果每个地方都添加if-else来检测处理,会导致代码臃肿、可读性很差,因此使用try-catch的方式,将可能有异常的进行集中处理,与正常代码区分开,使代码简洁、易于维护】

  • 方式一:try-catch-finally
  • 方式二:throws + 异常类型

4.2 方式一:try-catch-finally

【整体上类似if-else或switch-case】

用法:

  1. finally是可选的
  2. 使用try将可能出现的异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中依次匹配
  3. 一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理(进入一个catch后,不会再进入其他catch);一旦处理完成,就跳出当前try-catch结构(没有finally的情况下),继续执行try-catch之后的代码
  4. catch中的异常类型之间,若无子父类关系,则谁先谁后无所谓;若存在子父类关系,子类必须在前面,否则会报错(跟if-else稍有区别,不仅是执行不到,干脆编译就不通过)
  5. catch中常用的异常对象的处理方法:① String getMessage();② void printStackTrace()
  6. 在try结构中声明的变量,作用域就在try内,外部不可使用(想使用,就try外声明,try内赋值)
  7. try-catch-finally结构可以嵌套使用(比如案例代码)

注意:

  • 体会1:使用try-catch-finally处理编译时异常,使得程序在编译时不再报错,但运行时仍可能出现异常
    【相当于使用try-catch将编译时异常,延迟到了运行时出现】
  • 体会2:实际开发中,通常也不会去处理运行时异常
    【因为运行时异常比较常见,编译时也不会报错、很难发现,而且加不加try-catch,其实最后都是抛出一堆红字。所以通常不处理,出现异常了就回来改代码得了】
    (当然,编译时异常必须处理,不然编译都过不了,程序跑都跑不起来)

finally的作用(finally中的代码是一定会被执行的)

  • 【这跟不在finally中写,而直接写在try-catch后面有什么区别?】
    finally中的代码是一定会被执行的,即使catch中又出现了异常,或者try中&catch中有return语句
    1. catch中的代码可能还有异常(出现异常后,没有处理,方法直接结束了,try-catch后的代码也不会执行,但是finally中的一定会执行)
    2. 方法有返回值的话,try中、catch中有return语句,在return之前,finally中的代码也一定要执行
    3. GC机制只能回收JVM堆内存中的对象空间,对于其他的物理连接(比如:数据库连接、IO流、Socket连接等)无能为力,必须我们手动释放这些连接资源
      【即使出现异常,也必须要关掉这些连接,否则会造成"内存泄露" ----> 放在finally中】

【快捷操作:选中异常代码部分,右键—>Surround With —> Try/catch Block】

代码实例:

//try-catch方式处理异常
@Test
public void test2(){
	
	FileInputStream fis = null;
	try{
		File file = new File("hello.txt");
		fis = new FileInputStream(file);
		
		int data = fis.read();
		while(data != -1){
			System.out.println(data);
			data = fis.read();
		}
	}catch(FileNotFoundException e){
		System.out.println("文件未找到异常!");
		
		//catch中常用的两个方法:
		System.out.println(e.getMessage());
		e.printStackTrace();
	}catch(IOException e){
		e.printStackTrace();
	}finally{
		try {
			if(fis != null) //为了避免空指针异常(fis本身也可能出异常,即:根本没创建成功)
				fis.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}

4.3 方式二:throws + 异常类型

用法:

  1. "throws + 异常类型"写在方法声明处。指明该方法执行时,可能会抛出的异常类型;
    【一旦方法执行时出现了异常,仍会在异常代码处生成一个异常类的对象,此对象若满足throws的异常类型,就会被抛出】
  2. 谁调用该方法,就抛给谁
    【对于当前方法而言,该异常就相当于处理结束,(遇到异常,后续代码不会执行)】
  3. 调用该方法的方法,可以继续往上抛,直到main()方法;
    【虽然main方法也可以继续抛,但那就是抛给JVM了,相当于完全没处理,所以一般情况下至少最终要在main方法中处理)】
  4. 调用的方法也可以自行 try-catch 处理,这样该异常就在这一层被解决了,再往上层的调用就不会有异常了

注意:

  • 体会:throws本质上相当于没有处理掉异常,只是抛给调用者来处理,治标不治本;而try-catch-finally才是真正地将异常处理掉了

代码实例:

//throws测试
public static void main(String[] args) {
	//最终要在main()方法中解决,(不要再继续抛了,再抛就又抛给JVM了)
	try {
		method4();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

public static void method4() throws IOException{
	method3();
}

public static void method3() throws FileNotFoundException, IOException{
	
	File file = new File("hello.txt");
	FileInputStream fis = new FileInputStream(file);
	
	int data = fis.read();
	while(data != -1){
		System.out.println(data);
		data = fis.read();
	}
	
	fis.close();
}

补充:为什么子类中重写的方法,抛出的异常必须比父类抛出的异常小?

【因为体现多态性的时候,方法调用,形参是父类对象,方法中就要对父类抛出的异常进行处理(例如:IOException);如果子类中抛出的异常比父类小(例如:FileNotFoundException),那么到这里,catch依然能罩得住;但如果比父类异常大(例如:Exception),那么在该方法内,catch(IOException e)就罩不住了】
【相当于语言设计本身有问题了:我明明都把异常处理了,你还报异常】


4.4 开发中如何选择使用try-catch-finally还是throws

  • 如果父类中被重写的方法没有throws抛出异常,则子类中重写的方法也不能throws(因为要求必须比父类异常要"小"),这就意味着如果子类的重写方法中出现了异常,只能使用try-catch自行处理,不能向上throws
  • 执行的方法func()中,先后调用了另外的几个方法(例如:A()、B()、C()),而这几个方法之间是递进关系的(例如:A的运行结果,作为B的参数,B的结果,又作为C的参数),
    【此时我们建议:若A、B、C方法中有异常,不要自行进行try-catch,而是throws,由调用他们的方法func()来try-catch处理】
    【因为:①各自处理,代码繁琐,不如统一处理了;②A、B、C是递进关系,A出现了异常的话,通常得出的结果对B来说也用不了,如果A中处理了异常,那么程序就能正常往下执行,结果就会传给B,然而结果已经错误了,传给B也没用,这样逻辑上也不合适】

注意

try-catch异常处理只是为了处理编译时异常,真要在运行时出现了异常,给个友好提示,最终还是要找到错误原因,回去改代码
【即:一旦出现问题,给用户一个友好的展示,而不是一堆乱码;实际上还是要发到后台,进行记录,然后由我们去修改源码,让其不再有异常出现】
【即:目的仍然是,例如:当用户点了一个按钮以后,出现的是该出现的界面,而不是一个友好的提示"出现问题了"】


五、手动抛出异常

关于异常对象的产生:

  • 系统自动生成的异常对象(上述所有异常都是如此)
  • 手动生成一个异常对象,并抛出(throw)
    【一般throw的都是Exception或者RuntimeException】

注意:区分 throw 和 throws

  • throw是在"抛"的过程中,产生异常对象的一种方式
  • throws是在"抓"的过程中,处理异常的一种方式
public class ThrowTest {
	public static void main(String[] args) {
		try {
			Student stu = new Student();
			stu.register(-1001); //输入数据非法的时候,后续代码不应该执行了,(否则容易误认为是 "id=0")
			System.out.println(stu);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}
}

class Student{
	private int id;
	
	public void register(int id) throws Exception{
		if(id > 0){
			this.id = id;
		}else{
//			System.out.println("输入数据非法!");
//			throw new RuntimeException("输入数据非法!");
			throw new Exception("输入数据非法!");
		}
	}

	@Override
	public String toString() {
		return "Student [id=" + id + "]";
	}
}

六、自定义异常

  • 如何自定义异常类(仿照已有的异常类,例如:Exception):
    1. 继承于现有的异常结构:RuntimeException、Exception(前者是运行时异常,编译时不必处理;后者则必须显式处理,否则报错)
    2. 提供一个全局常量:serialVersionUID(序列号,可以理解为该类的一个"唯一标识",后期讲到IO流中的对象流会说)
    3. 提供几个重载的构造器

代码实例:

//见名知意
class NegativeNumberException extends Exception{
	static final long serialVersionUID = -7034897196220766939L; //序列号
	
	public NegativeNumberException() {
        super();
    }
	
	public NegativeNumberException(String message) {
        super(message);
    }
}

class Student{
	private int id;
	
	public void register(int id) throws Exception{
		if(id > 0){
			this.id = id;
		}else{
			//不能输入负数
			throw new NegativeNumberException("不能输入负数!");
		}
	}

}

七、总结

在这里插入图片描述
用如上5个关键字,就能总结异常部分的总体内容


注意:throw 和 throws 的区别

  • throw是"抛"异常的过程中,生成异常对象的一种方式;throws则是"抓"异常的过程中,处理异常的一种方式
    【throw:我还没异常对象呢,怎么生成一个对象;throws:已经出现异常对象了,我怎么处理】
    【二者的关系:类似于"上游排污,下游治污"】

面试题:final、finally、finallize()三者的区别

  • 三者没啥关系,分开说清楚就可以了

【类似的一系列面试题(结构很相似的,可能真就没啥关系):throw和throws;Collection和Collections;String、StringBuffer和StringBuilder;ArrayList和LinkedList;HashMap和LinkedHashMap;重写、重载】

【还有另外一系列面试题(结构不相似的,反而可能有相似之处):抽象类、接口;== 和 equals();sleep()和wait()】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值