为什么必须首先在 Java 构造函数中委托给不同的构造函数?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/508639/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-29 12:42:00  来源:igfitidea点击:

Why must delegation to a different constructor happen first in a Java constructor?

javasyntax

提问by Kip

In a constructor in Java, if you want to call another constructor (or a super constructor), it has to be the first line in the constructor. I assume this is because you shouldn't be allowed to modify any instance variables before the other constructor runs. But why can't you have statements before the constructor delegation, in order to compute the complex value to the other function? I can't think of any good reason, and I have hit some real cases where I have written some ugly code to get around this limitation.

在 Java 中的构造函数中,如果要调用另一个构造函数(或超级构造函数),则必须在构造函数的第一行。我认为这是因为在其他构造函数运行之前不应允许您修改任何实例变量。但是为什么不能在构造函数委托之前有语句,以便计算另一个函数的复数值?我想不出任何好的理由,而且我遇到了一些真实的案例,我编写了一些丑陋的代码来解决这个限制。

So I'm just wondering:

所以我只是想知道:

  1. Is there a good reason for this limitation?
  2. Are there any plans to allow this in future Java releases? (Or has Sun definitively said this is not going to happen?)
  1. 这种限制有充分的理由吗?
  2. 是否有计划在未来的 Java 版本中允许这样做?(或者,Sun 是否明确表示这不会发生?)


For an example of what I'm talking about, consider some code I wrote which I gave in this StackOverflow answer. In that code, I have a BigFraction class, which has a BigInteger numerator and a BigInteger denominator. The "canonical" constructor is the BigFraction(BigInteger numerator, BigInteger denominator)form. For all the other constructors, I just convert the input parameters to BigIntegers, and call the "canonical" constructor, because I don't want to duplicate all the work.

作为我所谈论内容的示例,请考虑我在StackOverflow answer 中给出的一些我编写的代码。在该代码中,我有一个 BigFraction 类,它有一个 BigInteger 分子和一个 BigInteger 分母。“规范”构造函数是BigFraction(BigInteger numerator, BigInteger denominator)表单。对于所有其他构造函数,我只是将输入参数转换为 BigIntegers,并调用“规范”构造函数,因为我不想重复所有工作。

In some cases this is easy; for example, the constructor that takes two longs is trivial:

在某些情况下,这很容易;例如,带有两个longs的构造函数是微不足道的:

  public BigFraction(long numerator, long denominator)
  {
    this(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
  }

But in other cases, it is more difficult. Consider the constructor which takes a BigDecimal:

但在其他情况下,就更难了。考虑采用 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()));
  }

I find this pretty ugly, but it helps me avoid duplicating code. The following is what I'd like to do, but it is illegal in Java:

我觉得这很丑陋,但它帮助我避免重复代码。以下是我想要做的,但在 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);
  }


Update

更新

There have been good answers, but thus far, no answers have been provided that I'm completely satisfied with, but I don't care enough to start a bounty, so I'm answering my own question (mainly to get rid of that annoying "have you considered marking an accepted answer" message).

有很好的答案,但到目前为止,还没有提供我完全满意的答案,但我不太关心开始赏金,所以我正在回答我自己的问题(主要是为了摆脱那个烦人的“您是否考虑过标记已接受的答案”消息)。

Workarounds that have been suggested are:

已建议的解决方法是:

  1. Static factory.
    • I've used the class in a lot of places, so that code would break if I suddenly got rid of the public constructors and went with valueOf() functions.
    • It feels like a workaround to a limitation. I wouldn't get any other benefits of a factory because this cannot be subclassed and because common values are not being cached/interned.
  2. Private static "constructor helper" methods.
    • This leads to lots of code bloat.
    • The code gets ugly because in some cases I really need to compute both numerator and denominator at the same time, and I can't return multiple values unless I return a BigInteger[]or some kind of private inner class.
  1. 静态工厂。
    • 我在很多地方都使用过这个类,所以如果我突然摆脱公共构造函数并使用 valueOf() 函数,代码就会中断。
    • 感觉像是一种限制的解决方法。我不会得到工厂的任何其他好处,因为它不能被子类化,并且因为公共值没有被缓存/实习。
  2. 私有静态“构造函数助手”方法。
    • 这会导致大量代码膨胀。
    • 代码变得难看,因为在某些情况下我真的需要同时计算分子和分母,而且我不能返回多个值,除非我返回 aBigInteger[]或某种私有内部类。

