kotlin data class constructors not getting picked up - kotlin

I am creating a data class in kotlin as such
data class User(val name: String, val age: Int)
{
constructor(name: String, age: Int, size: String): this(name, age) {
}
}
In my main function, I can access the objects as such:
fun main(){
val x = User("foo", 5, "M")
println(x.name)
println(x.age)
println(x.size) // does not work
}
My problem is that I can't get access to size.
What I am trying to do is, create a data class where top level params are the common items that will be accessed, and in the constructors, have additional params that fit certain situations. The purpose is so that I can do something like
// something along the lines of
if (!haveSize()){
val person = User("foo", 5, "M")
} else {
val person = User("foo", 5)
}
}
Any ideas?

In Kotlin you do not need separate constructors for defining optional constructor params. You can define them all in a single constructor with default values or make them nullable, like this:
data class User(val name: String, val age: Int, val size: String = "M")
fun main(){
val x = User("foo", 5, "L")
val y = User("foo", 5)
println(x.size) // "L" from call site
println(y.size) // "M" from default param
}

You can not access size variable, because this is from secondary construct, but we have alternative variant.
data class User(var name: String, var age: Int) {
var size: String
init {
size = "size"
}
constructor(name: String, age: Int, size: String) : this(name, age) {
this.size = size
}
}

In short, you want to have one property that can be one of a limited number of options. This could be solved using generics, or sealed inheritance.
Generics
Here I've added an interface, MountDetails, with a generic parameter, T. There's a single property, val c, which is of type T.
data class User(
val mountOptions: MountOptions,
val mountDetails: MountDetails<*>,
)
data class MountOptions(
val a: String,
val b: String
)
interface MountDetails<T : Any> {
val c: T
}
data class MountOneDetails(override val c: Int) : MountDetails<Int>
data class MountTwoDetails(override val c: String) : MountDetails<String>
Because the implementations MountDetails (MountOneDetails and MountTwoDetails) specify the type of T to be Int or String, val c can always be accessed.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
println(user.mountDetails)
}
fun main() {
val mt = MountOptions("foo", "bar")
val mountOneDetails = MountOneDetails(111)
anotherCaller(User(mt, mountOneDetails))
val mountTwoDetails = MountTwoDetails("mount two")
anotherCaller(User(mt, mountTwoDetails))
}
Output:
foo
bar
MountOneDetails(c=111)
foo
bar
MountTwoDetails(c=mount two)
Generics have downsides though. If there are lots of generic parameters it's messy, and it can be difficult at runtime to determine the type of classes thanks to type-erasure.
Sealed inheritance
Since you only have a limited number of mount details, a much neater solution is sealed classes and interfaces.
data class User(val mountOptions: MountOptions)
sealed interface MountOptions {
val a: String
val b: String
}
data class MountOneOptions(
override val a: String,
override val b: String,
val integerData: Int,
) : MountOptions
data class MountTwoOptions(
override val a: String,
override val b: String,
val stringData: String,
) : MountOptions
The benefit here is that there's fewer classes, and the typings are more specific. It's also easy to add or remove an additional mount details, and any exhaustive when statements will cause a compiler error.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
// use an exhaustive when to determine the actual type
when (user.mountOptions) {
is MountOneOptions -> println(user.mountOptions.integerData)
is MountTwoOptions -> println(user.mountOptions.stringData)
// no need for an 'else' branch
}
}
fun main() {
val mountOne = MountOneOptions("foo", "bar", 111)
anotherCaller(User(mountOne))
val mountTwo = MountTwoOptions("foo", "bar", "mount two")
anotherCaller(User(mountTwo))
}
Output:
foo
bar
111
foo
bar
mount two

