Java Exception

Java异常

Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以强制程序必须处理所有的Checked异常,而Runtime异常无需处理。Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也给编程带来了一些繁琐之处。

异常处理机制

当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将”业务功能实现代码”和”错误处理代码”分离,提供更好的可读性。

使用try-catch捕获异常

1
2
3
4
5
try {
//业务实现代码
} catch(Exception e) {
//Exception处理代码
}

如果没有找到处理异常的catch块,程序就会直接退出。

异常类的继承体系

Java_exception_1

Java_exception_2

Error错误一般指与虚拟机相关的严重问题,应用程序通常无法处理,无需捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DivTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException ie)
{
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch (NumberFormatException ne)
{
System.out.println("数字格式异常:程序只能接受整数参数");
}
catch (ArithmeticException ae)
{
System.out.println("算术异常");
}
catch (Exception e)
{
System.out.println("未知异常");
}
}
}

异常捕获时,一定要记住先捕获小异常,再捕获大异常

Java 7提供的多异常捕获

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
  • 捕获多种类型的异常时,异常变量有隐式的final修士,因此程序不能对异常变量重新赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MultiExceptionTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException|NumberFormatException
|ArithmeticException ie)
{
System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
// 捕捉多异常时,异常变量默认有final修饰,
// 所以下面代码有错:
ie = new ArithmeticException("test"); //①
}
catch (Exception e)
{
System.out.println("未知异常");
// 捕捉一个类型的异常时,异常变量没有final修饰
// 所以下面代码完全正确。
e = new RuntimeException("test"); //②
}
}
}

