Reputation: 40510
Let's say I have a data class like this:
data class MyData(val something: Int, val somethingElse : String) {
init {
require(something > 20) { "Something must be > 20" }
require(StringUtils.isNotEmtpy(somethingElse)) { "Something else cannot be blank" }
}
}
I'd like to be able to apply a function to somethingElse
before the init
method is called. In this case I want to remove all \n
characters from the somethingElse
String while maintaining immutability of the field (i.e. somethingElse
must still be a val
). I'd like to do something similar to this in Java:
public class MyData {
private final int something;
private final String somethingElse;
public MyDate(int something, String somethingElse) {
this.something = something;
this.somethingElse = StringUtils.replace(somethingElse, '\n', '');
Validate.isTrue(something > 20, "...");
Validate.isTrue(StringUtils.isNotEmtpy(this.somethingElse), "...");
}
// Getters
}
I could of course create a normal class (i.e. no data class) in Kotlin but I want MyData
to be a data class.
What is the idiomatic way to do this in Kotlin?
Upvotes: 3
Views: 2178
Reputation: 10559
While you can not literally do what you want, you can fake it.
operator fun invoke
.Usages of Companion.invoke
will -- in Kotlin! -- look just like constructor calls.
In your example:
data class MyData private constructor(
val something: Int,
val somethingElse : String
) {
init {
require(something > 20) { "Something must be > 20" }
require("" != somethingElse) { "Something else cannot be blank" }
}
companion object {
operator fun invoke(something: Int, somethingElse: String) : MyData =
MyData(something, somethingElse.replace("\n", " "))
}
}
fun main(args: Array<String>) {
val m = MyData(77, "something\nwicked\nthis\nway\ncomes")
println(m.somethingElse)
}
Prints:
something wicked this way comes
You'll note the helpful warning:
Private data class constructor is exposed via the generated 'copy' method.
This method can not be overridden (as far as I can tell) so you have to take care, still. One solution is to hide the actual data class away:
interface MyData {
val s: Int
val sE: String
private data class MyDataImpl(
override val s: Int,
override val sE: String
) : MyData {
init {
require(s > 20) { "Something must be > 20" }
require("" != sE) { "Something else cannot be blank" }
}
}
companion object {
operator fun invoke(s: Int, sE: String) : MyData =
MyDataI(s, sE.replace("\n", " "))
}
}
Now your invariant (no line breaks) is maintained, copy
and other dangerous methods (if any, I haven't checked) are hidden away -- but therefore also unavailable, potentially removing some of the convenience data classes provide.
Choose your poison.
Upvotes: 2