DEV Community

Cover image for Kotlin in Action Summary - Part 2
Mahdi Abolfazli
Mahdi Abolfazli

Posted on

Kotlin in Action Summary - Part 2

This is part 2 of the series Kotlin in Action Summary. Here is the link to part 1 which we talked about chapter 2 of the book with the title "Kotlin basics". As mentioned before, it is highly recommended to read the book first and consider this series as a refresher. We do NOT cover all the materials in the book. Let's start with part 2, chapter 3 of the book: "Defining and Calling Functions"!

Creating Collections

Let's see how we create collections in Kotlin:

val set = hashSetOf(1, 7 ,53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
Enter fullscreen mode Exit fullscreen mode

NOTE: Note that to in the hashMapOf is a normal function Kotlin. We will cover it later in the section Infix Calls.

NOTE: Kotlin uses standard Java collections to make it easier to interact with Java.

Java collections have a default toString implementation and the formatting of the output is fixed. If we want to change this formatting, we use third party libraries like Guava or Apache Commons. In Kotlin, a function to handle this issue is part of the standard library.
We use joinToString function to accomplish the mentioned task. This function appends elements of the collection to a StringBuilder, with a separator between them, a prefix at the beginning and a postfix at the end:

fun <T> joinToString(
    collection: Collection<T>,
    separator: String,
    prefix: String,
    postfix: String

) : String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
Enter fullscreen mode Exit fullscreen mode

We will edit this function to make it less verbose.

Named Arguments

The first problem with Java's function calls is that if they have a number of arguments especially with the same type (as we have with the joinToString function), the role of each argument is not clear. This is especially problematic with boolean flags. In Java, some programmers use enums as boolean flags and others write comments in the function declaration before each parameter.
Fortunately, Kotlin has come up with a better solution. We can write the name of each argument and assign a value to that. This makes the code much more readable:

val list = listOf(1, 7, 53)

joinToString(
collection = list, separator = ",", prefix = "(", postfix = ")"
)
Enter fullscreen mode Exit fullscreen mode

NOTE: If we specify the name of an argument in a call, we must specify the name of all other arguments after that.

NOTE: Unfortunately, we can't use name arguments when calling methods from Java.

Default Parameter Values

One of the problems with Java functions is the huge number of overloaded methods. Programmers provide overloaded methods for several reasons: backward compatibility, convenience of API users or other reasons. Most of the times, we have a method with all the parameters and other overloaded methods are just a simpler version of that method that omitted some of the parameters and provided them with default values.
In Kotlin, we can avoid overloaded methods because we can provide default parameter values:

fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""

): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

val list = listOf(1, 7, 53)

joinToString(
collection = list, prefix = "(", postfix = ")"
)
joinToString(
collection = list, separator = "- "
)
Enter fullscreen mode Exit fullscreen mode

Here, we have provided default values for separator, prefix and postfix. So, we can omit them in some calls or provide all of them. If we wanted to have exact the same functionality in Java, we should have provided 8 overloaded methods.

NOTE: When we are using normal function calls, we should keep the ordering of the arguments the same as the parameters in the function and you can only omit the trailing arguments. But with name arguments, we can provide arguments at any order and we can omit any argument with default values anywhere in the function.

NOTE: Default values are encoded in the function being called, not at the call site.

Default Values and Java

When we want to use default values from Java code, we should use the @JvmOverloads annotation on the function. This annotation will tell the Kotlin compiler to provide the Java overloaded functions we need to call from Java.

Top-Level Functions and Properties

Unlike Java, Kotlin does not require us to write all functions and properties in classes. This is specially useful because we can avoid create Utility classes. We can write functions at the top-level of a source file, outside of any class. Such functions are still a part of the package they are declared in and we should import them to use them in other packages.

package strings

fun joinToString(...) : String { ... }
Enter fullscreen mode Exit fullscreen mode

How is this compiled and how does it run? Top-level functions and properties are compiled to static methods and static members of a class. Which class? The compiler will create a class called FileNamekt.java and when we want to call these functions from Java, we should use filename.theFunction().
Why does this happen? Because JVM can only execute codes in a class.
Let's assume the above code is in join.kt file. Here's how we can use it in our Java code:

import strings.JoinKt

