1. Overview

In this article, we’ll explore different approaches to iterate over object components. We’ll dive into how to access properties and functions of classes and data classes in Kotlin using multiple methods, such as reflection. This is useful for various tasks, especially when we need to introspect or manipulate objects dynamically at runtime.

2. Dependencies

During this article, we’ll use the kotlin-reflect module, therefore, let’s include it in our pom.xml:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.9.22</version>
</dependency>
Finally, let’s create a Person and an Employee class to work with:
data class Person(val name: String, val age: Int) : Employee() {
    val isAdult: Boolean
        get() = age >= 18

    val Person.isTeenager: Boolean
        get() = age in 13..19

    fun Person.isRetired(): Boolean {
        return age >= 65
    }
}
open class Employee() {
    var employeeId: Int = 0
    var salary: Double = 0.0

    val currentSalary: Double
        get() = salary

    val Employee.isSenior: Boolean
        get() = salary >= 1000.0

    fun Employee.isPromoted(): Boolean {
        return salary >= 2000.0
    }
}

3. Using Kotlin Reflection

Once the module is imported, we gain access to a variety of properties, each serving its specific function and advantage, usable in both classes and data classes. We’ll look at the most common ones provided, and then implement a simple example.

3.1. Class Members

Let’s first check out how we can obtain all the members of a class or data class. In order to do this, we’ll use members and declaredMembers.

The members property returns all class members (properties and functions), including those inherited from superclasses and interfaces. The declaredMembers only returns what was specifically declared in the class or interface, excluding inherited superclasses and interfaces.

Both provide access to all public, private, protected, and internal members of the current class.

Let’s have a look at some examples:

@Test
fun `Get data class members for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.members)
      .extracting("name")
      .contains("age", "name", "isAdult", "currentSalary", "employeeId", "salary")

    assertThat(person::class.declaredMembers)
      .extracting("name")
      .contains("age", "isAdult", "name")
}

3.2. Class Properties

While checking for members returns all properties and functions of a class, in some cases, we might need just the class properties. For this, we have access to memberProperties, declaredMemberProperties, memberExtensionProperties, declaredMemberExtensionProperties, and staticProperties.

Their function is pretty straightforward. On the one hand, we have memberProperties and memberExtensionProperties that return all class properties, including those inherited from superclasses and interfaces:

@Test
fun `Get data class member properties for Person`() {
    val person = Person("Daniel", 25)

    assertThat(person::class.memberProperties)
      .extracting("name")
      .contains("age", "isAdult", "name", "employeeId", "salary")

    assertThat(person::class.memberExtensionProperties)
      .extracting("name")
      .containsOnly("isTeenager", "isSenior")
}

On the other hand, we can use declaredMemberProperties and declaredMemberExtensionProperties only to include those directly stated within the class, and not inherited from other classes or interfaces:

@Test
fun `Get data class declared member properties for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.declaredMemberProperties)
      .extracting("name")
      .containsOnly("age", "isAdult", "name")

    assertThat(person::class.declaredMemberExtensionProperties)
      .extracting("name")
      .containsOnly("isTeenager")
}

In the context of the Person class, extension properties attach additional attributes, such as isTeenager and isSenior, to the class.

With staticProperties, we can directly access static fields within Java classes that have the static modifier. Specifically, we’ll need to create the Circle Java class and store a constant value:

public class Circle {
    public static final double PI = 3.14;
}
@Test
fun `Get Java class static properties`() {
    val circle = Circle()

    assertThat(Circle::class.staticProperties)
      .extracting("name")
      .containsOnly("PI")
}

It’s worth noting that Kotlin supports the concept of static members, although not in the same way that Java does. The primary way to achieve this behavior in Kotlin is by using companion objects.

3.3. Class Functions

Just like iterating through properties, we can also iterate through the functions of a class by using memberFunctions, memberExtensionFunctions, declaredMemberFunctions, declaredMemberExtensionFunctions, and staticFunctions.

By using memberFunctions and memberExtensionFunctions we can return all class functions including those inherited from superclasses and interfaces:

@Test
fun `Get data class member functions for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.memberFunctions)
      .extracting("name")
      .contains("component1", "component2", "copy", "equals", "hashCode", "toString")

    assertThat(person::class.memberExtensionFunctions)
      .extracting("name")
      .containsOnly("isRetired", "isPromoted")
}

Alternatively, declaredMemberFunctions and declaredMemberExtensionFunctions give us direct access to functions stated within the class, not inherited from other classes or interfaces:

@Test
fun `Get data class declared member functions for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.declaredMemberFunctions)
      .extracting("name")
      .contains("component1", "component2", "copy", "equals", "hashCode", "toString")

    assertThat(person::class.declaredMemberExtensionFunctions)
      .extracting("name")
      .containsOnly("isRetired")
}

As with staticProperties which look into returning static fields from Java classes, we can use staticFunctions to get all the functions available. Let’s add a static function to our Circle class and see how we can retrieve it:

public static double calculateArea(double radius) {
    return PI * radius * radius;
}
@Test
fun `Get data class static functions for Person`() {
    val circle = Circle()

    assertThat(circle::class.staticFunctions)
      .extracting("name")
      .contains("calculateArea")
}

3.4. Companion Objects and Nested Classes

In a previous subchapter, we talked about companion objects used as a substitute for Java static properties and functions. Now it’s time to have a deeper look at how we can iterate through them. Let’s add the Create companion object to the Person class:

companion object Create{
    fun create(name: String, age: Int) = Person(name, age)
}

Let’s write a test that uses the companionObject property to return the Create companion object:

@Test
fun `Get data class companion object for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.companionObject)
      .isNotNull
      .extracting("simpleName")
      .isEqualTo("Create")
}

With the recent addition to the Person class, it’s worth noting that if we execute the test that checks the class members, it returns the correct result. This is because members only takes into account properties and functions, excluding companion objects. This also applies to inner classes and objects.

Let’s add the Job inner data class and the Address object to our Person class:

data class Job(val title: String, val salary: Float)

object Address {
    const val planet: String = "Earth"
}

To iterate through these nested objects, we use the nestedClasses property:

@Test
fun `Get inner data class for Person`() {
    val person = Person("Daniel", 37)

    assertThat(person::class.nestedClasses)
      .extracting("simpleName")
      .contains("Job", "Address")
}

4. Destructuring Declarations

Kotlin supports destructuring declarations, which allow you to break down an object into its parts and assign them to variables in a single statement. This feature is particularly useful when iterating over objects with a fixed number of components, such as pairs or triples. Now, let’s apply the destructuring declarations logic to the Person class:

@Test
fun `Destructuring declaration for data class`() {
    val person = Person("Daniel", 37)
    val (name, age) = person

    assertThat(name).isEqualTo("Daniel")
    assertThat(age).isEqualTo(37)
}

5. Custom Iterator Function

Reflection provides us with the necessary tools to navigate through object components. An alternative approach is creating an extension function for the Person class:

fun Person.components(): Iterator<Pair<String, Any>> {a
    return listOf(
      "name" to name,
      "age" to age,
      "isAdult" to isAdult,
      "isTeenager" to isTeenager,
      "isRetired" to isRetired()
    ).iterator()
}

The disadvantage here is that if the class structure changes, the components function must also be updated.

6. Conclusion

In this article, we’ve explored multiple ways to iterate through object components.

First, we looked at Kotlin reflection which helped us to access class members, properties, and functions.

We then checked out a simpler way to iterate through objects by using destructuring declarations, when handling a small number of properties.

Finally, we brought it together with a custom extension function that achieves the same goal, without using reflection.

As always, the full implementation of these examples can be found over on GitHub.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments