【后续内容:异常、集合、常用类,其实都是在面向对象的基础上,(即:我们需要解决一个实际的问题,Java为我们提供了一套API,API中可能有很多类、接口,它们之间有复杂的继承、实现关系),我们要做的就是学习这些类、接口,他们之间的关系,以及他们当中的一些方法】
一、异常概述:
-
异常:程序执行过程中的"不正常"情况,(开发过程中的语法错误、逻辑错误,不算异常)
【编写代码的过程中,不可能一点bug不出,很多问题并不是靠编码就能解决的,比如:用户输入的格式不对(整型故意或无意输错成String型),读取的文件不存在(误删了),网络是否保持畅通(断网了)】 -
错误分两类:
- Error:Java虚拟机也无法解决的严重问题,如:JVM系统内部错误(虚拟机自己都崩了)、资源耗尽等严重情况,这种问题一般不专门编写新代码来处理;
【解决不了,必须回头重写对应部分的源代码】
【例如:栈溢出 StackOverflowError,堆溢出 OutOfMemoryError(OOM)】 - Exception:其他因编程错误或偶然的外因造成的一般性问题,这种问题可以通过编写针对性的代码来处理;
【例如:空指针异常、试图读取的文件不存在、网络连接中断、数组下标越界】
- Error:Java虚拟机也无法解决的严重问题,如:JVM系统内部错误(虚拟机自己都崩了)、资源耗尽等严重情况,这种问题一般不专门编写新代码来处理;
【平时所说的异常都是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));
}
- 两种解决方案:
- 遇到错误,直接终止程序(即:啥也不做);
- 提前考虑到错误的检测、错误消息的提示,以及解决方案
【就像:出去旅游,途中生病了,①直接躺了,啥也干不了了;②或者提前考虑到可能感冒、肚子疼,带好了药,如果途中没生病,更好,如果生病了,该吃药吃药】
- 异常(Exception)的分类:
- 编译时异常:
- 运行时异常:
【处理错误的最佳时期是编译时,但有些错误只有在运行时才会发生(比如:除数为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】
用法:
- finally是可选的
- 使用try将可能出现的异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中依次匹配
- 一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理(进入一个catch后,不会再进入其他catch);一旦处理完成,就跳出当前try-catch结构(没有finally的情况下),继续执行try-catch之后的代码
- catch中的异常类型之间,若无子父类关系,则谁先谁后无所谓;若存在子父类关系,子类必须在前面,否则会报错(跟if-else稍有区别,不仅是执行不到,干脆编译就不通过)
- catch中常用的异常对象的处理方法:① String getMessage();② void printStackTrace()
- 在try结构中声明的变量,作用域就在try内,外部不可使用(想使用,就try外声明,try内赋值)
- try-catch-finally结构可以嵌套使用(比如案例代码)
注意:
- 体会1:使用try-catch-finally处理编译时异常,使得程序在编译时不再报错,但运行时仍可能出现异常
【相当于使用try-catch将编译时异常,延迟到了运行时出现】 - 体会2:实际开发中,通常也不会去处理运行时异常
【因为运行时异常比较常见,编译时也不会报错、很难发现,而且加不加try-catch,其实最后都是抛出一堆红字。所以通常不处理,出现异常了就回来改代码得了】
(当然,编译时异常必须处理,不然编译都过不了,程序跑都跑不起来)
finally的作用(finally中的代码是一定会被执行的)
- 【这跟不在finally中写,而直接写在try-catch后面有什么区别?】
finally中的代码是一定会被执行的,即使catch中又出现了异常,或者try中&catch中有return语句- catch中的代码可能还有异常(出现异常后,没有处理,方法直接结束了,try-catch后的代码也不会执行,但是finally中的一定会执行)
- 方法有返回值的话,try中、catch中有return语句,在return之前,finally中的代码也一定要执行
- 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 + 异常类型
用法:
- "throws + 异常类型"写在方法声明处。指明该方法执行时,可能会抛出的异常类型;
【一旦方法执行时出现了异常,仍会在异常代码处生成一个异常类的对象,此对象若满足throws的异常类型,就会被抛出】 - 谁调用该方法,就抛给谁;
【对于当前方法而言,该异常就相当于处理结束,(遇到异常,后续代码不会执行)】 - 调用该方法的方法,可以继续往上抛,直到main()方法;
【虽然main方法也可以继续抛,但那就是抛给JVM了,相当于完全没处理,所以一般情况下至少最终要在main方法中处理)】 - 调用的方法也可以自行 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):
- 继承于现有的异常结构:RuntimeException、Exception(前者是运行时异常,编译时不必处理;后者则必须显式处理,否则报错)
- 提供一个全局常量:serialVersionUID(序列号,可以理解为该类的一个"唯一标识",后期讲到IO流中的对象流会说)
- 提供几个重载的构造器
代码实例:
//见名知意
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()】