Если вы уже писали код на другом языке, Java покажется одновременно знакомой и непривычно строгой. Здесь у каждой переменной заранее объявлен тип, программа не запустится, пока не скомпилируется, а часть привычных вольностей просто запрещена. Разберём базу: как объявить переменную, чем int отличается от Integer, почему строки нельзя изменить и из чего состоит управляющая логика.
Примитив хранит значение прямо в переменной. Обёртка хранит только ссылку: автобоксинг создаёт объект в куче, анбоксинг достаёт значение обратно — а если ссылки нет, распаковка `null` роняет программу.
Зачем такая строгость
Java — язык со статической типизацией. Это значит, что тип каждой переменной известен ещё до запуска программы, на этапе компиляции. Компилятор проверяет, что вы не складываете число со списком и не вызываете у строки несуществующий метод. Ошибки такого рода ловятся сразу, а не во время работы у пользователя.
Короткая формула: в Java сначала компиляция, потом запуск. Исходный код (.java) превращается в байт-код (.class), который исполняет виртуальная машина JVM. Поэтому опечатка в имени метода — это не падение у пользователя, а ошибка сборки прямо в редакторе.
Путь от файла до запуска: выделен javac, потому что именно он проверяет типы и имена, и до JVM доходит только то, что проверку прошло.
Точка входа: метод main
Любая Java-программа начинается с метода main. JVM ищет именно его и запускает первым:
живой пример
public class App {
public static void main(String[] args) {
System.out.println("Привет, Java"); // вывод строки в консоль
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Сигнатура по словам:
public— метод доступен извне, JVM должна его увидеть;static— метод принадлежит классу, а не объекту, его можно вызвать без создания экземпляра;void— метод ничего не возвращает;String[] args— массив аргументов командной строки.
Что стоит за class, public и static, разобрано в статье про ООП.
Переменные и var
Переменная объявляется по схеме «тип имя = значение»:
int age = 30;
String name = "Анна";
boolean active = true;
Тип слева обязателен — компилятор должен знать, что вы собираетесь хранить. Но с Java 10 появилось ключевое слово var: оно просит компилятор вывести тип автоматически из правой части.
var age = 30; // компилятор видит число → int
var name = "Анна"; // видит строку → String
var items = new ArrayList<String>(); // тип ясен из конструктора
Важно: var — это не «нетипизированная» переменная, как в динамических языках. Тип всё равно фиксируется намертво, просто его не пишут руками. Записать потом в age строку нельзя. И var работает только там, где тип очевиден из значения: для локальных переменных внутри методов, для переменной цикла for, для ресурса в try (var conn = ...) и для параметров лямбды. В полях класса, параметрах и типе результата метода var писать нельзя — там тип нужен тем, кто читает сигнатуру.
Примитивы против объектов
В Java есть два мира значений.
Примитивные типы — это «голые» значения, лежащие в памяти напрямую. Их восемь, но на старте важны эти:
int count = 100; // целое, 32 бита
long big = 9_000_000_000L; // 64 бита: в int такое уже не влезает
double distance = 19.99; // дробное
boolean ok = false; // true / false
char letter = 'A'; // один символ в одинарных кавычках
Остальные три — byte и short (те же целые числа, только уже: 8 и 16 бит) и float (дробное, вдвое короче double и вдвое грубее). В обычном коде они попадаются редко, но в схемах базы данных и в разговоре про точность всплывут.
Про double сразу одна оговорка. Он хранит дробь приближённо, поэтому 0.1 + 0.2 в нём не равно 0.3, а копейки после десятка арифметических действий разъезжаются. Цену в примере так писать можно, деньги в настоящем коде — нет: для них берут BigDecimal или считают в копейках обычным long.
Объектные (ссылочные) типы — это всё остальное: строки, списки, ваши собственные классы. Переменная хранит не само значение, а ссылку на объект в памяти. У каждого примитива есть объектный «двойник»-обёртка: int → Integer, long → Long, boolean → Boolean, double → Double.
Зачем нужны обёртки? Многие части стандартной библиотеки (например, коллекции) умеют работать только с объектами, а не с примитивами. В список нельзя положить int, но можно Integer.
Автобоксинг
Чтобы не переключаться между мирами вручную, Java делает преобразование сама — это автобоксинг (примитив → обёртка) и анбоксинг (обёртка → примитив). Запустите и посмотрите на вывод:
живой пример
public class Boxing {
public static void main(String[] args) {
Integer boxed = 5; // автобоксинг: int 5 → Integer
int back = boxed; // анбоксинг: Integer → int
System.out.println("boxed=" + boxed + ", back=" + back);
Integer a = 100, b = 100;
Integer x = 1000, y = 1000;
System.out.println("100 == 100 → " + (a == b));
System.out.println("1000 == 1000 → " + (x == y));
System.out.println("equals → " + x.equals(y));
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Две последние строки и есть главная ловушка. == для объектов сравнивает ссылки, а не значения, и с маленькими числами это особенно коварно: значения от −128 до 127 Java кэширует и переиспользует, поэтому две сотни — это один и тот же объект и == даёт true, а две тысячи — уже разные объекты и false. Причём верхнюю границу кэша можно сдвинуть ключом запуска -XX:AutoBoxCacheMax=2000 — и тогда тот же самый код напечатает true там, где печатал false. Поэтому «до 127 работает» — не правило, на которое можно опереться, а объяснение, почему нельзя опираться на ==. Сравнивать обёртки нужно через .equals(); подробнее — в материале про коллекции.
Вторая ловушка: обёртка может быть null, и попытка распаковать null в примитив уронит программу с NullPointerException. Правило простое: если значение точно есть и оно одно — берите примитив int; если значение может отсутствовать или его надо положить в коллекцию — берите Integer.
Строки и их неизменяемость
String — это объект, и в Java строки неизменяемы. Любая операция, которая «меняет» строку, на самом деле создаёт новую, а старая остаётся прежней. Заодно видно, чем .equals() отличается от ==:
живой пример
public class Strings {
public static void main(String[] args) {
String greeting = "Привет";
String full = greeting + ", мир"; // создалась НОВАЯ строка
System.out.println(greeting); // «Привет» — не изменилась
System.out.println(full);
var sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i).append(' '); // меняем один и тот же буфер
}
System.out.println(sb.toString());
String copy = new String("Привет");
System.out.println(copy.equals(greeting) + " — содержимое совпало");
System.out.println((copy == greeting) + " — а объекты разные");
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Неизменяемость сделана ради безопасности и предсказуемости: раз строку нельзя испортить, её можно спокойно передавать куда угодно и использовать как ключ. Минус — если в цикле склеивать тысячи строк через +, каждый раз создаётся новый объект; для такого случая и нужен StringBuilder, который меняет один буфер.
Неизменяемость даёт и второй эффект, ради которого в примере выше две строки с одинаковым текстом оказались разными объектами. Строковые литералы в коде Java держит в общем хранилище, пуле строк: когда в двух местах программы написано "Привет", это один объект, и == для них даст true. Так можно, потому что строку никто не испортит. А new String("Привет") создаёт отдельный объект в обход пула, и == с литералом уже false, хотя содержимое то же. Склейка констант вроде "при" + "вет" тоже сворачивается компилятором в литерал и попадает в пул; склейка с переменной создаёт новый объект. Метод intern() возвращает экземпляр из пула для любой строки, но в прикладном коде он почти не нужен. Вывод для вас один: == на строках иногда работает случайно, и полагаться на это нельзя; сравнивайте через equals.
А с Java 15 для многострочного текста есть «текстовые блоки» в тройных кавычках:
String json = """
{
"name": "Анна"
}
""";
Операторы
Арифметика, сравнение и логика записываются как почти в любом языке, и одна деталь в них важнее всего остального: логические && и || ленивы. Если левая часть && уже false, правую Java не вычисляет, поэтому if (user != null && user.isActive()) не падает на null: до второго условия дело не доходит. Поменяйте части местами, и проверка на null перестанет защищать.
int sum = 2 + 3; // арифметика: + - * / %
boolean adult = age >= 18; // сравнение: == != < > <= >=
boolean both = active && adult; // логика: && (и), || (или), ! (не)
Есть и тернарный оператор — короткая форма выбора одного из двух значений:
String label = adult ? "взрослый" : "несовершеннолетний";
Управляющие конструкции
Ветвление — if / else if / else:
if (price > 100) {
System.out.println("дорого");
} else if (price > 50) {
System.out.println("средне");
} else {
System.out.println("дёшево");
}
Циклы — for, while и for-each для перебора коллекций:
for (int i = 0; i < 3; i++) { // классический счётчик
System.out.println(i);
}
int n = 3;
while (n > 0) { // пока условие истинно
n--;
}
var names = List.of("Анна", "Борис");
for (String person : names) { // for-each: по каждому элементу
System.out.println(person);
}
Выбор по значению — switch. В современной Java есть короткая стрелочная форма, которая сразу возвращает результат и не требует break:
живой пример
public class Roles {
public static void main(String[] args) {
for (String role : new String[]{"admin", "editor", "guest"}) {
int level = switch (role) {
case "admin" -> 3;
case "editor" -> 2;
default -> 1; // обязательная ветка «всё остальное»
};
System.out.println(role + " → " + level);
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Старая форма с case ... : и break тоже работает, но стрелочный switch короче и не даёт случайно «провалиться» в следующую ветку.
Глубже: массивы: объявление, length, многомерные и Arraysрасширенное
Статья про коллекции начинается словами «массив в Java фиксированного размера», как о чём-то известном, а в этой фазе массив ещё не встречался. Вот он целиком, потому что под каждым ArrayList лежит именно он.
Массив это блок из фиксированного числа ячеек одного типа. Размер задаётся при создании и не меняется; индексы от нуля до length - 1, и выход за них роняет программу с ArrayIndexOutOfBoundsException. Ячейки заполняются значением по умолчанию: ноль для чисел, false для boolean, null для объектов.
живой пример
import java.util.Arrays;
public class ArraysDemo {
public static void main(String[] args) {
int[] scores = new int[3];
scores[0] = 7;
String[] names = {"Анна", "Борис", "Вера"};
System.out.println(scores.length + " " + scores[1] + " " + names[2]);
int[] sorted = {5, 1, 4};
Arrays.sort(sorted);
System.out.println(Arrays.toString(sorted));
System.out.println(Arrays.toString(Arrays.copyOf(sorted, 5)));
int[][] grid = new int[2][3];
grid[1][2] = 9;
System.out.println(Arrays.deepToString(grid));
System.out.println(sorted.equals(new int[]{1, 4, 5}) + " " + Arrays.equals(sorted, new int[]{1, 4, 5}));
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Пять вещей из вывода. length это поле, а не метод, в отличие от size() у коллекций и length() у строки. Arrays.toString нужен, потому что сам массив печатается как [I@1b6d3586, адрес, а не содержимое; для вложенных массивов deepToString. Arrays.copyOf это единственный способ «увеличить» массив: создать новый и скопировать. Многомерный массив это массив массивов, и строки в нём могут быть разной длины. И equals у массива сравнивает ссылки, а не содержимое, для содержимого есть Arrays.equals; та же история с hashCode, и поэтому массив плохой ключ для HashMap.
Что ещё есть в Arrays: asList делает список фиксированного размера поверх массива (добавить нельзя, изменить элемент можно), fill заполняет, binarySearch ищет в отсортированном, stream превращает в поток. В прикладном коде массив встречается реже коллекций: String[] args, буферы байтов, результаты из библиотек; но ArrayList внутри это массив с копированием при росте, и его поведение из раздела про коллекции объясняется именно этим.
Глубже: стек и куча: в Java всё передаётся по значениюрасширенное
Три темы фазы объясняют один и тот же механизм каждая заново: изменяемый ключ в HashMap, защитные копии в record, корни для сборщика мусора. Механизм один, и его стоит знать отдельно.
У работающей программы две области памяти. Стек у каждого потока свой: при вызове метода на нём появляется кадр с локальными переменными и параметрами, при возврате кадр исчезает. Куча общая: там живут все объекты, созданные через new, и туда же попадают строки и массивы. Локальная переменная примитивного типа хранит само значение прямо в кадре; локальная переменная объектного типа хранит ссылку, адрес объекта в куче.
Отсюда правило, которое звучит парадоксально: в Java всё передаётся по значению, включая ссылки. При вызове метода в его кадр копируется значение переменной; для примитива это число, для объекта это ссылка. Метод получает копию ссылки на тот же объект, поэтому может изменить объект, но не может подменить объект у вызывающего:
живой пример
import java.util.ArrayList;
import java.util.List;
public class ByValue {
static void change(int n, List<String> list, StringBuilder sb) {
n = 42;
list.add("добавлено внутри");
sb = new StringBuilder("новый объект");
}
public static void main(String[] args) {
int n = 1;
List<String> list = new ArrayList<>();
StringBuilder sb = new StringBuilder("исходный");
change(n, list, sb);
System.out.println(n + " | " + list + " | " + sb);
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Число осталось единицей: метод менял свою копию. Список изменился: обе ссылки ведут в один объект в куче. StringBuilder остался исходным: метод переназначил свою копию ссылки на новый объект, а копия у вызывающего смотрит туда же, куда смотрела.
Что из этого следует для остальной фазы. Ключ HashMap это ссылка на объект, и если объект изменить после put, ключ в таблице «переедет» вместе с ним, а его хеш останется старым. record с полем-списком хранит ссылку на список, и тот, кто передал список, может менять его дальше, отсюда защитная копия. Сборщик мусора начинает от корней, и локальные переменные в кадрах стека это главные корни: объект жив, пока на него есть ссылка с какого-нибудь стека. И String неизменяем именно затем, чтобы передавать ссылку на него было безопасно.
Глубже: числа и точность: деление, переполнение, double и BigDecimalрасширенное
Оговорка про double выше требует продолжения, потому что ошибок с числами четыре, и все четыре молчаливые: программа не падает, а считает неверно.
живой пример
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Numbers {
public static void main(String[] args) {
System.out.println(7 / 2 + " " + 7 / 2.0 + " " + 7 % 2);
System.out.println(Integer.MAX_VALUE + 1);
System.out.println(0.1 + 0.2);
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
BigDecimal price = new BigDecimal("19.99");
System.out.println(price.multiply(new BigDecimal("3")).setScale(2, RoundingMode.HALF_UP));
System.out.println(new BigDecimal("1.10").equals(new BigDecimal("1.1")) + " " + (new BigDecimal("1.10").compareTo(new BigDecimal("1.1")) == 0));
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Целочисленное деление. 7 / 2 даёт 3: оба операнда целые, дробная часть отбрасывается без предупреждения; 7 / 2.0 даёт 3.5, потому что один операнд дробный. Средний балл, посчитанный как sum / count при целых sum и count, всегда будет округлён вниз, и это самая частая ошибка первого месяца.
Переполнение. int вмещает числа до двух с небольшим миллиардов, и Integer.MAX_VALUE + 1 даёт отрицательное число, а не ошибку: биты переворачиваются по кругу. Счётчик просмотров, сумма в копейках за год, идентификатор из внешней системы упираются в это; для них long, а для проверки на переполнение Math.addExact, который бросает исключение вместо тихого мусора.
Погрешность double. 0.1 + 0.2 даёт 0.30000000000000004: двоичная дробь не может точно представить десятичную, как десятичная не может точно представить треть. Для расстояний, физики и графики это несущественно; для денег недопустимо, потому что копейки разъезжаются после нескольких операций и не сходятся с бухгалтерией.
BigDecimal для денег. Хранит десятичное число точно, считает без потерь, округляет только там, где вы явно попросили (setScale с режимом). Создают его из строки, а не из double (new BigDecimal(0.1) уже содержит погрешность), сравнивают через compareTo, потому что equals учитывает масштаб и считает 1.10 и 1.1 разными. В базе денежная колонка это numeric, а не float, о чём статья про типы чисел в PostgreSQL; в JSON сумму отдают строкой или целым в копейках, о чём статья про формат ответа в разделе REST.
Коротко
- Java — статически типизированный язык: тип известен на этапе компиляции, ошибки ловятся до запуска.
- Любая программа стартует с метода
public static void main(String[] args). varне отменяет типизацию — он лишь выводит тип автоматически для локальных переменных.- Примитивы (
int,double,boolean) хранят значение напрямую; объектные обёртки (Integer,Double) хранят ссылку, могут бытьnullи нужны для коллекций. Перевод между ними — автобоксинг. - Обёртки и строки сравнивают через
.equals():==сравнивает ссылки, на числах больше 127 это видно сразу, а на строках иногда работает случайно из-за пула литералов. - Строки неизменяемы: «изменение» создаёт новый объект, для активной склейки есть
StringBuilder. - Управляющие конструкции стандартны:
if/else,for,while,for-eachи современный стрелочныйswitch. - Массив это блок фиксированного размера с
length, индексами от нуля и значениями по умолчанию; печать, сравнение и копирование черезArrays, «увеличить» значит скопировать. - Примитивы лежат в кадре стека, объекты в куче, переменная хранит ссылку; всё передаётся по значению, включая ссылки: метод меняет объект, но не подменяет его у вызывающего.
- Целое на целое даёт целое,
intпереполняется молча,doubleхранит дробь приближённо; деньги этоBigDecimalиз строки со сравнением черезcompareTo.
Что почитать дальше
- ООП в Java: классы, объекты, наследование — что на самом деле стоит за
class,publicиstatic. - Коллекции Java — где обёртки
Integerи неизменяемость строк проявляются на практике. - Records, Optional и современный Java — куда выросли
var,switchи текстовые блоки. - Инструменты Java-разработчика — как собрать и запустить код, который мы здесь писали.