十、接口(2)

本章概要

  • 抽象类和接口
  • 完全解耦
  • 多接口结合
  • 使用继承扩展接口
    • 结合接口时的命名冲突

抽象类和接口

尤其是在 Java 8 引入 default 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分:

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

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

有一条实际经验:在合理的范围内尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。

完全解耦

每当一个方法与一个类而不是接口一起工作时(当方法的参数是类而不是接口),你只能应用那个类或它的子类。如果你想把这方法应用到一个继承层次之外的类,是做不到的。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。

例如有一个类 Processor 有两个方法 name()process()process() 方法接受输入,修改并输出。把这个类作为基类用来创建各种不同类型的 Processor。下例中,Processor 的各个子类修改 String 对象(注意,返回类型可能是协变类型而非参数类型):

// interfaces/Applicator.java
import java.util.*;

class Processor {
    
    
    public String name() {
    
    
        return getClass().getSimpleName();
    }
    
    public Object process(Object input) {
    
    
        return input;
    }
}

class Upcase extends Processor {
    
    
    // 返回协变类型
    @Override 
    public String process(Object input) {
    
    
        return ((String) input).toUpperCase();
    }
}

class Downcase extends Processor {
    
    
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toLowerCase();
    }
}

class Splitter extends Processor {
    
    
    @Override
    public String process(Object input) {
    
    
        // split() divides a String into pieces:
        return Arrays.toString(((String) input).split(" "));
    }
}

public class Applicator {
    
    
    public static void apply(Processor p, Object s) {
    
    
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }
    
    public static void main(String[] args) {
    
    
        String s = "We are such stuff as dreams are made on";
        apply(new Upcase(), s);
        apply(new Downcase(), s);
        apply(new Splitter(), s);
    }
}

输出:

Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on
Using Processor Splitter
[We, are, such, stuff, as, dreams, are, made, on]

Applicatorapply() 方法可以接受任何类型的 Processor,并将其应用到一个 Object 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为_策略_设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。在这里,Processor 对象是策略,main() 方法展示了三种不同的应用于 String s 上的策略。

split()String 类中的方法,它接受 String 类型的对象并以传入的参数作为分割界限,返回一个数组 String[]。在这里用它是为了更快地创建 String 数组。

假设现在发现了一组电子滤波器,它们看起来好像能使用 Applicatorapply() 方法:

public class Waveform {
    
    
    private static long counter;
    private final long id = counter++;
    
    @Override
    public String toString() {
    
    
        return "Waveform " + id;
    }
}
public class Filter {
    
    
    public String name() {
    
    
        return getClass().getSimpleName();
    }
    
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class LowPass extends Filter {
    
    
    double cutoff;
    
    public LowPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input; // Dummy processing 哑处理
    }
}
public class HighPass extends Filter {
    
    
    double cutoff;
    
    public HighPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class HighPass extends Filter {
    
    
    double cutoff;
    
    public HighPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class BandPass extends Filter {
    
    
    double lowCutoff, highCutoff;
    
    public BandPass(double lowCut, double highCut) {
    
    
        lowCutoff = lowCut;
        highCutoff = highCut;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}

Filter 类与 Processor 类具有相同的接口元素,但是因为它不是继承自 Processor —— 因为 Filter 类的创建者根本不知道你想将它当作 Processor 使用 —— 因此你不能将 Applicatorapply() 方法应用在 Filter 类上,即使这样做也能正常运行。主要是因为 Applicatorapply() 方法和 Processor 过于耦合,这阻止了 Applicatorapply() 方法被复用。另外要注意的一点是 Filter 类中 process() 方法的输入输出都是 Waveform

但如果 Processor 是一个接口,那么限制就会变得松动到足以复用 Applicatorapply() 方法,用来接受那个接口参数。下面是修改后的 ProcessorApplicator 版本:

public interface Processor {
    
    
    default String name() {
    
    
        return getClass().getSimpleName();
    }
    
    Object process(Object input);
}
public class Applicator {
    
    
    public static void apply(Processor p, Object s) {
    
    
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }
}

复用代码的第一种方式是客户端程序员遵循接口编写类,像这样:

// interfaces/interfaceprocessor/StringProcessor.java
// {java interfaces.interfaceprocessor.StringProcessor}
package interfaces.interfaceprocessor;
import java.util.*;

interface StringProcessor extends Processor {
    
    
    @Override
    String process(Object input); // [1]
    String S = "If she weighs the same as a duck, she's made of wood"; // [2]
    
    static void main(String[] args) {
    
     // [3]
        Applicator.apply(new Upcase(), S);
        Applicator.apply(new Downcase(), S);
        Applicator.apply(new Splitter(), S);
    }
}

class Upcase implements StringProcessor {
    
    
    // 返回协变类型
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toUpperCase();
    }
}

class Downcase implements StringProcessor {
    
    
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toLowerCase();
    }
}

class Splitter implements StringProcessor {
    
    
    @Override
    public String process(Object input) {
    
    
        return Arrays.toString(((String) input).split(" "));
    }
}

输出:

Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]

