Files
obsidian/Kotlin/ООП/Классы и конструкторы.md
T

14 KiB
Raw Blame History

title, description, tags
title description tags
Классы и конструкторы Основной конструктор, параметры-свойства, вторичные конструкторы и фабрики
kotlin
oop

Минимальный класс - одна строка:

class Empty

fun main() {
    val e = Empty()        // new не нужен
    println(e::class.simpleName)    // Empty
}

Оператора new в Kotlin нет: конструктор вызывается как функция.

Основной конструктор

Объявляется в заголовке класса:

class User(name: String, age: Int) {
    val displayName = name.uppercase()     // параметры доступны в теле
    val isAdult = age >= 18
}

fun main() {
    val u = User("иван", 20)
    println("${u.displayName} ${u.isAdult}")    // ИВАН true
}

Параметры name и age - только параметры: они видны в инициализаторах свойств и в блоках init, но не становятся членами класса.

class User(name: String) {
    val displayName = name.uppercase()
    // fun show() = name     // ошибка: name недоступен в методах
}

fun main() { println(User("иван").displayName) }   // ИВАН

Параметры-свойства

Добавьте val или var - параметр станет свойством:

class User(val name: String, var age: Int) {
    fun show() = "$name, $age"       // теперь доступны везде
}

fun main() {
    val u = User("Иван", 20)
    u.age = 21
    println(u.show())      // Иван, 21
}

Это самая частая форма: одна строка вместо поля, конструктора, геттера и сеттера.

Можно смешивать: часть параметров сделать свойствами, часть оставить аргументами:

class User(val name: String, password: String) {
    private val hash = password.hashCode()    // пароль не хранится как есть

    fun check(input: String) = input.hashCode() == hash
}

fun main() {
    val u = User("Иван", "secret")
    println(u.check("secret"))      // true
    println(u.check("wrong"))       // false
}

Значения по умолчанию

class Server(
    val host: String = "localhost",
    val port: Int = 8080,
    val secure: Boolean = false,
)

fun main() {
    println(Server().port)                        // 8080
    println(Server("example.com", 443, true).host) // example.com
    println(Server(port = 9090).port)              // 9090 — именованный аргумент
}

Это заменяет телескопические конструкторы и во многих случаях - паттерн Builder. Подробнее: Функции

Висячая запятая после последнего параметра разрешена - упрощает правки и diff.

Модификаторы и аннотации

Если у конструктора есть модификатор видимости или аннотация, слово constructor обязательно:

class Service private constructor(val id: Int) {
    companion object {
        fun create(id: Int) = if (id > 0) Service(id) else null
    }
}

fun main() {
    println(Service.create(1)?.id)      // 1
    println(Service.create(-1))         // null
    // Service(1)                       // ошибка: конструктор приватный
}

Приватный конструктор + фабрика в компаньоне - способ контролировать создание: валидация, кеширование, возврат null вместо исключения.

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

Объявляются в теле класса словом constructor:

class Point(val x: Int, val y: Int) {
    constructor(value: Int) : this(value, value)        // точка на диагонали
    constructor() : this(0, 0)

    override fun toString() = "($x, $y)"
}

fun main() {
    println(Point(1, 2))     // (1, 2)
    println(Point(5))        // (5, 5)
    println(Point())         // (0, 0)
}

Вторичный конструктор обязан делегировать основному через this(...) - напрямую инициализировать свойства он не может.

Класс без основного конструктора может иметь только вторичные:

class Config4 {
    val source: String

    constructor(file: java.io.File) {
        source = "файл ${file.name}"
    }

    constructor(url: String, timeout: Int) {
        source = "$url (таймаут $timeout)"
    }
}

fun main() {
    println(Config4("http://x.ru", 30).source)   // http://x.ru (таймаут 30)
}

Здесь делегировать некому, поэтому свойство инициализируется в каждом конструкторе. Форма редкая - обычно хватает основного конструктора со значениями по умолчанию.

Когда нужны вторичные

Почти никогда. Значения по умолчанию покрывают большинство случаев:

// вместо трёх конструкторов
class Point(val x: Int = 0, val y: Int = x)

fun main() {
    println("${Point().x} ${Point(5).y} ${Point(1, 2).y}")   // 0 5 2
}

Вторичный конструктор оправдан, когда нужно преобразовать аргументы другого типа или выполнить работу перед делегированием:

class Duration2(val millis: Long) {
    constructor(seconds: Int) : this(seconds * 1000L)
    constructor(text: String) : this(text.removeSuffix("s").toLong() * 1000)
}

fun main() {
    println(Duration2(2).millis)        // 2000
    println(Duration2("3s").millis)     // 3000
}

Но и здесь фабричные функции в компаньоне обычно читаются лучше - у них есть имена:

class Duration3 private constructor(val millis: Long) {
    companion object {
        fun ofSeconds(s: Int) = Duration3(s * 1000L)
        fun parse(text: String) = Duration3(text.removeSuffix("s").toLong() * 1000)
    }
}

fun main() {
    println(Duration3.ofSeconds(2).millis)    // 2000
    println(Duration3.parse("3s").millis)     // 3000
}

Два конструктора с одинаковыми типами параметров объявить нельзя, а фабрик с разными именами - сколько угодно.

Валидация в конструкторе

class Person(val name: String, val age: Int) {
    init {
        require(name.isNotBlank()) { "имя не может быть пустым" }
        require(age in 0..150) { "недопустимый возраст: $age" }
    }
}

fun main() {
    println(Person("Иван", 20).name)      // Иван
    try {
        Person("", 20)
    } catch (e: IllegalArgumentException) {
        println(e.message)                 // имя не может быть пустым
    }
}

Блок init выполняется при создании объекта. Подробнее о порядке: Инициализация

Создание объекта

class User(val name: String)

fun main() {
    val u = User("Иван")              // просто вызов
    val list = List(3) { User("u$it") }  // в лямбде
    val byRef = ::User                 // ссылка на конструктор
    println("${u.name} ${list.size} ${byRef("Пётр").name}")   // Иван 3 Пётр
}

::User - ссылка на конструктор, её можно передать как функцию. Подробнее: Лямбды и функциональные типы

[!info] Отличия от Java

Java Kotlin
new User("Иван") User("Иван")
конструктор = метод с телом основной конструктор в заголовке
поле + присваивание в конструкторе val в параметрах конструктора
телескопические конструкторы значения по умолчанию
Builder для многих параметров именованные аргументы
this(...) для делегирования то же
private конструктор + static фабрика private constructor + companion object

Главная экономия - параметры-свойства. В Java класс из четырёх полей занимает 15+ строк:

public class User {
    private final String name;
    private final int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }
    public int getAge() { return age; }
}

В Kotlin это class User(val name: String, val age: Int).

Builder обычно не нужен. В Java паттерн Builder решает проблему конструктора с десятком опциональных параметров. Именованные аргументы и значения по умолчанию дают то же самое без дополнительного класса:

class Request(
    val url: String,
    val method: String = "GET",
    val timeout: Int = 30,
    val retries: Int = 0,
)

fun main() {
    println(Request("http://x.ru", timeout = 60).timeout)   // 60
}

Builder остаётся полезен, когда объект собирается по частям в разных местах кода.

Сравнить: Конструкторы в Java, Builder

[!warning] Подводные камни Параметр без val/var недоступен в методах - только в инициализаторах и init. Ошибка «unresolved reference» в методе обычно означает забытый val.

val в конструкторе делает свойство частью публичного API. Если значение нужно только внутри, пишите private val:

class Service4(private val apiKey: String) {
    fun call() = "запрос с ключом длины ${apiKey.length}"
}

fun main() {
    val s = Service4("secret")
    println(s.call())        // запрос с ключом длины 6
    // println(s.apiKey)     // ошибка: приватное свойство
}

Значения по умолчанию не видны из Java. Без @JvmOverloads Java-код обязан передать все аргументы:

class Server2 @JvmOverloads constructor(
    val host: String = "localhost",
    val port: Int = 8080,
)

fun main() { println(Server2().port) }   // 8080

Вторичный конструктор не может объявлять свойства:

class Point2(val x: Int) {
    // constructor(val y: Int) : this(y)    // ошибка: val запрещён здесь
    constructor(y: Long) : this(y.toInt())
}

fun main() { println(Point2(5L).x) }   // 5

Порядок инициализации не совпадает с порядком конструкторов. Блоки init и инициализаторы свойств выполняются до тела вторичного конструктора:

class Order2 {
    val a = "свойство".also { println("1: $it") }
    init { println("2: init") }

    constructor() { println("3: тело вторичного") }
}

fun main() { Order2() }

Вывод: 1, 2, 3. Подробнее: Инициализация

Два конструктора с одинаковой сигнатурой невозможны, даже если смысл разный:

class Range3 private constructor(val from: Int, val to: Int) {
    companion object {
        fun ofBounds(from: Int, to: Int) = Range3(from, to)
        fun ofStart(start: Int, length: Int) = Range3(start, start + length)
    }
}

fun main() { println(Range3.ofStart(1, 5).to) }   // 6

Фабричные функции решают это именами.

Исключение в конструкторе оставляет объект недосозданным. Если в init что-то упало, ссылка на объект не вернётся - но уже выполненные побочные эффекты (запись в реестр, открытый файл) останутся. Поэтому в конструкторе делают только валидацию и присваивание.

Дополнительно: Свойства Далее: Свойства