Chapter 1. Kotlin Essentials
Kotlin was created by the JetBrains team from St. Petersburg, Russia. JetBrains is perhaps best known for the IntelliJ Idea IDE, the basis for Android Studio. Kotlin is now used in a wide variety of environments across multiple operating systems. It has been nearly five years since Google announced support for Kotlin on Android. According to the Android Developers Blog, as of 2021, over 1.2 million apps in the Google Play store use Kotlin, including 80% of the top one thousand apps.
If you’ve picked up this book, we are assuming that you are already an Android developer and that you are familiar with Java.
Kotlin was designed to interoperate with Java. Even its name, taken from an island near St. Petersburg, is a sly allusion to Java, an island in Indonesia. Though Kotlin supports other platforms (iOS, WebAssembly, Kotlin/JS, etc.), a key to Kotlin’s broad use is its support for the Java virtual machine (JVM). Since Kotlin can be compiled to Java bytecode, it can run anywhere that a JVM runs.
Much of the discussion in this chapter will compare Kotlin to Java. It’s important to understand, though, that Kotlin is not just warmed-over Java with some added bells and whistles. Kotlin is a new and different language with connections to Scala, Swift, and C# that are nearly as strong as its connection with Java. It has its own styles and its own idioms. While it is possible to think Java and write Kotlin, thinking in idiomatic Kotlin will reveal the full power of the language.
We realize that there may be some Android developers who have been working with Kotlin for some time, and who have never written any Java at all. If this sounds like you, you may be able to skim this chapter and its review of the Kotlin language. However, even if you are fairly handy with the language, this may be a good chance to remind yourself of some of the details.
This chapter isn’t meant to be a full-fledged primer on Kotlin, so if you are completely new to Kotlin, we recommend the excellent Kotlin in Action.1 Instead, this chapter is a review of some Kotlin basics: the type system, variables, functions, and classes. Even if you are not a Kotlin language expert, it should provide enough of a foundation for you to understand the rest of the book.
As with all statically typed languages, Kotlin’s type system is the meta language that Kotlin uses to describe itself. Because it is an essential aspect for discussing Kotlin, we’ll start by reviewing it.
The Kotlin Type System
Like Java, Kotlin is a statically typed language. The Kotlin compiler
knows the type of every entity that a program manipulates. It can
make deductions2
about those entities and, using those deductions,
identify errors that will occur when code contradicts them. Type
checking allows a compiler to catch and flag an entire large class of
programming errors. This section highlights some of the
most interesting features of Kotlin’s type system, including the
Unit
type, functional types, null safety, and
generics.
Primitive Types
The most obvious difference between Java’s and Kotlin’s type systems is that Kotlin has no notion of a primitive type.
Java has the types int
, float
, boolean
, etc. These types are
peculiar in that they do not inherit from Java’s base type, Object
.
For instance, the statement int n = null;
is not legal Java. Neither
is List<int> integers;
. In order to mitigate this inconsistency, each
Java primitive type has a boxed type equivalent. Integer
, for instance, is the analog of int
; Boolean
of boolean
; and so on. The
distinction between primitive and boxed types has nearly vanished
because, since Java 5, the Java compiler automatically converts between
the boxed and unboxed types. It is now legal to say
Integer i = 1
.
Kotlin does not have primitive types cluttering up its type system.
Its single base type, Any
, analogous to Java’s Object
,
is the root of the entire Kotlin type hierarchy.
Note
Kotlin’s internal representation of simple types is not connected to
its type system. The Kotlin compiler has sufficient information to
represent, for instance, a 32-bit integer with as much
efficiency as any other language. So, writing val i: Int = 1
might
result in using a primitive type or a boxed type, depending on how the i
variable is used in the code. Whenever possible, the Kotlin compiler will
use primitive types.
Null Safety
A second major difference between Java and Kotlin is that
nullability is part of Kotlin’s type system. A nullable type is
distinguished from its nonnullable analog by the question mark at
the end of its name; for example, String
and String?
, Person
and
Person?
. The Kotlin compiler will allow the assignment of null
to a
nullable type:
var name: String? = null
. It will not, however, permit
var name: String = null
(because String
is not a nullable type).
Any
is the root of the Kotlin type system, just like Object
in Java. However, there’s a significant difference: Any
is the base class for all nonnullable classes, while Any?
is the base class for all nullable ones. This is the basis
of null safety. In other words, it may be useful to think of Kotlin’s type system as two identical
type trees: all nonnullable types are subtypes of Any
and all nullable types are subtypes of Any?
.
Variables must be initialized. There is no default value for a variable. This code, for instance, will generate a compiler error:
val
name
:
String
// error! Nonnullable types must be initialized!
As described earlier, the Kotlin compiler makes deductions using type
information. Often the compiler can figure out the type of an
identifier from information it already has. This process is called type inference. When the compiler can infer a type, there is no need for the developer to specify it. For instance, the assignment var name = "Jerry"
is perfectly legal, despite the fact that the type of the variable name
has not been specified. The compiler can infer that the variable name
must be a String
because it is assigned the value "Jerry"
(which is a String
).
Inferred types can be surprising, though. This code will generate a compiler error:
var
name
=
"Jerry"
name
=
null
The compiler inferred the type String
for the variable name
,
not the type String?
. Because String
is not a nullable type,
attempting to assign null
to it is illegal.
It is important to note that a nullable type is not the same as its
nonnullable counterpart. As makes sense, a nullable type behaves as
the supertype of the related nonnullable type. This code, for instance, compiles with no problem because a String
is a String?
:
val
name
=
Jerry
fun
showNameLength
(
name
:
String?
)
{
// Function accepts a nullable parameter
// ...
}
showNameLength
(
name
)
On the other hand, the following code will not compile at all, because a String?
is not a String
:
val
name
:
String?
=
null
fun
showNameLength
(
name
:
String
)
{
// This function only accepts non-nulls
println
(
name
.
length
)
}
showNameLength
(
name
)
// error! Won't compile because "name"
// can be null
Simply changing the type of the parameter will not entirely fix the problem:
val
name
:
String?
=
null
fun
showNameLength
(
name
:
String?
)
{
// This function now accepts nulls
println
(
name
.
length
)
// error!
}
showNameLength
(
name
)
// Compiles
This snippet fails with the error Only safe (?.) or non-null asserted (!!.)
calls are allowed on a nullable receiver of type String?
.
Kotlin requires that nullable variables be handled safely—in a way that cannot generate a null pointer exception. In order
to make the code compile, it must correctly handle the case in which
name
is null
:
val
name
:
String?
=
null
fun
showNameLength
(
name
:
String?
)
{
println
(
if
(
name
==
null
)
0
else
name
.
length
)
// we will use an even nicer syntax shortly
}
Kotlin has special operators, ?.
and ?:
, that simplify working
with nullable entities:
val
name
:
String?
=
null
fun
showNameLength
(
name
:
String?
)
{
println
(
name
?.
length
?:
0
)
}
In the preceding example, when name
is not null
, the value of name?.length
is the same as the value of name.length
. When name
is null
, however, the value of name?.length
is null
. The expression does not throw a null pointer exception. Thus, the first operator in the previous example, the safe operator ?.
, is syntactically equivalent to:
if
(
name
==
null
)
null
else
name
.
length
The second operator, the elvis operator ?:
, returns the left expression if it is non-null, or the right expression otherwise. Note that the expression on the right-hand side is evaluated only if the left expression is null.
if
(
name
?.
length
==
null
)
0
else
name
.
length
The Unit Type
In Kotlin, everything has a value. Always. Once you understand this,
it is not difficult to imagine that even a method that doesn’t specifically return
anything has a default value. That default value is named Unit
.
Unit
is the name of exactly one object, the value things have if
they don’t have any other value. The type of the Unit
object is,
conveniently, named Unit
.
The whole concept of Unit
can seem odd to Java developers who are
used to a distinction between expressions—things that have a value—and statements—things that don’t.
Java’s conditional is a great example of the distinction between a statement and an expression because it has one of each! In Java you can say:
if
(
maybe
)
doThis
()
else
doThat
();
You cannot, however, say:
int
n
=
if
(
maybe
)
doThis
()
else
doThat
();
Statements, like the if
statement, do not return a value. You cannot
assign the value of an if
statement to a variable, because if
statements don’t return anything.
The same is true for loop statements, case statements, and so on.
Java’s if
statement, however, has an analog, the ternary expression.
Since it is an expression, it returns a value and that value
can be assigned. This is legal Java (provided both doThis
and
doThat
return integers):
int
n
=
(
maybe
)
?
doThis
()
:
doThat
();
In Kotlin, there is no need for two conditionals because if
is an expression and returns a value.
For example, this is perfectly legal:
val
n
=
if
(
maybe
)
doThis
()
else
doThat
()
In Java, a method with void
as the return type is like a statement. Actually, this is a
bit of a misnomer because void
isn’t a type. It is a reserved word in the Java
language that indicates that the method does not return a value. When Java introduced generics, it
introduced the type Void
to fill the void (intended!). The two
representations of “nothing,” the keyword and the type, however, are
confusing and inconsistent: a function whose return type is Void
must explicitly return null
.
Kotlin is much more consistent: all functions return a value and have a type. If the code for a function does not return a value explicitly, the function has the value Unit
.
Function Types
Kotlin’s type system supports function types. For example, the following code defines a variable, func
, whose value is a function, the lambda { x -> x.pow(2.0) }
:
val
func
:
(
Double
)
->
Double
=
{
x
->
x
.
pow
(
2.0
)
}
Since func
is a function that takes one Double
type argument and returns a Double
, it’s type is (Double) ->
Double
.
In the previous example, we specified the type of func
explicitly.
However, the Kotlin compiler can infer a lot about the type of the
variable func
from the value assigned to it. It knows the return
type because it knows the type of pow
. It doesn’t, however, have
enough information to guess the type of the parameter x
. If we
supply that, though, we can omit the type specifier for the variable:
val
func
=
{
x
:
Double
->
x
.
pow
(
2.0
)}
Note
Java’s type system cannot describe a function type—there is no way
to talk about functions outside the context of the classes that
contain them. In Java, to do something similar to the previous example, we would use the functional type Function
, like this:
Function
<
Double
,
Double
>
func
=
x
->
Math
.
pow
(
x
,
2.0
);
func
.
apply
(
256.0
);
The variable func
has been assigned an anonymous instance of the
type Function
whose method apply
is the given lambda.
Thanks to function types, functions can receive other functions as parameters or return them as values. We call these higher-order functions.
Consider a template for a Kotlin type: (A, B) -> C
. It describes a function
that takes two parameters, one of type A
and one of type B
(whatever types those may be), and returns a value of type C
. Because
Kotlin’s type language can describe functions, A
, B
, and C
can all,
themselves, be functions.
If that sounds rather meta, it’s because it is. Let’s make it more
concrete. For A
in the template, let’s substitute (Double, Double)
->
Int
. That’s a function that takes two Double
s and returns an Int
. For
B
, let’s just substitute a Double
. So far, we have ((Double,
Double)
->
Int,
Double)
->
C
.
Finally, let’s say our new functional type returns a (Double)
->
Int
,
a function that takes one parameter, a Double
, and returns an Int
. The following code shows the complete signature for our hypothetical function:
fun
getCurve
(
surface
:
(
Double
,
Double
)
->
Int
,
x
:
Double
):
(
Double
)
->
Int
{
return
{
y
->
surface
(
x
,
y
)
}
}
We have just described a function type that takes two arguments. The first is a function (surface
) of two parameters, both Double
s, that returns an Int
. The second is a Double
(x
).
Our getCurve
function returns a function that takes one parameter, a
Double
(y
),
and returns an Int
.
The ability to pass functions as arguments into other functions is a pillar of functional languages.
Using higher-order functions, you can reduce code redundancy, while not having to create new
classes as you would in Java (subclassing Runnable
or Function
interfaces).
When used wisely, higher-order functions improve code
readability.
Generics
Like Java, Kotlin’s type system supports type variables. For instance:
fun
<
T
>
simplePair
(
x
:
T
,
y
:
T
)
=
Pair
(
x
,
y
)
This function creates a Kotlin Pair
object in which both of the
elements must be of the same type. Given this definition,
simplePair("Hello", "Goodbye")
and simplePair(4, 5)
are both
legal, but simplePair("Hello", 5)
is not.
The generic type denoted as T
in the definition of simplePair
is a type variable:
the values it can take are Kotlin types (in this example, String
or
Int
). A function (or a class) that uses a type variable is said to be
generic.
Variables and Functions
Now that we have Kotlin’s type language to support us, we can start to discuss the syntax of Kotlin itself.
In Java the top-level syntactic entity is the class. All variables and methods are members of some class or other, and the class is the main element in a homonymous file.
Kotlin has no such limitations. You can put your entire program in one file, if you like (please don’t). You can also define variables and functions outside any class.
Variables
There are two ways to declare a variable: with the keywords val
and
var
. The keyword is required, is the first thing on the line, and
introduces the declaration:
val
ronDeeLay
=
"the night time"
The keyword val
creates a variable that is read-only: it cannot be
reassigned. Be careful, though! You might think val
is like a Java variable
declared using the final
keyword. Though similar, it is not the same!
Although it cannot be reassigned, a val
definitely can change value! A val
variable in Kotlin is more like a Java class’s field, which has a getter but no setter,
as shown in the following code:
val
surprising
:
Double
get
()
=
Math
.
random
()
Every time surprising
is accessed, it will return a different random value. This is an example of a property with
no backing field. We’ll cover properties later in this chapter.
On the other hand, if we had written val rand = Random()
, then rand
wouldn’t change in value and would be more like a final
variable in Java.
The second keyword, var
, creates a familiar mutable variable: like a
little box that holds the last thing that was put into it.
In the next section, we will move on to one of Kotlin’s features as a functional language: lambdas.
Lambdas
Kotlin supports function literals: lambdas. In Kotlin, lambdas are always surrounded by curly braces.
Within the braces, the argument list is to the left of an arrow, ->
, and the expression that is the
value of executing the lambda is to the right, as shown in the following code:
{
x
:
Int
,
y
:
Int
->
x
*
y
}
By convention, the returned value is the value of the last expression in
the body of the lambda. For example, the function shown in the following code is of type
(Int, Int) -> String
:
{
x
:
Int
,
y
:
Int
->
x
*
y
;
"down on the corner"
}
Kotlin has a very interesting feature that allows actually extending the language. When the last argument to a function is another function (the function is higher-order), you can move the lambda expression passed as a parameter out of the parentheses that normally delimit the actual parameter list, as shown in the following code:
// The last argument, "callback", is a function
fun
apiCall
(
param
:
Int
,
callback
:
()
->
Unit
)
This function would typically be used like this:
apiCall
(
1
,
{
println
(
"I'm called back!"
)})
But thanks to the language feature we mentioned, it can also be used like this:
apiCall
(
1
)
{
println
(
"I'm called back!"
)
}
This is much nicer, isn’t it? Thanks to this feature, your code can be more readable. A more advanced
usage of this feature are DSL
s.3
Extension Functions
When you need to add a new method to an existing class, and that class comes from a dependency whose source code you don’t own, what do you do?
In Java, if the class isn’t final
, you can subclass it. Sometimes this isn’t ideal, because there’s one more
type to manage, which adds complexity to the project. If the class is final
, you can define a static method
inside some utility class of your own, as shown in the following code:
class
FileUtils
{
public
static
String
getWordAtIndex
(
File
file
,
int
index
)
{
/* Implementation hidden for brevity */
}
}
In the previous example, we defined a function to get a word in a text file, at a given index. On the use site, you’d write String word =
getWordAtIndex(file,
3)
, assuming you make the static import of
FileUtils.getWordAtIndex
.
That’s fine, we’ve been doing that for years in Java, and it works.
In Kotlin, there’s one more thing you can do. You have the ability to define a new method on a class, even though it isn’t a real member-function of that class. So you’re not really extending the class, but on the use site it feels like you added a method to the class. How is this possible? By defining an extension function, as shown in the following code:
// declared inside FileUtils.kt
fun
File
.
getWordAtIndex
(
index
:
Int
):
String
{
val
context
=
this
.
readText
()
// 'this' corresponds to the file
return
context
.
split
(
' '
).
getOrElse
(
index
)
{
""
}
}
From inside the declaration of the extension function, this
refers to the receiving type instance (here, a File
).
You only have access to public and internal attributes and methods, so private
and protected
fields are
inaccessible—you’ll understand why shortly.
On the use site, you would write val word = file.getWordAtIndex(3)
. As you can see, we invoke the getWordAtIndex()
function on a File
instance, as if the File
class had the getWordAtIndex()
member-function. That makes the use
site more expressive and readable. We didn’t have to come up with a name for a new utility class: we can declare
extension functions directly at the root of a source file.
Note
Let’s have a look at the decompiled version of getWordAtIndex
:
public
class
FileUtilsKt
{
public
static
String
getWordAtIndex
(
File
file
,
int
index
)
{
/* Implementation hidden for brevity */
}
}
When compiled, the generated bytecode of our extension function is the equivalent of a static method which
takes a File
as its first argument. The enclosing class, FileUtilsKt
, is named after the name of the source
file (FileUtils.kt) with the “kt” suffix.
That explains why we can’t access private fields in an extension function: we are just adding a static method that takes the receiving type as a parameter.
There’s more! For class attributes, you can declare extension properties. The idea is exactly the same—you’re not really extending a class, but you can make new attributes accessible using the dot notation, as shown in the following code:
// The Rectangle class has width and height properties
val
Rectangle
.
area
:
Double
get
()
=
width
*
height
Notice that this time we used val
(instead of fun
) to declare the extension property. You would use it like so:
val area = rectangle.area
.
Extension functions and extension properties allow you to extend classes’ capabilities, with a nice dot-notation usage, while still preserving separation of concern. You’re not cluttering existing classes with specific code for particular needs.
Classes
Classes in Kotlin, at first, look a lot like they do in Java: the
class
keyword, followed by the block that defines the class. One of
Kotlin’s killer features, though, is the syntax for the constructor
and the ability to declare properties within it. The following code shows the definition of a simple Point
class along with a couple of uses:
class
Point
(
val
x
:
Int
,
var
y
:
Int?
=
3
)
fun
demo
()
{
val
pt1
=
Point
(
4
)
assertEquals
(
3
,
pt1
.
y
)
pt1
.
y
=
7
val
pt2
=
Point
(
7
,
7
)
assertEquals
(
pt2
.
y
,
pt1
.
y
)
}
Class Initialization
Notice that in the preceding code, the constructor of Point
is embedded in
the declaration of the class. It is called the primary constructor.
Point
’s primary constructor declares two
class properties, x
and y
, both of which are integers. The first,
x
, is read-only. The second, y
, is mutable and nullable, and has a
default value of 3.
Note that the var
and val
keywords are very significant! The
declaration class Point(x: Int, y: Int)
is very different from the
preceding declaration because it does not declare any member properties.
Without the keywords, identifiers x
and y
are simply arguments to
the constructor. For example, the following code will generate an
error:
class
Point
(
x
:
Int
,
y
:
Int?
)
fun
demo
()
{
val
pt
=
Point
(
4
)
pt
.
y
=
7
// error! Variable expected
}
The Point
class in this example has only one constructor, the one
defined in its declaration. Classes are not limited to this single constructor,
though. In Kotlin, you can also define both secondary constructors and
initialization blocks, as shown in the following definition of the Segment
class:
class
Segment
(
val
start
:
Point
,
val
end
:
Point
)
{
val
length
:
Double
=
sqrt
(
(
end
.
x
-
start
.
x
).
toDouble
().
pow
(
2.0
)
+
(
end
.
y
-
start
.
y
).
toDouble
().
pow
(
2.0
))
init
{
println
(
"Point starting at
$
start
with length
$
length
"
)
}
constructor
(
x1
:
Int
,
y1
:
Int
,
x2
:
Int
,
y2
:
Int
)
:
this
(
Point
(
x1
,
y1
),
Point
(
x2
,
y2
))
{
println
(
"Secondary constructor"
)
}
}
There are some other things that are of interest in this example.
First of all, note that a secondary constructor must delegate to the
primary constructor, the : this(...)
, in its declaration. The
constructor may have a block of code, but it is required to delegate,
explicitly, to the primary constructor, first.
Perhaps more interesting is the order of execution of the code in the
preceding declaration. Suppose one were to create a new Segment
, using
the secondary constructor. In what order would the print statements
appear?
Well! Let’s try it and see:
>>> vals
=
Segment(
1
,2
,3
,4
)
Point starting at Point(
x
=
1
,y
=
2
)
with length2
.8284271247461903 Secondary constructor
This is pretty interesting. The init
block is run before the code
block associated with secondary constructor! On the other hand, the
properties length
and start
have been initialized with their
constructor-supplied values. That means that the primary constructor
must have been run even before the init
block.
In fact, Kotlin guarantees this ordering: the primary constructor (if
there is one) is run first. After it finishes, init
blocks are run
in declaration order (top to bottom). If the new instance is being
created using a secondary constructor, the code block associated with
that constructor is the last thing to run.
Properties
Kotlin variables, declared using val
or var
in a constructor, or at
the top level of a class, actually define a property. A property, in
Kotlin, is like the combination of a Java field and its getter (if
the property is read-only, defined with val
), or its getter and setter (if
defined with var
).
Kotlin supports customizing the accessor and mutator for a property
and has special syntax for doing so, as shown here in the definition of
the class Rectangle
:
class
Rectangle
(
val
l
:
Int
,
val
w
:
Int
)
{
val
area
:
Int
get
()
=
l
*
w
}
The property area
is synthetic: it is computed from the values
for the length and width. Because it wouldn’t make sense to assign
to area
, it is a val
, read-only, and does not have a set()
method.
Use standard “dot” notation to access the value of a property:
val
rect
=
Rectangle
(
3
,
4
)
assertEquals
(
12
,
rect
.
area
)
In order to further explore custom property getters and setters,
consider a class that has a hash code that is used frequently
(perhaps instances are kept in a Map
), and that is quite
expensive to calculate. As a design decision, you decide to
cache the hash code, and to set it when the value of a class
property changes. A first try might look something like this:
// This code doesn't work (we'll see why)
class
ExpensiveToHash
(
_summary
:
String
)
{
var
summary
:
String
=
_summary
set
(
value
)
{
summary
=
value
// unbounded recursion!!
hashCode
=
computeHash
()
}
// other declarations here...
var
hashCode
:
Long
=
computeHash
()
private
fun
computeHash
():
Long
=
...
}
The preceding code will fail because of unbounded recursion: the assignment to summary
is a call to summary.set()
! Attempting to set the value of the property
inside its own setter won’t work.
Kotlin uses the special identifier field
to address this problem.
The following shows the corrected version of the code:
class
ExpensiveToHash
(
_summary
:
String
)
{
var
summary
:
String
=
_summary
set
(
value
)
{
field
=
value
hashCode
=
computeHash
()
}
// other declarations here...
var
hashCode
:
Long
=
computeHash
()
private
fun
computeHash
():
Long
=
...
}
The identifier field
has a special meaning only within the custom
getter and setter, where it refers to the backing field that
contains the property’s state.
Notice, also, that the preceding code demonstrates the idiom for initializing a property that has a custom getter/setter with a value provided to the class constructor. Defining properties in a constructor parameter list is really handy shorthand. If a few property definitions in a constructor had custom getters and setters, though, it could make the constructor really hard to read.
When a property with a custom getter and setter must be initialized
from the constructor, the property is defined, along with its custom
getter and setter, in the body of the class. The property is
initialized with a parameter from the constructor (in this case,
_summary
). This illustrates, again, the importance of the keywords
val
and var
in a constructor’s parameter list. The parameter
_summary
is just a parameter, not a class property, because it is
declared without either keyword.
lateinit Properties
There are times when a variable’s value is not available at the site
of its declaration. An obvious example of this for Android
developers is a UI widget used in an Activity
or Fragment
. It is not
until the onCreate
or onCreateView
method runs that the variable,
used throughout the activity to refer to the widget, can be
initialized. The button
in this example, for instance:
class
MyFragment
:
Fragment
()
{
private
var
button
:
Button?
=
null
// will provide actual value later
}
The variable must be initialized. A standard technique, since we
can’t know the value, yet, is to make the variable nullable and
initialize it with null
.
The first question you should ask yourself in this situation is
whether it is really necessary to define this variable at this
moment and at this location. Will the button
reference really be
used in several methods or is it really only used in one or two
specific places? If the latter, you can eliminate the class global altogether.
However, the problem with using a nullable type is that whenever you use
button
in your code, you will have to check for nullability. For example:
button?.setOnClickListener { .. }
. A couple of variables like this and you’ll end up with a lot of annoying question marks! This can look particularly cluttered if you are used to Java and its simple dot notation.
Why, you might ask, does Kotlin prevent me from declaring the button
using a non-null type when you are sure that you will initialize it
before anything tries to access it? Isn’t there a way to relax the
compiler’s initialization rule just for this button
?
It’s possible. You can do exactly that using the lateinit
modifier,
as shown in the following code:
class
MyFragment
:
Fragment
()
{
private
lateinit
var
button
:
Button
// will initialize later
}
Because the variable is declared lateinit
, Kotlin will let you
declare it without assigning it a value. The variable must be mutable,
a var
, because, by definition, you will assign a value to it, later.
Great—problem solved, right?
We, the authors, thought exactly that when we started using Kotlin. Now,
we lean toward using lateinit
only when absolutely necessary,
and using nullable values instead. Why?
When you use lateinit
, you’re telling the compiler, “I don’t have a value to give you right now. But I’ll give you a value later, I promise.” If the Kotlin compiler could talk, it would answer, “Fine! You say you know what you’re doing. If something goes wrong, it’s on you.” By using the lateinit
modifier, you disable Kotlin’s
null safety for your variable. If you forget to initialize the
variable or try to call some method on it before it’s initialized,
you’ll get an UninitializedPropertyAccessException
, which is
essentially the same as getting a NullPointerException
in Java.
Every single time we’ve used lateinit
in our code, we’ve been burned
eventually. Our code might work in all of the cases we’d foreseen.
We’ve been certain that we didn’t miss anything… and we were wrong.
When you declare a variable lateinit
you’re making
assumptions that the compiler cannot prove. When you or other
developers refactor the code afterward, your careful design might get
broken. Tests might catch the error. Or not.4 In our
experience, using lateinit
always resulted in runtime crashes. How did
we fix that? By using a nullable type.
When you use a nullable type instead of lateinit
, the Kotlin
compiler will force you to check for nullability in your code, exactly
in the places that it might be null. Adding a few question marks is
definitely worth the trade-off for more robust code.
Lazy Properties
It’s a common pattern in software engineering to put off creating and initializing an object until it is actually needed. This pattern is known as lazy initialization, and is especially common on Android, since allocating a lot of objects during app startup can lead to a longer startup time. Example 1-1 is a typical case of lazy initialization in Java.
Example 1-1. Java lazy initialization
class
Lightweight
{
private
Heavyweight
heavy
;
public
Heavyweight
getHeavy
()
{
if
(
heavy
==
null
)
{
heavy
=
new
Heavyweight
();
}
return
heavy
;
}
}
The field heavy
is initialized with a new instance of the
class Heavyweight
(which is, presumably, expensive to create) only when
its value is first requested with a call, for example, to lightweight.getHeavy()
.
Subsequent calls to getHeavy()
will return the cached instance.
In Kotlin, lazy initialization is a part of the language. By using
the directive by lazy
and providing an initialization block, the
rest of the lazy instantiation is implicit, as shown in
Example 1-2.
Example 1-2. Kotlin lazy initialization
class
Lightweight
{
val
heavy
by
lazy
{
// Initialization block
Heavyweight
()
}
}
We will explain this syntax in greater detail in the next section.
Note
Notice that the code in Example 1-1 isn’t thread-safe.
Multiple threads calling Lightweight
’s getHeavy()
method simultaneously
might end up with different instances of Heavyweight
.
By default, the code in Example 1-2 is thread-safe. Calls to
Lightweight::getHeavy()
will be synchronized so that only one thread at
a time is in the initialization block.
Fine-grained control of concurrent access to a lazy initialization block
can be managed using LazyThreadSafetyMode
.
A Kotlin lazy value will not be initialized until a call is made at
runtime. The first time the property heavy
is referenced, the initialization block will be run.
Delegates
Lazy properties are an example of a more general Kotlin feature, called
delegation. A declaration uses the keyword by
to define a
delegate that is responsible for getting and setting the value of the
property. In Java, one could accomplish something similar with, for example,
a setter that passed its argument on as a parameter to a call to a
method on some other object, the delegate.
Because Kotlin’s lazy initialization feature is an excellent example of the power of idiomatic Kotlin, let’s take a minute to unpack it.
The first part of the declaration in Example 1-2 reads val
heavy
. This is, we know, the declaration of a read-only variable,
heavy
. Next comes the keyword by
, which introduces a delegate.
The keyword by
says that the next identifier in the declaration is
an expression that will evaluate to the object that will be
responsible for the value of heavy
.
The next thing in the declaration is the identifier lazy
. Kotlin is
expecting, an expression. It turns out that lazy
is just a
function! It is a function that takes a single argument, a lambda,
and returns an object. The object that it returns is a Lazy<T>
where T
is the type returned by the lambda.
The implementation of a Lazy<T>
is quite simple:
the first time it is called it runs the lambda and caches its value.
On subsequent calls it returns the cached value.
Lazy delegation is just one of many varieties of property
delegation. Using keyword by
, you can also define observable properties (see the Kotlin documentation for delegated properties). Lazy
delegation is, though, the most common property delegation used in
Android code.
Companion Objects
Perhaps you are wondering what Kotlin did with static variables. Have no fear; Kotlin uses companion objects. A companion object is a singleton object always related to a Kotlin class. Although it isn’t required, most often the definition of a companion object is placed at the bottom of the related class, as shown here:
class
TimeExtensions
{
// other code
companion
object
{
const
val
TAG
=
"TIME_EXTENSIONS"
}
}
Companion objects can have names, extend classes, and inherit interfaces.
In this example, TimeExtension
’s companion object is named StdTimeExtension
and inherits the interface Formatter
:
interface
Formatter
{
val
yearMonthDate
:
String
}
class
TimeExtensions
{
// other code
companion
object
StdTimeExtension
:
Formatter
{
const
val
TAG
=
"TIME_EXTENSIONS"
override
val
yearMonthDate
=
"yyyy-MM-d"
}
}
When referencing a member of a companion object from outside a class that contains it, you must qualify the reference with the name of the containing class:
val
timeExtensionsTag
=
TimeExtensions
.
StdTimeExtension
.
TAG
A companion object is initialized when Kotlin loads the related class.
Data Classes
There is a category of classes so common that, in Java, they
have a name: they are called POJOs, or plain old Java objects. The idea
is that they are simple representations of structured data. They are a
collection of data members (fields), most of which have getters and
setters, and just a few other methods: equals
, hashCode
, and
toString
. These kinds of classes are so common that Kotlin has made
them part of the language. They are called data classes.
We can improve our definition of the Point
class by making it a data class:
data
class
Point
(
var
x
:
Int
,
var
y
:
Int?
=
3
)
What’s the difference between this class, declared using the data
modifier, and the original, declared without it? Let’s try a simple experiment,
first using the original definition of Point
(without the data
modifier):
class
Point
(
var
x
:
Int
,
var
y
:
Int?
=
3
)
fun
main
()
{
val
p1
=
Point
(
1
)
val
p2
=
Point
(
1
)
println
(
"Points are equals:
${
p1
==
p2
}
"
)
}
The output from this small program will be "Points are equals: false"
.
The reason for this perhaps unexpected result is that Kotlin
compiles p1 == p2
as p1.equals(p2)
. Since our first definition of
the Point
class did not override the equals
method, this turns into
a call to the equals
method in Point
’s base class, Any
.
Any
’s implementation of equals
returns true
only when an object is compared to itself.
If we try the same thing with the new definition of Point
as a data
class, the program will print "Points are equals: true"
. The new
definition behaves as intended because a data class automatically
includes overrides for the methods equals
,
hashCode
, and toString
.
Each of these automatically generated methods depends on all of a class’s
properties.
For example, the data class
version of Point
contains an equals
method that is equivalent to this:
override
fun
equals
(
o
:
Any?
):
Boolean
{
// If it's not a Point, return false
// Note that null is not a Point
if
(
o
!is
Point
)
return
false
// If it's a Point, x and y should be the same
return
x
==
o
.
x
&&
y
==
o
.
y
}
In addition to providing default implementations of equals
and
hashCode
, a data class
also provides the copy
method.
Here’s an example of its use:
data
class
Point
(
var
x
:
Int
,
var
y
:
Int?
=
3
)
val
p
=
Point
(
1
)
// x = 1, y = 3
val
copy
=
p
.
copy
(
y
=
2
)
// x = 1, y = 2
Kotlin’s data classes are a perfect convenience for a frequently used idiom.
In the next section, we examine another special kind of class: enum classes.
Enum Classes
Remember when developers were being advised that enums were too expensive for Android? Fortunately, no one is even suggesting that anymore: use enum classes to your heart’s desire!
Kotlin’s enum classes are very similar to Java’s enums. They create a class that cannot be subclassed and that has a fixed set of instances. Also as in Java, enums cannot subclass other types but can implement interfaces and can have constructors, properties, and methods. Here are a couple of simple examples:
enum
class
GymActivity
{
BARRE
,
PILATES
,
YOGA
,
FLOOR
,
SPIN
,
WEIGHTS
}
enum
class
LENGTH
(
val
value
:
Int
)
{
TEN
(
10
),
TWENTY
(
20
),
THIRTY
(
30
),
SIXTY
(
60
);
}
Enums work very well with Kotlin’s when
expression. For example:
fun
requiresEquipment
(
activity
:
GymActivity
)
=
when
(
activity
)
{
GymActivity
.
BARRE
->
true
GymActivity
.
PILATES
->
true
GymActivity
.
YOGA
->
false
GymActivity
.
FLOOR
->
false
GymActivity
.
SPIN
->
true
GymActivity
.
WEIGHTS
->
true
}
When the when
expression is used to assign a variable, or as an expression body of a function as in the previous example, it must be exhaustive. An exhaustive when
expression is
one that covers every possible value of its argument (in this case,
activity
). A standard way of assuring that a when
expression is
exhaustive is to include an else
clause. The else
clause matches
any value of the argument that is not explicitly mentioned in its case list.
In the preceding example, to be exhaustive, the when
expression must
accommodate every possible value of the function parameter activity
.
The parameter is of type GymActivity
and, therefore, must be one of
that enum’s instances. Because an enum has a known set of instances,
Kotlin can determine that all of the possible values are covered as
explicitly listed cases and permit the omission of the else
clause.
Omitting the else
clause like this has a really nice advantage: if
we add a new value to the GymActivity
enum, our code suddenly won’t
compile. The Kotlin compiler detects that the when
expression is no
longer exhaustive. Almost certainly, when you add a new case to an
enum, you want to be aware of all the places in your code that have to
adapt to the new value. An exhaustive when
expression that does not
include an else
case does exactly that.
Note
What happens if a when
statement need not return a value (for
instance, a function in which the when
statement’s value is not the
value of the function)?
If the when
statement is not used as an expression, the Kotlin compiler
doesn’t force it to be exhaustive. You will, however, get a lint warning
(a yellow flag, in Android Studio) that tells you that it is recommended
that a when
expression on enum be exhaustive.
There’s a trick that will force Kotlin to interpret any when
statement as an expression (and, therefore, to be exhaustive). The extension function defined in
Example 1-3 forces the when
statement to return a
value, as we see in Example 1-4. Because it must have a value,
Kotlin will insist that it be exhaustive.
Example 1-3. Forcing when
to be exhaustive
val
<
T
>
T
.
exhaustive
:
T
get
()
=
this
Example 1-4. Checking for an exhaustive when
when
(
activity
)
{
GymActivity
.
BARRE
->
true
GymActivity
.
PILATES
->
true
}.
exhaustive
// error! when expression is not exhaustive.
Enums are a way of creating a class that has a specified, static set of instances. Kotlin provides an interesting generalization of this capability, the sealed class.
Sealed Classes
Consider the following code. It defines a single type, Result
,
with exactly two subtypes. Success
contains a value; Failure
contains an Exception
:
interface
Result
data
class
Success
(
val
data
:
List
<
Int
>
)
:
Result
data
class
Failure
(
val
error
:
Throwable?)
:
Result
Notice that there is no way to do this with an enum
. All of the values of an enum must be
instances of the same type. Here, though, there are two distinct types that are subtypes of Result
.
We can create a new instance of either of the two types:
fun
getResult
():
Result
=
try
{
Success
(
getDataOrExplode
())
}
catch
(
e
:
Exception
)
{
Failure
(
e
)
}
And, again, a when
expression is a handy way to manage a Result
:
fun
processResult
(
result
:
Result
):
List
<
Int
>
=
when
(
result
)
{
is
Success
->
result
.
data
is
Failure
->
listOf
()
else
->
throw
IllegalArgumentException
(
"unknown result type"
)
}
We’ve had to add an else
branch again, because the Kotlin compiler
doesn’t know that Success
and Failure
are the only Result
subclasses. Somewhere in your program, you might create another
subclass of result Result
and add another possible case. Hence the
else
branch is required by the compiler.
Sealed classes do for types what enums do for instances. They allow
you to announce to the compiler that there is a fixed, known set of
subtypes (Success
and Failure
in this case) for a certain base type
(Result
, here). To make this declaration, use the keyword sealed
in the declaration, as shown in the following code:
sealed
class
Result
data
class
Success
(
val
data
:
List
<
Int
>
)
:
Result
()
data
class
Failure
(
val
error
:
Throwable?)
:
Result
()
Because Result
is sealed, the Kotlin compiler knows that Success
and Failure
are the only
possible subclasses. Once again, we can remove the else
from a when
expression:
fun
processResult
(
result
:
Result
):
List
<
Int
>
=
when
(
result
)
{
is
Success
->
result
.
data
is
Failure
->
listOf
()
}
Visibility Modifiers
In both Java and Kotlin, visibility modifiers determine the scope of a variable, class, or method. In Java, there are three visibility modifiers:
private
-
References are only visible to the class that they are defined within, and from the outer class if defined in an inner class.
protected
-
References are visible to the class that they are defined within, or any subclasses of that class. In addition, they are also visible from classes in the same package.
public
Kotlin also has these three visibility modifiers. However, there are some subtle differences. While you can only use them with class-member declarations in Java, you can use them with class-member and top-level declarations in Kotlin:
private
-
The declaration’s visibility depends on where it is defined:
-
A class member declared as
private
is visible only in the class in which it is defined. -
A top-level
private
declaration is visible only in the file in which it is defined.
-
protected
-
Protected declarations are visible only in the class in which they are defined, and the subclasses thereof.
public
In addition to these three different visibilities, Java has a fourth, package-private, making references only visible from classes that are within the same package. A declaration is package-private when it has no visibility modifiers. In other words, this is the default visibility in Java.
Kotlin has no such concept.5 This might be surprising, because Java developers often rely on package-private visibility to hide implementation details from other packages within the same module. In Kotlin, packages aren’t used for visibility scoping at all—they’re just namespaces. Therefore, the default visibility is different in Kotlin—it’s public.
The fact that Kotlin doesn’t have package-private visibility has quite a significant impact on how we design and
structure our code. To guarantee a complete encapsulation of declarations (classes, methods, top-level fields, etc.),
you can have all these declarations as private
within the same file.
Sometimes it’s acceptable to have several closely related classes split into different files. However, those classes
won’t be able to access siblings from the same package unless they are public
or internal
. What’s internal
?
It’s the fourth visibility modifier supported by Kotlin, which makes the reference visible anywhere within the containing
module.6 From a module standpoint, internal
is
identical to public
. However, internal
is interesting when this module is intended as a library—for example, it’s a
dependency for other modules. Indeed, internal
declarations aren’t visible from modules that import your library.
Therefore, internal
is useful to hide declarations from the outside world.
Summary
Table 1-1 highlights some of the key differences between Java and Kotlin.
Feature | Java | Kotlin |
---|---|---|
File contents |
A single file contains a single top-level class. |
A single file can hold any number of classes, variables, or functions. |
Variables |
Use |
Use |
Type inferencing |
Data types are required. |
Data types can be inferred, like |
Boxing and unboxing types |
In Java, data primitives like |
Kotlin doesn’t have primitive types out of the box. Everything is an object. When compiled for the JVM, the generated bytecode performs automatic unboxing, when possible. |
Access modifiers |
Public and protected classes, functions, and variables can be extended and overridden. |
As a functional language, Kotlin encourages immutability whenever possible. Classes and functions are final by default. |
Access modifiers in multi-module projects |
Default access is package-private. |
There is no package-private, and default access is public. New |
Functions |
All functions are methods. |
Kotlin has function types. Function data types look like, for example, |
Nullability |
Any non-primitive object can be null. |
Only explicitly nullable references, declared with the |
Statics versus constants |
The |
There is no |
Congratulations, you just finished a one-chapter covering Kotlin’s essentials. Before we start talking about applying Kotlin to Android, we need to discuss Kotlin’s built-in library: collections and data transformations. Understanding the underlying functions of data transformations in Kotlin will give you the necessary foundation needed to understand Kotlin as a functional language.
1 Dmitry Jemerov and Svetlana Isakova. Kotlin in Action. Manning, 2017.
2 Kotlin officially calls this type inferencing, which uses a partial phase of the compiler (the frontend component) to do type checking of the written code while you write in the IDE. It’s a plug-in for IntelliJ! Fun fact: the entirety of IntelliJ and Kotlin is made of compiler plug-ins.
3 DSL stands for domain-specific language. An example of a DSL built in Kotlin is the Kotlin Gradle DSL.
4 You can check whether the latenit
button
property is initialized using this::button.isInitialized
. Relying on developers to add this check in all the right places doesn’t solve the underlying issue.
5 At least, as of Kotlin 1.5.20. As we write these lines, Jetbrains is considering adding a package-private visibility modifier to the language.
6 A module is a set of Kotlin files compiled together.
Get Programming Android with Kotlin now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.