[Java开发之路](5)异常详解

简介:
1. 异常分类

在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。其是如果Java中的异常类不能满足需求,用户可以创建自己的异常类。

下图是Java异常层次结构的一个简化示意图。



从图上可以看出,所有的异常都是继承于 Throwable类,但是在下一层立即分解为两个分支:Error和Exception。

(1)Error

Error描述了Java运行时系统的内部错误资源耗尽错误。应用程序不应该抛出这种类型的错误,如果出现了这样的内部错误,除了通告用户,并尽力使程序安全的终止之外,再也无能为力了。这种情况很少见。 大多数错误与代码编写者执行的操作无关,而表示代码运行时  JVM 出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。



这些错误表示故障发生于 虚拟机自身、或者发生在 虚拟机试图执行应用时,并且这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。

(2)Exception

程序设计者应该关注的是Exception,这一层次异常又分为两个分支:IOException和RuntimeException。划分这两个分支的规则是:由 程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但是由于IO错误这类问题导致的异常属于 IOException



派生于 RuntimeException的异常包括:
  • 异常的运算条件,如一个整数除以0时
  • 错误的类型转换
  • 数组访问越界
  • 访问空指针
派生于IOException的异常包括:
  • 试图在文件尾部后面读取数据
  • 试图打开一个不存在的文件
如果出现RuntimeException异常则表明一定是你的问题”,这是一条相当有道理的规则。


     注意:
             异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

(3)Checked Exception 与 UnChecked Exception

Java语言规范将派生于Error类或者RuntimeException类的所有异常称为未检查异常( Un Checked 异常),所有其他的异常(包括IOException)称为已检查异常( Checked 异常)。 编译器将核查是否为所有的 Checked 异常提供了异常处理器。

2. 声明已检查异常

如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理很简单:一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
例如:一段读取文件的代码知道优肯风读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出IOException异常。

方法应该声明所有可能抛出的已检查异常,这样可以反映出这个方法可能抛出哪类已检查异常

例如:下面是标准类库中提供的FileInputStream类的一个构造方法的声明异常情况:
 
  
public FileInputStream(String name) throws FileNotFoundException
这个异常声明表示这个构造方法根据给定的字符串name正确情况下产生一个FileInputStream对象,但是也有可能抛出一个FileNotFoundException异常。如果抛出异常,方法不会初始化一个 FileInputStream对象,而是抛出一个 FileNotFoundException对象。抛出异常之后,运行时系统开始搜索异常处理器,以便知道如何处理  FileNotFoundException对象。

不是所有可能抛出的异常都必须进行声明,以下4种情况时记得抛出异常:
  • 调用一个抛出已检查异常的方法,如FileInputStream构造方法
  • 程序运行过程中发现错误,并且利用throw语句抛出一个已检查异常
  • 程序出现错误,例如,a[-1]=0会抛出一个下标越界的未检查异常
  • Java虚拟机和运行库出现的内部错误

对于前两种情况必须进行声明。

不需要声明Java的内部错误,即从Error继承的错误,任何程序代码都具有抛出那些异常的潜能,但是它们在我们的控制范围之外。同样也不应该声明从RuntimeException继承的那些未检查异常。

 
  
void Read(int index) throws ArrayIndexOutOfBoundsException // bad style
{
...
}
这些运行时错误完全在我们的控制范围之内, 如果特别关注数组下标引发的错误,就会将更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上


总结:

  1. 一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。
  2.  如果一个方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误信息。
  3. 如果类中的一个方法声明会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类,或者这个类一个子类的异常。


3. 如何抛出异常

假设有一个方法用来读取文件内容,给定的文本长度为1024,但是读到700个字符之后文件就结束了,我们认定这不是一种正常的情况,希望抛出异常。
首先决定应该抛出什么类型的异常(EOFException),知道之后抛出异常的语句如下:

 
  
// 第一种方法
throw new EOFException();
 
// 第二种方法
EOFException e = new EOFException();
throw e;
 

EOFException类还有一个含有一个字符串参数的构造方法,可以更加细致描述异常出现的状况:
 
  
String gripe = "未到指定长度,文件读取结束";
throw new EOFException(gripe);

对于一个已经存在的异常类,抛出异常过程:
  • 找到一个合适的异常类
  • 创建这个异常类的一个对象
  • 将对象抛出

4. 创建异常类

在程序中,可能会遇到任何标准程序类都没有能够充分描述清楚的问题,这种情况下,我们需要创建我们自己的异常类。我们需要做的就是定义一个派生类于Exception,或者派生于Exception子类的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器,一个是带有详细描述信息的构造器。
 
  
public class FileFormatException extends Exception{
 
/**
*
*/
private static final long serialVersionUID = 1L;
// 默认构造器
public FileFormatException(){
}
// 带有详细描述信息的构造器
public FileFormatException(String gripe){
super(gripe);
}
}
现在我们可以抛出我们自己定义的异常类型了。
 
  
throw new FileFormatException;

5. 捕获异常

如果某个异常发生的时候没有任何地方进行捕获,那程序就会终止,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
 
  
package com.qunar.test;
public class ExceptionTest {
public static void main(String[] args) {
int a = 10;
int b = 0;
System.out.printf("%d / %d = %d",a,b,a/b);
System.out.println("测试结束...");
}
}

控制台信息:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.qunar.test.ExceptionTest.main(ExceptionTest.java:8)

从异常信息可以看出程序并没有运行完全,没有输出“测试结束...”,程序就终止,并且在控制台打印出异常信息。

要想捕获异常,必须使用try/catch语句块。
 
  
try{
code
more code
more code
}
catch (Exception e) {
handle for this type
}

(1)如果在try语句块中任何代码抛出一个在catch子句中说明的异常类,那么:
  • 程序将跳过try语句块的剩余代码
  • 程序将执行catch子句的处理器代码
(2)如果在 try语句块中代码没有抛出异常,那么程序将跳过catch子句。
(3)如果方法中的任何代码抛出了一个在catch子句没有声明的异常类型,那么这个方法就会立刻退出。

 
  
package com.qunar.test;
 
public class ExceptionTest {
 
public static void main(String[] args) {
int a = 10;
int b = 0;
try{
System.out.printf("%d / %d = %d",a,b,a/b);
}
catch (ArithmeticException e) {
System.out.println("a / b b 不能等于0");
}
System.out.println("测试结束...");
}
}

运行结果:

a / b  b 不能等于0
测试结束...  

看一个例子:(读取文本程序代码)
 
  
public void read(String name){
try{
InputStream inputStream = new FileInputStream(name);
int b;
while((b = inputStream.read()) != -1){
// ...
}//while
}
catch (IOException e) {
e.printStackTrace();
}
}
对于一个普通的程序来说,这样的处理异常基本上合乎情理,但是,通常最好的情况是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去处理,如果采用这种处理方式,就必须声明这个方法可能会抛出一个IOException。

 
  
public void read(String name) throws IOException{
InputStream inputStream = new FileInputStream(name);
int b;
while((b = inputStream.read()) != -1){
// ...
}//while
}
如果调用了一个抛出已检查异常的方法,就必须对它进行处理或者将它继续进行传递。

出现了两种处理方式,那到底哪种方式更好呢?
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。如果想传递一个异常,就必须在方法添加一个throws说明符。仔细阅读Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理,还是加到throws列表中。

6. 捕获多个异常

在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
 
  
try{
}
catch (FileNotFoundException e) {
// emergency action for missing files
}
catch (UnknownException e) {
// emergency action for unknown hosts
}
catch (IOException e) {
// emergency action for all other I/O problems
}

7. 再次抛出异常与异常链

在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么用于表示系统故障的异常类型会有多种解释。ServletException就是这样一个异常类型的例子。执行 Servlet的代码可能不想知道发生的错误的细节原因,但希望知道servlet是否有问题。

下面给出了捕获异常并将它再次抛出的基本方法:
 
   
try{
access the database
}
catch(SQLException e){// 发生错误的细节原因
throw new ServletException("database error:"+e.getMessage());// 只希望知道servlet是否有问题
}
再给出一个更好的方法: 包装技术
 
   
try{
access the databse
}
catch(SQLException e){
Throwable se = new ServletException("database error");
// 设置初始异常
se.initCause(e);
throw se;
}
这个方法更好的地方在于当捕获到异常时可以使用下面语句得到原始异常( 不会丢失原始异常的细节):
 
   
Throwable e = se.getCause();
这种方法还可以解决一下问题:
如果一个方法中发生了一个已检查异常,而不允许抛出它,那么包装技术就十分有用,我们可以捕获这个已检查异常,并把它包装成一个运行时异常。

8. finally子句

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法知道,同时这些资源再退出之前必须回收。这就需要finally子句来解决。不管是否有异常,finally子句的代码都被执行。

举个例子,当发生异常时,恰当的关闭所有数据库的链接是非常重要的,这种情况就可以使用finally。
 
   
try{
}
catch (Exception e) {
}
finally{
 
}
(1)如果代码没有抛出异常。首先执行try语句块中的全部代码,然后执行finally子句中的代码。
(2)如果代码抛出异常,并且在catch子句可以捕获到。首先执行try语句块汇总的所有代码,直到发生异常为止,此时跳过try语句块中剩余代码,去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。
(3)如果代码抛出异常,但这个异常不是由catch子句捕获的。 首先执行try语句块汇总的所有代码,直到发生异常为止,此时跳过try语句块中剩余代码,然后执行finally子句中的代码,并将异常抛给这个方法的调用者。

