43

Situation:

I have a class with lateinit fields, so they are not present in the constructor:

class ConfirmRequest() {
    lateinit var playerId: String
}

I'd like to have a toString() method with all fields and don't want to write it manually, to avoid boiler print. In Java I'd use the Lombok @ToString annotation for this problem.

Question:

Is there any way to implement it in Kotlin?

Rene Knop
  • 1,788
  • 3
  • 15
  • 27
awfun
  • 2,316
  • 4
  • 31
  • 52
  • 2
    Lombok might still work, no? – voddan Nov 29 '16 at 09:33
  • Lombok annotations don't work, explanation: http://stackoverflow.com/questions/35517325/kotlin-doesnt-see-java-lombok-accessors – awfun Nov 29 '16 at 09:40
  • 1
    I recommend challenging the need for a `lateinint var` and a "data" `toString()` in the same class. Without understanding more of how `ConfirmRequest` is used it is hard to make a recommendation but `data class ConfirmRequest(var playerId: String? = null)` seems to work fine to me. If you knew it was never going to be used while `playerId == null` then you could make the data member private and expose a public non-null property for convenience. – mfulton26 Nov 29 '16 at 13:30
  • This class is deserialized from JSON and requires having empty consrtuctor – awfun Nov 29 '16 at 14:57
  • Depending on the Json library used you actually don't always need a default constructor, Jackson for instance doesn't require it. – M Platvoet Jan 08 '17 at 10:12
  • did you find a satisfying solution for automatic toStrings? – Martin Mlostek Nov 09 '17 at 19:26
  • override fun toString(): String{ return "$fName $lName $strAddress" } will do. You need ID for all the instances of this class, or you may want to print all the data in the class in one line. Otherwise data class automatically does it for comparisons and getter setters. If you like, you can use GSON to generate JSON and return it in this method. – Abhinav Saxena Aug 25 '21 at 11:56

6 Answers6

16

The recommended way is to write toString manually (or generate by IDE) and hope that you don't have too many of such classes.

The purpose of data class is to accommodate the most common cases of 85%, which leaves 15% to other solutions.

voddan
  • 31,956
  • 8
  • 77
  • 87
14

I find Apache Commons Lang's ToStringBuilder with reflection useful, but it calls hashCode() and other methods when I don't need that (and one called hashCode() from a 3rd-party lib generates an NPE).

So I just go with:

// class myClass
    override fun toString() = MiscUtils.reflectionToString(this)

// class MiscUTils
fun reflectionToString(obj: Any): String {
    val s = LinkedList<String>()
    var clazz: Class<in Any>? = obj.javaClass
    while (clazz != null) {
        for (prop in clazz.declaredFields.filterNot { Modifier.isStatic(it.modifiers) }) {
            prop.isAccessible = true
            s += "${prop.name}=" + prop.get(obj)?.toString()?.trim()
        }
        clazz = clazz.superclass
    }
    return "${obj.javaClass.simpleName}=[${s.joinToString(", ")}]"
}
u8188526
  • 171
  • 2
  • 5
  • plain solution, but gut enough when one cannot use a data class – Jaroslav Jul 30 '18 at 19:05
  • Works fine except for [delegated properties](https://kotlinlang.org/docs/reference/delegated-properties.html) -- you will get your string representation as `Bean=[property1$delegate={}, property2$delegate={}, property3$delegate={}]`. – Bass Nov 19 '18 at 11:43
11

Like you, I was used to using lombok for toString() and equals() in Java, so was a bit disappointed that non-data classes in Kotlin required all of the standard boilerplate.

So I created Kassava, an open source library that lets you implement toString() and equals() without any boilerplate - just supply the list of properties and you're done!

e.g.

// 1. Import extension functions
import au.com.console.kassava.kotlinEquals
import au.com.console.kassava.kotlinToString

import java.util.Objects

class Employee(val name: String, val age: Int? = null) {

    // 2. Optionally define your properties for equals()/toString() in a  companion
    //    object (Kotlin will generate less KProperty classes, and you won't have
    //    array creation for every method call)
    companion object {
        private val properties = arrayOf(Employee::name, Employee::age)
    }

    // 3. Implement equals() by supplying the list of properties to be included
    override fun equals(other: Any?) = kotlinEquals(
        other = other, 
        properties = properties
    )

    // 4. Implement toString() by supplying the list of properties to be included
    override fun toString() = kotlinToString(properties = properties)

    // 5. Implement hashCode() because you're awesome and know what you're doing ;)
    override fun hashCode() = Objects.hash(name, age)
}
Braian Coronel
  • 22,105
  • 4
  • 57
  • 62
James Bassett
  • 9,458
  • 4
  • 35
  • 68
4

This is what I endup doing.

Create extension function on Any class

fun Any.toStringByReflection(exclude: List<String> = listOf(), mask: List<String> = listOf()): String {
    val propsString = this::class.memberProperties
            .filter { exclude.isEmpty() || !exclude.contains(it.name) }
            .joinToString(", ") {
                val value = if (!mask.isEmpty() && mask.contains(it.name)) "****" else it.getter.call(this).toString()
                "${it.name}=${value}"
            };

    return "${this::class.simpleName} [${propsString}]"
}

Then you can call this method from individual type.

override fun toString(): String {
    return this.toStringByReflection()
}

It generates string below

Table [colums=[], name=pg_aggregate_fnoid_index, schema=pg_catalog, type=SYSTEM INDEX]

With name field masked:

override fun toString(): String {
    return this.toStringByReflection(mask= listOf("name"))
}

It generates,

Table [colums=[], name=****, schema=pg_catalog, type=SYSTEM INDEX]
metasync
  • 338
  • 1
  • 10
  • However cool this is, using reflection is discouraged. As stated in the Kotlin Documentation: "This is called reflection, and it's not very performant, so avoid it unless you really need it" (https://kotlinlang.org/docs/tutorials/kotlin-for-py/member-references-and-reflection.html#obtaining-member-references-from-a-class-reference) – traneHead Apr 10 '20 at 15:33
  • 1
    @traneHead agree in general about reflection being slow. If that is not a constraint for your usecase, then this approach is quick way to add toString. – metasync Apr 10 '20 at 21:35
1

You can define a data class that contains the data that you want to use and implement methods by delegating to that.

https://stackoverflow.com/a/46247234/97777

Duncan McGregor
  • 17,665
  • 12
  • 64
  • 118
1

What about using Kotlin reflection? I am into Kotlin for a few days, so apologies, if I misunderstood question, or wrote "Kotlin inefficient" example.

override fun toString() : String{
    var ret : String = ""
    for (memberProperty in this.javaClass.kotlin.memberProperties){
        ret += ("Property:${memberProperty.name} value:${memberProperty.get(this).toString()}\n");
    }
    return ret
}

This can also could be implemented in newly created interface for example ToString2Interface as fun toString2. Then all classes which implements ToString2Interface would have toString2()

vukis
  • 381
  • 2
  • 10