Wrapper классы ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-09-29

Классы обертки в Java (оболочки типов)

Два одинаковых на вид сравнения дают разный результат: для Integer a = 127, b = 127; выражение a == b возвращает true, а для Integer c = 128, d = 128; выражение c == d возвращает false. Причина в том, как устроены классы обертки и их кэш, и это один из самых частых вопросов на собеседованиях по Java.

Классы обертки (оболочки типов, wrapper-классы) в Java — это классы из пакета java.lang, объект которых хранит одно значение примитивного типа: Integer для int, Double для double, Boolean для boolean и т.д. Они нужны там, где требуется объект, а не примитив: в коллекциях и дженериках, для значения null, а также как набор полезных статических методов для преобразования и сравнения значений.

Что такое классы обертки и зачем они нужны

Примитивные типы (int, double, boolean и другие) быстрые и компактные, но это не объекты: у них нет методов, они не могут быть null и их нельзя положить в коллекцию. Чтобы каждый разработчик не писал свой класс-контейнер для числа, в Java SE уже есть готовые классы обертки.

Класс-обертка (класс оболочка) нужен, чтобы:

  • хранить примитивы в коллекциях и обобщенных классах: List<Integer>, Map<String, Double> — дженерики работают только с объектами;
  • обозначать отсутствие значения через null (например, поле Integer age, которое еще не заполнено, или результат из базы данных);
  • преобразовывать строки в числа и обратно: Integer.parseInt("42"), Integer.toString(42);
  • пользоваться константами и утилитами: Integer.MAX_VALUE, Integer.compare(), Character.isDigit(), Double.isNaN().

Имя класса-обертки совпадает с именем примитивного типа, но пишется с заглавной буквы. Исключения — Integer (примитив int) и Character (примитивный тип char).

Объекты классов оберток неизменяемые (immutable): значение, переданное при создании, изменить нельзя. Операция i++ над переменной типа Integer не меняет старый объект, а создает (или берет из кэша) новый и присваивает ссылку на него переменной:

Integer i = 10;
Integer before = i;
i++;                          // i = Integer.valueOf(i.intValue() + 1)
System.out.println(before);   // 10 - старый объект не изменился
System.out.println(i);        // 11 - переменная указывает на другой объект

Во всех классах-обертках переопределен метод equals(Object): он сравнивает хранимые значения, а не ссылки. Но учитывается и тип: Integer.valueOf(1).equals(1L) вернет false, потому что 1L упаковывается в Long.

Таблица: примитивные типы и их обертки

В Java восемь примитивных типов, и у каждого есть свой класс-обертка. Отдельно стоит класс Void: он соответствует ключевому слову void, не хранит никакого значения и не может быть создан. Его используют в дженериках, когда тип результата формально нужен, но возвращать нечего, например Callable<Void> или CompletableFuture<Void> (единственное допустимое значение — null).

Примитивный тип Класс-обертка Наследует Number Кэш valueOf()
byte Byte да все значения (-128..127)
short Short да -128..127
int Integer да -128..127 (верхнюю границу можно увеличить)
long Long да -128..127
float Float да нет
double Double да нет
char Character нет 0..127
boolean Boolean нет Boolean.TRUE и Boolean.FALSE
void Void нет объекты не создаются

Автоупаковка и распаковка (autoboxing и unboxing)

Автоупаковка (autoboxing) — автоматическое преобразование примитива в объект класса-обертки. Распаковка (unboxing) — обратное преобразование объекта-обертки в примитив. Оба преобразования выполняет компилятор, начиная с Java 5, поэтому вручную писать Integer.valueOf() и intValue() почти никогда не нужно.

import java.util.ArrayList;
import java.util.List;

public class AutoboxingExample {
    public static void main(String[] args) {
        Integer boxed = 42;          // автоупаковка: Integer.valueOf(42)
        int primitive = boxed;       // распаковка: boxed.intValue()

        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);              // автоупаковка при добавлении в коллекцию
        int first = numbers.get(0);  // распаковка при чтении

        Integer sum = boxed + first; // распаковка обоих, сложение int, упаковка результата
        System.out.println(sum);     // 47
    }
}

