相互实现两个Java 8默认方法是否是一种好习惯?


14

我正在设计一个具有两个相关方法的接口,类似于此方法:

public interface ThingComputer {
    default Thing computeFirstThing() {
        return computeAllThings().get(0);
    }

    default List<Thing> computeAllThings() {
        return ImmutableList.of(computeFirstThing());
    }
}

大约一半的实现只会计算一件事,而另一半可能会计算更多。

这在广泛使用的Java 8代码中是否有先例?我知道Haskell在某些类型类中做类似的事情(Eq例如)。

好处是,与拥有两个抽象类(SingleThingComputerMultipleThingComputer)相比,我必须编写更少的代码。

不利的一面是,一个空的实现可以编译,但是在运行时会用炸掉StackOverflowError。可以使用a来检测相互递归ThreadLocal并给出更好的错误,但这会增加非Buggy代码的开销。


3
为什么您会故意编写保证无限递归的代码?一切都不会因此而来。

1
嗯,这不是“保证”的。适当的实现将覆盖这些方法之一。
塔维安·巴恩斯

6
你不知道那个。类或接口需要独立存在,而不是假设子类将以某种方式避免发生重大逻辑错误。否则,它会很脆,无法兑现其保证。请注意,我并不是说您的接口可以或应该强制执行一个实现类,throw new Error();或者做一些愚蠢的事情,只是说接口本身不应该通过default方法具有脆弱的契约。

我同意@Snowman,这是一枚地雷。我认为这是教科书“邪恶的代码”,在某种意义上讲,乔恩·斯凯特(Jon Skeet)的意思是-注意邪恶与是不一样的,它是邪恶的/聪明的/诱人的,但仍然是错误的:)我推荐youtu.be/lGbQiguuUGc?t=15m26s(甚至整个范围内的讨论都是非常有趣的)。
Konrad Morawski 2015年

1
仅仅因为每个函数都可以彼此定义即可,并不意味着它们可以互相定义。那不会以任何语言进行。您需要一个带有2个抽象继承程序的接口,每个抽象继承程序都实现一个接口,但又抽象另一个接口,因此实现程序可以通过从正确的抽象类继承来选择要实现的哪一侧。一侧必须独立于另一侧定义,否则弹出堆栈。您有疯狂的默认值,假定实现者将为您解决问题,abstract强迫他们解决问题。
Jimmy Hoffa 2015年

Answers:


16

TL; DR:请勿执行此操作。

您在此处显示的是易碎的代码。

接口是合同。它说:“不管得到什么对象,它都可以做X和Y。” 在编写时,您的接口既不执行 X也不执行Y,因为可以保证引起堆栈溢出。

引发错误或子类表示不应捕获的严重错误:

错误是Throwable的子类,它指示合理的应用程序不应尝试捕获的严重问题。

此外,VirtualMachineErrorStackOverflowError的父类)说:

抛出该错误以表明Java虚拟机已损坏或已用尽其继续运行所必需的资源。

您的程序不应与JVM资源有关。这就是JVM的工作。编写导致JVM错误作为正常操作的一部分的程序是不好的。它可以保证您的程序将崩溃,或迫使该接口的用户捕获它不应该关注的错误。


您可能会从名誉“ C#语言设计委员会成员”的努力中了解Eric Lippert。他谈到了促使人们走向成功或失败的语言功能:虽然这不是语言功能或语言设计的一部分,但他的观点在实现接口或使用对象时同样有效。

您还记得在《公主新娘》中,韦斯特利醒来时发现自己和一个嘶哑的白化病和险恶的六指男人鲁根伯爵被困在《绝望的深渊》中吗?绝望之坑的原理是双重的。首先,它是一个坑,因此很容易掉进去但很难爬出来。其次,它引起了绝望。由此得名。

资料来源:C ++和绝望的坑

StackOverflowError默认情况下,让接口抛出a 将开发人员推入“绝望深渊”,这不是一个好主意。相反,将开发人员推向成功之路。使它容易正确和不崩溃的JVM使用的接口。

在这两种方法之间委托是可以的。但是,依赖性应该是一种方式。我喜欢将方法委托视为有向图。每种方法都是图上的一个节点。每次一个方法调用另一个方法时,都要从调用方法到被调用方法画一条边。

如果您绘制图形并注意到它是循环的,那就是代码的味道。这有可能将开发人员推入绝望深渊。请注意,不应绝对禁止使用它,只是必须谨慎使用。递归算法将在调用图中专门包含循环:这很好。记录下来并警告开发人员。如果不是递归的,请尝试中断该循环。如果不能,请找出哪些输入可能导致堆栈溢出,并缓解它们或将其记录为最后一种情况(如果其他方法无效)。


好答案。我很好奇为什么这在Haskell中如此常见。我猜是不同的开发文化。
塔维安·巴恩斯

我没有Haskell的经验,也无法与我谈谈这是否或为什么在函数式编程中更普遍。

再仔细研究一下,似乎Haskell社区也不是很满意。编译器最近还学会了检查“最小完整定义”,因此,空的实现至少会产生警告。
塔维安·巴恩斯

1
大多数功能性编程语言都具有尾调用优化功能。通过这种优化,尾递归不会破坏堆栈,因此这种做法很有意义。
2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.