Basic Syntax
Core Concepts
- Packages and imports: Organize code into namespaces.
mainfunction: The entry point for a Kotlin application. It can accept an array of strings as an argument.- Printing to standard output: Use
print()andprintln()to display information.println()appends a newline after the output. - Reading from standard input:
readln()reads a line of text from the console. - Functions: Defined with the
funkeyword. They can have expression bodies and inferred return types. TheUnitreturn type is used for functions that do not return a meaningful value. - Variables:
val: Immutable (read-only) variables.var: Mutable variables.- Type inference: Kotlin can usually determine the type automatically.
- Classes and instances: Defined with the
classkeyword. Properties can be declared in the constructor or class body. Inheritance uses the colon:. Classes arefinalby default; useopento allow inheritance. - Comments: Single-line (
//) and multiline (/* ... */). Multiline comments can be nested. - String templates: Use
$variableor${expression}to embed variables and expressions directly in strings. - Conditional expressions:
ifstatements.ifcan also be used as an expression. - Loops:
forloops (iterating over collections or ranges) andwhileloops. whenexpressions: A more powerful alternative toswitchstatements.- Ranges: Use the
inoperator to check whether a value is within a range. Useforto iterate over ranges. - Collections: Use
forto iterate andinto check membership. Use lambda expressions for filtering, mapping, and other operations. - Nullable values and null checks: Mark a type as nullable with
?. Check fornullbefore using a nullable value. - Type checks and automatic casts: Use the
isoperator to check a variable's type. Kotlin automatically casts the variable to the checked type inside theifblock.
Common Syntax
Creating DTOs (POJOs/POCOs)
data class Customer(val name: String, val email: String)
- A
data classautomatically generatesgetters(andsettersforvarproperties),equals(),hashCode(),toString(),copy(), andcomponentN()functions for destructuring. This greatly simplifies creating data classes.
Default Values for Function Parameters
fun foo(a: Int = 0, b: String = "") { ... }
- You can specify default values for parameters in a function declaration. When calling the function, you can omit parameters that have default values.
Filtering a List
val positives = list.filter { x -> x > 0 }
val positives = list.filter { it > 0 } // 更简洁
- Use the
filterfunction with a lambda expression to create a new list containing only the elements that satisfy a given condition.itis the implicit name for a lambda expression's single parameter.
Checking Whether a Collection Contains an Element
if ("john@example.com" in emailsList) { ... }
if ("jane@example.com" !in emailsList) { ... }
- Use the
inand!inoperators to easily check whether a collection contains an element.
String Interpolation
println("Name $name")
- Use the
$symbol to embed variables directly in strings without string concatenation.
Safely Reading Standard Input
toIntOrNull()attempts to convert a string to an integer. If the conversion fails, it returnsnulland avoids aNumberFormatException.
Type Checks
when (x) {
is Foo -> ...
is Bar -> ...
else -> ...
}
- Use the
isoperator to check an object's type. In awhenexpression, the object is automatically cast to that type if the check succeeds.
Read-Only List
val list = listOf("a", "b", "c")
listOf()creates an immutable list.
Read-Only Map
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
mapOf()creates an immutable map.
Accessing Map Entries
println(map["key"])
map["key"] = value // 对于可变映射
- Use square brackets (
[]) to access values in a map.
Iterating Over a Map or List of Pairs
for ((k, v) in map) {
println("$k -> $v")
}
- Use a destructuring declaration to iterate over a map or a list of
Pairobjects.kandvcan be any convenient names.
Iterating Over Ranges
- Use
..to create a closed range and..<to create an open range.stepspecifies the step size, whiledownTocreates a descending range.
Lazy Properties
val p: String by lazy { // 仅在第一次访问时计算该值
// 计算字符串
}
- The
lazydelegate ensures that a property is initialized only on first access. This can improve performance, especially for properties that are expensive to compute.
Extension Functions
fun String.spaceToCamelCase() { ... }
"Convert this to camelcase".spaceToCamelCase()
- Add new functions to existing classes without inheriting from or modifying the original classes.
Creating a Singleton
object Resource {
val name = "Name"
}
- An
objectdeclaration creates a singleton object. Kotlin automatically handles its creation and management.
Type-Safe Values with Inline Value Classes
@JvmInline
value class EmployeeId(private val id: String)
@JvmInline
value class CustomerId(private val id: String)
- A
value class(which requires@JvmInlineon the JVM) wraps an existing type and provides compile-time type safety. It prevents accidentally passing anEmployeeIdto a function that requires aCustomerId.
Instantiating an Abstract Class
- Use an anonymous object to instantiate an abstract class. All abstract methods must be implemented.
Null-Safe Shorthand
val files = File("Test").listFiles()
println(files?.size) // 如果 files 不为空,则打印 size
- With the safe-call operator
?., the result of the expression isnulliffilesisnull, so noNullPointerExceptionis thrown.
Provide a Fallback with the Elvis Operator
- Use the Elvis operator
?:to provide a default value when the expression on the left isnull.runlets you execute a block of code to calculate that default.
Throw an Exception When a Value Is Null
val values = ...
val email = values["email"] ?: throw IllegalStateException("Email is missing!")
- Use the Elvis operator
?:to throw an exception whenvalues["email"]isnull.
Getting the First Item from a Possibly Empty Collection
val emails = ... // 可能为空
val mainEmail = emails.firstOrNull() ?: ""
firstOrNull()returns the first element of a collection, ornullif the collection is empty. The Elvis operator provides a default value whenfirstOrNull()returnsnull.
Run Code Only When a Value Is Not Null
val value = ...
value?.let {
... // 如果 value 不为空,则执行此代码块
}
- The
letfunction runs only when the value is notnull.itis the implicit name for the value inside theletblock.
Map a Nullable Value When It Is Not Null
val value = ...
val mapped = value?.let { transformValue(it) } ?: defaultValue
// 如果 value 或 transform 结果为空,则返回 defaultValue。
- Combines the safe-call operator
?., theletfunction, and the Elvis operator?:to transform a nullable value concisely while providing a default when either the value or the transformation result isnull.
Returning a Value from a when Expression
- A
whenexpression can return a value, making the code more concise.
try-catch Expressions
fun test() {
val result = try {
count()
} catch (e: ArithmeticException) {
throw IllegalStateException(e)
}
// 使用 result
}
- A
try-catchblock can return a value, allowing you to embed exception-handling logic in an expression.
if Expressions
val y = if (x == 1) {
"one"
} else if (x == 2) {
"two"
} else {
"other"
}
- An
ifstatement can return a value, allowing you to select different values based on a condition.
Builder-Style Use of Methods That Return Unit
fun arrayOfMinusOnes(size: Int): IntArray {
return IntArray(size).apply { fill(-1) }
}
- The
applyfunction lets you call multiple methods on an object and returns the object itself. This allows you to configure an object in a builder-style manner.
Single-Expression Functions
- If a function contains only one expression, you can omit the braces and the
returnkeyword.
Calling Multiple Methods on an Object (with)
- The
withfunction lets you call multiple methods on a single object without repeating the object's name.
Configuring an Object's Properties (apply)
val myRectangle = Rectangle().apply {
length = 4
breadth = 5
color = 0xFAFAFA
}
- The
applyfunction lets you configure an object's properties and returns the object itself. This is useful for configuring properties that are not present in the constructor.
Java 7 try-with-resources
val stream = Files.newInputStream(Paths.get("/some/file.txt"))
stream.buffered().reader().use { reader ->
println(reader.readText())
}
- The
usefunction ensures that resources, such as streams, are closed properly after use, even if an exception occurs.
Generic Functions That Require Type Information
inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)
- The
reifiedkeyword lets you access generic type information at runtime. This is useful for functions that need to know the generic type.
Swapping Two Variables
var a = 1
var b = 2
a = b.also { b = a }
- The
alsofunction performs an operation before the assignment. In this case,alsoassigns the value ofatob, then assigns the value ofbtoa.
Data Types
Type List
- Numbers:
Byte(8-bit)Short(16-bit)Int(32-bit)Long(64-bit)Float(32-bit)Double(64-bit)- Unsigned types:
UByte,UShort,UInt,ULong
- Booleans:
Boolean(true/false) - Characters:
Char(Unicode) - Strings:
String(immutable) - Arrays:
Array<T>(mutable); typed arrays:IntArray,ByteArray, and so on
Special Types
Any: The supertype of all non-null types (similar to Java'sObject)Nothing: The type that "never returns" (for example, a function that throws an exception)Unit: The type for "no meaningful return value" (similar to Java'svoid)
Type Checks and Casts
- Type check:
is - Safe cast:
as?(returnsnullon failure) - Unsafe cast:
as(may throw an exception)
Key Points
- Kotlin has no primitive types.
- Type inference is powerful, so explicit type declarations are usually unnecessary.
Examples
val age: Int = 30
val name = "Alice" // 类型推断为 String
val pi = 3.14 // 类型推断为 Double
Control Statements
if Expressions
if (condition) result else result(elseis required unless the result isUnit)- Can return a value, similar to a ternary operator.
val max = if (a > b) a else b // 返回 a 或 b 中较大的值
when Statements and Expressions
- An alternative to
switch. when (variable) { value -> result ... else -> result }- When used as an expression, it must cover every case or include an
elsebranch. - Can check types (
is) and ranges (in).
for Loops
for (item in collection) { ... }- Iterates over the elements of a collection.
for (i in range) { ... }iterates over a numeric range.
val numbers = listOf(1, 2, 3)
for (number in numbers) {
println(number)
}
for (i in 1..5) { // 1 到 5 (包含 5)
println(i)
}
while Loops
while (condition) { ... }checks the condition before executing the body.do { ... } while (condition)executes the body before checking the condition, so it always runs at least once.
Controlling Loops
break: Exit the loop.continue: Skip the rest of the current iteration.
for (i in 1..10) {
if (i == 3) continue // 跳过 i=3 的迭代
if (i == 7) break // 循环在 i=7 时终止
println(i)
}
Jump Expressions
return
- By default, returns from the nearest enclosing function or anonymous function.
break
- Terminates the nearest enclosing loop.
continue
- Continues with the next iteration of the nearest enclosing loop.
The type of these expressions is Nothing.
Labels
In Kotlin, you can label an expression using the form identifier@. Labels let you control the behavior of break and continue more precisely.
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...) break@loop // 退出标记为 loop 的循环
}
}
break@label: Jumps to the execution point after the loop marked withlabel.continue@label: Jumps to the next iteration of the loop marked withlabel.
Returning to Labels
Inside nested functions (function literals, local functions, and object expressions), you can use a qualified return statement to return from an outer function. This is primarily used to return from a lambda expression.
Returning from a Lambda Expression
Explicit label
CodeBlock Loading...In this example,
return@litreturns from the lambda expression labeledlit@.Implicit label: More concise, with the label name matching the name of the function to which the lambda expression is passed.
CodeBlock Loading...In this example,
return@forEachreturns from the lambda expression passed toforEach.Anonymous functions: You can use an anonymous function instead of a lambda expression. A
returnstatement returns from the anonymous function itself.CodeBlock Loading...Using an anonymous function has a effect similar to
continue.
Simulating break Behavior
You can simulate break behavior with nested lambda expressions and a non-local return.
Note
When returning a value, the parser prefers a labeled return statement. For example, return@a 1 is parsed as "return 1 at label @a" rather than "return an expression (1) labeled @a".
Classes and Inheritance
Default Superclass
- All classes in Kotlin share a common superclass,
Any. - If a class does not declare a supertype,
Anyis its default superclass.
class Example // 隐式继承自 Any
Anyhas three methods:equals(),hashCode(), andtoString(). These methods are defined for every Kotlin class.
Open Classes
- By default, Kotlin classes are
finaland cannot be inherited from. - Mark a class with the
openkeyword to make it inheritable.
open class Base // 类可以被继承
Explicit Supertypes
- To declare an explicit supertype in a class header, place the type after a colon.
open class Base(p: Int)
class Derived(p: Int) : Base(p)
- Primary constructor: If the derived class has a primary constructor, it must initialize the base class in that primary constructor according to its parameters.
- Secondary constructors: If the derived class has no primary constructor, each secondary constructor must initialize the base type using the
superkeyword or delegate to another constructor that does so. Different secondary constructors can call different constructors of the base type.
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
Overriding Methods
- Kotlin requires explicit modifiers for overridable members and overrides.
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
- The
overridemodifier is required forCircle.draw(). The compiler reports an error if it is missing. - If a function does not have the
openmodifier, such asShape.fill(), a subclass cannot declare a function with the same signature, with or withoutoverride. - The
openmodifier has no effect when a member is added to afinalclass. - A member marked with
overrideis itselfopen, so it can be overridden in a subclass. - Use
finalto prevent further overrides.
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
Overriding Properties
- The override mechanism works for properties in the same way as it does for methods.
- A property declared in a superclass and redeclared in a derived class must begin with
override, and the types must be compatible. - A property declared with an initializer can be overridden by a property with an initializer or with a
getmethod.
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
- You can override a
valproperty with avarproperty, but not the other way around. This is because avalproperty essentially declares agetmethod, while overriding it with avaralso declares asetmethod in the derived class. - You can use the
overridekeyword as part of a property declaration in a primary constructor.
Derived Class Initialization Order
- When constructing a new instance of a derived class, base-class initialization is performed first, apart from evaluating the arguments to the base constructor. This means it occurs before the derived class's initialization logic runs.
- While the base constructor is executing, properties declared or overridden in the derived class have not yet been initialized. Using these properties in base-class initialization logic, either directly or indirectly through another overridden open member, can cause incorrect behavior or runtime failures.
- When designing a base class, avoid using
openmembers in constructors, property initializers, orinitblocks.
Calling Superclass Implementations
- Code in a derived class can use the
superkeyword to call its superclass's function and property accessor implementations.
- In an inner class, use a
superkeyword qualified by the outer class name to access the outer class's superclass:super@Outer.
Overriding Rules
- In Kotlin, inherited implementations follow these rules: If a class inherits multiple implementations of the same member from its direct supertypes, it must override that member and provide its own implementation, possibly using one of the inherited implementations.
- To identify the supertype from which to inherit an implementation, use
superqualified by the supertype name in angle brackets, such assuper<Base>.
- A class can inherit from both
RectangleandPolygon, but because each has its owndraw()implementation,Squaremust overridedraw()and provide a separate implementation to resolve the ambiguity.
Generics: in, out, and where
Generic Classes
- Declaration: Declare type parameters with angle brackets (
<>).
class Box<T>(t: T) {
var value = t
}
- Instantiation: Provide the type arguments or let the compiler infer them.
val box: Box<Int> = Box<Int>(1) // 显式指定类型
val box = Box(1) // 编译器推断类型为 Box<Int>
Variance
- Invariance: By default, generic types in Kotlin are invariant. This means
List<String>is not a subtype ofList<Object>. - Covariance: Declare a covariant type parameter with the
outkeyword. A covariant type parameter can appear only in anoutposition, meaning it can be a return type but not a parameter type. If the type parameterTof classCis declaredout,C<Base>can safely be a supertype ofC<Derived>. In other words,Cis a producer ofT, not a consumer. - Contravariance: Declare a contravariant type parameter with the
inkeyword. A contravariant type parameter can appear only in aninposition, meaning it can be a parameter type but not a return type. You can call a method onList<? super String>that takes aStringargument. If you call a method that returnsT, it returns anObjectrather than aString. - Declaration-site variance: Specify variance where the type parameter is declared, using the
inandoutkeywords. - Use-site variance: Specify variance where the type is used, through type projections.
- PECS principle (Producer Extends, Consumer Super):
- If an object is a producer and you only read data from it, use
out(the equivalent of Java'sextends). - If an object is a consumer and you only write data to it, use
in(the equivalent of Java'ssuper).
- If an object is a producer and you only read data from it, use
out
- Returns only
T
in
- Accepts only
T
Type Projections
- Concept: Restrict how a generic type can be used to ensure type safety.
outprojection: Prohibits writes and permits only reads (similar toArray<? extends Object>).inprojection: Prohibits reads and permits only writes (similar toArray<? super String>).
Star Projections
- Concept: Indicate that you know nothing about a type parameter while still allowing safe use of the type.
Foo<out T : TUpper>is equivalent toFoo<out TUpper>(you can safely read values of typeTUpper).Foo<in T>is equivalent toFoo<in Nothing>(you cannot safely write to it).Foo<T : TUpper>is equivalent toFoo<out TUpper>when reading andFoo<in Nothing>when writing.- Similar to Java's raw types, but safer.
Generic Functions
- Declaration: Put the type parameters before the function name.
fun <T> singletonList(item: T): List<T> { ... }
fun <T> T.basicToString(): String { ... } // 扩展函数
- Invocation: Specify the type arguments explicitly or let the compiler infer them.
val l = singletonList<Int>(1) // 显式指定类型
val l = singletonList(1) // 编译器推断类型
Generic Constraints
- Concept: Restrict the range of types that can substitute for a type parameter.
- Upper bounds: Specify an upper bound with a colon (
:), similarly to Java'sextends.
fun <T : Comparable<T>> sort(list: List<T>) { ... } // T 必须是 Comparable<T> 的子类型
whereclauses: Use awhereclause when you need multiple upper bounds.
Definitely Non-Nullable Types
- Declare a type parameter as definitely non-nullable with
T & Any. - This is commonly used when interoperating with Java methods annotated with
@NotNull.
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
override fun load(x: T1 & Any): T1 & Any // T1 是明确的非空类型
}
Type Erasure
- Kotlin performs generic type-safety checks at compile time.
- At runtime, instances of generic types contain no information about their actual type arguments.
- Instances of
Foo<Bar>andFoo<Baz?>are both erased toFoo<*>at runtime.
Generic Type Checks and Casts
- Because of type erasure, you cannot check at runtime whether an instance of a generic type was created with a particular type argument, such as by checking
ints is List<Int>. - You can check an instance against a star-projected type, such as
something is List<*>. - When type arguments have already been checked statically, you can check or cast the non-generic portion of a type, such as
list as ArrayList. - Type arguments in generic function calls are checked only at compile time. Inside a function body, type arguments cannot be used for type checks, and casts to type arguments are unchecked. The only exception is an inline function with reified type arguments, which has the actual type argument inlined at each call site.
Unchecked Casts
- You cannot check a cast to a generic type with concrete type arguments at runtime, such as
foo as List<String>. - These unchecked casts can be used when higher-level program logic implies type safety but the compiler cannot infer it directly.
- You can suppress unchecked-cast warnings by annotating a statement or declaration with
@Suppress("UNCHECKED_CAST").
Underscore Operator for Type Arguments
- You can use the underscore operator (
_) for a type argument. When other types are specified explicitly, its type is inferred automatically.
Data Classes
Core Concepts
- Data classes in Kotlin are primarily used to hold data. For each data class, the compiler automatically generates additional member functions that allow instances to be printed in a readable form, compared, copied, and so on. Data classes are marked with
data.
data class User(val name: String, val age: Int)
Automatically Generated Members
- For every property declared in the primary constructor, the compiler automatically derives the following members:
- An
equals()/hashCode()pair. - A
toString()in the format"User(name=John, age=42)". componentN()functions corresponding to the order in which the properties were declared.- A
copy()function.
- An
Data Class Requirements
- To ensure consistent generated code and meaningful behavior, a data class must meet these requirements:
- Its primary constructor must have at least one parameter.
- Every primary-constructor parameter must be marked
valorvar. - A data class cannot be
abstract,open,sealed, orinner.
Member Inheritance Rules
- If you explicitly implement
equals(),hashCode(), ortoString()in the body of a data class, or if the superclass provides an explicit final implementation, these functions are not generated and the existing implementations are used. - If a supertype has
componentN()functions that areopenand return compatible types, the corresponding functions are generated for the data class and override the supertype's functions. An error is reported if the supertype's functions cannot be overridden because their signatures are incompatible or they arefinal. - You cannot provide explicit implementations of the
componentN()andcopy()functions. - A data class can extend another class.
No-Argument Constructors on the JVM
- On the JVM, if the generated class needs a no-argument constructor, you must provide default values for its properties:
data class User(val name: String = "", val age: Int = 0)
Properties Declared in the Class Body
- The compiler uses only properties defined in the primary constructor to generate the functions automatically. To exclude a property from the generated implementations, declare it in the class body:
data class Person(val name: String) {
var age: Int = 0
}
- In this example, by default only the
nameproperty is used in the generatedtoString(),equals(),hashCode(), andcopy()implementations, and there is only one component function,component1(). Theageproperty is declared in the class body, so it is excluded. Consequently, twoPersonobjects with the samenamebut differentagevalues are considered equal becauseequals()evaluates only the properties from the primary constructor:
Copying
- Use the
copy()function to copy an object while changing some properties and leaving the rest unchanged. The implementation of this function for theUserclass above is:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
Data Classes and Destructuring Declarations
- The component functions generated for a data class allow it to be used in destructuring declarations:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age
Standard Data Classes
- The standard library provides
PairandTripleclasses. However, in most cases, a named data class is a better design choice because meaningful property names make the code easier to read.
Enum Classes
Core Concepts
- Enum classes in Kotlin represent a fixed set of distinct values, with each value being an instance of the enum class.
Basic Usage
- Declaration: Declare an enum class with the
enum classkeyword.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
- Constants: Enum constants are objects of the class, separated by commas.
Initialization
- Constructors: Enum constants can be initialized like classes, with arguments passed through a constructor.
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
Anonymous Classes
- Definition: An enum constant can declare its own anonymous class with methods of its own and overrides of the base class's methods.
- Member definitions: If an enum class defines any members, use a semicolon to separate the constant declarations from the member declarations.
Implementing Interfaces
- Implementing interfaces: An enum class can implement interfaces, but cannot inherit from classes. It can provide a common implementation of interface members for every entry or a separate implementation in each entry's anonymous class.
- By default, every enum class implements the
Comparableinterface. Enum constants are defined in a natural order and can be compared.
Using Enum Constants
Enum classes in Kotlin provide synthetic properties and methods for listing the declared enum constants and obtaining an enum constant by name.
EnumClass.valueOf(value: String): EnumClass: Returns the enum constant with the specified name. If the name does not match any enum constant defined in the class, thevalueOf()method throws anIllegalArgumentException.EnumClass.entries: EnumEntries<EnumClass>: Introduced in Kotlin 1.9, this returns the enum entries as a specializedList<EnumClass>type. Kotlin 2.0 replacesenumValues<T>(), which returns an array, with this API.CodeBlock Loading...
Every enum constant also has properties:
name: Gets the name of the enum constant.ordinal: Gets the position of the enum constant in the enum class declaration, starting at 0.KOTLINprintln(RGB.RED.name) // prints RED println(RGB.RED.ordinal) // prints 0
Accessing Enum Constants Generically
enumEntries<T>(): Introduced in Kotlin 2.0, this returns a list of all entries for the given enum typeT. It is the recommended option and performs well.
Inline Value Classes
Core Concepts
- An inline value class is a special class that wraps a single value. It does not create an actual class instance at runtime, reducing memory usage and improving performance.
Declaration and Usage
- Declaration: Declare an inline value class with the
value classkeyword. On the JVM, add the@JvmInlineannotation.
@JvmInline
value class Password(val value: String)
- Characteristics:
- It can have only one property, which must be initialized in the constructor.
- Performance optimization: At runtime, an instance of an inline value class directly uses the underlying value and does not create a new object.
- Type safety: Although the underlying type is
String, aPasswordcannot be used directly as aString, and vice versa. This differs fromtypealias, which merely gives a type another name and can be used interchangeably.
Use Cases
- When you need to create a new type without sacrificing performance.
- When you need to impose stronger constraints on an existing type.
Example
The object Keyword
Singleton Objects
- Definition: The
objectkeyword declares a singleton class, a class with exactly one instance. - Characteristics:
- A singleton object is created on first access and exists for the lifetime of the application.
- A singleton object cannot have a constructor.
- It can inherit from a class or implement an interface.
Companion Objects
- Definition: The
companion objectkeyword declares a companion object within a class. - Characteristics:
- A companion object is associated with the class but is not an instance of it.
- A companion object can access the class's private members.
- Companion objects are commonly used to define factory methods for a class or hold its static-like members.
Anonymous Objects
- Definition:
object : ClassName()creates an anonymous object, an object with no name. - Characteristics:
- Anonymous objects are commonly used to implement an interface or inherit from a class with a one-off implementation.
- They can access variables from the enclosing scope.
- They are initialized immediately.
Object Declaration vs. Object Expression
| Feature | Object Declaration (Singleton) | Object Expression (Anonymous Object) |
|---|---|---|
| Purpose | Create a singleton | One-time use |
| Initialization timing | Lazily loaded on first access | Initialized immediately |
| Access method | Accessed directly by name | Expression result (variable reference) |
| Naming | Must have a name | Does not need a name |
Packages and Imports
Core Concepts
- Package: Used to organize Kotlin code, much like a folder. Each source file can declare the package to which it belongs.
- Import: Allows you to use classes, functions, and other declarations from another package without writing the full package name.
Key Information
Package declaration
- Use the
packagekeyword at the beginning of a source file to declare the package name:package org.example - Everything in the source file, such as classes and functions, belongs to that package.
- For example:
org.example.printMessageandorg.example.Message - If no package is specified, the declaration belongs to the default package, which has no name.
- Use the
Default imports
- Kotlin imports some commonly used packages by default, so you do not need to import them manually.
- These include
kotlin.*,kotlin.annotation.*,kotlin.collections.*, and so on. - Depending on the target platform (JVM or JS), additional packages are imported, such as
java.lang.*on the JVM orkotlin.js.*on JS.
Custom imports
- Use the
importkeyword to import an individual name or an entire package. - Import an individual name:
import org.example.Message(you can then useMessagedirectly) - Import an entire package:
import org.example.*(you can then use everything inorg.example)
- Use the
Aliases
- If imported names conflict, use the
askeyword to rename an imported declaration and resolve the conflict. Example:
KOTLINimport org.example.Message import org.test.Message as TestMessage // 现在可以用 TestMessage 代替 org.test.Message
- If imported names conflict, use the
What can be imported
- Top-level functions and properties
- Functions and properties in object declarations
- Enum constants
Visibility of top-level declarations
- If a top-level declaration, such as a top-level function or variable, is marked
private, it can only be accessed in the file containing the declaration.
- If a top-level declaration, such as a top-level function or variable, is marked
Interfaces
Core Concepts
- Kotlin interfaces are similar to Java 8+ interfaces. They can contain declarations for abstract methods as well as method implementations. The key difference is that interfaces cannot store state. They can have properties, but those properties must be abstract or provide an accessor implementation.
Defining an Interface
- Define an interface with the
interfacekeyword:
Implementing an Interface
- A class or object can implement one or more interfaces:
class Child : MyInterface {
override fun bar() { // 必须实现 MyInterface 中的抽象方法 bar()
// 方法体
}
}
- Use a colon (
:) to indicate an inheritance or implementation relationship. - Use the
overridekeyword when overriding an interface method.
Properties in Interfaces
- Interfaces can declare properties.
- Interface properties cannot have backing fields.
- An interface property can be abstract or provide an accessor implementation:
Interface Inheritance
- An interface can inherit other interfaces.
- An inherited interface can provide member implementations or declare new functions and properties.
- An implementing class only needs to define the missing implementations:
Resolving Override Conflicts
- When a class implements multiple interfaces, it may inherit multiple implementations of the same method. In that case, you must explicitly specify how to implement the method:
- If you inherit multiple implementations with the same signature and want to call a particular interface's implementation, specify it with
super<InterfaceName>.functionName(). - Even if you inherit only one implementation, such as
bar(), you must still override it and specify the implementation to make the class's behavior explicit. The compiler cannot determine automatically which interface's default implementation to use, so failing to override it causes a compilation error.
Functional (SAM) Interfaces
Core Concepts
- A functional interface, also called a single abstract method (SAM) interface, is an interface with only one abstract member function. It can have multiple non-abstract member functions, but only one abstract member function. Kotlin provides the
fun interfacekeyword for declaring functional interfaces and supports SAM conversion, allowing you to use a lambda expression instead of manually implementing the class and making the code more concise.
Declaring a Functional Interface
- Declare one with the
fun interfacekeyword:
fun interface KRunnable {
fun invoke() // 唯一的抽象方法
}
SAM Conversions
- Concept: SAM conversion lets you use a lambda expression instead of manually creating a class that implements a functional interface. Kotlin automatically converts a lambda expression whose signature matches the interface's single abstract method into an instance of that interface.
- Benefit: Simplifies the code and improves readability.
- Example:
In this example, the lambda expression
{ it % 2 == 0 }is automatically converted into an implementation of theIntPredicateinterface.Java interfaces also support SAM conversion
Migrating from an Interface with a Constructor to a Functional Interface
In Kotlin 1.6.20 and later, callable references to functional interface constructors are supported. This provides a source-compatible way to migrate from an interface with a constructor function to a functional interface.
Example:
- Maintaining binary compatibility: Use the
@Deprecatedannotation to hide the legacyPrinterfunction withDeprecationLevel.HIDDEN.
@Deprecated(
message = "使用函数式接口 Printer 代替",
level = DeprecationLevel.HIDDEN
)
fun Printer(...) {...}
Functional Interfaces vs. Type Aliases
Although type aliases can also simplify the use of function types, functional interfaces and type aliases serve different purposes:
Type alias: Merely aliases an existing type and does not create a new type.
KOTLINtypealias IntPredicate = (i: Int) -> Boolean // IntPredicate 仅仅是函数类型 (i: Int) -> Boolean 的别名Functional interface: Creates a new type.
KOTLINfun interface IntPredicate { fun accept(i: Int): Boolean // IntPredicate 是一个真实的接口类型 }
Summary of Differences
| Feature | Type Alias | Functional Interface |
|---|---|---|
| Type creation | Does not create a new type | Creates a new type |
| Members | Can have only one member (the function type) | Can have multiple non-abstract member functions and one abstract member function |
| Interface extension | Does not support implementing or inheriting other interfaces | Supports implementing or inheriting other interfaces |
| Extension functions | Extension functions apply to all function types with the same signature, not just a particular alias. | Extension functions apply only to instances of that functional interface and do not affect other function types. |
| Use cases | Useful for simplifying function types when an API needs to accept any function with specific parameter and return types. | Useful for defining more complex entities with non-trivial contracts and/or operations that cannot be represented by a function type's signature. |
| Performance | Lower overhead | May involve some overhead, such as when converted to a specific interface |
How to Choose
- Type alias: Use a type alias or a simple function type when an API needs to accept any function with specific parameter and return types.
- Functional interface: Use a functional interface when an API needs a more complex entity than a function, such as one with non-trivial contracts or operations that cannot be represented by a function type's signature.
Visibility Modifiers
Core Concepts
- Visibility modifiers control the visibility of classes, objects, interfaces, constructors, functions, properties, and their setters. A getter always has the same visibility as its property. Kotlin provides four visibility modifiers:
private,protected,internal, andpublic. The default visibility ispublic.
Visibility Modifier Overview
| Modifier | Description |
|---|---|
public | (Default) Visible anywhere the declaration is accessible. |
private | Visible only within the file or class in which it is declared. |
protected | Visible only within the class in which it is declared and its subclasses. |
internal | Visible only within the same module. A module is a group of Kotlin files compiled together. |
Package-Level Declarations
Functions, properties, classes, objects, and interfaces declared directly in a package are called top-level declarations.
- To use a visible top-level declaration from another package, import it.
Class Members
For members declared inside a class:
private: Visible only inside that class, including all of its members.protected: Has the same visibility asprivate, but is also visible in subclasses.internal: Visible to any client that can see the class in which the declaration appears, as long as it is in the same module.public: Visible to any client that can see the class in which the declaration appears.
- An outer class cannot see the
privatemembers of its inner class. - If you override a
protectedorinternalmember without explicitly specifying visibility, the override has the same visibility as the original member.
Constructors
- You can specify the visibility of a class's primary constructor with the following syntax:
class C private constructor(a: Int) { ... } // 构造函数是 private
- By default, all constructors are
public, which effectively means they are visible anywhere the class is visible. This means that constructors of aninternalclass are visible only within the same module. - For sealed classes, constructors default to
protected.
Local Declarations
- Local variables, functions, and classes cannot have visibility modifiers.
Modules
- The
internalvisibility modifier means that a member is visible within the same module. Specifically, a module is a group of Kotlin files compiled together, such as:- An IntelliJ IDEA module.
- A Maven project.
- A Gradle source set, except that a test source set can access
internaldeclarations inmain. - A group of files compiled with a single
<kotlinc>Ant task invocation.
Delegated Properties
Core Concepts
- A delegated property is a powerful Kotlin feature that lets you delegate a property's
get()andset()methods to another object, the delegate. This promotes code reuse, generic behavior, and simpler property implementations. Delegated properties are especially useful for common programming patterns such as lazy initialization, change listeners, and data storage.
Basic Syntax
kotlin
Copy
var <propertyName>: <type> by <expression>
<propertyName>: The name of the property you are declaring.<type>: The property's type. It must match the return type of the delegate object'sgetValue()method.<expression>: An expression that returns the delegate object. This object handles the actual property access and modification logic.by: The keyword that tells Kotlin to use delegation.
Delegate Object Requirements
A delegate object must provide a
getValue()function with the required signature for bothvalandvarproperties, and asetValue()function forvarproperties. These functions must be declared asoperatorfunctions.getValue() function (required):
kotlin
Copy
KOTLINoperator fun getValue(thisRef: Any?, property: KProperty<*>): 返回值类型 { ... }thisRef: (Any?) A reference to the object that contains the property. It isnullif the property is top-level. This parameter lets the delegate access the state of the containing object, although this is generally not recommended.property: (KProperty<*>) AKPropertyinstance representing the delegated property. You can use it to access property metadata such as the name (property.name).返回值类型: The type of the delegated property.getValue()must return a value of this type or a subtype.
setValue() function (for
varproperties only):KOTLINoperator fun setValue(thisRef: Any?, property: KProperty<*>, value: 新值类型) { ... }thisRef: (Any?) A reference to the object that contains the property, as withgetValue().property: (KProperty<*>) AKPropertyinstance representing the delegated property, as withgetValue().value: (the same type as the property) The new value to assign to the property.setValue()should store this value in its backing storage.新值类型: The type of the delegated property.setValue()must accept a parameter of this type or a supertype.
Standard Delegates
The Kotlin standard library provides a useful set of predefined delegates available through kotlin.properties.Delegates and the kotlin package:
lazy(): Lazily initializes a property. Its value is calculated by the supplied lambda expression only on first access. Later accesses return the cached value.CodeBlock Loading...LazyThreadSafetyMode: Controls how lazy initialization handles multithreaded access.SYNCHRONIZED(default): Guarantees that only one thread initializes the value. It is thread-safe but may introduce performance overhead.PUBLICATION: Allows multiple threads to attempt to initialize the value, but guarantees that only one result is used. Use this option if the initialization lambda is idempotent, meaning that multiple executions produce the same result.NONE: Provides no thread-safety guarantees. You can use this option for better performance if the property is accessed only from a single thread.
Delegates.observable(): Creates an observable property. The supplied handler is called whenever a new value is assigned to the property.CodeBlock Loading...- The handler function has the signature
(KProperty<*>, oldValue, newValue) -> Unit.
- The handler function has the signature
Delegates.vetoable(): Creates a vetoable property. The supplied handler is called before every attempted assignment of a new value. If the handler returnstrue, the assignment proceeds; if it returnsfalse, the assignment is blocked.CodeBlock Loading...- The handler function has the signature
(KProperty<*>, oldValue, newValue) -> Boolean.
- The handler function has the signature
Delegating to Another Property
Kotlin allows a property to delegate its getter and setter to another property in the same class or a different class. This is useful for implementing backward-compatible renames or keeping multiple properties in sync.
- In this example, both the getter and setter of
delegatedToMemberdelegate tomemberInt. ChangingdelegatedToMemberalso changesmemberInt, and vice versa. @Deprecated: Tells users to usenewNameinstead ofoldName.
Storing Properties in a Map
You can use a Map or MutableMap as a delegate object, allowing you to retrieve property values dynamically from the map with string keys. This is useful when working with JSON or other dynamic data structures.
- In this example, the
Userclass constructor receives aMap<String, Any?>. The values of thenameandageproperties are retrieved from this map using the string keys"name"and"age".
Local Delegated Properties
Local delegated properties let you declare delegated properties inside a function or block. They are useful for caching calculations or lazily initializing local variables that are needed only under certain conditions.
- In this example,
processedDatais initialized only whendatais neithernullnor an empty string. Ifdataisnullor empty, the initialization lambda is not executed.
The provideDelegate Function
The provideDelegate function lets you intercept the creation of a delegated property. This is useful for validating correct usage, setting up a custom delegate instance, or performing other initialization logic.
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): PropertyDelegate { ... }
thisRef: A reference to the object that contains the property, as withgetValue().property: AKPropertyinstance representing the delegated property, as withgetValue().- Return value: The actual object used as the property delegate. It may differ from the original delegate object. This allows a custom delegate to replace a user-provided delegate.
- The
provideDelegatefunction is called when the property is created, allowing logic to run before the actual delegate object is used. The example prints a message indicating that the property and ID have been bound, then creates the actualResourceDelegate, which lazily loads the resource.
Compiler Translation Rules for Delegated Properties
When the Kotlin compiler encounters a delegated property, it generates the following helper code:
- Hidden
$delegateproperty: Forvar prop: Type by MyDelegate(), the compiler creates a hidden property such asprop$delegateof typeMyDelegate. This property stores the actual delegate object. Getter and setter methods: The compiler generates getter and setter methods for
prop. These methods call the delegate object'sgetValue()andsetValue()methods.CodeBlock Loading...Optimized cases: In some cases, such as when directly delegating to another property, the compiler accesses the delegated property's backing field directly and omits the
$delegateproperty to optimize performance.
Extensions
Core Concepts
- Kotlin extensions add functionality to classes or interfaces without inheritance or design patterns such as decorators. They are declared through special declarations called extensions. You can write new functions for classes or interfaces in third-party libraries without modifying them.
Extension Functions
- Declaration: Prefix the function name with the receiver type.
In an extension function, the
thiskeyword refers to the receiver object, the object before the dot.Invocation: Call an extension function just like a regular method.
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' 在 'swap()' 内部将保存 'list' 的值
Static Resolution of Extensions
- Extensions do not actually modify the classes they extend. Defining an extension does not insert a new member into the class; it only makes the new function callable with dot notation on a variable.
- Extension functions are dispatched statically. The compiler determines which extension function to call based on the receiver's type at compile time.
- Members take priority: If a class has a member function and an extension function has the same receiver type, name, and applicable arguments, the member function always takes priority.
- You can overload a member function with an extension function that has the same name but a different signature.
Nullable Receivers
- You can define an extension with a nullable receiver type. Such extensions can be called when the object variable's value is
null. If the receiver isnull,thisis alsonull. Checkthis == nullinside the function body to avoid compiler errors.
Extension Properties
- Kotlin supports extension properties similar to extension functions.
val <T> List<T>.lastIndex: Int
get() = size - 1
- Because extensions do not actually insert members into a class, extension properties have no valid way to have a backing field. This is why initializers are not allowed for extension properties. The behavior of an extension property can only be defined by explicitly providing a getter or setter.
class House
val House.number = 1 // 错误:不允许对扩展属性进行初始化
Companion Object Extensions
- If a class defines a companion object, you can define extension functions and properties for that companion object. Like regular companion object members, they can be called using only the class name as the qualifier.
Scope of Extensions
- In most cases, you define extensions at the top level, directly in a package:
package org.example.declarations
fun List<String>.getLongestString() { /*...*/ }
- To use an extension outside the package in which it is declared, import it at the use site:
Declaring Extensions as Members
- You can declare an extension for another class inside a class. Such an extension has multiple implicit receivers: objects whose members can be accessed without a qualifier. The instance of the class in which the extension is declared is the dispatch receiver, while the instance of the extension receiver type is the extension receiver.
- If there is a name conflict between a dispatch receiver's member and an extension receiver's member, the extension receiver takes priority. Use qualified
thissyntax to refer to the dispatch receiver's member.
- A member-declared extension can be declared
openand overridden in a subclass. This means dispatch for such functions is virtual for the dispatch receiver type but static for the extension receiver type.
Visibility Considerations
- Extensions use the same visibility modifiers as regular functions declared in the same scope. For example:
- An extension declared at the top level of a file can access other
privatetop-level declarations in the same file. - If an extension is declared outside its receiver type, it cannot access the receiver's
privateorprotectedmembers.
- An extension declared at the top level of a file can access other
Type Aliases
Core Concepts
- A type alias is like giving a type a nickname. You can use the nickname or the real name interchangeably, but the nickname may be shorter and easier to remember.
Uses
Simplifying long type names: A long type such as
Set<Network.Node>can be given a simpler name such asNodeSet. This is especially useful when working with complex generic types.Aliasing function types: A function type such as
(Int, String, Any) -> Unitcan be aliased asMyHandlerto make code easier to read and use.Aliasing inner or nested classes: If several classes have inner classes, aliases can make them easier to distinguish. For example,
A.InnerandB.Innercan be replaced withAInnerandBInner.
Examples
Important: Type Aliases Are Not New Types!
typealias Predicate<T> = (T) -> Booleanmerely gives(T) -> Booleananother name.- At compile time, the compiler replaces every type alias with its underlying type.
- This means you can freely use the alias in place of the original type, and vice versa; the Kotlin compiler considers them exactly the same.
Example
Functions
Core Concepts
- Functions are fundamental building blocks of code and are used to perform specific tasks. Kotlin provides flexible ways to define and use them.
Function Declaration
- Kotlin functions are declared with the
funkeyword. - Declaration format:
fun functionName(parameterName: parameterType): returnType { functionBody }
fun double(x: Int): Int {
return 2 * x
}
Function Usage (Invocation)
- Invoke a function in the standard way:
functionName(arguments) - Invoke a member function, which is a function in a class or object, with dot notation:
instance.functionName(arguments)
val result = double(2) // 调用 double 函数
Stream().read() // 创建 Stream 类的实例并调用 read 函数
Parameters
- Function parameters use Pascal-style notation:
parameterName: parameterType - Separate multiple parameters with commas.
- Every parameter must have an explicit type.
fun powerOf(number: Int, exponent: Int): Int { /* 函数体 */ }
- You can use a trailing comma when declaring function parameters:
fun powerOf(
number: Int,
exponent: Int, // 尾随逗号
) { /* 函数体 */ }
Default Parameters
- Function parameters can have default values. When you omit such a parameter in a call, its default value is used. This reduces the number of function overloads.
- Set a default value by adding
=after the type, for example,parameterName: parameterType = defaultValue.
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /* 函数体 */ }
- An overriding method always uses the base method's default argument values. When overriding a method that has default argument values, omit the default values from the signature:
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) { /*...*/ } // 不允许设置默认值.
}
- If a parameter with a default value precedes one without a default value, you can use the default value only by invoking the function with named arguments:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // 使用默认值 bar = 0
- If the final parameter after a parameter with a default value is a lambda expression, you can pass it as a named argument or outside the parentheses:
Named Arguments
- When invoking a function, you can name one or more of its parameters. This is helpful when a function has many parameters and associating values with them is difficult, especially when the parameters are Boolean or null values.
- When using named arguments in a function call, you can freely change the order in which they are listed. You can omit parameters completely if you want to use their default values.
- Consider the
reformat()function, which has four parameters with default values.
- When invoking this function, you do not have to name all of its arguments:
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
- You can skip every parameter that has a default value:
reformat("This is a long String!")
- You can also skip specific parameters with default values rather than omitting all of them. However, after the first skipped parameter, every subsequent parameter must be named:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
- You can pass a variable number of arguments (varargs) by name using the spread operator, with
*as a prefix to the array:
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
- When invoking a Java function on the JVM, you cannot use named-argument syntax because Java bytecode does not always retain parameter names.
Functions Returning Unit
- If a function does not return a useful value, its return type is
Unit.Unitis a type with only one value:Unit. You do not have to return this value explicitly:
- Declaring the
Unitreturn type is also optional. The code above is equivalent to:
fun printHello(name: String?) { ... }
Single-Expression Functions
- When a function body consists of a single expression, you can omit the braces and specify the body after the
=symbol:
fun double(x: Int): Int = x * 2
- When the compiler can infer the return type, declaring the return type explicitly is optional:
fun double(x: Int) = x * 2
Explicit Return Types
- A function with a block body must always have an explicit return type unless it is intended to return
Unit, in which case specifying the return type is optional. - Kotlin does not infer return types for functions with block bodies because their bodies may have complex control flow, and the return type may not be obvious to readers or even to the compiler.
Variable Number of Arguments (Varargs)
- You can mark a function parameter, usually the last one, with the
varargmodifier:
- In this case, you can pass a variable number of arguments to the function:
val list = asList(1, 2, 3)
- Inside the function, a
varargparameter of typeTappears as an array ofT. In the example above, thetsvariable has typeArray<out T>. - Only one parameter can be marked
vararg. If thevarargparameter is not the last parameter in the list, subsequent parameter values must be passed with named-argument syntax or, if the parameter has a function type, by passing a lambda expression outside the parentheses. - When invoking a
varargfunction, you can pass the arguments individually, such asasList(1, 2, 3). If you already have an array and want to pass its contents to the function, use the spread operator, with*as a prefix to the array:
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
- If you want to pass an array of primitive values to a
vararg, convert it to a regular typed array with thetoTypedArray()function:
val a = intArrayOf(1, 2, 3) // IntArray 是一个原始类型数组
val list = asList(-1, 0, *a.toTypedArray(), 4)
Infix Notation
- A function marked with the
infixkeyword can also be called using infix notation, omitting the dot and parentheses in the invocation. An infix function must meet these requirements:- It must be a member function or an extension function.
- It must have exactly one parameter.
- The parameter cannot accept a variable number of arguments and must not have a default value.
infix fun Int.shl(x: Int): Int { /* 函数体 */ }
// 使用中缀符号调用函数
1 shl 2
// 与以下相同
1.shl(2)
An infix function call has lower precedence than arithmetic operators, casts, and the
rangeTooperator. The following expressions are equivalent:1 shl 2 + 3is equivalent to1 shl (2 + 3)0 until n * 2is equivalent to0 until (n * 2)xs union ys as Set<*>is equivalent toxs union (ys as Set<*>)
On the other hand, an infix function call has higher precedence than the Boolean operators
&&and||,isandinchecks, and several other operators. These expressions are also equivalent:a && b xor cis equivalent toa && (b xor c)a xor b in cis equivalent to(a xor b) in c
Note that an infix function call always requires an explicit receiver and argument. When invoking a method on the current receiver with infix notation, use
thisexplicitly to ensure unambiguous resolution.
Function Scope
- Kotlin functions can be declared at the top level of a file, so you do not need to create a class to hold a function, as is required in languages such as Java, C#, and Scala (top-level definitions are available in Scala 3 and later). In addition to top-level functions, Kotlin functions can be declared locally as member functions and extension functions.
Local Functions
- Kotlin supports local functions, which are functions inside other functions:
- Local functions can access local variables in the enclosing function, forming a closure. In the example above,
visitedcan be a local variable:
Member Functions
- A member function is a function defined inside a class or object:
class Sample {
fun foo() { print("Foo") }
}
- Invoke a member function with dot notation:
Sample().foo() // 创建 Sample 类的实例并调用 foo
Generic Functions
- A function can have generic parameters, specified with angle brackets before the function name:
fun <T> singletonList(item: T): List<T> { /*...*/ }
Tail Recursive Functions
- Kotlin supports a functional programming style called tail recursion. For some algorithms that would normally use loops, you can use a recursive function instead without risking a stack overflow. When a function is marked with the
tailrecmodifier and meets the required formal conditions, the compiler optimizes away the recursion, leaving a fast, efficient loop-based version:
- To meet the requirements of the
tailrecmodifier, the function must call itself as its last operation. You cannot use tail recursion when there is more code after the recursive call, when the call occurs in atry/catch/finallyblock, or when the function isopen. Currently, tail recursion is supported in Kotlin for the JVM and Kotlin/Native.
Higher-Order Functions and Lambda Expressions
Core Concepts
- Functions are values: In Kotlin, you can assign functions to variables, store them in data structures, pass them as arguments to other functions, and return them from functions just as you can with numbers and strings.
- Higher-order function: In simple terms, a function that can accept a function as an argument or return a function.
- Lambda expression: In simple terms, an anonymous function that lets you quickly define and use a function.
What Is a Higher-Order Function?
- Definition: A function that accepts a function as an argument or returns a function.
Example: The
foldfunction is a typical higher-order function. It takes an initial value and a combining function. It traverses every element of a collection, using the combining function to combine the current element with the accumulated value, and ultimately returns a result.Code example (simplified):
CodeBlock Loading...- The
combineparameter has a function type. It takes two arguments, of typesRandT, and returns a value of typeR.
- The
How to invoke
fold: Use a lambda expression or a function reference.Example:
CodeBlock Loading...
Function Types
- Purpose: Kotlin uses function types to represent functions. For example,
(Int) -> Stringrepresents a function that takes anIntargument and returns aString. - Syntax:
(parameterType1, parameterType2, ...) -> returnType.()means no arguments, as in() -> Unit.Unitis equivalent to Java'svoid.A.(B) -> Crepresents a function type with a receiver. You can think of it as an extension function on typeA.
- Nullable function type:
((Int, Int) -> Int)?means this function type can benull. - Type alias: You can use
typealiasto give a function type an alias for easier use.
typealias MyHandler = (String, Int) -> Unit
How to Create an Instance of a Function Type?
There are several ways to do so:
- Lambda expression:
{ a, b -> a + b } - Anonymous function:
fun(s: String): Int { return s.toIntOrNull() ?: 0 } - Function reference:
::isOddfor a top-level function, orString::toIntfor a member function - A class that implements a function type interface:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO() // 实现 invoke 方法
}
Invoking a Function Type Instance
- Use
invoke(...):f.invoke(x) - Invoke it directly:
f(x) - If the function type has a receiver, you can invoke it like an extension function:
1.foo(2).
Closures
- Definition: A lambda expression or anonymous function can access variables defined in an enclosing scope.
- Modifying outer variables: A lambda expression can modify variables in the enclosing scope.
var sum = 0
numbers.forEach { sum += it }
println(sum) // 输出:numbers所有元素的总和
Function Literals with Receiver
- Type:
A.(B) -> C - Purpose: Lets you invoke methods on a receiver object inside a lambda expression as if it were an extension function.
thiskeyword: Inside a lambda expression, you can use thethiskeyword to refer to the receiver object.- Example:
val sum: Int.(Int) -> Int = { other -> this + other }
println(1.sum(2)) // 输出:3
Type-Safe Builders
Lambda expressions with receivers are commonly used to build type-safe DSLs (domain-specific languages).
Lambda Expressions
- Syntax:
{ parameters -> body } - Parameter type inference: You can omit parameter types if the compiler can infer them.
- Trailing lambda: If a function's last parameter has a function type, you can place the lambda expression outside the parentheses.
numbers.filter { it > 0 }
itkeyword: If a lambda expression has only one parameter, you can use theitkeyword to refer to it.numbers.map { it * 2 }
- Return value: The value of the last expression in a lambda expression is its return value. You can also return explicitly with
return@label. - Underscore
_: If a lambda expression's parameter is unused, you can replace its name with the underscore (_).map.forEach { (_, value) -> println(value) }
Anonymous Functions
- Syntax:
fun(parameters: Type): ReturnType { body } - Characteristics:
- You can explicitly specify the return type.
- A
returnstatement returns from the anonymous function itself, not from the function containing it, unlike in a lambda expression.
Inline Functions
Core Concepts
- An inline function is a special function that lets the compiler insert the function body directly at the call site instead of making a regular function call. This can reduce call overhead and improve performance, especially when the function is called very frequently.
Why Do You Need Inline Functions?
- Performance optimization: In some cases, function-call overhead can be significant, especially for a function called frequently in a loop. Inline functions avoid this overhead and improve execution efficiency.
- Non-local returns: Inline functions allow a lambda expression to use a
returnstatement to return directly from the containing function, which can be very useful in some cases.
How to Use an Inline Function?
- Add the
inlinekeyword before the function declaration:
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }
- This tells the compiler to try to inline the code of the
lock()function directly into the call site.
Restrictions on Inline Functions
- Performance concerns: Although inline functions can improve performance, using too many can increase the size of the generated code. Use them in moderation and avoid inlining large functions.
- Non-local returns: In an inline function, a lambda expression can use a
returnstatement to return directly from the containing function. This can be very useful, but it can also make the code logic more complex.
The noinline Keyword
- If you only want to inline some lambda-expression parameters, use the
noinlinekeyword:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
- The
inlinedparameter is inlined, whilenotInlinedis not. - A
noinlinelambda expression can be used like a regular function. You can store it in a variable or pass it to another function.
Non-Local Returns
- In an ordinary lambda expression, you cannot use
returndirectly because a lambda expression cannot return from the outer function. - However, if the lambda expression is inlined, you can use
returnto return directly from the outer function containing the lambda expression. This is called a non-local return.
fun foo() {
inlined {
return // OK: the lambda is inlined, 直接从 foo() 返回
}
}
The crossinline Keyword
- If an inline function invokes a lambda expression in another execution context, such as a local object or nested function, the lambda expression cannot use a non-local return. Use the
crossinlinekeyword to enforce this restriction:
break and continue
- In an inline function, a lambda expression can use
breakandcontinueto control the flow of the containing loop.
The reified Keyword
- In Kotlin, generics are erased at runtime. In other words, you cannot access a generic's concrete type inside a function.
- The
reifiedkeyword lets you access a generic type inside an inline function as if it were an ordinary class. For example:
inline fun <reified T> TreeNode.findParentOfType(): T? { ... }
- This lets you use operations such as
T::classandT is Typewithout reflection. - Note: Only inline functions can use the
reifiedkeyword.
Inline Properties
- The
inlinekeyword can be used for a property'sget()andset()methods. You can inline the entire property or only its getter or setter.
Restrictions on Inline Functions in Public APIs
- If your inline function is part of a public API (
publicorprotected), it can be called from other modules and will also be inlined. - To avoid potential binary compatibility issues, a public-API inline function cannot use declarations that are not part of the public API (
privateorinternal). - You can use the
@PublishedApiannotation to allow aninternaldeclaration to be used in a public-API inline function.
Operator Overloading
Core Concepts
- Operator overloading lets you define operator behavior for custom types. You can then use standard operators, such as
+,-,*, and/, with custom types just as you can with primitive types.
How to Implement Operator Overloading?
- In Kotlin, operator overloading is implemented by defining specific functions in a class. These functions are marked with the
operatorkeyword.
Unary Operators
| Operator | Corresponding Function | Example | Description |
|---|---|---|---|
+ | unaryPlus() | +a | Unary plus (usually makes no change) |
- | unaryMinus() | -a | Unary minus |
! | not() | !a | Logical not (negation) |
Example
Increment and Decrement Operators (++, --)
a++(postfix increment): Returns the original value ofa, then incrementsa.++a(prefix increment): Incrementsa, then returns its new value.a--and--awork similarly.Important: The
inc()anddec()functions should return a new value and assign it to the variable. They should not mutate the original object directly.
Binary Operators
| Operator | Corresponding Function | Example | Description |
|---|---|---|---|
+ | plus(b) | a + b | Addition |
- | minus(b) | a - b | Subtraction |
* | times(b) | a * b | Multiplication |
/ | div(b) | a / b | Division |
% | rem(b) | a % b | Remainder (modulus) |
.. | rangeTo(b) | a..b | Creates a range, such as 1..10 |
..< | rangeUntil(b) | a..<b | Creates a range that excludes the last element |
Example
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
The in Operator
a in b: Checks whetherais inb. Corresponding function:b.contains(a)a !in b: Checks whetherais not inb. Corresponding function:!b.contains(a)
Indexing Operator ([])
a[i]: Accesses an element in an array or collection. Corresponding function:a.get(i)a[i] = b: Sets an element's value in an array or collection. Corresponding function:a.set(i, b)
invoke Operator (())
a(): Invokes an object like a function. Corresponding function:a.invoke()
Augmented Assignment Operators (+=, -=, *=, /=, %=)
a += b: Equivalent toa = a + bif noplusAssignfunction exists. Prefera.plusAssign(b).- The others work similarly.
Equality and Inequality Operators (==, !=)
a == b: Checks whetheraandbare equal. Corresponding function:a?.equals(b) ?: (b === null)a != b: Checks whetheraandbare not equal. Corresponding function:!(a?.equals(b) ?: (b === null))
Comparison Operators (>, <, >=, <=)
a > b: Compares the size ofaandb. Corresponding function:a.compareTo(b) > 0- The others work similarly.
Note
===and!==, which check whether two references point to the same object, cannot be overloaded.
The this Expression
Core Concepts
thisrefers to the "current object", much as you might use "I" to refer to yourself while speaking to someone else.
Uses of this
- Inside a class:
thisrefers to the object of the current class. For example, in aPersonclass,thisrefers to the currentPersonobject. - Inside an extension function or a function literal with a receiver:
thisrefers to the receiver parameter, the object to the left of the dot (.).
Determining What this Refers To
When there is only one this, it usually refers to the innermost scope. To access this in an outer scope, use a qualified this.
Qualified this
To access this from an outer scope, such as an outer class, extension function, or labeled function literal, use the this@label form.
@labelis the label for the scope.
Example
Implicit this
When you invoke a member function on a this object, you can omit the this. portion.
Note
Be careful when using this if you have a non-member function with the same name, because in some cases it may call the non-member function instead.
Example
Annotations
Core Concepts
- An annotation is a way to add extra information to code, much like attaching a label to it. This information can be used by the compiler, tools, or the runtime.
How to Declare an Annotation?
- Add the
annotationkeyword before theclasskeyword.
annotation class Fancy
Meta-Annotations
Meta-annotations annotate annotations and specify their behavior.
@Target: Specifies the code elements on which the annotation can be used, such as classes, functions, and properties.@Retention: Specifies whether the annotation is stored in the compiled class file and whether it can be accessed through reflection at runtime. By default, both aretrue.@Repeatable: Allows the same annotation to be used multiple times on the same code element.@MustBeDocumented: Specifies that the annotation is part of the public API and should be included in generated API documentation.
Example
How to Use an Annotation?
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
Annotation Constructors
- An annotation can have a constructor with parameters.
annotation class Special(val why: String)
@Special("example") class Foo {}
Allowed Parameter Types
- Java primitive types, such as Int and Long
- Strings (
String) - Classes (
Foo::class) - Enums (
Enum) - Other annotations
- Arrays of the above types
Note
- Annotation parameters cannot have nullable types because the JVM does not support using
nullas an annotation property value.
Annotations as Parameters
- If an annotation is used as an argument to another annotation, its name does not need the
@symbol.
Classes as Parameters
- If you need to use a class as an annotation parameter, use Kotlin's
KClass. The Kotlin compiler automatically converts it to a Java class so that Java code can access the annotation and its arguments correctly.
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
Instantiating an Annotation Class
- In Kotlin, you can invoke an annotation class's constructor just like a regular class constructor and use the generated instance.
Annotations on Lambda Expressions
- Annotations can be applied to lambda expressions. They are applied to the
invoke()method generated for the lambda expression.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
Use-Site Targets
- When you annotate a property or primary-constructor parameter, multiple Java elements are generated, and the annotation can be placed on different elements. Use-site targets specify precisely which Java element receives the generated annotation.
Common Use-Site Targets
file: The entire fileproperty: The property (not visible in Java)field: The fieldget: The property getterset: The property setterreceiver: The receiver parameter of an extension function or propertyparam: The constructor parametersetparam: The property setter parameterdelegate: The delegate-instance field of a delegated property
Multiple Annotations with the Same Target
- If multiple annotations have the same use-site target, you can enclose them in brackets to avoid repeating the target.
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
Java Annotation Compatibility
- Kotlin is 100% compatible with Java annotations.
Java Annotation Parameters
- Because the order of Java annotation parameters is undefined, you cannot pass them with ordinary function-call syntax. You must use named-argument syntax.
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
The Special Case of the value Parameter
- In Java, if an annotation has only one parameter named
value, you can omit the parameter name in Kotlin.
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
Arrays as Annotation Parameters
- If a Java annotation parameter has an array type, it becomes a
varargparameter in Kotlin.
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
- For other array types, use array-literal syntax or
arrayOf(...).
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
Accessing an Annotation Instance's Properties
- An annotation instance's values are exposed as properties in Kotlin.
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value // 直接访问 value 属性
}
Repeatable Annotations
- Kotlin supports repeatable annotations, allowing the same annotation to be used multiple times on the same code element. Mark the annotation declaration with the
@kotlin.annotation.Repeatablemeta-annotation to make it repeatable in both Kotlin and Java.
@Repeatable
annotation class Tag(val name: String)
// 编译器会自动生成包含注解 @Tag.Container
- You can use the
@kotlin.jvm.JvmRepeatablemeta-annotation and pass an explicitly declared containing annotation class as an argument to give the containing annotation a custom name.
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
- To retrieve repeatable Kotlin or Java annotations through reflection, use the
KAnnotatedElement.findAnnotations()function.
Destructuring
Core Concepts
- A destructuring declaration is convenient syntax that lets you break an object into multiple variables.
Syntax
How It Works: componentN() Functions
- Kotlin implements destructuring by calling a series of
componentN()functions. component1()corresponds to the first variable.component2()corresponds to the second variable.And so on with
component3(),component4(), and so forth.Therefore, any object with
componentN()functions can be used in a destructuring declaration.
val name = person.component1() // 等价于 name = person 的第一个属性
val age = person.component2() // 等价于 age = person 的第二个属性
The operator Keyword
- A
componentN()function must be marked with theoperatorkeyword before it can be used in a destructuring declaration.
Use Cases
forloops: Conveniently extract elements from collections.
for ((a, b) in collection) {
// a 和 b 分别是集合中每个元素的 component1() 和 component2() 的返回值
}
- Returning multiple values from a function: A
data classmakes it convenient to return multiple values that can be destructured directly.
- Traversing a map: You can destructure key-value pairs directly in a
forloop.
Ignoring Unneeded Variables: the Underscore _
- If you do not need a variable during destructuring, you can replace its name with the underscore (
_). Kotlin does not call the correspondingcomponentN()function, saving resources.
val (_, status) = getResult() // 只关心 status,忽略第一个返回值
Destructuring in Lambda Expressions
- You can use a destructuring declaration in a lambda expression's parameters to make code more concise.
Type Declarations in Lambdas
- You can specify a type for the entire destructured parameter or for an individual component.
Class, Function, Property, Constructor, and Bound References
Kotlin provides several kinds of references that let you manipulate a program's structure at runtime. Understanding them is essential for writing more flexible and powerful code.
Class References: ::class
- Purpose: Obtain the runtime
KClassobject for a Kotlin class. - Syntax:
ClassName::class - Type:
KClass<ClassName> - Uses:
- Type checks with the
isoperator - Obtaining class information, such as its name, constructors, and properties
- Interoperability with Java reflection through the
.javaproperty, which returns ajava.lang.Classobject
- Type checks with the
- Example:
Function References: ::
- Purpose: Obtain a reference to a function that can be used like a function-type value.
- Syntax:
::functionName - Type:
(ParameterTypes) -> ReturnTypeorKFunction<out R> - Uses:
- Passing a function as an argument to a higher-order function, such as
filter,map, orreduce - Creating function compositions
- Invoking functions dynamically
- Passing a function as an argument to a higher-order function, such as
- Example:
Property References: ::
- Purpose: Obtain a reference to a property so you can access its value and metadata.
- Syntax:
::propertyName - Type:
KProperty0<Type>for a top-level property or a property without a receiverKProperty1<Receiver, Type>for a property with a receiverKMutableProperty...for a mutable property
- Uses:
- Dynamically getting and setting property values
- Accessing a property's name and type
- Use in higher-order functions, such as extracting an object's property values with
map
- Example:
Constructor References: ::ClassName
- Purpose: Obtain a reference to a constructor so you can create instances of the class dynamically.
- Syntax:
::ClassName - Type:
(ConstructorParameterTypes) -> ClassNameorKFunction<out ClassName> - Uses:
- Dependency injection
- Dynamically creating objects
- The factory pattern
- Example:
Bound References: instance::member
- Purpose: Bind a function or property to a specific object instance.
- Syntax:
instance::memberName, wherememberNamecan be a function or property - Type: The type is adjusted for the bound member and instance and no longer includes the receiver parameter.
- Uses:
- Simplifying code by avoiding repeated passing of the object instance
- Creating state-related function references
- Passing an object's specific behavior in functional programming
- Example:
Summary of the Main Differences
| Feature | Class Reference (::class) | Function Reference (::) | Property Reference (::) | Constructor Reference (::ClassName) | Bound Reference (instance::member) |
|---|---|---|---|---|---|
| Referenced object | Class | Function | Property | Constructor | Instance + member (function or property) |
| Primary use | Type information and interoperability | Functional programming | Dynamic access | Dynamic object creation | Simplified code and state-related behavior |
| Binding | Not bound | Not bound | Not bound | Not bound | Bound to a specific object instance |
Choosing the Right Reference Type
- Use a class reference (
::class) when you need to manipulate information about the class itself. - Use a function reference (
::functionName) when you need to pass a function as an argument or invoke it dynamically. - Use a property reference (
::propertyName) when you need to access property values dynamically or retrieve property metadata. - Use a constructor reference (
::ClassName) when you need to create instances of a class dynamically. - Use a bound reference (
instance::member) when you need to bind a function or property to a specific object instance.