In this series, we will explore the most important topics in the Kotlin in Action book. Because this is just a summary of the book without the details, and to get the most of this series, I highly recommend that you read the book and after reading each chapter, you can read the corresponding article to review the concepts and material. This series is also great for developers who are getting ready for an interview. Because chapter 1 of the book is just some general information, we will skip this chapter and start from chapter 2 of the book. I will mention the page number after each section title, so you will be able to read the concept if it is not clear by the article. Let's dive in.
If you have already read this article or you feel you do not need to read this chapter, you can read part 2 here.
Functions
pages 18, 19
- To declare a function, we use the fun keyword.
- Functions can be declared at the top level of the file(outside the class).
- Parameter types are written after the parameter name separated by a colon.
- Return type of the function is written after the parameters' parentheses separated by a colon.
- If function is a block expression, we can omit the return type and the curly braces(block expression is discussed later).
Statements vs Expressions
page 19
An expression has a value which can be used as a part of another expression.
A statement is a top-level element in its enclosing block and does not have its own value.
Most of Java statements can be used as expressions in Kotlin(if, try, when). Because if can be used as an expression in Kotlin, we do not have ternary operator(int k = booleanVariable ? 1 : 0;) that Java has.
Please bear in mind that if can be either used as an expression or statement in Kotlin. If it covers all the cases and assigned to a variable, it is an expression. Otherwise, it is a statement.
Expression Body vs Block Body
page 19
If a function returns a value directly without using the curly braces, it has an expression body. On the other hand, if the function has the curly braces, it has a block body.
Variables
pages 20, 21
Unlike Java, Kotlin does not require us to begin variable with their type because there are times we can omit the variable type(using the type inference capability of the Kotlin compiler).
We use val or var to declare variables in Kotlin. val is the shorthand for value and var is the shorthand for variable. As the names suggest, val is immutable and var is mutable.
NOTE: We should always use the val keyword and use the var if we really need to change the variable, then we should use the var.
NOTE: Although val creates an immutable variable, bear in mind that the object itself might be mutable.
val s = arrayListOf("hi")
s.add("hello")
String Templates
page 22
Kotlin supports String templates. We can declare a variable, then use it alongside with the dollar sign($) to mention it in strings.
val appleNumbers = 1100
println("there are $appleNumbers in the inventory")
NOTE: If we want to use the dollar sign itself in a string, we should escape it:
println("\$x")
This will print $x, not the value of a variable named x.
Classes and Properties
pages 23, 24, 25
class Person {
val name: String,
var isMarried: Boolean
Classes that contain only data and no other code are called value objects.
In Kotlin, public is the default visibility modifier.
In Java, the combination of a field and its accessors is often referred to as a property. In Kotlin, properties are supported by the language and replace fields and accessor methods.
When we declare a property as val, a field and a getter is generated for them. Whereas for the var properties, a field, a getter and a setter is generated.
When we call the property of an object, it seems like we are accessing it directly, but under the hood, we are accessing it via the getter method generated.
val person = Person("Bob", true)
println(person.name)
Bob
>>>print(person.isMarried)
true
There are cases when we do not need a backing field for a property. For example, if we can calculate the property from the value of other properties. We can declare a custom getter for such a property, and no backing field will be generated for that.
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
In the code above, the compiler will not generate any backing field for the isSquare property.
Enums
pages 28, 29
enum class Color {
RED, BLUE, YELLOW, GREEN
}
Enum is a rare case in Kotlin that uses more keywords than the Java's equivalent.
In Kotlin, enum is a soft keyword. It has a special meaning when it comes before class, but we can use it elsewhere in our code.
Enums are not just a list of values. We can declare methods and properties in enums.
enum class Color(
val r: Int, val g: Int, val b: Int
) {
RED(255, 0, 0), BLUE(0, 0, 255), YELLOW(255, 255, 0),
GREEN(0, 255, 0);
fun rgb() = (r * 256 + g) * 256 + b
}
- In the enum constructor, we have declared the properties of enum constants.
- Semicolon is required if we declare methods in the enum.
When
pages 29, 30, 31
Like if, when is an expression that returns a value.
Unlike Java, when does not require us to write break after each case.
fun getWarmth(color: Color) =
when (color) {
Color.RED -> "warm"
Color.BLUE -> "cold"
Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
}
We can combine multiple values in the same branch separated with a coma(,).
fun getWarmth(color: Color) =
when (color) {
Color.RED, Color.YELLOW -> "warm"
Color.BLUE -> "cold"
Color.GREEN -> "neutral"
}
Unlike switch in Java which requires us to use constants(enums, strings, or number literals) as branch conditions, when in Kotlin allows us to use any objects.
fun mix(c1: Color, c2: Color) =
when(setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(BLUE, YELLOW) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}
}
Here we have used Set as the branch condition.
The previous example is not efficient because it creates a set for every invocation. We can instead use a when without an argument to accomplish the same result:
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 = YELLOW) ||
(c1 == YELLOW && c2 = RED) -> ORANGE
(c1 == BLUE && c2 = YELLOW) ||
(c1 == YELLOW && c2 = BLUE) -> GREEN
(c1 == BLUE && c2 = VIOLET) ||
(c1 == VIOLET && c2 = BLUE) -> GREEN -> INDIGO
else -> throw Exception("Dirty color")
}
}
NOTE: If no argument is supplied for the when expression, the branch condition is any boolean expression.
Smart Casts
pages 31, 32, 33
To illustrate the casting mechanism in Kotlin, we will use an example in arithmetic expression. In this example, we will just use one operation: the sum of two numbers. Let's dive in:
First we declare an interface to have the same type for both a value and an expression of two values. Then, we implement the interface in two classes Sum and Num.
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
NOTE: To implement an interface in a class, we use a colon after the class name, and then the interface name:
The Expr interface has two implementations, so we have two options:
- If an expression is a number, we return the corresponding value.
- If it's a sum, we have to evaluate the left and right expressions and return their sum.
First, let's see a simple implementation:
fun eval(e: Expr) : Int {
if (e is Num) {
val n = e as Num
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("unknown expression")
}
is vs instanceof
The is check in Kotlin is the same as the instanceof in Java. In Java, if we have checked a variable with the instanceof, we also have to cast it to the type. However in Kotlin, if we check a variable with the is check, it already can be used as the type we have checked with the is check. In effect, the compiler performs the cast for us(which is called smart cast).
NOTE: The smart cast works only if a variable could not have changed after the is check. For a property of a class, it must be val or it should not have a custom accessor!
Because we have the smart cast option in Kotlin, we can remove the cast in the above code.
We can also remove the curly braces and the return keywords, because in Kotlin if is an expression.
The result of the two above modifications is:
fun eval(e: Expr) =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else
throw IllegalArgumentException("unknown expression")
NOTE: Both if and when can have blocks. In this case, the last expression in the block is the result.
We can also replace the if branches with when:
fun eval(e: Expr) =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> throw IllegalArgumentException("unknown expression")
}
The While Loop
page 35
The while and do while loop in Kotlin is the same as their corresponding loops in Java.
while (condition) {
//while block
}
do {
//do-while block
} while(condition)
For Loops and Ranges
pages 36, 37
Java for loop: We initialize a variable, update its value on every step in the loop, exit the loop when the value reaches a certain bound.
Kotlin introduced ranges to replace the common loop use-case.
Range: A range is an interval between two values, usually numbers: a start and an end. We write it using the .. operator.
NOTE: Ranges in Kotlin are closed or inclusive(the second value is always part of the range).
val oneToTen = 1..10
for (i in oneToTen) {
println(i)
}
for (i in 1..10) {
println(i)
}
In ranges, we can use downTo, step and until functions.
downTo: iterates backward in a loop
step: allows us to skip some numbers
until: allows us define a half-closed range(the last value is not included in the range)
for (i in 100 downTo 1 step 2) {
println(i) //prints 100, 98, 96 ... 4, 2
}
for (i in 1 until 100) {
println(i) //prints 1, 2, 3, ..., 98, 99
}
Iterating over Collections
pages 37, 38
The most common use-case of a for in loop is to iterate over a collection. Let's see how we can do this in Kotlin.
At first, we will see how to use the for loop to iterate through a map:
val binaryReps = TreeMap<Char, String>()
//here we will create a range of characters
for (c in 'A'..'F') {
val binary = Integer.toBinary(c.toInt())
//put each binary character in the map
binaryReps[c] = binary
}
//iterating through the map
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
In the above code, we can see that the range function not only works for numbers, but also works fine for characters.
We can also see that we are unpacking the elements of a collection to iterate over them. In the case of maps, we should unpack it into two variables: one for the key the other for the value.
NOTE: As we can see, we are using the shorthand version of accessing and updating the map values. We use the map[key] = value instead of using the get and put functions:
map[key] = value
We can also iterate through a collection and keep track of the index of the current item:
val list = arrayListOf("10", "11", "1001")
//here we iterate through each element in the list
for (element in list) {
println("$element")
}
//here we iterate through each element in the list and keep the index of current element
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
The in operator
pages 38, 39
We can use the in operator to check whether a value is in a range, or its opposite, !in, to check whether a value is NOT in a range:
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
The in and !in operators also work in when expressions:
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z' , in 'A'..'Z' -> "It's a letter!"
else -> "I don't know!"
}
NOTE: Ranges are not restricted to the characters either. If you have any class that supports comparing instances (by implementing the java.lang.Comparable interface), you can create ranges of objects of that type. But keep in mind that you cannot enumerate all objects in the range.
Exceptions
pages 39, 40
Exception handling Kotlin is similar to Java. A function can complete in a normal way, or throw an exception. Either the caller catches and handles the exception, or the exception propagates and is rethrown further up the stack.
Unlike Java, in Kotlin the throw construct is an expression and can be used as a part of other expressions:
val percentage =
if (number in 0..100)
number
else
throw java.lang.IllegalArgumentException("Illegal percentage value")
try, catch, and finally
pages 40, 41
Just like Java, we can use the try with catch and finally blocks.
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: java.lang.NumberFormatException) {
return null
} finally {
reader.close()
}
}
The biggest difference in exception handling between Java and Kotlin is that throws clause is NOT present in the code because Kotlin does not differentiates between checked and unchecked exceptions.
try as an Expression
pages 41, 42
The try keyword in Kotlin, just like if and when, introduces an expression, and you can assign it to a variable. Unlike if, you have to enclose the try statement body in curly braces.
fun readNumber(reader: BufferedReader) {
val number = try {
val line = reader.readLine()
Integer.parseInt(line)
} catch (e: java.lang.NumberFormatException) {
null
}
}
If the execution of a try block behaves normally, the last expression in the block is the result. If an exception is caught, the last expression in a corresponding catch block is the result.
This is the end of part 1 which was the summary of chapter 2. There were other parts in chapter 2 that I did not cover here because I felt they are not as important and they would have made the article too long. Anyhow, if you found this article useful, please like it and share it with other fellow developers. If you have any questions or suggestions, please feel free to comment. Thanks for your time.
Here is the link to part 2 of this series.
Top comments (0)