Допустим, объект фактически создан как экземпляр класса, но сохранён в переменной интерфейсного типа: доступен ли через такую переменную публичный метод, которого нет в интерфейсе?
Нет. На этапе компиляции доступные методы определяются типом ссылки — интерфейсом, а не фактическим классом объекта. Метод, объявленный только в классе реализации, можно вызвать после явного приведения ссылки к этому классу, если такое приведение допустимо.
Интерфейсы отделяют контракт объекта от его конкретной реализации. Такой подход позволяет клиентскому коду зависеть от минимального набора операций и заменять одну реализацию другой без изменения этого кода.
Статическая проверка доступных методов появилась как средство обнаруживать ошибки до запуска программы. Поэтому компилятор не использует потенциально доступные методы фактического класса, иначе код через интерфейсную ссылку зависел бы от конкретной реализации.
Фактический объект может содержать больше методов, чем объявлено в интерфейсе. Однако вызов дополнительного метода через интерфейсную ссылку не компилируется, даже если во время выполнения объект действительно принадлежит нужному классу.
Попытка обойти ограничение без проверки типа создаёт риск ClassCastException. Кроме того, добавление специфичных для реализации вызовов уменьшает заменяемость реализации и связывает код с конкретным классом.
При обращении к методу компилятор сначала проверяет, является ли этот метод членом статического типа ссылки. Для интерфейсной ссылки он видит методы самого интерфейса, его родительских интерфейсов и методы Object, доступные через интерфейсную ссылку.
Во время выполнения выбор переопределённой реализации уже зависит от фактического типа объекта. Но динамический полиморфизм выбирает реализацию только среди методов, доступных по контракту ссылки; он не расширяет этот контракт новыми методами.
Вызов save корректен, потому что метод объявлен в Storage, а фактический объект предоставляет его реализацию. Вызов compress через Storage недоступен; после приведения компилятор рассматривает ссылку как FileStorage.
Безопасность приведения можно обеспечить проверкой типа, например через instanceof, либо спроектировать интерфейс так, чтобы нужная операция входила в его контракт. Первый вариант сохраняет исходный интерфейс, но добавляет ветвление и зависимость от реализации; второй лучше поддерживает полиморфизм, но расширяет публичный контракт.
Важно отличать это от переопределения. Если метод присутствует в интерфейсе и переопределён классом, вызов через интерфейсную ссылку выполнит реализацию фактического класса. Если метода нет в интерфейсе вообще, динамический диспетчер не может сделать его доступным через такую ссылку.
В системе хранения данных есть интерфейс Storage с операциями сохранения и чтения. Конкретная реализация CloudStorage дополнительно предоставляет настройку репликации, а часть бизнес-кода пытается вызывать её через переменную типа Storage.
Первый вариант — добавить настройку репликации в Storage. Это делает операцию доступной всем реализациям, но может навязать неуместный метод локальному хранилищу и расширить контракт большим числом специфичных возможностей.
Второй вариант — повсеместно приводить Storage к CloudStorage. Он быстро решает задачу, но ломает заменяемость реализаций и приводит к ошибкам при использовании другого хранилища.
Выбранный вариант — выделить отдельный интерфейс возможностей, например для репликации, и проверять поддержку этой возможности на границе специализированного сценария. Основной код продолжает зависеть от Storage, а код, которому действительно нужна репликация, явно работает с дополнительным контрактом. Это уменьшает связанность и делает невозможную операцию заметной в типах.
Он влияет на выбор реализации уже доступного переопределённого метода, но не на набор методов, который разрешает компилятор. Набор определяется статическим типом ссылки. Поэтому объект FileStorage может выполнить собственную реализацию save, но наличие у него compress не разрешает вызов compress через Storage.
Безопасность зависит от конкретного значения ссылки в момент приведения, а не от типа переменной и не от предположения программиста. Если ссылка действительно указывает на экземпляр нужного класса или его подкласса, приведение успешно; иначе возникает ClassCastException. Если тип заранее неизвестен, следует проверить его или использовать отдельный интерфейсный контракт.
Метод интерфейса становится частью общего контракта, поэтому каждая неабстрактная реализация обязана его предоставить либо унаследовать готовую реализацию. Клиентский код после этого не зависит от конкретного класса и сохраняет полиморфность. Компромисс состоит в том, что изменение интерфейса затрагивает все реализации и может сделать контракт слишком широким, поэтому специализированные возможности часто выносят в отдельные интерфейсы.