Как стать автором
Обновить

Java: вещи, которые могут показаться любопытными и разработчику со стажем

Время на прочтение6 мин
Количество просмотров11K
Доброе время суток!

Статья написана по следам публикации «Вещи, которые вы [возможно] не знали о Java» другого автора, которую я бы отнёс к категории «для начинающих». Читая и комментируя её, я понял, что есть некоторое количество достаточно любопытных вещей, которые я узнал, уже программируя на java не один год. Возможно, эти вещи покажутся любопытными кому-то ещё.

Факты, которые, с моей точки зрения, могут быть полезны именно начинающим, я убрал в «спойлеры». Некоторые вещи там всё же могут быть интересны и для более опытных. Например, сам я до момента написания статьи не знал, что Boolean.hashCode(true) == 1231 и Boolean.hashCode(false) == 1237.

для начинающих
  • Boolean.hashCode(true) == 1231
  • Boolean.hashCode(false) == 1237
  • Float.hashCode(value) == Float.floatToIntBits(value)
  • Double.hashCode(value) — xor первого и второго 32-битных полуслов Double.doubleToLongBits(value)

Object.hashCode() уже давно не является адресом объекта в памяти


Оговорка: это деталь jvm от Oracle (HotSpot).

Когда-то очень давно это было так.
Из jdk1.2.1/docs/api/java/lang/Object.html#hashCode():
As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)

Потом от этого отказались. Вот что об этом написано в javadoc для jdk 12.

vladimir_dolzhenko подсказал, что старое поведение можно восстановить при помощи -XX:hashCode=4. И само изменение поведения было чуть ли не с версии java 1.2.

Integer.valueOf(15) == Integer.valueOf(15); Integer.valueOf(128) != Integer.valueOf(128)


Оговорка: это часть jls.

Понятно, при сравнении двух обёрток оператором == (!=) никакого autoboxing не происходит. Вообще говоря, именно первое равенство смущает. Дело в том, что для целых значений i: -129 < i < 128 объекты-обёртки Integer кэшируются. Поэтому для i из этого диапазона Integer.valueOf(i) не создаёт каждый раз новый объект, а возвращает уже созданный. Для i, не попадающих в этот диапазон, Integer.valueOf(i) всегда создаёт новый объект. Поэтому, если пристально не следить за тем, что именно и как именно сравнивается, можно написать код, который вроде бы работает и даже покрыт тестами, но в то же время содержащий такие вот «грабли».

В jvm от Oracle (HotSpot) верхний предел кэширования может быть изменён через property «java.lang.Integer.IntegerCache.high».

в некоторых случаях значения примитивных или строковых final static полей другого класса разрешаются во время компиляции


Звучит запутанно, да и утверждение длинновато. Смысл такой. Если у нас есть класс, в котором определены константы примитивных типов или строк как final static поля с немедленной инициализацией,
class AnotherClass {
    public final static String CASE_1 = "case_1";
    public final static String CASE_2 = "case_2";
}

то при их использовании в других классах,
class TheClass {
    // ...
    public static int getCaseNumber(String caseName) {
        switch (caseName) {
            case AnotherClass.CASE_1:
                return 1;
            case AnotherClass.CASE_2:
                return 2;
            default:
                throw new IllegalArgumentException("value of the argument caseName does not belong to the allowed value set");
        }
    }
}

значения этих констант («case_1», «case_2») разрешаются во время компиляции. И вставляются в код как значения, а не как ссылки. То есть, если мы используем такие константы из библиотеки, а потом получаем новую версию библиотеки, в которой значения констант изменились, нам стоит перекомпилировать проект. Иначе в коде могут продолжать использоваться старые значения констант.

Такое поведение наблюдается во всех местах, где либо обязаны быть использованы константные выражения (например, switch/case), либо компилятору разрешено преобразовывать выражения к константным и он может это сделать.

Эти поля не смогут использоваться в константных выражениях, как только мы уберём немедленную инициализацию, перенеся инициализацию в блок static.

для начинающих

при определённых условиях сборщик мусора может быть ни разу не запущен


Как следствие, ни один finalize() не будет запущен. Поэтому не стоит писать код, который полагается на то, что finalize() всегда отработает. Да и если объект попал в мусор перед окончанием работы программы, скорее всего, он не будет собран сборщиком.

метод finalize() для конкретного объекта может быть вызван один и только один раз


В finalize() мы можем сделать объект снова видимым, и сборщик мусора его в этот раз не «уберёт». Когда этот объект снова попадёт в мусор, он будет «собран» без вызова finalize(). Если в finalize() будет выброшено исключение и объект по-прежнему останется никому не видимым, он будет потом «собран». Повторно finalize() вызван не будет.

