Должен ли я использовать блоки инициализатора в Java?

16

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

public class Test {
  public Test() { /* first constructor */ }
  public Test(String s) { /* second constructor */ }

  // Non-static initializer block - copied into every constructor:
  {
    doStuff();
  }
}

Блок кода будет скопирован в каждый конструктор, т.е. если у вас есть несколько конструкторов, вам не нужно переписывать код.

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

  1. Это один из очень немногих случаев в Java, где важен порядок вашего кода, так как вы можете определить несколько блоков кода, и они будут выполняться в порядке их написания. Это кажется мне вредным, поскольку простое изменение порядка блоков кода на самом деле изменит код.
  2. Я не вижу никакой пользы от его использования. В большинстве случаев конструкторы будут вызывать друг друга с некоторыми предопределенными значениями. Даже если это не так, код можно просто поместить в закрытый метод и вызвать из каждого конструктора.
  3. Это снижает удобочитаемость, поскольку вы можете поместить блок в конец класса, а конструктор обычно находится в начале класса. Совершенно нелогично смотреть на совершенно другую часть файла кода, если вы не ожидаете, что это будет необходимо.

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

Восстановить Монику - Диркк
источник
3
Приведенный вами пример не содержит ничего похожего на блок инициализатора.
Симон Б,
6
Посмотрите @SimonBarker еще раз - { doStuff(); }на уровне класса блок инициализатора.
Амон
@SimonBarker Блок кода, который окружаетdoStuff()
Восстановите Монику - dirkk
2
«[S] подразумевает, что изменение порядка блоков кода фактически изменит код». И чем это отличается от изменения порядка инициализации переменных или отдельных строк кода? Если нет никаких зависимостей, то никакого вреда не возникает, и если есть зависимости, то размещение этих зависимостей не по порядку аналогично неправильному распределению зависимостей для отдельных строк кода. Тот факт, что Java позволяет ссылаться на методы и классы до их определения, не означает, что код, зависящий от порядка, встречается в Java редко.
JAB

Ответы:

20

Есть два случая, когда я использую блоки инициализатора.

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

Это действительно:

final int val = 2;

Это также верно:

final int val;

MyClass() {
    val = 2;
}

Это неверно:

final int val;

MyClass() {
    init();
}

void init() {
    val = 2;  // cannot assign to 'final' field in a method
}

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

final int val;
final int squareVal;

MyClass(int v, String s) {
    this.val = v;
    this.s = s;
}

MyClass(Point p, long id) {
    this.val = p.x;
    this.id = id;
}

{
    squareVal = val * val;
}

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

private Map<String, String> days = new HashMap<String, String>();
{
    days.put("mon", "monday");
    days.put("tue", "tuesday");
    days.put("wed", "wednesday");
    days.put("thu", "thursday");
    days.put("fri", "friday");
    days.put("sat", "saturday");
    days.put("sun", "sunday");
}
barjak
источник
Это не вызов метода, который является недопустимым. Это код внутри метода init, который является недопустимым. Только конструкторы и блоки инициализатора могут назначать конечную переменную-член, поэтому назначение в init не будет компилироваться.
Барджак
Ваш четвертый кодовый блок не компилируется. Блоки инициализатора запускаются раньше всех конструкторов, поэтому squareVal = val * valбудут жаловаться на доступ к неинициализированным значениям. Блоки инициализатора не могут зависеть от аргументов, переданных конструктору. Обычное решение, которое я видел для такого рода проблем, состоит в том, чтобы определить один «базовый» конструктор со сложной логикой и определить все другие конструкторы в терминах этого. Фактически, большинство применений инициализаторов экземпляров могут быть заменены этим шаблоном.
Малнормалуло
11

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

Запутанный синтаксис

Глядя на этот вопрос, есть 3 ответа, но вы обманули 4 человек с этим синтаксисом. Я был одним из них, и я пишу на Java в течение 16 лет! Очевидно, что синтаксис потенциально подвержен ошибкам! Я бы держался подальше от этого.

Телескопические Конструкторы

Для действительно простых вещей вы можете использовать «телескопические» конструкторы, чтобы избежать этой путаницы:

public class Test {
    private String something;

    // Default constructor does some things
    public Test() { doStuff(); }

