Kotlin is a statically typed, fluent & elegant programming language that compiles to Java or JavaScript. For a very quick start to Kotlin follow these links -
This document is split into 7 parts -
- Introduction to Kotlin
- Object Oriented Kotlin
- Functional Kotlin
- Fluent, Elegant & Efficient Kotlin
- Async Programming using Kotlin
- Interoperability with Java
- Metaprogramming & some Useful Constructs
My takeaways from Kotlin till now when compared to Java -
Key features:
- null-safe language - usage of nullable type - safe-call & elvis operators
- No checked exception - Reduces clutter in code
- Extension Functions
- Creating DSLs using
infix
notation & lambda extensions- Delegation
- Coroutines
Some more useful features:
- Object destructuring - Con is position based destructuring compared to JS where it is name based.
- Using
data
class accompanied by copy() method- Smart casting
- Using default & named parameters in functions
In this section we will learn about some basic constructs about Kotlin. IntelliJ or Eclipse already have the Kotlin compiler so don't need to download it separately if we are using any of these IDEs.
-
kotlinc
is the Kotlin Compiler. On Mac, Kotlin can be installed using brew which internally installskotlinc
by default under/usr/local/bin/kotlinc
-brew install kotlin ~ %which kotlinc /usr/local/bin/kotlinc
-
Once
kotlinc
is installed we can run Kotlin scripts on REPL or we can directly write programs and execute them. We'll take a look at both of them.
-
Some examples of using the Kotlin REPL from Terminal -
~ %kotlinc Welcome to Kotlin version 1.3.72 (JRE 1.8.0_251-b08) Type :help for help, :quit for quit >>> 2 + 2 res0: kotlin.Int = 4 >>> "Hello" + "World" res1: kotlin.String = HelloWorld >>> fun greet(name: String) {println("Hello $name")} >>> greet("Arunav") Hello Arunav >>> fun multiLineGreet(name: String){ ... println("Hello $name ! Hope you are doing great !") ... } >>> multiLineGreet("Sanjoy") Hello Sanjoy ! Hope you are doing great ! >>> :quit ~ %
-
Kotlin REPL can be run from IntelliJ Kotlin REPL, which provides syntax highlighting and suggestions for ease of use in REPL.
-
Below is a sample kotlin program
Basics.kt
fun greet(names: List<String>){ print("Welcome ") for (name in names) print(name + ", ") println("to the world of Kotlin !") } fun main(){ greet(listOf("Arunav", "Kaushik", "Sanjoy")) }
-
In order to compile and execute the program as Java bytecode, we need to compile it using
kotlinc-jvm
and then run askotlin
orjava
as shown below -~ %kotlinc-jvm Basics.kt ~ %ls BasicsKt.class META-INF ~ %kotlin Basics Welcome Arunav, Kaushik, Sanjoy, to the world of Kotlin !
-
Unlike languages like Java, Kotlin doesn’t require a statement or expression to belong to a method and a method to belong to a class, at least not in the source code we write. When the code is compiled, or executed as script, Kotlin will create wrapper classes and methods as necessary to satisfy the JVM expectations.
-
Immutable variables are defined as
val
and mutable variables are defined asvar
-
The type is automatically determined by Kotlin if not specified
var name="Arunav" var age=Int age = 20 name = "Madhuri" val address=String address = "Phoenix, Arizona"
-
Structural Equality:
equals()
method in Java, or==
operator in Kotlin, is a comparison of values, called structural equality. -
Referential Equality:
==
operator in Java, or===
in Kotlin, is a comparison of references, called referential equality. Referential equality compares references and returns true if the two references are identical—that is, they refer to the same exact instance.println("hi" == "hi") // true println("hi" == "Hi") // false println(null == "hi") // false println("hi" == null) // false println(null == null) // true
-
In Kotlin everything is an object.
-
The different data types in Kotlin are - Numbers, Characters, Booleans, Arrays, Unsigned integers, Strings.
-
There is no implicit type conversion. Using helper functions we can do type conversion.
-
There are different literal constants for integral values for -
- Decimal (123)
- Long (123L)
- Hexadecimal (0x0F)
- Binary (0b00001011)
- Double (123.5)
- Float (123.5f)
-
We can use underscores in numerical literals to make it more readable
val oneMillion = 1_000_000
-
Similar to Java,
Char
and is written inside single quotes on the other handString
is written inside double quotes. -
Multi-line
String
can be written inside a pair of 3 double quotes """val multiLineString = """ Hello there, How are you ? """
-
String literals can contain template expressions, i.e. pieces of code that are evaluated and whose results are concatenated into the string. A template expression starts with a dollar sign ($) and consists of either a simple name:
val person = "Name: $name; Age: $age; Address: $address"
-
Traditional
for
,while
,do..while
loops are supported in Kotlin -
break
andcontinue
also are supported -
We can use ranges inside for loops
..
: IncrementingdownTo
: Decrementingstep
: no. of steps increment or decrement can happen
-
For skipping values without a particular rythm we can use the
filter()
methodval oneToFive: IntRange = 1..5 // IntRange for (i in oneToFive) { println(i) } val aToe: CharRange = 'a'..'e' //CharRange for (i in aToe) { println(i) } // ClosedRange - range of Strings val seekHelp: ClosedRange<String> = "hell".."help" println(seekHelp.contains("helm")) // true println(seekHelp.contains("hello")) // true println(seekHelp.contains("helo")) // true println(seekHelp.contains("hels")) // false for (i in 1..10) { println(i) } for (i in 1..10 step 3) { println(i) } for (i in 10 downTo 1 step 2) { println(i) } // Using filters in ranges for (i in (1..12).filter { it % 3 == 0 || it % 5 == 0 }) { println(i) //3, 5, 6, 9, 10, 12 }
-
We can use label to break out of all the nested loops
-
In Kotlin,
if
can be used as a statement or an expression. When used as expression it returns a value and hence there is no separate ternary operator. -
Instead of switch statements, we have
when
statements in Kotlin. default case is referred aselse
. -
When
if
andwhen
are used as expression, theelse
is mandatory.fun whenStatement(str: String) { when(str){ "Red" -> println("The color code is 4") "Blue" -> println("The color code is 8") else -> println("Couldn't find the color code") } } fun whenExpression(dayOfWeek: Any) = when (dayOfWeek) { "Saturday", "Sunday" -> "Relax" in listOf("Monday", "Tuesday", "Wednesday", "Thursday") -> "Work hard" in 2..4 -> "Work hard" "Friday" -> "Party" is String -> "What?" else -> "No clue" } fun ifConditions() { val a = 30 val b = 20 val result = if (a > b){ "a is greater than b" } else { "b is greater than a" } println(result) }
-
By default Kotlin imports a number of packages.
-
Depending on target platform, additional packages are imported for JVM and JS.
-
The syntax for specifying a package and importing from a package are similar to what we have in Java.
-
A source file may start with a package declaration.
-
Apart from the default imports, each file may contain its own import directives.
-
If there is a name clash, we can disambiguate by using
as
keyword to locally rename the clashing entity.import org.example.Message // Message is accessible import org.test.Message as testMessage // testMessage stands for 'org.test.Message'
In this section, we'll be doing an introduction to functions in Kotlin
-
Functions in Kotlin are prefixed with
fun
-
Some return types in function -
Unit
: This is the default return type. Unit is kind of equivalent to void in other languages. But in kotlin we can check if the value of a variable isUnit
.Nothing
: Nothing is a return type when a function returns exception.Nothing
is substitutable for any class includingInt
,Double
,String
, etc.
-
In Kotlin, we can’t say
val
orvar
for parameters, they’re implicitlyval
, and any effort to change the parameters’ values within functions or methods will result in compilation errors. -
Single expression function doesn't need a function block
-
If a block body is assigned as a single expression function, Kotlin will treat that as a lambda expression or an anonymous function. Check out the below examples.
fun f1() = 2 fun f2() = { 2 } fun f3(factor: Int) = { n: Int -> n * factor } println(f1()) //2 println(f2()) //() -> kotlin.Int println(f2()()) //2 println(f3(2)) //(kotlin.Int) -> kotlin.Int println(f3(2)(3)) //6
-
We can pass default parameters to function arguments
-
The default argument doesn’t have to be a literal; it may be an expression. Also, you may compute the default arguments for a parameter using the parameters to its left.
-
In case of ambiguity in case of multiple parameters & default value being used in some cases, then the calling part of the function can use named parameters.
-
Also the ordering of the functions can be in any sequence when using named parameters
fun person(name: String, address: String = "", email: String = "$name${name.length}@kotlin.lang", phone: String) { println("Name=$name, Address=$address, Email=$email, Phone=$phone") }
-
When a function parameter is specified as
vararg
it means it can have unlimited number of parameters. -
In case of passing vararg to another function we can use the spread operator (*).
fun greetPeople(vararg names: String) { print("Welcome ") printNames(*names) println("to the world of Kotlin using vararg") } fun printNames(vararg names: String) { for (name in names) print("$name, ") }
We'll explore different types of collections in Kotlin, including
- Tuples - Pair & Triple
- Array
- List
- Set
- Map
-
Kotlin does not have its own collections. What it has is some interfaces on top of Java collections -
- Mutable
- Immutable
-
Collections are
- List
- Array (including equivalent primitive type)
- Set
- Map
- HashMap
- HashSet, etc...
-
Use Kotlin helper functions when working with Collections. It automatically determines which class to call. For example when defining an empty list using
emptyList<>()
, it useskotlin.collections.EmptyList
, whereas when defining usingArrays.asList()
it usesjava.util.Arrays$ArrayList
-
Tuples are sequences of objects of small, finite size.
-
Pair
is a tuple of size two andTriple
is a tuple of size three. -
Both
Pair
&Triple
are immutable. For mutability we can useArray
.println(Pair("Tom", "Jerry")) println(Triple("Tom", "Dick", "Harry"))
-
Example of Array
// Array of Strings val friends = arrayOf("Sanjoy", "Kaushik", "Ayon", "Debanjan") println(friends::class) println(friends.javaClass) println("${friends[0]}, ${friends[1]}, ${friends[2]}, ${friends[3]}")
-
Example of List
val fruits: List<String> = listOf("Apple", "Banana", "Grape") println(fruits) //[Apple, Banana, Grape] // Using the `index` operator to fetch from list println("${fruits[0]} == ${fruits.get(0)}") // Using the `in` operator to check if a value exists in list println("Apple" in fruits) println(fruits.contains("Apple")) // Same as the line above
-
Example of Set
val fruits: Set<String> = setOf("Apple", "Banana", "Apple") println(fruits) //[Apple, Banana]
-
The key-value pairs are created using the
to()
extension function, available on any object in Kotlin. -
Using
mapOf()
, that takes a vararg ofPair<K, V>
, we can create a map of key-values of different objects.val products = mapOf("PID1" to "Phone", "PID2" to "Watch", "PID3" to "Laptop") println(products) println(products.size) println(products.containsKey("PID1")) //true println(products.containsValue("Tablet")) //false println(products.containsValue("Watch")) //true println(products.contains("PID2")) //true - Checks if the key is present in the map println("PID3" in products) //true - Checks if the key is present in the map val unknownProduct: String? = products.get("PID4") // Using the get() method to fetch a value from map println("unknownProduct=$unknownProduct") val someProduct: String? = products["PID1"] // Using the index operator [] to fetch a value from map // Iterating over products for (product in products) println("${product.key} --> ${product.value}") // Iterating over the destructed map of products for ((pid, product) in products) println("$pid --> $product")
-
Common operations are available for both read-only and mutable collections. Common operations fall into these groups:
-
Some of the more frequently used operations are -
forEach
map
filter
reduce
flatMap
take
, etc...
- All classes in Kotlin extend from the base class
Any
similar toObject
class in Java. - Methods like
hashCode()
,equals()
andtoString()
are already implemented inAny
. - In addition to these,
Any
has extension functions liketo()
,let()
,run()
,apply()
,also()
.
-
When a function returns nothing, the corresponding return type in Kotlin is
Nothing
. There is nothing similar in Java. -
Nothing
actually returns nothing, not evenvoid
(orUnit
in Kotlin).Nothing
is literally deeper thanvoid
. -
Nothing
can be substituted for any class (Int
,Double
, etc). -
When used as a function return type that means that the function call will result in an exception.
fun computeSqrt(n: Double): Double { if(n >= 0) { return Math.sqrt(n) } else { throw RuntimeException("No negative please") } }
-
In the above example
if
returnsDouble
, but theelse
throws an Exception.
-
By default all objects in Kotlin is defined as not nullable unless specified otherwise. Every non-nullable Koltin object have a nullable counterpart. The nullable types have a
?
suffix. -
Kotlin does
null
check during compile time -- If the return type of a function is not defined as nullable and the function is returning
null
, the Kotlin code doesn't compile and throws an error. - If
null
is passed in a function parameter when the parameter in the function is defined as not nullable.
- If the return type of a function is not defined as nullable and the function is returning
-
Safe-Call
(?.)
Operator: Thenull
check and call to a method or property can be merged in a single step using the?.
operator. If the reference isnull
, the safe-call operator will result innull
, otherwise the result will be the result of the method call or property.fun getNickName(name: String?) : String? { return name?.reversed()?.toUppercase() }
-
Elvis
(?:)
Operator: If we want to return something instead ofnull
, when the reference isnull
, we can do that using the Elvis operator.fun getNickName(name: String?): String { return name?.reversed()?.toUppercase()?:"What's there in a name ?" }
-
Unsafe Assertion
(!!)
Operator: It is NOT advisable to use this operator. This operator tells Kotlin compiler that it doesn't have to do anynull
check on a particular reference during compile time. If that reference isnull
during runtime and we are calling a method or property on that reference then that will result in aNullPointerException
at runtime.fun getNickName(name: String?): String { return name!!.reversed().toUppercase() }
-
Smart Casts:
-
Using the
is
operator we can do type-checking in Kotlin. -
We can check whether an object conforms to a given type at runtime by using the
is
operator or its negated form!is
-
Once Kotlin determines the type during compile-time, Kotlin does a smart cast wherever possible.
-
The Kotlin compiler tracks
is
-checks and explicit casts for immuatable values and automatically inserts safe-castsfun demo(x: Any) { if (x is String) { print(x.length) // x is automatically cast to String } }
-
The compiler is smart enough to know a cast to be safe if a negative check leads to a return
if (x !is String) return print(x.length) // x is automatically cast to String
-
or in the right-hand side of
&&
and||
// x is automatically cast to string on the right-hand side of `||` if (x !is String || x.length == 0) return
-
Such smart casts work for when-expressions and while-loops as well
when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) }
-
-
Explicit Casts:
-
Similarly, once an object reference is not
null
, it can apply smart casts to automatically cast a nullable type to a non-nullable type, saving an explicit cast.fun whatToDo(dayOfWeek: Any) = when (dayOfWeek) { "Saturday", "Sunday" -> "Relax" in listOf("Monday", "Tuesday", "Wednesday", "Thursday") -> "Work hard" in 2..4 -> "Work hard" "Friday" -> "Party" is String -> "What?" else -> "No clue" }
-
Using the
as
andas?
operator we can do explicit type-casting in Kotlin. This might be required when Kotlin compiler is unable to do a smart cast. -
In Kotlin there is nothing as implicit casting. It either has to be a smart cast or an explicit cast.
-
- We can think of of objects from which we always read as Producers and the ones we write as Consumers. To remember this, we can use a mnemonic PECS (Producer -> extends and Consumer -> super).
- Let's say the class
Dog
is inherited from the classAnimal
. Any method that is expecting anAnimal
will be happy if we passDog
, but if we try to passMutableList<Dog>
whereMutableList<Animal>
is expected, Kotlin compiler will throw an error. This is true in case of JavaList<>
as well. The reason for this compilation error is because the list being a mutable one, we can add an instance of another animal, lets sayCat
in theMutableList<Animal>
. If aMutableList<Dog>
is passed toMutableList<Animal>
, then whenever aCat
is added to the list it will throw aClassCastException
at runtime, instead it is throwing the error at compile time itself. This is called type invariance, i.e., we cannot vary on the type, i.e.,List<Dog>
is not a subtype ofList<Animal>
.
-
Sometimes you want the compiler to permit covariance — that is, tell the compiler to permit the use of a derived class of a parametric type
T
in addition to allowing the typeT
, but without compromising type safety. In Java you use the syntax<? extends T>
to convey covariance, but there’s a catch. You can use that syntax when you use a generic class, which is called use-site variance or type-projection, but not when you define a class, which is called declaration-site variance. In Kotlin you can do both. -
The
out
modifier tells the Kotlin compiler that items can only be read out of the Generic class, and hence it allows derived classes to be passed as parameter by still maintaining type safety.fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) { for (i in 0 until from.size) { to[i] = from[i] } }
-
The
Array<T>
class has methods to both read out and set in an object of typeT
. Any function that usesArray<T>
may call either of those two types of methods. But using covariance, we’re promising to the Kotlin compiler that we’ll not call any method that sends in any value with the given parametric type onArray<T>
. This act of using covariance at the point of using a generic class is called use-site variance or type projection. -
Use-site variance is useful for the user of a generic class to convey the intent of covariance. But, on a broader level, the author of a generic class can make the intent of covariance — for all users of that class — that any user can only read out and not write into the generic class. Specifying covariance in the declaration of a generic type, rather than at the time of its use, is called declaration-site variance. A good example of declaration-site variance can be found in the declaration of the List interface — which is declared as
List<out T>
.
-
Other times you want to tell the compiler to allow contravariance — that is, permit a super class of a parametric type
T
where typeT
is expected. Once again, Java permits contravariance, with the syntax<? super T>
but only at use-site and not declaration-site. Kotlin permits contravariance both at declaration-site and use-site. -
In the above example, if we want to pass a
Fruit
or one of the derived classes ofFruit
into a collection ofFruit
, or any class that is collection of a base ofFruit
in theto
parameter, we can’t simply pass an instance ofArray<Any>
as an argument to theto
parameter. We have to explicitly ask the compiler to permit contravariance, i.e., to accept a parametric type of base where an instance of a parametric type is expected.fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) { for (i in 0 until from.size) { to[i] = from[i] } }
-
The only change was to the second parameter, what was to:
Array<Fruit>
is now to:Array<in Fruit>
. Thein
specification tells Kotlin to permit method calls that set in values on that parameter and not permit methods that read out. -
This again is a use-site variance, but this time for contravariance (
in
) instead of covariance (out
). Just like declaration-site variance for covariance, classes may be defined with parametric type<in T>
to universally specify contravariance, i.e., that type can only receive parametric types and can’t return or pass out parametric types.
-
Not only classes can have type parameters. Functions can, too. Type parameters are placed before the name of the function:
fun <T> singletonList(item: T): List<T> { // ... }
-
We can tell Kotlin to allow a particular type of parameter in a generic type
T
, by specifying it during the declaration, as shown in the below example -fun <T: AutoCloseable> useAndClose(input: T) { input.close() }
-
The function
useAndClose()
expects a parameteric typeT
as parameter, but only one that conforms to beingAutoCloseable
. For a single constraint this approach is fine, but if there are multiple constraints, this approach won't work and we need to usewhere
in that case. In the above example, in addition toAutoCloseable
if we want the parameterT
to conform toAppendable
, then we would need to usewhere
as shown below -fun <T> useAndClose(input: T) where T: AutoCloseable, T: Appendable { input.append("there") input.close() }
-
In Java we may use the
?
to specify that a function may receive a generic object of any type, but with a read-only constraint. We can also define raw types in Java, likeArrayList
. The equivalent in Kotlin is Star projection, defined as the parametric type with<*>
specifies both generic read-only type and raw type.fun printValues(values: Array<*>) { for (value in values) { println(value) } //values[0] = values[1] //ERROR } printValues(arrayOf(1, 2))
-
In the above example,
Array<*>
prohibits making any changes to the values array. Had it been declared asArray<T>
, then modification to the array would have been possible. -
The star projection
<*>
here, is equivalent toout T
, but is more concise to write. -
Star Projection Types
- For
Foo<out T : TUpper>
, whereT
is a covariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
. It means that when theT
is unknown you can safely read values ofTUpper
fromFoo<*>
. - For
Foo<in T>
, whereT
is a contravariant type parameter,Foo<*>
is equivalent toFoo<in Nothing>
. It means there is nothing you can write toFoo<*>
in a safe way whenT
is unknown. - For
Foo<T : TUpper>
, whereT
is an invariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
for reading values and toFoo<in Nothing>
for writing values.
- For
-
Some examples of Star Projection
Function<*, String>
meansFunction<in Nothing, String>
Function<Int, *>
meansFunction<Int, out Any?>
Function<*, *>
meansFunction<in Nothing, out Any?>
-
Star projections are very much like Java's raw types, but safe. It comes in handy when we don't know much about the type parameter, but still want it to be type-safe.
-
Similar to Java, at runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of
Foo<Bar>
andFoo<Baz?>
are erased to justFoo<*>
. -
These unchecked casts can be used when type safety is implied by the high-level program logic but cannot be inferred directly by the compiler. The compiler issues a warning on unchecked casts, and at runtime, only the non-generic part is checked (equivalent to
foo as List<*>
). -
The type arguments of generic function calls are also only checked at compile time. However, only in the case of inline functions, reified type parameters are substituted by the actual type arguments in the inlined function body at the call sites, during runtime, and can be used for type checks and casts, with the same restrictions for instances of generic types.
-
In the below examples, we'll see how reified type parameters will help reduce verbosity and clutter in the code in an inline function.
-
Without reified type parameters
fun <T> findFirst(books: List<Book>, ofClass: Class<T>): T { val selected = books.filter { book -> ofClass.isInstance(book) } if(selected.size == 0) throw RuntimeException("Not found") return ofClass.cast(selected[0]) }
-
With reified type parameters
inline fun <reified T> findFirst(books: List<Book>): T { val selected = books.filter { book -> book is T } if(selected.size == 0) throw RuntimeException("Not found") return selected[0] as T }
-
Reified type parameters are useful to reduce clutter and also to alleviate potential errors in code. Reified type parameters eliminate the need to pass extra class information to functions, help to write code with safe casts, and customize the return type of functions with compile-time safety.
-
However the point to note here is this works only in the case of inline function calls. For normal classes or functions, this isn't possible as type arguments are erased during runtime.
Kotlin is an object oriented language and it supports all the different features of object-oriented programming. In this section, we'll get started with the OO concepts in Kotlin.
-
In Kotlin we can directly create an object without creating a class.
-
Provides an easy way to create singletons.
-
Objects in an expression are initialized immediately, whereas object declarations are lazily instantiated.
-
We can add methods to objects as well, but in most of the scenarios we'll use a class to do this.
object Circle { // This is treated as an object declaration or singleton var radius = 2 } fun draw(){ var rectangle = object { // This is treated as object expression val length = 10 val breadth = 7 } println("Drawing Rectangle of length ${rectangle.length} and breadth ${rectangle.breadth}") println("Drawing Circle of radius ${Circle.radius}") }
-
With a slight change, anonymous objects can be useful as implementors of interfaces, i.e., as anonymous inner classes like in Java. Between the
object
keyword and the block{}
, we can mention the names of the interfaces we’d like to implement, comma separated. -
In this below example, the anonymous inner class (object) is implementing more than one interfaces -
Runnable
andAutoCloseable
, but returnsRunnable
as the return type.fun createRunnable(): Runnable = object: Runnable, AutoCloseable { override fun run() { println("I m running...") } override fun close() { println("I m closing...") } }
-
If the interface implemented by the anonymous inner class is a single abstract method interface (what Java 8 calls a functional interface), then we can directly provide the implementation without the need to specify the method name, like so:
fun createRunnable(): Runnable = Runnable { println("You called...") }
Top-level functions vs Singletons
- If a group of functions are high level, general, and widely useful, then placing them directly within a package as top-level functions make sense.
- If on the other hand, a few functions are more closely related to each other than the other functions, then place them within a singleton.
- If a group of functions needs to rely on state, you can place that state along with those related functions in a singleton, although a class may be a better option for this purpose.
- Caution: Placing mutable state in a singleton may cause issues in multithreaded applications.
-
class
:- Classes can be defined with or without any
{}
. - By default, access to a
class
, its members and itsconstructor
ispublic
. - To create instances of classes, we can simply call the Class using the className. There is no
new
keyword in Kotlin. - Classes have properties and not fields.
- Properties can be defined as
val
orvar
. - All properties needs to be initialized if they are not passed using constructor.
- If a parameter passed inside constructor is not defined as
val
orvar
, it can still be a parameter, but not a property of the class. - We can define member functions inside classes similar to how we defined functions. It will have access to all the properties inside the class.
- We can create custom getters and setters inside Kotlin classes by using the
get()
andset()
methods with each properties. field
is a special keyword in Kotlin that is used to set a value to a property.- The full syntax for declaring a property is
var <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>]
- Example of a Kotlin class
class Employee(val id: Int = Random.nextInt().absoluteValue, val name: String, val yearOfBirth: Int) { var age: Int = 0 get() = Calendar.getInstance().get(Calendar.YEAR) - yearOfBirth var ssn: String = "" set(value) { field = value } }
- Classes can be defined with or without any
Backing Field
In Kotlin we never define fields — backing fields are synthesized automatically, but only when necessary. If you define a field with both a custom getter and custom setter and don’t use the backing field using the
field
keyword, then no backing field is created. If you write only a getter or a setter for a property, then a backing field is synthesized.Since Kotlin synthesizes fields internally, it doesn’t give access to that name in code. We may refer to it using the keyword
field
only within getters and setters for thatfield
.
-
init
:- We can initialize properties inside an
init{}
block of code inside the class. - A class may have zero or more
init
blocks. - These blocks are executed as part of the primary constructor execution. The order of execution of the
init
blocks is top-down. - Within an
init
block we may access only properties that are already defined above the block. - Since the properties and parameters declared in the primary constructor are visible throughout the class,
any
init
block within the class can use them. But to use a property defined within the class, we’ll have to write theinit
block after the said property’s definition. - An ideal practise will be to first declare the properties at the top, then write one
init
block, but only if needed, and then implement the secondary constructors (again only if needed), and finally create any methods that may be needed.
class Car(val year: Int, var theColor: String) { var mileage: Int = 100 var color = theColor // Custom Setter for color property. We can give any name to the parameter of the setter (value) set(value) { if (value == "") throw Exception("ERROR: Color cannot be blank") field = value } init { if (year > 2020) { mileage = 0 } } override fun toString() = "year=$year, color=$color, mileage=$mileage " }
- We can initialize properties inside an
Additional Reading on Properties
Additional Reading on Access Modifiers
-
Constructor properties can have default or named parameters similar to functions.
-
Secondary constructors can be created by using the
constructor
keyword. -
If the class has a primary constructor, each secondary constructor needs to delegate to the primary constructor, either directly or indirectly through another secondary constructor(s) using the
this
keyword. -
Properties cannot be defined in secondary constructors. Hence
var
orval
is not allowed inside secondary constructors.class Person(val first: String, val last: String) { var fulltime = true var location: String = "-" constructor(first: String, last: String, fte: Boolean): this(first, last) { fulltime = fte } constructor(first: String, last: String, loc: String): this(first, last, false) { location = loc } override fun toString() = "$first $last $fulltime $location" }
-
inline
classes provides the benefit of classes at compile time, but we can also get the benefits of using a primitive at runtime — theinline
class is transformed into a primitive in the bytecode. -
inline
classes may have properties and methods and may implement interfaces as well. -
Under the hood, methods will be rewritten as static methods that receive the primitive types that are being wrapped by the inline class.
-
inline
classes are required to be final and aren't allowed to extend from other classes.inline class SSN(val id: String) { fun receiveSSN(ssn: SSN) { println("Received $ssn") } }
-
companion
objects are singletons defined within a class — they’re singleton companions of classes. -
Allows creating equivalent of static members in Java.
-
An
object
inside a class can be prefixed withcompanion
. This makes the functions inside the object accessible outside the class by without referencing using the object name. -
Each class can have only one single
companion
object, inside which we can multiple functions. -
By prefixing the functions inside the object with the annotation
@JvmStatic
, it becomes accessible to Java as static methods accessible by the classname. -
They may implement interfaces and may extend from base classes, and thus are useful for code re-usability.
-
The members of the companion object of a class can be accessed using the class name as reference.
-
We can access the companion of a class using
.Companion
on the class. The nameCompanion
is used only if the companion object doesn’t have an explicit name.companion object { var tires = 4 } val ref = Car.Companion // Referenced as .Companion companion object Wheels { var tires = 0 //... } val wheels = Car.Wheels
-
We can use companion object as a factory by providing a
private constructor
to the class. We can then provide one or more methods in the companion object that creates the instance and carries out the desired steps on the object before returning to the caller.class MachineOperator private constructor(val name: String) { fun checkin() = checkedIn++ fun checkout() = checkedIn-- companion object { var checkedIn = 0 fun minimumBreak() = "15 minutes every 2 hours" fun create(name: String): MachineOperator { val instance = MachineOperator(name) instance.checkin() return instance } } override fun toString() = "${checkedIn}" }
-
Caution: Placing mutable properties within companion objects may lead to thread-safety issues in multithreaded scenarios.
-
In order to reduce verbose code, Kotlin has
data
classes which automatically derives -equals()
/hashCode()
toString()
copy()
-
We can
override
and provide explicit implementations ofequals()
,hashCode()
ortoString()
in thedata
class body. -
Data classes must fulfill the following requirements -
- The primary constructor needs to have at least one parameter
- All primary constructor parameters need to be marked as
val
orvar
- Data classes cannot be abstract, open, sealed or inner
-
Data classes have
copy()
function, which can be used to copy an object altering some of its properties, but keeping the rest unchanged. -
It also creates special methods that start with the word
component
—component1()
,component2()
, and so on, to access each property defined through the primary constructor, generally referred to ascomponentN()
method. -
Any property defined within the class body
{}
, if present, will not be used in the generatedequals()
,hashCode()
, andtoString()
methods. Also, nocomponentN()
method will be generated for those. -
Unlike the other methods, the
copy()
method includes any property defined within the class, not just those presented in the primary constructordata class EmployeeData(var id: Int, var name: String, var email: String) { override fun toString(): String { return super.toString() } } fun main() { val e1 = EmployeeData(1, "Ram", "[email protected]") val e2 = EmployeeData(2, "Shyam", "[email protected]") val e3 = e1.copy(email = "[email protected]") }
Destructuring in Data Classes
- The main purpose of the
componentN()
methods is for destructuring. Any class, including Java classes, that hascomponentN()
methods can participate in destructuring.- To destructure, we have to extract the properties in the same order as they appear in the primary constructor. > >
kotlin > val (id, _, name) = employee > println("Employee Id=${id}, Employee Name=${name}") >
- The destructuring of data classes in Kotlin comes with a significant limitation. In JavaScript, object destructuring is based on property names, but, sadly, Kotlin relies on the order of properties passed to the primary constructor. If a developer inserts a new parameter in between current parameters, then the result may be catastrophic.
-
Enum classes are used for creating a bunch of enumerated values.
-
We can customize and iterate over them as well.
-
Each
enum
constant is an object. Enum constants are separated with commas. -
Enum constants can override base methods.
-
When enum class has any members, the enum constants must be separated from the member definitions with a semicolon.
enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState }
- Kotlin provides a way to define properties by not initializing them. This can be done by prefixing the property with
the keyword
lateinit
.
We'll extend the OO concepts in Kotlin by looking into Inheritance, Interfaces, Abstract classes & Generics.
-
Base class in Kotlin is
Any
, similar toObject
is Java. All classes inherits fromAny
. -
By default all classes are
final
. In order to make a class non-final we need to make itopen
. -
Similarly members are also by default
final
. In order to override in the child class, we need to make the methodopen
in the parent class. -
data
classes can also be inherited in Kotlin.open class Person() { open fun validate() { println("Validating in Person") } } open class Customer : Person { final override fun validate() { println("Validating in Customer") } constructor() : super() { println("Inside Customer constructor") } } data class SpecialCustomer(var id: Int) : Customer()
-
Similar to Java 8 onwards.
-
Can have default implementations.
-
Can define abstract properties but cannot have a state for it.
-
However, we can define custom getters and setters on interface properties. But properties inside interfaces doesn't have a backing
field
. -
We can use
companion
objects to createstatic
methods in interfaces.interface CustomerRepo { // Interfaces cannot maintain state, but can have custom getters & setters var isEmpty : Boolean get() = true set(value){ println("Doing something with the value $value") } // This is a default implementation fun load(obj: Customer){ println("Loading Customer Data") } // Implementing class needs to override this method fun getById(id: Int) : Customer }
-
When a class implements multiple interfaces, say
A
andB
, and both have a method with the same name, sayfoo()
, then Kotlin provides a way for the implementing class to resolve the conflict by specifying the interfaces in Angular brackets <>.interface A { fun foo() { print("A") } fun bar() } interface B { fun foo() { print("B") } fun bar() { print("bar") } } class C : A { override fun bar() { print("bar") } } class D : A, B { override fun foo() { super<A>.foo() super<B>.foo() } override fun bar() { super<B>.bar() // We don't have to specify super<B> in this case as there is no implementation inside A for bar() } }
-
Similar to Java
abstract class AbstractEntity { // Having a state inside abstract class val isActive = true // Abstract method abstract fun load() // Default method fun status(): String { return isActive.toString() } } class EmployeeEntity : AbstractEntity() { override fun load() { println("Loading EmployeeEntity..") } }
Difference between Abstract Classes and Interfaces:
In both abstract classes and interfaces, we can define abstract methods as well as provide default implementation of some methods. But below are the major differences between them -
- In abstract classes we can maintain state, but in interfaces we cannot. We can define properties inside an interface, but we cannot maintain value or state inside it.
- Secondly we can have a class that implements multiple interfaces, but it can extend only one class.
In short, when we want to reuse state between multiple classes, then abstract class is a good choice, and on the other hand, if we want classes to choose their own implementations, interfaces is a better option.
-
Similar to Java
interface Repository<T> { fun getById(id: Int): T fun getAll(): List<T> fun<U> getAddDataById(id: Int) : U } class GenericRepo<T> : Repository<T> { override fun getById(id: Int): T { println("Getting by id = $id ") throw UnsupportedOperationException("Not implemented") } override fun getAll(): List<T> { println("Fetching all") throw UnsupportedOperationException("Not implemented") } override fun <U> getAddDataById(id: Int): U { println("Getting additional data by Id") throw UnsupportedOperationException("Not implemented") } } fun main() { val customerRepo = GenericRepo<Customer>() val employeeRepo = GenericRepo<EmployeeEntity>() customerRepo.getById(10) employeeRepo.getAll() employeeRepo.getAddDataById<EmployeeAddData>(22) }
-
Kotlin supports nested class.
-
Nested classes can be accessed outer class by using the OuterClass.InnerClass notation both in Java and Kotlin.
-
Unlike in Java, Kotlin nested classes can’t access the private members of the nesting outer class. But if we mark the nested class with the
inner
keyword, then they turn into inner classes and the restriction goes away.class _TV { private var volume = 0 val remote: Remote get() = _TVRemote() override fun toString() = "Volume : ${volume}" inner class _TVRemote : Remote { override fun up() { volume++ } override fun down() { volume--; } // Inner class overriding the outer class method. override fun toString() = "Remote: ${this@_TV.toString()}" } }
-
A user of a
_TV
instance can obtain a reference to aRemote
for the_TV
instance using theremote
property. -
If a property or method in the inner class shadows a corresponding member in the outer class, we can access the member of the outer class from within a method of the inner using a special
this
expression (this@_TV
can be read as "this
of_TV
", i.e.,this
will refer to the_TVRemote
, whilethis@_TV
will refer to the instance of the outer class_TV
). -
Similar to
this@_TV
, we can refer to a method in the base class of the outer class usingsuper@OuterClass
, but we should try to avoid this because it is a design smell, by by-passing the outer class to get from base class. -
The
inner
keyword is not required for anonymous inner classes. We can use thecompanion
objects in this case.
- Only classes marked
open
may be inherited from. Onlyopen
methods of anopen
class may be overridden in a derived class and have to be marked withoverride
in the derived. - A method that isn’t marked
open
oroverride
can’t be overridden. An overriding method may be markedfinal override
to prevent a subclass from further overriding that method. - We can override a property, either defined within a class or within the parameter list of a constructor.
- A
val
property in the base may be overridden with aval
orvar
in the derived. But avar
property in the base may be overridden only usingvar
in the derived. - We can make a
private
orprotected
memberpublic
in the derived, but we can’t make apublic
member of baseprotected
in the derived.
-
sealed
classes are used for representing restricted class hierarchies, where a value can have one of the types from a limited set, but cannot have any other type. -
A class can be made
sealed
by prefixing it with thesealed
keyword. -
A
sealed
class is abstract by itself. It cannot be instantiated directly and can have abstract members. -
We can't instantiate an object of a sealed class, but we can create objects of classes that inherit from
sealed
classes. -
sealed
classes are open for extension by other classes defined in the same file but closed — that is,final
or notopen
for any other classes. -
The constructors of
sealed
classes aren’t markedprivate
, but they’re consideredprivate
. -
sealed
classes are not allowed to have non-private constructors (their constructors are private by default). -
We may also derive singleton
objects
fromsealed
classes.sealed class Card(val suit: String) class Ace(suit: String) : Card(suit) class King(suit: String) : Card(suit) { override fun toString() = "King of $suit" } class Queen(suit: String) : Card(suit) { override fun toString() = "Queen of $suit" } class Jack(suit: String) : Card(suit) { override fun toString() = "Jack of $suit" } class Pip(suit: String, val number: Int) : Card(suit) { init { if (number < 2 || number > 10) { throw RuntimeException("Pip has to be between 2 and 10") } } }
Some Tips
- The key benefit of using
sealed
classes comes into play when we use them in awhen
expression. If it's possible to verify that the statement covers all cases, we don't need to add anelse
clause to the statement. However, this works only ifwhen
is used as an expression (using the result) and not a statement.- Kotlin provides an auto-backing field for cases where we want to "store" information.
- We can use
typealias
for providing better semantics to other data types.
In delegation, two objects are involved in handling a request: a receiving object delegates operations to a delegate object. Kotlin inherently provides support for Delegation. In this section we'll take a look at it.
Inheritance vs Delegation:
- We should be using inheritance if we want an object of a class to be used in place of an object of another class.
- We should be using delegation if we want an object of a class to simply make use of an object of another class.
-
Kotlin requires the left side of the
by
to be aninterface
. The right side is an implementor of thatinterface
. -
In the below example,
Manager
class routes all calls towork()
andtakeVacation()
methods toJavaProgrammer
which was specified when defining theManager
class.interface Worker{ fun work() fun takeVacation() } class JavaProgrammer : Worker { override fun work(){ println("Writing Java Code..") } override fun takeVacation(){ println("Writing Java Code in vacation..") } } class Manager() : Worker by JavaProgrammer()
-
In the previous example, the
JavaProgrammer
is tightly coupled with theManager
class. Kotlin delegates also provides the flexibility to pass a parameter or property to which the called class can delegate to. Let's take a look at the below example -class Manager(val staff: Worker) : Worker by staff { fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}") }
-
The constructor of the
Manager
class receives a parameter namedstaff
which also serves as a property, due toval
in the declaration. -
If the
val
is removed,staff
will still be a parameter, but not a property of the class. -
Irrespective of whether or not
val
is used, theclass
can delegate to the parameterstaff
.
-
A delegating class if it decides to implement one of the methods from the delegate class, it can do so. So when that particular method is called on the delegating class, then the method on the delegating class will be called.
-
In case a delegating class is implementing multiple interfaces, and if there is a method collision in the two interfaces, then the delegating class must override that particular colliding method inside the class. If the method is not overridden inside the delegating class then we'll get a compilation error.
-
The delegating
class
implements the delegatinginterface
, so a reference of the delegatingclass
may be assigned to a reference of the delegatinginterface
. Likewise, a reference of a delegatingclass
may be passed to methods that expect a delegateinterface
.val manager = Manager(JavaProgrammer()) val coder : JavaProgrammer = manager // ERROR: type mismatch val employee : Worker = manager // This is OK
-
Kotlin doesn't delegate to a property of an object but to the parameter passed to the primary constructor. If we change the property that's used as delegate from
val
tovar
, the behavior is going to be different. Let's look at the following example -interface Worker { fun work() fun takeVacation() fun fileTimeSheet() = println("Why? Really?") } class JavaProgrammer : Worker { override fun work() = println("Write Java") override fun takeVacation() = println("...code at the beach...") } class KotlinProgrammer : Worker { override fun work() = println("Write Kotlin") override fun takeVacation() = println("...branch at the ranch...") } class Manager(var staff: Worker) : Worker by staff val manager = Manager(JavaProgrammer()) println("Staff is ${manager.staff.javaClass.simpleName}") // Staff is JavaProgrammer manager.work() // Write Java //Changing staff manager.staff = KotlinProgrammer() println("Staff is ${manager.staff.javaClass.simpleName}") // Staff is KotlinProgrammer manager.work() // Write Java
- The delegate at the far right in
Manager
class declaration is a parameter and not a property. It is taking a parameterstaff
and assigning it to a member namedstaff
(like,this.staff=staff
). So there are two references to the given object - one held inside the class as backing field and one held for the purpose of delegation. When we changed the propertystaff
to an instance ofKotlinProgrammer
, though, we only modified the field, but not the reference to the delegate. - When we replaced the property
staff
withKotlinProgrammer
, the originally assignedJavaProgrammer
is no longer attached to the object as its property. But it can't be garbage collected as well because the delegate holds on it. Thus a delegate's lifetime is same as object's lifetime, though properties may come and go.
- The delegate at the far right in
- When delegating properties, the delegated class needs to implement two functions
getValue()
andsetValue()
.
For knowing more about the property type passed in the
getValue()
andsetValue()
methods, visit this part after learning reflection in Kotlin.
-
lazy
:- Deferring creation of objects or executing computations until the time the result is truly needed. The
lazy
function takes as argument a lambda expression that will perform the computation, but only on demand and not eagerly or immediately. - The
lazy
function by default synchronizes the execution of the lambda expression so that at most one thread can execute it.
- Deferring creation of objects or executing computations until the time the result is truly needed. The
-
observable
:- This is useful to observe or monitor changes to the value of a property.
- The singleton object
kotlin.properties.Delegates
has anobservable()
convenience function to create aReadWriteProperty
delegate that will intercept any change to the variable or property it’s associated with. When a change occurs, the delegate will call an event handler you register with theobservable()
function. - The event handler receives three parameters - type
KProperty
which hold the metadata about the property, the old value, and the new value. It doesn’t return anything, i.e., it’s aUnit
orvoid
function.
-
vetoable
:- Unlike the handler registered with
observable
, whose return type isUnit
, the handler we register withvetoable
returns aBoolean
result. - A return value of
true
means a favorable nod to accept the change;false
means reject. The change is discarded if we reject. - Is used to reject changes to properties based on some rules or business logic.
- Unlike the handler registered with
We'll get functional in this section. Learn about some key concepts how Kotlin supports functional programming.
- A higher order function is a function that takes a function as an argument, or return a function.
-
Passing Lambdas to higher-order functions: In Kotlin, lambda expressions are expressed with "
->
", similar to Java. In the below example2 until n
returns anIntRange
class. The class has a methodnone()
that takes aPredicate
and returnsfalse
if thePredicate
is evaluated astrue
and vice-versa.fun isPrime(n: Int) = n==1 || (n > 1 && (2 until n).none({ i: Int -> n % i == 0 }))
-
Implicit Parameter -
it
: For single parameter in a lambda expression, we don't have to explicitly define the parameter in the lambda expression and instead, we can refer to the parameter asit
in the body of the expression. Let's see an example -someFunc(3, {x -> x*x}) someFunc(3, {it * it})
-
Receiving Lambdas: Let's take a look at a function that takes a lambda as a parameter.
fun actionFromOneTo(action: (Int) -> Unit, n: Int) = (1..n).forEach {action(it)}
-
Lambda as Last Parameter - Dropping the brackets: When the last or the only parameter of a function is a function, then when calling the higher-order function, the function parameter can be passed outside the parentheses of the function call
operation(5) { it * it }
-
Function References: In Kotlin, method references are denoted as
::
, similar to Java. We can pass a method reference or a lambda expression to a function that accepts a function as an argument. -
Function returning functions: A function returning a function is also a higher-order function. This might come in handy when we have multiple lambdas with same functionality but varies on some parameter. We can extract that particular lambda and return it from another function, but provide some parameter on which that lambda will work on. Let's look at the following function -
fun predicateOfLength(length: Int): (String) -> Boolean { return { input: String -> input.length == length } } println(names.find(predicateOfLength(5))) println(names.find(predicateOfLength(4)))
-
We can pass functions with no names as a parameter to higher-order functions. These are called anonymous functions.
-
Instead of lambdas we can use anonymous functions. But because of its verbosity it should be avoided in most cases, except for some cases which we'll discuss in following sections.
-
The
return
keyword is required for block-body anonymous functions that returns a value. -
The
return
will always return from the anonymous function and not from the encompassing function.val checkLength5 = fun(name: String): Boolean { return name.length == 5 }
-
A lambda is stateless; the output is dependent on the inputs parameters.
-
A lambda expression or anonymous function (as well as a local function and an object expression) can access its closure, i.e. the variables declared in the outer scope, also known as lexical scoping.
-
Sometimes we want to depend on external state. Such a lambda is called a closure — that’s because it closes over the defining scope to bind to the properties and methods that aren’t local.
-
The variables captured in the closure can be modified in the lambda, but this should be avoided.
val factor = 2 val doubleIt = { e: Int -> e * factor }
Note
- In lambdas as well we can use object destructuring.
- Keep closure as pure functions to avoid confusion and to minimize errors
By default, lambdas aren’t allowed to have the return
keyword, even if they return a value. This is a significant
difference between lambdas and anonymous functions - the latter is required to have return if returning values and it
signifies only a return from the immediate lambda and not the outer calling function.
-
In order to return from an enclosing lambda, we can use labeled
return
, i.e.,return@myLabel
wheremyLabel
is some label that we can create using syntaxmyLabel@
. -
We can also use implicit label, i.e., name of the function to which the lambda is passed.
-
Prefer explicit labels over implicit as it makes the intention clear and code more readable.
-
In the below example, the label
myLabel
behaves as acontinue
statement as in case of imperative programming.fun caller() { (1..5).forEach { i -> invokeWith(i) myLabel@{ if (it == 2) { return@myLabel // Return without a label will not be allowed here } } } }
-
Non-local
return
is useful to break out of the current function that's being implemented, right from within a lambda. -
Non-local returns, i.e.,
return
from a lambda function is allowed only when the function in which lambda expression is invoked is aninline
function. In the below exampleforEach()
is aninline
function.fun containingFunction(){ val numbers = 1..100 numbers.forEach{ if(it % 5 == 0) return } println("After forEach") }
Recap:
return
is not allowed by default within lambdas.- We may use a labeled return to step out of the encompassing lambda.
- Use of non-local
return
to exit from the encompassing function being defined is possible only if the function to which the lambda is passed is defined withinline
.- When a local
return
is called from an anonymous function it returns to the enclosing function and not to the outer function.
- Functions prefixed with
inline
are inline functions. - When bytecode is generated for
inline
functions, the body of the function is copied to the place from where the actual function call is done. This reduces the call stack. - Performance can be improved for functions by making it
inline
, if they have another function as a parameter. inline
functions without function as parameters will not have performance improvement.- When an exception is thrown from an
inline
function the stacktrace doesn't show the function separately.
- A particular lambda-function parameter in
inline
function can be made non-inline by prefixing the parameter withnoinline
. - We can use the
noinline
keyword only on function parameters and the function is itself marked asinline
. - There will be no stack call optimization if the function parameter is marked as
noinline
. - We cannot reference the function parameter with another variable inside an
inline
function. If the function parameter is declared asnoinline
then this is possible.
-
By default the functions defined as parameters in an
inline
function is alsoinline
unless specified otherwise. -
Lets look at the following scenario -
An
inline
function is accepting a function (by defaultinline
) as a parameter, and is not invoking the passed-in function inside the function body but rather passing it to another function or returning back to the calling function.- In such a scenario, the Kotlin compiler will be in conflict and give a compilation error. In order to overcome this there are 2 options -
- i. Make the particular function that is passed as parameter to the
inline
function asnoinline
. - ii. The other option is to make the function parameter
crossinline
. This will make the function to be called asinline
wherever it will be invoked.
- i. Make the particular function that is passed as parameter to the
- In such a scenario, the Kotlin compiler will be in conflict and give a compilation error. In order to overcome this there are 2 options -
-
A non-local
return
is not allowed in a lambda that is passed as acrossinline
parameter. The reason is that by the time the lambda is executed, it might have exited from the function to which it is passed as a parameter - hence no point trying to return from a function that has already completed.
Recap:
inline
performs inline optimization, to remove function call overhead.crossinline
also performs inline optimization, not within the function to which the lambda is passed, but wherever it is eventually called.- Only lambdas passed for parameters not marked
noinline
orcrossinline
can have non-localreturn
.
- Sequences are equivalent to Java Streams
- Parallel sequences is not available on Kotlin
- Using sequences for evaluating collections lazily -
asSequence()
generateSequence()
lazy()
, etc...
- Extension function allows extending functionality of class without inheriting from it.
- Scope of exception functions is packages. In order to use outside the package, we need to import the package with the extension function.
- Member function takes precedence over extension function when both have same names and definition.
- Extensions are resolved statically. By defining an extension, we do not insert new members into a class, but merely make new functions callable with the dot-notation on variables of this type.
Note :
- There needs to be a discipline when using extension methods
-
A bunch of String manipulation functions are available in Kotlin. Check it out here - Kotlin String Functions
-
Some utility functions to checkout -
run()
with()
apply()
let()
also()
takeIf()
takeUnless()
- Nested or local functions are functions defined inside another function. The local functions are not accessible outside the enclosing function.
- Functions prefixed with
infix
can be accessed without the dot notation and we can call the functions without parentheses()
- This is applicable to member and extension functions only
- Only one parameter is supported in an infix function
- Kotlin allows us to provide implementations for a predefined set of operators on our types.
- These operators have fixed symbolic representation (like + or *) and fixed precedence
-
Lambda extensions helps us create Domain Specific Languages (DSLs) like Gradle, SQL Dialects, JSON DSL, etc.
-
Using the
invoke()
function -
Learn more from here -
Open Source Library - funKTionale
- Supports composition, currying, memoization, etc...
- When a function is tail recursive, Kotlin provides us a way to optimize the execution of it by prefixing the function
with
tailrec
. This optimizes the generated bytecode by replacing it with for loops or GOTO calls.
Coroutines go hand in hand with suspendible functions, the execution of which may be suspended and resumed. These features are built in Kotlin using continuations, which are data structures used to preserve the internal state of a function in order to continue the function call later on.
- Unlike subroutines, which have a single point of entry, coroutines have multiple points of entry. Additionally, coroutines may remember state between calls.
- A call to a coroutine can jump right into the middle of the coroutine, where it left off in a previous call. Because of all this, we can implement cooperating functions—that is, functions that work in tandem—where two functions can run concurrently, with the flow of execution switching between them.
-
While coroutines are part of the language’s standard library, we’ll have to download an additional library to make use of that package, which contains functions to easily create and work with coroutines.
-
The
runBlocking()
function fromkotlinx.coroutines.*
package takes a lambda as an argument and executes that within a coroutine. -
launch()
function starts a new coroutine to execute the given lambda, likerunBlocking()
function does, except the invoking code isn’t blocked for the completion of the coroutine. -
Unlike the
runBlocking()
function, thelaunch()
function returns a job, which can be used to wait on for completion or to cancel the task.runBlocking { launch { task1() } launch { task2() } println("Calling task1() and task2() from main") }
-
Kotlin coroutines library comes with suspension points—a function that will suspend execution of the current task and let another task execute. There are two functions to achieve this in the
kotlinx.coroutines.*
library:delay()
andyield()
.- The
delay()
function will pause the currently executing task for the duration of milliseconds specified. - The
yield()
method doesn’t result in any explicit delays. But both these methods will give an opportunity for another pending task to execute.
- The
-
Kotlin will permit the use of suspension points only in functions that are annotated with the
suspend
keyword. Marking a function withsuspend
doesn’t automatically make the function run in a coroutine.suspend fun task2() { println("Starting task2 in ${Thread.currentThread()}") yield() println("Ending task2 in ${Thread.currentThread()}") }
When to use coroutines
- Suppose we have multiple tasks that can’t be run in parallel, maybe due to potential contention of shared resources used by them. Running the tasks sequentially one after the other may end up starving all but a few tasks. Sequential execution is especially not desirable if the tasks are long running or never ending. In such cases, we may let multiple tasks run cooperatively, using coroutines, and make steady progress on all tasks.
- We can also use coroutines to build an unbounded stream of data for creating Infinite Sequences.
-
The call to
launch()
andrunBlocking()
functions by default execute in the same thread as the caller's coroutine scope as they carry a coroutine context from their scope. -
We may pass a
CoroutineContext
to these functions to set the execution context of the coroutines these functions start.
-
We may pass some pre-defined thread pool (
Dispatchers
) asCoroutineContext
to these functions. Let's take a look at few of them -Dispatchers.Default
: This instructs the coroutine to execute in a thread from DefaultDispatcher thread pool. The number of threads in this pool is 2 or number of cores of the CPU, whichever is higher. This pool is for running computation intensive tasks.Dispatchers.IO
: This instructs coroutines to execute in a pool that is dedicated for running IO intensive tasks. That pool may grow in size if threads are blocked on IO and more tasks are created.Dispatchers.Main
: Used in android devices and Swing UI, for example, to run tasks that update the UI from only themain
thread.
-
We can pass custom thread pool (
Executors
API) to these functions as well.SingleThreadExecutor
: Coroutines using this context will run concurrently instead of parallel.FixedThreadPool
: Coroutines using this context can run in this custom pool with the number of threads passed in the configuration.
Note:
- When passing custom thread pool, we need to use Kotlin's extension functions to get a
CoroutineContext
from it using anasCoroutineDispatcher()
function.- Along with this function, we need to call
use()
function which takes care of closing theExecutors
pool. Thisuse()
function behaves like the try-with-resources feature in Java.
- We might have a scenario where we want a coroutine to start in the context of the caller but switch to a different
thread at suspension point. We can use the
CoroutineStart
, the second optional argument tolaunch()
. - The different enums for
CoroutineStart
are -DEFAULT
: To run the coroutine in the current contextLAZY
: Defer execution until an explicitstart()
is calledATOMIC
: Used to run in non-cancellable modeUNDISPATCHED
: To run in current context and then switch threads after suspension point
- We can run a coroutine in one context and then change the context midway using the
withContext()
.
Useful Tips
- Kotlin provides a command-line option
-Dkotlinx.coroutines.debug
to display the details of the coroutine executing a function.- Behind the scenes, coroutines are managed using continuations. Continuations are highly powerful data structures - programs can capture and preserve the state of execution in one thread and restore them when needed in another thread.
launch
returns an object ofJob
and there is no way to return a result from it. In order to execute a task asynchronously and return a value we need to useasync
.async()
has same parameters aslaunch()
, but async returns aDeferred<T>
future object, which has anawait()
method, among many other methods.- A call to
await()
will block the flow of execution but not the executing thread. Thus, the code in the caller and the code within the coroutine started byasync()
can run concurrently. - The call to
await()
will eventually return the result of the coroutine started usingasync()
. - If the coroutine started using
async()
throws an exception, then that exception will be propagated to the caller through the call toawait()
.
Kotlin is 100% interoperable with Java. Its 2 way - Java to Kotlin and Kotlin to Java as well.
- Java nullable types become Platform types in Kotlin
- Properties in Kotlin will automatically be converted to corresponding get() and set() methods when the same is used inside a Java class.
@JvmField
: For properties defined inside Kotlin code to be accessible from Java, this annotation in Kotlin code will help discover the property in Java code@JvmOverloads
: For default parameters defined in Kotlin methods, Java won't have direct access to the overloaded method without specifying value for the default parameter. With@JvmOverloads
annotation on the Kotlin method, Java class will now have access to the overloaded method without that particular default parameter.@JvmName
: Kotlin provides us with a feature of having a different name to a particular method when a method is referenced inside a Java code by using the@JvmName
annotation.@Throws
: When an exception is thrown from a method in Kotlin, the same needs to be annotated with@Throws
in order for the Java class to handle it.
-
Top level functions when compiled by default generates a bytecode file with name Kt.class.
-
In order to reference these functions inside Java we can simply call the function as Kt.functionName().
-
In order to change the name of the default class name we can use the annotation at the top of the Kotlin file -
@file:JvmName("MyKotlinClass")
-
Top level properties can again be accessed by default using get() and set() methods inside Java.
-
In case we want to access the property as a field name in Java class, then the same needs to be prefixed using
const
in the Kotlin file.
- Extension functions can be accessed from Java using the Kotlin-className.functionName() similar to accessing a static method in Java.
Highlights
- Allow for introspection of code at runtime
- Kotlin can use - >
- Java Reflections API
- Kotlin Reflection API
- Reified type parameters
- Avoid type erasure
- Limitations - applicable to
inline
only, cannot create instances ofT
In this section we will take a look at some of the useful language constructs that Kotlin provides, which will help us deepen our understanding of the further sections
-
Destructuring is to extract values into variables from an existing object.
-
The destructuring in Kotlin is based on the position of properties. In JavaScript object destructuring is based on name of properties.
-
We can skip properties while destructuring by putting underscores
(_)
fun getFullName() = Triple("John", "Quincy", "Adams") val (first, middle, last) = getFullName() val (f,_, l) = getFullName()
-
All exception classes in Kotlin are descendants of the class
Throwable
. -
Kotlin does not have checked exceptions.
-
Every exception has a message, stack trace and an optional cause.
-
Sample exception block
try { // some code } catch (e: SomeException) { // handler } finally { // optional finally block }
-
try
is an expression, i.e., it can return a value -
The returned value of a try-expression is either the last expression in the
try
block or the last expression in thecatch
block (or blocks). Contents of thefinally
block do not affect the result of the expression. -
throw
is an expression in Kotlin that returns a special typeNothing
.fun fail(message: String): Nothing { throw IllegalArgumentException(message) } val s = person.name ?: fail("Name required") println(s) // 's' is known to be initialized at this point