在super构造函数运行之前初始化字段?

44
在Java中,是否有任何方法可以在父类构造函数运行之前初始化字段?
即使是我能想到的最丑陋的hack也会被编译器拒绝:
class Base
{
    Base(String someParameter)
    {
        System.out.println(this);
    }
}

class Derived extends Base
{
    private final int a;

    Derived(String someParameter)
    {
        super(hack(someParameter, a = getValueFromDataBase()));
    }

    private static String hack(String returnValue, int ignored)
    {
        return returnValue;
    }

    public String toString()
    {
        return "a has value " + a;
    }
}

注意:当我从继承转为委托时,问题消失了,但我仍然想知道原因。

1
你是在尝试预初始化字段 a 吗? - Woot4Moo
1
我认为你做不到这一点。在类中进行的任何初始化(即使是在构造函数之外)都会移动到super调用后的每个构造函数中。因此,在字段初始化之前,始终会先运行超级构造函数。 - Rohit Jain
6
@FredOverflow,由于a只能在Derived中访问,那么在调用super()之前初始化它有什么影响呢?在你提供的示例中,紧随其后立即初始化并没有什么区别(除非你从基类构造函数调用一个已被覆盖的方法,这开始变得可疑)。 - assylias
8
《Effective Java》第17条款:“构造器不能直接或间接调用可被覆盖的方法(…)如果覆盖方法依赖于子类构造器执行的任何初始化操作,那么该方法将无法按预期工作。” - assylias
1
丑陋的黑客技巧:可以直接在Java字节码中创建派生类,就像这个答案中所示:https://dev59.com/fU_Sa4cB1Zd3GeqP9ylq#3280665 - Jörn Horstmann
显示剩余6条评论
7个回答

33
不,没有办法做到这一点。
根据语言规范,实例变量甚至没有被初始化,直到调用了super()
以下是从链接中提取的类实例创建过程中构造函数步骤:
为这个构造函数的参数分配新创建的参数变量。
如果这个构造函数以另一个在同一类中的构造函数的显式构造函数调用(使用this)开始,则评估参数并递归地使用这些相同的五个步骤处理该构造函数调用。如果该构造函数调用异常完成,则出于同样的原因,此过程也异常完成;否则,继续执行第5步。
该构造函数不以另一个在同一类中的构造函数的显式构造函数调用(使用this)开始。如果此构造函数是其他类的构造函数,则该构造函数将以超类构造函数的显式或隐式调用(使用super)开始。使用这些相同的五个步骤递归地评估参数并处理超类构造函数调用。如果该构造函数调用异常完成,则出于同样的原因,此过程也异常完成。否则,继续执行第4步。
执行此类的实例初始化器和实例变量初始化器,将实例变量初始化器的值分配给相应的实例变量,按照它们在类的源代码中文本上出现的从左到右的顺序。如果执行任何这些初始化器导致异常,则不会处理更多的初始化器,并且此过程会使用相同的异常异常完成。否则,继续执行第5步。
执行此构造函数的其余部分。如果该执行异常完成,则出于同样的原因,此过程也异常完成。否则,此过程正常完成。

@mkilic:我很确定 - Kindred

16

超级构造函数将在任何情况下运行,但由于我们正在讨论“最丑陋的黑客”,因此我们可以利用这一点。

public class Base {
    public Base() {
        init();
    }

    public Base(String s) {
    }

    public void init() {
    //this is the ugly part that will be overriden
    }
}

class Derived extends Base{

    @Override
    public void init(){
        a = getValueFromDataBase();
    }
} 

我从不建议使用这些类型的黑客手段。


1
+1 - 你说得对。额,我一开始想的是C ++。但是,我认为这种hack并不必要 - 请看我的回答。 - Andy Thomas
这并不是在超级构造函数运行之前,正如 OP 所问的那样。但这可能是你能得到的最接近的答案了。 - Keppil
@Keppil,我在答案中提到“超级构造函数将在任何情况下运行”,这只是在派生类中初始化变量的一种方法。 - Serkan Arıkuşu

9
我有一个方法可以做到这一点。
class Derived extends Base
{
    private final int a;

    // make this method private
    private Derived(String someParameter,
                    int tmpVar /*add an addtional parameter*/) {
        // use it as a temprorary variable
        super(hack(someParameter, tmpVar = getValueFromDataBase()));
        // assign it to field a
        a = tmpVar;
    }

    // show user a clean constructor
    Derived(String someParameter)
    {   
        this(someParameter, 0)
    }

    ...
}

8

正如其他人所说,你在调用超类构造函数之前不能初始化实例字段。

但是有一些解决方法。其中一个方法是创建一个工厂类来获取值并将其传递给派生类的构造函数。

class DerivedFactory {
    Derived makeDerived( String someParameter ) {
        int a = getValueFromDataBase();
        return new Derived( someParameter, a );
    }
}


class Derived extends Base
{
    private final int a;

    Derived(String someParameter, int a0 ) {
        super(hack(someParameter, a0));
        a = a0;
    }
    ...
}

这是我认为最好的解决方案。唯一需要改变的是我会在“派生”类中放置一个静态工厂方法。 - Lazar Petrovic

1

这是 Java 语言规范(第8.8.7节) 禁止的:

构造函数体的第一条语句 可以是同一类或直接超类的另一个构造函数的显式调用。

构造函数体应该像这样:

ConstructorBody:

{ ExplicitConstructorInvocationopt BlockStatementsopt }

0

我设计了一个UI表单的层次结构,遇到了类似的问题。我在这里找到了一个类似的案例和一些解决方法:https://www.javaspecialists.eu/archive/Issue086-Initialising-Fields-Before-Superconstructor-Call.html

我稍微改变了他们第一个解决方法的实现方式,使其更加通用(通过接受可变数量的参数):

public abstract class BaseClass {
    int someVariable; // not relevant for the answer
    
    public BaseClass (int someVariable, Object... arguments) {
        this.someVariable = someVariable;
        
        // Must be called before using local variables of derived class
        initializeLocalVariablesOfDerivedClass(arguments);
        
        useLocalVariablesOfDerivedClass();
    }
    
    protected abstract void initializeLocalVariablesOfDerivedClass (Object... arguments); 
    
    protected abstract void useLocalVariablesOfDerivedClass();
        
}

public class DerivedClass extends BaseClass {
    protected String param1;
    protected Integer param2;
    
    public DerivedClass(int someVariable, Object... arguments) {
        super(someVariable, arguments);
    }

    @Override
    protected void initializeLocalVariablesOfDerivedClass(Object... arguments) {
        // you must know the order and type of your local fields
        param1 = (String) arguments[0];
        param2 = (Integer) arguments[1];        
    }
    
    @Override
    protected void useLocalVariablesOfDerivedClass() {
        System.out.println("param1: " + param1);
        System.out.println("param2: " + param2);                
    }
    
    public static void main(String[] args) {        
        new DerivedClass(1, new String("cucu"), new Integer("4"));
    }
}

0

虽然直接做不可能,但你可以尝试使用嵌套对象来实现。

以你的例子为例:

open class Base {
    constructor() {
        Timber.e(this.toString())
    }
}

class Derived {
    val a = "new value"

    val derivedObject : Base =  object : Base() {
        override fun toString(): String {
             return "a has value " + a;
        }
    }
}

编程愉快 - 这是一个hack但有效的方法 :) 记得将derivedObject定义为LAST变量


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