本文全文参考《JAVA核心技术 卷1》
前言:人们在遇到错误时总会感觉不爽。如果一个用户在运行程序期间,由于程序的错误或一些外部环境的影响造成用户数据的丢失,用户就有可能不再使用这个程序了。为了避免这类事情的发生,至少应该做到以下几点:
1:向用户通知错误
2:保存所以的工作结果
3:允许用户以妥善的形式退出程序
一、处理错误
1.1.异常分类
- 1)按照异常需要处理的时机分为编译时异常(强制性异常)也叫CheckedException和运行时异常(非强制性异常)也叫RunntimeException。只有Java语言提供了编译时异常,Java认为编译时异常都是可以被处理的异常,所以Java程序必须显式处理编译时异常。如果程序没有处理Checked异常,改程序在编译时就会发生错误无法继续编译。这体现了Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种:
- 1.当前方法知道如何处理该异常,则用try…catch块来处理该异常。
- 2.当前方法不知道如何处理,则在定义该方法时声明抛出该异常。
- 2)运行时异常只有代码在运行时才发现的异常,编译时不需要try…catch。Runtime异常如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示声明或者捕获将对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
1.2.创建自定义异常类
// 自定义异常类
public class ExceptionDemo1 extends IOException {
public ExceptionDemo1(String s){//这个s就是自定义的精髓所在,想传啥传啥
super(s);
}
}
二、捕获异常
2.1 捕获异常
- 要想捕获一个异常,必须设置try/catch语句块。
- 如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
- 1)程序将跳过try语句块的其余代码。
- 2)程序将执行catch子句中的处理器代码。
- 如果在try语句块中的代码没有抛出异常,那么程序将跳过catch子句。
- 如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出(希望调用者为这种类型的异常设计了catch子句)。
public void read(String filename){
try {
FileInputStream in = new FileInputStream(filename);
int b;
while ((b=in.read())!=-1){
System.out.println("读取成功~");
}
} catch (IOException e) {
e.printStackTrace();
}
}
- 通常,最好的选择是什么都不做,而是将异常机制传递给调用者,如果read方法出现了错误,就让read方法的调用者去操心!
public void read(String filename) throws IOException {
FileInputStream in = new FileInputStream(filename);
int b;
while ((b=in.read())!=-1){
System.out.println("读取成功~");
}
}
- 问题:哪种方法更好呢?
- 通常,应该捕获那些知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。
- 如果想传递一个异常,就必须在昂发的首部添加throws说明符,以便告知调用者这个方法可能会抛出异常。
2.2 再次抛出异常与异常链
- 在catch中可以抛出一个异常,这样做的目的是改变异常的类型。
try
{}
catch(SQLException e)
{
throw new ServletException("data error : " + e.getMessage());
}
- 这里, ServletException 用带有异常信息文本的构造器来构造;
- 不过, 可以有一种更好的方法, 并且将原始异常设置为新异常的原因:
try
{}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
- 当捕获到这个异常时, 就可以使用下面的语句重新得到 原始异常:
Throwable e = se.getCause();
- 强烈建议使用这种包装技术, 这样可以让用户抛出子系统中的高级异常, 而不会丢失原始异常的小细节;
- 如果在一个方法中发生了一个已检查异常,而不允许抛出它, 那么包装技术就十分有用。 我们还可以捕获这个已检查异常, 并将它包装成一个 运行时异常;
- 有时候, 你可能只想记录一个异常,再将它重新抛出, 而不做任何改变:
try
{
access the database
}
catch(Exception e)
{
logger.log(level, message, e);
throw e;
}
- 在Java SE7 之前, 将上述代码放入下述方法中, 会出现一个问题;
public void updateRecord() throws SQLException
- 因为, java 编译器查看catch 块中的 throw 语句, 然后查看e的类型, 会指出这个方法可以抛出任何Exception而不仅仅是 SQLException;
- java se 7之后(编译器检测语法合法): 编译器会跟踪到 e 来自于try块中, 假设这个 try 块中仅有 的已检查异常 是 SQLException实例, 另外,假设e在catch块中未改变, 将外围方法声明为 throws SQLException 就是合法的;
三、使用异常机制的技巧
3.1.异常处理不能代替简单的测试
- 例:试着上百万次地对一个空栈进行退栈操作。在实施退栈操作之前,首先要查看栈是否为空。
if(!s.empty()) s.pop();
- 接下来,强行进行退栈操作。然后,捕获EmptyStackException异常来告知我们不能这样做:
try
{
s.pop();
}
catch(EmptyStackException e)
{
}
- 在测试的机器上,调用isEmpty的版本运行时间为646毫秒。捕获EmptyStackException的版本运行时间为21739毫秒。 可以看出,与执行简单的测试相比,捕获异常花费的时间大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常机制。
3.2.不要过分地细化异常
- 有必要将整个任务包装在一个try语句块中,这样,当任何一个操作出现问题时,整个任务都可以取消
try
{
for(i=0;i<100;i++)
{
n=s.pop();
out.writeInt(n);
}
}
catch(IOException e)
{
//problem writting to file
}
catch(EmptyStackException e)
{
//stack was empty
}
- 这段代码看起来清晰多了。这样也满足了异常处理机制的其中一个目标,将正常处理与错误处理分开。
3.3.利用异常层次结构
- 不要只抛出RuntimeException异常、应该寻找更加适当的子类或创建自己的异常类。
- 不要只捕获Throwable异常,否则,会使程序代码更难读、更难维护。
- 考虑受查异常与非受查异常的区别。已检查异常本来就很庞大,不要为逻辑错误抛出这些异常。
- 将一种异常转换成另一种更加适合的异常时不要犹豫,如:在解析某个文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成IOException或MySubsystemException的子类。
3.4.不要压制异常
- **在Java中,更倾向关闭异常。**如果编写了一个调用另一个方法的方法,而这个方法有可能100年才抛出一个异常,那么,编译器会因为没有将这个异常列在throws表中产生抱怨。
- 而没有将这个异常列在throws表中主要出于编译器将会对所有调用这个方法的方法进行异常处理的考虑。因此,应该将这个异常关闭:
public Image loadImage(String s)
{
try
{
//code that threatens to throw checked exceptions
}
catch (Exception e)
{} //so here
}
- 现在即使发生异常也会被忽略
3.5.不要羞于抛出异常
- 很多程序员认为应该捕获抛出的全部异常。
- 如果程序员调用了一个抛出异常的方法,例如,FileInputStream 构造器 或 readLine 方法,程序员就会本能地捕获这些可能产生的异常。
- 其实,传递异常比捕获这些异常更好:
public void readStuff(String filename) throws IOException // not a sign of shame!
{
InputStream in = new FileInputStream(filename);
...
}
- 让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。
“早抛出,晚捕获”
四、使用断言
五、记录日志
六、调试技巧
-
假设编写了一个程序,并对所有的异常进行了捕获和恰当的处理,然后运行这个程序,但还是出现问题,怎么办?
-
一个方便且功能强大的调试器
-
启动调试器之前,可以按照如下建议处理:
-
- 可以用如下方法打印或记录任意变量的值
System.out.println("x=" + x);
Logger.getGlobal().info("x=" + x);
Logger.getGlobal().info("this=" + this);
-
- 一个不太为人所知但却非常有效的技巧是在每一类中放置一个单独的 main 方法
-
对每一个类进行单元测试。
public class MyClass {
methods and fields
...
public static void main(String[] args) {
test code
}
}
-
利用这种技巧,只需要创建少量的对象,调用所有的方法,并检测每个方法是否能够正确地运行。
-
- JUnit
-
JUnit 是一个常见的单元测试框架,利用它可以很容易地组织测试用例套件。
-
- 日志代理(logging proxy)
-
日志代理(logging proxy)是一个子类对象,它可以截获方法调用,并进行日志记录,然后调用超类中的方法。
-
- 利用 Throwable 类提供的 printStackTrace 方法,可以从任何一个异常对象中获得堆栈情况
-
不一定要通过捕获异常来生成堆栈轨迹。
Thread.dumpStack()
-
- 一般来说,堆栈轨迹显示在 System.err 上
-
可以利用 printStackTrace(PrinterWriter s) 方法将它发送到一个文件中。
-
- 通常,将一个程序中的错误信息保存在一个文件中是非常有用的
-
然而,错误信息被发送到 System.err 中,而不是 System.out 中。
-
采用下面的方式捕获错误流:
java MyProgram 2> errors.txt
- 同一个文件中同时捕获 System.err 和 System.out:
java MyProgram 1> errors.txt 2>&1
-
- 让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法
-
比较好的方式是将这些内容记录到一个文件中。
-
- 要想观察类的加载过程,可以用 -verbose 标志启动 Java 虚拟机
-
有助于诊断由于类路径引发的问题
-
- -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检查
javac -Xlint:fallthrough
-
当 switch 语句中缺少 break 语句时,编译器就会给出报告。
-
术语 lint 最初用来描述一种定位 C 程序中潜在问题的工具,现在通常用于描述查找可疑但不违背语法规则的代码问题的工具。
-
- Java 虚拟机增加了对 Java 应用程序进行监控(monitoring)和管理(management)的支持
-
它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。
-
JDK 有一个称为 jconsole 的图形工具,可以用于显示虚拟机性能的统计结果。
jconsole processID
-
- 可以使用 jmap 实用工具获得一个堆的转储其中显示了堆中的每个对象
jmap -dump:format=b, file=dumpFileName processID
jhat dumpFileName
- 通过浏览器进入 localhost:7000,将会运行一个网络应用程序,借此探查转储对象时堆的内容。
-
- 如果使用 -Xprof 标志运行 Java 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法剖析信息将发送给 System.out。输出结果中还会显示哪些方法是由即时编译器编译的。
- 编译器的 -X 选项并没有正式支持,可以运行命令 java -X 得到所有非标准选项的列表。