Java基础(三)

8 阅读20分钟

9. 类的扩展

9.1. 接口

我们在日常生活中,很多时候我们更关注能力而不是对象本身。比如我想要拍照,我关注的是“拍照”这个能力,至于使用什么设备并不重要。再比如比较的能力,同一个类对象之间甚至不同类对象之间都有可能进行比较,比如手机和平板都可以拍照,那谁拍照更厉害点?

9.1.1. 案例

接下来我们假设有手机、平板若干,想要从这些设备里面选出拍照能力最强的设备。不同类别设备之间比较可以写方法来实现,但是类别较多的时候得写多少方法?下面看看使用接口的实现:

interface PhotoAble {
    void takePhoto();

    int photoAbility();

    int compareTo(PhotoAble o);
}

首先我们使用interface关键字定义了接口PhotoAble,这个接口定义了一些方法分别表示:拍照行为、计算设备拍照能力值、拍照设备之间比较。

比较的对象仍然是可拍照设备。

class Phone implements PhotoAble {
    private String name;

    public Phone(String name) {
        this.name = name;
    }

    @Override
    public void takePhoto() {
        System.out.println(name + "拍照");
    }

    @Override
    public int photoAbility() {
        // 复杂的拍照能力逻辑
        return 2;
    }

    @Override
    public int compareTo(PhotoAble o) {
        return photoAbility() - o.photoAbility();
    }
}

class Pad implements PhotoAble {
    private String name;

    public Pad(String name) {
        this.name = name;
    }

    @Override
    public void takePhoto() {
        System.out.println("平板拍照");
    }

    @Override
    public int photoAbility() {
        // 复杂的拍照能力逻辑
        return 1;
    }

    @Override
    public int compareTo(PhotoAble o) {
        return photoAbility() - o.photoAbility();
    }
}

接下来,我们定义了手机和平板类,它们都实现(implements)可拍照接口。compareTo方法返回一个整数,用这个整数的符号表示两个对象的大小关系。

public static void main(String[] args) {
    Pad pad = new Pad("平板");
    Phone phone1 = new Phone("手机1");
    Phone phone2 = new Phone("手机2");
    PhotoAble[] devices = new PhotoAble[]{pad, phone1, phone2};

    PhotoAble max = devices[0];
    for (PhotoAble pa : devices) {
        if (max.compareTo(pa) < 0) max = pa;
    }
    max.takePhoto();
}

测试方法里面把一堆设备放到PhotoAble数组中,然后找出能力值最强的设备,并使用这个设备拍照。

9.1.2. 注意事项
  1. Java 使用interface关键字定义接口,使用implements关键字实现接口。
  2. 接口内可以声明一些方法,接口方法不需要加修饰符,默认都是public abstract。在 Java8 之前,接口内不能实现方法。
  3. Java 继承只能是单继承,但是实现的接口可以是多个,表明这个类有多项能力。
9.1.3. 接口的细节
  1. 接口中是可以定义变量的,修饰符可以省略。省不省略修饰符都是public static final
  2. 接口之间是可以继承的,而且接口的继承可以是多继承,也是使用extends关键字。当类实现接口的时候必须把该接口及其父接口的所有方法都实现一遍。
  3. 类可以同时继承父类与实现接口,但若是同时存在继承必须在前面,例如class A extends B implements C, D {}
  4. 我们一样可以使用instanceof关键字来判断某对象是否实现了接口,例如p instanceof A
9.1.4. 使用接口替代继承

记得上面的“添加数字求和”案例,之前使用继承会破坏代码的封装性。于是后面说使用组合,在Child类中引用Base类对象,这样就可以保护封装性。但这样BaseChild就没有办法统一处理。现在,为了解决这个问题,就可以定义接口来做到统一处理。于是最后就变成组合+接口替换继承。

interface IAdd {
    void add(int number);

    void addAll(int[] numbers);
}

class Base implements IAdd {

    @Override
    public void add(int number) { }

    @Override
    public void addAll(int[] numbers) { }
}

class Child implements IAdd {
    private Base base = new Base();

    @Override
    public void add(int number) { }

    @Override
    public void addAll(int[] numbers) { }
}
9.1.5. 接口增强

