Java技术进阶之异常

简介: Java技术篇

为什么需要异常机制:

Java的基本理念是“结构不佳的代码不能运行” --- Java编程思想

最理想的是在编译时期就发现错误,但一些错误要在运行时才会暴露出来。对于这些错误我们当然不能置之不理。对于错误而言的两个关键是发现和处理错误。Java提供了统一的异常机制来发现和处理错误。

不考虑异常存在来看一下这个场景:

publicvoidshowObject(Objectobj) {
if (obj==null) {
System.out.println("error obj is null");
    } else {
System.out.println(obj.toString());
    }
}

对于showObject来说obj为null是一个错误,要在输出之前做错误判断,发生错误的话把错误打印出来作为错误报告和处理。这里把错误的发现、报告处理和正常业务逻辑放在了一起。但一些错误往往比这复杂且不只一个,如果我们为每一个错误都去定义一个独特错误报告的形式且都将错误处理代码和正常业务代码紧紧的耦合在一起那我们代码会变得难以维护。

合理的使用异常不仅能使我们的代码更加健壮,还能简化开发提升开发效率。

一、抛出异常(发现错误):

1、异常也是对象:

java使用使用异常机制来报告错误。异常也是普通的类类型。Java自身已经定义好了使用java时可能会产生的异常,使用java时java会自动去检查异常的发生,当异常发生时,java会自动创建异常对象的实例并将其抛出。我们经常看到的NullPointerException便是java已经定义好的异常。

除了java自身定义的异常外我们可以自定义异常,但自定义的异常需要我们自己去检查异常情形的发生,并自己创建异常对象和抛出。当然也可以创建java自定义的异常并抛出,抛出异常使用throw关键字:

thrownewException();

我们使用的第三方库大多封装了自己的异常,并在异常情形发生时将自定义异常通过throw抛出。所有的异常类型都继承自Throwable类,所有Throwable类型的对象都可被抛出。

2、抛出异常:

异常发生,系统自动创建异常实例并抛出,或我们自己创建异常实例抛出异常时,代码的正常执行流程将会被终止,转而去执行异常处理代码。

二、异常捕获(处理错误):

1、监控区域:

当异常抛出时,自然抛出的异常应该得到处理,这就需要将抛出的捕获异常。但一个异常类型可能在很多地方被抛出,那么怎么去对特定的地方编写特定的异常处理程序那?java采用一个最方便和合理的方式,即对可能产生异常的代码区域进行监控,并在该区域后添加处理程序。

监控的代码区域放在try{}中,而异常处理代码紧跟在try后的catch中:

try {
/***/} catch (ExceptionTypee) {
/*****/}

catch类似方法的申明括号中为异常的类型和该类类型的实例。表明当前catch块处理的是什么类型的异常,而e便是该异常抛出的实例对象。

当try内的代码抛出异常时,就会停止当前流程,去匹配第一个catch中申明的异常类型与抛出类型相同的catch,如果匹配到则执行其内代码。一个try中可能会抛出多种类型的异常,可以用多个catch去匹配。

2、捕获所有异常:

注意catch中声明的异常如果为当前抛出异常的父类型也可以匹配。所以一般将基类的异常类型放在后面。

因为所有可以进行捕获的异常都继承自Exception,所有可以catch中申明Exception类型的异常来捕获所有异常,但最后将其放在最后防止将其他异常拦截了。

三、重新抛出异常

一、异常的两种类型

先看一下异常的类层次结构图:

4.png

我们可以将异常分为检查和非检查两种类型:

  • 检查异常:该类异常包含Exception及其子类异常,这些类型的异常的抛出必须有相应的catch进行捕获,否则无法通过编译。
  • 非检查异常:该类型的异常包含RuntimeException、Error和两者的子类型,这类异常可以没有对应的try-catch进行捕获也可通过编译。当异常发生时没有相应的捕获则异常会自动向上一级抛出,如此如果一直到main方法中还未被捕获则会调用该异常的printStacjTrace方法输出异常信息,并终止main的运行。其中Error为Java自身错误,这类错误发生时我们并不能在业务代码中去解决,如内存不足,所以这类异常不需要去捕获。
1、异常声明:

catch中的语句执行完成后会继续执行try-catch后的其他语句。所以当try-catch后还有语句时,一定要保证但异常发生时在catch中已经对异常进行了正确处理,后面的代码可以得到正常的运行,如果不能保证则应该终止代码向后的执行或再次抛出异常。

一些异常在当前的方法中不需要或无法进行处理时,可以将其抛出到上一层。要在方法中将异常抛出需要在方法中对要抛出的异常进行声明,这样方法的调用者才能知道哪些异常可能会抛出,从而在调用方法时添加异常处理代码。

非检查异常抛出到上一级时可以不用进行声明,合理的使用非检查异常可以简化代码。

(1) 异常声明

