I have been exploring Kotlin and have written a small program/script that does a task I find boring.
In the development of the program I use data classes to represent a Playlist. at one point in the design I wanted to have a special type of Playlist
that was EmptyPlaylist
.
I couldn't get this to work.
How would you achieve this relationship with Kotlin?
In Java, I would just extend Playlist
(or perhaps create an interface/abstract class for them both to inherit from).
I just wanted to be able to have a List<Playlist>
rather than List<Playlist?>
In the end I just created a Playlist
object, but I am interested in if it is possible to create IS-A hierarchies with data classes.
UPD: Kotlin Beta added restrictions on data
classes, so the answer has also been updated.
- Data classes cannot be abstract, open, sealed or inner;
- Data classes may not extend other classes (but may implement interfaces).
So the only option for building their hierarchies is interfaces. This restriction may be released in future versions.
EmptyPlaylist
from Playlist
which is presumed non-empty according to your words looks a little bit contradictory, but there are options:
You can make both TracksPlaylist
and EmptyPlaylist
implement some Playlist
interface. This is reasonable because EmptyPlaylist
may not contain everything that a Playlist
does. This would look like:
public interface Playlist
data class TracksPlaylist(val name: String, val tracks: List<Track>) : Playlist
data class EmptyPlaylist(val name: String): Playlist
If you don't want multiple instances of EmptyPlaylist
to even exist, you can make it a val
instead of distinct class and avoid is-checks, just comparing to
val EMPTY_PLAYLIST = TracksPlaylist("EMPTY", listOf())
This is applicable when empty playlists are all equal so that a single object is sufficient to represent them all.
You can use sealed
classes. This doesn't allow you to use data
classes, but sealed
classes may fit for building class hierarchies like this.
sealed
class is an abstract class which can only have its subclasses be declared inside its body. This gives the compiler guarantees that these are the only subclasses. Example:
sealed class Playlist(val name: String) {
class Tracks(name: String, val tracks: List<Track>): Playlist(name)
class Playlists(name: String, val innerPlaylists: List<Playlist>): Playlist(name)
object Empty: Playlist("EMPTY")
}
After that, you will be able to use when
statement without specifying else
branch if you state all the subclasses and objects of the sealed class:
when (p) {
is Playlist.Tracks -> { /* ... */ }
is Playlist.Playlists -> { /* ... */ }
Playlist.Empty -> { /* ... */ }
}
However, this option requires you to deal with equals
, hashCode
, toString
, copy
and componentN
on your own, if you need them.