Упаковка и распаковка происходят при присваивании, передаче аргумента в метод, возврате из метода и в арифметических выражениях. Это удобно, но у удобства есть цена: каждая упаковка вне кэша создает новый объект, поэтому в «горячих» циклах лучше использовать примитивы (long sum, а не Long sum).

NullPointerException при распаковке

Распаковка — это вызов метода вроде intValue(). Если ссылка равна null, получите NullPointerException в строке, где на первый взгляд нет ни одного вызова метода: Integer count = null; int n = count;. Особенно коварен тернарный оператор: Integer x = flag ? map.get(key) : 0; — из-за операнда 0 результат map.get(key) распаковывается и при отсутствии ключа падает с NPE.

Создание объектов: valueOf() вместо конструкторов

Раньше объекты оберток создавали конструкторами: new Integer(42), new Integer("42"), new Boolean("true"). Все эти конструкторы объявлены deprecated начиная с Java 9, а с Java 16 помечены @Deprecated(forRemoval = true) (JEP 390) — то есть могут быть удалены в будущих версиях. Компилятор выдает предупреждение при их использовании.

Как не надо писать в современном коде:

// Устаревший способ - конструкторы deprecated for removal
Integer i1 = new Integer(42);
Integer i2 = new Integer("42");
Boolean b1 = new Boolean("true");

Как надо:

Integer i1 = 42;                     // автоупаковка
Integer i2 = Integer.valueOf("42");  // из строки
Float f1 = Float.valueOf(3.14f);
Float f2 = Float.valueOf("3.14");
Character c1 = 'c';                  // или Character.valueOf('c')
Boolean b1 = Boolean.valueOf("true");

Главное отличие: конструктор всегда создает новый объект, а valueOf() может вернуть уже существующий объект из кэша (для Integer — в диапазоне от -128 до 127). Это экономит память и именно поэтому valueOf() использует и автоупаковка.

Устаревший материал

Во многих старых книгах и тестах по Java (включая вопросы сертификации для Java 6–8) объекты оберток создаются через new Integer(...). Знать такой синтаксис нужно, чтобы читать старый код, но в новом коде используйте автоупаковку или valueOf().

Если строка не содержит корректного числа, valueOf(String) выбрасывает NumberFormatException. У класса Boolean поведение другое: исключение не выбрасывается никогда. Строка "true" в любом регистре дает true, любая другая строка (и даже null) — false:

public class WrapperExample1 {
    public static void main(String[] args) {
        Boolean boolean1 = Boolean.valueOf(true);
        Boolean boolean2 = Boolean.valueOf("Some String");
        boolean boolean3 = Boolean.parseBoolean("TRUE");

        System.out.println(boolean1); // true
        System.out.println(boolean2); // false
        System.out.println(boolean3); // true
    }
}

Кэш Integer: сравнение через == и equals()

Оператор == для объектов сравнивает ссылки, а не значения. Метод Integer.valueOf() (и автоупаковка) для чисел от -128 до 127 возвращает заранее созданные объекты из внутреннего кэша IntegerCache, поэтому две упаковки числа 127 указывают на один и тот же объект. Для 128 каждый раз создается новый объект:

public class IntegerCacheExample {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);       // true  - один объект из кэша

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);       // false - два разных объекта
        System.out.println(c.equals(d));  // true  - значения равны

        int e = 128;
        System.out.println(c == e);       // true  - c распаковывается, сравниваются int
    }
}

Правило простое: обертки сравнивайте через equals() (или Objects.equals(a, b), если возможен null), а для упорядочивания — через compareTo() или Integer.compare(). Оператор == между оберткой и примитивом работает корректно, потому что обертка распаковывается.

Нюанс

Диапазон -128..127 гарантирован спецификацией языка, но верхнюю границу кэша Integer можно увеличить параметром JVM -XX:AutoBoxCacheMax=1000. Поэтому результат c == d для 128 формально зависит от настроек запуска — еще один довод никогда не полагаться на == для оберток.

Методы классов оберток

Кроме хранения значения, классы-обертки предоставляют много статических и экземплярных методов. Сначала краткая шпаргалка по методам преобразования, которые чаще всего путают:

Метод Принимает Возвращает Пример
Integer.valueOf() int или String объект Integer Integer.valueOf("6")
Integer.parseInt() String примитив int Integer.parseInt("6")
intValue(), doubleValue() и др. ничего (экземплярный) примитив boxed.intValue()
Integer.toString() int (и основание) String Integer.toString(254, 16)
toString() ничего (экземплярный) String boxed.toString()

Методы valueOf()

Метод valueOf() — рекомендуемый способ получить объект-обертку. Метод перегружен: один вариант принимает значение соответствующего примитивного типа, второй — строку (String). Исключение — класс Character: у него есть только valueOf(char).

В целочисленных классах Byte, Short, Integer и Long есть третий вариант: он принимает строку с числом в произвольной системе счисления, а вторым параметром — основание этой системы (radix):

public class WrapperValueOf {
    public static void main(String[] args) {
        Integer integer1 = Integer.valueOf("6");
        Integer integer2 = Integer.valueOf(6);
        // преобразует двоичное 101011 в десятичное 43
        Integer integer3 = Integer.valueOf("101011", 2);

        System.out.println(integer1); // 6
        System.out.println(integer2); // 6
        System.out.println(integer3); // 43
    }
}

Методы parseXxx()

В каждом числовом классе-обертке (и в Boolean) есть статический метод, который преобразует строку в примитивное значение: Integer.parseInt(), Long.parseLong(), Double.parseDouble(), Boolean.parseBoolean() и т.д. Разница с valueOf(): valueOf() возвращает объект, а parseXxx() — примитив. Если нужен int, вызывайте parseInt(), чтобы не создавать лишний объект.

У Byte, Short, Integer и Long тоже есть вариант с системой счисления:

public class WrapperExample3 {
    public static void main(String[] args) {
        Long long1 = Long.valueOf("45");
        long long2 = Long.parseLong("67");
        long long3 = Long.parseLong("101010", 2);

        System.out.println("long1 = " + long1); // long1 = 45
        System.out.println("long2 = " + long2); // long2 = 67
        System.out.println("long3 = " + long3); // long3 = 42
    }
}

NumberFormatException при разборе строки

Если строка не является корректным числом, parseXxx() и valueOf(String) выбрасывают NumberFormatException. Это непроверяемое исключение, поэтому компилятор не заставит его обработать — при разборе пользовательского ввода оборачивайте вызов в try-catch:

public class ParseExample {
    public static void main(String[] args) {
        String[] inputs = {"42", "+42", " 42", "4.2", "abc"};
        for (String s : inputs) {
            try {
                System.out.println(Integer.parseInt(s));
            } catch (NumberFormatException e) {
                System.out.println("Не число: \"" + s + "\"");
            }
        }
    }
}

Результат выполнения:

42
42
Не число: " 42"
Не число: "4.2"
Не число: "abc"

Обратите внимание: пробелы не обрезаются автоматически (используйте s.trim() или s.strip()), а дробное число нельзя разобрать как int. Вызов Integer.parseInt(null) тоже бросает NumberFormatException, а не NullPointerException.

Методы toString()

  • Все классы-обертки переопределяют экземплярный метод toString(). Он возвращает строковое представление значения, поэтому объект-обертку можно сразу передать в println():
    Double double1 = Double.valueOf("4.6");
    System.out.println(double1); // 4.6
  • Все числовые обертки предоставляют статический метод toString(), принимающий примитивное значение и возвращающий String:
    String string1 = Double.toString(3.14); // "3.14"
  • Integer и Long предоставляют третий вариант toString() — для представления числа в любой системе счисления. Метод статический: первый аргумент — число, второй — основание системы счисления:
    String string2 = Long.toString(254, 16); // string2 = "fe"

Методы toHexString(), toOctalString(), toBinaryString()

Integer и Long позволяют перевести число из десятичной системы счисления в шестнадцатеричную, восьмеричную и двоичную:

public class WrapperToXString {
    public static void main(String[] args) {
        String string1 = Integer.toHexString(254);
        System.out.println("254 в 16-ой системе = " + string1);

        String string2 = Long.toOctalString(254);
        System.out.println("254 в 8-ой системе = " + string2);

        String string3 = Long.toBinaryString(254);
        System.out.println("254 в 2-ой системе = " + string3);
    }
}

Результат выполнения:

254 в 16-ой системе = fe
254 в 8-ой системе = 376
254 в 2-ой системе = 11111110

В классах Double и Float из этой группы есть только метод toHexString().

Сравнение и арифметика: compare(), sum(), max(), min()

Начиная с Java 7–8 в числовых обертках появились статические методы, которые удобно передавать как ссылки на методы в Stream API и компараторы:

System.out.println(Integer.compare(5, 10));  // -1 (отрицательное: 5 < 10)
System.out.println(Integer.sum(5, 10));      // 15
System.out.println(Integer.max(5, 10));      // 10
System.out.println(Integer.min(5, 10));      // 5
System.out.println(Double.compare(0.0, -0.0)); // 1

// ссылка на метод вместо лямбды
int total = List.of(1, 2, 3).stream().reduce(0, Integer::sum); // 6

Integer.compare(x, y) безопаснее, чем вычитание x - y в компараторе: вычитание может переполниться для больших по модулю значений.

Полезные методы Character

Класс Character не наследует Number, зато содержит набор статических методов для анализа символов:

System.out.println(Character.isDigit('7'));          // true
System.out.println(Character.isLetter('Ж'));         // true
System.out.println(Character.isLetterOrDigit('_'));  // false
System.out.println(Character.isWhitespace(' '));     // true
System.out.println(Character.isUpperCase('a'));      // false
System.out.println(Character.toUpperCase('a'));      // A
System.out.println(Character.getNumericValue('7'));  // 7

Класс Number

Все числовые классы-обертки (Byte, Short, Integer, Long, Float, Double) наследуют абстрактный класс Number. От него же наследуются BigInteger, BigDecimal и атомарные AtomicInteger, AtomicLong. Character и Boolean наследниками Number не являются.

Number объявляет методы, которые возвращают значение объекта в каждом из числовых форматов: byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue().

Иерархия класса Number и его наследников в Java

Пример приведения типов через методы Number

public class WrapperExample2 {
    public static void main(String[] args) {
        Integer iOb = 1000;
        System.out.println(iOb.byteValue());
        System.out.println(iOb.shortValue());
        System.out.println(iOb.intValue());
        System.out.println(iOb.longValue());
        System.out.println(iOb.floatValue());
        System.out.println(iOb.doubleValue());
    }
}

Результат выполнения:

-24
1000
1000
1000
1000.0
1000.0

Методы xxxValue() работают как обычное приведение примитивов: byteValue() для 1000 возвращает -24, потому что при сужении до 8 бит старшие биты отбрасываются.

Константы: MIN_VALUE, MAX_VALUE, бесконечность и NaN

Каждый числовой класс-обертка (а также Character) содержит статические константы с минимальным и максимальным значением типа. Например, Integer.MIN_VALUE — минимальное значение int (-2147483648), Integer.MAX_VALUE — максимальное (2147483647). Также есть константы SIZE (размер в битах) и BYTES (размер в байтах).

Обертки типов с плавающей точкой Float и Double дополнительно содержат константы:

  • NEGATIVE_INFINITY — отрицательная бесконечность;
  • POSITIVE_INFINITY — положительная бесконечность;
  • NaN — «не число» (Not a Number).

Обратите внимание: у Double константа MIN_VALUE — это не самое маленькое отрицательное число, а наименьшее положительное значение (около 4.9E-324). Самое маленькое отрицательное — -Double.MAX_VALUE.

Целочисленное деление на ноль выбрасывает ArithmeticException. А деление на ноль с плавающей точкой по стандарту IEEE 754 ошибкой не является: результат равен положительной или отрицательной бесконечности. Умножение бесконечности на ноль дает NaN:

public class InfinityExample {
    public static void main(String[] args) {
        int a = 7;
        double b = 0.0;
        double c = -0.0;
        double g = Double.NEGATIVE_INFINITY;
        System.out.println("7 / 0.0 = " + a / b);
        System.out.println("7 / -0.0 = " + a / c);
        System.out.println("0.0 == -0.0 = " + (b == c));
        System.out.println("-Infinity * 0 = " + g * 0);
    }
}

Результат выполнения кода:

7 / 0.0 = Infinity
7 / -0.0 = -Infinity
0.0 == -0.0 = true
-Infinity * 0 = NaN

NaN не равен ничему, даже самому себе: Double.NaN == Double.NaN дает false. Для проверки используйте Double.isNaN(x), а для бесконечности — Double.isInfinite(x). Метод equals() у Double ведет себя иначе, чем ==: Double.valueOf(Double.NaN).equals(Double.NaN) возвращает true, а Double.valueOf(0.0).equals(-0.0) — false.

Обертки в коллекциях и дженериках

Параметр типа в дженериках может быть только ссылочным типом, поэтому List<int> не скомпилируется — нужно писать List<Integer>. Благодаря автоупаковке работа с такой коллекцией выглядит почти как работа с примитивами:

// List<int> list = new ArrayList<>(); // ошибка компиляции
List<Integer> list = new ArrayList<>();
list.add(10);                 // автоупаковка
list.add(20);
int sum = 0;
for (int n : list) {          // распаковка каждого элемента
    sum += n;
}

Map<String, Integer> ages = new HashMap<>();
ages.put("Анна", 30);
Integer age = ages.get("Иван"); // null - ключа нет, распаковывать в int опасно

Если важна производительность при большом объеме чисел, используйте массивы примитивов (int[]) или специализированные потоки IntStream, LongStream, DoubleStream — они не создают объект на каждое значение.

remove() в List<Integer>

У List есть два метода: remove(int index) и remove(Object o). Вызов list.remove(10) удалит элемент по индексу 10 (автоупаковки не будет, так как подходит метод с примитивом). Чтобы удалить значение 10, пишите list.remove(Integer.valueOf(10)).

На чем чаще всего ошибаются

  • Сравнение оберток через ==. Работает для значений из кэша (-128..127) и ломается за его пределами. Используйте equals().
  • Распаковка null. int x = map.get(key); при отсутствии ключа дает NullPointerException.
  • equals() с разными типами. Long.valueOf(5).equals(5) возвращает false: 5 упаковывается в Integer, а не в Long.
  • Deprecated-конструкторы. new Integer(5) дает предупреждение компилятора и всегда создает новый объект; используйте Integer.valueOf(5) или автоупаковку.
  • Ожидание исключения от Boolean. Boolean.parseBoolean("yes") и Boolean.valueOf("Some String") возвращают false без исключения.
  • Упаковка в цикле. Long sum = 0L; for (...) sum += i; создает миллионы лишних объектов; объявите long sum.
  • Синхронизация на обертке. synchronized (count) над Integer опасна: после count++ переменная указывает на другой объект, а кэшированные объекты общие для всего приложения.

Часто задаваемые вопросы

Чем int отличается от Integer?

int — примитивный тип: хранит само значение, не может быть null, у поля класса значение по умолчанию 0. Integer — класс-обертка: переменная хранит ссылку на объект, может быть null (значение по умолчанию у поля — null), объект занимает больше памяти, зато его можно использовать в коллекциях и дженериках.

Является ли String классом-оберткой?

Нет. String тоже неизменяемый класс из java.lang, но он не оборачивает никакой примитивный тип. Классов-оберток восемь: Byte, Short, Integer, Long, Float, Double, Character, Boolean; к ним иногда относят и Void.

Можно ли изменить значение Integer, переданного в метод?

Нет. Объекты оберток неизменяемы, а ссылка передается в метод по значению. Присваивание param = param + 1 внутри метода создает новый объект и меняет только локальную копию ссылки — переменная вызывающего кода останется прежней. Чтобы изменить значение, верните результат из метода или используйте изменяемый контейнер, например AtomicInteger или массив.

Что будет при вызове Integer.parseInt(null)?

Будет выброшено NumberFormatException с сообщением «Cannot parse null string». При этом Double.parseDouble(null) выбрасывает NullPointerException, а Boolean.parseBoolean(null) просто возвращает false. Перед разбором внешних данных проверяйте строку на null и пустоту.

Что использовать в полях и параметрах: int или Integer?

По умолчанию выбирайте примитив: он быстрее, экономнее и не дает NullPointerException. Integer нужен, когда значение может отсутствовать (например, необязательное поле сущности JPA или DTO), и там, где требуется объект: в коллекциях, дженериках, Optional.

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.