public class MyClass {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("one", "two", "three");
        JoinKt.joinToString(list, ", ", "", "")
    }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: To change the class name that contains top-level functions generated by the Kotlin compiler, we can use the @file:JvmName("MyPreferredFileName") annotation. We must put this annotation at beginning of the file, even before the package name.
Here we can see a Kotlin file with top-level functions named Join.Kt:

@file:JvmName("StringFunctions")

package strings

fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""

): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
Enter fullscreen mode Exit fullscreen mode

Java class that calls the new name of the Kotlin top-level functions:

import strings.StringFunctions

public class MyClass {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("one", "two", "three");
        //notice that we changed the class name with @file:JvmName annotation
        StringFunctions.joinToString(list, ", ", "", "");
    }
}
Enter fullscreen mode Exit fullscreen mode

Top-Level Properties

Properties can also be placed at the top level of a file. The value of such a property is stored in a static field.
Top-level properties can be used to define constatns:

val UNIX_LINE_SEPARATOR = "\n"
Enter fullscreen mode Exit fullscreen mode

NOTE: By default, top-level properties are exposed to Java by accessor methods: a getter for a val and a getter and a setter for a var.
NOTE: If we want to declare constant to Java code as public static final field, we must use the const modifier (this is just allowed for primitive data types as well as for strings):

const val UNIX_LINE_SEPARATOR = "\n" //-> public static final String UNIX_LINE_SEPARATOR = "\n";
Enter fullscreen mode Exit fullscreen mode

Extension Functions

An extension function is a function that can be called as a member of a class but is defined outside of it.
To declare an extension function, you need to put the name of the class or interface that you're extending before the name of the function you are adding.

fun String.lastChar() : Char = this.get(this.length - 1)
Enter fullscreen mode Exit fullscreen mode

The class name you are extending upon is called the receiver type.
The value (or the object) on which you are calling the extension function is called the receiver object.

receiver type and receiver object

To call this function, we will use the same syntax for calling ordinary class functions:

println("Kotlin".lastChar()) //this prints n
Enter fullscreen mode Exit fullscreen mode

In a sense, we have added a method to the String class. Although String is not part of our code and we do not have access to its code to modify it. It does not even matter if it is any other JVM based language. We can use this feature as long as the code is compiled to a java class.

In the body of extension functions, we can use this as we would use in a method and we can also omit the this keyword, as we can do it in a normal method.
We also have direct access to the methods and properties of the class we are extending (just like the methods of the class). However, we cannot access the private fields and methods of the class. In other words, extension function do NOT allow us to break the encapsulation.

NOTE: On the call site, we cannot distinguish extension functions from member functions of the class.

Importing Extension Functions

When we are defining an extension function, it does not become available across our entire project. So, we have to import the function like any other class or function when we want to use inside other packages:

import strings.lastChar

val c = "Kotlin".lastChar()
Enter fullscreen mode Exit fullscreen mode

We can also change the name of the class or function you are importing using the as keyword:

import strings.lastChar as last

val c = "Kotlin".last()
Enter fullscreen mode Exit fullscreen mode

Extension Functions Under the Hood

Under the hood, an extension function is a static method that accepts the receiver object as its first argument. Calling an extension function does not involve creating adapter objects or any other runtime overhead.

To call an extension function from Java, you call the static method and pass the receiver object instance. The name of the class is determined from the name of the file where the function is declared.

char c = StringUtils.lastChar("Java");
Enter fullscreen mode Exit fullscreen mode

Utility Functions as Extensions

Now, we can write the final version of the joinToString function:

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
    val result = StringBuilder(prefix)
    for((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
Enter fullscreen mode Exit fullscreen mode

We have made it an extension function to the Collection interface so we can use it like a member of a class. We have also provided default values for all the arguments.

NOTE: Because extension functions are effectively syntactic sugar over static method calls, we can use a more specific type as a receiver type, not only a class or interface:

fun Collection<String>.joinToStringSpecificVersion(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) = joinToString(separator, prefix, postfix)
Enter fullscreen mode Exit fullscreen mode

As we can see, we have made the extension function only working for collections of strings. This is possible because they are normal static methods in Java.

NOTE: Because extension function are static, they cannot be overridden in subclasses because the function that's called depends on the declared static type of the variable, not on the runtime type of the value stored in that variable. In other words, overriding does not apply to extension functions. Kotlin resolves extension functions statically.

NOTE: Member functions always take precedence to the extension functions.

Extension Properties

Extension properties provide a way to extend classes with APIs that can be accessed using the property syntax, rather than the function syntax.
Even though we call them properties, they cannot have state because there is no place to store them (we cannot add fields to existing instances of Java objects).

val String.lastChar : Char
    get() = get(length - 1)
Enter fullscreen mode Exit fullscreen mode
  • an extension property looks like a regular property with a receiver type added
  • the getter must always be defined (because there's no backing field and no default getter)
  • initializers are not allowed either for the same reason

NOTE: If we define a property for a mutable object like StringBuilder, we can make it a var because the content of it can be modified:

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
Enter fullscreen mode Exit fullscreen mode

We can access extension properties exactly like member properties

println("Kotlin".lastChar) //prints n
val sb = StringBuilder("Kotlin?")
sb.lastChar = '!' //replaces ? with !
println(sb) //prints Kotlin!
Enter fullscreen mode Exit fullscreen mode

NOTE: To access an extension property from Java, we must invoke its getter explicitly:

StringUtils.getLastChar("Java");
Enter fullscreen mode Exit fullscreen mode

Varargs

In Kotlin, when we call a function to create a list, we can pass any number of arguments to it:

val list = listOf(2, 3, 5, 7, 11)
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the listOf method to see how it is declared:

fun <T> listOf(vararg values: T) : List<T> = if (values.isNotEmpty()) values.asList() else emptyList()
Enter fullscreen mode Exit fullscreen mode

As we can see, Kotlin uses the vararg to declare such library functions. As we know from Java, vararg is a feature that allows us to pass an arbitrary number of values to a method by packing them in an array.
Kotlin uses the same concept but with different syntax. In Kotlin, instead of three dots (...), we use the vararg modifier on the parameter.
The other difference between varargs in Java and Kotlin is that in Java we can pass an array as it is to a method argument of vararg. But in Kotlin, we must explicitly unpack the array, so that every element becomes a separate argument to the function being called. This features is called spread operator. How should we accomplish this? We should put * character before the corresponding argument:

fun spreadOperator(args: Array<String>) {
    val list = listOf(*args)
    val listWithFixedValue = listOf("args:", *args)
}
Enter fullscreen mode Exit fullscreen mode

As we can see in the listWithFixedValues, in Kotlin, we can pass some values alongside with the array when we are using varargs parameter. This feature is not supported by Java.

Infix Calls

In an infix call, the method name is placed immediately between the target object name and the parameter, with no extra separators. Infix call can be used with regular methods and extension functions that have one required parameter. The to function in the mapOf function to create a map is an infix call:

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
Enter fullscreen mode Exit fullscreen mode

To declare an infix call for a function, we need to mark it with the infix modifier. Here we can see a simplified version of the to function:

infix fun Any.to(other: Any) = Pair(this, other)
Enter fullscreen mode Exit fullscreen mode

Here, the function returns a Pair which is a Kotlin standard library class.

We can also initialize two variables with the contents of a Pair directly:

val (number, name) = 1 to "one"
Enter fullscreen mode Exit fullscreen mode

This feature is called destructuring declaration.

We can use the destructuring declaration feature in for loop with a map for its keys and values, with collections in a loop to have the element and the index. Let's take a look at some code:

val (pairNumber, pairName) = 1 to "one"

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
for ((number, name) in map) {
    println("$number = $name")
}

val list = listOf("one", "seven", "fifty-three")
for ((index, element) in list.withIndex()) {
    println("index $index element is: $element")
}
Enter fullscreen mode Exit fullscreen mode

Local Functions

Kotlin supports local functions: a function inside another function. A local function has access to the variables and parameters of the parent function. This feature is useful to remove duplicate code. As an example, if take a look at the code below, we can see that it has a duplicate code that can be removed:

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
    }
    // save the user to the database
}
Enter fullscreen mode Exit fullscreen mode

We can extract the validation logic into a local function, and call that function within the parent function. It reduces the duplicate code, and if we want to change the logic, we can do it in just ONE place! Let's take a look at the code:

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty())
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the local function has access to the parameters of the parent function!

To make the code even more tidy, we can make the saveUser() function an extension function(instead of being a part of the User class). This way, the class is not polluted with a code that is not a part of the class responsibility and not needed by most of the clients(here, just the repository class needs this function to save it to the database!).

This is the end of part 2 which was the summary of chapter 3. There were other parts in chapter 3 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.

Oldest comments (0)