Java8 之前,接口中的方法都是抽象方法,不能有实现。Java8 则对接口做了一些增强,允许接口里面定义静态方法和默认方法。看下面的例子:

interface IAdd {
    public static void test() {
        // 静态方法
    }

    default void test2() {
        // 默认方法
    }

    void add(int number);

    void addAll(int[] numbers);
}

静态方法和默认方法都有方法体,静态方法可以通过接口名直接调用,例如IAdd.test()。默认方法使用关键字default关键字修饰,实现类可以选择不实现默认方法,直接使用默认的实现。引入默认方法的原因是某些接口已经被人实现,那么此时我需要往接口里面添加方法,那么所有的实现类都必须做适配处理,比较麻烦。

Java8 中静态方法和默认方法都必须是public,Java9 去除了这个限制,可以使用private。静态方法直接把修饰符从public改为private即可;但对于默认方法而言,privatedefault两个关键字不同一起使用,可以直接定义私有方法,例如private void test() {}。允许定义私有方法主要方便公开的方法复用代码。

9.2. 抽象类

抽象类就是抽象的类,抽象类不能创建对象,它一般用于表达抽象的概念。例如圆形是具体的类,但图形是抽象的;苹果是具体的,水果就是抽象的等等。

之前说的Shape类,里面有一个draw方法。但对于图形来说,绘制是一件不知道如何实现的方法,只有具体的子类才能绘制。因此Shape就可以定义成抽象类,draw可以定义为抽象方法。抽象方法就是只有申明而没有实现的方法,接口里面的方法基本都是抽象的。抽象使用关键字abstract来表示,有抽象方法的类一定是抽象类,但抽象类不一定有抽象方法。

从形式上看,抽象类和接口很相似,有冗余的嫌疑。但实际上抽象类往往和接口互相配合,接口声明能力,抽象类提供默认实现。那有人问,接口有默认方法来提供默认实现,那么抽象类是不是就不需要了?并不是这样,首先抽象类是类,也就是说我们可以在抽象类里面正常定义实例变量之类的,这是接口做不到的。

对于实现接口的类来说,它可以选择自己实现接口或者选择继承抽象类。继承抽象类主要好处就是代码复用,但如果这个类已经有父类了,那么只能选择实现接口,因为 Java 类之间是单继承的。

interface IAdd {
    void add(int number);

    void addAll(int[] numbers);
}

abstract class AbstractAdder implements IAdd {
    @Override
    public void addAll(int[] numbers) {
        for (int num : numbers) {
            add(num);
        }
    }
}

class Adder extends AbstractAdder {
    private static final int MAX_NUM = 100;
    private int[] numbers = new int[MAX_NUM];
    private int count;

    @Override
    public void add(int number) {
        if (count >= MAX_NUM) return;
        numbers[count++] = number;
    }
}

9.3. 内部类

定义在一个类内部的类称为内部类,包含它的类称为外部类。内部类可以很方便访问外部类的私有变量,并且内部类本身可以定义为私有,这样就可以对外隐藏。但实际上,内部类只是编译器的概念,对 Java 虚拟机来说没有这样的概念,每一个内部类最后都会被编译成独立的类。内部类主要分为:

  1. 静态内部类;
  2. 成员内部类;
  3. 方法内部类;
  4. 匿名内部类;

下面我们一一讨论这些内部类。

9.3.1. 静态内部类

静态内部类和静态变量、静态方法定义的位置是一样的,只不过定义的是类。看下面的代码:

class Outer {
    private static int shared = 0;

    public static class StaticInner {
        public void test() {
            shared++;
            System.out.println("inner: " + shared);
        }
    }

    public void show() {
        System.out.println("outer: " + shared);
    }
}

静态内部类与独立的类差别不大,可以有静态变量、静态方法、实例变量、实例方法、构造方法等。静态内部类可以访问外部类的静态变量静态方法(私有的也能访问),但不可访问外部类的实例变量与实例方法。当内部类与外部类有变量同名的时候,需要使用外部类.变量名的方式使用。

外部类里面使用静态内部类,直接使用new StaticInner()即可。非私有的静态内部类可以被其它类访问,需要使用外部类.静态内部类的方式使用。看下面位于一个独立的类中的测试代码:

public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.StaticInner inner = new Outer.StaticInner();
    outer.show();
    inner.test();
    outer.show();
}

上面我们说,外部类和内部类最后都会被编译成两个独立的类。那么独立出来是怎么访问外部类的私有静态变量的?我们看看具体实现:

public class Outer {
    private static int shared = 100;
    public void test(){
        Outer$StaticInner si = new Outer$StaticInner();
        si.innerMethod();
    }
    static int access$0(){
    	return shared;
    }
}

// 内部类的名字会加上外部类名的前缀
public class Outer$StaticInner {
    public void innerMethod() {
    	System.out.println("inner " + Outer.access$0());
    }
}

为了让内部类访问到外部类的私有静态变量或私有静态方法,单独为外部类添加帮忙访问的静态方法。

静态内部类主要的使用场景:当我们类内部需要某种数据结构,而又不依赖外部类的具体实例,就可以使用静态内部类。比如外部类有一个方法需要返回数组里的最大值和最小值,那么我们可以定义一个静态内部类Pair用来存储最大值和最小值。之所以不把Pair分离成一个独立的类,是因为这个名字比较普遍,若是其他人也需要定义类似名字的变量比较麻烦。

Java API 中LinkedList类中就有一个私有静态内部类Node表示链表的节点。

9.3.2. 成员内部类

从形式上说,今天内部类和成员内部类的区别仅仅是没有static关键字修饰。但两者实际差距很大,看一个例子:

public class Outer {
    private int a = 0;
    private static int shared = 0;

    public class Inner {
        private int a = 9;

        public void test() {
            Outer.shared++;
            show();
            System.out.printf("inner:%d, %d\n", a, Outer.this.a);
        }
    }

    public void show() {
        System.out.printf("outer:%d, %d\n", a, shared);
    }
}

与静态内部类不同,成员内部类除了可以访问外部类的静态变量与静态方法还能访问外部类的成员变量与成员方法(不管是否私有都可以)。内外两个类存在变量或方法重名的时候,可以使用外部类.变量名的方式访问外部类的静态变量或静态方法,也可以使用外部类.this.变量名的方式访问外部类的实例变量或实例方法。

在外部类里面想要使用成员内部类,直接使用new Inner()即可。非私有的成员内部类可以被其它类访问,但不能直接通过外部类名来访问,而是需要借助外部类的对象:

public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    inner.test();
}

想要在其它类里面创建成员内部类对象需要使用外部类对象.new 内部类()的方式。

具体的实现如下:

public class Outer {
    private int a = 100;
    private void action() {
    	System.out.println("action");
    }
    public void test() {
        Outer$Inner inner = new Outer$Inner(this);
        inner.innerMethod();
    }
    static int access$0(Outer outer) {
    	return outer.a;
    }
    static void access$1(Outer outer) {
    	outer.action();
    }
}

public class Outer$Inner {
    final Outer outer;
    public Outer$Inner(Outer outer){
    	ths.outer = outer;
    }
    public void innerMethod() {
        System.out.println("outer a " + Outer.access$0(outer));
        Outer.access$1(outer);
    }
}

成员内部类在创建的时候会得到一个外部类的实例的引用,后期便可以通过它来访问实例变量与实例方法。应用场景:当内部类与外部类关系密切,需要使用外部类的实例变量或实例方法,那么定义为成员内部类比较合适。

9.3.3. 方法内部类

定义在方法体里面的内部类称为方法内部类,看下面的代码:

public class Outer {
    private int a = 0;
    private static int shared = 0;

    public void test(final int param) {
        String name = "HH";
        class Inner {
            public void innerMethod() {
                a++;
                shared++;
                System.out.println(a);
                System.out.println(shared);
                System.out.println(param);
                System.out.println(name);
            }
        }
        Inner inner = new Inner();
        inner.innerMethod();
    }
}

内部类Inner定义在方法体里面,这个类只有在该方法内部可以使用。如果外部方法是静态的,那么内部类仅可以访问外部类的静态变量与静态方法;如果外部方法是实例方法,那么内部类可以访问外部类的静态变量、静态方法、实例变量、实例方法。不论外部方法是不是静态的,内部类都可以访问方法的参数与局部变量,前提是该参数或变量是final的或实际上是final的(没有修改)。

