“可怕的记忆”论点是完全错误的,但这是客观上的 “坏习惯”。从类继承时,您不仅继承了您感兴趣的字段和方法,还继承了一切。它声明的每个方法,即使对您没有用。最重要的是,您还继承了该类提供的所有合同和担保。
首字母缩写词SOLID为良好的面向对象设计提供了一些启发。在这里,我覆盖整个院落隔离原则(ISP)和大号 iskov换人Pricinple(LSP)有话要说。
ISP告诉我们保持接口尽可能小。但是,通过从继承ArrayList
,您可以获得许多方法。它是有意义的get()
,remove()
,set()
(替换),或add()
(插入)子节点在一个特定的指数?ensureCapacity()
对基础列表明智吗?对sort()
节点意味着什么?您班上的用户真的应该获得subList()
吗?由于您无法隐藏不需要的方法,因此唯一的解决方案是将ArrayList作为成员变量,并转发您实际想要的所有方法:
private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
如果您真的想要您在文档中看到的所有方法,我们可以继续使用LSP。LSP告诉我们,必须在期望父类的任何地方使用子类。如果一个函数接受一个ArrayList
as参数,而我们却传递了它Node
,那么什么都不会改变。
子类的兼容性始于诸如类型签名之类的简单事物。覆盖方法时,不能使参数类型更严格,因为这可能会排除对父类合法的使用。但这就是编译器使用Java检查我们的内容。
但是LSP的运行更加深入:我们必须与所有父类和接口的文档所承诺的所有内容保持兼容性。在他们的答案中,Lynn发现了一种这样的情况,其中List
接口(您已通过继承ArrayList
)保证了equals()
and hashCode()
方法应该如何工作。因为hashCode()
甚至为您提供了必须精确实现的特定算法。假设您已经写过Node
:
public class Node extends ArrayList<Node> {
public final int value;
public Node(int value, Node... children) {
this.value = Value;
for (Node child : children)
add(child);
}
...
}
这就要求value
不能对做出贡献hashCode()
,也不能影响equals()
。List
您必须通过继承来兑现您所承诺要尊重的接口new Node(0).equals(new Node(123))
。
因为从类继承很容易意外地打破了父类做出的承诺,并且由于它通常公开比预期更多的方法,所以通常建议您首选组合而不是继承。如果必须继承某些内容,建议仅继承接口。如果要重用特定类的行为,则可以将其作为一个单独的对象保留在实例变量中,这样就不必强加它的所有诺言和要求。
有时,我们的自然语言暗示了一种继承关系:汽车就是车辆。摩托车是车辆。我应该定义类Car
并Motorcycle
从Vehicle
类继承吗?面向对象的设计并不是要完全在我们的代码中反映现实世界。我们无法轻松地在源代码中对现实世界的丰富分类法进行编码。
一个这样的例子就是员工-老板的建模问题。我们有多个Person
,每个都有一个名称和地址。An Employee
是一个,Person
并且有个Boss
。一个Boss
也是Person
。因此,我应该创建一个Person
由Boss
和继承的类Employee
吗?现在我有一个问题:老板既是雇员又有另一个上司。因此似乎Boss
应该扩展Employee
。但是,CompanyOwner
是Boss
但不是Employee
?任何种类的继承图都会在某种程度上分解。
OOP不是关于层次结构,继承和对现有类的重用,而是关于行为的概括。OOP的主题是“我有很多对象,希望它们做特定的事情-我不在乎如何做。”这就是接口的作用。如果因为要使其可迭代而Iterable
为您实现该接口,Node
那将很好。如果Collection
由于要添加/删除子节点等来实现接口,那么就可以了。但是从另一个类继承,是因为它碰巧会给您带来所有不该拥有的东西,或者至少不会给您带来所有的一切,除非您如上所述进行了仔细的考虑。