Java 8默认方法作为特征:安全吗?

120

在Java 8中,使用默认方法作为traits的替代品是否是一种安全实践?

有人声称如果你只是出于酷而使用它们,则可能会让熊猫感到悲伤,但这并不是我的意图。通常提醒我们,默认方法是为了支持API演进和向后兼容而引入的,这是真的,但这并不意味着将它们用作trait本身就是错误或扭曲的。

我有以下实际用例

public interface Loggable {
    default Logger logger() {
        return LoggerFactory.getLogger(this.getClass());
    }
}

或者,定义一个PeriodTrait

public interface PeriodeTrait {
    Date getStartDate();
    Date getEndDate();
    default isValid(Date atDate) {
        ...
    }
}

可以承认,可以使用组合(或甚至辅助类),但这似乎更冗长和混乱,并且不允许从多态性中受益。

因此,将默认方法用作基本特征是否安全?还是我应该担心意想不到的副作用?

有几个问题与Java vs Scala traits有关;这不是重点。 我也不仅仅是询问意见。 相反,我正在寻找权威答案或至少领域洞察力:如果您在公司项目中将默认方法用作特征,结果是否会成为定时炸弹?


在我看来,您可以通过继承抽象类获得相同的好处,而不必担心让熊猫哭泣...我能想到使用接口中默认方法的唯一原因是您需要该功能,并且无法修改基于该接口的大量遗留代码。 - Deven Phillips
1
我同意@infosec812的观点,即扩展一个定义了自己静态日志记录器字段的抽象类。你的logger()方法不会在每次调用时实例化一个新的日志记录器实例吗? - Eidan Spiegel
对于日志记录,您可能想查看Projectlombok.org以及他们的@Slf4j注释。 - Deven Phillips
1个回答

130

简短回答:只要你安全使用它们,就是安全的 :)

带点反讽的回答:告诉我什么是 "traits" ,也许我会给你一个更好的答案 :)

说真的,术语 “trait” 没有很好的定义。 许多 Java 开发人员最熟悉 Scala 中表达的 traits,但是 Scala 远不是第一种具有 traits 的语言,无论是以名称还是效果而言。

例如,在 Scala 中,traits 是有状态的(可以有 var 变量); 在 Fortress 中,它们是纯行为。Java 接口具有默认方法,是无状态的; 这是否意味着它们不是 traits?(提示:这是一个套路问题。)

同样,在 Scala 中,traits 通过线性化进行组合; 如果类 A 扩展 traits X 和 Y,则混合 X 和 Y 的顺序决定了如何解决 X 和 Y 之间的冲突。在 Java 中,该线性化机制不存在(部分原因是因为它过于不像 Java。)

添加接口默认方法的直接原因是支持接口演进,但我们非常清楚我们超出了那个范围。您认为这是“接口演变 ++”还是“ traits--”是个人解释问题。 因此,回答您关于安全性的问题......只要您坚持实际支持的机制,而不是试图将其愿望般地扩展到不支持的东西,您就应该没问题。

一个关键设计目标是,从接口的客户端的角度来看,默认方法应该与“常规”接口方法无法区分。 因此,方法的默认性质仅对接口的设计者实现者有意义。

以下是一些符合设计目标的用例:

  • 接口演进。 在这里,我们正在向现有接口添加一个新方法,该方法在该接口的现有方法中具有合理的默认实现。 例如,将forEach方法添加到Collection中,其中默认实现是基于iterator()方法编写的。

  • “可选”方法。 在这里,接口的设计者说:“如果实现者愿意接受相关功能的限制,他们不需要实现此方法”。 例如,Iterator.remove被赋予了抛出UnsupportedOperationException的默认值; 由于绝大多数Iterator的实现都具有此行为,因此默认情况下使得此方法基本上是可选的。(如果将AbstractCollection的行为表达为Collection的默认值,则可以对修改方法执行相同操作。)

  • 便利方法。 这些方法只是为了方便而存在,通常是基于类上的非默认方法实现的。您第一个示例中的logger()方法就是一个合理的例子。

  • 组合器。 这些是基于当前实例实例化接口的组合方法。例如,Predicate.and()Comparator.thenComparing()方法就是组合器的示例。

如果您提供了默认实现,则还应为其提供一些规范(在JDK中,我们使用@implSpec javadoc标签)。以帮助实现者理解他们是否需要重写该方法。某些默认值,例如便利方法和组合器,几乎永远不会被覆盖; 其他的,如可选方法,则经常被覆盖。您需要提供足够的规范(而不仅仅是文档)来说明默认值承诺做什么,以便实现者可以做出明智的决策,是否需要覆盖它。


10
感谢Brian提供这个全面的答案,现在我可以毫不担心地使用默认方法了。读者们可以从Brian Goetz在NightHacking Worldwide Lambdas中获取有关接口演变和默认方法的更多信息。 - youri
2
感谢@brian-goetz。根据您的说法,我认为Java的默认方法更接近于Ducasse等人在论文中定义的传统特征的概念(http://scg.unibe.ch/archive/papers/Duca06bTOPLASTraits.pdf)。对我来说,Scala“traits”似乎根本不像特征,因为它们具有状态,在组合中使用线性化,并且似乎它们还会隐式解决方法冲突 - 而这些都是传统特征所没有的。实际上,我认为Scala traits更像混入而不是特征。你怎么看?PS:我从未在Scala中编写过代码。 - adino
使用接口中的空默认实现作为监听器,这个想法怎么样?监听器实现可能只对几个接口方法感兴趣,因此通过将方法设置为默认值,实现者只需要实现它需要监听的方法。 - Lahiru Chandima
2
@LahiruChandima 像 MouseListener 一样吗?在这个 API 风格有意义的范围内,它适用于“可选方法”桶。请确保清楚地记录可选性! - Brian Goetz
是的。就像在 MouseListener 中一样。感谢您的回复。 - Lahiru Chandima
我认为对于与问题域无关的实用方法(例如在OP的示例中),可选性是可以接受的。但是,我认为业务规则不应作为默认方法实现,并且在使用面向对象编程(OOP)、设计模式等实现领域规则方面的投资是非常值得的——以帮助推理和维护代码中不断变化的领域状态。 - awgtek

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