By Aasmund Eldhuset, Software Engineer at Khan Academy. Published on November 29, 2018. This document is not a part of Khan Academy's official product offering, but rather an internal resource that we're providing "as is" for the benefit of the programming community. If you find any errors, please submit an issue or a pull request.
Kotlin is a compiled, statically typed language, which might provide some initial hurdles for people who are used to the interpreted, dynamically typed Python. This document aims to explain a substantial portion of Kotlin's syntax and concepts in terms of how they compare to corresponding concepts in Python.
Kotlin can be compiled for several different platforms. In this document, we assume that the target platform is the Java virtual machine, which grants some extra capabilities - in particular, your code will be compiled to Java bytecode and will therefore be interoperable with the large ecosystem of Java libraries.
Even if you don't know Python, this document should hopefully be a useful introduction to Kotlin, in particular if you are used to other dynamically typed languages. However, if you're coming from a Java background, you're probably better off diving directly into the excellent official docs (from which this doc has drawn a lot of inspiration). To some extent, you can try to write Java code and look stuff up whenever what you're trying to do doesn't work - and some IDEs can even automatically convert Java code to Kotlin.
Only imports and declarations can exist at the top level of a Kotlin file. Therefore, "running" an individual file only makes sense if it contains an entry point, which must be a function called main with one argument called args of the type "array of strings". args will contain the command-line arguments that the program is invoked with, similarly to sys.argv in Python; it can be omitted if your program does not need to accept command-line arguments and you are using Kotlin 1.3:
funmain() {
println("Hello World!")
}
The function body is delimited by curly braces - indentation is generally not significant in Kotlin, but you should of course indent your code properly for the benefit of human readers.
Comments are initiated with // and last until the end of the line. Block comments start with /* and end with */.
Like in Python, statements may be terminated by a semicolon, but it's discouraged. There is no line continuation character; instead, a line is automatically joined with one or more of the subsequent lines if that's the only way to make the code parse correctly. In practice, that means that a statement continues on the next line if we're inside an open parenthesis (like in Python), or if the line ends with a "dangling operator" (unlike in Python) or the following line doesn't parse unless it's joined to the previous one (also unlike in Python). Note that this is pretty much the opposite of JavaScript, which generally will keep joining lines as long as the resulting code still parses. Thus, the following is two expressions in Kotlin and in Python (because + can be unary, so the second line parses on its own), but one in JavaScript:
1+2+3
This is one expression in both Kotlin (because the first line doesn't parse on its own) and JavaScript, and doesn't parse in Python:
1+2+3
So is the following. The difference between + and . is that + can be a unary operator, but . can't, so the only way to get the second line to parse is to join it with the preceding line:
x.foo()
.bar()
This is one expression in all three languages:
(1+2+3)
Don't split lines if the resulting two lines are also grammatically valid on their own (even if it results in a compilation error that is not directly related to the grammar of Kotlin). The following does not actually return the result of foo() - it returns a special value called Unit, which we'll cover later, and foo() is never called.
The author strongly recommends that you use an IDE with Kotlin support, as the static typing allows an IDE to do reliable navigation and code completion. I recommend IntelliJ IDEA, which is built by the same company that created Kotlin. The Community Edition is free; see instructions for getting started (it comes bundled with Kotlin, and you can run your program from the IDE).
If you insist on using a plain editor and the command line, see these instructions instead. In short, you need to compile your Kotlin code before running it. Assuming that your Kotlin file is called program.kt:
By default, Kotlin compiles down to Java (so you have the entire Java Standard Library available to you, and interacting with Java libraries is a breeze), so you now have a Java Archive (program.jar) which includes the Java libraries that are necessary to support the Kotlin features (thanks to -include-runtime), and you can run it using an out-of-the-box Java runtime:
java -jar program.jar
Declaring variables
Every variable must be declared. Any attempt to use a variable that hasn't been declared yet is a syntax error; thus, you are protected from accidentally assigning to a misspelled variable. The declaration also decides what kind of data you are allowed to store in the variable.
Local variables are typically declared and initialized at the same time, in which case the type of the variable is inferred to be the type of the expression you initialize it with:
var number =42var message ="Hello"
We now have a local variable number whose value is 42 and whose type is Int (because that's the type of the literal 42), and another local variable message whose value is "Hello" and whose type is String. Subsequent usages of the variable must use only the name, not var:
number =10
number +=7println(number)
println(message +" there")
However, you cannot change the type of a variable: number can only ever refer to Int values, and message can only ever refer to String values, so both number = "Test" and message = 3 are illegal and will produce syntax errors.
Read-only variables
Frequently, you'll find that during the lifetime of your variable, it only ever needs to refer to one object. Then, you can declare it with val (for "value") instead:
val message ="Hello"val number =42
The terminology is that var declares a mutable variable, and that val declares a read-only or assign-once variable - so both kinds are called variables.
Note that a read-only variable is not a constant per se: it can be initialized with the value of a variable (so its value doesn't need to be known at compile-time), and if it is declared inside a construct that is repeatedly invoked (such as a function or a loop), it can take on a different value on each invocation. Also, while the read-only variable may not be reassigned while it is in scope, it can still refer to an object which is in itself mutable (such as a list).
Constants
If you have a value that is truly constant, and the value is a string or a primitive type (see below) that is known at compile-time, you can declare an actual constant instead. You can only do this at the top level of a file or inside an object declaration (but not inside a class declaration):
constval x =2
Specifying the type explicitly
If you really want to, you can both initialize and specify the type on the same line. This is mostly useful if you're dealing with a class hierarchy (more on that later) and you want the variable type to be a base type of the value's class:
val characters:CharSequence="abc"
In this doc, we'll sometimes specify the type unnecessarily, in order to highlight what type is produced by an expression. (Also, a good IDE will be able to show you the resulting type.)
For completeness: it is also possible (but discouraged) to split the declaration and the initial assignment, and even to initialize in multiple places based on some condition. You can only read the variable at a point where the compiler can prove that every possible execution path will have initialized it. If you're creating a read-only variable in this way, you must also ensure that every possible execution path assigns to it exactly once.
val x:String
x =3
Scopes and naming
A variable only exists inside the scope (curly-brace-enclosed block of code; more on that later) in which it has been declared - so a variable that's declared inside a loop only exists in that loop; you can't check its final value after the loop. Variables can be redeclared inside nested scopes - so if there's a parameter x to a function and you create a loop and declare an x inside that loop, the x inside the loop is a different variable than the parameter x.
Variable names should use lowerCamelCase instead of snake_case.
In general, identifiers may consist of letters, digits, and underscores, and may not begin with a digit. However, if you are writing code that e.g. autogenerates JSON based on identifiers and you want the JSON key to be a string that does not conform to these rules or that collides with a keyword, you can enclose it in backticks: `I can't believe this is not an error!` is a valid identifier.
Primitive data types and their limitations
The primitive data types are the most fundamental types in Kotlin; all other types are built up of these types and arrays thereof. Their representation is very efficient (both in terms of memory and CPU time), as they map to small byte groups that are directly manipulatable by the CPU.
Integer types
Integer types in Kotlin have a limited size, as opposed to the arbitrarily large integers in Python. The limit depends on the type, which decides how many bits the number occupies in memory:
Type
Bits
Min value
Max value
Long
64
-9223372036854775808
9223372036854775807
Int
32
-2147483648
2147483647
Short
16
-32768
32767
Byte
8
-128
127
Bytes are -128 through 127 due to Kotlin inheriting a bad design decision from Java. In order to get a traditional byte value between 0 and 255, keep the value as-is if it is positive, and add 256 if it is negative (so -128 is really 128, and -1 is really 255). See the section on extension functions for a neat workaround for this.
An integer literal has the type Int if its value fits in an Int, or Long otherwise. Long literals should be suffixed by L for clarity, which will also let you make a Long with a "small" value. There are no literal suffixes for Short or Byte, so such values need an explicit type declaration or the use of an explicit conversion function.
val anInt =3val anotherInt =2147483647val aLong =2147483648val aBetterLong =2147483649Lval aSmallLong =3Lval aShort:Short=32767val anotherShort =1024.toShort()
val aByte:Byte=65val anotherByte = (-32).toByte()
Beware that dividing an integer by an integer produces an integer (like in Python 2, but unlike Python 3). If you want a floating-point result, at least one of the operands needs to be a floating-point number (and recall that like in most languages, floating-point operations are generally imprecise):
println(7/3) // Prints 2println(7/3.0) // Prints 2.3333333333333335val x =3println(7/ x) // Prints 2println(7/ x.toDouble()) // Prints 2.3333333333333335
Whenever you use an arithmetic operator on two integers of the same type (or when you use a unary operator like negation), there is no automatic "upgrading" if the result doesn't fit in the type of the operands! Try this:
val mostPositive =2147483647val mostNegative =-2147483648println(mostPositive +1)
println(-mostNegative)
Both of these print -2147483648, because only the lower 32 bits of the "real" result are stored.
When you use an arithmetic operator on two integers of different types, the result is "upgraded" to the widest type. Note that the result might still overflow.
In short: think carefully through your declarations of integers, and be absolutely certain that the value will never ever need to be larger than the limits of the type! If you need an integer of unlimited size, use the non-primitive type BigInteger.
Floating-point and other types
Type
Bits
Notes
Double
64
16-17 significant digits (same as float in Python)
Float
32
6-7 significant digits
Char
16
UTF-16 code unit (see the section on strings - in most cases, this is one Unicode character, but it might be just one half of a Unicode character)
Boolean
8
true or false
Floating-point numbers act similarly to in Python, but they come in two types, depending on how many digits you need. If you need larger precision, or to work with monetary amounts (or other situations where you must have exact results), use the non-primitive type BigDecimal.
Strings
Unicode correctness can be onerous in Python 2, since the "default" string type str is really just a byte array, while unicode is actually a sequence of code units (see below) - and whether the code units are 16 or 32 bits wide depends on how your Python distribution was built. In Kotlin, there's no such confusion: String, which is what you get when you make a string literal (which you can only do with double quotes), is an immutable sequence of UTF-16 code units. ByteArray is a fixed-size (but otherwise mutable) byte array (and String can specifically not be used as a byte array).
A UTF-16 code unit is a 16-bit unsigned integral value that represents either one Unicode code point (character code) or must be combined with another code unit to form a code unit. If this makes no sense, I strongly recommend Joel Spolsky's excellent essay on Unicode and its encodings. For most Western scripts, including English, all code points fit inside one code unit, so it's tempting to think of a code unit as a character - but that will lead astray once your code encounters non-Western scripts. A single UTF-16 code unit can be represented with single quotes, and has the type Char:
val c ='x'// Charval message ="Hello"// Stringval m = message[0] // Char
Thus, single quotes can not be used to form string literals.
Given a string s, you can get a ByteArray with the UTF-8 encoding of the string by calling s.toByteArray(), or you can specify another encoding, e.g. s.toByteArray(Charsets.US_ASCII) - just like encode() in Python. Given a byte array b that contains a UTF-8-encoded string, you can get a String by calling String(b); if you've got a different encoding, use e.g. String(b, Charsets.US_ASCII), just like decode() in Python. You can also call e.g. b.toString(Charsets.US_ASCII), but do not call b.toString() without parameters (this will just print an internal reference to the byte array).
You can do string interpolation with $, and use curly braces for expressions:
val name ="Anne"val yearOfBirth =1985val yearNow =2018val message ="$name is ${yearNow - yearOfBirth} years old"
If you want a literal $, you need to escape it: \$. Escaping generally works the same way as in Python, with a similar set of standard escape sequences.
Conditionals
if/else
if/else works the same way as in Python, but it's else if instead of elif, the conditions are enclosed in parentheses, and the bodies are enclosed in curly braces:
val age =42if (age <10) {
println("You're too young to watch this movie")
} elseif (age <13) {
println("You can watch this movie with a parent")
} else {
println("You can watch this movie")
}
The curly braces around a body can be omitted if the body is a oneliner. This is discouraged unless the body goes on the same line as the condition, because it makes it easy to make this mistake, especially when one is used to Python:
if (age <10)
println("You're too young to watch this movie")
println("You should go home") // Mistake - this is not a part of the if body!
Without the curly braces, only the first line is a part of the body. Indentation in Kotlin matters only for human readers, so the second print is outside the if and will always be executed.
An if/else statement is also an expression, meaning that a ternary conditional (which looks like result = true_body if condition else false_body in Python) looks like this in Kotlin:
val result =if (condition) trueBody else falseBody
When using if/else as an expression, the else part is mandatory (but there can also be else if parts). If the body that ends up being evaluated contains more than one line, it's the result of the last line that becomes the result of the if/else.
Comparisons
Structural equality comparisons are done with == and !=, like in Python, but it's up to each class to define what that means, by overridingequals() (which will be called on the left operand with the right operand as the parameter) and hashCode(). Most built-in collection types implement deep equality checks for these operators and functions. Reference comparisons - checking if two variables refer to the same object (the same as is in Python) - are done with === and !==.
Boolean expressions are formed with && for logical AND, || for logical OR, and ! for logical NOT. As in Python, && and || are short-circuiting: they only evaluate the right-hand side if it's necessary to determine the outcome. Beware that the keywords and and or also exist, but they only perform bitwise operations on integral values, and they do not short-circuit.
There are no automatic conversions to boolean and thus no concept of truthy and falsy: checks for zero, empty, or null must be done explicitly with == or !=. Most collection types have an isEmpty() and an isNotEmpty() function.
when
The when expression has similarities with pattern matching introduced in Python 3.10. It lets you compare one expression against many kinds of expressions in a very compact way (but it's not a full functional-programming-style pattern matcher). For example:
val x =42when (x) {
0->println("zero")
in1..9->println("single digit")
else->println("multiple digits")
}
Collections
Arrays in Kotlin have a constant length, so one normally uses lists, which are similar to the ones in Python. What's called a dict in Python is called a map in Kotlin (not to be confused with the function map()). List, Map, and Set are all interfaces which are implemented by many different classes. In most situations, a standard array-backed list or hash-based map or set will do, and you can easily make those like this:
val strings =listOf("Anne", "Karen", "Peter") // List<String>val map =mapOf("a" to 1, "b" to 2, "c" to 3) // Map<String, Int>val set =setOf("a", "b", "c") // Set<String>
(Note that to is an infix function that creates a Pair containing a key and a value, from which the map is constructed.) The resulting collections are immutable - you can neither change their size nor replace their elements - however, the elements themselves may still be mutable objects. For mutable collections, do this:
val strings =mutableListOf("Anne", "Karen", "Peter")
val map =mutableMapOf("a" to 1, "b" to 2, "c" to 3)
val set =mutableSetOf("a", "b", "c")
You can get the size/length of a collection c with c.size (except for string objects, where you for legacy Java reasons must use s.length instead).
Unfortunately, if you want an empty collection, you need to either declare the resulting collection type explicitly, or supply the element type(s) to the function that constructs the collection:
val noInts:List<Int> =listOf()
val noStrings = listOf<String>()
val emptyMap = mapOf<String, Int>()
The types inside the angle brackets are called generic type parameters, which we will cover later. In short, it's a useful technique to make a class that is tied to another class (such as a container class, which is tied to its element class) applicable to many different classes.
Coming from Python, you might be used to creating lists that contain elements of different types. This is discouraged in Kotlin, except when dealing with polymorphic types. In many cases, such as when returning multiple values from a function, the differently-typed values represent different kinds of information; it would then be better to create a data class with named properties of the appropriate types, or to use the per-element-typed Pair or Triple instead. However, if you really need to, you can put anything inside listOf() and the other collection creation functions. Kotlin will then infer the "lowest common denominator" supertype of the types of the given values, and you'll get a list of that element type. If the values have nothing in common, the element type will be Any, or Any? if one or more of the values are null:
If you need a collection with a more general type than the values you are initializing it with, you can specify the type like this: listOf<Number>(1, 2, 3).
Loops
for
Kotlin's loops are similar to Python's. for iterates over anything that is iterable (anything that has an iterator() function that provides an Iterator object), or anything that is itself an iterator:
val names =listOf("Anne", "Peter", "Jeff")
for (name in names) {
println(name)
}
Note that a for loop always implicitly declares a new read-only variable (in this example, name) - if the outer scope already contains a variable with the same name, it will be shadowed by the unrelated loop variable. For the same reason, the final value of the loop variable is not accessible after the loop.
In every iteration, the type of the loop variable is the same as the element type of the iterable, even if you are iterating over a mixed-type list. With for (x in listOf("a", 2, 3.14)), the type of x will always be Any, and you'll need to cast it in order to perform any operations that depend on knowing the "real" type. This is one of the reasons that mixed-type lists are usually only useful with polymorphic types, where the common supertype defines operations that are applicable to all the subtypes. In the example below, Number is a supertype of both Int and Double; it defines toDouble(), which converts the number to a Double, which can be multiplied. It would not work to simply write x * 2.
for (x in listOf<Number>(2, 3.14)) {
println(x.toDouble() *2)
}
You can create a range with the .. operator - but beware that unlike Python's range(), it includes its endpoint:
for (x in0..10) println(x) // Prints 0 through 10 (inclusive)
请发表评论