Skip to content

Kotlin Core – Basic Questions

Open all questions about Spring

Open all questions about Android

Content:

In Kotlin, inner classes are classes that are declared inside another class. They have access to the members of the outer class and can be used to implement design patterns such as Strategy or Observer.

Unlike Java, in Kotlin, inner classes are static by default. That is, they do not have access to non-static members of the outer class and can be created without creating an instance of the outer class.

However, if the inner class is marked with the inner keyword, it becomes non-static, and then it has access to the non-static members of the outer class and requires an instance of the outer class to be created.
Example of an inner class in Kotlin:

class Outer {
    private val outerField = "Outer field"   
    inner class Inner {
        fun innerMethod() {
            println("Accessing outer field: $outerField")
        }
    }
}

fun main() {
    val outer = Outer()
    val inner = outer.Inner()
    inner.innerMethod() // Will output "Accessing outer field: Outer field"
}

▲To the list of questions▲

Kotlin has five Scope Functions that allow you to change the scope of variables, making your code easier to read and reducing the likelihood of errors:

let:
allows you to execute a block of code on an object passed as an argument and return the result of that block. Within the block, you can use a reference to the object via it.

val result = someObject?.let { it.property } ?: defaultValue

run:
executes a block of code on the object passed as this and returns the result of that block. Within the block, you can reference the object via this.

val result = someObject?.run { property } ?: defaultValue

with:
executes a block of code that is passed an object as an argument, and returns the result of that block.

val result = with(someObject) { property } ?: defaultValue

apply:
executes a block of code on the object passed as this and returns that object. Within the block, you can reference the object via this.

val someObject = SomeClass().apply {
    property1 = value1
    property2 = value2
}

also:
allows you to execute a block of code on an object passed as an argument and return that object. Within the block, you can use a reference to the object via it.

val someObject = SomeClass().also {
    it.property1 = value1
    it.property2 = value2
}

they can be combined

variable?.let { variable is not null } ?: run { variable is null }
// the same as 
if(variable != null) { variable is not null } else { variable is null }

▲To the list of questions▲

Normal function:
have no state and always start "from scratch" (unless, of course, they use global variables)
must complete its execution before returning control to the code that called it

Coroutine:
has a state and can pause and resume its execution at certain points, thus returning control before its execution has completed.
A coroutine is not tied to a native thread, it can pause execution in one thread and resume execution in another, coroutines do not have their own stack, do not require switching the processor context, so they work faster.

There are two functions to start a coroutine:
launch{} and async{}.

launch{}:
returns nothing

async{}:
returns a Deferred instance that has an await() function that returns the result of the coroutine

CoroutineScope:
watches any coroutine created with launchor async (these are extension functions in CoroutineScope).
Current work (running coroutines) can be cancelled by calling scope.cancel() at any time.

Job:
the control element of a coroutine. For each coroutine created (using launch or async), it returns a Job instance, which uniquely identifies the coroutine and manages its lifecycle. A Job can go through many states: new, active, finishing, completed, canceled, and cancelled. Although we don't have access to the states themselves, we can access the Job properties: isActive, isCancelled, and isCompleted.

CoroutineContext:
This is a set of elements that define the behavior of a coroutine.
It consists of:
Job: manages the life cycle of the coroutine.
CoroutineDispatcher: sends work to the appropriate thread.
CoroutineName: name of the coroutine, useful for debugging.
CoroutineExceptionHandler: handles uncaught exceptions, which will be covered in Part 3 of the coroutines series.
In coroutines there is delay(1000L) – non-blocking delay

suspend fun doSomeWork() = coroutineScope {
        launch { doWork() }
    }
suspend fun doWork() {
        println("before")
        delay(400L)
        println("after")
    }

Since this function uses the delay() function, doWork() is defined with the suspend modifier. The coroutine itself is also created using the launch() function, which calls the doWork() function.
When you call a delay using the delay() function, the coroutine releases the thread it was running on and saves it in memory. The released thread can be used for other tasks. And when the running task (for example, execution of the delay() function) completes, the coroutine resumes its work on one of the free threads.

▲To the list of questions▲

There are two functions to launch a coroutine: launch{} and async{}.

launch{}:
returns nothing,

fun main(args: Array<String>) {
    print("1 ")
    val job: Job = GlobalScope.launch {
        print("3 ")
        delay(1000L)
        print("4 ")
    }
    print("2 ")
// 1 2 but it won't run further because the program will end earlier
// or you can do job.cancel()
}

async{}:
returns a Deferred instance that has an await() function that returns the result of the coroutine, just like Future in Java where we do future.get() to get the result.

suspend fun main(args: Array<String>) {
    print("1 ")
    val deferred: Deferred<String> = GlobalScope.async {
        return@async "3 "
    }
    print("2 ")
    print(deferred.await())
    print("4 ")
    // 1 2 3 4
}

▲To the list of questions▲

Class Delegation:
The by keyword in the Derived table of contents, located after the type of the delegated class, says that the object b of type Base will be stored inside the Derived instance, and the compiler will generate the corresponding methods from Base in Derived, which will be passed to the object b when called.

interface Base {
    fun print()
}
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() // prints 10
}

