Book Image

Hands-On Object-Oriented Programming with Kotlin

By : Abid Khan, Igor Kucherenko
Book Image

Hands-On Object-Oriented Programming with Kotlin

By: Abid Khan, Igor Kucherenko

Overview of this book

Kotlin is an object-oriented programming language. The book is based on the latest version of Kotlin. The book provides you with a thorough understanding of programming concepts, object-oriented programming techniques, and design patterns. It includes numerous examples, explanation of concepts and keynotes. Where possible, examples and programming exercises are included. The main purpose of the book is to provide a comprehensive coverage of Kotlin features such as classes, data classes, and inheritance. It also provides a good understanding of design pattern and how Kotlin syntax works with object-oriented techniques. You will also gain familiarity with syntax in this book by writing labeled for loop and when as an expression. An introduction to the advanced concepts such as sealed classes and package level functions and coroutines is provided and we will also learn how these concepts can make the software development easy. Supported libraries for serialization, regular expression and testing are also covered in this book. By the end of the book, you would have learnt building robust and maintainable software with object oriented design patterns in Kotlin.
Table of Contents (14 chapters)

Null safety, type casting, Pair, and Triple

In this section, we will learn about one of the most important topic of Kotlin, which is null safety. We will learn how null safety works, why it is important, and how Kotlin helps to improve the code quality. Later in this section, we will learn about type casting and its implications and we will conclude this chapter by discussing to useful data structures Pair and Triple.

Null safety

Nullability is one of the reasons that most applications crash. Kotlin is very strict when it comes to safety. Every application user (especially mobile users) want a nice, simple, and smooth user experience. An application that crashes makes more than 90% of users frustrated, causing them to instantly uninstall the application.

See the following example of Java. Here, the variable name is assigned a null value, which is the correct syntax in Java:

String name = null; // OK in java
int length = name.length(); // application crashed

But when name.length will be executed, Java will throw NullPointerException. In Kotlin, it is very important to note that variables are non-nullable by default and we cannot assign null values to them. Let's take a look at an example and assign a null value to a variable as follows:

var notNull : String = null

If we try to assign a null value to a variable, the compiler will immediately throw an error:

Error: "Null cannot be a value of a non-null type String".

At the time of declaration, a value must be assigned to the variable as follows:

var notNull : String = "Hello"

It is now safe to use the length function. The length is a function provided by Kotlin that returns the length of a string:

var length = notNull.length

Kotlin is compatible with Java, and we can write both Kotlin and Java code in one application (see Chapter 8, Interoperability). Java is not a null-safe language, and because of this, Kotlin designers enable programmers to assign null values by defining a nullable variable:

var mayBeNull : String? = null

Adding a question mark to a command indicates to the compiler that a null value can be assigned to the variable:

fun main(args: Array<String>) {
var notNull : String = "Hello"
notNull = null // not allowed

var len = notNull.length
println("Value is $notNull and length is ${notNull.length} ")

var mayBeNull : String?
mayBeNull = null // allowed
}

Safe call operators

Now we are able to declare a nullable type, but what if we try to get a length of a string that is nullable? See the following example:

var mayBeNull : String? = null
var length = mayBeNull.length

Kotlin null safety will trigger the following error:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

In simple terms, a programmer will be notified that the declared variable (string in this case) can have a null value, and this must be verified before calling. It can be validated in different ways, but one way of doing this is by using an if statement:

var mayBeNull : String? = null
if(mayBeNull != null && mayBeNull.length > 0){
var length = mayBeNull.length
}

Within the if condition, notice that Kotlin does not throw any errors. The if statement will be executed if the variable has a value, otherwise it will be skipped:

fun main(args: Array<String>) {
var name : String?
name = null // allowed
var length = 0
if(name != null && name.length > 0) {
length = name.length
}
}

In this example, we have declared a nullable string variable name and assigned a null to it. Later, we check if the name string variable is not null and length of the name variable is not zero. In this case, an if statement will skip the code block because name is assigned with null.

