This is the first day that I participated in the challenge of More text

Kotlin 1.5 has a Sealed Interface. What’s the difference between this and Sealed Class?

Before we talk about sealed interfaces, let’s review the evolution of sealed interfaces.

Evolutionary history of hermetic species

Sealed classes can constrain the type of subclasses, similar to enumerated classes, but more flexible than enumerations:

  • Enum Class: Each enumeration is an instance of an enumeration Class and can be used directly
  • Sealed Class: A subclass of a Sealed Class constraint is just a type. You can define methods and properties for different subclasses and align them with dynamic instantiations

Kotlin 1.0

In earlier Kotlin 1.0 sealed classes, the subtype must be an inner class of the sealed class:

// Programming language
sealed class ProgrammingLang {
    object Assembly : ProgrammingLang()
    class Java(ver: String) : ProgrammingLang()
    class JavaScript(ver: String) : ProgrammingLang()
}
Copy the code

This prevents the creation of a new derived class for a sealed class without compiling it. Any addition of derived classes must recompile the enclosing class itself so that external callers can synchronize all subclass types at all times to ensure that the WHEN statement is valid:

// Get the ranking of the specified language
val ranking = when (val item: ProgrammingLang = getProgramLang()) {
    Assembly -> TODO()
    is Java -> TODO()
    is JavaScript -> TODO()
}
Copy the code

Another potential benefit is that subclasses must appear with the name of their parent class, such as Programminglang.java, which helps clarify their namespace.

Kotlin 1.1

Kotlin 1.1 removes the constraint that subclasses must be defined inside a sealed class, and that subclasses of a sealed class can be declared at the top-level of a file. However, in order to ensure synchronization of compilations, they still need to be in the same file.

sealed class ProgrammingLang

object Assembly : ProgrammingLang()
class Java(ver: String) : ProgrammingLang()
class JavaScript(ver: String) : ProgrammingLang()
Copy the code

Kotlin 1.5

In Kotlin 1.5, the restrictions were further relaxed to allow subclasses to be defined in different files, as long as they are in the same Gradle Module and have the same package name as their parent. In a module, you can ensure that all files are compiled at the same time, and you can still ensure that the compilation is synchronized.

// Lang.kt
sealed class ProgrammingLang

// Compiled.kt
class Java(ver: String) : ProgrammingLang()
class Cpp(ver: String) : ProgrammingLang()

// Interpreted.kt
class JavaScript(ver: String) : ProgrammingLang()
class Lua(ver: String) : ProgrammingLang()

// LowLevel.kt
object Assembly : ProgrammingLang()

Copy the code

This relaxation makes it easier for subclasses to be grouped into files, while splitting longer subclasses into separate files makes it easier to read.

If you violate the same Module, package name restriction, compile error:

e: Inheritance of sealed classes or interfaces from different module is prohibited
Copy the code
e: Inheritor of sealed class or interface must be in package where base class is declared
Copy the code

Sealed Interface

In addition to further relaxing restrictions on the use of sealing classes, Kotlin 1.5 also introduces sealing interfaces.

Usually the main purpose of introducing interfaces is nothing more than to hide the implementation from the outside, but 1.5 sealed classes can already be split file to hide the subclass, what is the significance of the existence of sealed interfaces?

Sealing interfaces can make up for the deficiency of sealing in the following scenarios:

1. The “final” interface

Sometimes we expose an interface, but we don’t want it to be implemented. Take kotlinx.coroutines for example

public interface Job : CoroutineContext.Element {.public fun start(a): Boolean.public fun cancel(a): Unit. }Copy the code

Job is an interface that can be implemented anywhere, but that’s clearly not what Kotlinx. coroutines wants. As the coroutine function is iterated in the future, the common attributes and methods in the Job may change, increase or decrease, and it is easy to have binary compatibility problems if there are external derived classes.

If the Job is defined as a sealed interface, this problem can be avoided.

It is a safe guess that in some future version of the coroutine jobs will appear as sealed interfaces. We can also consider using sealed interfaces in our own libraries to prevent exposed interfaces from being arbitrarily implemented.

2. Enumeration of “nested”

Enumerations and sealed classes are very similar in function. Apart from the differences introduced at the beginning of this article, enumerations cannot inherit from other classes.

Enumeration classes are essentially subclasses of Enum:

enum class JvmLang {
    Java, Kotlin, Scala
}
Copy the code

When you decompile class, you will find that JvmLang inherits from Enum.

public final class JvmLang extends Enum{
    private JvmLang(String s,int i){
        super(s,i);
    }
    public static final JvmLang Java;
    public static final JvmLang Kotlin;
    public static finalJvmLang Scala; .static{
        Java = new Action("Java".0);
        Kotlin = new Action("Kotlin".1);
        Scala = new Action("Scala".2); }}Copy the code

Enumeration classes cannot inherit classes other than Enum due to the restriction of single inheritance:

e: Enum class cannot inherit from classes
Copy the code

Sometimes, however, we need enumerations that can be nested to handle more complex classification logic. The sealed interface is the only option

sealed interface Language

