该笔记学习自华中科技大学Java专选课…不看廖雪峰的了.

异常处理

概述

异常(Exception):又称为例外,是程序在运行过程中发生的非正常事件,其发生会影响程序的正常执行

当一个方法中发生错误时,将创建一个对象并将它交给运行时系统,此对象被称为异常对象(exception object)

创建异常对象并将它交给运行时系统被称为抛出一个异常(throw an exception)

异常产生的原因

  • Java虚拟机同步检测到一个异常的执行条件,间接抛出异常
    • 表达式违反了正常的语义,例如整数除零
    • 通过空引用访问实例变量或方法。
    • 访问数组超界
    • 资源超出了某些限制,例如使用了过多的内存
  • 显式地执行throw语句抛出异常

异常的抛出都是由throw语句直接或间接抛出:

  • Java异常都必须继承Throwable的直接或间接子类。用户通过继承自定义异常。
  • Java的异常分为二大类:从Exception派生的是程序级错误,可由程序本身处理;从Error派生是系统级错误,程序可不用处理(也基本上处理不了,例如JVM内存空间不够)。
  • Exception的子类里,除了RuntimeException这个分支外,其他的都是必检异常(即:要么在函数里用catch子句捕获并处理,要么在所在函数加上异常声明,PPT第5页例子)。 RuntimeException的子类是非必检异常(PPT第4页例子)

运行时异常系统处理异常的过程如下:

  • 当发生异常时,运行时系统按与方法调用次序相反的次序搜索调用堆栈,寻找一个包含可处理异常的代码块的方法,这个代码块称为异常处理器(exception handler),即try/catch语句
  • 如果被抛出的异常对象与try/catch块可以处理的类型匹配,运行时系统将异常对象传递给它,这称为捕获异常(catch the exception)
  • 如果运行时系统彻底搜索了调用堆栈中的所有方法,但没有找到合适的异常处理器,程序则终止

非必检异常(Unchecked Exception)是运行时异常(RuntimeException)和错误(Error)类及它们的子类, 非必检异常在方法里可不捕获异常同时方法头可不声明异常,编译器不会报错。但该发生的异常还是要发生

其它的异常称为必检异常(Checked Exception),编译器确保必检异常被捕获或声明(即要不在方法里捕获异常,要不在方法头声明异常)

声明,抛出和捕获异常

声明:方法可以在方法头使用throws子句声明可能抛出异常

方法可以抛出的异常

  • 方法里调用throw语句直接抛出的任何异常
  • 调用另一个方法时,由被调用方法间接抛出的异常

捕获:方法可以通过try/catch语句来捕获异常

异常声明:由方法声明可能抛出的异常
如果方法不捕获其中发生的必检异常,那么方法必须声明它可能抛出的这些异常
通过throws子句声明方法可能抛出的异常。throws子句由throws关键字和一个以逗号分隔的列表组成,列表列出此方法抛出的所有异常,即一个方法可以声明多个可能抛出的异常

public void myMethod() throws IOException {
	InputStream in = 
			new FileInputStream(new File(“C:\\1.txt”));
}

显式抛出异常

public class A {
   //由于main方法里抛出的异常没有被处理,因此在main方法必须加上异常声明throws Exception
   public static void main(String[] args) throws Exception{
       int i = new Scanner(System.in).nextInt();
       if(i > 10){ //假设应用逻辑要求用户输入整数不能大于10
		 throw new Exception("Input value is too big"); //显式地用throw抛出异常
       }
   }
}

捕获异常

try {
		statements
	} catch (ExceptionType1 id1) {
		statements1
	} catch (ExceptionType2 id2) {
		statements2
	} finally {
		statements3
	}

当包含catch子句时,finally子句是可选的。
当包含finally子句时,catch子句是可选的。

  • 将可能抛出异常的语句放在try块中。当try块中的语句发生异常时,异常由后面的catch块捕获处理。
  • 一个try块后面可以有多个catch块。每个catch块可以处理的异常类型由异常类型参数指定。异常参数类型必须是从Throwable派生的类。
  • 当try块中的语句抛出异常对象时,运行时系统将调用第一个异常对象类型与参数类型匹配的catch子句。如果被抛出的异常对象可以被合法地赋值给catch子句的参数,那么系统就认为它是匹配的(和方法调用传参一样,子类异常对象匹配父类型异常参数类型)。
  • 无论try块中是否发生异常,都会执行finally块中的代码。通常用于关闭文件或释放其它系统资源。
  • 处理异常时,也可以抛出新异常,或者处理完异常后继续向上(本方法调用者)抛出异常以让上层调用者知道发生什么事情:链式异常。

下面是一个例子

方法异常声明与方法内捕获处理异常的关系

public class ThrowDeclaration1 {
    //由于m1内部处理了所有异常,因此不用加throws声明
    public void m1(){
        try{
            //执行可能抛出异常的语句
        }
        catch(Throwable e){ //由于Throwable是所有异常的父类,因此这里可以捕获所有异常
            //处理异常
        }
    }

