美文网首页
第二十条:接口优先于抽象类

第二十条:接口优先于抽象类

作者: Js_Gavin | 来源:发表于2021-02-07 15:39 被阅读0次

    Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从Java8为继承引入了缺省方法(default method),这两种机制都允许为某些实例方法提供实现。主要的区别在于,为了实现由抽象类定义的类型,类就必须成为抽象类的一个子类。因为Java只允许单继承,所以用抽象类作为类型定义受到了限制。任何定义了所有必要的方法遵守通用约定的类,都允许实现一个接口,无论这个类是处在类层次结构中的什么位置

    现有的类可以很容易被更新,以实现新的接口。如果这些方法尚不存在,你所需要做的就只是增加必要的方法,然后在类的声明中增加一个implements子语句。例如,当Comparable、Iterable和AutoCloseable接口被引入Java平台时。更新了许多现有的类,以实现这些接口。一般来说,无法更新现有的类来扩展新的抽象类。如果你希望两个类扩展同一个抽象类,就必须把抽象类放在类型层次的高处,这样它就成了这两个类的一个祖先。遗憾的是,这样做会间接的伤害到类层次,迫使这个公共祖先的所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适

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

    接口允许构造非层次结构的类型框架。类型层次对于组织某些事物是非常合适的,但是其他事物并不能被整齐的组织成一个严格的层次结构:例如,假设我们有一个接口代表一个singer(歌唱家),另一个接口代表一个songwriter(作曲家):

    public interface Singer { 
      AudioClip sing(Song s);
    }
    public interface Songwriter {
      Song compose(int chartPosition); 
    }
    

    在现实生活中,有些歌唱家本身也是作曲家。因为我们使用了接口而不是抽象类来定义这些类型,所以对于单个类而言,他同时实现Singer和Songwriter是完全允许的。实际上,我们可以定义第三个接口,它同时扩展Singer和Songwriter,并添加一些适合于这种组合的新发方法:

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

    也许并非总是需要这种灵活性,但是一旦这样做了,接口可就成了救世主。另外一种做法是编写一个臃肿的类层次,对于每一种要被支持的属性组合,都包含一个单独的类。如果在整个类型系统中有n个属性,那么就必须支持2n种可能的组合。这种现象被称为“组合爆炸”。类层次臃肿会导致类也会臃肿,这些类包含许多方法,并且这些方法只是在参数的类型上有所不同而已,因为类层次中没有任何类型体现了公共的行为特征。

    通过第18条介绍的包装类模式,接口使得安全的增强了类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加功能,再没有其他的选择了。这样得到的类与包装类相比,功能更差,也更加脆弱。

    当一个接口方法根据其他接口方法有了明显的实现时,可以考虑以缺省方法的形式为程序员提供实现协助。关于这种方法的范例,请参考第21条中的removeIf方法。如果提供了缺省方法,要确保利用Javadoc标签@implSpec建立文档(详见第19条)。

    通过缺省方法可以提供的实现协助是有限的。虽然许多接口都定义了Object方法的行为。如equals和hashCode,但是不允许给它们提供缺省方法。而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外,版本要求Java9或以上)。最后一点,无法给不受你控制的接口添加缺省方法

    但是,通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的发部分工作。这就是模板方法模式。

    按照惯例,骨架实现类被称之为AbstractInterface,这里的Interface是指所实现的接口名字。例如,Collections Framework为每个重要的集合接口都提供一个骨架实现,包括AbstractCollection、AbstractSet、AbstractLsit合AbstractMap。将它们称作SkeletalCollection、SkeletalSet、SkeletalList和SkeletalMap也是有道理的,但是现在Abstract的用法已经根深蒂固。如果设计得当,骨架实现(无论是单独一个抽象类,还是接口中唯一包含的缺省方法)可以使程序员非常容易地提供它们自己地接口实现。例如,下面是一个静态工厂方法,除了AbstractList之外,它还包含了一个完整地、功能全面地List实现:

    // Concrete implementation built atop skeletal implementation
    static List<Integer> intArrayAsList(int[] a) {
      Objects.requireNonNull(a);
    
      // The diamond operator is only legal here in Java 9 and later 
      // If you're using an earlier release, specify <Integer>
      return new AbstractList<Integer>() {
           @Override 
           public Integer get(int i) { 
              return a[i]; // Autoboxing (Item 6)
           }
           @Override 
           public Integer set(int i, Integer val) { 
              int oldVal = a[i];
              a[i] = val; // Auto-unboxing
              return oldVal; // Autoboxing
           }
           @Override 
           public int size() {
              return a.length; 
           }
      }; 
    }
    

    如果想知道一个List实现应该为你完成哪些工作,这个例子就充分演示了骨架实现的强大功能。顺便提一下,这个例子是个Adapter,它允许将int数组看作Integer实例的列表。由于在int值和Integer实例之间来回转换需要开销,它的性能不会很好。注意,这个实现采用了匿名类的形式(详见第24条)。

    骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。对于接口的大多数实现来讲,扩展骨架实现类是个很显然的选择,但并不是必须的。如果预置的类无法扩展骨架实现类,这个类始终能手工实现这个接口。同时,这个类本身仍然受益于接口中出现的任何缺省方法。此外,骨架实现仍然有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有实例上,这个内部私有类扩展了骨架实现类。这种方式被称作模拟多重继承,它与第18条中讨论过的包装类模式密切相关。这项技术具有多重继承的绝大多数优点。同时又避免了相应的缺陷。

    编写骨架实现类相对比较简单,只是过程有点乏味。首先,必须认真研究接口,并确定哪些方法是最基本的,其他的方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。接下来,在接口中为所有可以在基本方法之上直接实现的方法提供缺省方法,但是记住,不能为Object方法(如equals和hashCode)提供缺省方法。如果基本方法和缺省方法覆盖了接口,你的任务就完成了,不需要骨架实现类了。否则,就要编写一个类,声明实现接口,并实现所有剩下的接口方法。这个类中可以包含任何非公有的域,以及适合该任务的任何方法。

    以Map.Entry接口为例,举个简单的例子。明显的基本方法是getKey、getValue和(可选的)setValue。接口定义了equals和hashCode的行为,并且有一个明显的toString实现。由于不允许给Object方法提供缺省实现,因此所有实现都放在骨架实现类中:

    // Skeletal implementation class
    public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
      // Entries in a modifiable map must override this method 
      @Override
      public V setValue(V value) {
         throw new UnsupportedOperationException();
      }
      // Implements the general contract of Map.Entry.equals 
      @Override
      public boolean equals(Object o) {
         if (o == this) return true;
         if (!(o instanceof Map.Entry)) return false;
         Map.Entry<?,?> e = (Map.Entry) o;
         return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue());
      }
      // Implements the general contract of Map.Entry.hashCode 
      @Override
      public int hashCode() {
         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
      }
      @Override 
      public String toString() { 
         return getKey() + "=" + getValue();
      } 
    }
    

    注意,这个骨架实现不能在Map.Entry接口中实现,也不能作为子接口,因为不允许缺省方法覆盖Object方法,如equals、hashCode和toString。

    因为骨架实现类是为了继承的目的而设计的,所有应该遵从第19条中介绍的所有关于设计和文档的指导原则。为了简洁起见,上面例子中的文档注释部分被省略掉了,但是对于骨架实现类而言,好的文档绝对是非常必要的,无论它是否在接口或者单独的抽象类中包含了缺省方法。

    相关文章

      网友评论

          本文标题:第二十条:接口优先于抽象类

          本文链接:https://www.haomeiwen.com/subject/dmdztltx.html