Использование инициализаторов и конструкторов в Java

Так что в последнее время я освежал свои навыки Java и нашел несколько функциональных возможностей, о которых раньше не знал. Статические инициализаторы и инициализаторы экземпляра - два таких метода.

У меня вопрос: когда можно использовать инициализатор вместо включения кода в конструктор? Я подумал о паре очевидных возможностей:

  • Инициализаторы static / instance могут использоваться для установки значения "final" переменных static / instance, тогда как конструктор не может

  • статические инициализаторы могут использоваться для установки значения любых статических переменных в классе, что должно быть более эффективным, чем наличие блока кода «if (someStaticVar == null) // do stuff» в начале каждого конструктора.

Оба этих случая предполагают, что код, необходимый для установки этих переменных, более сложен, чем просто «var = value», поскольку в противном случае не было бы никаких причин использовать инициализатор вместо простой установки значения при объявлении переменной.

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

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

Я что-то упускаю? Есть ли еще ряд ситуаций, в которых следует использовать инициализатор? Или это действительно довольно ограниченный инструмент, который можно использовать в очень специфических ситуациях?


person Inertiatic    schedule 29.04.2009    source источник
comment
Поскольку инициализаторы экземпляров - это малоизвестная функция, вот пример в помощь читателям: private final int somevar; {somevar = 2;} (обратите внимание, без конструктора.) Для большего удовольствия выполните поиск по инициализации двойными скобками (взлом синтаксиса).   -  person Luke Usherwood    schedule 09.09.2016


Ответы (9)


Статические инициализаторы полезны, как упоминал cletus, и я использую их таким же образом. Если у вас есть статическая переменная, которая должна быть инициализирована при загрузке класса, тогда вам подойдет статический инициализатор, особенно потому, что он позволяет вам выполнять сложную инициализацию и по-прежнему иметь статическую переменную final. Это большая победа.

Я считаю "if (someStaticVar == null) // делать что-нибудь" беспорядочным и подверженным ошибкам. Если он инициализирован статически и объявлен final, вы избегаете того, чтобы он был null.

Однако меня сбивает с толку, когда вы говорите:

Инициализаторы static / instance могут использоваться для установки значения "final" статических переменных / переменных экземпляра, тогда как конструктор не может

Я предполагаю, что вы говорите оба:

  • статические инициализаторы могут использоваться для установки значения "конечных" статических переменных, тогда как конструктор не может
  • инициализаторы экземпляра могут использоваться для установки значения "конечных" переменных экземпляра, тогда как конструктор не может

и вы правы в первом пункте, неправы во втором. Вы можете, например, сделать это:

class MyClass {
    private final int counter;
    public MyClass(final int counter) {
        this.counter = counter;
    }
}

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

class MyClass {
    private final int counter;
    public MyClass() {
        this(0);
    }
    public MyClass(final int counter) {
        this.counter = counter;
    }
}
person Eddie    schedule 29.04.2009
comment
Это то, что я сказал «да». Я думал, что финалы должны быть назначены при их объявлении, а не только один раз. Когда я думаю об этом, это немного глупая идея, но, тем не менее, она была у меня в голове. Спасибо, что прояснили это. - person Inertiatic; 30.04.2009
comment
Я забыл добавить кое-что о конструкторах цепочки, поэтому просто добавил его. - person Eddie; 30.04.2009
comment
IMHO инициализаторы экземпляров просто копируются в конструктор, поэтому они могут делать то же самое, что и код конструктора, это код конструктора, хотя они визуально разделены. - person Rostislav Matl; 12.09.2013

Анонимные внутренние классы не могут иметь конструктора (поскольку они анонимны), поэтому они вполне естественно подходят для инициализаторов экземпляров.

person Alex Martelli    schedule 29.04.2009

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

public class Deck {
  private final static List<String> SUITS;

  static {
    List<String> list = new ArrayList<String>();
    list.add("Clubs");
    list.add("Spades");
    list.add("Hearts");
    list.add("Diamonds");
    SUITS = Collections.unmodifiableList(list);
  }

