Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

Kotlin 1.4.30의 새로운 언어 기능 테스트 버전

Kotlin 1.5에 다음과 같은 언어 기능을 새로 추가할 계획이며, 이 기능은 Kotlin 1.4.30에서 사용해 볼 수 있습니다.

새 기능을 사용하려면 1.5 언어 버전을 지정해야 합니다.

새로운 릴리스 주기에 따라 Kotlin 1.5는 몇달 후에 출시될 예정이지만 새로운 기능은 1.4.30에서 테스트 버전으로 제공됩니다. 조기에 받는 여러분의 의견은 저희에게 매우 소중하므로 지금 새 기능을 평가해주시면 감사하겠습니다.

inline value 클래스 안정화

inline 클래스는 Kotlin 1.3부터 알파 버전으로 제공되었으며 1.4.30에서는 베타 버전으로 승격되었습니다.

Kotlin 1.5에서는 inline 클래스의 개념을 안정화하는 동시에 더 일반적인 기능인 value 클래스의 일부로 만듭니다. 이에 관해서는 이 글의 후반부에서 설명드리겠습니다.

먼저 inline 클래스의 작동 원리를 상기해보죠. inline 클래스에 익숙하시다면 이 섹션을 건너뛰고 새로운 변경 내용을 바로 보시면 됩니다.

간단히 환기해 드리면, inline 클래스는 다음과 같이 값을 감싼 래퍼를 제거합니다.

inline 클래스는 기본 유형(primitive type)이나 참조 유형(예: String)의 래퍼가 될 수 있습니다.

컴파일러는 가능한 경우 inline 클래스 인스턴스(이 예시에서는 Color 인스턴스)를 바이트코드의 기본 유형으로 바꿉니다.

내부적으로 컴파일러는 Int를 매개변수로 사용하여 맹글링된 이름을 가진 changeBackground 함수를 생성하고, 호출 사이트에서 래퍼를 만들지 않고 255 상수를 직접 전달합니다.

이름을 맹글링하면 다른 inline 클래스의 인스턴스를 사용하는 함수를 원활하게 오버로드하고, inline 클래스의 내부 제약 조건에 위반되는 Java 코드의 우발적 호출을 방지할 수 있습니다. 맹글링된 이름의 함수를 Java에서 사용할 수 있게 하는 방법은 아래를 참조하세요.

래퍼가 바이트코드에서 항상 제거되는 것은 아닙니다. 가능한 경우에만 제거되며 기본 제공되는 기본 유형과 매우 유사하게 작동합니다. Color 유형의 변수를 정의하거나 함수에 직접 전달하면 해당 변수가 기본 값으로 대체됩니다.

이 예시에서 color 변수는 컴파일 중에 Color 유형을 갖지만 바이트코드에서 Int로 대체됩니다.

그러나 이 변수를 컬렉션에 저장하거나 제네릭 함수에 전달하면 Color 유형의 일반 객체에 박싱됩니다.

박싱 및 박싱 해제는 컴파일러가 자동으로 수행합니다. 따라서 사용자가 신경 쓸 필요는 없으나 내부의 작동 원리를 이해하면 유용합니다.

Java 호출에 대한 JVM 이름 변경

1.4.30부터 Java에서 사용할 수 있도록 inline 클래스를 매개변수로 삼는 함수의 JVM 이름을 변경할 수 있습니다. 기본적으로 이러한 이름은 Java에서 우발적으로 사용되거나 오버로드가 충돌하는 것을 방지하기 위해 맹글링됩니다(위 예시의 changeBackground-euwHqFQ 참조).

@JvmName으로 함수에 어노테이션을 달면 바이트코드에서 이 함수의 이름을 변경하여 Java에서 호출하고 값을 직접 전달할 수 있습니다.

@JvmName 어노테이션이 달린 함수와 마찬가지로 Kotlin에서는 해당 함수를 Kotlin 이름으로 호출합니다. Timeout 유형의 값만 인수로 전달할 수 있고 단위를 사용 위치에서 분명히 알 수 있기 때문에 Kotlin 사용 위치에서는 유형이 안전합니다.

Java에서 long 값을 직접 전달할 수도 있습니다. 그러나 이렇게 하면 유형이 안전하지 않으므로 기본적으로 작동하지 않습니다. 코드에서 greetAfterTimeout(2)을 보면 2초인지, 2밀리초인지, 2년인지 바로 알기 어렵습니다.

어노테이션을 제공하면 이 함수를 Java에서 호출하도록 의도했다는 것을 명시적으로 강조할 수 있습니다. 설명적인 이름을 사용하면 혼동을 피할 수 있습니다. JVM 이름에 “Millis”라는 접미사를 추가하면 Java 사용자가 단위를 명확하게 알 수 있습니다.

Init 블록

1.4.30에서 inline 클래스에 적용한 또 다른 개선 사항은 init 블록에서 초기화 논리를 정의할 수 있다는 것입니다.

이것은 이전에 금지되었습니다.

inline 클래스에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

inline value 클래스

Kotlin 1.5에서는 inline 클래스의 개념을 안정화하는 동시에 더 일반적인 기능인 value 클래스의 일부로 만듭니다.

지금까지 ‘inline’ 클래스는 별도의 언어 기능이었으나 이제 하나의 매개변수가 있는 value 클래스의 특정 JVM 최적화 요소가 되었습니다. value 클래스는 더 일반적인 개념을 나타내며 향후 다양한 최적화 요소를 지원할 것입니다. 이번에는 inline 클래스를, 다음에 Valhalla 프로젝트를 사용할 수 있게 되면 Valhalla 기본 클래스를 지원할 예정입니다(자세한 내용은 아래 참조).

현재 유일하게 변경된 것은 구문입니다. inline 클래스가 최적화된 value 클래스이기 때문에 다음과 같이 예전과는 다른 방식으로 선언해야 합니다.

하나의 생성자 매개변수로 value 클래스를 정의하고 @JvmInline으로 어노테이션을 추가하세요. Kotlin 1.5부터 이 새로운 구문을 모두가 사용해 주시기를 바랍니다. 이전 구문인 inline 클래스는 한동안 계속 작동합니다. 그러나 1.5에서는 모든 선언을 자동으로 마이그레이션하는 옵션이 포함된 경고가 표시되면서 더 이상 지원되지 않습니다. 추후에는 오류가 표시되며 사용 중지됩니다.

value 클래스

value 클래스는 데이터가 있는 불변 엔티티를 나타냅니다. 현재 value 클래스는 ‘예전’ inline 클래스의 사용 사례를 지원하기 위해 단 하나의 프로퍼티만 포함할 수 있습니다.

향후 Kotlin 버전에서는 이 기능을 완벽하게 지원하여 여러 프로퍼티를 가진 value 클래스를 정의할 수 있습니다. 모든 값은 읽기 전용 val이어야 합니다.

value 클래스에는 동일성(identity)이 없습니다. 저장된 데이터로 완전히 정의되며 === 동일성 확인이 허용되지 않습니다. == 상등 검사는 기본 데이터를 자동으로 비교합니다.

이러한 ‘동일성 없는’ value 클래스의 특성은 향후 중요한 최적화의 기반입니다. 프로젝트 Valhalla 기능을 JVM에서 사용할 수 있게 되면 내부에서 value 클래스를 JVM 기본 클래스로 구현할 수 있습니다.

불변성 제약, 그리고 이에 따른 Valhalla 최적화의 가능성으로 value 클래스는 data 클래스와 차별화됩니다.

향후 Valhalla 최적화

프로젝트 Valhalla는 Java 및 JVM에 새로운 개념인 기본 클래스를 도입합니다.

기본 클래스의 주요 목표는 성능 기본 요소를 일반 JVM 클래스의 객체 지향 이점과 결합하는 것입니다. 기본 클래스는 인스턴스를 변수, 계산 스택에 저장하고 헤더와 포인터 없이 직접 작동할 수 있는 데이터 홀더입니다. 이러한 점에서 int, long 등의 기본 값과 유사합니다(Kotlin에서 사용자가 기본 유형으로 직접 작업할 일이 없지만 이 유형은 컴파일러 내부에서 생성됩니다).

기본 클래스의 주요 이점은 메모리에서 객체의 밀도 있는 플랫 레이아웃을 허용한다는 것입니다. 현재 Array<Point>는 참조 배열입니다. Valhalla가 지원되면 Point를 기본 클래스(Java 용어)로 또는 기본 최적화 기능이 있는 value 클래스(Kotlin 용어)로 정의할 때 JVM은 이를 최적화하고 ‘플랫’ 레이아웃으로 Point 배열을 저장할 수 있습니다(참조의 배열이 아니라 수많은 xy의 직접 배열).

저희는 이러한 예정된 JVM 변경 내용이 구현되기를 기대하고 있으며 Kotlin이 그 혜택을 받기를 바랍니다. 동시에 커뮤니티가 value 클래스를 사용하기 위해 새 JVM 버전을 강제로 사용하기를 바라지 않으므로 이전 JVM 버전에서도 이를 지원할 것입니다. Valhalla를 지원하는 JVM으로 코드를 컴파일할 때 최신 JVM 최적화 기능이 value 클래스에 대해 작동합니다.

mutating 메서드

value 클래스의 기능과 관련하여 얘기할 것이 아직 많이 남았습니다. value 클래스는 ‘불변’ 데이터를 나타내기 때문에 Swift에서와 마찬가지로 mutating 메서드를 적용할 수 있습니다. mutating 메서드는 멤버 함수나 프로퍼티 setter가 기존 인스턴스를 업데이트하지 않고 새 인스턴스를 반환하는 경우이며, 주요 이점은 익숙한 구문으로 다룰 수 있다는 것입니다. 이는 언어에서 여전히 프로토타이핑해야 합니다.

자세한 내용

@JvmInline 어노테이션은 JVM 전용입니다. 다른 백엔드에서는 value 클래스를 다르게 구현할 수 있습니다. 예를 들어 Kotlin/Native에서는 Swift 구조체로 구현됩니다.

value 클래스에 관한 자세한 내용을 알아보려면 Kotlin value 클래스에 관한 설계 노트를 참조하거나 Roman Elizarov의 “A look in the future” 강연의 발췌 내용을 시청하세요.

JVM 레코드 지원

JVM 에코시스템을 위해 마련된 또 다른 개선 사항은 Java 레코드입니다. Java 레코드는 Kotlin data 클래스와 유사하며 주로 단순한 데이터 홀더로 사용됩니다.

Java 레코드는 JavaBeans 규칙을 따르지 않으며 익숙한 ‘getX()’ 및 ‘getY()’ 대신 ‘x()’ 및 ‘y()’ 메서드를 사용합니다.

Java와의 상호운용성은 항상 Kotlin의 우선순위였으며 앞으로도 그럴 것입니다. Kotlin 코드는 새 Java 레코드를 ‘이해’하고 Kotlin 프로퍼티가 있는 클래스로 간주하며, JavaBeans 규칙을 따르는 일반 Java 클래스에서와 마찬가지로 작동합니다.

상호운용성을 주요 목적으로, data 클래스에 @JvmRecord를 어노테이션으로 추가하여 새 JVM 레코드 메서드를 생성할 수 있습니다.

@JvmRecord 어노테이션은 컴파일러가 표준 getX()getY() 메서드 대신 x() and y() 메서드를 생성하도록 합니다. 이 어노테이션은 Java에서 Kotlin으로 변환할 때 클래스의 API를 보존하기 위해서만 사용하면 될 것입니다. 다른 모든 사용 사례에서는 Kotlin의 익숙한 data 클래스를 문제 없이 대신 사용할 수 있습니다.

이 어노테이션은 Kotlin 코드를 JVM 버전 15 이상으로 컴파일하는 경우에만 사용할 수 있습니다. 이 기능에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

sealed 인터페이스 및 sealed 클래스 개선 사항

클래스를 sealed로 만들면 계층 구조를 정의된 하위 클래스로 제한하여 when 브랜치에서 엄격하게 검사할 수 있습니다. Kotlin 1.4에서 sealed 클래스 계층 구조에는 두 가지 제약 조건이 있습니다. 첫째, 최상위 클래스는 sealed 인터페이스가 될 수 없으며 클래스여야 합니다. 둘째, 모든 하위 클래스는 동일한 파일에 있어야 합니다.

Kotlin 1.5에서는 두 가지 제약 조건이 모두 제거되어 이제 인터페이스를 sealed로 만들 수 있습니다. 하위 클래스(sealed 클래스 및 sealed 인터페이스 모두)는 상위 클래스와 동일한 컴파일 유닛 및 동일한 패키지에 있어야 하지만 이제 다른 파일에 있어도 됩니다.

sealed 클래스 및 인터페이스는 추상 데이터 형식(ADT) 계층 구조를 정의하는 데 유용합니다.

sealed 인터페이스로 원활하게 처리할 수 있는 또 다른 주요 사용 사례는 라이브러리 외부에서 상속과 구현을 위해 인터페이스를 닫을 때입니다. 인터페이스를 sealed로 정의하면 해당 구현이 동일한 컴파일 유닛 및 패키지로 제한되므로 라이브러리의 경우 라이브러리 외부에서 구현할 수 없습니다.

예를 들어, kotlinx.coroutines 패키지의 Job 인터페이스는 kotlinx.coroutines 라이브러리 내에서만 구현하도록 의도되어 있습니다. 이 의도는 해당 인터페이스를 sealed로 만들면 명확해집니다.

라이브러리 사용자는 더 이상 자신의 고유한 Job 하위 클래스를 정의할 수 없습니다. 이는 항상 ‘묵시적’으로 제한되어 있었지만 sealed 인터페이스를 사용하면 컴파일러가 이를 공식적으로 금지할 수 있습니다.

향후 JVM 지원 사용

sealed 클래스의 테스트 버전 지원은 Java 15 및 JVM에 도입되었습니다. 앞으로 사용자가 Kotlin 코드를 최신 JVM으로 컴파일하는 경우(이 기능이 안정화되면 JVM 17 이상일 가능성이 높음) sealed 클래스에 대한 안정된 JVM 지원을 제공할 계획입니다.

Java에서는 다음과 같이 지정된 sealed 클래스 또는 인터페이스의 모든 하위 클래스를 명시적으로 나열합니다.

이 정보는 새로운 PermittedSubclasses 속성을 사용하여 클래스 파일에 저장됩니다. JVM은 런타임 시 sealed 클래스를 인식하고 이 클래스가 승인되지 않은 하위 클래스에 의해 확장되지 않도록 합니다.

향후 Kotlin을 최신 JVM으로 컴파일할 때 sealed 클래스에 대한 이 새로운 JVM 지원이 사용됩니다. 내부적으로 컴파일러는 JVM 지원 및 추가 런타임 검사가 있는지 확인하기 위해 바이트코드에서 허용된 하위 클래스 목록을 생성합니다.

Kotlin에서는 하위 클래스 목록을 지정할 필요가 없습니다. 컴파일러가 동일한 패키지에 선언된 하위 클래스를 기반으로 목록을 생성합니다.

상위 클래스 또는 인터페이스의 하위 클래스를 명시적으로 지정하는 기능은 나중에 선택적 사양으로 추가될 수 있습니다. 지금은 필요하지 않다는 판단이지만 여러분의 사용 사례와 이 기능의 필요성에 대해 의견을 주시면 기꺼이 경청하겠습니다.

이전 JVM 버전의 경우 이론적으로는 Java 하위 클래스를 Kotlin sealed 인터페이스에 정의할 수 있지만 그렇게 하지 않는 것이 좋습니다. 허용된 하위 클래스에 대한 JVM 지원은 아직 제공되지 않기 때문에 이 제약 조건은 Kotlin 컴파일러에서만 적용됩니다. 실수로 이 작업이 수행되는 일을 방지하기 위해 IDE에 경고도 추가할 예정입니다. 향후 Java에서 ‘승인되지 않은’ 하위 클래스가 없는지 확인하기 위해 최신 JVM 버전에 새 메커니즘이 사용됩니다.

sealed 인터페이스 및 완화된 sealed 클래스 제한에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

새 기능 사용 방법

이 기능을 사용하려면 Kotlin 1.4.30을 사용해야 합니다. 새 기능을 활성화하려면 다음과 같이 언어 버전을 1.5로 지정하세요.

JVM 레코드를 사용하려면 추가로 jvmTarget 15를 사용하고 JVM 테스트 버전 기능을 활성화해야 합니다. -language-version 1.5-Xjvm-enable-preview 컴파일러 옵션을 추가하세요.

사전 릴리스 노트

새로운 기능에 대한 지원은 실험적이며 1.5 언어 버전 지원은 사전 릴리스 상태입니다. Kotlin 1.4.30 컴파일러에서 언어 버전을 1.5로 설정하면 1.5-M0 테스트 버전을 사용하게 됩니다. 이전 버전과의 호환성은 사전 릴리스 버전에는 적용되지 않습니다. 기능 및 API는 후속 릴리스에서 변경될 수 있습니다. 최종 Kotlin 1.5-RC에 도달하면 사전 릴리스 버전에서 생성된 모든 바이너리가 컴파일러에서 사용 금지되며 1.5-Mx로 컴파일된 모든 항목을 다시 컴파일해야 합니다.

의견을 공유해 주세요

이 글에서 설명한 새로운 기능을 사용해보고 의견을 공유해주세요. KEEP에서 자세한 내용을 확인하고 YouTrack에서 논의에 참여할 수 있으며 문제가 발생하면 새로 보고할 수 있습니다. 새로운 기능이 프로젝트의 사용 사례를 얼마나 잘 처리하는지 여러분의 결과를 공유해 주세요.

추가 읽을거리 및 논의:

이 게시물은 Svetlana Isakova가 작성한 New Language Features Preview in Kotlin 1.4.30를 번역한 글입니다.

Kotlin

Kotlin 1.4.30: новые возможности языка

Согласно новому графику релизов Kotlin 1.5 выйдет только через несколько месяцев, но попробовать новые возможности языка можно уже сейчас — в версии 1.4.30:

Чтобы попробовать новые функции, нужно указать версию языка 1.5.

Нам важно ваше мнение, поэтому мы будем рады получить от вас фидбек о новом синтаксисе.

Стабилизация inline-классов значений

В альфа-режиме inline-классы были представлены еще в Kotlin 1.3, а в версии 1.4.30 они перешагнули на стадию беты.

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

Для начала вспомним, как работают inline-классы. Если вы с ними уже знакомы, смело переходите к следующему разделу.

Напоминаем, что inline-класс ликвидирует обертку вокруг значения:

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

Компилятор заменяет экземпляры inline-класса (в нашем примере экземпляр Color) соответствующим типом (Int) в байт-коде, если это возможно:

При этом компилятор генерирует функцию changeBackground с искаженным именем, принимающую Int в качестве параметра, и передает константу 255 напрямую, без создания обертки в месте вызова:

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

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

В этом примере переменная color во время компиляции имеет тип Color, а в байт-коде заменена на Int.

Однако если вы сохраните ее в коллекции или передадите в обобщенную функцию, она будет упакована в обычный объект типа Color:

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

Изменение JVM-имен для вызовов из Java

Начиная с версии 1.4.30, вы можете изменить JVM-имя функции, принимающей inline-класс в качестве параметра, чтобы ее можно было вызывать из Java. По умолчанию такие имена иcкажаются, чтобы предотвратить случайное использование Java и конфликтующие перегрузки (например, changeBackground-euwHqFQ в примере выше).

Аннотация @JvmName изменяет имя функции в байт-коде и позволяет вызывать ее из Java и напрямую передать значение:

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

Из Java можно напрямую передать значение long. Это уже не будет типобезопасно и поэтому не работает по умолчанию. Если вы видите в коде greetAfterTimeout(2), не сразу понятно, что имеется в виду: 2 секунды, 2 миллисекунды или 2 года.

Добавляя аннотацию, вы подчеркиваете, что собираетесь вызывать эту функцию из Java. Смысловое имя помогает избежать путаницы: добавление суффикса «Millis» делает единицы понятными для пользователей Java.

Блоки init

Еще одно улучшение inline-классов в 1.4.30 заключается в том, что теперь можно задавать логику инициализации в блоке init:

Раньше так было делать нельзя.

Подробнее об inline-классах читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.

Inline-классы значений

В Kotlin 1.5 inline-классы станут стабильными и будут относиться к более широкому понятию — классам значений.

До сих пор inline-классы были отдельной языковой конструкцией, а теперь представляют собой специальную JVM-оптимизацию класса значений с одним параметром. Классы значений — более широкий концепт, и в дальнейшем они будут поддерживать различные оптимизации. Сейчас они поддерживают inline-классы, а впоследствии будут поддерживать примитивные классы Valhalla, когда проект Valhalla будет завершен (подробности далее).

Единственное, что меняется сейчас, — это синтаксис. Поскольку inline-класс является оптимизированным классом значений, его следует объявлять по-новому:

Вы определяете класс значений с одним параметром конструктора и добавляете аннотацию @JvmInline. Мы ожидаем, что с выходом Kotlin 1.5 все будут использовать новый синтаксис. Старый синтаксис inline class будет работать еще некоторое время, но будет считаться устаревшим. В версии 1.5 появится предупреждение с предложением миграции. А впоследствии старый синтаксис будет считаться ошибочным.

Классы значений

Класс value представляет собой неизменяемую сущность с данными. На данный момент такой класс может содержать только одно свойство для поддержки использования «старых» inline-классов.

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

Классы значений не имеют идентичности: они полностью определяются хранящимися данными, и проверки === для них не разрешены. Проверка на равенство == автоматически сравнивает содержащиеся данные.

Такое отсутствие идентичности дает много возможностей для дальнейших оптимизаций: проекта Valhalla позволит реализовывать классы значений как примитивные JVM-классы.

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

Оптимизации из проекта Valhalla

Проект Valhalla предлагает новый концепт для Java и JVM — примитивные классы.

Основная цель примитивных классов — добавить к производительности примитивов объектно-ориентированные преимущества обычных JVM-классов. Примитивные классы — это носители данных, экземпляры которых могут храниться в переменных и в стеке вычислений. Работать с ними можно напрямую, без заголовков и указателей. В этом отношении они похожи на примитивные значения, такие как int, long и т. д. (в Kotlin вы не работаете с примитивными типами напрямую, но компилятор генерирует их под капотом).

Важным преимуществом примитивных классов является то, что они позволяют плоскую и плотную компоновку объектов в памяти. Сейчас Array<Point> представляет собой массив ссылок. Проект Valhalla подразумевает, что при определении Point как примитивного класса (в терминологии Java) или как класса значений с соответствующей оптимизацией (в терминологии Kotlin) JVM может оптимизировать его и хранить массив Point в «плоской» компоновке, как массив из множества x и y, а не как массив ссылок.

Мы с нетерпением ждем предстоящих изменений в JVM и хотим, чтобы Kotlin извлек из них пользу. В то же время мы не хотели бы заставлять вас использовать только новые версии JVM для работы с value classes, поэтому собираемся поддерживать и более ранние версий JVM. При компиляции кода в JVM с поддержкой Valhalla последние JVM-оптимизации будут работать для классов значений.

Изменяющие методы

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

Где читать подробнее

Аннотация @JvmInline предназначена для JVM. На других бэкендах классы значений могут быть реализованы иначе, например как структуры Swift в Kotlin/Native.

Читайте подробнее о классах значений в этом документе или смотрите отрывок из выступления Романа Елизарова.

Поддержка JVM-записей

Еще одно предстоящее улучшение экосистемы JVM — это Java records (записи). Они аналогичны data-классам в Kotlin и в основном используются как простые хранилища данных.

Записи Java не придерживаются соглашения JavaBeans, и в них используются методы «x()» и «y()» вместо привычных «getX()» и «getY()».

Совместимость с Java всегда была для нас в приоритете. Kotlin-код «понимает» новый синтаксис и рассматривает записи как классы со свойствами Kotlin. Работает так же, как и для обычных Java-классов, соответствующих JavaBeans:

В основном из-за совместимости нельзя аннотировать класс data с помощью @JvmRecord, чтобы сгенерировать новые методы JVM-записи:

Аннотация @JvmRecord заставляет компилятор генерировать методы x() и y() вместо стандартных getX() и getY(). Предполагаем, что эта аннотация нужна вам только для сохранения API класса при его преобразовании из Java в Kotlin. Во всех остальных случаях можно без проблем использовать привычные data-классы Kotlin.

Аннотация доступна, только если вы компилируете Kotlin-код под JVM версии 15+. Подробнее об этой функции читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.

Sealed-интерфейсы и улучшения sealed-классов

Когда класс изолирован (объявлен как sealed), иерархия ограничивается определенными подклассами — это позволяет проверить, что ветви when охватывают все случаи. В Kotlin 1.4 иерархия sealed-классов имеет два ограничения. Во-первых, вышестоящий класс не может быть sealed-интерфейсом — он должен быть классом. Во-вторых, все подклассы должны находиться в одном файле.

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

Sealed-классы, а теперь и интерфейсы, полезны при создании иерархий абстрактных типов данных (ADT).

Еще один важный вариант использования sealed-интерфейсов — это ограничение наследования и реализации за пределами библиотеки. Интерфейс, определенный как sealed, может быть реализован только в той же единице компиляции и в том же пакете, что делает невозможным реализацию вне библиотеки.

Например, подразумевается, что интерфейс Job из пакета kotlinx.coroutines может быть реализован только внутри библиотеки kotlinx.coroutines. Если сделать интерфейс изолированным, это будет выражено явно:

Как пользователю библиотеки вам больше нельзя определять собственный подкласс Job. Это всегда «подразумевалось», но с sealed-интерфейсами может быть формально запрещено компилятором.

Использование JVM-поддержки в будущем

Предварительная поддержка sealed-классов была представлена ​​в Java 15 и JVM. В дальнейшем мы собираемся использовать стандартную JVM-поддержку sealed-классов при компиляции Kotlin-кода под JVM последней версии (скорее всего, это будет JVM 17 или более поздняя версия — когда эта функция станет стабильной).

В Java вы явно перечисляете все подклассы данного изолированного класса или интерфейса:

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

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

А в Kotlin список подклассов указывать не нужно. Компилятор сгенерирует список на основе объявленных подклассов в том же пакете.

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

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

Подробнее об изолированных интерфейсах и ослабленных ограничениях для изолированных классов читайте в соответствующем разделе KEEP, в документации и в комментариях к этой задаче.

Как все это попробовать

Чтобы попробовать новые возможности, используйте Kotlin 1.4.30 и укажите версию языка 1.5:

Чтобы попробовать JVM-записи, нужно также использовать jvmTarget 15 и включить JVM-превью: добавьте параметры компилятора -language-version 1.5 и -Xjvm-enable-preview.

Примечания к предрелизной версии

Обратите внимание, что поддержка новых функций является экспериментальной, а поддержка версии 1.5 считается предварительной. Выбрав версию 1.5 в компиляторе Kotlin 1.4.30, вы будете как будто использовать превью-версию 1.5-M0. Гарантии обратной совместимости не распространяются на предварительные версии. Последующие релизы могут вносить изменения в функциональность и API. После выхода версии Kotlin 1.5-RC все бинарные файлы, созданные предрелизными версиями, будут запрещены в компиляторе: вам потребуется перекомпилировать все, что было скомпилировано в версиях 1.5-Mx.

Что думаете?

Пробуйте новые функции и делитесь своими впечатлениями! Подробнее о нововведениях читайте в соответствующих разделах KEEP и присоединяйтесь к обсуждениям в YouTrack. А если у вас что-то не работает, обязательно сообщите нам. Мы будем ждать отзывов о том, как новые возможности языка реализуются в ваших проектах.

Что еще почитать:

Ваша команда Kotlin
The Drive to Develop

News

New Language Features Preview in Kotlin 1.4.30

We’re planning to add new language features in Kotlin 1.5, and you can already try them out in Kotlin 1.4.30:

To try these new features, you need to specify the 1.5 language version.

The new release cadence means that Kotlin 1.5 is going to be released in a few months, but new features are already available for preview in 1.4.30. Your early feedback is crucial to us, so please give these new features a try now!

Stabilization of inline value classes

Inline classes have been available in Alpha since Kotlin 1.3, and in 1.4.30 they are promoted to Beta.

Kotlin 1.5 stabilizes the concept of inline classes but makes it a part of a more general feature, value classes, which we will describe later in this post.

We’ll begin with a refresher on how inline classes work. If you are already familiar with inline classes, you can skip this section and go directly to the new changes.

As a quick reminder, an inline class eliminates a wrapper around a value:

An inline class can be a wrapper both for a primitive type and for any reference type, like String.

The compiler replaces inline class instances (in our example, the Color instance) with the underlying type (Int) in the bytecode, when possible:

Under the hood, the compiler generates the changeBackground function with a mangled name taking Int as a parameter, and it passes the 255 constant directly without creating a wrapper at the call site:

The name is mangled to allow the seamless overload of functions taking instances of different inline classes and to prevent accidental invocations from the Java code that could violate the internal constraints of an inline class. Read below to find out how to make it usable from Java.

The wrapper is not always eliminated in the bytecode. This happens only when possible, and it works very similarly to built-in primitive types. When you define a variable of the Color type or pass it directly into a function, it gets replaced with the underlying value:

In this example, the color variable has the type Color during compilation, but it’s replaced with Int in the bytecode.

If you store it in a collection or pass it to a generic function, however, it gets boxed into a regular object of the Color type:

Boxing and unboxing is done automatically by the compiler. You don’t need to do anything about it, but it’s useful to understand the internals.

Changing JVM name for Java calls

Starting from 1.4.30, you can change the JVM name of a function taking an inline class as a parameter to make it usable from Java. By default, such names are mangled to prevent accidental usages from Java or conflicting overloads (like changeBackground-euwHqFQ in the example above).

If you annotate a function with @JvmName, it changes the name of this function in the bytecode and makes it possible to call it from Java and pass a value directly:

As always with a function annotated with @JvmName, from Kotlin you call it by its Kotlin name. Kotlin usage is type-safe, since you can only pass a value of the Timeout type as an argument, and the units are obvious from the usage.

From Java you can pass a long value directly. It’s no longer type-safe, and that’s why it doesn’t work by default. If you see greetAfterTimeout(2) in the code, it’s not immediately obvious whether it’s 2 seconds, 2 milliseconds, or 2 years.

By providing the annotation you explicitly emphasize that you intend this function to be called from Java. A descriptive name helps avoid confusion: adding the “Millis” suffix to the JVM name makes the units clear for Java users.

Init blocks

Another improvement for inline classes in 1.4.30 is that you now can define initialization logic in the init block:

This was previously forbidden.

You can read more details about inline classes in the corresponding KEEP, in the documentation, and in the discussion under this issue.

Inline value classes

Kotlin 1.5 stabilizes the concept of inline classes and makes it a part of a more general feature: value classes.

Until now, “inline” classes constituted a separate language feature, but they are now becoming a specific JVM optimization for a value class with one parameter. Value classes represent a more general concept and will support different optimizations: inline classes now, and Valhalla primitive classes in the future when project Valhalla becomes available (more about this below).

The only thing that changes for you at the moment is syntax. Since an inline class is an optimized value class, you have to declare it differently than you used to:

You define a value class with one constructor parameter and annotate it with @JvmInline. We expect everyone to use this new syntax starting from Kotlin 1.5. The old syntax inline class will continue to work for some time. It will be deprecated with a warning in 1.5 that will include an option to migrate all your declarations automatically. It will later be deprecated with an error.

Value classes

A value class represents an immutable entity with data. At the moment, a value class can contain only one property to support the use-case of “old” inline classes.

In future Kotlin versions with full support for this feature, it will be possible to define value classes with many properties. All the values should be read-only vals:

Value classes have no identity: they are completely defined by the data stored and === identity checks aren’t allowed for them. The == equality check automatically compares the underlying data.

This “identityless” quality of value classes allows significant future optimizations: project Valhalla’s arrival to the JVM will allow value classes to be implemented as JVM primitive classes under the hood.

The immutability constraint, and therefore the possibility of Valhalla optimizations, makes value classes different from data classes.

Future Valhalla optimization

Project Valhalla introduces a new concept to Java and the JVM: primitive classes.

The main goal of primitive classes is to combine performant primitives with the object-oriented benefits of regular JVM classes. Primitive classes are data holders whose instances can be stored in variables, on the computation stack, and operated on directly, without headers and pointers. In this regard, they are similar to primitive values like int, long, etc. (in Kotlin, you don’t work with primitive types directly but the compiler generates them under the hood).

An important advantage of primitive classes is that they allow the flat and dense layout of objects in memory. Currently, Array<Point> is an array of references. With Valhalla support, when defining Point as a primitive class (in Java terminology) or as a value class with the underlying optimization (in Kotlin terminology), the JVM can optimize it and store an array of Points in a “flat” layout, as an array of many xs and ys directly, not as an array of references.

We’re really looking forward to the upcoming JVM changes and we want Kotlin to benefit from them. At the same time, we don’t want to force our community to depend on new JVM versions to use value classes, and so we are going to support it for earlier JVM versions as well. When compiling the code to the JVM with Valhalla support, the latest JVM optimizations will work for value classes.

Mutating methods

There’s much more to say regarding the functionality of value classes. Since value classes represent “immutable” data, mutating methods, like those in Swift, are possible for them. A mutating method is when a member function or property setter returns a new instance rather than updating an existing one, and the main benefit is that you use them with a familiar syntax. This still needs to be prototyped in the language.

More details

@JvmInline annotation is JVM-specific. On other backends value classes can be implemented differently. For instance, as Swift structs in Kotlin/Native.

You can read the details about value classes in the Design Note for Kotlin value classes, or watch an extract from Roman Elizarov’s “A look into the future” talk.

Support for JVM records

Another upcoming improvement in the JVM ecosystem is Java records. They are analogous to Kotlin data classes and are mainly simple holders of data.

Java records don’t follow the JavaBeans convention, and they have x() and y() methods instead of the familiar getX() and getY().

Interoperability with Java always was and remains a priority for Kotlin. Thus, Kotlin code “understands” new Java records and sees them as classes with Kotlin properties. This works like it does for regular Java classes following the JavaBeans convention:

Mainly for interoperability reasons, you can annotate your data class with @JvmRecord to have new JVM record methods generated:

The @JvmRecord annotation makes the compiler generate x() and y() methods instead of the standard getX() and getY() methods. We assume that you only need to use this annotation to preserve the API of the class when converting it from Java to Kotlin. In all the other use cases, Kotlin’s familiar data classes can be used instead without issue.

This annotation is only available if you compile Kotlin code to version 15+ of the JVM version. You can read more about this feature in the corresponding KEEP or in the documentation, as well as in the discussion in this issue.

Sealed interfaces and sealed classes improvements

When you make a class sealed, it restricts the hierarchy to defined subclasses, which allows exhaustive checks in when branches. In Kotlin 1.4, the sealed class hierarchy comes with two constraints. First, the top class can’t be a sealed interface, it should be a class. Second, all the subclasses should be located in the same file.

Kotlin 1.5 removes both constraints: you can now make an interface sealed. The subclasses (both to sealed classes and sealed interfaces) should be located in the same compilation unit and in the same package as the super class, but they can now be located in different files.

Sealed classes, and now interfaces, are useful for defining algebraic data type (ADT) hierarchies.

Another important use case that can now be nicely addressed with sealed interfaces is closing an interface for inheritance and implementation outside the library. Defining an interface as sealed restricts its implementation to the same compilation unit and the same package, which in the case of a library, makes it impossible to implement outside of the library.

For example, the Job interface from the kotlinx.coroutines package is only intended to be implemented inside the kotlinx.coroutines library. Making it sealed makes this intention explicit:

As a user of the library, you are no longer allowed to define your own subclass of Job. This was always “implied”, but with sealed interfaces, the compiler can formally forbid that.

Using JVM support in the future

Preview support for sealed classes has been introduced in Java 15 and on the JVM. In the future, we’re going to use the natural JVM support for sealed classes if you compile Kotlin code to the latest JVM (most likely JVM 17 or later when this feature becomes stable).

In Java, you explicitly list all the subclasses of the given sealed class or interface:

This information is stored in the class file using the new PermittedSubclasses attribute. The JVM recognizes sealed classes at runtime and prevents their extension by unauthorized subclasses.

In the future, when you compile Kotlin to the latest JVM, this new JVM support for sealed classes will be used. Under the hood, the compiler will generate a permitted subclasses list in the bytecode to ensure there is JVM support and additional runtime checks.

In Kotlin, you don’t need to specify the subclasses list! The compiler will generate the list based on the declared subclasses in the same package.

The ability to explicitly specify the subclasses for a super class or interface might be added later as an optional specification. At the moment we suspect it won’t be necessary, but we’ll be happy to hear about your use cases, and whether you need this functionality!

Note that for older JVM versions it’s theoretically possible to define a Java subclass to the Kotlin sealed interface, but don’t do it! Since JVM support for permitted subclasses is not yet available, this constraint is enforced only by the Kotlin compiler. We’ll add IDE warnings to prevent doing this accidentally. In the future, the new mechanism will be used for the latest JVM versions to ensure there are no “unauthorized” subclasses from Java.

You can read more about sealed interfaces and the loosened sealed classes restrictions in the corresponding KEEP or in the documentation, and see discussion in this issue.

How to try the new features

You need to use Kotlin 1.4.30. Specify language version 1.5 to enable the new features:

To try JVM records, you additionally need to use jvmTarget 15 and enable JVM preview features: add the compiler options -language-version 1.5 and -Xjvm-enable-preview.

Pre-release notes

Note that support for the new features is experimental and the 1.5 language version support is in the pre-release status. Setting the language version to 1.5 in the Kotlin 1.4.30 compiler is equivalent to trying the 1.5-M0 preview. The backward compatibility guarantees do not cover pre-release versions. The features and the API may change in subsequent releases. When we reach a final Kotlin 1.5-RC, all binaries produced by pre-release versions will be outlawed by the compiler, and you will be required to recompile everything that was compiled by 1.5‑Mx.

Share your feedback

Please try the new features described in this post and share your feedback! You can find further details in KEEPs and take part in the discussions in YouTrack, and you can report new issues if something doesn’t work for you. Please share your findings regarding how well the new features address use cases in your projects!

Further reading and discussion:

Discover more