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

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

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

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

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

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

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

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

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

10 ответов

Решение

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

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

Тем не менее, я запутался, когда вы говорите:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/**
 * 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

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

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

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

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

Есть один важный аспект, который вы должны учитывать при выборе:

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

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

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

class ParentWithInitializer {
    protected final 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 final 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 по умолчанию, даже если это может быть не тот результат, который вы хотели.

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

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

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

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

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

Я видел, как библиотеки делают это, и это большая боль.

Так что лучше всего оставить чистыми только эти статические инициализаторы.

Другие вопросы по тегам