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

327 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)