Non-trivial constructors in Kotlin
Alex ShabanovKotlin really simplifies things such as defining constructors and writing immutable objects for your application. For example, flexible Kotlin constructor definitions eliminate the need of builder classes (you simply don’t need them in 99% of all the possible use cases, if you use Kotlin), thus reducing overhead of having immutable objects in your application, while retaining full flexibility and expressiveness.
However, if you want to define non-trivial constructor (especially for data classes) it might not be as trivial as just writing a function.
For example, when I started playing with Kotlin, I decided to start with something as simple as defining a class that would represent a rational number.
My first brute force (or naive, if you like) attempt to define that class was as follows:
data class Ratio (val numerator : Int, val denominator : Int)
The problems with this class are obvious: you can create ratios like Ratio(1, 2) and Ratio(2, 4) and they won’t be equal to each other. And I wanted exact opposite – whenever the user of this class constructs a ratio it divides a numerator and denominator to their greatest common divisor – to have coprime numerator and denominator in the corresponding fields of the newly constructed instance of Ratio class. Also I wanted to retain nice-to-have features of the data class – I didn’t want to define copy, hashCode and equals myself.
So, at this point you’re welcome to play before reading my solution.
OK, now if you came up with your approach or just would like to see the possible solution – here is the link to the full class definition for those who interested.
In short: you can define custom class constructor (and yet retain call semantics) is to define invoke
function in ‘class object’ section of your class that has special semantics: you can define static functions as well as factory function invoke
. It may look as follows (simplified):
class Ratio private (val numerator : Int, val denominator : Int) {
class object {
val ZERO = Ratio(0, 1) // static member!
val ONE = Ratio(1, 1) // static member!
fun invoke(numerator : Int = 1, denominator : Int = 1) : Ratio {
if (denominator == 0) throw IllegalArgumentException("denominator can't be zero")
if (numerator == 0) return ZERO
if (numerator == denominator) return ONE
val d = gcd(numerator, denominator)
return Ratio(numerator / d, denominator / d)
} // <-- end of static function invoke
fun gcd(a : Int, b : Int) : Int { /*omitted*/ } // static function!
}
}
The beauty of Kotlin here is that you'll still be able to use 'constructor' semantics whenever you need to create an instance of Ratio
, i.e. you can write as follows as if you had ordinary constructor:
val r1 = Ratio(3, 7) // invoke will be called here
val r2 = Ratio(numerator = 1, denominator = 4) // invoke will be called here
This is it, I hope you find it useful.