建议独立使用try/catch和try/finally语句块。这样可以提高代码的清晰度。例如:

 
   
package com.qunar.test;
 
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
 
public class ExceptionTest {
public static void main(String[] args) {
String name = "";
InputStream in = null;
// 确保报告出现的错误
try{
in = new FileInputStream(name);
// 确保关闭输入流
try{
// code that might throw exceptions
}
finally{
in.close();
}
}
catch (IOException e) {
// show error message
}
}
}
内层的try语句块只有一个职责,就是 确保关闭输入流。外层的try语句块也只有一个职责,就是 确保报告出现的错误。这种设计不仅清楚,而且还具有一个功能,就是 将会报告finally子句中出现的错误

finally子句包含return语句时,会出现意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。
 
   
package com.qunar.test;
 
 
public class ExceptionTest {
public static int function(int n){
try{
int r = n * n;
return r;
}
finally{
if(n == 2){
return 0;
}//if
}//finally
}
public static void main(String[] args) {
System.out.println(function(2));
}
}
如果调用function(2),那么try语句块中计算结果为4,并执行return语句。但是在方法真正返回前,还要执行finally子句,将使得方法返回0,覆盖了原始的返回值4。

有时候,finally也会有麻烦。
 
   
InputStream in = ...;
try{
// code that might throw exceptions
}
finally{
in.close();
}
假设在try语句块抛出了一个非IOException的异常,这个异常只有方法的调用者才能处理。执行finally子句,并调用in.close()方法,而close方法本身也可能抛出IOException异常。当这种情况出现时,原始的异常将会丢失,转而抛出clsoe方法的异常。

目录
相关文章
|
20天前
|
监控 JavaScript 前端开发
《理解 WebSocket:Java Web 开发的实时通信技术》
【4月更文挑战第4天】WebSocket是Java Web实时通信的关键技术,提供双向持久连接,实现低延迟、高效率的实时交互。适用于聊天应用、在线游戏、数据监控和即时通知。开发涉及服务器端实现、客户端连接及数据协议定义,注意安全、错误处理、性能和兼容性。随着实时应用需求增加,WebSocket在Java Web开发中的地位将更加重要。
|
5天前
|
IDE Java 数据库连接
使用 Java 进行桌面应用开发
【4月更文挑战第19天】Java 是一款广泛应用于企业级、网络和桌面应用开发的编程语言。其跨平台特性使Java程序能在不同操作系统上运行,而JDK提供了开发所需工具和库。使用Swing等GUI库构建用户界面,结合JDBC进行数据库操作,Socket实现网络通信。虽然面临性能和用户体验的挑战,但通过优化和选用合适的IDE,Java仍能开发出高效稳定的桌面应用。
|
5天前
|
前端开发 Java Go
开发语言详解(python、java、Go(Golong)。。。。)
开发语言详解(python、java、Go(Golong)。。。。)
|
6天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
49 10
|
6天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
13天前
|
运维 NoSQL 算法
Java开发-深入理解Redis Cluster的工作原理
综上所述,Redis Cluster通过数据分片、节点发现、主从复制、数据迁移、故障检测和客户端路由等机制,实现了一个分布式的、高可用的Redis解决方案。它允许数据分布在多个节点上,提供了自动故障转移和读写分离的功能,适用于需要大规模、高性能、高可用性的应用场景。
16 0
|
15天前
|
人工智能 小程序 Java
JAVA开发智慧学校系统源码+人脸电子班牌布局
智慧校园是通过利用物联网,大数据技术来改变师生和校园资源相互交互的方式,以便提高交互的明确性、灵活性和响应速度,从而实现智慧化服务和管理的校园模式。
|
16天前
|
Java
Java中的异常类总结
Java中的异常类总结
|
18天前
|
安全 前端开发 Java
Java Web开发知识点学习总结
Java Web开发涉及Java基础、Servlet、JSP、数据库操作(SQL+JDBC)、MVC设计模式、Spring框架、Hibernate ORM、Web服务(SOAP&RESTful)、安全认证(HTTP Basic/Digest/OAuth)及性能优化(缓存、异步、负载均衡)。
17 3
|
21天前
|
XML JSON JavaScript
使用JSON和XML:数据交换格式在Java Web开发中的应用
【4月更文挑战第3天】本文比较了JSON和XML在Java Web开发中的应用。JSON是一种轻量级、易读的数据交换格式,适合快速解析和节省空间,常用于API和Web服务。XML则提供更强的灵活性和数据描述能力,适合复杂数据结构。Java有Jackson和Gson等库处理JSON,JAXB和DOM/SAX处理XML。选择格式需根据应用场景和需求。