Irbisa · Шпаргалка

Kotlin: ООП и функциональщина

Классы, data class, sealed class, object, extension-функции, scope-функции

Junior-разработчик · 25 вопросов

  1. 01

    Что такое data class, что для него генерирует компилятор и какие у него ограничения?

    data class предназначен для хранения значений: по свойствам primary-конструктора компилятор генерирует equals()/hashCode(), toString(), copy() и componentN() для деструктуризации.

    data class User(val id: Long, val name: String) {  var lastSeen: Long = 0          // body property: not part of equals/toString} val a = User(1, "Alice")val b = a.copy(name = "Alicia")   // new instance, id kept…
  2. 02

    Что такое sealed-классы и sealed-интерфейсы и как они делают when исчерпывающим?

    У sealed-класса или sealed-интерфейса закрытый набор прямых подтипов, известный на этапе компиляции, поэтому when по нему покрывает все случаи без else, а компилятор подсвечивает каждый такой when сразу, как только вы добавляете новый подтип.

    sealed interface LoginState {  data object Idle : LoginState  data object Loading : LoginState  data class Success(val userId: Long) : LoginState  data class Error(val message: String) : LoginState}…
  3. 03

    Что такое enum class, могут ли его константы хранить данные и когда лучше выбрать sealed-класс?

    enum class задаёт фиксированный набор именованных экземпляров одного типа; у констант могут быть свойства из конструктора, методы и даже собственные переопределения, но форма у всех одна и каждая существует ровно в одном экземпляре.

    enum class OrderStatus(val label: String) {  PENDING("Waiting"),  SHIPPED("On the way"),  UNKNOWN("Unknown");   companion object {…
  4. 04

    Чем абстрактный класс отличается от интерфейса в Kotlin и почему классы по умолчанию final?

    Абстрактный класс может хранить состояние и имеет конструктор, но унаследовать можно только один; интерфейс состояния не хранит, зато класс может реализовать сколько угодно интерфейсов, и в обоих могут быть реализованные методы.

    interface Logger {  val tag: String                                  // no backing field allowed  fun log(msg: String) = println("[$tag] $msg")    // default body} abstract class BaseRepo(protected val baseUrl: String) {   // state + constructor…
  5. 05

    Назовите четыре принципа ООП. Чем перегрузка отличается от переопределения?

    Четыре принципа: инкапсуляция, абстракция, наследование и полиморфизм; перегрузка (overloading) даёт одно имя с разными параметрами и выбирается при компиляции, а переопределение (overriding) заменяет метод в подклассе и выбирается во время выполнения по реальному типу.

  6. 06

    Какие модификаторы видимости есть в Kotlin и что на самом деле охватывает internal?

    В Kotlin есть public (по умолчанию), private, protected и internal; internal означает видимость везде внутри одного модуля, то есть набора файлов, которые компилируются вместе, например source set main одного Gradle-модуля.

  7. 07

    Что такое функции- и свойства-расширения, во что они компилируются и есть ли у них доступ к private-членам?

    Расширение добавляет функцию или свойство существующему типу, не меняя его исходник; оно компилируется в статический метод, который получает receiver первым параметром, поэтому видит только public- и internal-члены receiver, но не private.

    // StringExt.ktfun String.initials(): String =  split(" ").filter { it.isNotBlank() }.joinToString("") { it.first().uppercase() } val String.isEmail: Boolean          // no backing field, computed on each read  get() = contains('@')…
  8. 08

    Что такое infix-функция и каким правилам она должна соответствовать?

    infix-функция представляет собой член класса или расширение ровно с одним параметром, которую можно вызывать без точки и скобок: a to b вместо a.to(b).

    infix fun Int.percentOf(total: Int): Int = this * total / 100 val tax = 20 percentOf 150             // 30val pair = "id" to 42                  // Pair("id", 42)for (i in 0 until 10 step 2) print(i)  // 02468…
  9. 09

    Чем отличаются scope-функции let, run, with, apply и also?

    Все они выполняют блок над объектом и различаются двумя вещами: доступен ли объект внутри блока как this или как it и что возвращает вызов (сам объект или результат блока).

    val length: Int? = name?.let { it.length }          // null-safe transform val intent = Intent(context, DetailActivity::class.java).apply {  putExtra("id", id)                                 // this = Intent  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)}…
  10. 10

    Что такое контракт equals/hashCode и почему переопределять нужно оба метода сразу?

    Равные объекты обязаны возвращать одинаковый hashCode; если переопределить только equals, HashSet или HashMap обычно ищет не в той корзине и не находит объект, равный уже сохранённому.

    open class User(val id: Long, var displayName: String) {  override fun equals(other: Any?): Boolean {    if (this === other) return true    if (other == null || javaClass != other.javaClass) return false    return id == (other as User).id        // entity: id only, displayName is mutable  }…
  11. 11

    Чем вложенный (nested) класс отличается от inner-класса в Kotlin и как inner может привести к утечке Fragment?

    Вложенный класс не хранит ссылку на внешний экземпляр (это статический вложенный класс в терминах Java), а inner-класс хранит скрытую ссылку на него: может обращаться к членам внешнего класса, но и держит внешний объект в памяти.

    class Outer(private val name: String) {  class Nested {    fun describe() = "no access to name"            // no outer reference  }  inner class Inner {    fun describe() = "inner of ${this@Outer.name}"   // holds Outer…
  12. 12

    Что выведет этот код и что возвращает каждая из функций apply, also, let и run?

    val list = mutableListOf(1, 2) val a = list.apply { add(3) }val b = list.also { it.add(4) }.sizeval c = list.let { it.add(5) }val d = list.run { size } println(a)println("$b $c $d")

    Код выведет [1, 2, 3, 4, 5], а затем 4 true 5: apply и also возвращают receiver, а let и run возвращают последнее выражение блока.

  13. 13

    Чем отличаются class, object и companion object и потокобезопасен ли синглтон object?

    class служит шаблоном, по которому создаётся сколько угодно экземпляров, object задаёт синглтон, который объявляется и создаётся за один шаг, а companion object описывает синглтон, привязанный к классу, в котором живёт то, что в Java было бы static.

    object Analytics {                    // one instance, created on first access  private val events = mutableListOf<String>()  @Synchronized fun track(e: String) { events += e }   // state still needs guarding} class User private constructor(val name: String) {…
  14. 14

    В каком порядке выполняются companion object, инициализаторы свойств, блоки init и вторичный конструктор?

    Companion object инициализируется один раз, при первой загрузке класса; для каждого экземпляра сначала строится суперкласс, затем инициализаторы свойств и блоки init выполняются в порядке их появления в теле класса, а тело вторичного конструктора выполняется последним.

    class User(val name: String, age: Int) {  companion object { init { println("0 companion") } }   // once per class load   val greeting = "Hello, $name".also { println("1 initializer") }   init {…
  15. 15

    Что выведет этот код и что он говорит о copy() у data class?

    data class Cart(val owner: String, val items: MutableList<String>) val original = Cart("ann", mutableListOf("tea"))val copy = original.copy(owner = "bob")copy.items += "milk" println(original.items)println(original == copy)println(original.items === copy.items)

    Код выведет [tea, milk], false, true: copy() поверхностный, поэтому обе корзины ссылаются на один и тот же MutableList, и добавление в список копии меняет оригинал.

  16. 16

    Что выведет этот код и как диспетчеризуются функции-расширения по сравнению с членами класса?

    open class Shape { open fun name() = "Shape" }class Circle : Shape() { override fun name() = "Circle" } fun Shape.label() = "shape"fun Circle.label() = "circle"fun Circle.name() = "extension" fun main() {  val s: Shape = Circle()  println(s.name())  println(s.label())  println(Circle().name())}

    Код выведет Circle, shape, Circle: члены класса диспетчеризуются по типу во время выполнения, расширения выбираются по объявленному типу, а член класса всегда побеждает расширение с той же сигнатурой.

  17. 17

    Найдите баг: завершённые загрузки не уходят из active, а activeCount() только растёт.

    data class Download(val url: String, var progress: Int = 0) class DownloadTracker {    private val active = HashSet<Download>()     fun start(url: String): Download =        Download(url).also { active += it }     fun onProgress(d: Download, percent: Int) { d.progress = percent }    fun finish(d: Download) { active.remove(d) }    fun activeCount() = active.size}

    progress объявлен как var в конструкторе data class, поэтому он входит в hashCode; после того как onProgress его меняет, remove(d) ищет по другому хешу, ничего не находит, и каждая завершённая загрузка навсегда остаётся в active.

  18. 18

    Почему for (d in downloads) if (d.done) downloads.remove(d) бросает ConcurrentModificationException в одном потоке и как это исправить?

    Итератор списка запоминает счётчик изменений в начале цикла и проверяет его при каждом next(); удаление через сам список меняет этот счётчик, и следующий шаг цикла, как правило, бросает исключение.

    data class Download(val id: Long, val done: Boolean) val downloads = mutableListOf(  Download(1, done = true), Download(2, done = false), Download(3, done = true))…
  19. 19

    Что такое ссылка на функцию (::) и что захватывает связанная ссылка вроде viewModel::onSubmit?

    :: превращает существующую функцию, свойство или конструктор в функциональное значение; связанная ссылка (obj::method) захватывает объект в момент создания ссылки, а несвязанная (Type::method) принимает receiver первым параметром.

    class SubmitViewModel {  fun onSubmit(text: String) { /* ... */ }} val vm = SubmitViewModel()val bound: (String) -> Unit = vm::onSubmit                       // receiver fixed here…
  20. 20

    Как работает перегрузка операторов в Kotlin и как заставить +, [] и in работать с вашим типом?

    Каждый оператор соответствует функции с фиксированным именем, и достаточно пометить её operator: a + b вызывает a.plus(b), a[i] вызывает a.get(i), x in a вызывает a.contains(x).

    @JvmInlinevalue class Money(val cents: Long) : Comparable<Money> {  operator fun plus(other: Money) = Money(cents + other.cents)  operator fun times(factor: Int) = Money(cents * factor)  override fun compareTo(other: Money) = cents.compareTo(other.cents)}…
  21. 21

    Чем отличается цепочка на Sequence от цепочки на List и когда asSequence() вредит?

    Sequence прогоняет каждый элемент через всю цепочку лениво, только когда запускается терминальная операция вроде toList() или first(), а цепочка на List выполняет каждый шаг для всех элементов и создаёт промежуточный список на каждом шаге.

    val firstThree = generateSequence(1) { it + 1 }      // infinite source  .filter { println("filter $it"); it % 2 == 0 }  .map { println("map $it"); it * it }  .take(3)  .toList()                                      // terminal: starts the work// prints filter 1, filter 2, map 2, filter 3, filter 4, map 4, ... one element at a time…
  22. 22

    Что такое SAM-конверсия и что добавляет fun interface для интерфейсов Kotlin?

    SAM-конверсия позволяет передать лямбду туда, где ожидается интерфейс с единственным абстрактным методом; у Java-интерфейсов она работает автоматически, а Kotlin-интерфейсу нужен модификатор fun interface.

    fun interface Validator {  fun validate(s: String): Boolean} val notBlank = Validator { it.isNotBlank() }       // SAM constructorfun check(v: Validator, input: String) = v.validate(input)…
  23. 23

    Почему композицию часто предпочитают наследованию и как её поддерживает делегирование интерфейса через by в Kotlin?

    Наследование привязывает подкласс к реализации родителя, и изменение базового класса может сломать подклассы; композиция держит зависимость за интерфейсом, а class LoggingRepo(inner: Repo) : Repo by inner заставляет компилятор написать за вас все пробрасывающие методы.

    class User(val id: Long) interface Repo {  fun load(id: Long): User  fun refresh()}…
  24. 24

    Что такое принципы SOLID? Приведите пример каждого на Android и пример нарушения.

    SOLID объединяет пять правил для классов, которые остаются лёгкими для изменений: единственная ответственность, открытость/закрытость, подстановка Лисков, разделение интерфейсов и инверсия зависимостей.

  25. 25

    Является ли Singleton антипаттерном на Android и как держать один экземпляр без глобального object?

    Singleton сам по себе не антипаттерн, но глобальный object, к которому код обращается напрямую, создаёт скрытое глобальное состояние: вызывающий код его не объявляет, в тестах его не подменить, и живёт он весь процесс.