Ненужное создание объектов приводит к расточительности памяти, особенно при использовании оберток примитивных типов, таких как String, Boolean, и регулярных выражений.
Использование строковых литералов, применение Boolean.valueOf(), кэширование шаблонов Pattern и другие методы позволяют минимизировать ненужное создание объектов. При повторном использовании изменяемых объектов следует учитывать защитное копирование.
Автоматическое преобразование типов (автобоксинг) может приводить к снижению производительности при смешивании примитивных типов и их оберток. Поэтому лучше отдавать предпочтение примитивным типам и избегать ненужного создания пулов объектов.
Случаи создания ненужных объектов
Использование new String()
Строки a, b и c все будут содержать строку «hi». Однако, поскольку адреса, на которые ссылаются эти три строки, различны, происходит ненужное выделение памяти для одних и тех же данных.
Поэтому при объявлении строк следует использовать литералы, а не ключевое слово new.
В приведенном выше коде используется только один экземпляр. Более того, при использовании этого метода гарантируется, что все фрагменты кода, использующие строковый литерал «hi» в той же JVM, будут использовать один и тот же объект. Это происходит из-за особенности пула констант Java.
Использование new Boolean()
В приведенном выше коде создается экземпляр Boolean с помощью конструктора, принимающего строку в качестве параметра. Boolean может иметь только значения true или false, поэтому создание экземпляра каждый раз является пустой тратой памяти. Поэтому лучше использовать статический фабричный метод Boolean.valueOf().
Использование String.matches()
Если стоимость создания велика, то лучше кэшировать и повторно использовать объекты, но мы не всегда знаем стоимость создания объекта. Например, если мы хотим написать метод, который проверяет, является ли данная строка допустимым римским числом, то проще всего использовать регулярное выражение следующим образом.
Однако метод String.matches() имеет проблемы с производительностью. Экземпляр Pattern, который этот метод создает внутри себя для регулярных выражений, используется один раз и сразу становится кандидатом на сборку мусора. По мере увеличения частоты использования этого регулярного выражения увеличивается стоимость создания и удаления одинаковых экземпляров Pattern. Поэтому лучше кэшировать экземпляр Pattern заранее и повторно использовать его при каждом вызове метода isRomanNumeral().
Примечание
Во всех приведенных выше примерах кэшируемые ненужные объекты были сделаны неизменяемыми. Это необходимо для безопасного повторного использования. Однако бывают ситуации, когда повторное использование противоречит интуиции неизменяемых объектов.
Адаптер (представление) — это объект, который делегирует фактическую работу объекту бэкэнда, а сам выступает в роли вторичного интерфейса. Адаптеру нужно управлять только объектом бэкэнда, поэтому для каждого объекта бэкэнда достаточно создать только один адаптер.
Например, метод keySet() интерфейса Map возвращает представление Set, содержащее все ключи объекта Map. Пользователь может подумать, что при каждом вызове метода keySet() создается новый экземпляр Set, но на самом деле в реализации JDK возвращается один и тот же изменяемый экземпляр Set.
Это связано с тем, что все функции, выполняемые возвращаемым экземпляром Set, одинаковы, и все экземпляры Set представляют экземпляр Map. Поэтому, даже если keySet() создает несколько объектов представления, это не имеет значения и не приносит никакой пользы.
Поэтому, если изменить экземпляр names1, экземпляр names2 также будет затронут.
Но лично я считаю, что возвращаемое значение метода keySet() должно использовать защитное копирование и возвращать новый объект каждый раз. Если экземпляр Set, полученный с помощью метода keySet(), также используется в другом месте, и в коде есть код, который изменяет состояние этого экземпляра, то мы не сможем быть уверены в значениях используемого в данный момент экземпляра Set и экземпляра Map.
Кроме того, если среда не использует keySet() слишком часто, создание интерфейса Set каждый раз не окажет существенного влияния на производительность. Лучше сделать интерфейс Set неизменяемым и обеспечить стабильность в обслуживании.
Автоматическая распаковка
Автоматическая распаковка — это технология, которая автоматически преобразует примитивные типы и типы-оболочки при смешанном использовании программистом. Однако автоматическая распаковка просто размывает различие между примитивными типами и типами-оболочками, но не устраняет его полностью.
С точки зрения логики проблем нет, но с точки зрения производительности этот код очень неэффективен. Причиной этого является тип sum и тип i внутри цикла for.
Тип sum — Long, а тип i — long. То есть, когда тип long i добавляется к sum во время итерации цикла, создается новый экземпляр Long. В результате следует использовать примитивные типы вместо типов-оболочек и избегать непреднамеренной автоматической распаковки.
Что следует понимать неправильно
Не следует неправильно понимать совет избегать ненужного создания объектов как необходимость избегать их из-за высокой стоимости создания.
В частности, в современных JVM создание и освобождение ненужных небольших объектов не является обременительной задачей. Поэтому не стоит создавать собственный пул объектов, если это не объекты с высокой стоимостью, например, подключение к базе данных.
Более того, помните, что ущерб от повторного использования объектов в ситуациях, когда необходимо защитное копирование, намного больше, чем ущерб от ненужного повторного создания объектов. Побочные эффекты повторного создания влияют только на форму кода и производительность, но сбой защитного копирования приводит к ошибкам и проблемам безопасности.