异常是运行时错误,异常处理使得程序可以处理运行时错误,并且继续通常的执行。异常机制提供了程序退出时的安全通道。当出现错误后,程序执行流程发生改变,程序的控制权转移到异常处理器。
编译错误:程序没有遵循语法规则,编译程序能够自己发现并提示我们错误的原因和位置。
运行时错误:程序在执行时,运行环境发现了不能执行的操作。
逻辑错误:程序没有按照预期的逻辑顺序执行。
在程序运行过程中,如果JVM检测出一个不可能执行的操作,就会出现运行时错误。例如,如果使用一个越界的下标访问数组,程序就会产生一个ArrayIndexOutOfBoundsException的运行时错误;如果程序需要输入一个整数的时候用户输入了一个double值,会得到InputMismatchException的运行时错误。
在Java中,运行时错误会被作为异常抛出。异常就是一种对象,表示阻止正常运行的错误或者情况。如果异常没有处理,那么程序将会非正常终止。
异常是从方法抛出的,方法的调用者可以捕获并处理该异常。
为了演示异常处理,包括异常是如何创建以及抛出的,我们从读取两个整数并显示它们商的例子开始。
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: Quotient
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class Quotient {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
System.out.println("Enter two integer:");
int num1=input.nextInt();
int num2=input.nextInt();
System.out.println(num1+"/"+num2+"="+(num1/num2));
}
}
如果输入0赋值给第二个数字,那就会产生一个运行时错误,因为不能用一个整数除以0(注意,一个浮点数除以0是不会产生异常的)。解决这个错误的一个简单方法就是添加一个if语句来测试第二个数字。
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: QuotientWithIf
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class QuotientWithIf {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
System.out.println("请输入两个整数:");
int num1=input.nextInt();
int num2=input.nextInt();
if(num2!=0){
System.out.println(num1+"+"+num2+"="+(num1/num2));
}else{
System.out.println("被除数不能为0");
}
}
}
为了介绍异常处理,我们使用一个方法计算商,程序如下:
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: QuotientWithMethod
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class QuotientWithMethod {
public static int quotient(int num1,int num2){
if(num2==0){
System.out.println("被除数不能为0");
System.exit(1);
}
return num1/num2;
}
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
System.out.println("请输入两个整数:");
int num1=input.nextInt();
int num2=input.nextInt();
System.out.println(num1+"/"+num2+"="+quotient(num1,num2));
}
}
方法quotient返回两个整数的商,如果num2为0,则不能返回一个值,程序在System.exit(1)处终止,这显然是一个问题。不应该让方法来终止程序—应该由调用者决定是否终止程序。
方法如何通知它的调用者一个异常产生了呢?Java可以让一个方法抛出一个异常,该异常可以被调用者捕获和处理。
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: QuotientWithException
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class QuotientWithException {
public static void quotient(int num1, int num2) {
try {
if (num2 == 0) {
throw new ArithmeticException("被除数不能等于0");//抛出一个异常
}
System.out.println(num1 + "/" + num2 + "=" +(num1/num2));
}catch (ArithmeticException e) {
System.out.println("被除数不能为0");
}
System.out.println("程序继续......");
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("请输入两个整数:");
int num1 = input.nextInt();
int num2 = input.nextInt();
quotient(num1,num2);
}
}
//异常就是从一个异常类创建的对象
//本程序中异常就是Java.lang.ArithmeticException的构造方法ArithmeticException(str)被调用以创建异常类对象,str是描述异常的消息
如果num2等于0,,方法通过执行以下语句抛出一个异常:throw new ArithmeticException(“被除数不能为0”)。在这种情况下,抛出的值为new ArithmeticException(“被除数不能为0”),称为一个异常。throw语句的执行称为抛出一个异常。异常就是从异常类创建的对象。在这种情况下,异常类就是java.lang.ArithmeticException。构造方法ArithmeticException(str)被调用以构建一个异常对象,其中str是描述异常的消息。
当异常被抛出时,正常的执行流程被中断。就像他的名字所提示的,“抛出异常”就是将异常从一个地方传递到另一个地方。调用方法的语句也包含在一个try块和一个catch块中。try块包含了正常情况下执行的代码,异常被catch块所捕获。catch块中的代码被执行以处理异常,之后,catch块之后的语句被执行。
throw语句类似方法的调用,但不同于调用方法的是,它调用的是catch块。从某种意义上讲,catch块就像带参数的方法定义,这些参数匹配抛出的值的类型。但是它不像方法,在执行完catch块之后,程序控制不返回到throw语句,而是执行catch块后的下一条语句。
catch(ArithmeticException ex)中的标识符ex的作用很像是方法中的参数,这个参数称为catch块中的参数。ex之前的类型(如,ArithmeticException)指定了catch块可以捕获的异常类型。一旦捕获该类型,就能从catch块体中的参数访问这个抛出的值。
一个异常可能是通过try块中的throw语句直接抛出,或者调用一个可能会抛出异常的方法而抛出。
异常处理的优点:它能使方法抛出一个异常给它的调用者,并由调用者处理该异常。如果没有这个能力,那么被调用的方法就必须自己处理异常或者终止该程序。被调用的方法通常不知道在出现错误时应该做些什么,只有调用者自己清楚出现错误时需要做些什么。异常处理最根本的优势就是将检测错误(由被调用的方法完成)从处理错误中分离出来。
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: InputMismatchException
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class InputMismatchException {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
boolean continueInput=true;
while(continueInput){
try{
System.out.println("请输入一个整数:");
input.nextInt();
continueInput=false;
}catch(java.util.InputMismatchException e){
System.out.println("输入的不是整数,请重新输入");
input.nextLine();
}
}
}
}
//变量continueInput控制循环,它的初始值是true,当键入一个整数时,该值就变成false
当执行input.nextInt()时,如果输入的不是一个整数,就会产生一个InputMismatchException异常。
异常是对象,对象都采用类来定义。异常的根类是java.lang.Throwable。Throwable类是所有异常类的根,所有Java异常类都直接或间接地继承自Throwable,可以通过继承Exception或者Exception的子类来创建自己的异常类。
这些异常类可以分为三种主要类型:系统错误、异常或运行时异常。(1)系统错误:系统错误是由Java虚拟机抛出的,用Error类表示的。Error类描述的是内部系统错误,这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也做不了。(2)异常:异常是用Exception类表示的,它描述的是由你的程序和外部环境引起的错误,这些错误能被程序捕获和处理。(3)运行时异常:运行时异常是用RuntimeException类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界数组或数值错误,运行时异常通常表明了编程错误。
RuntimeException、Error以及它们的子类都称为免检异常,在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象赋值给它,就会抛出NullPointerException异常;如果访问一个数组的越界元素,就会抛出IndexOutOfBoundsException异常。这些都是程序中必须纠正的逻辑错误。免检异常可能在程序的任何一个地方出现,为避免过多地使用try-catch块,Java语言不强制要求编写代码捕获或声明免检异常。所有其它异常称为必检异常,意味着编译器会强制程序员检查并通过try-catch块处理它们,或者在方法头中声明。
必检异常增加了代码的健壮性,但是为了对异常进行抛出、捕获和处理,需要增加较多代码,会降低代码的可读性。如果异常影响到系运行的安全性和正确性的时候,必须对必检异常进行处理,否则这些必检异常是可以转换成免检异常。
Java的异常处理模型基于三种基本操作:声明一个异常、抛出一个异常、捕获一个异常。
Java解释器调用main方法开始执行一个程序,当前执行的语句必属于某个方法,每个方法都必须声明它可能抛出的必检异常的类型,称为声明异常。因为任何代码都可能发生系统错误和运行时错误,因此Java不要求在方法中显示地声明Error和RuntimeException。然而,方法要跑出的其它异常都必须在方法头中显示声明,这样,方法的调用者会被告知有异常。
为了在方法中声明一个异常,就要在方法头中使用关键字throws,如:public void method() throws IOException,关键字throws表明method()方法可能会抛出异常IOException。如果方法可能抛出多个异常,就可以在关键字throws后添加一个用逗号分隔的异常列表:public void method() throws Exception1,Exception2,…,ExceptionN。如果父类中的方法没有声明异常,那么就不能在子类中对其重写时声明异常。
检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这称为抛出一个异常,如throw new IllegalArgumentException(“Wrong argument”)。IllegalArgumentException是Java API中的一个异常类,通常,Java API中每个异常类都至少有两个构造方法:一个无参构造方法和一个带有可以描述这个异常的String参数的构造方法,该参数称为异常消息,它可以通过一个异常对象调用getMessage()获取。
声明异常的关键字是throws,抛出异常的关键字是throw。
当抛出一个异常时,可以在try-catch块中捕获和处理它。
try{
}catch(Exception1 ex1){
}catch(Exception2 ex2){
}
...
catch(ExceptionN exN){
}
如果在执行try块的过程中没有出现异常,则跳过catch子句。如果try块中的某条语句抛出一个异常,Java就会跳过try块中剩余的语句,然后开始查找处理这个异常的代码,处理异常的这个代码称为异常处理器。从第一个到最后一个逐个检查catch块,判断在catch块中的异常类实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行catch块中的代码。如果没有发现异常处理器,Java会退出这个方法,把异常传递给这个方法的调用者,继续同样的过程来查找处理器。如果在调用的方法链中找不到处理器,程序就会终止并且在控制台上打印出错误信息。查找处理器的过程称为捕获一个异常。
假设main方法调用method1()方法,method1()方法调用method2()方法,method2()方法调用method3()方法,method3()方法抛出一个异常,考虑下面的情形:
main method{
try{
method1();
statement1
}catch(Exception1 ex1){
}
statement2
}
class method1{
try{
method2();
statement3
}catch(Exception2 ex2){
}
statement4
}
class method2{
try{
method3();//一个异常在method3()方法中抛出
statement5
}catch(Exception3 ex3){
}
statement6
}
如果异常类型是Exception3,它就会被method2()中处理异常ex3的catch块捕获,跳过statement5,然后执行statement6。
如果异常类型是Exception2,,则退出method2()方法,控制被返回给method1()方法,而这个异常就会被method1()中处理异常ex2的catch块捕获,跳过statement3,然后执行statement4。
如果异常类型是Exception1,则退出method1()方法,控制被返回给main()方法,而这个异常就会被main()方法中处理异常ex1的catch块捕获,跳过statement1,然后执行statement2。
如果异常类型没有在method2、method1、main方法中捕获,程序就会终止,不执行statement1和statement2。
各种异常类都可以从一个共同的父类中派生。如果一个catch块可以捕获一个父类的异常对象,那么它就可以捕获那个父类的所有子类的异常对象。
在catch块中异常被指定的顺序是非常重要的,如果父类的catch块出现在子类的catch块之前,就会导致编译错误。
try{
}catch(Exception e){
}catch(RuntimeException ex){
}
//错误的顺序
try{
}catch(RuntimeException e){
}catch(Exception ex){
}
//正确的顺序
对于使用同样的代码处理多种异常的情况,可以使用JDK7的新的多捕获特征简化异常的代码书写。语法是:
catch(Exception1 | Exception2 | ... |ExceptionN e){
}
每个异常使用竖线(|)与下一个分隔,如果其中一个异常被捕获,则执行处理的代码。
创建一个CircleWithException类的对象,如果setRadius(double radius)中radius是负数,则抛出一个IllegalArgumentException异常。
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: CircleWithException
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class CircleWithException {
private double radius;
private static int numberOfCircle=0;
public CircleWithException(double radius){
setRadius(radius);
numberOfCircle++;
}
public CircleWithException(){
this(0);
}
public double getRadius(){
return radius;
}
public void setRadius(double radius){
if(radius>=0) {
this.radius = radius;
}else{
throw new IllegalArgumentException("半径不能为负数");
}
}
public double area(){
return radius*radius*3.14;
}
public static int getNumberOfCircle(){
return numberOfCircle;
}
public static void main(String[] args) {
try{
CircleWithException circleWithException1=new CircleWithException(2.0);
CircleWithException circleWithException2=new CircleWithException(-2.0);
CircleWithException circleWithException3=new CircleWithException();
}catch(IllegalArgumentException e){
System.out.println(e.toString());
}
System.out.println("共有"+numberOfCircle+"个圆被创建");
}
}
IllegalArgumentException是异常类RuntimeException(免检异常)的子类,所以,如果不使用try语句,这个测试程序也能编译成功。
万一出现了异常,程序仍然会继续,而如果处理器没有捕获到这个异常,程序就会突然中断。
当没有异常发生时,try-catch的存在对系统性能的影响很小,可以说不会引起额外的系统开销。
异常发生后:(1)初始化异常对象(2)从调用栈返回(3)沿方法调用链来传播异常,以找到它的异常处理器。
无论异常是否发生,finally子句总会被执行。
有时候,不论异常是否出现或是否被捕获,都希望执行某些代码。Java的finally子句可以用来实现这个目的,finally子句的语法如下:
try{
statements;
}catch(Exception e){
}finally{
finalStatements;
}
在任何情况下,finally块中的代码都会执行,不论try块中是否出现异常或者是否被捕获。考虑以下三种可能出现的情况:(1)如果try块中没有出现异常,执行fianlStatements,然后执行try语句的下一条语句;(2)如果try块中有一条语句引起了异常并被catch块捕获,会跳过try块的其它语句,执行catch块和finally子句,并执行try语句后的下一条语句;(3)如果try块中的一条语句引起异常,但是没有被任何catch块捕获,就会跳过try块中的其它语句,执行finally子句,并且将异常传递给这个方法的调用者。
即使在到达finally块之前有一个return语句,finally块还是会执行,同时使用finally子句时可以省略catch块。
当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。
try块包含正常情况下执行的代码,catch块包含异常情况下执行的代码。异常处理将错误处理代码从正常的编程中分离出来,这样,就可以使程序更易读、更易修改。由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以异常处理需要更多的时间和资源。
异常发生在方法中,如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。一般来说,一个项目中多个类都会发生的共同异常应该考虑设计为一个异常类。对于发生在个别方法中的简单错误最好进行局部处理,无需抛出异常,这可以通过使用if语句来检测错误并实现。
如果异常处理器不能处理一个异常,或者只是简单地希望他的调用者注意到该异常,Java允许该异常处理器重新抛出异常。
try{
statements;
}catch(Exception ex){
throw ex;
}
语句throw ex重新抛出异常给调用者,以便调用者的其它处理器获得处理异常ex的机会。
与另一个异常一起抛出一个异常,构成了链式异常。
有时候,可能需要同最初异常一起抛出一个新异常(带有附加信息),称为链式异常。
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: ChainedExceptionDemo
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class ChainedExceptionDemo {
public static void method3() throws Exception{
throw new Exception("异常来自method3()方法");
}
public static void method2() throws Exception{
try{
method3();
}catch(Exception exc){
throw new Exception("异常来自method2()方法",exc);
}
}
public static void method1() throws Exception{
try{
method2();
}catch(Exception ex){
throw new Exception("异常来自method1()方法",ex);
}
}
public static void main(String[] args) {
try{
method1();
}catch(Exception e){
e.printStackTrace();
}
}
}
可以通过继承Java.lang.Exception类来定义一个自定义异常类。
Java提供相当多的异常类,尽量使用它们而不要创建自己的异常类。如果遇到一个不能用预定义异常类来充分描述的问题,那就可以通过继承Exception类或其子类来创建自己的异常类。
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: InvalidRadiusException
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class InvalidRadiusException extends Exception{
private double radius;
public InvalidRadiusException(double radius){
super("Invalid radius:"+radius);
this.radius=radius;
}
public double getRadius(){
return radius;
}
}
这个自定义异常类继承自java.lang.Exception,而Exception类继承自java.lang.Throwable,Exception类中的所有方法(例如,getMessage()、toString()、printStackTrace())都是从Throwable继承而来。
Exception类的构造方法:
Exception():构建一个没有信息的异常
Exception(String message):构建一个给定信息的异常
Exception(String message,Exception cause):构建一个具有指定信息和子句的异常,这形成了一个链式异常
/**
* @author MNH
* @version 1.0
* @project Name: JAVA基础
* @file Name: TestCircleWithCustomException
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestCircleWithCustomException {
public static void main(String[] args) {
try {
CircleWithCustomException circle1 = new CircleWithCustomException(2);
CircleWithCustomException circle2 = new CircleWithCustomException(-2);
CircleWithCustomException circle3 = new CircleWithCustomException();
} catch (InvalidRadiusException ex) {
System.out.println(ex);
}
System.out.println("创建圆的个数:" + CircleWithCustomException.getNumberOfCircle());
}
}
class CircleWithCustomException {
private double radius;
private static int numberOfCircle = 0;
public CircleWithCustomException(double radius) throws InvalidRadiusException {
setRadius(radius);
numberOfCircle++;
}
public CircleWithCustomException() throws InvalidRadiusException {
this(0);
}
public void setRadius(double radius) throws InvalidRadiusException {
if (radius >= 0) {
this.radius = radius;
} else {
throw new InvalidRadiusException(radius);
}
}
public double getRadius() {
return radius;
}
public static int getNumberOfCircle() {
return numberOfCircle;
}
public double area() {
return 3.14 * radius * radius;
}
}
也可以继承RuntimeException声明一个自定义类异常,但这不是一个好方法,因为这会使自定义异常成为免检异常。最好使自定义异常是必检的,这样编译器就可以在程序中强制捕获这些异常。