--- title: Классы и конструкторы description: Основной конструктор, параметры-свойства, вторичные конструкторы и фабрики tags: - kotlin - oop --- Минимальный класс - одна строка: ```kotlin class Empty fun main() { val e = Empty() // new не нужен println(e::class.simpleName) // Empty } ``` Оператора `new` в Kotlin нет: конструктор вызывается как функция. ## Основной конструктор Объявляется в заголовке класса: ```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`, но не становятся членами класса. ```kotlin class User(name: String) { val displayName = name.uppercase() // fun show() = name // ошибка: name недоступен в методах } fun main() { println(User("иван").displayName) } // ИВАН ``` ## Параметры-свойства Добавьте `val` или `var` - параметр станет свойством: ```kotlin 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 } ``` Это самая частая форма: одна строка вместо поля, конструктора, геттера и сеттера. Можно смешивать: часть параметров сделать свойствами, часть оставить аргументами: ```kotlin 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 } ``` ## Значения по умолчанию ```kotlin 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. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) Висячая запятая после последнего параметра разрешена - упрощает правки и diff. ## Модификаторы и аннотации Если у конструктора есть модификатор видимости или аннотация, слово `constructor` обязательно: ```kotlin 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`: ```kotlin 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(...)` - напрямую инициализировать свойства он не может. Класс без основного конструктора может иметь только вторичные: ```kotlin 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) } ``` Здесь делегировать некому, поэтому свойство инициализируется в каждом конструкторе. Форма редкая - обычно хватает основного конструктора со значениями по умолчанию. ### Когда нужны вторичные Почти никогда. Значения по умолчанию покрывают большинство случаев: ```kotlin // вместо трёх конструкторов 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 } ``` Вторичный конструктор оправдан, когда нужно **преобразовать** аргументы другого типа или выполнить работу перед делегированием: ```kotlin 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 } ``` Но и здесь фабричные функции в компаньоне обычно читаются лучше - у них есть имена: ```kotlin 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 } ``` Два конструктора с одинаковыми типами параметров объявить нельзя, а фабрик с разными именами - сколько угодно. ## Валидация в конструкторе ```kotlin 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` выполняется при создании объекта. Подробнее о порядке: [Инициализация](Инициализация.md) ## Создание объекта ```kotlin 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` - ссылка на конструктор, её можно передать как функцию. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `new User("Иван")` | `User("Иван")` | > | конструктор = метод с телом | основной конструктор в заголовке | > | поле + присваивание в конструкторе | `val` в параметрах конструктора | > | телескопические конструкторы | значения по умолчанию | > | Builder для многих параметров | именованные аргументы | > | `this(...)` для делегирования | то же | > | `private` конструктор + `static` фабрика | `private constructor` + `companion object` | > > **Главная экономия - параметры-свойства.** В Java класс из четырёх полей занимает 15+ строк: > ```java > 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 решает проблему конструктора с десятком опциональных параметров. Именованные аргументы и значения по умолчанию дают то же самое без дополнительного класса: > ```kotlin > 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](../../Программирование%20на%20Java/ООП/Конструкторы.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) > [!warning] Подводные камни > **Параметр без `val`/`var` недоступен в методах** - только в инициализаторах и `init`. Ошибка «unresolved reference» в методе обычно означает забытый `val`. > > **`val` в конструкторе делает свойство частью публичного API.** Если значение нужно только внутри, пишите `private val`: > ```kotlin > 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-код обязан передать все аргументы: > ```kotlin > class Server2 @JvmOverloads constructor( > val host: String = "localhost", > val port: Int = 8080, > ) > > fun main() { println(Server2().port) } // 8080 > ``` > > **Вторичный конструктор не может объявлять свойства:** > ```kotlin > 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` и инициализаторы свойств выполняются **до** тела вторичного конструктора: > ```kotlin > class Order2 { > val a = "свойство".also { println("1: $it") } > init { println("2: init") } > > constructor() { println("3: тело вторичного") } > } > > fun main() { Order2() } > ``` > Вывод: `1`, `2`, `3`. Подробнее: [Инициализация](Инициализация.md) > > **Два конструктора с одинаковой сигнатурой невозможны**, даже если смысл разный: > ```kotlin > 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` что-то упало, ссылка на объект не вернётся - но уже выполненные побочные эффекты (запись в реестр, открытый файл) останутся. Поэтому в конструкторе делают только валидацию и присваивание. Дополнительно: [Свойства](Свойства.md) Далее: [Свойства](Свойства.md)