How to use simple inheritance for data-class?

1.4k views Asked by At

In java,

abstract class NumericValue{
    private String a;
    private String b;

    public String getA() { return a; }

    public void setA(String a) { this.a = a; }

    public String getB() { return b; }

    public void setB(String b) { this.b = b; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        NumericValue that = (NumericValue) o;

        if (a != null ? !a.equals(that.a) : that.a != null) return false;
        return b != null ? b.equals(that.b) : that.b == null;
    }

    @Override
    public int hashCode() {
        int result = a != null ? a.hashCode() : 0;
        result = 31 * result + (b != null ? b.hashCode() : 0);
        return result;
    }
}

class Abc extends NumericValue{
    public static void main(String[] args) {
        Abc abc = new Abc();
        abc.getA();
    }
}

In Kotlin, this boils down to:

Approach 1:

sealed class NumericValueA{
    abstract var a: String
    abstract var b: String
}

data class AbcA(
        override var a:String,
        override var b:String
):NumericValueA()

Approach 2:

open class NumericValueB(
        open var a:String,
        open var b:String
)

data class AbcB(
        override var a:String,
        override var b:String
):NumericValueB(a,b)

Both approaches tend to massive duplication when you have data classes that simply inherit attributes since you have to write down everything you specified again - this simply doesn't scale and somehow feels wrong.

Is this state of the art or is that really the best way to translate the former java code to kotlin?

1

There are 1 answers

4
Les On BEST ANSWER

IntelliJ Idea translates your Java code into the following which seems reasonable and reduced in boiler plate. So, I would answer, "No, your premise does not accurately characterize whether or not Kotlin is state of the art ".

internal abstract class NumericValue {
    var a: String? = null
    var b: String? = null

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

        val that = o as NumericValue?

        if (if (a != null) a != that!!.a else that!!.a != null) return false
        return if (b != null) b == that.b else that.b == null
    }

    override fun hashCode(): Int {
        var result = if (a != null) a!!.hashCode() else 0
        result = 31 * result + if (b != null) b!!.hashCode() else 0
        return result
    }
}

internal class Abc : NumericValue() {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val abc = Abc()
            abc.a
        }
    }
}

But, your question specifically targets "data" classes. Data classes are a nice component of the language that gives us "deconstruction" and some useful automatically generated methods for deconstruction (such as componentN). So, using the code above (and adding open to class and the declarations of a and b), here is slightly different implementation of your example derived class.

internal data class AbcB (override var a: String?, override var b: String?) : NumericValue() {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val abc = AbcB("a","b")
            println("b = " + abc.component2())
            val n: NumericValue = abc
            println("a = " + n.a)
        }
    }
}

Which seems reasonable, in that, your starting example is not a data class and your apparent desire is to make use of the Kotlin data class. It gives you a desirable feature (if you need it), at the cost of a little more code verbage.

The derived class is the same code if you declare the base as sealed and a and b as abstract.

So, in the case of data classes, there is duplication of any part of the base class that you want to expose as "data" in the derived class (it's already exposed, just not as "data class" special members, as shown in the example below). But this is analogous to overrides in other contexts. Just for thought, consider now the following derived class.

internal data class AbcCD (var c: String?, var d: String?) : NumericValue() {
    companion object {
        @JvmStatic
        fun x() {
            val abc = AbcCD("c","d")
            abc.b = "B"
            abc.a = "A"
            println("d = " + abc.component2())
            abc.a
        }
    }
}

You get all the members of base class, and the new data members of the derived class. But if you want override benefits, it again costs some syntactic verbage (for derived data classes and regular classes).

One last point. Data classes still have other weirdness-es associated with inheritance and overrides that may still have to be worked out. toString, hashCode and equals get their own special implementations and the documentation says...

If there are explicit implementations of equals(), hashCode() or toString() in the data class body or final implementations in a superclass, then these functions are not generated, and the existing implementations are used;

... which I find to be confusing to read (leading me to experiment rather than rely upon the docs). And there are other SO questions dealing with struggles over toString and data classes (for example: this OP trying to create a DTO).

So, I think this is the state-of-the-art and it's not that bad (IMO). And yes, if you want the features of data classes, you can translate it pretty much as you've done.