jemerov_00 By Dmitry Jemerov and Svetlana Isakova

In this article, excerpted from Kotlin in Action,

 

Java collections have a default toString implementation, but its output is fixed and it’s not always what you need. Almost every project has its own StringUtil class with a bunch of functions including joinToString. The Kotlin standard library is no exception.

>>> val list = arrayListOf(1, 2, 3)
 >>> println(list)  
 [1, 2, 3]

invokes toString()

Imagine we need the elements to be separated by semicolon and surrounded by round brackets instead of the default square ones:

>>> println(joinToString(list, "; ", "(", ")"))
 (1; 2; 3)

Let’s introduce the function joinToString without using any of Kotlin’s new features in this area, and then rewrite it in a more idiomatic style. The function just appends the elements of the collection to a StringBuilder, with a separator between them and surrounded by prefix and postfix. The function is generic: it works on collections that contain elements of any type. As you can see, the syntax for generics is very similar to Java. There are a few differences, but we’ll leave them for a later chapter.

fun <T> joinToString(
         collection: Collection<T>,
         separator: String,
         prefix: String,
         postfix: String
 ): String {  
     val result = StringBuilder(prefix)
       var count = 0
     for (element in collection) {
         if (count++ > 0) result.append(separator) 
         result.append(element)
     }
  
     result.append(postfix)
     return result.toString()
 }

don’t append a separator before the first element

The implementation above is fine, and we’ll mostly leave it as is. What we’ll focus on is the declaration: how can we change it to make calls of this function less verbose?

Maybe we could avoid having to pass four arguments every time we’re calling the function? Let’s see what we can do.


Named Arguments

The first problem that we’re going to address has to do with the readability of function calls. For example, look at the following call of the joinToString function:

joinToString(collection, " ", " ", ".")

Can you tell what parameters all these Strings correspond to? Are the elements separated by the whitespace or the dot? These questions are very hard to answer without looking at the signature of the function. Maybe you remember it, or maybe your IDE can help you, but it’s not obvious from the calling code.

This problem is especially common with boolean flags. To solve it, some Java coding styles recommend creating enum types instead of using booleans. Others even require you to specify the parameter names explicitly in a comment (like for String arguments below).

joinToString(collection, /* separator */ " ",  /* prefix */ " ",     /* postfix */ ".");

With Kotlin, we can do better than that.

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

When calling a method written in Kotlin, we can specify the names of some arguments that we’re passing to the function. Needless to say, IntelliJ IDEA will keep the names up to date if we rename the parameter of the function being called. If you specify the names of some arguments, the remaining arguments should go accordingly to their order in the parameter list.

WARNING

Named Arguments and Java

Unfortunately you can’t use named arguments when calling methods written in Java, including methods from the JDK and the Android framework. Method parameter names are not preserved in Java .class files, so the compiler will not be able to recognize the parameter names used in your call and match them against the method definition.

LIBRARIES AND NAMED ARGUMENTS

Even though named arguments are great for developers that need to call a library function, they make life somewhat more difficult for a library developer. In Java, when you’re writing a library and want to maintain backwards compatibility with previous versions of your library, you only need to make sure that your method names, parameter types and return types stay the same. Parameter names do not matter, because they are not part of a method signature. This means that you can always rename parameters, and it can never break anything in code that uses your library.

In Kotlin, the situation is different. Because any client of your library can use named arguments when calling any of your functions, parameter names also become part of the API of your library. If you rename a parameter of a public API method, compiled code that uses your library will continue to work. However, if a client of your library was passing a value to that parameter using the named parameter syntax, the client code will no longer compile after you rename the parameter, because it will refer to a parameter name that no longer exists. Therefore, you need to make sure that you don’t change parameter names as you evolve your library.

Named arguments work especially well with default parameter values, which we’re going to look at next.


Default Parameter Values

Another Java problem that comes into play fairly often is the over-abundance of overloaded methods in some classes. Just look at java.lang.Thread and its eight constructors ! There are multiple reasons why this can happen – maybe we’ve had to [1] introduce new parameters without breaking backwards compatibility, or maybe we want to provide maximum convenience to the users of our API – but the end result is the same: duplication. The parameter names are repeated over and over, and if we’re being good citizens, we also have to repeat most of the documentation in every overload. At the same time, if we call an overload that omits some parameters, it’s not always clear which values are used for them.

Kotlin gives us a much better solution by letting us specify default values for parameters in a function declaration. Let’s use that to improve our joinToString function! For most cases the strings could be separated by commas without any prefix or postfix. So let’s make these values the default ones:

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

  default parameter values

Now we can either invoke the function with all the arguments or omit some of them:

>>> joinToString(list)
 1, 2, 3
 >>> joinToString(list, "; ")
 1; 2; 3

When using the regular call syntax, you can omit only trailing arguments. If you use named arguments, you can omit some arguments from the middle of the list and specify only the ones you need:

>>> joinToString(list, prefix = "# ")
 # 1, 2, 3

Note that the default values of the parameters are encoded in the class being called, not in the calling class. If you change the default value and recompile the class containing the function, the callers which haven’t specified a value for the parameter will start using the new default value.

We wrote a nice utility function without paying much attention to the surrounding context. Surely, it must have been a method of some class, and we have simply omitted the surrounding class declaration, right? In fact, Kotlin makes this unnecessary.


Getting Rid of Static Utility Classes: Top-level Functions and Properties

You all know that Java, as an object-oriented language, requires all code to be written as methods of classes. Usually, this works out nicely, but in reality almost every large project ends up with a lot of code that does not clearly belong to any single class. Sometimes an operation works with objects of two different classes which play an equally important role for it, and sometimes there is one primary object but you don’t want the operation to be its instance method in order to avoid bloating the API.

As a result of this, you end up with classes that don’t contain any state or any instance methods, and act simply as containers for a bunch of static methods. A perfect example of such a class is the Collections class in the JDK. To find examples of such classes in your own code, simply look for classes which have Util as part of the name.

Kotlin lets you avoid creating all those meaningless classes by allowing you to place functions directly at the top level of a source file, outside of any class. Such functions are still members of the package declared on the top of the file, and you still need to import them if you want to call them from other packages, but the extra unnecessary level of nesting no longer exists.

Let’s put the joinToString function into the strings package directly. We will create a file join.kt with the following contents:

package strings
public fun joinToString(...): String { ... }

Now, how does this run? You know that, when you compile that file, some classes will be produced, because the JVM can only execute code in classes. When you work only with Kotlin, that’s all you need to know. However, if you’re gradually introducing Kotlin into an existing Java project, you need to understand which classes and methods exactly will be generated, so that you would know how to call the methods. In order to make it clear, let’s look at the Java code that would compile to the same class:

package strings;
 public class JoinKt {  
     public static String joinToString(...) { ... }
  
 }

❶ corresponds to “join.kt”, the filename of the previous example

You can see that the name of the class generated by the Kotlin compiler corresponds to the name of the file in which the function was contained. All top-level functions in the file are compiled to static methods in that class. Therefore, calling this method from Java is as easy as calling any other static method:

import strings.JoinKt;
  
 ...
  
 JoinKt.joinToString(list, ", ", "", "");
  

WARNING

Changing the File Class Name

To change the name of the class, you can add a special @JvmName annotation to the file. The syntax for this looks like this: You place it in the beginning of the file before the package name, like in the following example:

@file:JvmName("StringFunctions")    
package strings  
public fun joinToString(...): String { ... }

the annotation to specify the class name

the package statement follows the file annotations

Now the function can be called as:

import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");

A detailed discussion of the annotation syntax comes later in the book.

TOP-LEVEL PROPERTIES

Just like functions, properties can be placed at the top level of a file as well. Storing individual pieces of data outside of a class is not as often needed, but is still useful. The most common case is probably constants:

val UNIX_LINE_SEPARATOR = "\n"

As you probably expect for a constant, this will be compiled to a public static final field, equivalent to the following Java code:

public static final String UNIX_LINE_SEPARATOR = "\n";

var properties are supported as well. For example, you can use a var property to count the number of times some operation has been performed.

var operationCount = 0  
  
 fun performOperation() {
     operationCount++        // ...
 }  
 fun reportOperationCount() {
     println("Operation performed $operationCount times")  
 }

  Package-level property declaration

  Change the value of the property

  Read the value of the property

The value of such a property will be stored in a static field.

You’ve improved our joinToString utility function quite a lot. Now you are ready to make it even more handy!