在方法声明的参数列表之后使用throws进行异常声明,多个异常类型使用逗号隔开:

voidt () thrwosExcptionTyep1, ExceptionType2 {
}

在方法中声明了的异常在方法中可以不进行捕获,直接被抛出到上一级。异常声明父类异常类型可以匹配子类异常类型,这样当有多个子类异常抛出时,只用声明一个父类异常即可,子类异常将被自动转换为父类型。

四、创建自定义异常:

要创建自己的异常必须得继承自其它的异常,一般继承Exception创建检测异常,继承RumtimeException创建非检查异常。

一般情况下异常提供了默认构造器和一个接受String参数的构造器。对于一般自定义的异常来说,只需要实现这两个构造方法就足够了,因为定义异常来说最有意义的是异常的类型,即异常类的名字,但当异常发生时只需看到这个异常的类型就知道发生了什么,而其他一些操作在Throwable中已经有定义。所以除非有一些特殊操作,不然在自定义异常时只需只需简单的实现构造方法即可。

五、异常信息:

所以异常的根类Throwable定义了我们需要的大多数方法:

// 获取创建异常时传入的字符串StringgetMessage()
// 使用System.err输出异常发生的调用栈轨迹voidprintStackTrace()
// 使用传入的PrintStream打印异常调用栈voidprintStackTrace(PrintStreams)
// 使用PrintStreamOrWriter打印异常调用栈voidprintStackTrace(PrintStreamOrWriters)
获取调用栈实例StackTraceElement[] getStackTrace()
该方法放回StackTraceElement数组,StackTraceElement为调用方法栈的实例,改类型有以下常用方法:// 返回栈代码所在的文件名StringgetFileName()
// 返回异常抛出地的行号intgetLineNumber()
// 返回栈的类名StringgetClassName()
// 放回栈的方法名StringgetMethodName()

六、异常链:

1、重新获取异常

当我们捕获到一个异常时可能想将他在次抛出,但这样直接抛出的话异常的栈信息是该异常原来的栈信息,不会是最新的再次抛出的异常的栈信息。如下:

classSimpleExceptionextendsException {
publicSimpleException() {
    }
publicSimpleException(Stringmsg) {
super(msg);
    }
}
publicclassTest {
publicvoids() throwsSimpleException {
thrownewSimpleException();
    }
publicvoids2() throwsSimpleException {
try {
s();
        } catch(SimpleExceptione) {
throwe;
        }
    }
publicstaticvoidmain(String[] args) {
Testt=newTest();
try {
t.s2();
        } catch (SimpleExceptione) {
e.printStackTrace();
        } 
    }
}

上面代码输出为:

com.ly.test.javatest.exceptiontest.SimpleExceptionatcom.ly.test.javatest.exceptiontest.Test.s(Test.java:19)
atcom.ly.test.javatest.exceptiontest.Test.s2(Test.java:24)
atcom.ly.test.javatest.exceptiontest.Test.main(Test.java:33)

可以看到异常抛出最终地为 com.ly.test.javatest.exceptiontest.Test.s(Test.java:19),但如果我们想让异常抛出地变为s2那?毕竟我们在这里自己抛出了异常。Thrwoable类的fillInStackTrac创建一个新的Throwable对象,并将当前栈信息做新创建的Throwable异常的异常栈信息,然后返回。

2、异常链:

上面的做法又有另外一个问题,如果我们使用fillInStackTrace获得新的异常,那原来的异常信息也就丢失了,如果我们想抛出新的异常当又得包含原来的异常那?

Error、Exception和RuntimeException都含有一个接受Throwable对象的构造方法,在创建新的异常时时传入原来异常,即可保存原来异常。需要时使用getCause来获取到。除了使用构造方法传入异常,还可使用initCase方法传入异常。这其中的潜台词是“改异常是由什么异常造成的”。如下:

publicclassTest {
publicvoids() throwsException {
thrownewException();
    }
publicvoids2() throwsException {
try {
s();
        } catch(Exceptione) {
Exceptionne= (Exception)e.fillInStackTrace();
ne.initCause(e);
throwne;
        }
    }
publicstaticvoidmain(String[] args) {
Testt=newTest();
try {
t.s2();
        } catch (Exceptione) {
e.printStackTrace();
        } 
    }
}

六、总是执行的finally

看一下面的代码:

publicclassTest {
publicstaticvoids() throwsIOException {
thrownewIOException();
    }
publicstaticvoidmain(String[] args) {
StringfileName="C:\\temp\\test.txt";
Filefile=newFile(fileName);
InputStreamin=null;
try {
in=newFileInputStream(file);
s();
inttempbyte=in.read();
in.close();
        } catch (IOExceptione) {
if (in!=null) {
System.out.println("in");
            }
e.printStackTrace();
        }
    }
}

