• No se han encontrado resultados

Descripción del programa

2.2. Adaptación y ampliación del programa

Now that you know how to create a collection of elements, let’s do something straightforward: print its contents. Don’t worry if this seems overly simple; along the way, you’ll meet a bunch of important concepts.

Java collections have a default toString implementation, but the formatting of the output is fixed and not always what you need:

Invokes toString()

Imagine that you need the elements to be separated by semicolons and surrounded by parentheses, instead of the brackets used by the default implementation: (1; 2; 3). To solve this, Java projects use third-party libraries such as Guava and Apache Commons, or reimplement the logic inside the project. In Kotlin, this function is part of the standard library.

In this section, you’ll implement this function yourself. You’ll begin with a straightforward implementation that doesn’t use Kotlin’s facilities for simplifying function declarations, and then you’ll rewrite it in a more idiomatic style.

The followingjoinToString function appends the elements of the collection to a

StringBuilder, with a separator between them, a prefix at the beginning, and a postfix at the end:

>>> val list = listOf(1, 2, 3)

>>> println(list)

for ((index, element) in collection.withIndex()) { if (index > 0) result.append(separator) result.append(element)

}

result.append(postfix) return result.toString() }

Don’t append a separator before the first element.

The function is generic: it works on collections that contain elements of any type. As you can see, the syntax for generics is similar to Java. (A more detailed discussion of generics will be the subject of chapter 9.)

Let’s verify that the function works as intended:

The implementation is fine, and you’ll mostly leave it as is. What we’ll focus on is the declaration: how can you change it to make calls of this function less verbose? Maybe you could avoid having to pass four arguments every time you call the function. Let’s see what you can do.

3.2.1 Named arguments

The first problem we’ll address concerns the readability of function calls. For example, look at the following call ofjoinToString():

Can you tell what parameters all these `String`s correspond to? Are the elements separated by the whitespace or the dot? These questions are 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, as is the case for String

arguments:

With Kotlin, you can do better:

When calling a method written in Kotlin, you can specify the names of some arguments that you’re passing to the function. If you specify the name of an argument in

>>> val list = listOf(1, 2, 3)

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

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

/* Java */

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

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

a call, you should also specify the names for all the arguments after that, to avoid confusion.

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

3.2.2 Default parameter values

Another common Java problem is the overabundance of overloaded methods in some classes. Just look at java.lang.Thread and its eight constructors 7! The overloads can be provided for the sake of backward compatibility, for convenience of API users, or for other reasons, but the end result is the same: duplication. The parameter names and types are repeated over and over, and if you’re being a good citizen, you also have to repeat most of the documentation in every overload. At the same time, if you call an overload that omits some parameters, it’s not always clear which values are used for them.

Footnote 7 http://mng.bz/1vKt

In Kotlin, you can often avoid creating overloads because you can specify default values for parameters in a function declaration. Let’s use that to improve the

joinToString function. For most cases, the strings can be separated by commas without any prefix or postfix. So, let’s make these values the defaults:

TIP Tip

Needless to say, IntelliJ IDEA can keep explicitly written argument names up to date if you rename the parameter of the function being called. Just ensure that you use the Rename or Change Signature action instead of editing the parameter names by hand.

WARNING Warning

Unfortunately you can’t use named arguments when calling methods written in Java, including methods from the JDK and the Android framework. Storing parameter names in .class files is supported as an optional feature only starting with Java 8, and Kotlin maintains compatibility with Java 6. As a result, the compiler can’t recognize the parameter names used in your call and match them against the method definition.

fun <T> joinToString(

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

): String

Default parameter values

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

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:

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

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

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

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

>>> joinToString(list, prefix = "# ")

# 1, 2, 3

/* Java */

String joinToString(Collection<T> collection, String separator, String prefix, String postfix);

String joinToString(Collection<T> collection, String separator, String prefix);

String joinToString(Collection<T> collection, String separator);

String joinToString(Collection<T> collection);

NOTE Default values and Java

Given that Java doesn’t have the concept of default parameter values, you have to specify all the parameter values explicitly when you call a Kotlin function with default parameter values from Java. If you frequently need to call a function from Java and want to make it easier to use for Java callers, you can annotate it with

@JvmOverloads. This instructs the compiler to generate Java overloaded functions, omitting each of the parameters one by one, starting from the last one.

For example, if you annotate joinToString() with

@JvmOverloads, the following overloads are generated:

Each overload uses the default values for the parameters that have been omitted from the signature.

So far, you’ve been writing your utility function without paying much attention to the surrounding context. Surely it must have been a method of some class, and you’ve omitted the surrounding class declaration, right? In fact, Kotlin makes this unnecessary.

3.2.3 Getting rid of static utility classes: top-level functions and properties

We all know that Java, as an object-oriented language, requires 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 doesn’t clearly belong to any single class.

Sometimes an operation works with objects of two different classes that play an equally important role for it. Sometimes there is one primary object, but you don’t want to bloat its API by adding the operation as an instance method.

As a result, you end up with classes that don’t contain any state or any instance methods and that act as containers for a bunch of static methods. A perfect example is the

Collections class in the JDK. To find other examples in your own code, look for classes that haveUtilas part of the name.

In Kotlin, you don’t need to create all those meaningless classes. Instead, you can place functions directly at the top level of a source file, outside of any class. Such

functions are still members of the package declared at the top of the file, and you still need to import them if you want to call them from other packages, but the unnecessary extra level of nesting no longer exists.

Let’s put thejoinToString function into thestringspackage directly. Create a file called join.kt with the following contents:

How does this run? You know that, when you compile the 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. But if you need to call such a function from Java, you have to understand how it will be compiled. To make this clear, let’s look at the Java code that would compile to the same class:

/* Java */

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 containing the function. All top-level functions in the file are compiled to static methods of that class. Therefore, calling this method from Java is as easy as calling any other static method:

package strings

fun joinToString(...): String { ... }

/* Java */

import strings.JoinKt;

...

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

@file:JvmName("StringFunctions")

package strings

fun joinToString(...): String { ... }

/* Java */

import strings.StringFunctions;

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

SIDEBAR Changing the file class name

To change the name of the generated class that contains Kotlin top-level functions, you add a @JvmName annotation to the file. Place it at the beginning of the file, before the package name:

Annotation to specify the class name

The package statement follows the file annotations.

Now the function can be called as follows:

A detailed discussion of the annotation syntax comes later, in chapter 10.