🎯 Kotlin

POO: Classes e Objetos em Kotlin: um guia detalhado

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — архитектор, которому нужно построить 100 одинаковых домов. Что ты будешь делать: рисовать чертеж каждого дома отдельно или создашь один универсальный чертеж, а потом построишь по нему 100 зданий? Конечно, второй вариант удобнее! В программировании этот принцип называется Объектно-Ориентированное Программирование (ООП).

Давай разберем на жизненном примере. Представь, что ты печешь печенье. У тебя есть форма для печенья (например, в виде звездочки). Эта форма — это класс. А каждое конкретное печенье, которое ты вырезаешь этой формой — это объект (экземпляр класса).

Важно: Класс — это чертеж, описание. Объект — это конкретная реализация этого чертежа в памяти компьютера.

Рассмотрим другой пример — автомобиль. Класс «Автомобиль» описывает, что у любой машины есть колеса, двигатель, руль, и она может ехать и тормозить. А конкретные объекты — это «Мой красный Ford Focus 2020 года» и «Мамин белый Toyota Camry 2022 года». У них одинаковая структура (класс), но разные характеристики (цвет, год выпуска, владелец).

Зачем это нужно в программировании?

  • Упрощение сложности: Мы можем мыслить не байтами и переменными, а реальными сущностями (пользователь, заказ, товар).
  • Переиспользование кода: Написал класс один раз — используй его в сотне мест.
  • Модульность: Если нужно изменить поведение всех машин, меняешь только класс, а не каждый объект отдельно.
  • Безопасность: Можно спрятать внутренние детали (как устроен двигатель) и дать доступ только к важным функциям (кнопка «завестись»).

Теория: как это работает изнутри

Чтобы понять ООП, нужно разобраться с четырьмя китами, на которых оно держится. В Kotlin они реализованы очень элегантно.

1. Инкапсуляция (упаковка)

Представь, что у тебя есть робот-пылесос. Ты не лезешь внутрь него отверткой, чтобы покрутить шестеренки. Ты просто нажимаешь кнопку «Старт». Внутри робота спрятаны (инкапсулированы) все сложные детали. В программировании это означает, что мы объединяем данные (свойства) и методы (функции) внутри класса и скрываем внутреннюю реализацию от внешнего мира.

2. Наследование (иерархия)

Есть базовый класс «Животное». У него есть свойства: возраст, вес. Методы: есть, спать. Теперь мы создаем класс «Собака», который наследует все от «Животного», но добавляет свой метод «гавкать». И класс «Кошка» — добавляет метод «мурлыкать». Это позволяет не писать общий код (есть, спать) для каждого животного отдельно.

3. Полиморфизм (много форм)

Продолжим с животными. У нас есть метод «подать голос()». Для собаки он выведет «Гав!», для кошки — «Мяу!», для коровы — «Му!». Один и тот же метод работает по-разному в зависимости от того, какой объект его вызывает. Это полиморфизм.

4. Абстракция (выделение главного)

Когда ты садишься в машину, тебя не волнует, как именно поршни двигателя преобразуют энергию. Тебе важно: руль, педали, приборная панель. Абстракция — это выделение существенных характеристик объекта и игнорирование несущественных. В коде это реализуется через абстрактные классы и интерфейсы.

Синтаксис в Kotlin: все формы и варианты

Kotlin — современный язык, который сделал ООП очень лаконичным. Давай разберем все варианты создания классов.

Базовый синтаксис класса

// Объявляем класс "Человек"
class Person {
    // Свойства (поля) класса
    var name: String = "Имя не указано"
    var age: Int = 0
    
    // Метод (функция) класса
    fun introduce() {
        println("Привет! Меня зовут $name, мне $age лет.")
    }
}

fun main() {
    // Создаем объект (экземпляр) класса
    val person1 = Person()
    person1.name = "Анна"  // Устанавливаем свойство
    person1.age = 25
    person1.introduce()    // Вызываем метод
}

Конструкторы — как создавать объекты с параметрами

В Kotlin есть два типа конструкторов: первичный (в заголовке класса) и вторичные (внутри класса).

// Первичный конструктор — самый популярный способ
class Car(val brand: String, val model: String, var year: Int) {
    // val — свойство только для чтения (getter)
    // var — свойство для чтения и записи (getter + setter)
    
    fun info() {
        println("Автомобиль: $brand $model, $year года выпуска")
    }
}

