I was testing something in Kotlin and now I won't be able to sleep properly if I don't find an answer.
Look at this class:
data class Person(
val name: String,
val age: Int? = 0
) {
constructor(
secondName: String,
secondAge: Int = 5
) : this(
name = "$secondName 2",
age = secondAge
)
}
fun main() {
val firstCase = Person("Lorem") // Complaints about ambiguity.
val secondCase = Person("Lorem", 42) // Calls secondary constructor.
}
Complaining about ambiguity in the firstCase is understandable, but why it is not happening the same in the secondCase? And why did it decide to call the secondary constructor and not the main constructor?
Now if I add another parameter on the main constructor with a default value:
data class Person(
val name: String,
val age: Int? = 0,
val isAdult: Boolean = false
) {
constructor(
secondName: String,
secondAge: Int = 5
) : this(
name = "$secondName 2",
age = secondAge
)
}
fun main() {
val thirdCase = Person("Lorem") // Calls secondary constructor.
}
I was expecting to have ambiguity for the thirdCase, just like in the firstCase. But no! It calls the secondary constructor. Why?!
The rules of method resolution in Kotlin (and in Java) can be a bit arcane. Thankfully, they do the obvious thing in nearly all situations — which is clearly the point! — but there are a few surprising corner cases.
The general principle is that it picks the most specific method that could match, and only gives an error if there isn't a single winner.
So in your second case, the arguments are String
and Int
. The candidates are the primary constructor (taking String
and Int?
), and the secondary constructor (taking String
and Int
). The latter is a more specific match, because Int
is a subtype of Int?
, and so it picks that one.
But in your first case, the only argument provided is a String
, which matches both constructors equally, so there's no clear winner, and it flags up the ambiguity.
Your third case is even less obvious. However, the section of the Kotlin spec that discusses how to choose the most specific candidate from all the overloads says:
For each candidate we count the number of default parameters not specified in the call (i.e., the number of parameters for which we use the default value). The candidate with the least number of non-specified default parameters is a more specific candidate
I think that's what's happening in your third case: it picks the secondary constructor (which would leave only one parameter with its default value) over the primary (which would leave two).