Kotlin - generate toString() for a non-data class
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)
}
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]
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(", ")}]"
}
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.