[1] 该声明不是必要的,即使移除它,编译器也不会报错。但是注意这里的协变返回类型从 Object 变成了 String。

[2] S 自动就是 final 和 static 的,因为它是在接口中定义的。

[3] 可以在接口中定义 main() 方法。

这种方式运作得很好,然而你经常遇到的情况是无法修改类。例如在电子滤波器的例子中,类库是被发现而不是创建的。在这些情况下,可以使用_适配器_设计模式。适配器允许代码接受已有的接口产生需要的接口,如下:

// interfaces/interfaceprocessor/FilterProcessor.java
// {java interfaces.interfaceprocessor.FilterProcessor}
package interfaces.interfaceprocessor;
import interfaces.filters.*;

class FilterAdapter implements Processor {
    
    
    Filter filter;
    
    FilterAdapter(Filter filter) {
    
    
        this.filter = filter;
    }
    
    @Override
    public String name() {
    
    
        return filter.name();
    }
    
    @Override
    public Waveform process(Object input) {
    
    
        return filter.process((Waveform) input);
    }
}

public class FilterProcessor {
    
    
    public static void main(String[] args) {
    
    
        Waveform w = new Waveform();
        Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
        Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
        Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w);
    }
}

输出:

Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0

在这种使用适配器的方式中,FilterAdapter 的构造器接受已有的接口 Filter,继而产生需要的 Processor 接口的对象。你可能还注意到 FilterAdapter 中使用了委托。

协变允许我们从 process() 方法中产生一个 Waveform 而非 Object 对象。

将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。

多接口结合

接口没有任何实现——也就是说,没有任何与接口相关的存储——因此无法阻止结合的多接口。这是有价值的,因为你有时需要表示“一个 x 是一个 a 和一个 b 以及一个 c”。

在这里插入图片描述

派生类并不要求必须继承自抽象的或“具体的”(没有任何抽象方法)的基类。如果继承一个非接口的类,那么只能继承一个类,其余的基元素必须都是接口。需要将所有的接口名称置于 implements 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个由多个接口组合而成的具体类产生的新类:

// interfaces/Adventure.java
// Multiple interfaces
interface CanFight {
    
    
    void fight();
}

interface CanSwim {
    
    
    void swim();
}

interface CanFly {
    
    
    void fly();
}

class ActionCharacter {
    
    
    public void fight(){
    
    }
}

class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
    
    
    public void swim() {
    
    }
    
    public void fly() {
    
    }
}

public class Adventure {
    
    
    public static void t(CanFight x) {
    
    
        x.fight();
    }
    
    public static void u(CanSwim x) {
    
    
        x.swim();
    }
    
    public static void v(CanFly x) {
    
    
        x.fly();
    }
    
    public static void w(ActionCharacter x) {
    
    
        x.fight();
    }
    
    public static void main(String[] args) {
    
    
        Hero h = new Hero();
        t(h); // Treat it as a CanFight
        u(h); // Treat it as a CanSwim
        v(h); // Treat it as a CanFly
        w(h); // Treat it as an ActionCharacter
    }
}

Hero 结合了具体类 ActionCharacter 和接口 CanFightCanSwimCanFly。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。

接口 CanFight 和类 ActionCharacter 中的 fight() 方法签名相同,而在类 Hero 中也没有提供 fight() 的定义。可以扩展一个接口,但是得到的是另一个接口。当想创建一个对象时,所有的定义必须首先都存在。类 Hero 中没有显式地提供 fight() 的定义,是由于该方法在类 ActionCharacter 中已经定义过,这样才使得创建 Hero 对象成为可能。

在类 Adventure 中可以看到四个方法,它们把不同的接口和具体类作为参数。当创建一个 Hero 对象时,它可以被传入这些方法中的任意一个,意味着它可以依次向上转型为每个接口。Java 中这种接口的设计方式,使得程序员不需要付出特别的努力。

记住,前面例子展示了使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。

使用继承扩展接口

通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。这两种情况都可以得到新接口,如下例所示:

// interfaces/HorrorShow.java
// Extending an interface with inheritance
interface Monster {
    
    
    void menace();
}

interface DangerousMonster extends Monster {
    
    
    void destroy();
}

interface Lethal {
    
    
    void kill();
}

class DragonZilla implements DangerousMonster {
    
    
    @Override
    public void menace() {
    
    }
    
    @Override
    public void destroy() {
    
    }
}

interface Vampire extends DangerousMonster, Lethal {
    
    
    void drinkBlood();
}

class VeryBadVampire implements Vampire {
    
    
    @Override
    public void menace() {
    
    }
    
    @Override
    public void destroy() {
    
    }
    
    @Override
    public void kill() {
    
    }
    
    @Override
    public void drinkBlood() {
    
    }
}

public class HorrorShow {
    
    
    static void u(Monster b) {
    
    
        b.menace();
    }
    
    static void v(DangerousMonster d) {
    
    
        d.menace();
        d.destroy();
    }
    
    static void w(Lethal l) {
    
    
        l.kill();
    }
    
    public static void main(String[] args) {
    
    
        DangerousMonster barney = new DragonZilla();
        u(barney);
        v(barney);
        Vampire vlad = new VeryBadVampire();
        u(vlad);
        v(vlad);
        w(vlad);
    }
}

接口 DangerousMonsterMonster 简单扩展的一个新接口,类 DragonZilla 实现了这个接口。

Vampire 中使用的语法仅适用于接口继承。通常来说,extends 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。

结合接口时的命名冲突

当实现多个接口时可能会存在一个小陷阱。在前面的例子中,CanFightActionCharacter 具有完全相同的 fight() 方法。完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?这里有一个例子:

// interfaces/InterfaceCollision.java
interface I1 {
    
    
    void f();
}

interface I2 {
    
    
    int f(int i);
}

interface I3 {
    
    
    int f();
}

class C {
    
    
    public int f() {
    
    
        return 1;
    }
}

class C2 implements I1, I2 {
    
    
    @Override
    public void f() {
    
    }
    
    @Override
    public int f(int i) {
    
    
        return 1;  // 重载
    }
}

class C3 extends C implements I2 {
    
    
    @Override
    public int f(int i) {
    
    
        return 1; // 重载
    }
}

class C4 extends C implements I3 {
    
    
    // 完全相同,没问题
    @Override
    public int f() {
    
    
        return 1;
    }
}

// 方法的返回类型不同
//- class C5 extends C implements I1 {}
//- interface I4 extends I1, I3 {}

覆写、实现和重载令人不快地搅和在一起带来了困难。同时,重载方法仅根据返回类型是区分不了的。当不注释最后两行时,报错信息如下:

error: C5 is not abstract and does not override abstract
method f() in I1
class C5 extends C implements I1 {}
error: types I3 and I1 are incompatible; both define f(),
but with unrelated return types
interfacce I4 extends I1, I3 {}

当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

猜你喜欢

转载自blog.csdn.net/GXL_1012/article/details/132325894