Hunt2021 Book BeginnerSGuideToKotlinProgramm
Hunt2021 Book BeginnerSGuideToKotlinProgramm
Hunt
Beginner’s
Guide
to Kotlin
Programming
Beginner’s Guide to Kotlin Programming
John Hunt
© The Editor(s) (if applicable) and The Author(s), under exclusive license to Springer Nature
Switzerland AG 2021
This work is subject to copyright. All rights are solely and exclusively licensed by the Publisher, whether
the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse
of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and
transmission or information storage and retrieval, electronic adaptation, computer software, or by similar
or dissimilar methodology now known or hereafter developed.
The use of general descriptive names, registered names, trademarks, service marks, etc. in this publication
does not imply, even in the absence of a specific statement, that such names are exempt from the relevant
protective laws and regulations and therefore free for general use.
The publisher, the authors and the editors are safe to assume that the advice and information in this book
are believed to be true and accurate at the date of publication. Neither the publisher nor the authors or
the editors give a warranty, expressed or implied, with respect to the material contained herein or for any
errors or omissions that may have been made. The publisher remains neutral with regard to jurisdictional
claims in published maps and institutional affiliations.
This Springer imprint is published by the registered company Springer Nature Switzerland AG
The registered company address is: Gewerbestrasse 11, 6330 Cham, Switzerland
For my son, Adam. I could not be more proud
of you and the man you have grown up to be.
Preface
So you are interested in Kotlin; you are not alone! Kotlin is now the second most
popular programming language on the Java Virtual Machine (JVM). Only Java is
more popular, with Scala and Clojure now significantly behind Kotlin in popularity.
The aim of this book is to introduce Kotlin and Android development to those
with limited programming experience via a set of examples developed as the book
progresses.
As such this book is divided into five parts; Part I introduces the functional side
of the Kotlin programming language, Part II develops the object-oriented features
in the Kotlin language, and Part III considers collections or container data types
in Kotlin. Part IV expands on this with some of the features in Kotlin that allow
concurrent programs to be written. The final part of the book, Part V, introduces
Android Development using Kotlin.
Some of the key aspects of this book are as follows:
1. It assumes very little knowledge or experience of Kotlin or programming.
2. It provides a basic introduction to Kotlin but expands on those to end on advanced
topics such as generic types and co-routines.
3. Kotlin’s support for functional programming is presented.
4. Following on from introducing the basic ideas behind functional programming,
the book presents how advanced functional concepts such as closures, currying
and higher-order functions work in Kotlin.
5. The book also provides extensive coverage of object orientation and the features
in Kotlin supporting classes, inheritance and interfaces.
6. The book includes exercises at the end of most chapters with online solutions.
7. Android mobile application development using Kotlin is also introduced. The
focus is on the core concepts used within Android development such as
Activities, Fragments and Views.
8. All code examples (and exercise solutions) are provided online in a GitHub
repository.
vii
viii Preface
Chapter Organization
Each chapter has a brief introduction, the main body of the chapter, followed by a
list of (typically) online references that can be used for further reading.
Following this, there is typically an Exercises section that lists one or more
exercises that build on the skills you will have learnt in that chapter.
Sample solutions to the exercises are available in a GitHub online repository that
supports this book.
You can of course just read this book; however, following the examples in this book
will ensure that you get as much as possible out of the content.
For this, you will need a computer.
Kotlin is a cross platform programming language and as such you can use Kotlin
on a Windows PC, a Linux box or an Apple Mac, etc. So you are not tied to a particular
type of operating system; you can use whatever you have available. However you
will need to install some software on that computer. At a minimum, you will need
Kotlin and editor to write your programs in.
You will also need some form of editor in which to write your programs. There
are numerous generic programming editors available for different operating systems
with VIM on Linux, Notepad++ on Windows and Sublime Text on Windows and
Macs being popular choices.
Using an integrated development environment (IDE) editor such as IntelliJ IDEA,
however, will make writing and running your programs much easier.
Using an IDE
A very widely used IDE for Kotlin is IntelliJ IDEA (and for Android development
its cousin the Android Studio IDE); it is not the only IDE for Kotlin by any means,
but it is the most popular.
Other IDEs available for Kotlin include
• Visual Studio Code: This is a very good free editor from Microsoft that has really
useful features https://code.visualstudio.com.
• Sublime Text is more of a text editor that color codes Kotlin; however, for a simple
project it may be all you need https://www.sublimetext.com.
• Eclipse with the Kotlin plugin see https://marketplace.eclipse.org/content/kotlin-
plugin-eclipse.
Preface ix
IntelliJ is provided by JetBrains who make tools for a variety of different languages.
The IntelliJ IDEA IDE can be downloaded from their site—see https://www.jetbra
ins.com/. Look for the menu heading ‘Tools’ and select that. You will see a long list
of tools, which should include IntelliJ IDEA.
Select the IntelliJ IDEA option. The resulting page has a lot of information on
it; however, you only need to select the ‘DOWNLOAD’ option. Make sure that you
select the operating system you use (there are options for Windows, macOS and
Linux).
There are then two download options available: Professional and Community. The
Professional version is charged for option while the Community version is free. For
most of the work I do in Kotlin, the Community version is more than adequate, and
it is therefore the version we will download and install (note with the Professional
version you do get a free trial but will need to either pay for the full version at the
end of the trial or reinstall the Community version at that point).
Assuming you selected the Community edition, the installer will now download,
and you will be prompted to run it. Note you can ignore the request to subscribe if
you want.
You can now run the installer and follow the instructions provided.
Conventions
Throughout this book, you will find a number of conventions used for text styles.
These text styles distinguish different kinds of information.
Code words, variables and Kotlin values, used within the main body of the text,
are shown using a Courier font. For example:
This program creates a top-level activity (the MainActivity). This activity can be
used to display Fragments (Fragment) that act as sub-windows displayed within
the activity.
In the above paragraph, MainActivity and Fragment are classes available in a
Kotlin Android library.
A block of Kotlin code is set out as shown here:
print("Enter an integer: ")
val input = Scanner(System.`in`)
val num = input.nextInt()
if (num > 0) {
println("$num is positive")
println("$num squared is ${num * num}")
}
Note that keywords and strings are shown in bold font.
Any command line or user input is shown in italics and colored purple:
> kotlinc hello.kt
x Preface
Or
Hello, world
Enter your name: John
Hello John
The examples used in this book (along with sample solutions for the exercises at the
end of most chapters) are available in a GitHub repository. GitHub provides a web
interface to, and a server environment hosting, Git.
Git is a version control system typically used to manage source code files (such
as those used to create systems in programming languages such as Kotlin but also
Python, Java, C#, C++ and Scala). Systems such as Git are very useful for collab-
orative development as they allow multiple people to work on an implementation
and to merge their work together. They also provide a useful historical view of the
code (which also allows developers to roll back changes if modifications prove to be
unsuitable).
If you already have Git installed on your computer, then you can clone (obtain a
copy of) the repository locally using:
git clone https://github.com/johnehunt/kotlin-book.git
If you do not have Git, then you can obtain a zip file of the examples using
https://github.com/johnehunt/kotlin-book/archive/mas
ter.zip
It is of course possible to install Git yourself if you wish. To do this, see https://git-
scm.com/downloads. Versions of the Git client for macOS, Windows and Linux/Unix
are available here.
However, many IDEs such as IntelliJ IDEA come with Git support and so offer
another approach to obtaining a Git repository.
For more information on Git, see http://git-scm.com/doc. This Git guide provides
a very good primer and is highly recommended.
If you have any questions regarding the examples, please contact
john.hunt10@gmail.com.
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
What is Kotlin? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Kotlin Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Kotlin Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Kotlin Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Kotlin Execution Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Where is Kotlin Used . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Kotlin JVM Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Kotlin Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Running Kotlin Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Interactively Using the Kotlin REPL Interpreter . . . . . . . . . . . . . . . . . . 7
Running a Kotlin File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Executing a Kotlin Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Using Kotlin in an IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Useful Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
xi
xii Contents
Comments in Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Basic Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Unsigned Integer Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Numeric Conversions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3 Flow of Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Comparison Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Numeric Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Assignment Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
The if Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Working with an if Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Else in an if Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
The Use of Else If . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Kotlin if Expression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Nesting if Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Null and Conditional Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Explicitly Checking for Null Conditions . . . . . . . . . . . . . . . . . . . . . . . . 43
The Safe Call Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
The Elvis Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
The Not-Null Assertion Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Smart Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Safe Casting to Non Null Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
When Expression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
While Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Do Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
For Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Repeat Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Loop Control Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Break Loop Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Continue Loop Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
When with Continue and Break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
A Note on Loop Variable Naming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Dice Roll Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Check Input is Positive or Negative . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Test if a Number is Odd or Even . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Kilometres to Miles Converter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Calculate the Factorial of a Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Contents xiii
Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
16 Nested/Inner Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
What are Nested/Inner Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
Nested Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Member Inner Classes/Anonymous Objects . . . . . . . . . . . . . . . . . . . . . . . . 265
Member Function Inner Classes/Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Function Inner Classes/Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
17 Data Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Defining Data Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Data Classes with Non Constructor Properties . . . . . . . . . . . . . . . . . . . . . . 273
Data Classes and Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Data Classes and Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Copying Data Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Data Classes and De-structuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
18 Sealed and Inline Classes and Typealias . . . . . . . . . . . . . . . . . . . . . . . . . 279
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Sealed Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Sealed Classes in Kotlin 1.4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Sealed Classes and Interfaces in Kotlin 1.5 . . . . . . . . . . . . . . . . . . . . . . 281
Inline Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
Inline Class Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
Simple Inline Class Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Additional Inline Class Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
Typealias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
19 Operator Overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Why Have Operator Overloading? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Why Not Have Operator Overloading? . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Implementing Operator Overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Overloading the Operator Member Functions . . . . . . . . . . . . . . . . . . . . 290
Mutable Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Numerical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
Unary Prefix Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
Postfix Increment and Decrement Operators . . . . . . . . . . . . . . . . . . . . . . . . 294
Comparison Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Augmented Assignment Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
Containment (in) Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Infix Named Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Contents xix
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
20 Error and Exception Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Errors and Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
What is an Exception? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
What Is Exception Handling? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Handling an Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
Multiple Catch Blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
Accessing the Exception Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Jumping to Exception Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
The Finally Clause . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Throwing an Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
Try Catch as an Expression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
Defining an Custom Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Chaining Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
Nesting Exception Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
Limitations of Try-Catch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
Functional Exception Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
Accessing the Failure Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Accessing the Successful Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Treating Result as a Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Recovery Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Providing Explicit OnFailure and OnSuccess Behaviour . . . . . . . . . . . 325
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
21 Extension Functions and Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Extension Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Extension Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Infix Extension Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
Extensions Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
25 Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Creating Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Set Properties and Member Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
Accessing Elements in a Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Working with Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Checking for Presence of an Element . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Adding Items to a Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Removing an Item . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
Nesting Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Set like Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
26 Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Creating a Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Map Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Working with Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Accessing Items via Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Adding a New Entry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
Changing a Keys Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Removing an Entry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Values, Keys and Items . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
Iterating Over Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
Iterating Over Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
Iterating Over Map Entries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
Checking Key Membership . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
Nesting Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
27 Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Creating Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Creating Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Creating Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
Working with Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Accessing Elements of a Pair or Triple . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Copying Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Converting Pairs and Tiples to Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
Destructuring Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
Nested Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
Things You Can’t Do with Pairs and Triples . . . . . . . . . . . . . . . . . . . . . . . . 392
xxii Contents
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392
28 Generic Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
The Generic Set Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
Writing a Generic Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
Declaration-Site Variance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Generic Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
Generic Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
Creating a Parameterised Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
29 Functional Programming and Containers . . . . . . . . . . . . . . . . . . . . . . . 405
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
ForEach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
Flatten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
FlatMap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Sorting Iterables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
Fold . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414
FoldRight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
Reduce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Labelled Return from Lambdas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Online Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
What is Kotlin?
6. The ability to run Kotlin natively on the host operating system which has
potential performance benefits.
7. It is free!
Kotlin itself is now looked after by the Kotlin Foundation. Originally most of
the development costs associated with maintaining and developing the language are
borne by JetBrains although other organisations such as Google are increasingly
becoming involved with the language and the Kotlin Foundation.
Kotlin Versions
There have been several major version of Kotlin over the years. A brief history of
the releases of the Kotlin language is given below:
• July 2011 JetBrains unveils project Kotlin.
– Feb 2012 JetBrains open source Kotlin under Apache 2 license.
• Kotlin v 1.0 released 15th Feb 2016.
• 2017 Google announced first-class support for Kotlin on Android.
• Kotlin v1.1 released 1st March 2017.
– introduced support for JavaScript runtime.
• Kotlin v1.2 released 28th Nov 2017.
• Kotlin v1.3 released 29th Oct 2018.
– introduced coroutines for asynchronous programming.
• 7th May 2019 Google makes Kotlin its preferred language for Android develop-
ment.
• 17th Aug 2020 Kotlin 1.4.
• 5th May 2021 Kotlin 1.5.
This raises the question which version of Kotlin to use. In this book we are
focussing on Kotlin 1.4/1.5 as those are the current releases of Kotlin at the time of
writing.
Kotlin Programming
if statements and loop constructs are used to manage which steps are executed
and how many times. Languages typifying this approach include C and Pascal.
• Declarative Programming languages that allow developers to describe how a
problem should be solved. The language/environment then determines how the
solution should be implemented. SQL (a database query language) is one of the
most common declarative languages that you are likely to encounter.
• Object Oriented Programming is an approach that represent a system in terms
of the objects that form that system. Each object can hold its own data (also
known as state) as well as implement behaviour that defines what the object can
do. An object oriented program is formed from a set of these objects co-operating
together. Languages such as Java and C# typify the object oriented approach.
• Functional Programming languages decompose a problem into a set of func-
tions. Each function is independent of any external state operating only on
the inputs they received to generate their outputs. The programming languages
Haskell and Clojure are examples of a functional programming language.
Some programming languages are considered to be hybrid languages; that is they
allow developers to utilise a combination of difference approaches within the same
program. Kotlin is an example of a hybrid programming language as it allows the
developer to write very procedural code, to use objects in an object oriented manner
and to write functional programs. Elements of each of these programming styles are
covered in this book.
Kotlin Libraries
As well as the core language, there are numerous libraries available for Kotlin. These
libraries extend the functionality of the language and make it much easier to develop
applications. These libraries cover.
• RESTful Web Service frameworks such as Ktor.
• Database access libraries.
• Concurrency libraries.
• Logging frameworks allowing you to record information during the execution of
an application that allows for detailed analysis at a later date.
• Testing and mocking libraries.
• Machine Learning and Data Analysis libraries.
• Mobile application development libraries.
• Image compression libraries.
• Games libraries such as KTX.
A very useful resource to look at, which introduces many of these libraries is
https://www.kotlinresources.com/.
4 1 Introduction
The core part of the language is Common Kotlin and this is the part of the language
that is available on all platforms on which Kotlin executes.
However, Kotlin can be executed on JVM, JavaScript or Native runtimes. Each
of these provides access to underlying facilities. For example, when running on
the Kotlin/JVM platform it is possible to access any and all Java libraries. In turn
when running on the Kotlin/JS platform it is possible to access underlying JavaScript
libraries. These platforms are executed by an underlying runtime such as the JVM
(Java Virtual Machine) or Node.js (a JavaScript runtime environment) or using the
JavaScript runtime of a Web Browser.
Kotlin/Native is slightly different in that it is a technology used to compile Kotlin
to native binaries that are run directly on the underlying operating system and do not
need another runtime environment.
Given that there are several different ways in which Kotlin can be executed, what
platforms are commonly used? The answer is that the Kotlin/JVM platform is the
most widely used platform by far, with minimal current uptake for Kotlin/JS and
Kotlin/Native (at the time of writing). Ae Kotlin survey in 2020 rlooked at the plat-
forms targeted by Kotlin developers. This found that 60% of the developers surveyed
were using the JVM runtime and 60% were targeting Android. Only 7% were using
the Kotlin Native runtime and only 6% were using the JavaScript runtime. This indi-
cates that the majority of developers are using Kotlin on the JVM platform or on the
Android mobile operating system.
Where is Kotlin Used 5
However, this only illustrates part of the story, the same survey also found that
Kotlin developers are nearly equally split between working on Android mobile appli-
cations (57%) and backend services (47% probably accessed from those mobile
applications). Only 10% of Kotlin developers are developing desktop applications
with just 6% developing Kotlin based web front ends.
The above diagram is more complicated (and slightly different) for Kotlin Android
application; however this will be discussed in Part 5 of this book which explores
running Kotlin on Android devices.
Kotlin Documentation
As previously noted, there are versions of Kotlin that can run on the JVM, as
JavaScript (or JS) using Node.js and natively on the underlying operating system.
Each of these versions have some differences as well as Common aspects. This is
reflected in the online documentation available for Kotlin. For example, the following
image shows the type of buttons displayed across the top of the documentation web
pages:
There are several ways in which you can run a Kotlin program, including
• Interactively using the Kotlin REPL (or Read Evaluate Print Loop) interpreter.
• Compiled to a class file and run using the java command with appropriate Kotlin
libraries.
• Compiled into a class file and run using the kotlin command.
Running Kotlin Programs 7
It is possible to use Kotlin in interactive mode. This uses the Kotlin REPL (named
after Read Evaluate Print Loop style of operation).
Using the REPL, Kotlin statements and expressions can be typed into the Kotlin
prompt and will then be executed directly. The values of variables will be remembered
and may be used later in the session.
To run the Kotlin REPL, Kotlin must have been installed onto the computer
system you are using. Depending on your platform this can be done in several ways,
for example on an Apple Mac you can use Homebrew:
While on a Window machine you can download the kotlin-compiler zip from
GitHub and add the bin directory to the system PATH.
Once installed you can open a Command Prompt window (Windows) or a Terminal
window (MacOS) and type kotlinc-jvm into the prompt. This is shown for an
Apple Mac computer below:
In the above example, we interactively typed in several Kotlin commands and the
Kotlin interpreter ‘Read’ what we have typed in, ‘Evaluated ‘it (worked out what it
should do), ‘Printed’ the result and then ‘Looped’ back ready for further input. In
this case we
8 1 Introduction
We can of course store the Kotlin commands into a file. This creates a program file
that can then be run as an argument to the java or kotlin command.
For example, the following file (called hello.kt) contains the 4
commands/statements defined within a special function called main(). Don’t
worry about this for the moment; it is only needed to indicate to the runtime JVM
environment where the main entry point to your program is.
To run the hello.kt program you must first compile it into the byte code format
understood by the JVM. This is done using either the javac command (with the
appropriate Kotlin libraries) or by using the kotlinc compiler command directly.
For example:
Running Kotlin Programs 9
In this case we are compiling the hello.kt file and including the
-include-runtime and -d options. The -include-runtime option ensures
that the output of the compiler includes all Kotlin dependencies required to run the
file. The -d option provides the destination for the compiled code; which in this case
is the hello.jar file. This is a file that contains one or more class files wrapped
up in a format that the JVM understands. Jar files are just a convenient way to group
together all the class files needed to run our hello world application.
We can now run the program contained within the hello.jar file using the
JVM. The JVM is represented by the command java:
We can now run this Kotlin script using the kotlinc command with the
-script option:
Another scripting option is to explicitly define the Kotlin file as a Script file (this
option has been available since 2016). This requires that a special line to be added to
the start of the Kotlin file that indicates the Kotlin command (or interpreter) to use
with the rest of the file. This line must start with ‘#!’ (also known as a shebang or
sha-bang and even hashing) and must come at the start of the file.
To convert the previous sections file into a Script we would need to add a reference
to the Kotlin script interpreter. For example:
Running Kotlin Programs 11
However, we cannot just run the file as it stands. If we tried to run the file without
any changes then we will get an error indicating that the permission to execute the
file has been denied:
$ ./hello.kts
-bash: ./hello.kts: Permission denied
$
This is because by default you can’t just run a file. We need to mark it as executable.
There are several ways to do this, however one of the easiest on a Mac or Linux box is
to use the chmod command (which can be used to modify the permissions associated
with the file). To make the file executable we can change the file permissions to include
making it executable by using the following command from a terminal window when
we are in the same directory as the hello.kts file:
$ chmod +x hello.kts
Where +x indicates that we want to add the executable permission to the file.
Now if we try to run the file directly it executes and the results of the commands
within the file are printed out:
Note the use of the ‘./’ preceding the file name in the above; this is used on
Linux/Unix computers to tell the operating system to look in the current directory
for the file to execute.
12 1 Introduction
We can also use an IDE such as IntelliJ IDEA to write and execute our Kotlin
programs. A simple program is shown using IntelliJ below:
In the above figure the same set of commands are again listed in a file called
hello.py. However, the program has been run from within the IDE and the output
is shown in an output console at the bottom of the display.
Useful Resources
There are a wide range of resources on the web for Kotlin; we will highlight a few here
that you should bookmark. We will not keep referring to these to avoid repetition,
but you can refer back to this section whenever you need to:
• https://kotlinlang.org/ The main Kotlin home page.
• https://kotlinlang.org/foundation/kotlin-foundation.html Kotlin Foundation.
• https://kotlinlang.org/docs/reference/ The main Kotlin documentation site. It
contains tutorials, library references, set up and installation guides as well as
Kotlin how-tos.
• https://kotlinlang.org/user-groups/user-group-list.html List of Kotlin User Groups
world wide.
Part I
Kotlin Programming
Chapter 2
A First Kotlin Program
Introduction
In this chapter we will look at a simple Kotlin program and understand what it is
doing. We will also modify it to become more interactive and will explore the concept
of Kotlin variables (specifically vals and vars).
Hello World
fun main() {
println("Hello World!")
}
This defines the main function; that is the entry point to the application. The main
function is defined using the keyword fun which precedes the name of the function;
in this case main.
The body of the function (what the function does) is defined between the opening
and closing curly brackets ({}). In this case all this does is call the println() function
passing in the string “Hello World!”. This function is used to generate a line of output
from the program.
You can use any text editor or IDE (Integrated Development Editor) to create a
Kotlin file. Examples of editors commonly used with Kotlin include Emacs, Vim,
Notepad++, Sublime Text or Visual Studio Code; examples of IDEs for Kotlin include
IntelliJ IDEA and Eclipse. Using any of these tools we can create file with a .kt
extension. Such a file can contain one or more Kotlin statements that represent a
Kotlin program. Note that any file containing Kotlin code must always have a .kt
extension.
For example, we can create a file called hello.kt containing the above main()
function in it.
To write our hello world program we will create a new Kotlin file within the IntelliJ
IDE. Of course, you don’t have to use IntelliJ for this, but we will assume you are; if
you are using a different editor or IDE then you may have to look up the equivalent
steps.
Starting up Kotlin
The first thing to do is to start up your IntelliJ IDE if it is not already running. When
you do this it will ask you whether you want to create a new project, open an existing
project or obtain a project from VCS (Version Control System), for example:
Now click ‘Next’. You will now see information on the main module. A Kotlin
application can be comprised of one or more modules. A module is a group of
associated features that might be built and distributed together. We will be using a
single module for our application, you can therefore just click ‘Finish’.
You should now see an empty editor window displayed to you.
We will now create a new Kotlin file to hold your program. To do this open the src
node in the left-hand Project view by clicking on the arrow next to the src label.
Then continue to expand the tree of nodes down to the directory shown in blue called
kotlin. This is where we will define our application. Once you have selected the
kotlin node use the right mouse button menu. On this menu select ‘New’ followed
by ‘Kotlin Class/File’, see below:
18 2 A First Kotlin Program
First select the File option which is the second option down below the input field in
the ‘New Kotlin Class/File’ dialog. When you do this, you will prompted to provide
a name for the Kotlin file; you can call the file whatever you like, but I am using the
name ‘main’ as that is descriptive as it indicates it will hold the main entry point to
my application and will help me find this file again later:
Now click OK. Notice I did not need to add .kt to the end of the filename; IntelliJ
will do that for me (if you did add .kt then don’t worry IntelliJ will be fine with that
as well).
The first thing we will do is create the main function for our hello world application.
This is shown below:
fun main() {
println("Hello World!")
}
Hello World 19
One question this raises is where does the println() function come from?
In fact, println() is a predefined function that can be used to print things
out followed by a carriage return/newline (there is also a print() function that
merely prints things out—it does not include the carriage return/newline). On the
JVM runtime it actually maps to the underlying JVM System.out.println()
operation.
The output is actually printed to what is known as the standard output stream.
This handles a stream (sequence) of data such as letters and numbers. This output
stream of data can be sent to an output window such as the Terminal on a Mac or
Command window on a Windows PC. In this case we are printing the string “Hello
World”.
By predefined here we mean that it is built into the Kotlin environment and is
understood by the JVM runtime system This means that the JVM knows where to
find the definition of the println() function which tells it what to do when it
encounters that function.
You can of course write your own functions and we will be looking at how to do
that later in this book.
The println() function actually tries to print whatever you give it,
• when it is given a string it will print a string,
• if it is given an integer such as 42 it will print 42,
• if it is a given a real number such as 23.56 then it will print 23.56,
• If a boolean value (true or false) is given that it will print out true or false.
Also note that the text forming the Hello World string is wrapped within two
double quote characters; these characters delimit the start and end of the string; if
you miss one of them out then there will be an error.
To run the program, if you are using an IDE such as IntelliJ, then you can select
the green arrow next to the line with fun main() {on it as this will run your application
for you. Alternatively you can select the file in the left hand tree and from the right
mouse button menu where you can select Run. If you want to run this program again
you can merely click on the green arrow in the tool bar at the top of the IDE.
The result of running this very simple hello world program is given below:
20 2 A First Kotlin Program
Let us make our program a little more interesting; lets get it to ask us our name and
say hello to us personally.
The updated program is:
fun main() {
println("Hello World!")
print("Please Enter your Name: ")
val name = readLine()
println("Hello $name")
}
Now after printing the original ‘Hello World’ string, the program then has three
additional statements. One prompts the user to enter their name, the next actually
reads in the user input (this is the readLine() function) and the final statement
prints out a welcome message to the user.
The result of running this program is:
Hello, World!
Please Enter Your Name: John
Hello John
Notice that were have used a different print style function here—print(). This
function prints out the value passed to it without a carriage return/new line. This
means that the cursor is left on the same line as the prompt allowing the user to type
in their name after the prompt string.
The next statement is:
This statement does several things. It first executes another function called
readLine(). This function, is again a built-in function that is part of the Kotlin
language. In this case it will wait until the user types something in followed by the
return key. It then returns that string which is stored in the variable name. In Kotlin
there are actually two types of variable; there are vals and there are vars. In this
case we used a val which indicates that once the value is set in name it cannot be
Interactive Hello World 21
changed. That is, it is only possible to write a value into name once. In contrast a
var can have a new value assigned to it any number of times. We will return to vals
and vars in a later section.
The end result of the above statement is that whatever the user types in is then
returned as a String as the result of executing the readLine() function. In this
case that result is then stored in the variable name (which will be of type String).
The final statement we added to this program is shown below:
println("Hello $name")
The last statement again uses the built-in println() function, but this time
it uses a facility within Kotlin known as string templates. A string template is a
very useful feature that allows Kotlin values or expression to be embedded within a
String. That is a programmer can embed variable names within a string as long as
they are preceded by a dollar sign ($). When Kotlin processes this string it replaces
the variable (in this case $name) with the value of that variable. In fact any Kotlin
code can be embedded in a string if you enclose it within a ${…} block within a
string. This this feature will be used extensively in the examples presented in this
book.
Variables
Kotlin is a statically typed language. The word static is used here to indicate that
the type of data that a variable can hold will be determined when the program is
compiled (aka at compile time). Later on it will not be possible to change the type of
data that the variable can hold; thus if a variable is to hold an Integer it cannot later
on hold a String. This is the approach adopted by languages such as Kotlin, Java and
C#.
Although this may seem like an obvious approach, it is not the only way in
which programming languages handle typing. For example, Python and JavaScript
are what might be called dynamically typed languages. That is, the type of a variable
is determined by the type of data it holds. In this case if a variable holds a number then
its type is numeric, but if it holds a string then its type might be String. In languages
such as Python and JavaScript the type of a variable can therefore change over time.
For example, a variable might hold the number 42 at one point in the program and
the string “John” at another.
This can add flexibility to the program but can also lead to unexpected issues
at other times. Both approaches have their pros and cons; but for many people the
flexibility of a dynamic language is outweighed by the security afforded by a static
typed language.
22 2 A First Kotlin Program
Kotlin takes a different approach; it allows the programmer to decide if they wish
to specify the type or rely on the compiler to infer the type of the variable at compile
time. In both cases the type is fixed at that point before the program is actually run.
The ability of the Kotlin compiler to determine the type of the variable statically
at compile time for the developer is known as type inference and is a concept that
is becoming increasingly common with the more recent versions of Java starting to
adopt it to some extent.
For example, in Kotlin the above code can be written as:
In this case the type of p is still Person but this has been inferred by the Kotlin
compiler for the developer.
It is of course still possible to explicitly state the type of the val p in Kotlin (and
for a public API this is considered good style) but it is optional. If we did want to
specify the type of the val p we can do so by following the declaration of p with a
colon and the type that p can hold, for example:
Another aspect that is different between a language such as Java and Kotlin is that
it has the concept of val and var. Both keywords are used to declare a new variable.
However, once a val has been set it is not possible to change its value; where as a
var can have its value changed as many times as required.
For example, in the following simple variation on the Hello World application,
the val message is declared to hold the string "Hello Kotlin World!". This is the value
that it will hold for the lifetime of the program; it is not possible to change it:
fun main() {
val message = "Hello Kotlin World!"
println(message)
}
Variables 23
In contrast if we had used the keyword var to declare the variable; then its value
can change during the lifetime of the program, for example:
fun main() {
var message = "Hello Kotlin World!"
println(message)
String Formatting
As well as the use of String Templates in Kotlin, it is possible to use special control
characters to help layout the output produced by a string. An control control character
is a character precede by a back slash (‘’\). It indicates to the print or println
functions that it should treat that following character as special and that it will be
used to help manage the output of the string.
The control characters supported in Kotlin are:
24 2 A First Kotlin Program
• \t—Inserts tab
• \b—Inserts backspace
• \n—Inserts newline
• \r—Inserts carriage return
• \ —Inserts single quote character
• \ —Inserts double quote character
• \\—Inserts backslash
• \$—Inserts dollar character.
For example running the following code:
println("\tHello”)
Naming Conventions
You may have noticed something about some of the variable names we have intro-
duced above such as message and name. Both these variable names are formed of
a set of lower case characters. However in many cases a variable name needs to be
more descriptive such as userName or courseTitle with the second ‘words’
in the variable name capitalised. This is known as modified Camel Case (as it looks
a bit like a camels hump). Standard Camel Case is illustrated by a variable such as
CourseList where each word is capitalised. This format is usually reserved for
classes in Kotlin.
All these examples highlight a very widely used naming convention in Kotlin,
which is that variable names should:
• be all lowercase except when starting a new word within a variable name,
• be more descriptive than variable names such as a or b (although there are some
exceptions such as the use of variables i and j in looping constructs),
• with individual words separated by capital letters as necessary to improve
readability.
This last point is very important as in Kotlin (and most computer programming
languages) spaces are treated as separators which can be used to indicate where one
thing ends and another starts. Thus it is not possible to define a variable name such
as:
• user name
As the space is treated by Kotlin as a separator and thus the compiler thinks you
are defining two things ‘user’ and ‘name’.
When you create your own variables, you should try to name them following the
Kotlin accepted style thus name such as:
Naming Conventions 25
Assignment Operator
One final aspect of the statement shown below has yet to be considered.
What exactly is this ‘ = ‘ between name variable and the readLine() function?
It is called the assignment operator. It is used to assign the value returned by the
function readLine() to the variable name. It is probably the most widely used
operator in Kotlin. Of course, it is not just used to assign values from functions as
the earlier examples illustrated. For example, we can also use to store a string into a
variable directly:
Throughout this chapter we have used the phrase statement to describe a portion of
a Kotlin program, for example the following line of code is a statement that prints
out a string ‘Hello’ and the value held in name.
println("Hello $name")
26 2 A First Kotlin Program
Comments in Code
It is common practice (although not universally so) to add comments to code to help
anyone reading the code to understand what the code does, what its intent was, any
design decisions the programmer made etc.
Comments are sections of a program that are ignored by the Kotlin compiler—they
are not executable code.
A single line comment is indicated by the // characters in Kotlin. Anything
following those characters to the end of the line will be ignored by the compiler
as it will be assumed to be a comment, for example:
// This is a comment
print("Please Enter your Name: ")
val name = readLine()
// This is another comment
println("Hello $name") // comment to the end of the line
In the above, the two lines starting with a // are comments—they are for our
human eyes only. Interestingly the line containing the println() function also
has a comment—that is fine as the comment starts with the // and runs to the end
of the line, anything before the // characters is not part of the comment.
Comments in Code 27
Kotlin also supports multiple line comments. These are comments that span
multiple lines and usually provide extended descriptions for the benefit of developers
reading the code at a later date. An example of a multiple line comment provided for
the main function is given below:
/*
* The main Function used to start
* this simple Hello World
* application.
*/
fun main() {
A multiple line comment starts with /* and ends with */. Everything between
/* and */ is part of the comment and will be ignored by the compiler.
Multi-line comments may be used to explain a complex or non obvious algorithm,
allowing clarity to be brought to such code.
In many cases multi-line comments are used to explain the purpose or role of a
function or a class of things. Such multi-line comments typically start with /** and
end with */. These can be picked up by a Kotlin comment processing tool such as
Dokka. These comments often embed Markdown code as well as special elements
prefixed with an ‘@’ symbol used to describe parameters to functions (@param),
the author of the code, cross references (@see) and versions (@since). These @
symbols are collectively known as KDoc. Some of these are shown below:
/**
* The *main* Function used to start
* this simple Hello World
* application.
* @author John Hunt
* @since 1.0
* @see Main
*/
fun main() {
val message = "Hello Kotlin World!"
println(message)
}
In the above the word main is emphasised and information on the author, which
version introduced this function and a cross reference are provided. For more
information see https://kotlinlang.org/docs/kotlin-doc.html.
28 2 A First Kotlin Program
Basic Types
In Kotlin there are a set of built-in data types that you can use. For example, Int
is a built in data type used to represent integer numbers. String is a built in type
used to represent an ordered sequence of characters. In turn Double is a built in
type used to represent real or floating point numbers.
The following tables summarise these types. Notice that for both integers and real
numbers you can choose the level of range of values that are supported by different
integer or real number types. This is because a Byte takes up less memory than an
Int. If all your numbers can fit within the range of values that can be represented
by a Byte then this will use less memory than if you used the same number if Ints.
If you are hold a million integer values all in the range 1 to 10 then this can make a
significant difference in the memory footprint of your application.
Integer types
Note that if you do not specify the type of the val or var holding an integer, then the
type defaults to Int. In the above we have used the letter L to explicitly indicate that
the literal 3,000,000,000 is of type long (although Kotlin can infer this itself).
Floating point types
Note that to explicitly specify the Float type for a value, add the suffix f or F.
If such a value contains a value that cannot be held within a Float then it will be
truncated.
Boolean types
The standard Kotlin library has included unsigned integers since Kotlin 1.5 (they were
in Beta between Kotlin 1.3 and 1.4 which means that they were still experimental up
until Kotlin 1.5). This library includes unsigned integer types UInt, ULong, UByte
and UShort. It also includes a set of related functions such as conversion operations.
Unsigned integers are integers that can only contain zero or positive integers (they
cannot hold negative numbers). This is because they do not use any memory to store
the sign (+ or −) with the number (as they can only be non negative numbers). This
means that they can hold an integer you know to only be positive in an unsigned type
more efficiently than in a regular signed integer type.
For example, if we are counting the number of players in a game there will be
zero or more players; there will never be a negative number of players. We could
there for use a UByte to hold such a number which is (slightly) more efficient than
using a regular Byte.
Some examples of using unsigned integer types are given below:
However, as of Kotlin 1.5 arrays of unsigned integers remain in Beta; this is likely
to change in subsequent versions.
30 2 A First Kotlin Program
Numeric Conversions
Unlike many languages Kotlin does not automatically convert from one numeric type
to another. Instead explicit conversion operators/methods must be used. All numeric
types support conversions to other types using the following methods:
• toByte(): Byte
• toShort(): Short
• toInt(): Int
• toLong(): Long or use the latter L after an integer literal, e.g,. 34L
• toFloat(): Float or use the letter F after ta numeric literal e.g. 34.5F
• toDouble(): Double
• toChar(): Char
For example, a Double to an Int you can use:
val d = 45.3
val i: Int = d.toInt()
It is also possible to convert Strings to numeric values (assuming the string hold
a number, for example:
val s = "42"
val I: Int = s.toInt()
Online Resources
Exercises
At this point you should try to write your own Kotlin program. It is probably easiest
to start by modifying the Hello World program we have already been studying. The
following steps take you through this:
Exercises 31
1. If you have not yet run the Hello World program, then do so now. To run your
program you have several options. If you have set up an IDE such as IntelliJ
IDEA, then the easiest thing is to use the ‘run’ menu option. Otherwise if you
have set up the Kotlin compiler and runtime on your computer, you can run it
from a Command prompt (on Windows) or a Terminal window (on a Mac/Linux
box).
2. Now ensure that you are comfortable with what the program actually does. Try
commenting out some lines—what happens; is that the behaviour you expected?
Check that you are happy with what it does.
3. Once you have done that, modify the program with your own prompts to
the user (the string argument given to the print() function before the
readLine() function). Make sure that each string is surrounded by the double
quote characters (".."); remember these denote the start and end of a string.
4. Try creating your own val and var variables and storing values into those
instead of the val variable name.
5. Add a print() or println() statement to the program with your own
prompt.
6. Include an assignment that will add two numbers together (for example 4+ 5)
and then assign the result to a val variable called total.
7. Now print out that variables value once it has been assigned a value.
Make sure you can run the program after each of the above changes. If there is an
error reported attempt to fix that issue before moving on.
You must also be careful with the case of the letters in your program—Kotlin is
very case sensitive; therefore the function print() is not the same as Print()—so
be careful.
Chapter 3
Flow of Control
Introduction
In this chapter we are going to look at the flow of control statements in Kotlin. These
statement are used to control the flow of execution within a program based on some
condition. These conditions represent some choice point that will be evaluated too
true or false. To perform this evaluation it is common to use a comparison operator
(for example to check to see if a temperature is greater than some threshold or while
some boolean flag is true then repeat a set of instructions). In many cases these
comparisons need to take into account several values and in these situations logical
operators can be used to combine two or more comparison expressions together.
This chapter first introduces comparison and logical operators before discussing
the null and condition operators. It then considers if statements, when expressions,
while and do-while loops, for loops and repeat loops. It concludes by introducing a
simple application that uses both a while loop and an if statement.
Operators
Comparison Operators
Logical Operators
Numeric Operators
Kotlin has several numeric operators such as the + operator used for addition. These
numeric operators are listed below:
Assignment Operators
As well as the basic + , −numeric operators there are also a set of assignment
operators. These act as short cut operators that provide a concise way of performing
common arithmetic operations.
The if Statement
if (<condition-evaluating-to-boolean>)
statement
Note that the condition must evaluate to true or false. If the condition is true
then we will execute the statement or block of statements. If you want to group a
block of statements together to be run if the condition is true then you can use curly
brackets to create a code block. For example,
if (<condition-evaluating-to-boolean>) {
statement
statement
statement
}
Note that it is a common convention to use the curly brackets with an if statement
even if there is only a single statement as this helps to make it clear which statements
are associated with the if block. Indentation is not significant in Kotlin and is only
used as an aid to the reader. However, if code is mis-aligned it can give a false
impression to the that reader and therefore curly bracket code blocks are considered
good style as it always makes it explicit what is part of the conditional statement and
what is not.
Let us look at a simple example of an if statement in Kotlin:
val i = 5
val j = 10
if (i < j) {
println(i)
}
The if Statement 37
In this example, a test is made to see if i is less than j; if the result of this
condition is true then the value of i will be printed out. If the condition is not true;
then nothing will be output.
For example, the above code outputs:
fun main() {
println("Starting")
if (number > 0) {
// Multiple lines grouped together for the if statement
println("$number is positive")
println("$number squared is ${number *number}")
}
println("Bye")
Starting
Enter a number: 2
2 is positive
2 squared is 4
Bye
Starting
Enter a number: -1
Bye
Note that neither of the statements within the if condition block were executed.
This is because the two statements are associated with the if statement and will only
be executed if the Boolean condition evaluates (returns) true. However, the state-
ment println("Bye") is not part of the if statement; it is merely the next state-
ment to executed after the if statement (and its associated println() statements)
have finished.
38 3 Flow of Control
Else in an if Statement
We can also define an else part of an if statement; this is an optional element that
can be run if the conditional part of the if statement returns false. For example:
fun main() {
println("Starting")
println("Bye")
Now when this code is executed, if the number entered is less than zero then the
else condition will be run. However, we are guaranteed that at least one (and at most
one) of the two blocks of code associated with the if statement will execute.
For example, in the first run of the program, if we enter the value 1 then the output
will be:
Starting
Enter a number: 1
1 is positive
1 squared is 1
Bye
In the second run of the program, if we enter the value -1, then the block of code
associated with the else element of the if statement will be run:
Starting
Enter a number: -1
-1 Its negative
Bye
The if Statement 39
In some cases there may be several conditions you want to test, with each condition
being tested if the previous one failed. This else-if scenario is supported in Kotlin
by the else if elements.
The else if elements of an if statement follows the if part and comes before
any (optional) else part. For example
fun main() {
println("Starting")
if (number > 0) {
println("$number is positive")
println("$number squared is: ${number *number}")
} else if (number == 0) {
println("$number Its Zero")
} else {
println("$number Its negative")
}
println("Bye")
}
If we run this version of the program and enter 0, we will see:
Starting
Enter a number: 0
0 Its Zero
Bye
Here we can see that the first if condition failed (as the number is not greater than
Zero). However, the next else if must have returned true as the number is equal
to Zero and then the message 0 Its Zero was printed out. Having executed this
statement the if statement then terminated (the remaining else part was ignored).
Kotlin if Expression
In Kotlin if statements are actually expressions. In all of the previous examples, the
if statement actually returned the value Unit (represented by ()) which represents
40 3 Flow of Control
fun main() {
val age = 18
var status = ""
If we run this, we get the string 18- you are teenager printed out.
However, this is quite long and it may not be obvious that the real intent of this
code was to assign an appropriate value to status.
An alternative in Kotlin is to use the if statement as an if expression. The format
of an if expression is
That is the result returned from the if expression is the first value unless the
condition fails in which case the result returned will be the value after the else. It
may seem confusing at first, but it becomes easier when you see an example.
For example, using the if expression we can perform a test to determine the value
to assign to status and return it as the result of the if expression. For example:
fun main() {
val age = 18
}
The if Statement 41
Again, the result printed out is 18- you are a teenager, however now the
code is much more concise, and it is clear that the purpose of the test is to determine
the result to assign to status. We can also make status a val rather than a var now;
which is considered much better Kotlin style (often referred to as idiomatic Kotlin).
Nesting if Statements
It is possible to nest one if statement inside another. This term nesting indicates
that one if statement is located within part of another if statement and can be used
to refine the conditional behaviour of the program.
An example is given below. Note that it allows some behaviour to be performed
before and after the nested if statement is executed/run.
fun main() {
val snowing = true
val temp = -1
if (temp < 0) {
println("It is freezing")
if (snowing) {
println("Put on boots")
}
println("Time for Hot Chocolate")
}
println("Bye")
}
In this example, if the temperature if less than Zero then we will enter the outer
if block of code. If it is not less than zero we will skip the whole if statement and
jump to the println("Bye") statement which is after both if statements.
In this case the temperature is set to -1 and so we will enter the if statement.
We will then print out the ‘It is freezing’ string. At this point another if statement
is nested within the first if statement. A check will now be made to see if it is
snowing. Notice that snowing is already a Boolean value and so will be either
true or false and illustrates that a Boolean value on its own can be used here.
As it is snowing, we will print out “Put on boots”.
However, the statement printing out “Time for Hot Chocolate” is not part of the
nested if. It is part of the outer if. Thus we print out this string whether it is snowing
or not.
42 3 Flow of Control
In the above code snippet the commented out line will not compile as it will
generate a compiler error stating that ‘Null can not be a value of a non-null type
String’.
To specify a nullable type we need to explicitly specify the type with a trailing
‘?’, for example:
nullableVar: abc
nullableVar now: null
Note that the type of the variable nullableVar is String? not String; this
indicates that it is a nullable type that can hold a reference to a String or the value
null.
The benefit of non-null and nullable types in Kotlin is that for standard/normal
variables they can never be null and thus calling a property such as length on
a variable of type String will never generate a NullPointerException (as a
standard String variable can never have the value null).
Null and Conditional Statements 43
However, a nullable variable can have the value null, thus nullable
Var.length may work or may result in a NullPointer Exception being
generated depending upon whether it holds the value null or not.
Because of this it is not legal to write the following code:
println(nullableVar.length)
The compiler will provide a warning indicating that the variable nullableVar
can be null and therefore this operation is not safe.
To overcome this you have several options:
1. explicitly check for null conditions,
2. use the safe call operator which is written as ?.,
3. use the elvis operator that is written as ?:,
4. Apply the not-null assertion operator which is written as !!,
5. Perform a safe cast to covert the variable type into a non null type.
Each of these approaches is described below.
It is possible to explicitly check whether a nullable variable is null or not and take
appropriate action. For example:
nullableVar = null
println(nullableVar?.length)
The above code snippet uses the ?. safe call operator to invoke the length
property on the string held in nullableVar. In the first example this is the string
“John”. However, in the second example the variable has been set to null. This
does not however, result in a NullPointerException being raised. Instead the
value null is returned as the result of the call to length. The output from this
code example is shown below:
4
Null
It is also possible to chain the safe call operator such that the result returned by
an operation is used to call a subsequent member function for example:
company?.department?.head?.name
In this case if company is null or the property department or head are null the
overall result of this call chain will be null.
It is also possible to use the ?. safe call operator on the left hand side of an
assignment, for example:
company?.department?.head?.name ="John"
In this case if a null value is returned by any of the elements on the left hand side,
then the assignment is not evaluated (performed) at all.
Thus the safe call operator ensures that you will never get an error generated from
the operation being invoked.
The so called elvis operator is also formed of two characters. These characters are
the “?” and “:” characters as in ?:. It is called the elvis operator as the question mark
looks a little like the quiff Elvis wore.
The purpose of the Elvis operator is to allow a default value other than null to be
returned when calling a function or property on a nullable variable. For example it
is the equivalent of writing the following in a shorthand form:
Null and Conditional Statements 45
In this example, if nullableVar currently holds the value null then the result of the
if expression is -1 rather than null.
Using the Elvis operator we can write:
val l2 = nullableVar?.length ?: -1
In this case if the result of the safe call operator is null then the elvis operator will
return -1 otherwise it will just return the value returned from length. For example:
nullableVar = "John"
val l2 = nullableVar?.length ?: -1
println(l2)
nullableVar = null
val l3 = nullableVar?.length ?: -1
println(l3)
4
-1
The not-null assertion operator is formed of two exclamation marks i.e. !!.
This operator is used to treat a nullable variable as a non-null type. If the
current value of the nullable type is null when the operator is applied then a
NullPointerException error will be generated. Otherwise it will be treated
as a normal non-null type. For example:
nullableVar = "John"
val l4 = nullableVar!!.length
println(l2)
nullableVar = null
val l5 = nullableVar!!.length
println(l3)
However, the compiler will analyse the code and see that the nullableVar is
set to null the line before the not-null assertion operator is applied; in this case
it will not even compile the code as it knows that it will always result in a
NullPointerException error being generated.
46 3 Flow of Control
Smart Compilation
The Kotlin compiler is smart enough to work out whether a value is null or not in
many situations. This is illustrated below.
The first line of this code declares a new var called total of type Int? (that
is it is a nullable integer). However, it currently holds the value null. This means
that if we attempt to increment total using the increment (++) operator (which is the
same as saying total = total + 1) then we would be trying to add 1 to null which
is illegal. Thus the second line in the example has to be commented out as it would
generate a compile time error. We could of course use one of the nullable operators
to handle this such as the safe dot operator (?.). However, in this case we are using
an explicit if statement that will check to see if total is null. If it is not null then
we can safely increment total. The compiler is smart enough to understand this and
allow the increment of total within the if block.
The final option related to nullable values is to cast them to a non-null type explicitly.
This is done using the as? operator known as the safe cast operator. This type of
cast is safe because it will either convert a value in a type to a non-null type or return
the value null (if the value to be cast is null). For example:
null
When Expression
val value = 1
when (value) {
0 -> println("It is a 0")
1, 2 -> println("It is a 1 or 2")
3 -> println("It is to high")
else -> println("Default")
}
It is a 1 or 2
In this case we have not used the fact that the when expression is an expression,
however this usage is illustrated below:
val value = 1
val message = when(value) {
0 -> "Invalid number"
1, 2 -> "Number too low"
3 -> "Number correct"
4 -> "Number too high, but acceptable"
in 5..10 -> "Number too high, might be acceptable"
!in 10..20 -> "What are you on"
else -> "Bad number"
}
println("message: $message")
In the above example the result of the when expression is saved into the message
val. This example also illustrates a few other when condition examples:
• in 5...10 indicates that if the value is in the range 5 up to and including 10
then use the associated string.
• !in 10...20 indicates that if the value is not in the range 10 to 20 use the
associated string.
Note that only one of the conditions will be triggered and that each condition is
tested in turn and thus the first one that matches will be used and only the first one.
The output from this example is therefore:
We are not restricted to just using numbers in a when condition (as is the case in
some other languages). For example, the following example uses a String as the
type to be tested:
In this case if value2 holds a reference to the String “John” we will print out
“Dad”, if holds reference to the String “Denise” it will print out “Mum” otherwise
it will print out the message “Not mum or dad”. The actual output from this code is:
Dad
We can in fact use any type with a when expression, if we have a custom type
Person then we can use such a type with the when expression and test for equality
between instances of the type, for example:
its me
We can even use a when expression to determine what type of thing we have.
This can be done using the is operator. Thus allows us to check whether a val
or var holds something that is an Int, or a String, or a Double etc. This is
illustrated below (note Any means that the val x is any type of thing):
val x: Any = 32
when (x) {
is Int -> println("Its a Int’’)
is Double -> println("Its a Double")
is Boolean -> println("Its a Boolean")
is String -> println("Its a String")
is Person -> println("Its a Person")
else -> println("its something else")
}
Its a Int
If you need to have more than one statement associated with each condition within
a when expression, you can use curly brackets to group them together. For example:
50 3 Flow of Control
val total = 34
when (total) {
0 -> {
print("Its Zero")
println("Never mind")
}
in 1..5 -> println("Not Bad")
else -> println("Excellent")
}
While Loop
The while loop exists in almost all programming languages and is used to iterate
(or repeat) one or more code statements as long as the test condition (expression) is
true. This iteration construct is usually used when the number of times we need to
repeat the block of code to execute is not know. For example, it may need to repeat
until some solution is found or the user enters a particular value.
A while loop is known as a conditional loop as the number of times the loop
will be repeated is not known at the start. Instead the loop is repeated while some
condition is true.
The behaviour of the while loop is illustrated in below.
While Loop 51
The Kotlin while loop is a statment (rather than an expression) and has the basic
form:
while (<test-condition-is-true>) {
statement or statements
}
As shown both in the diagram and can be inferred from the code; while the test
condition/expression is true then the statement or block of statements in the while
loop will be executed. Note that if a single statement is present then the curly brackets
are optional (but their use is considered good style).
Note that the test is performed before each iteration, including the first iteration;
therefore, if the condition fails before the first iteration of the loop statement or block
of statements, then the code contained in the loop may never be executed at all.
The following illustrates an example while loop in Kotlin:
fun main() {
var count = 0
println("Starting")
while (count < 10) {
print("$count, ")
count++
}
println()
println("Done")
}
In this example while some variable count is less than the value 10 the while
loop will continue to iterate (will be repeated). The while loop itself contains two
statements; one prints out the value of the count variable while the other increments
count (remember count++ is equivalent to count = count + 1).
Note we have used the version of print() that does not print a carriage return
when it prints out a value.
The result of running this example is:
Starting
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Done
As you can see the statements printing the “Starting” and “Done” messages are
only run once. However, the statement printing out the count variable is run 10
times (printing out the values 0 to 9).
52 3 Flow of Control
Once the value of count is equal to 10 then the loop finishes (or terminates).
Note that we needed to initialise the count variable before the loop. This is
because it needs to have a value for the first iteration of the while loop. That is
before the while loop does anything the program needs to already know the first
value of count so that it can perform that very first test. This is a feature of the while
loops behaviour.
Do Loop
In some cases, we want to execute the body of statements at least once; you can
accomplish this with the do loop construct:
do
statement
while (test expression)
This loop is guaranteed to execute at least once, as the test is only performed after
the statement has been evaluated. As with the while loop, the do loop repeats until
the condition is false. You can repeat more than one statement by bracketing a series
of statements into a block using curly brackets {}, for example:
fun main() {
var count = 0
println("Starting")
do {
print("$count, ")
count++
} while (count < 10)
println()
println("Done")
}
The above do loop prints the numbers from 1 up to 9 and terminates when count
equals 10.
Do Loop 53
Starting
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Done
For Loop
In many cases we know how many times we want to iterative over one or more
statements (as indeed we did in the previous section). Although the while loop can
be used for such situations, the for loop is a far more concise way to do this. It is
also typically clearer to another programmer that the loop must iterate for a specific
number of iterations.
The for loop is used to step a variable through a series of values until a given
test is met. It is also known as a counted loop as the number of times the loop will
be repeated is known at the start of the loop.
The behaviour of the for loop is illustrated in below.
54 3 Flow of Control
This flow chart shows that some sequence of values (for example all integer values
between 0 and 9) will be used to iterate over a block of code to process. When the
last item in the sequence has been reached, the loop will terminate.
Many languages have a for loop of the form:
for (i in iterable){
statement or statements
}
Where iterable can be an integer range, an integer progression or some object that
is iterable such as a String or some from of data collection or container. If you are
familiar with for loops in languages such as C or C++ this may seem strange as
a for loop in those languages only loops over a series of integer values. However
the for loop construct in Kotlin is very flexible and can loop over not only a range
of integer values but also a set of values held in data structures such as a list of
integers or Strings. We will return to this feature of the for loop when we look at
collections/containers of data in a later chapter.
The simplest format of the Kotlin for loop when using a range of values is
For Loop 55
An example is shown below which equates to the while loop we looked at earlier:
As can be seen from the above; the end result is that we have generated a for
loop that produces the same set of values as the earlier while loop. However,
• the code is more concise,
• it is clear we are processing a range of values from 0 to 9 (note that it is up to and
including the final value) and
• we did not need to define the loop variable first.
For these reasons for loops are more common in programs in general than while
loops.
One thing you might notice though is that in the while loop we are not
constrained to incrementing the count variable by one (we just happened to do this).
For example, we could have decided to increment count by 2 each time round the
loop (a very common idea). In fact, the Kotlin for loop allows us to do exactly this;
a third element that can be provided to the for loop is the step value to increment the
loop variable by each time round the loop, for example:
Thus the value of the loop variable has jumped by 2 starting at 0. Once its value
was 10 or more then the loop terminates. Of course, it is not only the value 2 we
could use; we could step (increment) by any meaningful integer, 3, 4 or 5 etc.
Above we noted that the for loop included the values in the range 0 to 9; however
if you wish to loop up until the maximum value (that is not to include the maximum
value) then you can use the until operator:
This will loop from the value 0 up to the value 8 but not the value 9:
0, 1, 2, 3, 4, 5, 6, 7, 8,
Done
Also notice that in the above example we did not use the curly brackets as the
for loop only contained a single statement. This is legal but is not considered best
practice.
We can also loop using until with a step value other than 1:
It is also possible to loop from a higher integer value down to a lower value using
the downTo operator:
For Loop 57
This generates:
Repeat Loops
Kotlin includes another loop statement called the repeat statement. This is similar
to a for loop in that it is a counted loop. That is the number of times the loop will
be repeated is known at the start. However, it differs from a for loop in that the loop
variable is not made explicit. This is useful in situations where you know you want
to loop a certain number of times, but you do not need to reference the loop index.
The repeat loop has the following syntax:
fun main() {
repeat(3) {
print("Hello, ")
}
println()
Kotlin allows programmers to decide whether they want to break out of a loop early
or not (whether we are using a for, while or do-while loop). This is done using
the break statement.
The break statement allows a developer to alter the normal cycle of the loop
based on some criteria which may not be predictable in advance (for example it may
be based on some user input).
The break statement, when executed, will terminate the current loop and jump
the program to the first line after the loop. The following diagram shows how this
works for a for loop:
Typically, a guard statement (if statement) is placed on the break so that the
break statement is conditionally applied when appropriate.
This is shown below for a simple for loop that is set up to loop from 0 up until
10. However within the for loop an if statement is used such that when the value
Loop Control Statements 59
of i is 5 the break statement will be executed. This will cause the program to break
out of the loop. This means the values of i from 6 to 9 will never be executed:
val y = 10
val x = 5
for (i in 0 until y) {
if (i == x) {
break
}
print("i: $i, ")
}
println()
i: 0, i: 1, i: 2, i: 3, i: 4,
The break statement can come anywhere within the block of code associated
with the loop construct (whether that is a for loop, a while loop or a do-while
loop). This means that there can be statements before it and after it.
It is also possible to label an outer loop construct. If this label is then used with
the break statement, the break statement will break out of the labelled loop rather
than the loop it is directly defined within. For example, in the following nested loop
example, the outer most for loop is labeled with loop@. Note that the format of
the label is < label-name>@ when used on the looping construct which we will
break out of. Within the inner for loop there is a condition, that if i equal 3 then
the program will break out of the loop with the label loop. Note the syntax is now
break@ <label-name>:
0 - 0,
0 - 1,
1 - 0,
1 - 1,
2 - 0,
2 - 1,
60 3 Flow of Control
As can be seen the outer loop is terminated when i is 3 (which also terminates
the inner loop). This allows for fine grained control when breaking out of a loop.
The continue statement also affects the flow of control within the looping
constructs for, while anddo-while. However, it does not terminate the whole
loop; rather it only terminates the current iteration round the loop. This allows you
to skip over part of a loop’s iteration for a particular value, but then to continue with
the remaining values in the sequence.
A guard (if statement) can be used to determine when the continue statement
should be executed.
Loop Control Statements 61
As with the break statement, the continue can come anywhere within the
body of the looping construct. This means that you can have some statements that
will be executed for every value in the sequence and some that are only executed
when the continue statement is not run.
This is shown below. In this program the continue statement is executed only
for odd numbers and thus the two println() statements are only run if the value
of i is even:
fun main() {
for (i in 0 until 10) {
print("$i ")
if ((i % 2) == 1) {
continue
}
println("hey its an even number")
println("we love even numbers")
}
println("Done")
}
As you can see, we only print out the messages about a number being even when
the values are 0, 2, 4, 6 and 8.
It is also possible to label a loop construct for use with the continue statement.
In this case a labelled continue statement will cause the program to jump out of any
loops within the labelled loop and then to continue with the next iteration of the
labelled loop. This is illustrated by the following program:
62 3 Flow of Control
fun main() {
outer@ for (i in 1..4) {
println("i = $i")
for (j in 1..4) {
val result = i + j
if (result == 5) continue@outer
println("\t$i + $j = $result ")
}
}
}
i = 1
1 + 1 = 2
1 + 2 = 3
1 + 3 = 4
i = 2
2 + 1 = 3
2 + 2 = 4
i = 3
3 + 1 = 4
i = 4
As can be seen from this the inner loop is terminated each time the value of
result is 5 at which point the next iteration of the outer loop is then triggered.
Prior to Kotlin 1.4 it was not possible to use an unlabelled continue or break with
an iteration construct inside a when expression. That is you could not use unlabelled
continue and breakstatements within a when expression to determine if the
program should continue or break out of a loop. This was because continue and
break were reserved with respect to the when expression for future use. It was thus
necessary to explicitly label a loop and then use the loop with continue and break.
In Kotlin 1.4 it was decided that continue and break would not be used
specifically by the when expression and thus from this version on it is possible
to use these special control operations without labels. This is illustrated below for
Kotlin 1.4 and 1.5:
Loop Control Statements 63
fun main() {
for (i in 0..10) {
when (i) {
2 -> continue
4 -> break
else -> println(i)
}
}
}
Earlier in the book we said that variable names should be meaningful and that names
such as ‘a’ and ‘b’ were not in general a good idea. The one exception to this rule
relates to loop variable names used with for loops. It is very common to find that
these loop variables are called ‘i’, ‘j’ etc.
It is such a common convention that if a variable is called ‘i’ or ‘j’ people expect
it to be a loop variable. As such.
• you should consider using these variable names in looping constructs,
• and avoid using them elsewhere.
But this does raise the question why ‘i’ and ‘j’; the answer is that it all goes back
to a programming language called Fortran which was first developed in the 1950s. In
this programming language loop variables had to be called ‘i’ and ‘j’ etc. Fortran was
so ubiquitous for mathematical and scientific programming, where loops are almost
di rigour, that this has become the convention in other languages which do not have
this restriction.
The following short program illustrates how a while loop can be used to control
the execution of the main body of code. In this program we will continue to roll a
pair of dice until the user indicates that they do not want to roll again. When this
occurs the while loop will terminate:
64 3 Flow of Control
import kotlin.random.Random
fun main() {
val MIN = 1
val MAX = 6
When we run this program the results of rolling two dice are shown. The program
will keep looping and printing out the two dice values until the user indicates that
they no longer want to roll the dice:
There are two things to note about this program; the first is that we have used the
readLine() function to obtain input from the user. This function returns a string
or a null value and thus the rollAgain variable must be nullable.
The other thing to note is that we are using the Random type from the
kotlin.random library. As this is not one of the default libraries loaded when
Dice Roll Game 65
Kotlin starts up we must indicate to the program that it should load the Random type
from this library by adding the following import statement to the start of the program
file:
import kotlin.random.Random
Online Resources
Exercises
There are five different exercises in this section, you can select which you are
interested in or do all five.
The aim of this exercise is to write a small program to test if an integer is positive or
negative.
Your program should:
1. Prompt the user to input a number (use the readLine() function). You can
assume that the input will be some sort of number.
2. Convert the string into an integer using the toInt() method available to convert
Strings to Ints. We will assume that the user either enters nothing or they enter
a valid integer.
3. You can check whether they entered something by testing for a null value.
4. Now check whether the integer is a positive number or a negative number.
5. You could also add a test to see if the number is Zero using an else block.
The output from such a program might be:
66 3 Flow of Control
Or
Or indeed:
This exercise requires you to write a program to take input from the user and determine
if the number is odd or even. Again you can assume that the user will enter a valid
integer number.
Print out a message to the user to let them know the result.
To test if a number is even you can use
(number % 2) == 0
Or
1. Take input from the user for a given distance in Kilometres. This can be done
using the readLine() function.
2. Convert the value returned by the readLine() function from a string into an
integer using the toInt() method.
3. Now convert this value into miles—this can be done by multiplying the
kilometres by 0.621371
4. Print out a message telling the user what the kilometres are in miles.
Once you have done this you can add a few further features to vitrify the input
entered by the user:
1. Modify your program such that it verifies that the user has entered a positive
distance (i.e. they cannot enter a negative number).
An example of the output from the conversion program is given below:
Write a program that can find the factorial of any given number. For example, find
the factorial of the number 5 (often written as 5!) which is 1 * 2 * 3 * 4 *
5 and equals 120.
The factorial is not defined for negative numbers and the factorial of Zero is 1;
that is 0!= 1.
Your program should take as input an integer from the user.
You should:
1. Determine if the number is less than Zero, in which case you should print out
an error message.
2. Check to see if the number is Zero—if it is then the answer is 1—print this out.
3. Otherwise use a loop to generate the result and print it out.
For example:
Or
And
Introduction
In this chapter we are going to bring everything we have learned so far together to
create a simple number guessing game.
This will involve creating a new Kotlin program, handling user input, using the if
statement as well as using looping constructs.
We will also use an additional library or package, that is not by default available
to your program; this will be the random number generator library.
We want to make sure that we don’t overwrite whatever you have done so far, and
we would like this Kotlin program to be separate from your other work. As such we
will create a new Kotlin file to hold this program in the intelliJ IDE.
We will create a new Kotlin file to hold your program. To do this select the src
node in the left-hand Project view. Once you have selected this use the right mouse
button menu. On this menu select ‘New’ followed by ‘Kotlin Class/File’, see below:
First select the File option which is the second option down below the input field in
the ‘New Kotlin Class/File’ dialog. When you do this, you will prompted to provide
a name for the Kotlin file; you can call the file whatever you like, but I am using the
name ‘NumberGuessGame’ as that is descriptive and will help me find this file again
later:
We can now run our embryonic program. To do this we can select the green arrow
next to the fun main() line in the editor window. Notice that the generated code
that sits behind our main() function is called NumberGuessGameKt (which is
derived from the name of the file that the function is defined in).
When we do this the Run console will be opened at the bottom of the IDE and
the output displayed:
Setting Up the Program 71
The aim of our number guess game is to guess the number that the program has come
up with.
The main flow of the program is shown in the following diagram:
• The player will have 4 goes to guess the number correctly; if they don’t guess the
number within this number of attempts, then they will be informed that they have
lost the game and will be told what the actual number was.
We will start off by looking at how we can generate a random number. Up to this
point we have only used the built-in functions that are provided by Kotlin by default.
In actual fact Kotlin comes with very many packages provided by the Kotlin Foun-
dation itself, by third party vendors and by the open source (typically free) software
community.
The Kotlin kotlin.random package (or library) is one that is provided with
Kotlin as part of the default environment; but the functions within it are not automat-
ically loaded or made available to the programmer. This is partly because there are
so many facilities available with Kotlin that it could become overwhelming if they
were all included by default. For this reason Kotlin only makes available by default
the most commonly used facilities. Programmers can then explicitly specify when
they want to use some facilities from one of the other libraries or packages.
The kotlin.random package provides implementations of pseudo-random
number generators for use in application programs. These random number generators
are referred to as pseudo because it is very hard for a computer to truly generate a
series of random numbers; instead it does its best to mimic this using an algorithm;
which by its very nature will be based on some logic which will mean that it is not
impossible to predict the next number—hence it is not truly random. For our purposes
this is fine but there are applications, such as security and encryption, where this can
be a real problem.
To access the kotlin.random module in Kotlin you need to import it; this
makes the module visible in the rest of the Kotlin file (in our case to our program).
This is done by placing the following statements are the start of the file before the
declaration of the main() function.
import kotlin.random.Random
Once we have imported it, we can use the functions within this package, such
as Random.nextInt(). This function returns a random integer between the first
and (up to but not including) the second parameter’s value. In the above example it
means that the random number generated will be between 1 and 10.
The val numberToGuess will now hold an integer which the player of the game
must guess.
Creating the Game 73
We now need to obtain input from the user representing their guess. We have already
seen how to do this in previous chapters; we can use the readLine() function
which returns a String (or null if there is no input) and then the toInt() function
that will convert that String into an integer (we will ignore error checking to make
sure they have typed in a number at this point). Kotlin however, does not allow an
operation to be invoked when a value might be null; it is therefore necessary to
specify what value should be used if the call to readLine() returned null. To
do this we use the Elvis operator ?:. This operator was introduced int he last chapter
and indicates that the left hand value should be returned unless it is null, in which
case the right hand value will be used instead. Thus the following statement says
“take the users input unless its null in which case use the empty string”:
The string is then turned into an Int value using toInt() and stored in the var
intGuess.
We now need to check to see whether the player has guessed the correct number.
We could use an if statement for this, however we are going to want to repeat
this test until the user has guessed correctly.
We will therefore use a while loop and check to see if their guess equals the
number to be guessed:
The loop above will be repeated if the number they entered did not match the
number to be guessed.
We can therefore print a message telling the player that their guess was wrong:
74 4 Number Guessing Game
Now we need the player to make another guess otherwise the program will never
terminate, so we can again prompt them to enter a number:
We also said above that the player can’t play forever; they have to guess the correct
number within 4 goes. We therefore need to add some logic which will stop the game
once they exceed this number.
We will therefore need a variable (var) to keep track of the number of attempts
they have made.
We should call this variable something meaningful so that we know what it repre-
sents, for example we could call it countNumberOfTries and initialise it with
the value 1:
var countNumberOfTries = 1
if (countNumberOfTries == 4) {
break
}
If we don’t break out of the loop we can increment the count using the ‘+ + ’
increment operator. We thus now have:
if (countNumberOfTries == 4) {
break
}
We also said at the beginning that to make it easier for the player to guess the number;
we should indicate whether their guess was higher or lower than the actual number.
To do this we can again use the if statement; if the guess is lower we print one
message but if it was higher we print another.
At this point we have a choice regarding whether to have a separate if statement
to that used to decide if the maximum goes has been reached or to extend that one with
an else if. Each approach can work but the latter indicates that these conditions
are all related so that is the one we will use.
76 4 Number Guessing Game
var countNumberOfTries = 1
while (numberToGuess != intGuess) {
println("Sorry wrong number")
if (countNumberOfTries == 4) {
break
} else if (intGuess < numberToGuess) {
println("Your guess was lower than the number")
} else {
println("Your guess was higher than the number")
}
Notice that the if statement has a final else which indicates that the guess was
higher; this is fine as by this point it is the only option left.
We have now covered all the situations that can occur while the game is being played;
all that is left for us to do is to handle the end of game messages.
If the player has guessed the number correctly, we want to congratulate them; if
they did not guess the number, we want to let them know what the actual number
was. We will do this using another if statement which checks to see if the player
guessed the number or not. After this we will print an end of game message:
Creating the Game 77
if (numberToGuess == intGuess) {
println("Well Done You Won!")
println("You took $countNumberOfTries goes to complete the
game")
} else {
println("Sorry - You Loose")
println("The number you needed to guess was $numberToGuess")
}
println("Game Over")
Note that we are using String templates so that the output generated will include
information such as the number of tries made or the number to be guessed. This is
done using the ‘$’ notation which indicates that the current value of the following
val or var should be inserted into the string.
import kotlin.random.Random
val numberToGuess = Random.nextInt(0, 11)
fun main() {
var countNumberOfTries = 1
while (numberToGuess != intGuess) {
println("Sorry wrong number")
if (countNumberOfTries == 4) {
break
} else if (intGuess < numberToGuess) {
println("Your guess was lower than the number")
} else {
println("Your guess was higher than the number")
}
78 4 Number Guessing Game
if (numberToGuess == intGuess) {
println("Well Done You Won!")
println("You took $countNumberOfTries goes to complete
the game")
} else {
println("Sorry - You Loose")
println("The number you needed to guess was
$numberToGuess")
}
println("Game Over")
Hints
You may have noticed that we have used blank lines to group together certain lines
of code in this example. This is intended to make it easier to read the code and are
perfectly allowable in Kotlin. Indeed, the Kotlin layout guidelines encourage it.
Exercises 79
Exercises
For this chapter the exercises all relate to adding additional features to the game:
1. Provide a cheat mode, for example if the user enters -1 print out the number
they need to guess and then loop again. This does not count as one of their goes.
2. If their guess is within 1 of the actual number tell the player this.
3. At the end of the game, before printing ‘Game Over’, modify your program so
that it asks the user if they want to play again; if they say yes then restart the
whole game.
Chapter 5
Functions in Kotlin
Introduction
When you build an application of any size you will want to break it down into more
manageable units; these units can then be worked on separately, tested and maintained
separately. One way in which these units can be defined is as Kotlin functions.
This chapter will introduce functions, how they are defined, how they can be
referenced and executed. It will also introduce the use of parameters to functions and
values being returned from functions. It also introduces anonymous functions and
lambdas. It concludes by considering function closures and recursion.
In Kotlin functions are groups of related statements that can be called together,
that typically perform a specific task. Such functions may or may not take a set of
parameters or return a value.
Functions can be defined in one place and called or invoked in another. This helps
to make code more modular and easier to understand.
It also means that the same function can be called multiple times or in multiple
locations. This help to ensure that although a piece of functionality is used in multiple
places; it is only defined once and only needs to be maintained and tested in one
location.
Functions are also part of the type system in Kotlin. Thus a function that takes an
Int and returns an Int has the type (Int) - > Int (which can be read as a function(Int)
returns Int).
Defining Functions
The following is one of the simplest functions you can write; the function takes no
parameters and has only a single statement that prints out the message "Hello
World":
Defining Functions 83
fun printMessage() {
println("Hello World!")
}
This function is called printMessage and when called (also known as invoked)
it will run the body of the function which will print out the String, for example:
fun main() {
printMessage()
}
Note that the case is significant here; in Kotlin printMessage() and printmessage()
are two completely different functions.
The output generated by the function is:
Hello World!
Be careful to include the round brackets () when you call the function. This is
because if you just use the functions’ name then you are merely referring to the
location in memory where the function is stored, and you are not invoking it.
Function Parameters
A function can have zero or more parameters. That is, data can be supplied to a
function when it is invoked, and this data is made available via a set of function
parameters. At this point it is worth clarifying some terminology that relates to the
parameters defined as part of the function header and the data passed into the function
via these parameters:
• A parameter is a variable defined as part of the function header and is used to
make data available within the function itself.
• An argument is the actual value or data passed into the function when it is called.
The data will be held within the parameters.
Unfortunately many developers use these terms interchangeably but it is worth
being clear on the distinction.
We could modify the function to make it a little more general and reusable by
providing a parameter. This parameter could be used to supply the message to be
printed out, for example:
84 5 Functions in Kotlin
fun main() {
printMessage2("Hello World")
printMessage2("Good day")
printMessage2("Welcome")
printMessage2("Ola")
}
The output from calling this function with each of these strings being supplied as
the argument value is:
Hello World
Good day
Welcome
Ola
So far the functions we have defined have only had zero or one parameters; however
that was just a choice. We could easily have defined a function which defined two or
more parameters. In these situations, the parameter list contains a list of parameter
names separated by a comma.
For example.
Function Parameters 85
fun main() {
greeter("Jasmine", "Have a Nice Day!")
}
Here the greeter() function defines two parameters; name and message
both of type String. These parameters (which are local to the function and cannot
be seen outside of the function) are then used within the body of the function.
The output is.
You can have any number of parameters defined in a function. However each
parameter is separated from the next parameter by a comma ‘,’. In addition each
parameter must include the type of the parameter (even if all the parameters have the
same type).
Once you have one or more parameters you may want to provide default values for
some or all of those parameters; particular for ones which might only be required in
some situations.
This can be done very easily in Kotlin; all that is required is that the default value
must be declared in the function header along with the parameter name and type.
The default value comes after the type and is preceded by an equals sign ‘= ‘. Note
that the type of the default value must match the type of the parameter.
If a value is supplied for the parameter, then it will override the default. If no value
is supplied when the function is called, then the default will be used.
For example, we can modify the greeter() function from the previous section
to provide a default message such as “Live Long and Prosper”.
fun main() {
greeter2("Theeban")
greeter2("Jasmine", "Have a Nice Day!")
}
86 5 Functions in Kotlin
Now we can call the greeter2() function with one or two arguments.
When we run this example, we will get:
As you can see from this in the first example (where only one argument was
provided) the default message was used. However, in the second example where a
message was provided, along with the name, then that message was used instead of
the default.
Note we can use the terms mandatory and optional for the parameters in
greeter2(). In this case.
• name is a mandatory field/parameter,
• message is an optional field/parameter as it has a default value.
Named Arguments
This now raises the question how do we provide the name and the message
arguments when we would like to use the default values for title and prompt?
The answer is to use named parameter passing. In this approach we provide the
name of the parameter we want an argument/value to be assigned to; position is no
longer relevant. For example:
In this example we are using the default values for title and prompt. This
produces the following output:
Now that we are using named parameters, we do not need to worry about the order
of those parameters. We can thus change the order of the parameters, for example:
This is completely legal and results in the same output as the previous example:
We can actually mix positional and named arguments in Kotlin, for example:
Here “Jasmine” is bound to the name parameter as it is the first parameter, but
“Have a Nice Day!” is bound to message parameter as it is a named argument. We
can of course have any number of positional parameters, followed by any number of
named parameters, for example:
greeter3("Jasmine",
title = "Ms",
message="Have a Nice Day!")
Prior to Kotlin 1.4 you could not have named parmeters followed by positional
parameters. That is the following example would not work prior to Kotlin 1.4:
max(x=3,4): 4
However, the position parameter and the named parameters must make sense. It
if not possible to write:
This is because the positional argument is assumed to be for the parameter x and
thus there are two values being bound to x. This will generate a compile time error.
In some cases, you do not know how many arguments will be supplied when a
function is called. Kotlin allows you to pass an arbitrary number of arguments into
a function and then process those arguments inside the function.
To define a parameter list as being of arbitrary length, a parameter is marked with
the keyword vararg. For example:
fun main() {
greeter4("John")
greeter4("John", "Denise")
greeter4("John", "Denise", "Phoebe", "Adam")
greeter4("John", "Denise", "Phoebe",
"Adam", "Jasmine", "Gryff")
}
The vararg keyword can only be applied to one parameter in a given function.
In addition, if a vararg parameter is not the last parameter in the list, then the
values for the following parameters must be passed in using the named argument
syntax.
The above code snippet generates.
Function Parameters 89
John ,
John , Denise ,
John , Denise , Phoebe , Adam ,
John , Denise , Phoebe , Adam , Jasmine , Gryff ,
Also note that this version of the greeter function uses a forEach operation
to iterate over the parameter values passed into the function. We will look at this
operation in more detail later in the book. For now, accept that the value of the
special variable it is bound to each of the parameter values passed into the function
in turn.
In Kotlin all parameters to functions are vals. This means that once the function has
been invoked it is not possible to reassign a value to that parameter (this is not the
case for example in Java). This means that it is not possible to write the following;
It is very common to want to return a value from a function. In Kotlin this can be
done using the return statement. Whenever a return statement is encountered
within a function then that function will terminate and return any values following
the return keyword. Note that the function declaration or header must indicate
that a value will be returned and what the type of that value will be. Within the
function body, the type of the actual value returned must match that in the function
declaration.
For example, the following defines a simple function that squares whatever integer
values have been passed to it:
Note that the IDE has tried to help the developer here by suggesting that a fix
could be to change the return type to Int.
The square() function defined earlier will multiply whatever it is given by
itself and then return that value. The returned value can then be used at the point that
the function was invoked, for example:
fun main() {
// Store result from square in a variable
val result = square(4)
println("result: $result")
// Send the result from square immediately to
// another function
println("square(5): ${square(5)}")
// Use the result returned from square in a
// conditional expression
if (square(3) < 15) {
println("square(3) is less than 15")
}
}
result: 16
square(5): 25
square(3) is less than 15
Anonymous Functions 91
Anonymous Functions
So far in this chapter we have focussed on named functions; that is functions defined
with an explicit name.
Kotlin also possesses another type of function known as an anonymous function.
This is a type of function that does not have a specific name provided for it when it
is defined.
The format used to define an anonymous function is:
fun main() {
println(func(5))
}
When the val func, along with the round brackets () is used above, it accesses
the anonymous function and executes it. This causes the value 5 to be passed into the
anonymous function and to have its value incremented by one. This value is returned
by the function which is then printed out, for example:
Kotlin provades a shorthand form syntax for functions where the body of the function
is a single expression and that expression generates the result for the function. This
shorthand form is available in both the named function and anonymous function
formats.
92 5 Functions in Kotlin
In both cases the return type is automatically inferred by the compiler from the
result of the single expression that forms the body of the function.
An example of using this shorthand function definition form for a named function
is given below:
Here the function incrementer(Int) has a return type of Int while the
body of the function is comprised of the single expression x+ 1. It is of course
possible to define a return type explicitly and this can be considered good practice
as it is useful documentation to any developer reading the function definition. For
example:
Again the return type is inferred from the result of adding one to the integer i.
We can use these function in the normal way:
fun main() {
println(incrementer(5))
println(adder(5))
}
15
6
Lambdas 93
Lambdas
Lambdas can have any number of arguments but only one expression (that is a
statement that returns a value) as their body. When the expression is executed, and
the value generated from it is returned as the result of the function.
As an example, let us define a couple of lambdas:
fun main() {
println(increment(5))
println(increase(5))
}
In both these examples, the lambda only takes one parameter. In both cases the
lambda is stored in a val. In the first example we explicitly state that type of the
val; in this case (Int) - > Int. This indicates that we expect the val to hold a
reference to a function that takes an Int as a parameter and returns an Int as a
result.
In the second example the Kotlin compiler is used to infer the type of the val
increase. In both cases the result of the lambda is generated by the expressionx+
1 which returns an Int. Note that for the second lambda we had to specify the type
of the variable x as it could not be inferred by the compiler as the val increase
does not specify the type of function it will reference.
To invoke the lambdas, we can access the reference to the function held in the
vals and then use the round brackets to cause the function to be executed, passing in
any values to be used for the parameters:
94 5 Functions in Kotlin
fun main() {
println(increment(5))
println(increase(5))
}
6
6
Other examples of lambda functions are given below (illustrating that a lambda
function can take any number of arguments from zero upwards):
fun main() {
println(increment(5))
println(increase(5))
func0()
func1()
println(func2(3, 4))
println(func3(2, 3))
no args
no args
12
6
Lambdas Versus Anonymous Functions 95
You may be wondering why Kotlin has both anonymous functions and lambda func-
tions as they seem to play a similar role. The short answer is that anonymous func-
tions are more flexible than lambda functions and have one particular feature which
is significantly different to lambdas—they can have multiple return statements.
An anonymous function is more flexible than a lambda function because:
• It can have any level of complexity to the body of the function where as a lambda
has a body comprised of a single expression.
• There can be multiple return statements in the body of the anonymous function
where as a lambda has a single implied returned value for the single expression
that makes up the lambda body.
• It is possible to explicitly specify the return type of an anonymous function, where
as it is determined by the compiler for a lambda.
Recursive Functions
This function will return the value 1 if the number passed in is 1—this is the base
case. Otherwise it will multiply the value passed in to it with the result of calling
itself (the factorial() function) with n − 1 which is the recursive part.
The key to understanding this function is that it has:
1. A termination condition that is guaranteed to execute when the value of n is 1.
This is the base case; we cannot reduce the problem down any further as the
factorial of 1 is 1!
2. A recursive part. In this part, the function recursively calls itself but with n −
1 as the argument; this means each time it calls itself the value of n is smaller.
Thus the value returned from this call is the result of a smaller computation.
To clarify how this works we can add some print statements (and a depth indicator)
to the function to indicate its behaviour:
Calculating Factorial Recursively 97
fun main() {
println("Calling factorial(5)")
println(factorial2(5))
}
When we run this version of the program then the output is:
Calling factorial(5)
Recursively calling factorial(4)
Recursively calling factorial(3)
Recursively calling factorial(2)
Recursively calling factorial(1)
Returning 1
Returning: 2
Returning: 6
Returning: 24
Returning: 120
120
Note that the depth parameter is used merely to provide some indentation to the
print statements.
98 5 Functions in Kotlin
From the output we can see that each call to the factorial() function results
in a simpler calculation until the point where we are asking for the value of 1! which is
1. This is returned as the result of calling factorial(1). This result is multiplied
with the value of n prior to that; which was 2. The causes factorial(2) to return
the value 2 and so on.
Although Recursion can be a very expressive way to define how a problem can be
solved, it is not as efficient as iteration using a loop based operation such as a for or
while loop. This is because a function call is more expensive for the JVM runtime to
process that a for loop. In part this is because of the infrastructure that goes along
with a function call; for example it is necessary to set up an internal stack with values
stored for each separate function invocation so that all local variables are independent
of any other call to that function. It is also related to associated unwinding of the
stack when a function returns. However, it is also affected by the increasing amount
of memory each recursive call must use to store all the data on that stack.
In some languages optimisations are possible to improve the performance of a
recursive solution. One typical example relates to a type of recursion known as tail
recursion. A tail recursive solution is one in which the calculation is performed before
the recursive call. The result is then passed to the recursive step, which results in the
last statement in the function just calling the recursive function.
In such situations the recursive solution can be expressed (internally to the
computer system) as an iterative problem. That is the programmer is able to write the
solution as a recursive algorithm but the interpreter or compiler converts it into an
iterative solution. This allows programmers to benefit from the expressive nature of
recursion while also benefiting from the performance of an iterative solution. Kotlin
is one of the languages that allows for this optimisation.
You might think that the factorial function presented earlier is tail recursive;
however it is not because the last statement in the function performs a calculation
that multiples n by the result of the recursive call.
However, we can refactor the factorial function to be tail recursive. This
version of the factorial function passes the evolving result along with each
recursive call, via the accumulator parameter. We can then mark the function
with the keyword tailrec which tells the Kotlin compiler that it should try to
perform the tail recursion optimisation on this function. The resulting function is
given below:
Tail Recursion Optimisation 99
The difference between a tailrec recursive function and a non tailrec recursive
function in Kotlin can be seen using the following two recursive functions. The initial
bang() function will fail when the RuntimeException is thrown (a type of error
condition that causes the program to terminate). This causes the program to print the
error stack trace which will show what functions were called and when:
From this it is clear that the bang() function has been called once from the
main() function, but a further 4 times due to recursion (I.e. The bang() function
has called the function bang() 4 times).
If we now modify this function to be a tail recursive version and mark it with
tailrec as shown below:
When we run this version of the function we now get the following output:
100 5 Functions in Kotlin
Which shows that the bangTailRec() function has been invoked only once.
This is because the recursive algorithm described in our code has been converted
into a loop by the compiler which results in a more efficient runtime version of the
function.
Inline Functions
One issue with all of the above is that a function will be invoked with all the inherit
overheads of this. If all our function does is add one to a number that could be quiet
expensive. To get around this overhead, whilst still allowing the expressiveness of a
function, Kotlin provides the keyword inline. This allows the compiler to try to
inline the function. That is the compiler can try and replace the function call with
the actual expression represented by the function. For example:
Online Resources
Exercises
The aim of this exercise is to write a function that can identify (print out) all the
prime numbers in a given range.
A Prime Number is a positive whole number, greater than 1, that has no other
divisors except the number 1 and the number itself.
That is, it can only be divided by itself and the number 1, for example the numbers
2, 3, 5 and 7 are prime numbers as they cannot be divided by any other whole number.
Exercises 101
However, the numbers 4 and 6 are not prime numbers because they can both be divided
by the number 2. In addition the number 6 can also be divided by the numbers 2 and
3.
You should write a function to calculate prime number starting from 1 up to the
value passed into the function. In the main() function ask the user for the maximum
number to calculate prime numbers up to and then invoke your prime() function
passing in this number.
If the user inputs a number below 2, print an error message.
For any number greater than 2 the prime() function should loop for each integer
from 2 to that number and determine if it can be divided by another number (you
will probably need two for loops for this; one nested inside the other).
For each number that cannot be divided by any other number (that is its a prime
number) print it out.
For example:
You can go further with the number guess game presented in the last chapter.
Take the number guess game and break it up into a number of functions. There is
not necessarily a right or wrong way to do this; look for functions that are meaningful
to you within the code, for example:
1. You could create a function to obtain input from the user.
2. You could create another function that will implement the main game playing
loop.
3. You could also provide a function that will print out a message indicating if the
player won or not.
4. You could create a function to print a welcome message when the game starts
up.
Chapter 6
Higher Order Functions
Introduction
In this chapter we will explore the concept of high-order functions. These are func-
tions that take as a parameter, or return (or both), a function. To do this we will
first look into how Kotlin represents functions in memory and explore what actually
happens when we execute a Kotlin function.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 103
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_6
104 6 Higher Order Functions
We can then call it by specifying its name and the round brackets:
fun main() {
val message = getMessage()
println(message)
}
This of course prints out the string "Hello Kotlin World!” which is what
you should expect by now.
Functions as Entities
A few chapters back we threw in something stating that if you forgot to include the
round brackets then you were referencing the function itself rather than trying to
execute it!
What exactly does that mean? Let’s see what happens if we forgot to include
the round brackets but included the box operator :: which allows us to reference the
function:
fun main() {
val message2 = ::getMessage
println(message2)
}
fun main() {
val func: () -> Unit = { println("Hello World") }
val func1 = func
func()
func1()
}
This illustrates how we might define a lambda function and store a reference to
it into a variable func. Notice that the type of this variable is () - > Unit in
other words it references a function that takes no parameters and return Unit. We
then assign this reference to the val func1. This means they both reference the same
lambda function and both func and func1 are of the same type.
Box Operator
The box operator (::) can be used to obtain a reference to a named function. By
default when you access a named function it must be invoked, it is not possible to
merely reference the function, thus in Kotlin it is not possible to write:
fun main() {
println(increase(5))
val inc = increase
}
If you try this you will get a compilation error telling you.
However, you can use the box operator (::) to obtain a reference to the named
function that allows you to reference it without invoking it.
We can now modify the above program as shown below:
fun main() {
println(increase(5))
val inc = ::increase // obtain reference to function
println(inc)
println(inc(5))
}
6
fun increase(kotlin.Int): kotlin.Int
6
Note that the reference held in inc is to the increase function that takes and
Int and returns an Int. This the type of the val inc is (Int) - > Int.
Given that we can assign a reference to a function into a val or var; then this might
imply that we can also use the same approach to pass a reference to a function as an
argument to another function.
This means that one function can take another function as a parameter. Such
functions are known as Higher Order Functions and are one of the key constructs in
Functional Programming.
In fact in Kotlin, Higher-Order Functions are functions that do at least one of the
following (and may do both):
• take one or more functions as a parameter,
• return as a result a function.
All other functions in Kotlin are first-order functions.
Higher Order Function Concepts 107
Many of the functions found in the Kotlin libraries are higher order functions. It
is a common enough pattern that once you are aware of it you will recognise it in
many different libraries.
The function processor is a higher order function because its behaviour (and
its result) will depend on the behaviour defined by another function—the one passed
into it.
We could now define a function to increment a value by 1 or a function to multiple
an integer by itself. These functions could then be used with the processor()
function to obtain a result. For example:
fun main() {
// Anonymous function
val increment = fun(i: Int)= i + 1
println(processor(5, increment))
// Lambda function
val mult: (Int) -> Int = { x -> x * x }
println(processor(5, mult))
}
Note that we have used both an anonymous function and a lambda function in
this example. Now we can use the functions increment and mult as parameters
to processor().
The output from the above program is:
6
25
108 6 Higher Order Functions
It is common to define the lambda to be used with a Higher Order Function at the
point at which the Hight Order Function is being invoked.
This generates:
This is such a common pattern in Kotlin that it even provides a special syntax
to make it easy to create lambda functions at the point of invocation. This syntax
allows the lambda to be defined after the function parameters as long as the function
is the last parameter in the higher order functions parameter list. It is referred to as
Trailing Lambda Syntax and is the preferred Kotlin style.
This is illustrated below:
Kotlin even allows for an even shorter form of this syntax using an implicit it
parameter (if there is just one parameter specified):
Looking at the previous section you may be wondering why you would want to use
a higher-order function or indeed why define one at all. After all could you not have
called one of the functions (increment, mult) directly by passing in the integer
to used? The answer is of course, we could have done:
multi(10)
processor(10, mult)
Using Higher Order Functions 109
The first approach would seem to be both simpler and more efficient.
The key to why higher-order functions are so powerful is to consider what would
happen if we know that some function should be applied to the value 10 but we do
not yet know what it is. The actual function will be provided at some point in the
future. Now we are creating a reusable piece of code that will be able to apply an
appropriate function to the data we have, when that function is known.
For example, let us assume that we want to calculate the amount of tax someone
should pay based on their salary. However, we do not know how to calculate
the tax that this person must pay as it is dependent on external factors. The
calculateTax() function could take an appropriate function that performs that
calculation and provides the appropriate tax value.
The following listing implements this approach. The function
calculateTax() does not know how to calculate the actual tax to be paid, instead
a function must be provided as a parameter to the calculateTax() function.
The function passed in takes a number and returns the result of performing the calcu-
lation. It is used with the salary parameter also passed into the calculateTax()
function.
fun main() {
val tax = calculateTax(45000.0, ::simpleTaxCalculator)
println("Tax payable: $tax")
}
fun main() {
val tax = calculateTax(45000.0) {amount -> amount * 0.25}
println("Tax payable: $tax")
}
In this case the trailing lambda syntax is used and the lambda function calculates
the tax as 25% of the amount. This generates:
In Kotlin as well as passing a function into another function; functions can be returned
from a function. This can be used to select amongst a number of different options or
to create a new function based on the parameters.
For example, the following code creates a lambda function that can be used to
check whether a number is even, odd or negative based on the string passed into
it. It also provides a default lambda function that always returns true. Notice that
the syntax used allows the body of the function to be defined using a single when
expression, thus the when expression defines the complete body of the function and
returns a value given any input. The returned value is of course a lambda function:
Note the use of the else clause to handle input to the function that is not “even”,
“negative” or “positive”.
This function is a function factory for lambda functions that can be created to
perform specific operations. It is used below to create four functions that can be used
to validate what type a number is:
Using Higher Order Functions 111
fun main() {
val isEven = makeChecker("even")
val isPositive = makeChecker("positive")
val isNegative = makeChecker("negative")
val isInteger = makeChecker("")
println("isEven(3): ${isEven(3)}")
println("isPositive(3): ${isPositive(3)}")
println("isNegative(3): ${isNegative(3)}")
println("isInteger(3): ${isInteger(3)}")
}
isEven(3): false
isPositive(3): true
isNegative(3): false
isInteger(3): true
Of course, it is not only lambda functions that can be returned from a function;
it is also possible to return anonymous functions. As with a lambda function, an
anonymous function can be returned directly from a function as long as the return
type matches the function type:
It is also possible to returned a named function (which can also be defined within
the scope of the function) from a function. However, it is then necessary to return
the callable reference to the named function using the box operator:
fun main() {
val func1 = makeAnonFunction()
println("func1(3, 2): ${func1(3, 2)}")
println("func1(3, 3): ${func1(3, 3)}")
println("func1(3, 1): ${func1(3, 1)}")
println("-------------")
func1(3, 2): 5
func1(3, 3): 6
func1(3, 1): 4
-------------
func2(3, 2): 5
func2(3, 3): 6
func2(3, 1): 4
Online Resources
Further information on higher order functions in Kotlin can be found using the
following online resources:
• https://en.wikipedia.org/wiki/Higher-order_function Wikipedia page on Higher
Order functions.
• https://kotlinlang.org/docs/reference/lambdas.html Higher Order Functions and
Lambdas in Kotlin.
• https://www.tutorialspoint.com/functional_programming/functional_program
ming_higher_order_functions.htm A tutorial on higher order functions.
Exercises
fun
This function takes an integer parameter and a second function to apply to the
parameter. The second function takes an Int and returns an Int.
Now you should write a simple program that uses the higher order function you
just created to execute a function passed to it.
An example of the sort of thing you might implement is given below:
fun main() {
println(myHigherOrderFunction(5) { it + 1 })
println(myHigherOrderFunction(5) { it - 1 })
println(myHigherOrderFunction(5) { it * 2 })
6
4
10
7
10
15
Chapter 7
Curried Functions
Introduction
Currying Concepts
At an abstract level, consider having a function that takes two parameters. These two
parameters, x and y are used within the function body with the multiply operator in
the form x * y. For example, we might have:
fun operation(x, y) = x * y
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 115
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_7
116 7 Curried Functions
operation(2, 5)
operation(2, 10)
operation(2, 6)
operation(2, 151)
All of the above would double the second number. However, we have had to
remember to provide the value 2 so that the number can be doubled. Of course the
number 2 has not changed between any of the invocations of the operation()
function. What if we fixed the first parameter to always be 2, this would mean that we
could create a new function that apparently only takes one parameter (the number to
double). For example, let us say that in pseudo code we could write something like:
Such that the ‘*’ acts as a wild card for the missing parameter. We could now
write:
double(5)
double(151)
A curried function in Kotlin is a function where one or more of its parameters have
been applied or bound to a value, resulting in the creation of a new function with
one or more fewer parameters than the original. For example, let us create a named
function that multiplies two numbers together:
This is a general function that does exactly what it says; it multiplies any two
numbers together. These numbers could be any two integers etc.
We can thus invoke it in the normal manner:
Kotlin and Curried Functions 117
fun main() {
println("multiply(2, 5): ${multiply(2, 5)}")
}
multiply(2, 5): 10
We could now define a new function that takes a function and a number and
returns a new (anonymous) function that takes one new parameter and calls the
function passed in, with the number also passed in, and the new parameter:
fun multyBy(num: Int, func: (Int, Int) -> Int): (Int) -> Int {
return fun(y: Int) = func(num, y)
}
Look carefully at this function; it has used or bound the number passed into the
multBy() function to the invocation of the function passed in, but it has also defined
a new variable ‘y’ that will have to be provided when this new anonymous function
is invoked. It then returns a reference to the anonymous function as the result of
multBy().
The multBy() function can now be used to bind the first parameter of the
multiply() function to anything we want. For example, we could bind it to 2
so that it will always double the second parameter and store the resulting function
reference into a val double:
We could also bind the value 3 to the first parameter of multiple to make a function
that will triple any value:
println("double(5): ${double(5)}")
println("triple(5): ${triple(5)}")
double(5): 7
triple(5): 8
118 7 Curried Functions
You are not limited to just binding one parameter; you can bind any number of
parameters in this way.
We could of course have also used an anonymous function or a lambda with the
multBy() factory function, for example:
println("doubler(5): ${doubler(5)}")
println("tripler(5): ${tripler(5)}")
doubler(5): 10
tripler(5): 1
Curried functions are therefore very useful for creating new functions from
existing functions.
Closure
as the variable is defined within the body of the main() function. Thus the variable
more is within scope at the point of definition.
fun main() {
var more = 100
val increase: (Int) -> Int = { i -> i + more }
println(increase(10))
more = 50
println(increase(10))
}
Within the main() function we then invoke the increase function by passing in the
value 10. This is done twice with the variable more being reset to 50 between the
two. The output from this program is shown below:
110
60
Note that it is the current value of more that is being used when the function
executes, and not the value of more present at the point that the function was defined.
Hence the output is 110 and 60 that is 100 + 10 and then 50 + 10.
This might seem obvious as the variable more is still in scope within the main()
function. Thus when the lambda function is invoked it can reference the var more.
However, consider the following example:
fun main() {
println(increment(5))
resetFunc()
println(increment(5))
}
fun resetFunc() {
// Local val is bound and stored in function
// as it is used within the function body
val addition = 50
increment = { a -> a + addition }
}
Online Resources
Exercises
For example:
fun main() {
val dollarsToSterling = curry(0.77, ::convert)
println(dollarsToSterling(5.0))
3.85
13.2
9.1
10.26
Part II
Object-Oriented Kotlin
Chapter 8
Kotlin Classes
Introduction
A class is one of the basic building blocks of Kotlin. It is also a core concept in
a style of programming known as Object Oriented Programming (or OOP). OOP
provides an approach to structuring programs/applications so that the data held, and
the operations performed on that data, are bundled together into classes and accessed
via instances (or examples) of those classes.
This chapter considers the core constructs in Kotlin used to define classes.
Classes
Classes act as templates which are used to construct instances. Classes allow
programmers to specify the structure of an instance (i.e. its member properties)
and the behaviour of an instance (i.e. its member functions) separately from the
instance itself. This is important, as it would be extremely time-consuming (as well
as inefficient) for programmers to define each instance individually. Instead, they
define classes and create instances of those classes.
As an example, in an OOP style program, employees might be represented by a
class Employee where each employee has an id, a name, a department and a
deskNumber etc. They might also have operations associated with them such as
takeAHoliday() or getPaid().
A particular employee instance would then have their own values to represent
their name, employee id, desk number etc. An instance is therefore an example of
a class. All instances of a class possess the same data properties and behaviour but
contain their own data values. Each instance of a class has the same programmer
interface.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 125
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_8
126 8 Kotlin Classes
In many cases classes are used to represent real world entities (such as employees,
bank accounts, orders, players in a game etc.) but they do not need to, they can
also represent more abstract concepts such as a transaction between one person and
another (for example an agreement to buy a meal).
We might represent any type of data item using a combination of properties (or fields)
and behaviours. These properties will use existing data types such as Ints, Doubles,
Booleans and String or other classes.
For example, when defining the class Person we might give it:
• a property for the person’s name of type String,
• a property for their age of type Int,
• a property for their email of type Email,
• some behaviour to give them a birthday (which will increment their age),
• some behaviour to allow us to send them a message via their email,
• etc.
In Kotlin classes are thus used:
• as a template to create instances of that class,
• define member functions for common behaviours for a class of things,
• define member properties to hold data within the instances.
Instances of a class, on the other hand, can:
• be created from a class,
• hold their own values for properties,
• execute member functions within the context of the instance,
• may have many copies in the system (all with their own data).
A class should accomplish one specific purpose; it should capture only one idea. This
is know as the Single Responsibility Principle. If more than one idea is encapsulated
in a class, you may reduce the chances for reuse, as well as contravene the laws of
encapsulation in object-oriented systems. For example, you may have merged two
concepts together so that one can directly access the data of another. This is rarely
desirable.
What are Classes for? 127
Class Terminology
The following terms are used in Kotlin (and other languages that support object
orientation):
• Class A class defines a combination of data and behaviour that operates on that
data. A class acts as a template when creating new instances.
• Instance An instance is an example of a class. All instances of a class possess the
same data fields/attributes but contain their own data values. Each instance of a
class responds to the same set of requests.
• Property The data held by an object is represented by its properties (also some-
times known as an attribute, field or an instance variable). The “state” of an object
at any particular moment relates to the current values held by its properties.
• Member Function A member function (referred to in several other Object Oriented
languages as a method) is a function defined within a class.
• Message A message is sent to an instance requesting some operation to be
performed or some property to be accessed. It is a request to the object to do
something or return something. However, it is up to the instance to determine
how to execute that request. A message may be considered akin to a function call
for object oriented programming. A message call is typically represented using
the dot notation in object oriented languages. Thus the receiver of the message is
to the left of the ‘dot’ and the requested behaviour or property to the right.
Note many Object Oriented Programming Languages use the terms instance and
object interchangeable (such languages include Java, C#, Python etc.). However in
Kotlin an object is a different concept to an instance. Objects will be discussed in
the next chapter.
Property is the special name in Kotlin given to the data which is held by an
instance. The “state” of an instance at any particular moment relates to the current
values held by its properties.
Class Definitions
Although you should note that you can mix the order of the definition of the
init{} blocks, properties and member functions as required within a single class.
The following code is an example of a class definition:
Although this is not a hard and fast rule in Kotlin, it is common to define a class
in a file named after that class. For example, the above code would be stored in a
file called Person.kt; this makes it easier to find the code associated with a class.
This is shown below using the IntelliJ IDE:
Class Definitions 129
This very simple class definition actually captures several things, these are:
• It defines a new public class called Person.
• The class Person has two properties, name and age.
• The name property is a public read-only property (referred to as a val property).
• The age property is a public read–write property (referred to as a var property).
• The class defines a constructor that takes two parameters, one of type String
that will be used to initialise the name property; and one of type Int that will
be used to initialise the age property.
• The keyword constructor is optional but is used here to explicitly state that
the contents of the brackets will define the constructor. Strictly speaking this is
the primary constructor (there can be secondary constructors known as auxiliary
constructors which we will look at later in this chapter).
• The {} for the class body is optional here as there is no class body.
New instances (examples) of the class Person can be created by using the name
of the class and passing in the values to be used for the parameters required by the
constructor.
For example, the following creates two instances of the class Person:
fun main() {
val p1 = Person("John", 36)
val p2 = Person("Phoebe", 21)
}
The val p1 holds a reference to the instance of the class Person whose properties
hold the values “John” (for the name) and 36 (for the age). In turn the val p2
references an instance of the class Person whose name and age properties hold
the values “Phoebe” and 21. Thus in memory we have:
130 8 Kotlin Classes
The two vals reference separate instances or examples of the class Person. They
therefore respond to the same set of operations or member functions and have the
same set of properties (such as name and age); however, they have their own values
for those attributes (such as “John” and “Phoebe”).
Each instance also has its own unique identifier—that shows that even if the
attribute values happen to be the same between two objects (for example there happen
to be two people called John who are both 36); they are still separate instances of the
given class. This identifier or hashcode can be seen when the instances are printed,
for example:
fun main() {
val p1 = Person("John", 36)
val p2 = Person("Phoebe", 21)
println(p1)
println(p2)
}
When this code is run p1 and p2 will generate different identifiers as hexadecimal
numbers along with the type of instance being printed, for example:
Person@1b28cdfa
Person@eed1f14
Note that actual number generated may vary from that above but should still be
unique (within your program).
Given that in the above example, p1 and p2 reference different instances of the class
Person; what happens when p1 or p2 are assigned to another variable? That is,
what happens in this case:
What does the val px reference? Actually, it makes a complete copy of the value
held by p1; however, p1 does not hold the instance of the class Person; it holds
the address of the instance. It thus copies the address held in p1 into the variable
px. This means that both p1 and px now reference (point at) the same instance in
memory; we therefore have this:
Of course, if p1 was a var and was subsequently assigned a different object (for
example if we ran p1 = p2) then this would have no effect on the value held in px;
indeed, we would now have:
When we used the println() function to print the instances held by p1 and p2,
we got what might at first glance appear to be a slightly odd result:
Person@1b28cdfa
Person@eed1f14
132 8 Kotlin Classes
What this is showing is the name of the class (in this case Person) and a hexadec-
imal number indicating a unique value for each instance held in memory. Neither of
which is particularly useful as it certainly doesn’t help us in knowing what informa-
tion p1 and p2 are holding. The only thing it does it indicate that they are separate
instances in memory.
We can access the properties held by p1 and p2 using what is known as the dot
notation. This notation allows us to follow the variable holding the instance with
a dot (‘.’) and the property we are interested in accessing. For example, to access
the name of a Person instance we can use p1.name or for their age we can use
p1.age:
John
36
println("${p1.name} is ${p1.age}")
println("${p2.name} is ${p2.age}")
In this case we are accessing the name and age properties of the instances held by
p1 and p2 and so these expressions will both be wrapped in curly brackets (${…})
within the String template. If we run this code we now get:
John is 36
Phoebe is 21
Working with Properties 133
Updating Properties
We can also update the attributes of an object directly, for example we can write:
p1.age = 57
This will update the age property of the instance being referenced by the val p1.
If we now run
println("${p1.name} is ${p1.age}")
John is 57
However, we cannot modify the name property as this was defined as a val. A
val is a read-only property and thus any attempt to reset the name once the class has
been instantiated will result in a compile time error.
We will see in a later chapter that we can refine how properties are accessed,
stored and where they are visible.
We can now return to the definition of the class Person we presented earlier:
Kotlin will infer the constructor keyword between the name of the class and the
round brackets (). In fact the presence of a constructor at all is optional. If we are
creating a class with no properties and no behaviour, then the minimal class definition
in Kotlin is illustrated by the class Bag below:
class Bag
This is a complete Kotlin class, albeit one that does not do very much. It defines
a new class, with a default zero parameter constructor (provided by the compiler)
and no additional behaviour over the vary basic default behaviour available for all
instances. That is, it can be printed etc. We can therefore write:
class Bag
fun main() {
val b = Bag()
println(b)
}
Bag@1fb3ebeb
Primary Constructors
It is possible to provide default values for the parameters to a constructor. This makes
any parameters with a default value an optional parameter. To define a default value
for a parameter all you need to add is an assignment operator (=) followed by the
Classes and Constructors 135
value to use if that parameter is not provided. This is shown for the class Person2
below:
In class Person2 both name and age are now optional constructor params; as
name defaults to “” and age to 0.
If all params have default values then the compiler generates a zero parameter
constructor which can be useful for Java inter-operability.
Given the definition of the Person2 class all of the following are now valid ways
to create an Person2 instance:
fun main() {
val p1 = Person2("John", 36)
val p2 = Person2("Denise")
val p3 = Person2()
}
The example creates an instance of the class Person2 with the String “John”
and the Int 36. The second creates an instant of the class Person2 with the
String “Denise” and default value for the age parameter (the Int 0). The final
example creates an instance of the class Person2 with the empty String “” and
the Int 0.
Using the class Person2 we can go further as we can use named parameter passing
rather than positional parameter passing. This is a similar idea to that used with Kotlin
functions.
Using named parameter passing the position that a parameter is placed is no longer
significant. Thus we can also create instances of the Person2 class using:
In this code snippet, the first Person2 instance uses positional parameter passing
for the first parameter name (which obtains the value “Adam”) and named parameter
passing for the age parameter.
In the second example, named parameter passing is used for both the age and
the name parameters and thus the position of these parameters is not significant.
136 8 Kotlin Classes
In the final example only the age parameter is passed in and thus the name
parameter will use the default value “”.
Private Properties
This means that when an instance of the Person3 class is created a value for
the id must be provided although name and age are optional parameters to the
constructor. In turn name and age are public read-only properties, however id is a
private property and thus not accessible from outside the class. This is shown by the
following simple application:
fun main() {
val p1 = Person3("id1", "John", 50)
println("${p1.name} is ${p1.age}")
// compile time error id is a private property
// println("${p1.id}")
}
John is 50
Classes and Constructors 137
Auxiliary Constructors
Every class in Kotlin has a primary constructor, however optionally any class in
Kotlin can also have one or more auxiliary constructors. Auxiliary constructors are
defined:
• within the body of the class,
• using the constructor keyword,
• do not have an explicit name,
• must call another constructor (either another auxiliary constructor or the primary
constructor). This ensures that the primary constructor is the sole point of entry
to the class. This is done by specifying the constructor to invoke by postfixing the
constructor signature with ‘:’ followed by this() and the parameter list used to
select the constructor to invoke.
To illustrate the use of an auxiliary constructor, if we wanted to allow a Person to
be instantiated with just an age, then one way to do it would be to define an auxil-
iary constructor. This is illustrated in the following example. It defines an auxiliary
constructor for the Person4 class that takes an integer to use for a Persons age
without the need to define the person’s name:
This example illustrates the syntax used with an auxiliary constructor. It uses
the constructor keyword to define the auxiliary constructor and then it uses the:
this() syntax to invoke another constructor (in this case the primary constructor)
that takes a String and an Int.
Note that the only thing this auxiliary constructor does is to call the primary
constructor providing a default name for all unnamed person. There is therefore no
need to define a constructor body and thus we could have written the class as follows:
fun main() {
val p1 = Person4(21)
println("${p1.name} is ${p1.age}")
}
In auxiliary constructor
unnamed is 21
It should be noted that thanks to the availability of default values for parameters in
the primary constructor, it is far less common to need to define auxiliary constructors
than might be the case in other languages. Indeed the example used for the class
Person4 would be much simpler to define using default parameter values.
Initialisation Behaviour
One issue with the approach taken to defining constructors in Kotlin is how do you
define any behaviour you want to run when an instance of a class is created? The
answer is that you can define one or more initialisation blocks.
An initialisation block is a block of code that is run after the instance of the class
is created and just after the primary constructor is executed (which will initialise all
the constructor parameters) but before any auxiliary constructors are executed. The
initialisation behaviour is guaranteed to execute before any client code has access to
the instance.
An initialisation block is defined using the keyword init followed by a block
of code, for example:
/**
* Defines initialisation behaviour that is run just
* after the class is instantiated.
* Has access to all constructor parameters whether they
* are properties or not
*/
init {
println("In Init")
}
}
Initialisation Behaviour 139
Now when an instance of the class Person5 is created the init{} block will
be executed and the String “In Init” will be printed out. This is illustrated by the
following code:
fun main() {
println("Creating instance")
val p1 = Person5("Jasmine", 23)
println(p1)
println("Done")
}
Creating instance
In Init
Person5@1b28cdfa
Done
As you can see between the String “Creating instance” being printed out and
printing out the instance the “In Init” string has been printed.
Actually it is possible to have multiple init{} blocks in a class. If there are
multiple init blocks then each is run in turn in the order that they are defined in
the class. For example:
init {
println("In Init 2")
}
init {
println("In Init 3")
}
}
Creating instance
In Init
In Init 2
In Init 3
Person5@1b28cdfa
Done
140 8 Kotlin Classes
This does raise the question why would you have multiple initialisation blocks.
There are several justifications for the presence of multiple init blocks in Kotlin:
• Modularisation of your initialisation behaviour. Each block could be used to
initialise a specific part of the class, thus separating out different initialisation
concerns.
• Stepped initialisation behaviour. A property needs to be defined based on
behaviour in one init{} block which is then used by another init{} block.
For example:
class Foo() {
val x = Bar.getValue()
init {
// check the validity of x and abort if invalid
}
init {
// Check validity of y and
// continue with initialisation if all ok
}
}
In this case the first init block checks that the value provided is valid and if it is
not then the initialisation process will abort (for example it could throw an exception
which is discussed in a later chapter). If the value of x is valid then the property y
is set based on the value in the property x. This is then checked in the second init
block and if all is ok then the remainder of the initialisation process is completed.
Classes do not only hold data; they can also hold behaviour. This behaviour is defined
within member functions. That is functionality that is a member (part of) a classes
definition. Or to put it another way member functions represent functionality that is
tied to an instance of the class. Note that in several other Object Oriented Program-
Defining Instance Behaviour 141
ming languages member functions are referred to as methods and many developers
still colloquially use the term with reference to Kotlin.
Member functions are defined:
• within the body of a class,
• are declared using the keyword fun,
• followed by the name of the member function,
• will have a parameter list which may be empty defined using round brackets,
• have a return type that will either be Unit or a specific type such as Int or String,
• have a body which defines the behaviour of the member function,
• have access to all other member functions and member level or constructor
properties.
An example of a simple member function is given by the following class that has a
member function called birthday(). This member function takes no parameters
and increments the age property by 1:
fun birthday() {
println("Happy birthday you were $age")
age++
println("You are now $age")
}
If we now create an instance of the class Person6 and call the birthday()
member function on it using the dot notation, then the age will be incremented by
1, for example:
fun main() {
val p1 = Person6("Adam", 21)
println("${p1.name} is ${p1.age}")
p1.birthday()
println("${p1.name} is now ${p1.age}")
}
Adam is 21
Happy birthday you were 21
You are now 22
Adam is now 22
142 8 Kotlin Classes
As you can see Adam is initially 21; but after his birthday he is now 22.
Member functions (like standard functions) can be given parameters. Thes are defined
within the member functions round brackets and there can be zero or more parameters.
Each parameter has its own type specified using the format:
If there is more than one parameter then each parameter declaration is separate
by a comma, for example:
class MathUtils {
fun add(x: Int, y: Int) {
println(x + y)
}
}
In this example, the member function add() prints out the result of adding two
parameters together. The parameters are declared within the round brackets of the
member function declaration. In this case both parameters are of type Int and their
declarations are separated by a comma.
Member functions also have a return type specified; the default if a type is not explic-
itly specified is Unit. Unit indicates that the member function returns nothing.
However, any valid type can be returned from a member function. This is specified
by:
: <Type>
After the member function name and parameter list, for example:
class MathUtils {
fun add(x: Int, y: Int): Int {
return x + y
}
}
Defining Instance Behaviour 143
In the above example, the add() member function function returns a value of
type Int. The actual value to return is indicated using the keyword return. Thus
in this case the value returned is the result of adding x and y together.
In Kotlin all parameters to member functions are vals (which is the same as is the
case for functions). This means that once the member function has been invoked it is
not possible to reassign a value to that parameter. This means that it is not possible
to write the following;
class MathUtils {
fun add(i: Int) {
i = i + 1 // won't compile i is a val
}
}
As is the case with both functions and constructor parameters, values can be passed
to member functions using either positional parameter passing or named parameter
passing. It is also possible to mix positional and named parameter passing in the
same member function invocation. Some examples of the options available when
invoking a member function are given below:
144 8 Kotlin Classes
class Math {
fun main() {
val math = Math()
As you can see from this there is a lot of flexibility in how parameters are passed
in. Also note that once you are using named parameter passing then the order in
which the parameters are presented is no longer significant.
Prior to Kotlin 1.4 it was possible to mix positional and named parameter passing.
Instead it was necessary to list all the positional parameters first and then all the named
parameters; that it was not possible to use named parameter passant then position
in the same member function invocation. However, since Kotlin 1.4 this has been
allowed. The is illustrated below:
fun main() {
val math = Math()
However, it should be noted that if you do this you need to make sure that the named
parameters do not clash with the positional parameters, for example math.max(y=
3, 4) would be illegal and will not compile as the named parameter is y and the
positional parameter is also bound to y.
Defining Instance Behaviour 145
As with both functions and constructors member function parameters can be provided
with a default value. This means that they become optional parameters. For example:
class Math {
fun add(x: Int, y: Int = 1): Int = x + y
}
fun main() {
val math = Math()
math.add(5, 3): 8
math.add(5): 6
class Printer {
fun echo(vararg args: String) {
for (arg in args) println(arg)
}
}
146 8 Kotlin Classes
The echo member function can be given zero or more strings; these strings can
be comma separated and can be processed within the echo member function. In this
case we use the for loop structure to loop over each of the values in the args String
array in turn, printing out each value.
Examples of using the echo() member function are given below:
fun main() {
val printer = Printer()
printer.echo()
println("--------")
printer.echo("John")
println("--------")
printer.echo("John", "was", "here")
}
--------
John
--------
John
was
here
Let us look for a moment at the special variable this. It is used to represent the
instance within which a member function is executing. This provides the context
within which the member function runs and allows the member function to access
the data held by the object. Thus this is the instance itself.
All member properties and member functions can be explicitly referenced using
the this self reference. It can be useful to do this when a local val or var has
the same name as a member level property. By default Kotlin looks locally within
a member function for a specific named local val or var before looking in the
instance for a member property. Thus if a local val or var has the same name as a
member level property then it is necessary to prefix the member level property with
the this keyword.
An example of this is shown below:
Defining Instance Behaviour 147
fun birthday() {
val age = this.age
println("Happy birthday you were $age")
this.age++
println("You are now ${this.age}")
}
println("funcs('3'): ${func3("3")}")
If we want to apply this operation to a specific string we can use a bound callable
reference:
This can be invoked just by calling the function (as no parameter is required):
println("func4(): ${func4()}")
148 8 Kotlin Classes
Kotlin actually provides two ways in which a member function can be defined; as
well as the approach presented so far there is the single expression member function
definition. This form of member function definition is intended as a convenient
shorthand form that can be used if:
• a member function body is formed on just a single expression,
• the return value for the function can be directly inferred from the single expression.
In this case there is no need to use curly brackets to define the body of the member
function nor is there a need to use the return keyword to explicitly return the value.
In addition the return type can be inferred by Kotlin based on the result of the
single expression.
The general format of a single line member function is
For example, the following class Author, all of the member functions provide
the same behaviour with the same signature. However, each definition uses a different
syntax form:
Each version of the printMe() member function does the same thing; the first
version printMe1(), uses the traditional long hand form of a member function
declaration.
The second form uses the shorthand form but explicitly specifies the return type
as Unit. However, this could have been inferred by Kotlin and thus the third version
is the most concise and most common shorthand form.
Any code that uses the class Author however will not see a difference between
any of these member functions, for example:
fun main() {
val author = Author("John")
author.printMe1()
author.printMe2()
author.printMe3()
}
Single Expression Member Functions 149
Author - John
Author - John
Author - John
In a previous section we printed out information from the instances of class Person
by accessing the properties name and age.
However, to do this we needed to know the internal structure of the class Person.
That is, we need to know that there are properties called name and age available in
this class.
It would be much more convenient if the instance itself knew how to convert itself
into a String to be printed out!
In fact we can make the class Person do this by defining a member function that
can be used to convert an instance into a String for printing purposes. This member
function is the toString(): String member function. It is expected to return
a String which can be used to represent appropriate information about a class.
The signature of the member function is:
This is to some extent a special member function as all things in Kotlin know how to
convert themselves to a String to be printed or logged etc. In fact our class Person
already knows how to print itself as a String, its just that the default behaviour
provided isn’t very useful to us, it merely prints the name of the class and a unique
hashcode generated for the instance.
However, we can override this default behaviour with our own definition of the
toString() member function defined within the class Person.
To do this we must define a new member function but mark it with the keyword
override (the exact meaning of override will be explained in the chapter on class
inheritance).
We can add this member function to our class Person and see how that affects
the output generated when using the println() function.
We will return a String from the toString() member function that provides
and the name and age of the Person instance:
Note that in the toString() member function we format the returned string
using a String template that references the name and age properties. If we now run
the following code:
fun main() {
val p1 = Person("John", 56)
val p2 = Person("Denise", 53)
val p3 = Person("Phoebe", 23)
val p4 = Person("Adam", 21)
println(p1)
println(p2)
println(p3)
println(p4)
}
Person(John, 56)
Person(Denise, 53)
Person(Phoebe, 23)
Person(Adam, 21)
Providing KDoc
/**
* This is a simple class representing information
* about a person.
*
* @property name the name of this person.
* @property age the age of this person
* @constructor Creates a new person.
*
* @author John Hunt
* @since 1.0
*/
class Person(val name: String = "", var age: Int = 0) {
override fun toString() = "Person($name, $age)"
}
We can now use this class and its implementation of the equals(Any?) member
function in the following program:
fun main() {
val p1 = Person("John", 21)
val p2 = Person("John", 21)
val p3 = p1
// checks for reference equality
println("p1 === p2: ${p1 === p2}")
// checks for structural equality
println("p1 == p2: ${p1 == p2}")
// again structural equality
println("p1.equals(p2): ${p1.equals(p2)}")
println("p1 !== p2: ${p1 !== p2}")
println("p1 === p3: ${p1 === p3}")
println("p1 !== p3: ${p1 !== p3}")
}
As can be seen from this p1 and p2 are structurally equal but not referentially
equal (that is they contain the same data but are not the same reference). In contrast
p1 and p3 both reference the same instance of the class Person and are therefore
referentially equal.
The creation and deletion of objects (and their associated memory) is managed by the
Kotlin Memory Manager. Indeed, the provision of a memory manager (also known
as automatic memory management) is one of Kotlin’s advantages when compared
to languages such as C and C++. It is not uncommon to hear C++ programmers
complaining about spending many hours attempting to track down a particularly
awkward bug only to find it was a problem associated with memory allocation or
pointer manipulation. Similarly, a regular problem for C++ developers is that of
memory creep, which occurs when memory is allocated but is not freed up. The
Automatic Memory Management 153
application either uses all available memory or runs out of space and produces a run
time error.
Most of the problems associated with memory allocation in languages such as
C++ occur because programmers must not only concentrate on the (often complex)
application logic but also on memory management. They must ensure that they
allocate only the memory which is required and deallocate it when it is no longer
required. This may sound simple, but it is no mean feat in a large complex application.
An interesting question to ask is “why do programmers have to manage memory
allocation?”. There are few programmers today who would expect to have to manage
the registers being used by their programs, although 30 or 40 years ago the situation
was very different. One answer to the memory management question, often cited
by those who like to manage their own memory, is that “it is more efficient, you
have more control, it is faster and leads to more compact code”. Of course, if you
wish to take these comments to their extreme, then we should all be programming
in assembler. This would enable us all to produce faster, more efficient and more
compact code than that produced by Kotlin or languages such as Java.
The point about high level languages, however, is that they are more produc-
tive, introduce fewer errors, are more expressive and are efficient enough (given
modern computers and compiler technology). The memory management issue is
somewhat similar. If the system automatically handles the allocation and deallo-
cation of memory, then the programmer can concentrate on the application logic.
This makes the programmer more productive, removes problems due to poor
memory management and, when implemented efficiently, can still provide acceptable
performance.
Kotlin therefore provides automatic memory management. Essentially, it allocates
a portion of memory as and when required. When memory is short, it looks for areas
which are no longer used. These areas of memory are then freed up (deallocated) so
that they can be reallocated. This process is often referred to as Garbage Collection.
The following guidelines may help you to decide whether to split the class with
which you are working. Look at how you describe the class. Consider the following
points:
• Is the description of the class short and clear? If not, is this a reflection on the
class? Consider how the comment can be broken down into a series of short clear
comments. Base the new classes around those comments.
• If the comment is short and clear, do the class and instance variables make sense
within the context of the comment? If they do not, then the class needs to be re-
evaluated. It may be that the comment is inappropriate, or the class and instance
variables inappropriate.
154 8 Kotlin Classes
• Look at how and where the attributes of the class are used. Is their use in line with
the class comment? If not, then you should take appropriate action.
Online Resources
If you want to explore some of the ideas presented in this chapter in more detail here
are some online references:
• https://en.wikipedia.org/wiki/Object-oriented_programming This is the
wikipedia entry for Object Oriented Programming and thus provides a quick
reference to much of the terminology and history of the subject and acts as a
jumping off point for other references.
• https://dev.to/charanrajgolla/beginners-guide---object-oriented-programming
which provides a light hearted look at the four concepts within object orientations
namely abstraction, inheritance, polymorphism and Encapsulation.
• https://www.tutorialspoint.com/kotlin/kotlin_class_and_object.htm A Tutorials
Point course on Object Oriented Programming and Kotlin.
• https://kotlinlang.org/docs/reference/equality.html discusses equality in Kotlin.
See the following for further information on Kotlin classes:
• https://kotlinlang.org/docs/reference/classes.html the Kotlin Standard library
Class tutorial.
• https://kotlinlang.org/docs/reference/kotlin-doc.html documentation on KDoc
documentation tags.
Exercises
fun main() {
val acc1 = Account("123", "John", 10.05, "current")
val acc2 = Account("345", "Denise", 23.55, "savings")
val acc3 = Account("567", "Phoebe", 12.45, "investment")
println(acc1)
println(acc2)
println(acc3)
acc1.deposit(23.45)
acc1.withdraw(12.33)
println("balance: ${acc1.balance}")
}
The following output illustrates what the result of running this code might look
like:
Introduction
This chapter will discuss the difference between objects in Kotlin and instance of
a class. This is important as many other object-oriented languages use these terms
interchangeably. However, in Kotlin they are significantly different concepts, defined
with different language constructs and used in different ways.
Singleton Objects
Kotlin provides another type that can sit along side the class type. It is directly
supported by the language construct object. A Kotlin object is a singleton object
that is accessible to any Kotlin code that has visibility of that object definition. The
term singleton here refers to the fact that there is a single example of the object
definition within the Virtual Machine (JVM) executing the Kotlin program. This is
guaranteed by the language itself and does not require any additional programmer
intervention.
If you have never come across the concept of a Singleton object before it may
appear an odd idea. However, it is very widely and commonly used within the object
oriented programming world. Examples of the singleton concept can be found in
Java, C#, Smalltalk, C++ etc. and have been documented in various ways since it
was first popularised in the so-called Gang of Four patterns book. The four authors
of this book are Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides
(collectively known as the “Gang of Four”, or GoF for short). They popularised the
patterns concepts and ideals (see the online and book resources sections at the end
of this chapter for links and references).
So what are design patterns? They are essentially useful recurring solutions to
problems within software design. For example, “I want to loosely couple a set of
objects, how can I do this?”, might be a question facing a software designer. The
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 157
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_9
158 9 Objects and Companion Objects
Mediator design pattern is one solution to this. If you are familiar with design patterns
you can use them to solve problems in your designs with well established solutions.
Typically early in the design process, the problems are more architectural/structural
in nature, while later in the design process they may be more behavioural. Design
patterns actually provide different types of patterns some of which are at the archi-
tectural/structural level and some of which are more behavioural. They can thus help
every stage of the design process.
The Singleton design pattern describes a type that can only have one instance
constructed for it. That is, unlike other types it should not be possible to obtain
more than one instance within the same program. Thus the Singleton design pattern
ensures that only one instance of a type is created. All elements that use an instance
of that type; use the same instance.
The motivation behind this pattern is that some classes, such as those classes that
involve the central management of a resource, should have exactly one instance. For
example, an object that manages the reuse of database connections (i.e. a connection
pool) could be a singleton.
However, implementing a singleton in some language can be more complex than
initially thought, as it is necessary to ensure that it is not possible to have multiple
instances of the singleton concept, to ensure thread safely in a concurrent environment
etc. Kotlin solves this problem by making the singleton concept part of the language.
As mentioned above Kotlin supports the concept of a singleton object using the
keyword object. An object can have .
• one or more init{} blocks,
• zero or more member functions,
• zero or more member properties,
• its own override of toString().
The member functions and properties can be public or private etc.
Note, however that an object cannot have a constructor.
An example of the definition of a simple object MathsUtils is given below:
Singleton Objects in Kotlin 159
This MathsUtil object has three public member level properties ZERO, MIN
and MAX.
• ZERO and MIN are initialised at the point of declaration.
• The MAX property is initialised within the init{} block that is run when the
singleton object is created.
The object also provides three member functions that can be used to add two
numbers together, subtract two numbers and check whether a number is less than the
MAX number allowed.
It is now possible to directly access the member functions and member properties
of the object in a program. For example:
160 9 Objects and Companion Objects
Note that there will only ever be a single instance of the MathsUtil object in a
single running program.
The output from this program is given below:
It should also be noted that objects are also part of the Kotlin type system and that
defining a new object defines a new type that can be used in your application.
For example, if we create an object Session then we can create vals and vars
of type Session and we can assign the singleton object to a val or var of that
type. This is illustrated below:
In the above code, the Session object has a public val id property and its
own implementation of toString(). The main() function then prints out the
Session singleton object before assigning it to the val s. The val s references the
same instance of the Session object as the name Session does. In fact when
we define an object the name of the object is both its type (i.e., Session) and the
name used to reference the single instance of that type (i.e. Session again). The
end result is that in effect s becomes an alias for the Session object.
Anonymous Objects 161
Anonymous Objects
It is also possible to create anonymous objects that are defined at the point that they
are used. As they are anonymous they do not have a name and cannot be referenced
elsewhere within a program. They can have all the features of a named object such as
member properties, member functions, init{} blocks and a toString() member
function etc. However, they do not have a name and do not explicitly add a new type
to the type system.
An example of an anonymous object is given below:
fun main() {
init {
MAX = 100
}
fun printMe() {
println("Print Me")
}
}
In the above program an anonymous object is created and stored into a val obj.
This object has three properties x, y and MAX. MAX is initialised in an init{} block.
It also possesses a member function printMe(). The output from this program is
given below:
AnonymousObjectAppKt$main$obj$1@1fb3ebeb
100
obj.x = 0, obj.y = 42
obj.x = 50, obj.y = 42
Print Me
Note that the name of the anonymous object is comprised of $ and a sequence
number—that is it has a name but not one you are expected to use.
Companion Objects
Companion objects are singleton objects’ for a class—they can be used to provide
utility functions such as factory member functions that will support the concept being
modelled by the class (they therefore play a similar role to static content within Java
or C# classes).
As a companion object is an object, it is a singleton instance that sits alongside
the class. To define a companion object it must.
• be defined within the body of the class,
• be marked as a companion object,
• must not have a name.
When used in this way companion objects are useful placeholders for house
keeping behaviour, for factory operations, for data shared across all instances of a
class etc.
From the point of view of the user of the class; the companion object and the class
appear to be a single concept. For example, consider the following definition of a
UserSession class and a companion object.
Companion Objects 163
fun printData() {
println("Id is $id and MAX is $MAX")
}
fun main() {
val session1 = UserSession.create()
println(session1)
val session2 = UserSession.create()
println(session2)
}
Notice that the companion object for the UserSession class has a private
val MAX, a private var count (initialised to zero) and a private member function
next().
By default all member functions and properties are public (accessible anywhere);
here we are making the property MAX and count properties and the member function
next() visible only within the UserSession type. That is they are available to
the companion object and also to the code defined within the class UserSession.
However, they are not available to anything external to the UserSession.
164 9 Objects and Companion Objects
The companion object also has a factory member function create(). This
member function uses the companion object member function next() to increment
the count property before it creates a new UserSession instance.
Also as a reminder note that the next() member function uses the single line
member function declaration format where as the create(), printData() and
toString() are using the long hand member function declaration form.
A final point to note is that the constructor for the UserSession class has been
defined using the long hand form; that is using the constructor keyword. This
is so that we can mark the constructor as being private. This indicates that the
constructor can only be accessed from within the scope of the class UserSession
or its companion object.
It is therefore no longer possible to directly create an instance of the class
UserSession externally to the class. The only way to obtain a new instance of the
class is via the create() companion object factory member function. This means
that full control of how UserSessions are created is given over to the companion
object—this is a common factory pattern (another Gang of Four or GoF pattern) and
provides controlled access to the instance creation process.
From a client of the UserSession concepts’ point of view, they can now
only create a new session using the create() factory member function. This is
illustrated below:
fun main() {
val session1 = UserSession.create()
println(session1)
val session2 = UserSession.create()
println(session2)
session1.printData()
}
The two UserSession instances above are created using the UserSession
objects’ create() member function. Their ids are automatically allocated via this
member function (which also increments the count property). The output from the
program is given below:
UserSession(1)
UserSession(2)
Id is 1 and MAX is 100
Note that the toString() member function is used to print the formatted String
version of the instances and printData() printed the id and the value of the MAX
property.
Companion Objects 165
It may at first seem unclear what should normally go in a member function defined in
the class as opposed to what should go in a member function defined in a companion
object. After all, they are both defined with the class as a whole and relate to the
same overall concept.
However, it is important to remember that one defines the behaviour which will
be part of an instance and the other the behaviour which can be shared across all
instances of the class being implemented.
In order to maintain clarity companion object member functions should only
perform one of the following roles:
• Application Entry Point It is common to see main() function but sometimes it
is useful to define an object with a main() member function. This object acts
as an entry point for your application and the main() member function as the
behaviour to be executed. Although note the member function must be marked
with @JvmStatic for compatibility with the JVM runtime environment. For
example:
object HelloWorldApp {
/**
* Defines entry point for a Kotlin application.
* Need to indicates that the compiler should generate
* byte codes that are compatible with the Java definition
* of a static member function that it expects to find
* to run a program
*/
@JvmStatic
fun main(args: Array<String>) {
println("Hello World!")
}
• Answering enquiries about the class This role can provide generally useful infor-
mation, frequently derived from companion object properties. For example, it
may be possible to obtain data on the number of instances of the class that have
been created.
• Instance management In this role companion object member functions control the
number of instances created. For example, a class may only a certain number of
instances to be created (for example a database connection pool manager object
that may only allow 10 database connections to be created).
166 9 Objects and Companion Objects
An Object or An Instance
In some situations you may only need to create a single instance of a class and
reference it wherever it is required. A continuing debate ponders whether it is worth
creating such an instance or whether it is better to define the required behaviour in
an object. The answer to this is not straight forward as there are several factors that
should be taken into account including:
• The use of an object in Kotlin guarantees you a singleton instance within the
current JVM. This means that it also limits you to a single instance in the current
JVM and over time this may be a problem.
• The creation of an instance has a very low overhead. This is a key feature in Kotlin
and it has received extensive attention.
• You may be tempted to treat the object as a global reference. This suggests that
the implementation has been poorly thought out.
• It is not possible to extend objects using a concept known as inheritance (which
will be discussed later in this book). However, it is possible to extend a class via
inheritance. Since inheritance is one of the ways in which the reuse of data and
behaviour can be achieved, this limits the future development of objects within
your application.
In deciding whether to use a Kotlin object or a Kotlin class to hold data and/or
behaviour you need to consider the context in which it will be used, how you expect
to develop the concept and whether there will ever be a need for more than one
instance of that concept.
Online Resources 167
Online Resources
Book References
Exercises
The aim of this exercise is to add housekeeping style member functions to the
Account class.
You should follow these steps:
1. We want to allow the Account class from the last chapter to keep track of the
number of instances of the class that have been created.
2. Print out a message each time a new instance of the Account class is created
indicating the account holder name and the type of account created.
3. Print out the number of accounts created at the end of the previous test program.
4. An example of the type of output generated might be:
Introduction
Property Declarations
Properties can be defined in several locations within a program, for example they
can be defined within the body of an object, within the constructor of a class or the
body of a class, within a companion object or indeed at the top level of a program.
The section will discuss some of these aspects of properties in more detail.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 169
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_10
170 10 Further Kotlin Properties
val MIN = 1
val MAX = 100
val calculator = Calculator()
var count = 0
These statements declare three top level vals MIN, MAX and calculator. They
also defines a top level var count.
The top level properties are now accessible anywhere within the rest of the file.
For example we could now write the following main() function within the same
file:
Property Declarations 171
fun main() {
println(MIN)
println(MAX)
println(count)
count++
println(count)
println(calculator)
}
Here the properties MIN, MAX, calculator and count can be accessed
directly.
The output from this program is:
1
100
0
1
Calculator@548c4f57
Constructor Parameters
Properties can be defined within the constructor of a class, however they are not the
only element that can be defined within the constructor. It is also possible to have
plain constructor parameters.
Constructor parameters are parameters that must be provided when a class is
instantiated but which are not properties (and thus do not have an associated keyword
val or var). Such constructor parameters are only available within the init{}
block of the class (and when invoking superclass constructors as discussed in a later
chapter on class inheritance).
There are therefore two types of parameter to a constructor:
• Constructor parameters with a very limited scope (i.e. The init{} block).
• Constructor properties which have at least a scope for the whole body of the class
and by default are publicly accessible from outside the class.
The following class illustrates the differences between a constructor parameter
and a constructor property.
172 10 Further Kotlin Properties
fun birthday() {
val oldAge = age
age++
println("$message $fullname, you were $oldAge you are
now $age")
}
}
In this case firstName and surname are constructor parameters, they are
therefore only accessible within the init{} block of the class.
However age, id and message are properties accessible anywhere within the
body of the class, with age and id also being publicly accessible.
In this class there is also a member level property filename which is initialised
within the init{} block.
When an instance of the GamePlayer class is created all five parameters to the
constructor must be provided. However, once the class has been instantiated only the
public properties age, id and filename are externally accessible.
Within the body of the class age, id, filename and message are accessible
as illustrated by the birthday() member function.
The following program illustrates how the GamePlayer class is used:
Property Declarations 173
fun main() {
val player = GamePlayer("John", "Hunt", 36, "123AA")
// age is a read-write property initialised in constructor
println(player.age)
player.age = player.age + 1
println(player.age)
// id read-only property initialised in the constructor
println(player.id)
// fullname is a readonly property
println(player.fullname)
// Can invoke the birthday member function
player.birthday()
}
Of course it is not possible to access firstName and surname as they are only
constructor parameters and thus not accessible outside of the init{} block.
The output from this program is:
36
37
123AA
John Hunt
Happy Birthday John Hunt, you were 37 you are now 38
init {
fullname = "$firstName $surname"
favouriteGame = ""
}
}
Alternatively the member level properties could have been initialised as part of
their declaration:
Note that firstName and surname are also visible when the member level
properties are declared as logically their declaration and initialisation are all part of
the instance initialisation process.
Property Visibility
In this case the properties id and desk are only accessible within the body of
the class Person; they are not visible externally to the class. However as id is a
constructor property a value must be provided for this property when the class is
instantiated.
Note that private properties may be vals or vars (that is they may be read-only or
read–write properties).
There are two member property modifiers const and lateinit as well as the
option to indicate that a property should be initialised lazily which affect the way in
which a property is declared. Each of these will be discussed in this section.
The const property declaration modifier keyword is used to indicate to the compiler
that a val property is a compile time constant. There are two important constraints
associated with a const val, these are:
• It can only be used with val member level properties and
• the values used must be derivable at compile time (that is it must not depend on
any runtime calculations or operations). It is thus limited to use with fundamental
types such as Int, Short, Byte, Long, Float, Double and Boolean plus
Strings.
This is illustrated in the following object where a set of val properties are marked
as const:
object MathUtils {
// Specifying useful compile-time
// constant properties
const val ZERO = 0;
const val MIN: Int = -100
const val MAX = 100
}
The lateinit property modifier can only be applied to var properties. It is used
to indicate that the var property will be initialised at a later date; and not at the
point that it is declared nor within an init{} block. Essentially the developer takes
responsibility for ensuring that the var property will be initialised before it is first
used. If they do not then a runtime error will be generated.
For example, the following code defines an object MyUtils that has a
lateinit var title. It also defines a member function printTitle() that
will access the String held in the title property and print out its length as well as
the String itself.
object MyUtils {
// Lateinit allows value to be provided later on
lateinit var title: String
fun printTitle() {
println(title.length)
println(title)
}
}
In this case the title is a public member var property and thus code external to
the MyUtils object is expected to provide a String to be used.
The following code illustrates this:
fun main() {
MyUtils.title = "KVaders"
MyUtils.printTitle()
}
7
KVaders
However, if the client code of the MyUtils objects does not initialise the title,
for example as shown below:
fun main() {
MyUtils.printTitle()
}
Then the printTitle() member function will try to obtain the length of the
uninitialised title property and a runtime error will be shown to the user:
Property Declaration Modifiers 177
Using lateinit can therefore allow the developer a great deal of flexibility,
however this flexibility must be used with care.
There are three important restrictions on the use of lateinit:
• can only be used with var (read–write properties),
• it is not possible to use lateinit with nullable properties,
• lateinit cannot not be used with basic or fundamental types such as Byte,
Short, Int, Long, Float, Double, Boolean etc.
A third option is to initialise a read-only val property lazily. For example the following
standard definition of a val is initialised immediately the first time that the object
Utils is referenced:
object Utils {
val myString: String = “Hello”
}
However, you can use the by lazy to indicate that the val should only be evaluated
the first time it is accessed:
object Utils {
val myLazyString: String by lazy { "Hello" }
}
This can be used to improve the performance of a system where some prop-
erty is expensive to initialise (either in terms of time and/or resources) but is only
infrequently required.
Once the property has been initialised then the value will be reused for any
subsequent access.
178 10 Further Kotlin Properties
Summary
nthis::title.isInitialized
Let us revisit what a property is; a property is an item of data, held within an instance
of a class or within an object, that can be accessed, possibly externally to that instance
or object, either as a read-only property or as a read–write property.
Behind the scenes to support his idea, Kotlin creates a field to store the value in
and a reader (also known as a getter) member function and a writer (also known as
a setter) member function associated with each property. If the property is marked
as val, then it would only create the reader or getter function. Indeed if you were
to look at a Kotlin class that had one or more properties from the Java would, you
would find that it possessed methods of the form get<Name of Property>()
and set<Name of Property>().
Depending upon the context in which you reference the property, Kotlin knows
whether to invoke the reader or writer. For example if you are attempting to access the
value of the property then it knows to invoke the reader. Where as if you are attempting
to set the value of the property it knows to use the writer member function.
Kotlin also allows a programmer to override the default readers and writers if
required; it is just that the default behaviour provided by Kotlin generally meets the
requirements of most developers.
If you wish to define your own readers and writers then you can.
For the reader element of a property you define a get() function and for the
writer side of the property you define a set() function (hence the terms getters and
setters).
Creating Custom Property Setters and Getters 179
class GameObject {
val y: Int
get() = // calc current y location
val x: Int
get() = // calc current x location
Set(value) = // set the current x location
In the above outline code the val property has a get() function defined for it
and the var name property has both a get() and a set(value) function defined.
Note that the return type of the get() member function is the same as the type
specified for the property.
The type of the parameter passed into set() member function is also the same
as the type of the property.
The following code illustrates the implementation of the x and y getter and setter
functions.
180 10 Further Kotlin Properties
class GameObject {
var x = 0
// implicit backing 'field' generated by compiler
get() = field
set(value) {
if (value >= 0) {
field = value
}
}
var y: Int = 0
get() = field
set(value) {
if (value >= 0) {
field = value
}
}
}
This example uses what is known as an implicit backing field. This is used to
store the value for the property and is represented by the keyword field. The
Kotlin compiler will generate a unnamed internal variable to hold the value that is
hidden from the developer.
In these examples the get() function merely returns the field value and thus
the shorthand member function declaration form is being used. In fact as this is all
the get() function does it could be omitted and the Kotlin compiler will provide
the implementation.
The setters both check that the value supplied is valid. If the value is valid then the
backing field is updated with the new value. Note that the name of the parameter
for the setter function can be whatever you like although it is common practice to
call it value. In this case if the value presented is negative it is ignored; in practice
this should be indicated back to the code calling this possible by raising an exception
- this will be covered in a later chapter.
We can now use the GameObject class and its properties x and y as follows:
fun main() {
val obj = GameObject()
println("initial obj.x: ${obj.x}, obj.y: ${obj.y}")
obj.x = 10
obj.y = 10
println("updated obj.x: ${obj.x}, obj.y: ${obj.y}")
}
As you can see the initial values for the properties x and y were zero. Having set
the properties to 10 then the output is updated.
If you choose you can use your own backing field to hold the value represented by
the property. There are two issues to note about this:
• The first is that the only way to do this is to create a hidden property that is used
by the public properties. This hidden property can be made private as this is one
way to ensure that it is not publicly available. However a private property is still
accessible from anywhere within the body of the class (or object). Developers
must therefore be careful not to use the hidden value rather than the published
property. For this reason the common convention is to prefix the hidden property
name with an underbar to indicate that it should not be used.
• The second is that you must be careful not to name the internal property with the
same name as the property itself, otherwise things get recursive and will generate
a run time error.
As such the following illustrates the previous GameObject now using an explicit
backing field for a new property background:
class GameObject {
// … Code hidden for brevity
The background var allows client code to set the colour of the property. In
this case if no background colour has been set when the getter is called then the
background colour is set to blue. The actual field holding the value is _background
which is of course a hidden (private) property in its own right as that is the only option
in Kotlin.
182 10 Further Kotlin Properties
In some cases it may be useful to make something look like a property but in reality
it is just a hard coded value. For example:
class GameObject {
// .. Code removed for brevity
In this example, to client code it appears that the GameObject has a property
isHome. However, the getter of this property merely returns the Boolean value
false.
The visibility of the getter function for a property is always the same as that of the
property itself. However, this does not have to be true for the setter function (whether
it relies on the default implementation or whether a custom implementation is used).
In either case the setter function can have a different visibility to that of the property.
This is sown below:
class GameObject {
// … Code hidden for brevity
// Appears as a val outside the class
// but internally can be set
var location: Location = Location()
private set
In this case the setter function has private visibility where as the property
(and its getter function) are public. This means that as far as client code of the
GameObject is concerned the property appears to be a read-only val property.
However, internally to the GameObject the property appears to be a read–write
property with both getter and setter functionality.
GameObject with Properties 183
For ease of reference the complete listing for the GameObject class is given below:
class Location
class GameObject {
var x = 0
get() = field // implicit backing field generated
set(value) {
if (value >= 0) {
field = value
}
}
var y: Int = 0
get() = field
set(value) {
if (value >= 0) {
field = value
}
}
}
184 10 Further Kotlin Properties
Online Resources
Exercises
In this exercise you will create a custom definition for the balance property such that
an opening balance is provided when the instance is created which is used to update
a property balance that is read-only externally but read–write internally to the class.
fun main() {
val acc1 = Account("123", "John", 10.05, "current")
val acc2 = Account("345", "Denise", 23.55, "savings")
val acc3 = Account("567", "Phoebe", 12.45, "investment")
println(acc1)
println(acc2)
println(acc3)
acc1.deposit(23.45)
acc1.withdraw(12.33)
println("balance: ${acc1.balance}")
println("balance: ${acc1.balance}")
acc1.withdraw(300.00)
println("balance: ${acc1.balance}")
Account.printInstancesCreated()
}
Introduction
Kotlin provides five functions whose sole purpose is to execute a block of code
within the context of an instance or an object. When you call such a function on
an instance with a lambda expression, it forms a temporary scope (hence the term
scope function). In this scope, you can access the instance directly typically using
the this reference or in two cases using the it implicit parameter.
There are five higher order scope functions:
• apply{}, let{}, run{}, with{}, and also{}
In addition there is the runCartching{} exception handling scope function
which will be discussed in the chapter on Error and Exception Handling.
Each of these scope functions and their typical usages will be discussed in the
remainder of this chapter.
Several of the examples in the reminder of this chapter make use of the following
class:
The apply scope function binds the receiving instance/object to the this variable
within the lambda function provided. This allows the body of the lambda to reference
the public interface of the instance/object directly without needing to reference it via
a named parameter (or indeed explicitly using the this reference).
It is typically used to allow some code to run that will be used to initialise data or
execute behaviour on a newly created instance before any other code has access to
the instance.
The following illustrates a typical usage of the apply scope function:
Also note that the result returned by the apply{} scope function is the instance
of the class User. That is apply{} returns the object it is applied/bound to.
The also scope function is an alternative to the apply scope function. It also
returns the object it is applied to, it can also be used to initialise an instance prior to
the instance being accessed by other code and like apply it returns the receiving
object/instance.
However, unlike apply the receiver is bound to the implicit parameter it. Thus
it is not possible to reference the public interface of the instance or object directly,
all references must be made via the it parameter.
The Also Scope Function 187
For example:
If you prefer not to use the implicit it parameter you can define your own
parameter, for example:
user.run{
print("Please input the users favourite game: ")
val favouriteGame: String ? = readLine()
game = favouriteGame ?: ""
}
println("user: $user")
188 11 Scope and Conditional Functions
In this example the run scope function has bound the this reference to the
instance held in the val user. In this case we have mimicked some operation that
has generated a new favourite game for the user.
Note that because the readLine() function returns a nullable String it is neces-
sary to use a nullable operator to extract the value or use a default. In this case we
have used the elvis operator to either take the value held in favouriteGame or, if
the value is null, to use an empty String.
Note that the result of executing the run scope function is the result returned by
the lambda function. In the above example the result returned will therefore be Unit
as the last expression is an assignment and assignments always return Unit.
The let scope function has the same purpose as the run function but slightly
different semantics. The use of let indicates that we wish to execute some behaviour
within the context of the receiving instance/object. As such the receiver is bound to
the implicit parameter it by default (although this can be overridden).
Within the lambda applied to the let scope function it is necessary to access the
bound object/instance via the it reference (there is no implicit this).
For example:
s2 JOHN
User(name='John', id='123', game='Nightfall')
John
s3 JOHN
In both these examples of the let scope function, the instance held in user is
bound to the it parameter. The name property is then accessed. In both cases the
last thing that the lambda function does is to convert the name into upper case. As
this is the last expression in the lambda it is returned as the result of the lambda and
thus this is also the result of the let scope function.
If you do not want to use the it parameter you can override this to give a more
semantically meaningful name to the reference inside the lambda function:
val r1 = user.name.let{
name -> name.toUpperCase()
}
println("r1: $r1")
This example binds the String held in the user.name property to the param-
eter name within the lambda function applied to the let scope function. Within
the body of the lambda we can then reference the name directly and invoke the
toUpperCase() member function on it.
The with scope function takes a first parameter which is the object/instance to be
bound and a second parameter which is the function to apply (although typically the
trailing lambda function syntax is usually adopted).
The with scope function allows a sequence of operations to be written all relating
to the object passed to the associated lambda function. Within the lambda function
the bound instance/object is accessible via the this reference. Which means that the
public interface of the receiver is directly accessible to the code within the lambda.
This helps to avoid repeatedly referencing the instance/object.
An example of using the with scope function is given below.
190 11 Scope and Conditional Functions
Scope functions are often used in combination with nullable types. This is because it
is possible to combine the scope function with the use of the safe dot operator (?.)
so that the scope function is only invoked if a value is not null.
For example:
println("result: $result")
In this case the run scope function lambda is only executed if the value held in
service is not null.
If service is null then the run scope function is not run and the result val is
set to null.
If service is not null then the run scope function is executed and the services’ port
is set to 8080 and the member function query is called on the service. The result of
the run scope function is then the data returned from query("get") call.
Scope Function Categories 191
Scope functions can be categorised by the way they bind the object/instance they are
applied to. Specifically:
• run, with, and apply refer to the context object/instance as this,
• let and also refer to the context object as it.
Another way to categorise the scope functions is based on what is the result
returned from the scope function:
• apply and also return the bound instance/ object,
• let, run and with return the lambda function result.
This is illustrated in the following table:
Conditional Functions
There are two higher order conditional functions provided in Kotlin. These are
functions which will either return null or the receiver object depending upon some
conditional element. The conditional element is a lambda function that must return
a Boolean.
The first conditional function is the takeIf function. This takes a lambda func-
tion as an argument If the lambda function returns true then the receiving instance
or object is returned, if the lambda returns false then null is returned.
The conditional functions are often combined with the scope functions to provide a
functional style of programming that captures the if then style of imperative programs.
For example:
Take the value in service if service.port is greater than 1024 otherwise take the value null.
The safe dot operator will then only execute the run lambda if the result returned from takeIf
is not null.
In this example takeUnless will take the value held in service unless the port
is greater than 1024. In which case the run block will only execute if the service point
is less than or equal to 1024.
Exercises
In this exercise you will use both the run{} and the apply{} scope functions.
Use the run{} scope function to determine what message to present to a user.
The message will differ depending on whether they are in credit or not. For example,
if they are in credit then print a message “You are a good customer; you are in credit”.
However, if they are not in credit then they should have a message “You need an
overdraft”.
The second part of the exercise is to create a new instance of a current Account for
Jasmine. However, Jasmine has deposited 10.55 when she opened the account. Ensure
that the deposit is registered against the account (which has an opening balance of
0.0) before any client code can access the Account.
Chapter 12
Class Inheritance
Introduction
What is Inheritance?
Inheritance allows features defined in one class to be inherited and reused in the
definition of another class. For example, a Person class might have the properties
name and age. It might also have behaviour associated with a Person such as
birthday().
We might then decide that we want to have another class Employee and that
employees also have a name and an age and will have birthdays. However, in addi-
tion an Employee may have an employee id property and a calculatePay()
behaviour.
At this point we could duplicate the definition of the name and age properties
and the birthday() behaviour in the class Employee (for example by cutting
and pasting the code between the two classes).
However, this is not only inefficient; it may also cause problems in the future.
For example we may realise that there is a problem or bug in the implementation of
birthday() and may correct it in the class Person; however, we may forget to
apply the same fix to the class Employee.
In general, in software design and development it is considered best practice to
define something once and to reuse that something when required.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 193
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_12
194 12 Class Inheritance
In an object oriented system we can achieve the reuse of data and/or behaviour via
inheritance. That is one class (in this case the Employee class) can inherit features
from another class (in this case Person). This is shown pictorially below:
In this diagram the Employee class is shown as inheriting from the Person
class. This means that the Employee class obtains all the data and behaviour
of the Person class. It is therefore as though the Employee class has defined
three properties name, age and id and two member functions birthday() and
calculatePay().
Note that a set of classes, involved in an inheritance hierarchy, such as those shown
above is often named after the class at the root (top) of the hierarchy; in this case it
would make these classes part of the Person class hierarchy.
Types of Hierarchy
In most object oriented systems there are two types of hierarchy; one refers to inher-
itance (whether single or multiple) and the other refers to instantiation. The inheri-
tance hierarchy has already been described. It is the way in which one class inherits
features from a superclass.
The instantiation hierarchy relates to instances or objects rather than classes and
is important during the execution of the program.
There are two types of instance relationships: one indicates a part-of relationship,
while the other relates to a type of relationship (it is referred to as an is-a relationship).
This is illustrated below:
The confusion is due to the fact that in modern English we tend to overuse the
term is-a. For example, in English we can say that an Employee is a type of Person
or that Andrew is a Person; both are semantically correct. However, in Kotlin classes
such as Employee and Person and an instance such as Andrew are different things.
We can distinguish between the different types of relationship by being more precise
about our definitions in terms of a programming language such as Kotlin.
Purpose of Subclasses
Subclasses are used to refine the behaviour and data structures of a superclass.
A parent class may define some generic/shared properties and member functions;
these can then be inherited and reused by several other (sub) classes which add
subclass specific properties and behaviour.
In fact, there are only a small number of things that a subclass should do relative
to its parent or super class. If a proposed subclass does not do any of these then your
selected parent class is not the most appropriate super class to use.
A subclass should modify the behaviour of its parent class or extend the data held
by its parent class. This modification should refine the class in one or more of these
ways:
• Changes to the external protocol or interface of the class, that is it should extend
the set of member functions or properties provided by the class.
Purpose of Subclasses 197
As an example, consider the class hierarchy illustrated above. A generic root class
has been defined. This class defines a Conveyance which has doors, fuel (both
with default values) and a member function, startUp() that starts the engine of
the conveyance.
Three subclasses of Conveyance have also been defined: Dinghy, Car and
Tank. Two of these subclasses are appropriate, but one should probably not inherit
from Conveyance. We shall consider each in turn to determine their suitability.
• The class Tank overrides the number of doors inherited, uses the startUp()
member function within the member function move(), and provides a new
property. It therefore matches our criteria.
• Similarly, the class Car overrides the number of doors and uses the member
function startUp(). It also uses the instance variable fuel within a new
member function accelerate(). It therefore also matches our criteria.
• The class Dinghy defines a new property sails and a new member func-
tion setSail(). As such, it does not use any of the features inherited from
Conveyance. However, we might say that it has extended Conveyance by
providing this property and member function. We must then consider the features
provided by Conveyance. We can ask ourselves whether they make sense within
the context of Dinghy. If we assume that a dinghy is a small sail-powered boat
with no cabin and no engine, then nothing inherited from Conveyance is useful.
In this case, it is likely that Conveyance is misnamed, as it defines some sort
of a motorised conveyance, and the Dinghy class should not have extended it.
198 12 Class Inheritance
Kotlin supports single inheritance between classes. This means that a class can only
inherit from one single class. The root of the class hierarchy in Kotlin is the class
Any. All classes in Kotlin eventually inherit from this class either directly or through
intermediate classes.
The above diagram illustrates some of the classes that inherit from (or extend) the
class Any in Kotlin. As you can see there are fundamental class such as Int as well
as built-in classes such as String. However it is also true of user defined classes
such as Person and for objects such as GameObject.
Any behaviour defined in the class Any is thus inherited by all classes and objects
in Kotlin. Any defines the following:
• A zero parameter constructor.
It also defines the following member functions:
equals(other: Any?): Boolean This member function is used to indi-
cate whether some other object is “equal to” this one. Implementations must fulfil
the following requirements:
• Reflexive: for any non-null value x, x.equals(x) should return true.
• Symmetric: for any non-null values x and y, x.equals(y) should return true if and
only if y.equals(x) returns true.
• Transitive: for any non-null values x, y, and z, if x.equals(y) returns true
and y.equals(z) returns true, then x.equals(z) should return true.
• Consistent: for any non-null values x and y, multiple invocations
of x.equals(y) consistently return true or consistently return false, provided
no information used in equals comparisons on the objects is modified.
hashCode(): Int Returns a hash code value for the object. The general
contract of hashCode is:
• Whenever it is invoked on the same object more than once, the hashCode member
function must consistently return the same integer, provided no information used
in equals comparisons on the object is modified.
• If two objects are equal according to the equals() member function, then calling
the hashCode member function on each of the two objects must produce the same
integer result.
Kotlin Class Hierarchy 199
A class that is defined as extending a parent class has the following syntax:
Note that the parent class is specified by providing the name of the that class
following a colon (:) after the name of the new (child) class.
We can make the class Person extendable with one modification; we must mark
it as open:
fun birthday() {
println("Happy birthday you were $age")
age++
println("You are now $age")
}
}
200 12 Class Inheritance
Now we can say that the class SalesPerson has a name, an age and an
id as well as a region and a sales total. It also has the member functions
birthday(), calculatePay(hourseWorked) and bonus().
In this case the SalesPerson declaration indicates that Employee class
constructor, that takes three parameters for the name, age and id, is invoked. This is
because it is the constructor in the next class up in the hierarchy that we need to run
when a class is initialisation.
We can now write code such as:
Kotlin Classes and Inheritance 201
fun main() {
val p = Person("Jasmine", 21)
println("Person - ${p.name} is ${p.age}")
println("----------------")
val e = Employee("Adam", 23, "ABC123")
println("Employee(${e.id}) - ${e.name} is ${e.age}")
println("----------------")
val s = SalesPerson("Phoebe", 32, "XYZ987", "South West",
905.55)
println("SalesPerson(${s.id}) - ${s.name} is ${s.age} for
region ${s.region} with ${s.sales} sales")
}
Person - Jasmine is 21
----------------
Employee(ABC123) - Adam is 23
----------------
SalesPerson(XYZ987) - Phoebe is 32 for region South West with
905.55 sales
It is important to note that we have not done anything to the class Person by
defining Employee and SalesPerson; that is it is not affected by those class
definitions. Thus, a Person does not have an employee id. Similarly, neither an
Employee nor a Person have a region or a sales total.
In terms of behaviour, instances of all three classes can run the member function
birthday(), but .
• only Employee and SalesPeron instances can run the member function
calculcatePay() and
• only SalesPerson objects can run the member function bonus().
In Kotlin it is not only classes that can be involved in inheritance; objects can also
extend an open class.
If an object extends a open class then the object is also an instance of the super-
class. It will inherit all the data and behaviour from the superclass. The syntax for
this is:
The colon after the object’s name is followed by the superclass that the object
inherits from.
However, you should note that it is not possible to extend an object.
202 12 Class Inheritance
fun main(){
HelloWorldLogger.doSomething()
}
log
Anonymous objects can also extend open classes. In this case the: < superclass>
declaration comes straight after the keyword object.
As with named objects it is also necessary to indicate which constructor will be
invoked in the parent class.
An anonymous object inheriting from a class is illustrated below:
Anonymous Objects and Inheritance 203
fun main() {
val obj = object : LogIt() {
var x: Int = 0
val y = 42
}
println("obj.x = ${obj.x}, obj.y = ${obj.y}")
obj.log()
}
In this case the anonymous object extends the LogIt class and thus inherits the
log() member function. The anonymous object can also define its own member
functions and member properties as required. In this case it defines the properties x
and y which are both of type Int.
The output from this program is presented below:
obj.x = 0, obj.y = 42
log
Every class in Kotlin extends one or more superclasses. This is true even of the class
User shown below:
This is because if you do not specify a superclass explicitly then the Kotlin
compiler automatically adds in the class Any as a parent class. Thus the above
is exactly the same as the following declaration which explicitly specifies the class
Any as the superclass of Person:
Both listings above define a class called User that extends the class Any.
The fact that all class eventually inherit from the class Any means that behaviour
defined in Any is available for all classes everywhere in Kotlin.
Also note that if you do not specify a superclass in Kotlin, then the Kotlin compiler
not only selects Any as the superclass it also calls the zero parameter constructor for
you on the class Any.
204 12 Class Inheritance
Overriding occurs when a member function is defined in a class (for example the
class Person) and also in one of its subclasses (for example Employee). It means
that instances of Person and Employee both respond to requests for this member
function to be run, but each has their own implementation of the member function.
For example, let us assume that we define the member function toString()
in these classes (so that we have a string representation of their instances to use with
the println() function). The pseudo code definition of this in Person might be:
function toString() {
return result "Person(${name} is $age)")
}
function toString() {
return result "Employee($id)"
}
The member function in Employee replaces the version in Person for all
instances of Employee. If we ask an instance of Employee for the result of
toString(), we get the string “Employee(<some_id>)”. If you are confused,
think of it this way:
If you ask an object to perform some operation, then to determine which version of the
member function is run, look in the class used to create the instance. If the member function
is not defined there, look in the class’s parent. Keep doing this until you find a member
function which implements the operation requested. This is the version which is used.
fun birthday() {
println("Happy birthday you were $age")
age++
println("You are now $age")
}
}
open class Employee(name: String,
age: Int,
val id: String) : Person(name, age) {
fun main() {
val p = Person("Jasmine", 21)
val e = Employee("Adam", 23, "ABC123")
val s = SalesPerson("Phoebe", 32, "XYZ987", "South West",
905.55)
println(p)
println(e)
println(s)
}
206 12 Class Inheritance
Person(Jasmine, 21)
Employee(Adam, 23, ABC123)
SalesPerson(Phoebe, 32, XYZ987, South West, 905.55)
As can be seen from this the Employee class prints the name, age and id of
the employee while the Person class only prints the name and age properties. In
turn the SalesPerson prints the name, age, id, region and sales total.
Also note from this example, an open class can be extended by an open class or
a closed class. If the subclass is not marked with the keyword open then it defaults
to closed and cannot be extended.
In the above classes, the class Base defines a single member function print():
Unit that is marked as open and thus can be overriden. In the subclass Derived
we can therefore redefine the member function print(): Unit but we must use
the keyword override to allow us to do so.
We can now use the Base and Derived classes as shown below:
fun main() {
val base = Base()
base.print()
println("----------")
Base print
----------
Derived print
It is not only member functions that can be overridden; it is also possible to override
properties. Both constructor properties and member properties can be overriden in a
subclass.
As with member functions it is necessary to mark a property as open to allow it
to be overriden; if you do not do this then the property will be closed and cannot be
overriden in a subclass.
To mark a property as open the declaration of the property must be preceded by
the keyword open. The overriding property (in the subclass) must be marked with
the keyword override.
A simple example of doing this is shown below:
In the above classes the Base class declares a property and a member function
as being open and the whole class is marked as open. In the subclass Derived,
both the label property and the print() member function are overriden.
Notice that it is still necessary to use the override keyword when overriding a
property declaration.
A simple program using these two classes is given below:
208 12 Class Inheritance
fun main() {
val base = Base()
println(base.label)
base.print()
println("----------")
Base
Base print
----------
Derived
Derived print
A further example illustrates some of the options available when defining and
overriding properties:
In this example, a member level property name in the class Cat is overridden
by a constructor property in the class Tiger. In turn the member level property
dangerous is overridden by a member level property in the class Tiger.
As a final example, the following code illustrates both that a constructor property
can be overriden and that once the property is marked as open in the root class, it is
open for all subclasses:
Inheritance and Properties 209
In this case the Base class defines an open member function print(). In
the Derived class the member function print() is overridden. However, the
Derived class’s version of print() contains the statement super.print()
in it. This means that when you call print() on the Derived class it will print
out the contents of the version in Derived but in the middle it will call the version
defined in Base.
This is illustrated by the following program:
210 12 Class Inheritance
fun main() {
val derived = Derived()
derived.print()
}
Derived before
Base
Derived After
You can clearly see the printouts from both the Derived.print() member
function and the Base.print() member function displayed.
The effect of the overridden print() member function in Derived is that it
calls the parent class’s version of print(). This means that in effect it extends,
rather than replaces, the behaviour of the original version of print().
Note that super tells Kotlin to start searching up the class hierarchy for a version
of print() defined above the current class in the hierarchy. In this case it is defined
in the parent class, but it could have been defined in a parent of Base – that is it
starts searching in Base and will continue searching up the class hierarchy until it
finds another definition of print() to execute.
We might refer to this class hierarchy as the Person hierarchy as the Person
class is the root of all the classes. Within this hierarchy instances of all the classes
can also be considered to be of type Person. In fact on this diagram, the only direct
Casting and Inheritance 211
subclass of Person is Employee. As such all the subclass of Employee are both
examples of a Person and an Employee.
This means that it is possible to treat an instances of the Sales or Manager
class as if they were instances of the class Employee or indeed of class Person.
This can be done by assigning an instance of the Sales or Manager class to
a variable of type Employee. This is referred to as casting and casting back up
a class hierarchy is done automatically by the Kotlin compiler as it will always be
a safe cast. This is because all the features of an Employee are always available
for all subclass of Employee (even if the internal implementation is different). In
Kotlin this means that whatever properties and member functions have been defined
in Employee will be available in all subclasses either because they were inherited
or because they were overridden.
Thus it is possible to write the following:
fun main() {
var e: Employee = Employee("John", 55, 1234)
val s = Sales("Adam", 20, 3456, ‘‘South West’’, 500.0)
e = s
println(e)
}
From the point of view of the variable e what we can say is that it will hold an
instance of a class that is at last an Employee but may be an instance of a subclass
of Employee. However, the only behaviour that will be able from an API point of
view, via the variable e, will be the behaviour (including properties) defined at the
Employee level (or indeed inherited by Employee from Person etc.).
When we run this code however, it is the type of thing that is referenced by the
variable e that will determine which version of the toString() member function
will be executed at runtime. This is referred to as runtime binding. Thus the output
from this program is:
Smart Casting
Casts down the class hierarchy in Kotlin are more complicated. In some cases the
Kotlin compiler can determine whether the cast will work. This is referred to as
the Kotlin compiler inferring a smart cast. In such situations the Kotlin compiler
analyses the code and calculates what type might actually be held in a variable and
thus whether it is possible to perform an assignment.
For example:
212 12 Class Inheritance
fun main() {
var e: Employee = Employee("John", 55, 1234)
val s = Sales("Adam", 20, 3456)
e = s
val salesPerson: Sales = e
}
In the above example, the val salesPerson is of type Sales. It can therefore
only held references to an instance of the Sales class.
However, it is legal to for the var e to hold a reference to an instance of any
of the subclasses of the class Employee. This means that the var e could hold a
reference to an instance of the Sales class, but equally it could hold a reference to
a Manager etc.
Therefore without analysing the code that precedes the assignment of e to
salesPerson it is impossible to say whether this will cause a problem or not!
Indeed in some other languages this would be illegal and cause a compile time error
to be generated.
The Kotlin compiler however can infer smart casts by analysing the preceding
code. In this case it will find that e holds a reference to an instances of the class
Sales as it is assigned the value held in s in the preceding line. Thus the assignment
will be safe. The compiler will therefore allow the assignment to the salesperson
variable.
In other situations it either may not be able to determine the value held in a variable
or it may determine that the variable does not hold a references to an appropriate
instance of a given class; in which case a compiler error will be generated.
It is possible to check the actual type being referenced by a variable using the is
and the !is (is not) operators. These can be used with the name of a type to verify if
a variable contains an instance of that type. This is illustrated below using the when
expression:
fun main() {
val any: Any = "Hello"
when (any) {
is String -> {
println("any contains a String")
println(any.length)
}
!is String -> {
println("any does not contain a string")
println(any::class.simpleName)
}
}
}
Casting and Inheritance 213
In this short program we create a variable any of type Any. This means that any
can contain a reference to an instances of any class in Kotlin (or indeed any object).
The when expression first checks to see if any contains a String using the is
operator. If any does hold a reference to a String then the code prints out a message
and the length of the string.
The when expression also contains a second test that checks to see if the contents
of any is not a String using the !is operator. In this case a message is printed out to
indicate that it is not a string and the actual type is obtained using a member function
reference::class.simpleName.
The output from this program is:
Explicit Casts
In the previous section the Kotlin compiler could determine that the contents of
any was actually a String and thus the statement any.length did not cause a
compilation issue.
However, the compiler does not always succeed in doing this either because a
value may be generated outside the current code analysis scope (for example in a
function invoked from a third party library).
As an example, consider the function getData(): Any. It could return any
type of thing in the Kotlin type system. This means that it can be very hard for the
compiler to determine what the actual type returned by getData() actually is.
We can illustrate that using the following program:
fun main() {
val any: Any = getData()
println(any.length)
}
214 12 Class Inheritance
This program may not compile if the smart cast cannot be determined for the val
any. Thus the statement println(any.length) may generate a compile time
error specifying that the property length is unresolved for the type Any.
It is therefore necessary for the programmer to explicitly define the cast to the
type String so that we can access the length property.
The casting operator in Kotlin is the as operator. It is used to cast the value in a
val or var to another type, for example:
The above would now treat the value in any as a String and thus the type of the
variable message can be String. We can now access the length property via
message. The modified program using the as operation is now:
fun main() {
val any: Any = getData()
val message: String = any as String
println(message.length)
}
Safe Casts
One issue with the as operator and the code illustrated in the Explicit Cast section is
that from the code in the main() function we cannot actually say that any holds a
String; we have merely assumed that. Without looking at the function getData()
we can’t be sure that a String is actually returned. If in fact getData() returns the
value 42.5 then there will be a runtime error such as:
We could check for this of course using the is operator, for example:
if (any is String) {
val message: String = any as String
println(message.length)
}
Casting and Inheritance 215
However, this is a common enough pattern that the Kotlin language provides what
is known as the safe cast operator as?. This operator either performs the cast if it is
valid or returns null if it is not. For example:
fun main() {
val any: Any = getSomeData()
val message: String? = any as? String
println(message?.length)
}
In this program we are now using the safe cast operation as? which requires the
type of message to be nullable. In turn this means that we now use the safe dot
operator to invoke the length of the message.
When we run this program if getSomeData() returns a String then the output
generated is:
However, if the getSomeData() function returns the Double 42.5 then the
value null will be output and no exception will be raised.
Closing a Member
One issue with the ability to mark a member function as open in a root class is that
it is then open for all subclasses of the root class. This may not be what is required;
if you wish to close a member function in a subclass then it is necessary to mark that
member function as final in the appropriate subclass.
In the above example the class Food defines an open member function eatMe().
This is overridden in both the class Cake and the class Biscuit.
216 12 Class Inheritance
In the Biscuit class this member function is still open and can be overridden
in subclasses of Biscuit.
However, in Cake class the member function is marked as both overridden and
final. This means that subclass of Cake cannot override eatMe().
Closing Properties
fun main() {
val c = Cake()
println(c.type)
}
In this case the property type in the base class Food is marked as open and
thus can be overridden in subclasses. However the override defined in the Cake
class is marked as final and thus subclasses of the Cake class cannot override the
property type. In contrast in the class Biscuit subclasses can still override the
type property.
Inheritance Summary
Online Resources
There are many resources available online relating to class inheritance including:
• https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/ Documentation for the
Kotlin root class Any.
• https://kotlinlang.org/spec/inheritance.html Kotlin language specification on
inheritance.
• https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)
Wikipedia page on class inheritance.
Exercises
The aim of these exercises is to extend the Account class you have been developing
from the last two chapters by providing DepositAccount, CurrentAccount
and InvestmentAccount subclasses.
Each of the classes should extend the Account class by:
1. CurrentAccount adding an overdraft limit as well as redefining the
withdraw member function.
2. DepositAccount by adding an interest rate.
3. InvestmentAccount by adding an investment type property.
These features are discussed below:
The CurrentAccout class can have an overdraftLimit property. This
can be set when an instance of a class is created and altered during the lifetime of
the object. The overdraft limit should be included in the toString() member
function used to convert the account into a String.
The CurrentAccount withdraw() member function should verify that the
balance never goes below the overdraft limit. If it does then the withdraw()
member function should not reduce the balance instead it should print out a warning
message.
The DepositAccount should have an interest rate associated with it which is
included when the account is converted to a string.
The InvestmentAccount will have a investmentType property which
can hold a string such as ‘safe’ or ‘high risk’.
This also means that it is no longer necessary to pass the type of account as a
parameter - it is implicit in the type of class being instantiated.
For example, given this code snippet:
218 12 Class Inheritance
fun main() {
val acc1 = CurrentAccount("123", "John", 10.05, -100.00)
val acc2 = DepositAccount("345", "Denise", 23.55, 0.5)
val acc3 = InvestmentAccount("567", "Phoebe", 12.45,
"high risk")
println(acc1)
println(acc2)
println(acc3)
acc1.deposit(23.45)
acc1.withdraw(12.33)
println("balance: ${acc1.balance}")
acc1.withdraw(300.00)
println("balance: ${acc1.balance}")
Account.printInstancesCreated()
Introduction
This chapter presents abstract classes. An abstract class is a class that you cannot
instantiate and that is expected to be extended by one or more subclasses. Typically
some aspect of the abstract class needs to be implemented by its subclasses.
An abstract class is a class from which you cannot create an instance. It is typically
missing one or more elements required to create a fully functioning instance.
In contrast a non-abstract (or concrete) class leaves nothing undefined and can be
used to create a working instance or object.
You may therefore wonder what use an abstract class is?
The answer is that you can group together elements which are to be shared
amongst a number of subclasses, without providing a complete implementation.
In addition, you can force subclasses to provide specific member functions and/or
properties ensuring that implementers of a subclass at least supply appropriate data
and behaviour. You should therefore use abstract classes when:
• you wish to specify data or behaviour common to a set of subclasses, but
insufficient for a single instance or object,
• you wish to force subclasses or objects to provide specific behaviour or data.
In many cases, the two situations go together. Typically, the aspects of the class
to be defined as abstract are specific to each class, while what has been implemented
is common to all classes.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 219
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_13
220 13 Abstract Classes
fun startup() {
running = true
consumeFuel()
while (fuel > 0) {
consumeFuel()
}
running = false
}
Given an abstract class Conveyance we can define a concrete class Car that
extends it and implements the abstract functions.
To create a subclass of an abstract class we use the: symbol to represent inheri-
tance and must indicate which constructor will be invoked for the parent class. We
will then need to define the concrete version of any abstract member functions in the
parent class. Such member functions must include the keyword override before
the keyword fun in their definition. For example:
222 13 Abstract Classes
The above definition of the class Car defines the consumeFuel() and
drive(Int): Boolean member functions. Both functions must have exactly
the same signature as the versions defined within the abstract class (that is the capital-
isation of names must be the same, the return types must match as must the parameter
types). Both member functions must also be preceded by the keyword override.
We can now create instances of the class Car and invoke the consumeFuel()
and drive() member functions.
The following program illustrates the use of the Car class:
fun main() {
val c: Conveyance = Car()
c.startup()
val result = c.drive(10)
println(result)
}
In this program the type of the val c is Conveyance however this is merely to
show that instances of Car can be treated as a Car or a Conveyance (or indeed
as something of type Any).
The output from this program is given below:
consuming,
consuming,
consuming,
consuming,
consuming,
Driven 10
true
Abstract Properties
In Kotlin, member functions are not the only things that can be declared as abstract,
it is also possible to declare properties as abstract. An abstract class can have zero
or more abstract properties, these abstract properties:
Abstract Properties 223
// Abstract property
abstract val owner: String
fun main() {
val l = Lorry("Smith and Co.")
l.load("Hay")
}
loading Hay
224 13 Abstract Classes
Abstract Subclasses
The subclass of an abstract class may itself be abstract in which case it must be
marked as abstract.
An abstract subclass may be abstract either because it defines one or more abstract
members (that is a property or a member function that is abstract) or because it does
not implement one or more of the abstract members inherited from the parent class.
For example, the class Van is abstract as it does not implement the owner
property nor the load() member function:
However, in this example the class has not been marked as abstract, this will
result in a compile time error being generated:
Class 'Van' is not abstract and does not implement abstract base
class member public abstract fun load(contents: String): Unit
defined in CommercialVehicle
To solve this problem we must add the keyword abstract before the keyword
class in the definition of the class Van. For example:
Online Resources
Exercises
Introduction
The Kotlin type system is comprised of more than just classes and objects; it includes
interfaces and enumerated types. In this chapter we will explore the concept of an
interface and what components can be defined within an interface. We will also
consider a Kotlin language facility that allows an interface to be implemented via
delegation.
The chapter concludes by looking at enumerated types. An enumerated type is
a type consisting of a set of named values also called elements or members of the
enumeration. These values have a specific name and an ordering.
Kotlin Interfaces
The interface construct is another element in the Kotlin type system. An interface is a
specification of a set of properties or member functions that a class should implement.
An interface can contain
• abstract properties,
• abstract member functions,
• concrete member functions,
• a companion object.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 227
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_14
228 14 Interfaces, Delegation and Enumerated Types
Defining an Interface
interface Organizer {
val owner: String
fun add(appointment: String, date: String)
fun get(date: String): String?
fun remove(date: String): Boolean
}
This represents the most basic definition of an interface and only contains abstract
specifications. In this case it contains the specification for an abstract val prop-
erty owner of type String. It also contains the signatures of three abstract member
functions add(), get() and remove().
Kotlin Interfaces 229
This means that any class or object implementing the Organizer interface
must provide an implementation of the owner property and the add(), get() and
remove() member functions. Any class that does provide all these implementations
must be marked as abstract.
In addition the interface also specifies the signature of the member functions, that
is that the get() member function takes a String parameter and should return a
nullable String and that the remove() member function takes a String parameter
and should return a Boolean etc.
It is not necessary to define the property or the member functions as being abstract
because they are abstract by default.
It may appear at this point that an interface is the same as an abstract class, however
they differ in a number of ways:
• Any class can implement one (or more) interfaces. A class can inherit from only
one parent class.
• Interfaces can inherit from zero or more interfaces.
• A class cannot extend an interface (it can only implement it or “fill it out”).
• Interfaces are a compile time feature; they influence the static analysis of the
program being compiled. Abstract classes involve run time issues associated with
member function selection, execution, etc.
• Interfaces can be implemented by any class from anywhere in the class hierarchy
whether that class explicitly specifies a super class or not. Thus, you can define
an interface which is implemented by classes in completely different hierarchies.
Implementing an Interface
Classes and objects can implement one or more interfaces. This is done by listing
the interface (or interfaces) to be implemented following the colon (:) after the class
name and any constructor present. Thus the syntax is
Or for an object
This class meets the contract defined in the Organise interface as it provides
an implementation of all the element in the Organiser. That is it has a concrete
val property owner that is defined as part of the constructor. It also has concrete
implementations of the three member functions.
Note however that is has been necessary to use the override keyword with
the property owner and the three member functions. This is required by Kotlin and
indicates, in this case, that it is implementing a member defined in the interface. If
you omit the override keyword then a compiler error will be generated.
If a class does not implement all the abstract elements of the interface (such as an
abstract member function or an abstract property) then the class must be marked as
being abstract, otherwise you will generate a compile time error. In this way you are
guaranteed that if an instance is created form a class that implements an interface,
then that instance will have an implementation for all the abstract elements in the
interface.
An object can also implement an interface, however in this case it must always
implement all the abstract members of the interface as it is not possible to mark an
object as abstract (as it is not possible to extend an object). An example of an object
Kotlin Interfaces 231
As with the class, the object implements the three member functions and defines
a member level property val owner.
1. Interfaces and Types.
2. In addition to acting as a contract with a class or an object that specifies what that
class (or object) must provide, an interface can also be used as a type specifier.
This means that you can define an interface and then use it to specify the type
a var or val can hold.
In Kotlin, the Organiser is a type as are its sub types Calendar and Diary.
This means that an instance of the Calendar class is:
• of type Calendar,
• of type Any (via inheritance) and
• of type Organiser by virtue of the class implementing the interface.
fun main() {
val cal = Calendar("John")
cal.add("Dentist", "Monday")
val org: Organizer = cal
org.add("Garage", "Tuesday")
Diary.add("Opticians", "Wednesday")
val diary: Organizer = Diary
diary.add("Doctors", "Thursday")
}
Dentist - Monday
Garage - Tuesday
add Opticians - Wednesday
add Doctors - Thursday
5. The Organiser type can also be used to specify the type of a parameter to a
function or a member function:
class Home {
// ...
fun add (Organizer temp) {
// ...
}
}
This means that the member function add(Organiser) can take an instance
of any class or object that implements the Organizer interface.
6. Implementing Multiple Interfaces
A class or object can implement zero or more interfaces. This means that when a
class or object is defined there can be a comma separated list of interfaces following
the colon (:). For example:
Kotlin Interfaces 233
interface Organizer {
val owner: String
fun add(appointment: String, date: String)
fun get(date: String): String?
fun remove(date: String): Boolean
}
interface Printer {
fun prettyPrint()
}
interface Speaker {
fun saySomething()
}
A class or object can extend another class and also implement one or more interfaces.
This allows a class or object to inherit behaviour from a named class as well as
implement the abstract members in multiple interfaces. The parent or super class
is specified after the colon (:) along with the interfaces. In Kotlin the order that
this is done is not significant, although it is common to place the class first in the
comma separated list. This should not be confusing as a class always indicates which
constructor should be run where as interfaces do not have a constructor.
An example of defining a class that extends an open class is given below. The
class TechnicalAuthor extends the Author class and implements the interfaces
Writer and Speaker:
interface Writer {
fun writeSomething()
}
interface Speaker {
fun saySomething()
}
Or indeed even:
Inheritance by Interfaces
Interfaces can also inherit from zero or more interfaces. Thus, for example we can
define a hierarchy of interfaces where a single interface can extend zero or more
other interfaces.
This is illustrated below:
236 14 Interfaces, Delegation and Enumerated Types
interface Workers {
fun doWork()
}
interface Employers: Clonable {
fun printSelf()
}
interface Records : Workers, Employers {
fun doSomething()
}
In this case the interface Employers extends the interface Cloneable. In turn
the interface Records extends the interfaces Workers and Employers (and by
inheritance Clonable).
When an interface extends more than one interface then the result is the union
of all declarations in the inherited interfaces. Thus any class or object implementing
Records must implement the member functions:
• doSomething() from Records,
• printSelf() from Employers and
• doWork() from Workers
• plus anything defined in Cloneable.
As mentioned at the start of this chapter, interface definitions can have any number or
combination of concrete member functions in addition to the abstract specification
of properties and member functions. This is illustrated in the following interface:
interface Speaker {
fun saySomething()
fun sayHello() {
println("Speaker - Hello World")
}
}
This is illustrated below where the Person class is instantiated and then the
sayHello() member function is invoked on that instance:
fun main() {
val p = Person()
p.sayHello()
p.saySomething()
}
Interfaces can contain concrete member functions and classes or objects can imple-
ment multiple interfaces. This means that there is the possibility that a class or object
could implement two or more interfaces that have concrete member functions with
the same signature.
For example given the following two interfaces:
interface Speaker {
fun saySomething()
fun sayHello() {
println("Speaker - Hello World")
}
}
interface Translator {
fun sayHello() {
println("Translator - Bonjour")
}
}
Any type implementing these two interfaces would find that the member function
sayHello() was duplicated. This would be identified by the compiler and an error
similar to the following would be generated;
238 14 Interfaces, Delegation and Enumerated Types
This error makes it clear that the implementing class must override the member
function sayHello() to deal with the potential confusion between the behaviour
implemented in the two interfaces.
This could be done simply by defining a local implementation in the implementing
class. For example the class Employee does just that by providing its own definition
of the sayHello() member function thus hiding both versions defined in the two
interfaces:
In this revised version of the class Employee the sayHello() member function
invokes the version of sayHello() defined in the Speaker interface.
This last version of the class Employee is used in the following short program:
fun main() {
val e = Employee()
e.sayHello()
e.saySomething()
}
Kotlin Interfaces 239
Interfaces can also have companion objects. Such companion objects can provide
functionality and properties that can only be accessed directly from the interface or
from within concrete member functions defined within the interface.
An interface companion object is defined in exactly the same way as a class
companion object using the keywords companion object.
This is illustrated in the Printer interface definition below:
interface Printer {
fun prettyPrint()
fun printLabel() {
println(label)
printMe()
}
companion object {
private const val label = "Label"
fun printMe() {
println("Printer - printMe")
}
}
}
fun main() {
// Can call member function on interface
Printer.printMe()
// Compile error
// ShoppingBasket.printMe()
val basket = ShoppingBasket()
// Compile error
// basket.printMe()
basket.prettyPrint()
// Compile error
// Printer.prettyPrint()
}
We have commented out all lines which would generate a compiler error. This
illustrates several things:
1. It is possible to access the interface’s companion object’s members via the
interface name, for example Printer.printMe().
2. It is not possible to access the interface companion object members from the
implementing class; thus you cannot write ShoppingBasket.printMe().
3. An instance of the class implementing the interface also does not inherit and
cannot directly access, the interfaces companion object and its members thus
basket.printMe() is invalid.
4. It is not possible to invoke an abstract member function on an interface thus
Printer.prettyPrint() is also invalid.
The output from the program is:
Printer - printMe
ShoppingBasket - prettyPrint
Marker Interfaces
interface Decorator
interface Service
Any type can implement one or more interfaces, thus any type can implement a
marker interface and any other interfaces as required. For example:
Delegation
The delegation pattern is an object oriented design pattern. The purpose of the
delegate design pattern is to use instance composition to achieve code reuse without
using inheritance.
In delegation, an instance handles a request for some behaviour (i.e. for some
member function to run) by delegating to a second instance (the delegate). The
delegate is a helper instance of a difference class to the original message receiver.
In the delegate pattern, this is accomplished by holding a reference to a delegate
within the original instance and mirroring the member functions in the delegate with
member functions in the receiver that merely pass the request onto the delegate.
242 14 Interfaces, Delegation and Enumerated Types
The basic idea behind the delegation pattern is shown below. There is a type (class
or object0 that is the Delegator. This delegates responsibility for some behaviour to
a delegate. This delegate is often defined as an interface or an abstract class. One
or more concrete implementations are then provided for the Delegate that can be
plugged in as required.
To illustrate this we will first define an interface and a class that implements that
interface:
interface Role {
fun printRole(): Unit
}
For example, given an interface Role and a default implementation of that inter-
face (the TellerRole) we could now create a class that implemented the interface
Role by delegating (or forwarding) the result onto an instance of TellerRole:
Instances of the class User still implement the Role interface via delegation to
an instance of the TellerRole class however it is now explicit in the specification
of the class and avoids the developer having to write a set of boiler plate member
functions and properties.
An example of using the User class is given below:
fun main() {
val user = User("Player1")
user.printRole()
}
Role: Player1
Note that you can implement any number of interfaces in this way by using a
series of <interface> by <class>declarations separated by a comma. For
example assuming we have a set of interfaces Role, Accessor and Printable
then we could delegate the responsibility for each to a separate class:
Enumerated Types
An enumerated type (also called an enum) is another part of the Kotlin type system.
It consists of a set of named values called the elements or members of the enumerated
type. The values are names associated with an integer index that starts from zero.
The values can also posses properties that allow them to represent other values.
Kotlin Enums
In Kotlin an enumerated type is defined using the keywords enum class. This is
illustrated below for a simple enumerated type representing directions:
244 14 Interfaces, Delegation and Enumerated Types
Note that the values that comprise the enumerated type are defined as named
constants in a comma separated list.
In this case the enumerated type Direction defines the values NORTH, SOUTH,
EAST and WEST. There is also an explicit ordering defined for the values in an
enumerated type. In this case NORTH comes before SOUTH and SOUTH comes after
NORTH.
It is possible to define a property or a local var or val as having the type
Direction. The val or var can then be assigned one of the values defined for
the enumerated type Direction, for example:
fun main() {
val d = Direction.NORTH
println(d)
println("d.name: ${d.name}")
println("d.ordinal: ${d.ordinal}")
if (d == Direction.NORTH) {
println("We are heading North")
}
println(d < Direction.SOUTH)
}
The result of running this program is:
Enumerated Types 245
NORTH
d.name: NORTH
d.ordinal: 0
We are heading North
true
The typical use of enumerated types is associated with having an ordered sequence
of values where the ordering is significant.
This is illustrated below for the DaysOfWeek enumerated type. There is a very
definite ordering to the days of the week, with Monday coming before Tuesday and
Friday coming at the end of the week. In an application it may be useful to be able
to reason about this ordering explicitly.
The following code example illustrates this idea:
fun main() {
val day = DaysOfWeek.WEDNESDAY
println(day)
println(day < DaysOfWeek.FRIDAY)
println(day < DaysOfWeek.MONDAY)
}
WEDNESDAY
true
false
Notice that when each of the enumerated values is defined it is now provided with
an appropriate value for bearing.
We can now use the CompassDirection enumerated type and its set of values
in a simple program to illustrate accessing the bearing property associated with a
enumerated CompassDirection:
fun main() {
val d = CompassDirections.WEST
when (d) {
CompassDirections.WEST -> println("Heading " +
d.bearing)
else -> println("Unknown heading")
}
}
Online Resources
The following provide further information on interfaces, the delegation pattern and
enums classes:
1. https://kotlinlang.org/docs/interfaces.html Interfaces in Kotlin documentation.
2. https://en.wikipedia.org/wiki/Delegation_pattern Wikipedia delegation design
pattern.
3. https://kotlinlang.org/docs/enum-classes.html Enumerated types in Kotlin.
Exercises
The aim of this exercise is to define an interface for our account types and also to
create an enumerated type for the types of investment accounts.
First create rename your Account to AbstractAccount.
Exercises 247
Next create an interface called Account. Note that this interface should have the
following abstract properties:
1. A val accountNumber: String
2. A val holder: String
3. A val balance: Double. Note this is a val not a var in the interface. The
class has a private setter and makes it a var but that is internal to the class. The
public API to the class makes it look like the balance is a val property - which
we need to reflect in the Kotlin interface Account.
The interface should also have two abstract member functions deposit() and
withdraw(). Both these abstract member functions should take a Double as a
parameter.
In the AbstractAccount class make it implement the Account interface.
Note you will need to mark the deposit() and withdraw() member
functions with override. You will also need to mark the properties with
override.
Next create an enum class called InvestmentAccountTypes which will
define three classes of investment account, High, Medium and Low risk.
Now change the InvestmentAccount class so that the type field is an
InvestmentAccountTypes not a string and instantiate it with an appropriate
value, for example:
Introduction
Packages are a language construct used in Kotlin to organise larger programs. This
chapter introduces packages as well as visibility modifiers.
Packages
You can bring a set of related functions, properties, classes and objects together
in a single compilation unit by defining them all within one directory. By default,
this creates an implicit (unnamed) package; classes and objects in the same direc-
tory can access properties and member functions that are only visible in the current
package. However, such a group of functions, properties, classes and objects cannot
be accessed by code defined in another directory with a different name. This is there-
fore an acceptable approach when creating sample code or exploring the language,
however for real world applications the is not appropriate.
A much better approach is to group the classes together into an explicit named
package.
Packages are encapsulated units which can possess classes, objects, interfaces,
functions, properties and enumerated types. Packages are extremely useful as they:
• Help organise your code base. They allow you to associate related types,
functions and properties.
• Provide a namespace. They resolve naming problems which would otherwise
cause confusion.
• Support visibility/access controls. They allow some privacy for objects, classes,
member functions, properties and functions that should not be visible outside the
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 249
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_15
250 15 Packages
package. You can provide a level of encapsulation such that only those elements
which are intended to be public can be accessed from outside the package.
Indeed the Kotlin language and associated third party libraries provides a wide
range of packages which, as your applications develop and particularly if you use
Android, you will need to work with.
Declaring a Package
An explicit package is defined by the package keyword at the start of the file
in which one or more functions, classes, objects, enumerated types, interfaces or
properties are defined:
package benchmarks
The package statement must be the first executable line in a file (comments are
not executable code).
Package names should be unique to ensure that there are no name conflicts. Kotlin
adopts a naming convention by which a package name is made up of a number of
components separated by a full stop/period. These components correspond to the
location of the files. Thus if the files in a particular package are in a directory called
benchmarks, within a directory called tests, then the package name is given as:
package tests.benchmarks
Notice that this assumes that all files associated with a single package are in the
same directory. It also assumes that files in a separate package will be in a different
directory. Any number of files can become part of a package, however any one file
can only be part of/specify a single package.
A further convention that Kotlin adopts is that to ensure that your packages are
unique, that you should prefix your packages with your domain name in reverse.
For example, if you worked for a company called Midmarsh Technology and
your web domain was www.midmarsh.com then your packages would all start
com.midmarsh, for example:
package com.midmarsh.util
How Packages Relate to Directories 251
Packages relate to the directory structure in which the.class files generated from
your source code are stored. This structure is created by the compiler and is based on
the package names. Each part of the package name maps onto a directory with each
full stop (.) acting as a separator between parts of the package name. For example,
given the following code:
package com.jjh.util
When this is compiled, the compiler will generate a directory structure that match
the com.jjh.util package name.
In this diagram the source code is stored under a directory called src while
the.class files created by the compiler are stored under the target directory.
Note that the source code directory also follows the convention that the package
name elements match onto directories, however for the source code this is just a
convention; it is not enforced (although many IDEs will complain if you do not
follow this convention).
However, it is enforced for the target directory, the class Person must be
compiled into Person.class file which must be stored within a directory structure
com/jjh/util.
Finding a Package
All components in the package are relative to the contents of the CLASSPATH
variable. This environment variable tells the Kotlin compiler and Virtual Machine
runtime where to start looking for class definitions.
Thus, if the CLASSPATH variable is set to C:\jjh\kotlin then the following
path is searched for the elements of the package com.midmarsh.utils:
252 15 Packages
c:\jjh\kotlin\com\midmarsh\utils
Notice how the whole path is a combination of the value in the CLASSPATH
environment variable and the directory structure indicated by the package name.
All the files associated with the com.midmarsh.util package should
therefore be in this directory.
The Kotlin compiler can be told where to place the.class files using the -d
compiler option (which indicates the destination directory). For example, in the
previous digram the source code is under src directory but the.class files are
under the target directory, thus we might compile this code using:
Which assumes that the kotlinc command is being issued in the directory
above both the src and target directories. The ‘*/kt’ indicates the files to compile.
The classpath does not need to be set via an environment variable, it is also possible
to set it when a program is run using the command line option -cp or -classpath.
This can be done wether you are using either the kotlin command or using the
java command with the appropriate Kotlin libraries added to the classpath, for
example:
This is often hidden from the developer as IDEs such as IntelliJ handle this for
you.
It is also command to create a Jar file that wraps all the directory structure
and.class files up into a single file (which makes it much easier to deploy). This
can be created using the jar command for example:
Importing a Package
This allows code in a separate package to access the definition of the class
Person and create a new instance of that class. The same is true for objects,
functions and top level properties. For example let us assume that there is a func-
tion printer(Person) defined in the same com.jjh.util package then we
could reference that via:
com.jjh.util.printer(p1)
Always referencing elements in other packages using their fully qualified names is
certainly possible but it is rather long winded and would result in quiet verbose code.
In Kotlin it is possible to import objects, classes, enumerated types, interfaces,
functions and properties from one package into another packaging using the import
statement.
The import statement follows any package declaration in a file and comes before
any type, function or property declarations.
An example of importing the class Person from the package com.jjh.util
into a package com.jjh.payroll is given below:
package com.jjh.payroll
import com.jjh.util.Person
This makes the class Person directly accessible within the current
file within the package com.jjh.payroll. Within this file the type
com.jjh.util.Person can be referenced just using the name Person.
254 15 Packages
Importing does not affect the size of the class file being created as importing is
actually a visibility thing; it does not bring the definition of the class Person into
the current file; it only makes it visible.
In addition importing a class, function or property into a file only makes it visible
within that file; it is not made visible to the package as a whole. Any other files in
the same package must import their own elements as required.
Import Options
There are in fact several ways and things that can be imported. You can import a
specific type, function or property. This is the example illustrated in the previous
section for the class Person, here are some other examples:
As the above examples show you can import anything from a package including
objects, classes, properties and functions. It is also possible to import all public
elements in a package using the asterisk (*) wild card.
It is also possible to import member functions and properties from within an
object and then reference them directly, for example given an object Course with
a property present, you can write:
import com.jjh.util.Course.present
The above code imports the property present from the object Course in the
package com.jjh.util. If this is done then within the rest of the file it is possible
to merely reference the property present to get to the property; it is not necessary
to prefix it with the name of the object.
It is also possible to import all elements from an object using the asterisk (*)
wildcard, for example:
import com.jjh.util.Course.*
Importing a Package 255
Now all public properties and member functions are accessible within the current
file.
You can also import values from within an enumerated type, for example:
import com.jjh.util.VehicleTypes.SUV
It is now possible to access the enum value SUV directly in the current file.
In some cases you might find that the name of a package element (such as a class
or object) clashes with the name of another element in another package. In such
situations you can either resort to using fully qualified names as these ensure that
even if the class Person is defined in a package com.jjh.util and a package
com.staff then their fully qualified named will be different, for example:
val p1 = com.jjh.util.Person()
val p2 = com.staff.Person()
Here the use of fully qualified names completely differentiates between the two
classes.
However, having to use fully qualified names is quiet long and laborious; Kotlin
provides an alternative, you can use an alias for a class so that any potential clashes are
avoided. This is done by appending an as<newName> to the end of the import
statement. For example:
Several packages are imported by default into your code by the Kotlin compiler,
these are listed below:
256 15 Packages
• java.lang provides access to default Java types such as String and is only
available when Kotlin is being executed using the JVM runtime environment.
• kotlin.jvm Functions and annotations specific to the Kotlin Java platform.
• kotlin Core functions and types.
• kotlin.annotation Library support for Kotlin annotations.
• kotlin.collections Collection types
• kotlin.comparisons Helper functions for creating Comparators (since 1.1)
• kotlin.io API for files and streams
• kotlin.ranges ranges and progressions library
• kotlin.sequences lazily evaluated collections
• kotlin.text functions for text and regular expressions.
Visibility Modifiers
All the components in a package are public by default. That is, they are visible
anywhere that the package is accessible. However, this is just the default. You can
change the visibility of an object, class, interface, enumerated type, constructor,
member function, function, top level and member properties by using a modifier
keyword before the declaration keyword.
There are four visibility modifiers:
• public This is the default for all package elements as well as for member
functions and properties. No explicit modifier is required to declare something
as public. However the keyword public does exist and can be applied to any
element although it is redundant.
• private The effect of the private keyword depends on where it is being
applied, there are two situations in which you can mark somethings private:
– private top level elements such as classes, objects, interfaces, enumerated types,
functions and properties are only accessible in current file,
– private members elements elements (such as constructors, properties or
member functions) are only accessible in same class or object.
• protected Member functions or properties can be marked as protected
which indicates that they are only accessible to the current class and subclasses.
They are not accessible anywhere else. Note that the subclasses can be defined
anywhere in any package. Also note that it is not possible to mark top level package
elements as protected; that is you cannot mark a top level class as protected.
• internal This indicates that the associated element is only accessible within
the same module. Modules are used by developers within the IntelliJ IDE to help
them organise larger, more complex systems. They are frequently used within
Android applications to help organise the code base.
Visibility Modifiers 257
It should also be noted that although a property can have one visibility, its setter
function can have a different visibility. The getter functions alway have the same
visibility as the property. However, the setter functions can have a different visibility;
a common idiom is to define a property that is a var with the getter being public
but the setter being private to a particular scope (such as a class, object or package).
For example:
The above declares a public property name. However although it is a var the
setter is marked as private. Thus the property is read-only externally to wherever it
is defined but read-write internally. Note that we did not have to explicitly specify
the get() function as that is implied by the definition of the property.
package com.jjh.util
This indicates that the class Person can only be instantiated from within the
class.
This may at first seem nonsensical; however it is usual that such a class would
have an associated companion object with a factory member function on it that is
used to create instances of the class. This is illustrated below.
258 15 Packages
fun main() {
val session1 = Session.create()
println(session1)
val session2 = Session.create()
println(session2)
}
In this example, the Session class has a companion object that defines a
create() member function. This member function can create instances of the
class Session as it is defined within the class Session and thus has access to the
private constructor.
However, no code outside of the class can access the constructor. In this case it is
done to ensure that each Session created has a unique id.
Online Resources
Exercise
The aim of this exercise is to create a package for the classes you have been
developing.
Exercise 259
package fintech.main
import fintech.accounts.*
fun main() {
val acc1 = CurrentAccount("123", "John",
10.05, -100.00)
val acc2 = DepositAccount("345", "Denise",
23.55, 0.5)
val acc3 =
InvestmentAccount("567",
"Phoebe",
12.45,
InvestmentAccountTypes.HighRisk)
println(acc1)
println(acc2)
println(acc3)
acc1.deposit(23.45)
acc1.withdraw(12.33)
println("balance: ${acc1.balance}")
acc1.withdraw(300.00)
println("balance: ${acc1.balance}")
AbstractAccount.printInstancesCreated()
}
Chapter 16
Nested/Inner Types
Introduction
Nested and inner types are types that are defined with the scope of an outer (or top-
level) type. This chapter introduces nested as well as inner types and considers where
and when they can be defined.
All of these types can be defined inside another, top level type. They are not part of
the outer type per say but are part of the namespace created by the outer type.
Inner types are different to nested types. An Inner type is a type defined within
the scope of another type and is part of that type. You can define an inner classes and
anonymous objects within a class, object, enumerated type or interface.
Additionally, you can define function and member function level inner classes
and anonymous objects. These are classes and anonymous objects that are defined
within a function or member function.
The attributes that a nested/inner type possess will depend on whether they are
nested types or member inner types or function/member function inner types.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 261
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_16
262 16 Nested/Inner Types
Nested Types
A nested top level type (class, object, enum or interface) is exactly like a normal top
level type, except that it has been placed within an existing type. Such types can be
used in the same way as any other top level type.
Thus defining nested types within an existing type is really a namespace thing.
That is nested types are grouped together within an outer type for convenience and
may be treated like any normal type. Although, they must be referenced either via
their outer type or imported directly using the import statement, for example import
outerClassName.innerClass-Name.
Thus such types may be referenced by program elements outside of the top level
type. This means that:
• instances can be created from nested classes,
• nested interfaces can be used with objects and classes,
• nested objects can be referenced,
• nested enumerated types can be accessed,
• nested abstract classes can be extended.
Nested classes and objects can implement any interfaces and extend any required
class.
Some examples of top level nested types are given below:
package com.midmarsh.inner
class Util {
The class Util acts as the top level class that contains four nested types. In this
case there is a:
• nested class Printer,
• nested interface Printable,
• nested object Session and
• nested abstract class DefaultPrintable.
To reference any of the nested types they must either be referenced using their
fully qualified name, for example:
import com.midmarsh.inner.*
val printer = Util.Printer()
Alternatively the members of the Util class can be imported directly either using
the wildcard asterisk format:
import com.midmarsh.inner.Util.*
fun main() {
Session.doSomething()
}
264 16 Nested/Inner Types
A final option is to specifying the elements from the Util class that should be
imported:
import com.midmarsh.inner.Util.Session
fun main() {
Session.doSomething()
}
As previously mentioned, it is not only classes that can have nested types; enumer-
ated types, objects and interfaces can have them as well. The following illustrates an
object that contains a set of nested types:
object MyObject {
class Author(val name: String)
interface Reviewable {
fun review(): BooleanArray
}
object InnerObject {
fun prettyPrint() {
println("prettyPrint")
}
}
enum class DaysOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY
}
}
In this case the object MyObject contains a nested class, interface, object and
an enum class (enumerated type).
To complete the set here is an example of an interface containing a set of nested
types:
interface Processable {
class InnerClass(val id: String)
object InnerObject{
val name: String = "Title"
}
fun doSomething(): Boolean
}
This interface definition contains a nested class, a nested object and a member
function signature that will need to be provided by any class implementing this
interface. This is illustrated below:
Nested Types 265
Note that the class Processor does not have access to the InnerClass
nor the InnerObject directly. To access them it must user the Processable
namespace, for example Processable.InnerObject.
Finally, the following code illustrates the same use of nested types within an
enumerated type:
Inner member classes and anonymous objects are defined within the scope of an
existing type but outside of any member function or init{} block.
The keyword inner is used to indicate that an inner class (rather than a nested
class) is being defined. This is important as missing out the keyword inner changes
the type being defined significantly.
An inner class or anonymous object is defined at the member level of an existing
type. They possess very specific attributes which include being:
• defined within the scope of an existing type including classes, objects, enumerated
types and interfaces,
• able to access the outer class/object properties and member functions. This is also
true of an member inner type within an interfaces where the inner type can access
the concrete member functions of the interface,
266 16 Nested/Inner Types
• able to access outer class “this” variable. This can be done using
this@<outerclassname> syntax,
• able to be an interface specification,
• able to have default (public), private, protected or internal visibility,
• able to be an abstract class,
• able to be an anonymous object.
For example, in the following class Calculator, an inner member class is defined
that is used to perform a calculation as a separate Worker instance.
The class Calculator has a private constructor property val value. It also
has a var result that has a private set() function. Note that this means that
the result property is a read-only property to anything defined outside of the
Calculator class but is a read–write property internally to the class.
The Calculator class also has a count private var property that is used when
creating Worker instances in the get() function of the worker property. Notice
that this means that from outside of this class worker, it appears to be a read-only
property, however internally to the class the getter function generates a new Worker
each time the client code calls get().
The Worker class is an inner class that has its own id val property and a member
function performCalculation().
The performCalculation() member function accesses the outer class’s
value property and updates the result property of the outer class. The inner
class can do this because, although the value property is a private property and the
result setter function is marked as private, the inner class is defined within
Member Inner Classes/Anonymous Objects 267
the scope of the outer class and thus has direct access to all the private elements of
the class.
The following program uses the Calculator class and illustrates how the
Worker can be accessed:
fun main() {
val calc = Calculator(5)
val worker = calc.worker
worker.performCalculation()
println("calc.result: " + calc.result)
}
calc.result: 10
These are classes or anonymous objects that are defined within a member function.
They have the scope of the enclosing block, thus they may only be visible for part
of a member function’s execution. They can access the enclosing types properties
and member functions and any local vals, vars or parameters.
An example of a member function inner class is given below:
268 16 Nested/Inner Types
object CalculateFib {
fun printFibSequence(number: Int) {
val separator = ","
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Function Inner Classes/Objects 269
It is also possible to define function inner classes and anonymous objects. These are
classes and anonymous objects defined within the scope of a function. As such they
can access any local vals or vars and parameters available within the function.
A simple example of a function level inner class is given below using the main()
function:
fun main() {
class Person(val name: String) {
override fun toString() = "Person($name)"
}
val p = Person("John")
println(p)
}
In this case the class Person is only accessible from within the function main().
Online Resources
Exercise
This should be defined as an inner class within the Account class that implements
the Transaction interface.
270 16 Nested/Inner Types
interface Transaction {
val amount: Double
val type: String
val datetime: LocalDateTime
val transactionBalance: Double
}
Note that the member functions in the Account interface will need to be updated
to return a Transaction.
Note in the CurrentAccount you will need to decide what to return if the
withdrawal will take the customer over the over draft limit. For now you can set the
amount for the transaction to 0.0 and the type to “failed–exceeded overdraft”.
Finally you should now be able to print the result of calling deposit or withdraw
on any account:
println(acc1.deposit(23.45))
println(acc1.withdraw(12.33))
println("balance: ${acc1.balance}")
println(acc1.withdraw(300.00))
println("balance: ${acc1.balance}")
Introduction
We have now explored Kotlin classes, nested classes and inner classes, however there
is another type of class in Kotlin called a Data Class.
Data classes can be used to represent data-oriented concepts. That is concept that
represent data but tend not to have much related functionality. Such a class might
contain several properties but other than member functions for equality or string
conversions they do not contain any behaviour. This chapter presents Data Classes.
A data class is defined in the same way as a normal class, with the addition of the
keyword data placed in front of the class keyword. For example:
The above definition creates a new data class called Person that has two
constructor properties name and age. This illustrates that the constructor prop-
erties can be vals or vars, although it is far more common for all the constructor
properties in a data class to be vals.
It is not possible to define plain constructor parameters in data class.
In fact there are several effects of marking the class as a data class as well as
restricting the constructor parameter types, these are:
• The class definition must start with the keyword data.
• It must have at least one constructor property.
• All constructor parameters must be vals or vars.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 271
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_17
272 17 Data Classes
The GameContent data class can be used within a program in exactly the same
way as any other class, for example:
fun main() {
val inst1 = GameContent(10.0, 10.0)
println(inst1)
val inst2 = GameContent(10.0, 10.0)
// uses default implementation of toString()
println(inst2)
println("x: ${inst2.x}, obj2.y: ${inst2.y}")
// uses default implementation of equals()
println(inst1 == inst2)
// Uses default implementation of hashCode()
println(inst1.hashCode())
}
In this program two instances of the data class GameContent are created. Both
are initialised with the values 10.0 for their x and y coordinates. The program then
illustrates some of the features listed above.
Defining Data Classes 273
• When a data class instance is printed the default toString() generated for the
data class will print out the name of the data class and the values of x and y.
• The equality operator (==) will use the equals() member function which was
generated by the compiler for the data class.
• The hashCode() member function was also generated for the data class.
The output from this program is:
GameContent(x=10.0, y=10.0)
GameContent(x=10.0, y=10.0)
x: 10.0, obj2.y: 10.0
true
75497472
Data classes can be defined with any number of member level properties such as that
defined in the data class Ship below:
However, it is only the constructor properties that are used in the toString(),
copy(), equals() and hashCode() member functions. Thus in this case the
value of the image property will be ignored by all of these member functions.
This is illustrated below where two instances of the data class Ship are created.
Their constructor parameters are equal, however the image property of the first ship
has been changed.
fun main() {
val ship1 = Ship(10.0, 10.0)
println(ship1)
println(ship1.image)
ship1.image = "default.png"
println(ship1)
val ship2 = Ship(10.0, 10.0)
println(ship1 == ship2)
}
274 17 Data Classes
Ship(x=10.0, y=10.0)
ship.png
Ship(x=10.0, y=10.0)
true
Data classes can extend any open class. As usual this means that they will inherit
all the properties and member functions defined in the parent class. They can also
override any open properties or member functions etc. A simple example is given
below:
Data classes can implement any number of interfaces. As usual this means that the
data class must implement any abstract members defined in the interface and will
inherit any concrete member functions defined in the interface. For example:
It is useful to be able to make copies of data classes, particularly as the default pattern
is to define immutable data classes.
The Kotlin compiler provides a default implementation of the copy() member
function that allows you to create a copy of a data class instance while overriding
one or more property values (even if they are vals).
This is illustrated below for the data class Ship we presented earlier:
fun main() {
ship1.image = "default.png"
println("ship1: $ship1")
println("ship1.image: ${ship1.image}")
val otherShip = ship1.copy(x = 20.0)
println("othership: $otherShip")
println("otherShip.image: ${otherShip.image}")
}
In this program we make a copy of the original ship1 using the copy() member
function and overriding the value held in the property x. The means that we end up
with two instances of the data class ship; ship1 and otherShip.
The otherShip has the value 20.0 for the property x which was supplied
when the copy was made. The value for the property y however was copied from the
original ship1 and is thus 10.0.
However, the property image was not involved in the copy() operation and
thus the “default.png” String was not copied over. This means that the otherShip
still has the default value provided when the data class was instantiated.
The output is shown below:
It is possible to unpack values from data classes into distinct variables. This can be
done at the point that the receiving variables are defined using the syntax:
However, this only works for constructor properties (any member level properties
are not included).
An example of doing this for the Ship data class is given below:
fun main() {
val ship = Ship(10.0, 10.0)
val (x, y) = ship
println("x $x, y $y")
}
In this case two vals are being created called x and y; they will be populated with
the constructor property values held in ship. Note that the names of the vals are
not significant as the values are extracted/restructured based on their positions in the
constructor rather than on the property names.
The output generated from this code is:
x 10.0, y 10.0
The variables used to hold the extracted values must be declared at the point that
the destructing happens but can be a val or a var, for example:
fun main() {
val ship = Ship(10.0, 10.0)
val otherShip = ship.copy(x = 20.0)
var (a, b) = otherShip
println("a $a, b $b")
}
a 20.0, b 10.0
It is also possible to indicate that you are not interested in certain values using
the underbar syntax (_). This is necessary as the values are extracted based on their
position, thus if we are only interested in the y value for the ship we still need
to indicate that it is the second property defined within the data class constructor.
However using the underbar (_) we can indicate this and ignore the value in x, for
example:
y1: 10.0
Exercise
The aim of this exercise is to create a data class to represent a Customer for our
fintech system.
In this simple example, the Customer data class will have a name, an address and
an email. All three of these properties will be vals and will hold Strings.
You should be able to create a Customer using:
Introduction
There are two further types of classes in Kotlin specifically Sealed Classes and
inline Classes. This chapter will explore each of these. The chapter will conclude by
introducing type aliases.
Sealed Classes
For Kotlin 1.4 and earlier versions of the language, a sealed class is a special type of
class with the following properties:
• A sealed class is a class that can only be extended within the same file that it is
defined in. That is only the classes and objects within the same file as the sealed
class can extend it. Note that sub-sub classes can be defined anywhere as they
only indirectly inherit from the sealed class.
• Sealed classes are implicitly abstract and therefore it is not possible to instantiate
a sealed class directly.
• A sealed class is implicitly open and as such it is possible to extend a sealed class
using a class or object.
• The sealed class can be referenced anywhere that it is visible so that it can be
used as the type for parameters, vals, vars or the return type for functions/member
functions.
• A sealed class is indicated using the keyword sealed before the keyword class.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 279
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_18
280 18 Sealed and Inline Classes and Typealias
An example of a very simple sealed class is given below along with the subclasses
that extend it within the same file:
In this case a simple sealed class called Trade has been defined. In practice this
class acts as a marker class as it provides no properties nor does it define any member
functions.
The sealed class Trade is then extended by the classes EquityTrade, FxTrade,
InterestRateSwap and Swaption. Assuming that this is the entire contents of the file
then we know that there are no other direct subclasses of the sealed class Trade within
the code base.
Sealed classes can have a public primary and any number of auxiliary constructors,
they can have constructor and member level properties and member functions.
The following code extends the definition of the Trade sealed class with a
constructor property id, a counterParty var member level property and a member
function printId():
Although the Trade sealed type can only be extended in the current file, it can be
used as a type in other files. The following code resides in a separate file but is able
to reference the Trade type as a parameter type for the function describe(Trade):
Sealed Classes 281
fun main() {
println(describe(EquityTrade("IBM")))
println(describe(FxTrade("GBP", "USD")))
println(describe(InterestRateSwap(5.0, 3.4)))
println(describe(Swaption("Bermudan")))
}
EquityTrade
FxTrade
An InterestRateSwap
A Swaption
It is not only classes that can extend a sealed class. The following example illus-
trates how a set of objects can be used to create singleton instances of a sealed class
Genre:
Kotlin 1.5 also introduces the concept of a sealed interface and relaxes the restric-
tions on sealed classes. From Kotlin 1.5 onwards a sealed class can be implemented
282 18 Sealed and Inline Classes and Typealias
within the same module rather than just within the same file. This means that if your
application is defined completely within a single module (the default) then a sealed
class is just like any other class. However, if your application is comprised of multiple
modules (as may be the case with an Android application) then a sealed class is only
accessible within the current module.
As from Kotlin 1.5 it is also possible to define a sealed interface. The sealed
interface can be implemented by any class, data class or object defined in the same
module. A sealed interface is implemented using the keyword sealed before the
keyword interface, for example:
Note that in Kotlin 1.5 sealed interfaces are considered experimental so may
change in later versions of the language.
Inline Classes
Kotlin 1.3 introduced a new type of class called the inline class. This type of class was
experimental in Kotlin 1.3, was in beta in Kotlin 1.4 and was stable (fully released)
in Kotlin 1.5.
An inline class can be used to represent value style classes. A value class is a type
where the actual value being represented by the class is held directly by a variable,
at runtime (within the JVM) rather than needing to access that value via a reference
(an address in memory). This can be more efficient for simple types like Int.
Examples of value types include Booelan, Int and Double which can have the
values true, false, 32, 45.7 etc. Inline classes effectively allow developers to create
user defined value types that will be as efficient as native Kotlin value types at runtime,
but allow more semantic meaning to be defined for developers, for their code and for
those maintaining the code base.
To define an inline class in Kotlin it is necessary prefix the keyword class by the
keyword inline.
Inline (value) classes are treated as special by the Kotlin compiler. That is, the
compiler will determine if it can inline the value held by the class directly. This
Inline Classes 283
avoids the need to allocate runtime instances and is thus more efficient and faster (as
no allocation must be made and no reference must be followed).
To ensure that the compiler can treat an instance of the class in this way it is
necessary for the programmer to ensure that no instance allocation is performed
within the type. Thus an inline class cannot hold within itself a reference to a non-
Value type (that is it cannot hold a reference to an instance of the class Person).
To be an inline class the compiler must also ensure that the class being defined:
• has a single public val parameter for the underlying type (that is the fundamental
or basic type being wrapped),
• has an underlying type that must be one of Byte, Short, Int, Long, Float, Double,
String or Boolean,
• is immutable by nature (that is it should not change itself but return a new instance
whenever a change in value is required),
• cannot have any auxiliary constructors,
• cannot define any nested types such as classes, objects or interfaces,
• is not used in tests used to determine their type,
• must not override the equals() or hashcode() member functions,
• cannot have any initialisation blocks (i.e. no init{} blocks).
However, they can have
• any member functions as required,
• properties that do not use a backing field.
The following inline class meets the criteria defined in the last section. That is, the
inline class Name has a single val property value of type String, it is marked with
the keyword inline, it has one property length which is defined relative to the value
property (and thus does not use a backing field) and has a simple member function
greet().
fun greet() {
println("Hello, $value")
}
}
The following simple application illustrates how this class may be used:
284 18 Sealed and Inline Classes and Typealias
fun main() {
val name = Name("Kotlin")
name.greet()
println(name.length)
}
In this example, we create a new instances of the Name class and store it in a
local val name. We then invoke the member function greet() and access the property
length. Note that this looks very much as it would if name held a normal class. The
result is then printed out. The effect of running this application is shown below:
Hello, Kotlin
6
Interestingly the compiler actually replaces the references to Name with the primi-
tive held within the instance at compile time. The method greet() will be replaced with
the String “Hello, Kotlin” and name.length will be repeated with “Kotlin”.length.
Thus there is virtually no overhead in using the inline Name class compared with
using the String class directly.
This raises the question “Why bother?”. The answer is two fold:
• Name is more semantically meaningful than String. That is String is a generic way
of representing a set of characters. The inline class Name represents the concept
of the name of something.
• Name also allows member functions to be defined that allow semantically mean-
ingful operations to be provided that can also indicate what is being done at a
higher level of abstraction than the basic type String would allow.
Inline classes are implicitly treated as closed/final classes, thus ensuring that they
cannot be extended by other classes. That is, it is not possible to combine the keywords
inline and open together.
This is important as it restricts the need for polymorphism and thus allows the
compiler to inline the values being represented.
Inline classes are implicitly assumed to have structural equality and hashcodes.
That is, their equals() and hashcode() member functions are treated as being defined
as:
Inline Classes 285
Where value equates to the underlying property (such as String, Double, or Int).
In other words if the underliers have the same value then the value types are equal
otherwise they are not equal. In addition the hashcode of a value type of the hashcode
of its underlier.
Typealias
A type alias allows you to provide an alternative name for an existing type. It is used
extensively to provide Kotlin like names for Java types. Within your own programs
it can be used:
• to create domain specific names for existing types,
• to provide more meaningful name for generic types,
• or semantic names for function types.
To do this use the typealias keyword followed by the new name you want to give
to a type. The syntax is:
Within the code the developer can now refer to the class Player or to the collection
NodeSet or the function type Test <T>.
286 18 Sealed and Inline Classes and Typealias
Introduction
We will explore Operator Overloading in this chapter; what it is, how it works and
why we want it.
Operator overloading allows user defined classes to appear to have a natural way of
using operators such as +, -, , > or == as well as logical operators such as
& (and) and | (or).
This leads to more succinct and readable code as it is possible to write code such
as:
val q1 = Quantity(5)
val q2 = Quantity(10)
val q3 = q1 + q2
It feels more natural for both developers and those reading the code. The alternative
would be to create member functions such as add() and write code such as
val q1 = Quantity(5)
val q2 = Quantity(10)
val q3 = q1.add(q2)
Which semantically might mean the same thing but feel less natural to most
developers.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 287
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_19
288 19 Operator Overloading
If operator overloading is such a good idea, why don’t all programming languages
support it? Interestingly Java does not support operator overloading!
One answer is because it can be abused! For example, what is the meaning of the
following code:
val p1 = Person("John")
val p2 = Person("Denise")
val p3 = p1 + p2
It is not clear what ‘+’ means in this context; in what way is Denise being added
to John; does it imply they are getting married? If so, what is the result that is held
in p3?
The problem here is that from a design perspective (which in this case may be
purely intuitive but in other cases may relate to the intention of an application) the
plus operator does not make sense for the type Person. However, there is nothing
in the Kotlin language to indicate this and thus anyone can code any operator into
any class!
As a general design principle; developers should follow the semantics of built-in
types and thus should only implement those operators which are appropriate for the
type being developed. For example, for arithmetic value types such as Quantity it
makes perfect sense to provide a plus operator but for domain specific data-oriented
types such as Person it does not.
class and the decision to make it immutable are common approaches when working
with classes such as Quantity.
To implement the ‘+’ and ‘−’ operators we need to provide two special member
functions one will provide the implementation of the ‘+’ operator and one will provide
the implementation of the ‘−’ operator. Such member functions will be prefixed with
the keyword operator:
• ’+’ operator is implemented by a member function with the signature
• operator funplus(other: Quantity): Quantity
• ‘−’ operator is implemented by a member function with the signature
• operator funplus(other: Quantity): Quantity.
Where other represents another Quantity which will be either added to, or
subtracted from, the current Quantity instance.
The member functions will be mapped by the Kotlin compiler to the operators
‘+’ and ‘−‘; such that if someone attempts to add two quantities together then the
plus(Quantity) member function will be called etc.
The definition of the class Quantity is given below:
Using this class definition, we can create two instances of the type Quantity
and add them together:
fun main() {
val q1 = Quantity(5)
val q2 = Quantity(10)
println("q1 = $q1, q2 = $q2")
val q3 = q1 + q2
println("q3 = $q3")
}
q1 = Quantity(value=5), q2 = Quantity(value=10)
q3 = Quantity(value=15)
As the Quantity class is immutable, when two quantities are added tougher a
new instance of the class Quantity is created. This is analogous to how integers
work, if you add together 2+ 3 then you get 5; neither 2 or 3 are modified however;
290 19 Operator Overloading
One issue with the current implementation of Quantity is that it is only possible
to add two quantities together, what about if we want to add an Int to a Quantity?
To do this we will need to overload the plus() and minus() member functions.
That is we will need to define additional versions of the plus() and minus()
member functions that take different types of parameters such as Int, for example:
In this second version of the Quantity class we have two versions of the plus
member function and two version of minus member function. Each version takes a
different type of parameter, one takes a Quantity the other takes an Int. This is
a common thing to do to allow different types to be used with operators as required
by your application.
This means we can now modify our application such that we can add an integer
to a Quantity:
fun main() {
val q1 = Quantity(5)
val q2 = Quantity(10)
println("q1 = $q1, q2 = $q2")
val q3 = q1 + q2
println("q3 = $q3")
val q4 = q3 + 7
println("q4 = $q4")
}
Introduction 291
q1 = Quantity(value=5), q2 = Quantity(value=10)
q3 = Quantity(value=15)
q4 = Quantity(value=22)
Not all types are naturally immutable and not all operators can be used with
immutable types. For example, increment and decrement operators cannot be used
with immutable types as they operate on the instance itself. That is the increment
operator, when used with a var a, such as a++, is equivalent to writing a= a+ 1
which might be most naturally implemented as modifying the current value.
The data class Counter is defined to be mutable, that is its value can be changed
directly.
This means that the operator member functions plus and minus are changed so
that they do not return a value, instead they affect the current instance directly. It also
means that we can now implement += and −+ related operator member functions.
These are the plusAssign() and minusAssign() member functions.
We can now use these operators in a program with the Counter data class, for
example:
292 19 Operator Overloading
fun main() {
val c1 = Counter(1)
val c2 = Counter(5)
c2 += c1
println("c2 += c1: $c2")
}
Here we are using the += operator which will call the plusAssign()
operator member function and modify the value of the Counter referenced by c1.
The output from this program is:
c2 += c1: Counter(value=6)
Numerical Operators
There are five different numerical operators plus the rangeTo operator (represented
by ..) that can be implemented by special member functions; these operators are
listed in the following table.
We have already seen examples of add and subtract; this table indicates how we
can also provide operators for multiplication and division etc.
The updated Quantity class is given below for the five numerical operators
(excluding the range operator):
This means that we can now extend our simple application that uses the
Quantity class to include some of these additional numerical operators:
fun main() {
val q1 = Quantity(5)
val q2 = Quantity(10)
println("q1 = $q1, q2 = $q2")
q1 = Quantity(value=5), q2 = Quantity(value=10)
q1 + q2 = Quantity(value=15)
q2 - q1 = Quantity(value=5)
q2 * 3 = Quantity(value=30)
q2 / 2 = Quantity(value=5)
q2 % 3 = Quantity(value=1)
A unary operator is an operator that only has one operand. That is it is only applied
to a single value. This is in contrast to binary operators such as multiple or divide
which have two operands (i.e. two values to which they are applied).
There are three unary prefix operators available in Kotlin:
• Unary positive (+) The result of the unary positive operator is the value of its
operand.
• Unary negative (−) this operator produces the negative of the operand value, this
-x will produce the negative of the value held in x, if x has the value 2 then -x will
produce −2. However, if x has the value −2 then -x will produce the value 2.
• Not operator (!) This operator logically inverse the value of its operand. For
example !flag will logically invert the value of flag, if flag has the value true then
!flag return false, in turn if the value of flag is false then !flag returns the value
true.
294 19 Operator Overloading
The operators and their member functions are listed in the following table.
An example of defining the dec() and inc() operator member functions for
the Counter class is given below:
This means that we can now apply the increment and decrement operators to a
var of type Counter (note this must be a var as we are changing the value of the
var):
fun main() {
var c = Counter(1)
c++
println(c)
}
Postfix Increment and Decrement Operators 295
Counter(value=2)
Comparison Operators
Numerical types (such as integers and real numbers) also support comparison opera-
tors such as equals, not equals, greater than, less than as well as greater than or equal
to and less than or equal to.
Kotlin allows these comparison operators to be defined for user defined
types/classes as well.
Just as numerical operators such as ‘+’ and ‘−’ are implemented by special
member functions so are comparison operators. For example the ‘<’ operator is
implemented by a member function called compareTo() and a logical test of its
result.
The complete list of comparison operators and the associated special member
functions is given in the following table.
We can add these definitions to our Quantity class to provide a more complete
type that can be used in comparison style tests (such as if statements). However,
the equals() functionality is already provided as we marked the class as a data
class. We therefore only need to implement the compareTo() member function
to provide a complete set of logical operators.
The updated Quantity class is given below (with some of the numerical
operators omitted for brevity):
296 19 Operator Overloading
This now means that we can update our sample application to take advantage of
these comparison operators:
fun main() {
val q1 = Quantity(5)
val q2 = Quantity(10)
println("q1 = $q1, q2 = $q2")
q1 = Quantity(value=5), q2 = Quantity(value=10)
q1 < q2: true
q1 < q2: true
q1 < q2: false
q1 < q2: true
q1 < q2: false
q1 < q2: falsee
This category of operators provide a set of shorthand operators that can be used to
apply a numerical operator and an assignment in one go. For example a+ = b is a
shorthand form for a = a + b. These operators are presented in the following table:
Augmented Assignment Operators 297
We can now modify the class Counter to define these augmented assignment
operators:
Note that we have chosen the parameter types used with the operators based on
what makes sense for the Counter type. Thus we have decided that you cannot
multiple a Counter by a Counter but you can multiple a Counter by an integer.
We can now uses these operators with an instance of the class Counter:
fun main() {
var count = Counter(10)
count /= 2
println(count)
count *= 3
println(count)
}
Counter(value=5)
Counter(value=15)
One point to note about the numerical operators and the augment assignment
operators is that there can be ambiguity in which operator to apply. For example,
298 19 Operator Overloading
if we define both the plus operator member function and the plusAssign operator
member function then when we write count+ = Counter(1) the compiler
will not know which operator member function to use and will generate a compiler
time error as shown below:
At which point you can decide how to handle this by either only defining one
of the operators or by changing the parameter types of one or more of the member
functions.
These operators are used with types that contain values such as collections. They
can be used to check that a value is held within the type being tested. There are
two operators in and !in but it is only necessary to implement a single operator
member function called contains():
As these operators do not really make sense for the Quantity type, we will not
define them. For example, what would it mean to say:
q1 in q2
• they are defined using the keyword infix before the member function definition.
Once defined they can be used without resorting to the dot notation to invoke the
associated behaviour.
The following revised Quantity class definition defines two infix named
operators add and sub.
fun main() {
val q1 = Quantity(5)
println("q1 add 5: ${q1 add 5}")
println("q1 sub 5: ${q1 sub 5}")
}
q1 add 5: Quantity(value=10)
q1 sub 5: Quantity(value=0)
Summary
Only use operators when they make sense and only implement those operators that
work/make semantic sense for the type you are defining. In general, this means
• Arithmetic operators should only be used for values types with a numeric property.
• Comparison operators typically only make sense for classes that can be ordered.
• Containment (In) operators typically work for types that in some way contain
other a collection of data items.
Online Resources
Exercises
The aim of this exercise is to define some operators for the Account classes to
make it more natural to work with them. For example, it should be possible to add
an amount to an account and subtract an amount from an account.
Exactly how you implement this is up to you. One approach is to merely
wrap the add and sub member functions around the deposit and withdraw
member functions. To obtain a suitable return type you can then merely retrieve the
transactionBalance from the tractions instance.
You should be able to run the following code:
println(acc1 + 10.55)
println(acc1 - 5.0)
Chapter 20
Error and Exception Handling
Introduction
This chapter considers exception and error handling and how it is implemented in
Kotlin. The chapter first introduces the object oriented model of exception handling
as well as how to define custom exceptions and exception chaining. The chapter then
explores the functional approach to exception handling in Kotlin.
When something goes wrong in a computer program someone needs to know about
it. One way of informing other parts of a program (and potentially those running
a program) is by generating an error object and propagating that through the code
until either something handles the error and sorts thing out or the point at which the
program is entered is found.
If the error propagates out of the program, then the user who ran the program needs
to know that something has gone wrong. Typically they are notified of a problem
via a short report on the error that occurred and a stack trace of where that error can
be found. The stack trace shows the sequence of calls (both functions and member
functions) that were invoked up until the point at which the error occurred.
You may have seen these yourself when writing your own programs. For example,
the following program will cause an exception to be generated as it is not possible
to convert the string “42a” into an integer:
package com.jjh.exp.basic
fun main() {
val numberString = "42a"
println(numberString.toInt())
}
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 301
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_20
302 20 Errors and Exception Handling
When we run this program we obtain a stack trace in the output console of your
program, such as that displayed within the IntelliJ IDE. This is because the exception
is not handled by the program and instead it has propagated out of the program and
a stack trace of the code that was called is presented. Note the line numbers are
included which helps with debugging the problem.
What is an Exception?
The above diagram illustrates the class hierarchy for some of the common types
of errors and exceptions in Kotlin.
When an exception occurs, this is known as raising an exception and when it
is passed to other code to handle the error or exception this is known as throwing
an exception. The code which receives and handles the error exception is described
as having caught the exception. These are terms that will hopefully become more
obvious as this chapter progresses.
As stated above all exceptions inherit from kotlin.Throwable.
On the JVM runtime these are actually a set of typealiases that map the Kotlin
types to the underlying Java (JVM) runtime exceptions, for example:
• kotlin.Throwable maps to java.lang.Throwable
• kotlin.Exception maps to java.lang.Exception
• kotlin.NullPointerException maps to java.lang.NullPointerException.
An exception moves the flow of control from one place to another. In most situations,
this is because a problem occurs which cannot be handled locally but that can be
handled in another part of the system.
The problem is usually some sort of error (such as dividing by zero), although
it can be any problem (for example, identifying that the postcode specified with an
address does not match). The purpose of an exception, therefore, is to handle an error
condition when it happens at runtime.
304 20 Errors and Exception Handling
It is worth considering why you should wish to handle an exception; after all the
system does not allow an error to go unnoticed. For example, if we try to divide by
zero, then the system generates an error for you. However, in general we do not want
programs to fail or crash in an uncontrolled manner. We can therefore use exceptions
to identify that an issue has occurred and to determine how to correct it, for example
we might request that the user correct the mistake and rerun the calculation.
The following table illustrates terminology typically used with exception/error
handling in Kotlin.
Different types of error produce different types of exception. For example, if the
error is caused by trying to divide an integer by zero then the exception generated
might be an ArithmeticException.
The actual type of exception is represented by exception instances and can be
caught and processed by exception handlers. Each handler can deal with exceptions
associated with its class of error or exception (and its subclasses).
An exception is instantiated when it is thrown. The system searches back up the
execution stack (the set of functions or member functions that have been invoked
in reverse order) until it finds a handler which can deal with the exception. The
associated handler then processes the exception. This may involve performing some
remedial action or terminating the current execution in a controlled manner. In some
cases, it may be possible to restart executing the code.
As a handler can only deal with an exception of a specified class (or subclass), an
exception may pass through a number of handler blocks before it finds one that can
process it.
Handling an Exception
You can catch an exception by implementing the try catch construct. This
construct is broken into three parts:
• try block. The try block indicates the code which is to be monitored for the
exceptions listed in the catch expressions.
• catch clauses. You can use one or more optional catch clause to indicate what
to do when certain classes of exception/error occur (e.g. resolve the problem or
generate a warning message). There can be any number of catch clauses in
sequence checking for different types of error/exceptions.
• finally clause. The optional finally clause runs after the try block exits (whether
or not this is due to an exception being thrown). You can use it to clean up any
resources, close files, etc.
This language construct may at first seem confusing, however once you have
worked with it for a while you will find it less daunting.
As an example, consider the following function which divides a number by zero;
this will throw the ZeroDivisionError when it is run for any number:
If we now call this function, we will get the exception stack trace in the standard
output:
fun main() {
runcalc(5)
}
However, we can handle this by wrapping the call to runcalc within a try
block and providing a catch clause. The syntax for the try-catch construct is:
try {
<code to monitor>
} catch(<variabe-name>: <type of exception to monitor for>) {
<code to call if exception is found>
}
A concrete example of this is given below for a try block that will be used to
monitor a call to runcalc:
fun main() {
try {
runcalc(5)
} catch(exp: ArithmeticException) {
println("Opps")
}
}
which now results in the string ‘Oops’ being printed out instead of the exception
stack trace:
This is because when runcalc is called the ‘/’ operator throws a the
ArithmeticException which is passed back to the calling code which has
an catch clause specifying this type of exception. This catches the exception and
runs the associated code block which in this case prints out the string ‘Oops’.
If we want to log the error and allow the program to proceed we can use the
printStackTrace() member function, for example:
fun main() {
try {
runcalc(5)
} catch(exp: ArithmeticException) {
println("Opps")
exp.printStackTrace()
}
}
Handling an Exception 307
In fact, we don’t have to be as precise as this; the catch clause can be given the
class of exception to look for and it will match any exception that is of that exception
type or is an instance of a subclass of the exception. We therefore can also write:
fun main() {
try {
runcalc(5)
} catch(exp: Exception) {
println("Opps")
exp.printStackTrace()
}
}
If you don’t want to have a common block of code handling your exceptions, you can
define different behaviours for different types of exception. This is done by having
a series of catch clauses; each monitoring a different type of exception:
fun main() {
try {
runcalc(5)
} catch (exp: NullPointerException) {
println("NullPointerException")
} catch (exp: IllegalArgumentException) {
println("IllegalArgumentException")
} catch (exp: ArithmeticException) {
println("ArithmeticException")
} catch (e: Exception) {
println("Duh!")
}
}
In this case the first catch monitors for a NullPointerException but the
other catches monitor for other types of exception. Thus the second catch monitors for
IllegalArgumentException, the third for the ArithmeticException
etc.
Note that the catch (e: Exception) is the last catch clause in the list.
This is because NullPointerException, IllegalArgumentException
and ArithmeticException are all eventual subclasses of Exception and
308 20 Errors and Exception Handling
thus this clause would catch any of these types of exception. As only one catch block
is allowed to run; if this catch handler came fist the other catch handers would never
ever be run.
It is possible to gain access to the exception instance being caught by the catch clause
as it is available within the catch handler code block. You can name this parameter
whatever you like, however names such as e, exp, ex are commonly used. For
example:
fun main() {
try {
runcalc(5)
} catch(exp: Exception) {
println("Opps")
println(exp)
println(exp.message)
exp.printStackTrace()
}
}
Which produces:
One of the interesting features of exception handling in Kotlin is that when an Error or
an Exception is raised it is immediately thrown to the exception handlers (the catch
blocks). Any statements that follow the point at which the exception is raised are not
run. This means that a function or member function may be terminated early and
further statements in the calling code will not be run.
Handling an Exception 309
fun main() {
println("Starting")
try {
println("Before the call to divide")
val result = divide(6,2)
println("After the call to divide: $result")
} catch (exp: ArithmeticException) {
println("Opps")
}
println("Done")
}
Starting
Before the call to divide
entering divide(6, 2)
exiting divide 3
After the call to divide: 3
Done
In this example we have run every statement with the exception of the catch
clause as the ArithmeticException was not thrown.
If we now change the call to divide(Int, Int) such that we pass in 6 and
0 we will throw the ArithmeticException.
310 20 Errors and Exception Handling
fun main() {
println("Starting")
try {
println("Before the call to divide")
val result = divide(6,0)
println("After the call to divide: $result")
} catch (exp: ArithmeticException) {
println("Opps")
}
println("Done")
}
Starting
Before the call to divide
entering divide(6, 0)
Opps
Done
An optional finally clause can also be provided with the try statement. This
clause is the last clause in the construct and must come after any catch classes.
It is used for code that you want to run whether an exception occurred or not. For
example, see the following program:
Handling an Exception 311
fun main() {
println("Starting")
try {
println("Before the call to divide")
val result = divide(6,2)
println("After the call to divide: $result")
} catch (exp: ArithmeticException) {
println("Opps")
} finally {
println("Always runs")
}
println("Done")
}
The try block will run, if no error is thrown then the finally code will run,
we will therefore have as output:
Starting
Before the call to divide
entering divide(6, 2)
exiting divide 3
After the call to divide: 3
Always runs
Done
fun main() {
println("Starting")
try {
println("Before the call to divide")
val result = divide(6,0)
println("After the call to divide: $result")
} catch (exp: ArithmeticException) {
println("Opps")
} finally {
println("Always runs")
}
println("Done")
}
Starting
Before the call to divide
entering divide(6, 0)
Opps
Always runs
Done
Throwing an Exception
An error or exception is thrown using the keyword throw. The syntax of this is
For example:
fun functionBang() {
println("entering functionBang")
throw RuntimeException("Bang!")
println("exiting functionBang")
}
In the above function the second statement in the function body will create a new
instance of the RuntimeException class and then throw it allowing it to be
caught by any exception handlers that have been defined.
We can handle this exception by writing a try block with an catch clause for
the RuntimeException class. For example:
fun main() {
try {
functionBang()
} catch (exp: RuntimeException) {
println(exp.message)
}
}
entering functionBang
Bang!
Throwing an Exception 313
You can also re-throw an error or an exception; this can be useful if you merely
want to note that an error has occurred and then re-throw it so that it can be handled
further up in your application. To do this you use the throw keyword and the
parameter used to hold the exception for the catch block. For example:
fun main() {
try {
functionBang()
} catch (exp: RuntimeException) {
println(exp.message)
throw exp
}
}
entering functionBang
Bang!
Exception in thread "main" java.lang.RuntimeException: Bang!
at
com.jjh.exp.raise.RaisingAnExceptionKt.functionBang(RaisingAnExc
eption.kt:5)
at
com.jjh.exp.raise.RaisingAnExceptionKt.main(RaisingAnException.k
t:11)
at
com.jjh.exp.raise.RaisingAnExceptionKt.main(RaisingAnException.k
t)
In Kotlin most statements are actually expressions that return a value. The try-catch-
finally expression is no different. You can assign the value returned by the try{}
block (or the catch{} block if an error occurs) to a value.
For example, the following listing instantiates the class Rational and stores
that instance into the val result unless an exception is thrown.
If an exception is thrown then if the exception is of type RuntimeException
then a default Rational instance is returned by the catch block.
Thus the val result with either have an instance created from with in the try
block or an instance created within the catch block:
314 20 Errors and Exception Handling
init {
if (d == 0)
throw RuntimeException(
"Denominator cannot be Zero")
denominator = d
}
fun main() {
val result = try {
Rational(5, 0)
} catch (exp: RuntimeException) {
Rational(5, 1)
}
println(result)
}
Rational(numerator=5, denominator=1)
However, care should be taken when using the try-catch expression with a
finally block. The finally block is optional but when present runs after the
try block and if an exception occurs it also runs after any catch block has run.
For example:
The question here is what is the value of result? It will either be the value
returned from the try block if there is no exception or it will be the value returned
from the catch block.
Try Catch as an Expression 315
Notice that although the finally block is executed any value returned from
that block will be ignored. Thus the output generated for the above code is:
result1: 3
One common idiom or pattern used with try-catch expressions is that they
can represent the whole body of a function or member function. This ensures that the
try expression encapsulates all the functionality in the function block. For example:
You can define your own exception types, which can give you more control over what
happens in particular circumstances. To define an exception, you create a subclass
of the Exception class or one of its subclasses.
For example, to define a InvalidAgeException, we can extend the
Exception class and generate an appropriate message:
This class can be used to explicitly represent an issue when an age is set on a
Person which is not within the acceptable age range.
We can use this with the class Person that we defined earlier in the book; this
version of the Person class defined age as a property and attempted to validate
that an appropriate age was being set. We can modify this class so that if the value
being used for age is less than zero or greater than 120 then we will throw an
InvalidAgeException. For example:
316 20 Errors and Exception Handling
init {
age = _age
}
fun main() {
try {
val p1 = Person("Adam", -1)
println(p1)
} catch (exp: InvalidAgeException) {
println(exp.invalidAge)
println(exp.message)
println(exp)
}
}
When this code runs and we try and create an instance of the Person class
with an age of -1 the set() function for the age property will throw the
InvalidAgeException passing in information to help any exception handler
determine the issue with the data and what that data was.
When the above code runs we will see:
-1
Age must be between 0 and 120
com.jjh.exp.people.InvalidAgeException: Age must be between 0
and 120
Chaining Exceptions 317
Chaining Exceptions
A feature that can be useful when creating your own exceptions is to chain them
to a generic underlying exception. This can be useful when a generic exception is
thrown, for example, by some library or by the Kotlin system itself, and you want to
convert it into a more meaningful application exception.
For example, let us say that we want to create an exception to represent a specific
issue with the parameters passed to a function divide(), but we don’t want
to use the generic ArithmeticException, instead we want to use our own
DivideByYWhenZeroException. This new exception could be defined as:
class DivideByYWhenZeroException(
message: String = "",
cause: Throwable? = null): Exception(message, cause)
fun main() {
divide(6, 0)
}
It is also possible to nest one try catch expression or statement inside another.
As a handler can only deal with an exception of a specified class (or subclass), an
exception may pass through a number of handler blocks before it finds one that can
process it.
fun main() {
try {
try {
try {
println("In here")
val result = 5 / 0
} catch (exp: NullPointerException) {
println("Its an NullPointerException")
}
} catch (ire: IllegalArgumentException) {
println("Its an IllegalArgumentException")
}
} catch (ae: ArithmeticException) {
println("Its an ArithmeticException")
ae.printStackTrace()
}
}
Limitations of Try-Catch
For those of you who are familiar with exception handling in other languages such
as Java, C# etc. you will see that Kotlin’s try-catch-finally construct is very similar.
However, it is not without its drawbacks. The need to work with some resources
(such as database connections or connections to a file) in multiple places can result
in some awkward programming solutions. In general there are three key problems
with this approach:
• The syntax results in scoping issues leading to unnecessarily complicated
constructs as well as the need to nest try-catch block within either the try,
catch or finally part of the top level construct! For example, to open a network
connection you might write something like:
320 20 Errors and Exception Handling
• It forces developers to use vars for local variables as in the above code connection
must be accessible in at least two different code blocks. This goes against the
general approach of using vals in Kotlin programs.
• Multi-threaded code can be difficult to deal with using the
try-catch-finally construct. For example, how should you react to
an exception which occurs in a separate thread but which impacts the data being
accessed? In fact the try-catch-finally construct primarily assumes that
the exception is handled in the current execution thread (that is the exception is
completely handled within the thread in which it occurred). This is not ideal for
a concurrent program and seems at odds with the coroutine model implemented
by Kotlin (see later in this book for chapters on Kotlin coroutines).
Kotlin 1.3 added a new type of run scope function called runCatching{}. This
scope function supports functional style exception handling.
It also introduce the kotlin.Result type which encapsulates whether the
runCatching{} block was successful or not.
Using runCatching{}
To use the runCatching{} scope function all you have to do is embed the code
to be monitored within the body of the runCatching block, for example:
Functional Exception Handling 321
runCatching {
"32".toInt()
}
This scope function will attempt to convert the String “32” to an Integer. The
scope function runCatcing{} will return a result representing success or failure
of the block of code.
Thta is, runCatching{} allows the behaviour defined within the scope block
to execute. It then generates a result which is a discriminated union that encapsulates:
• a successful outcome representing a value generated by the runCatching{}
block or
• a failure which holds a reference to the Throwable instance (the exception) that
caused the code to fail.
It is possible to check the result instance to identify whether it represents a success
or failure using the isSuccess and isFailure properties.
For example:
fun main() {
This example attempts to convert a String to an integer for the val result1 and
the val result2. The String used for result1 can be converted to an Int (the
integer 32) but the String used for result2 cannot be converted into an integer
(as it contains the character ‘a’). Thus the first expression should succeed, but the
second should fail. The output of this program is:
Success(32)
true
Failure(java.lang.NumberFormatException: For input string:
"32a")
true
322 20 Errors and Exception Handling
As you can see from this the value held in r1 is a Success type but the value held
in r2 is a Failure type. The Failure also contains information about the problem that
occurred and the value associated with it.
To access the exception associated with the Failure you can use the
exceptionOrNull() member function, for example:
To obtain the value held in the Result if it is successful there are a set of getter
style member functions available:
• getOrDefault(<defaultValue>) Returns the encapsulated value if this
instance represents success or the defaultValue if it is failure.
• GetOrElse{<onFailure function>} Returns the encapsulated value if
this instance represents success or the result of the onFailure function for the
encapsulated Throwable exception if it is failure.
• getOrNull() Returns the value generated or null.
• getOrThrow() Returns the encapsulated value if this instance represents
success or re-throws the encapsulated Throwable exception if it is failure.
Examples of each of these getter member functions are given below:
Functional Exception Handling 323
result0 32
java.lang.NumberFormatException: For input string: "32a"
result1 0
result2 null
Exception in thread "main" java.lang.NumberFormatException: For
input string: "32a"
at
java.base/java.lang.NumberFormatException.forInputString(NumberF
ormatException.java:68)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at exp.func.FunctionalStyleExceptionHandlingAppKt.main
runCatching { "32".toInt() }
.map { println(it) }
runCatching { "32a".toInt() }
.map { println(it) }
In this case the map function is being applied to the result instance. The
result either contains a value if it is a success or it contains a Throwable
(which is not considered to be a value returned by the block). As such map will be
applied to a successful result but will have no effect of there if an exception thrown.
The result generated by this code snippet is thus just:
32
This comes for the first runCatching{} block. The second runCatching{}
block generates an exception and there is therefore no successful result to apply map
to. This approach is fine if you are not interested in the exception and just want to do
something if everything goes ok. However in many situation s you want to at least
log the fact that an exception happened and may want to take some remedial action.
Recovery Operations
The recover{} operation can be used to handle an exception situation and recover
from it. It can therefore be combined with map to provide some exception handling
behaviour followed by the map operation. This is an approach favoured by many in
the functional programming community.
For example:
runCatching { "32a".toInt() }
.recover {
it.printStackTrace()
-1
}
.map { println(it) }
In this case we are mimicking the try-catch behaviour where the catch behaviour
provides an alternative value to use. Thus in this case, if the String to be converted
to an Int cannot be represented as an Int we will default to the value -1 however
we are also logging the exception so that it does not fail silently.
Functional Exception Handling 325
In many situations developers want to one thing if the code works successful but do
a different thing if it fails. This may be because there is no obvious recovery step.
For example, if an error occurs while trying to connect to a server then there may
not be a way to recover from that if the server is unavailable.
To support this scenario kotlin.Result provide the onFailure{} and
onSuccess{} functions. These higher order functions take a function that is
run depending on whether the kotlin.Result represents a success or failure
scenario. These functions can be chained together for the result instance.
The order in which the functions are chained together is not significant and as such
they can be chained to getter using result.onSuccess{}.onFailure{} or
result.OnFailure{}.onSuccess{}.
Within both function blocks the variable it is used to represent the informa-
tion supplied. For the onFailure{} block it represents the exception that was
thrown. In the onSuccess{} block it represents the result generated by the
runCatching{} block.
An example of using onSuccess and onFailure is given below:
326 20 Errors and Exception Handling
runCatching {
"32a".toInt()
}.onFailure {
when (it) {
is NumberFormatException -> {
print("Oops - number wasn't formatted correctly: ")
println(it.message)
}
is Exception -> {
println("some other exception")
it.printStackTrace()
}
else -> throw it
}
}.onSuccess {
println("All went well")
println(it)
}
The onSuccess block will print out a message and the value generated.
The onFailure block will use a when expression to check the type
of the exception. Depending upon the exception type, different message
are printed out. If the issue is not a NumberFormatException or any
type of Exception (it must then be an Error) it is thrown out of the
runCatching{}.onFailure{].onSuccess{} chain.
The output from this code is:
Online Resources
Exercises
try {
acc1.deposit(-1.0)
} catch (exp: AmountException) {
exp.printStackTrace()
}
This should result in the exception stack trace being printed out, for example:
Next modify the class such that if an attempt is made to withdraw money which
will take the balance below the over draft limit threshold a BalanceException
is thrown. This is again an application specific or custom exception that you can
define.
The BalanceException exception should hold information on the account
that generated the error.
This means that you can refactor the withdraw() member function in the
CurrentAccount so that it does not print a message out to the user and it does
not return a default zero transaction. Instead it throws the BalanceException
which is a more explicit representation of the situation. The BalanceException
can take the same message string as was previously printed out, for example:
if (amount < 0) {
throw AmountException("Cannot deposit negative amounts")
}
328 20 Errors and Exception Handling
Write code that will use try and catch blocks to catch the exception you have
defined.
You should be able to add the following to your test application:
try {
println("balance: ${acc1.balance}")
acc1.withdraw(300.00)
print("balance: ${acc1.balance}")
} catch (exp: BalanceException) {
println("Handling Exception")
println("Problem occurred on account: ${exp.acc}")
}
Handling Exception
Problem occurred on account: CurrentAccount('123',
'Customer(name=John, address=10 High Street,
email=john@gmail.com)', 21.17) - overdraft -100.0
Chapter 21
Extension Functions and Properties
Introduction
In this chapter we will look at both Extension Functions and Extension Properties as
well as Infix Extension Operators.
Such extensions are extensions to existing types either to provide additional func-
tionality, to meet some library or framework requirements or just to make the type
they are applied to easy to use in Kotlin.
Extensions
Kotlin provides the ability to extend a type with new behaviour without having to
modify that type or inherit from the type or to wrap that type etc. This is done via
special declarations called extensions.
This means that in Kotlin a type such as a class can have its behaviour and data
extended even when:
• The type is closed or final and thus cannot normally be extended.
• When you do not have access to the source code and thus cannot change the type.
• The additional functionality or data is only required in one or a small number of
situations and you do not wish to pollute the interface for the type in the general
case.
• You want to extend the behaviour or data of a built-in type without modifying that
type.
Extensions can provide these additional features (sometimes also known as the
pimp my type design pattern).
There are three types of extension:
• extension functions that add functionality,
• extension properties that add data,
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 329
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_21
330 21 Extension Functions and Properties
Extension Functions
For example, to add additional behaviour to the built-in closed/final class String
we can write:
fun main() {
val s = "John"
println("s.hasLength(4): ${s.hasLength(4)}")
println("-".mult(25))
}
s.hasLength(4): true
--------------------------
Extension Functions 331
It therefore appears that the String class supports this behaviour. Of course if
you look at the documentation for the class String you will not find these member
functions listed. Indeed if you do not define these extension functions in your own
code you will find that the above main() function will not even compile.
This illustrates the way in which extension functions add behaviour to an existing
type.
Extension Properties
For example, to add additional properties to the built in closed/final class String
we can write:
This will define a new property size on the class String. The read-only (val)
property size has a get() function that uses the length property already defined
on the class String to generate the size of the String.
We can therefore now write:
fun main() {
val s = "John"
println("s.size: ${s.size}")
}
s.size: 4
332 21 Extension Functions and Properties
We can define a read–write extension property that uses the existing var name
property to get and set the property tag:
var Person.tag
get() = name
set(value) { name = value }
We can now use this new read–write tag property in our applications as an alias
for the name property:
fun main() {
p.tag = "Bob"
println("p: $p")
John
p: Person(Bob, 21)
It is also possible to add named infix extension operators. These are infix operators
that are defined outside the scope of a given type using the syntax:
Infix Extension Operators 333
For example, to add a new named infix operator to the closed class String we
could write:
This adds a new operator ‘m’ to the class String (which represents the ability
to multiple the string a specific number of times).
We can now use this in our own applications as shown below:
fun main() {
Not that this illustrates that the infix operator extension function can be called
using traditional member function syntax or using operator syntax.
The output from the above program is:
--------------------------
--------------------------
Extensions Scope
To define an extension function for the class String which is limited to the class
MyClass you can define new member level functions that follow the synt:ax.
class <classname> {
fun <TypeToExtend>.<function-name>() { … function body … }
}
For example:
class MyClass {
// Extension function for a String
// But only accessible from within class
fun String.rev(): String {
return this.reversed()
}
fun printMe(s: String) {
println(s.rev())
}
}
The extension function rev for the class String is only accessible from within
the class MyClass. Thus the member function printMe() can access rev() on
a String but code outside of the class cannot.
We can do a similar thing with objects, for example:
object MyObject {
fun String.rev(): String {
return this.reversed()
}
fun printMe(s: String) {
println(s.rev())
}
}
Online Resources
More information on Kotlin extensions and the pimp my type design pattern see:
• https://kotlinlang.org/docs/reference/extensions.html Introduces Kotlin Exten-
sions.
• http://blog.rcard.in/jvm/programming/design-pattern/2019/12/15/pimp-my-lib
rary-pattern.html Pimp My Type Design Pattern.
Exercises 335
Exercises
acc1.prettyPrint()
class fintech.accounts.CurrentAccount
Customer(name=John, address=10 High Street,
email=john@gmail.com)
123
26.720000000000002
Part III
Data Containers
Chapter 22
Arrays
Introduction
Earlier in this book we looked at some Kotlin built-in types such as String, Int and
Double as well as Boolean. These are not the only built-in types in Kotlin; another
group of built-in types are collectively known as container types. This is because
they contain other types (such as a collection of Strings or Ints).
There are in effect two categories of container types Arrays and Collections.
Arrays are always mutable but of fixed size where as collections may be mutable or
immutable, that may grow in size (and may have different characteristics depending
upon the type of collection being used). This chapter introduces Arrays and the next
chapter introduces Collections.
Kotlin Arrays
Arrays are a common way in which multiple values can be held together in one place.
Arrays in Kotlin have several features:
• Arrays are another part of the type system in Kotlin. Thus Array <String>
specifies something of type String array, that is an array that can hold Strings.
• Arrays in Kotlin are instance of the class Array.
• Arrays are mutable that is the values they hold can be changed.
• Once created the size of an Array is fixed, it cannot grow.
• Arrays know their size.
• Arrays have a range of common behaviours defined for them that allow developers
to obtain elements at the start or end of an array, a subset of an array known as a
slice etc.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 339
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_22
340 22 Arrays
• Arrays hold elements of data in an order specified by an index. Like many other
languages the elements of an array are indexed from zero, which means that an
array with 10 elements is indexed from 0 to 9.
• Index access is supported using the [index] notation.
Creating Arrays
There are several ways in which a new array can be created. Each of these are
discussed din this section.
Arrays can be created via the class Array using an initial size and a function that
will be used to initialise the values held by the array.
This is the long hand form which is very flexible but not commonly used for basic
arrays:
In fact there are specialised array classes for all of the primitive types in Kotlin.
The specialised array types should be used in preference to the generic Array
class as they are more efficient as they avoid a JVM concept known as boxing (which
has overheads in terms of memory and performance).
The available specialised array types are ByteArray, ShortArray,
IntArray, LongArray, DoubleArray, FloatArray and BooleanArray.
Creating Arrays 341
Note that as this is an array of null values, it is necessary to indicate the type of
the array created. In the above example an array of Strings is created.
For the primitive type arrays there are specialised version of the
arrayOf() factory function. These specialised functions are used
to create arrays of the specified primitive type. The functions are
intArrayOf(), byteArrayOf(), shortArrayOf(), longArrayof(),
floatArrayOf(), doubleArrayOf() and booleanArrayOf().
As an example here is the intArrayOf() factory function being used to create
an array of 4 elements with the initial values 2, 4, 6 and 8:
An empty array can be created using emptyArray() or via the factory func-
tion arrayOf(). Here are some examples of using the emptyArray() factory
function to create a set of empty arrays:
An empty array is an instance of the class Array which has a length zero. This can
be useful for testing purposes but also in situations where it is necessary to provide
an array instance but there is no data to populate that array.
Note that in the above example the type of the emptyArray() is indicated
by the type of the vals for anything, names and data. Thus Kotlin is inferring
the appropriate type of empty array to create. However, for someStrings and
friends the type of the empty array to create is specified when invoking the
emptyArray function. This is referred to as a generic function where the specific
type to be used is included between angle brackets following the function name but
before the parentheses.
It is also possible to create an array that can hold different types of things, for example:
In this case things will hold an array of any Comparable type of thing (i.e.
Array <Comparable>) this is because Comparable is the lowest common
denominator between the String "John", the Int 2 and the Boolean true.
A Comparable is something that can be compared. Comparable is an inter-
face which is implemented by a wide range of types in Kotlin including Strings, Ints
and Booleans.
Working with Arrays 343
We can create an array of Strings either by using the Array class directly and
providing a function to initialise the values of the array or using the arrayOfNulls
factory function, for example:
This creates an array of four elements containing the strings “John”, “Denise”,
“Phoebe” and “Adam”. We can change any of these fields by specifying the
appropriate index and replacing the existing value with a new string:
names2[2] = "Jasmine"
The above statement replaces the string “Phoebe” with the string “Jasmine”.
Remember arrays are indexed from Zero thus the first position has index 0, the
second has index 1 and so on.
Merely being able to put values into an array would be of little use; we can also
access the array locations in a similar manner:
As arrays are instances we can also obtain information from them. For example,
to find out how many elements are in the array we can use the property size:
println("anything.size: ${anything.size}")
We can also work with primitive type arrays in a similar manner to the String array
example in the previous section. However, we now use the appropriate specialised
array class such as IntArray or DoubleArray. Such an array also has a property
size and values can be set using index access and accessed in the same8 way. For
example:
moduleMarks[0] = 26
moduleMarks[1] = 15
moduleMarks[2] = 56
moduleMarks[3] = 72
moduleMarks[4] = 34
println("moduleMarks[0]: ${moduleMarks[0]}")
moduleMarks: [I@1218025c
moduleMarks.size: 5
moduleMarks[0]: 26
Note the slightly strange way in which an array is printed (the ‘[‘ indicates that
it is an array and the following I indicates that it is an Int array.
It is also possible to create arrays of user defined types such as the class Person.
This is done in exactly the same way as creating an array of Strings, for example:
Working with Arrays 345
The for statement can be used with any iterable type of thing. Earlier in this book we
iterated over a range of values for example from 0until 10 etc. In Kotlin arrays
are iterable so we can iterate over all the values in an array, using the in operator
for example:
item: 26
item: 15
item: 56
item: 72
item: 34
It is also possible to use the indices for each element in the array. This is done using
the indices property of an array and the in operator. The indices property
returns a range which is from 0 to the last index in the array. Thus if you have an
346 22 Arrays
array of 10 elements then the indices will return a range from 0 to 9. This is illustrated
below:
index: 0 - value 26
index: 1 - value 15
index: 2 - value 56
index: 3 - value 72
index: 4 - value 34
Multi-Dimensional Arrays
As in most high level languages multi dimensional arrays can be defined in Kotlin.
This can be done in several ways, but the following illustrates the simplest approach
for an array of Int:
Note that the type of the val seats is actually Array <Array<Int>>. It is
therefore useful to consider what this actually means. It states that the val seats
can hold references to an Array of Arrays of Ints.
Thus if you did this long hand for a val and explicitly defined the type for the val,
you would:
1. Define the variable family as hold a reference to an array of arrays
family = arrayOf (
arrayOf("John", "Denise", "Phoebe", "Adam"),
arrayOf("Paul", "Fi", "Andrew", "James")
)
This code block accesses the first element of the family Array of Arrays which
returns the first Array of Strings. We can then iterate over these values in turn. The
output of this code is thus:
John
Denise
Phoebe
Adam
We can also change an individual element of the sub array using the double index
operations [][], for example:
family[0][2] = "Jasmine"
for (name in family[0]) {
println(name)
}
This code block accesses the first array element (index 0) and then in that array
accesses the third element (index 2) and updates it. The cod then loops through the
first array element and prints out each value, the result is:
John
Denise
Jasmine
Adam
348 22 Arrays
As you can see from this last example, multi-dimensional arrays are accessed in
exactly the same way as single dimensional arrays with one indices following another
(note each is within its own set of square brackets—[]).
Ragged Arrays
You can always find out the size of an array (or an array of arrays) using the size
property, thus you can find out that the outer array has 2 elements, the first inner
array has 4 elements and the second inner array has 5 elements, for example:
println("family.length: ${family.size}")
println("family(0).length: ${family[0].size}")
println("family(1).length: ${family[1].size}")
Of course the way that multi-dimensional arrays are implemented in Kotlin means
that you can easily implement any number of dimensions as required.
Array Operations/Functions
In addition to the size property there are a whole range of member functions defined
for Arrays that can be used to find the first element in an array or the last, make a
copy of an array, obtain a slice (sub section) of an array or convert an array into a
list. None of these member functions modify the original array. However you can
also reverse the array which changes the underlying array itself.
These features are illustrated below:
Array Operations/Functions 349
fun main() {
val names = arrayOf("John", "Denise", "Adam", "Phoebe")
println(names)
println("size(): ${names.size}")
println("first(): ${names.first()}")
println("last(): ${names.last()}")
println("copyOf(): ${names.copyOf()}")
println("sliceArray(2..3): ${names.sliceArray(2..3)}")
names.reverse()
println("reversed array: $names")
for (name in names) { print("$name, ") }
println("\nasList(): ${names.asList()}")
}
[Ljava.lang.String;@1c20c684
size(): 4
first(): John
last(): Phoebe
copyOf(): [Ljava.lang.String;@448139f0
sliceArray(2..3): [Ljava.lang.String;@6f496d9f
reversed array: [Ljava.lang.String;@1c20c684
Phoebe, Adam, Denise, John,
asList(): [Phoebe, Adam, Denise, John]
Chapter 23
Collections
Introduction
Collection classes are often used as the basis for more complex or application specific
data structures and data types.
The collection types support various types of data structures (such as lists and
maps) and ways to process elements within those structures. This chapter introduces
the collection types in Kotlin.
Collections Library
The Collections library is one of the main categories within the set of Kotlin libraries
that you will work with. It provides types (Interfaces, Objects and Classes) that
support various data structures (such as Lists, Sets and Maps) and ways to process
elements within those structures. A collection is a single instance representing a
group of instances (or objects). That is they are a collection of other things.
The Kotlin collections framework is defined within the kotlin.collection
package. This package provides a collections framework for holding references to
objects, instances and values. The Collection types also define a group of higher
order functions, such as the foreach function that can be used to apply an operation
to each of the elements held in a collection (this is discussed in the next chapter).
The Kotlin collection library is split into Mutable and Immutable concepts. The
result is that Lists, Sets, Maps etc. can be mutable or immutable. Thus all the mutable
collections can be updated, added to, removed from etc. In practice this means that
you can change the contents of the mutable collection after it has been created.
All immutable collections however, once created cannot change their content. Thus
when you create an immutable list then the elements that comprised that list cannot
be removed, added to etc.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 351
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_23
352 23 Collections
The concepts of mutability and immutability (and their meaning for different
collection types) are represented via a hierarchy of interfaces. The core elements of
this hierarchy are presented below:
In this diagram all the immutable interfaces capture the core essence of that type
of Collection. These are represented in blue in the diagram. Thus all collections
are Iterable as represented by the Iterable interface. This concept is extended
by the concept of being a Collection as represented by the interface Collection
extending the interface Iterable. Within this Lists are immutable types of collec-
tions that are indexed and have an order. In contrast Sets are immutable collections
that are not indexed, do not have an order and do not allow duplicates. Maps are
separate as they represent an associated between a set of Key to Value pairs.
Additionally, the diagram illustrates that each of the core interface concepts
has an associated mutable version. Thus the List interface is extended by the
MutableList interface, in turn the Set interface is extended by the MutableSet
and the Map interface is extend day the MutableMap interface. In addition there
are also MutableIterable and MutableCollection interfaces.
Various implementations of each interface are provided that capture what interface
is implanted and in what way for example the ArrayList class implements the
List concept using a backing array where as the HashSet class implements the Set
interface using a hashing member function.
In this chapter and the next few chapters we will look at the following core collection
clas
• Lists Lists hold a collection of objects that are ordered and mutable (changeable),
they are indexed and allow duplicate members.
Kotlin Collection Types 353
• Sets Sets are a collection that is unordered and unindexed. They are mutable
(changeable) but do not allow duplicate values to be held.
• Map A Map is an unordered collection that is indexed by a key which references
a value. The value is returned when the key is provided. No duplicate keys are
allowed. Duplicate values are allowed. Dictionaries are mutable containers.
• Pairs Pairs represent a collection of exactly two values. They are ordered and
immutable (cannot be modified), allow duplicate members and are indexed.
• Triples Triples represent a collection of exactly three values. They are ordered
and immutable (cannot be modified), allow duplicate members and are indexed.
Like arrays collections are generic containers and thus can hold any type of
instance of object within your Kotlin runtime environment. This includes system
provided and user defined types.
Chapter 24
Lists
Introduction
Lists
In this case we have created a list of four elements with the first element being
indexed from zero, we thus have:
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 355
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_24
356 24 Lists
List Creation
Lists can be created by instantiating the appropriate class such as ArrayList. For
example:
Note that the ArrayList class implements both the List and the
MutableList interface. It is therefore your choice whether you view the
ArrayList as being mutable or not. If it is referenced through the variable list0
then it will appear immutable, however it is accessed via the val list1 it will reaper
mutable.
The example illustrated in the previous sub section is not idiomatic Kotlin, that is it
is not the recommended way of creating lists in Kotlin. Instead you should use either
the listOf() or mutableListOf() factory functions. These are the List
equivalent of the arrayOf() factory function.
Using these factory functions you choose whether you want to create a mutable or
immutable list, although note the default is an immutable list and you must explicitly
choose the mutableListOf() function to create a mutable version:
The type can be inferred for all lists that provide a default set of values. However
an empty list has no values and thus Kotlin cannot infer the appropriate type to use
in which case you must specify the type using the generic <type> syntax.
Unlike Arrays the default toString() member function provided for a list will
print the contents of the list, for example:
println("myList1: $myList1")
println("myList2: $myList2")
println("myList3: $myList2")
println("myList4: $myList4")
List Creation 357
Will generate the following output:
Nested Lists
Lists can be nested within Lists; that is a List can contain, as one of its elements,
another List. For example, the following diagram illustrates the nesting of a tree of
Lists:
We can thus create the following structure of nested Lists:
fun main() {
val list1 = listOf(1, 43.5, Person("Phoebe"), true)
val list2 = listOf("apple", "orange", 31)
val rootList = listOf("John", list1, list2, "Denise")
println(rootList)
}
Like Arrays lists have a property size that will tell you how many elements they
contain. In addition they have a property lastIndex which tells you the index of
the last element in the list. As lists are indexed from zero (just as Arrays are) then
the size of a list might be 3 but the last index would be 2. For example:
fun main() {
val list = listOf("John", "Paul", "Bill")
println("list.size: ${list.size}")
println("list.lastIndex: ${list.lastIndex}")
}
list.size: 3
list.lastIndex: 2
You can access elements from a list using an index (within square brackets []). The
index returns the element at that position, for example:
fun main() {
val list = listOf("John", "Paul", "George", "Ringo")
println(list[1])
}
This will print out the element at index 1 which is Paul (lists are indexed from
Zero so the first element is the zeroth element).
Note that the index access operation [] is actually implemented by the underlying
operation member function get() on the List interface. You could therefore write:
Indexed access can also be used to update the value at an index within a mutable list.
However such a list must be a mutable list. It is therefore necessary to create it using
the mutableListOf() factory function. For example:
In actual fact the index update operator maps to the set() member function on
the List interface, you could therefore also write:
Rather than use an index update. However, this is not considered idiomatic Kotlin
and so should not generally be used.
Which produces:
Adding to a List
You can add an item to a list using the add() member function of the
MutableList interface. This changes the actual list; it does not create a copy
of the list and thus the list must be mutable. The syntax of this member function is:
<list>.add(<value>)
fun main() {
val list1= mutableListOf("John", "Paul", "George", "Ringo")
list1.add("Pete")
println(list1)
}
You can also add all the items in a list to another list using the addAll() member
function:
fun main() {
val list1= mutableListOf("John", "Paul", "George", "Ringo")
list1.add("Pete")
println(list1)
list1.addAll(listOf("Albert", "Bob"))
println(list1)
}
Note that strictly speaking addAll() takes a collection and thus a set or a list
could be added to a list.
Accessing List Data 361
You can also insert elements into an existing mutable list. This is done using the
add() member function of the List interface. The syntax of this member function
is:
<list>.add(<index>, <value>)
This version of the add() member function takes an index indicating where to
insert the value and the value to be inserted.
For example, we can insert the string “Paloma” in between the Zeroth and oneth
item in the following list of names:
println("Inserting an element")
val aList = mutableListOf("Adele", "Madonna", "Cher")
println(aList)
aList.add(1, "Paloma")
println(aList)
Inserting an element
[Adele, Madonna, Cher]
[Adele, Paloma, Madonna, Cher]
In other words, we have inserted the string “Paloma” into the index position 1
pushing “Madonna” and “Cher” up one within the List.
List Concatenation
We can remove an element from a mutable list using the remove() and
removeAt(Int) member functions. The syntax for these member function is:
<list>.remove(<object>)
<List>.removeAt(index)
This will remove the element from the list; if the element is not in the list then an
error will be generated by Kotlin.
You can iterate over the contents of a List (that is process each element in the list
in turn). This is done using the for loop in which the iteration is performed over the
List:
fun main() {
val myList1 = listOf<String>("One", "Two", "Three")
println("Iterating over a list")
for (item in myList1) {
println(item)
}
}
This prints out each of the elements in the the list in turn:
fun main() {
println("Accessing index and value in a list")
for (index in myList1.indices) {
println("index: $index - value ${myList1[index]}")
}
}
Kotlin has a set of built-in member functions that you can use on lists.
fun main() {
val list = listOf("One", "Two", "Three")
println("list.first(): ${list.first()}")
println("list.last(): ${list.last()}")
println("list.takeLast(2): ${list.takeLast(2)}")
println("list.isEmpty(): ${list.isEmpty()}")
println("list.isNotEmpty(): ${list.isNotEmpty()}")
println("list.asReversed(): ${list.asReversed()}")
}
list.first(): One
list.last(): Three
list.takeLast(2): [Two, Three]
list.isEmpty(): false
list.isNotEmpty(): true
list.asReversed(): [Three, Two, One]
Online Resources
• https://kotlinlang.org/docs/reference/constructing-collections.html constructing
collections.
• https://kotlinlang.org/docs/reference/list-operations.html List specific operations.
Exercises
Depending upon the exact set of transactions you have performed (deposits and
withdrawals) you should get a list of those transactions being printed out
Introduction
Sets
A Set is an unordered (un indexed) collection of immutable objects that does not
allow duplicates. Sets have the following characteristics:
• Sets are unordered.
• Sets are not indexed.
• By default sets are immutable.
• Sets do not allow duplicates.
• Sets do not maintain the order in which items are added or inserted.
• Sets are iterable.
• MutableSets are growable, that is as you add elements to a set it can grow.
Creating Sets
An immutable set can be created using a named class such as HashSet or by using
a factory function such as the setOf() and mutableSetOf() factory functions.
Some examples are given below:
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 367
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_25
368 25 Sets
fun main() {
val set1: Set<String> = HashSet()
val set2: MutableSet<String> = HashSet()
// More idiomatic Kotlin
val set3 = setOf<String>("One", "Two", "Three")
val set4 = setOf("A", "B", "C")
val set5 = mutableSetOf("Chasing Pavements",
"Rumour Has it",
"Turning Tables")
println(set1)
println(set2)
println(set3)
println(set4)
println(set5)
}
The first two statements create new sets using the HashSet class directly. The
HashSet class implements both the Set and the MutableSet interfaces and thus
can be used as either type of set.
In this example, set1 is declared to hold reference to an immutable Set and
thus set1 cannot be modified. In turn set2 holds a reference to a MutableSet
and thus this set can be modified. In both sets the values can be Strings.
Both set3 and set4 hold references to immutable sets of Strings and use the
more idiomatic Kotlin style of creating a set.
Finally set5 holds a reference to a mutable set of Strings. When this program
runs the output generated is:
[]
[]
[One, Two, Three]
[A, B, C]
[Chasing Pavements, Rumour Has it, Turning Tables]
Like both Arrays and Lists, a Set knows its own size and can indicate whether it is
empty or not:
fun main() {
val set5 = mutableSetOf("Chasing Pavements",
"Rumour Has it",
"Turning Tables")
println(set5.size)
println(set5.isEmpty())
println(set5.isNotEmpty())
}
Set Properties and Member Functions 369
This produces:
3
false
true
Unlike Lists it is not possible to access elements from a Set via an index; this is
because they are unordered containers and thus there are no stable indexes available.
However, they are iterable containers.
Elements of a Set can be iterated over using the for statement:
This applies the println() function to each item in the set in turn.
You can check for the presence of an element in a set using the contains()
member function. This returns true or false depending on whether the value is
contained in the set:
This will print true if “Rumour Has it” is a member of the set.
fun main() {
val set5 = mutableSetOf("Chasing Pavements",
"Rumour Has it",
"Turning Tables")
// Can only add a value once
println(set5)
set5.add("Skyfall")
println(set5)
set5.add("Skyfall")
println(set5)
}
This generates:
As you can see from this “Skyfall” has only been added once.
If you want to add more than one item to a Set you can use the addAll()
member function:
Generating:
Removing an Item
To remove an item from a mutable set, use the remove() member function. The
remove() function removes a single item from a Set.
Working with Sets 371
fun main() {
val set5 = mutableSetOf("Chasing Pavements",
"Rumour Has it",
"Turning Tables")
set5.remove("Rumour Has it")
println(set5)
}
This generates:
Nesting Sets
We can of course also nest Lists in Sets and Sets in List. For example, the following
structure shows Sets (the ovals) hold references to Lists (the rectangles) and vice
versa:
fun main() {
val set1 = setOf(1, "John", 34.5)
val list1 = listOf("Smith", "Jones")
val list2 = listOf(set1, list1)
val set2 = setOf(list2, "apple")
println(set2)
}
which produces:
372 25 Sets
You can also perform mathematical set like operations with Sets in Kotlin.
To merge two collections into one, use the union() function. It can also be used
in the infix form, for example a union b.
To find an intersection between two collections (elements present in both sets),
use the intersect() function.
To find collection elements not present in another collection, use the
subtract() function.
Both the intersect() and subtract() functions can be called in the infix
form as well, for example, a intersect b.
Online Resources
Exercises
// Set up sets
val exam = setOf("Andrew", "Kirsty", "Beth", "Emily", "Sue")
val project = setOf("Kirsty", "Emily", "Ian", "Stuart")
• List all students who took either (or both) of the exam and the project.
The output from the associated project might look like:
Introduction
Maps
A Map is a set of associations between a key and a value that is unordered, changeable
(mutable) and indexed. Pictorially we might view a Map as shown below for a set of
countries and their capital cities. Note that in a May the keys must be unique but the
values do not need to be unique.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 375
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_26
376 26 Maps
Creating a Map
Maps can be created using the mapOf() and mutableMapOf() factory functions.
Each Map has the type used to represent the key and the type used to represent the
value in the Map.
These Map factory function are illustrated below:
When defining values to be used to initialise the Map, the to operator is used to
associate keys with values. Thus the expression:
is used to create an association for the key Ireland to the value Dublin. Note
that values can be any type within Kotlin and thus a value could be a List, a Set or
indeed another Map.
The first two statements in the above program create immutable Maps which map
an Int to a String. The third statement creates a mutable map that holds a set of
key:value pairs for the Capital cities of different countries.
When this code is run we see:
Map Properties 377
Map Properties
Maps have a set of characteristics such as their size, whether they are empty, their
keys and the values they hold. These can be accessed using three properties (size,
keys and values) and a member function isEmpty():
You can access the values held in a Map using their associated key. This is specified
using either the square bracket (‘[]’) notation (where the key is within the brackets)
or the get() member function. The get() is an operator member function that is
invoked when you use the more idiomatic Kotlin index [] operator. Both approaches
either return the value associated with the key or null:
378 26 Maps
If you do not want the value null to be returned if the key is not present then you
can use the getOrDefault() member function instead, for example:
A new entry can be added to a MutableMap using the idiomatic Kotlin approach
via the index accessor and the new value to be assigned to that key. Alternatively
you can use the operator member function put(kay, value) which is indirectly
invoked when you use the index approach. For example:
Working with Maps 379
Which produces:
The value associated with a key can be changed in a mutable map by reassigning a
new value using the square bracket notation, for example:
Removing an Entry
An entry into the MutableMap can be removed using the remove() member
function. This function removes the entry with the specified key from the map.
The following example removes the value for the key “Germany”:
There are two properties that allow you to obtain a view onto the contents of a Map,
these are values and keys properties.
• The values property returns a mutable collection of the map’s values.
• The keys property returns a mutable set of the onto a map’s keys.
The following code uses the cities dictionaries with these two member
functions:
You can loop through a Map using the for loop statement and the Maps keys
property. The for loop processes each of the keys in the Map in turn. This can be
used to access each of the values associated with the keys, for example:
If you want to iterate over all the values directly, you can do so using the Maps’
values property. This returns a collection of all the values, which of course you
can then iterate over:
Changing a Keys Value 381
This generates:
A final option is to iterate over the key-value entires in the Map directly. This can be
done by applying the for loop to directly to the Map rather than to either the keys
or the values:
This produces:
You can check to see if a key is a member of a map using the contains() member
function:
382 26 Maps
Nesting Dictionaries
The key and value in a map must be an valid Kotlin type; thus anything can be used
as a key or a value.
One common pattern is where the value in a Map is itself a container such as a
List, Pair, Set or even another Map.
The following example uses Lists to represent the months that make up the
seasons:
Each season has a List for the value element of the entry. When this List is
returned using the key it can be treated just like any other List.
Note that it was necessary to use the safe dot operator as the index access to a
map key can return null.
Online Resources
Exercises
The aim of this exercise is to use a Map as a simple form of data cache.
Calculating the factorial for a very large number can take some time. For example
calculating the factorial of 150,000 can take several seconds. We can verify this using
Exercises 383
a timer() function that records the start and end time of a calculation and runs a
function between the two time stamps.
Note that the range of values supported by the types such as Int and Long are
limited and are not sufficient to represents the results of calculating the factorial of a
number such as 30 or above. To allow larger numbers to be represented the following
program uses the java.math.BigInteger type. This type is used to represent
immutable arbitrary-precision integers. To convert any integer into a Biginteger use
toBigInteger(), for example 0.toBigInteger().
To time how long a function (or indeed member function) takes to run you can
use kotlin.system.measureTimeMillis{}. This takes a lambda function
that is executed and timed. For example:
The following program runs several factorial calculations on large numbers and
prints out the time taken for each:
package timers
import java.math.BigInteger
import kotlin.system.measureTimeMillis
val cache = mutableMapOf<BigInteger, BigInteger>()
val BigIntegerZero = 0.toBigInteger()
val OneBigInteger = 1.toBigInteger()
} else {
var factorial = OneBigInteger
var i = OneBigInteger
while (i.compareTo(number) == -1) {
factorial *= i
i = i.inc()
}
cache[number] = factorial
factorial
}
}
fun main() {
As can be seen from this, in this particular run, calculating the factorial of 25,000
took 224 ms, while the factorial of 150,000 took 4658 ms etc.
Exercises 385
In this particular case we have decided to re run these calculations so that we have
actually calculated the factorial of 25,000, 50,000 and 150,000 twice.
The idea of a cache is that it can be used to save previous calculations and reuse
those if appropriate rather than have to perform the same calculation multiple times.
The use of a cache can greatly improve the performance of systems in which these
repeat calculations occur.
There are many commercial caching libraries available for a wide variety of
languages including Kotlin. However, at their core they are all somewhat Map like;
that is there is a key which is usually some combination of the operation invoked and
the parameter values used. In turn the value element is the result of the calculation.
These caches usually also have eviction policies so that they do not become
overly large; these eviction policies can usually be specified so that they match the
way in which the cache is used. One common eviction policy is the Least Recently
Used (or LRU) policy. When using this policy once the size of the cache reaches a
predetermined limit the Least Recently Used value is evicted etc.
For this exercise you should implement a simple caching mechanism using a Map
(but without an eviction policy).
The cache should use the parameter passed into the cachedFactorial()
function as the key and return the stored value if one is present.
The logic for this is usually:
1. Look in the cache to see if the key is present.
2. If it is return the value.
3. If not perform the calculation.
4. Store the calculated result for future use.
5. Return the value.
Note as the cachedFactorial() function is exactly that a function; you will
need to think about using a global read-only property to hold the cache.
Once the cache is used with the cachedFactorial() function, then each
subsequent invocation of the function using a previous value should return almost
immediately. This is shown in the sample output below. In this output you can see
that subsequent method calls, with the same parameter values, return in less than a
millisecond.
Introduction
Creating Pairs
Pairs are defined using either the Pair class and its constructor or the to operator.
For example:
Each of the pairs in the above code contain exactly two elements.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 387
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_27
388 27 Pairs and Triples
The first two pairs contain an Int and a String while the third and fourth pairs
contain two Strings.
The first definition of a pair for pair1 explicitly specifies the types used for the
first and second elements in the pair. The remaining definitions rely on Kotlin to infer
the types of the two elements of the Pair.
Pairs 1, 2 and 3 use the long hand form where by a pair is created using the class
name Pair and the constructor which takes two parameters.
Pair 4 is created using the shorthand form via the to operator.
The output from this program is:
Creating Triples
Triples are created using the Triple class and its constructor that takes exactly
three parameters. For example:
The elements of a Pair can be accessed using the named properties first and
second. The elements of a Triple can be accessed using the named properties
first, second and third.
For example:
Although both Pairs and Triples are immutable, a copy() member function is
provided that allows a copy of a Pair or Triple to be made with one or more of the
values held in the Pair or Triple overwritten.
The copy() member function has two named parameters first and second
for a Pair and three named parameters first, second and third for a Triple.
Examples of using copy() with both a Pair and a Triple are given below:
390 27 Pairs and Triples
A Pair or Triple can be converted into a list using the toList() member function,
for example:
The elements held in a Pair or Triple can be destructured in a similar way to data
held in data classes. That is, it is possible to unpack values from Pairs and Triples
into distinct variables. This can be done at the point that the receiving vals or vars
are defined using the syntax:
Pairs and Triples can be nested within Pairs and triples; that is a Pair can contain,
as one of its elements, another Pair or indeed a Triple (and vice versa).
For example, the following diagram illustrates the nesting of a group of Pairs
within a Triple:
Note the nesting of round brackets in the printout illustrating where one Pair or
Triple is contained within another.
In fact, a Pair or Triple can have nested within it not just other Pairs and
Triples but any type of container, and thus it can contain Lists, Sets, Maps etc. This
provides for a huge level of flexibility when constructing data structures for use in
Kotlin programs.
It is not possible to add or remove elements from a Pair or Triple; they are
immutable. It should be particularly noted that none of the functions or member
functions presented above actually change the original pair or triple they are applied
to.
Exercises
• The aim of this exercise is to create a concordance program in Kotlin using the
Pair and Map collection classes.
• For the purposes of this exercise a concordance is an alphabetical list of the words
present in a text or texts with a count of the number of times that the word occurs.
• Your concordance program should include the following steps:
• Ask the user to input a sentence.
• Split the sentence into individual words (you can use the split() member
function of the String class for this).
• Use a Map to keep a list of the words found and a count of the number of times
they are found. To do this you could use the String as the key and a Pair as the
value. The pair can contain the string itself and a count of the number of times
the string has been found.
• Produce a list of the words with their counts. You can do this by accessing the
values property of the map—which will return a sequence of Pairs (where each
pair is the string of interest and the count of the number of times it appears).
An example of how the program might operate is given below:
Chapter 28
Generic Types
Introduction
This chapter introduces Type Parameterisation aka Generic Types. We have already
seen examples of type parameterisation in the collection types where a List can be
parameterised to hold only Strings, Person instances or integers etc. In this chapter,
we will look at how you can create your own parameterised types and the options
available for controlling the types that can be used with those parameterisations.
As a concrete example of type parameterisation, we can refer back to the Set class
from a few chapters ago. The Set class allows the type of element that it will hold to
be specified between angle brackets ‘<..>’ after the name of the class when declaring
a val or var. It can also be used with the factory function setOf() to indicate the
type of Set being created.
Of course, this being Kotlin it is also possible to allow the compiler to infer the
type to use with the Set based on the contents of the collection. For example, sets
1 to 4 defined below are all sets that will contain only Strings, while set5 is a
MutableSet that can only contain Strings:
fun main() {
val set1: Set<String> = HashSet()
val set2: MutableSet<String> = HashSet()
// More idiomatic Kotlin
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 393
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_28
394 28 Generic Types
println(set1)
println(set2)
println(set3)
println(set4)
println(set5)
}
Thus all of the above vals reference sets that can only contain Strings. It is therefore
not possible to add a Person instance or an Int to such a set.
Equally, if we wanted a set to be limited to only holding integers then we could use
Set <Int> and to only hold Person types then we would use Set <Person>
(or the equivalent factory function such as setOf <Int> (1, 2, 3) or setOf
<Person> (Person("Phoebe"), Person("Gryff"))).
In all of the above examples the class Set is referred to as a generic class. If you
look at the definition of Set in the Kotlin documentation then you will see that it is
defined as being something like:
Set<E>
Which indicates that Sets can hold elements of a type E where E is a placeholder for
the type to be defined. Thus in Set <String> and Set <Int> the placeholder
E has been replaced by String and Int respectively. The result of applying a type
to a Set is a parameterised Set.
It is possible to create user defined or custom generic types. To do this the generic
type is specified after the name of the class or interface being defined. This is done
using angle brackets followed by one or more labels to use for the generic type. If
more than one generic type is being defined then a comma separated list of labels is
used. The labels can be anything you want but it is common to use single letters and
for these letters to be related to the class or interface being created. For example, T is
often used to represent a type, although E might be used for a container of elements
or K and V for something that rates keys to values.
The following class defines a generic type Bag that can hold a single data item
of type T:
Writing a Generic Type 395
fun get(): T {
return data
}
fun set(value: T) {
data = value
}
}
The class Bag is defined using the generic type syntax as Bag<T>. This means
that within the body of the class we can refer to T as if it was a concrete type. In
this case it means that the private val data is marked as being of type T. In turn the
member function get() returns a value of type T and the set() member function
takes a value of type T.
To use the class Bag either the programmer must specify the type to use with the
Bag (such as String or Int) or the Kotlin compiler must be able to infer the type.
In the following program we create two bags, one that can only hold Strings and
one that can only hold Ints.
fun main() {
val bag1 = Bag<String>("Phoebe")
println(bag1.get())
bag1.set("Gryff")
println(bag1.get())
The first Bag can hold a String and this is explicitly indicated when we create
the Bag. Thus the string “Phoebe” and the String “Gryff” can be stored in the Bag.
The get() member function will return a String and the set() member function
will receive a String.
In turn the second Bag is inferred to hold an Int. Thus the result returned by the
get() member function will now be a value of type Int. In turn the value passed
into the set() member function must be an Int.
The output generated by the above program is:
396 28 Generic Types
Phoebe
Gryff
42
42
35
Declaration-Site Variance
Kotlin allows the programming to indicate that a generic type is only used to produce
data. This is done by using the keyword out before the type in the Type Parameter
(i.e. within the angle brackets). For example given some DataSource class, we
can write:
In this case Kotlin makes it explicit that the type T is expected to be used when
data is generated, there are no member functions that consume data of type T defined.
If we now write:
fun main() {
val source = DataSource<Any>("Phoebe")
println(source.next())
}
Then the source can produce any thing that is of type Any or a subclass of Any.
Since Any is the root of the Kotlin class hierarchy this means that the data source
can be a producer of instances of any type in Kotlin including Strings, Ints, Persons,
any object etc.
Where as if we now write:
This limits the DataSource to producing instances of the class Person or any
subtype of the class Person.
In fact Kotlin goes further than this by checking that if you have marked a type as
being out then it will only be used in out-positions or producer locations within the
class. For example, as return types for member functions rather than as parameters
to a member function.
Declaration-Site Variance 397
Interestingly if you now look at the definition of the Set interface in Kotlin you
will find that it is defined as:
Which indicates that the type E will only be used by member functions that supply
data from a Set (remember that Sets are by default immutable).
In addition to the out keyword, Kotlin as provides the complimentary version,
the in keyword. It is also used with Type Parameterisation, but this time it is used
to indicate that a generic type can only be consumed that is it can only be used in
in-positions that is as parameters to member function and not as the return type of a
member function. This is known as making a type parameter contra variant.
For example, if we define:
Now we can use the DataConsumer class by specifying a concrete type to use.
However, it will only be able to consume (or receive) data of that type, no member
functions can be defined that return data of type T.
We can thus use the DataConsumer as shown below:
hwp main() {
xcn consumer = DataConsumer<String>()
xcn success = consumer.consume("Theeban")
println(success)
}
Generic Functions
It is not only classes that can have type parameters; it is also possible to have generic
functions.
When a generic function is being defined, the type parameter information is placed
before the name of the function, for example:
To call a generic function, the type of the argument can be specified at the call
site after the name of the function, for example:
printer<String>("Jasmine")
Or
printer<Int>(42)
However, in many cases the type can be inferred from the context/values being
passed in. As such it is more common to write:
printer("Jasmine")
printer(42)
In both of these cases the type of T is being inferred, first the type is inferred to
be a String and in the second it is inferred to be an Int.
Generic Constraints
If you want to limit the type used for the Type Parameterisation you can specify an
upper bound. This indicates that the type used must be a subtype of that specified
in the type constraint. The constraint is indicated using a colon (:) followed by the
upper bound type. For example, if we wish to indicate the type specified to be one
that implements the Comparable interface we can write:
This indicates that the type T used with the sort function must be a subtype of the
Comparable interface.
Integers and Strings are sub type of Comparable and so we are able to write:
However, the data class Person given below does not implement the Comparable
interface and therefore is not compatible with the constraint defined for T. Thus the
following code does not compile:
1. Queue collections
Queues are very widely used within Computer Science and in Software Engineering.
They allow data to be held for processing purposes where the guarantee is that the
earlier elements added will be processed before later ones.
There are numerous variations on the basic queue operations but in essence all
queues provide:
• Queue creation.
• Add an element to the back of the queue (known as enqueuing).
• Remove an element from the front of the queue (known as dequeuing).
• Find out the length of the queue.
• Check to see if the queue is empty.
• Queues can be of fixed size or variable (growable) in size.
The basic behaviour of a queue is illustrated by:
Here there are five elements in the queue, one element has already been removed
from the front and another is being added at the back. Note that when one element is
removed from the front of the queue all other element moved forward one position.
Thus, the element that was the second to the front of the queue becomes the front of
the queue when the first element is dequeued.
Many queues also allow features such as:
• Peek at the element at the front of the queue (that is see what the first element is
but do not remove it from the queue).
400 28 Generic Types
• Provide priorities so that elements with a higher priority are not added to the back
of the queue but to a point in the middle of the queue related to their priority.
In the remainder of this section we will create a parametrised Queue type.
2. The MyQueue Mutable class
You can create your own types that are parameterised types. For example, the mutable
collection class MyQueue, presented in the following listing, is a programmer defined
parameterised type. It defines a placeholder T that will be used to represent various
concrete types. Note by convention the letter T is used to indicate a type to specify
(but we could use any letter or sequence of letters thus if the types for a key and a
value were being specified we might use K and V).
package com.jjh.collections
class MyQueue<T> {
val head: T
get() = content.first()
The class MyQueue uses T as if it were an actual type in the body of the class.
Thus the type of element held by the MutableList is T, the type of element that
can be enqueued using the enqueue() member function is T and the result returned
by the dequeue() member function is T. The type of the head property is also T.
This class is used in exactly the same way as one of the built-in collection classes
as shown below:
Creating a Parameterised Type 401
fun main() {
val q = MyQueue<String>()
q.enqueue("John")
q.enqueue("Denise")
q.enqueue("Gryff")
q.enqueue("Jasmine")
println(q)
println("q.head: " + q.head)
val name = q.dequeue()
println(name)
println("q.head: " + q.head)
q.enqueue("Phoebe")
println("q.head: " + q.head)
println(q)
}
In the above example, the type String is used to parameterise MyQueue such
that it now holds Strings. The effect is that the letter T is replaced by the type String
throughout the instance of the MyQueue class referenced by the variable q. Thus the
type of the head property is String, the type used as a parameter to enqueue()
is String and the type returned by dequeue() is String. Thus it is the same
as writing:
val q2 = MyQueue<Int>()
q2.enqueue(12)
q2.enqueue(42)
println(q2)
println(q2.head)
As well as String and Int any type can be used as the concrete type including
Double, Boolean as well as user defined types such as the class Person or the
interface Model.
Thus a parameterised class and type parameterisation provide a powerful construct
for creating type safe, reusable code.
Note that you can create generic type parameterised Classes and Interfaces as
both can be instantiated with a concrete type. You cannot create generic Objects as
you do not instantiate an Object (this is handled for you by the Kotlin runtime).
Exercises
The aim of this exercise is to create your own generic Stack class in Kotlin. You
should be able to create your Stack class as shown below:
Stacks are a very widely used data type within computer science and in software
applications. They are often used for evaluating mathematical expressions, parsing
syntax, for managing intermediate results etc.
The basic facilities provided by a Stack include:
1. Stack creation.
2. Add an element to the top of the stack (known as pushing onto the stack).
3. Remove an element from the top of the stack (known as popping from the stack).
4. Find out the length of the stack.
5. Check to see if the stack is empty.
6. Stacks can be of fixed size or a variable (growable) stack.
Exercises 403
This diagram illustrates the behaviour of a Stack. Each time a new element is
pushed onto the Stack, it forces any existing elements further down the stack. Thus
the most recent element is at the top of the stack and the oldest element is at the
bottom of the stack. To get to older elements you must first pop newer elements off
the top etc.
Many stacks also allow features such as:
1. Top which is often an operation that allows you to peek at the element at the top
of the stack (that is see what the element is but do not remove it from the stack).
The Stack class should provide:
1. A push(element) member function used to add an element to the Stack.
2. A pop() member function to retrieve the top element of the Stack (this
member function removes that element from the stack).
3. A top() member function that allows you to peek at the top element on the
stack (it does not remove the element from the stack).
4. A size property which will indicate the number of elements in the stack.
5. An isEmpty property to indicate whether the stack contains any elements.
6. A toString() member function so that the stack can be printed appropriately.
7. A data property which returns the underlying data structure you are using to
hold the data, for example a MutableList etc.
One you have created your Stack class you should be able to run the following
code:
404 28 Generic Types
fun main() {
val stack = Stack<String>()
println("stack.isEmpty: ${stack.isEmpty}")
stack.push("Task1")
stack.push("Task2")
stack.push("Job1")
stack.push("Task3")
stack.push("Job2")
println(stack)
println("stack.size: ${stack.size}")
println("stack.isEmpty: ${stack.isEmpty}")
println("Stack.top(): ${stack.top()}")
println("Stack.top(): ${stack.pop()}")
println(stack)
}
stack.isEmpty: true
Stack([Task1, Task2, Job1, Task3, Job2])
stack.size: 5
stack.isEmpty: false
Stack.top(): Job2
Stack.top(): Job2
Stack([Task1, Task2, Job1, Task3])
Chapter 29
Functional Programming and Containers
Introduction
Kotlin provides several functions that are widely used to implement functional
programming style solutions in combination with collection container types.
These functions are higher-order functions that are applied to a collection and
are given a function that will be used in various ways with the data in the receiving
collection.
This chapter introduces several functions forEach(), filter(), map(),
flatMap(), sortedBy(), fold(), foldRight() and reduce().
ForEach
The forEach() function is a higher order function that can be used to apply a
function (which can be a callable reference to a named function, an anonymous
function or most commonly a lambda function) to each item of data in an iterable.
It is thus the functional programming equivalent of a for loop.
The result of forEach is Unit and thus it is used where no return values are
expected.
An iterable is any type of container that supports the ability to iterate over its
contents. Many types are iterable including arrays, sets, lists, maps, pairs and triples.
The format of foreach is:
iterable.forEach(function)
This can of course be used with the trailing lambda syntax used with lambda
functions and higher order functions in Kotlin.
The following code creates a list of Strings and applies the forEach() higher
order function to this list. Three styles are illustrated:
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 405
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_29
406 29 Functional Programming and Containers
• The first uses a named function (in this case the built-in function println()
with a callable reference (created using the box operator).
• The second uses an anonymous function.
• The third approach uses a lambda function and the trailing lambda syntax format.
Two examples are given one where an explicitly named parameter is used and one
where the implicit parameter it is used.
The listing is given below:
The output from each of the forEach examples above is the same as each
generates:
Zero
One
Two
Note that for the lambda functions we have allowed the Kotlin compiler to infer
the type of the variable s (and indeed the implicit parameter it). However, the type
is determined by the type of elements held in the collection that forEach is applied
to. Thus the type of s is String and we could have written this long hand if we
had so desired:
We can apply forEach to any type of iterable container including Sets, Lists,
Maps, Pairs and Triples as well as Arrays.
The following examples applies the forEach function to an array:
This generates:
Adam
Jasmine
Phoebe
Gryff
It is also possible to apply forEach to a Map. In the case of a Map there are
three options; apply forEach to all the keys, all the values or to all the key-value
pairs:
print("Keys: ")
map.keys.forEach { print("$it, ") }
print("\nValues: ")
map.values.forEach { print("$it, ") }
print("\nKey-Value pairs: ")
map.forEach{ (k, v) -> print("$k -> $v, ") }
println()
Note that when forEach is applied to the whole Map then two parameters are
supplied to the lambda function representing the key and the value.
Finally, there is a variation on the basic forEach function that allows you
to obtain the index for a value in a collection as well as that value. This is the
forEachIndexed() function. The function passed to forEachIndexed()
must take two parameters, the index of the value and the value itself. This is illustrated
below:
Filter
The filter() function is a higher order function that takes a predicate function
(i.e. a function that returns true or false) to be used to filter out elements from
a collection. The result of the filter() function is a new iterable containing only
those elements selected by the predicate function.
That is, the function passed into filter() is used to test all the elements in
the collection that filter is applied to. Those where the test filter returns true are
included in the list of values returned. Note that the order of the elements is preserved.
The syntax of the filter() function is:
iterable.filter(function)
Note that the receiver of the filter() function is anything that is iterable which
includes all Lists, Pairs, Triples, Sets and Maps, Arrays and many other types etc.
The predicate function passed in to filter() can be an anonymous function,
a lambda function or a named function (via a callable reference).
The result returned will be an iterable containing all those elements that passed
the test function.
Here are some examples of using filter() with a simple list of integers:
fun main() {
fun isEven(i :Int) = i % 2 == 0
Note that the type of d1, d2 and d3 is inferred by Kotlin and is List <Int>.
One difference between the named function example and the anonymous and
lambda function examples is that with a named isEven() function it is more
obvious what the role is of the test function. The anonymous function and the lambda
function do exactly the same, but it is necessary to understand the test function itself
to work out what they are doing.
It is also worth pointing out that defining a named function such as isEven()
may actually pollute the namespace of the package if it is defined at the top level
as there would then be a function that others might decide to use even though the
original designer of this code never expected anyone else to use the isEven()
function. This is why lambda functions are often used with filter(). However
in the above code we have defined isEven() within the scope of the main()
function, it is therefore not available anywhere outside of this function.
Of course, you are not just limited to applying filter() to an iterable containing
fundamental built-in types such as Ints, Floats, Booleans or indeed Strings; any type
can be used. For example, if we have a class Person such as:
Then we can create a list of instances of the class Person and then filter()
out all those over 21:
fun main() {
val data = listOf(
Person("Alun", 56),
Person("Nikki", 51),
Person("Megan", 21))
println("data: $data")
Map
The map() function is another higher order function available in Kotlin. The map()
function applies the supplied function to all items in the receiver iterable. It returns
a new List of the results generated by the applied function.
It is the functional equivalent of a for loop applied to an iterable where the results
of each iteration round the for loop are gathered up into a new List collection.
The map() function is very widely used within the functional programming
world and it is certainly worth becoming familiar with it.
The function signature of map is:
iterable.map(function)
The function passed into the map() function is applied to each item in the iterable.
The result returned from the function is then gathered up into a new List.
The following example applies map() to a list of integers. Each version of the
function passed to map increments the value passed to it by 1:
fun main() {
As with the filter() function, the function to be applied can either be defined
inline as an anonymous function or a lambda function or it can be named func-
tion as illustrated by increment(). Either can be used, the advantage of the
increment() named function is that it makes the intent of the function explicit;
however, it can pollute the namespace of the package. Which is why we have defined
it within the scope of the main() function; thus it cannot be used outside of the
scope of main().
As with the filter() function, it is not only built-in types such as Ints that can
be processed by the function supplied to map; we can also use user defined types
such as the class Person. For example, if we wanted to collect all the ages for a list
of Person we could write:
fun main() {
val data = listOf(
Person("Alun", 56),
Person("Nikki", 51),
Person("Megan", 21)
)
println("data: $data")
Flatten
The flatten() function is not a higher order function instead it is a function that
can flatten a list of containers of containers down into a single level of container. It
is thus often used with the higher order container functions described in this chapter.
For example, given a list of lists such as the data list below, we can use
flatten() to remove one level of nesting:
412 29 Functional Programming and Containers
fun main() {
val data = listOf(
listOf("John", "Paul", "George", "Ringo"),
listOf("Freddie", "Brian", "Roger", "John")
)
println("data: $data")
In the above program the val data holds a list containing references to two
sub-lists. One contains the Strings, “John”, “Paul”, “George” and “Ringo” while the
second list contains the strings “Freddie”, “Brian”, “Roger” and “John”. When we
apply the flatten() function to this list a new list is generated which directly
contains the all the strings.
This output from the above program is:
Notice that one level of square brackets (used to indicate a list) have been removed
in the flattenedData.
FlatMap
The flatMap() higher order function essentially combines the map() function
with a call to flatten().
That is the function passed to flatMap() is mapped to the receiver. The result
of the map() part of the operation is a List. The function flatten() is then
applied to this results list. The overall result of flatMap() is then returned.
As a simple example, consider the following program:
fun main() {
val listOfLists = listOf(listOf(1, 2), listOf(4, 5))
println("listOfLists: $listOfLists")
println("---------------")
FlatMap 413
println("---------------")
This program creates a list of lists. The inner most lists contain integers (that is
a list of 1, 2 and a list of 4, 5). The code first applies the map() function and then
the flatten() function to illustrate their combined behaviour. The flatMap()
function is then applied to this list of lists. The lambda applied to the inner lists
itself applies a filter to each list which will filter out any inner list values less
then 2. However, this results in the generation of two lists that are then flattened
automatically.
The output from this program is:
Sorting Iterables
Kotlin provides two functions that can be used to sort the contents of an iterable,
sorted() and sortedBy{}.
The sorted() function sorts the contents of an iterable using the natural
ordering of a type.
The sortedBy{} function is a higher order function that uses a supplied function
to generate how a type should be ordered.
An example of using both sorted() and sortedBy{} are given below:
414 29 Functional Programming and Containers
fun main() {
val myList = listOf("Zero", "Fifteen", "One", "Two")
println(myList)
println(myList.sorted())
The sorted() function will sort the list of strings using their natural ordering,
where as the sortedBy{} function uses the lambda passed to it. In this case the
lambda returns the length of each string and thus the list will be sorted based not he
strings lengths.
Note that a named function or an anonymous function could have been used as
well as a lambda.
The output generated by this program is:
Fold
The fold() function applies a function to an iterable and combines the result
returned for each element together into a single result. It thus reduces or folds the
iterable down to a single value.
The fold() function takes two parameters, the value to be used to initialise the
accumulated value and the function to apply to each element in the iterable.
One point that is sometimes misunderstood with fold() is that the function
passed into it takes two parameters, which are the previous result and the next value in
the sequence; it then returns the result of applying some operation to these parameters.
The fold() function applies the function passed to it to the elements in the
iterable from left to right, that is the element with index zero is precessed first etc.
The signature of the fold() function is:
iterables.fold(initialValue, function)
Of course the trailing lambda syntax can be used with fold so that it is often written
as:
iterables.fold(initialValue){ function }
Fold 415
fun main() {
val data = listOf(1, 3, 5, 2, 7, 4, 10)
println("data; $data")
result2: 32
Although it might appear that fold is only useful for numbers such as integers;
it can be used with other types as well. For example, let us assume that we want to
calculate the average age for a list of people, we could use fold to add together all
the ages and then divide by the length of the data list we are processing:
fun main() {
val data = listOf(
Person("Alun", 56),
Person("Nikki", 51),
Person("Megan", 21)
)
println("data: $data")
In this code example, we have a data list of three People. We then use the
fold function to apply a lambda to the data list. The lambda takes a total and
adds a person’s age to that total. The value zero is used to initialise this running
total. When the lambda is applied to the data list, we will add 56, 51 and 21 together.
We then divide the final result returned by the size of the data list (3) using the /
(division) operator. Finally, we print out the average age:
FoldRight
It will start by applying the supplied function to the value 10, then 4, then 7
etc. until it reaches the value 1. It is thus the equivalent of reversing a list and then
applying fold to the reversed list.
An example of using foldRight is given below:
fun main() {
val data = listOf(1, 3, 5, 2, 7, 4, 10)
println("data; $data")
Reduce
The reduce() function is a higher order function that acts as a shorthand form for
the most common usage of the fold function. That is it is used to reduce all the
values in an iterable down to a single value. However, unlike fold it is not possible
to define your own initial value for the running total as it is always initialised to zero.
An example of using reduce() is given below:
fun main() {
val data = listOf(1, 3, 5, 2, 7, 4, 10)
println("data; $data")
This section describes one of the difference between anonymous functions and
lambdas and considers examples of labelled returns from lambdas used with
container higher order functions.
We will start this section by looking at how the return statement works when
used within an anonymous function. When return is used with anonymous function
it returns from that anonymous function. For example, in the following function
processList0() the anonymous function used with the forEach() higher
order container function includes a return statement when the value passed to it
is 3. This means that when the anonymous function is applied to each of the values
in the list (1, 2, 3, 4, 5) then when it reaches the number 3 the anonymous function
will immediately return and will not print the value out. However, it only returns
from the anonymous function and thus the forEach() function will continue to
apply the higher order function to the remainder of the values in the list. Once this
has completed the function will then print out the message “Done”.
418 29 Functional Programming and Containers
fun processList0() {
println("Starting processList0")
listOf(1, 2, 3, 4, 5).forEach(
fun (i: Int) {
if (i == 3) return // Return from anonymous function
print("$i, ")
}
)
println("\nDone")
}
fun main () {
processList0()
}
Starting processList0
1, 2, 4, 5,
Done
fun processList1() {
println("Starting processList1")
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // global return for the function
print("$it, ")
}
println("\nDone")
}
fun main () {
processList1()
}
Starting processList1
1, 2,
Notice that the numbers 3 and 4 nor the String “Done” are printed out.
This is because the return inside the lambda does not return from the lambda
it instead returns from the enclosing context, which in this case is the function
processList1. It thus returns from the outer function instead of the lambda.
Labelled Return from Lambdas 419
This is because lambdas are treated by default as being inlineable and thus the
return relates to the contained function. This is why in general we do not use returns
from within lambdas and rely instead on the implicit return of the last expression to
execute within the lambda (and in fact why Kotlin restricts the use of return within
lambdas in most cases).
However, in some cases such as that presented in processList1(), there
may be a reason why we want to return from the inner lambda rather than the outer
function.
To do this we can label the higher order container function and indicate that the
return is scoped within the labelled container operation. This is done using the label
syntax used with break and continue. For example the forEach function can be
labelled with <label>@ and when the return statement is invoked it can indicate
the label to return to via return @<label>.
This is illustrated below:
fun processList2() {
println("Starting processList2")
listOf(1, 2, 3, 4, 5).forEach lit@{
// local return to the caller of the lambda,
// i.e. the forEach loop
if (it == 3) return@lit
print("$it, ")
}
println("\nDone with explicit label")
}
fun main () {
processList2()
}
Starting processList2
1, 2, 4, 5,
Done with explicit label
As you can see we now have the equivalent behaviour to that exhibited by the
anonymous function.
In fact this is such a common pattern that Kotlin gives us a shorthand form that
avoids the need to label container higher order functions such as forEach.
In the shorthand form it is only necessary to label the return using the nota-
tion return @<contianer-function> such as return@forEach (where the
lambda is defined within a forEach function).
For example:
420 29 Functional Programming and Containers
fun processList3() {
println("Starting processList3")
listOf(1, 2, 3, 4, 5).forEach {
// local return forEach loop - shorthand form
if (it == 3) return@forEach
print("$it, ")
}
println("\nDone with implicit label")
}
fun main () {
processList3()
}
Starting processList3
1, 2, 4, 5,
Done with implicit label
Online Resources
More information on map, filter, flatMap and reduce can be found using
the following online resources:
• https://kotlinlang.org/docs/reference/collection-operations.html Summary of
container related operations.
Exercises
Using the Stack class you created in the last chapter, explore the use of forEach,
map and filter.
You should now be able to use the forEach, filter and map functions with
the Stack class as shown below:
Exercises 421
The output from the above sample main() function might look like:
Introduction
In this chapter we will introduce Kotlin coroutines. We will first discuss asynchronous
programming/concurrency in computer programs. We will then consider threading
which is the underlying mechanism used to support concurrency in programs running
on the JVM including Kotlin. We will then discuss why threading is too low a level
for many applications and introduce Coroutines as the Kotlin higher level solution
to concurrency.
Concurrency
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 425
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_30
426 30 Coroutines
For example, let us assume that we have a program that will call three independent
functions, these functions are:
• make a backup of the current data held by the program,
• print the data currently held by the program,
• run an animation using the current data.
Let us assume that these functions run sequentially, with the following timings:
• the backup function takes 13 s,
• the print function takes 15 s,
• the animation function takes 10 s.
This would result in a total of 38 s to perform all three operations. This is illustrated
graphically below:
However, the three functions are all completely independent of each other. That
is they do not rely on each other for any results or behaviour; they do not need one of
the other functions to complete before they can complete etc. Thus we can run each
function concurrently.
If the underlying operating system and program language being used support
multiple processes, then we can potentially run each function in a separate process
at the same time and obtain a significant speed up in overall execution time.
If the application starts all three functions at the same time, then the maximum
time before the main process can continue will be 15 s, as that is the time taken by
the longest function to execute. However, the main program may be able to continue
as soon as all three functions are started as it also does not depend on the results from
any of the functions; thus the delay may be negligible (although there will typically
be some small delay as each process is set up). This is shown graphically below:
Concurrency 427
Parallelism
Threads
For any JVM language, including Kotlin, the Thread class represents an activity
that is run in a separate thread of execution within a single process. These threads of
execution are lightweight, pre-emptive execution threads. A thread is a lightweight
process because it does not possess its own address space and it is not treated as a
428 30 Coroutines
separate entity by the host operating system; it is not a separate operating system
process. Instead, it exists within a single machine process using the same address
space as other threads.
Threads are a very well established technology, however they provide fairly low
level concurrency features.
There are several issues associated with the direct use of Threads to implement
concurrency, including:
• Threads represent a low level API for concurrency.
• Threads are expensive in terms of CPU and System overheads.
• Threads are expensive to create, schedule and destroy.
• A thread may spend a lot of time waiting for Input/Output actions.
• A thread may spend a lot of time waiting for results produced by other treads.
• Even when threads are run natively by the operating system, there are issues such
as:
• in practice the number of threads that can run at the same time (concurrently) is
limited by the number of CPUs/cores available,
• if your program requires more threads than there are cores then some form of
scheduler is required to coordinate the execution of the threads on the cores.
Coroutines Introduction
Coroutines are a Kotlin specific, lighter weight alternative to the JVM based
Threading model. They are an abstraction layer on top of threads and thus at runtime
it is still threads that execute the coroutine code for you. However, this is now done in
an efficient manner, with the reuse of threads via a pool of reusable threads (referred
to as a Thread Pool) and by sharing threads between coroutines.
A Thread can be shared between coroutines whenever a coroutine needs to wait for
some reason (for example for some input/output operations need to be performed).
This means that rather than blocking a Thread during such waits, other coroutines
can make used of the Thread instead. This means coroutines use Threads in a much
more efficient manner than would be the case just from directly executing behaviour
within an individual Thread.
To summarise the Coroutine to Thread relationship we can say:
• coroutines are assigned to threads. The thread will then execute the coroutine for
you.
• When an active coroutine is suspended, for example when waiting for input/output
operations such as waiting for a database system to respond, then:
Coroutines Introduction 429
Coroutines are not by default apart of the Kotlin environment, as such it is necessary
to add the Kotlin Coroutine library to your project. In IntelliJ IDE you can do this by
adding the coroutines library directly to your project (see the IntelliJ documentation
for more details as this may change between versions—https://www.jetbrains.com/
help/idea/library.html).
Once you have done this you can start to use the Coroutine library in your code.
Implementing a Coroutine
Launching Coroutines
To use the coroutines library in Kotlin you will need to import seam or all of the
contents of the kotlin.coroutines package. In the following program we
430 30 Coroutines
import three elements from this package, the GlobalScope, the delay function and
the launch function, for example:
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
To start a coroutine we need to launch it, this can be done using the coroutine
builder GlobalScope.launch{}. This allows a coroutine to be started on a
separate thread (to the main application). We will return to the term scope with
respect to coroutines below.
The GlobalScope.launch{} launcher implements a fire and forget policy
that will start an asynchronous task/coroutine which will run to completion. That is
there is no result returned to the main thread in which the application runs nor is
there any interaction between the task and the main application - hence the term fire
(off the task) and forget (about it in the main program).
The following program launches a simple coroutine using a lambda function
passed to the launch member function (using the trailing lambda syntax). The
coroutine prints out a message and then sleeps for 5,000 ms using the delay()
function. This function delays/sleeps a coroutine for a given amount of time without
blocking the underlying thread and resumes the coroutine after a specified time
(potentially in another thread):
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch {
println("coroutine ---> Starting Task")
delay(5000)
println("coroutine ---> Done Task")
}
println("---------------------------------")
println("Main -> After launching coroutine")
println("Main -> Waiting for task - press enter to
continue:")
readLine()
println("---------------------------------")
println("Main -> Done")
}
Working with Coroutines 431
The main() function prints out a message after the coroutine is launched and then
asks the user for input so that the main program does not terminate. This is necessary
as the launcher runs the coroutine in a background thread. On the JVM, programs
automatically terminate when there are no more foreground threads to execute, as this
coroutine runs in the background this is not sufficient to stop the program terminating,
thus we cause the main application thread (which is a foreground thread) to wait for
user input.
The output from this coroutine program is:
---------------------------------
Main -> Done
Note that the output from the main application thread (the lines prefixed with ‘Main
- > ’ are intertwined with the output from the coroutine running in the background
thread.
Suspending Functions
We can define a named function to run as the coroutine task by prefixing the function
definition with the keyword suspend. For example:
import kotlinx.coroutines.delay
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch {
executeSlowTask()
}
println("---------------------------------")
println("Main -> After launching coroutine")
println("Main -> Waiting for task - press enter to
continue:")
readLine()
println("---------------------------------")
println("Main -> Done")
}
The output from this second version of the program is given below:
---------------------------------
Main -> Done
As before you can see that the output from the main application (running in
the main Thread) is interspersed with the output from the suspending function
executeSlowTask() running in a background thread.
GlobalScope.launch { executeSlowTask1() }
GlobalScope.launch { executeSlowTask2() }
GlobalScope.launch { executeSlowTask3() }
Coroutine Scope
There are other alternatives to the global scope for coroutines. One option is to create
a custom scope. This allows an application to manage a group of coroutines together.
For example the application can then cancel all coroutines with the same scope in
one go if required.
Custom scopes are built on top of a Coroutine Dispatcher. A Coroutine Dispatcher
is the part of the coroutine runtime that handles how coroutines are executed and is
discussed in the next section.
A new custom scope can be created by instantiating a CoroutineScope
instance and passing in a suitable dispatcher, for example:
This can then be used with the launch() function, to launch a coroutine within
a specific scope, for example:
Coroutine Dispatchers
Coroutine Dispatches handle how coroutines are run (dispatched). That is they handle
how coroutines are assigned to JVM Threads. They also handle how coroutines are
suspended (which stops them running and unloads them from a particular thread)
and how they are resumed (how they are reassigned to an available thread).
There are three built-in dispatchers:
• Dispatchers.Default This is the default dispatcher and is intended
primarily for CPU intensive tasks.
• Dispatchers.IO This is the dispatcher that is recommended for network,
disk, database or other IO operations.
• Dispatchers.Main (Android specific) This is the dispatcher used to run
coroutines on the main Android thread. This is significant as it allows a coroutine
to make changes to UI components within an Android application.
Coroutine Builders/Launchers
An example of a coroutine that returns a result using the async{} launcher and
the GloabalScope is given below:
fun main() {
println("Main -> Launching deferred result task")
GlobalScope.launch {
val result = executeSlowTaskWithResultAsync().await()
println("coroutine --> result: $result")
}
println("Main -> After launching coroutine")
println("Main -> Waiting for task - press enter to
continue:")
readLine()
println("Main -> Done")
}
As you can see from this the result is returned from the
executeSlowTaskWithResult() suspending function. The main launcher
waits for this result using the await() member function of a suspending function.
436 30 Coroutines
Coroutine Jobs
Each call to a launcher such as launch{}, async{} etc. returns a Job instance.
These jobs are instances of the kotlinx.coroutines.Job class. Jobs can be
used to track and manage the lifecycle of coroutines. In turn calls to coroutines within
a coroutine result in child jobs.
Once you have a reference to a coroutine job you can cancel a job. If the associated
coroutine has children then you can also cancel child jobs.
The status of the associated coroutine can also be obtained from a job using
Boolean properties such as isActive, isCompleted and isCancelled.
There are also a range of member functions defined for jobs including:
• invokeOnCompletion{} this is used to provide behaviour to run when job
completed,
• join() which suspends the current coroutine until the receiver coroutine
completes,
• cancel(CalcellationException?) which cancels a job,
• cancelChildren() which cancels child jobs of a receiver job,
• cancelAndJoin() cancel the receiving coroutine and wait for it to complete
before continuing.
The following program illustrate some of these ideas. It creates a new coroutine
using the GlobalScope.launch{} launcher and stores the resulting job refer-
ence in the val job. It then uses this job to check to see if it is still active, whether it
has completed, has it been cancelled and whether it has any child jobs or not. It also
registers a callback lambda to be invoked when the job completes:
Coroutine Jobs 437
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
fun main() {
println("Launching fire-and-forget task")
val job = GlobalScope.launch {
executeSlowTask()
}
println("After launching coroutine")
println("job.isActive: ${job.isActive}")
println("job.isCompleted: ${job.isCompleted}")
println("job.isCancelled: ${job.isCancelled}")
println("job.children.count(): ${job.children.count()}")
job.invokeOnCompletion { println("I am Completed") }
Done
As can be seen from this output the job is still active, has not been completed or
cancelled, has zero child jobs etc. It also calls the on completion callback after the
task completes.
Online Resources
Exercise
In this exercise you will create a suspending function and run several coroutines
concurrently.
Create a suspending function called printer() that takes three parameters:
• a message to be printed out,
• a maximum value to use for a period to sleep,
• the number of times that the message should be printed.
Within the function create a loop that iterates the number of times indicated by
the third parameter. Within the loop.
• generate a random number from 0 to the max period specified and then sleep
for that period of time. You can use the Random.nextLong(0, sleep)
function for this,
• once the sleep period has finished print out the message passed into the function,
• then loop again until this has been repeated the number of times specified by the
final parameter.
Next run the printer() function with various different parameters. Each execu-
tion of the suspending function printer() should be launched independently
using the GlobalScope.
An example program to concurrently run the printer() function five times is
given below:
import kotlin.random.Random
import kotlinx.coroutines.delay
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
fun main() {
println("Main -> Launching fire-and-forget tasks")
GlobalScope.launch { printer("A", 100, 10) }
GlobalScope.launch { printer("B", 200, 5) }
GlobalScope.launch { printer("C", 50, 15) }
GlobalScope.launch { printer("D", 30, 7) }
GlobalScope.launch { printer("E", 75, 12) }
println("---------------------------------")
println("Main -> After launching coroutine")
println("Main -> Waiting for task - press enter to
continue:")
readLine()
println("---------------------------------")
println("Main -> Done")
}
Introduction
Coroutine Channels
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 441
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_31
442 31 Coroutine Channel Communications
channel.send(data)
It is also possible to close a channel to indicate that there will be no more data
sent. A channel can be closed using the close() member function:
channel.close()
There are four types of channels, and they differ in the number of values they can
hold at a time. These four are:
• Rendezvous Channel used to coordinate the send and receive of messages.
• Buffered Channel provides a predefined buffer that allows several messages
to be sent before the channel blocks while waiting for receives to handle those
messages.
• Unlimited Buffered Channel provides for an unlimited size buffer so any number
of messages can be sent without blocking the sending coroutine.
• Conflated Channel When using this channel type, the most recent value
overwrites any previous values sent but not yet received.
Examples of each of these types of channel will be given in the remainder of this
section.
Note to use challenge we need to import the appropriate type form the
kotlinx.coroutines.channels package.
Rendezvous Channel 443
Rendezvous Channel
Note that the Channel is a generic type and thus the type of data handled by
the channel is specified when it is created. In the above example we are creating a
channel that can handle integers, if we wanted to create a channel that could handle
Strings then we would write:
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
val channel = Channel<Int>() // Shared rendezvous Channel
As can be seen from the output the sending and receiving coroutines coordinate
their sending and receiving actions. That is, the receiving coroutine blocks until the
sending coroutine publishes some data.
Buffered Channel
The Buffered Channel is a channel type that has a predefined buffer. The buffer size
is specified when the Channel is created. For example to create a Channel that can
handle Ints and has a buffer of 10 integers you can write:
This means that the publishing coroutine can send 10 integers to the channel
before the send operation will block that coroutine.
Buffered Channel 445
A modified version of the program shown in the previous section is given below.
It differs in three ways:
• The channel created is now a shared buffered channel with a buffer of size 2.
• The receiveDataFromChannelTask() suspending function has an initial
delay of 50 ms before it enters a while loop that will allow it to continue processing
data while there is data available.
• Inside the wile loop of the receiveDataFromChannelTask() suspending
function a further delay of 100 ms has been used to ensure that the receiver has to
wait between reads. This means that the sending coroutine will always run ahead
of the receiving coroutine and thus the sender will be able to send up to 2 values
into the buffered channel before it is blocked.
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
val channel = Channel<Int>(2) // Shared Buffered Channel
Looking at the output it is possible to see that when the sending coroutinesend
the 3rd value into the channel that it is blocked. It must then wait for the receiving
coroutine to receive a value before it can send another value. This behaviour is
repeated until the sending coroutine has sent all its values. The receiver can then
read the remaining values held in the buffer.
An unlimited buffered channel has an unlimited buffer size. Therefore any number of
messages can be sent without blocking the sending coroutine. The receiving coroutine
will receive buffered data when it is ready to process that data.
The unlimited buffered channel may generate an OutOfMemoryError as the
unlimited size of the buffer will allow it to fill up all available memory within the
application runtime.
An Unlimited Buffered Channel is created by providing UNLIMITED as the
constructor parameter when instantiating a Channel. For example:
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
As you an see from this the sending coroutine publishes all its data to the unlimited
buffered channel before the receiver even starts processing that data.
Conflated Channel
A Conflated Channel will conflate published but as yet un read values. That is, the
most recently written value overrides the previously written values that have not yet
been received (read) by any coroutines. Thus:
• the send member function of the channel never suspends,
• the receive member function receives only the latest value.
To create a Conflated Channel you create a Channel and pass in the CONFLATED
value to the constructor:
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
Notice how the only value received by the consuming coroutine is the value 4.
This is the last value published before the producer coroutine finishes.
The examples presented so far all show a single sending coroutine and a single
receiver coroutine; however it is possible to have multiple senders and multiple
receivers.
There can be multiple senders to a channel. The values sent are processed in the order
they are sent from the various different coroutines using that channel. The values will
be received by the receiver in the time order that they were sent (no matter which
sending coroutine published that data).
A simple program illustrating the use of two coroutines that publish data to a
common rendezvous channel with one receiver is given below:
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
As you can see the output intermixes Sender1 and Sender2 with the delay
used for Sender1 double that used for Sender2.
Just as there can be multiple sending coroutines there can be multiple receiving
coroutines. In this scenario when the single sending coroutine publishes data to the
channel, if there is an available receiving coroutine then that coroutine receives the
data. Once the data has been consumed it is removed from the data stream. Thus only
one receiving coroutine will receive each data item.
A simple program using multiple receiver coroutines is presented below:
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
fun main() {
As you can see the data sent by the Sender coroutine is received in turn by either
Receiver1 or Receiver2.
Pipelines
Pipelines are a design pattern where one coroutine produces a set of values and
another coroutines consume those values, does some processing on the values, and
then sends the modified values onto another channel.
452 31 Coroutine Channel Communications
In this diagram an initial source coroutine sends data to a first channel. Another
coroutine then receives that data, processes it in some way and then sends the results
onto a new channel. The second channel then passes the data onto the final coroutine
that consumes the results.
Depending upon the type of channels used, the final coroutine can be the control-
ling coroutine that determines how data is sent through the pipeline. For example,
if both intermediate channels are rendezvous channels then it is only when the final
coroutine consumes the data that the next data item can be sent through the whole
pipeline.
A simple example application that implements the two channel pipeline illustrated
above is given below.
In this pipeline an initial source numberGenerator() coroutine generates a
series of integer numbers. These numbers are sent to channel1. Another corou-
tine running the doubler() suspending function receives data from channel1,
doubles the number and sends it onto channel2. Finally a third coroutine running the
printer() suspending function receives the number from channel2 and prints
it out.
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
while (true) {
val num = inputChannel.receive()
println("doubler received --> $num")
val newNum = num * 2
println("doubler sending --> $newNum")
outputChannel.send(newNum)
}
}
// Set up channels
val channel1 = Channel<Int>()
val channel2 = Channel<Int>()
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Pipelines 455
fun main() {
GlobalScope.launch {
// produces integers from 1 and on
val numberChannel = numberGenerator()
// doubles integers received from number channel
// and resends
val doublerChannel = doubler(numberChannel)
Exercise
In this exercise you will create three suspending functions; one suspending function
will publish data to a channel and two other receivers will handle that data when
emitted by the channel.
The channel (called msgChannel) will handle strings, it can therefore be defined
as:
Each publishing suspending function and the two receivers will all be launched
independently.
The publishing suspending function should take the channel to use, and a timeout
to be used between each value being published. The strings to be published should
be held in an array of Strings such as:
fun main() {
println("---------------------------------")
println("Main -> After launching coroutines")
println("Main -> Waiting for tasks - press enter to
continue:")
readLine()
msgChannel.close()
println("---------------------------------")
println("Main -> Done")
---------------------------------
Main -> Done
Part V
Android Development
Chapter 32
Android Overview
Introduction
In 2019 Google declared that Kotlin was its preferred development language for
Android development. This chapter will therefore introduce Android Mobile device
application development.
What is Android?
Android is an open software platform for mobile development. It is very widely used
from smart phones, to tablets and from smart TVs to in car systems. It provides
a complete system stack, from the low level operating system kernel, through
the runtime environment and system provided services and libraries to application
execution.
This is illustrated in the following diagram:
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 461
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_32
462 32 Android Overview
There are several different levels in this diagram represented by each of the boxes,
a short summary of each of these is given below:
• System Applications. Android provides a set of core applications such as an
email client, a calendar, a camera or a telephone dialer etc. The exact applications
depends on the actual environment on which Android is running (for example
the dialer may not be appropriate for Android TV and the Camera may not be
appropriate for Android Auto). Custom applications can interact with these system
provided applications using the Android infrastructure.
• API Framework. Developers have access to framework APIs that allow custom
applications to either be notified of events occurring within the device (such as the
device being tilted) or to interact with facilities provided by the Android platform
such as the Location Manager or the Telephony Manager.
• Native Libraries. These are a set of libraries provided by Android that are imple-
mented natively, that is they run directly on the underling operating system. These
libraries include secure socket communications, OpenGL graphics libraries and
WebKit for HTML rendering. They are exposed to Kotlin developers through the
Android application framework.
• Android Runtime. This is divided into two parts, the Android Runtime (aka
ART) itself and the set of core Android runtime libraries. The ART runs each
application in its own process with its own instances of the ART. It converts byte
codes into native instructions in a similar manner to the plain old JVM in a desktop
application. However, it is optimised for the Android platform. The core libraries
provide most of the functionality available to a Kotlin developer; they include data
structures, utilities, file and network access, application infrastructure classes etc.
• Hardware Abstraction Layer (HAL). This layer provides a set of standard inter-
faces that expose device hardware capabilities to the higher level Kotlin API
Framework. It consists of multiple library modules each of which implements
an interface for a specific type of hardware component, such as the camera or
bluetooth modules. These Library modules are loaded on demand when they are
required. They provide a level of abstraction between the software system and the
actual hardware platform on which it is running. This allows Android far greater
portability than would otherwise be the case.
• Linux Kernel. This is the underlying operating system on top of which the whole
of Android runs. It provides the facilities for memory and process management,
network protocols, driver management, underlying security, input and outputs etc.
What is Android? 463
From this we can see that Android is an environment for running applications
written in Kotlin (or Java) on physical devices. Although the most popular devices
are smart phones such as Android phones including those from Samsung and Google
itself, it is also used on Android Tablets, Android Televisions and Android Auto for
motor vehicles.
Android Versions
There have been numerous versions of Android released over the years. Each of these
versions provides a particular range of facilities and certain versions have represented
a step change in the way in which Android applications are built. For many versions
there is both a code name such as Jelly Bean and an API level number such as 16. To
add to the confusion for some codenames there are several API versions, for example
Oreo is associated with both API level 26 and 27. Finally, each version has a release
number associated with it. Thus the two releases with the codename Oreo are version
8.0 and 8.1 (which are at API level 26 and 27). This is illustrated below, along with
when the version was released, for some key versions:
You will find that applications, libraries and developers will talk about things at
both the code name and the API level. You may therefore find that a library specifies
that it requires API Level 30 to run or that another application says that it requires
Lollipop to be installed. In some cases you might find that a framework specifies the
version number it requires such as version 8.0 onwards. It is therefore useful to be
familiar with all of these terms.
It is worth noting that Google does not provide security updates for all versions,
at the time of writing it only provided security updates for version 8.0 onwards (aka
Oreo or API 26 onwards).
464 32 Android Overview
Another point worth noting is that there was a change in the way Android appli-
cations were run with the Lollipop/version 5.0 operating system. From this point
onwards Android applications always run in their own process with their own instance
of the ART (Android Runtime). Because of this you often find that discussion relating
to Android consider pre Lollipop/Version 5.0 systems and post Lollipop/Version 5.0
systems.
Both Java and Kotlin are widely used programming languages on the Android plat-
form. Java was the original language that was used to implement Android applications
and it is still very widely used.
However, on the 7th of May 2019 Google announced that Kotlin was now the
preferred development language to be used with Android. Although it may have
taken a while for development organisations and developers to switch over, by 2021
this process was accelerating and more and more applications were being developed
or enhanced using Kotlin.
Another alternative is to use C++. This approach is known as native coding and is
supported by the Android Native Development Kit. It represents application coding
at a lower level than Kotlin. By this we mean that the code that is written does not
use an intermediate runtime (such as the ART) and instead is executed directly by
the underling operating system. This can have significant performance benefits but
can be more complex to implement.
Android Runtime
Deploying to Android
The following diagram illustrates how a source code file written in Kotlin is converted
into a form that can be deployed onto a device and executed by the Android Runtime
(ART) environment:
Android Runtime 465
• Core Libraries. These are pre installed as part of the Android platform and can
be accessed directly by your application code.
If you find this diagram daunting don’t worry, 99% of the time most of this is
hidden from you. What you will see as a Kotlin developer is that you write your code
in the Android Studio IDE (which is built onto of IntelliJ IDEA) and then when you
run the application it must be deployed onto either a device emulator (which is a
smart device such as a mobile phone implemented in software on your computer) or
to a physical device such as an actual smart phone. The program then runs and you
interact with it on the physical or emulated device.
Packaging an Application
The end result is that a file is created with an.apk extension that contains one or
more Dex files and all required supporting files and assets including any application
security certificates, the AndroidManifest.xml file, images, sound files etc.
You may have more than one Dex files as the size of a Dex file is optimised such
that if it gets too big a second Dex file will be created to ensure speed of access to
the contents of the file.
Application Core Building Blocks 467
The following diagram illustrates the core elements used to create an Android appli-
cation in Kotlin. Each of these elements has a specific structure and a defined life
cycle and method of operation. That is they are more than conceptual elements in an
application. For example, Android understands the role of an Activity, it understands
what a Content Provider is etc. And has a specific lifecycle of actions associated
with these elements.
• Content Providers. Content Providers are components that can be used to share
information between applications.
• Broadcast Receivers. Applications can send and receive broadcast messages.
These messages can be sent from within the same application or from external
applications.
• Notifications. Notifications provide a way for an application to present infor-
mation to the user even when they are in the background via the Notifications
Drawer.
However, the previous section merely tells you what the components available in
Android are, it does not indicate how an application may be created from these
components.
The following diagram illustrates how these components can fit together to create
an Android application. Note that other than the AndroidManifest.xml file
and the main Activity; all the other elements are optional.
Within this diagram we can see that the green boxes relate to user interface
elements. In this case there is a single main activity and two fragments which may
be displayed within this activity. The actual structure of the Activities user interface
is defined within an xml layout file.
The activity uses a Service that implements its business logic.
Both the Activity and the Service interact with a Content Provider that reads and
writes data to an SQLite database (which is provided as part of the Android runtime).
At some point in the applications lifetime it uses an Intent to initiate another
application (such as the Dialer application). It can also receive broadcast messages
using its Broadcast Receiver.
Android JetPack 469
Android JetPack
Android JetPack was introduced back in 2018. It is a suite of libraries designed to help
developers create Android applications that following best practices, with reduced
boiler plate code, and that are easier to port from one Android version to another and
from one Android device to another.
The key elements provided by JetPack include:
• Android Architecture Components to support best practices in structuring appli-
cations.
• Android Support Library to make it easier to access common facilities such as
when building mobile camera applications.
• Android Studio which is part of the JetPack world and makes it easier to create,
test and run Android applications.
• Set of guidelines on how to build Android applications.
However, it is important to understand the basics before getting to into using
Android JetPack as some of the libraries assume you understand the underpinnings
of the environment.
Online Resources
Introduction
In this chapter we will look at the Android Studio and a simple Hello World style
Android application.
Android Studio
Android Studio is the development IDE for Android applications. It is built on top of
the IntelliJ IDEA IDE. It extends this IDE with specific Android development tools
such as the AVD Manager (which helps managed Android Virtual Devices which
are emulators for things such as Smart phones or Smart TVs) and the Android SDK
manager.
Android Studio is available for a wide range of platforms including Microsoft
Windows 64-bit version (the 32-bit version deprecated as of December 2019), macOS
X 10.10 or later and Linux 64-bit distribution capable of running 32-bit apps.
Android Studio is quiet memory hungry and the recommendation is that the host
computer should have at least 8 GB of memory (although in practice more is better).
To download your own copy of Android Studio go to the URL https://developer.
android.com/studio. Follow the instructions provided.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 471
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_33
472 33 Applications and Activities
Setting Up a Project
Once you have installed the Android Studio IDE. You can create a new project and
install specific versions of the Android SDK and create Android Virtual Devices
(AVD) to emulate running Android applications on a Smart Phone.
Android SDKs
The Android Software Development Kit or Android SDK provides a suite of tools
and a specific version of the android operating system that can be deployed to a AVD
to test out applications. You will need to to install each version of the Android SDK
that you want to use.
The Android SDK includes the following:
• Required libraries.
• Debugger.
• An emulator.
• API Documentation.
• Sample source code.
• Tutorials for the Android OS.
Android Studio provides the SDK Manager which is used to install and manage
the versions of the Android SDK that you want to work with. For example, depending
upon the version of Android Studio you are using, you should be able to find it under
Tools>SDK Manager menu option.
An Android Studio AVD (Android Virtual Device) is an emulator that will run a
particular Android device with a particular Android SDK version so that you can test
your Android applications without resorting to using a physical device.
The Android AVD Manager is a tool used to configure these emulators and well
as to manage what is installed on them, when they should be restarted etc.
When you first run Android Studio you will typically find that there is only one
AVD installed, however you can configure additional devices as required. This is done
using the Tools>AVD Manager menu option. This will display the ‘Select Hardware’
dialog:
Setting Up a Project 473
For example:
Select the device you want to install and follow through the Next button option and
the screen presented to you. As part of the device set up you can specify the version
of Android SDK that you want to install on that device. If the selected Android SDK
is not currently installed on your local machine, then it will need to be downloaded
first and Android Studio will do that for you.
In this section we will look at how to create a new Android Application and what its
constituent parts are.
You can either create a new Android development project from File>New>New
project… or via the Android Studio start up screen where you can select the ‘Create
New Project’ option.
On the dialog presented to you can select what type of Project Template you want
to use. These templates are used to provide a skeleton of the type of project selected
with the core elements in place.
474 33 Applications and Activities
We will use the ‘Empty Activity’ Project Template as that will give us the flexibility
to create our own applications. To do this select the ‘Empty Activity’ then select
‘Next’.
On the Configure Your Project dialog you will see the following fields:
1. Name. Enter a suitable name for your project, for example HelloWorld.
2. Package name. Check the default package name. You can also specify a default
package name, for example com.jjh.app. Also note that we have changed
the package name from the default generated by the Android Studio.
3. Save location. Check the location you are planning to save your project in.
4. Language. Also make sure you select Kotlin as the programming language (the
current version of Android Studio now defaults to Kotlin but still double check
that Kotlin is selected).
5. Minimum SDK. You can also select the minimum Android SDK to use (for
example version 8.1 Oreo which is one of the oldest versions that currently
obtains security updates).
6. This is all illustrated below:
We will now open the layout editor to modify the display a bit. To do this select
the res (resources) node in the Android View tree on the left hand side of the IDE.
Next expand the layout node and double click on the activity_main.xml file.
This is shown below:
You should now see the default display in the layout editor with both the Design
view and the Blueprint view showing.
476 33 Applications and Activities
Select the Text View in the centre of the display (the element currently displaying
“Hello World!”). We will now change this slightly so that your application has been
configured to some extent. To do this follow these steps:
1. Change the text to say “Hello Mobile World!” You can do this by selecting the
text view and then changing the text to be displayed—this is in the properties
box on the right handside of the editor. It is under Declared Attributes.
2. Change the font colour to Red—to do this look at All Attributes and scroll
down until you find textColor (note the spelling). Change this to the colour
red—you can do this interactively from the colour dialog.
3. Change the font size to be 34sp—this is done via the textSize attribute under
All Attributes.
4. Change the style of the text to be bold. This is done via the textStyle attribute
under All Attributes.
Notice two things.
1. That the display updates immediately for you so that you can see what you will
get.
2. That the attributes you have explicitly specified are now added to the Declared
Attributes list.
Creating an Android Application 477
You can then run the application on that emulated device. Do this by ensuring that
the device is selected in the run tools next to the green run button:
Now run the application by clicking in the green arrow button to the right of the
device on which to run the app.
In your device emulator you should see the application output, it should look
similar to that shown below:
478 33 Applications and Activities
Android Application
We will now examine the Android application that you have just created.
The basic structure of the simple hello world application that we created using the
Empty Activity template is given below.
The Empty Activity template creates the core skeleton of an Android application.
This skeleton is comprised of several key elements these are:
Android Application 479
AndroidManifest.xml. This is the XML file that defines the application to the
Android runtime. It notifies ART of the entry point to the application and can
contain information such as the services, intents and security permissions related
to the application. The AndroidManifest.xml file generated by the template is
given below:
Notice that the single activity defined within this XML file has the
name.MainActivity. This is because the name of the activity is added to the
package declaration at the top of the XML file. This declaration indicates the default
package that is used with activities within the file. Also note that the activity element
contains an action and a category element. The action element indicates that this
a ativity is the entry point to the application; it is the equivalent of the main()
function in a standalone Kotlin application. The category element indicates that this
application should appear in the launcher window of the Android device on which
the application is deployed.
MainActivity. This is the entry point for the application. ART loads this activity to
run the application. That is ART instantiates the MainActivity class and then calls
several member functions on the instance to initiate the running Android application.
activity_main.xml. This is an XML file that defines the layout of the User Interface
presented by the MainActivity. For example, the layout file can indicate how
buttons, fields, and labels are laid out across or down the screen. The layout generated
by the template included a single TextView contained within a constraint layout.
The layout file, modified as described earlier in this chapter, is given below:
480 33 Applications and Activities
strings.xml. The strings.xml file defines a set of key to string mappings that allow
textual content within the User Interface to be localised. The strings.xml file
generated by the template contains a single string entry which is the name of the
application. For example:
Resources. These represent other resources included in the application. The Empty
Activity Template generates a set of default icons, colours, User Interface themes
etc. Strictly speaking the strings.xml file is part of the resources but as it is
something you are likely to need to change as development progresses we have listed
it separately.
In addition to the application code file the template also generated several Gradle
build files. Gradle is a library dependency management and build system for JVM
languages such as Java and Kotlin. It is not the only option available as Maven
performs a similar function. However, Gradle is the default build system used by
Android developers. Indeed Android Studio itself uses the Gradle build system to
manage the libraries that your application uses, to build the APK files to deploy and
to deploy your application to the emulator etc.
Android Application 481
The Gradle files are listed together under the Gradle Scripts node in the Android
view.
Note that there are two Gradle files, one is at the Project level and one is at the
Module level.
When working with Android Studio a project is made up of one or more modules.
Each module can have its own Gradle file that is used to define the dependencies for
that module as well as information such as the version of the module, how tests will
be run etc. The top level Gradle file indicates which version of Kotlin is being used,
which central library repositories to use (such as the mavenCentral and google
repositories).
Another point to note is that the underlying directory structure differs from the way
in which the Android view in the Android Studio presents the files. The under-
lying directory structure is in many ways more like a traditional Kotlin project than
suggested by the Android view.
The reason that this layout is hidden to some extent by the Android view in
Android Studio is that it aims to make it simpler to work with the code you are
developing.
HelloWorld Application
The entry point for the application is the MainActivity class. This class is listed
below in its entirety:
482 33 Applications and Activities
This class represent the first activity run by the application. In this case the whole
application is defined by a single Activity which is common practice and is considered
the best development style to use.
The MainActivity class itself extends the AppCompatActivity class.
This class provides backward compatibility with older versions of the Android SDK.
It thus allows code to be written for later versions of the SDK using features not
available on older version, but to allow that code to work on older versions as well
(by emulating the newer features).
The AppCompatActivity class eventually extends the Activity class
which supports the core concepts of being an Activity within an Android application.
The actual class hierarchy is given below. This indicates that Activities are also
a type of Context which is a concept that reoccurs numerous times within the
Android libraries. Both Services and Activities are types of Context and certain things
require a context to be used. Note that the Context is an abstract class and normally
developers will work at the Activity level or the AppCompatActivity level.
Online Resources
Introduction
In this chapter we will look at views and layouts within an Android application.
As described in the previous chapter an Activity can use an XML layout file to define
the contents of its user interface. When using the Empty Activity Template to create
a new project the layout file is called activity_main.xml and is defined within
the layout directory of the res (resources) directory. For example the relationship
between the layout file and the Activity is shown below along with the location of
the files:
The layout file can hold any view elements. The term view is Androids’ termi-
nology for a graphical UI component such as a button, a label or a checkbox etc.
In Android terminology the act of using an XML file to create a User Interface is
known as inflating the view.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 485
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_34
486 34 Android Layouts and Events
Views
Views are all instances of a view class of some type typically defined within the
android.widgets package. Some of the view classes available include:
• TextView A user interface element that displays text to the user.
• EditText A user interface element for entering and modifying text.
• Button A user interface element the user can tap or click on to perform an action.
• Checkbox A checkbox is a specific type of two-state button that can be either
checked or unchecked.
• RadioButton A radio button is a two-state button that can be either checked or
unchecked. When the radio button is unchecked, the user can press or click it to
check it.
Android also provides more complex views such as the Table View which can be
used to create displays made up of rows and columns.
View Layouts
In this section we will look at how views are organised, defined and managed.
View Hierarchy
Strictly speaking the display used to generate the UI for an activity contains a single
top level view, however this top level view can be a container that can hold sub
views. These sub views can themselves be views and this results in a hierarchy of
view elements. This is illustrated below:
View Layouts 487
In terms of terminology all elements in a UI are views, but some views can hold
other views and are known as view groups. In the above diagram the top level view
is a View Group. It contains three sub views. One of these sub views is itself a view
group and holds two further views.
A View Group can be a layout or a container. A Layout is a view group that has
no visual representation of its own where as a container is a view group that typically
has some visual aspect to it such as a tabbed display or a navigation display.
In the above diagram we might have chosen to use a simple Linear Layout for the
top level view group. A LinearLayout is a layout that can organise its contents
vertically or horizontally. Typically it does not have any visual presence itself, instead
it organises the views it contains. Within this layout the sub views can indicate how
they are laid out with respect to the LinearLayout (such as whether they match
the height of the parent layout or not). This is illustrated below:
There are in fact several different layouts available with Android Studio. Each type
of layout provides a different way of organising and managing the views it contains.
Some of these are described below:
• LinearLayout. Organises views across or down the screen (in horizontal or
vertical orientation). When the edge of the screen is reached then the layout
automatically moves to the next line or row across the screen depending on the
orientation.
488 34 Android Layouts and Events
• FrameLayout. A FrameLayout is used to hold a single child view, which will fill
up the available space.
• RelativeLayout. Positions elements relative to each other within the containing
layout.
• TableLayout. Layouts out child views based on rows and columns within the
display.
• AbsoluteLayout. This is used to position views at specific X and Y coordinates
on the screen. The use of this view is generally discouraged as different devices
may have different size displays which may result in unusable UIs being created.
• ConstraintLayout. This is a very flexible layout which essentially replaces the
RelativeLayout as it is more flexible and easier to use with the Android Studio
Layout Editor. It is part of the android extensions to the core Android classes.
Linear Layout
In this case there is a LinearLayout which does not have a visual presence
itself. The LinearLayout contains a TextView with the string “Hello Mobile
World!” in it. The TextView is the first element in the LinearLayout. In this
case the LinearLayout has a vertical orientation thus when the second view
is added it is placed below the first view. The second view is a Button and it is
displayed below the text view. The second button is then placed below the first button.
The XML layout file used for this layout is given below:
View Layouts 489
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello Mobile World!"
android:textColor="#F44336"
android:textSize="36sp"
android:textStyle="bold" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
Note that the TextView and the two Button definitions contain references to
the layout_width and layout_height. The layout_width is set to be
the same as the parents width. In this case their parent is the LinearLayout which
itself has a layout_width of match_parent. Thus the TextView and the two
Buttons will have the same width as the screen.
The layout_height of both the TextView and the two Buttons is set to
wrap_content. This means that they will be only as high as they need to be to
incorporate the visual elements displayed within them (in this case the strings being
displayed).
Constraint Layout
when different size displays need to be supported by your application. It was added
as part of AndroidX libraries, this means that to use it you must add a dependency
to your grade project dependencies to include the constraint layout library:
implementation
'androidx.constraintlayout:constraintlayout:2.0.4'
In the above diagram, the left side of the view is connected to the left edge of the
parent layout.
Alignment Constraint. Using an alignment constraint you can align the edge of a
view to the same edge of another view. For example:
View Layouts 491
In the above diagram, the left side of View C is aligned to the left side of View A.
Baseline alignment. A baseline alignment, aligns the text baseline of a view to the
text baseline of another view.
Constrain to a guideline It is also possible to add a vertical or horizontal guideline
to which you can constrain views, and the guideline will be invisible to app users.
Constrain to a barrier This constraint is similar to a guideline, a barrier is an
invisible line that you can constrain views to. The difference is that a barrier does
not define its own position; instead, the barrier position moves based on the position
of views contained within it.
Containers
A layout is a general purpose View Group, although different layouts may organise
the views they hold in different ways, the layout itself is not expected to have any
visual presence. Layouts also deal directly with the views that they hold.
Containers are different to plain layouts, they meet more specific requirements
which usually involves some aspect of a visual presence on the screen. They also
typically have additional requirements on how many and what kind of child views
they can accept. In many cases a container requires an Adapter class to support their
own requirements.
Examples of Containers include the ScrollView and HorizontalScrollView, the
CardView, the NavigationView and the TabLayout.
• ScrollView and HorizontalScrollView. A ScrollView is a container that is used
to make vertically scrollable views. A scroll view contains a single direct child.
A ScrollView supports Vertical scrolling only, in order to create a horizontally
scrollable view, a HorizontalScrollView is used.
• CardView. This is a container with rounded corners and a shadow based on its
elevation with one child view.
• NavigationView. This is a type of view with a standard navigation menu that is
used for an application. It can be displayed from the navigation drop down button.
• TabLayout. This layout provides a horizontal layout to display tabs. Population
of the tabs to display is done through TabLayout.Tab instances.
492 34 Android Layouts and Events
Creating Displays
You can create the user interface for an Activity in several ways, you can:
• Write the XML layout file by hand.
• You can use the Android Studio Layout Editor which is an interactive UI design
editor.
• You can create the user interface programmatically.
In this section we will look at how to use the Android Studio Layout Editor and
how to create the user interface programmatically.
Although it is certainly possible to write the layout XML file by hand, it is much
easier to use the Android Studio Layout Editor to interactively create the layout file.
If you double click on the activity_main.xml file in Android Studio then
the Layout Editor will automatically be opened for you. In fact we used this in the
last chapter to help us change the text displayed in the application created by the
Empty Activity Template. We will now return to that application.
The Layout Editor is displayed below for the application we created in the last
chapter:
Creating Displays 493
Note that by default the layout editor is organised into three areas, the Palette and
Component Tree to the left, the Attributes pane to the right and the layout drawing
area in the middle.
Also note that in the top right hand corner of the display are three icons labelled
‘Code’, ‘Split’ and ‘Design’. These control what the Layout Editor displays; in
‘Code’ is shows you the XML file, in ‘Split’ it shows you both the XML view and
the design view and in ‘Design’ it shows you the interactive layout design view (as
shown above). If we switch to ‘Split’ then we can see both the XML file and the
design view.
The template created a layout which had a top level ConstraintLayout, however
we now want to use a LinearLayout as the top level view group. We can change
thus using the Component Tree panel. In this panel select the top most node labelled
ConstraintLayout. From the right mouse menu select ‘Convert view …’ as shown
below:
This will cause a new dialog to be displayed that will allow you to select a new
layout to use, select the LinearLayout option.
The Component tree panel will now change to show that the top level ViewGroup
is now a LinearLayout and the display in the design view will change to show the
TextView displayed at the top of the screen.
Currently the LinearLayout has defaulted to the Horizontal orientation, however
we want to use the Vertical orientation, we thus need to change the orientation. To
do this right click on the LinearLayout node in the Component Tree panel and select
the ‘LinearLayout>Convert orientation to vertical’ menu option.
Next we will select the TextView displayed in the design view and look at
the Declared Attributes panel. It now shows that the layout_width and the
layout_height are set to wrap_content:
494 34 Android Layouts and Events
We can now add two buttons to the Linear Layout. To do this select the Button
component in the Palette pane. Now drag the Button view onto the design view, do
this twice to add two buttons. The buttons should each be added below the TextView,
for example:
You could now change the labels not he two buttons such that the first button has
the label add and the second has the label subtract.
Each button also has an id, to make it easier to work with these buttons their ids
could also be changed to be add and sub. All of this can be done via the Declared
Attributes on the right and side of the layout editor.
button.setLayoutParams(LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
addButton.text = "Add"
addButton.setOnClickListener(addButtonHandler())
layout.addView(button)
}
The previous sections have added buttons to our Android application. However, these
buttons currently do not do anything when they are clicked. That is they do not trigger
any behaviour when the user clicks on a button. This is because we are yet to set up
any behaviour to run when a button is clicked. In this section we will look at adding
such behaviour.
496 34 Android Layouts and Events
Event Generation
When a user interacts with an Android application, behind the scenes the ART trans-
lates key presses, taps etc. into events. An Event represents an action happening with
respect to a view such as a button or a menu item. This event is then used to trigger
appropriate behaviour registered with the view.
The operations that handle these events are known as Event Handlers and imple-
ment Event Listener interfaces. These event handlers can be registered on any type
of view, including buttons, menus, lists or even the whole UI itself.
Within the View Handler architecture, the view is the presentation aspect of an
application such as a button or a menu. The Handler defines the behaviour to invoke
when the user interacts with the view. Between these two elements is the Event
which indicates the type of interaction performed such as a click on a button or a
menu selection etc.
This is illustrated in the following diagram:
The Handler is defined by a Listener interface that indicates the type of event that
can be handled. In the above example, there is a View (such as a Button) which can
generate a Click event which will trigger a Handler instance that implements the
View.OnClickListener interface.
This is actually indicative of the style of the interfaces to be implemented, that is
they are all of the form View.On<Event>Listener. Some examples include:
• View.OnClickListener invoked when a view is clicked.
• View.OnDragListener invoked when a drag event is being dispatched to
the view.
• View.OnFocusChangeListener invoked when the focus state of a view
changes.
• View.OnKeyListener invoked when a key event is dispatched to this view.
• View.OnTouchListener invoked when a touch event is dispatched to this
view.
We can now implement a class that will handle a specific event based on the
interface implemented. For example to implement an event handler for the Add
button created in the previous section we can write:
Interacting with the Application 497
We can now bring this all together and update the MainActivity class with
two OnCLickListeners, one for the add button and one for the subtract button. We
can then register these handlers with the appropriate button in the onCreate()
MainActivity member function.
The updated class is:
498 34 Android Layouts and Events
package com.jjh.android.helloworld
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
Now when we run the application in our emulator we can click on the Add and
Subtract buttons and the result is that the total displayed in the textView will be
updated using the value held in the count property. For example:
Interacting with the Application 499
The previous section has introduced the View Handler architecture to allow us to
implement how we want to handle user interactions. However, this seems a bit long
winded as we had to create a couple of inner classes and implement a specific
interface.
As being able to handle a user clicking on a button is such a common event, there
is actually a short cut available when working with Activities. This is the onClick
Resource. This is a resource that can be set within the XML layout file that indicates a
member function, defined within the associated Activity, that can be directly invoked
when a Click event occurs.
The android:onClick attribute takes the name of the member function in the
views context (defined within the Activity) to invoke when the view is clicked. This
name must correspond to a public member function that takes exactly one param-
eter of type View. Thus if you specify android:onClick="sayHello", you
must declare a fun sayHello(v: View): Unit member function in your
Activity class.
For the simple counter application we are creating, this means that we can add
a member function called something like onAddButtonClick(View) to the
MainActivity and add the android:OnClick resource to the Add button
XML configuration information. We can do the same for the Subtract button using
a member function called onSubButtonCLick(View).
For example
500 34 Android Layouts and Events
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/total"
android:textColor="#4CAF50"
android:textSize="24sp"
android:textStyle="bold" />
<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onAddButtonClick"
android:text="@string/add" />
<Button
android:id="@+id/sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onSubButtonClick"
android:text="@string/subtract" />
</LinearLayout>
We must of course update the MainActivity class with these new member
functions. Note that in doing so can remove the inner classes we previous had to
define and delete the lines registering the handlers with the buttons in the onCreate()
member function.
For example, the definition of the MainActivity class now looks like:
Interacting with the Application 501
package com.jjh.android.helloworld
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.TextView
Online Resources
Introduction
In this chapter we will explore the creation of a simple TicTacToe (or Noughts and
Crosses) Android game. This sample Kotlin Android application utilises:
• Kotlin classes, member functions and properties.
• Kotlin Arrays.
• A simple piece of game playing logic.
• While loops, for loops and if statements for flow of control behaviour.
• An Android Activity and associated layout XML file.
• Event handling behaviour shared across all the buttons used for the locations in
the TicTacToe grid.
The aim of the game is to make a line of 3 counters (either X or O) across a 3 by
3 grid. Each player takes a turn to place a counter. The first player to achieve a line
of three (horizontal, vertically or diagonally) wins.
We will begin by identifying the key classes in the game. Note that there is not
necessarily a right or wrong answer here; although one set of classes may be more
obvious or easier to understand than another.
In our case we will start with what data we will need to represent our TicTacToe
game.
Our key data elements include:
• the tic-tac-toe board itself,
• the players involved in the game (both computer and human),
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2021 503
J. Hunt, Beginner’s Guide to Kotlin Programming,
https://doi.org/10.1007/978-3-030-80893-8_35
504 35 Android Tic Tac Toe
• the state of the game, i.e. whose go it is and whether someone has won,
• the moves being made by the players etc.,
• the counters used which are traditionally O and X (hence the alternative name
‘Noughts and Crosses’).
• the Android MainActivity used to run the application.
Based on an analysis of the data one possible set of classes is shown below:
We are now ready to look at the Kotlin implementation of our class design.
Counter Class
The Counter class is given below. There will only ever be two instances of the
Counter class created, one to represent X counters and one to represent O counters.
These two instances are defined as val properties on the Counters’ companion object.
package com.jjh.android.tictactoe
/**
* Represents a Counter used on the board
*/
class Counter private constructor(private val label: String) {
companion object {
// Set up X and O Counter
val X = Counter("X")
val O = Counter("O")
}
Note that the constructor is marked as private, this ensures that only code defined
within the scope of the class Counter can be used to create an instance of a
Counter. Thus it is only the companion object that will create these two instances
of the Counter anywhere in this program.
Move Class 507
Move Class
package com.jjh.android.tictactoe
data class Move(val x: Int,
val y: Int,
val counter: Counter)
It holds the X and Y coordinates within the three by three grid for a given counter.
The root of the Player class hierarchy is presented below. The class main-
tains a reference to the counter used by the player. It also has a property
isAutomatedPlayer to indicate whether it is a human or computer player. This
is set to false by default but can be overriden by subclasses.
package com.jjh.android.tictactoe
Note that counter is defined as a Kotlin property. The class Player is extended
by the classes ComputerPlayer.
package com.jjh.android.tictactoe
import android.util.Log
import java.util.Random
}
}
}
}
return move!!
}
The Board class holds a 3 by 3 grid of cells in the form of a array or arrays. It
also defines the member functions used to verify or make a move on the board. The
checkForWinner() member function determines if there is a winner given the
current board positions.
510 35 Android Tic Tac Toe
package com.jjh.android.tictactoe
import android.util.Log
import kotlin.random.Random
class Board {
init {
// Randomly allocate user to X or O
if (Random.nextInt(100) > 49) {
humanPlayer = Player(Counter.X)
computerPlayer = ComputerPlayer(Counter.O, this)
firstPlayer = humanPlayer
} else {
computerPlayer = ComputerPlayer(Counter.O, this)
humanPlayer = Player(Counter.X)
firstPlayer = computerPlayer
}
}
val isFull: Boolean
get() {
for (row in cells) {
for (c in row) {
if (isCellEmpty(c)) {
return false
}
}
}
return true
}
}
512 35 Android Tic Tac Toe
package com.jjh.android.tictactoe
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
if (finished) {
restartButton.isEnabled = true
}
}
}
}
val buttonSelected =
mainLayout.findViewWithTag<Button>(tag)
buttonSelected.text = move.counter.toString()
return buttonSelected
}
}
The Screen Layout 515
The main part of the display is a TableLayout with multiple Table Rows.
Each table row has three cells, in which each cell contains a button and each button
has a tag associated with it. The tags represent the position of the button within the
table. This information is used within the application to determine which button has
been pressed by the user so that the backing model of the Board can be updated
appropriately.
The activity_main.xml file is given below:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="TicTacToe"
android:textColor="#3F51B5"
android:textSize="36sp"
android:textStyle="bold" />
<TableLayout
android:layout_width="match_parent"
android:layout_height="259dp">
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="80dp"
android:orientation="horizontal">
<Button
android:id="@+id/button0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="0,0"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="0,1"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="0,2"
android:onClick="onButtonClick"
android:text=" " />
</TableRow>
The Screen Layout 517
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="80dp"
android:orientation="horizontal">
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="1,0"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="1,1"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="1,2"
android:onClick="onButtonClick"
android:text=" " />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="80dp"
android:orientation="horizontal">
518 35 Android Tic Tac Toe
<Button
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="2,0"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="2,1"
android:onClick="onButtonClick"
android:text=" " />
<Button
android:id="@+id/button8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="2,2"
android:onClick="onButtonClick"
android:text=" " />
</TableRow>
</TableLayout>
<Button
android:id="@+id/restartButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restart"
android:onClick="onRestartButtonClick"
android:textSize="30dp" />
</LinearLayout>
When the game is run on an AVD emulator the display presented to the user is shown
below:
Running the Game 519
The human user can click on any one of the buttons in the three by three grid.
The application will register this and display the users counter on the button. The
application will then generate a computer player move. The result is that the user
will see a display similar to:
The user and the computer will continue to play the game until either a tie occurs
or a player has won, at this point the Restart button will be enlaced: