Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book “Kotlin In Action”. While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.

Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics


Create the collection in Kotlin

In the previous chapter we created a set using the setOf function. Similarly, we can create a list or map in a similar way:

val set = setOf(1, 2, 3)
val list = listOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two")
Copy the code

To is not a special structure, but a general function, which we will explore later. Have you ever wondered what types of sets, lists, and maps are created here? The type can be obtained via the.javaclass property, which is equivalent to the getClass() method in Java:

Println (set.javaclass) println(list.javaclass) println(map.javaclass) // Output class java.util.linkedhashSet class java.util.Arrays$ArrayList
class java.util.LinkedHashMap
Copy the code

As you can see, all are standard Java collection classes. Kotlin doesn’t have its own special collection class to make it easier to interact with Java code. When a Java function is called from Kotlin, it doesn’t have to convert its collection class to match the Java class, and vice versa. Although Kotlin’s collection classes are identical to Java’s, Kotlin doesn’t stop there. For example, to get the last element in a list, or to get the maximum value of a list of numbers:

val strings = listOf("first"."second"."fourteenth")
println(strings.last())
val numbers = setOf(1, 14, 2) println(number.max ()) // Output fourteenth 14Copy the code

Maybe you should know that last() and Max () do not exist in Java’s collection classes. This should be Kotlin’s own extension of the methods. You should know that the types we printed above are explicitly Java’s collection classes, but the objects that call the methods here are those collection classes. How do you get a Java class to call a method that it doesn’t already have? You’ll find out later when we talk about extension functions!

Make the function easier to call

Now that we know how to create a collection, let’s print its contents. Java collections all have a default toString implementation, but its output is fixed and often not what you need:

Val list = listOf(1, 2, 3) println(list)Copy the code

Suppose you want to separate each element with a semicolon and enclose it in parentheses instead of the default implementation. To solve this problem, Java projects use third-party libraries, such as Guava and Apache Commons, or rewrite the printing function in this project. In Kotlin, its standard library has a special function to handle this situation. But instead of using Kotlin’s tools, we’ll write our own implementation function:

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        ifAppend (separator) // Do not add the separator before the first element result.append(element)} result.append(postfix)return result.toString()
}
Copy the code

This function is generic and can support collections of elements of any type. Let’s verify that this function works:

val list = listOf(1, 2, 3)
println(joinToString(list, ";"."(".")") // Output (1; 2; 3)Copy the code

It seems to work, so what we need to think about is how can we make this function call cleaner? After all, passing in four arguments per call is a hassle.

Named parameters

Our first concern is the readability of the function. Consider joinToString:

joinToString(list, "".""."")
Copy the code

Can you see what these strings correspond to? You may have to use an IDE tool or look at the function description or the function itself to know what these parameters mean. In Kotlin, it can be done more elegantly:

println(joinToString(list, separator = "", prefix = "", postfix = ""))
Copy the code

When you call a function defined by Kotlin, you can display the names of the parameters. If you specify the name of an argument when calling a function, all subsequent arguments should be named to avoid confusion.

Ps: When you call java-defined functions in Kotlin, you cannot use named arguments. Because saving parameter names to.class files is an optional feature of Java8 and later, Kotlin needs to remain compatible with Java6.

At this point you might just think naming the parameters makes the function easier to understand, but the calls get complicated and I have to write the names of the parameters! Don’t worry, when combined with the default parameters described below, you will know how to name parameters.

Default Parameter Value

Another common problem with Java is that some classes have too many overloaded functions. Most of these overloads are intended for backward compatibility and convenience to users of the API, which ultimately results in duplication. In Kotlin, you can specify default values for parameters when declaring functions to avoid creating overloaded functions. Let’s try to improve the previous joinToString function. In most cases, we’ll probably just change the delimiter or change the prefix and suffix, so we’ll set these to default:

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ",",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        ifAppend (separator) // Do not add the separator before the first element result.append(element)} result.append(postfix)return result.toString()
}
Copy the code

Now when you call this function, you can omit arguments with default values, just like overloaded functions declared in Java.

