Главная » Java, Советы » Не создавайте дублирующих объектов

0

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

Рассмотрим оператор, демонстрирующий, как делать не надо:

 

String s = new String("silly");                                              / / Никогда не делайте так!

 

При каждом проходе этот оператор создает новый экземпляр String, но ни одна из процедур создания объектов не является необходимой. Аргумент конструктора String – "silly" – сам является экземпляром класса String и функционально равнозначен всем объектам, создаваемым конструктором. Если этот оператор попадает в цикл или в часто вызываемый метод, без всякой надобности могут создаваться миллионы экземпляров String.

Исправленная версия выглядит просто:

String s = "No longer silly";

В этом варианте используется единственный экземпляр String вместо создания новых при каждом проходе. Более того, гарантируется, что этот объект будет повторно использоваться любым другим программным кодом, выполняемым на той же виртуальной машине, где содержится эта строка-константа [JLS, 3.10.5]. Создания дублирующих объектов часто можно избежать, если неизменяемом классе, имеющем и конструкторы, и статические методы генерации (статья 1), предпочесть вторые первым. Например, статический метод генерации Boolean.val’ueOf(String) почти всегда предпочтительнее конструктора Boolean(String). При каждом вызове конструктор создает новый объект, тогда как от статического метода генерации этого не требуется. Вы можете повторно использовать не только неизменяемые объекты, но и изменяемые, если знаете, что последние уже не будут меняться. Рассмотрим более тонкий и более распространенный пример того, как не надо поступать, в том Числе с изменяемыми объектами, которые, будучи получены один раз, впоследствии остаются без Изменений:

 

import java.util.*;

public class Person {

    private final Date birthDate;

 

  // Прочие поля опущены

 

  public Person(Date birthDate) {

        this.birthDate = birthDate;

    }

 

      // Никогда не делайте так!

  

 public boolean isBabyBoomer() {

        Calendar gmtCal =

            Calendar.getInstance(TimeZone.getTimeZone("GMT"));

        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);

        Date boomStart = gmtCal.getTime();

        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);

        Date boomEnd = gmtCal.getTime();

        return birthDate.compareTo(boomStart) >= 0 &&

               birthDate.compareTo(boomEnd)   <  0;

    }

 

    public static void main(String[] args) {

        Person p = new Person(new Date());

 

        long startTime = System.currentTimeMillis();

        for (int i=0; i<1000000; i++)

            p.isBabyBoomer();

        long endTime = System.currentTimeMillis();

        long time = endTime – startTime;

        System.out.println(time+" ms.");

    }

}

 

Метод isBabyBoomer при каждом вызове создает без всякой надобности новые экземпляры Calendar, TimeZone и два экземпляра Date. В следующей версии подобная расточительность пресекается с помощью статического инициализатора:

import java.util.*;

class Person {

    private final Date birthDate;

   public Person(Date birthDate) {

        this.birthDate = birthDate;

}

     /**

     * Даты начала и конца демографического взрыва

     */

    private static final Date BOOM_START;

    private static final Date BOOM_END;

  static {

        Calendar gmtCal =

            Calendar.getInstance(TimeZone.getTimeZone("GMT"));

        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);

        BOOM_START = gmtCal.getTime();

        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);

        BOOM_END = gmtCal.getTime();

    }

      public boolean isBabyBoomer() {

        return birthDate.compareTo(BOOM_START) >= 0 &&

               birthDate.compareTo(BOOM_END)   <  0;

    }

      public static void main(String[] args) {

        Person p = new Person(new Date());

        long startTime = System.currentTimeMillis();

        for (int i=0; i<1000000; i++)

            p.isBabyBoomer();

        long endTime = System.currentTimeMillis();

        long time = endTime – startTime;

        System.out.println(time+" ms.");

    }     }

В исправленной версии класса Person экземпляры Calendar, ТimeZone и Date создаются только один раз в ходе инициализации, а не при каждом вызове метода isBabyBoomer. Если данный метод вызывается часто, это приводит к значительному выигрышу в производительности. На моей машине исходная версия программы тратит на миллион вызовов 36000 мс, улучшенная – 370 мс, т. е. она работает в сто раз быстрее. Причем повышается не только производительность программы, но и наглядность. Замена локальных переменных boomStart и boomEnd статическими полями типа final показывает, что эти даты рассматриваются как константы, и программный код становится более понятным. Для полной ясности заметим, что экономия от подобной оптимизации не всегда будет столь впечатляющей, просто здесь много ресурсов требует создание экземпляров Calendar.

Если метод isBabyBoomer вызываться не будет, инициализация полей BOOM_START и BOOM_END в улучшенной версии класса Person окажется напрасной. Ненужных действий можно избежать, использовав для этих полей отложенную инициализацию (lazily initializing) (статья 48), которая бы выполнялась при первом вызове метода     

      14

isBabyBoomer, однако делать это не рекомендуется. Как часто бывает в случаях с отложенной инициализацией, это усложнит реализацию и вряд ли приведет к заметному повышению производительности (статья 37).

Во всех примерах, приведенных в этой статье, было очевидным то, что рассматриваемые объекты можно использовать повторно, поскольку они неизменяемые. Однако в ряде ситуаций это не столь очевидно. Рассмотрим случай с адаптерами (adapter) [Сатта95, стр. 139], известными также как представления (view). Адаптер – это объект, который делегирован нижележащим объектом и который создает для него альтернативный интерфейс. Адаптер не имеет иных состояний, помимо состояния нижележащего объекта, поэтому для адаптера, представляющего данный объект, не нужно создавать более одного экземпляра.

Например, в интерфейсе Мар метод keySet возвращает для объекта Мар представление Set, которое содержит все ключи данной схемы. По незнанию можно подумать, что каждый вызов метода keySet должен создавать новый экземпляр Set. Однако в действительности для некоего объекта Мар любые вызовы keySet могут возвращать один и тот же экземпляр Set. И хотя обычно возвращаемый экземпляр Set является изменяемым, все’ возвращаемые объекты функционально идентичны: когда меняется один из них, то же самое происходит со всеми остальными экземплярами Set, поскольку за всеми ними стоит один и тот же экземпляр Map.

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

И наоборот, отказ от создания объектов и поддержка собственного пула объектов (object pool) – плохая идея, если только объекты в этом пуле не будут крайне ресурсоемкими. Основной пример объекта, для которого оправданно создание пула, – соединение с базой данных (database connection). Затраты на установление такого соединения высоки, и потому лучше обеспечить многократное использование этого объекта. Однако в общем случае создание собственного пула объектов загромождает.  Современные реализации JVM имеют хорошо оптимизированные сборщики мусора, которые при работе с небольшими объектами с легкостью превосходят подобные пулы объектов.

В противовес этой статье можно привести статью 24, посвященную резервному копированию (defensive copying). Если в настоящей статье говориться: ”Не создавайте новый объект, если вы обязаны исполнять имеющийся еще раз”, то статья 24 гласит: ” Не надо использовать имеющийся объект еще раз, если вы обязаны создать новый”. Заметим, что ущерб от повторного применения объекта, когда требуется резервное копирование, значительно превосходит ущерб от бесполезного создания дублирующего объекта. Отсутствие резервных копий там, где они необходимы, может привести к коварным ошибкам и дырам в системе безопасности, создание же ненужных объектов всего лишь влияет на стиль и производительность программы.

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии