Чи варто використовувати ініціалізаційні блоки на 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(); }на рівні класу це ініціалізаційний блок.
amon

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

Недійсний виклик методу. Це код всередині методу init, який недійсний. Тільки конструктори та блоки ініталізатора можуть призначати змінну остаточного члена, таким чином, призначення в init не буде компілюватися.
barjak

Ваш четвертий код коду не компілюється. Блоки ініталізатора працюють перед усіма конструкторами, тому squareVal = val * valскаржиться на доступ до неініціалізованих значень. Блоки ініціалізатора не можуть залежати від аргументів, переданих конструктору. Звичайне рішення, яке я бачив у цій проблемі, - це визначити єдиний "базовий" конструктор зі складною логікою та визначити всі інші конструктори з точки зору цієї. Більшість застосувань ініціалізаторів екземплярів, насправді, можна замінити цим шаблоном.
Malnormalulo

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 класи залежали від виклику статичних блоків статичних ініціалізаторів один одного до того, як клас міг бути повністю завантажений. Це призвело до невдалого завантаження класу або подібного розпливчастого повідомлення про помилку. Мені довелося порівняти файли з останньою відомою робочою версією в керуванні джерелами, щоб зрозуміти, в чому проблема. Не весело взагалі.

Ледача ініціалізація

Можливо, статичні ініціалізатори хороші з міркувань продуктивності, коли вони працюють і не надто заплутані. Але загалом я віддаю перевагу лінивій ініціалізації перед статичними ініціалізаторами в наші дні. Зрозуміло, що вони роблять, я ще не натрапив на помилку завантаження класу з ними, і вони працюють у більшій кількості ситуацій ініціалізації, ніж у блоках ініціалізаторів.

Визначення даних

Замість статичної ініціалізації для побудови структур даних (порівняйте з прикладами в інших відповідях) я тепер використовую допоміжні функції визначення даних Пагуро :

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

Висновок

На початку Java блоки ініціалізатора були єдиним способом зробити деякі речі, але тепер вони заплутані, схильні до помилок і в більшості випадків були замінені кращими альтернативами (детальніше вище). Цікаво знати про ініціалізаційні блоки на випадок, якщо ви бачите їх у застарілому коді, або вони виходять на тест, але якщо я робив огляд коду і бачив його в новому коді, прошу вас обґрунтувати, чому жоден із вище альтернативи були придатні, перш ніж надавати свій код великими пальцями.


3

На додаток до ініціалізації змінної екземпляра, яка оголошена як final(див . Відповідь barjak ), я також зазначив staticблок ініціалізації.

Ви можете використовувати їх як своєрідний "статичний кондуктор".

Таким чином, ви можете робити складні ініціалізації на статичній змінній a при першому посиланні на клас.

Ось приклад, натхненний бархаком:

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

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;у своєму блоці вилову, ви, можливо, зможете оголосити контекст остаточним.
GlenPeterson

@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; }
введете
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.