This is really the "default values" answer provided by Hubert Grzeskowiak adjusted to your example:
data class OneDetails(val c: Int)
data class TwoDetails(val c: String)
data class MountOptions(val a: String, val b: String)
data class User(
val mountOptions: MountOptions,
val detailsOne: OneDetails? = null,
val detailsTwo: TwoDetails? = null
)
fun main() {
fun anotherCaller(user: User) = println(user)
val mt = MountOptions("foo", "bar")
val one = OneDetails(1)
val two = TwoDetails("2")
val switch = "0"
when (switch) {
"0" -> anotherCaller(User(mt))
"1" -> anotherCaller(User(mt, detailsOne = one))
"2" -> anotherCaller(User(mt, detailsTwo = two))
"12" -> anotherCaller(User(mt, detailsOne = one, detailsTwo = two))
else -> throw IllegalArgumentException(switch)
}
}

Related

Why class member property reflection in Kotlin?

In 'Kotlin in Action', it says "if a memberProperty refers to the age property of the Person class, memberProperty.get(person) is a way to dynamically get the value of person.age" with code(10.2.1 The Kotlin Reflection API):
class Peron(val name: String, val age: Int)
>> val person = Person("Alice", 29)
>> val memberProperty = Person::age
>> println(memberProperty.get(person))
29
I can't understand why this example refers to "dynamically" getting the value of property. It just works when I run this code:
println(person.age)
Is there any other case or example of member property reflection?
For example, say you want to write a function which prints all the properties of an object along with their values, this is how you can do that:
inline fun <reified T: Any> T.printProperties() {
T::class.memberProperties.forEach { property ->
println("${property.name} = ${property.get(this)}") // You can't use `this.property` here
}
}
Usage:
data class Person(val firstName: String, val lastName: String)
data class Student(val graduate: Boolean, val rollNumber: Int)
fun main() {
val person = Person("Johnny", "Depp")
val student = Student(false, 12345)
person.printProperties()
student.printProperties()
}
Output:
firstName = Johnny
lastName = Depp
graduate = false
rollNumber = 12345

Convert String referential datatype to real referential datatypes

I have the following dataclasses:
data class JsonNpc(
val name: String,
val neighbours: JsonPreferences
)
data class JsonPreferences(
val loves: List<String>,
val hates: List<String>
)
I have a list of these, and they reference each other through strings like:
[
JsonNpc(
"first",
JsonPreferences(
listOf("second"),
listOf()
)
),
JsonNpc(
"second",
JsonPreferences(
listOf(),
listOf("first")
)
)
]
note that a likes b does not mean b likes a
I also have the Dataclasses
data class Npc(
val name: String,
val neighbours: NeighbourPreferences,
)
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
And I want to convert the String reference types to the normal reference types.
What I have tried:
recursively creating the npcs (and excluding any that are already in the chain, as that would lead to infinite recursion):
Does not work, as the Npc can not be fully created and the List is immutable (I dont want it to be mutable)
I have managed to find a way to do this. It did not work with Npc as a data class, as I needed a real constructor
fun parseNpcs(map: Map<String, JsonNpc>): Map<String, Npc> {
val resultMap: MutableMap<String, Npc> = mutableMapOf()
for (value in map.values) {
if(resultMap.containsKey(value.name))
continue
Npc(value, map, resultMap)
}
return resultMap
}
class Npc(jsonNpc: JsonNpc, infoList: Map<String, JsonNpc>, resultMap: MutableMap<String, Npc>) {
val name: String
val neighbourPreferences: NeighbourPreferences
init {
this.name = jsonNpc.name
resultMap[name] = this
val lovesNpc = jsonNpc.neighbours.loves.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
val hatesNpc = jsonNpc.neighbours.hates.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
this.neighbourPreferences = NeighbourPreferences(
lovesNpc, hatesNpc
)
}
}
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
checking in the debugger, the people carry the same references for each Neighbour, so the Guide is always one Npc instance.

Avoid repetition of same logic