поток, в котором будет вызван finalize(), заранее не известен


Гарантируется только то, что этот поток будет свободен от блокировок, видимых основной программой.

наличие переопределённого метода finalize() у объектов замедляет процесс сборки мусора


То, что лежит на поверхности, это необходимость двойной проверки на доступность объекта — один раз перед вызовом finalize(), один раз в одном из следующих запусков сборщика мусора.

с дедлоками в finalize() очень тяжело бороться


В нетривиальных finalize() могут быть необходимы блокировки, отлаживать которые, учитывая специфику, описанную выше, очень непросто.

Object.finalize() начиная с 9-ой версии java помечен как deprecated!


Что неудивительно, учитывая описанную выше специфику.

классическая «ленивая» инициализация синглтона: двойная блокировка обязательна


На эту тему есть заблуждение, что следующий подход (double-check idiom), выглядящий весьма логично, всегда работает:
public class UnsafeDCLFactory {
    private Singleton instance;

    public Singleton get() {
        if (instance == null) {  // read 1, check 1
            synchronized (this) {
                if (instance == null) { // read 2, check 2
                    instance = new Singleton();
                }
            }
        }
        return instance; // read 3
    }
}

Мы смотрим, создан ли объект (read 1, check 1). Если да, то возвращаем его. Если нет, то ставим блокировку, убеждаемся, что объект не создан, создаём объект, (блокировка убирается), и возвращаем объект.

Подход не работает по следующим причинам. (read 1, check 1) и (read 3) не синхронизированы. По концепции модели памяти java, изменения, сделанные в другом потоке, могут быть невидимы нашему потоку, пока мы не синхронизируемся.Спасибо mk2 за комментарий, вот правильное описание проблемы:
Да, read1 и read3 не синхронизированы, но проблема не в другом потоке. А в том, что несинхронизированные чтения могут быть переупорядочены, т.е. read1 != null, но read3 == null. И заодно, из-за «instance = new Singleton();» мы можем получить ссылку на объект до того, как он был до конца сконструирован, и это действительно проблема синхронизации с другим потоком, но не read1 и read3, а read3 и доступа к членам instance.
Лечится либо добавлением синхронизации при чтении, либо пометкой переменной, в которой живёт ссылка на синглтон, volatile. (Решение с volatile работает только с java 5+. До этого в java была модель памяти с неопределённостью именно в этой ситуации.) Вот работающий вариант (с дополнительной оптимизацией — добавлена локальная переменная `res`, чтобы уменьшить количество чтений из volatile поля).
public class SafeLocalDCLFactory {
    private volatile Singleton instance;

    public Singleton getInstance() {
        Singleton res = instance;     // read 1
        if (res == null) {            // check 1
            synchronized (this) {
                res = instance;       // read 2
                if (res == null) {    // check 2
                    res = new Singleton();
                    instance = res;
                }
            }
        }
        return res;
    }
}

Код взят отсюда, с сайта Алексея Шипилёва. Подробнее с этой проблемой можно ознакомиться на нём же.

«initialization-on-demand holder idiom» — очень красивая «ленивая» инициализация синглтона


java инициализирует классы (Class objects) только по мере необходимости и, естественно, только один раз. И этим можно воспользоваться! Механизм «initialization-on-demand holder idiom» именно этим и занимается. (Код отсюда.)
public class Something {
    private Something() {}

    private static class LazyHolder {
        static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

Класс LazyHolder будет инициализирован только в момент первого вызова Something.getInstance(). Jvm позаботится о том, чтобы это произошло только один раз и притом весьма эффективно — в случае, если класс уже инициализирован, никаких накладных расходов не будет. Соответственно, LazyHolder.INSTANCE будет тоже инициализирован единожды, «лениво» и потокобезопасно.
кусок спецификации про накладные расходы
If this initialization procedure completes normally and the Class object is fully initialized and ready for use, then the invocation of the initialization procedure is no longer necessary and it may be eliminated from the code — for example, by patching it out or otherwise regenerating the code.
Источник.

Вообще говоря, синглтоны считаются не самой лучшей практикой.

Материал не закончился. Так что, если «дойдут» руки и то, что уже написано будет востребовано, напишу как-нибудь ещё на эту тему.

Спасибо за конструктивные комментарии. Несколько мест в статье были расширены благодаря sergey-gornostaev, vladimir_dolzhenko, OlehKurpiak, mk2.
Теги:
Хабы:
Всего голосов 27: ↑22 и ↓5+27
Комментарии30

Публикации

Истории

Работа

Java разработчик
361 вакансия

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань