1.4버전까지는 책 Atomic Kotlin으로 커버했으므로 1.5 변경사항부터 유용해보이는걸 정리해보자.

TODO: 1.5.2부터 추가

# What’s new in Kotlin 1.9.20

java-test-fixtures 플러그인으로 생성한 testFixtures sourceSet에서 main의 internal 클래스에 접근할 수 있다

https://kotlinlang.org/docs/whatsnew1920.html#support-for-test-fixtures-to-access-internal-declarations

enumValues() 대신 enumEntries() (experimental)

enum을 generic으로 받아서 사용할 때 쓴다. 역시 배열이 아니라 List를 사용하기 때문에 성능 이슈가 없다고 한다.

https://kotlinlang.org/docs/whatsnew1920.html#replacement-of-the-enum-class-values-generic-function

enum class RGB { RED, GREEN, BLUE }
 
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T : Enum<T>> printAllValues() {
    print(enumEntries<T>().joinToString { it.name })
}
 
printAllValues<RGB>()
// RED, GREEN, BLUE

What’s new in Kotlin 1.9.0

enumvalues() 대신 entries property 사용

https://kotlinlang.org/docs/whatsnew19.html#stable-replacement-of-the-enum-class-values-function

Java의 values()는 배열을 return하고, 배열을 쓰면 성능 문제가 생길 수 있다고 함.

대신 entries 프로퍼티를 쓰면 된다. stable

enum class Color(val colorName: String, val rgb: String) {
    RED("Red", "#FF0000"),
    ORANGE("Orange", "#FF7F00"),
    YELLOW("Yellow", "#FFFF00")
}
 
fun findByRgb(rgb: String): Color? = Color.entries.find { it.rgb == rgb }

data object가 stable

https://kotlinlang.org/docs/whatsnew19.html#stable-data-objects-for-symmetry-with-data-classes

sealed와 함께 사용하면 좋음.

sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult
 
fun main() {
    println(Number(7)) // Number(number=7)
    println(EndOfFile) // EndOfFile
}

inline value class의 생성자에 body를 쓸 수 있다 (stable)

1.8.20에서 추가한 기능. 이제 stable이고 기본으로 사용할 수 있음

@JvmInline
value class Person(private val fullName: String) {
    // Allowed since Kotlin 1.4.30:
    init {
        check(fullName.isNotBlank()) {
            "Full name shouldn't be empty"
        }
    }
    // Allowed by default since Kotlin 1.9.0:
    constructor(name: String, lastName: String) : this("$name $lastName") {
        check(lastName.isNotBlank()) {
            "Last name shouldn't be empty"
        }
    }
}

..< 열린 구간 operator가 stable

What’s new in Kotlin 1.8.20

inline class의 생성자에 body 추가 (experimental)

이전까지는 생성자만 사용할 수 있고 생성자 내부에서 어떤 일을 할 수 없었다. 이제 primary constructor를 위한 init {}과 secondary constructor에 body를 사용할 수 있다.

@JvmInline
value class Person(private val fullName: String) {
// Allowed since Kotlin 1.4.30:
    init {
        check(fullName.isNotBlank()) {
            "Full name shouldn't be empty"
        }
    }
// Preview available since Kotlin 1.8.20:
    constructor(name: String, lastName: String) : this("$name $lastName") {
        check(lastName.isNotBlank()) {
            "Last name shouldn't be empty"
        }
    }
}

kapt를 사용해도 IR Backend를 사용할 수 있는 옵션이 기본으로 적용

kapt.use.jvm.ir=true 없어도 됨

Base64 인코딩 지원 (experimental)

https://kotlinlang.org/docs/whatsnew1820.html#support-for-base64-encoding

What’s new in Kotlin 1.8.0

K2 컴파일러 작업하느라 언어 변경사항이 많지 않은듯.

Java Optional 객체에 추가한 확장함수가 stable

1.7.0에 experimental이었던 기능

