Классы обертки в 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
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