Всем привет, меня зовут Сергей Прощаев. Я техлид в FinTech, каждый день работаю с Java и Kotlin, а ещё преподаю на курсах в Otus — помогаю разработчикам прокачиваться в современных технологиях. Продолжаю серию статей «Kotlin для новичков». В прошлый раз мы с вами настроили IntelliJ IDEA, разобрались с Gradle и даже запустили «Hello, World!». Надеюсь, у вас всё взлетело, и репозиторий с кодом уже форкнут.

Сегодня мы сделаем следующий, гораздо более важный шаг: начнём писать код, который действительно что‑то делает. Мы поговорим о фундаменте — переменных и базовых операциях. Знаете, обучая стажёров в FinTech, я заметил одну закономерность: новички часто запоминают синтаксис, но не понимают философию языка. Они пишут на Kotlin так, как писали на Java, и удивляются, почему код получается громоздким.

Наша цель сегодня — не просто выучить, что такое val и var, а прочувствовать, как Kotlin помогает нам проектировать правильные, надёжные программы с минимальными усилиями. Мы затронем темы, которые на первых порах кажутся разнородными (типы данных, строки, арифметика, ввод/вывод), но к концу статьи вы увидите, как они складываются в единую стройную картину.

Поехали!

1. В Kotlin всё начинается с объявления: val и var

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

Здесь у нас есть два главных инструмента.

  • val (от value — значение). Это неизменяемая ссыл��а. Представьте себе ячейку в памяти, которую мы заполнили один раз и «запечатали». Изменить содержимое этой ячейки нельзя. В других языках это называют константой или final‑переменной.

  • var (от variable). Это изменяемая ссылка. Содержимое такой ячейки можно перезаписывать сколько угодно раз.

fun main() {
    // Неизменяемая переменная (рекомендуется по умолчанию)
    val appName: String = "KotlinFinPortal"
    println("Добро пожаловать в $appName")

    // Изменяемая переменная
    var userScore: Int = 0
    println("Ваш текущий счёт: $userScore")
    userScore = 42 // Перезаписываем значение
    println("Новый счёт: $userScore")

    // Ошибка! val нельзя переназначить.
    // appName = "NewName"
}

Мой личный совет (из опыта продакшена): Я требую от своей команды использовать val везде, где это возможно. Почему? Это сильно упрощает чтение кода. Когда вы видите val, вы сразу знаете, что значение не изменится где‑то в другом потоке или через 50 строк кода. Вам не нужно держать в голове историю этой переменной. Это снижает когнитивную нагрузку и количество багов. Если вы через полгода вернётесь к коду и увидите var, вы должны задаться вопросом: «А почему, собственно, оно меняется? Может быть, это можно переписать на val?».

2. Волшебство вывода типов (Type Inference)

В примере выше я специально написал тип (: String: Int), чтобы было понятнее. Но Kotlin настолько умён, что в 90% случаев может определить тип сам. Это называется выводом типов.

Давайте посмотрим в примере:

fun main() {
    // Kotlin сам понимает, что это String и Int
    val appName = "KotlinFinPortal"
    var userScore = 0 // Тип Int

    val pi = 3.1415 // Тип Double
    val isEnabled = true // Тип Boolean
}

Это лаконично, но здесь кроется небольшая ловушка для новичка. Посмотрите на userScore = 0. Компилятор вывел тип Int. А что, если вы попытаетесь присвоить ему дробное число?

userScore = 42.5 // Ошибка! Требуется Int, а найден Double.

Это «защита от дурака» 😊 надежная защита на уровне Kotlin. Код не скомпилируется, и вы на месте исправите логику, вместо того чтобы получить странное поведение в рантайме.

3. Базовые типы: всё — объект

В Java есть примитивы (intboolean) и ссылочные типы (IntegerBoolean). В Kotlin этого разделения нет для нас как для разработчиков. Всё является объектом. Это удобно: мы можем вызвать метод у числа.

Давайте напишем еще один простой пример:

fun main() {
    val number = 42
    println(number.toDouble()) // Превращаем Int в Double
    println(number.toString()) // Превращаем в строку
    println(42.dec()) // Метод dec() возвращает число на 1 меньше (41)
}

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

Краткий обзор базовых типов, которые есть в Kotlin

В таблице приведены все типы данных, которые есть в Kotlin:

Категория

Тип

Размер (бит)

Мин. значение

Макс. значение

Точность (цифры)

Суффикс

Примечание

Целые (signed)

Byte

8

-128 (-2⁷)

127 (2⁷-1)

Самый маленький целочисленный

Short

16

-32,768 (-2¹⁵)

32,767 (2¹⁵-1)

Int

32

-2,147,483,648 (-2³¹)

2,147,483,647 (2³¹-1)

Тип по умолчанию для целых

Long

64

-9,223,372,036,854,775,808 (-2⁶³)

9,223,372,036,854,775,807 (2⁶³-1)

L

Для больших чисел

Целые (unsigned)

UByte

8

0

255 (2⁸-1)

u/U

Kotlin 1.3+

UShort

16

0

65,535 (2¹⁶-1)

u/U

UInt

32

0

4,294,967,295 (2³²-1)

u/U

ULong

64

0

18,446,744,073,709,551,615 (2⁶⁴-1)

UL

Вещественные

Float

32

1.4E-45

3.4E+38

6–7

f/F

IEEE 754 single precision

Double

64

4.9E-324

1.8E+308

15–16

Тип по умолчанию для дробных

Логический

Boolean

1

false

true

Только true/false

Символьный

Char

16

\u0000 (0)

\uFFFF (65,535)

Unicode символ

Строка

String

variable

Последовательность Char

Специальные

Unit

Аналог void в Java

Nothing

0

Тип без значений (never returns)

Any

Корень иерархии типов (non‑nullable)

Any?

Корень иерархии типов (nullable)

Кратко остановлюсь на основных.

  • Числовые:

    • Byte (-128..127)

    • Short (-32768..32767)

    • Int (миллиарды, используется чаще всего)

    • Long (ещё больше, нужно указывать суффикс L, например 100L)

    • Float (с плавающей точкой, суффикс f, например 3.14f)

    • Double (с плавающей точкой двойной точности, используется по умолчанию для дробей)

  • Символы: Char (один символ в кавычках, например 'A')

  • Логические: Boolean (true / false)

  • Строки: String (не является примитивом, но поддержана на уровне языка)

4. Работа с символами (Char) и Unicode

Один из моих любимых вопросов на собеседовании: «Что будет, если сложить два символа?». Новички часто теряются. Давайте разбираться.

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

Вернемся в интегрированную среду разработки IntelliJ IDEA и напишем еще пример:

fun main() {
    val letterA: Char = 'A'
    val emoji: Char = '\u0394' // Дельта (Δ) в Unicode
    val sigma: Char = 'Σ'

    println("Буква: $letterA, код: ${letterA.code}") // Код 65 (ASCII)
    println("Символ: $emoji") // Δ
    println("Сумма? ${letterA + emoji}") // Ошибка! Складывать Char нельзя.
}

Символы — это не числа. Хотя у каждого есть числовой код, Kotlin запрещает арифметические операции с Char, чтобы избежать логических ошибок. Не пытайтесь получить 'C' сложением 'A' и 2. Для этого есть специальные функции.

5. Строки: шаблоны и многогранность

Мы уже вовсю используем шаблоны строк (знак $). Это то, чего мне так не хватало в Java долгие годы.

Еще небольшой пример:

fun main() {
    val name = "Сергей"
    val age = 25
    // Простая вставка переменной
    println("Привет, $name!")

    // Вставка выражения (нужны фигурные скобки)
    println("Через год вам будет ${age + 1} лет.")
    println("Ваше имя из ${name.length} символов.")
}

Это не просто синтаксический сахар. Это читаемость вашего кода. Забудьте про конкатенацию через +. Шаблоны — это стандарт.

Многострочные строки (Raw Strings):Если вам нужно вывести JSON, SQL‑запрос или ASCII‑арт, используйте тройные кавычки.

fun main() {
    val logo = """
        |  ╔════════════╗
        |  ║  KOTLIN    ║
        |  ╚════════════╝
    """.trimMargin() // Убирает отступы и символ '|'
    println(logo)
}

Правда ли ведь элегантно? 😊

6. Арифметика и преобразование типов (Type Coercion)

С числами всё привычно: +-*/% (остаток от деления).

Но есть важное отличие от Java и C++, которое ловит многих новичков.

Напишем еще пример:

fun main() {
    val a: Int = 10
    val b: Int = 3

    val result = a / b
    println("Результат: $result") // Будет 3! (целочисленное деление)

    val correctResult = a / b.toDouble()
    println("Правильный результат: $correctResult") // 3.333...
}

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

Это называется type coercion (приведение типов), и в Kotlin оно менее автоматическое, чем в Java. Компилятор не даст вам просто так сложить Double и Int, не предупредив. Но если вы сложите их, будет работать неявное преобразование меньшего типа в больший (Int станет Double).

val sum = 10 + 3.5 // sum будет Double (10 преобразовалось в 10.0)

7. Остаток от деления (Modulo) — важный нюанс

Оператор % (rem — remainder) часто используют для проверки на чётность (x % 2 == 0). Но с отрицательными числами в Kotlin (и многих других языках) результат может быть неожиданным.

Давайте еще набросаем пару строк кода:

fun main() {
    println("10 % 3 = ${10 % 3}")   // 1
    println("10 % -3 = ${10 % -3}") // 1
    println("-10 % 3 = ${-10 % 3}") // -1 (не 2!)
}

Правило простое: знак результата всегда совпадает со знаком делимого (левого операнда). Это важно учитывать, например, в графике или математических расчётах, где нужен всегда положительный остаток.

8. Инкремент и декремент

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

Давайте посмотрим, как работают эти операторы на небольшом примере:

fun main() {
    var counter = 0
    println("Старт: $counter")
    println("Постфикс: ${counter++}") // Сначала выведет 0, потом увеличит до 1
    println("Сейчас: $counter") // 1
    println("Префикс: ${++counter}") // Сначала увеличит до 2, потом выведет
}

Best Practice: Если вам нужно просто увеличить счётчик, пишите counter++ на отдельной строке. Если это часть сложного выражения, сто раз подумайте, не сделает ли это код нечитаемым. Обыно лучше вынести в отдельную строку.

9. Ввод данных: знакомьтесь, readln()

До сих пор мы только выводили данные. Пришло время научиться их вводить. В Kotlin для этого есть простая функция readln() (или readLine(), но readln() — современный вариант, который бросает исключение при ошибке, что удобнее).

Небольшой пример:

import java.time.Year // Импортируем для примера

fun main() {
    println("Как вас зовут?")
    val userName = readln() // Читаем строку

    println("Привет, $userName! Сколько вам лет?")
    val ageInput = readln()
    val age = ageInput.toInt() // Преобразуем строку в число

    val birthYear = Year.now().value - age
    println("$userName, вы, вероятно, родились в $birthYear году.")
}

Здесь мы коснулись важной темы: преобразования типов. Из консоли мы всегда получаем строку (String). Чтобы работать с числами, мы вызываем у этой строки метод .toInt().toDouble() и так далее. Если пользователь введёт не число, программа упадёт с ошибкой. Обработку ошибок мы рассмотрим в следующих статьях.

Реальная история: как null‑безопасность и неизменяемость спасли нам релиз

Расскажу случай из моей практики в FinTech. Мы переписывали старый Java‑монолит, отвечающий за расчёт кредитных лимитов, на Kotlin. Кодовая база была огромной, с тысячами строк и множеством static методов, которые меняли состояние глобальных объектов.

В Java была стандартная проблема: чтобы понять, может ли переменная быть null, нужно было читать тонны документации или дебажить. В итоге в прод уходил код, который иногда падал с NullPointerException (NPE) при определённых комбинациях данных клиента.

Когда мы начали перенос на Kotlin, первое, что мы сделали — начали максимально использовать val и запретили var в публичных API. Второе — это null‑безопасность. В Kotlin типы по умолчанию не могут быть null. Если переменная может быть null, вы должны явно указать это знаком ?.

var userName: String = "Иван" // Никогда не null
var userMiddleName: String? = null // Может быть null

Так вот, в ходе рефакторинга одного из модулей, мы наткнулись на метод, который в Java мог вернуть null для одного из полей. При переносе на Kotlin наш новый разработчик (стажёр) случайно поставил обычный тип, без ?. Компилятор выдал ошибку! Он сказал: «Ты утверждаешь, что это поле никогда не равно null, но в коде есть путь, где ты пытаешься вернуть null».

Разработчик был вынужден пойти в бизнес‑логику и разобраться. Оказалось, что в том единственном кейсе, когда возвращался null, бизнес‑правило было устаревшим, и поле на самом деле всегда должно было быть заполнено. Мы исправили логику, убрали null, и код стал чище. Компилятор предотвратил потенциальный NPE, который гарантированно упал бы в продакшене через месяц. В Java мы бы это заметили только на нагрузочном тестировании или, что хуже, в реальной работе. Это та самая магия Kotlin, которая экономит миллионы.

Визуализация: поток данных в программе

Чтобы закрепить понимание, как данные движутся в нашей маленькой программе, давайте взглянем на простую диаграмму последовательности, изображенную на рисунке 1. Может показаться, что для этой статьи она избыточна, но с учетом того, что все разработчики должны уметь ее читать — решил, что мы должны познакомиться и с ней на каком‑то простом примере.

Рисунок 1. Схема взаимодействия пользователя с программой при вводе и выводе данных. Компилятор Kotlin гарантирует, что мы не потеряемся в типах на этом пути.
Рисунок 1. Схема взаимодействия пользователя с программой при вводе и выводе данных. Компилятор Kotlin гарантирует, что мы не потеряемся в типах на этом пути.

Заключение и путь вперёд

Мы прошли только по верхам, но, надеюсь, вы почувствовали философию Kotlin: он лаконичный, выразительный и очень прагматичный. Он не даёт вам стрелять себе в ногу, заставляя явно объявлять изменяемость (var), явно работать с nullable типами и явно преобразовывать типы.

Что мы разобрали:

  • Неизменяемые (val) и изменяемые (var) переменные.

  • Вывод типов и базовую числовую вселенную (IntDoubleChar).

  • Работу со строками и их шаблонами.

  • Арифметику и важные нюансы деления и взятия остатка.

  • Простое взаимодействие с пользователем через консоль.

Это основа, на которой строится всё остальное. В следующих статьях мы перейдём к логическим конструкциям (ifwhen), циклам и, конечно, к функциям. А ещё нас ждут корутины — тема, которая поначалу пугает, но в Kotlin сделана невероятно элегантно.

Весь код из этой статьи доступен в моём репозитории на GitHub

Если вы хотите не просто читать разрозненные статьи, а выстроить системное понимание языка, научиться применять эти паттерны в реальных проектах (как бэкенд, так и многоплатформенная разработка), приходите на наш курс «Kotlin-разработчик. Базовый уровень» в OTUS.

Не откладывайте изучение на завтра. Рынок ждёт грамотных Kotlin‑разработчиков уже сегодня!


Серия статей «Kotlin для новичков»:

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