异常
介绍
什么是异常:程序执行过程中的不正常的情况比如试图打开一个根本不存在的文件等,异常处理将会改变程序的控制流程,让程序有机会对错误做出处理
异常的作用:增加程序的健壮性
异常以对象形式存在
异常对应现实生活是怎么样的?
类是模板,对象是实际存在的个体
火灾(异常类):
- 2008年8月8日,小明家着火了(异常对象)
- 2008年8月9日,小红家着火了(异常对象)
- 2008年8月10日,小刚家着火了(异常对象)
public class ExceptionTest01 {
public static void main(String[] args) {
//通过"异常类"实例化"异常对象"
NumberFormatException nfe = new NumberFormatException("数字格式化异常");
//java.lang.NumberFormatException: 数字格式化异常
//输出一个引用,实际上是调用nfe.toString()
System.out.println(nfe);
NullPointerException npe = new NullPointerException("空指针异常");
//java.lang.NullPointerException: 空指针异常
System.out.println(npe);
}
}
异常UML继承图
Unified Modeling Language(UML),是一种统一建模语言。只要是面向对象的编程语言,都有UML。一般画UML图的都是软件架构师或者说是系统分析师。在UML图中可以描述类和类之间的关系、程序执行的流程、对象的状态等。
Object下有Throwable(可抛出的)
Throwable下有两个分支:
- Error:不可处理,直接退出JVM
- Exception:可处理的
Exception下有两个分支:
- Exception的直接子类--编译时异常:Java程序必须显示处理,否则程序就会发生错误,无法通过编译(又叫做受检异常或者受控异常--CheckedException)
- RuntimeException--运行时异常:无需显示处理,也可以和编译时异常一样处理(又叫做未受检异常或者非受控异常UnCheckedException)
编译时异常和运行时异常,都是发生在运行阶段,编译阶段异常是不会发生的。
编译时异常因为什么而得名?因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此而得名。所有异常都是在运行阶段发生的,因为只有程序运行阶段才可以new对象,因为异常的发生就是new异常对象。
猜想:如果一个程序的过程是 编译→运行 这个过程的话,编译时异常是第一次编译能通过,且运行了然后抛出异常,不处理的话第二次就不能编译通过了?
两者区别:
编译时异常,一般发生的概率比较高 eg:你看到外面下雨了,倾盆大雨的。你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常),而且这个异常发生的概率很高,所以我们出门之前要拿一把伞。"那一把伞"就是对''生病异常"发生之前的一种处理方式。
运行时异常,一般发生的概率比较低 eg:小明走在大街上,可能会被天上的飞机*砸到,所以在出门之前你没必要对这种概率很低的异常进行预处理
假设java中没有对异常进行划分,所有异常都需要在编写程序阶段对其进行预处理,将是怎么样的效果呢?首先,如果这样的话,程序肯定是绝对的安全的。但是程序员编写程序太累,到处都是处理异常的代码
异常的处理方式
主要针对编译时异常,运行时异常可以不用处理。注意:只要异常没有上报,没有被捕捉,此方法的后续代码都不会执行
- 第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。谁调用我,我就抛给谁,抛给上一级
- 第二种方式:使用try...catch语句进行异常的捕捉。这件事发生了,谁也不知道,因为我给抓住了
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式。Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道了这个异常发生,只有一个结果。终止java程序的执行。
例子如下:
如果抛出的运行时异常,是不会报错的
public class ExceptionTest02 {
public static void main(String[] args) {
doSome();
}
//该异常是运行时异常
public static void doSome() throws ClassCastException{
System.out.println("doSome!!");
}
}
如果抛出的是编译时异常,这是就要进行处理了,不然会报错:
public class ExceptionTest02 {
public static void main(String[] args) {
//需要进行预先处理,否则编译器报错
doSome();
}
//该异常的直接父类是:Exception,属于编译时异常
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!");
}
}
第一种处理方法:上抛
public class ExceptionTest02 {
public static void main(String[] args) throws ClassNotFoundException{
doSome();
}
//该异常的直接父类是:Exception,属于编译时异常
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!");
}
}
第二种处理方法: 捕获(快捷键:Alt+Enter)
public class ExceptionTest02 {
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//该异常的直接父类是:Exception,属于编译时异常
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!");
}
}
都是一样的结果,如下:
异常方法
异常对象可以调用如下方法的得到或输出有关异常的信息:
- public String getMessage():取得异常描述信息
- public void printStackTrace():取得异常的堆栈信息(比较适合于程序的调试阶段)
- public String toString();
下面是验证异常方法的例子:
public class ExceptionTest04 {
public static void main(String[] args) {
NullPointerException e = new NullPointerException("空指针异常");
//获取异常简单描述信息:这个信息实际上就是构造方法上面的String参数
String msg = e.getMessage();
System.out.println(msg);
//打印异常堆栈异常信息
//java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的.
e.printStackTrace();
System.out.println("Hello World");
}
}
结果:
finally关键字
- 在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常
- finally子句必须和try一起出现,不能单独编写,可以没有catch
- 通常在finally语句中完成资源的释放/关闭
例子1
public class ExceptionTest06 {
public static void main(String[] args) {
FileInputStream fis=null;
try {
fis = new FileInputStream("E:\\JavaTest\\ExceptionTest\\test01.txt");
//这里一定会出现空指针异常
String s=null;
s.toString();
//fis.close();这里可能会因为异常而执行不到
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}finally{
System.out.println("Hello SpiderMan!");
if(fis!=null){ //避免空指针异常
try {
//流开了要关闭,因为会占用资源
//close()方法有异常,采用捕捉的方式
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("Hello Kitty!");
}
}
例子2
以下代码执行顺序:先执行try... 再执行finally... 最后执行return(return只要执行,方法必定结束)
public class ExceptionTest07 {
public static void main(String[] args) {
try {
System.out.println("try...");
return;
}finally {
//这里能被执行到,所以能写语句
System.out.println("finally...");
}
//这里不能执行到,所以不能写语句
//System.out.println("Hello");
}
}
例子3
public class ExceptionTest07 {
public static void main(String[] args) {
try {
System.out.println("try...");
System.exit(0);//退出JVM后,finally中的代码就不执行了
}finally {
//这里能被执行到,所以能写语句
System.out.println("finally...");
}
//这里不能执行到,所以不能写语句
//System.out.println("Hello");
}
}
例子4
以下必定遵循两大定则:
- 方法体中的代码必定遵循自上而下顺序依次逐行执行(亘古不变的语法!)
- return一定是最后执行的,一旦执行整个方法必须结束(亘古不变的语法!)
public class ExceptionTest08 {
public static void main(String[] args) {
System.out.println(m()); //结果不是101,而是100
}
public static int m(){
int i=100;
try{
return i;
}finally{
i++;
}
}
}
上面两大定则存在造就了以下的编译过程:(顺序执行使得先返回100,return最后执行使得i++也会执行)下面代码可以用反编译软件DJ Java DeCompiler 进行反编译出来
//反编译之后的效果
public static int m(){
int i=100;
int j=i;
i++;
return j;
}
final、finally、finalize
public class ExceptionTest09 {
public static void main(String[] args) {
//final是一个关键字.表示最终的,不变的
final int i=100;
//finally也是一个关键字,和try联合使用,使用在异常处理机制中
try{
}finally{
System.out.println("finally...");
}
//finalize是Object中的一个方法.作为方法名出现.
//所以finalize是标识符
//finalize()方法是JVM的GC垃圾回收器负责调用
Object obj;
}
}
系统异常类
ArithmeticException
ArithmeticException继承自RunTimeException,属于运行时异常。在编写程序阶段不需要对这种异常进行预先的处理
public class Test {
public static void main(String[] args){
int a=100;
int b=0;
/*实际上JVM执行到此处时,会new异常对象:new ArithmeticException("/ by zero")
并且JVM将new的异常对象抛出,打印输出信息到控制台了。由于是main方法调用了100/0,
所以该异常抛给了main方法,main方法没有处理,将这个异常自动抛给了JVM,
JVM最终终止程序的执行*/
System.out.println(a/b);
//这里的Hello World没有输出,没有执行
System.out.println("Hello World");
}
}
以下是异常信息,这个信息是JVM打印的
NullPointerException
public class Test {
public static void main(String[] args){
//int a=100;
//int b=0;
//System.out.println(a/b);
Person person=new Person();
person=null;
person.speak();
}
}
class Person{
void speak(){
System.out.println("讲话");
}
}
Java使用try-catch语句来处理异常,将可能出现的异常操作放在try-catch语句的try部分,将发生异常后处理放在catch部分,try中如果出现异常,就进入catch分支。
try-catch语句的格式如下:
try{
包含可能发生异常的语句
}
catch(ExceptionSubClass1 e){
... ...
}
catch(ExceptionSubClass2 e){
... ...
}
try-catch应用例子
public class Test {
public static void main(String[] args){
int a=100;
int b=0;
//Person person=new Person();
//person=null;
try{
//person.speak();
int c=a/b;
}catch(NullPointerException e){
System.out.println(e.getMessage());
}catch(ArithmeticException e){
System.out.println(e.getMessage());
}
}
}
class Person{
void speak(){
System.out.println("讲话");
}
}
没注释那些代码的话结果是null
FileNotFoundException
源码如下:
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
正确的文件路径-----E:\JavaTest\ExceptionTest\test01.txt
public class ExceptionTest03 {
//一般不建议在main方法上使用throws,因为这个异常如果真正发生了,一定会抛给JVM.JVM只有终止
//推荐用try..catch
public static void main(String[] args) {
System.out.println("main begin");
try {
m1();
System.out.println("不会执行");//不会执行
} catch (Exception e) {
e.printStackTrace();
}
//catch处理完异常后还是能执行代码的
System.out.println("main over");
}
//Exception是IOException的父类,只能往父类方向抛,不能往子类方向抛
public static void m1()throws Exception{
System.out.println("m1 begin");
m2();
System.out.println("m1 over");//不会执行
}
//能抛异常的父类,ClassCastException是运行时编译
public static void m2() throws IOException,ClassCastException {
System.out.println("m2 begin");
m3();
//不会执行
System.out.println("m2 over");
}
//FileNotFoundException父类是IOException,IOException的父类是Exception
public static void m3() throws FileNotFoundException {
System.out.println("m3 begin");
//创建一个输入流对象,该流指向一个文件
new FileInputStream("E:\\JavaTest\\ExceptionTest\\test02.txt");
//一个方法体中的代码出现异常之后,如果上报,此方法体结束
System.out.println("m3 over");//不会执行
}
}
结果如下, 红色是e.printStackTrace()打印出来的信息:
catch后面小括号里的类型可以是具体的异常类,也可以是父类
try{
FileInputStream fis=new FileInputStream("E:\\JavaTest\\ExceptionTest\\test02.txt");
}catch(IOException e){ //多态:IOException e =new FileNotFoundWException();
System.out.println("文件不存在!");
}
catch写多个时,从上到下,必须遵守从小到大(从子类到父类,不然会报错)
以下报错信息:Exception 'java.io.IOException' has already been caught
try {
m1();
System.out.println("不会执行");//不会执行
} catch (Exception e) {
e.printStackTrace();
} catch (IOException e){
}
JDK新特性
//JDK8新特性
try{
FileInputStream fis=new FileInputStream("E:\\JavaTest\\ExceptionTest\\test02.txt");
System.out.println(100/0);
}catch(IOException | ArithmeticException | NullPointerException e){
System.out.println("文件不存在?|数学异常?|空指针异常? ");
}
自定义异常类
我们也可以扩展Exception类定义自己的异常类,然后规定哪些方法产生这样的异常。
一个方法在声明时可以使用 throws 关键字声明要产生的若干个异常,并在该方法的方法体中具体给出产生异常的操作,即用相应的异常类创建对象,并使用 throws 关键字抛出该异常对象,导致该方法结束执行。
自定义异常步骤:
- 第一步:编写一个类继承Exception或者RuntimeException
- 第二步:提供两个构造方法,一个无参数的,一个带有String参数的
public class ExceptionTest05 {
public static void main(String[] args){
Bank bank=new Bank();
try{
bank.income(50,-100);
}catch(BankException e){
e.showMessage();
}
}
}
class BankException extends Exception{
public BankException() {
}
public BankException(String message) {
super(message);
}
void showMessage(){
System.out.println("发生异常了");
}
}
class Bank{
//收入为正,支出为负才是正常的
void income(int in,int out)throws BankException{
if(in<0 || out>0){
throw new BankException();
}
System.out.println("你的操作是正常的");
}
}
重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少
例子:子类抛出更多的异常
以下报错:java: com.chj.exception.Cat中的doSome()无法覆盖com.chj.exception.Animal中的doSome() 被覆盖的方法未抛出java.lang.Exception
class Animal{
public void doSome(){}
}
class Cat extends Animal{
@Override
public void doSome() throws Exception{
super.doSome();
}
}
例子:子类抛出更宽泛的异常
以下报错:java: com.chj.exception.Cat中的doSome()无法覆盖com.chj.exception.Animal中的doSome() 被覆盖的方法未抛出java.lang.Exception
class Animal{
public void doSome()throws NullPointerException{
}
}
class Cat extends Animal{
@Override
public void doSome() throws Exception{
}
}
总结异常中的关键字
异常捕捉:
try
catch
finally
throws 在方法申明位置上使用,表示上报异常信息给调用者
throw 手动抛出异常!