We can check whether a variable is of a particular type by using the is keyword. For example, we could check whether settingsProvider is an instance of SettingsManager as follows:
fun main(args: Array<String>) {
val settingsProvider: SettingsProvider = SettingsManager
if (settingsProvider is SettingsManager) {
// do something
}
}
Conversely, you can check whether a variable is not of a certain type by using !is:
fun main(args: Array<String>) {
val settingsProvider: SettingsProvider = SettingsManager
if (settingsProvider !is SettingsManager) {
// do something
}
}
The Kotlin compiler can track explicit casts and type checks and can perform smart casting for us. This means that, once the compiler has validated a variable's type, it's no longer necessary to explicitly perform a cast. In this example, once we've checked that settingsProvider is an instance of SettingsManager, we do not have have to perform an explicit cast within the if block. The compiler performs the cast for us:
fun main(args: Array<String>) {
val settingsProvider: SettingsProvider = SettingsManager
if (settingsProvider is SettingsManager) {
settingsProvider.printSettings()
}
}
Kotlin helps to make patterns such as inheritance and composition easier to implement and more explicit in how they operate. The inheritance of interfaces, interface properties, and default method implementations on interfaces all provide a great deal of flexibility in how we model our data. We also have the freedom to create child classes and nested classes and have greater control over how those classes relate to one another through the open, final, and inner keywords. Finally, we explored how the Kotlin compiler handles type casting and smart casting, helping us to ensure strong type safety throughout our code.
In the next section, we're going to continue exploring classes in Kotlin by focusing on a special type of class: data classes.