What’s new in Kotlin 1.7.20

열린 구간(open-ended) operator ..< 추가 (experimental)

when (value) {
    in 0.0..<0.25 -> // First quarter
    in 0.25..<0.5 -> // Second quarter
    in 0.5..<0.75 -> // Third quarter
    in 0.75..1.0 ->  // Last quarter  <- Note closed range here
}

inline class에 generic 추가 (experimental)

@JvmInline
value class UserId<T>(val value: T)
 
fun compute(s: UserId<String>) {} // Compiler generates fun compute-<hashcode>(s: Any?)

kapt도 IR Backend를 사용할 수 있다 (experimental)

코틀린 컴파일러 백엔드

gradle.properties

kapt.use.jvm.ir=true

What’s new in Kotlin 1.7.0

Kotlin K2 컴파일러 alpha버전 릴리즈

https://kotlinlang.org/docs/whatsnew17.html#new-kotlin-k2-compiler-for-the-jvm-in-alpha

기존 컴파일러보다 2배 이상 빠르네

-Xuse-k2

inline class에 delegation을 쓸 수 있다

interface Bar {
    fun foo() = "foo"
}
 
@JvmInline
value class BarWrapper(val bar: Bar): Bar by bar
 
fun main() {
    val bw = BarWrapper(object: Bar {})
    println(bw.foo())
}

타입 파라미터에 _ 로 추론을 할 수 있다

abstract class SomeClass<T> {
    abstract fun execute(): T
}
 
class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}
 
class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}
 
object Runner {
    inline fun <reified S: SomeClass<T>, T> run(): T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}
 
fun main() {
    // T is inferred as String because SomeImplementation derives from SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")
 
    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

Definitely non-nullable types (stable)

generic 타입 파라미터에 nullable 타입을 넣어도 매개변수에 null값이 들어오지 않도록 강제할 수 있다. T & Any

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
 
fun main() {
    // OK
    elvisLike<String>("", "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String>("", null).length
 
    // OK
    elvisLike<String?>(null, "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String?>(null, null).length
}

DeepRecursiveFunction이 stable

1.4.0에 추가된 DeepRecursiveFunction이 stable이 되었다.

DeepRecursiveFunction 을 사용하는 재귀함수는 stack을 heap에 쌓기 때문에 깊게 재귀를 들어가도 stack overflow를 피할 수 있다.

class Tree(val left: Tree?, val right: Tree?)
 
val calculateDepth = DeepRecursiveFunction<Tree?, Int> { t ->
    if (t == null) 0 else maxOf(
        callRecursive(t.left),
        callRecursive(t.right)
    ) + 1
}
 
fun main() {
    // Generate a tree with a depth of 100_000
    val deepTree = generateSequence(Tree(null, null)) { prev ->
        Tree(prev, null)
    }.take(100_000).last()
 
    println(calculateDepth(deepTree)) // 100000
}

위 예제에서는 10만번을 들어가지만 stack overflow가 발생하지 않는다.

1000이상의 깊이에서는 DeepRecursiveFunction을 사용하는걸 추천.

Java의 Optional 타입에 대한 확장함수 추가 (experimental)

https://kotlinlang.org/docs/whatsnew17.html#new-experimental-extension-functions-for-java-optionals

  • getOrNull
  • getOrDefault
  • getOrElse
  • toList
  • toSet
  • asSequence
  • toCollection

정규표현식에서 group에 이름 짓기

이런게 됐었구나.. JS와 Native에서도 된다고 함.

fun main() {
    val regex = "\\b(?<city>[A-Za-z\\s]+),\\s(?<state>[A-Z]{2}):\\s(?<areaCode>[0-9]{3})\\b".toRegex()
    val input = "Coordinates: Austin, TX: 123"
    val match = regex.find(input)!!
    println(match.groups["city"]?.value) // "Austin" — by name
    println(match.groups[2]?.value) // "TX" — by number
}

backrefernce. 이전에 찾은 값과 동일해야만 매칭이 된다.

fun backRef() {
    val regex = "(?<title>\\w+), yes \\k<title>".toRegex()
    val match = regex.find("Do you copy? Sir, yes Sir!")!!
    println(match.value) // "Sir, yes Sir"
    println(match.groups["title"]?.value) // "Sir"
}

replace에서도 named group을 사용할 수 있음

fun dateReplace() {
    val dateRegex = Regex("(?<dd>\\d{2})-(?<mm>\\d{2})-(?<yyyy>\\d{4})")
    val input = "Date of birth: 27-04-2022"
    println(dateRegex.replace(input, "\${yyyy}-\${mm}-\${dd}")) // "Date of birth: 2022-04-27" — by name
    println(dateRegex.replace(input, "\$3-\$2-\$1")) // "Date of birth: 2022-04-27" — by number
}

자바의 SAM 메서드의 첫 번째 매개변수를 코틀린에서 this로 받을 수 있는 gradle plugin

https://kotlinlang.org/docs/whatsnew17.html#the-sam-with-receiver-plugin-is-available-via-the-plugins-api

public @interface SamWithReceiver {}
 
@SamWithReceiver
public interface TaskRunner {
    void run(Task task);
}
fun test(context: TaskContext) {
    val runner = TaskRunner {
        // Here 'this' is an instance of 'Task'
 
        println("$name is started")
        context.executeTask(this)
        println("$name is finished")
    }
}

What’s new in Kotlin 1.6.20

context receiver prototype 추가

React의 Context API와 유사하네. 아직 prototype이라 production code에서 사용할 수 없음.

interface LoggingContext {
  val log: Logger // This context provides a reference to a logger
}
 
context(LoggingContext)
fun startBusinessOperation() {
  // You can access the log property since LoggingContext is an implicit receiver
  log.info("Operation has started")
}
 
fun test(loggingContext: LoggingContext) {
  with(loggingContext) {
    // You need to have LoggingContext in a scope as an implicit receiver
    // to call startBusinessOperation()
    startBusinessOperation()
  }
}

Definitely non-nullable types (beta)

generic 타입 파라미터에 nullable 타입을 넣어도 매개변수에 null값이 들어오지 않도록 강제할 수 있다. T & Any

아직 beta

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
 
fun main() {
    // OK
    elvisLike<String>("", "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String>("", null).length
 
    // OK
    elvisLike<String?>(null, "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String?>(null, null).length
}

코틀린 인터페이스에 선언한 non-abstract property를 Java에서 default method로 사용할 수 있게 하는 방법 추가

원래는 컴파일러 옵션에 -Xjvm-default=all-compatibility를 추가하고 인터페이스에 @JvmDefaultWithoutCompatibility를 붙여야만 했었다. 이렇게 하면 새로 추가되는 모든 인터페이스에 @JvmDefaultWithoutCompatibility를 붙여줘야 해서 까먹기 쉬웠음.

1.6.20부터는 컴파일러 옵션에 -Xjvm-default=all 을 추가해서 모든 interface에 @JvmDefaultWithoutCompatibility를 붙일 수 있음. 컴파일러 옵션만 추가하면 non-abstract property를 Java에서 default method로 사용할 수 있다.

하나의 모듈에 대한 병렬 컴파일 옵션 추가

https://kotlinlang.org/docs/whatsnew1620.html#support-for-parallel-compilation-of-a-single-module-in-the-jvm-backend

-Xbackend-threads

거대한 single module에 대해서 병렬 컴파일을 하면 속도가 최대 15% 상승할 수 있음.

이미 gradle module로 여러 개의 모듈을 쪼개놓은 상태라면 gradle에서 모듈별로 알아서 병렬로 컴파일하므로 이 옵션을 켜면 오히려 속도가 느려질 수 있다, thread의 context switch때문에.

kapt를 사용하고 있으면 적용 못함.

annotation class에 대한 객체 생성 기능이 stable

https://kotlinlang.org/docs/whatsnew1620.html#instantiation-of-annotation-classes

What’s new in Kotlin 1.6.0

experimental stable

  • 1.5.30에 추가되었던 when절의 sealed, boolean 객체에 대한 경고 처리
  • 1.5.30에 추가되었던 suspend함수를 부모타입으로 가질 수 있는 기능

더 이상 experimental이 아니다

클래스 타입 파라미터에 대한 어노테이션 추가

https://kotlinlang.org/docs/whatsnew16.html#support-for-annotations-on-class-type-parameters

 
@Target(AnnotationTarget.TYPE_PARAMETER)
annotation class BoxContent
 
class Box<@BoxContent T> {}

@Repeatable 어노테이션에 대한 자바 호환성 향상

  • @java.lang.annotation.Repeatable
  • @kotlin.annotation.Repeatable

동일한 어노테이션을 여러 번 선언할 수 있게 해주는 @Repeatable 어노테이션. 자바와 코틀린 둘다 있었지만 코틀린 1.6.0 전까지는 자바의 @Repeatable 어노테이션을 코틀린에서 인식하지 못했음. 이제는 된다.

readln(), readlnOrNull() 함수 추가

Earlier versions1.6.0 alternativeUsage
readLine()!!readln()Reads a line from stdin and returns it, or throws a RuntimeException if EOF has been reached.
readLine()readlnOrNull()Reads a line from stdin and returns it, or returns null if EOF has been reached.

typeof<T>()가 stable

KType 객체를 리턴하는 typeof<T>()

inline fun <reified T> renderType(): String {
  val type = typeOf<T>()
  return type.toString()
}
 
fun main() {
  val fromExplicitType = typeOf<Int>()
  val fromReifiedType = renderType<List<Int>>()
}

collection builder가 stable

buildMap(), buildList(), buildSet() 을 더 이상 opt-in 어노테이션 없이도 사용할 수 있다.

Regex.splitToSequence(CharSequence), CharSequence.splitToSequence(Regex)가 stable

Bit rotation 함수가 stable

val number: Short = 0b10001
println(number
        .rotateRight(2)
        .toString(radix = 2)) // 100000000000100
println(number
        .rotateLeft(2)
        .toString(radix = 2))  // 1000100

Kover, 코틀린을 위한 코드 커버리지 툴 (JaCoCo같은거)

https://kotlinlang.org/docs/whatsnew16.html#kover-a-code-coverage-tool-for-kotlin

gradle plugin으로 사용할 수 있다. 아직 experimental

JaCoCo에서 잘 처리하지 못했던 inline function등에 대한 처리를 할 수 있음.

What’s new in Kotlin 1.5.30

selaed 객체 및 boolean 객체를 when 절에서 사용할 때 모든 케이스를 다루지 않으면 경고가 발생한다 (experimental)

아직까지는 experimental.

enum 에는 이미 적용되어 있음.

suspend 함수를 부모 타입으로 가질 수 있다 (experimental)

class MyClass: suspend () -> Unit {
  override suspend fun invoke() { TODO() }
}

함수 객체가 suspend () -> Unit 타입을 상속한다

재귀적인 generic type에 대한 처리 향상

https://kotlinlang.org/docs/whatsnew1530.html#improvements-to-type-inference-for-recursive-generic-types

언젠가 쓸 일이 있겠지

nullability 어노테이션의 경고 수준을 정할 수 있다

무시할건지, warn으로 경고로 처리할건지, strict로 에러로 처리할건지

https://kotlinlang.org/docs/whatsnew1530.html#improved-nullability-annotation-support-configuration

정규표현식과 매치되는 문자열의 index 위치를 검사하는 메서드 추가

https://kotlinlang.org/docs/whatsnew1530.html#splitting-regex-to-a-sequence

  • Regex.matchAt(input: CharSequence, index: Int): MatchResult?
  • Regex.matchesAt(input: CharSequence, index: Int): Boolean

input에서 index위치에 정규표현식을 만족하는 문자열이 있는지.

Regex.splitToSequence() 추가 (split()의 lazy 버전)

List가 아니라 Sequence를 리턴한다.

What’s new in Kotlin 1.5.20

JSpecify nullness 어노테이션 지원

https://kotlinlang.org/docs/whatsnew1520.html#support-for-jspecify-nullness-annotations

자바 코드에 JSpecify 써보면 괜찮겠다. 패키지나 클래스에 @NullMarked 어노테이션 붙이면 @Nullable 어노테이션이 붙어있지 않은 매개변수와 리턴타입은 not null이라고 표현할 수 있음. https://jspecify.dev/docs/user-guide, https://github.com/jspecify/jspecify/blob/eff8f186a7036d5f18108d27e09011a4f1198f05/src/main/java/org/jspecify/annotations/NullMarked.java#L115

What’s new in Kotlin 1.5.0

@JvmRecord

data class@JvmRecord 어노테이션을 붙이면 자바에서 record 클래스처럼 사용할 수 있음.

sealed interface

sealed class와 동일함. 이 인터페이스는 같은 파일 내에서만 implement 할 수 있다. 인터페이스를 구현하는 장소가 하나의 파일로 제한되므로 컴파일러가 구현의 모든 케이스를 검증할 수 있어서 when에서 모든 케이스를 다루는지 다루지 않는지 검사할 수 있다.

상속을 피하라는 이유중 하나는 누가 어디서 어떻게 상속하는지 추적이 안 되어서 자바의 instanceof를 사용해 타입으로 분기하는 코드가 변경에 취약하다는 것이다. 하지만 컴파일러가 상속의 모든 케이스를 알 수 있어서 코딩할 때 컴파일러의 도움을 받을 수 있다면 타입으로 분기하는 코드를 안전하게 사용할 수 있다.

패턴매칭(when)을 활용하기 편해짐.

sealed class를 상속한 클래스를 선언할 수 있는 범위가 파일에서 패키지 단위로 넓어졌다

동일한 파일에서 동일한 컴파일 단위(=패키지, 동일한 디렉토리)로 범위가 넓어졌다. 좋네.

inline classes

value class Password(val s: String)

프로퍼티를 하나만 갖는 value class를 선언할 수 있다. typealias와 유사한 용도로 사용하면 된다. 어떤 타입을 다른 이름으로 선언해서 도메인 언어를 더 풍부하게 사용하고 싶을 때 value class를 사용하면 됨. typealias와 다른 점은 다른 타입을 할당할 수 없다는 점이다. https://kotlinlang.org/docs/inline-classes.html#inline-classes-vs-type-aliases

var password = Password("don't do this on production")
password = "plain string" // 불가능

inline class라고 불리는 이유는 실행시간(runtime)에 Password라는 클래스의 인스턴스를 생성하는게 아니기 때문이다.

class UseInlineClass {  
    fun useInlineClass() {  
        val inlineClass = InlineClass("inline class")  
        println(inlineClass.getValue())  
  
        val basicClass = BasicClass("basic class")  
        println(basicClass.getValue())  
    }  
}

위 코드를 컴파일하고 자바 코드로 디컴파일하면 아래처럼 나온다.

public final class UseInlineClass {  
   public final void useInlineClass() {  
      String inlineClass = InlineClass.constructor-impl("inline class");  
      System.out.println(InlineClass.getValue-impl(inlineClass));  
      
      BasicClass basicClass = new BasicClass("basic class");  
      System.out.println(basicClass.getValue());  
   }  
}

new로 클래스를 만드는게 아니라 constructor-impl함수를 호출해서 String을 받는다. constructor-impl함수는 아래처럼 생겼음.

public final class InlineClass {
  ...
  
@NotNull  
  public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String value) {  
     Intrinsics.checkNotNullParameter(value, "value");  
     return value;  
  }
  
  ...
}

함수 호출을 하는 오버헤드는 있지만 객체 생성을 하지 않기 때문에 추가로 메모리를 사용하지 않는다.

최범균님의 책에서는 id클래스를 자바 기본 타입인 Long이 아니라 엔티티별로 id 클래스를 만들어서 사용한다. id를 구분할 때 타입으로 구분할 수 있어서 함수 매개변수에 여러 개의 id를 넣어야 할 때 실수하지 않을 수 있지만 @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용할 수 없어서 auto increment를 적용할 수 없었기 때문에 사용하지 못했다.

아래처럼 @IdLong타입에 놓고 property에서 value class를 리턴하도록 하면 위 목적을 달성할 수 있음

@MappedSuperclass  
abstract class BaseEntity<T>(  
    @field:Transient  
    protected var idConstructor: (id: Long) -> T,  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "id")  
    val _id: Long = 0L  
    val id: T  
        get() = idConstructor(_id)
    
    /**  
     * 자식 클래스에서 postLoad에서 idConstructor를 넣어줘야 id property를 조회할 때 에러가 발생하지 않는다.  
     * 예)  
     *   override fun postLoad() {
     *     this.idConstructor = ::ArticleId
     *   }
     **/
     @PostLoad  
     protected abstract fun postLoad()  
}

SAM(Single Abstract Method) adapters via invokedynamic

https://kotlinlang.org/docs/whatsnew15.html#sam-adapters-via-invokedynamic

SAM 인터페이스를 구현하는 객체에 대해서 컴파일 할 때 wrapper 클래스를 생성하지 않고 invokedynamic jvm instruction을 사용한다.

kotlin lambda에 대해서도 invokedynamic을 사용할 수 있긴 하지만 아직 experimental.

자바의 nullability 어노테이션에 대한 처리 향상

https://kotlinlang.org/docs/whatsnew15.html#improvements-to-handling-nullability-annotations

코틀린에서 자바로 작성된 클래스를 사용할 때 @Nullable등 Java 코드에 붙어있는 nullability 관련 어노테이션을 의미있는 정보로 처리해준다.

지원하는 nullability annotations: https://kotlinlang.org/docs/java-interop.html#nullability-annotations

locale에 영향받지 않는 대소문자 변환 함수 추가

https://kotlinlang.org/docs/whatsnew15.html#stable-locale-agnostic-api-for-upper-lowercasing-text

String

Earlier versions1.5.0 alternative
String.toUpperCase()String.uppercase()
String.toLowerCase()String.lowercase()
String.capitalize()String.replaceFirstChar { it.uppercase() }
String.decapitalize()String.replaceFirstChar { it.lowercase() }

Char

Earlier versions1.5.0 alternative
Char.toUpperCase()Char.uppercaseChar(): Char
Char.uppercase(): String
Char.toLowerCase()Char.lowercaseChar(): Char
Char.lowercase(): String
Char.toTitleCase()Char.titlecaseChar(): Char
Char.titlecase(): String

Path 표현할 때 나누기(/) operator를 사용할 수 있음

https://kotlinlang.org/docs/whatsnew15.html#stable-path-api

// construct path with the div (/) operator

val baseDir = Path("/base")
val subDir = baseDir / "subdirectory" // list files in a directory

val kotlinFiles: List<Path> = Path("/home/user").listDirectoryEntries("*.kt")

예전부터 됐었나본데 1.5.0에서 stable이 되었다.

문자 타입(Char)에 메서드 추가

유용한 메서드들이 있네. 숫자인지 문자인지 구분한다든지

https://kotlinlang.org/docs/whatsnew15.html#new-api-for-getting-a-char-category-now-available-in-multiplatform-code

Collections.firstNotNullOf()

https://kotlinlang.org/docs/whatsnew15.html#new-collections-function-firstnotnullof

mapNotNull().first()를 합쳐놓았다.