ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Типы, классы и ссылки Java: от переменной к контракту объекта

Автор: · Обновлено

Перед коллекциями и Stream API нужно различать значение переменной, ссылку на объект и действие метода. Иначе изменение общего списка кажется случайным, а null ошибочно…

Перед коллекциями и Stream API нужно различать значение переменной, ссылку на объект и действие метода. Иначе изменение общего списка кажется случайным, а null ошибочно воспринимается как пустой объект.

Тип переменной задаёт допустимые операции

У примитивных переменных, например int и boolean, хранятся примитивные значения. Переменная ссылочного типа указывает на объект либо содержит null. String, массив и пользовательский класс — ссылочные типы. В List<Integer> нужен тип-обёртка Integer, потому что параметр generic не может быть примитивным int. Примитивные типы.

Объявление int count = 3 фиксирует тип на этапе компиляции. Оно не позволит позднее записать строку. Для локальной переменной можно написать var count = 3: компилятор выведет тот же int, а не динамический тип. Для целочисленного деления 5 / 2 получится 2; результат 5 / 2.02.5. При расчётах выбирайте тип до операции: преобразование уже усечённого результата не вернёт дробную часть.

Класс описывает состояние и разрешённые действия

Сохраните полный пример как StockDemo.java и запустите двумя командами из первого урока, заменив имя класса:

public class StockDemo {
    static final class Stock {
        private final String name;
        private int count;

        Stock(String name, int count) {
            if (name == null || name.isBlank() || count < 0) {
                throw new IllegalArgumentException("Invalid stock");
            }
            this.name = name;
            this.count = count;
        }

        void add(int amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException("Amount must be positive");
            }
            count = Math.addExact(count, amount);
        }

        String describe() {
            return name + "=" + count;
        }
    }

    static void replaceLocal(Stock stock) {
        stock = new Stock("local", 100);
    }

    public static void main(String[] args) {
        Stock original = new Stock("notebook", 2);
        Stock alias = original;
        alias.add(3);
        replaceLocal(original);
        System.out.println(original.describe());
        System.out.println(original == alias);
        System.out.println(new String("java").equals("java"));
    }
}

Ожидаемый вывод:

notebook=5
true
true

Конструктор Stock(...) проверяет исходные данные, this.name обращается к полю создаваемого объекта. private закрывает поля от прямого доступа снаружи класса; метод add задаёт допустимый путь изменения. Вложенный static-класс выбран, чтобы пример оставался одним файлом. Определение класса.

Две ссылки не создают две копии

После Stock alias = original обе переменные указывают на один Stock. Поэтому вызов alias.add(3) виден через original. При передаче аргумента Java копирует значение переменной, включая значение ссылки. Метод может изменить объект через эту ссылку, но присваивание параметру другого объекта не меняет переменную вызывающего кода. Этим объясняется, почему replaceLocal не превращает исходный товар в local=100. Передача аргументов.

Ключевое слово final у переменной запрещает заменить её значение, но само по себе не замораживает объект по ссылке. У поля name здесь одновременно выбран неизменяемый тип String; поле count изменяемое по правилам метода. Не смешивайте эти два ограничения.

Равенство и отсутствие значения

Для ссылок == сравнивает, указывают ли они на один объект. equals выражает равенство, определённое классом: например, у String сравнивается содержимое. У собственного класса без переопределения equals логического сравнения полей автоматически не появляется. Поэтому два отдельно созданных Stock с одинаковыми данными ещё не обязаны быть равны. Контракт Object.

null означает отсутствие ссылки. На нём нельзя вызвать describe, а выражение Integer n = null; int value = n; падает при автоматической распаковке. Проверка входных данных в конструкторе приближает ошибку к её причине: читателю не приходится выяснять спустя десять методов, почему у товара исчезло имя.

Контроль границ на практике

Добавьте в main создание new Stock("", 1) и убедитесь, что конструктор отвергает пустое имя. Затем попробуйте alias.add(0). Это два нарушения разных контрактов, а не поводы перехватывать все исключения и продолжать работу с неизвестным состоянием.

Напишите отдельный метод totalMinutes(int lessons, int minutes) с проверкой неотрицательных аргументов. Проверьте (3, 40), (0, 40) и отрицательное число. Для больших значений сравните обычное умножение с Math.multiplyExact: переполнение целого не должно незаметно превращать отчёт в отрицательное число. Точные арифметические операции Math.

Самопроверка: что изменится, если метод вместо присваивания параметру вызовет stock.add(1)? Исходный объект изменится, потому что скопированная ссылка всё ещё ведёт к нему. Именно этот механизм позже объяснит, почему список внутри Map меняется после get.

Частые вопросы

Нужно ли писать класс для каждого числа?

Нет. Примитивы подходят для отдельных значений, а класс объединяет связанные данные и поведение. Выделение класса оправдано, когда появляется общий смысл и правила допустимого состояния, как у товара и его остатка.

Можно ли всегда заменить equals на ==?

Для логического сравнения объектов это неверно. == отвечает про одну и ту же ссылку, а equals — про договорённость конкретного типа. Строки с одинаковым текстом могут быть разными объектами.

Источники