Почему делегирование другому конструктору должно произойти первым в конструкторе Java? - PullRequest
34 голосов
/ 03 февраля 2009

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

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

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

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

Обновление

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

Предлагаемые обходные пути:

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

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

Ответы [ 8 ]

25 голосов
/ 24 ноября 2009

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

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

13 голосов
/ 04 февраля 2009

Я нахожу это довольно уродливым, но это помогает мне избежать дублирования кода. я хотел бы сделать следующее, но это недопустимо в 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) { ... }        
11 голосов
/ 03 февраля 2009

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

Редактировать: Подводя итог, можно сказать, что когда конструктор производного класса «выполняется» перед вызовом this (), применяются следующие моменты.

  1. Переменные-члены не могут быть затронуты, потому что они недопустимы до базы классы построены.
  2. Аргументы доступны только для чтения, поскольку кадр стека не выделен.
  3. Локальные переменные недоступны, поскольку кадр стека не выделен.

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

6 голосов
/ 04 февраля 2009

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

1 голос
/ 04 февраля 2009

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

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

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

0 голосов
/ 12 февраля 2009

Посмотри сюда.

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

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

Ok

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

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

Вот и все.

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

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

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

0 голосов
/ 04 февраля 2009

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

0 голосов
/ 03 февраля 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...