Delegating properties:
There are several basic types of properties that we implement manually each time we need them. However, it would be much more convenient to implement them once and for all and put them in some library. Examples of such properties:
lazy properties: the value is evaluated once, the first time it is accessed
properties whose change events can be subscribed to (observable properties)
properties stored in an associative list rather than in individual fields
For such cases, Kotlin supports delegated properties.
The expression after by is a delegate: calls (get(), set()) to the property will be handled by this expression. The delegate does not have to implement any interface, it is enough that it has getValue() and setValue() methods with a certain signature.

class Example {
    var p: String by Delegate()
}
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thanks for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value was assigned to the value '${property.name} in $thisRef.'")
    }
}

Standard delegates:

Lazy properties:
The first call to get() runs the lambda expression passed to lazy() as an argument and remembers the resulting value, and subsequent calls simply return the computed value.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}
val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}
fun main(args: Array<String>) {
    println(lazyValue) // computed! Hello
    println(lazyValue) // Hello
}

Observable properties:
The Delegates.observable() function takes two arguments: the initial value of the property and a handler (lambda) that is called when the property changes. The handler has three parameters: a description of the property that is changing, the old value, and the new value.

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}
fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}
// conclusion 
// <no name> -> first
// first -> second

Storing properties in an associative list:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}
val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

▲To the list of questions▲

Kotlin allows you to extend a class by adding new functionality.
Extensions do not actually make any modifications to the classes they extend. When you declare an extension, you create a new function, not a new class member. Such functions can be called with a dot, applicable to a specific type.
Extensions have static dispatch: this means that the extension function called is determined by the type of its expression at compile time, rather than by the type of the expression evaluated at runtime, as is the case with virtual function calls.

fun Any?.toString(): String {
    if (this == null) return "null"
    return toString()
}

▲To the list of questions▲

Something like a replacement for statics in Java, they are accessed via the class name. Such members of auxiliary objects look like static members in other programming languages. In fact, they are members of real objects and can implement, for example, interfaces:

interface Factory<T> {
    fun create(): T
}
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

▲To the list of questions▲

var stringRepresentation: String
    get() = this.toString()
    set(value) { 
      setDataFromString(value) 
    }
var setterVisibility: String = "abc" 
    private set // The setter has private access and a standard implementation

▲To the list of questions▲

Specifies that an additional static method should be generated from this element if it is a function. If this element is a property, then additional static getters/setters should be generated.

▲To the list of questions▲

Unit is the same as void in Java, it returns nothing.

fun main(args: Array<String>) {
    one { variable ->
        print(variable)
    }
    two { variable ->
        print(variable)
        return@two "4 "
    }
    three {
        return@three "5 "
    }
    four({ variableOne ->
        print(variableOne)
    }, { variableTwo ->
        print(variableTwo)
    })
    val listenerOne: (() -> Unit) = {
        print("8 ")
    }
    listenerOne.invoke()
    val listenerTwo: ((String) -> Unit) = { variable ->
        print(variable)
    }
    listenerTwo.invoke("9 ")
    val listenerThree: ((String) -> String) = { variable ->
        variable
    }
    val result = listenerThree.invoke("10 ")
    print(result)
}
fun one(callback: (String) -> Unit) {
// we pass to the lambda and pass to it a String, we don't expect anything back (Unit)
    callback("1 ")
}
fun two(callback: (String) -> String) {
// we pass to the lambda and pass to it a String, we expect a String back
    callback("2 ")
    print(callback("3 "))
}
fun three(callback: () -> String) {
// we call the lambda but don't pass anything to it, we expect a String back
    print(callback())
}
fun four(callbackOne: (String) -> Unit, callbackTwo: (String) -> Unit) {
// returns 2 callbacks
    callbackOne("6 ")
    callbackTwo("7 ")
}
// 1 2 3 4 5 6 7 8 9 10

Functional types:
Kotlin uses a family of functional types, such as (Int) -> String, for declarations that are part of functions: val onClick: () -> Unit = ….

All function types have a parenthesized list of parameter types and a return type: (A, B) -> C denotes a type that provides the function with two accepted arguments of types A and B, and returns a value of type C. The list of parameter types may be empty, as in () -> A. The return type Unit cannot be omitted.

Function types can have an additional receiver type, which is specified in the declaration before the dot: the type A.(B) -> C describes functions that can be called on a receiver object A with parameter B and return value C. Function literals with a receiver object are often used with these types.

Suspending functions are a special kind of function type that have the suspend modifier in their declaration, such as suspend () -> Unit or suspend A.(B) -> C.

A function type declaration can also include named parameters: (x: Int, y: Int) -> Point. Named parameters can be used to describe the meaning of each of the parameters.

To indicate that a function type may be nullable, use parentheses: ((Int, Int) -> Int)?.

Functional types can be combined using parentheses: (Int) -> ((Int) -> Unit).

The arrow in the declaration is right-associative, i.e. the declaration (Int) -> (Int) -> Unit is equivalent to the declaration in the previous example, and not ((Int) -> (Int)) -> Unit.

You can also give a functional type an alternative name using type aliases:
typealias ClickHandler = (Button, ClickEvent) -> Unit

Creating a functional type:
There are several ways to obtain an instance of a functional type:
Using a code block inside a function literal in one of the forms:
lambda expression: { a, b -> a + b },
anonymous function: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Using an instance of a custom class that implements a functional type as an interface:

class IntTransformer: (Int) -> Int {
 override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()

▲To the list of questions▲

Copyright: Roman Kryvolapov