协变返回类型的最佳实践

问题描述 投票:2回答:3

我依旧记得在大学里学习一种方法的返回类型应该尽可能地缩小,但我在网上搜索任何参考文献都是空的,而SonarQube称之为代码味道。例如。在下面的例子中(注意TreeSetSet只是例子)

public interface NumberGetter {
    Number get();

    Set<Number> getAll();
}

public class IntegerGetter implements NumberGetter {

    @Override
    public Integer get() {
        return 1;
    }

    @Override
    public TreeSet<Number> getAll() {
        return new TreeSet<>(Collections.singleton(1));
    }
}

SonarQube告诉我

声明应使用Java集合接口(如“List”)而不是特定的实现类(如“LinkedList”)。 Java Collections API的目的是提供一个定义良好的接口层次结构,以隐藏实现细节。 (鱿鱼:S1319)

我看到了隐藏实现细节的观点,因为我无法在不破坏向后兼容性的情况下轻松更改IntegerGetter :: getAll()的返回类型。但通过这样做,我还向消费者提供了可能有价值的信息,即他们可以改变他们的算法以更适合使用TreeSet。如果他们不关心这个属性,他们仍然可以使用IntegerGetter(但是他们获得了它),如下所示:

Set<Number> allNumbers = integerGetter.getAll();

所以我有以下问题:

  • IntegerGetter :: get()返回更窄的类型是否合适?
  • IntegerGetter :: getAll()返回更窄的类型是否合适?
  • 是否有关于此主题的最佳实践或答案只是“它取决于”?

(如果我用TreeSet替换SortedSet,N.B.SonarQube不会抱怨。这个代码是否只是没有使用Collection API接口?如果我的特定问题只有一个具体的类怎么办?)

java generics collections covariance
3个回答
0
投票

返回类型必须在调用者的需求和实现的需要之间取得平衡:您告诉调用者关于返回类型的越多,以后更难以更改该类型。

哪个更重要将取决于具体情况。你有没有看到这种类型的变化?了解来电者的类型有多大价值?

IntegerGetter.get()的情况下,如果返回类型发生变化将会非常令人惊讶,因此告诉调用者没有任何伤害。

IntegerGetter.getAll()的情况下,它取决于调用者使用该方法的内容:

  • 如果他只是想迭代,那么Iterable将是正确的选择。
  • 如果我们需要更多的方法,如大小,Collection可能。
  • 如果他依赖于唯一的数字,那就是Set
  • 如果他还依赖于正在排序的数字,那就是SortedSet
  • 如果它实际上需要是来自JDK的红黑树,那么它可以直接操纵其内部状态以进行丑陋的黑客攻击,那么TreeSet可能是正确的选择。

1
投票

我尝试 - 作为经验法则 - 在方法签名上使用支持我需要的API的最通用类型(类或接口无关紧要):更通用的类型,更少的API。所以,如果我需要一个表示相同类型对象族的参数,我开始使用Collection;如果在特定的场景中是重要的排序的想法,我使用List,避免发布我内部使用的特定List实现的任何信息:我的想法是保持改变实现的能力(可能是为了性能优化,支持一个不同的数据结构,等等)没有中断客户端。

正如你所说,发布像我使用TreSet这样的信息可以留给客户端优化 - 但我的想法是它依赖于:个案你可以评估特定场景是否需要放松一般规则以暴露更通用的接口您可以。

所以,回答你的问题:

  1. 是的,在IntegerGetter接口的NumberGetter实现中返回一个更窄的类型是合适的:Java允许你这样做而你没有打破我更通用的更美丽的规则:NumberGetter接口使用Number作为返回类型暴露更通用的接口但是在具体的实现中,我们可以使用较窄的返回类型来指导方法实现:引用更抽象的接口的客户端不受此选择的影响,引用特定子类的客户端可以尝试使用更具体的接口
  2. 与前一点相同,但我认为它对客户来说不如前一种情况有用:可能是客户可以找到有用的引用Integer而不是Number(如果我明确使用NumberGetter,可能我认为就Integers,不是Numbers的术语),但是只有当你需要子类暴露的API而不是接口时,引用TreeSet而不是Set才有用。
  3. 见初论文

这是一个准哲学问题 - 我的答案也是如此:我希望它对你有用!


0
投票

这不能只有一个答案,因为它取决于用例。 我的意思是,这取决于您希望实施的灵活程度,以及您希望为消费者提供API的灵活程度。

我会给你一个更一般的答案。

您是否希望您的消费者仅限环路?返回一个Collection<T>。 您希望您的消费者能够通过索引访问吗?返回一个List<T> 您是否希望您的消费者知道并能够有效地验证元素是否存在?返回一个Set<T> 等等。

相同的方法对输入参数有效。接受List<T>,甚至是ArrayList<T>LinkedList<T>这样的具体课程,如果你只是循环它有什么意义呢?您只是为代码提供了更少的灵活性,以便将来进行修改

你在这里使用IntegerGetter的继承方法返回类型所做的就是类型特化。只要您继续向外部世界公开接口,这没关系。

我的经验法则是在处理外部世界时尽可能通用,并且在实现我的应用程序的关键(核心)部分时尽可能具体,限制可能的用例,保护自己免受滥用我刚刚编写的内容,以及用于文档目的。

您不应该绝对做的是使用instanceofclass比较来发现实际类型并采取不同的路线。这破坏了代码库。

© www.soinside.com 2019 - 2024. All rights reserved.