Java构造函数继承

211

我想知道为什么在Java中构造函数不能被继承?你知道当你有这样一个类:

public class Super {

  public Super(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC){
    this.serviceA = serviceA;
    //etc
  } 

}

稍后当你从Super继承时,Java会报告没有定义默认构造函数。解决方案显然是这样的:

public class Son extends Super{

  public Son(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC){
    super(serviceA,serviceB,serviceC);
  }

}

这段代码重复冗余,不符合DRY原则,而且(在我看来)没有用处... 这就引出了一个问题:

为什么Java不支持构造函数继承?禁止此继承是否有任何好处?


3
我同意Son类中的构造函数显得有些重复。正因为如此,C++现在允许派生类继承基类的构造函数(请参见http://www2.research.att.com/~bs/C++0xFAQ.html#inheriting)。请注意,我强调“允许”,因为派生类必须明确声明它使用了基类的构造函数。 - Derek Mahar
1
像C++一样,Java也会受益于允许构造函数继承的语法。 - Derek Mahar
如果在超类Super中有一个private final int foo;,则无法在继承的构造函数Son中为foo赋值,因为它在Super中是私有的。 - Grim
10个回答

227

假设构造函数被继承...那么因为每个类最终都是从Object派生而来,每个类最终都会拥有一个无参构造函数。这是一个坏主意。你期望的究竟是什么:

FileInputStream stream = new FileInputStream();

有什么需要做的吗?

现在可能有一种简单创建“传递”构造函数的方法,这在相当常见,但我不认为它应该是默认设置。构造子类所需的参数通常与超类所需的参数不同。


41
为什么不像C++一样允许派生类可选择地继承其基类构造函数(请参见www2.research.att.com/~bs/C++0xFAQ.html#inheriting)? - Derek Mahar
4
一种有用的构造函数语法可以是允许派生构造函数继承基础构造函数的参数,并自动将它们转发到基础构造函数,这样派生构造函数就不需要重复这些参数。 - Derek Mahar
6
如果您不需要无参构造函数,为什么不将其声明为私有?这样做有什么阻止你的方式呢? - Mononofu
7
@Mononofu说:创建一个构造函数来实际上创建一个无法使用且永远不会被调用的对象,只是为了“移除”根本不需要提供的东西肯定是很烦人的。呃。我宁愿这种语言保持现在的工作方式 :) - Jon Skeet
2
@Mononofu 但是等等...要正确自信地使用它,您必须理解Java中构造函数和可见性的奇怪继承系统。世界已经够混乱了,不是吗?;) - Jan
显示剩余22条评论

34

当您从Super继承时,实际上发生的情况是:

public class Son extends Super{

  // If you dont declare a constructor of any type, adefault one will appear.
  public Son(){
    // If you dont call any other constructor in the first line a call to super() will be placed instead.
    super();
  }

}
所以,这就是原因,因为你必须调用你的唯一构造函数,因为"Super"没有默认构造函数。 现在,试图猜测为什么Java不支持构造函数继承,可能是因为一个构造函数只有在谈论具体实例时才有意义,在不知道它如何定义(通过多态性)时,不能创建某个东西的实例。

12
由于构建子类对象的方式可能与超类不同,因此您可能不希望子类的客户端能够调用超类中可用的某些构造函数。一个傻瓜式的例子:
class Super {
    protected final Number value;
    public Super(Number value){
        this.value = value;
    }
}

class Sub {
    public Sub(){ super(Integer.valueOf(0)); }
    void doSomeStuff(){
        // We know this.value is an Integer, so it's safe to cast.
        doSomethingWithAnInteger((Integer)this.value);
    }
}

// Client code:
Sub s = new Sub(Long.valueOf(666L)): // Devilish invocation of Super constructor!
s.doSomeStuff(); // throws ClassCastException

甚至更简单:
class Super {
    private final String msg;
    Super(String msg){
        if (msg == null) throw new NullPointerException();
        this.msg = msg;
    }
}
class Sub {
    private final String detail;
    Sub(String msg, String detail){
        super(msg);
        if (detail == null) throw new NullPointerException();
        this.detail = detail;
    }
    void print(){
        // detail is never null, so this method won't fail
        System.out.println(detail.concat(": ").concat(msg));
    }
}
// Client code:
Sub s = new Sub("message"); // Calling Super constructor - detail is never initialized!
s.print(); // throws NullPointerException

从这个例子可以看出,你需要一种声明“我想继承这些构造函数”或“我想继承除了这些之外的所有构造函数”的方法,然后你还需要指定默认的构造函数继承偏好,以防有人在超类中添加新的构造函数... 或者你可以要求重复从超类中复制构造函数,如果你想要“继承”它们,这可能是更明显的做法。


1
你的例子并没有说服我。仅仅因为你故意编写有错误的代码,并不能成为反对构造函数继承的有效论据。如果你搞砸了(有时甚至即使你没有),NullPointerExceptions是完全正常的。 - The incredible Jan
@TheincredibleJan,重要的一点是构造函数用于强制实现类不变量。隐式构造函数继承将使得为类的任何实例提供任何保证变得更加复杂(甚至是不可能的,如果您考虑到构造函数可能会被添加到超类中)。 - gustafc
1
这不是一个好的例子。你的问题在于你将非整数数字转换为整数。这与继承无关。 - LowKeyEnergy
@LowKeyEnergy 不,问题在于能够绕过构造函数将使您失去强制执行类不变量的唯一方法。但是,我承认我的示例可能更简洁 - 接受答案中提出的观点更为直接; 即所有内容都将具有默认构造函数。 - gustafc

6
由于构造函数是实现细节,用户无法调用接口/超类的构造函数。在用户获得实例时,它已经被构建;反之,在构建对象时,没有任何变量被定义为当前分配给它。
考虑一下强制所有子类具有继承构造函数所意味着的含义。我认为直接传递变量比让类“神奇地”具有特定数量参数的构造函数更加清晰。

2

David的回答是正确的。我想补充一下,你可能正在收到上帝的信号,表明你的设计混乱了,"Son"不应该是"Super"的子类,而是"Super"具有一些最好通过拥有儿子所提供的功能的实现细节,就像一种策略。

编辑:Jon Skeet的回答最棒了。


Super实际上是我的服务层的LayerSupertype(http://martinfowler.com/eaaCatalog/layerSupertype.html)。我需要在构造函数中注入依赖项以支持DI。我的设计并没有混乱。 - Pablo Fernandez
这是一个观点问题!在我看来,如果你的层次结构被扭曲以满足你所谓的框架的要求,那本身就可能是一种来自上方的信号。 - Jonathan Feinberg
请解释一下为什么您说我的层次结构是“扭曲”的。事实上,我从未提到过任何流行框架,我只是使用Guice +普通的servlets。 - Pablo Fernandez
我不知道你的设计是否扭曲,也不知道它是否设计得很糟糕。我只是想说,如果你的子类仅仅通过提供一些策略上的差异与其超类不同,那么它可能更好地表达为超类所拥有的东西,而不是超类本身。然后我说,如果是这种情况,但你仍然必须将软件设计为父子关系以支持某些框架的需求(无论是 DI 还是其他),那就是“扭曲”!但我不知道你的软件是如何设计的。 - Jonathan Feinberg

2

构造函数不具有多态性。
在处理已经构造好的类时,你可能正在处理对象的声明类型,也可能是它的任何子类。这就是继承的用处。
构造函数总是在特定类型上调用,例如new String()。假设的子类在这里没有作用。


OP可能想问的是:“既然编译器完全能够为我生成super调用,为什么我还要自己进行super调用呢?” - gustafc
例如,当超类具有默认构造函数且子类未指定任何构造函数时,它就会执行此操作。 - gustafc

0

从本质上讲,您确实继承了构造函数,因为只要适当时就可以简单地调用super,只是如果默认情况下发生这种情况,由于其他人提到的原因,它可能会出现错误。编译器无法假定何时适当何时不适当。

编译器的工作是尽可能提供灵活性,同时减少复杂性和意外副作用的风险。


0

因为(超)类必须完全控制其构造方式。如果程序员决定不提供默认(无参数)构造函数作为类合同的一部分,则编译器不应提供。


-1

我不知道有哪种语言可以让子类继承构造函数(但是,我并不是一个多语言编程专家)。

这里有一个关于C#同样问题的讨论。一般的共识似乎是这会使语言变得复杂,引入对基类更改的潜在副作用,并且在良好设计中通常不应该需要。


1
C++现在允许派生类继承基类构造函数(详见www2.research.att.com/~bs/C++0xFAQ.html#inheriting)。这避免了在派生类中发现的常见习语,即派生构造函数几乎只是声明与基本构造函数相同的参数,并将其转发到基本构造函数。 - Derek Mahar
Python的功能包括:https://dev59.com/XWw15IYBdhLWcg3wkMjz#6536027。Python还将“构造函数”阶段分为`__new__()`和`__init__()`方法,并且对多态方法有非常不同的处理方式。请参见https://pythonconquerstheuniverse.wordpress.com/2010/03/17/multiple-constructors-in-a-python-class/。 - Sarah Messer
任何非类型化语言都允许这样做。 - Alvaro

-2
一个派生类并不是其基类相同的类,您可能会或可能不会关心在派生类构造时是否初始化了基类的任何成员。这是由程序员而不是编译器决定的。

你没有任何选择。基类成员在派生类构造函数代码运行时已经被初始化。很难看出这与问题有什么关系。 - user207421

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