enum class HighLevelLang : Language {
    Java, Kotlin, CPP
}

enum class MachineLang : Language {
    ARM, X86
}

object AssemblyLang : Language
Copy the code

As above, we actually define a set of “nested” enumerations through the sealed interface.

After that, we can classify the items using multi-level WHEN statements:

 when (lang) {
        is Machine ->
            when (lang) {
                MachineLang.ARM -> TODO()
                MachineLang.X86 -> TODO()
            }
        is HighLevel ->
            when (lang) {
                HighLevelLang.CPP -> TODO()
                HighLevelLang.Java -> TODO()
                HighLevelLang.Kotlin -> TODO()
            }
        else -> TODO()
    }
    
Copy the code

3. Multi-inheritance sealed classes

The usage scenarios of the first two sealing ports have little to do with sealing, but sealing ports can also expand the usage scenarios of sealing:

The classification of programming languages shown above, for example, is difficult to describe with a single inheritance of sealed classes.

For example, when we define a sealed class like the following

sealed class JvmLang {
    object Java : JvmLang()
    object Kotlin : JvmLang()
    object Groovy : JvmLang()
}

sealed class CompiledLang {
    object Java : CompiledLang()
    object Kotlin : CompiledLang()
    object Groovy : CompiledLang()
    object Cpp : CompiledLang()

}
Copy the code

Java cannot inherit from CompiledLang and JvmLang at the same time, so it cannot be reused in two sealed classes and needs to be defined repeatedly.

At this point one might say that sealed classes can be inherited and JvmLang can inherit CompiledLang

sealed class JvmLang : CompiledLang

object Java : JvmLang()
object Kotlin : JvmLang()
object Groovy : JvmLang()

object Cpp : CompiledLang()

Copy the code

As mentioned above, Java is a subclass of both CompiledLang and JvmLang and does not violate the single inheritance structure.

But that’s just because the Java language features aren’t “complex” enough.

Groovy is not only a compiler language, but also has the features of an InterpretedLang, which can be classified as both CompiledLang and InterpretedLang. In this case, it is difficult to maintain a single inheritance structure, and multiple inheritance needs to be implemented by removing the interface:

sealed interface CompiledLang
sealed interface InterpretedLang
sealed interface FunctionalLang
sealed interface JvmLang : CompiledLang

object Java : JvmLang
object Kotlin : JvmLang, FunctionalLang
object Groovy : JvmLang, FunctionalLang, InterpretedLang
object JavaScript: InterpretedLang
object Cpp : CompiledLang, FunctionalLang

// Market share of programming languages
fun shareOfCompiledLang(lang: CompiledLang) = when(lang) {
    Java -> TODO()
    Kotlin -> TODO()
    Groovy -> TODO()
    Cpp -> TODO()
}

fun shareOfInterpretedLang(lang: InterpretedLang) = when(lang) {
    JavaScript -> TODO()
    Groovy -> TODO()
}

Copy the code

Whether handling InterpretedLang or CompiledLang, Groovy only needs to be defined once.

Of course, in order to display all the attributes of each Lang more clearly, we can delegate the inheritance relationship between interfaces:


sealed interface CompiledLang
sealed interface InterpretedLang
sealed interface FunctionalLang
sealed interface JvmLang

object Java : JvmLang, CompiledLang
object Kotlin : JvmLang, CompiledLang, FunctionalLang
object Groovy : JvmLang, CompiledLang, FunctionalLang, InterpretedLang
object JavaScript: InterpretedLang
object Cpp : CompiledLang, FunctionalLang
Copy the code

Compatibility with Java

In JDK15, Java also introduces sealing classes and interfaces. Therefore, after JDK15, the sealing classes and interfaces between Kotlin and Java can be mapped and interoperated.

Even below JDK15, inheritance of Java code can be prevented by the prevate modifier added to the constructor of the bytecode for sealed classes

//kotlin
sealed class ProgrammingLang
Copy the code
//java
class Java extends ProgrammingLang
Copy the code

The compiler reported an error when attempting to inherit the sealed ProgrammingLang class from the Java side:

e: There is no default constructor available in 'ProgrammingLang'
Java class cannot be a part of Kotlin sealed hierarchy 
Copy the code

But for sealed interfaces, up to JDK15, Java code can be implemented at will, which needs special attention

JetBrains has announced that it will issue a warning on the IDE level. If you use the IntelliJ IDEA family of ides, you will also receive a compilation error when implementing a sealed interface on the Java side:

e: Java class cannot be a part of Kotlin sealed hierarchy
Copy the code

However, it is recommended that you keep the Kotlin syntactic features of your code to a minimum in Java.

conclusion

Kotlin 1.5 further removes the restrictions on the use of the sealing class, and also introduces the sealing interface, bringing us the following convenience:

  1. The interface that defines “final”
  2. Defines a “nested” enumeration
  3. Helps seal classes for multiple inheritance

In the future, sealed interfaces should be used as much as possible to replace sealed classes that are not defined by any member. In addition, when a Library provides services externally, it can also be used more to prevent external abuse. It can be predicted that sealed interfaces will be used more and more.