可以看到要对in进行close但正常的流程中发生了异常,导致正常流程中的in.close无法执行,便跳到cattch中去执行,上面于是又在catch中写了一个关闭。着只是一个简单清理操作,但如果需要执行的清理操作不止一行而是非常多那?也是在正常流程和catch中写两遍吗,这样是非常不友好的,所以java提供了finally,如下

publicclassTest {
publicstaticvoids() throwsIOException {
thrownewIOException();
    }
publicstaticvoidmain(String[] args) {
StringfileName="C:\\temp\\test.txt";
Filefile=newFile(fileName);
InputStreamin=null;
try {
in=newFileInputStream(file);
s();
inttempbyte=in.read();
        } catch (IOExceptione) {
e.printStackTrace();
        } finally {
if (in!=null) {
System.out.println("in");
            }
        }
    }
}

finally中的代码无论异常是否发生都会被执行,即使try中包含return语句,也会在放回之前执行finally语句。

对于清理操作和一些异常发生也必得到执行的代码都应该放到finally中。

清理未创建的资源:

上面介绍使用finally来释放资源,但看下面这个情形:

publicvoidtest() {
try {
in=newBufferedReader(newFileReader());
Strings=in.readLine();
    } catch (FileNotFoundExceptione) {
    } catch (Exceptione) {
try {
in.close();
        } catch (IOExceptione2) {
System.out.println("in class false");
        }
    } finally {
//in.close();    }
}

这个例子可以看到如果new FileReader抛出了FileNotFoundException,那么in是不会被创建的,如果此时还在finally中执行in.close()那么自然是行不同的。但如果抛出了IOExceptin异常,那么说明in成功创建但在readLine时发生错,所以在catch中进行close时in肯定已经被创建。这种情形资源的释放应该放到catch中。

七、异常丢失:
1、在fianlly中return
publicclassTest {
publicstaticvoidmain(String[] args) {
try {
inti=throwException();
System.out.println(i);
        } catch (Exceptione) {
e.printStackTrace();
        }
    }
publicstaticintthrowException () throwsException {
try {
thrownewException();
        } catch (Exceptione) {
throwe;
        } finally {
return1;
        }
    }
}

上面代码输出:1

2、在finally中抛出异常
publicclassTest {
publicstaticvoidmain(String[] args) {
try {
inti=throwException();
System.out.println(i);
        } catch (Exceptione) {
e.printStackTrace();
        }
    }
publicstaticintthrowException () throwsException {
try {
thrownewException();
        } catch (Exceptione) {
throwe;
        } finally {
thrownewNullPointerException();
        }
    }
}

上面代码输出为:

java.lang.NullPointerExceptionatcom.ly.test.javatest.Test.throwException(Test.java:20)
atcom.ly.test.javatest.Test.main(Test.java:7)

可以看到main中捕获到的是NullPointerException,首先抛出的Exception异常丢失了。

在开发中非特殊情形应避免以上两种情况的出现。

八、异常限制:

父类构造器中声明的异常在基类的构造器中必须也声明,因为父类的构造器总是会显示会隐式(默认构造器)的被调用,而在子类构造器中是无法捕获父类异常的。但子类可以添加父类中没有声明的异常。

重载方法时子类只可抛出父类中声明的异常,因为我们会将子类对象去替换基类,这时如果重载的方法添加类新的异常声明,那么原来的异常处理代码将无法再正常工作。但子类方法可以减少或不抛出父类方法声明的异常。

相关文章
|
4天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
4天前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
|
5天前
|
IDE 前端开发 Java
怎样避免 Java 中的 NoSuchFieldError 异常
在Java中避免NoSuchFieldError异常的关键在于确保类路径下没有不同版本的类文件冲突,避免反射时使用不存在的字段,以及确保所有依赖库版本兼容。编译和运行时使用的类版本应保持一致。
|
6天前
|
Java 编译器
如何避免在 Java 中出现 NoSuchElementException 异常
在Java中,`NoSuchElementException`通常发生在使用迭代器、枚举或流等遍历集合时,尝试访问不存在的元素。为了避免该异常,可以在访问前检查是否有下一个元素(如使用`hasNext()`方法),或者使用`Optional`类处理可能为空的情况。正确管理集合边界和条件判断是关键。
|
9天前
|
Java
Java异常捕捉处理和错误处理
Java异常捕捉处理和错误处理
11 1
|
11天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
26 2
|
11天前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
24 1
|
16天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
30 3
|
17天前
|
Java
如何在 Java 中处理“Broken Pipe”异常
在Java中处理“Broken Pipe”异常,通常发生在网络通信中,如Socket编程时。该异常表示写入操作的另一端已关闭连接。解决方法包括:检查网络连接、设置超时、使用try-catch捕获异常并进行重试或关闭资源。
|
16天前
|
SQL 监控 Java
Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面
本文探讨了Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,以实现高效稳定的数据库访问。示例代码展示了如何使用HikariCP连接池。
10 2