The main argument against this functionality is that the compiler would have to check that you didn't use any instance variables or methods before calling the superconstructor, because the object would be in an invalid state. I agree, but I think this would be an easier check than the one which makes sure all final instance variables are always initialized in every constructor, no matter what path through the code is taken. The other argument is that you simply can't execute code beforehand, but this is clearly false because the code to compute the parameters to the superconstructor is getting executed somewhere, so it must be allowed at a bytecode level.

反对此功能的主要论点是编译器必须在调用超构造函数之前检查您没有使用任何实例变量或方法,因为该对象将处于无效状态。我同意,但我认为这比确保所有最终实例变量始终在每个构造函数中初始化的检查更容易,无论采用哪种代码路径。另一个论点是您根本无法预先执行代码,但这显然是错误的,因为计算超构造函数参数的代码正在某处执行,因此必须在字节码级别允许它。

Now, what I'd like to see, is some goodreason why the compiler couldn't let me take this code:

现在,我想看到的是编译器不能让我使用此代码的一些充分理由:

public MyClass(String s) {
  this(Integer.parseInt(s));
}
public MyClass(int i) {
  this.i = i;
}

And rewrite it like this (the bytecode would be basically identical, I'd think):

并像这样重写它(字节码基本上是相同的,我认为):

public MyClass(String s) {
  int tmp = Integer.parseInt(s);
  this(tmp);
}
public MyClass(int i) {
  this.i = i;
}

The only real difference I see between those two examples is that the "tmp" variable's scope allows it to be accessed after calling this(tmp)in the second example. So maybe a special syntax (similar to static{}blocks for class initialization) would need to be introduced:

我在这两个示例之间看到的唯一真正区别是“ 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;
}

采纳答案by Zach Scrivena

I find this pretty ugly, but it helps me avoid duplicating code. The following is what I'd like to do, but it is illegal in Java ...

我觉得这很丑陋,但它帮助我避免重复代码。以下是我想要做的,但它在 Java 中是非法的......

You could also work around this limitation by using a static factory method that returns a new object:

您还可以通过使用返回新对象的静态工厂方法来解决此限制:

public static BigFraction valueOf(BigDecimal d)
{
    // computate numerator and denominator from d

    return new BigFraction(numerator, denominator);
}

Alternatively, you could cheat by calling a private static method to do the computations for your constructor:

或者,您可以通过调用私有静态方法来为构造函数进行计算来作弊:

public BigFraction(BigDecimal d)
{
    this(computeNumerator(d), computeDenominator(d));
}

private static BigInteger computeNumerator(BigDecimal d) { ... }
private static BigInteger computeDenominator(BigDecimal d) { ... }        

回答by u7867

I think several of the answers here are wrong because they assume encapsulation is somehow broken when calling super() after invoking some code. The fact is that the super can actually break encapsulation itself, because Java allows overriding methods in the constructor.

我认为这里的几个答案是错误的,因为他们假设在调用一些代码后调用 super() 时封装在某种程度上被破坏了。事实是 super 实际上可以破坏封装本身,因为 Java 允许在构造函数中覆盖方法。

Consider these classes:

考虑这些类:

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; }
}

If you do

如果你这样做

new B("Wubba lubba dub dub");

the message printed out is "null". That's because the constructor from A is accessing the uninitialized field from B. So frankly it seems that if someone wanted to do this:

打印出来的消息是“null”。那是因为 A 的构造函数正在访问 B 的未初始化字段。坦率地说,如果有人想这样做:

class C extends A {
  public C() { 
    System.out.println(i); // i not yet initialized
    super();
  }
} 

Then that's just as much their problem as if they make class B above. In both cases the programmer has to know how the variables are accessed during construction. And given that you can call super()or this()with all kinds of expressions in the parameter list, it seems like an artificial restriction that you can't compute any expressions before calling the other constructor. Not to mention that the restriction applies to both super()and this()when presumably you know how to not break your own encapsulation when calling this().

那么这就是他们的问题,就好像他们在上面制作了 B 类一样。在这两种情况下,程序员都必须知道在构造期间如何访问变量。鉴于您可以调用super()this()参数列表中的所有类型的表达式,这似乎是一种人为的限制,即您不能在调用其他构造函数之前计算任何表达式。更不用说该限制适用于两者,super()并且this()当您大概知道如何在调用this().

My verdict: This feature is a bug in the compiler, perhaps originally motivated by a good reason, but in its current form it is an artifical limitation with no purpose.

我的结论是:这个特性是编译器中的一个错误,最初可能是出于一个很好的理由,但在目前的形式中,它是一个没有目的的人为限制。

回答by Tmdean

The constructors must be called in order, from the root parent class to the most derived class. You can't execute any code beforehand in the derived constructor because before the parent constructor is called, the stack frame for the derived constructor hasn't even been allocated yet, because the derived constructor hasn't started executing. Admittedly, the syntax for Java doesn't make this fact clear.

必须按顺序调用构造函数,从根父类到最派生的类。您不能在派生构造函数中预先执行任何代码,因为在调用父构造函数之前,派生构造函数的堆栈帧甚至尚未分配,因为派生构造函数尚未开始执行。诚然,Java 的语法并未明确说明这一事实。

Edit: To summarize, when a derived class constructor is "executing" before the this() call, the following points apply.

编辑:总而言之,当派生类构造函数在 this() 调用之前“执行”时,以下几点适用。

  1. Member variables can't be touched, because they are invalid before base classes are constructed.
  2. Arguments are read-only, because the stack frame has not been allocated.
  3. Local variables cannot be accessed, because the stack frame has not been allocated.
  1. 成员变量是不能动的,因为在构造基类之前它们是无效的。
  2. 参数是只读的,因为尚未分配堆栈帧。
  3. 无法访问局部变量,因为尚未分配堆栈帧。

You can gain access to arguments and local variables if you allocated the constructors' stack frames in reverse order, from derived classes to base classes, but this would require all frames to be active at the same time, wasting memory for every object construction to allow for the rare case of code that wants to touch local variables before base classes are constructed.

如果您以相反的顺序分配构造函数的堆栈帧,从派生类到基类,您可以获得对参数和局部变量的访问,但这将要求所有帧同时处于活动状态,为每个对象构造浪费内存以允许对于在构造基类之前想要接触局部变量的代码的罕见情况。

回答by Esko Luontola

"My guess is that, until a constructor has been called for every level of the heierarchy, the object is in an invalid state. It is unsafe for the JVM to run anything on it until it has been completely constructed."

“我的猜测是,在为层次结构的每个级别调用构造函数之前,对象处于无效状态。JVM 在其上运行任何东西直到它被完全构造出来是不安全的。”

Actually, it ispossible to construct objects in Java without calling everyconstructor in the hierarchy, although not with the newkeyword.

其实,这可以构建在Java对象而不调用每一个层次结构中的构造函数,虽然不能与new关键字。

For example, when Java's serialization constructs an object during deserialization, it calls the constructor of the first non-serializable class in the hierarchy. So when java.util.HashMap is deserialized, first a java.util.HashMap instance is allocated and then the constructor of its first non-serializable superclass java.util.AbstractMap is called (which in turn calls java.lang.Object's constructor).

例如,Java 的序列化在反序列化过程中构造对象时,会调用层次结构中第一个不可序列化类的构造函数。因此,当 java.util.HashMap 被反序列化时,首先分配一个 java.util.HashMap 实例,然后调用其第一个不可序列化的超类 java.util.AbstractMap 的构造函数(依次调用 java.lang.Object 的构造函数) .

You can also use the Objenesislibrary to instantiate objects without calling the constructor.

您还可以使用Objenesis库在不调用构造函数的情况下实例化对象。

Or if you are so inclined, you can generate the bytecode yourself (with ASMor similar). At the bytecode level, new Foo()compiles to two instructions:

或者,如果您愿意,可以自己生成字节码(使用ASM或类似方法)。在字节码级别,new Foo()编译为两条指令:

NEW Foo
INVOKESPECIAL Foo.<init> ()V

If you want to avoid calling the constructor of Foo, you can change the second command, for example:

如果要避免调用 Foo 的构造函数,可以更改第二个命令,例如:

NEW Foo
INVOKESPECIAL java/lang/Object.<init> ()V

But even then, the constructor of Foo must contain a call to its superclass. Otherwise the JVM's class loader will throw an exception when loading the class, complaining that there is no call to super().

但即便如此,Foo 的构造函数也必须包含对其超类的调用。否则 JVM 的类加载器在加载类时会抛出异常,抱怨没有调用super().

回答by Esko Luontola

Allowing code to not call the super constructor first breaks encapsulation - the idea that you can write code and be able to provethat no matter what someone else does - extend it, invoke it, instansiate it - it will always be in a valid state.

允许代码不调用超级构造函数首先打破了封装——你可以编写代码并能够证明无论其他人做什么——扩展它、调用它、实例化它——它总是处于有效状态的想法。

IOW: it's not a JVM requirement as such, but a Comp Sci requirement. And an important one.

IOW:这不是 JVM 要求,而是 Comp Sci 要求。还有一个重要的。

To solve your problem, incidentally, you make use of private static methods - they don't depend on any instance:

顺便说一下,为了解决您的问题,您可以使用私有静态方法 - 它们不依赖于任何实例:

public BigFraction(BigDecimal d)
{
  this(appropriateInitializationNumeratorFor(d),
       appropriateInitializationDenominatorFor(d));
}

private static appropriateInitializationNumeratorFor(BigDecimal d)
{
  if(d.scale() < 0)
  {
    return d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale()));
  }
  else
  {
    return d.unscaledValue();
  }
}

If you don't like having separate methods (a lot of common logic you only want to execute once, for instance), have one method that returns a private little static inner class which is used to invoke a private constructor.

如果您不喜欢使用单独的方法(例如,许多您只想执行一次的常见逻辑),请使用一个返回私有小静态内部类的方法,该类用于调用私有构造函数。

回答by amit

Well, the problem is java cannot detect what 'statements' you are going to put before the super call. For example, you could refer to member variables which are not yet initialized. So I don't think java will ever support this. Now, there are many ways to work around this problem such as by using factory or template methods.

好吧,问题是 java 无法检测到您要在超级调用之前放置什么“语句”。例如,您可以引用尚未初始化的成员变量。所以我认为java永远不会支持这个。现在,有很多方法可以解决这个问题,例如使用工厂方法或模板方法。

回答by amit

My guessis that, until a constructor has been called for every level of the heierarchy, the object is in an invalid state. It is unsafe for the JVM to run anything on it until it has been completely constructed.

我的猜测是,在为层次结构的每个级别调用构造函数之前,对象都处于无效状态。在完全构建之前,JVM 在其上运行任何东西都是不安全的。

回答by OscarRyz

Look it this way.

这样看。

Let's say that an object is composed of 10 parts.

假设一个对象由 10 个部分组成。

1,2,3,4,5,6,7,8,9,10

1、2、3、4、5、6、7、8、9、10

Ok?

好的?

From 1 to 9 are in the super class, part #10 is your addition.

从 1 到 9 是超类,第 10 部分是您的补充。

Simple cannot add the 10th part until the previous 9 are completed.

Simple 不能添加第 10 部分,直到前 9 部分完成。

That's it.

而已。

If from 1-6 are from another super class that fine, the thing is one single object is created in a specific sequence, that's the way is was designed.

如果 1-6 来自另一个超类,那很好,事情是按特定顺序创建一个对象,这就是设计的方式

Of course real reason is far more complex than this, but I think this would pretty much answers the question.

当然,真正的原因远比这复杂得多,但我认为这几乎可以回答这个问题。

As for the alternatives, I think there are plenty already posted here.

至于替代方案,我认为这里已经发布了很多。