飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

为什么JAVA一般继承有缺陷以及如何最终修复它

时间:2021-12-10  作者:匿名  

通用继承,即公共类扩展到包边界,提供了许多挑战和缺点,几乎在所有情况下都应该避免。可以创建类和方法,final这意味着不允许子类化,这有效地防止了继承。虽然在 Java 这样的面向对象语言中这样做听起来很奇怪,但它确实为大量的类类型带来了显着的好处。

但是,类或方法什么时候应该是final,为什么一般继承有问题?

不可变类

不可变类是无法从外部世界观察到其状态发生变化的类。这为不可变对象提供了固有线程安全的优势,并且它们可以无限期地重用。

Java 的内置String类是不可变类的一个例子。它确实有一个很可能在第一次hashCode()被调用时改变的内部状态,但是这个内部状态不能被外部调用者观察到(除非诉诸反射)。应始终声明不可变类,final否则子类可能会破坏不可变契约,只需添加和公开可变状态即可。 

为了完整起见,值得一提的是,不可变类应将其所有字段声明为private,final并确保对任何可变子组件(例如数组)的独占访问,例如使用防御性复制。

不可实例化的类(AKA Utility Classes)

不可实例化的类通常被非正式地称为“实用类”,并且仅包含静态方法(可能还有静态字段)。静态方法不是类方法,而是附加到“载体类”的全局函数。理想情况下,不可实例化的类在它们的(静态)状态(如果有)方面应该是不可变的。

应该使用它们的运营商类名称和方法名称(例如域名yList())来调用这些方法。对不可实例化的实用程序进行子类化可能会导致不直观的行为,并且很可能会导致混淆,因为无论如何都不能覆盖这些方法,只能按如下所示进行替换:

public class FooUtil {
    static void print() {
        lower();
    }
    static void lower() {
        域名tln("lower foo");
    }
}
public class BarUtil extends FooUtil {
    static void lower() {
        域名tln("lower bar");
    }
}

调用BarUtil::print将产生“lower foo”而不是“lower bar”,这意味着BarUtil::lower没有覆盖FooUtil::lower. 但是,如果BarUtil::lower直接调用,它会打印“lower bar”。

因此,通常应声明不可实例化的类final。附带说明一下,不可实例化的类应该声明一个默认构造函数,private以防止实例化不可实例化的类(顾名思义)。

构造函数调用的方法

类的构造函数调用的方法应该始终是final,要么声明整个类,要么final声明这些方法为final。不这样做可能会导致对象(例如“ this”)的泄漏,该对象仅部分初始化并因此可能处于非法状态。例如,这种泄漏可能由尚未初始化的实例向侦听器注册自身而发生。如果这些错误是公开的,则可能很难识别。

一般继承

使用/不使用通用继承在相当长的一段时间内引发了自以为是的讨论。在早期,继承通常被认为是代码重用的通用方式。后来证明,包外的继承可能会导致无法满足和错误的行为,除非特别注意提供适合跨包边界扩展的类 [Bloch18, Item18]。 

此外,通用继承破坏了封装 [Snyder80],因为超类实现可能会随着时间的推移而改变,这可能导致子类失败,即使没有进行任何更改。如果承诺永不改变超类,有效地使超类成为永恒时代的大型整体化石 API 承诺,则可以避免这个问题。平心而论,即使问题泄漏到代码中的方式较少,也可以针对使用组合的类提出这个论点。因此,这不是最终确定的论据,而是代码重用的更基本问题。

由于self-use,继承可能会产生意想不到的效果,其中一个可覆盖的方法调用基类中的另一个可覆盖的方法:想象一个扩展的类,ArrayList它应该跟踪添加到类中的元素数量。如果我们覆盖将计数器增加 1 并覆盖添加到计数器,然后调用相应的超级方法,那么我们会感到惊讶:  add() addAll(Collection)域名()

因为ArrayList::addAll碰巧自己使用ArrayList::add来单独添加元素,所以添加通过addAll()会计算两次。此外,除非记录在案,否则无法保证此行为会随着时间的推移保持不变。也许将来会有一种更高效的批量添加元素的方式,即元素直接插入后备数组而不调用add()?

自用的另一个常见问题是当子类覆盖了一个应该调用一个或几个其他方法的方法,但程序员忘记调用超级方法时。一个相关的问题是决定重写方法是否应该在被重写方法的开头或结尾(或实际上介于两者之间)调用超级方法的问题。其中一些问题的解决方案可能是在基类中声明顶级方法final并提供可覆盖的受保护“挂钩方法”,可以以更可控的方式覆盖这些方法。 

通用继承也带来了潜在的安全漏洞:假设 anArrayList被扩展以确保只能添加​​满足特定谓词的对象(例如,它们必须处于有效状态)。然后,在以后的版本中,通过基类引入了一种添加元素的新方法AbstractList。这种新方式现在将在所谓的受保护类中可见,有效地为将非法对象添加到列表中提供了一个后门。 

另一个问题是“传播曝光”,例如域名st(“a”, “b”)它返回一个“固定大小的列表”(但应该返回一个不可修改的List,这里是一个不可变的,List因为元素本身都是不可变的)。事实证明,返回的元素List现在不仅可以通过 an 替换,Iterator还可以通过替换List::replaceAll,这是在 JDK 8 中添加的一个方法Arrays::asList。

如果子类向基类的方法添加新方法,则可能会出现另一类问题。如果在稍后阶段,将具有相同签名的方法添加到基类,则该方法将被子类巧合地覆盖。这可能根本不是预期的行为。如果添加了具有相同名称和参数但具有不同返回类型的方法,则代码可能无法编译。因此,在一般情况下,不可能在非最终公共类中添加方法,因为无法控制类的子类化方式。

另一个问题可能是偶然的继承。JDK 本身有几个有问题的继承,其中类是偶然继承的,因为它显然“方便”而不是因为B类确实是A类。例如,没有充分的主要原因Stack扩展旧的Vector类。这会阻止Stack演变为更高效和高性能的实现。

总而言之,一个应该被普遍继承的类很难改变并且必须[Bloch18, Item19]:

  • 记录它对可覆盖方法的自我使用

  • 可能以明智选择的保护方法的形式提供挂钩

  • 伴随着使用子类的测试

  • 不提供调用可覆盖方法的构造函数

  • 不允许序列化调用可覆盖的方法

如果hashCode()/equals()被覆盖,继承也会产生约束和问题。如果我们有一个名为 的基类Fruit,那么Apple颜色是否与Pear等号相同?的实例SevilleOrange可以等于一个BergamontOrange实例吗?一般来说,决定这些类型的问题并不容易。重要的是要记住,任何子类都不应该覆盖这些方法中的任何一个,或者应该同时覆盖它们。

应该注意的是,根据定义在公共 API 中公开公共非最终类意味着它为跨包边界的继承打开了大门,因为用户代码可以将扩展类放在任何包中。由于根据 JPMS 的使用,强烈不鼓励甚至可能完全禁止拆分包,因此对此类类进行子类化意味着在包边界上进行子类化。

避免所有这些事情的一种方法是声明类final并使用组合而不是继承,有效地放弃跨包的继承。这通常提供了一个更清晰的 API,由此只能公开接口并且具体的类不会在 API 中泄漏。这样,任何使用的超类都只是包私有的,并且按照约定或定义,永远不会在外部使用。

具有委托的组合可以防止上述大多数问题,包括意外自用、基类中额外方法的安全漏洞、签名冲突、偶然继承、子类测试的需要、“ this”的意外泄漏和许多其他问题。过去,人们担心这会导致性能下降,但事实并非如此。

出于充分的理由,Java 中的继承仅限于一个超类,这自然限制了该概念的可扩展性。另一方面,组合允许使用任意数量的委托。

组合的一个小缺点可能与某些回调的使用相结合。但是,如果提供适当的规定,这个问题就可以避免。换句话说,如果一个组件(在组合中使用)将自己注册到一个监听器,那么监听器将调用组件本身而不是组合类。

密封类

在最近的 Java 版本中,引入了密封类 ( JEP 409 )的概念。在此之前,final关键字是一个布尔属性:一个类要么是可扩展的(在其声明的访问类型内),要么不是。密封类引入了一种更细粒度的机制,可以说 aFruit可以是 an Apple,Pear,或者Orange仅此而已。这基本上是 的一种更一般化的形式final。为具有此类特性的 Java 语言付出的努力表明类可扩展性是一个重要的特性。有趣的是,在一个密封的界面允许的类必须指定它是final,non-final,或permits随后的子类。 

继承强加的 API 承诺

在本文中,该类Stack被称为失败的继承实现。它主要介绍的方法push(),pop(),peek(),empty()和search()。但是,当它从继承Vector,我们也得到所有的方法/来自班List,AbstractList,RandomAccess,Cloneable,和Serializable。  AbstractList,而后者又继承自AbstractCollectionwhich implements Collection。

这将 API 的权重增加了几个数量级,我完全确定 Java 设计人员正在后悔他们 25 年的偶然继承。如果Stack它只是一个接口,并且有一个静态方法可以提供一个新的 empty Stack,事情看起来会好得多。属于Serializable或受制于其他序列化机制的类通常特别成问题,因为二进制(或其他)格式通常会限制实现随时间演变的方式。

正如上面和前面的条款所见,公共非最终类在许多情况下永远不会改变。

是否应该使用跨包边界的继承?

这是一个见仁见智的问题。 

很多时候,最好使用组合。在更简单的情况下,将函数传递给提供定制功能的具体类的构造函数比允许子类化和覆盖方法更可取。举个例子,不是覆盖处理程序方法,而是可以通过构造函数向不可扩展的类提供方法处理程序。

如果经过非常仔细的考虑,得出的结论是应该提供一个可扩展的类(跨包),那么必须仔细考虑上述所有约束。只允许默认子类化是一个正确的错误,特别是对于库和 API 设计者。相反,类应该final默认标记,只有经过仔细审查和测试,才能考虑开放子类。

最后一点

当我不再使用跨包的继承并转而只公开接口时,许多其他优势变得显而易见。保持内部考虑变得更加容易......很好的内部。

在单个类中可能使用多个组件的组合提供了比继承更多的代码重用能力,尽管在使用类中需要更多的代码仪式。它还可以简化代码的测试,并通过越来越少的脆弱测试提供更好的测试覆盖率。它也非常适合模块系统(JPMS)。将组件作为纯服务提供,例如,使用 Java 的ServiceLoader,增加了灵活性,同时最大限度地减少了 API 占用空间。这使得学习和使用 API 变得更容易,并提供更大的灵活性来随着时间的推移发展库。 

终于,这一切都说得通了!

湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。