为什么提倡面向接口编程

为什么先行者们要提倡面向接口编程?要回答这个问题,我们先以一个反问作为开场:

 

1. 如果不是接口,那是谁?
在百度搜了一堆“为什么要面向接口编程”,最有道理的答案似乎是“方便维护和扩展”,因为“规范和实现分离”,比如JDK只定义了数据库的接口,各大厂商自己提供具体实现。

但实体类就不能做到这一点吗?比如顶级实体类提供空方法,具体实现类提供实现细节。

当然,这样做对子类的控制力会减弱,那用抽象类不可以吗?一个只定义了抽象方法的抽象类在本质上和接口是没有什么差别的。

肯定会有道友说,一个类可以实现多个接口,但只能继承一个抽象类。没错,但这只不过是JAVA语言的一个特性而已,JAVA完全可以允许多继承,这样是不是接口就没有存在的意义了?换句话说,JAVA为什么要这样做?这样做的背后是不是因为遇到了什么实际的问题?

要回答这个问题,有必要引出下一个问题:

 

2. 接口和抽象类有什么区别?
对于这个问题,相信绝大多数JAVA开发人员都可以回答出很多点,但我这里只说一点,相信这一点以前从来没有人说过:接口是不会被实例化的。

肯定有的道友会说,抽象类也不能被实例化呀?没错,但这是有前提的:抽象类本身并不能被使用者直接实例化,但被具体类继承后,就可以在具体类实例化的过程中被实例化了。更直观的来说就是,子类可以使用super.xxx()调用抽象父类中的具体方法,但你无法用这种方式调用接口中的方法,即使是JAVA8之后接口中的default方法。

啰里啰唆说了这么多,你到底想说什么?

 

3. 类继承所面临的问题
终于要到正题了。

在先行者们的实践中发现,类继承有着一些无法避免的缺陷:

3.1 父类的实现细节会影响子类的行为
这看起来是一句废话,但如果父类具体实现细节的变化导致子类出现BUG了呢?

要知道,封装可是面向对象最引以为傲的卖点之一,其一大目标就是:你不用管我的具体实现逻辑,只需要知道入参和返回值就行了。但这个目标却被面向对象的另一引以为傲的卖点 -> 继承打破了。

下面是一个很经典的例子(摘自《Effective Java, 3rd Edition》Item 18: Favor composition over inheritance):

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;

public InstrumentedHashSet() {}

public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}

@Override public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}
这个例子的目的是统计历史上本集合一共放入过多少元素。实现中规中矩,看起来没有什么问题,但实际使用时会发现统计总是会翻倍,这是因为其父类的addAll方法在实现时调用了自己的add方法。其父类这样实现对不对?从理论上来讲当然是对的。那子类这样实现对不对?从理论上来讲当然也是没问题的。但两个在理论上没有问题的实现在实际应用中却产生了问题。

可能会有道友说,改一改子类的实现方式不就行了嘛。但问题是:你知道父类的拥有者什么时候会改变自己的实现细节?要知道,在面向对象理论中,实现细节是允许被随意改变的。

 

3.2 子类的实现可能会影响父类
如果说子类被父类影响还算情有可原的话,那么父类被子类影响实在是有点说不过去了。

下面的例子中,父类在构造器中调用了自己的一个方法,而这个方法被子类覆盖了,同时,子类在该方法中用到了本该在子类构造器中初始化的对象,最终导致该子类在实例化过程中报错。

public class Father {
public Father() {
start();
}
protected void start() {}
}

public class Child extends Father {
private Integer i;

public Child() {
i = 1;
start();
}

@Override public void start() {
System.out.println(i.byteValue());
}
}
new Child()后报以下错误:
Exception in thread "main" java.lang.NullPointerException
    at Child.start(Child.java:16)
    at Father.<init>(Father.java:8)
    at Child.<init>(Child.java:9)
    at Test.main(Test.java:10)
这里仅举这两个例子,事实上还有很多其他因继承而可能引起的问题,比如如死锁,这里就不再赘述。

 

不知道道友们此时心中是否已经有答案了,如果还没有,那么就由在下来捅破这最后一层窗户纸:

 

4. 为什么要有接口?
因为接口可以避免类继承的所有问题。

再说的严谨一点:JAVA语言中对接口的限制可以避免因类继承而引起的所有问题。

纵观类继承所引起的问题,都是由于其可被实例化造成的,而接口是不可被实例化的,所以其可以避免所有这些问题。由于其不能被实例化,所以不需要在其内部定义非static或非public的属性,进而导致定义非final的属性也是不恰当的(因为一个随时可被任何人随意修改的属性不符合面向对象的价值观);由于其不能被实例化,所以也不需要定义方法的实现,进而导致类可以实现多个接口而不至于担心不同接口出现相同方法签名却有不同实现的冲突(1.8之前)。

所以先行者们给出的建议是:如果确实是is a的关系,才使用继承,否则建议使用组合。他们还给这种方式起了个名字:装饰器模式。这个模式在JDK的IO包中体现的淋漓尽致。

 

5. 剧终?
所以,可以happy end了吗?No!只有死掉的人才能被封神,以此类推,只有不再被人使用的语言才能说完美。

1.8中interface对default方法的支持,部分打破了接口一直以来的端庄形象,为面向接口编程埋下了一个坑,或许若干年以后,江湖中盛传的就不再是面向接口编程,而是面向XX编程了。
————————————————
版权声明:本文为CSDN博主「晓岚45」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jifengnan/article/details/85411115

posted @ 2020-05-05 20:40  灰太狼&红太狼  阅读(372)  评论(0编辑  收藏  举报