一.异常
1.定义:Java程序也是会存在某些不正常的情况的,这些不正常的情况我们就统称异常。
2.异常体系:
--------| Throwable(Throwable类是Java语言中所有错误或异常的超类)
------------| Error(错误,错误一般是由Jvm或者是硬件引发的问题,所以我们一般不会通过修改代码去处理错误的)
------------| Exception(异常,是需要通过代码去处理的)
3.Throwable常用的方法:
(1) toString():返回当前异常对象的完整类名+病态信息。
(2) getMessage():返回的是创建Throwable传入的字符串信息。
(3) printStackTrace():打印异常的栈信息。这个方法以后会经常用到,有了这个方法以后我们想找这个程序所存在的问题是非常方便的,很快就可以定位到。而且打印出来的异常信息,也会让我们知道这是个什么样子的病。
4.实例:
(1) 实例一
class Demo1{
public static void main(String[] args){
//创建了一个Throwable对象。
Throwable t = new Throwable("头晕,感冒……");
String info = t.toString();
String message = t.getMessage();
System.out.println("toString:"+info);
//java.lang.Throwable 包名+类名 = 完整类名
System.out.println("message:"+message);
}
}
运行结果如下图所示:
(2) 实例二
class Demo2{
public static void main(String[] args){
test();
}
public static void test(){
Throwable t = new Throwable();//相当于程序在这里生病
t.printStackTrace();
}
}
运行结果如下图所示:
运行结果显示,最上面一行是描述这种病的类,先告诉你程序有了哪些病,紧接着这个病是由第6行引发的,而第6行的病导致了第3行的调用也出了问题。这就很像医生看病,先告诉你是什么病,这个病现在是在哪一行,一层一层的追踪病源下去。
5.如何区分错误与异常呢?
如果程序出现了不正常的信息,如果不正常的信息的类名是以Error结尾的,那么肯定是一个错误;如果是以Exception结尾的,那么肯定就是一个异常。
二.异常的处理
1.实例一:
(1) 实例:
class Demo1{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
int c = a/b;
System.out.println("c="+c);
}
}
(2) 运行结果如下图所示:
(3) 上面的信息是通过printStackTrace方法打印出来,(必须得有异常的对象才能调用到这个方法)那么异常对象从何而来呢?
Jvm运行到a/b这个语句的时候,发现b为0,除数为0在我们现实生活中是属于不正常的情况,Jvm一旦发现了这种不正常的情况时候,那么Jvm就会马上创建一个对应的异常对象,并且会调用这个异常对象 的printStackTrace的方法来处理。当Jvm遇到一个异常时比如在c = a/b发现除数为0.就不会再正常的继续往下执行。要想程序可以继续往下执行就必须要处理这个异常。如果不处理异常,程序还是会在这里生病,就不会再继续往下执行。这时候我们就要学习异常的处理方式;
2.处理异常的两种方式:
(1) 方式一:捕获处理;
(2) 方式二:抛出处理;
和我们现实中有病要去医院,医院的处理方式也是两种:如果年轻人得什么病,医院会要求你住院,这就相当于捕获处理;如果是那种老人,去小镇上的医院治病,那儿的医生不敢接,碰到这种情况,医院会抛出处理,转到大医院去。
3.捕获处理:
(1) 捕获处理的格式:
try{
可能发生异常的代码;
}catch(捕获的异常类型 变量名){
处理异常的代码;
}
注意:必须强调是可能发生异常的代码,而不是发生异常的代码,因为c = a/b并不是一定会出现异常,如果b不为0就不会有异常,所以只是有可能出现异常而已。
(2) 捕获处理要注意的细节:
① 如果try块中代码出了异常经过了处理之后,那么try-catch块外面的代码可以正常执行。
② 如果try块中出了异常的代码,那么在try块中出现异常代码后面的代码是不会执行了。
③ 一个try块后面是可以跟有多个catch块的,也就是一个try块可以捕获多种异常的类型。
④ 一个try块可以捕获多种异常的类型,但是捕获的异常类型必须从小到大进行捕获,否则编译报错。
(3) 捕获处理实例
①实例一: 一个try块中只有一种类型的异常
class Demo2{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
int c = 0;
try{
//变量c的作用域是这个大括号,出了这个大括号就不可见,所以应该把c声明到外面
c = a/b;
}catch(ArithmeticException e){//通过变量e就可以找到这个异常对象
//处理异常的代码
System.out.println("toString:"+e.toString());
}
System.out.println("c="+c);//经过异常处理后下面的代码也可以输出
}
}
运行结果如下图所示:
②实例二: 一个try块中有多种类型的异常
class Demo3{
public static void main(String[] args){
int[] arr = null;
div(4,2,arr);
}
public static void div(int a,int b,int[] arr){
int c = 0;
try{
c = a/b;
System.out.println("数组的长度:"+arr.length);
}catch(ArithmeticException e){
//处理异常的代码
System.out.println("toString:"+e.toString());
}
System.out.println("c="+c);
}
}
运行结果如下图所示:
疑问一:为什么有处理异常的代码,但却没有输出try-catch块外的语句?
因为代码中捕获处理捕获的是除数为0的异常,而现在报的是空指针异常,因为捕获的不是这种异常,所以不会进行处理。就好像你感冒发烧跑去妇产科那里也不会接收你,这时候就必须再加一个捕获异常处理。如下所示:
class Demo4{
public static void main(String[] args){
int[] arr = null;
div(4,2,arr);
}
public static void div(int a,int b,int[] arr){
int c = 0;
try{
c = a/b;
System.out.println("数组的长度:"+arr.length);
}catch(ArithmeticException e){
//处理异常的代码
System.out.println("toString:"+e.toString());
}catch(NullPointerException e){
System.out.println("出现了空指针异常");
}
System.out.println("c="+c);
}
}
运行结果如下图所示:
③实例三:Exception类是所有异常类的父类。
class Demo5{
public static void main(String[] args){
int[] arr = null;
div(4,2,arr);
}
public static void div(int a,int b,int[] arr){
int c = 0;
try{
c = a/b;
System.out.println("数组的长度:"+arr.length);
}catch(Exception e){
//此处运用了多态,Exception e = new NullPointerException();
System.out.println("我是急诊室,包治百病!");
}
System.out.println("c="+c);
}
}
运行结果如下图所示:
注解:如果出现算术异常,这个捕获异常也会起作用,同样用到的也是多态来捕获,父类指向子类的对象。如果同时有Exception,和其他子类异常,应该把Exception放在最后一个捕获,如果把Exception放在前面会报错,因为它已经拦截了所有类型的异常,当你出现任何类型的异常时它都能捕获,所以导致后面的捕获都没有机会执行到,因为Java编译器不允许写废话。
④实例四:一个try块可以捕获多种异常的类型,但是捕获的异常类型必须从小到大进行捕获,否则编译报错。
class Demo6{
public static void main(String[] args){
int[] arr = null;
div(4,0,arr);
}
public static void div(int a,int b,int[] arr){
int c = 0;
try{
c = a/b;
System.out.println("数组的长度:"+arr.length);
}catch(ArithmeticException e){
//处理异常的代码
System.out.println("toString:"+e.toString());
}catch(NullPointerException e){
System.out.println("出现了空指针异常");
}catch(Exception e){
System.out.println("我是急诊室,包治百病!");
}
System.out.println("c="+c);
}
}
运行结果如下图所示:
疑问二:既然Exception能拦截所有类型的异常,那么以后捕获处理的时候是否直接捕获Exception即可?
这种想法是错误的,因为我们在现实开发中遇到不同的异常类型的时候,往往会有不同的处理方式,所以要分开不同的异常类型处理。比如说:当一个音乐播放器遇到卡顿时的处理方式是会将异常信息发送给服务器,服务器获取到这个异常后处理发布新的版本;如果遇到下载音乐时WIFI突然断开,处理方式就不会再发送到服务器,而是保存他目前已经下载的信息和进度,等连上WIFI后继续下载。
4.抛出处理:
(1) 抛出处理主要用到两个关键字:throw和throws;
(2) 抛出处理要注意的细节:
① 如果一个方法的内部抛出了一个编译时异常对象,那么必须要在方法上声明抛出。
原因:因为一个方法的内部抛出一个异常对象,也就意味着这个方法在某种情况下是存在着危险性的,既然是危险的东西肯定要提示用户,就好像一栋楼会有安全通道这种提示信息,因为会有着火的风险,对于用户来说,一个方法的内部如何实现用户是不知道的,用户只能看到方法的声明部分,如果方法的声明上写上这句话,那么我们就会意识到用这个方法可能会存在出异常的情况。我们就会知道用这个方法会有一定的危险性。所以为了用户调用这个方法的安全性考虑,如果我们在方法的内部抛出了一个异常对象,就必须要在方法上声明抛出。因为调用者只能看到方法的声明部分,在方法上声明抛出Exception的意义就是告诉调用者使用这个方法有可能存在着哪种危险。
② 如果调用了一个声明抛出编译时异常 的方法,那么调用者必须要处理异常。
原因:如果在方法内部抛出了一个异常,谁调用了这个方法,那么这个异常对象就会抛给谁,也就是说div(4,0)调用这个方法,那么异常对象就会抛给调用它的main方法中,那么main方法也会出问题。
③ 如果一个方法内部抛出了一个异常对象,那么throw语句后面的代码都不会再执行了(一个方法遇到了throw关键字,该方法也会马上停止执行的)。
④ 在一种情况下,只能抛出一种类型异常对象。
(3) throw 与throws两个关键字的区别:
① throw关键字是用于方法内部的,throws是用于方法声明上的。
② throw关键字是用于方法内部抛出一个异常对象的,throws关键字是用于在方法声明上声明抛出异常类型的。
③ throw关键字后面只能有一个异常对象,throws后面一次可以声明抛出多种类型的异常。
(4) 实例:
①实例一:如果调用了一个声明抛出异常的方法,那么调用者必须要处理异常。
class Demo1{
public static void main(String[] args){
try{
div(4,0);//调用了一个声明抛出异常类型的方法
}catch(Exception e){
System.out.println("出现异常了");
e.printStackTrace();
}
}
public static void div(int a,int b) throws Exception{
if(b == 0){
throw new Exception();//抛出一个异常对象
}
int c = a/b;
System.out.println("c="+c);
}
}
运行结果如下图所示:
疑问:为什么不能输出c?
当b = 0时,通过throw new Exception( )抛出一个异常对象,c并不会输出,道理也很简单,就好比你去医院看病,医院都不收你,把你抛给上一级医院,根本不对你的病情做任何处理,自然你的病情也不会好,这时候c就没有输出。
②实例二:
调用者也可以不处理异常,而是把异常再往上一级抛,如果它的调用者是在main方法中,Jvm调用main方法,那么main方法会把异常抛给Jvm,而Jvm处理异常的方式是打印异常的栈信息。
class Demo2{
public static void main(String[] args) throws Exception{
div(4,0);//调用了一个声明抛出异常类型的方法
}
public static void div(int a,int b) throws Exception{
if(b == 0){
throw new Exception();//抛出一个异常对象
}
int c = a/b;
System.out.println("c="+c);
}
}
运行结果如下图所示:
(5) 疑问:何时使用抛出处理?何时使用捕获处理?
①如果你需要通知到调用者,你的代码出了问题,那么这时候就使用抛出处理.
②如果代码是直接与用户打交道遇到了异常千万不要再抛,再抛的话,就给了用户了,这时候就应该使用捕获处理。
(6) 使用抛出处理的原则:
MVC模式会把代码分成三层来管理,WEB层的代码是用来展示数据给用户去查看的,WEB层的代码是与用户打交道的;service层(业务逻辑层)负责把WEB层需要的数据处理成WEB层需要的格式;dao层(数据持久层)的代码主要是用于保存、删除、查询数据的,主要负责与数据库打交道。数据持久层会把查询出的数据交给业务逻辑层,业务逻辑层会对传过来的数据进行筛选、格式化筛选出符合WEB层需求的东西,经过service层的代码过滤后会把代码交给WEB层。所以,WEB层需要数据的话会和service层要,service层需要数据的话会和dao层要,也就是说WEB层会调用service层的代码,service层的代码会调用dao层的代码。假设用户需要在点餐系统中查询出所有的菜名,当用户点击展示所有菜名时,就会调用到service层的代码,service层就会调用daodao层的代码,这时候dao层就要执行select * from foods;去查询数据库中的所有菜名,假设在查询的过程中mysql数据库的服务器关闭了,这时候就会出异常。这时候dao层的代码出现了异常,如果dao层对异常的处理只是输出,被处理过之后对于service层来说,service层并不知道dao层出了问题,因为dao层已经把这个异常给处理掉了,所以service层会认为这个数据查询正确,service层就会对这个数据进行处理,因为没有查出来所以数据也是空的,这时候service层就会把空的数据给WEB层,WEB层拿到空的数据后,就会把空的展示给用户什么菜都没有。所以dao层出现异常用捕获处理是不行的,一般会有一个变量e指向出现异常的对象,我们捕获到这个对象后往往会把这个异常对象通过throw e抛出去,这时候service层调用dao层的代码后,这时候dao层会把异常抛给service层,service层接到异常后就会知道dao层做查询时出了问题,这时候service层对这个异常的处理也不能是只输出一句话,而应该捕获到这个异常后再把它抛出去,这时候WEB层就会接到这个异常,这时候WEB层就不能再抛出这个异常了,再抛就会抛给用户,而用户什么都不懂,凡是与用户打交道的代码,拿到这个异常千万不要抛,这时候WEB层就会对捕获到的异常做处理一般是跳转到一个友好的页面。
三.自定义异常类
1.sun公司提供了很多的异常类给我们用于描述程序中各种不正常的情况,但是sun公司给我们提供的异常类还不足以描述我们现实生活中所有不正常情况,那么这时候我们就需要自定义异常类。
2.自定义异常类的步骤:自定义一个类继承Exception即可。
3.实例
(1) 实例一:
①需求:模拟feiQ上线的时候,如果没有插上网线,那么就抛出一个没有插上网线的异常,如果已经插上了网线,那么就正常显示好友列表。
②分析:异常处理有两种方式,分别是捕获处理和抛出处理,首先我们实现插上网线显示好友列表的需求。
③插上网线显示好友列表:
class NoIpException extends Exception{
public NoIpException(String message){
super(message); //调用了Exception一个参数的构造函数。
}
}
class Demo1{
public static void main(String[] args) {
String ip = "192.168.10.100";
try{
feiQ(ip); // 如果调用了一个声明抛出异常类型的方法,那么调用者必须要处理。
}catch(NoIpException e){
e.printStackTrace();
System.out.println("马上插上网线!");
}
}
public static void feiQ(String ip) throws NoIpException{
if(ip==null){
throw new NoIpException("没有插网线吧");
}
System.out.println("正常显示好友列表..");
}
}
运行结果如下图所示:
④方式一:捕获处理
class NoIpException extends Exception{//自定义一个没有网线的异常类
public NoIpException(String message){
super(message); //调用了Exception一个参数的构造函数。
}
}
class Demo1{
public static void main(String[] args) {
String ip = "192.168.10.100";
ip = null;
try{
feiQ(ip); // 如果调用了一个声明抛出异常类型的方法,那么调用者必须要处理。
}catch(NoIpException e){
e.printStackTrace();
System.out.println("马上插上网线!");
}
}
public static void feiQ(String ip) throws NoIpException{
if(ip==null){
throw new NoIpException("没有插网线吧");
}
System.out.println("正常显示好友列表..");
}
}
运行结果如下图所示:
如果自定义的异常类中什么都不写,别人会不太明白你的这个异常类是用来描述什么不正常情况的,如果我们在自定义异常类中加一个字符串,对这个异常进行说明,别人就可以根据这个字符串判断出你的这个异常类是用来干嘛的。
⑤方式二:抛出处理
main方法中向外再抛出异常抛给Jvm。
class NoIpException extends Exception{
public NoIpException(String message){
super(message); //调用了Exception一个参数的构造函数。
}
}
class Demo1{
public static void main(String[] args) throws NoIpException{
String ip = "192.168.10.100";
ip = null;
feiQ(ip);
}
public static void feiQ(String ip) throws NoIpException{
if(ip==null){
throw new NoIpException("没有插网线吧");
}
System.out.println("正常显示好友列表..");
}
}
运行结果如下图所示:
(2) 实例二:
①需求:
模拟去吃木桶饭,如果带钱少于了10块,那么就抛出一个没有带够钱的异常对象,如果带够了,那么就可以吃上香喷喷的木桶饭.
②实例:
//定义没钱的异常
class NoMoneyException extends Exception {
public NoMoneyException(String message){
super(message);
}
}
class Demo1 {
public static void main(String[] args) {
try{
eat(9);
}catch(NoMoneyException e){
e.printStackTrace();
System.out.println("打电话给家人送钱");
}
}
public static void eat(int money) throws NoMoneyException{
if(money<10){
throw new NoMoneyException("吃霸王餐");
}
System.out.println("吃上了香喷喷的地沟油木桶饭!!");
}
}
③运行结果如下图所示:
四.异常的分类:
1.异常体系:
异常体系:
--------| Throwable 所有错误或者异常的父类
--------------| Error(错误)
--------------| Exception(异常) 异常一般都通过代码处理
------------------| 运行时异常: 如果一个方法内部抛出了一个运行时异常,那么方法上可以声明也可以不声明,调用者可以处理也可以不处理。
------------------| 编译时异常(非运行时异常、受检异常): 如果一个方法内部抛出了一个编译时异常对象,那么方法上就必须要声明,而且调用者也必须要处理。
2.异常的分类:
(1) 运行时异常: RuntimeException以及RuntimeException子类 都是属于运行时异常。
(2) 编译时异常: 除了运行时异常就是编译时异常。
3.疑问:为什么java编译器会如此严格要求编译时异常,对运行时异常如此宽松?
(1) 运行时异常都是可以通过程序员良好的编程习惯去避免,比如分母为0、强制转换异常、空指针异常,所以java编译器就没有严格要求处理运行时异常。
(2) 编译时异常是我们代码上无法避免的,比如读写文件中的IOException。
4.实例:
class Demo2{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
if(b==0){
throw new ArithmeticException();
}
int c = a/b;
System.out.println("c="+c);
}
}
运行结果如下图所示:
五.finally块
1.finally块的情景需求:
假设有一个资源文件,有一个程序1对这个文件进行读取,读取文件资源,读完之后继续保持着和这个文件的链接;在读取的过程中有另外一个程序2,程序2想对文件执行删除操作,程序2对文件的操作不会成功,因为程序1在占用着这个资源文件,也就是说资源文件一旦使用完毕,一定要释放资源文件,否则其他的程序无法对这个资源文件进行操作。只要使用完毕,就不应该继续霸占着这个资源,继续霸占这个资源就会导致所有程序都无法使用这个资源文件。那么一定要执行的代码应该放在哪里?假设程序1在读取文件的过程中,在程序1内部处理数据的过程中出现异常,程序1内部出现异常,那么程序1肯定要释放这个资源文件,不管你有没有读完,在读的过程中出现了问题,那么你也应该把这个资源文件释放出去。
2.finally块的使用前提:必须要存在try块才能使用。
3.finally块的代码不管是否发生异常都会执行。
(1) 没有发生异常的情况下:
①实例:
class Demo1{
public static void main(String[] args){
div(4,2);
}
public static void div(int a,int b){
try{
int c = a/b;
System.out.println("c="+c);
}catch(Exception e){
System.out.println("出了除数为0的异常……");
}finally{
System.out.println("finally块的代码执行了……");
}
}
}
②运行结果:
(2) 发生异常的情况如下:
①实例:
class Demo2{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
try{
int c = a/b;
System.out.println("c="+c);
}catch(Exception e){
System.out.println("出现了除数为0的异常……");
}finally{
System.out.println("finally块的代码执行了……");
}
}
}
②运行结果:
(3) 即使遇到return语句,一旦执行return语句整个方法结束,但是finally块的代码依然会执行。
①实例:
class Demo3{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
try{
if(b==0){
return;
}
int c = a/b;
System.out.println("c="+c);
}catch(Exception e){
System.out.println("出现了除数为0的异常……");
}finally{
System.out.println("finally块的代码执行了……");
}
}
}
②运行结果:
(4) 即使抛出异常,整个方法也会结束,但是finally块的代码仍然会执行。
①实例:
class Demo4{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
try{
int c = a/b;
System.out.println("c="+c);
}catch(Exception e){
System.out.println("出现了除数为0的异常……");
throw e;
}finally{
System.out.println("finally块的代码执行了……");
}
}
}
②运行结果:
4.finally块的代码在任何情况下都会执行的,除了jvm退出的情况。
(1) 实例:
class Demo5{
public static void main(String[] args){
div(4,0);
}
public static void div(int a,int b){
try{
if(b==0){
System.exit(0);//退出Jvm
}
int c = a/b;
System.out.println("c="+c);
}catch(Exception e){
System.out.println("出现了除数为0的异常……");
throw e;
}finally{
System.out.println("finally块的代码执行了……");
}
}
}
(2) 运行结果:
5.finally非常适合做资源释放的工作,这样子可以保证资源文件在任何情况下都会被释放。
6.finally释放资源的实例:
import java.io.*;
class Demo1 {
public static void main(String[] args) {
FileReader fileReader = null;
try{
//找到目标文件
File file = new File("e:\\a.txt");
//建立程序与文件的数据通道(读文件之前必须先建立一个管道确保文件的二进制数据通过管道流入程序中)
fileReader = new FileReader(file);
//读取文件
char[] buf = new char[1024];
int length = 0;
length = fileReader.read(buf);
System.out.println("读取到的内容:"+ new String(buf,0,length));
}catch(IOException e){
System.out.println("读取资源文件失败....");
}finally{
try{
//关闭资源
//必须要保证关闭资源的代码一定要执行,能不能释放成功是另外一回事。
fileReader.close();
System.out.println("释放资源文件成功....");
}catch(IOException e){
System.out.println("释放资源文件失败....");
}
}
}
}
运行结果如下图所示:
7.try块的三种组合方式:
(1) 方式一:比较适用于有异常要处理,但是没有资源要释放的。
try{
可能发生异常的代码;
}catch(捕获的异常类型 变量名){
处理异常的代码;
}
(2) 方式二:比较适用于既有异常要处理又要释放资源的代码。
try{
可能发生异常的代码;
}catch(捕获的异常类型 变量名){
处理异常的代码;
}finally{
释放资源的代码;
}
(3) 方式三:比较适用于内部抛出的是运行时异常(没有catch块就意味着不需要处理异常),并且有资源要被释放。这种用法在现实开发中很少见,但是我们要知道这种语法是合法的。
try{
可能发生异常的代码;
}finally{
释放资源的代码;
}