145

Given the following Kotlin class:

data class Test(val value: Int)

How would I override the Int getter so that it returns 0 if the value negative?

If this isn't possible, what are some techniques to achieve a suitable result?

spierce7
  • 14,797
  • 13
  • 65
  • 106
  • 20
    Please consider changing the structure of your code so that negative values are converted to 0 when the class is instantiated, and not in a getter. If you override the getter as described in the answer below, all other generated methods such as equals(), toString() and component access will still use the original negative value, which will likely lead to surprising behavior. – yole Jul 21 '16 at 07:52

9 Answers9

219

After spending almost a full year of writing Kotlin daily I've found that attempting to override data classes like this is a bad practice. There are 3 valid approaches to this, and after I present them, I'll explain why the approach other answers have suggested is bad.

  1. Have your business logic that creates the data class alter the value to be 0 or greater before calling the constructor with the bad value. This is probably the best approach for most cases.

  2. Don't use a data class. Use a regular class and have your IDE generate the equals and hashCode methods for you (or don't, if you don't need them). Yes, you'll have to re-generate it if any of the properties are changed on the object, but you are left with total control of the object.

    class Test(value: Int) {
      val value: Int = value
        get() = if (field < 0) 0 else field
    
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Test) return false
        return true
      }
    
      override fun hashCode(): Int {
        return javaClass.hashCode()
      }
    }
    
  3. Create an additional safe property on the object that does what you want instead of having a private value that's effectively overriden.

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }
    

A bad approach that other answers are suggesting:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

The problem with this approach is that data classes aren't really meant for altering data like this. They are really just for holding data. Overriding the getter for a data class like this would mean that Test(0) and Test(-1) wouldn't equal one another and would have different hashCodes, but when you called .value, they would have the same result. This is inconsistent, and while it may work for you, other people on your team who see this is a data class, may accidentally misuse it without realizing how you've altered it / made it not work as expected (i.e. this approach wouldn't work correctly in a Map or a Set).

spierce7
  • 14,797
  • 13
  • 65
  • 106
  • 1
    I disagree with what you claim to be the "best approach". The problem I see is that it's **very** common to want to set a value in a data class, and never change it. For example, parsing a string into an int. Custom getters/setters on a data class are not only useful, but also necessary; otherwise, you're left with Java bean POJOs that do nothing and their behavior + validation is contained in some other class. – Abhijit Sarkar Jul 07 '19 at 07:29
  • What I said is "This is probably the best approach for most cases". In most cases, unless certain circumstances arise, devs should have a clear separation between their model and algorithm / business logic, where the resulting model from their algorithm clearly represents the various states of the possible results. Kotlin is fantastic for this, with sealed classes, and data classes. For your example of `parsing a string into an int`, you are clearly allowing the business logic of parsing and error handling non-numeric Strings into your model class... – spierce7 Jul 08 '19 at 04:57
  • ...The practice of muddying the line between model and business logic always leads to less maintainable code, and I'd argue is an anti-pattern. Probably 99% of the data classes I create are immutable / lacking setters. I think you'd really enjoy taking some time to read about the benefits of your team keeping its models immutable. With immutable models I can guarantee my models aren't accidentally modified in some other random place in code, which reduces side-effects, and again, leads to maintainable code. i.e. Kotlin didn't separate `List` and `MutableList` for no reason. – spierce7 Jul 08 '19 at 05:08
  • In the solution number 3, what's the point of the safeValue variable? – The_Martian Sep 04 '20 at 18:24
38

You could try something like this:

data class Test(private val _value: Int) {
  val value = _value
    get(): Int {
      return if (field < 0) 0 else field
    }
}

assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)

assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
  • In a data class you must to mark the primary constructor's parameters with either val or var.

  • I'm assigning the value of _value to value in order to use the desired name for the property.

  • I defined a custom accessor for the property with the logic you described.

EPadronU
  • 1,783
  • 1
  • 15
  • 15
  • 2
    I got an error on IDE, it says "Initializer is not allowed here since this property has no backing field" – Cheng Jul 03 '18 at 14:02
11

The answer depends on what capabilities you actually use that data provides. @EPadron mentioned a nifty trick (improved version):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

That will works as expected, e.i it has one field, one getter, right equals, hashcode and component1. The catch is that toString and copy are weird:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

To fix the problem with toString you may redefine it by hands. I know of no way to fix the parameter naming but not to use data at all.

voddan
  • 31,956
  • 8
  • 77
  • 87
11

I have seen your answer, I agree that data classes are meant for holding data only, but sometimes we need to make somethings out of them.

Here is what i'm doing with my data class, I changed some properties from val to var, and overid them in the constructor.

like so:

data class Recording(
    val id: Int = 0,
    val createdAt: Date = Date(),
    val path: String,
    val deleted: Boolean = false,
    var fileName: String = "",
    val duration: Int = 0,
    var format: String = " "
) {
    init {
        if (fileName.isEmpty())
            fileName = path.substring(path.lastIndexOf('\\'))

        if (format.isEmpty())
            format = path.substring(path.lastIndexOf('.'))

    }


    fun asEntity(): rc {
        return rc(id, createdAt, path, deleted, fileName, duration, format)
    }
}
Simou
  • 682
  • 9
  • 28
  • 1
    Making fields mutable just so you can modify them during initialization is a bad practice. It'd be better to make the constructor private, and then create a function that acts as a constructor (i.e. `fun Recording(...): Recording { ... }`). Also maybe a data class isn't what you want, since with non-data classes you can separate your properties from your constructor parameters. It's better to be explicit with your mutability intentions in your class definition. If those fields also happen to be mutable anyways, then a data class is fine, but almost all my data classes are immutable. – spierce7 May 14 '20 at 15:44
7

I know this is an old question but it seems nobody mentioned the possibility to make value private and writing custom getter like this:

data class Test(private val value: Int) {
    fun getValue(): Int = if (value < 0) 0 else value
}

This should be perfectly valid as Kotlin will not generate default getter for private field.

But otherwise I definitely agree with spierce7 that data classes are for holding data and you should avoid hardcoding "business" logic there.

bio007
  • 893
  • 11
  • 20
  • 1
    I agree with your solution but than in the code you would have to call it like this `val value = test.getValue()` and not like other getters `val value = test.value` – gori Jun 24 '20 at 16:31
  • Yes. That's correct. It's little bit different if you call it from Java as there it's always `.getValue()` – bio007 Jun 25 '20 at 11:01
3

I found the following to be the best approach to achieve what you need without breaking equals and hashCode:

data class TestData(private var _value: Int) {
    init {
        _value = if (_value < 0) 0 else _value
    }

    val value: Int
        get() = _value
}

// Test value
assert(1 == TestData(1).value)
assert(0 == TestData(-1).value)
assert(0 == TestData(0).value)

// Test copy()
assert(0 == TestData(-1).copy().value)
assert(0 == TestData(1).copy(-1).value)
assert(1 == TestData(-1).copy(1).value)

// Test toString()
assert("TestData(_value=1)" == TestData(1).toString())
assert("TestData(_value=0)" == TestData(-1).toString())
assert("TestData(_value=0)" == TestData(0).toString())
assert(TestData(0).toString() == TestData(-1).toString())

// Test equals
assert(TestData(0) == TestData(-1))
assert(TestData(0) == TestData(-1).copy())
assert(TestData(0) == TestData(1).copy(-1))
assert(TestData(1) == TestData(-1).copy(1))

// Test hashCode()
assert(TestData(0).hashCode() == TestData(-1).hashCode())
assert(TestData(1).hashCode() != TestData(-1).hashCode())

However,

First, note that _value is var, not val, but on the other hand, since it's private and data classes cannot be inherited from, it's fairly easy to make sure that it is not modified within the class.

Second, toString() produces a slightly different result than it would if _value was named value, but it's consistent and TestData(0).toString() == TestData(-1).toString().

schatten
  • 1,497
  • 1
  • 12
  • 19
  • @spierce7 Nope, it's not. `_value` is being modified in the init block and `equals` and `hashCode` are not broken. – schatten Oct 29 '18 at 21:26
2

Seems to be an old but interesting question. Just want to contribute an option:

data class Test(@JvmField val value: Int){
    fun getValue() = if(value<0) 0 else value
}

Now you can override getValue, and still have component1() working.

anchor
  • 21
  • 1
1

This seems to be one (among other) annoying drawbacks of Kotlin.

It seems that the only reasonable solution, which completely keeps backward compatibility of the class is to convert it into a regular class (not a "data" class), and implement by hand (with the aid of the IDE) the methods: hashCode(), equals(), toString(), copy() and componentN()

class Data3(i: Int)
{
    var i: Int = i

    override fun equals(other: Any?): Boolean
    {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Data3

        if (i != other.i) return false

        return true
    }

    override fun hashCode(): Int
    {
        return i
    }

    override fun toString(): String
    {
        return "Data3(i=$i)"
    }

    fun component1():Int = i

    fun copy(i: Int = this.i): Data3
    {
        return Data3(i)
    }

}
Asher Stern
  • 2,476
  • 1
  • 10
  • 5
  • 2
    Not sure I'd call this a drawback. It's merely a limitation of the data class feature, which isn't a feature that Java offers. – spierce7 Sep 23 '17 at 05:41
0

You can follow the Builder Pattern for this I think it'd be much better.

Here is an example:

data class Test(
    // Fields:
    val email: String,
    val password: String
) {

    // Builder(User):
    class Builder(private val email: String) {

        // Fields:
        private lateinit var password: String

        // Methods:
        fun setPassword(password: String): Builder {
            // Some operation like encrypting
            this.password = password
            // Returning:
            return this
        }

        fun build(): Test = Test(email, password)
    }
}
ABDO-AR
  • 160
  • 3
  • 9