协变 return 类型最佳实践
Covariant return type best practices
我依稀记得在大学里学习过 return 类型的方法应该总是尽可能的窄,但是我在网上搜索任何参考资料都是空的,SonarQube 称之为代码味道。例如。在下面的例子中(注意 TreeSet
和 Set
只是例子)
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();
所以我有以下问题:
- IntegerGetter::get() 到 return 更窄的类型是否合适?
- IntegerGetter::getAll() 到 return 更窄的类型是否合适?
- 是否有关于此主题的任何最佳实践,或者答案只是 "it depends"?
(N.B。如果我用 SortedSet
替换 TreeSet
,SonarQube 不会抱怨。这段代码的味道是否只是因为没有使用 Collection API 接口?如果我的特定问题只有具体的 class 怎么办?)
我尝试 - 根据经验 - 在方法签名上使用最通用的 type (class 或接口无关紧要)支持 API 我需要:更通用的类型,更简洁的API。
所以,如果我需要一个参数代表相同类型对象的 family,我开始使用 Collection
;如果在特定场景中 ordering 的想法很重要,我使用 List
,避免发布任何关于我在内部使用的特定 List
实现的信息:我的想法是保持改变实现的能力(可能是为了性能优化,支持不同的数据结构,等等)而不中断客户端。
如您所说,发布信息如我使用TreSet
可以为客户端优化留出空间-但我的想法是它取决于:您可以根据具体情况评估是否需要放宽通用规则以公开您可以.
的更通用的接口
所以,回答你的问题:
- 是的,在
NumberGetter
接口的 IntegerGetter
实现中 return 更窄的类型是合适的:Java 允许你做并且你没有破坏我的越通用越漂亮规则:NumberGetter
接口暴露了使用Number
作为return类型的更通用的接口,但在具体实现中我们可以使用更窄的 return 类型来指导方法实现:引用更抽象接口的客户端不受此选择的影响,引用特定 subclass 的客户端可以尝试使用更具体的接口[=53] =]
- 与上一点相同,但我认为它对客户的用处不如前一个案例:客户可能会发现参考
Integer
比参考 Number
更有用(如果我明确使用 NumberGetter
,可能我认为是 Integer
s,而不是 Number
s),但指的是 TreeSet
而不是 [=24] =] 仅当您需要子 class 而不是接口公开的 API 时才有用...
- 见初始论文
这是一个准哲学问题-我的回答也是如此:希望对您有用!
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 类型所做的工作称为 类型专业化 。只要您继续将 接口 暴露给外部世界就可以了。
我的经验法则是在处理外部世界时尽可能通用,在实现我的应用程序的关键(核心)部分时尽可能具体,以限制可能的用例,保护自己免受滥用我刚刚编码的内容,并用于文档目的。
你绝对不应该做的是使用instanceof
或class
比较来发现实际类型并采取不同的路线。那会破坏代码库。
我依稀记得在大学里学习过 return 类型的方法应该总是尽可能的窄,但是我在网上搜索任何参考资料都是空的,SonarQube 称之为代码味道。例如。在下面的例子中(注意 TreeSet
和 Set
只是例子)
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();
所以我有以下问题:
- IntegerGetter::get() 到 return 更窄的类型是否合适?
- IntegerGetter::getAll() 到 return 更窄的类型是否合适?
- 是否有关于此主题的任何最佳实践,或者答案只是 "it depends"?
(N.B。如果我用 SortedSet
替换 TreeSet
,SonarQube 不会抱怨。这段代码的味道是否只是因为没有使用 Collection API 接口?如果我的特定问题只有具体的 class 怎么办?)
我尝试 - 根据经验 - 在方法签名上使用最通用的 type (class 或接口无关紧要)支持 API 我需要:更通用的类型,更简洁的API。
所以,如果我需要一个参数代表相同类型对象的 family,我开始使用 Collection
;如果在特定场景中 ordering 的想法很重要,我使用 List
,避免发布任何关于我在内部使用的特定 List
实现的信息:我的想法是保持改变实现的能力(可能是为了性能优化,支持不同的数据结构,等等)而不中断客户端。
如您所说,发布信息如我使用TreSet
可以为客户端优化留出空间-但我的想法是它取决于:您可以根据具体情况评估是否需要放宽通用规则以公开您可以.
所以,回答你的问题:
- 是的,在
NumberGetter
接口的IntegerGetter
实现中 return 更窄的类型是合适的:Java 允许你做并且你没有破坏我的越通用越漂亮规则:NumberGetter
接口暴露了使用Number
作为return类型的更通用的接口,但在具体实现中我们可以使用更窄的 return 类型来指导方法实现:引用更抽象接口的客户端不受此选择的影响,引用特定 subclass 的客户端可以尝试使用更具体的接口[=53] =] - 与上一点相同,但我认为它对客户的用处不如前一个案例:客户可能会发现参考
Integer
比参考Number
更有用(如果我明确使用NumberGetter
,可能我认为是Integer
s,而不是Number
s),但指的是TreeSet
而不是 [=24] =] 仅当您需要子 class 而不是接口公开的 API 时才有用... - 见初始论文
这是一个准哲学问题-我的回答也是如此:希望对您有用!
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 类型所做的工作称为 类型专业化 。只要您继续将 接口 暴露给外部世界就可以了。
我的经验法则是在处理外部世界时尽可能通用,在实现我的应用程序的关键(核心)部分时尽可能具体,以限制可能的用例,保护自己免受滥用我刚刚编码的内容,并用于文档目的。
你绝对不应该做的是使用instanceof
或class
比较来发现实际类型并采取不同的路线。那会破坏代码库。