Kotlin Data Types, Variables and Nullability

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 https://play.kotlinlang.org 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. 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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val mynumber = 10Code language: Kotlin (kotlin)

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:

1010Code language: plaintext (plaintext)

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'Code language: Kotlin (kotlin)

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:

10101100011Code language: plaintext (plaintext)

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).

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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.

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val myChar1 = 'f'
val myChar2 = ':'
val myChar3 = 'X'Code language: Kotlin (kotlin)

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

val myChar4 = '\u0058'Code language: Kotlin (kotlin)

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."Code language: Kotlin (kotlin)

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."""Code language: Kotlin (kotlin)

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val message = """You have 10 new messages,
                               5 old messages
               and 6 spam messages.""".trimMargin()Code language: Kotlin (kotlin)

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 messages. Message capacity remaining is ${maxcount - inboxCount} messages"
 
println(message)Code language: Kotlin (kotlin)

When executed, the code will output the following message:

John has 25 messages. Message capacity remaining is 75 messages.Code language: plaintext (plaintext)

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'Code language: Kotlin (kotlin)

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 = '\\'Code language: Kotlin (kotlin)

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

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

  • \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 that 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 = 10Code language: Kotlin (kotlin)

If the variable is declared without an initial value, the type of the variable must also be declared (a topic that 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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

var userCount: Int
userCount = 42Code language: Kotlin (kotlin)

Immutable variables are declared using the val keyword.

val maxUserCount = 20Code language: Kotlin (kotlin)

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

val maxUserCount: Int
maxUserCount = 20
Code language: Kotlin (kotlin)

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

Data types are objects

All of the above data types are 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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val myString = "The quick brown fox"
val uppercase = myString.toUpperCase()Code language: Kotlin (kotlin)

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

val length = myString.lengthCode language: Kotlin (kotlin)

Functions 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")Code language: Kotlin (kotlin)

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()Code language: Kotlin (kotlin)

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/

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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 with 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 = 10Code language: Kotlin (kotlin)

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"Code language: Kotlin (kotlin)

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val bookTitle = "Android Studio Development Essentials"Code language: Kotlin (kotlin)

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 App Development Essentials"
} else {
         bookTitle = "Android Studio Development Essentials"
}Code language: Kotlin (kotlin)

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 = nullCode language: Kotlin (kotlin)

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 StringCode language: plaintext (plaintext)

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val username: String<strong>?</strong> = nullCode language: Kotlin (kotlin)

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:

val username: String? = null
val firstname: String = usernameCode language: Kotlin (kotlin)

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

Error: Type mismatch: inferred type is String? but String was expectedCode language: plaintext (plaintext)

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
}Code language: Kotlin (kotlin)

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 could cause a function to be called on a null reference, the following code will be disallowed by the compiler:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val username: String? = null
val uppercase = username.toUpperCase()Code language: Kotlin (kotlin)

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?Code language: plaintext (plaintext)

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 is to add some code to verify that something other than null value has been assigned to the variable before making the function call:

if (username != null) {
         val uppercase = username.toUpperCase()
}Code language: Kotlin (kotlin)

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()Code language: Kotlin (kotlin)

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val uppercase = username?.lengthCode language: Kotlin (kotlin)

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!!.lengthCode language: Kotlin (kotlin)

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.KotlinNullPointerExceptionCode language: plaintext (plaintext)

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 having 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)Code language: Kotlin (kotlin)

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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val firstNumber = 10
val secondNumber: Int? = 20
 
val result = firstNumber.times(secondNumber)
print(result)Code language: Kotlin (kotlin)

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 expectedCode language: plaintext (plaintext)

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)
}Code language: Kotlin (kotlin)

A more convenient approach to addressing the issue, however, involves the 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)
}Code language: Kotlin (kotlin)

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.

Late initialization (lateinit)

As previously outlined, non-null types need to be initialized when they are declared. This can be inconvenient if the value to be assigned to the non-null variable will not be known until later in the code execution. One way around this is to declare the variable using the lateinit modifier. This modifier designates that a value will be initialized with a value later. This has the advantage that a non-null type can be declared before it is initialized, with the disadvantage that the programmer is responsible for ensuring that the initialization has been performed before attempting to access the variable. Consider the following variable declaration:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

var myName: StringCode language: Kotlin (kotlin)

Clearly, this is invalid since the variable is a non-null type but has not been assigned a value. Suppose, however, that the value to be assigned to the variable will not be known until later in the program execution. In this case, the lateinit modifier can be used as follows:

lateinit var myName: StringCode language: Kotlin (kotlin)

With the variable declared in this way, the value can be assigned later, for example:

myName = "John Smith"
print("My Name is " + myName)Code language: Kotlin (kotlin)

Of course, if the variable is accessed before it is initialized, the code will fail with an exception:

lateinit var myName: String
 
print("My Name is " + myName)
 
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property myName has not been initializedCode language: Kotlin (kotlin)
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property myName has not been initializedCode language: plaintext (plaintext)

To verify whether a lateinit variable has been initialized, check the isInitialized property on the variable. To do this, we need to access the properties of the variable by prefixing the name with the ‘::’ operator:

if (::myName.isInitialized) {
    print("My Name is " + myName)
}Code language: Kotlin (kotlin)

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 if 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:

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

if (myString != null) {
    return myString
} else {
    return "String is null"
}Code language: Kotlin (kotlin)

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

return myString ?: "String is null" Code language: Kotlin (kotlin)

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 KeyguardManagerCode language: Kotlin (kotlin)

The Kotlin language includes both safe and unsafe cast operators. The above cast is an unsafe cast and will cause the app to throw an 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? KeyguardManagerCode language: Kotlin (kotlin)

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

 

You are reading a sample chapter from Jetpack Compose 1.5 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

if (keyMgr is KeyguardManager) {
     // It is a KeyguardManager object
}Code language: Kotlin (kotlin)

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.


Categories