接口为我们提供了一种将解口与实现分离的更加结构化的方法。抽象类则是普通的类和接口之间的一种中庸之道。尽管在构建具有某些未实现方法的类时,第一想法是创建接口,但是抽象类仍旧是用于此目的的一种重要而必须的工具。这两个重要的概念,看似相近却又大有不同,以下几个问题值得我们思考:

  • 接口与抽象类有什么相同之处?
  • 接口与抽象类又有什么区别?
  • 两者各自的优点?
  • 两者使用的情景如何?程序员又该如何选择其使用?

接下来以这篇博文来一探究竟。


一. 介绍

1. 抽象类

这里写图片描述

用一个例子来熟悉抽象类的概念,如上类图所示,有一个Instrument(乐器)抽象类,该类的对象没有意义,我们创建抽象类的希望通过这个通用接口操纵一系列类。从Instrument继承来的Wind(管乐器)类,若想创建该类的对象,就必须为基类中所有抽象方法提供方法定义,必须要实现play()adjust()方法,否则该继承类也是抽象类。

abstract class Instrument {
  private int i; // Storage allocated for each
  public abstract void play(Note n);
  public String what() { return "Instrument"; }
  public abstract void adjust();
}

class Wind extends Instrument {
  public void play(Note n) {
    print("Wind.play() " + n);
  }
  public String what() { return "Wind"; }
  public void adjust() {}
}
......

class Woodwind extends Wind {
  public void play(Note n) {
    print("Woodwind.play() " + n);
  }
  public String what() { return "Woodwind"; }
}   

......

public class AbstactTest {
  // Doesn't care about type, so new types
  // added to the system still work right:
  static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  static void tuneAll(Instrument[] e) {
    for(Instrument i : e)
      tune(i);
  } 
  public static void main(String[] args) {
    // Upcasting during addition to the array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
  }
}

代码实现如上,注意这里数组中创建的新对象采用向上转型,皆为Instrument抽象类,最后tuneAll()方法中调用传入对象的paly()方法,这里编译器会自动调用具体继承类的实现方法。

输出:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样使用它们。抽象类是很有用的重构工具,因为它们使得开发者可以很容易地将公共方法沿着继承层次结构向上移动。



2. 接口

interface关键字使抽象的概念更向前迈进一步。abstract关键字允许人们在类中创建一个或多个没有任何定义的方法 —— 提供了接口部分,但是无具体实现,这些实现是由此类的继承者创建的。而interface产生一个完全抽象的类,根本没有提供任何具体实现。

一个接口表示:所有实现了该特定接口的类看起来都像那样。因此,任何使用某特定接口的代码都知道可以调用该接口哪些方法,仅需要知道这些即可。所以,接口被用来建立类与类之间的协议。但是,interface不仅是一个极度抽象的类,它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继承的特性。

抽象类的例子转化为接口,类图如下:

这里写图片描述

需要注意的是,在接口中的每一个方法只是一个声明,这是编译器所允许在接口中唯一能够存在的事物。此外接口中的所有方法无需显示声明public,默认如此。注意接口中的任何域都自动是static 和 final的,早期接口成为一种便捷用来创建常量组的工具。实现代码如下:

interface Instrument {
  // Compile-time constant:
  int VALUE = 5; // static & final
  // Cannot have method definitions:
  void play(Note n); // Automatically public
  void adjust();
}

class Wind implements Instrument {
  public void play(Note n) {
    print(this + ".play() " + n);
  }
  public String toString() { return "Wind"; }
  public void adjust() { print(this + ".adjust()"); }
}

...

class Woodwind extends Wind {
  public String toString() { return "Woodwind"; }
}

...

public class InterfaceTest{
  // Doesn't care about type, so new types
  // added to the system still work right:
  static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  static void tuneAll(Instrument[] e) {
    for(Instrument i : e)
      tune(i);
  } 
  public static void main(String[] args) {
    // Upcasting during addition to the array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
  }
}

如上所示,显示结果同抽象类的实例相同。无论是向上转型称为Instrument 的普通类,还是称为Instrument 的抽象类,还是称为Instrument 的接口,它们的行为都是相同的。最后在tune()方法中注意,你无法辨别Instrument 是一个普通类、抽象类还是一个接口。



3. Java中的多重继承

接口不仅是一种更纯粹形式的抽象类,它的目标更高。接口无任何具体实现,即没有任何与接口相关的存储,因此接口可以多个组合,可以表示“一个x是一个a且是一个b以及一个c”。Java中,可以执行相同的行为,但是只有一个类具体实现:

这里写图片描述

可以继承任意多个接口,并可以转型为每个接口,因为每个接口都是一个独立类型,下面以一个实例来证明:

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。在Adventure测试类中,有四个方法把上述各种接口和具体类作为参数。而Hero对象可以被传递到任意一个方法,这意味着它依次被向上转型为每一个接口。

此例子展示的是使用接口的核心原因:为了能够向上转型为多个基类型(以及由此带来的灵活性)。使用接口的第二个原因与使用抽象基类相同:防止开发者创建该类的对象,并确保这仅是一个接口。

这就带来一个问题:该使用接口还是抽象类?如果创建不带任何方法定义成员变量的基类,应该选择接口。事实上,如果知道某事物应该成为一个基类,第一选择应该使它成为一个接口。(后续继续讨论)





二. 接口优于抽象类

接口和抽象类,都可以用来定义允许多个实现的类型。这两种机制之间最明显的区别在于:抽象类允许包含某些方法的实现,但是接口不允许。一个更重要的区别是,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。

1. 接口三大优势

(1)现有的类可以很容易被更新,以实现新的接口。

当现有的类需要更新时,只需要增加必要的方法,然后在类的声明中增加一个implement子句。例如:当Comparable接口被引用到Java平台时,会更新许多现有的类,以实现接口。一般来说,无法更新现有的类来扩展新的抽象类。如果你希望两个类扩展同一个抽象类,就必须要把抽象类放到类型层次的高处,以便这两个类的祖先成为它的子类。可是,这样做会间接影响到类层次,迫使这个公共祖先的所有后代都扩展这个新的抽象类,无视它对这些后代类适不适合。

(2)接口是定义mixin(混合类型)的理想选择。

不严格地讲,mixin是指这样的类型:类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供类某些可供选择的行为。例如:Comparable是一个mixin接口,它允许类表明它的实例可以与其他的可比较对象进行排序。这样的接口之所以被称为mixin,是因为它允许任选的功能可被混合到类型的主要功能中。抽象类不能被用于定义mixin,同样也是因为它们不能被更新到现有的类中:类不可能有一个以上的父类,类层次结构中也没有合适的地方来插入mixin。

(3)接口允许我们构造非层次结构的类型框架。

类型层次对于组织某些事物是非常合适的,但是其他有些事物并不能被整齐地组织成一个严格的层次结构。例如:有一个接口代表一个Singer(歌唱家),一个接口代表一个Songwriter(作曲家):

public interface Singer{
    AudioClip sing(Song s);
}

public interface Songwriter{
    AudioClip compose(boolean hit);
}

有一种情况,有些歌唱家也是作曲家,因为我们使用的是接口而不是抽象类来定义这些类型,所以对于单个类来说,它同时实现Singer、Songwriter是合理的。不仅如此,还可以定义第三个接口,它同时扩展Singer、Songwriter,并添加类一些适合于这种组合的新方法:

public interface SingerSongwriter extends Singer, Songwriter{
    AudioClip strum();
    void actSensitive();
}

也许你并不总是需要这种灵活性,但是一旦你这样做了,可以很好的扩展。另一种做法是编写一个臃肿的类层次,对于每一种要被支持的属性组合,都包含一个到哪都的类,如果整个类型系统中有n个属性,就需要支持2^n种可能的组合,这种现象被称为“组合爆炸”。类层次臃肿会导致类也臃肿,这些类包含许多方法,并且这些方法只是在参数类型上有所不同而已,因为类层次中没有任何类型体现了公共的行为特征。



2.骨架实现(skeletal implementation)类

虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍腻味程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现接管类所有与接口实现相关的工作。

骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。编写骨架实现类相对比较简单,首先必须认真研究接口,并确定那些方法是最为基本的,其他方法则可以根据此来实现。这些基本方法将成为骨架实现类中的抽象方法。然后必须为借口中的所有其他方法提供具体的实现。例如:下面是Map.Entry接口的骨架实现类:

public abstact class AbstactMapEntry<K, V> implements map.Entry<K, V>{
    //最基本的方法
    public abstact K getKey();
    public abstact V getValue();

    //Entries in modifiable maps must override this method
    public V setValue(V value){
        throw new UnsupportedOperationException();
    }

    //实现接口
    @Override
    public boolean equals(Object o){
        if(o == this)
            return true;
        if(!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> arg = (Map.Entry) o;
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
    }

    public static boolean equals(Object o1, Object o2){
        return o1 == null ? o2 == null : o1.equals(o2);
    }

    //实现接口
    @Override
    public int hashCode(){
            return hashCode(getKey()) ^ hashCode(getValue());
    }

    public static int hashCode(Object obj){
        return obj == null ? 0 : obj.hashCode();
    }

}


3.抽象类与接口总结

看到这里,你可能疑惑以为接口一定是优于抽象类的?

其实不能以这么绝对的思想去评判两者,我们只能从两者各自的优势去入手,在某些方面,接口确是优于抽象类的,但这也不代表抽象类一无是处,只是两者运用的时机、区域、情况不同,希望读者带有这样的思想继续思考。

(1)抽象类的优势:

使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势: 抽象类的演变比接口要容易得多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有现有的实现都将提供这个新的方法。而对于接口,这样是不可以的。

(2)接口的注意点:

一般来说,要想在公有接口中增加方法,而不破坏这个接口的所有实现类,这是不可能的。之前实现该接口的类将会漏掉新增加的方法,并且无法再通过编译。因此设计公有的接口要非常谨慎!接口一旦被公开发行,并且一杯广泛实现。再想改这个接口几乎是不可能的。你必须在初次设计的时候就保证接口是正确的,如果接口包含微小的瑕疵,它将会一直影响接口实现类。最好的做法是,在接口被”冻结“之前,尽可能让更多的
程序员用尽可能多的方式来实现这个新接口,这样有助于及时改正缺陷。

(3)接口和抽象类之间的选择

简而言之,接口通常是定义允许多个实现的类型的最佳途径。这个规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。

上面的知识点了解完之后,相信你不会再因为看到这个标题就觉得“接口一定是优于抽象类”了。




三. 接口只用于定义类型

当类实现接口时,接口就充当这个类的实例类型。因此,类实现类接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。

当类实现接口时,接口就充当这个类的实例类型。因此,类实现类接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。

1.常量接口

之前介绍有关接口的知识点中提到过,接口中的域都是静态static且final的,因此有一种借口被称为常量接口,它不满足上述的条件。这种接口中没有包含任何方法,只包含静态的final域,每个域都到处一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。例子如下:

public interface PhysicalConstants{
    //(1/mol)
    static final double AVOGADROS_NUMBER = 6.02214199e23;
    //(J/K)
    static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    //(kg)
    static final double ELECTRON_MASS = 9.10938188e-31;
}

常量接口模式是对接口的不良使用。类在内部实现某些常量,这存粹是实现细节。实现常量接口,会导致把这样的细节泄露给该类导出API中。类实现常量接口,对这个类的用户来说无价值,反而代表类一种承诺:如果将来发行版本中,此类被修改,就不再需要这些常量了,它必须实现这个接口,以确保二进制兼容性。如果非final类实现了常量接口,它的所有类的命名空间也会被接口中的常量“污染”。

2.常量接口替代方法

如果要导出常量,有几种合理的选择方案。如果遮羞常量与某个现有的类或者接口紧密相关,应该把这些常量添加到这个类或者接口中。例如:在Java平台类库中所有的数值包装类,如Interger,都导出了MIN_VALUE 和MAX_VALUE常量。

如果这些常量被看作枚举类型的成员,就应该用枚举类型来导出,否则用不可实例化的工具类来导出,下面是一个工具类的例子:

public class PhysicalConstants{
    private PhysicalConstants(){}

    public static final double AVOGADROS_NUMBER = 6.02214199e23;
    public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    public static final double ELECTRON_MASS = 9.10938188e-31

}

工具类通常要求客户端用类名来修饰这些常量名,例如PhysicalConstants.AVOGADROS_NUMBER。如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名,例子如下:

public class Test{
    double atoms(double mols){
        return AVOGADROS_NUMBER * mols;
    }
    ......

}

3.总结

简而言之,接口应该只被用来定义类型!而不是被用来导出常量。




在此声明,以上部分内容摘于《Java编程思想》、《Effective Java》,最后融于自己的理解和总结,如有错误,还请指正。

以上是关于接口和抽象类之间的探讨,其实关于这个探讨很受重视,特别是在面试中,但是我看过那种面试大全里面关于这个问题只是几句话概括,固然是精华但是知识面掌握的不全,所以此篇博客是从一些基础概念到进阶探讨,内容稍微丰富一些。

希望对你们有帮助 :)


本文转载:CSDN博客