Вопросы на собеседовании Java: коллекции, исключения и разбор кода

Вопросы Java с разбором кода: BigDecimal, equals/hashCode, ключи коллекций и ошибки read/close. Проверьте результаты и границы импорта на 33 локальных тестах.

Author: PracHub

Published: 10/11/2026

Вопросы на собеседовании Java: коллекции, исключения и разбор кода

October 11, 2026

Quick Overview

Русскоязычный разбор Java: независимый пример импорта транзакций, масштаб BigDecimal, ключи с валютой и матрица основного/подавленного исключения.

Software EngineerFree

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

Разберём эти ситуации на авторском импорте пяти транзакций. Здесь важнее предсказать результат, назвать нарушенный контракт и проверить исправление, чем перечислить реализации Map. Для начала откройте Debug and Fix Failing Unit Tests in Java: используйте тот же подход «ожидание — фактический результат — причина».

Граница доказательств: правила библиотек и языка подтверждены официальной документацией Oracle Java SE 25; пример запущен на OpenJDK 17.0.20.1. Транзакции и источник данных вымышлены. Это не отчёт кандидата, не список частых вопросов работодателя и не платёжная система. Денежные переводы, база данных, производительность и восстановление реального файла не проверялись.

Числовое значение, ключ с валютой и ошибки ресурса рассматриваются отдельно

Какие данные импортируем и что считаем одинаковым?

Вход — упрощённые строки id;currency;amount, где разрешены только точные обозначения USD и EUR. Идентификатор непустой; повтор любого уже принятого ID считается ошибкой. Положительные, отрицательные и нулевые десятичные значения разрешены правилами упражнения; точность и длина входа здесь не ограничены отдельной бизнес-валидацией. Это политика учебного набора, а не стандарт обработки денег.

СтрокаНазначение в проверке
t1;USD;10.0Первая транзакция с числовой суммой десять
t2;USD;10.00Другая транзакция, то же числовое значение
t3;EUR;10Та же величина в другой валюте
t4;USD;-4Отрицательное значение в разрешённом диапазоне модели
t5;USD;0Ноль должен оставаться допустимым

Уникальность транзакции определяется ID. Для отдельного отчёта о повторяющихся суммах группировка использует валюту и числовое значение. Поэтому t1 и t2 нельзя удалить как дубликат транзакции: это две разные записи, которые попадают в одну группу сумм.

Ожидаем пять транзакций, четыре группы валюты и суммы, итог USD 16 и EUR 10. Общую сумму 26 без валюты не вычисляем: преобразования валют в задании нет. Если интервьюер просит единый итог, сначала требуется отдельное правило конвертации и источник курсов.

Почему BigDecimal.equals и compareTo дают разные ответы?

Официальное API BigDecimal различает равенство представления и числовое сравнение. Для 10.0 и 10.00 метод equals учитывает масштаб, а compareTo возвращает ноль. Это предусмотренное поведение класса, не ошибка Java.

BigDecimal a = new BigDecimal("10.0");
BigDecimal b = new BigDecimal("10.00");
System.out.println(a.equals(b));       // false
System.out.println(a.compareTo(b));    // 0

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

Спросите, что именно нужно сравнивать: одинаковый текст, одинаковое представление или одинаковую числовую величину в конкретной валюте. Требование «убрать одинаковые суммы» без этого уточнения недостаточно. Один метод сравнения не определяет весь доменный смысл данных.

Как сломать контракт equals/hashCode своим адаптером?

В ошибочной обёртке автор решил считать значения одинаковыми через compareTo, но оставил исходный хеш суммы:

static final class Broken {
    final BigDecimal amount;
    Broken(String value) { amount = new BigDecimal(value); }
    public boolean equals(Object other) {
        return other instanceof Broken b
            && amount.compareTo(b.amount) == 0;
    }
    public int hashCode() { return amount.hashCode(); }
}

Обёртки 10.0 и 10.00 теперь равны, но в локальном запуске имеют разные хеши. Контракт Object требует одинакового хеша для равных объектов. Нельзя менять правило равенства, не согласовав с ним вычисление хеша.

В тесте HashSet с этими двумя обёртками получил размер два. Это наблюдение конкретного запуска для некорректного класса, а не обещание спецификации. Не объясняйте его тем, что HashSet «вообще не вызывает equals»: проблема в несогласованном контракте ключа.

Коллизия — обратная ситуация: разные объекты могут иметь одинаковый хеш и должны различаться по равенству. Исправление не обязано создавать уникальный хеш для каждой суммы. Оно обязано давать согласованное поведение для выбранного отношения равенства.

Как задать правильный ключ группировки?

Для отчёта этого упражнения нормализуем представление суммы при создании ключа и сохраняем валюту:

record AmountKey(String currency, BigDecimal amount) {
    AmountKey {
        Objects.requireNonNull(currency);
        amount = Objects.requireNonNull(amount).stripTrailingZeros();
    }
}

stripTrailingZeros сохраняет числовую величину, убирая конечные нули; это официальное поведение BigDecimal. Нормализация здесь служит нашей политике группировки. Исходная Tx сохраняет введённую сумму отдельно, поэтому ключ не становится форматом отображения или исходным документом.

