Java继承与初始化对比

11

我正在阅读J. Bloch的Effective Java,现在我正在学习继承与组合的区别。据我理解,他认为继承并不总是好的。

子类易受破坏的相关原因是它们的超类可能会在后续版本中获取新方法。假设一个程序依赖于所有插入到某个集合中的元素都满足某个谓词来保证其安全性。可以通过对集合进行子类化并覆盖每个能够添加元素的方法,以确保在添加元素之前满足谓词来保证此限制。这样做很好用,直到在后续版本中向超类添加了一种能够插入元素的新方法

但是,为什么这行不通呢?超类只是Collection接口,如果我们添加了一个新方法,只会在编译时出现错误,这并没有任何危害...


1
为超类添加一个新的方法不会导致编译时错误。事实上,向接口添加"default"方法也不会产生编译时错误。 - Peter Lawrey
如果你在谈论 java.util.Collection,那不是一个超类。那只是一个接口。 - Alderath
如果重新编译子类,则向超类或接口添加抽象方法会导致编译时错误,但这可能不会发生。 - Peter Lawrey
1
这被称为脆弱基类问题 - Seelenvirtuose
4个回答

19

假设你在某个v1.0版本的库中有一个Collection超类:

public class MyCollection {
    public void add(String s) {
        // add to inner array
    }
}

您可以创建一个子类来仅接受长度为5的字符串:

public class LimitedLengthCollection extends MyCollection {
    @Override
    public void add(String s) {
        if (s.length() == 5) {
            super.add(s);
        }
    }
}

这个类的不变量是它永远不会包含长度不为5的字符串。

现在版本2.0的库已经发布,你开始使用它。基类被修改为:

public class MyCollection {
    public void add(String s) {
        // add to inner array
    }

    public void addMany(String[] s) {
        // iterate on each element and add it to inner array
    }
}

并且你的子类不需要修改。现在,你的子类的用户可以

LimitedLengthCollection c = new LimitedLengthCollection();
c.addMany(new String[] {"a", "b", "c"});

你的子类合同就这样被打破了。它原本只应该接受长度为5的字符串,但现在不再如此,因为超类中添加了一个额外的方法。


虽然这是问题的核心(脆弱的基类),但仅在 addMany 方法不使用 add 方法而是自行实现添加行为时才会出现。也许你应该给所有这些方法添加一些实现细节。 - Seelenvirtuose
2
正确。这将引起另一个问题。2.0版本可以委派给add(),您可以依靠它在add()中进行检查,然而3.0版本不再委派给add(),因此您的检查将被绕过。 - JB Nizet
所以,这里的重点是MyCollection是一个具体类。明白了。但在Java 8中,实现库接口仍然安全吗?它们可以为某些方法提供默认实现,如果我们在版本1.0中实现其中一个方法,并且在版本2.0中添加了默认实现的某个方法,我们也可能会遇到麻烦(正如您指出的破坏不变式)。 - St.Antario
即使在Java 8中使用默认实现? - St.Antario
这正是为什么引入默认方法的原因:它们保持与现有实现的兼容性,因为默认方法只能委托给接口的其他方法。通过添加返回其他内容或不执行与现有实现方法相同操作的默认方法,它们可能会破坏实现,但这种情况较少发生。 - JB Nizet
显示剩余3条评论

3
问题并不是继承不能工作。
问题在于使用继承时,开发人员无法强制执行某些行为(例如满足某些谓词的集合示例)。
当我们创建一个新类时,很少有真正的专门类型是另一个类型。更常见的情况是创建一个使用其他类的新东西。
因此,我们很少需要继承,更经常需要创建一个使用其他类来完成某些任务的类。
“IS A”与“HAS A”
你必须问自己:
类B是类A的新子类型,以不同的方式执行与A相同的操作吗?
还是
类B具有内部类来执行与A意图不同的操作?
要知道,更经常的答案是后者。

2

通常情况下,这会破坏已实现Collection类的客户端代码。

在这个特定的例子中,安全性将被破坏,因为恶意用户可以使用尚未被覆盖的方法来插入项目,而这个方法是在您发布代码之后添加的。

基于不受您控制的继承类编写代码可能会在将来捉住你。


在这种情况下,这将是一件好事;如果客户端代码出现错误(无法编译),那么在运行之前就需要修复它,这意味着安全威胁被避免了。 - Sam Estep
你能想象Oracle在以后的某个时候更改Collection类/接口吗?这会带来什么问题? - idipous
我并不反对改变公共接口的做法可能会带来不良影响;但我只是想指出问题是关于安全方面的考虑,因此代码出现错误似乎比带来安全问题更可取。 - Sam Estep
如果您已经发布了您的代码,那么您就存在安全问题。 - idipous

2

如果我们添加一个新方法,我们只会得到编译时错误。

只有在超类/接口中添加了一个抽象方法时才是真的。如果添加了非抽象方法,则不覆盖该新方法是完全有效的。


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