    // Other constructors call the default constructor
    public Test(String s) {
        this(); // Call default constructor
        something = s;
    }
}

Образец Строителя

Если вам нужно сделать doStuff () в конце каждого конструктора или другой сложной инициализации, возможно, будет лучше использовать шаблон компоновщика. Джош Блох перечисляет несколько причин, почему строители - хорошая идея. Строителям требуется немного времени для написания, но правильно написано, что ими приятно пользоваться.

public class Test {
    // Value can be final (immutable)
    private final String something;

    // Private constructor.
    private Test(String s) { something = s; }

    // Static method to get a builder
    public static Builder builder() { return new Builder(); }

    // builder class accumulates values until a valid Test object can be created. 
    private static class Builder {
        private String tempSomething;
        public Builder something(String s) {
            tempSomething = s;
            return this;
        }
        // This is our factory method for a Test class.
        public Test build() {
            Test t = new Test(tempSomething);
            // Here we do your extra initialization after the
            // Test class has been created.
            doStuff();
            // Return a valid, potentially immutable Test object.
            return t;
        }
    }
}

// Now you can call:
Test t = Test.builder()
             .setString("Utini!")
             .build();

Петли статического инициализатора

Я использовал статический инициализаторы, но иногда сталкивался с циклами, в которых 2 класса зависели от того, как блоки статического инициализатора друг друга вызывались до того, как класс мог быть полностью загружен. Это приводило к «не удалось загрузить класс» или аналогично расплывчатое сообщение об ошибке. Мне пришлось сравнить файлы с последней известной рабочей версией в системе контроля версий, чтобы выяснить, в чем проблема. Не весело вообще.

Ленивая инициализация

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

Определение данных

Вместо статической инициализации для построения структур данных (сравните с примерами в других ответах), теперь я использую вспомогательные функции определения неизменных данных Paguro :

private ImMap<String,String> days =
        map(tup("mon", "monday"),
            tup("tue", "tuesday"),
            tup("wed", "wednesday"),
            tup("thu", "thursday"),
            tup("fri", "friday"),
            tup("sat", "saturday"),
            tup("sun", "sunday"));

Conculsion

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

GlenPeterson
источник
3

В дополнение к инициализации переменной экземпляра, которая объявлена ​​как final(см . Ответ Барджака ), я бы также упомянул staticблок инициализации.

Вы можете использовать их как своего рода «статический конструктор».

Таким образом, вы можете выполнять сложные инициализации статической переменной при первом обращении к классу.

Вот пример, вдохновленный примером Барджака:

public class dayHelper(){
    private static Map<String, String> days = new HashMap<String, String>();
    static {
        days.put("mon", "monday");
        days.put("tue", "tuesday");
        days.put("wed", "wednesday");
        days.put("thu", "thursday");
        days.put("fri", "friday");
        days.put("sat", "saturday");
        days.put("sun", "sunday");
    }
    public static String getLongName(String shortName){
         return days.get(shortName);
    }
}
C.Champagne
источник
1

Что касается нестатических блоков инициализатора, их голая функция - выступать в качестве конструктора по умолчанию в анонимных классах. Это в основном их единственное право на существование.

Нико
источник
0

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

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

private static final JAXBContext context = JAXBContext.newInstance(Foo.class); //doesn't compile

Но вместо этого вы должны сделать:

private static JAXBContext context;
static {
    try
    {
        context = JAXBContext.newInstance(Foo.class);
    }
    catch (JAXBException e)
    {
        //seriously...
    }
}

Я нахожу эту идиому очень уродливой (она также мешает вам пометить contextкак final), но это единственный способ, поддерживаемый Java для инициализации таких полей.

пятнистый
источник
Я думаю, что если вы установите context = null;в своем блоке catch, вы сможете объявить контекст как окончательный.
ГленПетерсон
@GlenPeterson Я пытался, но он не компилируется:The final field context may already have been assigned
Пятнистый
упс! Могу поспорить, что вы можете сделать свой контекст окончательным, если вы static { JAXBContext tempCtx = null; try { tempCtx = JAXBContext.newInstance(Foo.class); } catch (JAXBException ignored) { ; } context = tempCtx; }
введете