В чём разница между abstract class и interface в Kotlin?
abstract class хранит состояние и имеет конструктор, допускает одно наследование. interface описывает контракт без backing fields, поддерживает default-методы и множественную реализацию.
Ключевые отличия
abstract class — это класс с частичной реализацией. Он может хранить состояние (поля с backing fields), иметь конструкторы с параметрами, и от него можно наследоваться только один раз. interface — это контракт без состояния (нет backing fields), он может содержать default-методы, но не может иметь конструктор. Класс может реализовать сколько угодно интерфейсов.
Когда выбирать abstract class
- Нужно разделить общее состояние между наследниками (например,
val scope: CoroutineScope). - Нужен конструктор с обязательными параметрами — интерфейс так не может.
- Хотите запретить реализацию вне модуля —
sealed abstract class.
Когда выбирать interface
- Нужна множественная реализация — класс реализует несколько интерфейсов одновременно.
- Описываете чистый контракт без привязки к иерархии (
Comparable,Serializable). - Нужна binary compatibility при эволюции API — добавление default-метода не ломает клиентов.
Пример
// abstract class хранит состояние и имеет конструктор
abstract class BaseRepository(
protected val db: Database
) {
abstract suspend fun findById(id: Long): Entity?
suspend fun exists(id: Long): Boolean = findById(id) != null
}
// interface описывает контракт — без состояния
interface Cacheable {
val cacheKey: String
fun invalidate() {
CacheManager.evict(cacheKey) // default-реализация
}
}
// Класс наследует один abstract class и реализует несколько интерфейсов
class UserRepository(
db: Database,
private val redis: RedisClient
) : BaseRepository(db), Cacheable, AutoCloseable {
override val cacheKey = "users"
override suspend fun findById(id: Long): Entity? =
redis.get("user:$id") ?: db.query("SELECT * FROM users WHERE id = ?", id)
override fun close() = redis.close()
}
Kotlin vs Java: важные нюансы
В Kotlin интерфейсы могут содержать свойства — но только вычисляемые (без backing field). Если в интерфейсе написать val name: String = "default" — это ошибка компиляции. Abstract class может иметь val name: String = "default" с backing field.
При конфликте default-методов из двух интерфейсов компилятор требует явного override в классе:
interface A { fun greet() = println("A") }
interface B { fun greet() = println("B") }
class C : A, B {
override fun greet() = super<A>.greet() // обязательно выбрать
}
Подводные камни
- Backing fields в интерфейсах недоступны —
val x: Intв интерфейсе не имеет поля хранения, каждый реализатор создаёт своё. - Конструктор abstract class вызывается при создании экземпляра наследника — не вызывайте переопределяемые методы в конструкторе, они вызовутся в контексте дочернего класса до инициализации его полей.
- Sealed interface vs sealed abstract class — sealed interface позволяет реализовывать другие интерфейсы, sealed abstract class — нет. Выбор влияет на exhaustive when.
- Java-совместимость — default-методы интерфейсов Kotlin компилируются в Java через DefaultImpls inner class; при interop с Java это может удивить.
- Visibility — interface не может иметь private constructor, поэтому нельзя ограничить реализацию только своим модулем без sealed.
- Делегирование — Kotlin поддерживает делегирование интерфейса через
by:class LoggingRepo(delegate: Repo) : Repo by delegate. Для abstract class такой механизм недоступен. - Тестируемость — abstract class труднее мокировать: Mockito/MockK создают subclass, что может конфликтовать с
finalметодами. Interface мокируется тривиально.
Common mistakes
- Объяснять «abstract class и interface» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Не стоит выбирать abstract class только ради общего helper-кода, если зависимость должна быть ортогональной роли.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «abstract class и interface» своими словами и связывает ее с кодом.
- Называет механизм: Класс может реализовать несколько interfaces, но наследоваться только от одного класса; выбор влияет на композицию и binary compatibility.
- Видит production-последствие: Не стоит выбирать abstract class только ради общего helper-кода, если зависимость должна быть ортогональной роли.