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")
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()
}
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 = ")"
)
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 = "- "
)
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 { ... }
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, ", ", "", "")
}
}
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()
}
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, ", ", "", "");
}
}
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"
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";
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)
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.
To call this function, we will use the same syntax for calling ordinary class functions:
println("Kotlin".lastChar()) //this prints n
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()
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()
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");
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()
}
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)
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)
- 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)
}
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!
NOTE: To access an extension property from Java, we must invoke its getter explicitly:
StringUtils.getLastChar("Java");
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)
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()
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)
}
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")
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)
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"
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")
}
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
}
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")
}
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.
Top comments (0)