Type Checking and Casting

Sometimes you may wonder if the object at hand is of a particular type you expect. And, once you verify, you often cast the reference to that type so you can invoke the methods you desire—well, at least, that’s the case in Java. Here we’ll see how Kotlin supports these two operations of type checking and type casting. Along the way you’ll see how to benefit from Kotlin’s type safety, and at the same time see how the language removes your need to write mundane code.

Type Checking

“Is it a feature or a flaw?” is an unsettled debate about runtime type checking. It’s necessary to check an object’s type occasionally, but from the extensibility point of view, we should use it sparingly. Checking for arbitrary types can make the code brittle when new types are added and lead to failure of the Open-Closed Principle—see Agile Software Development, Principles, Patterns, and Practices [Mar02]. Think many times before writing code to check for runtime types.

Having said that, checking for runtime type is useful and unavoidable in two situations. One is in the implementation of the equals() method—we need to know if the instance at hand is of the current class. The other is within when, if the path to take depends on the type of an instance.

Let’s first look at how to check for the runtime type, and then at a nice little feature that removes the burden of casting once the type is confirmed.

Using is

The equals() method of Object performs reference-based comparison, but classes may override that method to determine equality. Continuing that tradition, all Kotlin classes, which extend from the Any class (discussed in Any, the Base Class), may override the equals() method. We also saw in Improved Equality Check, that Kotlin maps the == operator to the equals() method. Even though we’ve not looked at classes in Kotlin yet, let’s move forward to implement the equals() method in a class for the sake of exploring type checking.

In the code below, an Animal class overrides the equals() method with an implementation that wants to treat all Animal instances as being equal. But if the parameter given—which has to be of type Any? due to the signature of the method being overridden from Any—is not an instance of Animal, it wants to return false.

 class​ Animal {
 override​ ​operator​ ​fun​ ​equals​(other: Any?) = other ​is​ Animal
 }

The is operator is used to check if the object pointed to by a reference, other in this example, is of a particular type, Animal in this case. If the instance is of the expected type, then the result of that expression is true; otherwise it’s false.

Let’s use that equals() method in an example:

 val​ greet: Any = ​"hello"
 val​ odie: Any = Animal()
 val​ toto: Any = Animal()
 
 println(odie == greet) ​//false
 println(odie == toto) ​//true

In Improved Equality Check, we saw the difference between == and ===. In this example we access the equals() method using the == operator. Comparing an instance of Animal with a String returns false, but comparing two instances of Animal results in true. All the references in this example are of type Any. If the reference types were of the specific class types—for example, if the greet reference were defined as type String and the odie reference as type Animal—then the Kotlin compiler would barf at the equality check due to a type mismatch that can be detected from the source code. We kept the references types as Any to get around that check, to illustrate type checking.

The is operator can be used on any type of reference. If the reference is null, then the result of using the is operator is false. But if the instance is either of the type specified after is or one of its derived types, then the result is true.

You may use the is operator with negation as well. For example, you can use other !is Animal to check if the given reference isn’t pointing to an instance of Animal.

Kotlin comes with a buy-one, get-one-free offer—if you buy the use of is or !is, you get casting for free. Let’s see how.

Smart Casts

Suppose the Animal class has an age property and we need to use that property when comparing objects. In the equals() method, the reference type of the parameter is Any, so we can’t directly get the age value from that reference. If we were programming in Java, the equals() method for the Animal class would look like this:

 //Java code
 @Override ​public​ ​boolean​ ​equals​(Object other) {
 if​(other ​instanceof​ Animal) {
 return​ age == ((Animal) other).age;
  }
 
 return​ ​false​;
 }

It’s not enough that we used instanceof to ask Java if the type of the reference other was Animal. In addition, to access age, we had to write ((Animal)(other)).age. What’s that feature of Java in that code snippet called?

Punishment, but some call it casting.

Kotlin can perform automatic or smart casts once the type of a reference is confirmed. Let’s change the Animal class we wrote earlier to include age and then modify the equals() method:

 class​ Animal(​val​ age: Int) {
 override​ ​operator​ ​fun​ ​equals​(other: Any?):Boolean {
 return​ ​if​ (other ​is​ Animal) age == other.age ​else​ ​false
  }
 }

Within the equals() method, we’re able to use other.age directly without any casts. That’s because within the if expression’s condition, Kotlin confirmed that other is an instance of Animal. If you try to access other.age before the if, you’ll get a compilation error. But once you’ve paid for the is check, so to speak, you get the casting for free.

Let’s use this modified version of the Animal class in an example before refactoring further to reduce some noise.

 val​ odie = Animal(2)
 val​ toto = Animal(2)
 val​ butch = Animal(3)
 
 println(odie == toto) ​//true
 println(odie == butch) ​//false

The example confirms the behavior of the type checking with smart casts. Smart casts work as soon as Kotlin determines the type. It may also be used after the || and && operators, for example, not just with an if expression.

Let’s refactor the equals() method we wrote to make use of this capability:

 override​ ​operator​ ​fun​ ​equals​(other: Any?) =
  other ​is​ Animal && age == other.age

Kotlin will apply smart casts automatically where possible. If it can determine with confidence that the type of a reference is of a particular type, then it’ll let you avoid casts. Likewise, once Kotlin determines that an object reference isn’t null, it can apply smart casts to automatically cast a nullable type to a non-nullable type, again saving you an explicit cast.

Using Type Checking and Smart Cast with when

We can use is and !is and the smart casts within a when expression or statement.

We saw the following whatToDo() function in When It’s Time to Use when:

 fun​ ​whatToDo​(dayOfWeek: Any) = ​when​ (dayOfWeek) {
 "Saturday"​, ​"Sunday"​ -> ​"Relax"
 in​ listOf(​"Monday"​, ​"Tuesday"​, ​"Wednesday"​, ​"Thursday"​) -> ​"Work hard"
 in​ 2..4 -> ​"Work hard"
 "Friday"​ -> ​"Party"
 is​ String -> ​"What?"
 else​ -> ​"No clue"
 }

One of the paths in when is performing type check, using the is operator, to verify if the given parameter is of type String at runtime. We can go further and use the properties and methods of String within that path without any explicit casts. For example, we can change that path for String type to:

 is​ String -> ​"What, you provided a string of length ${dayOfWeek.length}"

Even though dayOfWeek is of type Any in the parameter list for the function, within this path—and only in this path—we’re able to use that reference as type String. That’s Kotlin’s smart casts capability in action again. Smart cast is our ally; rely on it as much as possible.

Occasionally you’ll want to do explicit casts, and Kotlin offers a few options for that.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.121.160