Creating a map with values containing generics - kotlin

I want to create a map with a key being a single object and a value being many objects, some containing generics. Is there a concise way to do this in Kotlin? I've used data classes in the past, but haven't found a way to make that work with generics.
Thanks!
Edit: Here's an example:
class SomeClass<E> {
data class Data(val str: String, val int: Int, val e: E) //the last value is invalid
val map: MutableMap<String, Data> = mutableMapOf()
}

Working from your example, this should work for you.
data class Data<E>(val str: String, val int: Int, val e: E)
class SomeClass<E> {
val map: MutableMap<String, Data<E>> = mutableMapOf()
}
I'm defining Data as an external, generic class, and use that inside the actual class.
Edit: Actually, you don't even need to move the data class outside of the outer class:
class SomeClass<E> {
data class Data<T>(val str: String, val int: Int, val e: T)
val map: MutableMap<String, Data<E>> = mutableMapOf()
}

Related

kotlin data class constructors not getting picked up

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)
}
}

Define Kotlin interface with all properties to be the same type

In TypeScript I can create a mapped type, along the lines of
interface IConfig {
[s : string]: number
}
is this possible in Kotlin?
I wanna be able to do something like
data class ConfigDataClass(val volume : number) : IConfig
Then later I can loop through all the data class members of a data class that satisfies IConfig and know that they are numbers
There's a few options that are close to TypeScript's Mapped Types
The basic option is just to use a Map<String, Int>. The keys will always be strings, and the values will always be integers. This matches your first definition of IConfig.
fun main() {
val iConfigMap = mapOf(
"blah" to 123,
)
println(iConfigMap)
// {blah=123}
}
What about if you want a distinct type for ConfigDataClass? We can use delegation to easily make ConfigDataClass a map, but without having to re-implement lots of code.
class ConfigDataClass(
private val map: Map<String, Int>
) : Map<String, Int> by map
// ^ delegate the implementation of Map to map
IConfig can now be used exactly like a Map<String, Int>, but because it's a distinct type, we can easily write specific functions for it
data class ConfigDataClass(
private val map: Map<String, Int>
) : Map<String, Int> by map {
// add a helper constructor to emulate mapOf(...)
constructor(vararg pairs : Pair<String, Int>) : this(pairs.toMap())
// an example function that's only for ConfigDataClass
fun toStringUppercaseKeys() : ConfigDataClass =
ConfigDataClass(map.mapKeys { (key, _) -> key.uppercase() })
}
fun main() {
val iConfig = ConfigDataClass(
"blah" to 123,
)
// I can call Map.get(...) and .size,
// even though ConfigDataClassdoesn't implement them
println(iConfig["blah"]) // 123
println(iConfig.size) // 1
println(iConfig.toStringUppercaseKeys()) // ConfigDataClass(map={BLAH=123})
}
Finally, we can also easily add a specific named field - volume. Kotlin has a really a niche feature to allow properties to be delegated to values in a map.
data class ConfigDataClass(
private val map: Map<String, Int>
) : Map<String, Int> by map {
constructor(vararg pairs : Pair<String, Int>) : this(pairs.toMap())
val volume: Int by map // finds the value of "volume" in the map
}
fun main() {
val iConfig = ConfigDataClass(
"volume" to 11,
)
println(iConfig.volume) // 11
}
Note that if there's no key "volume" in the map, you'll get a nasty exception!
fun main() {
val iConfig = ConfigDataClass(
"blah" to 123,
)
println(iConfig.volume)
}
// Exception in thread "main" java.util.NoSuchElementException: Key volume is missing in the map.
If you want your data to be mutable, you can instead delegate to a MutableMap, and change val volume to var volume.

How to map the result of jooq multiset into Hashmap(Java Map)?

I have the following class and query. I want to use multiset to map the result of the images into Map<String, String>(Key: OrderNumber / Value: FileKey), but I don't know how to do it. Could you help me how to map the multiset result into hashmap?
data class User(
val id: UUID,
val name: String,
val images: Map<String, String>?
)
#Repository
#Transactional(readOnly = true)
class FetchUserRepository(private val ctx: DSLContext) {
private val user = JUser.USER
private val userImage = JUserImage.USER_IMAGE
override fun fetch(): List<User> {
return ctx.select(
user.ID,
user.NAME,
multiset(
select(userImage.ORDER_NUMBER.cast(String::class.java), userImage.FILE_KEY)
.from(userImage)
.where(userImage.USER_ID.eq(user.ID))
).convertFrom { r -> r.map(mapping(???)) } // I'm not sure how to map the result to hashmap
)
.from(user)
.fetchInto(User::class.java)
}
jOOQ 3.16 solution
The type of your multiset() expression is Result<Record2<String, String>>, so you can use the Result.intoMap(Field, Field) method, or even Result.collect(Collector) using the Records.intoMap() collector, which allows for avoiding the repetition of field names:
{ r -> r.collect(Records.intoMap()) }
I've explained this more in detail in a blog post, here.
jOOQ 3.17 solution
In fact, this seems so useful and powerful, let's add some convenience on top of the existing API using some extensions (located in the jOOQ-kotlin extensions module):
// New extension functions, e.g.
fun <R : Record, E> Field<Result<R>>.collecting(collector: Collector<R, *, E>)
= convertFrom { it.collect(collector) }
fun <K, V> Field<Result<Record2<K, V>>>.intoMap(): Field<Map<K, V>>
= collecting(Records.intoMap())
// And then, you can write:
multiset(...).intoMap()
The feature request is here: https://github.com/jOOQ/jOOQ/issues/13538
In addition to Lukas's answer, I would like to provide an alternative option with jsonObject & jsonObjectAgg.
The result of this query would be returned as JSON format, and it can be easily projected to the target class via Jackson or whatever. (It is really powerful feature when it comes to nested collection within the target class)
I believe it is the one of the coolest features of jOOQ as MULTISET :)
data class User(
val id: UUID,
val name: String,
val images: Map<String, String>?
)
#Repository
#Transactional(readOnly = true)
class FetchUserRepository(private val ctx: DSLContext) {
private val user = JUser.USER
private val userImage = JUserImage.USER_IMAGE
override fun fetch(): List<User> {
return ctx.select(
jsonObject(
key("id").value(user.ID),
key("name").value(user.NAME),
key("images").value(
field(
select(
jsonObjectAgg(
userImage.ORDER_NUMBER.cast(String::class.java),
userImage.FILE_KEY
)
)
.from(userImage)
.where(userImage.USER_ID.eq(user.ID))
)
)
)
)
.from(user)
.fetchInto(User::class.java)
}
}

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.

Transform data class to map kotlin

My problem is that I need to transform a data class in kotlin to a map, because I need to work with this structure as a requirement, because this response will be used for a groovy classes and there is a post-process where there are validations iterations etc, with this map. My data class is the next (Podcast):
data class PodCast(val id: String, val type: String, val items: List<Item>, val header: Header, val cellType:String? = "")
data class Item(val type: String, val parentId: String, val parentType: String, val id: String, val action: Action, val isNew: Boolean)
data class Header(val color: String, val label: String)
data class Action(val type: String, val url: String)
I made the transformation manually, but I need a more sophisticated way to achieve this task.
Thanks.
You can also do this with Gson, by serializing the data class to json, and then deserializing the json to a map. Conversion in both directions shown here:
val gson = Gson()
//convert a data class to a map
fun <T> T.serializeToMap(): Map<String, Any> {
return convert()
}
//convert a map to a data class
inline fun <reified T> Map<String, Any>.toDataClass(): T {
return convert()
}
//convert an object of type I to type O
inline fun <I, reified O> I.convert(): O {
val json = gson.toJson(this)
return gson.fromJson(json, object : TypeToken<O>() {}.type)
}
See similar question here
I have done this very simple. I got the properties of the object, just using the .properties groovy method, which gave me the object as a map.