"Kotin Minimalist Tutorial" Chapter 15 Kotlin File IO Operations, Regular Expressions and Multithreading

Chapter 15 Kotlin File IO Operation and Multithreading

When we use Groovy's file IO operation, it feels very convenient. The same Kotlin also has a useful file IO operation API. Similarly, some practical extensions have been made to the regular expression function of Java in Kotlin. And the multi-threading in Kotlin mainly encapsulates the multi-threading API of Java. Because these Java already has a lot of basic APIs, Kotlin does not repeat the implementation by itself, but expands practical functions on the basis of Java.

In this chapter, we will introduce Kotlin file IO operations, regular expressions, and multithreading related content.

15.1 Introduction to Kotlin IO

Kotlin's IO operations are all under the kotlin.io package. Kotlin's principle is that Java already has it. If it is easy to use, use it directly. If it doesn't or is not easy to use, encapsulate and extend it based on the original class. For example, Kotlin writes an extension function for the File class. This is the same idea as Groovy's extended API.

15.2 Terminal IO

Java's very long output statement System.out.println() has continued to the present! The same work can be done in C++ with a simple cout<<. Of course, if necessary, we can directly encapsulate System.out.println() as a simple printing method in the project.

It's very simple in Kotlin, just use the two global functions println or print. We no longer need a lengthy prefix. Of course, if we are nostalgic and just want to use System.out.println(), Kotlin still supports direct use (seamless interoperability with Java).

>>> System.out.println("K")
K
>>> println("K")
K

The println function here in Kotlin is implemented as follows

@kotlin.internal.InlineOnly
public inline fun println(message: Any?) {
    System.out.println(message)
}

Of course, Kotlin is only encapsulated on the basis of System.out.println().

Reading data from the terminal is also very simple. The most basic method is the global function readLine, which reads a line directly from the terminal as a string. If you need further processing, you can use various string processing functions provided by Kotlin to process and convert strings.

Kotlin's class for encapsulating terminal IO is in the source file stdlib/src/kotlin/io/Console.kt.

15.3 File IO operation

Kotlin provides a lot of useful extension functions for java.io.File, these extension functions are mainly in the following three source files:

kotlin/io/files/FileTreeWalk.kt
kotlin/io/files/Utils.kt
kotlin / io / FileReadWrite.kt

At the same time, Kotlin also made simple extensions for InputStream, OutputStream and Reader. They are mainly in the following two source files:

kotlin/io/IOStreams.kt
kotlin/io/ReadWrite.kt

Koltin's serialization directly uses the type alias of Java's serialization class:

internal typealias Serializable = java.io.Serializable

Let's briefly introduce the read and write operations of Kotlin files.

15.3.1 Read file

Read the entire contents of the file

If we simply read a file, we can use the readText() method, which directly returns the entire file content. The code example is as follows

    /**
     * 获取文件全部内容字符串
     * @param filename
     */
    fun getFileContent(filename: String): String {
        val f = File(filename)
        return f.readText(Charset.forName("UTF-8"))
    }

We directly use the File object to call the readText function to get the entire content of the file, which returns a string. If you specify the character encoding, you can specify it by passing in the parameter Charset, and the default is UTF-8 encoding.

If we want to get the content of each line of the file, we can simply split("\n")get an array of the content of each line.

Get the content of each line of the file

We can also directly call the readLines function encapsulated by Kotlin to get the content of each line of the file. The readLines function returns a List that holds the content of each line.

    /**
     * 获取文件每一行内容,存入一个 List 中
     * @param filename
     */
    fun getFileLines(filename: String): List<String> {
        return File(filename).readLines(Charset.forName("UTF-8"))
    }

Direct manipulation of byte arrays

If we want to directly manipulate the byte array of the file, we can use readBytes(). If you want to use the traditional Java method, you can also use it as freely as Groovy in Kotlin.

    //读取为bytes数组
    val bytes: ByteArray = f.readBytes()
    println(bytes.joinToString(separator = " "))

    //直接像 Java 中的那样处理Reader或InputStream
    val reader: Reader = f.reader()
    val inputStream: InputStream = f.inputStream()
    val bufferedReader: BufferedReader = f.bufferedReader()
}

