Object Expressions, Declarations, and Inline Classes in Kotlin

Anonymous Objects with Object Expressions

Sometimes a slight modification of an existing class is needed, but without the overhead of creating an explicit named subclass. Kotlin addresses this through object expressions and object declarations.

An object expression creates an instance of an anonymous class that extends one or more supertypes:

panel.addMouseListener(object : MouseAdapter() {
    override fun mousePressed(e: MouseEvent) {
        // handle press
    }
    override fun mouseReleased(e: MouseEvent) {
        // handle release
    }
})

Constructor arguments are passed to the supertype when required. Multiple supertypes are separated by commas after a colon:

open class Vehicle(val wheels: Int) {
    open val info: String = "generic vehicle"
}

interface Movable {
    fun move()
}

val bike: Vehicle = object : Vehicle(2), Movable {
    override val info = "bicycle"
    override fun move() { println("riding") }
}

When no explicit supertype is needed, a plain object can be created:

fun createLocal() {
    val localValues = object {
        var alpha: Double = 0.0
        var beta: Double = 0.0
    }
    println(localValues.alpha + localValues.beta)
}

An anonymous object is only visible as its declared type when used in private or local scopes. If a public function returns an anonymous object or a public property holds it, the declared type becomes Any (or the explicit supertype), and the extra members are inaccessible:

class Container {
    private fun internal() = object {
        val secret = "hidden"
    }

    fun exposed() = object {
        val visible = "shown"
    }

    fun test() {
        val a = internal().secret      // OK
        // val b = exposed().visible   // ERROR: unresolved reference
    }
}

Object expressions can access variables from the enclosing scope and modify them:

fun trackEvents(component: JComponent) {
    var clicks = 0
    var entries = 0
    component.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) { clicks++ }
        override fun mouseEntered(e: MouseEvent) { entries++ }
    })
}

Object Declarations (Singletons)

The singleton pattern is easy in Kotlin. An object declaration defines a single instance eagerly initialized in a thread‑safe manner on first acccess:

object ServiceRegistry {
    fun register(service: Service) {
        // implementation
    }

    val allServices: Collection<Service>
        get() = listOf()
}

The instance is referenced by its name directly:

ServiceRegistry.register(myService)

Object declarations can have supertypes:

object DefaultMouseHandler : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /* ... */ }
    override fun mouseEntered(e: MouseEvent) { /* ... */ }
}

Note: object declarations cannot be local (directly inside a function), but they can be nested in other object declarations or in non‑inner classes.

Companion Objects

An object declaration inside a class can be marked with companion to create a companion object. Its members are accessible using the class name as qualifier:

class Connection {
    companion object Builder {
        fun connect(url: String): Connection = Connection()
    }
}

val conn = Connection.connect("jdbc:...")

The companion object name can be omitted; the default name becomes Companion:

class Session {
    companion object { }
}

val ref = Session.Companion

The class name itself is a valid reference to its companion object (whether named or anonymous):

class Alpha {
    companion object Named { }
}
val x = Alpha

class Beta {
    companion object { }
}
val y = Beta

Companion objects can implement interfaces, so they behave like real objects at runtime:

interface Factory<T> {
    fun create(): T
}

class Item {
    companion object : Factory<Item> {
        override fun create(): Item = Item()
    }
}

val factory: Factory<Item> = Item

On the JVM, members of a companion object can be compiled to true static methods and fields by using the @JvmStatic annotation.

Differences Between Object Expressions and Declarations

  • Object expressions are executed and initialized immediately at the point of use.
  • Object declarations are initialized lazily on first access.
  • Companion objects are initialized when the enclosing class is loaded (resolved), matching Java static initializer semantics.

Type Aliases

A type alias provides an alternative name for an existing type. This is useful for shortening long generic types or clarifying code without introducing a new type.

typealias UserSet = Set<User>
typealias CacheStore<K> = MutableMap<K, MutableList<CacheEntry>>

Function types can also be shortened:

typealias EventHandler<T> = (T) -> Unit
typealias Predicate<T> = (T) -> Boolean

You can create aliases for inner or nested classes:

class Container {
    inner class Item
}
class Box {
    inner class Item
}
typealias ContainerItem = Container.Item
typealias BoxItem = Box.Item

Type aliases do not introduce a new type; they are equivalent to the underlying type. The compiler always expands them:

typealias Predicate<T> = (T) -> Boolean

fun check(p: Predicate<Int>) = p(42)

fun main() {
    val funLiteral: (Int) -> Boolean = { it > 0 }
    println(check(funLiteral))  // true
    val alias: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(alias)) // [1]
}

Inline Classes

When wrapping a value in a new type, there is usually a heap allocation overhead. Inline classes (available since Kotlin 1.3 as an experimental feature) avoid this overhead by inlining the underlying value at the use site, similar to inline functions.

An inline class is declared with the inline modifier and must have exactly one primary constructor property:

inline class AuthToken(val token: String)

val secureToken = AuthToken("sensitive-data")
// At runtime, secureToken is just a String; no wrapper object exists.

Members

Inline classes can declare properties and functions, but they are compiled to static methods:

inline class DisplayName(val text: String) {
    val length: Int
        get() = text.length

    fun greet() {
        println("Hello, $text")
    }
}

fun main() {
    val name = DisplayName("Kotlin")
    name.greet()            // static method call
    println(name.length)    // static getter call
}

Restrictions:

  • No init blocks
  • No backing fields (only simple computed properties, no lateinit or delegated properties)

Inheritance

Inline classes can implement interfaces:

interface Printable {
    fun prettyPrint(): String
}

inline class DisplayName(val text: String) : Printable {
    override fun prettyPrint(): String = "Hello $text!"
}

fun main() {
    val name = DisplayName("Kotlin")
    println(name.prettyPrint()) // static dispatch
}

They cannot extend other classes and must be final.

Representation

At runtime an inline class instance can be represented either as the wrapper or as the underlying base type. The compiler prefers the base type but will box when the inline class is used as a different type (generic type parameter, interface, nullable inline type):

interface Identifiable
inline class Wrapper(val id: Int) : Identifiable

fun acceptInline(w: Wrapper) {}
fun <T> acceptGeneric(x: T) {}
fun acceptIdentifiable(i: Identifiable) {}
fun acceptNullable(w: Wrapper?) {}

fun <T> identity(x: T): T = x

fun main() {
    val w = Wrapper(42)
    acceptInline(w)        // unboxed (used as Wrapper)
    acceptGeneric(w)       // boxed (generic type)
    acceptIdentifiable(w)  // boxed (interface)
    acceptNullable(w)      // boxed (nullable)

    // The id function boxes when passed, then unboxes on return
    val c = identity(w)   // contains the underlying Int 42
}

Reference equality is meaningless and is prohibited for inline classes.

Name Mangling

Because inline class are compiled to their underlying types, signature clashes can occur. The compiler appends a stable hash to the method name:

inline class SafeInt(val raw: Int)

fun calculate(x: Int) {}
fun calculate(x: SafeInt) {}
// JVM bytecode becomes:
// calculate(int)
// calculate-<hashcode>(int)

This means such methods are not directly callable from Java (the - is invalid in Java identifiers).

Inline Classes vs Type Aliases

While type aliases just rename an existing type (assignment‑compatible), inline classes are a real new type and are not assignment‑compatible with thier underlying type or with other aliases:

typealias NameAlias = String
inline class NameInline(val s: String)

fun useString(s: String) {}
fun useAlias(n: NameAlias) {}
fun useInline(n: NameInline) {}

fun main() {
    val alias: NameAlias = ""
    val inline: NameInline = NameInline("")
    val string: String = ""

    useString(alias)   // OK: alias is String
    // useString(inline)  // ERROR: NameInline is not String
    useAlias(string)   // OK
    // useInline(string)  // ERROR
}

Experimental Status

Inline classes are experimental in Kotlin 1.3+. The compiler shows a warning unless the feature is explicitly enabled.

Enabling in Gradle:

compileKotlin {
    kotlinOptions.freeCompilerArgs += ["-Xinline-classes"]
}

tasks.withType<KotlinCompile> {
    kotlinOptions.freeCompilerArgs += "-Xinline-classes"
}

Enabling in Maven:

<configuration>
    <args>
        <arg>-Xinline-classes</arg>
    </args>
</configuration>

Tags: kotlin Object Expressions Object Declarations Companion Objects Type Aliases

Posted on Wed, 20 May 2026 03:25:00 +0000 by a-scripts.com