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


Basic data types

Java makes a distinction between primitive data types and reference types. A variable of a primitive data type (such as int) stores its value directly, whereas a variable of a reference type (such as String) stores a reference to the memory address containing the object. Values of primitive data types can be stored and passed around more efficiently, but you can’t call methods on them or store them in collections. Java provides special wrapper types (such as Integer) to encapsulate basic data types when you need objects. Therefore, you cannot use Collection

to define a Collection of integers, but must use Collection

. Kotlin doesn’t distinguish between primitive data types and wrapper types, you always use the same type (e.g. Int) :

val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
Copy the code

It’s very convenient. In addition, you can call a method on a value of numeric type. For example, the standard library function coerceIn is used to restrict values to a specific range:

fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)
    println("We're $percent% done!")
}

>>> showProgress(146)
We're 100% done!
Copy the code

If the base data type and reference type are the same, does that mean that Kotlin’s use of objects to represent all numbers is inefficient? It’s inefficient, so Kotlin didn’t do it. At run time, numeric types are represented in the most efficient way possible. In most cases, Kotlin’s Int type will be compiled to the Java basic Int. Of course, generics, collections, and the like are still compiled to their Java wrapper types. Kotlin types like Int can easily be compiled to their Java counterparts at the bottom, because neither type can store null references. The reverse is also true. When you use Java declarations in Kotlin, Java primitive data types become non-null types (as opposed to platform types, as shown in Kotlin tutorial 4), because they cannot hold null values.

Nullable basic data types

Nullable types in Kotlin (e.g. Int?) Cannot be represented by Java primitive data types because NULL can only be stored in variables of Java reference types. This means that any time a nullable version of the base datatype is used, it will compile to the corresponding wrapper type Int? – > Integer.

/* Kotlin */
class Dog(val name: String, val age: Int? = null)

/* Java */
Dog dog = new Dog("julie", 3);
Integer age = dog.getAge();
Copy the code

You can see val age: Int? Use in Java compiles to Integer, so be aware of the possibility of null when using in Java. And of course in Kotlin, right? .,!!!!! And so on.

To digital conversion

One important difference between Kotlin and Java is the way it handles number conversions. Kotlin does not automatically convert a number from one type to another, even to a wider range of types.

Val I = 1 val l: Long = ICopy the code

Must display for conversion:

val i = 1
val l: Long = i.toLong()
Copy the code

Every basic data type (except Boolean) defines conversion functions: toByte(), toShort(), toChar(), and so on. These functions support bidirectional conversions: you can extend a small range of types to a large range of int.tolong (), or you can intercept a large range of types to a small range of long.toint (), similar to Java, where you first ensure that the value of a large range of types exceeds the small range limit. To avoid surprises, Kotlin requires that the conversion be explicit, especially when comparing boxing values. The Equals method, which compares two boxing values, not only checks the values they store, but also compares the boxing type. In Java, new Integer(42).equals(new Long(42)) returns false. Assuming Kotlin supports implicit conversions, you might write:

val x = 1
val list = listOf(1L, 2L, 3L)
x inList // returns if Kotlin supports implicit conversionsfalse
Copy the code

But this is not what we expected. Therefore, the x in list line will not compile at all. Kotlin requires that you explicitly cast the type so that only values of the same type can be compared:

>>> val x = 1
>>> println(x.toLong() in listOf(1L, 2L, 3L))
true
Copy the code

If different numeric types are used in your code, you must explicitly convert these variables to avoid unexpected behavior.

In addition to supporting simple decimal numbers, Kotlin supports the following ways of writing numeric literals in code:

  • Use the suffix L for literal values of type Long :123L
  • Use standard floating-point numbers to represent Double literals :0.12, 2.0, 1.2e10, 1.2E-10
  • Use the suffix F to denote Float literals: 123.4f,.456f, 1e3f
  • Use the 0x or 0x prefixes for hexadecimal literals: 0xCAFEBABE, 0xbcdL
  • Use the 0b or 0b prefix to represent the binary literal: 0b000000101

Note that Kotlin 1.1 just started supporting underscores in numeric literals. For character literals, you can use almost the same syntax as Java. Write characters in single quotes, and escape sequences can be used if necessary: ‘1’, ‘\t’ (tabs), ‘\u0009’ (tabs represented by Unicode escape sequences).

You generally do not need to use conversion functions when writing numeric literals. Arithmetic operators are also overloaded and can accept all appropriate numeric types:

Fun foo(l: Long) = println(l) >>> val b: Byte = 1 >>> val l = b + 1L //Byte + Long -> Long >> foo(42) //42 is treated as Long type 42Copy the code

The Kotlin library provides a similar set of extensions to convert strings to basic data types: “42”.toint (). Each of these functions tries to parse the contents of the string to the corresponding type and throws a NumberFormatException if parsing fails.

Any) and Any? : the root type

In much the same way that Object is the root of the Java class hierarchy, Any is the supertype of all non-null Kotlin types, or Any if null values are possible. Type. At the bottom, the Any type corresponds to java.lang.object. Kotlin considers the Object type used in Java method arguments and return types to be Any (more precisely platform type, since its nullability is unknown). When the Kotlin function uses Any, it is compiled into Java bytecode Object.

Public static final void a(@notnull Object any) {} public static final void a(@notnull Object any) {}Copy the code

All Kotlin classes contain three methods: toString, equals, and hashCode. These methods all inherit from Any. Any does not use other Object methods (such as wait and notify). If you want to use these methods, you can call them by manually converting the value to Object.

Unit type: void of Kotlin

The Unit type in Kotlin does what void does in Java. It can be used as the return type of a function when it has nothing interesting to return:

fun f(): Unit {}
Copy the code

In tutorial 1, we said that Unit can be omitted: fun f() {}. In most cases, you won’t notice the difference between void and Unit. If your Kotlin function uses Unit as its return type and does not override the generic function, underneath it will be compiled into the old void function. So what’s the difference between Kotlin’s Unit and Java’s void? Unit is a complete type and can be used as a type parameter, whereas void is not. There is only one value of type Unit, which is also called Unit, and is returned implicitly (no need to display return NULL). This is useful when you’re overwriting a function that returns a generic parameter, just make the method return a value of type Unit:

interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> {
    override fun process() {
        //doSomething doesn't need to be explicitreturn}}Copy the code

By contrast, none of the possible solutions to using “no value” as a parameter type in Java is as beautiful as Kotlin’s solution. One option is to use separate interface definitions to distinguish between interfaces that indicate that a return value is required and those that do not. The other is to use a special Void type as a type parameter. Even if you choose the latter, you still need to add a return NULL; Statement to return a value that uniquely matches this type, because as long as the return type is not void, you must always have an explicit return. You may be wondering why Kotlin chose to use a different name Unit instead of calling it Void. In functional programming languages, the name Unit is traditionally used to mean “only one instance,” which is a formal distinction between Kotlin’s Unit and Java’s void. Kotlin could have used the name Void, but there is also a type of Nothing that has an entirely different function. The names of Void and Nothing are so similar that it can be confusing.

Type Nothing: This function never returns

For some Kotlin functions, the concept of return types makes no sense because they never end successfully. For example, many test libraries have a function called Fail that fails the current test by throwing an exception with a specific message. A function that contains a wireless loop will never end successfully. When analyzing code that calls such a function, it is helpful to know that the function never terminates properly. Kotlin uses a special return type Nothing to represent:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}
Copy the code

The Nothing type has no value and is meaningful only if it is used as the return value of a function or as a type parameter of the return value of a generic function. A function that returns Nothing can do a prerequisite check by calling the right side of the Elvis operator:

val address = company.address ? : fail("No address")
println(address)
Copy the code

The example above shows why having Nothing in a type system is extremely useful. The compiler knows that a function of this return type terminates abnormally, and then uses this information when analyzing the code that calls the function. In this case, the compiler will infer that the type of address is non-null because the branch handling of null will always throw an exception.

Vacuumability and set

It is important for a consistent type system to know whether collections can hold null elements. Kotlin can be very visible.

fun readNumbers(reader: BufferedReader): List<Int? > { val result = ArrayList<Int? > ()for (line in reader.lineSequence()) {
        try {
            val number = line.toInt()
            result.add(number)
        } catch (e: NumberFormatException) {
            result.add(null)
        }
    }
    return result
}
Copy the code

This function reads a list of text lines from a file and attempts to parse each line of text into a number. List

and List < Int >? The difference between. The former means that the list itself is never null, but every value in the list can be null. The latter type of variable may contain an empty reference rather than a list instance, but the elements of the list are guaranteed to be non-empty. When processing a nullable value set, first determine whether it is null. If it is, null values are not processed, that is, null values are filtered out. Kotlin provides a library function called filterNotNull to do this:
?>

>>> val list = listOf(1L, null, 3L)
>>> println(list)
[1, null, 3]
>>> println(list.filterNotNull())
[1, 3]
Copy the code

This filtering also affects the types of collections. List

, List

after filtering, because filtering ensures that the collection does not contain any null elements.

?>

Read-only and mutable collections

Kotlin split Java’s collection interface between accessing collection data and modifying collection data. Isolated read-only Collection kotlin. Collections. The Collection, using this interface can traverse the elements in the Collection, access to set size, whether the Collection contains an element, and perform other reads data from the Collection operation, but the interface without any way to add or remove elements.

public interface Collection<out E> : Iterable<E> {
    public val size: Int
    public fun isEmpty(): Boolean
    public operator fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
Copy the code

The other is kotlin. Collections. MutableCollection interface can modify data in a collection. It inherits kotlin. Collections. The Collection, provides methods to add and remove elements, empty collections such as:

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
    override fun iterator(): MutableIterator<E>
    public fun add(element: E): Boolean
    public fun remove(element: E): Boolean
    public fun addAll(elements: Collection<E>): Boolean
    public fun removeAll(elements: Collection<E>): Boolean
    public fun retainAll(elements: Collection<E>): Boolean
    public fun clear(): Unit
}
Copy the code

Just like the separation between Val and VAR, the separation of the read-only and mutable collections interfaces makes it easier to understand what happens to the data in your program. If the function takes a Collection instead of a MutableCollection as an argument, you know that it does not modify the Collection, but just reads the data from the Collection. If a function asks you to pass it a MutableCollection as an argument, it can be assumed that it will modify the data.

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
    for (item in source) {
        target.add(item)
    }
}
Copy the code

