How to obtain all subclasses of a given sealed class?

Recently we upgraded one of our enum class to sealed class with objects as sub-classes so we can make another tier of abstraction to simplify code. However we can no longer get all possible subclasses through Enum.values() function, which is bad because we heavily rely on that functionality. Is there a way to retrieve such information with reflection or any other tool?

PS: Adding them to a array manually is unacceptable. There are currently 45 of them, and there are plans to add more.


This is how our sealed class looks like:

sealed class State


object StateA: State()
object StateB: State()
object StateC: State()
....// 42 more

If there is an values collection, it will be in this shape:

val VALUES = setOf(StateA, StateB, StateC, StateC, StateD, StateE,
StateF, StateG, StateH, StateI, StateJ, StateK, StateL, ......

Naturally no one wants to maintain such a monster.

33952 次浏览

A wise choice is using ServiceLoader in kotlin. and then write some providers to get a common class, enum, object or data class instance. for example:

val provides = ServiceLoader.load(YourSealedClassProvider.class).iterator();


val subInstances =  providers.flatMap{it.get()};


fun YourSealedClassProvider.get():List<SealedClass>{/*todo*/};

the hierarchy as below:

                Provider                    SealedClass
^                             ^
|                             |
--------------                --------------
|            |                |            |
EnumProvider ObjectProvider    ObjectClass  EnumClass
|            |-------------------^          ^
|                    <uses>                 |
|-------------------------------------------|
<uses>

Another option, is more complicated, but it can meet your needs since sealed classes in the same package. let me tell you how to archive in this way:

  1. get the URL of your sealed class, e.g: ClassLoader.getResource("com/xxx/app/YourSealedClass.class")
  2. scan all jar entry/directory files in parent of sealed class URL, e.g: jar://**/com/xxx/app or file://**/com/xxx/app, and then find out all the "com/xxx/app/*.class" files/entries.
  3. load filtered classes by using ClassLoader.loadClass(eachClassName)
  4. check the loaded class whether is a subclass of your sealed class
  5. decide how to get the subclass instance, e.g: Enum.values(), object.INSTANCE.
  6. return all of instances of the founded sealed classes

In Kotlin 1.3+ you can use sealedSubclasses.

In prior versions, if you nest the subclasses in your base class then you can use nestedClasses:

Base::class.nestedClasses

If you nest other classes within your base class then you'll need to add filtering. e.g.:

Base::class.nestedClasses.filter { it.isFinal && it.isSubclassOf(Base::class) }

Note that this gives you the subclasses and not the instances of those subclasses (unlike Enum.values()).


With your particular example, if all of your nested classes in State are your object states then you can use the following to get all of the instances (like Enum.values()):

State::class.nestedClasses.map { it.objectInstance as State }

And if you want to get really fancy you can even extend Enum<E: Enum<E>> and create your own class hierarchy from it to your concrete objects using reflection. e.g.:

sealed class State(name: String, ordinal: Int) : Enum<State>(name, ordinal) {
companion object {
@JvmStatic private val map = State::class.nestedClasses
.filter { klass -> klass.isSubclassOf(State::class) }
.map { klass -> klass.objectInstance }
.filterIsInstance<State>()
.associateBy { value -> value.name }


@JvmStatic fun valueOf(value: String) = requireNotNull(map[value]) {
"No enum constant ${State::class.java.name}.$value"
}


@JvmStatic fun values() = map.values.toTypedArray()
}


abstract class VanillaState(name: String, ordinal: Int) : State(name, ordinal)
abstract class ChocolateState(name: String, ordinal: Int) : State(name, ordinal)


object StateA : VanillaState("StateA", 0)
object StateB : VanillaState("StateB", 1)
object StateC : ChocolateState("StateC", 2)
}

This makes it so that you can call the following just like with any other Enum:

State.valueOf("StateB")
State.values()
enumValueOf<State>("StateC")
enumValues<State>()

UPDATE

Extending Enum directly is no longer supported in Kotlin. See Disallow to explicitly extend Enum class : KT-7773.

With Kotlin 1.3+ you can use reflection to list all sealed sub-classes without having to use nested classes: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/sealed-subclasses.html

I asked for some feature to achieve the same without reflection: https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087

Full example:

sealed class State{
companion object {
fun find(state: State) =
State::class.sealedSubclasses
.map { it.objectInstance as State}
.firstOrNull { it == state }
.let {
when (it) {
null -> UNKNOWN
else -> it
}
}
}
object StateA: State()
object StateB: State()
object StateC: State()
object UNKNOWN: State()


}

If you want use it at child class try this.

open class BaseSealedClass(val value: String, val name: Int) {
companion object {
inline fun<reified T:BaseSealedClass> valueOf(value: String): T? {
return T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
.associateBy { it.value }[value]
}


inline fun<reified  T:BaseSealedClass> values():List<T> =
T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
}
}


@Stable
sealed class Theme(value: String, name: Int): BaseSealedClass(value, name) {
object Auto: Theme(value = "auto", name = R.string.setting_general_theme_auto)
object Light: Theme(value= "light", name = R.string.setting_general_theme_light)
object Dark: Theme(value= "dark", name = R.string.setting_general_theme_dark)


companion object {
fun valueOf(value: String): Theme? = BaseSealedClass.valueOf(value)
fun values():List<Theme> = BaseSealedClass.values()
}
}




For a solution without reflection this is a library that supports generating a list of types to sealed classes at compile time: https://github.com/livefront/sealed-enum

The example in the docs

sealed class Alpha {
object Beta : Alpha()
object Gamma : Alpha()
    

@GenSealedEnum
companion object
}

will generate the following object:

object AlphaSealedEnum : SealedEnum<Alpha> {
override val values: List<Alpha> = listOf(
Alpha.Beta,
Alpha.Gamma
)


override fun ordinalOf(obj: Alpha): Int = when (obj) {
Alpha.Beta -> 0
Alpha.Gamma -> 1
}


override fun nameOf(obj: AlphaSealedEnum): String = when (obj) {
Alpha.Beta -> "Alpha_Beta"
Alpha.Gamma -> "Alpha_Gamma"
}


override fun valueOf(name: String): AlphaSealedEnum = when (name) {
"Alpha_Beta" -> Alpha.Beta
"Alpha_Gamma" -> Alpha.Gamma
else -> throw IllegalArgumentException("""No sealed enum constant $name""")
}
}

The short version is

State::class.sealedSubclasses.mapNotNull { it.objectInstance }