使用抽象类而不是特征有啥好处?

Posted

技术标签:

【中文标题】使用抽象类而不是特征有啥好处?【英文标题】:What is the advantage of using abstract classes instead of traits?使用抽象类而不是特征有什么好处? 【发布时间】:2010-12-31 18:05:22 【问题描述】:

使用抽象类而不是特征有什么好处(除了性能)?在大多数情况下,抽象类似乎可以被特征替换。

【问题讨论】:

【参考方案1】:

我能想到两个不同之处

    抽象类可以有构造函数参数和类型参数。特征只能有类型参数。有一些讨论认为,将来甚至 trait 也可以有构造函数参数 抽象类可与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,特征才能完全互操作

【讨论】:

非常重要的附录:一个类可以继承多个特征,但只能继承一个抽象类。我认为这应该是开发人员在考虑在几乎所有情况下使用哪个时提出的第一个问题。 lifesaver: "只有在不包含任何实现代码的情况下,Traits 才能完全互操作" abstract - 当集体行为定义或导致一个对象(对象的分支)但仍未组合为(准备好)对象时。特质,当你需要引入能力时,即能力永远不会源于对象的创建,当一个对象从孤立中出来并且必须进行通信时,它就会发展或需要。 Java8 中不存在第二个区别。 根据 Scala 2.12,特征编译为 Java 8 接口 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces。【参考方案2】:

Scala 编程中有一个名为"To trait, or not to trait?" 的部分解决了这个问题。由于第 1 版可以在线获取,我希望可以在这里引用整个内容。 (任何认真的 Scala 程序员都应该买这本书):

每当您实现可重用的行为集合时,您将 必须决定是要使用特征还是抽象类。 没有固定的规则,但本节包含一些指导方针 考虑一下。

如果该行为不会被重用,则将其设为具体类。它 毕竟不是可重用的行为。

如果它可以在多个不相关的类中重复使用,请将其设为 trait。 只有特质可以混合到类层次结构的不同部分中。

如果您想在 Java 代码中继承它,请使用抽象类。 由于带有代码的特征没有与 Java 相似的类似物,因此它往往是 从 Java 类中继承特征很尴尬。继承自 同时,Scala 类与从 Java 类继承完全一样。 作为一个例外,只有抽象成员的 Scala 特征可以翻译 直接连接到 Java 接口,所以你应该随意定义这样的 即使您希望 Java 代码从它继承,也可以使用特征。见第 29 章 了解有关一起使用 Java 和 Scala 的更多信息。

如果您打算以编译的形式分发它,并且您希望在外部 组来编写继承自它的类,您可能倾向于 使用抽象类。问题是当一个特征获得或失去时 一个成员,任何继承自它的类都必须重新编译,即使 他们没有改变。如果外部客户只会调用 行为,而不是从它继承,然后使用特征就可以了。

如果效率很重要,请倾向于使用类。大多数Java 运行时使类成员的虚拟方法调用更快 操作比接口方法调用。特征被编译为 接口,因此可能会支付轻微的性能开销。 但是,只有当您知道该特征时,您才应该做出此选择 有问题构成性能瓶颈并有证据 使用类实际上可以解决问题。

如果你还是不知道,在考虑了以上之后,那就从 使它成为一种特征。您以后可以随时更改它,一般来说 使用 trait 会保留更多选项。

正如@Mushtaq Ahmed 所提到的,特征不能将任何参数传递给类的主构造函数。

另一个区别是super的处理方式。

类和特征之间的另一个区别是,在类中,super 调用是静态绑定的,而在特征中,它们是动态绑定的。如果你在一个类中写super.toString,你就知道将调用哪个方法实现。但是,当您在 trait 中编写相同的内容时,为 super 调用调用的方法实现在您定义 trait 时是未定义的。

更多详情请参阅Chapter 12 的其余部分。

编辑 1 (2013):

