14 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Классы и конструкторы | Основной конструктор, параметры-свойства, вторичные конструкторы и фабрики |
|
Минимальный класс - одна строка:
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. Без
@JvmOverloadsJava-код обязан передать все аргументы: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что-то упало, ссылка на объект не вернётся - но уже выполненные побочные эффекты (запись в реестр, открытый файл) останутся. Поэтому в конструкторе делают только валидацию и присваивание.