Что позволяет ссылке интерфейсного типа вызывать методы Object, которых интерфейс явно не объявляет?
Java предоставляет специальное правило для публичных методов Object: они доступны через ссылку интерфейсного типа, даже если не записаны в объявлении интерфейса. Поэтому через такую ссылку можно вызвать, например, toString(), equals(Object), hashCode() и getClass().
Это не default-методы интерфейса. При выполнении вызова используется обычная реализация объекта: переопределённый метод класса либо унаследованный метод Object.
В Java интерфейс задаёт контракт поведения, а любой обычный объект одновременно является экземпляром Object. Специальное правило для его публичных методов позволяет работать с объектом через абстракцию интерфейса, не добавляя базовые операции в каждый интерфейс вручную.
Иначе универсальные операции вроде преобразования объекта в строку или сравнения пришлось бы отдельно объявлять почти во всех прикладных интерфейсах. При этом интерфейсы не становятся наследниками Object в обычном смысле: доступность этих методов обеспечивается правилами языка.
Рассмотрим ссылку, статический тип которой — интерфейс. Компилятор должен определить, существует ли у этого типа вызываемый метод, хотя интерфейс может содержать только прикладные методы.
Если метод объявлен в Object как публичный экземплярный метод, вызов разрешается. Если же попытаться вызвать через интерфейсную ссылку защищённый метод Object, например clone(), доступ будет запрещён: специальное правило распространяется не на все методы, а только на подходящие публичные операции.
На этапе компиляции Java проверяет вызов относительно статического типа ссылки. Для интерфейсного типа язык учитывает публичные экземплярные методы Object, поэтому вызовы toString(), equals(Object), hashCode() и getClass() корректны.
На этапе выполнения сохраняется полиморфизм экземплярных методов. Если реальный класс переопределяет toString(), будет вызвана его реализация; если переопределения нет, используется унаследованная реализация Object.
В примере интерфейс Describable не объявляет toString(), но вызов компилируется. Во время выполнения выбирается переопределение из User.
Это правило не превращает методы Object в default-реализации интерфейса и не распространяется на статические методы. Кроме того, защищённые методы вроде clone() нельзя вызвать через интерфейсную ссылку из обычного клиентского кода.
Практический компромисс таков: интерфейс может явно объявить прикладные операции, но базовые операции объекта остаются доступными без дублирования. При этом наличие toString() у интерфейсной ссылки не гарантирует полезный формат строки: конкретный класс может оставить стандартную реализацию Object.
Сервис принимает коллекцию объектов через интерфейс Event и пишет эти объекты в журнал. Вызов toString() через Event компилируется, но часть реализаций оставляет стандартное представление Object, поэтому журнал становится малоинформативным.
Возможны три подхода:
Object.toString() — минимум кода, но плохая диагностируемость;description() — явный контракт, но дополнительные обязательства для всех реализаций;toString() в каждой значимой реализации и использовать его для технического логирования — сохраняется стандартный Java-механизм, а результат становится полезным.Рациональным решением обычно является третий вариант, если требуется именно строковое представление объекта. Если строка является частью бизнес-контракта, лучше добавить отдельный явно названный метод: toString() не предназначен для строгого формата обмена данными.
Обязана ли реализация интерфейса явно переопределять toString()?
Нет. Если класс не объявляет собственную реализацию, он наследует публичный toString() из Object, и этого достаточно для вызова через интерфейсную ссылку. Однако результат может быть стандартным и не содержать полезных бизнес-данных.
Почему через интерфейсную ссылку нельзя вызвать clone() по тому же правилу?
clone() в Object имеет защищённый уровень доступа. Специальное правило касается публичных методов Object, а не всех его методов. Кроме того, корректное клонирование связано с реализацией Cloneable и обычно не считается универсальным контрактом интерфейса.
Вызывает ли toString() через интерфейсную ссылку реализацию интерфейса?
Нет, сам интерфейс не предоставляет для этого default-реализацию. Вызов остаётся полиморфным вызовом экземплярного метода: сначала используется переопределение реального класса, затем унаследованная реализация суперкласса, обычно Object. Поэтому изменение статического типа ссылки с класса на интерфейс не отключает динамический выбор реализации.