立即向下转换的好处是什么?

6

最近我一直在查看很多代码(为了我的利益,因为我还在学习编程),我注意到一些Java项目(来自那些看起来很受尊敬的程序员)使用某种即时向下转换

实际上,我有多个例子,但这是我从代码中直接提取出来的一个例子:

public Set<Coordinates> neighboringCoordinates() {
    HashSet<Coordinates> neighbors = new HashSet<Coordinates>();
    neighbors.add(getNorthWest());
    neighbors.add(getNorth());
    neighbors.add(getNorthEast());
    neighbors.add(getWest());
    neighbors.add(getEast());
    neighbors.add(getSouthEast());
    neighbors.add(getSouth());
    neighbors.add(getSouthWest());
    return neighbors;
}

同一个项目中,这里有另一个(也许更简洁)的例子:

private Set<Coordinates> liveCellCoordinates = new HashSet<Coordinates>();

在第一个示例中,您可以看到该方法的返回类型为Set<Coordinates> - 然而,该特定方法将始终只返回HashSet,而不是任何其他类型的Set
在第二个示例中,liveCellCoordinates最初被定义为Set<Coordinates>,但是立即转换为HashSet
而且这不仅仅是这个单独的项目——我发现在多个项目中都是如此。
我很好奇背后的逻辑是什么?是否存在某些代码约定认为这是良好的实践?它是否使程序更快或更高效?它有哪些好处?

3
这些实际上是向上转型的例子(从特定类型到更广泛类型的转换)。例如,请参阅维基百科有关下行转换的条目 - Ted Hopp
@TedHopp,看看我提供的第二个例子,在那个例子中,我从一个Set开始,然后转换为HashSet(我认为HashSetSet更具体)。这不是向下转型吗?因为我从广泛类型的Set转换为特定类型的HashSet - Bob
1
在第二个例子中,您正在创建一个 HashSet 并在将其分配给 liveCellCoordinates 时将其转换为 Set。在评估 new 表达式之前不存在任何 Set 对象,并且没有将 Set 转换为 HashSet - Ted Hopp
@TedHopp 好的,感谢澄清。我之前对于“起点”是从哪开始有些困惑,我以为它是从Set开始的,没想到实际上是相反的。 - Bob
8个回答

13

在设计方法签名时,通常最好只钉住必须钉住的东西。在第一个示例中,仅指定方法返回一个Set(而不是特别指定为HashSet),实现者可以自由更改其实现方式,如果发现HashSet不是正确的数据结构。如果该方法已声明为返回HashSet,那么所有依赖于对象特别是HashSet而不是更一般的Set类型的代码也需要进行修改。

一个现实的例子可能是决定neighboringCoordinates()需要返回一个线程安全的Set对象。按照所写的,这将非常简单-只需将该方法的最后一行替换为:

return Collections.synchronizedSet(neighbors);

结果表明,通过synchronizedSet()方法返回的Set对象与HashSet不兼容。幸好该方法被声明为返回一个Set对象!

第二种情况也是类似的。使用liveCellCoordinates的类的代码不需要知道它是什么除了一个Set对象。(事实上,在第一个例子中,我本来希望看到的是:

Set<Coordinates> neighbors = new HashSet<Coordinates>();

在方法的顶部。


2
+1:这是关于在选择时要有意识,以便明确您的最低要求。使用暗示实际上不需要的东西的类型更难理解和维护。 - Peter Lawrey

4

现在如果将来更改类型,任何依赖于neighboringCoordinates的代码都不必更新。

假设你有:

HashedSet<Coordinates> c = neighboringCoordinates()

现在,假设他们更改了代码以使用不同的set实现。你需要修改你的代码。

但是,如果你有:

Set<Coordinates> c = neighboringCoordinates()

只要它们的集合仍然实现了set,它们可以在不影响您的代码的情况下随意更改内部内容。
基本上,为了隐藏内部细节,只需要尽可能地不具体化(合理范围内)。您的代码只关心能够将集合作为set访问。如果这有意义的话,它并不关心具体类型的set是什么。因此,为什么要使您的代码与HashedSet耦合呢?

2
这里运用的设计原则是“优先使用抽象类型”。 Set 是抽象的;它没有具体的类 - 它是一个接口,按照定义,是抽象的。该方法的契约是返回一个 Set - 开发人员需要选择返回什么样的 Set
你应该在字段中也这样做,例如:
private List<String> names = new ArrayList<String>;

不是

private ArrayList<String> names = new ArrayList<String>;

稍后,您可能希望改用LinkedList——指定抽象类型允许您在没有代码更改的情况下执行此操作(当然,除了初始化之外)。


2
在第一个例子中,该方法将始终仅返回一个 HashSet 是一个用户不必知道的实现细节。这使开发人员在需要时可以使用不同的实现方式。

2
问题在于你希望如何使用变量。例如,在你的上下文中,它是重要的,它是一个HashSet吗?如果不是,你应该说出你需要什么,这只是一个Set
如果你使用TreeSet,情况就不同了。然后,你将失去Set已排序的信息,如果你的算法依赖于这个属性,那么改变实现为HashSet将是一场灾难。在这种情况下,最好的解决方案是编写SortedSet<Coordinates> set = new TreeSet<Coordinates>();。或者想象一下,你会写List<String> list = new LinkedList<String>();:如果你只想使用list作为列表,那么这很好,但你将无法再将LinkedList用作双端队列,因为像offerFirstpeekLast这样的方法不在List接口上。
因此,一般规则是:尽可能通用,但尽可能具体。问问自己你真正需要什么。某个接口是否提供了你需要的所有功能和承诺?如果是,那么使用它。否则,更具体地使用另一个接口或类本身作为类型。

2
这里还有一个原因。这是因为更一般(抽象)的类型具有更少的行为,这很好,因为出错的可能性较小。
例如,假设您实现了以下方法:List<User> users = getUsers();,而实际上您可以使用更抽象的类型:Collection<User> users = getUsers();。现在,Bob可能错误地认为您的方法按字母顺序返回用户并创建了一个错误。如果您使用了Collection,就不会有这样的混淆。

1

这很简单。

在你的例子中,该方法返回Set。从 API 设计者的角度来看,与返回 HashSet 相比,这具有一个显著的优点。

如果程序员在某个时候决定使用 SuperPerformantSetForDirections,则可以在不更改公共 API 的情况下执行此操作,前提是新类扩展了 Set


1

诀窍在于"面向接口编程"

之所以这样做的原因是,在99.9%的情况下,你只需要HashSet/TreeSet/WhateverSet遵循的Set接口所定义的行为。这样可以使你的代码更简单,实际上你需要指定HashSet的原因是为了说明你需要的Set的行为。

你可能知道,HashSet相对较快,但返回的项看起来是随机排序的。TreeSet稍慢一些,但按字母顺序返回项。但无论如何,你的代码都不关心,只要它像一个Set一样行为就可以。

这样可以使代码更简单,更易于操作。

请注意,Set的典型选择是HashSet,Map的选择是HashMap,List的选择是ArrayList。如果使用非典型(对你来说)的实现,应该有一个充分的理由(比如需要按字母顺序排序),并且将这个理由放在new语句旁边的注释中。这样可以使未来的维护者更容易理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接