327 lines
14 KiB
Markdown
327 lines
14 KiB
Markdown
---
|
||
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)
|