Enum в Java. Маленькая недоработка

Эта статья — для тех, кто успешно использует enum'ы и хочет сэкономить память. Для тех, кто не знает, что это, есть статья «Перечисления (enums)».

Несомненно, enum'ы круты и приносят очень много радости. Но вот с чем я столкнулся.

Мусорные объекты

В Java мы порой создаём объекты с очень коротким сроком жизни. Например, в конструкции for (T t : someIterable) создаётся итератор, который становится мусором, когда цикл заканчивается. Если someIterable instanceof List && someIterable instanceof RandomAccess, то итератор не нужен.

Есть множество ситуаций, в которых используют защитное копирование, например, метод List.toArray() вернёт новый массив. И это зачастую оправдано.

Обо всём этом мусоре, несомненно, заботится Generational GC в HotSpot и Concurrent GC в Android Runtime, но всё же стоит соблюдать осторожность. Например, переопределяя метод View.onDraw(), лучше обходить списки, не создавая итератор, как это делают for-each loop в Java и Kotlin, а по числовым индексам.

Мы же говорили про Enum'ы?

Если нужно сохранить значение enum'а для длительного хранения, например, в базу данных, можно сохранить его строчное представление (someEnumValue.name()). И, наоборот, получить значение enum'а по его строчному представлению (SomeEnum.valueOf(someEnumName)).

Если нужно сохранить значение enum'а для кратковременного и очень быстрого транспорта, например, реализовывая интерфейс Parcelable, лучше использовать порядковый номер (someEnumValue.ordinal()): в пределах одной запущенной виртуальной машины он не может измениться.

Чтобы выполнить обратное преобразование, можно написать SomeEnum.values()[someEnumOrdinal]. К сожалению, в Java нет неизменяемых массивов. Поэтому генерируемый компилятором метод values() каждый раз возвращает новый клон массива, взятого из синтетического, скрытого от посторонних глаз статического поля $VALUES. Конечно, никто не будет держать в массиве тысячи вариантов, так что клонирование массива происходит быстро, но сам по себе факт доступа за O(n) вызывает уныние. Да и у такого кода будут проблемы:

for (int i = 0; i < SomeEnum.values().length; i++) {
    if (SomeEnum.values()[i].someValue == 0) {
        System.out.println(SomeEnum.values()[i] + ".someValue равно нулю");
    } else if (SomeEnum.values()[i].someValue == 1) {
        System.out.println(SomeEnum.values()[i] + ".someValue равно единице");
    } else {
        System.out.println(SomeEnum.values()[i] + ".someValue = " + SomeEnum.values()[i].someValue);
    }
}

Конечно, пример утрирован, но похожие конструкции с беспорядочным выделением новых массивов в цикле мне встречались.

Решение

При экстремальной нехватке памяти я решал проблему, скопировав и закешировав массив единожды:

enum SomeEnum {
    SomeValue(0), AnotherValue(1), OneMoreValue(2);

    private static final SomeEnum[] VALS = values();
    
    public static SomeEnum valueOf(int ordinal) {
        return VALS[ordinal];
    }
    
    …
}

Другой способ, который приходит на ум, заключается в непосредственном использовании значения синтетического поля $VALUES, но для этого нужно либо договариваться с компилятором, либо использовать reflections, что в данной ситуации неоправдано.

Общедоступный список значений

Ещё одно решение заключается в создании неизменяемого списка из массива:

enum SomeEnum {
    SomeValue(0), AnotherValue(1), OneMoreValue(2);

    public static final List VALUES = 
    		Collections.unmodifiableList(Arrays.asList(values()));
    
    …
}

Грязный хак

Задолго после публикации этой статьи нашёл ещё один способ — получиение общего массива значений с помощью sun.misc.SharedSecrets.

Вывод

Конечно, нужно экономить память. Нужно ли запариваться этим так, как я? Решайте сами.

Комментарии к статье