Для USD 10.0 и USD 10.00 получаем равные ключи; для EUR 10 — другой ключ. Нули разных масштабов тоже должны объединяться. Эти случаи проверены локально. Не убирайте валюту из ключа только потому, что на одном маленьком примере итог кажется правдоподобным.

Нормализованное значение может отображаться в экспоненциальной форме. Отделяйте внутреннее представление от интерфейса: формат отчёта выбирается отдельно и не должен изменять логику равенства. В примере нет общей политики округления, количества знаков для валют или сохранения точного введённого текста.

Наконец, идентичность суммы не заменяет идентичность транзакции. Если вторая строка повторяет t1, importer отклоняет её, даже когда сумма совпадает. Это намеренно отличается от задачи, где одинаковые повторения разрешено молча пропускать. Сформулируйте правило до выбора коллекции.

Какие коллекции нужны разным частям задачи?

Разделите структуры по требованиям к идентификаторам, группам и итогам. Импорт использует множество ID для обнаружения повтора и список принятых транзакций для сохранения входного порядка. Отчёт группирует суммы в карте, а итоги по валютам сортирует отдельной картой.

static Map<AmountKey, Integer> groups(List<Tx> transactions) {
    Map<AmountKey, Integer> result = new HashMap<>();
    for (Tx tx : transactions) {
        result.merge(new AmountKey(tx.currency(), tx.amount()),
                     1, Integer::sum);
    }
    return result;
}

Документация HashMap не гарантирует порядок обхода. Проверка конкретного ключа не начнёт падать только из-за другого допустимого порядка обхода карты. В нашем запуске группа USD 10 содержит две транзакции; остальные три группы содержат по одной.

Для итогов используется TreeMap<String, BigDecimal> с точными кодами валют, чтобы получить порядок EUR, USD. TreeMap упорядочивает ключи по заданному компаратору или естественному порядку. Здесь сортируется строка валюты, а не исходные транзакции по сумме.

Смена HashMap на TreeMap для самих BigDecimal не была бы нейтральной оптимизацией. Сначала нужно проверить, согласован ли выбранный порядок с нужным равенством ключей. Перед заменой контейнера проверьте, сохраняется ли выбранное правило равенства.

Где проходит граница ошибки импорта?

Parser проверяет число полей, непустой ID и разрешённую валюту. Ошибка десятичного текста превращается в IllegalArgumentException с контекстом ID и сохранённой причиной NumberFormatException. Неправильная валюта и повтор ID тоже отклоняются, но не выдаются за ошибку формата числа.

try {
    return new Tx(fields[0], fields[1], new BigDecimal(fields[2]));
} catch (NumberFormatException cause) {
    throw new IllegalArgumentException(
        "invalid amount for " + fields[0], cause);
}

Фрагмент находится внутри parse после проверки полей. Полный учебный формат не поддерживает экранирование разделителя; пробелы автоматически не убираются. ID из одних пробелов отклоняется, но непустой ID сохраняется как введён. Если нужна другая нормализация, это отдельное изменение контракта.

Importer формирует локальный список и возвращает результат только после успешной обработки всех строк. При ошибке частичный список не возвращается. Это не доказывает откат внешних действий: в цикле нет записи в базу, отправки события или списания денег. Добавление таких действий потребовало бы другого анализа отказов.

Пустой вход возвращает пустой список; неисправный источник должен завершиться ошибкой. Не делайте catch (Exception) { return List.of(); }: вызывающая сторона перестанет различать отсутствие транзакций и сбой чтения. Перехватывайте ошибку там, где можете добавить полезный контекст или принять осмысленное решение.

Что происходит, когда read и close падают одновременно?

Второй авторский актив — ресурс Source, реализующий AutoCloseable. Его методы намеренно умеют выбрасывать IOException("read") и IOException("close"). Это управляемая имитация, а не проверка файловой системы.

static List<Tx> from(Source source) throws IOException {
    try (source) {
        return load(source.read());
    }
}

По официальным правилам try-with-resources ошибка закрытия подавляется, если блок уже завершился исключением. В нашем случае основной ошибкой остаётся чтение; ошибка закрытия доступна через getSuppressed().

При одновременном сбое чтения и закрытия read остаётся основным, close сохраняется как подавленное исключение

ЧтениеЗакрытиеРезультат проверенного вызова
УспешноУспешноВозвращён список из одной транзакции
УспешноОшибкаОсновное исключение close, подавленных нет
ОшибкаУспешноОсновное исключение read, подавленных нет
ОшибкаОшибкаОсновное исключение read, одно подавленное close

Во всех четырёх вариантах метод закрытия был вызван. Но вызов close и успешное освобождение реального ресурса — разные утверждения. Флаг в fake-объекте подтверждает первое; он не доказывает состояние дескриптора или удалённого сервиса.

Заметьте тонкость строки с успешным чтением и ошибкой закрытия: выражение результата уже могло быть вычислено, но вызывающий код не получает успешный возврат. Поэтому нельзя объявлять импорт успешным только по тому, что load закончил работу. Граница успеха — завершение всего вызова.

Чем cause отличается от suppressed?

Официальное API Throwable предоставляет отдельные способы получить причину и подавленные исключения. В parser причина объясняет, почему появилась новая ошибка с контекстом. В двойном отказе подавленное исключение сохраняет дополнительную ошибку закрытия рядом с основной.

Не называйте close причиной read: тест намеренно создаёт два разных отказа. И не заменяйте основной объект исключения новым только ради удобного текста, забыв вложенные сведения. При оборачивании ошибки сохраняйте исходный объект как cause; полный текст входной строки не обязательно должен попадать в пользовательское сообщение.

Запуск TransactionTrace завершился PASS 33 checks; набор проверяет: сравнение десятичных чисел, сломанный хеш, нормализованные ключи, валюту, нули, число транзакций и групп, итоги, повтор ID, ошибки полей и четыре комбинации ресурса. Он не покрывает все входные размеры, многопоточность и реальные ошибки ввода-вывода.

Для продолжения добавьте отдельный тест ошибки разбора после успешного чтения при сбое закрытия. Из правил языка следует ожидание, но этот дополнительный случай не входит в опубликованный счётчик 33. Различайте вывод из спецификации и выполненный эксперимент, даже если уверены в результате.

Перед расширением тестов составьте маленькую таблицу приёмки. Она фиксирует правила именно этого импортера и помогает заметить незапрошенное изменение поведения.

Изменение входаОжиданиеЧто оно защищает
Повторить t1;USD;10.0Ошибка повторного IDПовтор транзакции не принимается молча
Добавить новый ID с USD 10.00Новая транзакция, прежняя группа суммыРазделение двух видов идентичности
Передать t6;usd;10Ошибка поля валютыОтсутствие скрытой смены регистра
Передать t6;USD;noОшибка с причиной NumberFormatExceptionДиагностика формата суммы
Передать пустой списокПустой успешный результатОтличие отсутствия данных от неисправности

Не превращайте набор из 33 проверок в обещание полного покрытия. Например, он подтверждает конкретные нули и суммы, но не устанавливает максимальную длину числа и не проверяет каждое возможное десятичное представление. Для публичного сервиса потребовались бы ограничения размера запроса и числа записей. В этой статье они остаются предложением следующего шага, а не выполненной защитой.

Отдельно решите, где хранить исходную строку и нужен ли журнал отвергнутых записей. Текущий код выдаёт исключение и не создаёт такой журнал. Если требуется продолжать обработку, потребуется новый тип результата с принятыми и отклонёнными строками, а также политика повтора. Простое подавление исключения не создаёт эту политику автоматически.

При ревью попросите собеседника изменить одно условие, например разрешить повтор идентичного ID. Сначала обновите ожидаемое поведение в таблице, затем код и проверки. Так видно, что решение следует требованиям, а не защищает случайную реализацию любой ценой. Счётчик успешных проверок сам по себе не объясняет, какие правила вы выбрали.

Как дать короткую устную защиту решения?

Начните с требований: ID определяет транзакцию, валюта и нормализованная сумма — группу отчёта. Затем покажите контрпример t1/t2: одинаковая числовая сумма не делает их одной транзакцией. Это объясняет, почему у импортера и отчёта разные ключи.

Далее назовите проверенный результат: пять записей, четыре группы, USD 16, EUR 10. Уточните, что порядок групп HashMap не входит в контракт. После этого объясните двойной отказ: read — основное исключение, close — подавленное, успешного результата нет.

Завершите ограничением: пример работает в памяти, с двумя допустимыми валютами и управляемым источником. Округление, конвертация, безопасность повторной обработки и внешняя транзакция не реализованы. Такая формулировка позволяет обсуждать следующий шаг без обещаний, которых код не выполняет.

Практика на вопросах PracHub

В банке нет отдельного проверенного вопроса про эту конкретную нормализацию BigDecimal. Следующие задачи подходят для переноса навыка: искать нарушение контракта, исправлять проверки и отделять внутренний результат от внешних последствий. Это не подтверждение частоты вопросов на русскоязычных собеседованиях.

Полное название вопросаЧто перенести из упражнения
Debug and Fix Failing Unit Tests in JavaЗафиксировать ожидаемый результат перед исправлением
Design a generic key-value storeУточнить контракт ключа и значения
Code Review: Improve a Java Method That Builds a CSV StringПроверить границы формата и преобразования данных
Code Review: Partial Failure in a Java Payment Method That Charges Then RecordsОтличить локальную ошибку от уже выполненного внешнего действия
Review a Spring REST Controller for Correctness and Per-Request WasteОбосновать границу API и не скрывать неисправность

Начните с исправления падающих Java-тестов. Запишите ожидаемые равенства, группы и исключения, затем проверьте код. Сильная защита решения связывает каждое утверждение с контрактом или наблюдаемым запуском.

Sources and Further Reading

Источники проверены 11 октября 2026 года. Оригинальный пример прошёл 33 локальные проверки OpenJDK17; данные вымышлены, реальных операций с деньгами и отчётов кандидатов нет.


Comments (0)