Почему делегация в другом конструкторе происходит сначала в конструкторе Java?

В конструкторе в Java, если вы хотите вызвать другой конструктор (или супер-конструктор), он должен быть первой строкой в ​​конструкторе. Я предполагаю, что это связано с тем, что вам не разрешается изменять любые переменные экземпляра до запуска другого конструктора. Но почему у вас нет операторов перед делегацией конструктора, чтобы вычислить сложное значение для другой функции? Я не могу придумать какой-либо веской причины, и я попал в некоторые реальные случаи, когда я написал какой-то уродливый код, чтобы обойти это ограничение.

Так что мне просто интересно:

  • Есть ли веская причина для этого ограничения?
  • Есть ли планы разрешить это в будущих выпусках Java? (Или Солнце окончательно сказал, что этого не произойдет?)

Для примера того, о чем я говорю, рассмотрим некоторый код, который я написал, который я дал в qaru.site/info/15958/.... В этом коде у меня есть класс BigFraction, у которого есть числитель BigInteger и знаменатель BigInteger. "Канонический" конструктор - это форма BigFraction(BigInteger numerator, BigInteger denominator). Для всех других конструкторов я просто конвертирую входные параметры в BigIntegers и вызываю "канонический" конструктор, потому что я не хочу дублировать всю работу.

В некоторых случаях это легко; например, конструктор, который принимает два 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);
  }

Обновление

Были хорошие ответы, но до сих пор не было получено никаких ответов, что я полностью удовлетворен, но мне все равно, чтобы начать щедрость, поэтому я отвечаю на свой вопрос (в основном, чтобы получить избавиться от этого раздражающего сообщения "вы считали, что сообщение принято" ).

Обходными решениями, которые были предложены, являются:

  • Статический factory.
    • Я использовал класс во многих местах, так что код сломался бы, если бы я внезапно избавился от публичных конструкторов и пошел с функциями valueOf().
    • Похоже на обходное ограничение. Я не получил бы других преимуществ factory, потому что это не может быть подклассом и потому, что общие значения не кэшируются/интернированы.
  • Частные статические методы-помощники-конструкторы.
    • Это приводит к большому количеству раздувания кода.
    • Код становится уродливым, потому что в некоторых случаях мне действительно нужно вычислить как числитель, так и знаменатель одновременно, и я не могу вернуть несколько значений, если не вернусь 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;
}

Единственное реальное различие, которое я вижу между этими двумя примерами, заключается в том, что область переменных "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;
}

Ответ 1

Я нахожу это довольно уродливым, но это помогает я избегаю дублирования кода. Следующее - это то, что я хотел бы сделать, но это незаконно на Java...

Вы также можете обойти это ограничение, используя статический метод factory, который возвращает новый объект:

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) { ... }        

Ответ 2

Я думаю, что некоторые из ответов здесь неверны, потому что они предполагают, что инкапсуляция каким-то образом нарушена при вызове 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().

Мой вердикт: эта функция является ошибкой в ​​компиляторе, возможно, изначально мотивирована по уважительной причине, но в ее нынешнем виде это искусственное ограничение без каких-либо целей.

Ответ 3

Конструкторы должны быть вызваны по порядку, от корневого родительского класса до самого производного класса. Вы не можете выполнить какой-либо код заранее в производном конструкторе, потому что до вызова родительского конструктора кадр стека для производного конструктора еще не был выделен, потому что производный конструктор не начал выполняться. Разумеется, синтаксис Java не делает этот факт ясным.

Изменить: суммировать, когда конструктор производного класса выполняет "выполнение" перед вызовом this(), применяются следующие пункты.

  • Нельзя коснуться переменных-членов, поскольку они недействительны перед базовым классы построены.
  • Аргументы доступны только для чтения, поскольку фрейм стека не был выделен.
  • Локальные переменные недоступны, поскольку фрейм стека не был выделен.

Вы можете получить доступ к аргументам и локальным переменным, если вы выделили стоп-кадры конструкторов в обратном порядке, от производных классов до базовых классов, но для этого потребовалось бы, чтобы все кадры были активны одновременно, теряя память для каждого объекта чтобы разрешить редкий случай кода, который хочет коснуться локальных переменных до создания базовых классов.

Ответ 4

"Я предполагаю, что до тех пор, пока конструктор не будет вызван для каждого уровня иерархии, объект находится в недопустимом состоянии. Для JVM небезопасно запускать что-либо на нем, пока оно не будет полностью построено".

На самом деле, можно построить объекты в Java без вызова каждого конструктора в иерархии, хотя и не с ключевым словом new.

Например, когда сериализация Java создает объект во время десериализации, он вызывает конструктор первого несериализуемого класса в иерархии. Поэтому, когда java.util.HashMap десериализуется, сначала выделяется экземпляр java.util.HashMap, а затем вызывается конструктор его первого несериализуемого суперкласса java.util.AbstractMap(который, в свою очередь, вызывает конструктор java.lang.Object).

Вы также можете использовать библиотеку Objenesis для создания объектов без вызова конструктора.

Или, если вы так склонны, вы можете сами генерировать байт-код (ASM или аналогичный). На уровне байт-кода new Foo() скомпилируется с двумя инструкциями:

NEW Foo
INVOKESPECIAL Foo.<init> ()V

Если вы хотите избежать вызова конструктора Foo, вы можете изменить вторую команду, например:

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

Но даже тогда конструктор Foo должен содержать вызов его суперкласса. В противном случае загрузчик класса JVM генерирует исключение при загрузке класса, жалуясь на отсутствие вызова super().

Ответ 5

Разрешающий код не вызывать супер конструктор, сначала прерывает инкапсуляцию - идею о том, что вы можете писать код и быть в состоянии доказать, что независимо от того, что делает кто-то другой, - расширьте его, вызовите, инстанцируйте его - он всегда будет в действительное состояние.

IOW: это не требование JVM как таковое, а требование Comp Sci. И важный.

Чтобы решить вашу проблему, кстати, вы используете частные статические методы - они не зависят от какого-либо экземпляра:

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

Если вам не нравятся отдельные методы (например, много общей логики, которую вы хотите выполнить только один раз, например), у вас есть один метод, который возвращает частный небольшой статический внутренний класс, который используется для вызова частного конструктора.

Ответ 6

Я предполагаю, что до тех пор, пока конструктор не будет вызван для каждого уровня иерархии, объект находится в недопустимом состоянии. Для JVM небезопасно запускать что-либо на нем, пока оно не будет полностью сконструировано.

Ответ 7

Ну, проблема в том, что java не может определить, какие "заявления" вы собираетесь поставить перед супервызовом. Например, вы можете ссылаться на переменные-члены, которые еще не инициализированы. Поэтому я не думаю, что Java будет поддерживать это. Теперь есть много способов обойти эту проблему, например, с помощью методов factory или шаблонов.

Ответ 8

Посмотрите так.

Скажем, что объект состоит из 10 частей.

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

Ok?

От 1 до 9 находятся в суперклассе, часть № 10 - ваше дополнение.

Простой не может добавить 10-ю часть до завершения предыдущих 9.

Что это.

Если из 1-6 из другого суперкласса это прекрасно, вещь - один единственный объект создается в определенной последовательности, , что путь был разработан.

Конечно, настоящая причина гораздо сложнее, чем эта, но я думаю, что это в значительной степени ответит на вопрос.

Что касается альтернатив, я думаю, что здесь уже много.