I have the following data classes:
sealed class ExampleDto
object Type1ExampleDto : ExampleDto()
object Type2ExampleDto : ExampleDto()
data class Type3ExampleDto(val name: Int, val age: Int) : ExampleDto()
data class Type4ExampleDto(val name: Int, val age: Int) : ExampleDto()
data class Type5ExampleDto(val email: String) : ExampleDto()
data class Type6ExampleDto(val name: Int, val age: Int, val email: String) : ExampleDto()
In particular, Type3ExampleDto, Type4ExampleDto and Type6ExampleDto share some common fields but it's important for my business logic to distinguish between types (i.e. even if Type3ExampleDto and Type4ExampleDto are identical, I have to know if I'm in the type3 or type4 case).
In one of my method I have the following call:
when (type) {
is Type3ExampleDto -> myMethod(type.vote, type.text)
is Type4ExampleDto -> myMethod(type.vote, type.text)
is Type6ExampleDto -> myMethod(type.vote, type.text)
else -> null
}
I find very ugly that I'm doing the same operation in all 3 cases and repeating the same line...
It makes sense to made Type3ExampleDto, Type4ExampleDto and Type6ExampleDto an implementation of some kind of interface just because only in this point I'm doing this kind of ugly repetition?
If all three dtos implement the following interface
interface MyInterface{
fun getVote() : Int
fun getText() : String
}
I can write:
if (type is MyInterface) {
myMethod(type.getVote(), type.getText())
}
So, it's acceptable to create this interface just to solve this isolated repetition?
Thanks
Note you can do it much more cleanly like this:
interface NameAndAgeDto {
val name: Int
val age: Int
}
data class Type3ExampleDto(override val name: Int, override val age: Int) : ExampleDto(), NameAndAgeDto
if (type is NameAndAgeDto) {
myMethod(type.name, type.age)
}
Whether it's "acceptable" is opinion. Looks fine to me.
You may change your model to have your logic based on behaviour instead of inheritance.
This way of modelling is based on principles of (but ain't exactly) Strategy Design Pattern.
interface HasName {
val name: String
}
interface HasAge {
val age: Int
}
interface HasEmail {
val email: String
}
object Type1
object Type2
data class Type3(
override val name: String,
override val age: Int
) : HasName, HasAge
data class Type4(
override val name: String,
override val age: Int
) : HasName, HasAge
data class Type5(
override val email: String
) : HasEmail
data class Type6(
override val name: String,
override val age: Int,
override val email: String
) : HasName, HasAge, HasEmail
// Then you can pass any object to it.
fun main(obj: Any) {
// Koltin type-casts it nicely to both interfaces.
if (obj is HasName && obj is HasAge) {
myMethod(text = obj.name, vote = obj.age)
}
}
fun myMethod(vote: Int, text: String) {
}
If you still want all the types to belong to some parent type, you can use marker interface (without any methods).
interface DTO
interface HasName {
val name: String
}
interface HasAge {
val age: Int
}
interface HasEmail {
val email: String
}
object Type1 : DTO
object Type2 : DTO
data class Type3(
override val name: String,
override val age: Int
) : HasName, HasAge, DTO
data class Type4(
override val name: String,
override val age: Int
) : HasName, HasAge, DTO
data class Type5(
override val email: String
) : HasEmail, DTO
data class Type6(
override val name: String,
override val age: Int,
override val email: String
) : HasName, HasAge, HasEmail, DTO
// Here, it is DTO instead of Any
fun main(obj: DTO) {
if (obj is HasName && obj is HasAge) {
myMethod(text = obj.name, vote = obj.age)
}
}
And use sealed class instead of marker interface if you need classes as enum.
In that case, when over sealed class is exhaustive with all options without null ->.

Access properties of a subclass of the declared object type

I have the following abstract class:
abstract class AbstractBook {
abstract val type: String
abstract val privateData: Any
abstract val publicData: Any
}
and the following class which inherits the AbstactBook class:
data class FantasyBook (
override val type: String = "FANTASY",
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook()
And then there is this class which should include data from any type of AbstractBook:
data class BookState(
val owner: String,
val bookData: AbstractBook,
val status: String
)
If I have an instance of BookState, how do I check which type of Book it is and then access the according FantasyBookPrivateData, and FantasyBookPublicData variables?
I hope I described my issue well & thanks in advance for any help!
What you describe is a sealed class:
sealed class Book<T, K> {
abstract val type: String
abstract val privateData: T
abstract val publicData: K
data class FantasyBook(
override val type: String = "FANTASY",
override val privateData: String,
override val publicData: Int) : Book<String, Int>()
}
and in your data class you can do pattern matching like this:
data class BookState(
val owner: String,
val bookData: Book<out Any, out Any>,
val status: String) {
init {
when(bookData) {
is Book.FantasyBook -> {
val privateData: String = bookData.privateData
}
}
}
}
to access your data in a type-safe manner. This solution also makes type redundant since you have that information in the class itself.
I agree with #Marko Topolnik that this seems like a code smell, so you might want to rethink your design.
interface AbstractBook<T , U> {
val privateData: T
val publicData: U
}
data class FantasyBook (
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook<FantasyBookPrivateData , FantasyBookPublicData>
data class BookState(
val owner: String,
val bookData: AbstractBook<*, *>,
val status: String
)
if(bookState.bookData is FantasyBook) {
// Do stuff
}
Creating a type variable is a weak type language writing style. You should use generic class.

Kotlin constructor delegation to inner data class?

We have an abstract Java class (which we can't modify) called AbstractClass that we want to implement in Kotlin. A requirement is the Kotlin implementation is serializable/deserializable to JSON using vanilla Jackson Databind. This has lead us to the following implementation:
class MyClass(private val data: MyClassData? = null) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
override fun getData(): MyClassData? {
return data
}
}
This class will always be used from Java and currently you can instantiate it like this (Java):
MyClass myClass = new MyClass(new MyClassData("John Doe", 25));
But we'd prefer to instantiate it like this instead:
MyClass myClass = new MyClass("John Doe", 25);
I can of course change the Kotlin code to something like this:
class MyClass(#JsonIgnore private var name: String = "", #JsonIgnore private var age: Int = 0) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
private var data : MyClassData? = null
init {
data = MyClassData(name, age)
}
override fun getData(): MyClassData? {
return data
}
}
but this is very verbose and kind of defeats the purpose of using Kotlin.
What I think I'd like to do is something like this (pseudo code):
class MyClass(private val data: MyClassData? = null by MyClassData) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
override fun getData(): MyClassData? {
return data
}
}
(note the by MyClassData in the MyClass constructor which obviously doesn't work)
I.e. I'd like to somehow destruct or delegate the constructor of MyClass to take the same arguments as MyClassData without duplicating them. Is this something you can do in Kotlin or is there another way to solve it without adding too much code?
I think your main concerns are: (a) concise external API, (b) clean internal state (for Jackson)
Secondary Constructor
This is pretty lean:
class MyClass internal constructor(private val data: MyClassData)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
constructor(name: String, age: Int) : this(MyClassData(name, age))
override fun getData(): MyClassData? = data
}
Simple API without creating extra fields (though I think this syntax is misleading):
val myClass = MyClass("John Doe", 25)
Pass-Thru Params & Initializer:
This was my first idea: directly pull out the outer class' params (though I now think the secondary constructor is nicer since it doesn't pollute the outer class):
class MyClass(#JsonIgnore private val name: String, #JsonIgnore private val age: Int)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
private val data = MyClassData(this#MyClass.name, this#MyClass.age)
override fun getData(): MyClassData? = data
}
...again, same API:
val myClass = MyClass("John Doe", 25)
Self-Factory
This approach has potentially more descriptive syntax:
class MyClass(private val data : MyClassData)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
companion object Factory {
fun create(name: String, age: Int) = MyClass(MyClassData(name, age))
}
override fun getData(): MyClassData? = data
}
This can be called like this:
val myClass = MyClass.Factory.create("John Doe", 25)
Conceptual Syntax: Structuring (DOES NOT EXIST)
I kind of like the idea of a 'structuring' syntax for method arguments that would group inputs into an object (the opposite of destructuring); a bit like varargs (i.e. syntactic sugar):
class MyClass(
private val data: (name: String, age: Int) : MyClassData(name, age)
) { ... }
This could be called in either of two ways:
val myClass1 = MyClass(MyClassData("John Doe", 25))
val myClass2 = MyClass("John Doe", 25)
In practice it's a rare requirement, and easily manageable with explicit overloads for just a few extra chars, so I don't think it will ever happen.