Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin—the New and Noteworthy in 2.2

Kotlin—the New and Noteworthy in 2.2

Avatar for Anton Arhipov

Anton Arhipov

June 04, 2025
Tweet

More Decks by Anton Arhipov

Other Decks in Programming

Transcript

  1. Kotlin—the New and Notewor t hy in 2.2 : @antonarhipov

    Strengthening Safety and Abstractions
  2. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0
  3. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0 "A general purpose, statically typed, object-oriented alternative JVM programming language with type inference"
  4. public class Person { private final String name; private final

    int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public boolean equals(Object o) { if (this = = o) return true; if (o == null | | getClass() ! = o.getClass()) return false; Person person = (Person) o; if (age != person.age) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } data class Person( val name: String, val age: Int ) It used to be very easy to create the wow effect with this:
  5. data class Person( val name: String, val age: Int )

    But now Java has records . public record Person( String name, int age ) {}
  6. fun main() { val person = "Anton" println("Hello, $person!") }

    Concise Conciseness is not the primary goal. It is a extra convenience for reducing the cognitive load.
  7. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String { return map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Top-level functions
  8. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String { return map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Extension functions!
  9. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Single-expression functions!!
  10. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase()}!") } fun String.randomCase(chance: Double = 0.5) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Type inference!!!
  11. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase(offset = 0.25)}!") } fun String.randomCase(chance: Double = 0.5) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Default argument values
  12. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase(offset = 0.25)}!") } fun String.randomCase(chance: Double = 0.5, offset: Double = 0.1) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Named parameters
  13. fun main() { val person = "Anton" println("Hello, ${person.uppercase()}!") println("Hello,

    ${person.randomCase(offset = 0.25)}!") println("Hello, ${event.transform { it.randomCase(offset = 0.25) }} !") } fun String.transform(transformer: (String) -> String) = transformer(this) Trailing lambda as a parameter
  14. fun main() { val person: String? = getPersonName() println("Hello, ${person.uppercase()}!")

    println("Hello, ${person.randomCase(offset = 0.25)}!") } private fun getPersonName(): String? = "Anton" Nullable types
  15. fun main() { val person: String? = getPersonName() println("Hello, ${person

    ?. uppercase()}!") println("Hello, ${person !! .randomCase(offset = 0.25)}!") } private fun getPersonName(): String? = "Anton" Operators for working with null values
  16. fun main() { val person: Any? = getPersonName() if (person

    !is String) return println("Hello, ${person ?. uppercase()}!") println("Hello, ${person !! .randomCase(offset = 0.25)}!") } private fun getPersonName(): Any? = "Anton"
  17. fun main() { val person: Any? = getPersonName() if (person

    !is String) return println("Hello, ${person.uppercase()}!") println("Hello, ${person.randomCase(offset = 0.25)}!") } private fun getPersonName(): Any? = "Anton"
  18. fun main() { val person: Any? = getPersonName() if (person

    !is String) return println("Hello, ${person.uppercase()}!") println("Hello, ${person.randomCase(offset = 0.25)}!") } private fun getPersonName(): Any? = "Anton" Smar t -cast caused by type check
  19. val client = createClient { firstName = "Anton" lastName =

    "Arhipov" twitter { handle = "@antonarhipov" } company { name = "JetBrains" city = "Tallinn" } dob = 24 March 2000 } println("Created client is: ${client.toS}")
  20. Type-safe builders val client = createClient { firstName = "Anton"

    lastName = "Arhipov" twitter { handle = "@antonarhipov" } company { name = "JetBrains" city = "Tallinn" } dob = 24 March 2000 } println("Created client is: ${client.toS}")
  21. K2 : The new Kotlin compiler - why? 1. A

    few language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time per f ormance
  22. K2 : The new Kotlin compiler - why? 1. A

    few language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time per f ormance
  23. K2 : The new Kotlin compiler - why? 1. A

    few language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time per f ormance
  24. Kotlin 2.0 ( 2024 ) More than 80 features in

    the different subsystems Around 25 and small improvements within the language Main focus is on correctness and per f ormance
  25. Kotlin 2.0 ( 2024 ) More than 80 features in

    the different subsystems Around 25 and small improvements within the language Main focus is on correctness and per f ormance
  26. if (condition) { println("Hello") } when { condition -> println("Hello")

    } for (n in list) { println(n) } val <interator> = list.interator() while(<iterator>.hasNext()){ val s = <iterator>.next() println(s) } Frontend Intermediate Representation (FIR)
  27. if (condition) { println("Hello") } when { condition -> println("Hello")

    } for (n in list) { println(n) } val <interator> = list.interator() while(<iterator>.hasNext()){ val s = <iterator>.next() println(s) } val (a, b) = "a" to "b" val <pair> = "a" to "b" val a = pair.component1() val b = pair.component2() Frontend Intermediate Representation (FIR)
  28. New control flow engine read: more smar t -casts! -

    KT-7186 Smar t cast for captured variables inside changing closures of inline functions - KT-4113 Smar t casts for proper t ies to not-null functional types at invoke calls - KT-25747 DFA variables: propagate smar t cast results from local variables - KT-1982 Smar t cast to a common super t ype of subject types after || (OR operator) - .
  29. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } } Smar t -casts
  30. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } } Smar t -casts
  31. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() } } Smar t -casts from variables
  32. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() } } Smar t -casts from variables synthetic data flow variables propagate information about smar t -casts
  33. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } }
  34. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } }
  35. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card Smar t -casted to Card
  36. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card String? -> String Smar t -casted to String Smar t -casted to Card
  37. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card String? -> String Smar t -casted to String Smar t -casted to Card String? -> String
  38. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } }
  39. Smar t -casts from variables class Card(val holder: String?) fun

    findHolder(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } String
  40. What's next for Kotlin? More features are coming for working

    with data Stronger abstractions and improvements in the type system
  41. What's next for Kotlin? 2.0 2.1 2.2 2.4 May 2024

    November 2024 June 2024 Q2 2024
  42. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  43. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder()
  44. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder() Potentially a logical error Repetition is not nice
  45. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is YearlySubscription -> processSubscription(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder()
  46. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  47. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() Error: expecting ' -> ' &&
  48. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() Guarded conditions: KEEP - 371 if
  49. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  50. when(order) { is YearlySubscription -> processSubscription(order) is YearlySubscription if order.amount

    > 100 -> applyDiscount(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() 'when' branch is never reachable
  51. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() 2.1
  52. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() 2.1 2.2 Stabe
  53. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  54. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (id, name, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = getOrder() Destructuring
  55. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (id, name, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Destructuring Order 1: Anton, 12.0
  56. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Variable name 'id' matches the name of a different component Destructuring
  57. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Order Anton: 1, 12.0 Destructuring
  58. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Error: 'name' doesn’t match the proper t y 'customerName' Name-based destructuring
  59. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring
  60. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring Existing syntax
  61. when(order) { is YearlySubscription if order.amount > 100 -> {

    (val name, val id, val amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring
  62. when(order) { is YearlySubscription if order.amount > 100 -> {

    (val name, val id, val amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring sealed interface Order data class YearlySubscription(val productName: String, ... ) : Order Does not rely on data classes
  63. when(order) { is YearlySubscription if order.amount > 100 -> {

    (val name, val id, val amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring sealed interface Order data class YearlySubscription(val productName: String, ... ) : Order Experimental in 2.4 Does not rely on data classes
  64. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  65. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders (a.k.a DSLs)
  66. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders (a.k.a DSLs) Functional literal with receiver
  67. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library
  68. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = LocalDate.of(2000, 3, 10) } User code
  69. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = LocalDate.of(2000, 3, 10) } User code Can we do better?
  70. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) val dob = 10 March 2000
  71. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) val dob = 10 March 2000 How can we restrict the scope?
  72. object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year,

    Month.MARCH, this) DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code val dob = 10 March 2000 Context parameters (KEEP - 367 )
  73. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) Context parameters (KEEP - 367 )
  74. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) Context parameters (KEEP - 367 )
  75. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) The required context is missing Required context available in this block Context parameters (KEEP - 367 )
  76. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) The required context is missing Required context available in this block Context parameters (KEEP - 367 ) Beta in 2.2
  77. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  78. Errors are a way to express that a method can

    fail using type system capabilities error object NotFound TL;DR: Errors != Exceptions
  79. Errors are a way to express that a method can

    fail using type system capabilities error object NotFound TL;DR: Errors != Exceptions Experimental in 2.4
  80. What about exceptions? Kotlin don’t have checked exceptions for a

    purpose Primary use exceptions should be for unrecoverable cases
  81. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  82. They do not use return value of the call. What

    do those examples have in common?
  83. They do not use return value of the call. What

    do those examples have in common? Can we prevent this?
  84. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value!
  85. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value! Not always true, unfor t unately:
  86. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value! Not always true, unfor t unately: There are results which are supposed to be used and there are results which are auxiliary.
  87. CheckReturnValue as future default In Kotlin 2.2 : Kotlin stdlib

    and kotlinx libraries are marked with @MustUseReturnValue Opt-in to enable the checker
  88. CheckReturnValue as future default In Kotlin 2.2 : Kotlin stdlib

    and kotlinx libraries are marked with @MustUseReturnValue Opt-in to enable the checker Next steps: More libraries Application code by default
  89. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  90. Summary Kotlin 2.0 : new compiler allowing for smar t

    er features More features are coming for working with data Stronger abstractions and improvements in the type system https://speakerdeck.com/antonarhipov