与特征相比,抽象类的行为方式存在细微差别。线性化规则之一是它保留了类的继承层次结构,这倾向于将抽象类推到链中的后面,而特征可以很高兴地混入其中。在某些情况下,实际上最好位于类线性化的后面位置,因此可以使用抽象类。见constraining class linearization (mixin order) in Scala。

编辑 2(2018 年):

从 Scala 2.12 开始,trait 的二进制兼容性行为发生了变化。在 2.12 之前,向 trait 添加或删除成员需要重新编译继承该 trait 的所有类,即使这些类没有更改。这是由于 JVM 中的特征编码方式。

从 Scala 2.12 开始,traits compile to Java interfaces,所以要求放宽了一点。如果 trait 发生以下任何情况,其子类仍需要重新编译:

定义字段(valvar,但可以使用常量 - final val 没有结果类型) 致电super 正文中的初始化语句 扩展类 依靠线性化在正确的超特征中找到实现

但如果特征没有,您现在可以在不破坏二进制兼容性的情况下更新它。

【讨论】:

If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine - 有人能解释一下这里有什么区别吗? extendswith? @0fnt 他的区别不在于extends vs with。他的意思是,如果您只在同一编译中混合特征,则二进制兼容性问题不适用。但是,如果您的 API 旨在允许用户自己混合 trait,那么您将不得不担心二进制兼容性。 @0fnt:extendswith 之间绝对没有语义差异。它是纯粹的语法。如果你从多个模板继承,第一个得到extend,所有其他的得到with,就是这样。将with 视为逗号:class Foo extends Bar, Baz, Qux 这在 scala 中是什么意思 可以将特征添加到对象实例中。抽象类不能添加到对象实例中。【参考方案3】:

不管它值多少钱,Odersky 等人的Programming in Scala 建议,当你怀疑时,你可以使用特征。如果需要,您可以随时将它们更改为抽象类。

【讨论】:

【参考方案4】:

除了不能直接扩展多个抽象类,但可以将多个特征混合到一个类中之外,值得一提的是特征是可堆叠的,因为特征中的超级调用是动态绑定的(它指的是一个类或特征在当前特征之前混合)。

来自 Thomas 在Difference between Abstract Class and Trait 中的回答:

trait A
    def a = 1


trait X extends A
    override def a = 
        println("X")
        super.a
    
  


trait Y extends A
    override def a = 
        println("Y")
        super.a
    


scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1

【讨论】:

【参考方案5】:

当扩展一个抽象类时,这表明子类是类似的。我认为在使用特征时不一定是这种情况。

【讨论】:

这有什么实际意义,还是只是让代码更容易理解?【参考方案6】:

在Programming Scala 中,作者说抽象类构成了一种经典的面向对象的“is-a”关系,而特征是一种 scala 组合方式。

【讨论】:

【参考方案7】:

抽象类可以包含行为 - 它们可以使用构造函数参数(特征不能)进行参数化并表示一个工作实体。相反,特征只代表一个特性,一个功能的接口。

【讨论】:

希望你没有暗示特质不能包含行为。两者都可以包含实现代码。 @Mitch Blevins:当然不是。它们可以包含代码,但是当您使用大量辅助函数定义 trait Enumerable 时,我不会称它们为 行为,而只是与一个特性相关的功能。 @Dario 我认为“行为”和“功能”是同义词,所以我觉得你的回答很混乱。【参考方案8】:
    一个类可以继承多个特征,但只能继承一个抽象类。 抽象类可以有构造函数参数和类型参数。特征只能有类型参数。例如,你不能说 trait t(i: Int) ; i 参数是非法的。 抽象类可与 Java 完全互操作。您可以在没有任何包装器的情况下从 Java 代码中调用它们。只有在不包含任何实现代码的情况下,Traits 才能完全互操作。

【讨论】:

以上是关于使用抽象类而不是特征有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章

重构以避免在使用抽象类而不是接口时进行多重继承

Java 8 - 与默认方法和抽象类的接口

请问把基类构造函数声明为protected有啥好处呢(抽象基类)

接口和抽象类的用法与适用情况:

java抽象类与接口的选择

java接口与抽象类有啥区别?