协变 return 类型最佳实践

Covariant return type best practices

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

Declarations should use Java collection interfaces such as "List" rather than specific implementation classes such as "LinkedList". The purpose of the Java Collections API is to provide a well defined hierarchy of interfaces in order to hide implementation details. (squid:S1319)

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

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

所以我有以下问题:

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

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

如您所说,发布信息如我使用TreSet可以为客户端优化留出空间-但我的想法是它取决于:您可以根据具体情况评估是否需要放宽通用规则以公开您可以.

的更通用的接口

所以,回答你的问题:

  1. 是的,在 NumberGetter 接口的 IntegerGetter 实现中 return 更窄的类型是合适的:Java 允许你做并且你没有破坏我的越通用越漂亮规则:NumberGetter接口暴露了使用Number作为return类型的更通用的接口,但在具体实现中我们可以使用更窄的 return 类型来指导方法实现:引用更抽象接口的客户端不受此选择的影响,引用特定 subclass 的客户端可以尝试使用更具体的接口[=53] =]
  2. 与上一点相同,但我认为它对客户的用处不如前一个案例:客户可能会发现参考 Integer 比参考 Number 更有用(如果我明确使用 NumberGetter,可能我认为是 Integers,而不是 Numbers),但指的是 TreeSet 而不是 [=24] =] 仅当您需要子 class 而不是接口公开的 API 时才有用...
  3. 见初始论文

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

return 类型必须在调用者的需求和实现的需求之间取得平衡:你告诉调用者越多 return 类型,就越难改变那种类型以后。

哪个更重要要看具体情况。你见过这种类型的变化吗?了解来电者的类型有多大价值?

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

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

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

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

的消费者提供的灵活性程度

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

你想让你的消费者只循环吗? Return一个Collection<T>.
您希望您的消费者能够通过索引访问吗? Return一个List<T>
您是否希望您的消费者知道并能够有效地验证某个元素是否存在? Return一个Set<T>
等等。

相同的方法对输入参数有效。如果只循环接受 List<T>,甚至是 ArrayList<T>LinkedList<T> 等具体的 类,有什么意义呢?你只是让你的代码在未来的修改中缺乏灵活性。

您在这里使用 IntegerGetter's inherited methods' return 类型所做的工作称为 类型专业化 。只要您继续将 接口 暴露给外部世界就可以了。

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

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