  ...
}

Теперь этот пример можно выполнить с помощью одной строки кода:

private final static List<String> SUITS =
  Collections.unmodifiableList(
    Arrays.asList("Clubs", "Spades", "Hearts", "Diamonds")
  );

но статическая версия может быть намного аккуратнее, особенно когда элементы нетривиально инициализировать.

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

person cletus    schedule 29.04.2009
comment
Мне не очень нравится ваш конкретный пример, потому что он гораздо больше подходит для реализации как enum. - person JAB; 07.06.2013
comment
Затем измените SUITS на PAST_GIRLFRIENDS или что-то еще. Но вы правы, поскольку вероятность изменения классического набора карт примерно равна нулю, перечисления будут более ... подходящими. - person mike; 22.07.2013

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

public class MyClass {

    static private Properties propTable;

    static
    {
        try 
        {
            propTable.load(new FileInputStream("/data/user.prop"));
        } 
        catch (Exception e) 
        {
            propTable.put("user", System.getProperty("user"));
            propTable.put("password", System.getProperty("password"));
        }
    }

против

public class MyClass 
{
    public MyClass()
    {
        synchronized (MyClass.class) 
        {
            if (propTable == null)
            {
                try 
                {
                    propTable.load(new FileInputStream("/data/user.prop"));
                } 
                catch (Exception e) 
                {
                    propTable.put("user", System.getProperty("user"));
                    propTable.put("password", System.getProperty("password"));
                }
            }
        }
    }

Не забывайте, что теперь вам нужно синхронизировать на уровне класса, а не экземпляра. Это влечет за собой стоимость каждого созданного экземпляра вместо единовременной стоимости при загрузке класса. К тому же это некрасиво ;-)

person Robin    schedule 30.04.2009
comment
поэтому, если user.prop создается после того, как класс был использован в первый раз, он никогда не будет учитываться или это будет после компиляции? (для статической инициализации) - person Ced; 04.06.2016

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

/**
 * Demonstrate order of initialization in Java.
 * @author Daniel S. Wilkerson
 */
public class CtorOrder {
  public static void main(String[] args) {
    B a = new B();
  }
}

class A {
  A() {
    System.out.println("A ctor");
  }
}

class B extends A {

  int x = initX();

  int initX() {
    System.out.println("B initX");
    return 1;
  }

  B() {
    super();
    System.out.println("B ctor");
  }

}

Выход:

java CtorOrder
A ctor
B initX
B ctor
person Daniel    schedule 30.01.2012
comment
Именно тот пример, который я искал !!! Привет Даниэль, спасибо за бывшего. Возникает только один вопрос: почему сначала запустился A ctor? Я предсказал B initX, A ctor, B ctor. Кроме того, вы, кажется, довольно хорошо знаете язык, согласны ли вы? - person Cody; 27.06.2013
comment
Сначала запускается ctor, потому что для получения B вам сначала нужно иметь A. Не уверен, насколько хорошо я знаю язык, поскольку мой мозг отказывается изучать C ++ и Java лучше, чем он уже знает их, потому что эти языки не так хорошо разработаны так что вещи, которых я не знаю, часто на вкус как остатки отбросов на дне стакана: горькие. - person Daniel; 27.06.2013
comment
Если вы расширите этот пример за счет блоков инициализации и статических блоков инициализации, он будет еще более полезным. - person Thomas Weller; 02.12.2014

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

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

Иногда, если у вас есть какая-то логика, которую сложно связать между конструкторами (скажем, вы создаете подкласс и не можете вызвать this (), потому что вам нужно вызвать super ()), вы можете избежать дублирования, выполнив общие вещи в экземпляре инициализатор. Однако инициализаторы экземпляров настолько редки, что для многих они представляют собой удивительный синтаксис, поэтому я избегаю их и предпочел бы сделать свой класс конкретным, а не анонимным, если мне нужно поведение конструктора.

JMock - исключение, потому что именно так предполагается использовать фреймворк.

person Yishai    schedule 29.04.2009

При выборе необходимо учитывать один важный аспект:

Блоки инициализатора являются членами класса / объекта, а конструкторы - нет. Это важно при рассмотрении расширения / создания подкласса:

  1. Инициализаторы наследуются подклассами. (Хотя может быть затенен)
    Это означает, что в основном гарантируется, что подклассы инициализируются так, как задумано родительским классом.
  2. Однако конструкторы не наследуются. (Они вызывают только super() [т.е. без параметров] неявно, или вам нужно сделать конкретный вызов super(...) вручную.)
    Это означает, что неявный или неявный вызов super(...) может не инициализировать подкласс, как задумано родительским классом.

Рассмотрим этот пример блока инициализатора:

    class ParentWithInitializer {
        protected String aFieldToInitialize;

        {
            aFieldToInitialize = "init";
            System.out.println("initializing in initializer block of: " 
                + this.getClass().getSimpleName());
        }
    }

    class ChildOfParentWithInitializer extends ParentWithInitializer{
        public static void main(String... args){
            System.out.println(new ChildOfParentWithInitializer().aFieldToInitialize);
        }
    }

выход:

initializing in initializer block of: ChildOfParentWithInitializer
init

-> Независимо от того, какие конструкторы реализует подкласс, поле будет инициализировано.

Теперь рассмотрим этот пример с конструкторами:

    class ParentWithConstructor {
        protected String aFieldToInitialize;

        // different constructors initialize the value differently:
        ParentWithConstructor(){
            //init a null object
            aFieldToInitialize = null;
            System.out.println("Constructor of " 
                + this.getClass().getSimpleName() + " inits to null");
        }

        ParentWithConstructor(String... params) {
            //init all fields to intended values
            aFieldToInitialize = "intended init Value";
            System.out.println("initializing in parameterized constructor of:" 
                + this.getClass().getSimpleName());
        }
    }

    class ChildOfParentWithConstructor extends ParentWithConstructor{
        public static void main (String... args){
            System.out.println(new ChildOfParentWithConstructor().aFieldToInitialize);
        }
    }

выход:

Constructor of ChildOfParentWithConstructor inits to null
null

-> Это инициализирует поле по умолчанию на null, даже если это может быть не тот результат, который вам нужен.

person Vankog    schedule 30.05.2018
comment
Что вы имеете в виду, что инициализаторы можно затенять? - person Aleksandr Dubinsky; 09.12.2019
comment
Подкласс также может изменить или инициализировать родительское поле aFieldToInitialize в статическом блоке или другими способами. Родительский статический блок по-прежнему будет выполняться, но дочерний элемент может снова перезаписать данные. - person Vankog; 13.12.2019
comment
aFieldToInitialize окончательно. Его нельзя перезаписать. - person Aleksandr Dubinsky; 13.12.2019
comment
Туше. Но суть осталась прежней ;-) Я удалил финальную часть для простоты. - person Vankog; 18.12.2019
comment
То, что вы говорите, все еще не имеет смысла. Базовый класс всегда должен проектировать свои конструкторы для правильной инициализации, и тогда проблема, которую вы пытаетесь изобрести, не существует. Инициализаторы не имеют преимуществ перед конструкторами. - person Aleksandr Dubinsky; 18.12.2019
comment
Как вы должны или не должны использовать его, не является частью этого вопроса. - person Vankog; 23.12.2019

Я также хотел бы добавить одно замечание ко всем приведенным выше сказочным ответам. Когда мы загружаем драйвер в JDBC с помощью Class.forName (""), происходит загрузка класса и запускается статический инициализатор класса Driver, а код внутри него регистрирует драйвер в диспетчере драйверов. Это одно из важных применений статического блока кода.

person kmrinal    schedule 18.06.2011

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

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

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

person Bill K    schedule 29.04.2009