我最近一直在寻找很多代码(为了我自己的利益,因为我还在学习编程),并且我注意到了许多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
。
这不仅仅是这个单一的特定项目 - 我发现在多个项目中就是这种情况。
我很好奇这背后的逻辑是什么?是否有一些代码约定会考虑这种良好做法?它能以某种方式使程序更快或更高效吗?它有什么好处?
答案 0 :(得分:12)
当您设计方法签名时,通常最好只确定需要固定的内容。在第一个示例中,通过仅指定方法返回Set
(而不是具体的HashSet
),如果事实证明HashSet
是HashSet
,则实施者可以自由地更改实现不是正确的数据结构。如果声明该方法返回HashSet
,那么依赖于该对象的所有代码特别是Set
而不是更通用的neighboringCoordinates()
类型也需要修改。< / p>
一个现实的例子是,如果确定Set
需要返回一个线程安全的return Collections.synchronizedSet(neighbors);
对象。如上所述,这将非常简单 - 用以下方法替换方法的最后一行:
Set
事实证明,synchronizedSet()
返回的HashSet
对象与Set
不是分配兼容的。声明该方法返回liveCellCoordinates
的好方法!
类似的考虑适用于第二种情况。使用Set
的类中的代码不应该只知道它是Set<Coordinates> neighbors = new HashSet<Coordinates>();
。 (事实上,在第一个例子中,我原本希望看到:
{{1}}
位于方法的顶部。)
答案 1 :(得分:4)
因为现在如果他们将来更改类型,则不必更新任何依赖于neighborCoordinates的代码。
让你拥有:
HashedSet<Coordinates> c = neighboringCoordinates()
现在,让我们说他们改变他们的代码以使用不同的set实现。猜猜看,你必须改变你的代码。
但是,如果你有:
Set<Coordinates> c = neighboringCoordinates()
只要他们的集合仍然实现了set,他们就可以在不影响代码的情况下在内部更改他们想要的内容。
基本上,为了隐藏内部细节,它只是最不具体的(在合理范围内)。您的代码只关心它可以作为一个集合访问集合。它不关心它是什么特定类型的集合,如果这是有道理的。那么,为什么要将代码与HashedSet耦合?
答案 2 :(得分:2)
在第一个示例中,该方法将始终仅返回HashSet,这是该类用户不必知道的实现细节。如果需要,这可以使开发人员使用不同的实现。
答案 3 :(得分:2)
这里的设计原则是“总是喜欢指定抽象类型”。
Set
是抽象的;没有这样的具体类Set
- 它是一个接口,根据定义是抽象的。该方法的合同将返回Set
- 由开发人员选择要返回的{em>种 <{1}}。
您也应该使用字段来执行此操作,例如:
Set
不是
private List<String> names = new ArrayList<String>;
稍后,您可能希望更改为使用private ArrayList<String> names = new ArrayList<String>;
- 指定抽象类型允许您在没有代码更改的情况下执行此操作(当然,初始化除外)。
答案 4 :(得分:2)
问题是你想如何使用变量。例如在您的上下文中重要它是HashSet
吗?如果没有,你应该说出你需要的东西,这只是Set
。
如果您使用例如,情况会有所不同TreeSet
在这里。然后您将丢失Set
已排序的信息,如果您的算法依赖于此属性,则将实现更改为HashSet
将是一场灾难。在这种情况下,最好的解决方案是编写SortedSet<Coordinates> set = new TreeSet<Coordinates>();
。或者想象你会写List<String> list = new LinkedList<String>();
:如果你想使用list
作为列表,那就没关系,但你不能再使用LinkedList
作为deque,就像offerFirst
或peekLast
不在List
界面上。
所以一般规则是:尽可能一般,但具体要求。问问自己你真正需要什么。某个界面是否提供您需要的所有功能和承诺?如果是,那就使用它。否则更具体,使用另一个接口或类本身作为类型。
答案 5 :(得分:2)
这是另一个原因。这是因为更一般的(抽象)类型具有更少的行为,这是好的,因为没有更多的空间来搞乱。
例如,假设您实现了这样的方法:List<User> users = getUsers();
实际上您可以使用更像这样的抽象类型:Collection<User> users = getUsers();
。现在Bob可能错误地假设您的方法按字母顺序返回用户并创建错误。如果你使用Collection
,就不会有这样的混乱。
答案 6 :(得分:1)
这很简单。
在您的示例中,该方法返回Set
。从API设计者的角度来看,与返回HashSet
相比,这有一个显着的优势。
如果在某个时刻,程序员决定使用SuperPerformantSetForDirections
,那么如果新类扩展Set
,他可以在不更改公共API的情况下执行此操作。
答案 7 :(得分:1)
技巧是“接口的代码”。
原因在于,在99.9%的情况下,您只希望HashSet/TreeSet/WhateverSet
的行为符合所有人实现的Set
接口。它使您的代码更简单,您实际需要说HashSet
的唯一原因是指定您需要的行为。
您可能知道HashSet相对较快,但以看似随机的顺序返回项目。 TreeSet有点慢,但按字母顺序返回项目。您的代码无关紧要,只要它的行为类似于Set。
这样可以提供更简单的代码,更易于使用。
请注意,Set的典型选择是HashSet,Map是HashMap,List是ArrayList。如果你使用非典型(for you)实现,那么应该有一个很好的理由(比如,需要按字母顺序排序),并且应该将这个原因放在new
语句旁边的注释中。使未来的维护者的生活更轻松。