The ?. Safe call operator

An alternative way to verify the nullable variable is by using the safe call operator, which is a question mark followed by a full stop, ?.:

var length = mayBeNull?.length

In this case, if the string variable is not null, the safe call operator will return the length of the variable. Otherwise, null will be assigned to the length variable. This means that when the if variable is null, everything after the ?. operator will be ignored:

fun main(args: Array<String>) {
var mayBeNull : String?
mayBeNull = null // allowed
var length = mayBeNull?.length // Safe Call
println("value of length is " + length)
}

The output of this example is "value of length is null" because the safe call operator verified the mayBeNull variable and returned a null value.

The ?: Elvis operator

The safe call operator executes the called function if a variable contains a value. If not, it will return null. This is demonstrated as follows:

var mayBeNull : String? = null
var length = mayBeNull?.length

If the length variable has a null value, we again need to verify whether the variable length is null or not. In this case, we may be stuck in an unnecessary verification loop. This problem can be solved by using the Elvis operator. The Elvis operator makes sure that one out of two values must be returned:

var length = mayBeNull?.length ? : 0

If length is not null, it will return the size of the variable; otherwise, it returns 0. See the following example:

fun main(args: Array<String>) { 

var message: String? = null

var len = message?.length ?: 0
println("value of length is $len")

message = "Hello"
len = message?.length ?: 0

println("value of length is $len")
}

Create a nullable string variable called message and assign a null value to it. Use the Elvis operator, call the length function, and verify the value of the len variable, which should be 0. Now assign a value to message and verify the length.

The !! Sure operator

The not null assertion operator, also known as the sure operator, is used when it is sure that the provided variable always contains a value and is not null. Let's take a look at an example of how this works. Create a nullable string variable and assign a null value to it. Now try get the string's length with the null assertion operator:

fun main(args: Array<String>) {
var sureNotNull : String? = null
var length = sureNotNull!!.length // application will be crashed
println("value of length is " + length)
}

Of course, the application will crash. In this case, the programmer takes responsibility for variable nullability. Let's elaborate on this further with the another example.

The string class provides the lastOrNull function. This function returns the last character of the string, or null if the string is empty. We must declare a nullable character for assigning a value from the lastOrNull function:

val ch : Char? = "abc".lastOrNull()

If we try to declare a normal variable instead of nullable, Kotlin will throw a compile time error. See the following example:

val ch : Char = "abc".lastOrNull()
// Type mismatch: inferred type is Char? but Char was expected

If we are confident that an object (the "abc" string, in this case) is not null and we don't want to create a nullable variable with the null safety operator, we can use the null assertion operator:

val ch : Char = "abc".lastOrNull()!!

See the following example to verify how variables can be declared with and without the null safety operator:

fun mayBeNull(s : String ) : Char? {
val ch: Char? = s.lastOrNull()
return ch
}

fun notNull(s : String ) : Char{
val ch = s.lastOrNull()!!
return ch
}

fun main(args: Array<String>) {
var ch = notNull("abc")
// var ch = notNull("") program will crash.
println(ch)
}

The myBeNull function takes a string as a parameter and returns the last character of the string using the lastOrNull function. Notice that the function returns a nullable Char because function may receive an empty string as a parameter. On the other hand, the notNull function returns a normal character because this function uses the not null assertion operator to get the last character of the string, and we tell the compiler that this function will never receive an empty string.

Type casting

Converting data from one type to another is called type casting, for example, conversion from Float to Integer, or from Double to String. With Java or C++, type casting is a very straightforward process because these languages have primitive data types. Look at the following Java example:

double d = 10.50;
int i = (int) d;

In Kotlin, everything is an object and so it requires some extra steps to type cast from one type to another. However, Kotlin provides a rich library that helps perform these conversions. Let's explore this with the following example. Create a Byte variable and assign a value of 10 to it. Create an Integer variable and attempt to assign byteValue to intValue:

var byteValue : Byte = 10
var intValue : Int
intValue = byteValue

Since the data types are different, the preceding code block will throw a type mismatch error caused by the compiler. Using the following line will not help either:

intValue = (Int) byteValue

The preceding example shows that Kotlin does not support automatic type casting, so we will have to invoke it explicitly. Kotlin's library is packed with a number of useful functions, and each data type can use these functions for type conversion.

Converting from Byte to Float

Create a Byte variable and assign a value to it, then use the toFloat() function to convert this from Byte to Float as follows:

fun main (args: Array<String>) { 
var byteValue : Byte = 10
var floatValue : Float
floatValue = byteValue.toFloat()
println("From Byte $byteValue to Float $floatValue")
}

Converting from Double to Integer

Similarly, we can convert a Double variable into an Integer. Use the toInt() function to convert from Double to Int as follows:

fun main (args: Array<String>) {
var doubleValue : Double = 12.345
var intValue = doubleValue.toInt()
println("From Double $doubleValue to Int $intValue")
}

When converting from one type to another, we must take data loss into consideration. The following is the output of this example:

From Double 12.345 to Int 12

Double belongs to the real data type family. It can store values containing a decimal point. In contrast, Integer belongs to the number data type, which deals with whole numbers only. When the Double data type is converted to an Integer, the intValue variable simply ignores the .345 fraction value.

Converting from String to Integer

It is also possible to cast from String to Integer or from String to Double. To do this, create a String variable and cast it by using the toInt() function. See this example:

fun main (args: Array<String>) {
var
stringValue: String = "125"
var intValue = stringValue.toInt()
println("From string to int $intValue")
}

Everything is fine if the String variable contains a valid integer value, but if the String variable contains anything other than integer, Kotlin will throw a NumberCast exception. Update the following stringVariable in the previous example and verify the exception like so:

var stringValue : String = "A125"

To avoid this situation, Kotlin provides the toIntorNull function. This function will return a null if the String variable has an invalid value, such as an alpha-numeric character, whereas it will cast a string to integer if the value is numeric. It is also important to mention here that the toIntOrNull() function can return a null value, and thus the integer variable must be nullable when declared as Int?. See the following example. Create a string variable and convert it into string by using toIntOrNull function:

fun main (args: Array<String>) {
var stringValue : String = "125A"
var intValue : Int? = stringValue.toIntOrNull()

if(intValue is Int) {
println("From string to int $intValue")
}else{
println("Not a valid String")
}
}

If the string variable contains valid content, then conversion from String to Int will be successful otherwise false.

Smart cast

Any is a parent or a superclass of all classes in Kotlin. If our class is not derived from any class, then it has Any as a super class. All data types including Integer, Float, Double, and so on are derived from an Any class. (We will learn more about this in Chapter 3, The Four Pillars of Object-Oriented Programming). The following declarations are valid in Kotlin:

var any : Any? = null
any = 1234 // integer
any = "Hello" // String
any = 123.456 // Double

To understand the importance of smart casting, let's create a function with one parameter of the nullable Any? type:

fun mySmartCast(any :Any?)
{
if(any is Int)
{
var i = any + 5
println("Value is Int $i")
}
else if(any is String)
{
var s = "Hello " + any
println("Value is String $s")
}
else if (any == null) {
println("Object is null")
}
}

fun main (args: Array<String>) {
mySmartCast(8)
mySmartCast("Kotlin")
}

In the first function call with the integer value, the mySmartCast(8) smart cast not only takes care of the null type but also recognizes which type of class object it contains. Type checking, null safety, and unwrapping the object is handled by using the is operator. In the first if statement, the is operator verifies the null value and performs type casting as well.

Any is a superclass in Kotlin's class hierarchy.

Kotlin automatically converts Any into an integer to perform a mathematical operation on it, and we do not need to call toInt() function for type casting. Here is a smart cast example with the when expression:

fun mySmartCast(any :Any?){

when(any) {
is String -> println("String: $any")
is Int -> println("Integer: $any")
is Double -> println("Double: $any")
else -> println("Alian...")
}
}

Unsafe cast

The as operator is another method of type casting, but it is not considered a safe cast. Check out the following example of an unsafe cast:

fun myUnsafeCast(any : Any?) {
val s : String = any as String
println(s)
}

fun main (args: Array<String>) {
myUnsafetCast("Hello")
}

This code will execute successfully because a string variable is passed to this function, but the following function calls will throw a TypeCastException:

myUnsafetCast(2)
myUnsafetCast(null)

It is very important to secure our code before it crashes, so try to avoid unsafe casting. However, if it is necessary, do the following:

  • Declare a nullable variable with ? to store the value
  • Add a safe call with the as operator as ?

If the type casting is successful, it will return the original value. If not, it will become null. Let's take a look at the correct way to use the as operator in the following example:

fun myUnsafeCast(any : Any?){
val s : String? = any as? String
println(s)
}
fun main (args: Array<String>) {
myUnsafetCast(2)
}

This time, the program will execute normally without throwing any exceptions. Instead, it will display null on the screen.

Pair and Triple

Pair and Triple can store different values that are closely linked to each other, for example, a product name and price, x and y coordinates of a graph, or a phone book with a name, phone number, address, and so on. We can store these values by declaring a class and combining them in one object, but it is always good if a similar task can be performed by using pre-declared Kotlin classes. In this section, we will take a look at how to use Pair and Triple and how these data types help to organize different values in one place.

How to declare

Let's start by declaring Pair and Triple. Like other variables, Pair and Triple can be declared by using the val or var keyword:

val mobile = Pair("Google", 500)
val screenMirror = Pair("Chrome cast", 20.5)
val addressBook = Triple("Khan", 123456789, "Stockholm")

First, create a variable by directly assigning some values with Pair or Triple. As we can see, each Pair and Triple contains different data types—a mobile Pair contains a string and integer set, screenMirror contains a string and a double, and an addressBook Triple contains two strings and one integer. We can see that an explicit declaration of a data type is not required, as the Kotlin type inference automatically finds out the variable type.

How to retrieve values

There are plenty of ways to retrieve values from Pair and Triple, but let's start with simple one. The value of a Pair can be retrieved by assigning it to the following variables:

val (name , price) = mobile

The name variable is assigned with Google and price contains 500 Euros, which is the price of a Google mobile. We can verify this by printing these variables like so:

println("Mobile = $name , Prince = $price Euro")

A Triple can be deconstructed in a similar fashion:

val (name, phone, address) = addressBook
println("Name = $name , Phone = $phone , Address = $address")

There is another way to decompose the Pair and Triple classes. Each member of the Pair and Triple is assigned a name. The first element of the Pair can be accessed by using the property name first, the second with second, and in a Triple, the third element can be accessed with the property name third. For example, create a Pair and Triple of different types. Assign and retrieve the values as follows:

val mobile = Pair("Google", 500)
val
(name , price) = mobile
println("Mobile = ${mobile.first} , Prince = ${mobile.second}")

val addressBook = Triple("Khan", 123456789, "Stockholm")
val
(name, phone, address) = addressBook
println("Name = ${addressBook.first} , Phone = ${addressBook.second} , Address = ${addressBook.third}")

Kotlin also provides a default function for each element—component1() for the first element, component2() for the second element, and so on:

val (p_name, p_phone, p_address) = addressbook
println
("Name = ${addressbook.component1()} , Phone = ${addressbook.component2()} , Address = ${addressbook.component3()}")

While retrieving these values, if any are not required, we can ignore them by using the underscore symbol. See the following example:

val coordinates = Triple(5 , 9 , 11)
val (x, y , _) = coordinates

The coordinates variable contains three values, but by using the underscore symbol, we have simply ignored the z coordinate.