8.偏头痛杨的Java入门教学系列之异常篇

版权声明:偏头痛杨:本文为博主原创文章,未经作者允许不得转载,版权必究! https://blog.csdn.net/piantoutongyang/article/details/88094933


复习

1.使用接口的好处是什么?
2.Java的工作机制?
3.什么是方法重写与方法重载?
4.什么是Java三要素?
5.抽象类与接口的区别?
6.多态的三个必要条件?
7.this与super的用法?
8.final的用法?
9.方法有几种?分别是什么?区别是什么?
10.JDK&JRE&JVM分别是什么以及他们的关系?

前文链接

1.偏头痛杨的Java入门教学系列之认识Java篇
http://blog.csdn.net/piantoutongyang/article/details/70138697
2.偏头痛杨的Java入门教学系列之变量&数据类型篇
http://blog.csdn.net/piantoutongyang/article/details/70193622
3.偏头痛杨的Java入门教学系列之表达式&运算符&关键字&标识符&表达式篇
http://blog.csdn.net/piantoutongyang/article/details/71027446
4.偏头痛杨的Java入门教学系列之初级面向对象篇
http://blog.csdn.net/piantoutongyang/article/details/78135129
5.偏头痛杨的Java入门教学系列之流程控制语句篇
http://blog.csdn.net/piantoutongyang/article/details/71698589
6.偏头痛杨的Java入门教学系列之数组篇
http://blog.csdn.net/piantoutongyang/article/details/72510787
7.偏头痛杨的Java入门教学系列之进阶面向对象篇
http://blog.csdn.net/piantoutongyang/article/details/73176373


前戏

程序是程旭猿编的,程序猿也是人,是人就会有出错的地方,就会有想不到的地方,
那么程序在运行的时候就会有出错的地方,及时你能保证程序没有出错的地方,
但你能保证用户的输入就按照你的意愿来?用户不会输入一些非法信息导致程序出现问题?
你能保证网络传输永远良好?硬件设备永远不出问题?操作系统永远不出问题?等等等等。
有太多种的情况是我们无法保证了,那这些我们无法保证的情况出现后,程序出错了。

一旦程序出错了之后,我们应该如何处理?如何解决?
有没有一套机制来hold?是把异常信息记录到日志中?
还是在出现某种异常后,需要运行一些代码?
还是出现某种异常后短信&微信报警?还是什么也不做?
今天我们来学习Java的异常体系。


什么是异常

所谓异常就是不正常的情况,程序在运行中出现了程序猿们始料未及的状况。。
异常机制使得我们的程序拥有更强大的容错性、健壮性。
异常分两种情况讨论:运行期间出现的错误与编译时的语法错误。

编译期间的语法错误

所谓编译期间,就是在程序还没有真正运行之前,编译器需要把源代码编译成字节码。
例如:写程序的时候少写了一个分号,或者关键字写错了,语法错了,
此时你的IDE工具(eclipse或IDEA)会给你报错,注意,此时还没有进行编译。
一旦你编译后,便会出现java.lang.Error的提示,然后编译失败。

运行期间出现的错误

编译全部通过后,程序开始运行,运行期间,由于种种原因导致程序出现异常情况,
例如你使用11/0,Java是不允许0当作除数的,那么便会抛出java.lang.ArithmeticException,例如你调用了一个null对象的属性,那么便会抛出java.lang.NullPointerException,
此时程序继续执行,而不会中断。
当然,在运行时,也可能会出现java.lang.Error,导致程序中断,例如内存溢出等错误。
那对于运行期间出现的错误,就是今天我们要讨论的重点。

运行期间异常的出现时机

打开一个不存在的文件?
网络连接中断?
操作数组越界?
调用一个null对象的属性or方法?
用户输入了非法数据?

给2个小例子:

扫描二维码关注公众号,回复: 6045610 查看本文章
String name = null;
System.out.println(name.length());

上述代码执行之后会出现:
Exception in thread “main” java.lang.NullPointerException
这就是空指针异常,被誉为“世界上最简单的异常”。

int i = 0;
String greetings[] = {"Hello World", "Hello Dingdang", "Hello Kitty"};
while(i<4){
  System.out.println(greetings[i]);
  i++;
}

上述代码执行之后会出现:
Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: 3
这就是数组下标越界异常。


异常的分类与层次结构

异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。
Java中的异常信息通过对象来表示。
Java对异常的处理是按异常分类处理的,不同异常有不同的分类,
每种异常都对应一个类型(class),类与类之间具有继承关系,
每个异常都对应一个异常(类的)对象。
在这里插入图片描述

