使用接口的默认方法来实现方法——矛盾吗?

12

介绍

我已经阅读了多篇关于在SO上实现接口和抽象类的文章。我找到了一篇特别想在这里链接 - 链接 - 接口与默认方法 vs 抽象类, 它涵盖了同样的问题。作为被接受的答案,建议在可能的情况下使用接口的默认方法。但是下面的评论指出“这个功能对我来说更像是一个hack”,解释了我的问题。

默认方法被引入以使接口的实现更加灵活 - 当接口发生变化时,实现类不一定需要(重新)编写代码。因此,仅使用接口的默认方法来实现所有实现类中的方法-引用:“对我来说更像是一个hack”。

我的考试例子:

类概述:

  • Item - 所有物品的抽象超类
  • Water - 可消耗的物品
  • Stone - 不可消耗的物品
  • Consumable - 一些可消耗物品的方法的接口(这些方法必须由所有实现类重写)

结合这些:

Water是一个Item并且实现了Consumable; Stone也是一个Item,但没有实现Consumable。

我的考试

我想实现一个所有物品都必须实现的方法。因此,我在类Item中声明了这个方法的签名。

protected abstract boolean isConsumable(); 
//return true if class implements (or rather "is consumable") Consumable and false in case it does not

现在我有几个选择:
  1. 在每个Item子类中手动实现该方法(当扩展应用程序时,这绝对不可能)。
如上所述,当扩展应用程序时,这将是不切实际或高度低效的。
  1. 将该方法作为默认方法实现在接口内部,以便Consumable类已经实现了超类Item所需的方法。
这是其他帖子建议的解决方案-我看到了使用这种方式实现的优点:

引用 -“这个新功能的好处是,在你之前被迫使用抽象类来进行方便的方法时,从而限制了实现者的单一继承,现在你可以只使用接口和最少量的实现工作来获得真正干净的设计。”链接

但是,在我看来,它仍然似乎与我在介绍中提到的默认方法的原始想法相矛盾。此外,当扩展应用程序并引入更多共享所有Consumables的相同实现的方法(例如示例方法isConsumable())时,接口将实现多个默认方法,这与接口不实现实际方法的想法相矛盾。
介绍使用子超类而不是接口 - 例如将Consumable类作为Item的抽象子类和Water的超类。
这提供了在Item中编写方法的默认情况(例如:isConsumable() //return false),然后在子超类中重写此方法的机会。但问题在于:随着应用程序的扩展和引入更多的子超类(如Consumable类),实际项目将开始扩展超过一个子超类。这可能并不是件坏事,因为对于接口也需要做同样的事情,但它使继承树变得复杂 - 例如:现在一个项目可能扩展一个名为ALayer2的子超类,它是ALayer1的子超类,后者又是Item(layer0)的超类。
引入另一个超类(因此与Item具有相同的层次结构) - 例如将Consumable类作为抽象类引入,该类将是Water的另一个超类。这意味着Water将必须扩展Item&Consumable。
这个选项提供了灵活性。可以为新超类创建全新的继承树,同时仍能看到Item的实际继承关系。但我发现这种结构在实际类中的实现以及之后使用这些类的过程中存在缺点 - 例如:如果Consumable可以有不适用于项目的子类,那么如何说Consumable是Item。整个转换过程可能会引起头痛,比选项3的结构更加头痛。
什么是实现此结构的正确选项?
  • 这是我列出的选项之一吗?
  • 它是那些选项的变体吗?
  • 还是另一个我尚未考虑到的选项?

我选择了一个非常简单的例子 - 请在回答时记住将可扩展性考虑为将来的实现。 预先感谢您的任何帮助。

编辑#1

Java不允许多重继承。 这将影响选项4。 使用多个接口(因为你可以实现多个)可能是一种解决方法,不幸的是,再次需要默认方法,这正是我最初试图避免的实现类型。 链接 - 可能的解决方案的多重继承问题


2
对我来说,Item知道它的实例是否为Consumable听起来很奇怪。个人而言,我宁愿没有这样的方法,在需要时使用theInstance instanceof Consumable - sp00m
2
只有我觉得奇怪,你想要 Item::isConsumable 和一个单独的接口 Consumable 吗?为什么 Item 需要知道 Consumable 的存在呢? - Eugene
我目前正在努力提供更好的示例 - isConsumable() 方法是一个非常简单的示例,可以用另一种方式处理。关于这个例子,你是正确的。Roland 的答案也为这个特定的方法提供了快速的解决方案。 - CrimsonMike
1
你不能使用 default 方法来实现超类的 abstract 方法。因为 Item 没有实现 Consumable,所以 Consumable 中的 default 方法与 Item 中具有相同签名的已声明方法之间没有关系。因此你有两个不相关的方法。Java 设计者通过让非接口类的方法始终获胜来解决这个潜在的错误源,即使它是 abstract 的也是如此。这也兼容 Java 8 之前的代码。 - Holger
1
@CrimsonMike 如果有相同签名但不互相覆盖的任何方法,则它们是无关的,例如 class A { void foo() {} }class B { void foo() {} }。当然,在没有多重继承的情况下,它们之间没有问题。因此,只有涉及“默认”方法的情况,您可能会在一个类中遇到这样的方法,要么通过从两个不相关的接口继承它们(然后,除非您覆盖它们,否则会出现错误),要么通过从一个接口和超类继承一个(然后,您将获得超类方法)。 - Holger
显示剩余4条评论
1个回答

4

我缺少第五个选项(或者我没有正确阅读):在Item自身中提供该方法。

假设可消耗物品可以通过Consumable接口来识别,下面是我不能推荐你列出的大多数选项的原因: 第一个选项(即在每个子类中实现)对于像this instanceof Consumable这样简单的东西来说太麻烦了。第二个可能还可以,但不会是我的首选。第三和第四个我根本不能推荐。如果我只能给一个建议,那么就是要认真考虑继承,并且永远不要仅仅因为它们曾经让你的生活变得更轻松而使用中间类。也许当您的类层次结构变得更加复杂时,这将在将来损害您(注意:我并不是说您根本不应使用中间类;-))。

