【问题标题】:Covariant return type best practices协变返回类型最佳实践
【发布时间】:2019-07-12 07:45:50
【问题描述】:

我依稀记得在大学里学习,方法的返回类型应该总是尽可能窄,但我在网上搜索任何参考资料都是空的,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 告诉我

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

我明白了隐藏实现细节的意义,因为我不能轻易地改变 IntegerGetter::getAll() 的返回类型而不破坏向后兼容性。但是通过这样做,我还为消费者提供了潜在的有价值的信息,即他们可以更改他们的算法以更适合使用TreeSet。如果他们不关心这个属性,他们仍然可以使用IntegerGetter(不管他们如何获得它),就像这样:

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

所以我有以下问题:

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

(如果我用例如SortedSet 替换TreeSet,NB SonarQube 不会抱怨。这段代码的气味是否只是关于不使用 Collection API 接口?如果我的特定问题只有一个具体的类怎么办?)

【问题讨论】:

  • 如果他们想使用专门的算法,他们可以使用instanceof。一般来说,SonarQube 推荐正确的最佳做法:尽可能始终使用适当的收集界面。
  • 这真的是一个判断电话。有一个论点是您应该对接口进行编码,因此返回 Set 意味着您可以稍后进行更改并且不会搞砸。另一方面,如果您正在使用 getter 并且需要继续使用 TreeSet,那么这确实是您界面的一部分。如果你只在调用 getter 后执行 Set 方法,那么返回 Set 是安全的。如果你有理由,你总是可以让它更具体。

标签: java generics collections covariance


【解决方案1】:

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

正如您所说,发布信息,如 我使用 TreSet 可以为客户端优化留出空间 - 但我的想法是 这取决于:逐案您可以评估特定场景是否需要放宽一般规则以公开更通用的界面

所以,来回答你的问题:

  1. 是的,在NumberGetter 接口的IntegerGetter 实现中返回一个更窄的类型是合适的:Java 允许您这样做并且您不会违反我的更通用更漂亮 规则: NumberGetter 接口使用Number 作为返回类型公开了更通用的接口,但在具体实现中,我们可以使用更窄的返回类型来指导方法实现:引用更抽象接口的客户端不受此选择和客户端引用的影响到特定的子类可以尝试使用更具体的接口的优势
  2. 与上一点相同,但我认为它对客户的用处不如前一种情况:客户可能会发现参考Integer 比参考Number 有用(如果我明确使用@987654331 @,我认为可能是Integers,而不是Numbers),但仅当您需要子类公开的API而不是通过界面...
  3. 见初始论文

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

【讨论】:

    【解决方案2】:

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

    哪个更重要将取决于具体情况。你见过这种类型的变化吗?知道调用者的类型有多大价值?

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

    对于IntegerGetter.getAll(),这取决于调用者使用该方法的目的:

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

    【讨论】:

    【解决方案3】:

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

    我会给你一个更笼统的答案。

    你想让你的消费者只循环吗?返回Collection&lt;T&gt;
    您希望您的消费者能够通过索引访问吗?返回List&lt;T&gt;
    您是否希望您的消费者知道并能够有效地验证元素是否存在?返回Set&lt;T&gt;
    以此类推。

    同样的方法对输入参数有效。接受List&lt;T&gt;,甚至是ArrayList&lt;T&gt;LinkedList&lt;T&gt; 之类的具体类有什么意义,如果你只循环它?您只是为您的代码提供了较少的灵活性以供将来修改。

    您在这里对IntegerGetter 的继承方法的返回类型所做的操作称为类型特化。只要您一直将接口暴露给外部世界就可以了。

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

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

    【讨论】:

      猜你喜欢
      • 2018-03-26
      • 1970-01-01
      • 2018-12-22
      • 2022-06-14
      • 2021-02-24
      • 1970-01-01
      • 2015-02-07
      • 1970-01-01
      • 2022-06-15
      相关资源
      最近更新 更多