Skip to content

Instantly share code, notes, and snippets.

@PeTcHeNkA
Last active August 30, 2023 21:14
Show Gist options
  • Save PeTcHeNkA/3db4e221463380d42d0af6dcbd5901e8 to your computer and use it in GitHub Desktop.
Save PeTcHeNkA/3db4e221463380d42d0af6dcbd5901e8 to your computer and use it in GitHub Desktop.

Переход с Java на Kotlin: Полное руководство

Kotlin - это статически типизированный язык программирования, который полностью совместим с Java. Он предлагает множество улучшений и новых функций, которые делают разработку более удобной и продуктивной. В этой статье мы рассмотрим некоторые из ключевых различий между Java и Kotlin, а также примеры использования этих функций.

0. Основные типы данных и переменные в Kotlin

Типы данных

Kotlin имеет множество встроенных типов данных для представления чисел, символов, логических значений и строк. Например, есть типы:

  • Int - для целых чисел.
val a: Int = 1 // положительное целое число
val b: Int = -1 // отрицательное целое число
  • Double и Float - для чисел с плавающей точкой.
val c: Double = 2.0 // положительное число с плавающей точкой
val d: Double = -2.0 // отрицательное число с плавающей точкой
  • Long - для длинных целых чисел.
val e: Long = 1234567890123456789L // положительное длинное целое число
val f: Long = -1234567890123456789L // отрицательное длинное целое число
  • Short и Byte - для коротких целых чисел и байтов.
val g: Short = 32767 // положительное короткое целое число
val h: Short = -32768 // отрицательное короткое целое число
val i: Byte = 127 // положительный байт
val j: Byte = -128 // отрицательный байт
  • Char- для символов.
val k: Char = 'A' // символ
  • Boolean- для логических значений.
val l: Boolean = true // логическое значение true (истина)
val m: Boolean = false // логическое значение false (ложь)
  • String - для строк.
val n: String = "Hello" // строка

Кроме того, Kotlin предоставляет типы для беззнаковых целых чисел:

  • UByte - для беззнаковых байтов.
val o: UByte = 255u // беззнаковый байт (беззнаковые типы не могут быть отрицательными)
  • UShort - для беззнаковых коротких целых чисел.
val p: UShort = 65535u // беззнаковый короткий целый (беззнаковые типы не могут быть отрицательными)
  • UInt- для беззнаковых целых чисел.
val q: UInt = 4294967295u // беззнаковый целый (беззнаковые типы не могут быть отрицательными)
  • ULong - для беззнаковых длинных целых чисел.
val r: ULong = 18446744073709551615u // беззнаковый длинный целый (беззнаковые типы не могут быть отрицательными)

Переменные

В Kotlin есть два ключевых слова для объявления переменных: val и var. Ключевое слово val используется для объявления неизменяемых переменных, а ключевое слово var - для изменяемых переменных.

Например:

val x: Int = 1 // неизменяемая переменная типа Int со значением 1
var y: Double = 2.0 // изменяемая переменная типа Double со значением 2.0

// x = 2 // Ошибка! Нельзя переопределить значение переменной типа val

y = 3.0 // Можно изменить значение переменной типа var
println(y) // Выводит 3.0 на экран

Важно отметить, что ключевое слово val аналогично ключевому слову final в Java. Это означает, что значение переменной, объявленной с помощью ключевого слова val, не может быть изменено после ее объявления.

Однако это не означает, что объект, на который ссылается переменная типа val, не может быть изменен. Например:

val list = mutableListOf(1, 2, 3) // неизменяемая переменная типа MutableList<Int>
list.add(4) // добавляем элемент в список
println(list) // выводим список на экран: [1, 2, 3, 4]

В этом примере мы объявляем неизменяемую переменную list типа MutableList<Int> и присваиваем ей значение - список из трех элементов. Затем мы добавляем еще один элемент в список с помощью метода add. Как видите, хотя переменная list объявлена как неизменяемая с помощью ключевого слова val, мы все равно можем изменять объект, на который она ссылается.

1. Модификаторы доступа (Access Modifiers)

В Kotlin есть несколько модификаторов доступа, которые определяют, какие члены класса могут быть доступны извне. Они включают в себя private, protected, internal и public. По умолчанию все члены класса являются public, если не указано иное.

  • private: Члены класса, объявленные как private, доступны только внутри того же класса.
  • protected: Члены класса, объявленные как protected, доступны внутри того же класса и его подклассов.
  • internal: Члены класса, объявленные как internal, доступны внутри того же модуля. Модуль - это набор файлов, которые компилируются вместе.
  • public: Члены класса, объявленные как public, доступны из любого места.

Например:

open class Shape {
    private val a = 1
    protected val b = 2
    internal val c = 3
    val d = 4 // public по умолчанию
}

class Circle : Shape() {
    fun printProperties() {
        // println(a) // Ошибка: a имеет модификатор private в Shape
        println(b) // OK: b имеет модификатор protected в Shape
        println(c) // OK: c имеет модификатор internal в Shape
        println(d) // OK: d имеет модификатор public в Shape
    }
}

fun main() {
    val shape = Shape()
    // println(shape.a) // Ошибка: a имеет модификатор private в Shape
    // println(shape.b) // Ошибка: b имеет модификатор protected в Shape
    println(shape.c) // OK: c имеет модификатор internal в Shape
    println(shape.d) // OK: d имеет модификатор public в Shape

    val circle = Circle()
    circle.printProperties()
}

В этом примере мы определяем класс Shape, который является open и имеет четыре свойства с разными модификаторами доступа. Затем мы наследуем от этого класса класс Circle и определяем метод printProperties, который пытается получить доступ к этим свойствам. В зависимости от модификаторов доступа некоторые из этих свойств доступны, а некоторые нет.

В Kotlin классы и их функции по умолчанию являются final, что означает, что они не могут быть унаследованы или переопределены. Чтобы разрешить наследование класса, он должен быть помечен как open, что означает, что он «открыт для расширения». Чтобы разрешить переопределение функций и полей класса, они также должны быть помечены как open.

Например:

open class Shape2 {
    open fun draw() {
        println("Drawing a shape")
    }
}

class Circle2 : Shape2() {
    override fun draw() {
        super.draw()
        println("Drawing a circle")
    }
}

fun main() {
    val shape = Shape2()
    shape.draw() // Вывод: Drawing a shape

    val circle = Circle2()
    circle.draw() // Вывод: Drawing a shape
                  //         Drawing a circle
}

В этом примере мы определяем класс Shape2, который является open и имеет функцию draw, которая также помечена как open. Затем мы наследуем от этого класса класс Circle2 и переопределаем функцию draw. В функции main мы создаем экземпляры классов Shape2 и Circle2 и вызываем на них функцию draw. Как видим, в случае с классом Shape2 вызывается только его собственная реализация функции draw, а в случае с классом Circle2 вызывается как реализация функции draw в базовом классе Shape2, так и переопределенная реализация в классе Circle2.

Value классы в Kotlin - это классы, которые предназначены для представления одного простого значения, такого как число или строка. Они предназначены для представления простых значений, и добавление дополнительной сложности сделало бы их менее эффективными и менее полезными.

Value классы аннотируются аннотацией @JvmInline, которая сообщает компилятору Kotlin встроить класс в вызывающий код. Это означает, что код для value класса фактически копируется в вызывающий код, устраняя накладные расходы на создание объекта для value класса и вызов функций на нем.

Например:

@JvmInline
value class Password(private val s: String)

fun main() {
    val securePassword = Password("Don't try this in production")
    // Нет фактического создания экземпляра класса 'Password'
    // Во время выполнения 'securePassword' содержит только 'String'
}

Это основная особенность value классов, которая вдохновила название inline: данные класса встраиваются в его использования (аналогично тому, как содержимое inline функций встраивается в места вызова).

2. Безопасность отсутствия значения (Null Safety)

Одним из ключевых улучшений в Kotlin является введение null-безопасности. В Kotlin вы можете явно указать, может ли переменная иметь значение null или нет, используя знак вопроса после типа переменной. Это позволяет компилятору проверять наличие null и предотвращать NullPointerException во время выполнения.

var a: String = "abc" // Обычная инициализация означает, что не может быть null по умолчанию
a = null // Ошибка компиляции

var b: String? = "abc" // Может быть установлено значение null
b = null // ОК

В этом примере мы видим, как можно использовать знак вопроса после типа переменной в Kotlin, чтобы указать, что она может иметь значение null. Это позволяет компилятору проверять наличие null и предотвращать ошибки во время выполнения.

В Kotlin вы также можете использовать операторы ? и ?: для безопасного обращения к значениям, которые могут быть null. Оператор ?. позволяет вызывать методы и свойства у объектов, которые могут быть null, без необходимости проверки наличия значения.

val a: String? = null
println(a?.length) // null

val b: String? = "abc"
println(b?.length) // 3

Оператор ?: позволяет указать значение по умолчанию, которое будет использовано, если значение слева от оператора является null.

val a: String? = null
println(a?.length ?: -1) // -1

val b: String? = "abc"
println(b?.length ?: -1) // 3

В этом примере мы видим, как можно использовать операторы ?. и ?: для безопасного обращения к значениям, которые могут быть null.

3. Функции (Functions)

Функции в Kotlin имеют несколько отличий от функций в Java. Например, в Kotlin нет ключевого слова void. Вместо этого используется тип Unit, который указывает на отсутствие возвращаемого значения.

Также в Kotlin есть поддержка функций с типовыми параметрами, которые позволяют создавать обобщенные функции. Обобщенная функция - это функция, которая может работать с разными типами данных. Типовой параметр - это переменная, которая используется для указания типа данных, с которым работает обобщенная функция.

Java:

public static void printHello(String name) {
    if (name != null)
        System.out.println("Hello " + name);
    else
        System.out.println("Hi there!");
}

Kotlin:

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
}

В этом примере мы видим, как в Kotlin можно использовать тип Unit вместо ключевого слова void и создавать обобщенные функции с помощью типовых параметров.

4. Конструкторы (Constructors)

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

Java:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Kotlin:

class Person(val name: String, var age: Int) {
    // ...
}

Вторичные конструкторы определяются в теле класса с помощью ключевого слова constructor. Они могут вызывать другие конструкторы с помощью ключевого слова this.

Java:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this(name, 0);
    }
}

Kotlin:

class Person(val name: String, var age: Int) {
    constructor(name: String) : this(name, 0) {
        // ...
    }
}

В этом примере мы видим, как в Kotlin можно использовать первичные и вторичные конструкторы для инициализации свойств класса.

5. Условные операторы (Conditional Operators)

Kotlin имеет несколько условных операторов, таких как if, else, else if и when. Оператор when аналогичен оператору switch в Java, но имеет более гибкий синтаксис.

Java:

switch (x) {
    case 1:
        System.out.print("x == 1");
        break;
    case 2:
        System.out.print("x == 2");
        break;
    default:
        System.out.print("x is neither 1 nor 2");
}

Kotlin:

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // Обратите внимание на блок
        print("x is neither 1 nor 2")
    }
}

В этом примере мы видим, как в Kotlin можно использовать оператор when для выполнения различных действий в зависимости от значения переменной.

6. Перегрузка методов (Method Overloading)

Kotlin поддерживает перегрузку методов, как и Java. Это означает, что вы можете иметь несколько методов с одинаковым именем, но с разными параметрами.

Java:

public static String foo() {
    return "foo";
}

public static String foo(int a) {
    return "foo " + a;
}

public static String foo(int a, int b) {
    return "foo " + a + " " + b;
}

public static void main(String[] args) {
    System.out.println(foo());
    System.out.println(foo(1));
    System.out.println(foo(1, 2));
}

Kotlin:

fun foo() = "foo"
fun foo(a: Int) = "foo $a"
fun foo(a: Int, b: Int) = "foo $a $b"

fun main() {
    println(foo())
    println(foo(1))
    println(foo(1, 2))
}

В этом примере мы видим, как в Kotlin можно перегружать методы с одинаковым именем, но с разными параметрами.

7. Синглтон (Singleton)

В Kotlin есть ключевое слово object, которое используется для создания синглтона. Синглтон - это класс, который имеет только один экземпляр.

Java:

public class Singleton {
    private static Singleton instance;
    private String value;

    private Singleton() {
        value = "Hello";
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public String getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance().getValue());
        System.out.println(Singleton.getInstance().getValue());
    }
}

Kotlin:

object Singleton {
    val value = "Hello"
}

fun main() {
    println(Singleton.value)
    println(Singleton.value)
}

В этом примере мы видим, как в Kotlin можно использовать ключевое слово object для создания синглтона. В Java для достижения аналогичного результата необходимо использовать шаблон проектирования "Одиночка" (Singleton).

8. Корутины и потоки (Coroutines and Threads)

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

Java:

new Thread(new Runnable() {
    @Override
    public void run() {
        // ...
    }
}).start();

Kotlin:

GlobalScope.launch {
    // ...
}

В этом примере мы видим, как в Kotlin можно использовать корутины для асинхронного выполнения кода. В Java для достижения аналогичного результата необходимо использовать традиционные потоки.

Одним из ключевых преимуществ корутин перед традиционными потоками является то, что они позволяют писать асинхронный код в стиле последовательного выполнения. Это означает, что вы можете использовать обычные управляющие конструкции, такие как if, while и for, для работы с асинхронным кодом, без необходимости использования колбэков или других сложных конструкций.

Например, давайте рассмотрим следующий пример кода на Kotlin, который использует корутины для асинхронного выполнения двух задач:

suspend fun task1(): Int {
    delay(1000)
    return 1
}

suspend fun task2(): Int {
    delay(2000)
    return 2
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val result1 = async { task1() }
        val result2 = async { task2() }
        println("Result: ${result1.await() + result2.await()}")
    }
    println("Completed in $time ms")
}

В этом примере мы видим, как можно использовать корутины для асинхронного выполнения двух задач и ожидания их завершения. Код выглядит так же, как и обычный последовательный код, но на самом деле он выполняется асинхронно.

9. Расширения функций (Function Extensions)

Расширения позволяют добавлять новые функции и свойства к существующим классам без необходимости наследования или изменения исходного кода. Это может быть полезно, если вы хотите расширить функциональность стандартных классов или классов из сторонних библиотек.

Java:

public static String lastChar(String s) {
    return s.substring(s.length() - 1);
}

public static void main(String[] args) {
    System.out.println(lastChar("abc"));
}

Kotlin:

fun String.lastChar() = this[length - 1]

fun main() {
    println("abc".lastChar())
}

В этом примере мы видим, как в Kotlin можно использовать расширения для добавления новых функций к существующим классам. В Java для достижения аналогичного результата необходимо создавать отдельные статические методы.

Расширения могут быть определены не только для функций, но и для свойств. Например, вы можете определить расширение для свойства isEmpty класса String, чтобы проверять, является ли строка пустой или содержит только пробельные символы:

val String.isBlank: Boolean
    get() = this.trim().isEmpty()

fun main() {
    println("   ".isBlank) // true
}

В этом примере мы видим, как можно определить расширение для свойства isBlank класса String, чтобы проверять, является ли строка пустой или содержит только пробельные символы.

10. Делегирование свойств (Property Delegation)

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

Java:

public class Example {
    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

Kotlin:

class Example {
    var value: String by Delegates.observable("<no name>") { prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val example = Example()
    example.value = "first"
    example.value = "second"
}

В этом примере мы видим, как в Kotlin можно использовать делегирование свойств для автоматического генерирования методов доступа к свойствам с дополнительной логикой. В Java для достижения аналогичного результата необходимо явно определять методы доступа к свойствам.

Kotlin предлагает несколько встроенных делегатов для свойств, таких как Delegates.observable и Delegates.vetoable, которые позволяют добавлять дополнительную логику при чтении или записи свойств. Вы также можете определить свои собственные делегаты для свойств, если вам нужна более сложная логика.

11. Лямбда-выражения (Lambda Expressions)

Лямбда-выражения являются одной из ключевых функций Kotlin, которые позволяют создавать анонимные функции и передавать их как аргументы в другие функции. Это может быть очень полезно при работе с коллекциями и другими функциональными API.

Java:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer number) {
        System.out.println(number);
    }
});

Kotlin:

val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { number -> println(number) }

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

Лямбда-выражения могут содержать несколько строк кода и возвращать значение. Если лямбда-выражение содержит только один параметр, то его имя можно опустить, и вместо него использовать ключевое слово it.

val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // [2, 4]

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

12. Коллекции (Collections)

Kotlin предлагает множество удобных функций для работы с коллекциями, таких как списки, множества и карты. Эти функции позволяют легко выполнять такие операции, как фильтрация, сортировка и преобразование коллекций.

Java:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = new ArrayList<>();
for (int number : numbers) {
    if (number % 2 == 0) {
        evenNumbers.add(number);
    }
}

Kotlin:

val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }

В этом примере мы видим, как в Kotlin можно использовать встроенные функции для работы с коллекциями, чтобы легко выполнять различные операции. В Java для достижения аналогичного результата необходимо использовать циклы и условные операторы.

Kotlin также предлагает множество других удобных функций для работы с коллекциями, таких как map, flatMap, reduce, groupBy и другие. Эти функции позволяют легко преобразовывать и агрегировать данные из коллекций.

13. Деструктуризация (Destructuring)

Kotlin также предлагает удобную функцию деструктуризации, которая позволяет легко извлекать значения из объектов и присваивать их переменным.

Java:

Map.Entry<String, Integer> entry = new AbstractMap.SimpleEntry<>("Alice", 30);
String name = entry.getKey();
int age = entry.getValue();

Kotlin:

val entry = "Alice" to 30
val (name, age) = entry

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

Деструктуризация может быть использована не только для извлечения значений из пар и карт, но и для извлечения значений из любых других объектов. Например, вы можете определить класс Person с двумя свойствами name и age и использовать деструктуризацию для извлечения этих значений:

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 30)
    val (name, age) = person
    println("$name is $age years old")
}

В этом примере мы видим, как можно использовать деструктуризацию для извлечения значений из объекта класса Person.

14. Инфиксные функции (Infix Functions)

Kotlin позволяет определять инфиксные функции, которые могут быть вызваны с помощью инфиксной нотации. Это может улучшить читаемость кода и сделать его более выразительным.

infix fun Int.pow(exponent: Int): Int {
    return this.toDouble().pow(exponent).toInt()
}

fun main() {
    val result = 2 pow 3
    println(result) // 8
}

В этом примере мы видим, как можно определить инфиксную функцию pow, которая возводит число в степень. Затем мы вызываем эту функцию с помощью инфиксной нотации 2 pow 3, что эквивалентно вызову 2.pow(3).

15. Операторы расширения (Extension Operators)

Kotlin позволяет перегружать стандартные операторы, такие как +, -, * и другие, для работы с пользовательскими типами данных. Это может улучшить читаемость кода и сделать его более выразительным.

data class Point(val x: Int, val y: Int)

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main() {
    val p1 = Point(1, 2)
    val p2 = Point(3, 4)
    val p3 = p1 + p2
    println(p3) // Point(x=4, y=6)
}

В этом примере мы видим, как можно перегрузить оператор + для класса Point, чтобы складывать две точки вместе. Затем мы используем этот оператор для сложения двух точек p1 и p2.

16. Функции высшего порядка (Higher-Order Functions)

Kotlin поддерживает функции высшего порядка, которые позволяют передавать функции как аргументы в другие функции или возвращать их как результат. Это может быть очень полезно при работе с функциональными API.

fun applyTwice(f: (Int) -> Int, x: Int): Int {
    return f(f(x))
}

fun main() {
    val result = applyTwice({ x -> x * 2 }, 10)
    println(result) // 40
}

В этом примере мы видим, как можно определить функцию высшего порядка applyTwice, которая принимает функцию f и число x в качестве аргументов и применяет функцию f к числу x дважды. Затем мы вызываем эту функцию с лямбда-выражением { x -> x * 2 }, которое умножает число на 2.

17. Типовые алиасы (Type Aliases)

Kotlin позволяет определять типовые алиасы, которые являются сокращениями для длинных и сложных типов данных. Это может улучшить читаемость кода и сделать его более понятным.

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    val isEven: Predicate<Int> = { it % 2 == 0 }
    println(foo(isEven)) // true
}

В этом примере мы видим, как можно определить типовой алиас Predicate<T> для типа (T) -> Boolean, который представляет собой функцию, принимающую значение типа T и возвращающую логическое значение. Затем мы используем этот типовой алиас для определения переменной isEven и передачи ее в функцию foo.

18. Сопоставление с образцом (Pattern Matching)

Kotlin поддерживает сопоставление с образцом, которое позволяет проверять значения переменных на соответствие определенным условиям и извлекать значения из них. Это может быть очень полезно при работе со сложными структурами данных.

fun main() {
    val x: Any = "Hello"
    when (x) {
        is Int -> println(x + 1)
        is String -> println(x.length + 1)
        is Double -> println(x * 2)
        else -> println("Unknown type")
    }
}

В этом примере мы видим, как можно использовать оператор when с условиями is для проверки типа переменной x и выполнения различных действий в зависимости от ее типа.

19. Делегирование (Delegation)

Kotlin поддерживает делегирование, которое позволяет делегировать реализацию интерфейса или класса другому объекту. Это может быть полезно, если вы хотите повторно использовать код или добавить дополнительную логику к существующему классу.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print() // выводит 10
}

В этом примере мы видим, как можно использовать делегирование для передачи реализации интерфейса Base от объекта b класса BaseImpl к объекту класса Derived. Затем мы вызываем метод print у объекта класса Derived, который делегирует его выполнение объекту b.

20. Стандартная библиотека (Standard Library)

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

val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // [2, 4]

В этом примере мы видим, как можно использовать функцию filter из стандартной библиотеки Kotlin для фильтрации списка чисел и получения только четных чисел.

21. Интероперабельность с Java (Interoperability with Java)

Kotlin полностью совместим с Java, что означает, что вы можете использовать Java-код в Kotlin-проектах и наоборот. Это позволяет постепенно переходить с Java на Kotlin, используя уже существующий код и библиотеки.

import java.util.Date

fun main() {
    val date = Date()
    println(date)
}

В этом примере мы видим, как можно использовать класс Date из стандартной библиотеки Java в Kotlin-проекте. Мы импортируем класс Date и используем его для создания нового объекта даты.

22. Вывод на консоль (Print to Console)

В Kotlin есть несколько способов вывода информации на консоль. Один из них - это использование функции println, которая аналогична функции System.out.println в Java.

Java:

System.out.println("Hello, World!");

Kotlin:

println("Hello, World!")

В этом примере мы видим, как в Kotlin можно использовать функцию println для вывода информации на консоль.

23. Константы и переменные (Constants and Variables)

В Kotlin есть два типа переменных: val и var. Переменные типа val являются неизменяемыми и не могут быть изменены после инициализации. Переменные типа var являются изменяемыми и могут быть изменены в любое время.

Java:

final int x = 1;
int y = 2;
y = 3;

Kotlin:

val x = 1
var y = 2
y = 3

В этом примере мы видим, как в Kotlin можно использовать переменные типа val и var для создания неизменяемых и изменяемых переменных соответственно.

24. Присваивание значения null (Assigning the null value)

В Kotlin вы можете явно указать, может ли переменная иметь значение null или нет, используя знак вопроса после типа переменной. Это позволяет компилятору проверять наличие null и предотвращать NullPointerExceptions во время выполнения.

var a: String = "abc" // Обычная инициализация означает, что не может быть null по умолчанию
a = null // Ошибка компиляции

var b: String? = "abc" // Может быть установлено значение null
b = null // ОК

В этом примере мы видим, как можно использовать знак вопроса после типа переменной в Kotlin, чтобы указать, что она может иметь значение null. Это позволяет компилятору проверять наличие null и предотвращать ошибки во время выполнения.

25. Проверка значения на NotNull или NotEmpty (Verify if value is NotNull OR NotEmpty)

В Kotlin вы можете использовать функции расширения для проверки того, является ли значение NotNull или NotEmpty. Например, вы можете определить функцию расширения isNotNullOrEmpty для класса String, чтобы проверять, является ли строка не пустой и не равной null.

fun String?.isNotNullOrEmpty(): Boolean = this != null && this.isNotEmpty()

fun main() {
    val a: String? = null
    println(a.isNotNullOrEmpty()) // false

    val b: String? = ""
    println(b.isNotNullOrEmpty()) // false

    val c: String? = "abc"
    println(c.isNotNullOrEmpty()) // true
}

В этом примере мы видим, как можно определить функцию расширения isNotNullOrEmpty для класса String, чтобы проверять, является ли строка не пустой и не равной null.

26. Конкатенация строк (Concatenation of strings)

В Kotlin есть несколько способов конкатенации строк. Один из них - это использование оператора +, который позволяет склеивать строки вместе.

val a = "Hello"
val b = "World"
val c = a + b
println(c) // HelloWorld

Также в Kotlin есть поддержка шаблонов строк, которые позволяют встраивать значения переменных и выражений непосредственно в строку с помощью символа $.

val a = "Hello"
val b = "World"
val c = "$a $b"
println(c) // Hello World

В этом примере мы видим, как можно использовать шаблоны строк в Kotlin для встраивания значений переменных и выражений непосредственно в строку.

27. Новая строка в строке (New line in string)

В Kotlin вы можете использовать символ \n для добавления новой строки в строку.

val a = "Hello\nWorld"
println(a)
// Hello
// World

В этом примере мы видим, как можно использовать символ \n в Kotlin для добавления новой строки в строку.

28. Подстрока (Substring)

В Kotlin вы можете использовать функцию substring для извлечения подстроки из строки. Функция substring принимает один или два аргумента: начальный индекс и конечный индекс (необязательный). Если конечный индекс не указан, то извлекается подстрока до конца строки.

val a = "Hello World"
val b = a.substring(0, 5)
println(b) // Hello

val c = a.substring(6)
println(c) // World

В этом примере мы видим, как можно использовать функцию substring в Kotlin для извлечения подстроки из строки.

29. Тернарные операции (Ternary Operations)

В Kotlin нет тернарного оператора ?:, как в Java. Вместо этого вы можете использовать обычный оператор if для выполнения условных операций.

Java:

int x = 1;
int y = 2;
int max = x > y ? x : y;

Kotlin:

val x = 1
val y = 2
val max = if (x > y) x else y

В этом примере мы видим, как можно использовать оператор if в Kotlin для выполнения условных операций.

30. Побитовые операторы (Bitwise Operators)

Kotlin имеет несколько побитовых операторов, таких как shl, shr, ushr, and, or, xor и inv. Они аналогичны побитовым операторам в Java, но имеют более читаемый синтаксис.

Java:

int x = 1;
int y = 2;
int z = x | y;

Kotlin:

val x = 1
val y = 2
val z = x or y

В этом примере мы видим, как можно использовать побитовые операторы в Kotlin для выполнения побитовых операций.

31. Циклы (Loops)

Kotlin предоставляет несколько типов циклов, таких как for, while и do-while, которые позволяют повторять действия определенное количество раз или до выполнения определенного условия.

Цикл for используется для итерации по элементам коллекции или диапазона значений. Синтаксис цикла for в Kotlin отличается от Java и более краткий и читаемый:

for (i in 1..5) {
    println(i)
}

В этом примере мы используем цикл for для итерации по диапазону значений от 1 до 5 включительно и выводим каждое значение на консоль.

Цикл while используется для повторения действий, пока выполняется определенное условие. Синтаксис цикла while в Kotlin аналогичен Java:

var i = 1
while (i <= 5) {
    println(i)
    i++
}

В этом примере мы используем цикл while для повторения действий, пока значение переменной i меньше или равно 5. На каждой итерации мы выводим значение переменной i на консоль и увеличиваем его на 1.

Цикл do-while аналогичен циклу while, но проверка условия выполняется после выполнения тела цикла, а не до. Это означает, что тело цикла будет выполнено хотя бы один раз, даже если условие не выполняется:

var i = 1
do {
    println(i)
    i++
} while (i <= 5)

В этом примере мы используем цикл do-while для повторения действий, пока значение переменной i меньше или равно 5. На каждой итерации мы выводим значение переменной i на консоль и увеличиваем его на 1.

32. Массивы (Arrays)

В Kotlin есть класс Array, который представляет собой массив фиксированного размера. Вы можете создавать массивы с помощью функции arrayOf или используя конструктор класса Array.

Java:

int[] numbers = new int[] {1, 2, 3, 4, 5};

Kotlin:

val numbers = arrayOf(1, 2, 3, 4, 5)

Также в Kotlin есть специальные классы для массивов примитивных типов, таких как IntArray, DoubleArray и другие. Они предлагают более эффективное использование памяти и быстродействие по сравнению с обычными массивами.

Java:

int[] numbers = new int[] {1, 2, 3, 4, 5};

Kotlin:

val numbers = intArrayOf(1, 2, 3, 4, 5)

В этом примере мы видим, как можно использовать массивы в Kotlin для хранения набора значений.

33. Списки (Lists)

В Kotlin есть класс List, который представляет собой упорядоченный список элементов. Вы можете создавать списки с помощью функции listOf или используя конструктор класса ArrayList.

Java:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

Kotlin:

val numbers = listOf(1, 2, 3, 4, 5)

Также в Kotlin есть мутабельные списки (MutableList), которые позволяют изменять содержимое списка после его создания. Вы можете создавать мутабельные списки с помощью функции mutableListOf или используя конструктор класса ArrayList.

Java:

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);

Kotlin:

val numbers = mutableListOf(1, 2, 3, 4, 5)

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

34. Множества (Sets)

В Kotlin есть класс Set, который представляет собой множество уникальных элементов. Вы можете создавать множества с помощью функции setOf или используя конструктор класса HashSet.

Java:

Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));

Kotlin:

val numbers = setOf(1, 2, 3, 4, 5)

Также в Kotlin есть мутабельные множества (MutableSet), которые позволяют изменять содержимое множества после его создания. Вы можете создавать мутабельные множества с помощью функции mutableSetOf или используя конструктор класса HashSet.

Java:

Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);

Kotlin:

val numbers = mutableSetOf(1, 2, 3, 4, 5)

В этом примере мы видим, как можно использовать множества в Kotlin для хранения уникальных значений.

35. Карты (Maps)

В Kotlin есть класс Map, который представляет собой коллекцию пар ключ-значение. Вы можете создавать карты с помощью функции mapOf или используя конструктор класса HashMap.

Java:

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 40);
map.put("Charlie", 50);

Kotlin:

val map = mapOf("Alice" to 30, "Bob" to 40, "Charlie" to 50)

Также в Kotlin есть мутабельные карты (MutableMap), которые позволяют изменять содержимое карты после ее создания. Вы можете создавать мутабельные карты с помощью функции mutableMapOf или используя конструктор класса HashMap.

Java:

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 40);
map.put("Charlie", 50);

Kotlin:

val map = mutableMapOf("Alice" to 30, "Bob" to 40, "Charlie" to 50)

В этом примере мы видим, как можно использовать карты в Kotlin для хранения пар ключ-значение.

36. Классы и объекты (Classes and Objects)

В Kotlin классы и объекты работают так же, как и в Java. Вы можете определить класс с помощью ключевого слова class и создавать объекты этого класса с помощью оператора new.

Java:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.getName());
        System.out.println(person.getAge());
    }
}

Kotlin:

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 30)
    println(person.name)
    println(person.age)
}

В этом примере мы видим, как в Kotlin можно определить класс и создавать объекты этого класса.

37. Наследование (Inheritance)

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

Java:

public class Shape {
    private int x;
    private int y;

    public Shape(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
}

public class Circle extends Shape {
    private int radius;

    public Circle(int x, int y, int radius) {
        super(x, y);
        this.radius = radius;
    }
}

Kotlin:

open class Shape(val x: Int, val y: Int) {
    fun move(dx: Int, dy: Int) {
        x += dx
        y += dy
    }
}

class Circle(x: Int, y: Int, val radius: Int) : Shape(x, y)

В этом примере мы видим, как в Kotlin можно определить базовый класс Shape и наследовать от него класс Circle.

38. Абстрактные классы и интерфейсы (Abstract Classes and Interfaces)

В Kotlin абстрактные классы и интерфейсы работают так же, как и в Java. Вы можете определить абстрактный класс с помощью ключевого слова abstract и реализовывать интерфейсы с помощью ключевого слова :.

Java:

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    private int x;
    private int y;
    private int radius;

    public Circle(int x, int y, int radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    @Override
    public void draw() {
        // ...
    }
}

Kotlin:

interface Shape {
    fun draw()
}

class Circle(val x: Int, val y: Int, val radius: Int) : Shape {
    override fun draw() {
        // ...
    }
}

В этом примере мы видим, как в Kotlin можно определить интерфейс Shape и реализовать его в классе Circle.

Абстрактные классы могут содержать абстрактные методы без реализации, которые должны быть реализованы в подклассах. Они также могут содержать обычные методы с реализацией.

Java:

public abstract class Shape {
    public abstract void draw();

    public void move(int dx, int dy) {
        // ...
    }
}

public class Circle extends Shape {
    private int x;
    private int y;
    private int radius;

    public Circle(int x, int y, int radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    @Override
    public void draw() {
        // ...
    }
}

Kotlin:

abstract class Shape {
    abstract fun draw()