fun main() {
    val myCar = Car("Toyota", "Camry", 2022)
    myCar.info()
    // myCar.brand = "Honda" — Ошибка! brand объявлен как val (только чтение)
    myCar.year = 2023  // Работает, так как year — var
}

Вторичный конструктор

class Student {
    var name: String
    var id: Int
    
    // Вторичный конструктор
    constructor(name: String, id: Int) {
        this.name = name
        this.id = id
    }
    
    // Еще один вторичный конструктор (перегрузка)
    constructor(name: String) {
        this.name = name
        this.id = 0  // Значение по умолчанию
    }
    
    fun display() {
        println("Студент: $name, ID: $id")
    }
}

Свойства с геттерами и сеттерами

В Kotlin можно переопределить логику получения и установки свойств.

class Temperature {
    var celsius: Double = 0.0
    
    // Кастомный геттер — вычисляем значение на лету
    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32
    
    // Кастомный сеттер — проверяем входящее значение
    var kelvin: Double
        get() = celsius + 273.15
        set(value) {
            if (value < 0) {
                println("Ошибка: температура не может быть ниже абсолютного нуля!")
            } else {
                celsius = value - 273.15
            }
        }
}

fun main() {
    val temp = Temperature()
    temp.celsius = 25.0
    println("По Цельсию: ${temp.celsius}")  // 25.0
    println("По Фаренгейту: ${temp.fahrenheit}")  // 77.0
    temp.kelvin = 300.0
    println("После установки Кельвина: ${temp.celsius}")  // 26.85
}

Наследование классов

В Kotlin классы по умолчанию закрыты для наследования (final). Нужно явно указать open.

// Базовый класс (родительский)
open class Animal(val name: String) {
    // Метод тоже должен быть open, чтобы его можно было переопределить
    open fun sound() {
        println("$name издает звук")
    }
}

// Дочерний класс
class Dog(name: String, val breed: String) : Animal(name) {
    // Переопределяем метод родителя
    override fun sound() {
        println("$name (порода $breed) говорит: Гав-гав!")
    }
}

class Cat(name: String) : Animal(name) {
    override fun sound() {
        println("$name говорит: Мяу-мяу!")
    }
}

fun main() {
    val dog = Dog("Бобик", "Овчарка")
    val cat = Cat("Мурка")
    dog.sound()  // Бобик (порода Овчарка) говорит: Гав-гав!
    cat.sound()  // Мурка говорит: Мяу-мяу!
}

Абстрактные классы и интерфейсы

Абстрактный класс нельзя создать напрямую — только наследовать. Интерфейс описывает, что класс должен уметь делать, но не содержит реализации.

// Абстрактный класс
abstract class Shape {
    abstract fun area(): Double  // Абстрактный метод — без тела
    fun description() = "Это геометрическая фигура"  // Обычный метод
}

// Интерфейс
interface Drawable {
    fun draw()  // Абстрактный метод
    fun erase() {  // Метод с реализацией по умолчанию
        println("Стираем рисунок")
    }
}

// Наследуем и реализуем
class Circle(val radius: Double) : Shape(), Drawable {
    override fun area(): Double = Math.PI * radius * radius
    
    override fun draw() {
        println("Рисуем круг радиусом $radius")
    }
}

fun main() {
    val circle = Circle(5.0)
    println(circle.area())  // 78.53981633974483
    circle.draw()           // Рисуем круг радиусом 5.0
    circle.erase()          // Стираем рисунок
}

Data классы — для хранения данных

Специальный тип классов, который автоматически генерирует toString(), equals(), hashCode() и copy().

data class User(val login: String, val email: String, var age: Int)

fun main() {
    val user1 = User("ivan123", "ivan@mail.ru", 25)
    val user2 = User("ivan123", "ivan@mail.ru", 25)
    
    println(user1)  // User(login=ivan123, email=ivan@mail.ru, age=25)
    println(user1 == user2)  // true — сравниваются все поля
    
    // Копирование с изменением
    val user3 = user1.copy(age = 30)
    println(user3)  // User(login=ivan123, email=ivan@mail.ru, age=30)
}

Object — объект-одиночка (Singleton)

Когда нужен только один экземпляр класса на всю программу.

object DatabaseConfig {
    val url = "jdbc:mysql://localhost:3306/mydb"
    val user = "admin"
    val password = "secret"
    
    fun connect() {
        println("Подключаемся к $url")
    }
}

fun main() {
    // Не нужно создавать экземпляр — обращаемся напрямую
    println(DatabaseConfig.url)
    DatabaseConfig.connect()
}

Примеры кода — от простого к сложному

Давай закрепим теорию на практике. Мы создадим небольшую систему для управления библиотекой.

Пример 1: Простейший класс «Книга»

// Самый простой класс с конструктором
class Book(val title: String, val author: String) {
    fun read() {
        println("Читаем книгу '$title' автора $author")
    }
}

fun main() {
    val book = Book("Война и мир", "Лев Толстой")
    book.read()
}

Пример 2: Класс с проверкой данных

class BankAccount(val accountNumber: String) {
    var balance: Double = 0.0
        private set  // Сеттер теперь приватный — изменить баланс можно только внутри класса
    
    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
            println("Внесено $amount. Баланс: $balance")
        } else {
            println("Сумма должна быть положительной!")
        }
    }
    
    fun withdraw(amount: Double) {
        if (amount > 0 && amount <= balance) {
            balance -= amount
            println("Снято $amount. Баланс: $balance")
        } else {
            println("Недостаточно средств или неверная сумма!")
        }
    }
}

fun main() {
    val account = BankAccount("40817810099910004321")
    account.deposit(1000.0)
    account.withdraw(500.0)
    // account.balance = 999999.0 — Ошибка! Свойство balance имеет private set
}

Пример 3: Наследование — разные типы книг

open class LibraryItem(val id: Int, val title: String) {
    open fun getInfo(): String = "ID: $id, Название: $title"
}

class Book2(id: Int, title: String, val author: String) : LibraryItem(id, title) {
    override fun getInfo(): String = super.getInfo() + ", Автор: $author"
}

class DVD(id: Int, title: String, val duration: Int) : LibraryItem(id, title) {
    override fun getInfo(): String = super.getInfo() + ", Длительность: $duration мин."
}

fun main() {
    val items = listOf(
        Book2(1, "1984", "Джордж Оруэлл"),
        DVD(2, "Матрица", 136)
    )
    for (item in items) {
        println(item.getInfo())
    }
}

Пример 4: Полиморфизм — единый интерфейс

interface PaymentMethod {
    fun pay(amount: Double): Boolean
}

class CreditCard(val number: String) : PaymentMethod {
    override fun pay(amount: Double): Boolean {
        println("Оплата $amount с карты $number")
        return true
    }
}

class PayPal(val email: String) : PaymentMethod {
    override fun pay(amount: Double): Boolean {
        println("Оплата $amount через PayPal ($email)")
        return true
    }
}

class Cash : PaymentMethod {
    override fun pay(amount: Double): Boolean {
        println("Оплата наличными $amount")
        return true
    }
}

fun processPayment(method: PaymentMethod, amount: Double) {
    if (method.pay(amount)) {
        println("Платеж успешно обработан!")
    }
}

fun main() {
    processPayment(CreditCard("1234-5678-9012-3456"), 1500.0)
    processPayment(PayPal("user@example.com"), 2000.0)
    processPayment(Cash(), 500.0)
}

Пример 5: Композиция — класс внутри класса

class Engine(val type: String, val horsepower: Int) {
    fun start() = println("Двигатель $type запущен")
    fun stop() = println("Двигатель $type заглушен")
}

class Car2(val brand: String, val model: String) {
    // Композиция: автомобиль содержит двигатель
    private val engine = Engine("Бензиновый", 150)
    
    fun start() {
        println("Заводим $brand $model")
        engine.start()
    }
    
    fun stop() {
        engine.stop()
        println("$brand $model остановлен")
    }
}

fun main() {
    val car = Car2("Honda", "Civic")
    car.start()
    car.stop()
}

Пример 6: Data class с логикой

data class Order(val id: Int, val items: MutableList<String>, var total: Double) {
    fun addItem(item: String, price: Double) {
        items.add(item)
        total += price
        println("Добавлен товар '$item' на сумму $price")
    }
    
    fun removeItem(item: String): Boolean {
        return if (items.remove(item)) {
            println("Товар '$item' удален")
            true
        } else {
            println("Товар '$item' не найден")
            false
        }
    }
    
    fun printOrder() {
        println("Заказ #$id:")
        items.forEach { println("  - $it") }
        println("Итого: $total")
    }
}

fun main() {
    val order = Order(1, mutableListOf("Книга", "Ручка"), 0.0)
    order.addItem("Тетрадь", 50.0)
    order.addItem("Карандаш", 30.0)
    order.printOrder()
}

