Limit instance creation in sealed hierarchies

Hi,
I am wondering what the proper Kotlin way would be to handle situations like these:

sealed interface Year {
    companion object {
        val Y2023 = ConcreteYear(2023)
        val Y2024 = ConcreteYear(2024)
        val UNKNOWN = UnknownYear()
    }
}

class ConcreteYear(val year: Int): Year
class UnknownYear(): Year

Basically, I would like to limit the instances for Year. That is, that the only place that those instances can be created should be at one central location (here in the companion object).

What is the Kotlin way to do this?

In this case I would just use an enum.

Limiting instances is the “enum way”. Sealed types are about limiting subtypes:

sealed interface Year {
    object Y2023 : ConcreteYear(2023)
    object Y2024 : ConcreteYear(2024)
    object UNKNOWN : Year
}

sealed class ConcreteYear(val year: Int): Year

Also, it feels a little strange to limit years like this, but maybe your use case actually justifies this.

2 Likes

The use case is just a simplified example of a real world scenario. Thanks for your help!

How does this differ in its byte code implementation from something like that?

public abstract class JavaYear {
    
    public static JavaYear Y2025 = new ConcreteJavaYear();
                                   ^^^^^^^^^^^^^^^^^^^^^^
    
    public static class ConcreteJavaYear extends JavaYear {
    }
}

IDEA shows a warning for the instantiation: Referencing subclass ConcreteJavaYear from superclass JavaYear initializer might lead to class loading deadlock.

I would expect that the Kotlin version would run the same risks?

Removed my answer as I believe it was incorrect

Kotlin doesn’t create fields in JavaYear. When we do JavaYear.Y2024, we don’t get a Y2024 field of JavaYear class. We get a singleton object JavaYear.Y2024. Java doesn’t have a concept of singleton objects, so this is not directly translatable to it.

That started easy but got tricky quite fast!

sealed interface Year {
    object Y2023 : ConcreteYear(2023)
    object Y2024 : ConcreteYear(2024)
    object UNKNOWN : Year
}

sealed class ConcreteYear(val year: Int): Year

In Java that looks like Year.Y2023.INSTANCE. In other words, there is a field involved but with an indirection. Or is that indirection already enough to avoid the class loader deadlock? It could be, because the instance is created in another class, couldn’t it be?

I’m not sure if I understand correctly.

The problem in your Java code was that JavaYear in its static constructor calls the constructor of ConcreteJavaYear and as ConcreteJavaYear is a subtype of JavaYear, then it has to call the JavaYear back. In Kotlin, JavaYear can be initialized without initializing its subtype Y2024, so this problem doesn’t exist.

You can do the same in Java, but you will have to acquire Y2024 with either: JavaYear.Y2024.INSTANCE or JavaYear.getY2024().

You are right! My oversight is that the culprit is the constructor call and not that the sub class is referenced. That is indeed a completely different scenario from the internal Kotlin implementation.

Thanks for the explanation.

1 Like