Ефективність Java «Подвійна ініціалізація дужок»?


823

У Прихованих можливості Java верхній відповідь згадує Double Brace ініціалізацію , з дуже привабливим синтаксисом:

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

Ця ідіома створює анонімний внутрішній клас з просто ініціалізатором екземпляра, який "може використовувати будь-які [...] методи в області, що містить".

Головне питання: чи це неефективно, як це звучить? Чи має бути його використання обмеженим разовими ініціалізаціями? (І звичайно, показуючи!)

Друге питання: Новий HashSet повинен бути "цим", що використовується в ініціалізаторі екземпляра ... чи може хтось пролити світло на механізм?

Третє запитання: чи ця ідіома занадто незрозуміла для використання у виробничому коді?

Резюме: Дуже, дуже приємні відповіді, дякую всім. За запитання (3) люди вважали, що синтаксис повинен бути зрозумілим (хоча я рекомендую епізодичний коментар, особливо якщо ваш код буде переданий розробникам, які можуть не знати його).

За запитання (1), згенерований код повинен швидко працювати. Додаткові файли .class викликають загрозу файлів jar, а також повільний запуск програми (завдяки @coobird для вимірювання цього). @Thilo зазначив, що на збір сміття можуть вплинути, а вартість пам'яті для додатково завантажених класів може бути фактором у деяких випадках.

Питання (2) виявилося для мене найбільш цікавим. Якщо я розумію відповіді, те, що відбувається в DBI, полягає в тому, що анонімний внутрішній клас розширює клас об'єкта, який будується новим оператором, і, отже, має значення "це", що посилається на створений екземпляр. Дуже акуратно.

В цілому DBI вражає мене як щось інтелектуальне цікавість. Coobird та інші зазначають, що ви можете досягти такого ж ефекту за допомогою Arrays.asList, методів varargs, Google Collections та запропонованих літератур Java 7 Collection. Новіші мови JVM, такі як Scala, JRuby та Groovy, також пропонують стислі позначення для створення списків та добре взаємодіють з Java. Зважаючи на те, що DBI захаращує класний шлях, трохи сповільнює завантаження класу і робить код більш незрозумілим, я, мабуть, ухиляюся від нього. Однак я планую навести це на товариша, який щойно здобув свій SCJP і любить добродушні пориви щодо семантики Java! ;-) Дякую всім!

7/2017: Baeldung має хороший підсумок подвійної ініціалізації дужок і вважає це антидіаграмою .

12/2017: @Basil Bourque зазначає, що в новій Java 9 можна сказати:

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

Це точно шлях. Якщо ви зупинилися на більш ранній версії, подивіться на ImmutableSet Google Collections .


33
Я бачу тут запах коду, що наївний читач очікував flavorsби бути HashSet, але, на жаль, це анонімний підклас.
Елазар Лейбович

6
Якщо ви розглядаєте запуск замість завантаження продуктивності, різниці немає, дивіться мою відповідь.
Пітер Лоурі

4
Мені подобається, що ви створили резюме, я вважаю, що це варті вправи як для вас, щоб підвищити розуміння, так і для громади.
Патрік Мерфі

3
На мою думку, це не затьмарено. Читачі повинні знати, що подвійне ... o зачекайте, @ElazarLeibovich вже сказав це у своєму коментарі . Сам ініціалізатор подвійної дужки не існує як мовна конструкція, це лише комбінація анонімного підкласу та ініціалізатора екземпляра. Єдине, що люди повинні знати про це.
MC Імператор

8
Java 9 пропонує незмінні набори статичних заводських методів, які можуть замінити використання DCI в деяких ситуаціях:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Василь Бурк

Відповіді:


607

Ось проблема, коли я занадто захоплююся анонімними внутрішніми класами:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

Це всі класи, які були створені, коли я робив просту програму, і використовував велику кількість анонімних внутрішніх класів - кожен клас буде зібраний в окремий classфайл.

Як уже згадувалося, "подвійна дужка ініціалізації" - це анонімний внутрішній клас із блоком ініціалізації екземпляра, що означає, що для кожної "ініціалізації" створюється новий клас, все для того, щоб зазвичай зробити один об'єкт.

Враховуючи, що віртуальній машині Java потрібно буде прочитати всі ті класи при їх використанні, що може призвести до певного часу в процесі віртуації байт-коду тощо . Не кажучи вже про збільшення необхідного місця на диску для зберігання всіх цих classфайлів.

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


Для довідки подвійна дужка ініціалізації полягає в наступному:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Це схоже на "приховану" особливість Java, але це лише перезапис:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

Отже, це в основному блок ініціалізації екземплярів, який є частиною анонімного внутрішнього класу .


Пропозиція " Колекція літератури Джошуа Блоха" для проектної монети була такою:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

На жаль, він не пробився ні до Java 7, ні до 8, і був відкладений нескінченно.


Експеримент

Ось простий експеримент, який я перевірив - зробіть 1000 ArrayListс елементами "Hello"та додайте "World!"їх за допомогою addметоду, використовуючи два методи:

Спосіб 1: Подвійна дугова ініціалізація

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

Спосіб 2: Миттєвий ArrayListіadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Я створив просту програму, щоб виписати вихідний файл Java для виконання 1000 ініціалізації за допомогою двох методів:

Тест 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

Тест 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

Зауважте, що минулий час для ініціалізації 1000 ArrayLists та 1000 анонімних внутрішніх класів, що розширюється ArrayList, перевіряється за допомогою System.currentTimeMillis, тому таймер не має дуже високої роздільної здатності. У моїй системі Windows роздільна здатність становить приблизно 15-16 мілісекунд.

Результати 10 тестів двох тестів були такими:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

Як видно, подвійна дужка ініціалізації має помітний час виконання близько 190 мс.

Тим часом час ArrayListвиконання ініціалізації склав 0 мс. Звичайно, слід враховувати роздільну здатність таймера, але це, швидше за все, менше 15 мс.

Отже, очевидно, є помітна різниця у часі виконання двох методів. Здається, що у двох методів ініціалізації дійсно є деяка накладні витрати.

І так, було складено 1000 .classфайлів, складених Test1тестовою програмою подвійної дужки ініціалізації.


10
"Мабуть" є оперативним словом. Якщо не виміряно, жодні твердження про ефективність не мають сенсу.
Мисливець на екземпляр

16
Ви зробили таку чудову роботу, я навряд чи хочу це сказати, але в рази Test1 могли домінувати класові навантаження. Було б цікаво побачити, як хтось виконує один екземпляр кожного тесту в циклі for for, скажімо, 1000 разів, а потім запустити його ще раз в секунду для циклу в 1000 або 10000 разів і роздрукувати різницю в часі (System.nanoTime ()). Перший цикл повинен пройти всі ефекти розминки (JIT, classload, наприклад). Обидва тести моделюють різні випадки використання. Я спробую запустити це завтра на роботі.
Джим Ферранс

8
@ Джим Ферранс: Я цілком впевнений, що тестовий раз є від класових навантажень. Але наслідком використання подвійної дужки ініціалізації є справлення з класовими навантаженнями. Я вважаю, що більшість випадків використання для подвійних брекетів init. є для одноразової ініціалізації, тест ближче за умов до типового випадку використання цього типу ініціалізації. Я вважаю, що багаторазові повторення кожного тесту зменшать часовий розрив виконання.
coobird

73
Це доводить, що а) подвійна дужка ініціалізація повільніша; б) навіть якщо ви зробите це 1000 разів, ви, ймовірно, не помітите різниці. І це не так, як це може бути вузьким місцем у внутрішній петлі. Це накладає крихітний одноразовий штраф НА ДУЖЕ СКОРО.
Майкл Майерс

15
Якщо використання DBI робить код більш читабельним або виразним, тоді використовуйте його. Той факт, що це трохи збільшує роботу, яку повинен виконувати JVM, не є вагомим аргументом проти неї. Якби це було, то ми також повинні турбуватися про додаткові допоміжні методи / класи, віддаючи перевагу замість величезних класів із меншою кількістю методів ...
Rogério

105

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

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

Друге питання: Новий HashSet повинен бути "цим", що використовується в ініціалізаторі екземпляра ... чи може хтось пролити світло на механізм? Я б наївно очікував, що "це" стосується об'єкта, що ініціалізує "аромати".

