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

2022-09-01 03:50:34

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

所以我只是想知道:

  1. 这种限制有充分的理由吗?
  2. 有没有计划在将来的Java版本中允许这样做?(或者孙中山明确表示这不会发生?

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

在某些情况下,这很容易;例如,采用两个 s 的构造函数是微不足道的:long

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

更新

有很好的答案,但到目前为止,还没有提供我完全满意的答案,但我并不在乎开始赏金,所以我正在回答我自己的问题(主要是为了摆脱令人讨厌的“你有没有考虑过标记一个接受的答案”的消息)。

建议的解决方法是:

  1. 静态工厂。
    • 我已经在很多地方使用过这个类,所以如果我突然摆脱了公共构造函数并使用valueOf()函数,代码就会中断。
    • 这感觉就像是限制的解决方法。我不会从工厂获得任何其他好处,因为这不能被子类化,并且因为公共值没有被缓存/滞留。
  2. 私有静态“构造函数帮助程序”方法。
    • 这会导致大量代码膨胀。
    • 代码变得丑陋,因为在某些情况下,我真的需要同时计算分子和分母,除非我返回一个或某种私有的内部类,否则我无法返回多个值。BigInteger[]

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

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

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

我看到这两个示例之间唯一真正的区别是,在第二个示例中调用“”变量的作用域允许访问它。因此,也许需要引入一种特殊的语法(类似于用于类初始化的块):tmpthis(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;
}

答案 1

我认为这里的几个答案是错误的,因为它们假设在调用一些代码后调用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");

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

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

然后,这与他们上面的B类一样多。在这两种情况下,程序员都必须知道在构造过程中如何访问变量。鉴于您可以在参数列表中调用或使用各种表达式,这似乎是一个人为的限制,即在调用其他构造函数之前无法计算任何表达式。更不用说该限制适用于两者,并且当您知道如何在调用时不破坏自己的封装时。super()this()super()this()this()

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


答案 2

我发现这很丑陋,但它可以帮助我避免重复代码。以下是我想做的,但它在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) { ... }