概述
1. 异常处理
1.1 引言
异常是程序执行过程中产生的异常事件,它会打断正常的程序执行流,因此,在程序执行过程中就会发生异常,例如,你可能期望用户可以输入一个整数,但是输出的却是一个String的文本或者在运行时发生其他的一些没有预料到的错误都会导致异常的发生,最主要的问题是这些异常发生后我们该怎么办?总而言之就是我们如何去处理这些异常。如果这些异常没有被适当的处理,程序可能就会中断并且导致比较严重的后果,例如,网络连接,数据库连接或者文件在打开之后没有来得及关闭就发生异常,可能就会导致数据库或者文件记录丢失。
Java提供了一个内建的机制去处理这些运行时异常,叫做异常处理,它确保你可以书写健壮性比较强的代码。
像C语言这种比较古老的语言就在异常处理上面存在一些缺陷,例如,假设程序员像打开一个文件进行处理。
1、程序员在写代码的时候可能不会意识到异常的发生情况,例如,打开的文件可能不存在,因此,程序员可能不会在打开文件之前检测文件是否存在。
2、假设程序员意识到了异常的发生的情况,他可能想着会先写主逻辑代码,然后再考虑异常,但是往往写完主逻辑代码之后就会忘记写异常处理的代码了。
3、假设程序员考虑到了写异常处理代码,异常处理代码可能会使用if-elses语句跟主逻辑代码混合在一块,这就使得主逻辑比较混乱,程序阅读性差。例如:
if (file exists) {
open file;
while (there is more records to be processed) {
if (no IO errors) {
process the file record
} else {
handle the errors
}
}
if (file is opened) close the file;
} else {
report the file does not exist;
}
java克服了上面这些缺陷,它将异常的处理内置到语言之中,而不是让他们去限制程序员。
1、如果调用某个方法会发生异常,编译器会提醒程序员进行异常处理。因为在方法声明的时候也会声明相应的异常。
2、程序员会被编译器强制进行异常处理,否则编译会不通过,这样程序员就不会忘记进行异常的处理了。
3、异常处理代码和主逻辑代码是分开的,使用的就是try-catch-finally结构。
下面我们来详细说说这三个点:
1、异常必须被声明
例如,假设你想使用java.util.Scanner得到硬盘文件格式化的输入,Scanner构造函数如下:
public Scanner(File source) throws FileNotFoundException;
它声明了该方法可能会产生文件不存在的异常,通过在方法上声明该异常,这样在使用这个方法的时候,程序员就需要处理这个异常。
2、异常必须被处理
如果方法上面声明了一个异常,那么你在使用这个方法的时候就必须处理这个异常,否则编译不通过。
例如:
import java.util.Scanner;
import java.io.File;
public class ScannerFromFile {
public static void main(String[] args) {
Scanner in = new Scanner(new File("test.in"));
// do something ...
}
}
上面代码没有处理异常,编译器会提示如下错误:
ScannerFromFile.java:5: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
Scanner in = new Scanner(new File("test.in"));
^
如果你调用的某个方法可能会抛出异常,你必须进行如下处理:
(1)提供异常处理代码,使用try-catch或者try-catch-finally结构。
(2)在当前方法中对异常不进行处理,但是在该方法上需要声明将异常抛到调用栈中的下一个更高级别的方法中去处理。
例如:try-catch或者try-catch-finally结构处理
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class ScannerFromFileWithCatch {
public static void main(String[] args) {
try {
Scanner in = new Scanner(new File("test.in"));
// do something if no exception ...
// you main logic here in the try-block
} catch (FileNotFoundException ex) { // error handling separated from the main logic
ex.printStackTrace(); // print the stack trace
}
}
}
如果没有找到文件,异常就会在catch块中产生,在上面例子中它只是打印出了栈信息,它提供了有用的调试信息,在有些情况下,你需要做一些清除操作,或者打开另一个文件,可以看到上面的处理逻辑与主逻辑是分开的。
例如:将异常抛向调用栈的下一个更高级别的方法中去。
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class ScannerFromFileWithThrow {
public static void main(String[] args) throws FileNotFoundException {
// to be handled by next higher-level method
Scanner in = new Scanner(new File("test.in"));
// this method may throw FileNotFoundException
// main logic here ...
}
}
上面这个例子中,并没有使用try-catch块去处理FileNotFoundException异常,取而代之的是在它的调用方法main上面去对这个异常进行了声明,这就意味着这个异常会抛到调用栈中下一个更高级别的方法中去,在这个例子中,main()的下一个更高级别的方法是JVM,它会简单的终止程序并且打印栈信息。
3、主逻辑与异常处理逻辑是分开的
在上面的例子中,主逻辑程序是放在try块中,异常处理程序是放在catch块中,很明显它们是分开的,这很大程度上提高了程序的可阅读性。例如:
try {
// Main logic here
open file;
process file;
......
} catch (FileNotFoundException ex) { // Exception handlers below
// Exception handler for "file not found"
} catch (IOException ex) {
// Exception handler for "IO errors"
} finally {
close file; // always try to close the file
}
1.2 方法调用栈
对于应用方法调用的级别是使用方法调用栈进行管理,方法调用栈是一个先进后出的队列,在下面的例子中,main()方法调用methodA(); methodA() 调用 methodB(); methodB() 调用 methodC().
public class MethodCallStackDemo {
public static void main(String[] args) {
System.out.println("Enter main()");
methodA();
System.out.println("Exit main()");
}
public static void methodA() {
System.out.println("Enter methodA()");
methodB();
System.out.println("Exit methodA()");
}
public static void methodB() {
System.out.println("Enter methodB()");
methodC();
System.out.println("Exit methodB()");
}
public static void methodC() {
System.out.println("Enter methodC()");
System.out.println("Exit methodC()");
}
}
Enter main()
Enter methodA()
Enter methodB()
Enter methodC()
Exit methodC()
Exit methodB()
Exit methodA()
Exit main()
从上面的输出中可以看到
1、JVM调用 main()方法。
2、main()在调用methodA()方法之前入栈。
3、methodA()在调用methodB()之前入栈。
4、methodB()在调用methodC()之前入栈。
5、methodC()完成。
6、methodB()出栈并且执行完成。
7、methodA()出栈并且执行完成。
8、main()出栈并且执行完成,程序退出。
假设我们在methodC()中执行除以0的操作,它会引发一个ArithmeticException异常。
public static void methodC() {
System.out.println("Enter methodC()");
System.out.println(1 / 0); // divide-by-0 triggers an ArithmeticException
System.out.println("Exit methodC()");
}
异常消息很清楚的显示了方法调用栈信息并且还是方法的行数。
Enter main()
Enter methodA()
Enter methodB()
Enter methodC()
Exception in thread "main" java.lang.ArithmeticException: / by zero
at MethodCallStackDemo.methodC(MethodCallStackDemo.java:22)
at MethodCallStackDemo.methodB(MethodCallStackDemo.java:16)
at MethodCallStackDemo.methodA(MethodCallStackDemo.java:10)
at MethodCallStackDemo.main(MethodCallStackDemo.java:4)
MethodC()
触发了一个ArithmeticException异常,因为它没有处理这个异常,因此它就会立即出栈,MethodB()也不能处理这个异常并且它也会出栈, methodA() 和 main()也相同,main()会返回到JVM,它会终止程序并且向上面一样打印栈信息。
1.3 异常与调用栈
当java方法中出现一个异常,这个方法会创建一个Exception对象,并且传递这个Exception对象到JVM,Exception对象包含了异常的类型以及异常发生时程序的状态,JVM的责任就是寻找处理这个Exception对象的异常处理者,它在调用栈中不断的向后搜索直到找到一个匹配的异常处理者来处理这个Exception对象,如果JVM在方法栈的所有方法中没有找到一个匹配的异常处理者,它就会终止程序。
整个过程可以想象如下:假设methodD()发生了一个异常事件并且向JVM抛出了一个XxxException,JVM会在调用栈中向后搜索匹配的异常处理者,它一步一步沿着调用栈向后寻找,发现methodA()有一个XxxException处理者,并且会将这个异常对象传递给这个异常处理者者。这里需要注意的是,methodC() 和 methodB()被遍历到了,只是它们没有对异常处理,所以它们的方法会声明一个throws XxxException。
1.4 异常类 - Throwable, Error, Exception & RuntimeException
下图显示了Exception类的继承图,所有异常对象的基类是java.lang.Throwable,它包含有两个子类,java.lang.Exception 和 java.lang.Error。
Error表示系统内部异常(例如:VirtualMachineError, LinkageError),这种异常一般很少发生,如果这种异常发生,你基本上无法处理,程序会在终止。
Exception表示程序异常(例如:FileNotFoundException, IOException),这种异常可以被捕获并且处理。
1.5 可检查与不可检查异常
我们都知道Error的子类以及RuntimeException是不可检查异常,这些异常是编译器无法检查的,因此它们不需要被捕捉或者在方法中声明,因为这些异常例如除以0会导致一个ArithmeticException,索引越界会导致ArrayIndexOutOfBoundException都是程序的逻辑错误,都是可以避免的,应该在程序中进行修正而不是进行运行时异常处理。
其他的异常都是可检查的异常,它们可以被编译器检查到,并且进行捕获或者在方法中声明抛出。
1.6 异常处理操作
在异常处理中有5个关键字:try, catch, finally, throws 和 throw,需要注意throws 和 throw的不同。
Java的异常处理包含以下三种操作:
(1)声明异常
(2)抛出异常
(3)捕获异常
声明异常
Java方法必须对可检查异常进行声明,使用关键字throws。
例如,假设methodD()定义如下:
public void methodD() throws XxxException, YyyException {
// method body throw XxxException and YyyException
}
上面方法表示在调用methodD()可能会遇到两种可检查异常:XxxException 和 YyyException,换句话说,methodD()方法的内部可能会触发XxxException 或者 YyyException。
Error, RuntimeException以及RuntimeException的子类不需要被声明,它们是不可检查异常,因为它们是编译器无法检查到的。
抛出异常
当一个Java操作遇到一个异常,出现错误的语句可以创建一个指定的Exception对象并且通过throw XxxException语句将它抛给Java运行时。
例如:
public void methodD() throws XxxException, YyyException { // method's signature
// method's body
...
...
// XxxException occurs
if ( ... )
throw new XxxException(...); // construct an XxxException object and throw to JVM
...
// YyyException occurs
if ( ... )
throw new YyyException(...); // construct an YyyException object and throw to JVM
...
}
需要注意的是,在方法上声明异常的关键字是throws,在方法体中抛出一个异常对象的关键字是throw。
捕获异常
当一个方法抛出一个异常,JVM会在调用栈中向后搜索来寻找匹配的异常处理者,每个异常处理者可以处理一个指定的异常类,一个异常处理者可以处理一个指定的异常类以及它的子类。如果在方法调用栈中没有找到指定的异常处理者,程序就会终止。
例如,假设methodD()声明可能抛出XxxException 和 YyyException异常。
public void methodD() throws XxxException, YyyException { ...... }
在methodC()中调用methodD()方法可能出现以下情形:
(1)在调用methodD()的时候,使用try-catch (或者 try-catch-finally)语句去包裹methodD()。每个catch块都包含有一种类型的异常处理者。
public void methodC() { // no exception declared
......
try {
......
// uses methodD() which declares XxxException & YyyException
methodD();
......
} catch (XxxException ex) {
// Exception handler for XxxException
......
} catch (YyyException ex} {
// Exception handler for YyyException
......
} finally { // optional
// These codes always run, used for cleaning up
......
}
......
}
(2)假设调用methodD()的方法methodC()不想通过try-catch去处理异常,它可以声明将异常抛给调用栈的下一个方法。
public void methodC() throws XxxException, YyyException { // for next higher-level method to handle
...
// uses methodD() which declares "throws XxxException, YyyException"
methodD(); // no need for try-catch
...
}
在上面这个例子中,如果XxxException 或者 YyyException被methodD()抛出,JVM就会终止methodD()并且methodC()会将异常传递给调用栈中methodC()的调用者。
1.7 try-catch-finally
try-catch-finally的语法如下:
try {
// main logic, uses methods that may throw Exceptions
......
} catch (Exception1 ex) {
// error handler for Exception1
......
} catch (Exception2 ex) {
// error handler for Exception1
......
} finally { // finally is optional
// clean up codes, always executed regardless of exceptions
......
}
(1)如果在执行try块中的内容的时候没有异常发生,所有catch块中的逻辑将会跳过,在执行try块逻辑之后finally块将会执行。如果try块中抛出一个异常,Java运行时就会不顾try块后面的内容直接跳到catch块中去寻找对应的异常处理者,它会匹配每个catch块中的异常类型,如果某个catch块的异常类型跟抛出异常的类型相同或者是抛出异常类型的父类,那么匹配成功,这个catch就会执行,在执行完catch块内容执行接着就会执行finally块里面的逻辑,执行完了finally块里面的逻辑之后就会接着执行后面的逻辑了。
(2)如果没有匹配的catch块,异常就会在调用栈中传给该方法的调用者,当前的方法会执行finally块并且退出调用栈。
值得注意的是不管异常是否发生finally块总是会执行的。除非JVM遇到了严重的错误或者在catch块中执行了System.exit()方法。
例子1
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class TryCatchFinally {
public static void main(String[] args) {
try { // main logic
System.out.println("Start of the main logic");
System.out.println("Try opening a file ...");
Scanner in = new Scanner(new File("test.in"));
System.out.println("File Found, processing the file ...");
System.out.println("End of the main logic");
} catch (FileNotFoundException ex) { // error handling separated from the main logic
System.out.println("File Not Found caught ...");
} finally { // always run regardless of exception status
System.out.println("finally-block runs regardless of the state of exception");
}
// after the try-catch-finally
System.out.println("After try-catch-finally, life goes on...");
}
}
当FileNotFoundException被触发的时候输出如下:
Start of the main logic
Try opening a file ...
File Not Found caught ...
finally-block runs regardless of the state of exception
After try-catch-finally, life goes on...
当没有异常发生的时候,输出如下:
Start of the main logic
Try opening a file ...
File Found, processing the file ...
End of the main logic
finally-block runs regardless of the state of exception
After try-catch-finally, life goes on...
例子2
public class MethodCallStackDemo {
public static void main(String[] args) {
System.out.println("Enter main()");
methodA();
System.out.println("Exit main()");
}
public static void methodA() {
System.out.println("Enter methodA()");
try {
System.out.println(1 / 0);
// A divide-by-0 triggers an ArithmeticException - an unchecked exception
// This method does not catch ArithmeticException
// It runs the "finally" and popped off the call stack
} finally {
System.out.println("finally in methodA()");
}
System.out.println("Exit methodA()");
}
}
执行结果:
Enter main()
Enter methodA()
finally in methodA()
Exception in thread "main" java.lang.ArithmeticException: / by zero
at MethodCallStackDemo.methodA(MethodCallStackDemo.java:11)
at MethodCallStackDemo.main(MethodCallStackDemo.java:4)
try-catch-finally
(1)try块必须伴随着至少一个catch块或者一个finally块。
(2)可以有多个catch块,每个catch块对应一种异常类型。
(3)catch块中包含了一个参数,它是抛出的throwable对象,例如:
catch (AThrowableSubClass aThrowableObject) {
// exception handling codes
}
(4)对于catch块中的参数throwable对象,可以使用下面方法来得到异常的类型和程序的调用状态:
- printStackTrace():打印Throwable和它的调用栈信息,第一行就是异常的类型,后面的就是带有行数的方法的调用栈信息。
try {
Scanner in = new Scanner(new File("test.in"));
// process the file here
......
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}
也可以使用printStackTrace(PrintStream s) 或者 printStackTrace(PrintWriter s).
- getMessage(): 如果异常对象使用的是throwable(String message)构造函数,返回异常对象指定的message。
- toString(): 返回Throwable对象的短描述,它包含了"类名"+":"+getMessage()。
(6)catch块的顺序非常重要,子类异常必须在父类异常的前面,因为异常的匹配是从上到下进行的,如果子类异常在父类异常的后面,可以子类异常就没有作用了,因为它都被父类异常捕捉了,编译器也会报出"exception XxxException has already been caught"。
(7)finally块主要执行一些清除操作,例如文件的关闭,另外finally是总会执行的,除非catch块提前手动的结束当前方法。
如果对于异常并不关心
一般这个是不建议的,但是做法可以是在main方法和其他方法上使用"throws Exception"语句声明。
public static void main(String[] args) throws Exception { // throws all subclass of Exception to JRE
Scanner in = new Scanner(new File("test.in")); // declares "throws FileNotFoundException"
......
// other exceptions
}
方法的重新和重载
(1)重写方法必须有相同的参数列表和返回类型,重载方法必须有不同的参数列表,但是它可以有任意的返回类型。
(2)重写方法时不能添加有更多的访问限制,例如,protected的方法,重写的时候可以为protected或者public但是不能是private或者default,因为重写方法是为了覆盖原来的方法,因此不能有更多的限制。
(3)如果原始方法没有声明异常类型,那么重写这个方法不能声明异常类型,如果原始方法声明了异常类型,重写这个方法可以声明该类型或者该类型的子类型。
(4)重载方法必须有不同的参数列表,如果仅仅只是在返回类型,异常,或者限定符上不同,都是不合法的,当参数列表不同的时候,它可以有任意的返回类型,访问限制或者异常。
1.8 常用异常类
- ArrayIndexOutOfBoundsException:如果访问的数组索引超出了数组的限制,就会被JVM抛出。
int[] anArray = new int[3];
System.out.println(anArray[3]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
- NullPointerException:当代码尝试使用一个对象的空引用的时候,就会被JVM抛出。
String[] strs = new String[3];
System.out.println(strs[0].length());
Exception in thread "main" java.lang.NullPointerException
- NumberFormatException: 当尝试将一个字符串转换为一个数字类型,但是字符串没有合适的转换方法。
Integer.parseInt("abc");
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
- ClassCastException: 当对象类型转换失败的时候,就会被JVM抛出。
Object o = new Object();
Integer i = (Integer)o;
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Integer
- IllegalArgumentException:当一个方法接受到一个非法或者不合适的参数的时候就会抛出,我们可以在自己的代码中重用这个异常。
- IllegalStateException: 最典型的例子就是当可能某个方法只允许调用一次,但是被尝试多次调用,也就是当某个方法被调用,但是当前程序对于这个任务的执行并不在一个合适的状态下,就会抛出这个异常。
- NoClassDefFoundError: 当某个定义的类没有被找到,就会被抛出。在JDK 1.7之前,如果你尝试访问一个不存在的类,就会看到看到这个异常调用栈信息,在JDK 1.7之后,就只会打印简要的错误信息:"Error: Could not find or load main class xxx"。
值得注意的是,RuntimeException和它的子类是编译器不可检查的类,所以它们不需要在方法中声明,因此使用它们的时候应该小心,因为它们是由于不好的编程习惯造成的,是可以避免的。
例子:
// Create our own exception class by subclassing Exception. This is a checked exception
public class MyMagicException extends Exception {
public MyMagicException(String message) { //constructor
super(message);
}
}
public class MyMagicExceptionTest {
// This method "throw MyMagicException" in its body.
// MyMagicException is checked and need to be declared in the method's signature
public static void magic(int number) throws MyMagicException {
if (number == 8) {
throw (new MyMagicException("you hit the magic number"));
}
System.out.println("hello"); // skip if exception triggered
}
public static void main(String[] args) {
try {
magic(9); // does not trigger exception
magic(8); // trigger exception
} catch (MyMagicException ex) { // exception handler
ex.printStackTrace();
}
}
}
输出结果:
hello
MyMagicException: you hit the magic number
at MyMagicExceptionTest.magic(MyMagicExceptionTest.java:6)
at MyMagicExceptionTest.main(MyMagicExceptionTest.java:14)
2. 断言 (JDK 1.4)
JDK 1.4引入了一个新的关键字叫做"assert",它支持的就是断言特性,Assertion就是用来检测你关于程序逻辑的假设(例如:前提条件,后置条件和不变关系)。每个断言包含一个boolean表达式,如果为true表示假设与执行结果相同,否则JVM就会抛出一个AssertionError。它表示你有一个错误的假设,它需要被修正,使用断言比使用if-else表达式更好,它是对你假设的有效说明,并且它不影响程序性能。
assert声明有两种形式:
assert booleanExpr;
assert booleanExpr : errorMessageExpr;
当程序执行断言的时候,首先会执行booleanExpr,如果结果为true,程序会正常执行,否则,就会抛出一个AssertionError,如果是上面第一种声明,那么AssertionError的创建使用的就是一个无参构造器,如果是上面第二种声明,AssertionError的创建使用的就是一个有参构造器,参数为errorMessageExpr,如果errorMessageExpr是一个Object类型,它会自动执行toString函数转为一个字符串传递给AssertionError的构造函数。
在检测bug的时候,断言是非常有用的,它可以充当程序流内部执行的说明,可以提高程序的可维护性。
断言的一个比较好的使用地方就是在switch-case中,如果你相信肯定有一个case会被选择执行,如果执行到了default-case是不合理的,那么你可以在default-case中使用断言。
public class AssertionSwitchTest {
public static void main(String[] args) {
char operator = '%'; // assumed either '+', '-', '*', '/' only
int operand1 = 5, operand2 = 6, result = 0;
switch (operator) {
case '+': result = operand1 + operand2; break;
case '-': result = operand1 - operand2; break;
case '*': result = operand1 * operand2; break;
case '/': result = operand1 / operand2; break;
default: assert false : "Unknown operator: " + operator; // not plausible here
}
System.out.println(operand1 + " " + operator + " " + operand2 + " = " + result);
}
}
在上面的例子中,"assert false"总是会触发AssertionError。但是输出结果是不同的,它取决于是否断言可用,默认是不可用的。
> javac AssertionSwitchTest.java // no option needed to compile
> java -ea AssertionSwitchTest // enable assertion
Exception in thread "main" java.lang.AssertionError: %
at AssertionSwitchTest.main(AssertionSwitchTest.java:11)
> java AssertionSwitchTest // assertion disable by default
5 % 6 = 0
在上面的例子中,因为在default case中始终为flase,也就是总是会触发AssertionError,所以你可以执行在default case里面直接抛出一个AssertionError异常,这样就不需要看断言是否打开了。
default: throw new AssertionError("Unknown operator: " + operator);
另一种断言的使用就是断言一个内部变量的值。
public class AssertionTest {
public static void main(String[] args) {
int number = -5; // assumed number is not negative
// This assert also serve as documentation
assert (number >= 0) : "number is negative: " + number;
// do something
System.out.println("The number is " + number);
}
}
> java -ea AssertionSwitchTest // enable assertion
Exception in thread "main" java.lang.AssertionError: -5
at AssertionTest.main(AssertionTest.java:5)
断言可以用来对程序进行验证:
(1)内部变量的不变性:断言一个值是否在某个限制内,例如:assert x > 0。
(2)类的不变性:断言一个对象的状态是否在某个限制内,类的不变性一般通过私有的boolean方法进行验证,例如isValid() 方法用来验证一个Circle对象的半径值是否为正数。
(3)控制流的不变性:断言程序是否执行到了某个特定的位置。
(4)方法的前置条件:主要检查方法的参数或者它的对象的状态。
(5)方法的后置条件:查看一个方法是否成功执行。
public方法的前置条件
对于传入public方法的参数,不应该使用断言来检查参数的有效性,因为public方法是暴露的,任何人都可能使用非法参数调用这个方法,取而代之的是使用if-else语句进行判断并且抛出IllegalArgumentException,另外,对于private方法,它的执行比较单一,所以断言它的前置状态是比较合适的。
// Constructor of Time class
public Time(int hour, int minute, int second) {
if(hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
throw new IllegalArgumentException();
}
this.hour = hour;
this.minute = minute;
this.second = second;
}
原文链接:Java Programming Exception Handling & Assertion
欢迎关注我的公众号:DroidMind
精品内容,独家发布
最后
以上就是朴实哑铃为你收集整理的Java编程手册—异常处理与断言的全部内容,希望文章能够帮你解决Java编程手册—异常处理与断言所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复