Пример 7: Фабричный метод — создание объектов с логикой

class User2 private constructor(val name: String, val role: String) {
    // Компаньон-объект — аналог статических методов в Java
    companion object {
        fun createAdmin(name: String): User2 {
            return User2(name, "ADMIN")
        }
        
        fun createUser(name: String): User2 {
            return User2(name, "USER")
        }
        
        fun createGuest(): User2 {
            return User2("Гость", "GUEST")
        }
    }
    
    fun getPermissions(): List<String> {
        return when (role) {
            "ADMIN" -> listOf("READ", "WRITE", "DELETE", "MANAGE_USERS")
            "USER" -> listOf("READ", "WRITE")
            "GUEST" -> listOf("READ")
            else -> emptyList()
        }
    }
}

fun main() {
    val admin = User2.createAdmin("Иван")
    val user = User2.createUser("Мария")
    val guest = User2.createGuest()
    
    println("${admin.name} (${admin.role}): ${admin.getPermissions()}")
    println("${user.name} (${user.role}): ${user.getPermissions()}")
    println("${guest.name} (${guest.role}): ${guest.getPermissions()}")
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Kotlin Python
Объявление класса class Person(val name: String) class Person:
Конструктор Первичный (в заголовке) и вторичные __init__(self, name)
Типизация Строгая статическая (типы указываются явно) Динамическая (типы не указываются)
Модификаторы доступа public (по умолчанию), private, protected, internal Нет ключевых слов, соглашение: _private (одно подчеркивание)
Наследование Только одиночное (один родитель) Множественное (несколько родителей)
Data классы data class — встроенная поддержка @dataclass из модуля dataclasses
Свойства Встроенные геттеры/сеттеры @property декоратор
Объект-одиночка object ключевое слово Модуль или class Singleton:
Переопределение методов override fun Просто переопределяется (нет ключевого слова)
Производительность Высокая (компилируется в байт-код JVM) Ниже (интерпретируемый язык)
Совет: Если ты знаешь Python, то в Kotlin тебе покажется непривычной строгая типизация и необходимость явно указывать open для наследования. Но зато Kotlin поймает много ошибок еще на этапе компиляции, а не во время выполнения программы.

Типичные ошибки и подводные камни

1. Забыл про open для наследования

class Parent {
    fun sayHello() = println("Привет")
}

// Ошибка! Класс Parent не open
// class Child : Parent() { }

// Правильно:
open class Parent2 {
    open fun sayHello() = println("Привет")
}

class Child : Parent2() {
    override fun sayHello() = println("Привет от ребенка")
}

2. Путаница между val и var

class Person(val name: String) {
    // name — это свойство только для чтения
    // Нельзя написать: fun changeName() { name = "Новое имя" }
}

class Person2(var name: String) {
    // name — можно менять
    fun changeName() { name = "Новое имя" }  // Работает
}

3. Неправильное использование this

class Person3(val name: String) {
    fun printName(name: String) {
        // Параметр name затеняет свойство класса
        println(name)  // Выведет параметр, а не свойство
        println(this.name)  // Выведет свойство класса
    }
}

4. Забыл вызвать конструктор родителя

open class Animal(val name: String)

// Ошибка! Нужно передать параметр в конструктор родителя
// class Dog(val breed: String) : Animal() { }

// Правильно:
class Dog(name: String, val breed: String) : Animal(name)

5. Изменение data class после создания

data class Person4(val name: String, var age: Int)

fun main() {
    val person = Person4("Иван", 25)
    // person.name = "Петр" — Ошибка! name объявлен как val
    person.age = 26  // Работает, так как age — var
    
    // Лучше использовать copy() для неизменяемости
    val updatedPerson = person.copy(name = "Петр")
}

6. Путаница с модификаторами доступа

class MyClass {
    private val secret = "Секрет"
    protected val familySecret = "Семейный секрет"
    internal val appSecret = "Секрет приложения"
    
    fun showSecrets() {
        println(secret)  // Доступно внутри класса
        println(familySecret)  // Доступно внутри класса
        println(appSecret)  // Доступно внутри класса
    }
}

class ChildClass : MyClass() {
    fun showFamilySecret() {
        // println(secret) — Ошибка! private не виден в наследнике
        println(familySecret)  // Доступно! protected виден в наследниках
        println(appSecret)  // Доступно! internal виден в том же модуле
    }
}

Лучшие практики и советы профессионалов

  1. Принцип единственной ответственности: Один класс — одна задача. Не делай класс «Бог», который умеет всё. Разделяй логику.
  2. Предпочитай val вместо var: Неизменяемые объекты (immutable) легче отлаживать и они потокобезопасны. Используй copy() для data классов.
  3. Используй data классы для моделей данных: Они автоматически генерируют toString(), equals(), hashCode() — это экономит кучу времени.
  4. Делай конструкторы лаконичными: Используй значения по умолчанию вместо перегрузки конструкторов.
  5. Инкапсулируй внутреннее состояние: Делай свойства private set или полностью private, если внешний код не должен их менять.
  6. Используй интерфейсы для абстракции: Код должен зависеть от абстракций, а не от конкретных реализаций. Это упрощает тестирование и замену компонентов.
  7. Не бойся компаньон-объектов: Они заменяют статические методы из Java и отлично подходят для фабрик.
  8. Избегай глубокой иерархии наследования: Более 3 уровней — уже плохо. Предпочитай композицию наследованию (принцип «предпочитай композицию наследованию»).
  9. Используй sealed class для ограниченной иерархии: Когда ты знаешь все возможные подклассы (например, состояния загрузки: Loading, Success, Error).
  10. Пиши тесты для классов: Хорошо спроектированный класс легко тестировать. Если тестировать сложно — значит, класс делает слишком много.

Вопросы для самопроверки

  1. В чем разница между классом и объектом?
    Ответ: Класс — это шаблон (чертеж), а объект — конкретный экземпляр этого шаблона, созданный в памяти.
  2. Что такое инкапсуляция и зачем она нужна?
    Ответ: Это сокрытие внутреннего состояния объекта и предоставление доступа только через методы. Нужна для защиты данных и упрощения использования класса.
  3. Как объявить класс, который можно наследовать?
    Ответ: Нужно добавить ключевое слово open перед class: open class MyClass.
  4. Что такое data class и какие методы он генерирует автоматически?
    Ответ: Это класс для хранения данных. Генерирует: toString(), equals(), hashCode(), copy(), и методы компонентов.
  5. В чем разница между абстрактным классом и интерфейсом?
    Ответ: Абстрактный класс может иметь состояние (свойства с значениями) и конструктор. Интерфейс не может хранить состояние (только абстрактные свойства) и не имеет конструктора. Класс может реализовать несколько интерфейсов, но наследовать только один класс.
  6. Что такое компаньон-объект (companion object)?
    Ответ: Это объект, связанный с классом, который позволяет вызывать его методы без создания экземпляра класса (аналог static в Java).
  7. Как сделать свойство только для чтения снаружи, но изменяемым внутри класса?
    Ответ: Использовать var с private set: var balance: Double = 0.0; private set.
  8. Что такое полиморфизм? Приведи пример.
    Ответ: Способность объектов разных классов отвечать на одни и те же методы по-разному. Пример: метод sound() у классов Dog и Cat выводит разные звуки.
  9. Как создать объект-одиночку (singleton) в Kotlin?
    Ответ: Использовать ключевое слово object: object MySingleton { ... }.
  10. Что произойдет, если не указать override при переопределении метода?
    Ответ: Компилятор выдаст ошибку. В Kotlin нужно явно указывать override для всех переопределяемых методов.

Итог и что изучить дальше

Мы прошли большой путь! Ты узнал, что такое классы и объекты в Kotlin, разобрался с четырьмя принципами ООП, научился создавать классы с конструкторами, свойствами и методами, освоил наследование, интерфейсы и data классы. Теперь ты можешь проектировать программы, используя объектно-ориентированный подход.

Что изучать дальше?

  • Sealed классы и Sealed интерфейсы — для создания ограниченных иерархий типов.
  • Делегирование — паттерн, который позволяет переиспользовать реализацию без наследования.
  • Дженерики (Generics) — создание классов, работающих с разными типами данных.
  • Функции-расширения — добавление новых методов к существующим классам без наследования.
  • Корректность (Null Safety) — безопасная работа с nullable типами.
  • Паттерны проектирования — Singleton, Factory, Builder, Observer и другие.
  • Kotlin Coroutines — асинхронное программирование.
Запомни: ООП — это не просто синтаксис, это способ мышления. Старайся проектировать классы так, чтобы они были маленькими, понятными и делали только одну вещь. И помни: хороший код