Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Доброе время суток!
Статья написана по следам публикации «Вещи, которые вы [возможно] не знали о Java» другого автора, которую я бы отнёс к категории «для начинающих». Читая и комментируя её, я понял, что есть некоторое количество достаточно любопытных вещей, которые я узнал, уже программируя на java не один год. Возможно, эти вещи покажутся любопытными кому-то ещё.
Факты, которые, с моей точки зрения, могут быть полезны именно начинающим, я убрал в «спойлеры». Некоторые вещи там всё-же могут быть интересны и для более опытных. Например, сам я до момента написания статьи не знал, что Boolean.hashCode(true) == 1231 и Boolean.hashCode(false) == 1237.
Когда-то это было так.Теперь, как минимум с 11-ой версии java, это не так.
Оговорка: это деталь oracle jvm.
Понятно, при сравнении двух обёрток оператором == (!=) никакого autoboxing не происходит. Вообще говоря, именно первое равенство смущает. Дело в том, что для целых значений i: -129 < i < 128 объекты-обёртки Integer кэшируются. Поэтому для i из этого диапазона Integer.valueOf(i) не создаёт каждый раз новый объект, а возвращает уже созданный. Для i, не попадающих в этот диапазон, Integer.valueOf(i) всегда создаёт новый объект. Поэтому, если пристально не следить за тем, что именно и как именно сравнивается, можно написать код, который вроде-бы работает и даже покрыт тестами, но в то же время содержащий такие вот «грабли».
Звучит запутанно, да и утверждение длинновато. Смысл такой. Если у нас есть класс, в котором определены константы примитивных типов или строк как final static поля с немедленной инициализацией,
то при их использовании в других классах,
значения этих констант («case_1», «case_2») разрешаются во время компиляции. И вставляются в код как значения, а не как ссылки. То есть, если мы используем такие константы из библиотеки, а потом получаем новую версию библиотеки, в которой значения констант изменились, нам стоит перекомпилировать проект. Иначе в коде могут продолжать использоваться старые значения констант.
Такое поведение наблюдается во всех местах, где либо обязаны быть использованы константные выражения (например, switch/case), либо компилятору разрешено преобразовывать выражения к константным и он может это сделать.
Эти поля не смогут использоваться в константных выражениях, как только мы уберём немедленную инициализацию, перенеся инициализацию в блок static.
На эту тему есть заблуждение, что следующий подход (double-check idiom), выглядящий весьма логично, всегда работает:
Мы смотрим, создан ли объект (read 1, check 1). Если да, то возвращаем его. Если нет, то ставим блокировку, убеждаемся, что объект не создан, создаём объект, (блокировка убирается), и возвращаем объект.
Подход не работает по следующим причинам. (read 1, check 1) и (read 3) не синхронизированы. По концепции модели памяти java, изменения, сделанные в другом потоке, могут быть невидимы нашему потоку, пока мы не синхронизируемся. Лечится либо добавлением синхронизации при чтении, либо пометкой переменной, в которой живёт ссылка на синглтон, volatile. (Решение с volatile работает только с java 5+. До этого в java была модель памяти с неопределённостью именно в этой ситуации.) Вот работающий вариант (с дополнительной оптимизацией — добавлена локальная переменная `res`, чтобы уменьшить количество чтений из volatile поля).
Код взят отсюда, с сайта Алексея Шипилёва. Подробнее с этой проблемой можно ознакомиться на нём же.
java инициализирует классы (Class objects) только по мере необходимости и, естественно, только один раз. И этим можно воспользоваться! Механизм «initialization-on-demand holder idiom» именно этим и занимается. (Код отсюда.)
Класс LazyHolder будет инициализирован только в момент первого вызова Something.getInstance(). Jvm позаботится о том, чтобы это произошло только один раз и притом весьма эффективно — в случае, если класс уже инициализирован, никаких накладных расходов не будет. Соответственно, LazyHolder.INSTANCE будет тоже инициализирован единожды, «лениво» и потокобезопасно.
Вообще говоря, синглтоны считаются не самой лучшей практикой.
Материал не закончился. Так что, если «дойдут» руки и то, что уже написано будет востребовано, напишу как-нибудь ещё на эту тему.
Статья написана по следам публикации «Вещи, которые вы [возможно] не знали о 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() уже не является адресом объекта в памяти
Когда-то это было так.Теперь, как минимум с 11-ой версии java, это не так.
Integer.valueOf(15) == Integer.valueOf(15); Integer.valueOf(128) != Integer.valueOf(128)
Оговорка: это деталь oracle jvm.
Понятно, при сравнении двух обёрток оператором == (!=) никакого autoboxing не происходит. Вообще говоря, именно первое равенство смущает. Дело в том, что для целых значений i: -129 < i < 128 объекты-обёртки Integer кэшируются. Поэтому для i из этого диапазона Integer.valueOf(i) не создаёт каждый раз новый объект, а возвращает уже созданный. Для i, не попадающих в этот диапазон, Integer.valueOf(i) всегда создаёт новый объект. Поэтому, если пристально не следить за тем, что именно и как именно сравнивается, можно написать код, который вроде-бы работает и даже покрыт тестами, но в то же время содержащий такие вот «грабли».
в некоторых случаях значения примитивных или строковых 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() мы можем сделать объект снова видимым, и сборщик мусора его в этот раз не «уберёт». Когда этот объект снова попадёт в мусор, он будет «собран» без вызова finalize(). Если в finalize() будет выброшено исключение и объект по-прежнему останетсч никому не видимым, он будет потом «собран». Повторно finalize() вызван не будет.
поток, в котором будет вызван finalize() заранее не известен
Гарантируется только то, что этот поток будет свободен от блокировок, видимых основной программой.
наличие переопределённого метода finalize() у объектов замедляет процесс сборки мусора
То что лежит на поверхности, это необходимость двойной проверки на доступность объекта — один раз перед вызовом finalize(), один раз в одном из следующих запусков сборщика мусора.
с dead locks в 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, изменения, сделанные в другом потоке, могут быть невидимы нашему потоку, пока мы не синхронизируемся. Лечится либо добавлением синхронизации при чтении, либо пометкой переменной, в которой живёт ссылка на синглтон, 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.
Источник.
Источник.
Вообще говоря, синглтоны считаются не самой лучшей практикой.
Материал не закончился. Так что, если «дойдут» руки и то, что уже написано будет востребовано, напишу как-нибудь ещё на эту тему.