禁止Java中的继承有哪些好的理由?

205

有哪些好的理由可以禁用Java中的继承,例如使用final类或使用单个私有无参构造函数的类?使用final方法的好理由是什么?


1
小题大做:如果您在代码中没有明确编写构造函数,则默认构造函数是Java编译器为您生成的构造函数。您指的是无参数构造函数。 - John Topley
那不就是“隐式默认构造函数”吗? - cretzel
2
你不必为你的类提供任何构造函数,但在这样做时必须小心。编译器会自动为任何没有构造函数的类提供一个无参默认构造函数。- John Topley - John Topley
11个回答

202
你最好的参考资料是Joshua Bloch的优秀书籍“Effective Java”中的第19项,名为“为继承而设计和文档化,否则就禁止它”。(它在第二版中是第17项,在第一版中是第15项。)如果你真的想了解更多,请务必阅读这一部分。
如果祖先没有被设计成可继承的,那么继承类与其父类的交互可能会令人惊讶和不可预测。
因此,类应该分为两种:
1.**设计成可扩展的类**,并且有足够的文档描述如何进行扩展。 2.**标记为final的类**
如果你只是编写纯内部代码,这可能有点过度了。然而,在类文件中添加五个字符所需的额外工作非常小。如果你只是为内部使用而编写代码,那么未来的开发人员总是可以删除“final”--你可以把它看作一个警告,意思是“这个类不是为继承而设计的”。

6
根据我的经验,如果除了我以外还有其他人会使用这段代码,那么这并不是过度保险。我从来就不明白为什么 Java 默认情况下允许方法被覆盖。 - Eric Weilnau
11
了解Effective Java的一些内容,您需要了解Josh对设计的看法。他说,你应该总是将类接口设计得像一个公共API一样。我认为大多数人的观点是,这种方法通常过于繁重和不灵活。(YAGNI等) - Tom Hawtin - tackline
11
好的回答。(我忍不住指出它有六个字符,因为还有一个空格... ;-)) - joel.neely
49
请注意,将类设为final可能会使测试更加困难(难以存根或模拟)。 - notnoop
11
如果最终类是通过接口编写的,那么进行模拟测试就不应该成为一个问题。 - Fred Haslam
显示剩余7条评论

31

你可能希望将一个方法声明为final,这样覆盖它的类就不能改变其他方法所依赖的行为。在构造函数中调用的方法通常被声明为final,以避免在创建对象时出现任何不愉快的意外。


我不太理解这个答案。如果您不介意,能否解释一下您所说的“覆盖类无法更改其他方法中依赖的行为”是什么意思?我之所以问这个问题,首先是因为我没有理解这句话,其次是因为Netbeans建议我从构造函数调用的一个函数应该是“final”。 - Nav
1
@Nav 如果你将一个方法声明为final,那么继承该方法的子类就无法拥有自己的版本(否则将出现编译错误)。这样,你就知道在该方法中实现的行为在其他人扩展该类时不会改变。 - Bill the Lizard
C# 会在你从构造函数中调用虚方法时发出警告。在构造函数中调用的方法有点类似于这种情况。 - Petter Hesselberg

22
如果你想强制使用组合而非继承,那么将一个类声明为final是一个原因。这通常是为了避免类之间的紧耦合关系。

18

有三种情况可以使用final方法:

  1. 避免派生类覆盖特定基类功能。
  2. 出于安全目的,基类提供了框架的一些重要核心功能,派生类不应更改这些功能。
  3. Final方法比实例方法更快,因为对于final和private方法没有使用虚拟表概念。因此无论在哪里都有可能,请尽量使用final方法。

使类成为最终类的目的:

以便没有人可以扩展这些类并更改它们的行为。

例如:包装类Integer是一个最终类。如果该类不是最终类,则任何人都可以将其扩展到自己的类中并更改整数类的基本行为。为了避免这种情况,Java将所有包装类都设置为最终类。


对于你的例子我并不是很满意。如果是这样的话,为什么不将方法和变量设为final而不是将整个类设为final呢?请问你能否编辑你的答案并提供更多细节。 - Rajkiran
4
即使将所有变量和方法都声明为final,仍然有可能会有其他人继承你的类并添加额外的功能以及改变类的基本行为。 - user1923551
7
如果这个类不是final的,那么任何人都可以将Integer扩展为自己的类并更改其基本行为。假设允许这样做并且这个新类被称为“DerivedInteger”,它仍然不会改变原始的Integer类,任何使用“DerivedInteger”的人都要自担风险,所以我仍然不明白为什么这是一个问题。 - Siddhartha

13

6
继承就像是一把电锯 - 在正确的手中非常强大,但在错误的手中却很糟糕。你要么设计一个可被继承的类(这可能会限制灵活性并且需要更长时间),要么就应该禁止它。
请参阅《Effective Java》第二版的第16和17条,或者我的博客文章"Inheritance Tax"

1
博客文章的链接已经失效了。另外,您认为能否再详细解释一下这个问题? - user289086
@MichaelT:修复了链接,谢谢 - 点击链接获取更详细的解释 :) (我现在没有时间添加内容,恐怕。) - Jon Skeet
@JonSkeet,博客文章的链接已损坏。 - Lucifer
@Kedarnath:对我来说它有效...它曾经出了一些问题,但现在已经指向了正确的页面,在codeblog.jonskeet.uk上... - Jon Skeet

5

嗯... 我能想到两件事:

你可能拥有一个处理某些安全问题的类。通过对其进行子类化并提供系统子类化版本,攻击者可以规避安全限制。例如,你的应用程序可能支持插件,如果插件可以仅仅子类化你的安全相关类,它可以使用这个技巧将一个子类化版本偷偷地放在正确的位置上。然而,这实际上是Sun公司需要解决的问题,涉及小程序等,可能不是这样一个现实情况。

一个更实际的例子是避免对象变得可变。例如,由于字符串是不可变的,因此你的代码可以安全地保留对它的引用。

 String blah = someOtherString;

通过子类化String对象,可以添加方法来修改字符串的值,而不是先复制该字符串。但是,如果你能够子类化String,你可以为其添加方法,允许修改字符串的值,现在没有代码可以再依赖于只复制字符串就可以保持原样,相反,它必须复制字符串。


3
为了防止人们做出可能会让自己和他人感到困惑的行为。想象一下一个物理图书馆,其中有一些定义好的常量或计算。如果不使用final关键字,某个人可以随意重新定义基本计算或不应该更改的常量。

4
关于那个论点,有一些我不理解的地方 - 如果有人改变了这些计算/常数,而这个更改没有任何好处,那么任何失败都应该完全归他们的责任吧?换句话说,这个更改有什么��处吗? - matt b
是的,从技术上讲,“他们”有责任,但想象一下其他人使用未被告知更改的库可能会导致混淆。我一直认为这就是许多Java基础类标记为final的原因,以防止人们更改基本功能。 - Anson Smith
@matt b - 您似乎假定进行更改的开发人员是有意为之或者他们会关心这是他们的错。如果除我之外的其他人将要使用我的代码,我会标记它为最终版,除非它本来就是可以更改的。 - Eric Weilnau
这实际上是与之前提到的'final'用法不同的一种用法。 - DJClayworth
这个答案似乎将单个字段的继承和可变性与编写子类的能力混淆了。你可以将单个字段和方法标记为final(防止重新定义),而不会禁止继承。 - joel.neely

2
此外,如果您正在编写商业闭源类,您可能不希望人们能够在以后更改功能,特别是如果您需要为其提供支持并且人们已经覆盖了您的方法,并且抱怨调用它会产生意外结果。

2

如果您将类和方法标记为final,您可能会注意到一些小的性能提升,因为运行时不必查找要调用给定对象的正确类方法。非final方法被标记为虚拟方法,以便在需要时可以正确扩展,final方法可以直接链接或编译成类内联。


2
我认为这是一个谬论,因为当前的虚拟机应该能够根据需要进行优化和反优化。我记得看到一些基准测试将其归类为谬论。 - Miguel Ping
1
代码生成的差异并不是虚构的,但性能提升可能是。正如我所说,这只是一个小的提升,在某些网站上提到了3.5%的性能提升,有些更高,但在大多数情况下,并不值得一切都为了代码而努力。 - seanalltogether
1
这并不是一个神话。事实上,编译器只能内联V final方法。我不确定任何JIT是否可以在运行时“内联”东西,但我怀疑它可能无法做到,因为您稍后可能会加载派生类。 - Uri
1
微基准测试==谨慎对待。话虽如此,在我的笔记本电脑上使用Eclipse进行10B周期预热后,对10B次调用的平均值显示两种返回字符串的方法之间基本没有差异。最终结果为6.9643us和非最终结果为6.9641us。在我看来,这个差距是背景噪音。 - joel.neely

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接