Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why private constructor of sealed class can be called in sub class?

Tags:

kotlin

Sealed class in Kotlin can have private constructor only. That means we can call the constructor only in itself:

Sealed classes are not allowed to have non-private constructors (their constructors are private by default).

// `private` and `constructor()` are redundant.
sealed class Expr private constructor()

But, when we utilize sealed class, a sub class have to inherit seald class:

// Above Kotlin 1.1
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()

As you can see the code above, sealed class's private constructor is called outside of sealed class itself. When sub class is instantiated, the constructor of parent(sealed class) will be called before sub class's own constructor is called. Is it just exception to visibility modifiers?

https://kotlinlang.org/docs/reference/visibility-modifiers.html#classes-and-interfaces

For members declared inside a class: private means visible inside this class only (including all its members);

like image 344
rosshjb Avatar asked Feb 10 '19 06:02

rosshjb


2 Answers

Consider the following code:

open class A private constructor(var name: String){
    class B : A("B")
    class C : A("C")
}

The above code compiles fine, as the constructor is called inside the class A. If a class D tries to inherit outside A, it won't compile.

class D : A("D") // Error: Cannot access '<init>': it is private in 'A'

As mentioned on the page Sealed class in Kotlin,

A sealed class can have subclasses, but all of them must be declared in the same file as the sealed class itself. (Before Kotlin 1.1, the rules were even more strict: classes had to be nested inside the declaration of the sealed class).

It seems that kotlin relaxed the requirement of nested classes only.

So, the following code works fine in 1.1+ but would fail in earlier versions:

sealed class A(var name: String)
class B : A("B")
class C : A("C")

whereas the following code would have been required in versions before 1.1, which respects the private constructor.

sealed class A (var name: String){
    class B : A("B")
    class C : A("C")
}

So, allowing private constructors of sealed classes outside the class (but within the same file) can be considered an enhancement to make the code cleaner.

like image 195
Deepak Avatar answered Oct 05 '22 22:10

Deepak


You can figure out what's happening by taking a look at the generated bytecode (you can do this by going to Tools -> Kotlin -> Show Kotlin Bytecode and then choosing Decompile in the pane that appears.). Decompiling it to Java shows this code for the Expr class:

public abstract class Expr {
   private Expr() {
   }

   // $FF: synthetic method
   public Expr(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

So there is a non-private constructor for the Expr class generated, with a special parameter. Then, as you'd expect, if you look at the decompiled bytecode of Const for example, you'll see that it calls into this constructor:

public final class Const extends Expr {
   public Const(double number) {
      super((DefaultConstructorMarker)null);
      this.number = number;
   }

   // other fields and methods ...
}

You still can't subclass Expr from Kotlin, because the Kotlin compiler knows that it's a sealed class from the metadata in the file, and will respect that.

As for Java client code, there you can't access this same constructor yourself because the DefaultConstructorMarker is package-private in the kotlin.jvm.internal package that it's in, so even if you write out the import statement for it manually, the compiler won't allow it.

My guess is that the package-private visibility might only be enforced at compile time, and that's why the Kotlin compiler is able to output the bytecode corresponding to the snippet above (not completely sure though).

like image 30
zsmb13 Avatar answered Oct 05 '22 22:10

zsmb13