15.3.2 Write file

Similar to reading files, writing files is also very simple. We can write strings or byte streams. You can also directly use Java Writer or OutputStream.

Overwrite file

    fun writeFile(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }
        f.writeText(text, Charset.defaultCharset())
    }

Write file at the end

    fun appendFile(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }
        f.appendText(text, Charset.defaultCharset())
    }

15.4 Traverse the file tree

Like Groovy, Kotlin also provides convenient functions to traverse the file tree. To traverse the file tree, you need to call the extension method walk(). It will return a FileTreeWalk object, which has some methods for setting the traversal direction and depth. For details, see FileTreeWalk API documentation.

Tip: FileTreeWalk API document link https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/-file-tree-walk/

The following example traverses all files in the specified folder.

    fun traverseFileTree(filename: String) {
        val f = File(filename)
        val fileTreeWalk = f.walk()
        fileTreeWalk.iterator().forEach { println(it.absolutePath) }
    }

Test code:

    @Test fun testTraverseFileTree() {
        KFileUtil.traverseFileTree(".")
    }

Run the above test code, it will output all subdirectories and their files in the current directory.

We can also traverse all sub-directory files under the current file and store them in an Iterator

    fun getFileIterator(filename: String): Iterator<File> {
        val f = File(filename)
        val fileTreeWalk = f.walk()
        return fileTreeWalk.iterator()
    }

We traverse all sub-directory files under the current file, we can also filter according to conditions, and store the results in a Sequence

    fun getFileSequenceBy(filename: String, p: (File) -> Boolean): Sequence<File> {
        val f = File(filename)
        return f.walk().filter(p)
    }

Test code:

    @Test fun testGetFileSequenceBy() {
        val fileSequence1 = KFileUtil.getFileSequenceBy(".", {
            it.isDirectory
        })
        fileSequence1.forEach { println("fileSequence1: ${it.absoluteFile} ") }

        val fileSequence2 = KFileUtil.getFileSequenceBy(".", {
            it.isFile
        })
        fileSequence2.forEach { println("fileSequence2: ${it.absoluteFile} ") }

        val fileSequence3 = KFileUtil.getFileSequenceBy(".", {
            it.extension == "kt"
        })
        fileSequence3.forEach { println("fileSequence3: ${it.absoluteFile} ") }
    }

Run the above test code in the project, it will have output similar to the following:

...
...

fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KFileUtil.kt 
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KNetUtil.kt 
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KShellUtil.kt 
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/test/kotlin/com/easy/kotlin/fileio/KFileUtilTest.kt 


15.5 Network IO operation

Kotlin adds two extension methods to java.net.URL, readBytes and readText. We can easily use these two methods with regular expressions to realize the function of web crawlers.

Below we simply write a few function examples.

Get the response HTML function of the url according to the url


fun getUrlContent(url: String): String {
    return URL(url).readText(Charset.defaultCharset())
}

Get the url response bit array function according to the url

fun getUrlBytes(url: String): ByteArray {
    return URL(url).readBytes()
}

Write url response byte array to file

fun writeUrlBytesTo(filename: String, url: String) {
    val bytes = URL(url).readBytes()
    File(filename).writeBytes(bytes)
}

The following example simply obtains the source code of Baidu's homepage.

getUrlContent("https://www.baidu.com")

The following example obtains the bit stream of a picture according to the url, and then calls the readBytes() method to read the byte stream and write it to the file.

writeUrlBytesTo("图片.jpg", "http://n.sinaimg.cn/default/4_img/uplaod/3933d981/20170622/2fIE-fyhfxph6601959.jpg")

We can see the downloaded "picture.jpg" in the corresponding folder of the project.

15.6 kotlin.io standard library

Kotlin's io library is mainly an io library that extends Java. Below we briefly give a few examples.

appendBytes

Append byte array to the file

Method signature:

fun File.appendBytes(array: ByteArray)

appendText

Append text to the file

Method signature:


fun File.appendText(
    text: String, 
    charset: Charset = Charsets.UTF_8)

bufferedReader

Get the BufferedReader of the file

Method signature:

fun File.bufferedReader(
    charset: Charset = Charsets.UTF_8, 
    bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedReader

bufferedWriter

Get the BufferedWriter of the file

Method signature:

fun File.bufferedWriter(
    charset: Charset = Charsets.UTF_8, 
    bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedWriter

copyRecursively

Copy the file or recursively copy the directory and all its sub-files to the specified path. If the file in the specified path does not exist, it will be created automatically.

Method signature:

fun File.copyRecursively(
    target: File, 
    overwrite: Boolean = false, // 是否覆盖。true:覆盖之前先删除原来的文件
    onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean
Tip: Kotlin's API documentation for File extension functions https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/index.html
About kotlin.io the following API documentation is here https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/index.html

15.7 Execute Shell command line

We use Groovy's file IO operation feels very easy to use, for example

package com.easy.kotlin

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4)
class ShellExecuteDemoTest {
    @Test
    def void testShellExecute() {
        def p = "ls -R".execute()
        def output = p.inputStream.text
        println(output)
        def fname = "我图.url"
        def f = new File(fname)
        def lines = f.readLines()
        lines.forEach({
            println(it)
        })
        println(f.text)
    }
}

File IO and network IO operations in Kotlin are as simple as Groovy.

In addition, from the above code, we can see that it is very simple to execute terminal commands with Groovy:

def p = "ls -R".execute()
def output = p.inputStream.text

In Kotlin, currently there is no such function to extend the String class and Process. In fact, it is very simple to extend such a function. We can fully expand by ourselves.

First, let's extend the execute() function of String.

fun String.execute(): Process {
    val runtime = Runtime.getRuntime()
    return runtime.exec(this)
}

Then, let's extend a text function to the Process class.

fun Process.text(): String {
    var output = ""
    //	输出 Shell 执行的结果
    val inputStream = this.inputStream
    val isr = InputStreamReader(inputStream)
    val reader = BufferedReader(isr)
    var line: String? = ""
    while (line != null) {
        line = reader.readLine()
        output += line + "\n"
    }
    return output
}

After completing the above two simple extension functions, we can execute terminal commands like Groovy in the following test code:

val p = "ls -al".execute()

val exitCode = p.waitFor()
val text = p.text()

println(exitCode)
println(text)

In fact, through the study of many previous examples, we can see that Kotlin's extension functions are quite practical. The Kotlin language API also makes extensive use of extended functions.

15.8 Regular expressions

In addition to the Pattern, Matcher and other classes in Java that we can still use in Kotlin, Kotlin also provides a regular expression class kotlin/text/regex/Regex.kt, we create a regular expression through the Regex constructor .

15.8.1 Constructing Regex Expressions

Use Regex constructor

>>> val r1 = Regex("[a-z]+")
>>> val r2 = Regex("[a-z]+", RegexOption.IGNORE_CASE)

The matching option RegexOption is a regular matching option in the Java class Pattern that is used directly.

Use String's toRegex extension function

>>> val r3 = "[A-Z]+".toRegex()

15.8.2 Regex function

Regex provides a wealth of simple and practical functions, as shown in the following table

Function name Function Description
matches(input: CharSequence): Boolean All input strings match
containsMatchIn(input: CharSequence): Boolean At least one match of the input string
matchEntire(input: CharSequence): MatchResult? All input strings are matched, and a matching result object is returned
replace(input: CharSequence, replacement: String): String Replace the matched part of the input string with the content of replacement
replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String Replace the matched value in the input string with the new value after the function transform
find(input: CharSequence, startIndex: Int = 0): MatchResult? Returns the first matching value in the input string
findAll(input: CharSequence, startIndex: Int = 0): Sequence Returns a sequence of MatchResult of all matching values ​​in the input string

Below we give simple examples of the above functions.

matches

If the input string matches the regular expression, it returns true, otherwise it returns false.

>>> val r1 = Regex("[a-z]+")
>>> r1.matches("ABCzxc")
false
>>> 

>>> val r2 = Regex("[a-z]+", RegexOption.IGNORE_CASE)
>>> r2.matches("ABCzxc")
true

>>> val r3 = "[A-Z]+".toRegex()
>>> r3.matches("GGMM")
true

containsMatchIn

Return true if there is at least one match in the input string, and false if there is no match.

>>> val re = Regex("[0-9]+")
>>> re.containsMatchIn("012Abc")
true
>>> re.containsMatchIn("Abc")
false

matchEntire

The input string matches the regular expression and returns a MatcherMatchResult object, otherwise it returns null.

>>> val re = Regex("[0-9]+")
>>> re.matchEntire("1234567890")
kotlin.text.MatcherMatchResult@34d713a2
>>> re.matchEntire("1234567890!")
null

We can access the value of MatcherMatchResult to get the matched value.

>>> re.matchEntire("1234567890")?.value
1234567890

Since the return of the matchEntire function is a MatchResult? nullable object, here we use the safe call symbol ?..

replace(input: CharSequence, replacement: String): String

Replace the matched part of the input string with the content of replacement.

>>> val re = Regex("[0-9]+")
>>> re.replace("12345XYZ","abcd")
abcdXYZ

We can see that, "12345XYZ" in 12345a match the regular expression [0-9]+content, it is replaced abcd.

replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String

Replace the matched value in the input string with the new value after the function transform.

>>> val re = Regex("[0-9]+")
>>> re.replace("9XYZ8", { (it.value.toInt() * it.value.toInt()).toString() })
81XYZ64

We can see that 9XYZ8the number 8 and 9 are regular expression matching [0-9]+contents, which are respectively transform function maps (it.value.toInt() * it.value.toInt()).toString()the new values of 81 and 64 replaced.

find

Returns the first matching MatcherMatchResult object in the input string.

>>> val re = Regex("[0-9]+")
>>> re.find("123XYZ987abcd7777")
kotlin.text.MatcherMatchResult@4d4436d0
>>> re.find("123XYZ987abcd7777")?.value
123

findAll

Returns a sequence of MatchResult of all matching values ​​in the input string.

>>> val re = Regex("[0-9]+")
>>> re.findAll("123XYZ987abcd7777")
kotlin.sequences.GeneratorSequence@f245bdd

We can loop through forEach all matched values

>>> re.findAll("123XYZ987abcd7777").forEach{println(it.value)}
123
987
7777

15.8.3 Using Java regular expression classes

In addition to the functions provided by Kotlin above, we can still use Java's regular expression API in Kotlin.

val re = Regex("[0-9]+")
val p = re.toPattern()
val m = p.matcher("888ABC999")
while (m.find()) {
    val d = m.group()
    println(d)
}

The output of the code above:

888
999

15.9 Kotlin's multithreading

There is no synchronized keyword in Kotlin.
There is no volatile keyword in Kotlin.
Kotlin's Any is similar to Java's Object, but there is no wait(), notify() and notifyAll() methods.

So how does concurrency work in Kotlin? Don't worry, since Kotlin is standing on the shoulders of Java, of course the support for multi-threaded programming is indispensable-Kotlin simplifies our coding by encapsulating thread classes in Java. At the same time, we can also use some specific annotations, directly use the synchronization keywords in Java, etc. Below we briefly introduce the related content of multi-threaded programming using Kotlin.

15.9.1 Create Thread

We usually have two ways to create threads in Java:

  • Extend the Thread class
  • Or instantiate it and pass a Runnable through the constructor

Because we can easily use Java classes in Kotlin, both methods can be used.

Create using object expressions

    object : Thread() {
        override fun run() {
            Thread.sleep(3000)
            println("A 使用 Thread 对象表达式: ${Thread.currentThread()}")
        }
    }.start()

This code uses Kotlin's object expressions to create an anonymous class and override the run() method.

Use Lambda expressions

Here is how to pass a Runnable to a newly created Thread instance:

    Thread({
        Thread.sleep(2000)
        println("B 使用 Lambda 表达式: ${Thread.currentThread()}")
    }).start()

We don't see Runnable here, it is very convenient to directly use the above Lambda expression to express in Kotlin.

Is there an easier way? Look at the explanation below.

Use Kotlin encapsulated thread function

For example, we wrote the following piece of thread code

    val t = Thread({
        Thread.sleep(2000)
        println("C 使用 Lambda 表达式:${Thread.currentThread()}")
    })
    t.isDaemon = false
    t.name = "CThread"
    t.priority = 3
    t.start()

The next four lines can be said to be boilerplate code. In Kotlin, such an operation package is simplified.

    thread(start = true, isDaemon = false, name = "DThread", priority = 3) {
        Thread.sleep(1000)
        println("D 使用 Kotlin 封装的函数 thread(): ${Thread.currentThread()}")
    }

This code is more streamlined and cleaner. In fact, the thread() function abstracts and encapsulates the boilerplate code that is often used in our programming practice. Its implementation is as follows:

public fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
    val thread = object : Thread() {
        public override fun run() {
            block()
        }
    }
    if (isDaemon)
        thread.isDaemon = true
    if (priority > 0)
        thread.priority = priority
    if (name != null)
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}

This is just a very convenient wrapper function, simple and practical. From the above example, we can see that Kotlin simplifies the boilerplate code by extending Java's threading API.

15.9.2 Synchronization methods and blocks

synchronized is not a keyword in Kotlin, it is replaced by the @Synchronized annotation. The declaration of the synchronization method in Kotlin will look like this:

    @Synchronized fun appendFile(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }
        f.appendText(text, Charset.defaultCharset())
    }

The @Synchronized annotation has the same effect as synchronized in Java: it marks the JVM method as synchronized. For the synchronized block, we use the synchronized() function, which uses the lock as a parameter:

    fun appendFileSync(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }

        synchronized(this){
            f.appendText(text, Charset.defaultCharset())
        }
    }

Basically the same as Java.

15.9.3 Variable fields

Similarly, Kotlin does not have the volatile keyword, but has the @Volatile annotation.

@Volatile private var running = false
fun start() {
    running = true
    thread(start = true) {
        while (running) {
            println("Still running: ${Thread.currentThread()}")
        }
    }
}

fun stop() {
    running = false
    println("Stopped: ${Thread.currentThread()}")
}

@Volatile will mark the JVM backup field as volatile.

Of course, in Kotlin we have a more useful coroutine concurrency library. In the practice of code engineering, we can freely choose according to the actual situation.

chapter summary

Kotlin is a language with strong engineering and practicality. From the file IO, regular expressions, and multithreading introduced in this chapter, we can understand the basic principle of Kotlin: make full use of the existing Java ecosystem, based on this A simpler and more practical extension will greatly improve the productivity of programmers. From this, we also realized the minimalist concept in Kotlin programming-continuous abstraction, encapsulation, and expansion to make it more simple and practical.

Sample code for this chapter: https://github.com/EasyKotlin/chapter15_file_io

In addition, the author integrated the content of this chapter and wrote a simple image crawler web application using SpringBoot + Kotlin. Interested readers can refer to the source code: https://github.com/EasyKotlin/chatper15_net_io_img_crawler

In the next chapter, and our last chapter, let us break away from the JVM and directly use Kotlin Native to develop a Kotlin application that is directly compiled into machine code and run.


Kotlin Developer Community

Focus on sharing Java, Kotlin, Spring/Spring Boot, MySQL, redis, neo4j, NoSQL, Android, JavaScript, React, Node, functional programming, programming ideas, "high availability, high performance, high real-time" large-scale distributed system architecture design theme.

High availability, high performance, high real-time large-scale distributed system architecture design

Distributed framework: Zookeeper, distributed middleware framework, etc.
Distributed storage: GridFS, FastDFS, TFS, MemCache, redis, etc.
Distributed database: Cobar, tddl, Amoeba, Mycat
cloud computing, big data, AI algorithm
virtualization, cloud native Technology
Distributed computing framework: MapReduce, Hadoop, Storm, Flink, etc.
Distributed communication mechanism: Dubbo, RPC calls, shared remote data, message queues, etc.
Message queue MQ: Kafka, MetaQ, RocketMQ
how to build a highly available system: based on hardware, software Realization of some typical solutions such as middleware and system architecture: HAProxy, Corosync+Pacemaker-based high-availability cluster suite middleware system
Mycat architecture distributed evolution
The problems behind big data Join: contradictions and reconciliation of data, network, memory and computing capabilities
High-performance problems in Java distributed systems: AIO, NIO, Netty or self-developed frameworks?
High-performance event dispatch mechanism: thread pool model, Disruptor model, etc. . .