方法内部类访问外部类的成员变量的方式与上面其它内部类是一样,那局部变量或参数是怎么访问的呢?为什么要加上只能final的限制呢?

答:实际上,方法内部类在实例化的时候会把用到的参数与局部变量作为参数传到内部类里面。因此实际上,内部类访问到的局部变量和参数都是副本,根本和外部方法里面的变量不是同一个。所以你在内部类修改局部变量是不影响外面的,为了防止出现误解,故要求能访问的参数或局部变量必须要final的或者实际final的。你实在想在方法内部类里面修改外面的局部变量,那么就传一个数组进去,然后修改数组元素。

9.3.4. 匿名内部类

匿名内部类没有单独的类定义,它是在创建对象的同时定义类。语法如下:

new 父类(构造列表) {
	// 内部实现
}
new 父接口() {
    // 内部实现
}

看一个以Point类为父类的匿名内部类的例子:

public static void main(String[] args) {
    final int x = 1, y = 1;
    Point p = new Point(2, 3) {
        @Override
        public double distance() {
            return distance(new Point(x, y));
        }
    };
    System.out.println(p.distance());
}

我们这边创建了一个匿名内部类,其父类是Point。这个内部类重写了distance方法,并在方法里访问了外部方法的局部变量。

匿名内部类只能使用一次,创建一个对象。它没有名字,没有构造,可以调用对应的父类构造。它可以定义实例变量与实例方法,也可以有初始化代码块。与其它内部类相似,它可以访问外部类所有的变量与方法,同时也可以访问外部方法的final局部变量与参数。

这个例子的内部实现类似于:

public class Outer$1 extends Point {
    int x2;
    int y2;
    Outer outer;
    Outer$1(Outer outer, int x1, int y1, int x2, int y2){
        super(x1,y1);
        this.outer = outer;
        this.x2 = x2;
        this.y2 = y2;
    }
    @Override
    public double distance() {
    	return distance(new Point(this.x2, y2));
    }
}

很多方法调用的时候有个参数是接口类型,此时匿名内部类使用也较多。比如比较方法里面需要一个参数是Comparator类型:

Arrays.sort(arr, new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        // 比较逻辑
        return 0;
    }
});

9.4. 枚举

枚举是一种特殊的类型,它的取值是有限的。比如一周七天,一年四季。枚举的定义和使用很简单,下面使用表示大小的枚举来举例子:

enum Size {
    SMALL, MEDIUM, BIG
}

这个例子定义了一个枚举类,名字叫Size。它有三种取值,分别是:SMALLMEDIUMBIG

Size size = Size.SMALL;
System.out.println(size.name());
System.out.println(size.toString());

要取枚举类里面定义的值来给枚举变量赋值。我们可以使用nametoString方法返回枚举值的字面量,这边返回字符串SMALL

println(size == Size.BIG);
println(size.equals(Size.BIG));

枚举变量之间可以进行比较,可以使用equals方法也可以直接使用==进行比较。

println(size.ordinal());

调用ordinal方法可以返回枚举量的序号(从 0 开始编号)。

println(size.compareTo(Size.BIG));

枚举类型都实现了Comparable接口,因此可以调用compareTo方法返回两个变量的大小关系,比较的依据是两个变量的序号。

switch (size) {
    case SMALL:
        println("SMALL"); break;
}

枚举类型可以使用switch语句,但是在case里面直接写值即可,不需要添加类名前缀。例如可以写case BIG而不是case Size.BIG

Size s = Size.valueOf("SMALL");
for (Size s : Size.values()) {
    System.out.println(s);
}

枚举类有静态方法:

  • valueOf方法根据字面量返回枚举类型。
  • values方法按序返回所有的枚举值。

上面普通枚举类型将会被编译器转换成具体的类,转换后的代码大致如下:

public final class Size extends Enum<Size> {
    public static final Size SMALL = new Size("SMALL",0);
    public static final Size MEDIUM = new Size("MEDIUM",1);
    public static final Size LARGE = new Size("LARGE",2);
    private static Size[] VALUES = new Size[]{SMALL,MEDIUM,LARGE};
    