Throwable

Throwable有两个重要的子类:Exception(异常)和 Error(错误),
二者都是Java异常处理的重要子类,各自都包含大量子类。
通常,这些实例是在异常情况的上下文中新近创建的,因此包含了相关的信息(比如堆栈跟踪数据)。

Throwable类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时,
才能通过 Java 虚拟机或者 Java throw 语句抛出。
类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。

错误error

Error表示恢复不是不可能但很困难的情况下的一种严重问题。
错误不是异常,而是脱离程序员控制的问题,不可能指望程序能处理这样的情况。
错误在代码中通常被忽略。
错误类定义被认为是不能恢复的严重错误条件。
在大多数情况下,当遇到这样的错误时,建议让程序中断。
大多数错误与代码编写者执行的操作无关。

例如:系统崩溃,代码运行时JVM出现的问题,
Java虚拟机运行错误(Virtual MachineError),
类定义错误(NoClassDefFoundError),
内存溢出错误(OutOfMemoryError),当JVM不再有继续执行操作所需的内存资源时,
这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

异常exception

Exception是Java中所有异常的直接或间接父类,即Exception类是所有异常的根类。
Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件,
表示程序本身可以处理的异常。
Exception下面的异常又分两大类:检查性异常,非检查型异常。

检查性异常checked exception

检查性异常一般指程序编写没有错误,但程序运行后,可能由于用户的输入而造成的异常,
依赖于程序运行上下文。

若系统运行时可能产生该类异常,则必须写出相应的处理代码,否则无法通过编译,
checked异常相当于时刻在提醒程序员需要处理可能发生的异常,让程序变得更加严谨。
例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

非检查性异常unchecked exception

非检查异常也叫运行时异常runtime exception,运行时异常是可能被程序员避免的异常。
与检查性异常相反,运行时异常可以在编译时被忽略。
RuntimeException表示一种设计或实现问题,如果程序运行正常,从不会发生的情况,
默认不需要进行异常处理。比如,如果数组索引扩展不超出数组界限,
那么,ArrayIndexOutOfBoundsException异常不会抛出。
当程序中可能出现这类异常时,即使没有用try…catch语句捕获它,
也没有用throws字句声明抛出它,还是会编译通过,这种异常可以通过改进代码实现来避免。

注意事项

  1. Java中凡是继承自Exception,而不继承自RuntimeException类以及其子类的异常都是checked exception,需要检查。
  2. 异常类之间也有父子类继承的关系。
  3. 所有的异常类是从 java.lang.Exception 类继承的子类。
  4. Java程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

异常处理

异常处理允许程序捕获异常,处理捕获的异常,然后继续程序执行。
异常的处理过程:
抛出异常->捕获异常->处理异常

在Java程序执行过程中如果出现异常,系统会发出异常报告,这时系统将生成一个异常类对象,
异常类对象封装了异常事件的信息并将其提交给Java运行时系统。

需要试一下,如果没有catch住的异常,异常下面的程序是否还会被执行?
书上写的是不会执行直接退出了。。。

try-catch关键字

Java中可用于处理异常的两种方式:

自行处理

把可能引发异常的语句封入在try块内,
所谓try块就是使用try关键字,后面接着一个花括号,try{},花括号里面放置可能出现异常的代码,而处理异常的相应语句则封入在catch块内,所谓catch块就是使用catch关键字,后面接着一个小括号和花括号,catch(Exception e){},小括号里放置需要捕获的异常类,花括号里放置捕获异常处理的代码。

回避处理

在方法声明中包含throws子句,通知潜在调用者,如果发生了异常,必须由调用者处理。

try块里的代码如果没有抛出异常则程序继续往下正常运行,如果有抛出异常则进入catch块。
catch块是用来捕获并处理try块抛出的异常的代码块。没有try块,catch块不能单独存在。
我们可以有多个catch块,以捕获不同类型的异常。

try-catch用法

在这里插入图片描述
如果程序抛出多个不同类型的异常,我们需要多个catch()语句来处理。

try {

}catch (ArrayIndexOutOfBoundsException e) {
  System.out.println(“Out of Bounds!);
} catch (RuntimeException e) {
  System.out.println(“Runtime Exception!);
} catch (Exception e) {
  System.out.println(“Exception!);
}

当引发异常时,系统会按顺序来查看每个catch语句,并执行第一个类型与异常类型匹配的语句。
catch(Exception e)表示每一条catch语句对应一个异常对象,系统将异常对象传递给e,
允许程序员对e进行操作,以便查看更多的异常信息等。
程序执行其中的一条catch语句之后,其他的catch语句将被忽略。
catch块的顺序为异常类父子类顺序,父类写在下面,子类写在上面,否则下面的类永远得不到执行机会

在JDK7之后,java允许每个catch可以同时捕获多种类型的异常。

public class ExceptionDemo2 {
	public static void main(String[] args) throws Exception {
		try {
			eat(1);
		}catch(NullPointerException | ArrayIndexOutOfBoundsException e) {
			//jdk1.7允许每个catch块捕获多个异常
			System.out.println("jdk1.7");
			e.printStackTrace();
		}catch(Exception e) {
			e.printStackTrace();
		}
		System.out.println("看看抛异常后是否还会运行?");
	}
	
	public static void eat(int n) throws NullPointerException,ArrayIndexOutOfBoundsException,IOException {
		if(n==1) {
			throw new NullPointerException();
		}else if(n==2) {
			throw new ArrayIndexOutOfBoundsException();
		}else if(n==3) {
			throw new IOException();
		}
	}
}

访问异常信息

我们在catch块中会使用异常类的对象,并调用异常对象的方法。
例如调用printStackTrace()方法则是打出堆栈信息。

try{

}catch(Exception ex){
  ex.printStackTrace();
}

下面列出常见的异常方法:

方法定义 方法描述
public String getMessage() 返回关于发生的异常的描述信息(不详细)。
public Throwable getCause() 返回一个Throwable 对象代表异常原因。
public String toString() 使用getMessage()的结果返回类的串级名字。
public void printStackTrace() 打印异常的栈跟踪信息,常用于查找异常的原因。
public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
public Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

注意事项

  1. catch块里可以什么都不写,这样也算是处理异常,但通常不推荐这么做;
  2. 一旦异常被catch住,那么程序可以继续往下进行,除非你在catch块中又抛出了异常或返回;
  3. try{}和catch(){}之间不可以添加任何代码;
  4. try-catch是可以嵌套的;
  5. try{}中定义的变量属于局部变量,只能在try{}中使用;
  6. 如果没有catch住异常,那么理论上异常出现的代码之后的代码就没有机会执行了;

throws关键字

如果一个方法中的语句执行时可能抛出某种异常,但是并不能确定如何处理,
则可以在程序所在的方法声明后,使用throws关键字抛出异常 ,让调用者来解决这个问题。

class ThrowsDemo{
  public void proc() throws IOException{
    System.out.println("inside proc");
  }
}

你自己来决定出现异常后是由自己来处理还是往上抛,反弹给调用你方法的人。
父子类情况下的throws,如果子类重写父类方法:

  1. 如果父类方法没有throws异常,则子类方法也不能throws异常;
  2. 如果父类方法有throws异常,则子类方法throws的异常一定不能是父类异常的父类;
  3. 如果父类方法throws RuntimeException,则子类不能throws checked exception;

注意事项

  1. 所有的RuntimeException以及其子类异常,都不用显式的进行throws与try-catch,
    当然写了也不算错;
  2. throws后面的异常可以有多个,允许使用逗号分隔;

throw关键字

当程序出现异常时,一般情况下由系统自动抛出异常,但也可以由程序员手动抛出异常。
异常可以通过关键字throw抛出,不要与throws关键字混淆。

void doA() throws Exception {
  try {
    
  } catch(Exception1 e) {
    throw e;
  } catch(Exception2 e) {
    System.out.println("出错了");
  }
}

throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
如果抛出的是checked exception则不能单独使用,要么和try-catch一起使用,
要么和throws一起使用,如果抛出的是runtime exception,则允许单独使用。
throw语句的操作数一定是Throwable类类型或Throwable子类类型的一个对象。

catch与throw连用

  1. 有时候,在catch住异常进行相关处理后还需要调用方进行连带的处理,
    那么我们就需要在catch块进行相关处理后再throw异常,让调用方进行相应处理,
    如果catch块中没有写throw,则调用方是无法感知到异常存在的,同时也无法处理异常。
    类似于一个异常向上传递的过程。

  2. 我们完全没有必要把底层的一些异常直接抛到最上层,
    例如,不要把底层数据库的异常抛给用户去看,这样很不安全,
    因此可以通过在catch住sql的异常后,在catch块中做了相应的异常处理后,
    再通过throw往上抛一个业务相关的异常,这样对用户友好且安全。

finally关键字(不要与final关键字混淆)

finally语句放在try-catch语句后。
finally语句中的代码块不管异常是否被捕获总是要执行。