    public void m2(){
        m1(); //由于m1没有异常声明,因此m1的调用者不需要try/catch
    }
}
  • 每个catch根据自己的参数类型捕获相应的类型匹配的异常。
  • 由于父类引用参数可接受子类对象,因此,若把Throwable作为第1个catch子句的参数,它将捕获任何类型的异常,导致后续catch没有捕获机会。
  • 通常将继承链最底层的异常类型作为第1个catch子句参数,次底层异常类型作为第2个catch子句参数,以此类推。越在前面的catch子句其异常参数类型应该越具体。以便所有catch都有机会捕捉相应异常。
  • 无论何时,throw以后的语句都不会执行。
  • 无论同层catch子句是否捕获、处理本层的异常(即使在catch块里抛出或转发异常),同层的finally总是都会执行。
  • 一个catch捕获到异常后,同层其他catch都不会执行,然后执行同层finally。
自定义异常类
  • 自定义异常类必须继承Throwable或其子类。
  • 自定义异常类通常继承Exception及其子类,因为Exception是程序可处理的类。
  • 如果自定义异常类在父类的基础上增加了成员变量,通常需要覆盖toString函数。
  • 自定义异常类通常不必定义clone:捕获和处理异常时通常只是引用异常对象而已

例子

import  java.lang.Exception;
public class ValueBeyondRangeException extends Exception{
    int value, range;
    public ValueBeyondRangeException(int v, int r){ value=v; range=r; }
    public toString( ){ 
        return value + ” beyonds “ + range;
    }
}
//使用例子
int v = 1000,range = 100;
try{ 
	if(v > range)
		throw new ValueBeyondRangeException (v,range);
}
catch(ValueBeyondRangeException e){ System.out.println(e.toString( ));

文本IO

  • 文本:非二进制文件(参见FileInputStream、FileOutputStream)。
  • 类库:java.io.File、java.util.Scanner、java.io.PrinterWriter。
  • 类File: 对文件和目录的抽象,包括:路径管理,文件读写状态、修改日期获取等。
  • 类Scanner:从File或InputStream的读入。可按串、字节、整数、双精度、或整行等不同要求读入。
  • 类PrinterWriter : 输出到File或OutputStream: 可按串、字节、整数、双精度、或整行等不同要求输出

实例

package  filecopy;
import java.lang.System;
import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.Scanner;
public class Copy {
    public static void main(String[ ] args) {  //参数不含程序名
        if(args.length!=2){
            System.out.println("Usage: Java  Copy  <sourceFile>   <tagetFile>");
            System.exit(1);
        };
        File sF=new File(args[0]);                     //args[0]:源文件路径
        if(!sF.exists( )){
            System.out.println("Source Fiel "+args[0]+ "does not exist!");
            System.exit(2);
        };
        File tF=new File(args[1]);      //args[1]:目标文件
        if(tF.exists( )){
            System.out.println("Target File "+args[0]+ "already exist");
            System.exit(3);
        };
        try{
            Scanner input=new Scanner(sF);
            PrintWriter output=new PrintWriter(tF);
            while(input.hasNext( )){
                String s=input.nextLine(); //读取下一行
                output.println(s);		//打印这一行
            }
            input.close( );       
            output.close( );
        }
        catch(IOException ioe){       
		System.out.println(ioe.toString( ));      
	 }
    }
}

抽象类

  • 子类继承父类后,通常会添加新的属性和方法。因此沿着继承链越往下继承的子类其属性和方法越来越具体。相反,越上层的祖先类其实现越抽象,甚至无法给出具体实现。一个长方形图形有面积,但其祖先类GeometricObject的getArea()方法可能没法给出具体实现,这时可以定义成抽象方法。
  • Java可定义不含方法体的方法,其方法体由子类根据具体情况实现,这样的方法称为抽象方法(abstract method),包含抽象方法的类必须是抽象类(abstract class)。
  • 抽象类和抽象方法的声明必须加上abstract关键字。
  • 抽象方法的意义:加给子类的一个约束。例如Circle类和Rectangle类计算面积必须使用父类规定的函数签名。这样可以充分利用多态特性使得代码变得更通用
  • 包含抽象方法的类必须是抽象类
  • 抽象类和抽象方法必须用abstract关键字修饰
  • 没有包含抽象方法的类也可以定义成抽象类

小栗子

abstract class GeometricObject{
    //属性和方法定义

    public abstract double getArea();
    public abstract double getPerimeter();
}

class Circle extends GeometricObject{
    //新的属性
    
    @Override
    public double getArea() {
        //给出具体实现
    }
    @Override
    public double getPerimeter() {
        //给出具体实现
    }
}
  • 抽象方法:使用abstract定义的方法或者接口中定义的方法(接口中定义的方法自动是抽象的,可以省略abstract)。
  • 一个类C如果满足下面的任一条件,则该类包含抽象方法且是抽象类:
    • 类C显式地包含一个抽象方法的声明;
    • 类C的父类中声明的抽象方法未在类C中实现;
    • 类C所实现的接口中有的方法在类C里没有实现
    • 只要类C有一个未实现的方法(自己定义的或继承的),就是抽象类
  • 但是,一个不包含任何抽象方法的类,也可以定义成抽象类

小李子

abstract class A {
    public abstract void m1();
    public abstract void m2();
}

abstract class B extends A{
     //B继承了二个抽象方法,但是只实现了m1,方法m2在B里还是抽象的,因此B必须是抽象类
     @Override
     public void m1() { }
}
class C extends B{
     //C继承B,又实现了方法m2, 因此m1,m2二个方法在C里都有了具体实现
     //因此C就是可以是具体类
     @Override
     public void m2() { }
     
     //当然,C还可以继续覆盖B的m1,给出C的m1实现
}

interface I {
    void m3();      //接口里方法编译器自动加上public abstract来修饰
    void m4();
 }

 abstract class D implements I{
     //class D声明实现了接口I,但只实现了一个接口方法m3
     //接口方法m4在D里还是抽象的,因此D只能是抽象类
     @Override
     public void m3() { }
 }

 //类E继承D,并实现了另一个接口方法m4
 //因此类E是具体类
 class E extends D implements I{ //注意既然E继承了D,所以这里的implements I可以不写
     @Override
     public void m4() { }

     //当然,E还可以继续覆盖D的m3,给出自己的的m3实现
 }

只有实例方法可以声明为抽象方法(Java里所有实例方法自动是虚函数,因此Java里没有virtual关键字)

抽象类不能被实例化,即不能用new关键字创建对象(即new 右边的类型不能是抽象类)。

但是抽象类可以作为变量声明类型、方法参数类型、方法返回类型.为什么?因为一个抽象类型引用变量可以指向具体子类的对象

抽象类可以定义构造函数,并可以被子类调用

抽象类可以定义变量、非抽象方法并被子类使用

抽象类的父类可以是具体类:自己引入了抽象方法。例如,具体类Object是所有类的祖先父类

接口

接口是公共静态常量和公共抽象实例方法的集合。接口是能力、规范、协议的反映。

接口不是类

  • 不能定义构造函数;
  • 接口之间可以多继承,类可implements多个接口
  • 和抽象类一样,不能new一个接口

接口中的所有数据字段隐含为public static final
接口体中的所有方法隐含为public abstract

小李子

public interface I1{
    public static final int k = 1;//可省略public static final
    public abstract void m();      //可省略public abstract
}

public interface I1{
    int k = 1;  //=1不可省略,因为它是final的,必须初始化
    void m( );   //不可定义函数体,它是abstract
}

一个具体的接口实现实例

可以在能够使用任何其他数据类型的地方使用接口。

接口类型属于引用类型,接口类型的变量可以是:
空引用(null)
引用实现了该接口的类的实例

接口需要具体的类去实现。
除非类为abstract,所有接口的成员方法必须被实现
一个类只能继承一个父类,但可以实现多个接口,多个接口以","分开

接口不是类(Java支持单继承类),一个接口可以继承多个接口。

如果接口声明中提供了extends子句,那么该接口就继承了父接口的方法和常量。被继承的接口称为声明接口的直接父接口

任何实现该接口的类,必须实现该接口继承的其他接口。

public interface I1{
	public void m1();
} 
public interface I2 extends I1{
	public void m2();
}
public interface I3 {
	public void m3();
}
public class A implements I2, I3 {
	public void m1() { // implements}
	public void m2() { // implements}
	public void m3() { // implements}

}
// 当一个类实现多个接口时,
// 这个类的实例可以是多种类型
// 如下列表达式都返回true

A a = new A();

a instanceof I1         (true)
a instanceof I2         (true)
a instanceof I3         (true)
a instanceof Object     (true)

I1 i1 = new A();
I2 i2 = new A();
I3 i3 = new A();

Compareable接口: 有时需要比较二个对象,但不同类型对象的比较具有不同的含义,因此Java定义了Comparable接口。

因此,任何需要比较对象的类,都要实现该接口。

public class Max{
    public static Comparable findMax (Comparable o1, Comparable o2){
        if(o1.CompareTo(o2) > 0 )
            return o1;
        else
            return o2;
    }
}

public class ComparableRectangle extends Rectangle implements Comparable {
    /** Construct a ComparableRectangle with specified properties */
    public ComparableRectangle(double width, double height) {
		super(width, height);
    }
    /** Implement the compareTo method defined in Comparable */
    public int compareTo(Object o) {
        if (this.getArea( ) >((ComparableRectangle)o).getArea()) return 1;
        else if (this.getArea( ) <((ComparableRectangle)o).getArea()) return -1;
        else return 0;
    }
} 

Cloneable接口: 任何想克隆的类必须实现该接口,同时覆盖从Object类继承的clone方法,并将访问属性改为public

空接口称为标记接口(markup interface)

空接口有什么作用?唯一目的允许你用instanceof检查对象的类型.

接口与对象的区别

补充

final修饰

修饰对象 解释说明 备注
无子类,不可以被继承,更不可能被重写。 final类中的方法默认是final的
方法 方法不能在子类中被覆盖 类的private方法会隐式地被指定为final方法。final方法不能被子类的方法覆盖,但可以被继承。
变量 称为常量,初始化以后不能改变值。 用final修饰的成员变量表示常量,值一旦给定就无法改变!final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。

我很好奇