In this example we read elements from source and add them to target, so we can make a nice distinction when declaring functions: one read-only, one mutable.

One thing to keep in mind when using the collections interface is that read-only collections are not necessarily immutable. If the variable you use has a read-only interface type, it may be just one of many references to the same collection. There may be another mutable set that also refers to this set, and changes have been made to this set in another place (thread).

This separation only works in Kotlin’s code, and the above example is converted to Java code:

public static final void copyElements(@NotNull Collection source, @NotNull Collection target) {
      Iterator var3 = source.iterator();
      while(var3.hasNext()) { Object item = var3.next(); target.add(item); }}Copy the code

As you can see, it all becomes the Java Collection interface, which is a mutable complete Collection interface. That is, the collection is declared read-only in Kotlin. Java code can also modify this collection. The Kotlin compiler cannot fully analyze what the Java code is doing to the Collection, so Kotlin cannot refuse to pass a read-only Collection to Java code that can modify the Collection. If you define a function that passes read-only collections to Java, you are responsible for declaring the parameters to be of the correct type, depending on whether the Java code modifies the collection. This note also applies when Kotlin defines a collection of non-empty elements that may store null values when passed to Java.

* Collection creation function

Collection types read-only variable
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf,hashMapOf,linkedMapOf,sortedMapOf

As a collection of platform types

In the previous article on nullability, Kotlin looked at what types are defined in Java code as platform types. Kotlin does not have any nullability information about the platform type, so the compiler allows Kotlin code to treat it as nullable or non-nullable. Similarly, variables of declared collection types in Java are treated as platform types, and a collection of platform types is essentially a collection of unknown variability. Especially when you are rewriting or implementing A Java method with a collection type in its signature in Kotlin, consider which type to use:

/* Java */
interface Processor {
    void process(List<String> values);
}

/* Kotlin */
class ProcessorImpl : Processor {
    override fun process(values: MutableList<String?>?) {}
}

class ProcessorImpl2 : Processor {
    override fun process(values: MutableList<String>?) {}
}

class ProcessorImpl3 : Processor {
    override fun process(values: MutableList<String>) {}
}

class ProcessorImpl4 : Processor {
    override fun process(values: List<String>) {}
}
Copy the code

Any of these inheritance methods can be defined, and you need to make a choice based on the situation:

  • Is the set nullable?
  • Can elements in a collection be nullable?
  • Does your method modify the collection?

Of course, if you are not sure, you can use the safest way:

override fun process(values: MutableList<String?>?) {}
Copy the code

But when you use it, you have to consider all the possible empty cases.

Object and an array of primitive data types

Kotlin’s array appears in many previous examples:

Array<String>
Copy the code

An array in Kotlin is a class with type parameters whose element types are specified as the corresponding type parameters. Arrays can be created using arrayOf, arrayOfNulls, and Array constructors.

One of the most common cases of creating an array in Kotlin code is when you need to call a Java method that takes an array, or when you call a Kotlin function that takes a vararg argument. In these cases, the data is usually already stored in a collection and you just need to convert it to an array. This can be done using the toTypeArray method:

>>> val strings = listOf("a"."b"."c")
>>> println("%s/%s/%s".format(* strings.totypeArray ())) // Expects the varvag parameter to pass array A /b/c using the expansion operatorCopy the code

The type parameter of an array type always becomes an object type. If you declare an Array

it will be an Array of boxed integers []. If you need to create an array of unboxed primitive data types, you must use a special class for an array of primitive data types. An array to represent a primitive data type. Kotlin provides several separate classes, one for each of the basic data types, such as arrays of type Int called IntArray, ByteArray, BooleanArray, and so on. These correspond to arrays of basic data types in Java: int[], btye[], Boolean [], and so on. To wear an array of primitive data types, you can pass size or lambda through a factory method like intArrayOf, or through a constructor.