通常在finally语句中可以进行资源的清除操作,如:关闭打开文件、删除临时文件。

对应finally代码中的语句,即使try代码块和catch代码块中使用了return语句退出当前方法,
相关的finally代码块都有执行。

try{

catch (ArrayIndexOutOfBoundsException e) {
  System.out.println(“Out of Bounds!);
  return;
} catch (RuntimeException e) {
  System.out.println(“Runtime Exception!);
} catch (Exception e) {
  System.out.println(“Exception!);
} finally{
  System.out.println(“program is running into finally!);
}

注意事项

  1. 当try或catch代码块中执行了System.exit(0)时,finally代码块中的内容不被执行;
  2. 在try/catch后面添加finally块并非强制性要求的;
  3. try代码后不能既没catch块也没finally块,至少二选一,也可以同时出现;
  4. 垃圾回收器只能回内存上的内存,却不能回收物理资源,例如io、连接等等;

finally的大坑,非常常见的面试题

public static void main(String[] args) throws Exception {
  System.out.println("最后返回------>"+eat(null));
}

public static int eat(String name) {
  int temp = 0;
  try {
    if(name==null){
      throw new Exception();
    }
    return temp;
  } catch(Exception e){
    System.out.println("catch......");
    temp = 2;
    System.out.println("catch要准备返回了...");
    return temp;
  } finally {
    temp = 3;
    System.out.println("finally...");
    System.out.println("在finally的temp是"+temp);
  }
}

输出的结果是:
catch…
catch要准备返回了…
finally…
在finally的temp是3
最后返回------>2

为什么明明在finally里都已经把temp赋值3了,返回的依然是2呢?
在catch块中return的前一刻先按一下"暂停键",要返回的值依然是那个先不动,然后去调用finally,
无论finally怎么弄,都不会影响catch块中影响的结构,但除了一种情况,那就是在finally里写了return,
在finally中写了return和throw后,那么try/catch块中的return被throw就会被忽略掉。
因此尽量不要在finally中写throw和return;

finally {
  temp = 3;
  System.out.println("finally...");
  System.out.println("在finally的temp是"+temp);
  return temp;
}

输出的结果是:
catch…
catch要准备返回了…
finally…
在finally的temp是3
最后返回------>3

JDK7自动关闭资源的try块

JDK7之前,为了回收资源则需要通过编写finally块来搞定,显得十分臃肿。
JDK7之后,java提供了一种自动关闭资源的try块,来简化程序,try块程序执行结束后,
会自动关闭相关的资源,这里的try块允许独立存在,也可以接catch块与finally块。
JDK7将许多资源类进行改写,包括IO类、Connection、Statement等等,
让他们全部实现了AutoCloseable或Closeable接口。

相关的资源类必须实现AutoCloseable或Closeable接口,以及实现close()。

public class ExceptionDemo3 {
	public static void main(String[] args) throws Exception {
		//jdk7的会自动关闭资源的try块
		try (PrintStream ps = new PrintStream(new FileOutputStream("aaa.txt"));) {
			ps.println("abc");
		} 
	}
}

常见异常类

Java 语言定义了一些异常类在 java.lang 标准包中。
由于 java.lang 包是默认加载到所有的 Java 程序的,
所以大部分从运行时异常类继承而来的异常都可以直接使用。

在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,
有的需要程序员进行捕获处理或声明抛出,
有的是由Java虚拟机自动进行捕获处理。

常见unchenck异常

RuntimeException以及其子类

异常名字(英文) 异常名称(中文) 描述
ArrayIndexOutOfBoundsException 数组下标越界异常 数组索引越界异常,当对数组的索引值为负数或大于等于数组大小时抛出。
ArithmeticException 算术条件异常 整数除零等。
NullPointerException 空指针异常 当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
IllegalArgumentException 非法参数异常 表明向方法传递了一个不合法或不正确的参数。
ClassCastException 类型转换异常 当试图将对象强制转换为不是实例的子类时,抛出该异常。
NumberFormatException 数字转换异常 字符串转换为数字抛出的异常,当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

常见chenck异常

除了RuntimeException以及其子类之外的类

异常名字(英文) 异常名称(中文) 描述
ClassNotFoundException 找不到类异常 当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
FileNotFoundException 文件未找到异常 所需要的文件不存在。
NoSuchFieldException 属性未找到异常 所需要的属性不存在。
NoSuchMethodException 方法未找到异常 所需要的方法未不存在。
IllegalAccessException 非法访问某类异常 拒绝访问一个类的时候,抛出该异常。

自定义异常

异常类从哪里来?
1.Java语言本身定义的一些基本异常类型;
2.用户通过继承Exception类或者其子类自己定义的异常;
Exception类及其子类是Throwable一种形式,它指出了合理的应用程序想要捕获的条件。

如果Java提供的异常类型不能满足程序设计的需要,我们可以定义自己的异常类型。
我们可以通过判断自定义异常类,来做不同的异常处理,例如XXX异常需要短信报警,
YYY异常需要记录log,ZZZ异常需要去刷数据等等。

定义自己的异常类(这种异常类可以包含一个“普通”类所包含的任何东西)

如果你想写一个运行时异常类(unchecked exception),
那么需要继承 RuntimeException 类或其子类。

public class YangException extends RuntimeException {
     /**
      *
      */
     private static final long serialVersionUID = -1998686844673228031L;
     private String reason;
     private int port;

     public YangException(String reason, int port) {
          this.reason = reason;
          this.port = port;
     }

     public String getReason() {
          return reason;
     }

     public void setReason(String reason) {
          this.reason = reason;
     }

     public int getPort() {
          return port;
     }

     public void setPort(int port) {
          this.port = port;
     }
}

跟其他的异常一样处理

public class YangExceptionMainDemo {
     public static void main(String[] args) {
          try {
              eat();
          } catch (YangException e) {
              e.printStackTrace();
          }
     }

     public static void eat(){
          throw new YangException("hahaha",8080);
     }
}


异常使用建议

在日常的开发工作中,总结了一些所谓的经验,仅供参考。

  1. 不要滥用异常
    异常也需要耗费内存,过度使用异常机制不利于性能,尤其是高并发场景。
    抛出异常首先要创建一个新的对象。
    Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。
    只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。
    因此异常捕获是一件很”昂贵“的事情。
    异常机制确实方便好用,并不代表可以不节制的去滥用,能尽量不走异常机制的错误,
    尽量不走异常机制,例如:参数合法性校验,程序执行逻辑错误,逻辑控制等等。
    因此建议,对于那些我们全部能掌控的异常场景就不用异常机制,出乎意料外的场景再使用异常机制。

  2. 不要把try块设计的过大
    我们经常爱把一坨代码扔进try块中,因为方便对吗?但这样并不利于文异问题分析查找。

  3. 异常处理尽量放在高层进行
    尽量将异常统一抛给上层调用者,由上层调用者统一之时如何进行处理。
    如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,
    不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂。
    异常情况,不用挨个在controller里各种封装了,直接把抛出来的异常放到AOP里封装,
    两种自定义exception,一个是走json的,一个是走页面跳转的,就是抛出来一个自定义异常,
    然后封装成“系统内部错误”,不能让用户看到我们的具体出错信息。
    不用每个方法都throws了,也不用每个方法都try catch了,直接写一个自定义异常,让他继承至RuntimeException。

  4. 切忌使用空catch块
    在捕获了异常之后什么都不做,相当于忽略了这个异常。千万不要使用空的catch块,
    空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。
    如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用Log日志将该异常进行记录,
    以便日后方便更新和维护。

  5. 避免多次在日志信息中记录同一个异常
    只在异常最开始发生的地方进行日志信息记录(或者最外层)。
    很多情况下异常都是层层向上抛出的,如果在每次向上抛出的时候,
    都Log到日志系统中,则会导致无从查找异常发生的根源。


总结

如果在执行try块的代码时出现了异常,系统会自动生成一个Exception对象,该对象提交给java运行时环境,这个过程被称为抛异常。抛异常后,系统会寻找处理异常的catch块,这个过程被称为捕获异常。

Java异常处理的目的是提高程序的健壮性,你可以在catch和finally代码块中给程序一个修正机会,
使得程序不因异常而终止或者流程发生以外的改变。同时,通过获取Java异常信息,
也为程序的开发维护提供了方便,一般通过异常信息就很快就能找到出现异常的问题&代码所在。


作业

编写一个方法,入参为一个整型变量。

输入1则抛出空指针异常;
输入2则抛出数组下标越界异常;
输入3则抛出IO异常;
否则不抛异常,输出:“今天没有异常耶”。

不管是否有异常,都要输出:“方法正在呗调用”
异常需要在调用者的方法处理,

空指针异常则输出"给jack打电话"
IO指针异常则输出"给sean打电话"
数组下标越界异常则输出"给clarck打电话"

猜你喜欢

转载自blog.csdn.net/piantoutongyang/article/details/88094933