那么针对这种情况,我会在抽象的Item类中实现以下内容:

public final boolean isConsumable() {
  return this instanceof Consumable;
}

也许我不会提供这样的方法,因为它与在第一次编写item instanceof Consumable一样好。什么时候我会使用接口的默认方法呢?也许当接口具有混合特性或实现对接口更有意义时,例如Consumable的特定函数可能会作为默认方法提供,而不是在任何伪实现类中提供,以便其他类可以再次扩展它...我也非常喜欢关于混合的以下答案(或者引用)。关于你的编辑:"Java不允许多重继承"... 好吧,使用混合类可以实现类似于多重继承的功能。您可以实现许多接口,这些接口本身也可以扩展许多其他接口。然后使用默认方法就可以实现可重用性。那么,为什么在接口中使用default方法是可以的(或者不会违反接口定义本身)呢?
  • 为大多数使用情况提供一个简单或天真的实现(其中实现类可以提供特定、专业化和/或优化的功能)
  • 当从所有参数和上下文清楚地知道方法必须执行的操作时(并且没有适当的抽象类存在)
  • 对于模板方法,即当它们发出调用抽象方法来执行一些范围更广的工作时。典型的例子是Iterable.forEach,它使用Iterable的抽象方法iterator()并将提供的操作应用于其每个元素。

感谢Federico Peralta Schaffner的建议。

向后兼容性也是为了完整性而存在,但作为功能接口,它们被单独列出: 默认实现还有助于在添加新函数时不破坏现有代码(可以通过仅抛出异常使代码仍然编译,或提供适当的实现,适用于所有实现类)。对于函数接口,这是一个特殊的接口情况,其默认方法相当关键。函数接口可以轻松地增强功能,而这些功能本身不需要任何特定的实现。只需考虑 Predicate 作为一个例子。您提供test,但您还可以获得negateorand(作为默认方法提供)。许多功能接口通过默认方法提供其他上下文功能。


1
请注意,我不认为“默认”方法只是向后兼容性功能;-) 我经常希望早些时候已经有类似的东西可用... 而且我肯定也错过了“私有默认”方法... 因为有时它不应该是抽象类的一部分,而应该已经在接口中可用。 想象一下一个使用其他类和接口的接口,但功能非常清晰,以至于接口本身已经可以提供所有所需的方法... 我想知道我的意思是否清楚 :-) - Roland
3
不一定。接口可以提供默认、简单、天真的实现,然后一些子类可以用自己特定、专业、优化的实现来覆盖这些简单的实现。 - fps
2
然而,在极少数情况下,当所有参数和值都清楚地表明方法在大多数情况下会返回什么,并且没有合适的抽象类时,我可能也会在“普通”接口中有一个默认实现。 - Roland
2
@CrimsonMike 使用默认方法的另一个好处是用于模板方法,即当它们调用抽象方法来执行一些范围更广的工作时。典型的例子是Iterable.forEach,它使用Iterable的抽象方法iterator()并将提供的操作应用于迭代器返回的每个元素。 - fps
2
我认为你总结得非常好,这个答案对于所有学习Java(初学者和中级者)的人都非常有帮助。当我开始使用接口时,像这样的帖子和答案就像黄金一样珍贵 - 为我节省了大量时间。 - CrimsonMike
显示剩余7条评论

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