Type check is a way of checking the type(DataType
) or Class
of a particular instance or variable while runtime to separate the flow for different objects. In few languages, it’s also denoted as Run Time Type Identification (RTTI)
.
Let’s consider an example where we have an Interface
called Shape having an abstract method as calculateArea
.
interface Shape {
fun calculateArea(): Float
}
We have three different classes implementing the same interface and implementing their own area calculating method according to their shapes.
class Circle : Shape {
var radius: Float = 10.0f
override fun calculateArea(): Float {
return (22 * radius * radius) / 7
}
}
class Square : Shape {
var sideLength: Float = 10.0f
override fun calculateArea(): Float {
return sideLength * sideLength
}
}
class Rectangle : Shape {
var length: Float = 10.0f
var breadth: Float = 5.0f
override fun calculateArea(): Float {
return length * breadth
}
}
Now let’s create an object with reference to the Shape interface but implementation of different classes based on some condition.
var shapeObject: Shape
if (/* Some Condition*/) {
shapeObject = Circle()
} else if (/* Some Other Condition*/) {
shapeObject = Square()
} else {
shapeObject = Rectangle()
}
Now if we want to tweak the properties of the variable shapeObject (radius in case of Circle, sideLength in case of Square and length and breadth in case of Rectangle), you can’t simply use them directly as the reference to that object is Shape interface.
shapeObject.radius = 10.0f //compile time error
Before accessing the properties of shapeObject
one must ensure the type of variable shapeObject.
‘is’ and ‘!is’ Operators
is
operator checks the type of variable and returns boolean as true if it matches the type.
if (shapeObject is Circle) {
print("it’s a Circle")
} else if (shapeObject is Square) {
print("it’s a Square")
} else if (shapeObject is Rectangle) {
print("it’s a Rectangle")
}
!is
returns true if the type doesn’t matches. It’s just a not
operator for is
operator.
if (shapeObject !is Circle) {
print("it’s not a Circle")
} else if (shapeObject !is Square) {
print("it’s not a Square")
} else if (shapeObject !is Rectangle) {
print("it’s not a Rectangle")
}
Smart Casts
In other programming languages, the variable requires an explicit casting on the variable before accessing the properties of that variable but Kotlin does a smart casting. The compiler automatically converts the variable shapeObject to a particular class reference once it’s passed through any conditional operator.
var area: Float = 0.0f
if (shapeObject is Circle) {
shapeObject.radius = 10.0f //compiles fine
area = shapeObject.calculateArea()
} else if (shapeObject is Square) {
shapeObject.sideLength = 5.0f //compiles fine
area = shapeObject.calculateArea()
} else if (shapeObject is Rectangle) {
shapeObject.length = 10.0f //compiles fine
shapeObject.breadth = 5.0f //compiles fine
area = shapeObject.calculateArea()
}
The compiler is sufficiently smart to know a cast to be safe if a negative check leads to a return
.
if ( shapeObject !is Circle) return
shapeObject.radius = 3.0f /*compiles fine as the non Circle class reference were already returned*/
area = shapeObject.calculateArea()
It even works on the right-hand side of &&
and ||
/* Automatically cast the right-hand side of && to Circle */
if (shapeObject is Circle && shapeObject.radius > 5.0f){
print("Circle with radius more than 5.0")
}
/* Automatically cast the right hand side of || to Sqaure */
if (shapeObject !is Square || shapeObject.sideLength < 3.0f){
print("Either not square or is a square with side length less than 3.0f")
}
it even works with when
conditions or while
loop
when(shapeObject){
is Circle -> shapeObject.radius = 3.0f
is Square -> shapeObject.sideLength = 4.0f
is Rectangle -> {
shapeObject.length = 5.0f
shapeObject.breadth = 6.0f
}
else -> print("Undefined type")
}
var count = 0
while (count < 5 && shapeObject is Circle){
shapeObject.radius = count.toFloat() //compiles fine
area += shapeObject.calculateArea()
}
Note that smart casts don’t work when the compiler can’t guarantee that the variable can’t change between the check and the usage.
More specifically, smart casts are applicable according to the following rules:
val
local variables — alwaysval
properties — if the property is private or internal or the check is performed in the same module where the property is declared. Smart casts aren’t applicable to open properties or properties that have custom gettersvar
local variables — if the variable is not modified between the check and the usage and is not captured in a lambda that modifies itvar
properties — never (because the variable can be modified at any time by other code).
Explicit Cast operator ‘as’
as
operator works as other languages cast operators which casts the object to another object with particular reference.
var otherShapeObject = shapeObject as Circle
or the nullable type object can only be cast to a new nullable reference type object.
var nullableShapeObject : Circle? = shapeObject as Circle?
The above explicit cast is unsafe as it can throw an exception if the cast is not possible. That’s why as
operator called as unsafe cast operator.
Instead, we can use a safe cast operator as?
where it assigns a null
value if the cast is not possible without throwing an exception.
var safeCastObject : Circle? = shapeObject as? Circle