Kotlin Data Types, Variables and Nullability

From Techotopia
Jump to: navigation, search
PreviousTable of ContentsNext
An Introduction to KotlinKotlin Operators and Expressions


You are reading a sample chapter from the Android Studio 3.0 / Android 8 Edition book.

Purchase the fully updated Android Studio 3.2 / Android 9 / Jetpack Edition of this publication in eBook ($29.99) or Print ($45.99) format

Android Studio 3.2 Development Essentials - Kotlin Edition Print and eBook (ePub/PDF/Kindle) editions contain 96 chapters and over 800 pages

Buy Print Preview Book


Both this and the following few chapters are intended to introduce the basics of the Kotlin programming language. This chapter will focus on the various data types available for use within Kotlin code. This will also include an explanation of constants, variables, type casting and Kotlin’s handling of null values.

As outlined in the previous chapter, entitled “An Introduction to Kotlin” a useful way to experiment with the language is to use the Kotlin online playground environment. Before starting this chapter, therefore, open a browser window, navigate to http://try.kotl.in and use the playground to try out the code in both this and the other Kotlin introductory chapters that follow.




Kotlin Data Types

When we look at the different types of software that run on computer systems and mobile devices, from financial applications to graphics intensive games, it is easy to forget that computers are really just binary machines. Binary systems work in terms of 0 and 1, true or false, set and unset. All the data sitting in RAM, stored on disk drives and flowing through circuit boards and buses are nothing more than sequences of 1s and 0s. Each 1 or 0 is referred to as a bit and bits are grouped together in blocks of 8, each group being referred to as a byte. When people talk about 32-bit and 64-bit computer systems they are talking about the number of bits that can be handled simultaneously by the CPU bus. A 64-bit CPU, for example, is able to handle data in 64-bit blocks, resulting in faster performance than a 32-bit based system.

Humans, of course, don’t think in binary. We work with decimal numbers, letters and words. In order for a human to easily (‘easily’ being a relative term in this context) program a computer, some middle ground between human and computer thinking is needed. This is where programming languages such as Kotlin come into play. Programming languages allow humans to express instructions to a computer in terms and structures we understand, and then compile that down to a format that can be executed by a CPU.

One of the fundamentals of any program involves data, and programming languages such as Kotlin define a set of data types that allow us to work with data in a format we understand when programming. For example, if we want to store a number in a Kotlin program we could do so with syntax similar to the following:

val mynumber = 10

In the above example, we have created a variable named mynumber and then assigned to it the value of 10. When we compile the source code down to the machine code used by the CPU, the number 10 is seen by the computer in binary as:

1010

Similarly, we can express a letter, the visual representation of a digit (‘0’ through to ‘9’) or punctuation mark (referred to in computer terminology as characters) using the following syntax:

val myletter = 'c'

Once again, this is understandable by a human programmer, but gets compiled down to a binary sequence for the CPU to understand. In this case, the letter ‘c’ is represented by the decimal number 99 using the ASCII table (an internationally recognized standard that assigns numeric values to human readable characters). When converted to binary, it is stored as:

10101100011

Now that we have a basic understanding of the concept of data types and why they are necessary we can take a closer look at some of the more commonly used data types supported by Kotlin.

Integer Data Types

Kotlin integer data types are used to store whole numbers (in other words a number with no decimal places). All integers in Kotlin are signed (in other words capable of storing positive, negative and zero values).

Kotlin provides support for 8, 16, 32 and 64 bit integers (represented by the Byte, Short, Int and Long types respectively).


Floating Point Data Types

The Kotlin floating point data types are able to store values containing decimal places. For example, 4353.1223 would be stored in a floating point data type. Kotlin provides two floating point data types in the form of Float and Double. Which type to use depends on the size of value to be stored and the level of precision required. The Double type can be used to store up to 64-bit floating point numbers. The Float data type, on the other hand, is limited to 32-bit floating point numbers.

You are reading a sample chapter from the Android Studio 3.0 / Android 8 Edition book.

Purchase the fully updated Android Studio 3.2 / Android 9 / Jetpack Edition of this publication in eBook ($29.99) or Print ($45.99) format

Android Studio 3.2 Development Essentials - Kotlin Edition Print and eBook (ePub/PDF/Kindle) editions contain 96 chapters and over 800 pages

Buy Print Preview Book

Boolean Data Type

Kotlin, like other languages, includes a data type for the purpose of handling true or false (1 or 0) conditions. Two Boolean constant values (true and false) are provided by Kotlin specifically for working with Boolean data types.

Character Data Type

The Kotlin Char data type is used to store a single character of rendered text such as a letter, numerical digit, punctuation mark or symbol. Internally characters in Kotlin are stored in the form of 16-bit Unicode grapheme clusters. A grapheme cluster is made of two or more Unicode code points that are combined to represent a single visible character.

The following lines assign a variety of different characters to Character type variables:

val myChar1 = 'f'
val myChar2 = ':'
val myChar3 = 'X'

Characters may also be referenced using Unicode code points. The following example assigns the ‘X’ character to a variable using Unicode:

val myChar4 = '\u0058'

Note the use of single quotes when assigning a character to a variable. This indicates to Kotlin that this is a Char data type as opposed to double quotes which indicate a String data type.

String Data Type

The String data type is a sequence of characters that typically make up a word or sentence. In addition to providing a storage mechanism, the String data type also includes a range of string manipulation features allowing strings to be searched, matched, concatenated and modified. Double quotes are used to surround single line strings during assignment, for example:

val message = "You have 10 new messages."

Alternatively, a multi-line string may be declared using triple quotes

val message = """You have 10 new messages,
                               5 old messages
               and 6 spam messages."""

The leading spaces on each line of a multi-line string can be removed by making a call to the trimMargin() function of the String data type:

val message = """You have 10 new messages,
                               5 old messages
               and 6 spam messages.""".trimMargin()

Strings can also be constructed using combinations of strings, variables, constants, expressions, and function calls using a concept referred to as string interpolation. For example, the following code creates a new string from a variety of different sources using string interpolation before outputting it to the console:

val username = "John"
val inboxCount = 25
val maxcount = 100
val message = "$username has $inboxCount message. Message capacity remaining is ${maxcount - inboxCount}"
 
println(message)

When executed, the code will output the following message:

John has 25 messages. Message capacity remaining is 75 messages.

Escape Sequences

In addition to the standard set of characters outlined above, there is also a range of special characters (also referred to as escape characters) available for specifying items such as a new line, tab or a specific Unicode value within a string. These special characters are identified by prefixing the character with a backslash (a concept referred to as escaping). For example, the following assigns a new line to the variable named newline:

var newline = '\n'

In essence, any character that is preceded by a backslash is considered to be a special character and is treated accordingly. This raises the question as to what to do if you actually want a backslash character. This is achieved by escaping the backslash itself:

var backslash = '\\'

The complete list of special characters supported by Kotlin is as follows:

  • \n - New line
  • \r - Carriage return
  • \t - Horizontal tab
  • \\ - Backslash
  • \” - Double quote (used when placing a double quote into a string declaration)
  • \’ - Single quote (used when placing a single quote into a string declaration)
  • \$ - Used when a character sequence containing a $ is misinterpreted as a variable in a string template.
  • \unnnn – Double byte Unicode scalar where nnnn is replaced by four hexadecimal digits representing the Unicode character.

Mutable Variables

Variables are essentially locations in computer memory reserved for storing the data used by an application. Each variable is given a name by the programmer and assigned a value. The name assigned to the variable may then be used in the Kotlin code to access the value assigned to that variable. This access can involve either reading the value of the variable or, in the case of mutable variables, changing the value. Immutable Variables

Often referred to as a constant, an immutable variable is similar to a mutable variable in that it provides a named location in memory to store a data value. Immutable variables differ in one significant way in that once a value has been assigned it cannot subsequently be changed.

Immutable variables are particularly useful if there is a value which is used repeatedly throughout the application code. Rather than use the value each time, it makes the code easier to read if the value is first assigned to a constant which is then referenced in the code. For example, it might not be clear to someone reading your Kotlin code why you used the value 5 in an expression. If, instead of the value 5, you use an immutable variable named interestRate the purpose of the value becomes much clearer. Immutable values also have the advantage that if the programmer needs to change a widely used value, it only needs to be changed once in the constant declaration and not each time it is referenced.

Declaring Mutable and Immutable Variables

Mutable variables are declared using the var keyword and may be initialized with a value at creation time. For example:

var userCount = 10

If the variable is declared without an initial value, the type of the variable must also be declared (a topic which will be covered in more detail in the next section of this chapter). The following, for example, is a typical declaration where the variable is initialized after it has been declared:

var userCount: Int
userCount = 42

Immutable variables are declared using the val keyword.

val maxUserCount = 20

As with mutable variables, the type must also be specified when declaring the variable without initializing it:

val maxUserCount: Int
maxUserCount = 20

When writing Kotlin code, immutable variables should always be used in preference to mutable variables whenever possible.

You are reading a sample chapter from the Android Studio 3.0 / Android 8 Edition book.

Purchase the fully updated Android Studio 3.2 / Android 9 / Jetpack Edition of this publication in eBook ($29.99) or Print ($45.99) format

Android Studio 3.2 Development Essentials - Kotlin Edition Print and eBook (ePub/PDF/Kindle) editions contain 96 chapters and over 800 pages

Buy Print Preview Book

Data Types are Objects

All of the above data types are actually objects, each of which provides a range of functions and properties that may be used to perform a variety of different type specific tasks. These functions and properties are accessed using so-called dot notation. Dot notation involves accessing a function or property of an object by specifying the variable name followed by a dot followed in turn by the name of the property to be accessed or function to be called.

A string variable, for example, can be converted to uppercase via a call to the toUpperCase() function of the String class:

val myString = "The quick brown fox"
val uppercase = myString.toUpperCase()

Similarly, the length of a string is available by accessing the length property:

val length = myString.length

Function are also available within the String class to perform tasks such as comparisons and checking for the presence of a specific word. The following code, for example, will return a true Boolean value since the word “fox” appears within the string assigned to the myString variable:

val result = myString.contains("fox")

All of the number data types include functions for performing tasks such as converting from one data type to another such as converting an Int to a Float:

val myInt = 10
val myFloat = myInt.toFloat()

A detailed overview of all of the properties and functions provided by the Kotlin data type classes is beyond the scope of this book (there are hundreds). An exhaustive list for all data types can, however, be found within the Kotlin reference documentation available online at: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/

Type Annotations and Type Inference

Kotlin is categorized as a statically typed programming language. This essentially means that once the data type of a variable has been identified, that variable cannot subsequently be used to store data of any other type without inducing a compilation error. This contrasts to loosely typed programming languages where a variable, once declared, can subsequently be used to store other data types.

There are two ways in which the type of a variable will be identified. One approach is to use a type annotation at the point the variable is declared in the code. This is achieved by placing a colon after the variable name followed by the type declaration. The following line of code, for example, declares a variable named userCount as being of type Int:

val userCount: Int = 10

In the absence of a type annotation in a declaration, the Kotlin compiler uses a technique referred to as type inference to identify the type of the variable. When relying on type inference, the compiler looks to see what type of value is being assigned to the variable at the point that it is initialized and uses that as the type. Consider, for example, the following variable declarations:

var signalStrength = 2.231
val companyName = "My Company"

During compilation of the above lines of code, Kotlin will infer that the signalStrength variable is of type Double (type inference in Kotlin defaults to Double for all floating point numbers) and that the companyName constant is of type String.

When a constant is declared without a type annotation it must be assigned a value at the point of declaration:

val bookTitle = "Android Studio Development Essentials"

If a type annotation is used when the constant is declared, however, the value can be assigned later in the code. For example:

val iosBookType = false
val bookTitle: String
 
if (iosBookType) {
         bookTitle = "iOS 10 App Development Essentials"
} else {
         bookTitle = "Android Studio Development Essentials"
}

Nullable Type

Kotlin nullable types are a concept that does not exist in most other programming languages (with the exception of the optional type in Swift). The purpose of nullable types is to provide a safe and consistent approach to handling situations where a variable may have a null value assigned to it. In other words, the objective is to avoid the common problem of code crashing with the null pointer exception errors that occur when code encounters a null value where one was not expected.

By default, a variable in Kotlin cannot have a null value assigned to it. Consider, for example, the following code:

val username: String = null

An attempt to compile the above code will result in a compilation error similar to the following:

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

If a variable is required to be able to store a null value, it must be specifically declared as a nullable type by placing a question mark (?) after the type declaration:

val username: String? = null

The username variable can now have a null value assigned to it without triggering a compiler error. Once a variable has been declared as nullable, a range of restrictions are then imposed on that variable by the compiler to prevent it being used in situations where it might cause a null pointer exception to occur. A nullable variable, cannot, for example, be assigned to a variable of non-null type as is the case in the following code:

You are reading a sample chapter from the Android Studio 3.0 / Android 8 Edition book.

Purchase the fully updated Android Studio 3.2 / Android 9 / Jetpack Edition of this publication in eBook ($29.99) or Print ($45.99) format

Android Studio 3.2 Development Essentials - Kotlin Edition Print and eBook (ePub/PDF/Kindle) editions contain 96 chapters and over 800 pages

Buy Print Preview Book

val username: String? = null
val firstname: String = username

The above code will elicit the following error when encountered by the compiler:

Error: Type mismatch: inferred type is String? but String was expected

The only way that the assignment will be permitted is if some code is added to check that the value assigned to the nullable variable is non-null:

val username: String? = null
 
if (username != null) {
         val firstname: String = username
}

In the above case, the assignment will only take place if the username variable references a non-null value.

The Safe Call Operator

A nullable variable also cannot be used to call a function or to access a property in the usual way. Earlier in this chapter the toUpperCase() function was called on a String object. Given the possibility that this will cause a function to be called on a null reference, the following code will be disallowed by the compiler:

val username: String? = null
val uppercase = username.toUpperCase()

The exact error message generated by the compiler in this situation reads as follows:

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

In this instance, the compiler is essentially refusing to allow the function call to be made because no attempt has been made to verify that the variable is non-null. One way around this to add some code to verify that something other than null has been assigned to the variable prior to making the function call:

if (username != null) {
         val uppercase = username.toUpperCase()
}

A much more efficient way to achieve this same verification, however, is to call the function using the safe call operator (represented by ?.) as follows:

val uppercase = username?.toUpperCase()

In the above example, if the username variable is null, the toUpperCase() function will not be called and execution will proceed at the next line of code. If, on the other hand, a non-null value is assigned the toUpperCase() function will be called and the result assigned to the uppercase variable.

In addition to function calls, the safe call operator may also be used when accessing properties:

val uppercase = username?.length

Not-Null Assertion

The not-null assertion removes all of the compiler restrictions from a nullable type, allowing it to be used in the same ways as a non-null type, even if it has been assigned a null value. This assertion is implemented using double exclamation marks after the variable name, for example:

val username: String? = null
val length = username!!.length

The above code will now compile, but will crash with the following exception at runtime since an attempt is being made to call a function on a non existent object:

Exception in thread "main" kotlin.KotlinNullPointerException

Clearly, this causes the very issue that nullable types are designed to avoid. Use of the not-null assertion is generally discouraged and should only be used in situations where you are certain that the value will not be null.

Nullable Types and the let Function

Earlier in this chapter we looked at how the safe call operator can be used when making a call to a function belonging to a nullable type. This technique makes it easier to check if a value is null without have to write an if statement every time the variable is accessed. A similar problem occurs when passing a nullable type as an argument to a function which is expecting a non-null parameter. As an example, consider the times() function of the Int data type. When called on an Int object and passed another integer value as an argument, the function multiplies the two values and returns the result. When the following code is executed, for example, the value of 200 will be displayed within the console:

val firstNumber = 10
val secondNumber = 20
 
val result = firstNumber.times(secondNumber)
print(result)

The above example works because the secondNumber variable is a non-null type. A problem, however, occurs if the secondNumber variable is declared as being of nullable type:

val firstNumber = 10
val secondNumber: Int? = 20
 
val result = firstNumber.times(secondNumber)

print(result)

Now the compilation will fail with the following error message because a nullable type is being passed to a function that is expecting a non-null parameter:

Error: Type mismatch: inferred type is Int? but Int was expected

A possible solution to this problem is to simply write an if statement to verify that the value assigned to the variable is non-null before making the call to the function:

val firstNumber = 10
val secondNumber: Int? = 20
 
if (secondNumber != null) {
    val result = firstNumber.times(secondNumber)
    print(result)
}

A more convenient approach to addressing the issue, however, involve use of the let function. When called on a nullable type object, the let function converts the nullable type to a non-null variable named it which may then be referenced within a lambda statement.

secondNumber?.let {
    val result = firstNumber.times(it)
    print(result)
}

Note the use of the safe call operator when calling the let function on secondVariable in the above example. This ensures that the function is only called when the variable is assigned a non-null value.

The Elvis Operator

The Kotlin Elvis operator can be used in conjunction with nullable types to define a default value that is to be returned in the event that a value or expression result is null. The Elvis operator (?:) is used to separate two expressions. If the expression on the left does not resolve to a null value that value is returned, otherwise the result of the rightmost expression is returned. This can be thought of as a quick alternative to writing an if-else statement to check for a null value. Consider the following code:

if (myString != null) {
    return myString
} else {
    return "String is null"
}

The same result can be achieved with less coding using the Elvis operator as follows:

return myString ?: "String is null"

Type Casting and Type Checking

When compiling Kotlin code, the compiler can typically infer the type of an object. Situations will occur, however, where the compiler is unable to identify the specific type. This is often the case when a value type is ambiguous or an unspecified object is returned from a function call. In this situation it may be necessary to let the compiler know the type of object that your code is expecting or to write code that checks whether the object is of a particular type.

Letting the compiler know the type of object that is expected is known as type casting and is achieved within Kotlin code using the as cast operator. The following code, for example, lets the compiler know that the result returned from the getSystemService() method needs to be treated as a KeyguardManager object:

val keyMgr = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager

The Kotlin language includes both safe and unsafe cast operator. The above cast is an unsafe cast and will cause the app to throw and exception if the cast cannot be performed. A safe cast, on the other hand, uses the as? operator and returns null if the cast cannot be performed:

val keyMgr = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager

A type check can be performed to verify that an object conforms to a specific type using the is operator, for example:

if (keyMgr is KeyguardManager) {
     // It is a KeyguardManager object
}

Summary

This chapter has begun the introduction to Kotlin by exploring data types together with an overview of how to declare variables. The chapter has also introduced concepts such as nullable types, type casting and type checking and the Elvis operator, each of which is an integral part of Kotlin programming and designed specifically to make code writing less prone to error.


You are reading a sample chapter from the Android Studio 3.0 / Android 8 Edition book.

Purchase the fully updated Android Studio 3.2 / Android 9 / Jetpack Edition of this publication in eBook ($29.99) or Print ($45.99) format

Android Studio 3.2 Development Essentials - Kotlin Edition Print and eBook (ePub/PDF/Kindle) editions contain 96 chapters and over 800 pages

Buy Print Preview Book



PreviousTable of ContentsNext
An Introduction to KotlinKotlin Operators and Expressions