Саме так працюють внутрішні класи. Вони отримують свої власні this, але вони також мають вказівники на батьківський екземпляр, так що ви також можете викликати методи на об'єкт, що містить. У випадку конфлікту імен, внутрішній клас (у вашому випадку HashSet) має перевагу, але ви можете встановити префікс "це" з ім'ям класу, щоб отримати і зовнішній метод.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

Щоб зрозуміти створений анонімний підклас, ви також можете визначити методи. Наприклад, переосмисленняHashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

5
Дуже гарна точка прихованої посилання на клас, що містить. У оригінальному прикладі ініціалізатор екземпляра викликає метод add () нового HashSet <String>, а не Test.this.add (). Це говорить про те, що відбувається щось інше. Чи існує анонімний внутрішній клас для HashSet <String>, як пропонує Натан Кухня?
Джим Ферранс

Посилання на клас, що містить, також може бути небезпечним, якщо йдеться про серіалізацію структури даних. Клас, що містить, також буде серіалізований і, таким чином, повинен бути серіалізаційним. Це може призвести до незрозумілих помилок.
Просто ще один програміст Java

56

Кожного разу, коли хтось використовує подвійну ініціалізацію дужок, кошеня вбивається.

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

1. Ви створюєте занадто багато анонімних класів

Кожен раз, коли ви використовуєте подвійну дужку ініціалізації, робиться новий клас. Наприклад, такий приклад:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... буде виробляти наступні класи:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

Це зовсім небагато накладних витрат для вашого навантажувача - ні за що! Звичайно, це не займе багато часу для ініціалізації, якщо ви зробите це один раз. Але якщо ви зробите це 20 тис. Разів протягом свого корпоративного додатку ... вся ця купа пам’яті лише за трохи «синтаксичного цукру»?

2. Ви потенційно створюєте витік пам'яті!

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

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

Тепер повернення Mapміститиме посилання на примірник, що додається ReallyHeavyObject. Ви, мабуть, не хочете ризикувати цим:

Тут витоку пам'яті

Зображення з http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Ви можете зробити вигляд, що у Java є літеральна карта

Щоб відповісти на ваше актуальне запитання, люди використовують цей синтаксис, роблячи вигляд, що у Java є щось на зразок літеральних карт, подібних до існуючих літералів масиву:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

Деякі люди можуть вважати це синтаксично стимулюючим.


7
Врятуйте кошенят! Хороша відповідь!
Aris2World

36

Беручи наступний тестовий клас:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

а потім розкладаючи файл класу, я бачу:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

Це не здається мені надзвичайно неефективним. Якби я хвилювався за продуктивність за щось подібне, я б це профілював. І на ваше запитання №2 відповідає наведений вище код: Ви знаходитесь в неявному конструкторі (і ініціалізаторі екземпляра) для вашого внутрішнього класу, тому " this" відноситься до цього внутрішнього класу.

Так, цей синтаксис незрозумілий, але коментар може уточнити незрозуміле використання синтаксису. Для уточнення синтаксису більшість людей знайомі зі статичним блоком ініціалізаторів (JLS 8.7 Static Initializer):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Ви також можете використовувати подібний синтаксис (без слова " static") для використання в конструкторі (JLS 8.6 Instance Initializer), хоча я ніколи не бачив цього, що використовується у виробничому коді. Це набагато рідше відомо.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Якщо у вас немає конструктора за замовчуванням, то блок коду між {і }перетворюється в конструктор компілятором. Зважаючи на це, розгадайте подвійний дуговий код:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

Блок коду між найбільш внутрішніми дужками перетворювач перетворюється на конструктор. Зовнішні брекети розмежовують анонімний внутрішній клас. Щоб зробити це останнім кроком, щоб зробити все неанонімним:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

Для цілей ініціалізації я б сказав, що немає накладних витрат (або настільки малих, що їх можна знехтувати). Однак, кожне використання flavorsбуде йти не проти, HashSetа навпаки MyHashSet. Ймовірно, невелика (і цілком можлива мізерна) накладні витрати на це. Але знову ж таки, перш ніж я переживатиму про це, я б це профілював.

Знову ж таки, до вашого питання №2, наведений вище код є логічним і явним еквівалентом подвійної дужки ініціалізації, і це дає зрозуміти, куди " this" посилається: До внутрішнього класу, який поширюється HashSet.