访问异常信息

  • getMessage(): 返回该异常的详细描述字符串
  • printStackTrace(): 将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s): 将该异常的跟踪栈信息输出到制定输出流
  • getStackTrace(): 返回该异常的跟踪栈信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AccessExceptionMsg
{
public static void main(String[] args)
{
try
{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}

使用finally回收资源

Java的垃圾回收机制不会回收任何物理资源(数据库连接,网络连接,磁盘文件等),这些物理资源都必须显式回收。

异常处理语法结构中只有try快是必须的,但catch和finally块至少出现其中之一,也可以同时出现,且finally快必须位于所有catch块之后。如果直接抛出异常就可以不需要catch和finally。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class FinallyTest
{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
//return语句强制方法返回
return ; //①
//使用exit来退出虚拟机
//System.exit(1); //②
}
finally
{
//关闭磁盘文件,回收资源
if (fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收!");
}
}
}

除非在try块,catch块中调用了推出虚拟机的方法,否则不管在try,catch中执行了怎样的代码,是否抛出异常,是否return,异常处理的finally块总会被执行。

不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,会导致try块,catch块中的return,throw语句失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FinallyFlowTest
{
public static void main(String[] args)
throws Exception
{
boolean a = test();
System.out.println(a);
}
public static boolean test()
{
try
{
// 因为finally块中包含了return语句
// 所以下面的return语句失去作用
return true;
}
finally
{
return false;
}
}
}

当Java程序执行try块,catch块时遇到return或throw语句,这两个语句都会导致该块的执行立即结束,但系统并不会立即停止执行return或throw,而是先去寻找异常处理流程是否包含finally块,如果没有finally块,程序立即执行return或throw语句;如果有finally,系统立即执行finally块,只有finally块执行完成后,系统才会再次跳回来执行try块,cath块里的return或throw语句。如果finally块里也使用了return或throw,在方法在finally块就会终止,不会再执行try,catch中的return,throw代码。

异常处理的嵌套

最好不要使用超过两层的嵌套异常处理。

Java 7的自动关闭资源的try语句

为了保证try语句可以正常自动关闭资源,这些资源实现类必须实现AutoCloseable或者Closeable借口,也就是其中的close()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AutoCloseTest
{
public static void main(String[] args)
throws IOException
{
try (
// 声明、初始化两个可关闭的资源
// try语句会自动关闭这两个资源。
BufferedReader br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new
FileOutputStream("a.txt")))
{
// 使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}

Checked异常和Runntime异常体系

Checked异常必须处理,两种处理方式:

  1. 当前方法明确如何处理该异常,使用try…catch块捕获异常,然后修复。
  2. 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

使用throws声明抛出异常

1
2
3
4
5
6
7
8
public class ThrowsTest
{
public static void main(String[] args)
throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}

一旦使用throws语句声明抛出该异常,程序就无需使用try…catch块来捕获该异常。

使用throws声明抛出异常时有一个限制,就是方法重写时”两小”中的一条规则:子类方法声明抛出的异常应该是父类方法声明抛出的异常的相同或子类,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OverrideThrows
{
public void test()throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows
{
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test()throws Exception
{
}
}

在大部分时候推荐使用Runntime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runntime异常将更加简洁。

当使用Runtime异常时,程序无需在方法中声明抛出Checked异常,一旦发生了自定义作物,程序只管抛出Runtime异常即可。

如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以使用try…catch块来捕获Runtime异常。

使用throw抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ThrowTest
{
public static void main(String[] args)
{
try
{
// 调用声明抛出Checked异常的方法,要么显式捕获该异常
// 要么在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// 调用声明抛出Runtime异常的方法既可以显式捕获该异常,
// 也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
//自行抛出Exception异常
//该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
//自行抛出RuntimeException异常,既可以显式捕获该异常
//也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}

自定义异常类

在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常包含了该异常的有用信息,所以在选择抛出异常时,应该选择合适的异常类,从而可以明确描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。

用户自定义异常都应该继承Exception基类,如果希望自定义个Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息。(getMessage()).

1
2
3
4
5
6
7
8
9
10
public class AuctionException extends Exception
{
//无参数的构造器
public AuctionException(){} //①
//带一个字符串参数的构造器
public AuctionException(String msg) //②
{
super(msg);
}
}

如果需要自定义个Runtime异常,只需要将AuctionException.java程序中的Exception
基类改为RuntimeException基类,其他地方无需修改。

在大部分情况下,创建自定义异常都可采用与AuctionException.java相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常。

catch和throw同时使用

在更复杂的情况,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者再次捕获处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AuctionTest
{
private double initPrice = 30.0;
// 因为该方法中显式抛出了AuctionException异常,
// 所以此处需要声明抛出AuctionException异常
public void bid(String bidPrice)
throws AuctionException
{
double d = 0.0;
try
{
d = Double.parseDouble(bidPrice);
}
catch (Exception e)
{
// 此处完成本方法中可以对异常执行的修复处理,
// 此处仅仅是在控制台打印异常跟踪栈信息。
e.printStackTrace();
//再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,"
+ "不能包含其他字符!");
}
if (initPrice > d)
{
throw new AuctionException("竞拍价比起拍价低,"
+ "不允许竞拍!");
}
initPrice = d;
}
public static void main(String[] args)
{
AuctionTest at = new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
// 再次捕捉到bid方法中的异常。并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}

异常链

把原始异常信息隐藏起来,仅向上提必要的异常提示信息的处理方式,可以抱枕底层异常不会扩散到表现层,避免向上暴露太多的实现细节。

这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理方式,称为异常链。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SalException extends Exception
{
public SalException(){}
public SalException(String msg)
{
super(msg);
}
//创建一个可以接受Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出,开发者可以找到异常的源头,并跟踪异常一路触发的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SelfException extends RuntimeException
{
SelfException(){}
SelfException(String msg)
{
super(msg);
}
}
public class PrintStackTraceTest
{
public static void main(String[] args)
{
firstMethod();
}
public static void firstMethod()
{
secondMethod();
}
public static void secondMethod()
{
thirdMethod();
}
public static void thirdMethod()
{
throw new SelfException("自定义异常信息");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadExceptionTest implements Runnable
{
public void run()
{
firstMethod();
}
public void firstMethod()
{
secondMethod();
}
public void secondMethod()
{
int a = 5;
int b = 0;
int c = a / b;
}
public static void main(String[] args)
{
new Thread(new ThreadExceptionTest()).start();
}
}

异常处理规则

不要过度使用异常

对于完全已知的错误,应该编写处理这种错误的代码。只有对于外部的,不能确定和预支的运行时错误才使用异常。

异常处理的机制初衷是将不可预期的异常的处理代码和正常的业务逻辑处理代码分离,因此觉不要使用异常处理来代替正常的业务逻辑判断。

不要使用过于庞大的try块

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。

避免使用Catch All语句

所谓catch all语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。

不要忽略捕获到的异常

catch整个块为空,或者仅仅打印出出错信息都是不妥的。采取适当操作:

  1. 处理异常。如果知道如何处理,则采取相应措施
  2. 重新抛出新异常。把当前运行环境能做的做完,然后包装异常并抛出。
  3. 如果当前层不清楚如何处理,则不要在当前层使用catch语句,而是直接throws声明抛出该异常。