    private Size(String name, int ordinal){
    	super(name, ordinal);
    }
    public static Size[] values(){
        Size[] values = new Size[VALUES.length];
        System.arraycopy(VALUES, 0, values, 0, VALUES.length);
        return values;
    }
    public static Size valueOf(String name){
    	return Enum.valueOf(Size.class, name);
    }
}

可以看到这个类将会继承于Enum类,Enum类有两个实例变量nameordinal,在构造里需要传递。Enum类的name()、toString()、ordinal()、compareTo()、equals()都是基于这两个变量实现的。

上面我们只是介绍了最简单的枚举应用,在日常使用中,枚举值很可能有缩写或其它的别名。此时代码可以改成下面的样子:

enum Size {
    SMALL("S", "小号"),
    MEDIUM("M", "中号"),
    BIG("L", "大号");

    private String abbr;
    private String title;

    Size(String abbr, String title) {
        this.abbr = abbr;
        this.title = title;
    }

    public static Size fromAbbr(String abbr) {
        for (Size size : values()) {
            if (size.abbr.equals(abbr)) return size;
        }
        return null;
    }

    public String getAbbr() {
        return abbr;
    }

    public String getTitle() {
        return title;
    }
}
  • 我们给枚举类型添加了两个属性abbrtitle,它们分别表示缩写和中文名称。
  • 把这两个属性加到构造方法里面,注意:因为枚举变量的值是固定的,所以这个构造只能是私有的,不能被外人调用,private修饰符可以省略。
  • 为了方便,我们定义了一个根据缩写返回对应枚举值的静态方法。
  • 此时,我们在定义枚举值的时候需要:
    • 枚举值必须定义在类的最上面。
    • 枚举值定义的时候,需要以构造的方式传参。
  • 使用还是老样子Size s = Size.BIG;或者Size s = Size.fromAbbr("L");

这种情况下编译器转换的源码大致如下:

public final class Size extends Enum<Size> {
    public static final Size SMALL = new Size("SMALL",0, "S", "小号");
    public static final Size MEDIUM = new Size("MEDIUM",1,"M","中号");
    public static final Size LARGE = new Size("LARGE",2,"L","大号");
    private String abbr;
    private String title;
    
    private Size(String name, int ordinal, String abbr, String title){
        super(name, ordinal);
        this.abbr = abbr;
        this.title = title;
    }
    // 其它代码
}

一般情况下,每个枚举值都会关联一个id值。一个自然的想法是使用ordinal,如果这样写会有一个隐患:如果枚举变量的id已经被存储到了其它地方(例如数据库),我们这时准备再添加一个枚举值,那么就有可能改变枚举值的ordinal,那么存储的id就可能对应错误的枚举值。合理的做法:

public enum Size {
    XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40);
    private int id;
    
    private Size(int id){
    	this.id = id;
    }
    public int getId() {
    	return id;
    }
}

10. 异常

程序运行过程中会遇到许许多多的问题,比如内存耗尽、网络异常、使用未初始化的变量等。这些非正常的情况,在 Java 里面都认作异常。

10.1. 案例

下面的案例,我们在字符串s为空的情况下调用方法。

public static void main(String[] args) {
    String s = null;
    s.indexOf('a');
    System.out.println("end");
}

运行结果类似于:

Exception in thread "main" java.lang.NullPointerException:
	at com.luyan.Main.main(Main.java:6)

这个输出告诉我们Main类的main方法里面第 6 行出问题了。主要是null不能调用方法,所以程序没法继续执行下去了。这时就启动异常处理机制:

  1. 首先创建异常对象NullPointerException(空指针异常)。
  2. 然后查看谁能处理这个异常,没有人处理就启用默认机制。
  3. 默认打印异常栈并结束程序,因为是结束程序,所以出错代码后面的代码是运行不到的。

很多时候默认的处理机制我们并不满意,我们可以使用try/catch来捕获异常:

public static void main(String[] args) {
    try {
        String s = null;
        s.indexOf('a');
    } catch (NullPointerException e) {
        System.out.println("字符串为 null");
    }
    System.out.println("end");
}

