I am trying to create a nested data class in kotlin which contain several other data classes. Then access the main data class to get the objects of the nested data class.
Person data class is going to be the standard, but the other 3 data classes are the overloaded ones (not sure if i'm using the term overload correctly here?).
Note: the ide i'm using is intellij
What I mean:
data class User(val person: Person, val transport: Transport)
{
constructor(person: Person, activity: Activity): this(person, activity) //ERROR: There's a cycle in the delegation calls chain
constructor(person: Person, absent: Absent): this(person, absent) //ERROR: There's a cycle in the delegation calls chain
}
data class Activity(val game: String, val funLevel: Int)
data class Absent(val isSick: Boolean, val funLevel: Int)
data class Transport(val typeOfTransport: String)
data class Person(val name: String, val age: Int)
fun main(){
val x = User(Person("foo", 5), Activity("basketball", 10))
val y = User(Person("bar", 6), Absent(true, 0))
val z = User(Person("loo", 6), Transport("Bus"))
// Ultimately want to access the data objects like this
println(x.person.name)
println(x.activity.game) // not working
}
Ultimately, I am trying to be able to call the function easily based like this:
Any ideas?
data class User(val person: Person, val transport: Transport)
That bit in the parentheses there is the primary constructor - in fact that's shorthand, you can also write the class declaration like this:
data class User constructor(val person: Person, val transport: Transport)
When you have a primary constructor, any secondary constructors have to call through to the primary one.
// need to somehow create a Transport to call the primary with
constructor(person: Person, activity: Activity): this(person, someTransport)
// can't do this - the primary constructor doesn't take (Person, Activity)
constructor(person: Person, activity: Activity): this(person, activity)
That's because to actually create an instance of a class, you need to call its main constructor at some point - the secondaries can just do other stuff as well, but they need to actually instantiate the object.
You can omit the primary constructor (so the class can be instantiated with no arguments) and then your secondary constructors can do whatever they want:
class User {
constructor(person: Person, activity: Activity) {
// initialisation stuff
}
constructor(person: Person, absent: Absent) {
// other initialisation stuff
}
}
but at the end of the day it's still calling that no-args constructor to actually create the User - it's up to you to do something with those different arguments passed into the different constructors, and create the same type of object no matter which is called.
Do you have all the possible properties, and just leave them null if no value was provided? Do you have a special set of classes representing the different combos of data for each constructor, and assign an instance of one of those to a userData property? You need to work out how to have a single class that can be instantiated with different combinations of data.
Data classes are special, and require a primary constructor. That's actually what defines the data in the class, and all its handy overridden and generated methods (like equals, hashCode, copy) all work with those properties defined in the primary constructor.
The flipside of that is any property not defined in or derived from the primary constructor's parameters is not part of its "data". If you copy a data class, only the properties in the primary constructor are copied. It doesn't know anything about the rest of the class - so if you're relying on any of the data class features, be aware that other properties are not included by default.
So with that in mind, a typical data class approach would be to have all your data in the primary constructor:
data class User(
val person: Person,
val transport: Transport? = null,
val activity: Activity? = null,
val absent: Absent? = null
) {
constructor(person: Person, activity: Activity): this(person, null, activity, null)
constructor(person: Person, absent: Absent): this(person, null, null, absent)
}
That way every secondary constructor calls the main one, and all your data is defined and contained in the primary constructor. Some things just might be missing!
This is a bit awkward though:
this(person, null, activity, null)
We have named arguments, so you could try this:
constructor(person: Person, activity: Activity): this(person, activity = activity)
But that will actually call the same secondary constructor again, because its signature (takes a Person and an Activity) matches the call you're making. (That's why you're getting the cyclic error.) But if we're doing things this way, with default arguments, you can avoid the secondary constructors completely:
data class User(
val person: Person,
val transport: Transport? = null,
val activity: Activity? = null,
val absent: Absent? = null
)
// create an instance
User(somePerson, absent = someAbsent)
But this way limits your ability to restrict it to certain combinations, like a Person and one of Transport/Activity/Absent. That's a problem with data classes in general - you can make the primary constructor private and force users to go through secondary constructors or other functions that generate an instance, but the copy function allows people to mess with that data however they like.
In this case, you probably want a sealed class like IR42 mentions in the comments - a type which allows for a defined set of completely different subclasses. I just wanted to give an overview of how this all works and why maybe you'd want to try a different approach
Related
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).
Kotlin gives more priority to a function that is more specific. Consider this example:
foo(dog) // invokes foo(Dog)
fun foo(animal: Animal) {}
fun foo(dog: Dog) {}
In this case foo(dog) is also ambiguous, but in fact, it uses foo(Dog) function. To use the other one, we have to explicitly upcast:
foo(dog as Animal)
Because Int is a subtype of Int?, your secondary constructor is more specific than the primary constructor and this is why it is preferred.
In your first example, there is no ambiguity when you use two arguments, because it selects the constructor with the most specific type matching your parameter. There is no ambiguity to choose between the one with Int and the one with Int? because Int is more specific than Int?.
When you provide a single argument, it is ambiguous since you are not providing any argument that can help distinguish whether you want Int or Int?.
Example:
data class Car (
val type: TypeEnum,
val brand: BrandEnum,
val modelNumber: Int)
{
constructor(val type: TypeEnum,
val brand: BrandEnum,
val input: String) : this (
type,
brand,
Valdidator.validateModelNumber(input)
)
}
In the code above, the method validateModelNumber() validates a raw input and throws an exception if the model number has an invalid format. I want to force the user to use this constructor every time he/she wants to make a Car object.
Essentially: I want to make sure that no invalid Car object can exist, while still making the code as immutable as possible.
You could use the init block instead. Something like this
data class Car (
val type: TypeEnum,
val brand: BrandEnum,
val modelNumber: Int)
{
init {
Valdidator.validateModelNumber(input)
}
}
Using an init block for validation (as per another answer) can work well if it only needs the parameters/properties specified in the primary constructor. However, there are other approaches.
If you don't want the primary constructor to be used by other code, you can make it private, by changing:
data class Car(
to:
data class Car private constructor(
You could then leave a public secondary constructor for other classes to use, as in the question. However, that's still a bit limiting, as you can't do any serious processing before calling the primary constructor.
So the usual pattern is to have a private constructor and factory methods in the companion object. This is much more flexible: you can do any amount of processing before and after calling the actual constructor; you can even return cached instances, subclass instances, etc.
You can make those look like constructors by implementing them as operator fun invoke() with suitable parameters. In this case, that could look like:
data class Car private constructor(
val type: TypeEnum,
val brand: BrandEnum,
val modelNumber: Int)
{
companion object {
operator fun invoke(type: TypeEnum, brand: BrandEnum, input: String)
= Car(type, brand, Validator.validateModelNumber(input))
}
}
You could then create instances with e.g.:
Car(TypeEnum.SPORTS, BrandEnum.ASTON_MARTIN, "DB5")
looking just like an ordinary constructor.
Let's imagine that we have data class with two properties and we need secondary constructor for some reasons. Problem is that i need recalculate each argument in primary constructor call instead of using some cached value of raw.split("_"):
data class Id(
val arg1: String,
val arg2: String
) {
constructor(raw: String) : this(raw.split("_")[0], raw.split("_")[1])
}
I can do this in Java but how I can do this in Kotlin?
You can do it this way:
data class Id(
val arg1: String,
val arg2: String
) {
private constructor(splitted: List<String>) : this(splitted[0], splitted[1])
constructor(raw: String) : this(raw.split("_"))
}
It's a good and idiomatic way to solve your problem. Since all secondary constructors must delegate to primary constructor (data class always has it), you can't do what you want in constructor body. In Java it works because there are no primary constructors and no data classes at language level - in Kotlin you can do it like in Java too if you remove data modifier and move properties outside of constructor, but it's a really bad way.
Assume I have a data class:
data class SensorData(val name: String, val temp : Double)
I create this SensorData object from either an REST service or by internal setter method, whereas name is always populated and temp might be empty.
Further on, I need this SensorData object through several classes, thats I thought of using a singleton.
Obviously I need object keyword as described here, but how can I combine data class object ?
You can use companion object to keep a reference to your data object:
data class SensorData(val name: String, var temp : Double) {
companion object {
#Volatile
#JvmStatic
private var INSTANCE: SensorData? = null
#JvmStatic
#JvmOverloads
fun getInstance(name: String = "default", temp : Double = 0.0): SensorData = INSTANCE ?: synchronized(this) {
INSTANCE ?: SensorData(name, temp).also { INSTANCE = it }
}
}
}
And use it like this:
val name1 = SensorData.getInstance("name", 5.0).name
// Or with default values:
val name2 = SensorData.getInstance().name
I think you got the concept of a Singleton wrong:
"The singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance"
It is not only meant to be used to make it public to all classes, but to limit the number of instances.
A data class is a class to store data, why should it be a Singleton?
Rethink your architecture to make it accessible where you need it.
Don't use a data class as a singleton. That's not what they are designed for. A better approach would be to create a wrapper object around your data class, which handles your SensorData-Object(s). This will also allow you to use multiple SensorData objects (maybe needed in future) or replace the current one with a new one if you poll the REST-Service a second time.
object SensorDataService {
var sensorData: SensorData? = null
}
data class SensorData(val name: String, val temp : Double)
In my opinion, you should rethink your architecture as data class is used to store data so why it should be a singleton? It is not only meant to be used to make it public to all classes but to limit the number of instances.
I tried to resolve task #6 (DataClass) at Kotlin Koans. When I used the normal class in code, the test case failed.
Here's my code of the data class:
data class Person(val name: String, val age: Int)
fun task6(): List<Person> {
return listOf(Person("Alice", 29), Person("Bob", 31))
}
Here's result of the data class:
[Person(name=Alice, age=29), Person(name=Bob, age=31)]
Here's my code of the normal class:
class Person(val name: String, val age: Int)
fun task6(): List<Person> {
return listOf(Person("Alice", 29), Person("Bob", 31))
}
Here's result of the normal class:
[i_introduction._6_Data_Classes.Person#4f47d241, i_introduction._6_Data_Classes.Person#4c3e4790]
Does that mean there is difference between a normal class and a data class in Kotlin. If yes, what is that?
Updated:
Thank #Mallow, you are right. That works:
class Person(val name: String, val age: Int) {
override fun toString(): String {
return "Person(name=$name, age=$age)"
}
}
fun task6(): List<Person> {
return listOf(Person("Alice", 29), Person("Bob", 31))
}
Most of the time we developers use class to keep only data in classes. Classes have some methods which needs to be overridden wrt the data it holds. ex: hashCode(), equals().
Data classes automatically take care of such utilities.
From the official documentation:
We frequently create a class to do nothing but hold data. In such a class some standard functionality is often mechanically derivable from the data. In Kotlin, this is called a data class and is marked as data.
The compiler automatically derives the following members from all properties declared in the primary constructor:
equals()/hashCode() pair,
toString() of the form "User(name=John, age=42)",
componentN() functions corresponding to the properties in their order of declaration,
copy() function (see below).
If any of these functions is explicitly defined in the class body or inherited from the base types, it will not be generated.
To read more, check data-classes
About the result, Technically, you are getting is different because of implementation of toString() method. data class' toString() method uses data class properties and values to form returning string. General class' toString() method uses hash code to form returning string.
for a data class.
The compiler automatically derives the following members from all
properties declared in the primary constructor:
equals()/hashCode() pair,
toString() of the form "User(name=John, age=42)",
componentN() functions corresponding to the properties in their order
of declaration,
copy() function (see below).
see https://kotlinlang.org/docs/reference/data-classes.html
A class represents some data "type" and its behaviour(s) so from that point of view data class isn't any different than a class. But there are certain behaviours and rules about a data class that makes it a bit different:
Calling toString() on a data class dumps a string with all its member properties.
It has componentN method that get member properties by their order n.
It has a copy method which takes the member properties as parameters for making a diff copy of the object.
A data class can not be open. Cant be inherited.
It can not be abstract.
It can not be nested, inner or sealed.
Although it can inherit, define abstract methods and implement interfaces.
data class properties can be destructed into individual variables e.g val (name, address) = Person("name", "address")
Pair(a, b) internally uses data class.
It is very common to create classes whose main goal is to hold data. If you want your class to be a convenient holder for your data you need to override the universal object methods:
toString() - string representation
equals() - object equality
hashCode() - hash containers
Note: equals() is used for structural equality and it is often implemented among with hashCode().
Usually, the implementation of these methods is straightforward, and your IDE can help you to generate them automatically. However, in Kotlin, you don't have to general all of these boilerplate code. If you add the modifier data to your class, the necessary methods are automatically added for you.
The return value of toString() will have the format ClassName(parm1=value1, param2=value2, ...). equals() and hashCode() methods take into account all the properties declared in the primary constructor.
The copy() method
When you mark a class as a data class, the method copy() is also automatically generated which allows you to make copies of an existing instance. This feature is very handy when you are using your instances as keys for a HashMap or if you are dealing with multithreaded code.
Even though the properties of a data class are not required to be val, i.e., you can use var, it is strongly recommended that you use read-only properties, so that you make the instances immutable.
Finally, componentN() functions corresponding to the properties in their order of declaration are also generated by the compiler when you mark a class as a data class.
Sample Code
class PersonClass(val name: String, val age: Int)
data class PersonDataClass(val name: String, val age: Int)
>>> val ron = PersonClass("Ron", 18)
>>> val harry = PersonDataClass("Harry", 17)
>>> println(ron) // notice the string representation of a regular class
PersonClass#3b6eb2ec
>>> println(harry) // notice the string representation of a data class
PersonDataClass(name=Harry, age=17)
>>> val harryClone = harry.copy() // this creates a copy of the object referenced by harry
>>> val hermione = PersonDataClass("Hermine", 16)
>>> harry == harryClone
true
>>> harry == hermione
false
In summary, if you need a holder for data, you should use a data class which means adding the modifier data to your class. This will generate the following methods for you: toString(), equals(), hashCode(), componentN(), and copy(), so you avoid writing boilerplate code. If you use a regular class, you won't have all these "batteries included".
Data Class contains internal code which we have to override in Java-like Kotlin generates the equals(), hashCode(), and toString()
Kotlin:
data class User(val name: String, val age: String)
Java:
class Student {
public final String name;
public final String age;
public User(String name, String age) {
this.name = name;
this.age = age;
}
#Override
public boolean equals(Object other) {
}
#Override
public long hashCode() {
}
#Override
public String toString() {
return "User(name=" + name + ",age=" + age + ")";
}
}
Normal Class:
Can be abstract, open, sealed, or inner but not for Data Class
Constructor parameter can be declared without var and val