В разделе « Скрытые возможности Java» в верхнем ответе упоминается инициализация двойной скобки с очень заманчивым синтаксисом:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
Эта идиома создает анонимный внутренний класс с инициализатором экземпляра, который «может использовать любые [...] методы в содержащей области».
Главный вопрос: это так же неэффективно, как кажется? Следует ли ограничить его использование одноразовыми инициализациями? (И конечно же хвастаться!)
Второй вопрос: новый HashSet должен быть «этим», используемым в инициализаторе экземпляра ... может кто-нибудь пролить свет на механизм?
Третий вопрос: слишком ли неясна эта идиома для использования в рабочем коде?
Резюме: Очень, очень хорошие ответы, спасибо всем. На вопрос (3) люди считали, что синтаксис должен быть ясным (хотя я бы рекомендовал время от времени комментировать, особенно если ваш код будет передаваться разработчикам, которые могут быть не знакомы с ним).
На вопрос (1) сгенерированный код должен работать быстро. Дополнительные файлы .class вызывают беспорядок в jar-файле и слегка замедляют запуск программы (спасибо @coobird за это). @Thilo отметил, что это может повлиять на сборку мусора, и стоимость памяти для дополнительных загруженных классов может быть фактором в некоторых случаях.
Вопрос (2) оказался наиболее интересным для меня. Если я понимаю ответы, то в DBI происходит то, что анонимный внутренний класс расширяет класс объекта, создаваемого оператором new, и, следовательно, имеет значение «this», ссылающееся на создаваемый экземпляр. Очень аккуратный.
В целом, 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 .
источник
flavors
чтобы бытьHashSet
, но , увы , это анонимный подкласс.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Ответы:
Вот проблема, когда я слишком увлекаюсь анонимными внутренними классами:
Это все классы, которые были сгенерированы, когда я создавал простое приложение и использовал большое количество анонимных внутренних классов - каждый класс будет скомпилирован в отдельный
class
файл.«Двойная инициализация», как уже упоминалось, является анонимным внутренним классом с блоком инициализации экземпляра, что означает, что для каждой «инициализации» создается новый класс, обычно с целью создания единого объекта.
Учитывая, что виртуальной машине Java необходимо будет читать все эти классы при их использовании, это может привести к некоторому времени в процессе проверки байт-кода и тому подобное. Не говоря уже об увеличении необходимого дискового пространства для хранения всех этих
class
файлов.Кажется, что при использовании инициализации с двойными скобками есть некоторые издержки, так что, вероятно, не такая уж хорошая идея, чтобы зацикливаться на этом. Но, как заметил Эдди в комментариях, невозможно быть абсолютно уверенным в воздействии.
Просто для справки, инициализация двойной скобки следующая:
Это похоже на «скрытую» особенность Java, но это просто переписывание:
Таким образом, это в основном блок инициализации экземпляра, который является частью анонимного внутреннего класса .
Предложение Коллекционных Литералов Джошуа Блоха для Project Coin было следующим:
К сожалению, он не проник ни в Java 7, ни в 8 и был отложен на неопределенный срок.
эксперимент
Вот простой эксперимент, который я протестировал: 1000
ArrayList
с элементами"Hello"
и"World!"
добавление к ним с помощьюadd
метода, используя два метода:Метод 1: инициализация двойной скобки
Метод 2: создание
ArrayList
иadd
Я создал простую программу для записи исходного файла Java для выполнения 1000 инициализаций, используя два метода:
Тест 1:
Тест 2:
Обратите внимание, что истекшее время инициализации
ArrayList
расширения 1000 с и 1000 анонимных внутренних классовArrayList
проверяется с помощьюSystem.currentTimeMillis
, поэтому таймер не имеет очень высокого разрешения. В моей системе Windows разрешение составляет около 15-16 миллисекунд.Результаты 10 прогонов двух тестов были следующими:
Как видно, инициализация двойной скобки имеет заметное время выполнения около 190 мс.
Между тем время
ArrayList
выполнения инициализации оказалось равным 0 мс. Конечно, следует учитывать разрешение таймера, но оно, вероятно, будет ниже 15 мс.Таким образом, кажется, есть заметная разница во времени выполнения этих двух методов. Похоже, что в двух методах инициализации действительно есть издержки.
И да, было
.class
сгенерировано 1000 файлов при компиляции программыTest1
инициализации с двойной скобкой.источник
Одно из свойств этого подхода, которое не было упомянуто до сих пор, состоит в том, что, поскольку вы создаете внутренние классы, весь содержащий класс захватывается в его область видимости. Это означает, что пока ваш Set жив, он будет хранить указатель на содержащий экземпляр (
this$0
) и удерживать его от сбора мусора, что может быть проблемой.Это и тот факт, что новый класс создается в первую очередь, даже если обычный HashSet работал бы просто отлично (или даже лучше), заставляет меня не хотеть использовать эту конструкцию (даже если я действительно жажду синтаксического сахара).
Так работают внутренние классы. Они получают свои собственные
this
, но у них также есть указатели на родительский экземпляр, так что вы также можете вызывать методы для содержащего объекта. В случае конфликта именования внутренний класс (в вашем случае HashSet) имеет приоритет, но вы можете добавить префикс «this» к имени класса, чтобы также получить внешний метод.Для ясности в отношении создаваемого анонимного подкласса, вы также можете определить методы в нем. Например переопределить
HashSet.add()
источник
Каждый раз, когда кто-то использует двойную инициализацию, котенка убивают.
Помимо того, что синтаксис довольно необычен и не очень идиоматичен (вкус спорен, конечно), вы без необходимости создаете две существенные проблемы в своем приложении, о которых я недавно недавно более подробно писал в блоге .
1. Вы создаете слишком много анонимных классов
Каждый раз, когда вы используете двойную инициализацию, создается новый класс. Например, этот пример:
... будет производить эти классы:
Это довольно много для вашего загрузчика классов - ни за что! Конечно, это не займет много времени инициализации, если вы сделаете это один раз. Но если вы сделаете это 20 000 раз по всему корпоративному приложению ... всю эту кучу памяти только для небольшого количества "синтаксического сахара"?
2. Вы потенциально создаете утечку памяти!
Если вы возьмете приведенный выше код и вернете эту карту из метода, вызывающие этого метода могут ничего не подозревать о слишком больших ресурсах, которые невозможно собрать мусором. Рассмотрим следующий пример:
Возвращенный
Map
теперь будет содержать ссылку на включающий экземплярReallyHeavyObject
. Вы, вероятно, не хотите рисковать этим:Изображение с http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/
3. Вы можете делать вид, что у Java есть литералы карты
Чтобы ответить на ваш реальный вопрос, люди использовали этот синтаксис, притворяясь, что в Java есть что-то вроде литералов карты, аналогично существующим литералам массива:
Некоторые люди могут найти это синтаксически стимулирующим.
источник
Принимая следующий тестовый класс:
и затем декомпилируя файл класса, я вижу:
Это не выглядит ужасно неэффективным для меня. Если бы я беспокоился о производительности для чего-то подобного, я бы профилировал это. И на ваш вопрос № 2 отвечает приведенный выше код: вы находитесь внутри неявного конструктора (и инициализатора экземпляра) для вашего внутреннего класса, поэтому
this
" относится к этому внутреннему классу.Да, этот синтаксис неясен, но комментарий может прояснить использование неясного синтаксиса. Чтобы прояснить синтаксис, большинство людей знакомы со статическим блоком инициализатора (JLS 8.7 Статические инициализаторы):
Вы также можете использовать аналогичный синтаксис (без слова "
static
") для использования в конструкторе (инициализаторы экземпляров JLS 8.6), хотя я никогда не видел, чтобы это использовалось в рабочем коде. Это гораздо менее общеизвестно.Если у вас нет конструктора по умолчанию, тогда блок кода между
{
и}
превращается компилятором в конструктор. Имея это в виду, распутайте код двойной скобки:Блок кода между внутренними фигурными скобками превращается компилятором в конструктор. Внешние скобки ограничивают анонимный внутренний класс. Чтобы сделать этот последний шаг сделать все не анонимным:
В целях инициализации я бы сказал, что нет никаких накладных расходов (или настолько малых, что ими можно пренебречь). Тем не менее, каждое использование
flavors
будет идти не против,HashSet
а противMyHashSet
. Вероятно, есть небольшие (и вполне возможно незначительные) накладные расходы на это. Но опять же, прежде чем я беспокоюсь об этом, я бы профилировать это.Опять же, на ваш вопрос № 2, приведенный выше код является логическим и явным эквивалентом инициализации двойной скобки, и это делает очевидным, где "
this
" относится: к внутреннему классу, который расширяетсяHashSet
.Если у вас есть вопросы о деталях инициализаторов экземпляров, ознакомьтесь с подробностями в документации JLS .
источник
super()
в качестве второй строки неявного конструктора, но он должен стоять первым. (Я проверил его, и он не скомпилируется.)подвержен утечкам
Я решил вмешаться. Влияние на производительность включает в себя: работа с диском + распаковка (для jar), проверка классов, пространство perm-gen (для JVM Sun's Hotspot). Однако хуже всего: это подвержено утечкам. Вы не можете просто вернуться.
Таким образом, если набор уходит в любую другую часть, загруженную другим загрузчиком классов, и там сохраняется ссылка, все дерево классов + загрузчик классов будет пропущено. Чтобы избежать этого, необходима копия в HashMap
new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})
. Уже не так мило. Я сам не использую идиому, вместо этогоnew LinkedHashSet(Arrays.asList("xxx","YYY"));
источник
Загрузка многих классов может добавить несколько миллисекунд к началу. Если запуск не так критичен, и вы смотрите на эффективность классов после запуска, нет никакой разницы.
печать
источник
compareCollections
сочетании с,||
а не&&
? Использование&&
кажется не только семантически неправильным, оно противодействует намерению измерить производительность, поскольку только первое условие будет проверено вообще. Кроме того, умный оптимизатор может признать, что условия никогда не изменятся во время итераций.Для создания наборов вы можете использовать фабричный метод varargs вместо двойной скобки:
В библиотеке Google Collections есть много удобных методов, таких как этот, а также множество других полезных функций.
Что касается неясности идиомы, я сталкиваюсь с ней и постоянно использую ее в рабочем коде. Я был бы более обеспокоен программистами, которых смущает идиома, позволяющая писать производственный код.
источник
Помимо эффективности, я редко вижу желание создать декларативную коллекцию вне модульных тестов. Я верю, что синтаксис двойной скобки очень читабелен.
Еще один способ добиться декларативного построения списков - использовать
Arrays.asList(T ...)
так:Ограничение этого подхода состоит в том, что вы не можете контролировать конкретный тип создаваемого списка.
источник
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
чтобы обойти эту проблему.В этом нет ничего особенно неэффективного. Обычно для JVM не имеет значения, что вы создали подкласс и добавили в него конструктор - это обычная повседневная вещь на объектно-ориентированном языке. Я могу придумать довольно надуманные случаи, когда вы могли бы сделать это неэффективным (например, у вас есть многократно вызываемый метод, который в итоге принимает смесь разных классов из-за этого подкласса, тогда как обычный передаваемый класс будет полностью предсказуемым). - в последнем случае JIT-компилятор может делать оптимизации, которые не осуществимы в первом). Но на самом деле, я думаю, что случаи, когда это будет иметь значение, очень надуманные.
Я бы видел проблему больше с точки зрения того, хотите ли вы «загромождать вещи» множеством анонимных классов. В качестве грубого руководства рассмотрите возможность использования идиомы не более, чем, например, анонимных классов для обработчиков событий.
В (2) вы находитесь внутри конструктора объекта, поэтому «this» относится к объекту, который вы создаете. Это ничем не отличается от любого другого конструктора.
Что касается (3), это действительно зависит от того, кто поддерживает ваш код, я думаю. Если вы не знаете этого заранее, тогда я бы предложил использовать эталонный тест: «Вы видите это в исходном коде JDK?» (в этом случае я не помню, чтобы видел много анонимных инициализаторов, и, конечно, не в тех случаях, когда это единственный контент анонимного класса). В большинстве проектов среднего размера, я бы сказал, что вам действительно понадобятся ваши программисты, чтобы в какой-то момент понять источник JDK, поэтому любой используемый здесь синтаксис или идиома - это «честная игра». Кроме того, я бы сказал, обучайте людей этому синтаксису, если у вас есть контроль над тем, кто поддерживает код, а также комментируйте или избегайте.
источник
Двойная инициализация - это ненужный хак, который может привести к утечкам памяти и другим проблемам.
Нет законной причины использовать этот «трюк». Guava предоставляет хорошие неизменяемые коллекции, которые включают как статические фабрики, так и сборщики, что позволяет вам заполнить вашу коллекцию так, чтобы она была объявлена чистой, читаемой и безопасной. синтаксисе.
Пример в вопросе становится:
Это не только короче и легче для чтения, но и позволяет избежать многочисленных проблем с двойным шаблоном, описанных в других ответах . Конечно, он работает так же, как непосредственно построенный
HashMap
, но он опасен и подвержен ошибкам, и есть лучшие варианты.Каждый раз, когда вы решаете использовать инициализацию с двойными скобками, вам следует пересмотреть свои API или представить новые. для правильного решения проблемы, а не использовать синтаксические приемы.
Подверженный ошибкам теперь помечает этот анти-шаблон .
источник
static
контекстах). Ссылка на ошибку в нижней части моего вопроса обсуждает это дальше.Я исследовал это и решил сделать более глубокий тест, чем тот, который был дан в действительном ответе.
Вот код: https://gist.github.com/4368924
и это мой вывод
Дайте мне знать, что вы думаете :)
источник
Я второй ответ Ната, за исключением того, что я бы использовал цикл вместо создания и немедленного удаления неявного List из asList (elements):
источник
HashSet
предложенную емкость - запомните коэффициент загрузки).Хотя этот синтаксис может быть удобным, он также добавляет множество ссылок на $ 0, поскольку они становятся вложенными, и может быть трудно выполнить отладку в инициализаторах, если для каждой из них не установлены точки останова. По этой причине я рекомендую использовать это только для банальных сеттеров, особенно для констант, и мест, где анонимные подклассы не имеют значения (например, без сериализации).
источник
Марио Глейхман описывает, как использовать универсальные функции Java 1.5 для имитации литералов Scala List, хотя, к сожалению, вы получаете неизменный списки.
Он определяет этот класс:
и использует это таким образом:
Коллекции Google, теперь часть Guava, поддерживают аналогичную идею для построения списка. В этом интервью Джаред Леви говорит:
10.07.2014: Если бы это было так просто, как в Python:
21.02.2020: В Java 11 теперь можно сказать:
источник
Это будет призывать
add()
каждого участника. Если вы можете найти более эффективный способ поместить элементы в хэш-набор, используйте его. Обратите внимание, что внутренний класс, скорее всего, будет генерировать мусор, если вы чувствительны к этому.Мне кажется, что контекст - это возвращаемый объект
new
, который являетсяHashSet
.Если вам нужно спросить ... Скорее всего: люди, которые придут за вами, знают это или нет? Легко ли это понять и объяснить? Если вы можете ответить «да» на оба вопроса, не стесняйтесь использовать его.
источник