Note for Kotlin learning
https://mdes4214.github.io/kotlin-note/export/
Keywords: Cross-platform with JVM, Static typed programming language, Combines the features of OOPs and functional-oriented programming.
Kotlin (/ˈkɒtlɪn/) is a cross-platform, statically typed, general-purpose programming language with type inference. Kotlin is designed to interoperate fully with Java, and the JVM version of Kotlin's standard library depends on the Java Class Library, but type inference allows its syntax to be more concise. Kotlin mainly targets the JVM, but also compiles to JavaScript (e.g. for frontend web applications using React) or native code (via LLVM), e.g. for native iOS apps sharing business logic with Android apps. Language development costs are borne by JetBrains, while the Kotlin Foundation protects the Kotlin trademark.
On 7 May 2019, Google announced that the Kotlin programming language is now its preferred language for Android app developers. As a result many developers have switched to Kotlin Since the release of Android Studio 3.0 in October 2017, Kotlin has been included as an alternative to the standard Java compiler. The Android Kotlin compiler targets Java 6 by default, but lets the programmer choose to target Java 8 up to 13, for optimization, or more features.
Though in this note, we more focus on Kotlin applying in Web Service.
In Stack Overflow Annual Developer Survey 2020, Kotlin got 4th in developers most loved language, 6th in most wanted, and 13th in most popular.
And the size of Kotlin communities in Q3, 2020 ranked 9th.
We can use all the Java Libraries in Kotlin, and vice versa. Thus, in a Java project, we can partially add or step by step to convert the codes to Kotlin.
We have a Java library like this.
package sample.java.library;
public class HelloWorld {
public void print() {
System.out.println("Hello World!!");
}
}
And we can directly import and use the class in Kotlin.
import sample.java.library
class testHelloWorld {
fun testHelloWorld() {
val helloWorld = HelloWorld()
helloWorld.print() // will print "Hello World!!" in console
}
}
We only need to config some dependencies and plugins in Maven POM.
Assume that we already have some Maven configuration for Java project.
Dependencies
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.4.72</kotlin.version>
</properties>
<dependencies>
<!-- this library include kotlin-stdlib & kotlin-reflect -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
...
</dependencies>
Plugins
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
With the strong function of IntelliJ, we can leverage it to convert Java file to Kotlin code.
After conversion, we almost no need to do anything. While sometimes to fix the complilation error, we need to modify the null safety (add !!
, ?
, or other null value check), getter and setter (in Kotlin, object.getSomething()
is simplify as object.something
).
Kotlin is a statically type language. Regarding to that specification, that means that the compiler will know what is the type of a variable or the return type of a function. That's why you can write val str = "hello"
instead of val str: String = "hello"
, or even fun sum(ints: List<Int>) = ints.sum()
instead of fun sum(ints: List<Int>): Int = ints.sum()
. In a lot of cases you will be allowed to omit the declaration type, so your code should be more concise.
val str: String = "Hello"
val str = "Hello" // same as above, but more concise
There are two kinds of type inference supported by Kotlin.
- Local type inference: for inferring types of expressions locally, in statement/expression scope.
- Function signature type inference: for inferring types of function return values and/or parameters.
But before type inference, we need to talk smart cast first.
// Note: the most important exception when smart casts are used in type inference is direct property declaration.
fun noSmartCastInInference() {
var a: Any? = null
if (a == null) return
var c = a // Direct property declaration
c // Declared type of `c` is Any?
// However, here it’s smart casted to Any
}
fun <T> id(a: T): T = a
fun smartCastInInference() {
var a: Any? = null
if (a == null) return
var c = id(a)
c // Declared type of `c` is Any
}
Local type inference in Kotlin is the process of deducing the compile-time types of expressions, lambda expression parameters and properties.
Type inference in Kotlin is bidirectional; meaning the types of expressions may be derived not only from their arguments, but from their usage as well. Note that, albeit bidirectional, this process is still local, meaning it processes one statement at a time, strictly in the order of their appearance in a scope; e.g., the type of property in statement S1 that goes before statement S2 cannot be inferred based on how S1 is used in S2.
Function signature type inference is a variant of local type inference, which is performed for [function declarations], lambda literals and anonymous function declarations.
Named and anonymous function declarations
fun <T> foo(): T { … }
fun bar(): Int = foo() // an expected constraint T’ <: Int
// allows the result of `foo` to be infereed automatically.
Statements with lambda literals
fun <T> foo(): T { … }
fun <R> run(body: () -> R): R { … }
fun bar() {
val x = run {
run {
run {
foo<Int>() // last expression inferred to be of type Int
} // this lambda is inferred to be of type () -> Int
} // this lambda is inferred to be of type () -> Int
} // this lambda is inferred to be of type () -> Int
// x is inferred to be type Int
val y: Double = run { // this lambda has an external constraint R’ <: Double
run { // this lambda has an external constraint R’ <: Double
foo() // this call has an external constraint T’ <: Double
// allowing to infer T to be Double in foo
}
}
}
In Kotlin, we don't need to use semicolon in the end of statement.
Java
String str = "Hello";
String strAppend = str + " World!!";
Kotlin
val str = "Hello"
val strAppend = "$str World!!"
We only need to add semicolon in these two cases:
- An enum class that has a list of enums and also properties or functions
enum class Things {
ONE, TWO;
fun isOne(): Boolean = this == ONE
}
- Two statements in one line
val str = "Hello"; println(str)
In Kotlin, there are !!
and ?
operators, which are used for null safety. This feature can pre-check the codes to prevent NullPointerException in runtime caused by thoughtless programmer.
Regular initialization means non-null by default.
var a: String = "abc" // Regular initialization means non-null by default
a = null // compilation error
If we need to declare a nullable variable.
var b: String? = "abc" // can be set null
b = null // ok
print(b)
And if we consume that the variable won't be null.
var b: String? = "abc" // can be set null
if (StringUtils.isEmpty(b)) {
return
} else {
doSomething(b!!) // if no "!!", it will has a compilation error
}
fun doSomething(b: String) { // this function only accept non-null parameter
...
}
There are many APIs in Kotlin Standard Library using Lambda function, even String
, Collection
, and so on.
Kotlin functions are first-class, which means that they can be stored in variables and data structures, passed as arguments to and returned from other higher-order functions. You can operate with functions in any way that is possible for other non-function variables.
Instantiating a function type
- Lambda Expression
- Anonymous Function
Lambda expressions and anonymous function are ‘function literals’, i.e. functions that are not declared, but passed immediately as an expression.
A lambda expression is always surrounded by curly braces {}
, parameter declarations in the full syntactic form go inside curly braces and have optional type annotations, the body goes after an ->
sign. If the inferred return type of the lambda is not Unit
, the last (or possibly single) expression inside the lambda body is treated as the return value.
// val action: (A, B) -> ReturnType
val plus: (Int, Int) -> Int = { n1, n2 -> n1 + n2 }
println("${plus(2, 3)}") // 5
Also, due to type inference, we can rewrite above to this:
val plus = { n1: Int, n2: Int -> n1 + n2 }
println("${plus(2, 3)}") // 5
One thing missing from the lambda expression syntax presented above is the ability to specify the return type of the function. In most cases, this is unnecessary because the return type can be inferred automatically. However, if you do need to specify it explicitly, you can use an alternative syntax: an anonymous function.
An anonymous function looks very much like a regular function declaration, except that its name is omitted. Its body can be either an expression (as shown above) or a block:
val numbers = mutableListOf(1, 2, 3, 4, 5)
val result1 = numbers.filter(fun(number: Int): Boolean = number > 2) // body is an expression
println("$result1") // [3, 4, 5]
val result2 = numbers.filter(fun(number: Int): Boolean { return number > 2 }) // body can also be a block
println("$result2") // [3, 4, 5]
The parameters and the return type are specified in the same way as for regular functions, except that the parameter types can be omitted if they can be inferred from context:
val numbers = mutableListOf(1, 2, 3, 4, 5)
val result1 = numbers.filter(fun(number): Boolean = number > 2) // body is an expression
println("$result1") // [3, 4, 5]
val result2 = numbers.filter(fun(number): Boolean { return number > 2 }) // body can also be a block
println("$result2") // [3, 4, 5]
The name Elvis operator refers to the fact that when its common notation. ?:
, is viewed sideways, it resembles an emoticon of Elvis Presley with his quaff.
But it also looks like Giorno Giovanna in JOJOm haha.
This operator handle two states.
- If the first operand is NOT null, it will return the first operand
- If the first operand is null, then return the second operand
val firstOperand1: String? = "Non-null String"
val firstOperand2: String? = null
val secondOperand: String = "Non-null String 2"
val result1 = firstOperand1 ?: secondOperand // result1 = "Non-null String"
val result2 = firstOperand2 ?: secondOperand // result2 = "Non-null String 2"
When the variable shouldn't be modified, it should be val
, otherwise will be var
.
val constVal = "CONST_VALUE" // similar with `final` in Java
var canModifyVal = "This value can be modified."
If we put aside StringBuilder
, we may concat String in Java like this:
String hello = "Hello";
String world = "World";
String helloWorld = hello + " " + world + "!!"; // "Hello World!!"
In Kotlin, we have $
operator to do this concisely.
val hello = "Hello"
val world = "World"
val helloWorld = "$hello $world!!" // "Hello World!!"
Also, it implicit toString()
method. And if we want to use properties or functions in $
it can be wrapped in {}
.
val strs = mutableListOf("Hello", "World", "!!")
println("str list: $strs, list size: ${strs.size}") // "str list: [Hello, World, !!], list size: 3"
There are some implement of String methods in Kotlin may be different from Java.
String split – Java vs. Kotlin vs. apache StringUtils
val str = "A / B C /"
(str as java.lang.String).split(" /") // ["A", " B C"] : String[]
(str as kotlin.String).split(" /") // ["A", " B C", ""] : List<String>
(str as kotlin.String).split(" /".toRegex()) // ["A", " B C", ""] : List<String>
StringUtils.split(str, " /") // ["A", "B", "C"] : String[]
StringUtils.splitByWholeSeparator(str, " /") // ["A", " B C", ""] : String[]
In Java Official API document, you can find out public String[] split(String regex, int limit)
. The limit
is 0 as default, and the pattern will be applied as many times as possible, the array can have any length, and trailing empty strings will be discarded. In other words, only if limit = 0
, Java String split will trim the empty strings. Otherwise, Kotlin String split is smoother on limit
parameter.
String str = "a:b:c:d:";
str.split(":", limit);
// limit
// limit < 0 -> ["a", "b", "c", "d", ""]
// limit = 0 -> ["a", "b", "c", "d"] => special case
// limit = 1 -> ["a:b:c:d:"]
// limit = 2 -> ["a", "b:c:d:"]
// limit = 3 -> ["a", "b", "c:d:"]
// limit = 4 -> ["a", "b", "c", "d:"]
// limit = 5 -> ["a", "b", "c", "d", ""] (goes on the same as with limit < 0)
// limit = 6 -> ["a", "b", "c", "d", ""]
Unlike many languages, Kotlin distinguishes between mutable and immutable collections (lists, sets, maps, etc). Precise control over exactly when collections can be edited is useful for eliminating bugs, and for designing good APIs.
val numbers = listOf(1, 2, 3, 4, 5)
numbers.add(6) // Compile error - Unresolved reference: add
If the collection need to be able to edit, we should use Mutable
val numbers = mutableListOf(1, 2, 3, 4, 5)
numbers.add(6) // [1, 2, 3, 4, 5, 6]
Same as MutableMap
, MutableSet
.
In Java, we usually use IDE to generate POJO, e.g.,
- generate Getters and Setters
- generate hashCode() and equals()
- generate toString()
Although there are some libraries like Lombok, can use annotation to simplify the process.
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class User {
private int id;
private String name;
}
And finally can become
@Data
public class User {
private int id;
private String name;
}
In Kotlin, we can easly get this by data class
.
data class User (
var id: Int = 0,
var name: String = ""
)
val user = User()
// setter
user.id = 123
user.name = "Tom"
// getter
println("${user.id} / ${user.name}") // "123 / Tom"
Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
- A sealed class is abstract by itself, it cannot be instantiated directly and can have abstract members.
- Sealed classes are not allowed to have non-private constructors (their constructors are private by default).
- Note that classes which extend subclasses of a sealed class (indirect inheritors) can be placed anywhere, not necessarily in the same file.
- The key benefit of using sealed classes comes into play when you use them in a
when
expression. If it's possible to verify that the statement covers all cases, you don't need to add anelse
clause to the statement.
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// the `else` clause is not required because we've covered all the cases
}
There are more amazing features in Kotlin waiting for you to discover!
- Kotlin Proframming Language
- Kotlin Language Spec
- Kotlin Starter Pack - Type inference, checks, and smart casts
- Kotlin Wiki
- Elvis Operator
- 長的帥,連Code都是香的 - Elvis Operator ?:
- Spring Boot and Kotlin
- Create a Java and Kotlin Project with Maven
- 初探 Kotlin Lambda 表達式
- Java vs. Kotlin: Lambdas and Functions
- Kotlin vs Java: What's the Difference?
- What are the rules of semicolon inference?
- Project Lombok
- Stack Overflow Annual Developer Survey 2020
- SlashData State of the Developer Nation Q3 2020 Report