协变返回类型的最佳做法

时间:2019-02-18 21:19:06

标签: java generics collections covariance

我隐约记得在大学里学习过,方法的返回类型应始终尽可能地窄,但是我在网上搜索的所有引用都是空的,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,NB SonarQube不会抱怨。此代码仅散发出关于不使用Collection API接口的气味吗?如果只有一个特定的类适合我的特定情况,该怎么办?问题吗?)

3 个答案:

答案 0 :(得分:1)

我通常会尝试在方法签名上使用支持我需要的API的最通用的 type (类或接口无关紧要):更通用的类型,则API最少。 因此,如果我需要一个表示同类型对象的 family 的参数,则可以开始使用Collection;如果在特定情况下 ordering 的想法很重要,那么我使用List,避免发布有关内部使用的特定List实现的任何信息:我的想法是保持无需中断客户端即可更改实现的功能(可能用于性能优化,支持不同的数据结构等)。

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

所以,请问您的问题:

  1. 是的,在IntegerGetter接口的NumberGetter实现中返回一个较窄的类型是适当的:Java允许您这样做并且不会破坏我的更通用,更美丽规则:NumberGetter接口使用Number作为返回类型公开更通用的接口,但是在特定实现中,我们可以使用更窄的返回类型来指导方法实现:引用更抽象接口的客户端是不受此选择的影响,并且引用特定子类的客户端可以尝试使用更具体的接口来获得优势
  2. 与上一点相同,但我认为对于客户来说,它比以前的情况用处小:可能是客户发现引用Integer比引用Number有用(如果我使用明确地是NumberGetter,可能是我认为是用Integer而不是Number而言),而是指TreeSet而不是Set仅在需要子类而不是接口公开的API时才有用。
  3. 请参阅初始论文

这是一个准哲学的问题-我的回答也是:我希望它对您有用!

答案 1 :(得分:0)

返回类型必须在调用者的需求和实现需求之间取得平衡:您向调用者介绍的返回类型越多,以后更改该类型的难度就越大。

哪个更重要取决于具体情况。您见过这种类型的变化吗?知道呼叫者的类型有多有价值?

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

IntegerGetter.getAll()的情况下,这取决于调用者将方法用于以下目的:

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

答案 2 :(得分:0)

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

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

您是否要让消费者仅循环使用?返回Collection<T>
您是否希望您的消费者能够按索引访问?返回List<T>
您是否希望消费者知道并能够有效地验证是否存在某个元素?返回Set<T>
依此类推。

相同的方法对输入参数有效。如果仅循环接受List<T>或什至是ArrayList<T>LinkedList<T>之类的具体类,那有什么意义呢?您只是在给代码提供较少的灵活性以进行将来的修改。

您在这里使用IntegerGetter的继承方法的返回类型进行的操作称为类型专门化。只要您一直将接口暴露给外部世界,就可以了。

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

绝对不应做的事情是使用instanceofclass比较来发现实际类型并采取不同的路线。那毁了一个代码库。