Якщо у вас є питання щодо деталей ініціалізаторів екземплярів, ознайомтеся з деталями документації на JLS .


Едді, дуже приємне пояснення. Якщо байтові коди JVM будуть такими ж чистими, як і декомпіляція, швидкість виконання буде досить швидкою, хоча я б трохи занепокоєний додатковим завантаженням файлу .class. Мені все ще цікаво, чому конструктор ініціалізатора екземпляра бачить "це" як новий екземпляр HashSet <String>, а не тестовий екземпляр. Це просто чітко визначена поведінка в останній специфікації мови Java для підтримки ідіоми?
Джим Ферранс

Я оновив свою відповідь. Я покинув котельну таблицю класу Тест, що викликало плутанину. Я вкладаю це у свою відповідь, щоб зробити речі більш очевидними. Я також згадую розділ JLS для блоків ініціалізаторів екземпляра, які використовуються в цій ідіомі.
Едді

1
@Jim Інтерпретація "цього" не є особливим випадком; він просто посилається на екземпляр самого внутрішнього закритого класу, який є анонімним підкласом HashSet <String>.
Кухня Натана

Вибачте, що стрибайте через чотири з половиною роки. Але приємна річ у декомпільованому файлі класу (ваш другий блок коду) полягає в тому, що це не дійсна Java! Він є super()другим рядком неявного конструктора, але він повинен вийти першим. (Я перевірив це, і він не
складеться

1
@ chiastic-security: Іноді декомпілятори генерують код, який не буде компілюватися.
Едді

35

схильний до витоку

Я вирішив прозвучати. ​​Вплив на продуктивність включає: роботу диска + розпакування (для баночки), перевірку класів, простір перм-генів (для JVM Sun's Hotspot). Однак, найгірше: схильний до витоку. Ви не можете просто повернутися.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

Отже, якщо набір переходить на будь-яку іншу частину, завантажену іншим завантажувачем класів, і там зберігається посилання, все дерево класів + завантажувач буде просочено. Щоб уникнути цього, копію HashMap необхідно, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Більше не так мило. Я сам не використовую ідіому, натомість це схоже new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
На щастя, що стосується Java 8, PermGen - це вже не річ. Думаю, все ще є вплив, але не такий, який має потенційно дуже незрозуміле повідомлення про помилку.
Джої

2
@Joey, має нульову різницю, якщо пам'ять управляється безпосередньо GC (perm gen) чи ні. Витік у метапросторі все ще залишається витоком, якщо мета обмежена, не буде OOM (з пермського роду) такі речі, як oom_killer в Linux, збираються почати.
bestsss

19

Завантаження багатьох класів може додати кілька мілісекунд до початку. Якщо запуск не так критичний, і ви дивитесь на ефективність класів після запуску, різниці немає.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

відбитки

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

2
Немає різниці, крім того, що ваш простір PermGen випарується, якщо DBI використовується надмірно. Принаймні, це буде, якщо ви не встановите деякі незрозумілі параметри JVM, щоб дозволити розвантаження класів та збирання сміття простору PermGen. Зважаючи на поширеність Java як серверної мови, питання пам'яті / PermGen вимагає хоча б згадки.
aroth

1
@aroth, це хороший момент. Я визнаю, що за 16 років роботи над Java я ніколи не працював над системою, де вам довелося налаштувати PermGen (або Metaspace). Для систем, які я працював над розміром коду, завжди було досить мало.
Пітер Лорі

2
Чи не повинні умови compareCollectionsпоєднуватись, ||а не &&? Використання &&здається не тільки семантично неправильним, але протидіє наміру вимірювати ефективність, оскільки лише перша умова буде перевірена взагалі. Крім того, розумний оптимізатор може визнати, що умови ніколи не змінюватимуться під час ітерацій.
Холгер

@aroth лише як оновлення: оскільки Java 8, VM вже не використовує жодної хімічної речовини.
Angel O'Sphere

16

Для створення наборів ви можете використовувати заводський метод varargs замість подвійної дужки ініціалізації:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

Бібліотека колекцій Google має безліч зручних методів, таких як цей, а також безліч інших корисних функцій.

Що стосується незрозумілості ідіоми, я стикаюся з нею і весь час використовую її у виробничому коді. Мене більше турбують програмісти, котрі плутають ідіому, дозволену писати виробничий код.


Га! ;-) Я фактично Rip van Winkle, що повернувся до Яви через 1,2 дня (я написав голосовий веб-браузер VoiceXML на evolution.voxeo.com на Java). Це було весело, вивчаючи дженерики, параметризовані типи, колекції, java.util.concurrent, нові для синтаксису циклу тощо. Зараз це краща мова. На ваш погляд, навіть незважаючи на те, що механізм, що стоїть за DBI, може спочатку здаватися незрозумілим, значення коду має бути досить зрозумілим.
Джим Ферранс

10

Ефективність осторонь, я рідко виявляю бажання створити декларативні збори поза одиничними тестами. Я вважаю, що синтаксис подвійної дужки дуже читабельний.

Ще один спосіб досягти декларативної побудови списків - це використовувати Arrays.asList(T ...)так:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

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


1
Arrays.asList () - це те, що я зазвичай використовую, але ти маєш рацію, така ситуація виникає переважно в одиничних тестах; реальний код побудує списки із запитів БД, XML тощо.
Джим Ферранс

7
Однак остерігайтеся asList: повернений список не підтримує додавання та видалення елементів. Кожен раз, коли я використовую asList, я передаю отриманий список конструктору, як new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))би подолати цю проблему.
Майкл Майерс

7

Звичайно, в цьому немає нічого особливо неефективного. Як правило, JVM не має значення, що ви створили підклас і додали до нього конструктор - це звичайна, щоденна річ, що потрібно робити об'єктно-орієнтованою мовою. Я можу придумати досить надумані випадки, коли ви могли б викликати неефективність, роблячи це (наприклад, у вас є багаторазово викликаний метод, який закінчує отримання суміші різних класів через цей підклас, тоді як звичайний клас, який передається, був би цілком передбачуваним, - в останньому випадку компілятор JIT може зробити оптимізацію, що в першому не здійснено). Але насправді, я думаю, випадки, коли це буде мати значення, дуже надумані.

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

У (2) ви знаходитесь всередині конструктора об'єкта, тому "це" відноситься до об'єкта, який ви будуєте. Це не відрізняється від будь-якого іншого конструктора.

Щодо (3), я думаю, що насправді залежить від того, хто підтримує ваш код. Якщо ви не знаєте цього заздалегідь, то орієнтир, який я б запропонував використовувати, - це "ви бачите це у вихідному коді до JDK?" (у цьому випадку я не пригадую, щоб бачив багато анонімних ініціалізаторів, і, звичайно, не у випадках, коли це єдиний вміст анонімного класу). У більшості проектів середнього розміру я стверджую, що вам дійсно знадобляться ваші програмісти, щоб зрозуміти джерело JDK в той чи інший момент, тому будь-який синтаксис або ідіома там використовується "чесна гра". Крім того, я б сказав, навчіть людей до цього синтаксису, якщо ви маєте контроль над тим, хто підтримує код, інакше коментуйте або уникайте.


5

Подвійна ініціалізація - це зайвий злом, який може спричинити витоки пам’яті та інші проблеми

Немає законних причин використовувати цей "трюк". Guava пропонує приємні незмінні колекції, що включають як статичні заводи, так і будівельників, що дозволяє заповнити свою колекцію там, де вона оголошена чистим, читабельним та безпечним синтаксисом.

Прикладом у питанні стає:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

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

Кожного разу, коли ви обмірковуєте питання про подвійну ідентифікаційну ініціалізацію, вам слід переглянути свої API або ввести нові, щоб правильно вирішити проблему, а не скористатися синтаксичними підказками.

Помилка, схильна до помилок, тепер позначає цю антидіаграму .


-1. Незважаючи на деякі достовірні моменти, ця відповідь зводиться до "Як уникнути генерування непотрібного анонімного класу? Використовуйте рамки з ще більшими класами!"
Agent_L

1
Я б сказав, що це зводиться до "використання правильного інструменту для роботи, а не злому, який може зламати вашу програму". Guava - це досить поширена бібліотека для включення додатків (ви її точно не вистачаєте, якщо не використовуєте її), але навіть якщо ви не хочете її використовувати, ви можете і все ж уникати подвійної дужки ініціалізації.
dimo414

І як саме подвійна ініціалізація дужок призведе до витоку меорій?
Angel O'Sphere

@ AngelO'Sphere DBI - це неясний спосіб створення внутрішнього класу , і таким чином зберігається неявна посилання на його клас, що охоплює (якщо тільки ніколи не використовується в staticконтекстах). Посилання "Схильна до помилок" внизу мого питання обговорює це далі.
dimo414

Я б сказав, що це питання смаку. І немає нічого насправді задумливого про це.
Angel O'Sphere

4

Я досліджував це і вирішив зробити більш глибокий тест, ніж той, який надає дійсна відповідь.

Ось код: https://gist.github.com/4368924

і це мій висновок

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

Цікаво, що випадок, який створює 3 об'єкти на циклі, втрачає свою користь, швидше, ніж у інших випадках. Я не впевнений, чому це відбувається, і потрібно зробити ще тестування, щоб дійти до будь-яких висновків. Створення конкретних реалізацій може допомогти уникнути перезавантаження визначення класу (якщо це відбувається)

Однак очевидно, що в більшості випадків для будівництва одного елемента спостерігається не надто великі витрати, навіть при великій кількості.

Одним із повернених результатів буде той факт, що кожна з ініціатив подвійної дужки створює новий файл класу, який додає цілий блок диска до розміру нашого додатка (або близько 1 к при стисненні). Невеликий слід, але якщо його використовувати в багатьох місцях, він може мати вплив. Використовуйте це 1000 разів, і ви потенційно додасте цілий MiB до програми, що може стосуватися вбудованого середовища.

Мій висновок? Використовувати його можна нормально, доки його не зловживають.

Дайте мені знати, що ви думаєте :)


2
Це не є правильним тестом. Код створює об'єкти, не використовуючи їх, що дозволяє оптимізатору схилити все створення екземпляра. Єдиним залишеним побічним ефектом є просування послідовності випадкових чисел, накладні витрати яких переважують будь-що інше в цих тестах.
Холгер

3

Я другий відповідь Ната, за винятком того, що я б використовував цикл замість створення та негайного викидання неявного списку з asList (елементів):

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

1
Чому? Новий об'єкт буде створений в просторі eden, і тому для створення екземпляра потрібно лише два або три доповнення вказівника. JVM може помітити, що він ніколи не виходить за межі методу, і тому виділяє його на стек.
Нат

Так, це, швидше за все, виявиться більш ефективним, ніж цей код (хоча ви можете покращити його, повідомивши HashSetзапропоновану ємність - запам’ятайте коефіцієнт навантаження).
Том Хотін - тайклін

Що ж, конструктор HashSet повинен робити ітерацію в будь-якому випадку, тому він не буде менш ефективним. Код бібліотеки, створений для повторного використання, завжди повинен прагнути бути максимально можливим.
Лоуренс Дол

3

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


3

Маріо Гліхман описує, як використовувати загальні функції Java 1.5 для імітації літералів Scala List, хоча, на жаль, ви перебуваєте з незмінними списками.

Він визначає цей клас:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

і використовує його таким чином:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

Google Collections, яка зараз є частиною Guava, підтримує подібну ідею створення списків. У цьому інтерв'ю Джаред Леві каже:

[...] найбільш широко використовувані функції, які з'являються майже в кожному класі Java, який я пишу, - це статичні методи, що зменшують кількість повторюваних натискань клавіш у вашому Java-коді. Це так зручно, що можна вводити такі команди, як:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

10.07.2014: Якби це було так просто, як Python:

animals = ['cat', 'dog', 'horse']

21.02.2020: На Java 11 тепер можна сказати:

animals = List.of(“cat”, “dog”, “horse”)


2
  1. Це вимагатиме add()кожного члена. Якщо ви можете знайти більш ефективний спосіб вкласти елементи в хеш-набір, тоді використовуйте це. Зауважте, що внутрішній клас, ймовірно, генерує сміття, якщо ви до цього ставитеся.

  2. Мені здається, ніби контекст - це об'єкт, повернутий new, який є HashSet.

  3. Якщо вам потрібно запитати ... Швидше: люди, які приходять після вас, це знають чи ні? Чи легко це зрозуміти і пояснити? Якщо ви можете відповісти "так" обом, сміливо використовуйте його.

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