The embracing wood is born at the end of the mill; the nine-story platform starts from the basement; the journey of a thousand miles begins with a single step. If you don't accumulate steps, you can't reach a thousand miles; if you don't accumulate small streams, you can't become a river.

Introduction to Kotlin

Kotlin is a non-research language. It is a very pragmatic industrial-grade programming language. Its mission is to help programmers solve problems in actual engineering practice. Using Kotlin makes the lives of Java programmers better. The null pointer errors in Java, the lengthy boilerplate code that wastes time, the verbose syntax restrictions, etc., all disappear in Kotlin. Kotlin is simple and pragmatic, with concise and powerful syntax, safe and expressive, and extremely productive.

Java was born in 1995 and has a history of 23 years. The current latest version is Java 9. In the process of continuous development and prosperity of the JVM ecosystem, brother languages ​​such as Scala, Groovy, and Clojure were also born.

Kotlin is also an excellent member of the JVM family. Kotlin is a modern language (version 1.0 was released in February 2016). Its original purpose is to optimize the defects of the Java language like Scala, provide simpler and more practical programming language features, and solve performance problems, such as compilation time. JetBrains has done a great job in these areas.

Features of Kotlin language

After developing in Java for many years, it's great to be able to try something new. If you are a Java developer, Kotlin will be very natural and smooth. If you are a Swift developer, you will feel familiar, such as Nullability. The features of Kotlin language are:

1. Concise

Significantly reduce the amount of boilerplate code.

2. 100% interoperability with Java

Kotlin can directly interact with Java classes and vice versa. This feature allows us to directly reuse our code base and migrate it to Kotlin. Because Java's interoperability is almost everywhere. We can directly access platform APIs and existing code bases, while still enjoying and using all the powerful modern language features of Kotlin.

3. Extension function

Kotlin is similar to C# and Gosu, it provides the ability to provide new functional extensions for existing classes without having to inherit from the class or use any type of design patterns (such as decorator patterns).

4. Functional programming

The Kotlin language first supports functional programming, just like Scala. It has basic functional features such as high-order functions and lambda expressions.

5. Default and named parameters

In Kotlin, you can set a default value for the parameters in a function and give each parameter a name. This helps to write easy-to-read code.

6. Powerful development tool support

And because it is produced by JetBrains, we have great IDE support. Although the automatic conversion from Java to Kotlin is not 100% OK, it is indeed a very good tool. When using IDEA's tools to convert Java code to Kotlin code, 60%-70% of the resulting code can be reused easily, and the cost of modification is small.

In addition to its concise and powerful syntax features, Kotlin also has a very practical API and an ecosystem built around it. For example: collection API, IO extension, reflection API, etc. At the same time, the Kotlin community also provides a wealth of documents and a lot of learning materials, as well as online REPL.

A modern programming language that makes developers happier. Open source forever

Picture from "Kotlin from entry to advanced combat" (Chen Guangjian, Tsinghua University Press)

Picture from "Kotlin from entry to advanced combat" (Chen Guangjian, Tsinghua University Press)

https://kotlinlang.org/

Guess you like

Origin blog.csdn.net/universsky2015/article/details/108669487