println(joinToString(list))
println(joinToString(list, ";"1,2,3, 1; 2; 3Copy the code

When you use regular calling syntax, you must give the arguments in the order defined in the function specification. Only the last arguments can be omitted. If you use named arguments, you can omit some of the arguments, or you can give only the arguments you need in any order you want:

// Scrambles the argument order and the separator argument uses the default println(joinToString(prefix =)"{", collection = list, postfix = "}"{1,2,3}Copy the code

Note that the default value of the argument is compiled into the function being called, not where it was called. If you change the parameter defaults and recompile the function, callers who do not reassign parameters will start using the new default values.

Java has no concept of parameter defaults, and when you call the Kotlin function from Java, you must specify all parameter values. If you need to call it from Java code to make it easier, use the @jvMoverloads annotation function. This instructs the compiler to generate Java’s overloaded functions, omitting each one starting with the last. For example, in joinToString, the compiler generates the following overloaded functions: public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) public static final String joinToString(@NotNull Collection collection, Separator) public static final String joinToString(@notnull Collection Collection) When your project has both Java and Kotlin, it’s a good idea to habituallyannotate functions with @jvMoverloads that have default values.

Eliminate static utility classes: top-level functions and properties

Java, as a face-to-object language, requires all code to be written as class functions. However, there are always some functions in the project that don’t belong to any of the classes, resulting in classes that don’t contain any state or instance functions, but are simply containers for a bunch of static functions. In the JDK, the most obvious example is Collections, and does your project have a lot of classes with the Util suffix? In Kotlin, there is no need to create these meaningless classes, you can put these functions directly at the top of the code file, without belonging to any class. The joinToString function was actually defined directly in the join.kt file.

package com.huburt.imagepicker @JvmOverloads fun <T> joinToString(...) : String {... }Copy the code

How does this work? When this file is compiled, classes are generated because the JVM can only execute code in classes. That’s all you need to know when you’re using Kotlin. But if you need to call these functions from Java, you must understand how it will be compiled. Here’s what the compiled class looks like:

package com.huburt.imagepicker public class JoinKt { public static String joinToString(...) {... }}Copy the code

You can see the name of the class generated by Kotlin’s compilation, which corresponds to the name of the file containing the function, where all top-level functions are compiled as static functions of the class. Therefore, when calling this function from Java, it is as simple as calling any other static function:

import com.huburt.imagepicker.JoinKt

JoinKt.joinToString(...)
Copy the code

Change the file class name

Kotlin provides a way to change the name of the generated class. Just annotate the Kt file with @jVMName and place it at the beginning of the file, before the package name:

@file:JvmName("Join"Package com.huburt.imagepicker @jvMoverLoads fun <T> joinToString(...) : String {... }Copy the code

Now you can call this function with the new class name:

import com.huburt.imagepicker.Join

Join.joinToString(...)
Copy the code

The top attributes

Like functions, attributes can be placed at the top of a file. Static properties are nothing special from a Java perspective, and are rarely used because there are no classes. Note that top-level functions, like any other property, are exposed to Java for use by default through accessors (that is, through getter and setter methods). For ease of use, if you want to expose a constant to Java as a public static final property, you can use const to decorate the property:

const val TAG = "tag"
Copy the code

This is equivalent to Java:

public static final String TAG = "tag"
Copy the code

Add methods to someone else’s class: extension functions and attributes

One of Kotlin’s great features is smooth integration with existing code. You can start using Kotlin entirely from the original Java code. For the original Java code can not modify the source code under the circumstances of the extension function: extension function. This is where I think Kotlin’s greatest strength is. An extension function is very simple. It is a member function of a class, but it is defined outside the class. For the sake of illustration, let’s add a method that evaluates the last character of a string:

Fun String.lastchar (): Char = this.get(this.length - 1)Copy the code

All you have to do is put the name of the class or interface you want to extend in front of the function you want to add. The name of the class is called the receiver type. The object used to call this extension function is called the receiver object. We can then call this function as if it were a normal member function of the class:

println("Kotlin".lastchar ()) // outputs nCopy the code

In this case, String is the receiver type and “Kotlin” is the receiver object. In this extension function, we can use this like any other member function, or we can omit it like any normal function:

Package strings fun string.lastchar (): Char = get(length-1) // Omit this call String other functionsCopy the code

Import extension functions

The extension function you define does not automatically apply to the entire project. If you need to use it, you need to import it, and importing a single function has the same syntax as importing a class:

import strings.lastChar

val c = "Kotlin".lastChar()
Copy the code

You can also use * to represent everything in a file: import strings.*

You can also use the as keyword to change the name of an imported class or function:

import strings.lastChar as last

val c = "Kotlin".last()
Copy the code

Renaming at import time solves the problem of duplicate function names.

Call extension functions from Java

In fact, extension functions are static functions that take the calling object as the first argument to the function. Calling an extension function in Java is just like any other top-level function, using a.kt file to generate a Java class that calls a static extension function, passing the receiver object to the first argument. For example, the lastChar extension function mentioned above is defined in stringutil. kt and can be called in Java like this:

char c = StringUtilKt.lastChar("Java")
Copy the code

Utility functions as extension functions

Now we can write a final version of the joinToString function, which looks exactly like the one you see in the Kotlin library:

@JvmOverloads
fun <T> Collection<T>.joinToString(
        separator: String = ",",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)
    for ((index, element) inThis.withindex ()) {// This is a collection of receiver objects, that is, Tif (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    returnPrintln (list.joinToString()) println(list.joinToString())";"))
println(list.joinToString(prefix = "{", postfix = "}"))
Copy the code

An extension function that takes the original argument Collection and writes it as a receiver type uses methods like a member function of the Collection class (of course, the Java call is still a static method, passing the first argument to the Collection object).

An extension function that cannot be overridden

Let’s start with an example of rewriting:

Open class View {open fun click() = println()"View clicked")
}

class Button : View() {override fun click() = println("Button clicked")}Copy the code

When you declare a variable of type View, it can be assigned to an object of type Button, since Button is a subclass of View. When you call a generic function of this variable, such as click, if the Button overwrites the function, name will call the overwritten function of the Button:

Val View: view = Button() view.click() //Copy the code

But for extension functions, this is not the case. Extension functions are not part of the class; they are declared outside the class. Although it is possible to define an extension function of the same name for both a base class and a subclass, when this function is called, which one does it use? Here, it is determined by the static type of the variable, not the runtime type of the variable.

fun View.showOff() = println("i'm a view!")

fun Button.showOff() = println("i'm a button!") val view: view = Button() view.click('m a view!
Copy the code

The extension function is called when you call showOff on a variable of type View, even though the variable is now a Button object. Recall that extension functions are compiled as static functions in Java and accept the value as the first argument. The two showOff extension functions are static functions with different arguments,

View view = new Button(); XxKt.showOff(view); // defined in xx.kt fileCopy the code

The type of the argument determines which static function to call. To call the Button extension function, you must first convert the argument to the Button type: xxkt.showoff ((Button)view);

Therefore, extension functions also have limitations. Extension functions can be extended, that is, they can define new functions, and cannot be overridden to change the implementation of the original function (which is essentially a static function). What happens when Kotlin calls a class extension function with the same name as its member function? (There is no such concern in Java, which is called differently)

open class View {
    open fun click() = println("View clicked")
}

fun View.click() = println("Extension function") val view = view () view.click() //Copy the code

If you call code in Kotlin that always executes a member function with the same name as an extension function, the extension function is not defined. This point needs special attention in the actual development!

Extended attributes

Extended properties provide a way to extend a class’s API to access properties using property syntax instead of function syntax. Although they are called attributes, they can have no state, because there is no proper place to store them, and it is impossible to add additional fields to an existing instance of a Java object. Here’s an example:

val String.lastChar: Char
    get() = get(length - 1)
Copy the code

Also gets the last character of the string, this time defined as an extended attribute. The extended property is also like a normal member property of the receiver, where the getter function must be defined because there are no supporting fields and therefore no implementation of the default getter. Likewise, initialization does not work: there is no place to store the initial value. Var = val; var = val; var = val;

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value) {
        setCharAt(length - 1, value)
    }
Copy the code

Remember the custom accessors from the last article? The val property is immutable, so you only need to define the getter, while the var property is mutable, so you need both the getter and setter. While extended attributes may not be well understood or confused with real attributes, the Java code for converting extended attributes is listed below, so you’ll get a better idea.

   public static final char getLastChar(@NotNull String $receiver) {
      return $receiver.charAt($receiver.length() - 1);
   }

   public static final char getLastChar(@NotNull StringBuilder $receiver) {
      return $receiver.charAt($receiver.length() - 1);
   }

   public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
      $receiver.setCharAt($receiver.length() - 1, value);
   }
Copy the code

Same as extension functions, only static functions: provides the ability to get lastChar. This definition can be used in Kotlin in the same way as normal property calls, giving you the impression that they are properties, but are essentially static functions in Java.

Processing collections: mutable arguments, infix calls, and library support

An API that extends Java collections

val strings = listOf("first"."second"."fourteenth")
println(strings.last())
val numbers = setOf(1, 14, 2)
println(numbers.max())
Copy the code

Remember that we used the above method to get the last element in the list and the maximum value in the set. By now you probably know that last() and Max () are extension functions, so click on the method to check for yourself.

Variable parameter

If you also looked at the listOf function definition, you must have seen this:

public fun <T> listOf(vararg elements: T): List<T>
Copy the code

The vararg keyword, which allows the function to support any number of arguments. The same variable argument in Java is the type followed by… In Java, the above method is:

public <T> List<T> listOf(T... elements)
Copy the code

But Kotlin’s mutable arguments are a bit different from Java: Whereas the syntax of calling a function when the parameters to be passed are already wrapped in an array, in Java you can pass the array as is, Kotlin requires that you explicitly unpack the array so that each array element can be called as a separate argument in the function. Technically, this function is called the expansion operator, and is used simply by placing an * in front of the corresponding argument:

val array = arrayOf("a"."b")
val list = listOf("c", array)
println(list)
val list2 = listOf<String>("c", *array) println(list2) // output [c, [ljava.lang.string;@5305068a] [c, a, b]Copy the code

By contrast, you can see that if you don’t add *, you’re treating an array object as an element of a collection. The addition of * adds all the elements of the array to the collection. ListOf can also specify generic

, you can try to add generics to listOf(“c”, array), the second parameter array will tell you that the type is not correct.

Without expansion in Java, we could also call Kotlin’s listOf function, declared in the collections.kt file:

List<String> strings = CollectionsKt.listOf(array);
System.out.println(strings);
//List<String> strings = CollectionsKt.listOf("c", array); // cannot compile // output [a, b]Copy the code

In Java, you can pass in an array directly, but you cannot pass in both a single element and an array.

Handling of key-value pairs: infix calls and destruct declarations

Remember how to create a map?

val map = mapOf(1 to "one", 7 to "seven", 52 to "fifty-five")
Copy the code

As mentioned earlier, to is not a built-in construct, but a special function call called an infix call. In infix calls, no additional delimiters are added, and the function name is placed directly between the target object name and the parameter. The following two calls are equivalent:

1.to("one")// Normal call 1 to"one"// infix callCopy the code

Infix calls can be used with functions that have only one argument. In other words, infix calls can be supported in Kotlin as long as the function has only one argument, whether it is a normal function or an extension function. To allow a function to be called using an infix symbol, you need to mark it with the infix modifier. For example the declaration of the to function:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Copy the code

The to function returns an object of type Pair, a Kotlin library class that represents a Pair of elements. We can also initialize two variables directly from the contents of the Pair:

val (number, name) = 1 to "one"
Copy the code

This function is called a destruct declaration. 1 to “one” returns a Pair of elements, 1 and one, and then defines variables (number, name) to refer to 1 and one in the Pair. The destruct declaration feature is not just for pairs. You can also initialize two variables using the map’s key and value contents. It also applies to loops, as you can see in the joinToString implementation of the withIndex function:

for ((index, element) in collection.withIndex()) {
    println("$index.$element")}Copy the code

The to function is an extension function that can create a pair of any elements, which means it is an extension of the generic receiver: it can be written as 1 to “one”, “one” to 1, list to list.size(), and so on. Let’s look at the declaration of the mapOf function:

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
Copy the code

Like listOf, mapOf takes a variable number of arguments, but this time they should be key-value pairs. While creating a map in Kotlin might seem like special deconstruction, it’s just a regular function with a concise syntax.

String and regular expression processing

Kotlin defines a series of extension functions to make standard Java strings easier to use.

Split string

In Java, we use the String split method. But sometimes something unexpected happens, for example when we write “12.345-6.a “.split(“.”), we expect an array of [12, 345-6, A]. But Java’s split method returns an empty array! This is because it takes a regular expression as an argument and splits the string into multiple strings based on the expression. The point here (.) Is a regular expression that represents any character. This convoluted situation does not occur in Kotlin, because regular expressions require a Regex type bearer, not a String. This ensures that strings are not treated as regular expressions.

println(12.345 6. "A".split("\ \. | -".toregex ())) // Explicitly create A regular expression // output [12, 345, 6, A]Copy the code

Here the regular expression syntax is exactly the same as in Java, where we match a dot (escaping it to indicate the literal we are referring to) or a dash. For some simple cases, regular expressions are not needed, and other overloading of the spilt extension function in Kotlin supports any number of plain text string delimiters:

println(12.345 6. "A".split("."."-") // Specify multiple delimitersCopy the code

Equivalent to the partition of the re above.

Regular expression and triple quoted string

Now there is such a demand: parsing the full path to the file name/Users/Hubert/kotlin/chapter. Adoc to the corresponding components: directory, filename and extension. The Kotlin library contains functions that can be used to get substrings before (or after) the first (or last) occurrence of a given delimiter.

 val path = "/Users/hubert/kotlin/chapter.adoc"
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension"Dir: /Users/ Hubert /kotlin, name: chapter, ext: adocCopy the code

Parsing strings becomes easier in Kotlin, but if you still want to use regular expressions, there’s no problem:

val regex = """(. +)/(. +) \. (. +)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchResult ! = null) { val (directory, fileName, extension) = matchResult.destructured println("Dir: $directory, name: $fileName, ext: $extension")}Copy the code

Here the regular expression is written in a triple-quoted string. In such a string, no characters need to be escaped, including backslashes, so you can use \. Instead of \\. To represent dots, as in writing a literal of a normal string. In this regular expression: the first paragraph (.+) represents the directory, the/represents the last slash, the second paragraph (.+) represents the file name, \. Represents the last point, and the third paragraph (.+) represents the extension.

A multi-line, triple-quoted string

The purpose of the triple quoted string is not only to avoid escaping characters, but also to allow it to contain any character, including newlines. It provides an easier way to embed newline text into programs:

val kotlinLogo = """| / /. | / /. | / \""".trimMargin(".")
print(kotlinLogo) / / o | | / / / / \ |Copy the code

The multi-line string contains all the characters between the triple quotes, including the indentation used to format the code. To better represent such strings, you can remove the indentation (left margin). To do this, you prefix the content of the string, mark the end of the margin, and then call trimMargin to remove the prefix and the preceding whitespace from each line. Used in this example. As a prefix.

Make your code cleaner: local functions and extensions

Many developers believe that one of the most important criteria for good code is to reduce duplicate code. Kotlin provides local functions to solve common code duplication problems. The following example verifies the user’s information before saving it to the database:

class User(val id: Int, val name: String, val address: String)

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 Name"} // Save user to database}Copy the code

The code that checks each attribute individually is repetitive code, especially when there are many attributes. In this case, putting the validated code into local functions eliminates duplication while maintaining a clean code structure. A local function, as its name implies, is a function defined within a function. We use local functions to modify the above example:

class User(val id: Int, val name: String, val address: String) fun saveUser(user: Fun validate(value: String, fieldName: String) {// Declare a local function fun validate(value: String, fieldName: String) {if(value.isempty ()) {// Local functions can access arguments of external functions directly: user throw IllegalArgumentException(value.isempty ())"Can't save user ${user.id}:empty $fieldName")
        }
    }
    validate(user.name,"Name")
    validate(user.address,"Address"// Save user to database}Copy the code

We can still improve by extracting logic into extension functions:

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id:empty $fieldName")
        }
    }
    validate(name, "Name"// The extension function accesses the attribute validate(address,"Address")} fun saveUser(user: user) {user.validatebeforesave () // saveUser to database}Copy the code