七. 抽象类与接口

1. 抽象类和方法

在多态的 Fruit 例子中,可以把 Fruit 创建为抽象类,因为它的实际对象基本没有意义。创建一个抽象类是为了通过通用“接口”操纵一系列类。

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明,没有方法体。语法如下:

abstract void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。

如果一个抽象类是不完整的,当试图创建这个类的对象时,Java 不会创建抽象类的对象,只会得到编译器的错误信息,从而保证了抽象类的纯粹性。

如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类就必须是一个抽象类。

可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。

不能同时使用 privateabstract 修饰抽象类中的方法,因为抽象类本身设计的目的就是为了让新类继承并重写抽象方法。

2. 接口创建

使用 interface 关键字来创建接口,描述 Java 8 之前的接口比较容易,因为它们只允许抽象方法。代码示例:

public interface PureInterface {
    int m1(); 
    void m2();
    double m3();
}

接口中方法被隐式指定为 public abstract 修饰。

而到了Java 8之后,允许接口包含默认方法和静态方法。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 Fruit 或 Animal。

和类一样,需要在关键字 interface 前加上 public 关键字(但只是在接口名与文件名相同的情况下),否则接口只有包访问权限,只能在接口相同的包下才能使用它。

接口同样可以包含属性,这些属性被隐式指明为 staticfinal

使用 implements 关键字使一个类实现某个接口(或一组接口)。

默认方法

Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的类可以使用 default 创建的方法体。关键字 default 允许在接口中提供方法实现——在 Java 8 之前被禁止。

public interface MyInterface {

    void firstMethod();
    void secondMethod();

    default void newMethod() {
        System.out.println("newMethod");
    }
}
public class Test implements MyInterface {

    @Override
    public void firstMethod() {
        System.out.println("firstMethod");
    }

    @Override
    public void secondMethod() {
        System.out.println("secondMethod");
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.firstMethod();
        test.secondMethod();
        test.newMethod();
    }
}

输出结果为:

image-20200724111034115

尽管 Test 中未定义 newMethod(),但也能使用 newMethod()。

接口中的静态方法

Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具。

public interface Operations {

    void execute();

    static void runOps(Operations... ops) {
        for (Operations op: ops) {
            op.execute();
        }
    }

    static void show(String msg) {
        System.out.println(msg);
    }
}

这是模版方法设计模式的一个版本,runOps() 是一个模版方法,使用可变参数列表,因而我们可以传入任意多的 Operation 参数并按顺序运行它们:

public class Machine {

    public static void main(String[] args) {
        Operations.runOps(
                new Bing(), new Crack(), new Twist());
    }
}

class Bing implements Operations {
    @Override
    public void execute() {
        Operations.show("Bing");
    }
}

class Crack implements Operations {
    @Override
    public void execute() {
        Operations.show("Crack");
    }
}

class Twist implements Operations {
    @Override
    public void execute() {
        Operations.show("Twist");
    }
}

输出结果:

image-20200724131455932

3. 抽象类和接口的对比

特性接口抽象类
组合新类可以组合多个接口只能继承单一抽象类
状态不能包含属性(除了静态属性,不支持对象状态)可以包含属性,非抽象方法可能引用这些属性
默认方法和抽象方法不需要在子类中实现默认方法,默认方法可以引用其他接口的方法必须在子类中实现抽象方法
构造器没有构造器可以有构造器
可见性隐式 public可以是 protected 或友元

抽象类仍然是一个类,在创建新类时只能继承它一个,而创建类的过程中可以实现多个接口。

有一条实际经验:尽可能地抽象。因此,更倾向使用接口而不是抽象类,只有当必要时才使用抽象类。

4. 接口与继承

使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性),而继承只能单继承。

接口也能使用 extends 关键字来继承另一个接口,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。如:

public interface MyInterface extends MyInterface2, MyInterface3{
    
}

interface MyInterface2 {
    
}

interface MyInterface3 {
    
}

通常来说,extends 只能用于单一类,但是在构建接口时可以引用多个基类接口。当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,需要尽量避免这种情况。

5. 接口适配

接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由我们自己来做。

因此,接口的一种常见用法是策略**设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性。

例如,类 Scanner 的构造器接受的是一个 Readable 接口,而 Readable 没有用作 Java 标准库中其他任何方法的参数——它是单独为 Scanner 创建的,因此 Scanner 没有将其参数限制为某个特定类。通过这种方式,Scanner 可以与更多的类型协作。如果你创建了一个新类并想让 Scanner 作用于它,就让它实现 Readable 接口,像这样:

import java.nio.*;
import java.util.*;

public class RandomStrings implements Readable {
    
    private static Random rand = new Random(47);
    private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
    private static final char[] VOWELS = "aeiou".toCharArray();
    private int count;

    public RandomStrings(int count) {
        this.count = count;
    }

    @Override
    public int read(CharBuffer cb) {
        if (count-- == 0) {
            return -1; 
        }
        cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]);
        for (int i = 0; i < 4; i++) {
            cb.append(VOWELS[rand.nextInt(VOWELS.length)]);
            cb.append(LOWERS[rand.nextInt(LOWERS.length)]);
        }
        cb.append(" ");
        return 10; 
    }

    public static void main(String[] args) {
        Scanner s = new Scanner(new RandomStrings(10));
        while (s.hasNext()) {
            System.out.println(s.next());
        }
    }
}

输出结果:

image-20200724140348108

Readable 接口只需要实现 read() 方法(注意 @Override 注解的突出方法)。在 read() 方法里,将输入内容添加到 CharBuffer 参数中,或在没有输入时返回 -1。

6. 接口嵌套

接口可以嵌套在类或其他接口中。可以这样写:

class A {
    interface B {
        void f1();
    }

    public class BImp implements B {
        @Override
        public void f1() {}
    }
    
    private interface C {
        void f2();
    }
}
    
interface D {
    interface E {
        void f3();
    }
}

在接口嵌套在类中时,接口也可以是 private 的,实现 private 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式。

接口 D 说明了接口之间也能嵌套。然而,接口中的元素必须是 public 的,所以嵌套在另一个接口中的接口自动就是 public 的,不能指明为 private

当实现某个接口时,并不需要实现嵌套在其内部的接口。同时,private 接口不能在定义它的类之外被实现。

参考资料:On Java 8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值