Найти в Дзене

Как работает интерполяция строк в Kotlin?

Интерполяция строк в Kotlin позволяет легко объединять константные строки и переменные для создания новой строки — элегантно и читабельно. В этой статье мы посмотрим, как работает интерполяция под капотом, проанализируем сгенерированный байткод, а также рассмотрим возможные будущие оптимизации текущей реализации. Начнём с простого и знакомого примера: class Person(val firstName: String, val lastName: String, val age: Int) {
override fun toString(): String {
return "$firstName $lastName is $age years old"
}
} Как видно, мы используем интерполяцию строк для реализации метода toString(). Чтобы понять, как компилятор Kotlin реализует эту конструкцию, сначала скомпилируем класс с помощью kotlinc: >> kotlinc interpolation.kt Затем используем инструмент javap, чтобы заглянуть в байткод: >> javap -c -p com.baeldung.interpolation.Person Вывод (сокращённый): >> javap -c -p com.baeldung.interpolation.Person
// truncated
public java.lang.String toString();
Code:
0: new
Оглавление

1. Обзор

Интерполяция строк в Kotlin позволяет легко объединять константные строки и переменные для создания новой строки — элегантно и читабельно.

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

2. Интерполяция строк

Начнём с простого и знакомого примера:

class Person(val firstName: String, val lastName: String, val age: Int) {
override fun toString(): String {
return "$firstName $lastName is $age years old"
}
}

Как видно, мы используем интерполяцию строк для реализации метода toString().

Чтобы понять, как компилятор Kotlin реализует эту конструкцию, сначала скомпилируем класс с помощью kotlinc:

>> kotlinc interpolation.kt

Затем используем инструмент javap, чтобы заглянуть в байткод:

>> javap -c -p com.baeldung.interpolation.Person

Вывод (сокращённый):

>> javap -c -p com.baeldung.interpolation.Person
// truncated
public java.lang.String toString();
Code:
0: new #9 // создаёт StringBuilder
3: dup
4: invokespecial #13 // вызов конструктора
7: aload_0
8: getfield #17 // firstName
11: invokevirtual #21 // append(firstName)
14: bipush 32 // пробел
16: invokevirtual #24 // append(' ')
19: aload_0
20: getfield #27 // lastName
23: invokevirtual #21 // append(lastName)
26: ldc #29 // строка " is "
28: invokevirtual #21
31: aload_0
32: getfield #33 // age
35: invokevirtual #36 // append(age)
38: ldc #38 // " years old"
40: invokevirtual #21
43: invokevirtual #40 // toString()
46: areturn

Этот байткод эквивалентен следующему коду на Java:

new StringBuilder()
.append(firstName)
.append(' ')
.append(lastName)
.append(" is ")
.append(age)
.append(" years old")
.toString();

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

2.1. Плюсы и минусы

Преимущества:

  • Использование StringBuilder — простой и понятный механизм для большинства Java и Kotlin-разработчиков.

Недостатки:

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

Далее мы увидим, как Java 9 и Kotlin 1.4.20 решают эти проблемы.

3. Invoke Dynamic

Invoke Dynamic (также известный как Indy) был частью спецификации JSR 292 и предназначался для улучшения поддержки динамически типизированных языков на JVM. Начиная с Java 9, в рамках JEP 280, конкатенация строк в Java реализуется с использованием invokedynamic.

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

Начиная с Kotlin 1.4.20, компилятор Kotlin также может использовать invokedynamic для интерполяции строк. Для этого нужно:

  • Указать целевую JVM-версию 9 или выше, так как invokedynamic доступен только начиная с Java 9;
  • Указать флаг компилятора -Xstring-concat, чтобы включить поддержку invokedynamic.

Пример компиляции с нужными параметрами:

rubyКопироватьРедактировать>> kotlinc -jvm-target 9 -Xstring-concat=indy-with-constants interpolation.kt

Сгенерированный байткод будет выглядеть так:

>> javap -c -p com.baeldung.interpolation.Person
public java.lang.String toString();
Code:
0: aload_0
1: getfield #11 // firstName
4: aload_0
5: getfield #14 // lastName
8: aload_0
9: getfield #18 // age
12: invokedynamic #30, 0 // makeConcatWithConstants
17: areturn

Как видно, байткод остаётся простым и компактным, независимо от количества переменных в шаблоне. Это делает реализацию более надёжной и менее многословной.

Важно: это работает только с Java 9+ и Kotlin 1.4.20+ при задании целевой JVM 9+.

Планируется, что в
Kotlin 1.5 invokedynamic станет реализацией по умолчанию.

4. Заключение

В этой короткой статье мы рассмотрели две реализации интерполяции строк в Kotlin:

  • через StringBuilder — классическую и понятную;
  • через invokedynamic — современную и гибкую.

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