Как мне следовало объяснить разницу между интерфейсом и абстрактным классом? [закрыто]
Описание проблемы:
В одном из интервью мне задали вопрос о различии между интерфейсом и абстрактным классом. Я представил свой ответ, в котором указал, что методы интерфейса в Java являются неявно абстрактными и не могут иметь реализацию, тогда как абстрактный класс может содержать экземплярные методы с реализацией поведения по умолчанию. Кроме того, я отметил, что переменные в интерфейсе по умолчанию являются финальными, а в абстрактном классе могут быть не финальными; члены интерфейса являются публичными по умолчанию, тогда как абстрактный класс может иметь члены различной видимости и т.д.
Однако интервьюер остался недоволен моим ответом и заявил, что я представил лишь "книжные знания". Он потребовал более практического ответа, в котором я должен был объяснить, когда я выбрал бы абстрактный класс вместо интерфейса, приведя практические примеры.
Где я ошибся и как мог бы улучшить свой ответ, чтобы удовлетворить требования интервьюера?
5 ответ(ов)
В данном случае, вместо интерфейса LoginAuth
, который требует реализации обоих методов для каждого класса, вы можете использовать абстрактный класс. Это позволит избежать дублирования кода для метода encryptPassword()
, если его реализация не зависит от базы данных.
Вот как можно переписать ваш пример, используя абстрактный класс:
public abstract class LoginAuth {
public String encryptPassword(String pass) {
// Реализация общего поведения, которое будет использоваться всеми подклассами
}
// Каждому подклассу необходимо предоставить собственную реализацию только этого метода:
public abstract void checkDBforUser();
}
Теперь в каждом из подклассов вам нужно будет реализовать только метод checkDBforUser()
, который зависит от конкретной базы данных, например:
public class DBMySQL extends LoginAuth {
@Override
public void checkDBforUser() {
// Реализация проверки пользователя для MySQL
}
}
public class DBOracle extends LoginAuth {
@Override
public void checkDBforUser() {
// Реализация проверки пользователя для Oracle
}
}
public class DBAbc extends LoginAuth {
@Override
public void checkDBforUser() {
// Реализация проверки пользователя для ABC
}
}
Таким образом, данный подход позволяет вам избежать дублирования кода для одинаковой логики, обеспечивая более чистую и организованную структуру кода.
Ничто не идеально в этом мире. Возможно, они ожидали более практического подхода.
Но после вашего объяснения можно добавить несколько строк с немного другим подходом.
- Интерфейсы — это правила (правила, потому что вы должны предоставить реализацию для них, от которой нельзя уклоняться или игнорировать, поэтому они навязываются как правила), которые служат общим документом для понимания между различными командами в разработке ПО.
- Интерфейсы дают представление о том, что нужно сделать, но не о том, как это будет сделано. Таким образом, реализация полностью зависит от разработчика, который следует заданным правилам (то есть заданным сигнатурам методов).
- Абстрактные классы могут содержать как абстрактные декларации, так и конкретные реализации или и то, и другое.
- Абстрактные декларации похожи на правила, которые нужно соблюдать, а конкретные реализации — на рекомендации (вы можете использовать их как есть или игнорировать, переопределив и предоставив свою реализацию).
- Более того, методы с одинаковыми сигнатурами, которые могут изменять поведение в разных контекстах, предоставляются в качестве деклараций интерфейсов как правила для реализации в соответствии с различными контекстами.
Правка: Java 8 позволяет определять методы по умолчанию и статические методы в интерфейсе.
public interface SomeInterfaceOne {
void usualAbstractMethod(String inputString);
default void defaultMethod(String inputString){
System.out.println("Внутри SomeInterfaceOne defaultMethod::"+inputString);
}
}
Теперь, когда класс реализует SomeInterfaceOne, не обязательно предоставлять реализацию для методов по умолчанию интерфейса.
Если у нас есть другой интерфейс с следующими методами:
public interface SomeInterfaceTwo {
void usualAbstractMethod(String inputString);
default void defaultMethod(String inputString){
System.out.println("Внутри SomeInterfaceTwo defaultMethod::"+inputString);
}
}
Java не позволяет расширять несколько классов, потому что это приводит к "Проблеме алмаза", когда компилятор не может решить, какой метод суперкласса использовать. С методами по умолчанию проблема алмаза может возникнуть и для интерфейсов. Если класс реализует оба
SomeInterfaceOne и SomeInterfaceTwo
и не реализует общий метод по умолчанию, компилятор не сможет решать, какой из них выбрать. Чтобы избежать этой проблемы, в Java 8 обязательно реализовать общие методы по умолчанию различных интерфейсов. Если какой-либо класс реализует оба упомянутых интерфейса, ему необходимо предоставить реализацию метода defaultMethod(), иначе компилятор выдаст ошибку на этапе компиляции.
Интерфейс состоит из единственных переменных (public static final) и публичных абстрактных методов. Обычно мы предпочитаем использовать интерфейс в реальных задачах, когда мы знаем, что нужно сделать, но не знаем, как это сделать.
Эта концепция станет более понятной на примере:
Рассмотрим класс Payment. Платеж можно выполнить различными способами, такими как PayPal, кредитная карта и т. д. Поэтому мы обычно рассматриваем Payment как наш интерфейс, который содержит метод makePayment()
, а CreditCard и PayPal — это две реализационные класса.
public interface Payment {
void makePayment(); // по умолчанию это абстрактный метод
}
public class PayPal implements Payment {
public void makePayment() {
// некоторая логика для платежа через PayPal
// например, PayPal использует имя пользователя и пароль для платежа
}
}
public class CreditCard implements Payment {
public void makePayment() {
// некоторая логика для платежа с кредитной карты
// например, кредитная карта использует номер карты, дату окончания срока действия и т.д.
}
}
В приведенном выше примере CreditCard и PayPal — это два реализационных класса/стратегии. Интерфейс также позволяет нам использовать концепцию множественного наследования в Java, что невозможно с помощью абстрактного класса.
Мы выбираем абстрактный класс, когда есть некоторые функции, по которым мы знаем, что нужно сделать, и другие функции, которые мы знаем, как реализовать.
Рассмотрим следующий пример:
public abstract class Burger {
public void packing() {
// некоторая логика для упаковки бургера
}
public abstract void price(); // цена различается для разных категорий бургеров
}
public class VegBurger extends Burger {
public void price() {
// установка цены для вегетарианского бургера
}
}
public class NonVegBurger extends Burger {
public void price() {
// установка цены для невегетарианского бургера
}
}
Если в будущем мы добавим методы (конкретные/абстрактные) в определенный абстрактный класс, то реализационному классу не придется изменять свой код. Однако, если мы добавим методы в интерфейс в будущем, нам нужно будет добавить реализации во все классы, которые реализуют этот интерфейс, в противном случае возникнут ошибки компиляции.
Существуют и другие отличия, но это основные, которые могли интересовать вашего интервьюера. Надеюсь, это было полезно.
Ваше объяснение выглядит вполне неплохо, но не создается ли впечатление, что вы просто читали это из учебника? 😕
Меня больше беспокоит, насколько хорошо был проиллюстрирован ваш пример. Вы учли почти все различия между абстрактными классами и интерфейсами?
Лично я бы порекомендовал эту ссылку: http://mindprod.com/jgloss/interfacevsabstract.html#TABLE
Там приведен исчерпывающий список отличий.
Надеюсь, это поможет вам и всем другим читателям в будущих собеседованиях.
Многие начинающие разработчики ошибочно воспринимают интерфейсы, абстрактные и конкретные классы как незначительные вариации одного и того же, выбирая один из них сугубо по техническим причинам: «Нужна ли мне множественная наследственность? Нужно ли мне место для общих методов? Стоит ли заморачиваться с чем-то, кроме конкретного класса?». Это неверно, и в этих вопросах скрыта основная проблема: «Я». Когда вы пишете код для себя, вам редко приходит в голову, что другие разработчики сейчас или в будущем будут работать с вашим кодом.
Интерфейсы и абстрактные классы, хотя на первый взгляд и похожи с технической точки зрения, имеют совершенно разные значения и цели.
Резюме
- Интерфейс определяет контракт, который некоторая реализация выполнит для вас.
- Абстрактный класс предоставляет поведение по умолчанию, которое ваша реализация может переиспользовать.
Эти два пункта — то, что я ищу на собеседованиях, и достаточно компактное резюме. Читайте дальше для получения более подробной информации.
Альтернативное резюме
- Интерфейс предназначен для определения публичных API.
- Абстрактный класс — для внутреннего использования и определения SPIs (интерфейсов поставщика услуг).
На примере
Проще говоря: конкретный класс выполняет фактическую работу очень специфическим образом. Например, ArrayList
использует смежный участок памяти для компактного хранения списка объектов, что обеспечивает быстрый случайный доступ, итерацию и изменения на месте, но ужасен в операциях вставки и удаления; в то время как LinkedList
использует двусвязные узлы для хранения списка объектов, что, наоборот, обеспечивает быструю итерацию, изменения на месте, а также вставку, удаление и добавление, но ужасен в случайном доступе. Эти два типа списков оптимизированы для различных сценариев использования, и важно, как вы собираетесь их использовать. Когда вы пытаетесь выжать производительность из списка, с которым активно работаете, и выбор типа списка ложится на вас, вы должны тщательно выбирать, какой именно вы инстанцируете.
С другой стороны, высокоуровневым пользователям списка неважно, как он фактически реализован, и они должны быть защищены от этих деталей. Представьте себе, что в Java не было бы интерфейса List
, а был бы только конкретный класс List
, который на самом деле является тем самым LinkedList
. Все разработчики на Java адаптировали бы свой код под детали реализации: избегали случайного доступа, добавляли бы кеш для ускорения доступа или просто сами бы реализовали ArrayList
, хотя это было бы несовместимо со всем остальным кодом, который на самом деле работает только с List
. Это было бы ужасно... Но теперь представьте, что мастера Java на самом деле осознали, что связный список ужасен для большинства реальных сценариев использования, и решили перейти на массивный список для единственного доступного класса List
. Это отразилось бы на производительности каждой программы на Java в мире, и люди были бы недовольны. А главной причиной стало бы то, что детали реализации были доступны, и разработчики полагали, что эти детали — постоянный контракт, на который они могут полагаться. Именно поэтому важно скрывать детали реализации и только определять абстрактный контракт. Это и есть цель интерфейса: определить, какой тип входа метод принимает и какой результат ожидается, не раскрывая всех внутренностей, которые могли бы искушать программистов подстраивать свой код под внутренние детали, которые могут измениться в любой будущей версии.
Абстрактный класс находится посередине между интерфейсами и конкретными классами. Он предназначен для помощи в реализации общих или скучных фрагментов кода. Например, AbstractCollection
предоставляет базовые реализации для isEmpty
(на основе размера 0), contains
(как итерацию и сравнение), addAll
(как повторный add
) и так далее. Это позволяет реализациям сосредоточиться на ключевых частях, которые их различают: как фактически хранить и извлекать данные.
Другой взгляд: API против SPI
Интерфейсы являются низкосвязными воротами между различными частями кода. Они позволяют библиотекам существовать и развиваться, не ломая всех пользователей библиотеки, когда что-то изменяется внутри. Это называется Интерфейсом Программирования Приложений (API), а не Классами Программирования Приложений. В меньшем масштабе они также позволяют нескольким разработчикам успешно сотрудничать в крупных проектах, разделяя различные модули через хорошо документированные интерфейсы.
Абстрактные классы являются высокосвязными помощниками, которые используются при реализации интерфейса, предполагая некоторый уровень деталей реализации. Альтернативно, абстрактные классы могут использоваться для определения SPIs (интерфейсов поставщика услуг).
Разница между API и SPI тонкая, но важная: для API акцент делается на том, кто его использует, а для SPI — на том, кто его реализует.
Добавление методов в API легко, все существующие пользователи API по-прежнему будут компилироваться. Добавление методов в SPI сложно, так как каждый поставщик услуг (конкретная реализация) должен будет реализовать новые методы. Если интерфейсы используются для определения SPI, поставщик должен будет выпустить новую версию каждый раз, когда контракт SPI изменяется. Если вместо этого используются абстрактные классы, новые методы могут быть определены в терминах существующих абстрактных методов или как пустые заглушки throw not implemented exception
, что, по крайней мере, позволит старой версии реализации сервиса по-прежнему компилироваться и работать.
Примечание о Java 8 и default-методах
Хотя Java 8 представила методы по умолчанию для интерфейсов, что еще больше размывает границу между интерфейсами и абстрактными классами, это было сделано не для того, чтобы реализации могли переиспользовать код, а чтобы упростить изменение интерфейсов, которые служат как API, так и SPI (или неправильно используются для определения SPI вместо абстрактных классов).
«Книжные знания»
Технические детали, представленные в ответе оригинального автора, считаются «книжными знаниями», поскольку это обычно подход, используемый в школе и в большинстве технологических книг о каком-либо языке: что такое вещь, а не как ее использовать на практике, особенно в крупных проектах.
Вот аналогия: предположим, вопрос был бы:
Что лучше арендовать на вечер выпуска, машину или гостиничный номер?
Технический ответ может звучать так:
В машине вы можете это сделать быстрее, но в гостиничном номере — более комфортно. С другой стороны, гостиничный номер находится в одном месте, в то время как в машине вы можете это сделать в разных местах, например, можете поехать на смотровую площадку для красивого вида, или в кино на открытом воздухе, или в многом месте, или даже в нескольких местах. Кроме того, в гостиничном номере есть душ.
Это все правда, но совершенно игнорирует тот факт, что это два совершенно разных вида вариантов, и оба могут быть использованы одновременно для разных целей, а «сделать это» — не самое важное в обоих вариантах. Ответ лишен перспективы, он демонстрирует незрелое мышление, правильно представляя фактические «факты».
Почему стоит использовать геттеры и сеттеры?
Почему статические методы не могут быть абстрактными в Java?
Примеры паттернов проектирования GoF в ядре библиотек Java
Может ли абстрактный класс иметь конструктор?
Как определить класс объекта?