    fun move(dx: Int, dy: Int) {
        // ...
    }
}

class Circle(val x: Int, val y: Int, val radius: Int) : Shape() {
    override fun draw() {
        // ...
    }
}

В этом примере мы видим, как в Kotlin можно определить абстрактный класс Shape с абстрактным методом draw и обычным методом move. Затем мы наследуем от этого класса класс Circle и реализуем абстрактный метод draw.

39. Свойства (Properties)

В Kotlin свойства являются первоклассными элементами языка, которые объединяют поля и методы доступа (геттеры и сеттеры) в одном месте. Свойства могут быть объявлены с помощью ключевых слов val (для неизменяемых свойств) или var (для изменяемых свойств).

class Person {
    var name: String = ""
    val age: Int = 0
}

В этом примере мы определяем класс Person с двумя свойствами: name и age. Свойство name является изменяемым, поэтому у него есть автоматически сгенерированный геттер и сеттер. Свойство age является только для чтения, поэтому у него есть только автоматически сгенерированный геттер.

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

class Person {
    var name: String = ""
        get() = field.capitalize()
        set(value) {
            field = value.trim()
        }
}

В этом примере мы определяем пользовательский геттер и сеттер для свойства name. Геттер возвращает значение поля с заглавной буквы, а сеттер устанавливает значение поля после удаления пробелов по краям.

40. Инициализация (Initialization)

В Kotlin вы можете использовать блок инициализации (init) для выполнения дополнительных действий при создании объектов класса. Блок инициализации выполняется после вызова конструктора и может использовать параметры конструктора и свойства класса.

class Person(val name: String, val age: Int) {
    init {
        println("Person created with name $name and age $age")
    }
}

fun main() {
    val person = Person("Alice", 30)
}

В этом примере мы определяем класс Person с блоком инициализации, который выводит сообщение при создании объектов этого класса. Затем мы создаем объект класса Person, который вызывает блок инициализации и выводит сообщение.

41. Компаньон-объекты (Companion Objects)

Компаньон-объекты являются объектами, которые связаны с классом и могут содержать статические методы и свойства. Они объявляются внутри класса с помощью ключевого слова companion object.

class Person(val name: String, val age: Int) {
    companion object {
        fun create(name: String, age: Int) = Person(name, age)
    }
}

fun main() {
    val person = Person.create("Alice", 30)
}

В этом примере мы определяем класс Person с компаньон-объектом, который содержит статический метод create. Этот метод создает новый объект класса Person с переданными аргументами. Затем мы вызываем этот метод для создания объекта класса Person.

42. Объектные выражения и объявления (Object Expressions and Declarations)

В Kotlin вы можете использовать объектные выражения и объявления для создания анонимных классов и объектов на лету. Объектные выражения используются для создания одиночных объектов, а объектные объявления используются для создания единственных экземпляров класса.

val person = object {
    val name = "Alice"
    val age = 30
}

fun main() {
    println(person.name)
    println(person.age)
}

В этом примере мы используем объектное выражение для создания анонимного объекта с двумя свойствами: name и age. Затем мы обращаемся к этим свойствам, чтобы вывести их значения.

object Person {
    val name = "Alice"
    val age = 30
}

fun main() {
    println(Person.name)
    println(Person.age)
}

В этом примере мы используем объектное объявление для создания единственного экземпляра класса Person с двумя свойствами: name и age. Затем мы обращаемся к этим свойствам, чтобы вывести их значения.

43. Дата-классы (Data Classes)

Дата-классы являются специальными классами, которые предназначены для хранения данных. Они автоматически генерируют методы equals, hashCode и toString, а также методы для копирования объектов и деструктуризации. Дата-классы объявляются с помощью ключевого слова data.

data class Person(val name: String, val age: Int)

fun main() {
    val person1 = Person("Alice", 30)
    val person2 = Person("Alice", 30)
    println(person1 == person2) // true

    val person3 = person1.copy(age = 40)
    println(person3) // Person(name=Alice, age=40)

    val (name, age) = person1
    println("$name is $age years old") // Alice is 30 years old
}

В этом примере мы определяем дата-класс Person с двумя свойствами: name и age. Затем мы создаем два объекта этого класса с одинаковыми значениями свойств и сравниваем их с помощью оператора ==. Также мы создаем копию одного из объектов с измененным значением свойства age и выводим его значение. Наконец, мы используем деструктуризацию для извлечения значений свойств из объекта и выводим их.

44. Запечатанные классы (Sealed Classes)

Запечатанные классы являются специальными классами, которые используются для представления ограниченного набора типов. Они похожи на перечисления, но могут содержать различные типы данных и иметь различные конструкторы. Запечатанные классы объявляются с помощью ключевого слова sealed.

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

fun main() {
    val expr = Sum(Const(1.0), Const(2.0))
    println(eval(expr)) // 3.0
}

В этом примере мы определяем запечатанный класс Expr с тремя подклассами: Const, Sum и NotANumber. Затем мы определяем функцию eval, которая вычисляет значение выражения в зависимости от его типа. Наконец, мы создаем объект класса Sum и вычисляем его значение с помощью функции eval.

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

45. Ленивая инициализация (Lazy Initialization)

Ленивая инициализация является техникой, которая позволяет отложить инициализацию объекта или вычисление значения до тех пор, пока оно не понадобится. В Kotlin вы можете использовать делегат lazy для ленивой инициализации свойств.

val lazyValue: String by lazy {
    println("Computed!")
    "Hello"
}

fun main() {
    println(lazyValue) // Computed! Hello
    println(lazyValue) // Hello
}

В этом примере мы определяем свойство lazyValue с ленивой инициализацией. Значение этого свойства вычисляется только при первом обращении к нему, а затем кэшируется для последующих обращений. Мы можем видеть это по выводу на консоль: сообщение "Computed!" выводится только один раз.

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

46. Дженерики (Generics)

Дженерики в Kotlin позволяют определять классы, интерфейсы и функции, которые могут работать с различными типами данных, сохраняя при этом типобезопасность. Они очень похожи на дженерики в Java, но в Kotlin есть некоторые улучшения, такие как использование ключевых слов in и out для указания ковариантности и контравариантности.

Например, вы можете определить обобщенный класс Box, который может хранить значение любого типа:

class Box<T>(t: T) {
    var value = t
}

Чтобы создать экземпляр этого класса, вы можете указать тип данных в угловых скобках:

val box: Box<Int> = Box<Int>(1)

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

val box = Box(1) // 1 имеет тип Int, поэтому компилятор понимает, что это Box<Int>

Дженерики также могут использоваться с функциями. Например, вы можете определить обобщенную функцию swap, которая меняет местами два элемента массива:

fun <T> swap(arr: Array<T>, i: Int, j: Int) {
    val tmp = arr[i]
    arr[i] = arr[j]
    arr[j] = tmp
}

Вы можете вызвать эту функцию для массива любого типа:

val arr = arrayOf(1, 2, 3)
swap(arr, 0, 2) // [3, 2, 1]

47. Как использовать ключевые слова in, out, where, и inline для работы с обобщенными типами в Kotlin

Обобщенные типы позволяют создавать код, который работает с разными типами данных, не теряя при этом безопасности типов и выразительности. Однако, иногда нужно указать дополнительную информацию о том, как обобщенные типы могут быть использованы или ограничены. Для этого в Kotlin есть несколько ключевых слов, которые помогают управлять поведением обобщенных типов и функций. Это ключевые слова in, out, where, и inline. Рассмотрим, что они означают и как их применять на примерах.

Ключевое слово in

Ключевое слово in используется для указания контравариантности обобщенного типа. Это означает, что тип может быть заменен на его супертип без нарушения безопасности типов. Например, рассмотрим следующий интерфейс Comparator<T>, который определяет метод для сравнения двух объектов типа T:

interface Comparator<T> {
    fun compare(a: T, b: T): Int
}

Если мы хотим создать функцию, которая сортирует список чисел по возрастанию, используя компаратор, мы можем написать так:

fun sort(numbers: List<Number>, comparator: Comparator<Number>) {
    // сортируем список по критерию компаратора
    numbers.sortWith(comparator)
}

Однако, если у нас есть компаратор для целых чисел Comparator<Int>, мы не сможем передать его этой функции, потому что Int не является подтипом Number. Чтобы решить эту проблему, мы можем использовать ключевое слово in при объявлении параметра компаратора таким образом:

fun sort(numbers: List<Number>, comparator: Comparator<in Number>) {
    // сортируем список по критерию компаратора
    numbers.sortWith(comparator)
}

Теперь мы можем передать компаратор для целых чисел этой функции, потому что Int является супертипом Number. Ключевое слово in говорит компилятору, что параметр компаратора может принимать любой тип, который является супертипом для Number. Это полезно, когда мы хотим использовать обобщенный тип только как входной параметр функции или интерфейса.

Давайте посмотрим на другой пример. Рассмотрим следующий класс Box<T>, который представляет коробку с объектом типа T:

class Box<T>(var content: T)

Если мы хотим создать функцию, которая меняет содержимое двух коробок местами, мы можем написать так:

fun swap(box1: Box<Number>, box2: Box<Number>) {
    // сохраняем содержимое первой коробки во временную переменную
    val temp = box1.content
    // присваиваем содержимое второй коробки первой коробке
    box1.content = box2.content
    // присваиваем содержимое временной переменной второй коробке
    box2.content = temp
}

Однако, если у нас есть коробки для целых чисел Box<Int>, мы не сможем передать их этой функции, потому что Int не является подтипом Number. Чтобы решить эту проблему, мы можем использовать ключевое слово in при объявлении параметров коробок таким образом:

fun swap(box1: Box<in Number>, box2: Box<in Number>) {
    // сохраняем содержимое первой коробки во временную переменную
    val temp = box1.content
    // присваиваем содержимое второй коробки первой коробке
    box1.content = box2.content
    // присваиваем содержимое временной переменной второй коробке
    box2.content = temp
}

Теперь мы можем передать коробки для целых чисел этой функции, потому что Int является супертипом Number. Ключевое слово in говорит компилятору, что параметры коробок могут принимать любой тип, который является супертипом для Number. Это полезно, когда мы хотим использовать обобщенный тип только как входной параметр класса или функции.

Ключевое слово out

Ключевое слово out используется для указания ковариантности обобщенного типа. Это означает, что тип может быть заменен на его подтип без нарушения безопасности типов. Например, рассмотрим следующий интерфейс Producer<T>, который определяет метод для получения объекта типа T:

interface Producer<T> {
    fun produce(): T
}

Если мы хотим создать функцию, которая возвращает список объектов, полученных от производителя, мы можем написать так:

fun getObjects(producer: Producer<Number>): List<Number> {
    // создаем пустой список для хранения объектов
    val list = mutableListOf<Number>()
    // добавляем 10 объектов от производителя в список
    repeat(10) {
        list.add(producer.produce())
    }
    // возвращаем список
    return list
}

Однако, если у нас есть производитель для целых чисел Producer<Int>, мы не сможем передать его этой функции, потому что Int не является супертипом Number. Чтобы решить эту проблему, мы можем использовать ключевое слово out при объявлении параметра производителя таким образом:

fun getObjects(producer: Producer<out Number>): List<Number> {
    // создаем пустой список для хранения объектов
    val list = mutableListOf<Number>()
    // добавляем 10 объектов от производителя в список
    repeat(10) {
        list.add(producer.produce())
    }
    // возвращаем список
    return list
}

Теперь мы можем передать производитель для целых чисел этой функции, потому что Int является подтипом Number. Ключевое слово out говорит компилятору, что параметр производителя может возвращать любой тип, который является подтипом для Number. Это полезно, когда мы хотим использовать обобщенный тип только как возвращаемое значение функции или интерфейса.

Давайте посмотрим на другой пример. Рассмотрим следующий класс Box<T>, который представляет коробку с объектом типа T:

class Box<T>(var content: T)

Если мы хотим создать функцию, которая возвращает содержимое коробки, мы можем написать так:

fun getContent(box: Box<Number>): Number {
    // возвращаем содержимое коробки
    return box.content
}

Однако, если у нас есть коробка для целых чисел Box<Int>, мы не сможем передать ее этой функции, потому что Int не является супертипом Number. Чтобы решить эту проблему, мы можем использовать ключевое слово out при объявлении параметра коробки таким образом:

fun getContent(box: Box<out Number>): Number {
    // возвращаем содержимое коробки
    return box.content
}

Теперь мы можем передать коробку для целых чисел этой функции, потому что Int является подтипом Number. Ключевое слово out говорит компилятору, что параметр коробки может возвращать любой тип, который является подтипом для Number. Это полезно, когда мы хотим использовать обобщенный тип только как возвращаемое значение класса или функции.

Ключевое слово where

Ключевое слово where используется для указания ограничений на обобщенные типы. Это означает, что тип должен соответствовать определенным условиям, например, реализовывать определенный интерфейс или быть подтипом другого типа. Например, рассмотрим следующую функцию, которая принимает аргумент обобщенного типа T и возвращает его максимальное значение:

fun <T> max(t: T): T {
    // ...
}

Если мы хотим ограничить тип T только теми типами, которые реализуют интерфейс Comparable<T>, то есть могут быть сравнены между собой, мы можем использовать ключевое слово where при объявлении функции таким образом:

fun <T> max(t: T): T where T : Comparable<T> {
    // ...
}

Теперь, если мы попытаемся вызвать эту функцию с аргументом, который не реализует интерфейс Comparable<T>, код не скомпилируется. Ключевое слово where говорит компилятору, что тип T должен удовлетворять определенному ограничению. Мы можем указать несколько ограничений для одного типа, разделяя их запятыми. Это полезно, когда мы хотим использовать обобщенный тип в разных контекстах и операциях.

Давайте посмотрим на другой пример. Рассмотрим следующий класс Person, который представляет человека с именем и возрастом:

class Person(val name: String, val age: Int)

Если мы хотим создать функцию, которая принимает два объекта типа Person и возвращает того, кто старше, мы можем написать так:

fun older(person1: Person, person2: Person): Person {
    // сравниваем возраст двух людей
    return if (person1.age > person2.age) person1 else person2
}

Однако, если мы хотим сделать эту функцию более обобщенной и работающей с любыми типами данных, которые имеют свойство age, мы не можем просто заменить тип Person на обобщенный тип T, потому что компилятор не будет знать, что у типа T есть свойство age. Чтобы решить эту проблему, мы можем использовать ключевое слово where при объявлении функции таким образом:

fun <T> older(t1: T, t2: T): T where T : Any, T : HasAge {
    // сравниваем возраст двух объектов
    return if (t1.age > t2.age) t1 else t2
}

Теперь мы можем вызвать эту функцию с любыми типами данных, которые являются подтипами Any и реализуют интерфейс HasAge, который определяет свойство age. Ключевое слово where говорит компилятору, что тип T должен удовлетворять двум ограничениям: быть подтипом Any и реализовывать интерфейс HasAge. Это полезно, когда мы хотим использовать обобщенный тип с разными свойствами и методами.

Ключевое слово inline

Ключевое слово inline используется для указания, что функция должна быть встроена в месте вызова. Это означает, что компилятор заменит вызов функции на фактический код функции, что может улучшить производительность за счет уменьшения накладных расходов на вызов функции. Например, рассмотрим следующую функцию, которая принимает лямбда-выражение в качестве аргумента и выполняет его:

fun doSomething(body: () -> Unit) {
    body()
}

Если мы вызываем эту функцию таким образом:

doSomething { println("Hello") }

Компилятор сгенерирует код, который создает анонимный класс для лямбда-выражения и вызывает его метод invoke. Это может привести к дополнительным расходам памяти и времени. Однако, если мы изменяем сигнатуру функции, используя ключевое слово inline таким образом:

inline fun doSomething(body: () -> Unit) {
    body()
}

Теперь, когда мы вызываем эту функцию, компилятор встроит ее код в месте вызова, что означает, что он заменит вызов функции на фактический код лямбда-выражения. Это может привести к уменьшению расходов памяти и времени. Ключевое слово inline говорит компилятору, что функция должна быть встроена в месте вызова.

Давайте посмотрим на другой пример. Рассмотрим следующий класс Box<T>, который представляет коробку с объектом типа T:

class Box<T>(var content: T)

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

fun <T> withBox(box: Box<T>, body: (T) -> Unit) {
    // выполняем лямбда-выражение с содержимым коробки
    body(box.content)
}

Однако, если мы вызываем эту функцию таким образом:

val box = Box(42)
withBox(box) { println(it) }

Компилятор сгенерирует код, который создает анонимный класс для лямбда-выражения и передает его в качестве параметра функции. Это может привести к дополнительным расходам памяти и времени. Однако, если мы изменяем сигнатуру функции, используя ключевое слово inline таким образом:

inline fun <T> withBox(box: Box<T>, body: (T) -> Unit) {
    // выполняем лямбда-выражение с содержимым коробки
    body(box.content)
}

Теперь, когда мы вызываем эту функцию, компилятор встроит ее код в месте вызова, что означает, что он заменит вызов функции на фактический код лямбда-выражения. Это может привести к уменьшению расходов памяти и времени. Ключевое слово inline говорит компилятору, что функция должна быть встроена в месте вызова.

48. Использование let, when, with, also, apply

В этой частьи статьи мы рассмотрим, как использовать ключевые слова let, when, with, also и другие функции области видимости в Kotlin. Функции области видимости - это функции, которые позволяют выполнять блок кода в контексте некоторого объекта. Они полезны для упрощения и структурирования кода, избегания повторений и проверок на null, а также для создания цепочек вызовов. В этой статье мы рассмотрим, какие функции области видимости существуют в Kotlin, чем они отличаются и как их правильно использовать.

Функция let

Функция let принимает объект, на котором она вызывается, в качестве параметра (it) и возвращает результат лямбда-выражения. Функция let полезна для выполнения действий с объектом, если он не null, или для преобразования его в другой тип. Например:

val name: String? = "John"
name?.let {
    println("Hello, $it") // выводит "Hello, John", если name не null
}

val number = "123"
val parsed = number.let {
    it.toInt() // преобразует строку в число и возвращает его
}
println(parsed) // выводит 123

В первом примере мы используем let для проверки имени на null и вывода его значения. Заметьте, что внутри лямбда-выражения мы можем использовать параметр it, который содержит копию объекта. Во втором примере мы используем let для преобразования строки в число и возвращаем его как результат функции.

Функция when

Функция when является альтернативой оператору switch в Java. Она позволяет проверять значение переменной на соответствие различным условиям и выполнять соответствующий блок кода. Функция when полезна для замены длинных цепочек if-else или when без аргумента. Например:

val x = 5
when (x) {
    1 -> println("x == 1")
    2 -> println("x == 2")
    else -> {
        // блок кода
        println("x is neither 1 nor 2")
    }
}

val y = "Hello"
when {
    y.startsWith("He") -> println("y starts with He")
    y.endsWith("lo") -> println("y ends with lo")
    else -> println("y is something else")
}

В первом примере мы используем when с аргументом x и проверяем его на равенство 1 или 2. Если ни одно из условий не выполняется, то выполняется блок кода в ветке else. Заметьте, что мы можем использовать блок кода для любой ветки, если он заключен в фигурные скобки. Во втором примере мы используем when без аргумента и проверяем разные условия для переменной y. Здесь мы можем использовать любые булевы выражения для каждой ветки.

Функция with

Функция with принимает объект в качестве аргумента и возвращает результат лямбда-выражения. Функция with полезна для группировки вызовов функций или свойств на одном объекте. Например:

val numbers = mutableListOf(1, 2, 3)
with(numbers) {
    println("First element: ${first()}")
    println("Last element: ${last()}")
    println("Even numbers: ${filter { it % 2 == 0 }}")
}

В этом примере мы используем with для доступа к методам и свойствам списка numbers без повторения его имени. Заметьте, что внутри лямбда-выражения мы можем использовать this, который содержит ссылку на объект. Также заметьте, что with возвращает результат последнего выражения в блоке.

Функция also

Функция also принимает объект, на котором она вызывается, в качестве параметра (it) и возвращает сам объект. Функция also полезна для выполнения дополнительных действий с объектом, таких как логирование или печать. Например:

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

В этом примере мы используем also для вывода элементов списка перед добавлением нового элемента. Заметьте, что also не изменяет объект, а только возвращает его. Таким образом, мы можем использовать also для создания цепочек вызовов на одном объекте.

Функция apply

Функция apply принимает объект, на котором она вызывается, в качестве получателя (this) и возвращает сам объект. Функция apply полезна для конфигурации или инициализации объекта. Например:

val person = Person().apply {
    name = "John"
    age = 25
    occupation = "Developer"
}

В этом примере мы используем apply для установки свойств объекта person без повторения его имени. Заметьте, что внутри лямбда-выражения мы можем использовать this, который содержит ссылку на объект. Также заметьте, что apply возвращает сам объект, поэтому мы можем присвоить его переменной person.

В заключении, функции области видимости в Kotlin - это мощный инструмент для упрощения и структурирования кода. Они позволяют изменять контекст выполнения кода и возвращать разные результаты. Однако, они также требуют внимательности и понимания различий между ними. Вот таблица, которая поможет вам выбрать подходящую функцию области видимости для вашей задачи:

Функция Контекст Аргумент Результат Пример использования
let Объект it Любой Выполнить действие с объектом, если он не null; преобразовать объект в другой тип
run Объект или без контекста this или без аргумента Любой Выполнить блок кода на объекте или без контекста; вернуть результат блока
also Объект it Объект Выполнить дополнительное действие с объектом; вернуть объект
apply Объект this Объект Конфигурировать или инициализировать объект; вернуть объект
with Объект (аргумент) this Любой Группировать вызовы функций или свойств на объекте; вернуть результат блока
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment