Перейти к содержимому
Теория · Обёртки и равенство · урок 5 из 8

Кэш Integer и ==

Метод same сравнивает два Integer. Посмотрим, что он ответит для двух пар равных чисел:

JAVA
public class Main {
    static boolean same(Integer x, Integer y) {
        return x == y;
    }

    public static void main(String[] args) {
        System.out.println(same(100, 100));   // true
        System.out.println(same(1000, 1000)); // false
    }
}

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

Вернёмся к монетам и конвертам. При вызове same(100, 100) автоупаковка кладёт каждое число в конверт — объект Integer. А == для объектов сравнивает ссылки, как у массивов и строк. Он спрашивает «это один и тот же конверт?», а не «одинаковые ли в конвертах монеты?».

Значит, оба числа 100 оказались в одном и том же конверте. Откуда он взялся?

Кэш Integer

Маленькие числа встречаются в программах постоянно: счётчики, индексы, номера месяцев. Делать для каждого новый конверт было бы расточительно. Поэтому класс Integer заранее готовит по объекту на каждое число от -128 до 127 и хранит их на одном подносе. Это кэш Integer.

Автоупаковка вызывает Integer.valueOf, а он сначала смотрит на поднос:

  • для числа от -128 до 127 он отдаёт готовый объект из кэша, один и тот же при каждом вызове;
  • для любого другого числа он создаёт новый объект, каждый раз свой.

В виджете четыре строки: Integer a = 127;, Integer b = 127;, Integer c = 128; и Integer d = 128;. Поднос с кэшем виден с самого начала: он готов ещё до первой строки main. Выполняйте строки по одной и следите, откуда берётся объект для каждой переменной. Потом попробуйте угадать каждое сравнение и только затем нажмите на него.

Вживую127 из кэша, 128 — новый объектпеременных: 0 · объектов: 0
номера объектов условные

Кэш Integer готов заранее: в нём по объекту на каждое число от −128 до 127. Нажмите кнопку — она выполнит первую строку.

Следите, откуда берётся объект для каждой переменной

Разберём, что получилось:

  1. a == b даёт true: оба раза valueOf(127) отдал один и тот же объект с подноса.
  2. c == d даёт false: число 128 в кэш не входит, и каждый valueOf(128) создал свой объект.
  3. c.equals(d) даёт true: equals у Integer сравнивает числа, а не ссылки.

Спецификация Java требует, чтобы числа от -128 до 127 всегда брались из кэша. По умолчанию кэш ими и ограничен, хотя его верхнюю границу можно поднять настройкой JVM.

Здесь картинка с подносом неточна. С настоящего подноса конверт забирают, и его там больше нет. А объект из кэша один на всю программу, и его делят все переменные. Это безопасно только потому, что число внутри Integer изменить нельзя.

Сравнивайте через equals

Починим метод same: пусть он сравнивает числа, а не ссылки.

JAVA
public class Main {
    static boolean same(Integer x, Integer y) {
        return x.equals(y);
    }

    public static void main(String[] args) {
        System.out.println(same(100, 100));   // true
        System.out.println(same(1000, 1000)); // true
    }
}

Теперь обе пары дают true, и кэш на ответ больше не влияет. Что важно помнить про == с обёртками:

  • Integer == Integer сравнивает ссылки: true бывает, только если это один объект, например из кэша;
  • Integer == int распаковывает обёртку и сравнивает числа: при Integer c = 128; выражение c == 128 даёт true;
  • equals сравнивает числа всегда, в любом диапазоне.

На собеседовании вопрос звучит так: почему == для двух Integer со значением 127 даёт true, а со значением 128 — false? Хороший ответ состоит из трёх частей:

  • автоупаковка вызывает Integer.valueOf;
  • valueOf отдаёт готовые объекты из кэша для чисел от -128 до 127 и создаёт новые для остальных;
  • == сравнивает ссылки, поэтому обёртки сравнивают через equals.

Частая ошибка

Проверили через ==, исчерпан ли лимит:

JAVA
Integer limit = 500;
Integer used = 500;
if (used == limit) { // ✗
    System.out.println("limit reached");
}

Программа ничего не печатает и не сообщает об ошибке. Число 500 не входит в кэш, поэтому used и limit ведут к двум разным объектам, и == даёт false. Пишите used.equals(limit) или храните такие числа в int.

Главное

  • Автоупаковка вызывает Integer.valueOf, а он для чисел от -128 до 127 отдаёт объект из кэша — один и тот же при каждом вызове.
  • Для чисел вне кэша каждая упаковка создаёт новый объект.
  • == между двумя Integer сравнивает ссылки, поэтому со значением 127 он даёт true, а со значением 128 — false.
  • Числа в обёртках сравнивают через equals; Integer == int сравнивает числа, потому что распаковывает обёртку.