Предскажите поведение уже скомпилированного класса, реализующего интерфейс, в который позже добавили абстрактный метод.
Уже скомпилированный класс может успешно загрузиться, но вызов нового метода через ссылку интерфейсного типа завершится ошибкой AbstractMethodError, если в классе нет реализации этого метода. Компиляция старого класса не повторяется автоматически после изменения интерфейса.
Java поддерживает раздельную компиляцию библиотек и приложений: интерфейс и реализующие его классы могут поставляться в разных версиях и обновляться независимо. Поэтому совместимость Java делится как минимум на исходную — возможность перекомпилировать код, и бинарную — возможность запустить уже собранные class-файлы.
Добавление абстрактного метода обычно обнаруживается компилятором при повторной сборке реализации. Однако JVM не обязана заранее отвергать старый class-файл только потому, что новая версия интерфейса содержит больше методов. Это позволяет обнаружить проблему в момент фактического вызова.
Пусть версия библиотеки содержит интерфейс с методом open, а приложение собрано с классом LegacyStore, реализующим этот интерфейс. Затем библиотека выпускает новую версию, где в интерфейс добавлен абстрактный метод close.
Старый LegacyStore не содержит close, но его class-файл уже существует. Если приложение передаст объект как интерфейсную ссылку и вызовет новый метод, контракт интерфейса потребует реализацию, которой в классе нет. Результатом станет ошибка времени выполнения, хотя старое приложение ранее успешно компилировалось.
При вызове JVM разрешает символическую ссылку на метод интерфейса, а затем ищет подходящую реализацию в фактическом классе объекта и его иерархии. Если найденный класс должен реализовать метод, но конкретной реализации нет и у интерфейса нет подходящего default-метода, JVM не может выполнить вызов и выбрасывает AbstractMethodError.
Минимальная иллюстрация с двумя версиями библиотеки:
Фрагменты с одинаковым именем Store относятся к разным версиям интерфейса и не компилируются вместе; смысл примера — показать результат замены библиотеки без пересборки LegacyStore.
При повторной компиляции старого класса ошибка возникла бы раньше: компилятор потребовал бы реализовать close либо объявить класс абстрактным. При запуске без пересборки проблема откладывается до вызова.
Это отличается от NoSuchMethodError. NoSuchMethodError обычно означает, что вызывающий код ссылается на метод, отсутствующий в загруженном типе. AbstractMethodError означает, что метод ожидается контрактом или иерархией, но для конкретного объекта не найдено неабстрактной реализации.
Добавление default-метода часто снижает такой риск: старый класс может унаследовать готовую реализацию. Но это не универсальное решение: реализация по умолчанию может быть семантически неподходящей, а при конфликте нескольких default-методов возникнут отдельные правила разрешения или ошибка загрузки.
Команда выпускает SDK с публичным интерфейсом обработчика. В новой версии разработчики добавляют обязательный метод close, рассчитывая, что все потребители сразу пересоберут свои реализации. На практике часть плагинов загружается динамически и остаётся собранной со старой версией SDK.
Вариант с добавлением абстрактного метода хорошо выражает обязательность нового контракта, но может привести к AbstractMethodError в рабочей системе. Требование срочно пересобрать все плагины снижает риск, однако часто невозможно для внешних поставщиков.
Вариант с default-реализацией сохраняет бинарную совместимость и позволяет старым плагинам продолжить работу. Минус — невозможно гарантировать, что старый плагин корректно освобождает свои ресурсы через универсальную реализацию.
Практичное решение — сохранить старый интерфейс стабильным, добавить новый интерфейс расширения или выпустить новую версию контракта с адаптером. Это требует дополнительного API, но делает несовместимость явной и предотвращает скрытый сбой при вызове. В результате новые реализации получают строгий контракт, а старые плагины продолжают работать через прежний интерфейс.
1. Обязательно ли старый класс немедленно упадёт при загрузке?
Нет. Отсутствие нового метода не обязательно приводит к ошибке уже при загрузке class-файла. Класс может быть загружен и создан, а проблема проявится при разрешении и вызове конкретного метода. Поэтому проверка успешного старта приложения не доказывает бинарную совместимость всех вызовов.
2. Что изменится, если новый метод объявить default вместо абстрактного?
Класс без собственного метода может унаследовать default-реализацию интерфейса, поэтому вызов обычно продолжится без AbstractMethodError. Однако default-метод должен быть совместим с поведением старых реализаций. Если другой интерфейс или суперкласс создаёт конфликт, одного добавления default недостаточно для гарантии корректной работы.
3. Почему повторная компиляция может обнаружить проблему раньше запуска?
При компиляции класса компилятор видит актуальное содержимое интерфейса и проверяет, что неабстрактный класс удовлетворяет всем его абстрактным методам. Если новый метод не реализован, компиляция завершается ошибкой. При запуске старого class-файла этой проверки исходного кода уже нет, поэтому несовместимость может проявиться только во время вызова.