Summary 2
This atom summarizes and reviews the atoms in Section II, from Objects Everywhere through Property Accessors.
If you’re an experienced programmer, this is your next atom after Summary 1, and you will go through the atoms sequentially after this.
New programmers should read this atom and perform the exercises as review. If any information here isn’t clear to you, go back and study the atom for that topic.
The topics appear in appropriate order for experienced programmers, which is not the same as the order of the atoms in the book. For example, we start by introducing packages and imports so we can use our minimal test framework for the rest of the atom.
Packages & Testing
Any number of reusable library components can be bundled under a single library
name using the package keyword:
// Summary2/ALibrary.kt
package com.yoururl.libraryname
// Components to reuse ...
fun f() = "result"
You can put multiple components in a single file, or spread components out
among multiple files under the same package name. Here we’ve defined f() as
the sole component.
To make it unique, the package name conventionally begins with your reversed
domain name. In this example, the domain name is yoururl.com.
In Kotlin, the package name can be independent from the directory where its
contents are located. Java requires that the directory structure correspond to
the fully-qualified package name, so the package com.yoururl.libraryname
should be located under the com/yoururl/libraryname directory. For mixed
Kotlin and Java projects, Kotlin’s style guide recommends the same practice.
For pure Kotlin projects, put the directory libraryname at the top level of
your project’s directory structure.
An import statement brings one or more names into the current namespace:
// Summary2/UseALibrary.kt
import com.yoururl.libraryname.*
fun main() {
val x = f()
}
The star after libraryname tells Kotlin to import all the components of a
library. You can also select components individually; details are in
Packages.
In the remainder of this book we use package statements for any file that
defines functions, classes, etc., outside of main(). This prevents name
clashes with other files in the book. We usually won’t put a package
statement in a file that only contains a main().
An important library for this book is atomictest, our simple testing
framework. atomictest is defined in Appendix A:
AtomicTest, although it uses language features you
will not understand at this point in the book.
After importing atomictest, you use eq (equals) and neq (not equals)
almost as if they were language keywords:
// Summary2/UsingAtomicTest.kt
import atomictest.*
fun main() {
val pi = 3.14
val pie = "A round dessert"
pi eq 3.14
pie eq "A round dessert"
pi neq pie
}
/* Output:
3.14
A round dessert
3.14
*/
The ability to use eq/neq without any dots or parentheses is called infix
notation. You can call infix functions either in the regular way:
pi.eq(3.14), or using infix notation: pi eq 3.14. Both eq and neq are
assertions of truth that also display the result from the left side of the
eq/neq statement, and an error message if the expression on the right of
the eq isn’t equivalent to the left (or is equivalent, in the case of
neq). This way you see verified results in the source code.
atomictest.trace uses function-call syntax for adding results, which can then
be validated using eq:
// Testing/UsingTrace.kt
import atomictest.*
fun main() {
trace("Hello,")
trace(47)
trace("World!")
trace eq """
Hello,
47
World!
"""
}
You can effectively replace println() with trace().
Objects Everywhere
Kotlin is a hybrid object-functional language: it supports both object-oriented and functional programming paradigms.
Objects contain vals and vars to store data (these are called properties)
and perform operations using functions defined within a class, called member
functions (when it’s unambiguous, we just say “functions”). A class defines
properties and member functions for what is essentially a new, user-defined
data type. When you create a val or var of a class, it’s called creating
an object or creating an instance.
An especially useful type of object is the container, also called
collection. A container is an object that holds other objects. In this book,
we often use the List because it’s the most general-purpose sequence. Here we
perform several operations on a List that holds Doubles. listOf() creates
a new List from its arguments:
// Summary2/ListCollection.kt
import atomictest.eq
fun main() {
val lst = listOf(19.2, 88.3, 22.1)
lst[1] eq 88.3 // Indexing
lst.reversed() eq listOf(22.1, 88.3, 19.2)
lst.sorted() eq listOf(19.2, 22.1, 88.3)
lst.sum() eq 129.6
}
No import statement is required to use a List.
Kotlin uses square brackets for indexing into sequences. Indexing is zero-based.
This example also shows a few of the many standard library functions available
for Lists: sorted(), reversed(), and sum(). To understand these
functions, consult the online Kotlin
documentation.
When you call sorted() or reversed(), lst is not modified. Instead, a new
List is created and returned, containing the desired result. This approach of
never modifying the original object is consistent throughout Kotlin libraries
and you should endeavor to follow this pattern when writing your own code.
Creating Classes
A class definition consists of the class keyword, a name for the class, and
an optional body. The body contains property definitions (vals and vars)
and function definitions.
This example defines a NoBody class without a body, and classes with val
properties:
// Summary2/ClassBodies.kt
package summary2
class NoBody
class SomeBody {
val name = "Janet Doe"
}
class EveryBody {
val all = listOf(SomeBody(),
SomeBody(), SomeBody())
}
fun main() {
val nb = NoBody()
val sb = SomeBody()
val eb = EveryBody()
}
To create an instance of a class, put parentheses after its name, along with arguments if those are required.
Properties within class bodies can be any type. SomeBody contains a property
of type String, and EveryBody’s property is a List holding SomeBody
objects.
Here’s a class with member functions:
// Summary2/Temperature.kt
package summary2
import atomictest.eq
class Temperature {
var current = 0.0
var scale = "f"
fun setFahrenheit(now: Double) {
current = now
scale = "f"
}
fun setCelsius(now: Double) {
current = now
scale = "c"
}
fun getFahrenheit(): Double =
if (scale == "f")
current
else
current * 9.0 / 5.0 + 32.0
fun getCelsius(): Double =
if (scale == "c")
current
else
(current - 32.0) * 5.0 / 9.0
}
fun main() {
val temp = Temperature() // [1]
temp.setFahrenheit(98.6)
temp.getFahrenheit() eq 98.6
temp.getCelsius() eq 37.0
temp.setCelsius(100.0)
temp.getFahrenheit() eq 212.0
}
These member functions are just like the top-level functions we’ve defined
outside of classes, except they belong to the class and have unqualified
access to the other members of the class, such as current and scale. Member
functions can also call other member functions in the same class without
qualification.
-
[1] Although
tempis aval, we later modify theTemperatureobject. Thevaldefinition prevents the referencetempfrom being reassigned to a new object, but it does not restrict the behavior of the object itself.
The following two classes are the foundation of a tic-tac-toe game:
// Summary2/TicTacToe.kt
package summary2
import atomictest.eq
class Cell {
var entry = ' ' // [1]
fun setValue(e: Char): String = // [2]
if (entry == ' ' &&
(e == 'X' || e == 'O')) {
entry = e
"Successful move"
} else
"Invalid move"
}
class Grid {
val cells = listOf(
listOf(Cell(), Cell(), Cell()),
listOf(Cell(), Cell(), Cell()),
listOf(Cell(), Cell(), Cell())
)
fun play(e: Char, x: Int, y: Int): String =
if (x !in 0..2 || y !in 0..2)
"Invalid move"
else
cells[x][y].setValue(e) // [3]
}
fun main() {
val grid = Grid()
grid.play('X', 1, 1) eq "Successful move"
grid.play('X', 1, 1) eq "Invalid move"
grid.play('O', 1, 3) eq "Invalid move"
}
The Grid class holds a List containing three Lists, each containing three
Cells—a matrix.
-
[1] The
entryproperty inCellis avarso it can be modified. The single quotes in the initialization produce aChartype, so all assignments toentrymust also beChars. -
[2]
setValue()tests that theCellis available and that you’ve passed the correct character. It returns aStringresult to indicate success or failure. -
[3]
play()checks to see if thexandyarguments are within range, then indexes into the matrix, relying on the tests performed bysetValue().
Constructors
Constructors create new objects. You pass information to a constructor using its parameter list, placed in parentheses directly after the class name. A constructor call thus looks like a function call, except that the initial letter of the name is capitalized (following the Kotlin style guide). The constructor returns an object of the class:
// Summary2/WildAnimals.kt
package summary2
import atomictest.eq
class Badger(id: String, years: Int) {
val name = id
val age = years
override fun toString() =
"Badger: $name, age: $age"
}
class Snake(
var type: String,
var length: Double
) {
override fun toString() =
"Snake: $type, length: $length"
}
class Moose(
val age: Int,
val height: Double
) {
override fun toString() =
"Moose, age: $age, height: $height"
}
fun main() {
Badger("Bob", 11) eq "Badger: Bob, age: 11"
Snake("Garden", 2.4) eq
"Snake: Garden, length: 2.4"
Moose(16, 7.2) eq
"Moose, age: 16, height: 7.2"
}
The parameters id and years in Badger are only available in the
constructor body. The constructor body consists of the lines of code other
than function definitions; in this case, the definitions for name and age.
Often you want the constructor parameters to be available in parts of the class
other than the constructor body, but without the trouble of explicitly defining
new identifiers as we did with name and age. If you define your parameters
as vars or vals, they becomes properties and are accessible everywhere in
the class. Both Snake and Moose use this approach, and you can see that the
constructor parameters are now available inside their respective toString()
functions.
Constructor parameters declared with val cannot be changed, but those declared
with var can.
Whenever you use an object in a situation that expects a String, Kotlin
produces a String representation of that object by calling its toString()
member function. To define a toString(), you must understand a new keyword:
override. This is necessary (Kotlin insists on it) because toString() is
already defined. override tells Kotlin that we do actually want to replace
the default toString() with our own definition. The explicitness of
override makes this clear to the reader and helps prevent mistakes.
Notice the formatting of the multiline parameter list for Snake and
Moose—this is the recommended standard when you have too many parameters
to fit on one line, for both constructors and functions.
Constraining Visibility
Kotlin provides access modifiers similar to those available in other
languages like C++ or Java. These allow component creators to decide what is
available to the client programmer. Kotlin’s access modifiers include the
public, private, protected, and internal keywords. protected is
explained later.
An access modifier like public or private appears before the definition for
a class, function or property. Each access modifier only controls the access
for that particular definition.
A public definition is available to everyone, in particular to the client
programmer who uses that component. Thus, any changes to a public definition
will impact client code.
If you don’t provide a modifier, your definition is automatically public. For
clarity in certain cases, programmers still sometimes redundantly specify
public.
If you define a class, top-level function, or property as private, it is
available only within that file:
// Summary2/Boxes.kt
package summary2
import atomictest.*
private var count = 0 // [1]
private class Box(val dimension: Int) { // [2]
fun volume() =
dimension * dimension * dimension
override fun toString() =
"Box volume: ${volume()}"
}
private fun countBox(box: Box) { // [3]
trace("$box")
count++
}
fun countBoxes() {
countBox(Box(4))
countBox(Box(5))
}
fun main() {
countBoxes()
trace("$count boxes")
trace eq """
Box volume: 64
Box volume: 125
2 boxes
"""
}
You can access private properties ([1]), classes ([2]), and functions
([3]) only from other functions and classes in the Boxes.kt file. Kotlin
prevents you from accessing private top-level elements from another file.
Class members can be private:
// Summary2/JetPack.kt
package summary2
import atomictest.eq
class JetPack(
private var fuel: Double // [1]
) {
private var warning = false
private fun burn() = // [2]
if (fuel - 1 <= 0) {
fuel = 0.0
warning = true
} else
fuel -= 1
public fun fly() = burn() // [3]
fun check() = // [4]
if (warning) // [5]
"Warning"
else
"OK"
}
fun main() {
val jetPack = JetPack(3.0)
while (jetPack.check() != "Warning") {
jetPack.check() eq "OK"
jetPack.fly()
}
jetPack.check() eq "Warning"
}
-
[1]
fuelandwarningare bothprivateproperties and can’t be used by non-members ofJetPack. -
[2]
burn()isprivate, and thus only accessible insideJetPack. -
[3]
fly()andcheck()arepublicand can be used everywhere. -
[4] No access modifier means
publicvisibility. -
[5] Only members of the same class can access
privatemembers.
Because a private definition is not available to everyone, you can
generally change it without concern for the client programmer. As a library
designer, you’ll typically keep everything as private as possible, and expose
only functions and classes you want client programmers to use. To limit the
size and complexity of example listings in this book, we only use private in
special cases.
Any function you’re certain is only a helper function can be made private,
to ensure you don’t accidentally use it elsewhere and thus prohibit yourself
from changing or removing the function.
It can be useful to divide large programs into modules. A module is a
logically independent part of a codebase. An internal definition is accessible
only inside the module where it is defined. The way you divide a project into
modules depends on the build system (such as Gradle or
Maven) and is beyond the scope of this book.
Modules are a higher-level concept, while packages enable finer-grained structuring.
Exceptions
Consider toDouble(), which converts a String to a Double. What happens if
you call it for a String that doesn’t translate into a Double?
// Summary2/ToDoubleException.kt
fun main() {
// val i = "$1.9".toDouble()
}
Uncommenting the line in main() produces an exception. Here, the failing line
is commented so we don’t stop the book’s build (which checks whether each
example compiles and runs as expected).
When an exception is thrown, the current path of execution stops, and the exception object ejects from the current context. When an exception isn’t caught, the program aborts and displays a stack trace containing detailed information.
To avoid displaying exceptions by commenting and uncommenting code,
atomictest.capture() stores the exception and compares it to what we expect:
// Summary2/AtomicTestCapture.kt
import atomictest.*
fun main() {
capture {
"$1.9".toDouble()
} eq "NumberFormatException: " +
"""For input string: "$1.9""""
}
capture() is designed specifically for this book, so you can see the
exception and know that the output has been checked by the book’s build system.
Another strategy when your function can’t successfully produce the expected
result is to return null. Later in Nullable Types we
discuss how null affects the type of the resulting expression.
To throw an exception, use the throw keyword followed by the exception you
want to throw, along with any arguments it might need. quadraticZeroes() in
the following example solves the quadratic
equation that defines a
parabola:
ax2 + bx + c = 0
The solution is the quadratic formula:
The example finds the zeroes of the parabola, where the lines cross the x-axis. We throw exceptions for two limitations:
-
acannot be zero. - For zeroes to exist, b2 - 4ac cannot be negative.
If zeroes exist, there are two of them, so we create the Roots class to hold
the return values:
// Summary2/Quadratic.kt
package summary2
import kotlin.math.sqrt
import atomictest.*
class Roots(
val root1: Double,
val root2: Double
)
fun quadraticZeroes(
a: Double,
b: Double,
c: Double
): Roots {
if (a == 0.0)
throw IllegalArgumentException(
"a is zero")
val underRadical = b * b - 4 * a * c
if (underRadical < 0)
throw IllegalArgumentException(
"Negative underRadical: $underRadical")
val squareRoot = sqrt(underRadical)
val root1 = (-b - squareRoot) / (2 * a)
val root2 = (-b + squareRoot) / (2 * a)
return Roots(root1, root2)
}
fun main() {
capture {
quadraticZeroes(0.0, 4.0, 5.0)
} eq "IllegalArgumentException: " +
"a is zero"
capture {
quadraticZeroes(3.0, 4.0, 5.0)
} eq "IllegalArgumentException: " +
"Negative underRadical: -44.0"
val roots = quadraticZeroes(1.0, 2.0, -8.0)
roots.root1 eq -4.0
roots.root2 eq 2.0
}
Here we use the standard exception class IllegalArgumentException. Later
you’ll learn to define your own exception types and to make them specific to
your circumstances. Your goal is to generate the most useful messages possible,
to simplify the support of your application in the future.
Lists
Lists are Kotlin’s basic sequential container type. You create a read-only
list using listOf() and a mutable list using mutableListOf():
// Summary2/ReadonlyVsMutableList.kt
import atomictest.*
fun main() {
val ints = listOf(5, 13, 9)
// ints.add(11) // 'add()' not available
for (i in ints) {
if (i > 10) {
trace(i)
}
}
val chars = mutableListOf('a', 'b', 'c')
chars.add('d') // 'add()' available
chars += 'e'
trace(chars)
trace eq """
13
[a, b, c, d, e]
"""
}
A basic List is read-only, and does not include modification functions. Thus,
the modification function add() doesn’t work with ints.
for loops work well with Lists: for(i in ints) means i gets each value
in ints.
chars is created as a MutableList; it can be modified using functions like
add() or remove(). You can also use += and -= to add or remove
elements.
A read-only List is not the same as an immutable List, which can’t be
modified at all. Here, we assign first, a mutable List, to second, a
read-only List reference. The read-only characteristic of second doesn’t
prevent the List from changing via first:
// Summary2/MultipleListReferences.kt
import atomictest.eq
fun main() {
val first = mutableListOf(1)
val second: List<Int> = first
second eq listOf(1)
first += 2
// second sees the change:
second eq listOf(1, 2)
}
first and second refer to the same object in memory. We mutate the List
via the first reference, and then observe this change in the second
reference.
Here’s a List of Strings created by breaking up a triple-quoted paragraph.
This shows the power of some of the standard library functions. Notice how
those functions can be chained:
// Summary2/ListOfStrings.kt
import atomictest.*
fun main() {
val wocky = """
Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,
And the mome raths outgrabe.
""".trim().split(Regex("\\W+"))
trace(wocky.take(5))
trace(wocky.slice(6..12))
trace(wocky.slice(6..18 step 2))
trace(wocky.sorted().takeLast(5))
trace(wocky.sorted().distinct().takeLast(5))
trace eq """
[Twas, brillig, and, the, slithy]
[Did, gyre, and, gimble, in, the, wabe]
[Did, and, in, wabe, mimsy, the, And]
[the, the, toves, wabe, were]
[slithy, the, toves, wabe, were]
"""
}
trim() produces a new String with the leading and trailing whitespace
characters (including newlines) removed. split() divides the String
according to its argument. In this case we use a Regex object, which creates
a regular expression—a pattern that matches the parts to split. \W is
a special pattern that means “not a word character,” and + means “one or more
of the preceding.” Thus split() will break at one or more non-word
characters, and so divides the block of text into its component words.
In a String literal, \ precedes a special character and produces, for
example, a newline character (\n), or a tab (\t). To produce an actual \
in the resulting String you need two backslashes: "\\". Thus all regular
expressions require an extra \ to insert a backslash, unless you use a
triple-quoted String: """\W+""".
take(n) produces a new List containing the first n elements. slice()
produces a new List containing the elements selected by its Range argument,
and this Range can include a step.
Note the name sorted() instead of sort(). When you call sorted() it
produces a sorted List, leaving the original List alone. sort() only
works with a MutableList, and that list is sorted in place—the
original List is modified.
As the name implies, takeLast(n) produces a new List of the last n
elements. You can see from the output that “the” is duplicated. This is
eliminated by adding the distinct() function to the call chain.
Parameterized Types
Type parameters allow us to describe compound types, most commonly containers.
In particular, type parameters specify what a container holds. Here, we tell
Kotlin that numbers contain a List of Int, while strings contain a
List of String:
// Summary2/ExplicitTyping.kt
package summary2
import atomictest.eq
fun main() {
val numbers: List<Int> = listOf(1, 2, 3)
val strings: List<String> =
listOf("one", "two", "three")
numbers eq "[1, 2, 3]"
strings eq "[one, two, three]"
toCharList("seven") eq "[s, e, v, e, n]"
}
fun toCharList(s: String): List<Char> =
s.toList()
For both the numbers and strings definitions, we add colons and the type
declarations List<Int> and List<String>. The angle brackets denote a type
parameter, allowing us to say, “the container holds ‘parameter’ objects.” You
typically pronounce List<Int> as “List of Int.”
A return value can also have a type parameter, as seen in toCharList(). You
can’t just say it returns a List—Kotlin complains, so you must give the
type parameter as well.
Variable Argument Lists
The vararg keyword is short for variable argument list, and allows a
function to accept any number of arguments (including zero) of the specified
type. The vararg becomes an Array, which is similar to a List:
// Summary2/VarArgs.kt
package summary2
import atomictest.*
fun varargs(s: String, vararg ints: Int) {
for (i in ints) {
trace("$i")
}
trace(s)
}
fun main() {
varargs("primes", 5, 7, 11, 13, 17, 19, 23)
trace eq "5 7 11 13 17 19 23 primes"
}
A function definition may specify only one parameter as vararg. Any parameter
in the list can be the vararg, but the final one is generally simplest.
You can pass an Array of elements wherever a vararg is accepted. To create
an Array, use arrayOf() in the same way you use listOf(). An
Array is always mutable. To convert an Array into a sequence of arguments
(not just a single element of type Array), use the spread operator *:
// Summary2/ArraySpread.kt
import summary2.varargs
import atomictest.trace
fun main() {
val array = intArrayOf(4, 5) // [1]
varargs("x", 1, 2, 3, *array, 6) // [2]
val list = listOf(9, 10, 11)
varargs(
"y", 7, 8, *list.toIntArray()) // [3]
trace eq "1 2 3 4 5 6 x 7 8 9 10 11 y"
}
If you pass an Array of primitive types as in the example above, the Array
creation function must be specifically typed. If [1] uses arrayOf(4, 5)
instead of intArrayOf(4, 5), [2] produces an error: inferred type is
Array<Int> but IntArray was expected.
The spread operator only works with arrays. If you have a List to pass as a
sequence of arguments, first convert it to an Array and then apply the spread
operator, as in [3]. Because the result is an Array of a primitive type,
we must use the specific conversion function toIntArray().
Sets
Sets are collections that allow only one element of each value. A Set
automatically prevents duplicates.
// Summary2/ColorSet.kt
package summary2
import atomictest.eq
val colors =
"Yellow Green Green Blue"
.split(Regex("""\W+""")).sorted() // [1]
fun main() {
colors eq
listOf("Blue", "Green", "Green", "Yellow")
val colorSet = colors.toSet() // [2]
colorSet eq
setOf("Yellow", "Green", "Blue")
(colorSet + colorSet) eq colorSet // [3]
val mSet = colorSet.toMutableSet() // [4]
mSet -= "Blue"
mSet += "Red" // [5]
mSet eq
setOf("Yellow", "Green", "Red")
// Set membership:
("Green" in colorSet) eq true // [6]
colorSet.contains("Red") eq false
}
-
[1] The
Stringissplit()using a regular expression as described earlier forListOfStrings.kt. -
[2] When
colorsis copied into the read-onlySet colorSet, one of the two"Green"Strings is removed, because it is a duplicate. -
[3] Here we create and display a new
Setusing the+operator. Placing duplicate items into aSetautomatically removes those duplicates. -
[4]
toMutableSet()produces a newMutableSetfrom a read-onlySet. -
[5] For a
MutableSet, the operators+=and-=add and remove elements, as they do withMutableLists. -
[6] Test for
Setmembership usinginorcontains()
The normal mathematical set operations such as union, intersection, difference, etc., are all available.
Maps
A Map connects keys to values and looks up a value when given a key. You
create a Map by providing key-value pairs to mapOf(). Using to, we
separate each key from its associated value:
// Summary2/ASCIIMap.kt
import atomictest.eq
fun main() {
val ascii = mapOf(
"A" to 65,
"B" to 66,
"C" to 67,
"I" to 73,
"J" to 74,
"K" to 75
)
ascii eq
"{A=65, B=66, C=67, I=73, J=74, K=75}"
ascii["B"] eq 66 // [1]
ascii.keys eq "[A, B, C, I, J, K]"
ascii.values eq
"[65, 66, 67, 73, 74, 75]"
var kv = ""
for (entry in ascii) { // [2]
kv += "${entry.key}:${entry.value},"
}
kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
kv = ""
for ((key, value) in ascii) // [3]
kv += "$key:$value,"
kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
val mutable = ascii.toMutableMap() // [4]
mutable.remove("I")
mutable eq
"{A=65, B=66, C=67, J=74, K=75}"
mutable.put("Z", 90)
mutable eq
"{A=65, B=66, C=67, J=74, K=75, Z=90}"
mutable.clear()
mutable["A"] = 100
mutable eq "{A=100}"
}
-
[1] A key (
"B") is used to look up a value with the[]operator. You can produce all the keys usingkeysand all the values usingvalues. Accessingkeysproduces aSetbecause all keys in aMapmust already be unique (otherwise you’d have ambiguity during a lookup). -
[2] Iterating through a
Mapproduces key-value pairs as map entries. - [3] You can unpack key-value pairs as you iterate.
-
[4] You can create a
MutableMapfrom a read-onlyMapusingtoMutableMap(). Now we can perform operations that modifymutable, such asremove(),put(), andclear(). Square brackets can assign a new key-value pair intomutable. You can also add a pair by sayingmap += key to value.
Property Accessors
Accessing the property i appears straightforward:
// Summary2/PropertyReadWrite.kt
package summary2
import atomictest.eq
class Holder(var i: Int)
fun main() {
val holder = Holder(10)
holder.i eq 10 // Read the 'i' property
holder.i = 20 // Write to the 'i' property
}
However, Kotlin calls functions to perform the read and write operations. The
default behavior of those functions is to read and write the data stored
in i. By creating property accessors, you change the actions that occur
during reading and writing.
The accessor used to fetch the value of a property is called a getter. To
create your own getter, define get() immediately after the property
declaration. The accessor used to modify a mutable property is called a
setter. To create your own setter, define set() immediately after the
property declaration. The order of definition of getters and setters is
unimportant, and you can define one without the other.
The property accessors in the following example imitate the default
implementations while displaying additional information so you can see that the
property accessors are indeed called during reads and writes. We indent the
get() and set() functions to visually associate them with the property, but
the actual association happens because they are defined immediately after that
property:
// Summary2/GetterAndSetter.kt
package summary2
import atomictest.*
class GetterAndSetter {
var i: Int = 0
get() {
trace("get()")
return field
}
set(value) {
trace("set($value)")
field = value
}
}
fun main() {
val gs = GetterAndSetter()
gs.i = 2
trace(gs.i)
trace eq """
set(2)
get()
2
"""
}
Inside the getter and setter, the stored value is manipulated indirectly using
the field keyword, which is only accessible within these two functions. You
can also create a property that doesn’t have a field, but simply calls the
getter to produce a result.
If you declare a private property, both accessors become private. You can
make the setter private and the getter public. This means you can read the
property outside the class, but only change its value inside the class.
Exercises and solutions can be found at www.AtomicKotlin.com.