$ sudo teach IT
МОДУЛЬ 2 · УРОК 1

Целые числа — byte, short, int, long: диапазоны, литералы и переполнение

Знакомимся с четырьмя типами целых чисел в Java, узнаем, сколько памяти они занимают, как записывать числовые литералы и что такое переполнение

~25 минут Для новичков Java

Зачем нужны разные типы чисел?

Представьте, что вы арендуете ячейки в банке. Маленькая ячейка стоит дешевле, но в неё поместится только украшение. Большая ячейка дороже, но туда можно поставить сейф. Если вам нужно хранить только кольцо, было бы глупо арендовать огромный сейф.

С памятью компьютера происходит то же самое. Java предоставляет четыре размера «коробок» для хранения целых чисел: byte, short, int и long. Чем больше коробка, тем больше числа в неё помещаются, но тем больше оперативной памяти она занимает.

Почему бы просто не использовать всегда самую большую коробку? В реальных программах вы можете создавать миллионы переменных одновременно — например, массив из миллиона байт займёт всего 1 мегабайт, а массив из миллионов long-чисел — уже 8 мегабайт. В мобильной разработке или на серверах с ограниченной памятью это имеет огромное значение.

Тип byte — младенец среди чисел

Тип byte — самый маленький целочисленный тип в Java. Он занимает ровно 8 бит (1 байт) памяти. Один бит — это крошечная ячейка, которая может хранить только одно из двух значений: 0 или 1. Восемь таких ячеек вместе могут составить 2⁸ = 256 различных комбинаций.

Поскольку один бит отводится на знак (положительное или отрицательное число), диапазон byte составляет:

  • От -128 до 127

Это как однозначные числа: от -9 до 9 (но побольше). Byte подходит для situations, где вы точно знаете, что значения будут небольшими — например, возраст человека (вряд ли кому-то больше 127 лет), коды ошибок, или когда вы читаете файл побайтово.

public class ByteExample {
    public static void main(String[] args) {
        byte temperature = -10;
        byte age = 25;
        byte score = 127;

        System.out.println("Температура: " + temperature);
        System.out.println("Возраст: " + age);
        System.out.println("Рекорд: " + score);

        byte wrong = 128;
    }
}

Последняя строка вызовет ошибку компиляции! Число 128 уже не помещается в byte — оно на единицу больше максимального значения 127. Компилятор Java не позволит вам сделать такой «обман».

Почему диапазон симметричен вокруг нуля, но не совсем? Число 128 «съедается» знаковым битом. В двоичном представлении 127 — это 01111111 (все восемь битов, кроме знакового, единицы). Добавить ещё единицу невозможно без использования знакового бита, что превратило бы число в отрицательное.

Тип short — подросток

Тип short занимает 16 бит (2 байта) памяти. Это вдвое больше, чем byte, а значит, и «коробка» вдвое вместительнее:

  • От -32 768 до 32 767

Short хорош для situations, когда значение больше 127, но гарантированно не превышает 32 тысячи. Например, максимальная высота в метрах для географических данных, номера некоторых сущностей в базе данных.

public class ShortExample {
    public static void main(String[] args) {
        short maxHeight = 8848;
        short population = 32000;
        short year = 2024;

        System.out.println("Высота Эвереста: " + maxHeight + " м");
        System.out.println("Население города: " + population);
        System.out.println("Текущий год: " + year);
    }
}

Заметьте, что на практике short используется довольно редко. Программисты обычно выбирают между byte и int, а short как бы «между ними» — не такой крошечный, но и не такой удобный. Тем не менее, он встречается в некоторых API и форматах данных.

Тип int — основной рабочий тип

Тип int (от слова integer — целое число) занимает 32 бита (4 байта) памяти. Это самый часто используемый тип для целых чисел в Java. Его диапазон впечатляет:

  • От -2 147 483 648 до 2 147 483 647

Это более 4 миллиардов различных значений (для беззнакового диапазона) или более 2 миллиардов в каждую сторону от нуля. Для подавляющего большинства задач этого более чем достаточно: количество жителей Земли, расстояние в миллиметрах, количество секунд в месяце — всё это помещается в int.

public class IntExample {
    public static void main(String[] args) {
        int worldPopulation = 8_000_000_000;
        int secondsInDay = 86_400;
        int distanceToMoon = 384_400_000;

        System.out.println("Население мира: " + worldPopulation);
        System.out.println("Секунд в дне: " + secondsInDay);
        System.out.println("Расстояние до Луцы (км): " + distanceToMoon);
    }
}

Обратите внимание на подчёркивания в числах: 8_000_000_000. Это не переменная с именем — Java разрешает ставить подчёркивания в числовые литералы для удобства чтения. Компилятор их просто игнорирует. Подчёркивание можно ставить где угодно: 1_00, 1_000_000, 10_0_0 — всё это валидные числа. Но ставить их в начало или конец нельзя: _100 и 100_ — ошибка.

Важная деталь: когда вы пишете число без суффикса, Java автоматически считает его типом int. То есть запись 42 — это литерал типа int, а не byte, не short, не long.

Тип long — тяжеловес

Тип long (от слова long — длинный) занимает 64 бита (8 байт) памяти. Это самый большой целочисленный тип в Java. Его диапазон поражает воображение:

  • От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807

Это более 9 квинтиллионов! Long необходим, когда int не хватает: количество миллисекунд от начала эпохи Unix (1 января 1970 года), номера транзакций в банке, хэши, идентификаторы и другие большие числа.

Чтобы создать литерал типа long, нужно добавить суффикс L (или l, но рекомендуется заглавная L, потому что строчная l легко перепутать с цифрой 1):

public class LongExample {
    public static void main(String[] args) {
        long worldPopulation = 8_000_000_000L;
        long secondsInYear = 31_536_000L;
        long starsInGalaxy = 100_000_000_000L;
        long distanceInLightYears = 9_461_000_000_000_000L;

        System.out.println("Население мира: " + worldPopulation);
        System.out.println("Секунд в году: " + secondsInYear);
        System.out.println("Звёзд в галактике: " + starsInGalaxy);
        System.out.println("Расстояние в световых годах: " + distanceInLightYears);
    }
}

Почему 8_000_000_000L, а не просто 8000000000? Потому что без суффикса L компилятор попытается интерпретировать число как int, а 8 миллиардов не помещаются в int (максимум ~2,14 миллиарда). Это вызовет ошибку компиляции: «integer number too large». Суффикс L говорит компилятору: «это long, используй 64 бита».

Сравнение типов в таблице

Давайте обобщим информацию в удобной таблице:

Тип Размер Минимум Максимум
byte 8 бит (1 байт) -128 127
short 16 бит (2 байта) -32 768 32 767
int 32 бита (4 байта) -2 147 483 648 2 147 483 647
long 64 бита (8 байт) -9 223 372 036 854 775 808 9 223 372 036 854 775 807

Числовые литералы: способы записи

Литерал — это число, записанное прямо в коде. Java поддерживает несколько способов записи числовых литералов:

Десятичная запись (обычная)

Это запись, привычная нам с детства: 42, -100, 0, 123456789. Никаких специальных префиксов.

Шестнадцатеричная запись (hexadecimal)

Начинается с префикса 0x. Использует цифры 0-9 и буквы A-F (или a-f). Каждая цифра代表ляет 4 бита. Очень удобна для работы с битовыми операциями и цветами.

public class HexExample {
    public static void main(String[] args) {
        int hex1 = 0xFF;
        int hex2 = 0x1A;
        int hex3 = 0xDEAD_BEEF;

        System.out.println("0xFF = " + hex1);
        System.out.println("0x1A = " + hex2);
        System.out.println("0xDEADBEEF = " + hex3);
    }
}

0xFF = 255 в десятичной, 0x1A = 26, а 0xDEADBEEF = 3735928559. Шестнадцатеричная запись особенно полезна, когда вы работаете с цветами: 0xFF0000 — красный, 0x00FF00 — зелёный, 0x0000FF — синий.

Восьмеричная запись (octal)

Начинается с префикса 0. Использует цифры 0-7. Встречается реже, но иногда используется в UNIX-системах для прав доступа к файлам.

public class OctalExample {
    public static void main(String[] args) {
        int oct1 = 010;
        int oct2 = 077;
        int oct3 = 0100;

        System.out.println("010 = " + oct1);
        System.out.println("077 = " + oct2);
        System.out.println("0100 = " + oct3);
    }
}

010 в восьмеричной = 8 в десятичной, 077 = 63, 0100 = 64. Будьте осторожны: обычное число 010 (десять) и восьмеричное 010 (восемь) — это разные числа!

Двоичная запись (binary)

Начинается с префикса 0b. Использует только цифры 0 и 1. Сама «родная» система счисления для компьютера.

public class BinaryExample {
    public static void main(String[] args) {
        int bin1 = 0b1010;
        int bin2 = 0b1111_1111;
        int bin3 = 0b1000_0000;

        System.out.println("0b1010 = " + bin1);
        System.out.println("0b11111111 = " + bin2);
        System.out.println("0b10000000 = " + bin3);
    }
}

Двоичная запись идеальна, когда вы изучаете битовые операции или работаете с битовыми масками. С помощью подчёркиваний можно сделать читаемым любое двоичное число: 0b1101_0101_1111_1100.

Суффиксы литералов

Как мы уже видели, по умолчанию Java считает все целочисленные литералы типа int. Чтобы указать другой тип, добавляется суффикс:

Суффикс Тип Пример
(нет) int 42
L или l long 42L

Рекомендация: Всегда используйте заглавную L вместо строчной l. Строчная l в некоторых шрифтах выглядит почти так же, как цифра 1, что может привести к ошибкам при чтении кода.

Разделители разрядов — читаем большие числа

В Java 7 и новее можно использовать подчёркивания (_) в числовых литералах для повышения читаемости. Компилятор просто игнорирует эти подчёркивания.

Представьте, что вы читаете число 1000000000. Трудно сразу понять, сколько это — миллиард или миллион? А если записать 1_000_000_000 — сразу ясно!

public class SeparatorExample {
    public static void main(String[] args) {
        int million = 1_000_000;
        long billion = 1_000_000_000L;
        int hexColor = 0xFF_DD_00;
        long binary = 0b1010_1100_1111_0000;

        System.out.println("Миллион: " + million);
        System.out.println("Миллиард: " + billion);
        System.out.println("Цвет: " + hexColor);
        System.out.println("Двоичное: " + binary);
    }
}

Правила для подчёркиваний: Нельзя ставить подчёркивание в начале или конце числа (_42, 42_ — ошибка). Нельзя ставить два подчёркивания подряд (1__000 — ошибка). Нельзя ставить перед суффиксом (42_L — ошибка). Разрешено ставить между любыми цифрами и системами счисления.

Переполнение — когда число «не влезает»

Переполнение (overflow) — это ситуация, когда результат операции выходит за пределы диапазона типа. Это одна из самых коварных особенностей Java.

Представьте термометр, который показывает温度 от -128 до +127 градусов. Если сейчас 127 градусов и добавить ещё один градус, термометр не покажет 128 — он «перекинется» на -128. Это и есть переполнение: значение циклически «перескакивает» с одного конца диапазона на другой.

public class OverflowExample {
    public static void main(String[] args) {
        int maxInt = 2_147_483_647;
        System.out.println("Максимум int: " + maxInt);
        System.out.println("Максимум + 1: " + (maxInt + 1));

        int minInt = -2_147_483_648;
        System.out.println("Минимум int: " + minInt);
        System.out.println("Минимум - 1: " + (minInt - 1));

        byte maxByte = 127;
        System.out.println("Максимум byte: " + maxByte);
        System.out.println("Максимум + 1: " + (maxByte + 1));
    }
}

Вы увидите, что максимальный int + 1 даёт минимальный int, а минимальный int - 1 даёт максимальный int. Это происходит потому, что целые числа в компьютере хранятся в «кольцевом» представлении.

Для byte результат ещё интереснее: maxByte + 1 возвращает -128, потому что в выражении maxByte + 1 byte автоматически «промоутируется» (расширяется) до int, вычисляется результат, а затем при выводе вы видите -128 (результат переполнения byte).

Важно: Java не выбрасывает исключение при переполнении! Программа просто молча «перекинется» на другое значение. Это может привести к серьёзным багам, которые трудно найти. Будьте осторожны с арифметикой на больших числах.

Классический пример переполнения — попытка вычислить, когда произойдёт 1 января 3000 года:

public class Year2038Problem {
    public static void main(String[] args) {
        int maxUnixTimestamp = 2_147_483_647;
        System.out.println("Максимальная дата Unix (int): " + maxUnixTimestamp);

        long safeTimestamp = 2_147_483_647L + 1;
        System.out.println("Следующая секунда (long): " + safeTimestamp);
    }
}

Проблема 2038 года — это реальная проблема, связанная с переполнением 32-битного int в системах, которые хранят время в секундах с 1 января 1970 года.

Неявное расширение и явное сужение

Java автоматически расширяет (расширяет) smaller типы в larger без потери данных. Это называется неявным расширением (implicit widening):

public class WideningExample {
    public static void main(String[] args) {
        byte b = 42;
        short s = b;
        int i = s;
        long l = i;

        System.out.println("byte: " + b);
        System.out.println("short: " + s);
        System.out.println("int: " + i);
        System.out.println("long: " + l);

        long bigNumber = 100L;
        int smallNumber = (int) bigNumber;
        System.out.println("Приведение long к int: " + smallNumber);
    }
}

Обратите внимание на последнюю часть: присваивание long-значения int-переменной требует явного приведения типа (int). Это называется явным сужением (explicit narrowing). Компилятор требует явного приведения, потому что данные могут быть потеряны — bigNumber может быть больше, чем max int.

Правило неявного расширения: byte → short → int → long. Каждый следующий тип может безопасно вместить любое значение предыдущего. Но в обратную сторону (long → int → short → byte) данные могут быть потеряны, поэтому Java требует явного приведения.

Автопродвижение (binary promotion)

Когда вы выполняете арифметическую операцию с двумя operands разного типа, Java автоматически «промоутирует» (продвигает) меньший тип до большего:

public class PromotionExample {
    public static void main(String[] args) {
        byte a = 10;
        int b = 20;
        long c = 30L;

        int result1 = a + b;
        long result2 = a + c;
        long result3 = a + b + c;

        System.out.println("byte + int = " + result1);
        System.out.println("byte + long = " + result2);
        System.out.println("byte + int + long = " + result3);
    }
}

В выражении a + b byte «a» автоматически превращается в int перед сложением. Результат — int. В выражении a + c byte «a» превращается в long. В сложном выражении a + b + c всё вычисляется слева направо: сначала a+b (оба int), результат int, затем int + long (int превращается в long).

Это важно знать, потому что новички часто думают, что если сложить два byte, результат будет byte. Но нет — результат всегда минимум int!

Практический пример: калькулятор возраста

Давайте напишем программу, которая вычисляет возраст человека в днях и секундах:

public class AgeCalculator {
    public static void main(String[] args) {
        byte ageYears = 25;
        short daysInYear = 365;
        int totalDays = ageYears * daysInYear;
        long totalSeconds = totalDays * 24L * 60 * 60;

        System.out.println("Возраст: " + ageYears + " лет");
        System.out.println("Примерно " + totalDays + " дней");
        System.out.println("Примерно " + totalSeconds + " секунд жизни");
    }
}

Здесь мы использовали разные типы для разных целей: byte для возраста (хватит), short для дней в году (хватит), int для общего количества дней (нужно больше), и long для секунд (нужно очень много). Это хороший пример осознанного выбора типа.

Типы и локальные переменные

В Java существуют два способа объявления переменных:

Явное указание типа

int age = 25;
long population = 8_000_000_000L;
byte temperature = -10;

Вывод типа (local variable type inference)

Начиная с Java 10, можно использовать ключевое слово var вместо типа. Компилятор сам определит тип по значению:

public class VarExample {
    public static void main(String[] args) {
        var age = 25;
        var population = 8_000_000_000L;
        var temperature = -10;

        System.out.println(age);
        System.out.println(population);
        System.out.println(temperature);
    }
}

Переменная age станет типом int (потому что 25 — это int-литерал), population станет long (потому что суффикс L), а temperature станет int (потому что -10 — тоже int-литерал).

Когда использовать var, а когда указывать тип? Если тип очевиден из контекста (инициализация литералом, вызов метода с понятным возвращаемым типом) — можно var. Если тип неочевиден или важен для документации кода — лучше указать явно. В начале обучения рекомендуется всегда указывать тип явно для лучшего понимания.

Частые ошибки новичков

Давайте рассмотрим типичные ловушки, в которые попадаются начинающие:

Ошибка 1: Забытый суффикс L

long x = 8000000000;   // ОШИБКА компиляции!
long y = 8000000000L;  // Правильно

Ошибка 2: Переполнение без предупреждения

int a = 2_000_000_000;
int b = 2_000_000_000;
int sum = a + b;
System.out.println(sum);  // -294967296! Не 4000000000!

Ошибка 3: Деление большего на меньший

long large = 9_223_372_036_854_775_807L;
int small = (int) large;
System.out.println(small);  // -1! Данные потеряны

Ошибка 4: Путаница систем счисления

int hex = 0x10;    // 16 в десятичной!
int octal = 010;   // 8 в десятичной!
int decimal = 10;  // 10 в десятичной

System.out.println(hex);    // 16
System.out.println(octal);  // 8
System.out.println(decimal); // 10

Полный пример: работа со всеми типами

Давайте создадим программу, которая демонстрирует все основные концепции:

public class IntegerTypesOverview {
    public static void main(String[] args) {
        byte smallNumber = 42;
        short mediumNumber = 1_000;
        int bigNumber = 1_000_000;
        long hugeNumber = 1_000_000_000_000L;

        System.out.println("=== Все типы ===");
        System.out.println("byte:   " + smallNumber + " (от -128 до 127)");
        System.out.println("short:  " + mediumNumber + " (от -32768 до 32767)");
        System.out.println("int:    " + bigNumber + " (от -2.1 млрд до 2.1 млрд)");
        System.out.println("long:   " + hugeNumber + " (огромный диапазон)");

        System.out.println();
        System.out.println("=== Системы счисления ===");
        int decimal = 255;
        int hex = 0xFF;
        int octal = 0377;
        int binary = 0b1111_1111;

        System.out.println("Десятичная: " + decimal);
        System.out.println("Шестнадцатеричная: " + hex);
        System.out.println("Восьмеричная: " + octal);
        System.out.println("Двоичная: " + binary);

        System.out.println();
        System.out.println("=== Разделители разрядов ===");
        int withSeparator = 1_234_567_890;
        long bigWithSeparator = 9_223_372_036_854_775_807L;
        System.out.println("С разделителем: " + withSeparator);
        System.out.println("Большое число: " + bigWithSeparator);

        System.out.println();
        System.out.println("=== Автоматическое расширение ===");
        byte b = 10;
        short s = b;
        int i = s;
        long l = i;
        System.out.println("byte → short → int → long: " + l);

        System.out.println();
        System.out.println("=== Переполнение ===");
        int max = Integer.MAX_VALUE;
        System.out.println("MAX_VALUE: " + max);
        System.out.println("MAX_VALUE + 1: " + (max + 1));
    }
}

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

Для каждого примитивного типа существует соответствующий класс-обёртка (wrapper class): Byte, Short, Integer, Long. Эти классы提供了 полезные методы:

public class WrapperExample {
    public static void main(String[] args) {
        System.out.println("Максимум int: " + Integer.MAX_VALUE);
        System.out.println("Минимум int: " + Integer.MIN_VALUE);
        System.out.println("Максимум long: " + Long.MAX_VALUE);

        int parsed = Integer.parseInt("12345");
        System.out.println("Из строки: " + parsed);

        String hexString = Integer.toHexString(255);
        System.out.println("255 в hex: " + hexString);

        String binaryString = Integer.toBinaryString(42);
        System.out.println("42 в двоичной: " + binaryString);

        int compared = Integer.compare(10, 20);
        System.out.println("Сравнение 10 и 20: " + compared);
    }
}

Эти методы очень полезны на практике. Integer.MAX_VALUE и Integer.MIN_VALUE — константы, содержащие максимальное и минимальное значения int. Integer.parseInt() преобразует строку в число. Integer.toHexString() возвращает шестнадцатеричное представление.

Когда какой тип использовать?

Вот практические рекомендации:

  • int — используйте по умолчанию для целых чисел. Это «рабочая лошадка» Java
  • long — когда int не хватает: большие ID, временные метки в миллисекундах, большие вычисления
  • byte — когда экономия памяти критична: обработка бинарных данных, изображений, сети
  • short — почти никогда (особенно в начале карьеры). Используется в некоторых legacy API

Золотое правило: если сомневаетесь — используйте int. В современных компьютерах экономия几个 байтов редко оправдана, а читаемость и простота кода важнее.

Итоги урока

  • Java предоставляет 4 типа целых чисел: byte (8 бит), short (16 бит), int (32 бита), long (64 бита)
  • По умолчанию литералы без суффикса считаются типом int
  • Для long-литералов обязательный суффикс L
  • Используйте подчёркивания для читаемости больших чисел
  • Поддерживается запись в десятичной, шестнадцатеричной (0x), восьмеричной (0) и двоичной (0b) системах
  • Переполнение происходит молча — без исключений!
  • Меньшие типы автоматически расширяются до больших (неявное расширение)
  • Для сужения типа требуется явное приведение (casting)
  • В арифметических операциях byte/short автоматически промоутируются до int
  • По умолчанию используйте int; используйте long когда нужен больший диапазон

Тест: Целые числа — byte, short, int, long

10 вопросов

Диапазоны целых чисел

Premium