Switching from Java Lang to Kotlin, reading and understanding Kotlin, can be challenging for the first time. The very concise nature of the syntax used by Kotlin can be irritating and off-putting. It might even lead to not wanting to try Kotlin. In my opinion, this should not be the case. Little insights and tips can help overcome the first barrier. I am convinced that even the most hardened Java programmers can understand Kotlin easily. This article aims to clarify most of the strange syntax and features of Kotlin, which you will probably encounter the most. You might still not like Kotlin syntax, but at least you won’t have a hard time reading it.
Type declaration
val name1: String = "John" val name2 = "John" name2 = "Eric" // COMPILATION ERROR var name3 = "Jane" name3 = "Janice" fun greet(name: String): String { return "Hello " + name }
You declare the type of a variable and functions after the variable name with a colon. You declare a variable with val (for final variables) or var (for non-final variables). Most of the time the type is inferred and you do not need to declare the type explicitly.
String templates
fun greet1(name: String) : String { return "Hello $name" } fun greet2(name: String) : String { return "Hello ${name.uppercase()}" } fun greet3(name: String) : String { return "Hello $name.uppercase()" } println(greet1("John")) // Hello John println(greet2("John") // Hello JOHN println(greet3("John")) // Hello John.uppercase()
You can use variables in building strings using the dollar sign. Add curly braces for invocations.
Nullability
fun greet1(name: String): String {<br>return "Hello $name"<br>} fun greet2(name: String?): String { return "Hello $name" // COMPILATION ERROR } fun greet3(name: String?): String {= return "Hello ${name?.uppercase()}" } fun greet4(name: String?): String { return "Hello ${name!!.uppercase()}" } fun greet5(name: String?): String { return "Hello ${name?.uppercase() ?: "Jane Doe"}" } greet1(null) // COMPILATION ERROR greet2(null) // COMPILATION ERROR in method greet3(null) // Hello null greet4(null) // NullPointerException at runtime greet5(null) // Hello Jane Doe
You will see a lot of question marks. Kotlin has normal types and nullable types. For every type, there exists a nullable type. Like String
and String?
. The first type can never be null and the second type can be a String or null. Nullable types have to be checked. This can be done by adding? at the end of the type declaration.
If you are daring and you are sure the nullable type can never be null, use the double exclamation mark operator „!!“.
The elvis operator ?: states what has to be done if the value is null.
Lambdas with curly braces
fun greet(name: String, greeting: (String) -> String): String { return greeting(name) } greet("John", { x -> "Hello $x" }) greet("John") { x -> "Hello $x" } greet("John") { "Hello $it" }
- You can define functions as parameters. If the function is the last argument, you can pull this lambda out of the parentheses. Both examples are the same.
- If the lambda takes one argument, you can exchange it for a special keyword ‚it‘
- A lambda is always defined inside curly braces.
Lists and the keyword it
val numbers = listOf(1, 2, 3, 4, 5) numbers.filter({ n -> n % 2 == 0 }) numbers.filter { n -> n % 2 == 0 } numbers.filter { it % 2 == 0 }
Here is an example of the same filter, but written more concisely each time. The last one is the recommended use.
- There is no need for .stream(). You can call .map() or .filter() on a Collection directly
- Because the last parameter of map, filter, etc is the lambda, you can take out of the normal braces
- The special keyword ‚it‘ can also be used
- There is no need for .collect() or .toList() at the end of a stream
Extension functions
You can use one of the parameters of a method as a receiver of the method. These methods are called extension functions.
Instead of
fun add(x: Int, y: Int): Int { return x + y }
you can do
fun Int.add(y: Int): Int { return this + y }
you can call the last function as
3.add(5) // returns 8
Extension functions are used a lot. If done well, it enhances readability. The need for Util-classes is minimized.
Infix function
If the function is an infix function, then you can invoke the function without a dot and without braces. Like so
infix fun add(x: Int, y: Int): Int { return x + y } 3 add 5 // returns 8
Function body as expression body
If the function body is an expression, you can remove the curly braces and replaces them with an equal sign. You can also remove the return type.
fun add(x: Int, y: Int): Int { return x + y }
can be written as:
fun add(x: Int, y: Int) = x + y
Constructors
The constructor is defined in parentheses in the class header. This is called the primary constructor. You can also define other constructors in the class body.
class Person constructor(firstName: String, lastName: String ) {}
If the primary does not have any modifiers or annotations the keyword constructor can be omitted.
class Person(firstName: String, lastName: String) {}
Declare properties directly in the constructor by adding val or val in front of the declaration.
class Person(val firstName: String, lastName: String) {} fun main() { val person = Person("John", "Doe") val nameJohn = person.firstName<br>println(nameJohn) // prints "John" }
Accessing and encapsulating properties
class Address() { var street: String = "Baker Street" var city: String = "London" get() = field.toUpperCase() // field is a special keyword var zipcode: String = "12345" set(value) { field = "UK" + value } } fun main() { val address = Address() println(address.street) // prints "Baker Street" println(address.city) // prints "LONDON" address.street = "Another street" println(address.street) // prints "Another street" println(address.zipcode) // prints "12345" address.zipcode = "567" println(address.zipcode) // prints "UK567" }
You can access and set properties just by calling their variable name. Use set() and get() to add further features for getting and setting properties.
Scoping functions
Scoping functions have the purpose of executing a block of code within the context of an object.
let
The context object is available as an argument (it
). The return value is the lambda result.let
can be used to invoke one or more functions on the results of call chains.
Person("Alice", 20, "Amsterdam").let { println(it) it.moveTo("London") it.incrementAge() println(it) }
apply
val adam = Person("Adam").apply { age = 20 // same as this.age = 20 city = "London" }
with
Use with
for calling functions on the context object without providing the lambda result. In the code, with
can be read as „with this object, do the following.„
val numbers = mutableListOf("one", "two", "three") with(numbers) { println("'with' is called with argument $this") println("It contains $size elements") }
run
run
does the same with
but invokes as let
– as an extension function of the context object. run
is useful when your lambda contains both the object initialization and the computation of the return value.
val service = MultiportService("https://example.kotlinlang.org", 80) val result = service.run { port = 8080 query(prepareRequest() + " to port $port") } // the same code written with let() function: val letResult = service.let { it.port = 8080 it.query(it.prepareRequest() + " to port ${it.port}") }
also
also
is good for performing some actions that take the context object as an argument. Use also
for actions that need a reference to the object rather than its properties and functions, or when you don’t want to shadow the this
reference from an outer scope.
val numbers = mutableListOf("one", "two", "three") numbers .also { println("The list elements before adding new one: $it") } .add("four")
Quellen
- Kotlin, https://kotlinlang.org/
- Baeldung Lambda Expression, https://www.baeldung.com/kotlin/lambda-expressions