在Java的构造函数中,如果要调用另一个构造函数(或超级构造函数),它必须是构造函数中的第一行.我假设这是因为在其他构造函数运行之前不应该允许修改任何实例变量.但是为什么你不能在构造函数委托之前有语句,以便计算其他函数的复杂值?我想不出任何好的理由,而且我遇到了一些真实的案例,我写了一些丑陋的代码来解决这个限制.
所以我只是想知道:
有这个限制的充分理由吗?
是否有任何计划在未来的Java版本中允许这样做?(或者Sun明确表示这不会发生吗?)
举一个我正在谈论的例子,考虑一下我在StackOverflow中给出的一些代码.在那段代码中,我有一个BigFraction类,它有一个BigInteger分子和一个BigInteger分母."规范"构造函数是BigFraction(BigInteger numerator, BigInteger denominator)
表单.对于所有其他构造函数,我只是将输入参数转换为BigIntegers,并调用"规范"构造函数,因为我不想复制所有工作.
在某些情况下,这很容易; 例如,带两个long
s 的构造函数是微不足道的:
public BigFraction(long numerator, long denominator) { this(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator)); }
但在其他情况下,这更难.考虑采用BigDecimal的构造函数:
public BigFraction(BigDecimal d) { this(d.scale() < 0 ? d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale())) : d.unscaledValue(), d.scale() < 0 ? BigInteger.ONE : BigInteger.TEN.pow(d.scale())); }
我觉得这很难看,但它有助于我避免重复代码.以下是我想要做的,但它在Java中是非法的:
public BigFraction(BigDecimal d) { BigInteger numerator = null; BigInteger denominator = null; if(d.scale() < 0) { numerator = d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale())); denominator = BigInteger.ONE; } else { numerator = d.unscaledValue(); denominator = BigInteger.TEN.pow(d.scale()); } this(numerator, denominator); }
更新
有很好的答案,但到目前为止,还没有提供我完全满意的答案,但我并不在乎开始赏金,所以我回答了我自己的问题(主要是为了摆脱这个问题)恼人的"你考虑过标记一个接受的答案"的消息).
建议的解决方法是:
静电厂.
我已经在很多地方使用过这个类,所以如果我突然摆脱了公共构造函数并使用了valueOf()函数,代码就会破坏.
感觉就像一个限制的解决方法.我不会获得工厂的任何其他好处,因为这不能被子类化,因为常见值没有被缓存/实施.
私有静态"构造函数助手"方法.
这会导致很多代码膨胀.
代码变得丑陋,因为在某些情况下我真的需要同时计算分子和分母,除非我返回一个BigInteger[]
或某种私有内部类,否则我不能返回多个值.
反对此功能的主要论据是编译器必须在调用superconstructor之前检查您是否使用了任何实例变量或方法,因为该对象将处于无效状态.我同意,但我认为这将比确保所有最终实例变量始终在每个构造函数中初始化的更容易检查,无论采用什么样的路径.另一个参数是你不能事先执行代码,但这显然是错误的,因为计算超级构造函数的参数的代码是在某处执行的,所以必须允许它在字节码级别.
现在,我想看到的是编译器不能让我接受这段代码的一个很好的理由:
public MyClass(String s) { this(Integer.parseInt(s)); } public MyClass(int i) { this.i = i; }
并像这样重写它(字节码基本相同,我想):
public MyClass(String s) { int tmp = Integer.parseInt(s); this(tmp); } public MyClass(int i) { this.i = i; }
我在这两个例子中看到的唯一真正区别是" tmp
"变量的范围允许this(tmp)
在第二个示例中调用后访问它.因此,可能static{}
需要引入特殊语法(类似于类初始化的块):
public MyClass(String s) { //"init{}" is a hypothetical syntax where there is no access to instance //variables/methods, and which must end with a call to another constructor //(using either "this(...)" or "super(...)") init { int tmp = Integer.parseInt(s); this(tmp); } } public MyClass(int i) { this.i = i; }
Mr. Shiny an.. 26
我认为这里的几个答案是错误的,因为他们假设在调用某些代码后调用super()时,封装会以某种方式被破坏.事实上,super实际上可以破坏封装本身,因为Java允许在构造函数中覆盖方法.
考虑这些类:
class A { protected int i; public void print() { System.out.println("Hello"); } public A() { i = 13; print(); } } class B extends A { private String msg; public void print() { System.out.println(msg); } public B(String msg) { super(); this.msg = msg; } }
如果你这样做
new B("Wubba lubba dub dub");
打印出来的消息是"null".这是因为A的构造函数正在从B访问未初始化的字段.坦白说,如果有人想要这样做:
class C extends A { public C() { System.out.println(i); // i not yet initialized super(); } }
那就是他们的问题就像他们上面的B级一样.在这两种情况下,程序员必须知道在构造期间如何访问变量.并且鉴于您可以在参数列表中调用super()
或this()
使用各种表达式,这似乎是一种人为的限制,您无法在调用其他构造函数之前计算任何表达式.更不用说限制适用于两者super()
,this()
当你想知道如何在调用时不打破自己的封装时this()
.
我的结论:这个特性是编译器中的一个错误,可能最初是出于一个很好的理由,但在目前的形式下,这是一个没有目的的人为限制.
我认为这里的几个答案是错误的,因为他们假设在调用某些代码后调用super()时,封装会以某种方式被破坏.事实上,super实际上可以破坏封装本身,因为Java允许在构造函数中覆盖方法.
考虑这些类:
class A { protected int i; public void print() { System.out.println("Hello"); } public A() { i = 13; print(); } } class B extends A { private String msg; public void print() { System.out.println(msg); } public B(String msg) { super(); this.msg = msg; } }
如果你这样做
new B("Wubba lubba dub dub");
打印出来的消息是"null".这是因为A的构造函数正在从B访问未初始化的字段.坦白说,如果有人想要这样做:
class C extends A { public C() { System.out.println(i); // i not yet initialized super(); } }
那就是他们的问题就像他们上面的B级一样.在这两种情况下,程序员必须知道在构造期间如何访问变量.并且鉴于您可以在参数列表中调用super()
或this()
使用各种表达式,这似乎是一种人为的限制,您无法在调用其他构造函数之前计算任何表达式.更不用说限制适用于两者super()
,this()
当你想知道如何在调用时不打破自己的封装时this()
.
我的结论:这个特性是编译器中的一个错误,可能最初是出于一个很好的理由,但在目前的形式下,这是一个没有目的的人为限制.
我觉得这很难看,但它有助于我避免重复代码.以下是我想做的事,但它在Java中是非法的......
您还可以通过使用返回新对象的静态工厂方法来解决此限制:
public static BigFraction valueOf(BigDecimal d) { // computate numerator and denominator from d return new BigFraction(numerator, denominator); }
或者,您可以通过调用私有静态方法来为您的构造函数执行计算:
public BigFraction(BigDecimal d) { this(computeNumerator(d), computeDenominator(d)); } private static BigInteger computeNumerator(BigDecimal d) { ... } private static BigInteger computeDenominator(BigDecimal d) { ... }
必须按顺序调用构造函数,从根父类到最派生类.您不能在派生构造函数中预先执行任何代码,因为在调用父构造函数之前,派生构造函数的堆栈框架尚未分配,因为派生构造函数尚未开始执行.不可否认,Java的语法并没有明确这一事实.
编辑:总而言之,当派生类构造函数在this()调用之前"执行"时,以下几点适用.
无法触及成员变量,因为它们在构造基类之前无效.
参数是只读的,因为堆栈帧尚未分配.
无法访问局部变量,因为尚未分配堆栈帧.
如果以相反的顺序从派生类到基类分配构造函数的堆栈帧,则可以访问参数和局部变量,但是这将要求所有帧同时处于活动状态,浪费每个对象构造的内存以允许对于在构造基类之前想要触摸局部变量的罕见代码.
"我的猜测是,在为层次结构的每个级别调用构造函数之前,对象处于无效状态.对于JVM来说,在完全构造之前运行任何东西是不安全的."
其实,这是可以构建在Java对象而不调用每一个层次结构中的构造函数,虽然不能与new
关键字.
例如,当Java的序列化在反序列化期间构造对象时,它会调用层次结构中第一个非可序列化类的构造函数.因此,当反序列化java.util.HashMap时,首先分配一个java.util.HashMap实例,然后调用其第一个非可序列化的超类java.util.AbstractMap的构造函数(后者又调用java.lang.Object的构造函数) .
您还可以使用Objenesis库来实例化对象,而无需调用构造函数.
或者,如果您如此倾向,您可以自己生成字节码(使用ASM或类似代码).在字节码级别,new Foo()
编译为两个指令:
NEW Foo INVOKESPECIAL Foo.()V
如果要避免调用Foo的构造函数,可以更改第二个命令,例如:
NEW Foo INVOKESPECIAL java/lang/Object.()V
但即便如此,Foo的构造函数必须包含对其超类的调用.否则,JVM的类加载器将在加载类时抛出异常,抱怨没有调用super()
.