我们使用try块将可能出异常的代码块括起来,然后使用catch块捕获异常。当程序抛出NullPointerException异常的时候会被捕获,然后执行catch内部的代码。因为异常被处理了,所以不会启用默认机制,因此程序也不会退出,后面的代码也能继续执行。

10.2. 异常类

Java 里定义了许多异常类,例如上面见到的NullPointerException。所有的异常类都继承于Throwable,下面我们一一介绍。

10.2.1. Throwable

Throwable是所有异常类的父类,它有 4 个公开的构造:

public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)

构造的参数主要有两个:一个是message,表示异常信息;一个是cause,表示触发该异常的其它异常。异常可以构成一个异常链,上层异常由底层异常触发,cause就表示底层异常。除了构造方法外,Throwable还有一个方法Throwable initCause(Throwable cause)来设置cause,这个方法最多给调用一次

Throwable类有一个很重要的方法fillInStackTrace(),这个方法会在所有的构造里面调用,它表示将异常信息保存,这是我们能看到异常栈的关键。除此之外,Throwable还有一些查看异常信息的方法:

void printStackTrace()  // 打印异常栈信息到标准错误输出流

// 打印异常栈信息到指定的流
void printStackTrace(PrintStream s)
void printStackTrace(PrintWriter s)

String getMessage()  // 获取异常 message
Throwable getCause()  //឴ 获取异常 cause

// 获取异常栈每层的信息,每层信息包含文件名、类名、方法名、行号等信息
StackTraceElement[] getStackTrace()
10.2.2. 异常类体系

Throwable为根,Java 定义了很多异常,部分异常如下图:

Throwable有两个直接子类:ErrorException。其中Error表示系统错误或资源耗尽,应用层不应该抛出或处理这样的异常。Exception表示应用程序错误,我们可以继承它或它的子类来自定义异常。

RuntimeException叫做运行时异常,这个名字比较有误导性,因为其它的异常也都是运行时产生的。准确的说RuntimeException是未受检异常,所谓受检异常指的是强制要求程序员处理的异常,未受检异常则不强制。相比而言,Exception其它子类和它本身都是受检异常,所有Error则是未受检异常。

如此多的异常类相比Throwable这个基类而言并没有添加多少属性和方法,定义这么多异常,主要为了名字不同。因为异常的名字包含了错误的关键信息,取合适的名字可以增加代码的可读性。

10.2.3. 自定义异常

自定义异常只需要取一个合适的名字,然后选择一个合适的父类异常进行继承即可。自定义异常的受检性和其父类一致,一般来说自定义的异常我们也不会添加什么属性方法,只会写几个构造里面调用父类构造即可。

class SelfException extends Exception {
    public SelfException() {
        super();
    }
    public SelfException(String message) {
        super(message);
    }
    public SelfException(String message, Throwable cause) {
        super(message, cause);
    }
    public SelfException(Throwable cause) {
        super(cause);
    }
}

10.3. 异常处理

10.3.1. catch 匹配

我们在案例里面简单看了异常的捕获,实际上catch块可以有多个,表示捕获多种异常,看例子:

try{
	// 可能出异常代码
}catch(NumberFormatException e){
	System.out.println("not valid number");
}catch(RuntimeException e){
	System.out.println("runtime exception "+e.getMessage());
}catch(Exception e){
	e.printStackTrace();
}

当代码出异常的时候会查找能捕获的第一个catch块并执行里面的代码,其它catch里面的代码是不执行的。如果找不到能捕获的,此时后面的代码不会执行,会直接跳到上一层调用中继续查找,到最后都找不到就执行默认机制。

捕获异常的时候,若异常之间如有继承关系,则范围小的异常应该在上面,否则会报错的(因为大异常在上面,就没下面小异常的事情了)。这个例子里面使用了e.getMessage()e.printStackTrace()获取/输出异常信息。在实际应用中,我们会将异常信息保存在专门的日志文件里面而不是直接控制台输出。

当我们多种异常的处理方案是一样的时候,Java7 之后给我们提供了简化的语法|

try {
	// 可能出异常代码
} catch (ExceptionA | ExceptionB e) {
	e.printStackTrace();
}
10.3.2. 重新抛出异常

catch块里面处理完异常后,我们可以重新再抛出异常,可以抛出原来的,也可以抛出新建的异常。

try{
	// 可能出异常代码
}catch(NumberFormatException e){
    System.out.println("not valid number");
	throw new AppException("输入格式不对", e);
}catch(Exception e){
    e.printStackTrace();
    throw e;
}

抛出新异常的时候,把当前异常作为cause传递给新异常。这样就形成一个异常链,调用者可以使用getCause方法获取到原来的异常。

为什么要重新抛出?因为当前代码不能完全处理这个异常,需要调用者进一步处理。

为什么抛出新异常?抛出新异常可能是因为当前异常不合适或者需要补充新信息。

10.3.3. finally

finally是异常机制中重要的一部分,它可以跟在catch后面:

try{
	// 可能出异常代码
}catch(Exception e){
	// 异常捕获
}finally{
	// 无论有无异常都会执行
}
  1. 若无异常发生,finally代码块在try块结束之后执行。
  2. 异常被捕获,则finally块在catch块之后执行。
  3. 有异常但没被捕获,finally块在异常抛给上层之前执行。

因为这些特性,finally块一般用来释放资源,如数据库连接、文件流等。

在实际操作的时候,catch块可以没有,只有try/finally块,这表示出现异常时直接上抛,但finally里面的代码在出现异常的时候也要执行。

有个小细节,当我们在try/catch块里面返回变量ret的值时,此时我们在finally里面改变ret的值会怎样?

public static int test() {
    int ret = 0;
    try {
        return ret;
    } finally {
        ret = 2;
    }
}

最终还是返回 0,返回肯定还是在finally执行完毕之后才返回的。

原因是之前讲的,当遇到return之后,会先把返回值存起来,然后调用方获取存的值。

因此finally只是修改变量ret的值而不是返回值。

那如果finally里面有return呢?那么会返回finally里面的值。不仅如此,finally有返回值时还会掩盖掉上抛的异常,就像没有异常一样。除此之外,如果在finally里面抛出异常,原来的异常会被顶替掉,就消失了。基于此,我们应该避免在finally里面返回值或抛出异常。

10.3.4. try-with-resources

对于需要使用资源的场景,经典流程是在finally里面释放资源。Java7 支持了一种新语法,这称为try-with-resources,它能够自动关闭要释放的资源。这种语法针对实现了AutoCloseable接口的类对象。这个接口定义为:

public interface AutoCloseable {
	void close() throws Exception;
}

使用try-with-resources语法如下:

try(AutoCloseable r = new FileInputStream("Hello")) {
	// 使用资源
}

资源的定义放在try语句里面,我们无需在finally里面手动释放。系统会在执行完try之后自动调用对象的close方法。如果有多个资源,那么在try语句里面定义多个,使用;隔开。

Java9 之后可以把资源定义在外面,但要求资源是final或实际上是final的,如下面代码:

AutoCloseable r = new FileInputStream("Hello");
AutoCloseable r2 = new FileInputStream("Hello");
try(r; r2) {
	// 使用资源
}
10.3.5. throws

异常机制中有一个关键字throw,这个关键字用于代码抛出异常,例如throw new IllegalArgumentException();

还有一个throws关键字与之很像,它用于声明一个方法可能抛出的异常。语法如下:

public void test() throws SQLException, NumberFormatException {
	//主体代码
}

throws可以声明多个异常,异常间使用逗号隔开。它表示这个方法可能会抛出哪些异常,这些异常在方法内无法完全处理,需要调用者继续处理。但具体什么情况抛异常未知,因此我们应该将这些信息用注释的方式标注出来。

对于未受检异常不要求使用throws声明,但是受检异常必须声明然后才能抛出。受检异常声明之后可以不抛出,但抛出必须有声明。

若一个方法 A 调用了另一个有受检异常声明的方法 B,那么 A 必须要处理声明的受检异常(不受检异常不做要求)。处理的方式包括:

  1. 使用catch捕获异常。
  2. 继续使用throws往上抛出。