Skip to content

Questions & Answers – Java / Kotlin Core

The article covers questions on Java and Kotlin Core. Most of the examples and some descriptions are generated by ChatGPT 4 and 4o, the questions themselves were recorded during an interview for the above-described position, found on the Internet, in various video interviews on YouTube. The article will be supplemented and updated as much as I have the desire and free time.

Content:

Java:
➤ What are the data types in Java and what is their size
➤ What are the modifiers in Java and Kotlin
➤ What Java Collections do you know?
➤ What are the complexity levels for operations with collections
➤ What is a HashMap?
➤ How to sort a collection containing a complex class, Comparable and Comparable
➤ What is the difference between ArrayList and LinkedList
➤ How TreeMap works
➤ What are the naming rules in Java
➤ What are static classes in Java
➤ What is the difference between StringBuffer and StringBuilder
➤ What is reflection in Java
➤ In what situations does Memory leak occur
➤ What is Generic
➤ What methods does the Object class have in Java and Any in Kotlin
➤ What is Enum in Java
➤ What is the priority in Java when converting primitive types
➤ What is type casting / type conversion / casts
➤ What are annotations for and how to create them in Java
➤ What are Wrappers in Java
➤ What are the transition operators
➤ What is Stack and Heap in Java
➤ What is JDK, JRE and JVM
➤ What values ​​are variables initialized with by default
➤ Is it possible to narrow the access level/return type when overriding a method?
➤ What are the types of references in Java
➤ What is the difference between map and flatMap
➤ What are the types of errors in Java
➤ How does Garbage Collector work and how is memory organized in the JVM
➤ What is Java NIO (New Input/Output)
➤ What is the execution order of a Java class
➤ Can an abstract class inherit from a regular one
➤ What are the types of cycles in Java and Kotlin
➤ What is ClassLoader
➤ What is SecurityManager
➤ What is JavaCompiler
➤ How to run JavaScript from Java
➤ What is Optional in Java and Kotlin
➤ What are the bitwise operations in Java and Kotlin
➤ What is ByteBuffer and Unsafe
➤ What is lambda in Java
➤ How to download executable code from the network in Java
➤ What is AsynchronousServerSocketChannel

Kotlin:
➤ What are Inner Classes in Kotlin
➤ What are Scope Functions in Kotlin
➤ How does a coroutine work and what is suspend
➤ What is the difference between the coroutine launch method launch and async
➤ What are Delegates in Kotlin
➤ What is Extension in Kotlin
➤ What is a companion object in Kotlin
➤ How to use getters and setters in Kotlin
➤ What is the @JvmStatic annotation
➤ What is a callback, functional types and Unit in Kotlin and how is it used

Theory:
➤ What are the complexity levels of collections
➤ What are the most commonly used patterns
➤ What is the difference between inheritance and composition
➤ What is SOLID
➤ What are higher-order functions
➤ What is the difference structural, functional and object-oriented paradigms in programming
➤ What formats of data exchange with the server exist
➤ What is the syntax of a query to an SQL database

Multithreading:
➤ What are synchronized collections and how do they work
➤ What does volatile mean
➤ What does synchronized mean
➤ What problems can occur in multithreading in Java
➤ What is Lock / ReentrantLock
➤ What are Backpressure strategies
➤ What is the difference between hot and cold Observables
➤ How volatile differs from atomic
➤ What are Deferred coroutines in Kotlin
➤ What are LAZY coroutines in Kotlin
➤ What are Kotlin Channels
➤ What is Mutex, Monitor, Semaphore

Integer:

byte:
8 bits, from -128 to 127

short:
16 bits or 2 bytes, from -32_768 to 32_767

int:
32 bits or 4 bytes, from -2_147_483_648 to 2_147_483_647

long:
64 bits or 8 bytes, from -9_223_372_036_854_775_808 to 9_223_372_036_854_775_807

if the value written to long exceeds the maximum value for int, the letter l or L must be added after the number

Floating point:

float:
32 bits or 4 bytes, from ~1,410 to the -45th power to ~3,410 to the 38 power

double:
64 bits or 8 bytes, from ~4,910 to the -324 power to ~1,810 to the 308 power

Character:

char:
16 bits or 2 bytes, from ‘\u0000’ or 0 to ‘\uffff’ or 65_535

Boolean:

boolean: 8 bits or 1 byte;

Also remember that:
String is not a primitive data type
in Java, primitive types cannot be null, reference types can be null
in Kotlin, both primitive and reference types can be null if they are declared as null-unsafe, i.e. that they can be null (var i: Int? = null). In Kotlin, there are no explicit primitive types, the compiler itself decides whether to use a primitive type or a wrapper

Java methods and variables:

private:
Accessible within the class

default (not specified):
Accessible within the package

protected:
Accessible within the package or from inheriting classes

public:
Accessible everywhere

static:
The method or variable is static

transient:
The variable is not serializable and should not participate in overriding equals() and hashCode()

final for a method:
The method cannot be overridden

final for a variable:
The variable is a constant

abstract:
A method of an abstract class is abstract and has no implementation

Java classes:

default (not specified):
Accessible within the package

public:
Accessible everywhere

final:
The class cannot be inherited

abstract:
The class is abstract and may contain abstract methods

record:
This is a special type of class introduced in Java 14 (in preliminary form) and finally included in Java 16. Record provides a concise and an expressive way to define classes that are simple containers for immutable data. They greatly simplify the creation of classes that simply store data by automatically generating constructors, equals(), hashCode(), and toString() methods.

For Java interfaces:

default:
since Java 8, a regular method within an interface, designed to reduce code duplication if all implementations of an interface have the same method, can be overridden in the implementation if necessary. If a class implements two interfaces that have default methods with the same name, a name conflict will occur, and the method will need to be overridden.

static:
since Java 8, a static method in an interface, called only on behalf of the interface, not on behalf of the instance. Static methods are not inherited by the implementing class and cannot be overridden. Since Java 9, can be private, so that they can be called from another static method.

Kotlin also has:

internal:
Available inside a module
public (not specified)

open for class:
Class can be inherited from, otherwise not

open for method:
Method can be overwritten, otherwise not

object:
Used to declare singletones and to create anonymous objects, which are a replacement for anonymous inner classes in Java, or for internal parameters

vararg:
Allows you to pass a non-fixed number of arguments for a parameter

init:
Initialization block

lateinit:
Late-initialized property

typealias:
Provides alternative names for existing types

::
Reference to a class, function, or property

// reference to class
val c = MyClass::class
// reference to property
var x = 1
fun main(args: Array<String>) {
    println(::x.get()) // will output "1"
    ::x.set(2)
    println(x)         // will output "2"
}
// reference to function
fun isOdd(x: Int) = x % 2 != 0
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // will output [1, 3]
// reference to the constructor
class Foo
fun function(factory : () -> Foo) {
    val x : Foo = factory()
}

value class:
inline class, not present in bytecode – its value will be used instead, it is used to wrap data, does not cause a performance penalty

constructor:
secondary constructors

inline:
function modifier, using higher-order functions (a higher-order function is a function that takes functions as parameters, or returns a function as a result.) entails a performance penalty. The inline modifier affects both the function and the lambda passed to it: they will both be inlined at the call site. Inlining functions can increase the amount of code generated, but if you do it within reasonable limits (don’t inline large functions), you will get a performance boost, especially when calling functions with parameters of different types inside loops.

noinline:
if you want only some lambdas passed to an inline function to be inlined, you need to mark the noinline modifier on the function parameters that will not be inlined

data class:
data class, the compiler automatically generates the following members of this class from the properties declared in the main constructor: equals()/hashCode(), toString(), componentN(), copy()

inner class:
in Kotlin, static inner classes do not have any modifier except class, and regular inner classes that require an instance of the outer class to access have the inner class modifier

sealed class:
adding the sealed modifier to a superclass restricts the ability to create subclasses. All direct subclasses must be nested within the superclass. A sealed class cannot have descendants declared outside the class. By default, a sealed class is open and the open modifier is not required. Sealed classes are a bit like enums. You can create as many subclasses in a sealed class as needed to cover every situation. In addition, each subclass can have multiple instances, each of which will carry its own state. Each subclass of an isolated class has its own constructor with its own individual properties. Isolated classes are enums with superpowers. An isolated class can have descendants, but they must all be in the same file as the isolated class. Classes that extend descendants of an isolated class can be anywhere.

sealed class StringSource {
  data class Text(val text: String) : StringSource()
  data class Res(val resId: Int) : StringSource()
}

val text = StringSource.Text("123")
val textRes = StringSource.Res(123)

Sealed classes are abstract and can contain abstract components. The constructor of a sealed class is always private and this cannot be changed. Sealed classes cannot be initialized. The descendants of a sealed class can be classes of any type: a data class, an object, a regular class, or even another sealed class.

Also remember that:

All variables in interfaces are final by default

With inheritance and overriding, you can extend the access level default -> protected -> public

With inheritance and overriding, you can narrow the error level, for example, Exception -> RuntimeException -> ArithmeticException

interface Collection:
The base interface for collections and other collection interfaces.

interface List:
An ordered list where each element has an index, duplicate values ​​are allowed. Inherited from Collection.

interface Set:
An unordered set of unique elements. Inherited from Collection

interface Map:
Consists of key-value pairs. Keys are unique, and values ​​can be repeated. The order of elements is not guaranteed. Map allows you to look up objects (values) by key. Contains put instead of add. Contains entrySet methods for converting a key-value pair to a Set, keySet method for converting keys to a Set, values ​​method for converting values ​​to a Collection.
Don’t confuse the Collection interface with the Collections framework. Map does not inherit from the Collection interface, does not inherit from anything, but is part of the Collections framework.

interface Queue:
A queue. In such a list, elements can only be added to the tail, and removed only from the beginning – this is how the first in, first out concept is implemented. Inherits from Collection

interface Deque:
can act as both a queue and a stack. This means that elements can be added to its beginning or end. The same applies to deletion. Inherits from Queue

class Stack:
inherits from the Vector class, which in turn implements List, which implements a simple mechanism like last in, first out.

interface Iterator:
allows you to traverse collections except Map, contains methods hasNext, next, remove, also forEachRemaining Classes: List: AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector Set: AbstractSet, ConcurrentHashMap.KeySetView, ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, JobStateReasons, LinkedHashSet, TreeSet Map: AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap

Queue:
AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, LinkedTransferQueue, PriorityBlockingQueue, PriorityQueue, SynchronousQueue

Deque:
ArrayDeque, ConcurrentLinkedDeque, LinkedBlockingDeque, LinkedList

The most famous collections are:

ArrayList:
array-based list.

LinkedList:
doubly linked list.

Vector:
this is the same as ArrayList, but all methods are synchronized

PriorityQueue:
sorted by comparator array-based queue, for complex classes you can pass comparator to constructor

SortedSet:
sorted elements

NavigableSet:
allows to retrieve elements based on their values

TreeSet:
all objects are stored sorted in ascending order, works based on TreeMap

HashSet:
same as HashMap but the key is the object itself, works based on HashMap

LinkedHashSet:
observes the insertion order of objects, works based on LinkedHashMap

HashMap:
array key value, before collision is an array, after collision is a unidirectional list, when a certain threshold is reached, a node from a unidirectional list (stores the next element) is transformed into an element of the red-black tree TreeNode

Hashtable:
synchronized version HashMap

LinkedHashMap:
HashMap that remembers the order in which elements are added, it is preserved when pulled using entrySet or other methods. In addition to the node, Entry stores a link to the next and previous element.

TreeMap:
sorted map based on a red-black tree

Also remember that:

In Kotlin, collections are divided into mutable and immutable.

Lists:

ArrayList:
Append to end: O(1) amortized
Add to middle or beginning: O(n)
Delete from end: O(1)
Delete from middle or beginning: O(n)
Access by index: O(1)
Search: O(n)

LinkedList:
Append to beginning or end: O(1)
Add to middle: O(1) (if node referenced) or O(n) (find node)
Delete from beginning or end: O(1)
Delete from middle: O(1) (if node referenced) or O(n) (find node)
Access by index: O(n)
Search: O(n)

Sets:

HashSet:
Add: O(1) amortized
Delete: O(1) amortized
Search: O(1) amortized

LinkedHashSet:
Add: O(1) amortized
Delete: O(1) amortized
Search: O(1) amortized
Iteration: O(n) (preserves insertion order)

TreeSet:
Add: O(log n)
Delete: O(log n)
Search: O(log n)

Maps:

HashMap:
Add: O(1) amortized
Delete: O(1) amortized
Search: O(1) amortized

LinkedHashMap:
Add: O(1) amortized
Delete: O(1) amortized
Search: O(1) amortized
Iteration: O(n) (preserves insertion order)

TreeMap:
Add: O(log n)
Delete: O(log n)
Find: O(log n)

Queues and Deques:

PriorityQueue:
Add: O(log n)
Delete: O(log n)
Find: O(n)

ArrayDeque:
Add to front or back: O(1)
Delete from front or back: O(1)
Find: O(n)

LinkedList (used as a queue or deque):
Add to front or back: O(1)
Delete from front or back: O(1)
Find: O(n)

Tables:

ConcurrentHashMap:
Add: O(1) amortized
Delete: O(1) amortized
Find: O(1) amortized

HashMap uses a hash table to store the map, providing fast execution time for get() and put() requests. Hashes are stored in a Bucket. You need to override equals() and hashCode(). Hash is an Integer, which the key is converted to. First, the hash is compared, if it is the same, then the keys are compared, since a collision is possible when the hashes are the same, but the keys are not.

DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
DEFAULT_LOAD_FACTOR = 0.75f;
MAXIMUM_CAPACITY = 1 << 30 // 1_073_741_824 or half max int

class for storing data

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    // next node is stored here if hash matches
    Node<K,V> next;
}

If in

transient Node<K,V>[] table;

there are no elements passed to the method

final V putVal(
  int hash, 
  K key, 
  V value, 
  boolean onlyIfAbsent, 
  boolean evict
)

hash, is executed

tab[i] = newNode(hash, key, value, null);

and if there is, then in Node<K, V> next of the Node class the next element is added using

p.next = newNode(hash, key, value, null);

If the size is not enough, it is performed

newCap = oldCap << 1 // or multiply by 2

The resulting hash code can be a huge numeric value, and the original array is conditionally designed for only 16 elements. Therefore, the hash code must be transformed into values ​​from 0 to 15 (if the array size is 16), for this additional transformations are used.

Java 8 after reaching a certain threshold, balanced red-black trees TreeNodes are used instead of linked lists (works similarly to TreeMap).

This means that HashMap initially stores objects in a linked list, but after the number of elements in the hash reaches a certain threshold, a transition to balanced trees occurs, and then it can be transformed back into a regular node.

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent; 
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;
        boolean red;
}

In Kotlin you can also specify using to

val map = mapOf("a" to 1, "b" to 2, "c" to 3)

if the Hash in the HashMap matches, a new entry is added to the r element, referencing the old entry, to view the entries in this element, you will need to traverse the linked list of entries. This is called the Chaining Method

Comparable:
The class must implement the Comparable interface, implement the class as a generic (implements Comparable), implement the compareTo method

Comparator:
The Comparator class is a functional interface that is used to compare objects. In Kotlin, you can use lambda expressions to create instances of the Comparator class.
For example, if we have a Person class with name and age properties, we can create a Comparator to sort a list of Persons by age as follows:

Set set = new TreeSet<String>(new Comparator<String>() {
    public int compare(String i1, String i2) {
        return i2.compareTo(i1);
    }
});
// or
Comparator<String> comparator = (o1, o2) -> o1.compareTo(o2);
Set set = new TreeSet<String>(comparator);
// or
Collections.sort(list, comparator);
data class Person(val name: String, val age: Int)

fun main() {
    val people = listOf(
        Person("Alice", 25),
        Person("Bob", 30),
        Person("Charlie", 20)
    )
    
    val ageComparator = Comparator<Person> { p1, p2 -> p1.age - p2.age }
    val sortedByAge = people.sortedWith(ageComparator)
    
    println(sortedByAge)
}

ArrayList:
array-based list.

LinkedList:
doubly linked list.

If elements are often added and removed, especially from the middle or beginning of the sheet, then LinkedList is better, in other cases – ArrayList
When creating an ArrayList DEFAULT_CAPACITY = 10 or you can pass the quantity to the constructor, when reached, the array increases by 1.5 times using the grow method

int newCapacity = oldCapacity + (oldCapacity >> 1)

In LinkedList, information is stored in the internal static class Node. It implements Queue and is convenient to use as a queue and accordingly there are queue methods – peek (pull out and do not delete), pool (pull out and delete)

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

When adding an element, the first element is first placed at the root of the tree root and turns black, the following elements go to the right if they are greater than the top one, and to the left if they are less, and are attached to parent starting with root. The entry looks like this

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
}

Naming is very important for understanding the code, do not be lazy to name classes, methods and variables correctly according to what they mean and what they do

Classes and interfaces:
are written in CamelCase with a capital letter

Methods and variables:
are written in camelCase with a lowercase letter, except for constants

Constants:
are written in SNAKE_CASE with capital letters
A variable can start with _ $ or a letter, but cannot start with a number, but can contain numbers

Implementations of interfaces are often named Interface Name + Impl, for example InterfaceNameImpl

Names of variables starting with m and interfaces starting with I are obsolete

The rules for naming are described in Java Code Conventions.
To translate cases into other cases, IDEA has a convenient plugin “String Manipulation”

Only an inner class can be a static class, it can be inherited, as it can inherit from any other class and implement an interface.
A static nested class is no different from a normal inner class, except that its object does not contain a reference to the outer class object that created it. To use static methods/variables/class, we do not need to create an object of this class. In normal inner classes, without an instance of the outer class, we cannot create an instance of the inner one.

public class Vehicle {
  public static class Car {
     public int km;
  }
}
Vehicle.Car car = new Vehicle.Car();
car.km = 90;

StringBuffer and StringBuilder are used for operations with text data, StringBuffer is synchronized and thread-safe

Reflection is a mechanism that allows programs to examine and modify their structure and behavior at runtime.
Reflection allows you to access information about classes, interfaces, fields, methods, and constructors at runtime. This can be useful when you don’t know the structure of a class in advance, but want to process it dynamically.

For example, reflection can be used to create new objects, call methods, set field values, and even load new classes at runtime. However, you should be careful when using reflection, as it can lead to poor performance and poor code readability.

public class Main {
    public static void main(String[] args) throws Exception {
        ClosedClass closedClassNewInstance = new ClosedClass();
        Class closedClass = closedClassNewInstance.getClass();
        closedClass = ClosedClass.class;
        closedClass = Class.forName("com.company.ClosedClass");
        String className = closedClass.getName();
        closedClassNewInstance = (ClosedClass) closedClass.newInstance();
        Constructor[] constructors = closedClass.getConstructors();
        constructors = closedClass.getDeclaredConstructors();
        Method[] methods = closedClass.getMethods();
        Parameter[] parameters = constructors[0].getParameters();
        String name = parameters[0].getName();
        String type = parameters[0].getType().getName();
        Method method = closedClass.getMethod("method");
        method.invoke(closedClass);
        Field field =  closedClass.getField("variable");
        field.setAccessible(true);
        field.setInt(closedClass, 1);
    }
}
class ClosedClass {
    int variable;
    void method(){}
}

due to static fields, through unclosed resources, incorrect implementations of equals() and hashCode(), inner classes that reference outer classes, incorrect use of lazy in Kotlin, context leaks, etc.
An example of code in which a memory leak may occur:

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)
        val button = findViewById<Button>(R.id.my_button)
        var data: String = "Initial data"
        button.setOnClickListener {
			// Here the lambda captures a reference to data
			// and will continue to use it even after
			// data has been destroyed.
            Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
        }
		// Let's assume we want to free data from memory,
		// so that a memory leak doesn't occur.
		// We do this by setting data to null.
        data = null // This will not remove the reference to data from the lambda.
    }
}

With their help, you can declare classes, interfaces, and methods where the data type is specified as a parameter, for example , in this case you do not need to do a type cast (Integer). If you use Object in Java or Any in Kotlin as a generic, you can use any data type, but then you need to cast to the desired one using brackets in Java or “as” in Kotlin.

Generics are not used in old Java code.

Generics are visible only at compile time, then the cast is performed to the type specified in the generic.

<? extends Object>:
Generics can also be masks, such as Collection or List

<T>:
Letters can also be generics. In this example, we limited T to inheritors of the NewClass class and the Comparable interface, so T will have the same properties.

class NewClass<T extends NewClass & Comparable> {
    T variable;
    NewClass method(T variable) {
        variable.compareTo(1);
        variable.method(new NewClass());
        List<T> list =  new ArrayList<>();
        List<NewClass> list2 =  new ArrayList<>();
        list.add(variable);
        list2.add(variable);
        return list.get(0);
    }
}

Kotlin also has:

Source<out T>:
specifies that type T is covariant, that is, type List is a subtype of type List. Type projection allows type T to be used only as a method return type or property value type, but not as a method parameter type or property value type.

interface Producer<out T> {
    fun produce(): T
}

Source<in T>:
specifies that type T is contravariant, that is, type List is a subtype of type List. Type projection allows type T to be used only as a method parameter or property value type, but not as a method return type or property value type.

interface Consumer<in T> {
    fun consume(item: T)
}

<reified T>:
Normally, at runtime, the type information of the arguments is removed, and you cannot access the types used as arguments to a generic function. However, by using the reified keyword, you can access the type of an argument at runtime.

inline fun <reified T> getTypeName() = T::class.simpleName

fun main() {
    println(getTypeName<String>()) // prints "String"
    println(getTypeName<Int>()) // prints "Int"
}

In this example, we define a generic function getTypeName() that uses the reified keyword to get the class name of type T at runtime. We call this function with different types, and it returns the class name for each type. The reified keyword can only be used with generic functions, not with classes or interfaces. Also, a generic type declared using the reified keyword can only be used in a context where the type is explicitly specified, such as when calling another function.

<*>:
called Star projection
is used to specify an unspecified argument type when using generic types.
When you use a generic type in Kotlin code, you can specify the argument type in angle brackets. But sometimes you may want to use a generic type but not specify a specific argument type, for example, if you want to use a type that can contain objects of different types.
In this case, you can use the <*> symbol instead of the argument type. For example:

val list: List<*> = listOf("foo", 42, Any())

In this example, we created a list of type List<>, which can contain objects of any type. We added three objects of different types to the list: the string “foo”, the number 42, and an object of type Any(). Now the list contains all three objects. The <> symbol can also be used in place of the argument type when creating an object of a generic class, for example:

val map: Map<String, *> = mapOf("foo" to 42, "bar" to "baz")
Function<*, String>: means Function<in Nothing, String>;
Function<Int, *>: means Function<Int, out Any?>;
Function<*, *>: means Function<in Nothing, out Any?>

wait(), notify(), notifyAll():
three methods from the multithreading suite

getClass():
get the class of an object at runtime. Mostly used for reflection.

clone():
get an exact copy of the object. Not recommended. The copy constructor is more often recommended.

equals():
compares two objects.

hashcode():
numeric representation of the object. The default value is an integer address in memory.

toString():
returns the representation of the object as a string. By default, it returns classname@hashcode in hexadecimal.

If hashcode is not overridden, then the default value will be returned.

finalize():
If the object interacts with some resources, for example, opens an output stream and reads from it, then such a stream must be closed before the object is removed from memory. To do this in Java, it is enough to override the finalize() method, which is called in the Java runtime environment immediately before deleting an object of this class. In the body of the finalize() method, you need to specify the actions that must be performed before the object is destroyed. The finalize() method is called only immediately before garbage collection

The Any class in Kotlin has the following methods:
equals(), hashCode(), toString()

A special class that represents an enumeration
In its simplest implementation it looks like this

enum Size { SMALL, MEDIUM, LARGE }

its functionality can be expanded

enum Size {
    SMALL("small"),
    MEDIUM("medium"),
    LARGE("large") {
        @Override
        String getSize() {
            return size + ", this is maximum size";
        }
    };
    String size;
    Size(String size) {
        this.size = size;
    }
    String getSize() {
        return size;
    }
byte ➜ 
short ➜ 
int ➜ 
long ➜ 
float ➜ 
double ➜ 
Byte() ➜ 
Object() ➜ 
Byte()… ➜ 
byte… ➜ 
short… ➜ 
int… ➜ 
long… ➜ 
float… ➜ 
double… ➜ 
Object()…

There are explicit and implicit conversions

Automatic widening conversions are possible:
byte -> short -> int -> long
int -> double
short -> float -> double
char -> int

Conversions with loss of information are possible:
int -> float
long -> float
long -> double

Explicit conversions with loss of data:

int a = 258;
byte b = (byte) a; // 2

double a = 56.9898;
int b = (int) a; // 56

Conversions during operations:
if one of the operands of the operation is of type double, then the second operand is converted to type double
if the previous condition is not met, and one of the operands of the operation is of type float, then the second operand is converted to type float
if the previous conditions are not met, one of the operands of the operation is of type long, then the second operand is converted to type long
otherwise all operands of the operation are converted to type int

Kotlin has:

as:
regular unsafe type cast

as?:
safe type cast, returns null on failure

is:
is this type

!is:
is not this type

Annotations in Java are labels in code that describe metadata for a function/class/package.

@interface AnnotationName {
    int getResult() default 1;
    String value();
}

@AnnotationName("value")
public class Main {
    @AnnotationName("value")
    String text;
    @AnnotationName(value = "value", getResult = 1)
    public static void main(String[] args) {
    }
    public static void testAnnotation(Object object) throws Exception {
        boolean haveAnnotation = !object.getClass().isAnnotationPresent(AnnotationName.class);
        Class<Main> demoClass = Main.class;
        AnnotatedElement annotatedElement = demoClass;
        Annotation[] annotations = annotatedElement.getAnnotations();
        Method method = demoClass.getMethod("testAnnotation");
        annotations = method.getAnnotations();
    }
}

Annotations are classified by storage type:

SOURCE: used only when writing code and ignored by the compiler
CLASS: saved after compilation, but ignored by the JVM
RUNTIME: saved after compilation and loaded by the JVM

Object type on which it is specified:

ANNOTATION_TYPE: another annotation
CONSTRUCTOR: class constructor
FIELD: class field
LOCAL_VARIABLE: local variable
METHOD: class method
PACKAGE: package description package
PARAMETER: method parameter public void hello(@Annontation String param){}
TYPE: specified above the class

Java SE 1.8 standard library of the language provides us with 10 annotations

@Override
Retention: SOURCE; Target: METHOD.
Shows that the method on which it is written is inherited from the parent class.

@Deprecated
Retention: RUNTIME; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE.
Indicates methods, classes or variables that are “obsolete” and may be removed in future versions of the product.

@SuppressWarnings
Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
Disables compiler warnings that concern the element over which it is specified. It is a SOURCE annotation specified over fields, methods, classes.

@Retention
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Specifies the “storage type” of the annotation over which it is specified. Yes, this annotation is used even for itself.

@Target
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Specifies the type of object over which the annotation we create can be specified. And it is also used for itself.

In Java, annotations are created using @interface, in Kotlin using annotation class

Wrappers in Java are classes that wrap primitive data types such as int, char, boolean, etc. so that they can be used as objects. These classes are in the java.lang package and have the following names:

Integer() for int
Character()
for char
Boolean()
for boolean
Short()
for short
Long()
for long
Double()
for double
Float()
for float
Byte()
for byte

Each of these classes contains methods and fields that allow you to work with primitive data types as objects. For example, the Integer class contains methods to convert numbers to strings, compare numbers, etc.
Wrappers are also used to pass primitive data types as parameters to methods that expect objects. For example, if a method expects an Object parameter, and you want to pass an integer, you can create an Integer object, wrap the number in it, and pass that object to the method.
Another advantage of wrappers is the ability to use null values. Primitive types cannot be null, but wrappers can. If you are not sure whether you will have a value for a variable or not, you can use a wrapper to avoid errors.
In Kotlin, wrappers are not used, instead the compiler itself decides when to use a primitive type and when a reference type

return:
by default, returns from the closest enclosing function or anonymous function

break:
terminates the execution of the loop

continue:
continues the execution of the loop from its next step, without processing the remaining code of the current iteration

Any expression in Kotlin can be marked with a label:

loop@ for (i in 1..100) {
        for (j in 1..100) {
            if (true)
                break@loop
        }
    }
fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@lit
        print(it)
    }
}
fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach
        print(it)
    }
}
return@a 1 // return 1 in label @a, rather than return expression with label (@a 1)

For optimal operation of the application, the JVM divides the memory into a stack area and a heap area. The stack works on a LIFO (last in, first out) scheme. Whenever a new method containing primitive values ​​or references to objects is called, a block of memory is allocated for them at the top of the stack. The heap is used for objects and classes. New objects are always created on the heap, and references to them are stored on the stack. A primitive field of a class instance is stored on the heap. The heap is used by all parts of the application, while the stack is used only by one thread of program execution.

The stack memory contains only local variables of primitive types and references to objects on the heap. Objects on the heap are accessible from anywhere in the program, while stack memory cannot be accessed by other threads. If the stack memory is completely occupied, the Java Runtime throws a java.lang.StackOverflowError, and if the heap memory is full, the java.lang.OutOfMemoryError: Java Heap Space exception is thrown.

Each thread running in the Java Virtual Machine has its own stack. The stack contains information about which methods the thread has called. As soon as a thread executes its code, the call stack changes. The thread stack contains all the local variables for each method being executed. A thread can only access its own stack.

When a method is called, the JVM creates a new block on the stack, called a “frame” or “stack frame”. This block stores the local variables of the method, as well as references to the objects passed to the method as arguments.
Each new method call adds a new frame to the stack, and the completion of the method removes the last frame from the stack. This mechanism is called the “stack machine”.

In addition, the stack is used to store return information from methods. When a method is called, the return address is pushed onto the stack so that the JVM knows where to return to after the method completes.

The stack is limited in size and can cause a stack overflow error if too many frames are created on the stack. This can happen, for example, if a method recursively calls itself too many times. In a default JVM configuration, the stack size can range from a few megabytes to a few tens of megabytes, depending on the JVM version and operating system. However, the stack size can be changed at JVM startup using the -Xss option. For example, to set the stack size to 1 megabyte, you would start the JVM with the -Xss1m option.

In a default JVM configuration, the heap size can range from a few hundred megabytes to a few gigabytes, depending on the JVM version and operating system. However, the heap size can be changed at JVM startup using the -Xms and -Xmx options. The -Xms option sets the initial heap size, and the -Xmx option sets the maximum heap size. For example, to set the initial heap size to 256 megabytes and the maximum heap size to 1 gigabyte, you would start the JVM with the -Xms256m -Xmx1g options.

Local variables are invisible to all other threads except the thread that created them. Even if two threads are executing the same code, they will still create local variables of that code on their own stacks. Thus, each thread has its own version of each local variable.

All local variables of primitive types (boolean, byte, short, char, int, long, float, double) are stored entirely on the thread stack and are not visible to other threads. One thread can pass a copy of a primitive variable to another thread, but it cannot share a primitive local variable.

The heap contains all objects created in your application, regardless of which thread created the object. This includes versions of primitive types (e.g. Byte, Integer, Long, etc.). Whether an object is created and assigned to a local variable or created as a member variable of another object, it is stored on the heap.

A local variable can be of a primitive type, in which case it is stored entirely on the thread stack.

A local variable can also be a reference to an object. In this case, the reference (local variable) is stored on the thread stack, but the object itself is stored on the heap.

An object can contain methods, and these methods can contain local variables. These local variables are also stored on the thread stack, even if the object to which the method belongs is stored on the heap.

Member variables of an object are stored on the heap along with the object itself. This is true whether the member variable is of a primitive type or a reference to an object.

Static variables of a class are also stored on the heap along with the class definition.

Objects on the heap can be accessed by all threads that have a reference to the object. When a thread has access to an object, it can also access the member variables of that object. If two threads call a method on the same object at the same time, they will both have access to the member variables of the object, but each thread will have its own copy of the local variables.

public static void main(String[] args) {
  int x = 10; // variable of type int is stored on stack
  String str = "Hello World!"; // object of type String is stored on the heap
  System.out.println(str);
}

In this example, the int variable x is stored on the stack because it is a primitive data type. The str variable, a String variable, is an object and is stored on the heap.

When the program is executed, the main method is pushed onto the stack, the x variable is created, and the value 10 is assigned to it. Then, a String object is created containing the string “Hello World!”. A reference to this object is stored in the str variable. When the println method is called, the value of the reference to the str object is passed to the method, and it prints the string “Hello World!” to the console.

After the main method is executed, all variables are removed from the stack, and the String object continues to exist on the heap as long as it is referenced by other parts of the program.

In addition to the stack and the heap, Java has a storage area for persistent data (PermGen or Metaspace, depending on the version of Java). This area contains class metadata, information about methods, variables, and other data related to the program structure itself. In Java 8 and higher, PermGen has been replaced by Metaspace.
The program code is compiled into bytecode, which is stored in a .class file. When the program is run, the bytecode is loaded into memory and interpreted by the Java Virtual Machine (JVM).

JDK (Java Development Kit):
This is a set of tools that developers use to create Java applications. The JDK includes the Java compiler, Java class libraries, utilities for developing and debugging Java applications, and other tools. The JDK is a complete installation for developing Java applications.

JRE (Java Runtime Environment):
This is the Java execution environment that is used to run Java applications. The JRE includes the Java Virtual Machine (JVM), Java class libraries, and other components needed to run Java applications. The JRE does not contain tools for developing Java applications.

JVM (Java Virtual Machine):
This is a virtual machine that enables Java code to run on a computer. The JVM translates Java bytecode into machine code that can be executed on a specific platform. The JVM is a key component of the Java platform because it provides the ability to write Java code once and have it run on any platform where the JVM is installed.

Each component is an important part of the Java platform, and they interact with each other. Developers use the JDK to create Java applications, and then the JRE is used to run those applications, using the JVM to execute Java code.

The default value depends on the variable type:

For numeric types (byte, short, int, long, float, double), the default value is 0.

For the char type, the default value is ‘\u0000’ (the ‘NUL’ character).

For the boolean type, the default value is false.

For reference types (any class, interface, array), the default value is null.

No, because this would violate Barbara Liskov’s substitution principle. Access level expansion is possible.

Strong Reference:
This is a reference to an object that guarantees that the object will not be deleted from memory as long as there is at least one such reference to it. When an object is managed by a strong reference, its life cycle is determined by the life cycle of the reference. If the reference is not used, the object is also not deleted from memory, which can lead to a memory leak.

String title = “hello”;

SoftReference:
objects created via SoftReference will be collected if the JVM requires memory. This means that all soft reference objects are guaranteed to be collected before the JVM throws an OutOfMemoryError. SoftReference is often used for caches that consume a lot of memory.

class MyClass {
    private var softRef: SoftReference<HeavyObject>? = null
    fun doSomething() {
        var heavyObj = softRef?.get()
        if (heavyObj == null) {
            heavyObj = HeavyObject()
            softRef = SoftReference(heavyObj)
        }
        // use heavyObj here
    }
}

class HeavyObject {
    // ...
}

In this example, a SoftReference is used to store a reference to a HeavyObject, which can occupy a large amount of memory. In the doSomething() method, a check is made to see if the SoftReference already exists and to save the HeavyObject. If the object has not yet been created or has already been deleted from memory, a new object is created and a reference to it is saved using the SoftReference. Otherwise, if the HeavyObject already exists in memory and there is a reference to it via a SoftReference, then the reference to it is returned for further use.
It is important to note that a SoftReference does not guarantee that the object will always be available in memory. If the system fills up the memory and there is not enough space to store the HeavyObject, the object may be deleted and the reference via the SoftReference will be automatically cleared. The next time the object is accessed, a new object is created and a reference to it is saved in the SoftReference.

WeakReference:
weaker than SoftReference, does not save the object from finalization, even if there is enough free memory. As soon as there are no strong or soft references to the object, it can be finalized. Used for caches and for creating chains of related objects.

private var activityNavController: WeakReference<NavController>? = null
// or
var person: Person? = Person("John")
val weakReference = WeakReference(person)
println("Before garbage collection: $weakReference")
person = null
System.gc()
println("After garbage collection: $weakReference")

This example creates an instance of the Person class and then creates a weak reference to it using WeakReference. Afterwards, the reference to the original object is deleted and garbage collection is called using System.gc(). At the end, information about the reference to the Person object before and after garbage collection is printed.
Note that after garbage collection, the reference to the Person object becomes null because the original object was deleted from memory. The reference to the object via WeakReference also becomes null, indicating that the object was deleted.

PhantomReference:
Objects created via PhantomReference are destroyed when the GC determines that the referenced objects can be freed. This type of reference is used as an alternative to finalization for more flexible resource release.

class MyObject {
    // resources associated with the object
}
val phantomReferenceQueue = ReferenceQueue<MyObject>()
val myObject = MyObject()
val phantomReference = PhantomReference(myObject, phantomReferenceQueue)
// perform some actions
// check if the object has been deleted from memory
if (phantomReferenceQueue.poll() != null) {
    // free resources associated with the object
}

Here we create an object MyObject and put it in a PhantomReference. Then we do some actions and if the object has been removed from memory, we free the resources associated with it. Note that we created a ReferenceQueue to track the removal of the object.

map:
is a function that takes a collection and a function that is applied to each element of the collection. The result is a new collection with the same number of elements as the original collection, but with modified values.

val numbers = listOf(1, 2, 3, 4)
val doubledNumbers = numbers.map { it * 2 }
println(doubledNumbers) // [2, 4, 6, 8]

flatMap:
is a function that takes a collection and a function that returns a collection. flatMap then flattens all the resulting collections into a single collection.

val numbers = listOf(1, 2, 3)
val nestedNumbers = numbers.flatMap { listOf(it, it * 2) }
println(nestedNumbers) // [1, 2, 2, 4, 3, 6]

The difference between map and flatMap is that map returns a collection of elements obtained by applying a function to each element of the source collection, while flatMap returns a collection of elements obtained by applying a function that returns a collection to each element of the source collection. Also, flatMap can be useful when you want to “unroll” nested collections into a single larger collection.

Usage in Java Stream API:
map and flatMap are often used in the context of the Stream API, which was introduced in Java 8. Map transforms the elements of a collection without changing the nesting level, while flatMap flattens multiple streams into a single flat stream.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
        List<Integer> doubledNumbers = numbers.stream()
                                              .map(n -> n * 2)
                                              .collect(Collectors.toList());
        System.out.println(doubledNumbers); // [2, 4, 6, 8]
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        List<Integer> flatMappedNumbers = numbers.stream()
                                                 .flatMap(n -> Arrays.stream(new Integer[]{n, n * 2}))
                                                 .collect(Collectors.toList());
        System.out.println(flatMappedNumbers); // [1, 2, 2, 4, 3, 6]
    }
}

Usage in Kotlin Flow:
When we talk about Kotlin Flow, map and flatMap have similar concepts but are applied to data flows.

All errors are inherited from the Throwable class

Exception:
errors during program execution

Error:
system errors during JVM operation, do not need to be caught with rare exceptions, as this is a serious error and will lead to the program crash

Exceptions are:

Checked:
they must be caught and checked by the compiler, the cause of occurrence is potential errors in methods
examples: Throwable, Exception, IOException, ReflectiveOperationException

Unchecked:
they do not need to be caught and are not checked by the compiler, the cause of occurrence is errors in the code
examples: RuntimeException, IndexOutOfBoundsException

Garbage collector, there are (in HotSpot Oracle JVM):

Serial Garbage Collection:
sequential collection of young and old generations.

Parallel Garbage Collection:
default collector in Java 8, works the same way as Serial GC, but using multithreading

CMS Garbage Collection:
Concurrent Mark-and-Sweep. Makes two short pauses with a complete stop of all threads, these pauses in total are less than the general cycle of background collection. If possible, carries out garbage collection in the background. The first pause is called initial mark, during this time the stack is analyzed, after which the heap is bypassed in the background, the mark phase begins. After this, you need to stop the application again and remark – make sure that while we were doing in the background, nothing has changed. And only after this does sweep occur in the background – cleaning up unnecessary areas.

G1 Garbage Collection:
The default collector since Java 9, the idea behind G1 is called pause goal. This parameter specifies how long the program can pause during execution for the sake of garbage collection, for example, 20 ms once every 5 minutes. The garbage collector does not guarantee that it will work exactly like this, but it will try to work with the specified desired pauses. This is fundamentally different from all the garbage collectors we have encountered before. The developer can control the garbage collection process much more flexibly. G1 divides the heap into areas (regions) of equal size, for example, 1 megabyte. Then a set of such regions is dynamically selected, which are called the young generation, while the concept of Eden and Survivor is preserved. But the selection is dynamic.

Memory areas:

Eden:
A region of dynamic memory in which objects are initially created. Many objects never leave this memory area, as they quickly become garbage.
When we write something in the form of new Object() we create an object in Eden.
Belongs to the young generation.

Survivor:
There are two survivor areas in memory. Or you can think of the survivor area as usually being divided in half. This is where objects that survived the “expulsion from Eden” (hence its name) end up. Sometimes these two spaces are called From Space and To Space. One of these areas is always empty, unless a collection process is in progress.
From Space, objects are either removed by GC or migrate to To Space — the last place before they become too old and move to Tenured.
Belongs to the young generation.

Tenured:
The area where surviving objects that are considered “old enough” end up (thus leaving the Survivor area).
The storage is not cleared during a young collection.
Usually, by default, objects that have survived 8 garbage collections are placed here.
Belongs to the old generation.

Permanent generation of memory Metaspace and PermGen:
Before Java 8, there was a special section: PermGen, where space was allocated for internal structures, such as class definitions. PermGen was not part of the dynamic memory, ordinary objects never got here.
Metadata, classes, interned strings, etc. were stored here — this was a special memory area of ​​the JVM.
Since it is quite difficult to understand the required size of this area, before Java 8, you could often see the java.lang.OutOfMemoryError error. This happened because this area overflowed unless you set enough memory for it, and it was possible to determine whether there was enough memory or not only by trial and error.
Therefore, starting with Java 8, it was decided to remove this area altogether and all the information that was stored there is either transferred to the heap, such as interned strings, or moved to the metaspace area, to native memory. The maximum Metaspace by default is not limited by anything except the limit of the native memory. But it can be optionally limited by the MaxMetaspaceSize parameter, which is essentially similar to MaxPermSize in the PermGen ancestor. Metaspace is not cleaned by GC and can be cleaned manually if necessary.

String Pool:
A memory area where strings are stored, its purpose is that strings can be repeated, and in this case one string is written with different references to it.

String text = "text";
// will be created in String Pool
String text = new String("text");
// will be created outside String Pool in Heap
String text = new String("text").intern();
// will be created in String Pool due to intern method

There are several types of garbage collection:
minor, major and full.

minor garbage collection:
cleans Eden and Survivor (young generation)

major garbage collection:
cleans old generation

full garbage collection:
cleans everything

You can write and read data to native memory using ByteBuffer and Unsafe.

Java NIO (New Input/Output) is a set of APIs included in Java since version 1.4 that provides more efficient and scalable methods for working with input/output compared to the classic IO (Input/Output) API. The NIO API is designed for developing high-performance applications that work with large amounts of data or handle many connections simultaneously.

The main features of Java NIO:

Channels:
Channels are an abstraction over data streams. They can be asynchronous and non-blocking.
Examples: FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel.

Buffers:
Buffers are used to store data read from or written to a channel.
Examples: ByteBuffer, CharBuffer, IntBuffer, FloatBuffer.

Selectors:
Selectors allow a single thread to manage multiple channels. This is useful for developing servers that handle multiple connections simultaneously.
Examples: Selector.

Non-blocking mode:
Channels can operate in non-blocking mode, which allows the thread to not be blocked when performing read or write operations.

initialization of static variables of the superclass (when the class is first accessed)
static initializer of the superclass (when the class is first accessed)
initialization of static variables (when the class is first accessed)
static initializer (when the class is first accessed)
superclass constructor (if there is no superclass, then the constructor of the Object class,
if the superclass constructor has parameters, they must be passed during the call to the constructor of the called class using
super(parameters) )
regular initializer
initialization of regular variables
class constructor

Yes, also can overwrite methods making them abstract, interface can’t

class One{
    void one(){}
}
abstract class Two extends One {
    @Override
    abstract void one();
}
class Three extends Two {
    @Override
    void one() {}
}

Java:

for (int i = 0; i < 10; i++){
}

for ( ; ; ) {
}

for (String text : array) {
}

do { 
} while (true)
  
while (true){
}

list.stream().forEach((k) -> {
})
  
list.stream().forEach(System.out::println);

Kotlin:

for (value in array){
}

for (x in 1..5){
}

for (x in 9 downTo 0 step 3) {
}

list.forEach { 
  print(it) 
}

val items = setOf("one", "two", "three")
when {
    "one" in items -> println("one")
    "two" in items -> println("two")
}
// checking an element in a collection will print one

There are three standard class loaders in Java, each of which loads a class from a specific location:

Bootstrap:
The base class loader, also called Primordial ClassLoader. Loads the standard JDK classes from the rt.jar archive

Extension ClassLoader:
The extension class loader. Loads extension classes that are located in the jre/lib/ext directory by default, but can be specified by the java.ext.dirs system property

System ClassLoader:
The system class loader. Loads application classes defined in the CLASSPATH environment variable

Every class loader except the base class is a descendant of the abstract java.lang.ClassLoader class. For example, the extension class is implemented by sun.misc.Launcher$ExtClassLoader, and the system class is implemented by sun.misc.Launcher$AppClassLoader. The base loader is native and its implementation is included in the JVM.
You can also implement your own ClassLoader, for this you need to extend ClassLoader and override its methods

public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}

When a class is run, it is verified so that the bytecode cannot be changed using a hex editor, thereby breaking the logic. If the class does not pass verification, the java.lang.VerifyError error is thrown.

The java.lang.SecurityManager class allows applications to implement security policy. Without a SecurityManager, permissions are unlimited.

A SecurityManager allows an application to determine, before performing a possibly unsafe or sensitive operation, what the operation is and whether it is being attempted in a security context that allows the operation. The application can allow or deny the operation.

The SecurityManager has many check… methods to check.
For example: checkAccept throws a SecurityException if the calling thread is not allowed to accept a socket connection from the specified host and port number. Permissions are typically found in the java.policy or default.policy file

SecurityManager security = System.getSecurityManager();
if (security != null) {
    security.checkAccept (String host, int port);
}

You can create your own file with permissions, and then either in vm options write -Djava.security.policy=src/…/my.policy or

System.setProperty("java.security.policy", "src/…/my.policy");
System.setSecurityManager(new SecurityManager());

An interface that allows you to compile code from a program

JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
int resultCode = javaCompiler.run(null, null, null ,”path/test.java”);

Before running, check if the JavaScript engine is installed

ScriptEngine engine = new ScriptEngineManager(null).getEngineByName("JavaScript");
// or
engine = new ScriptEngineManager(null).getEngineByName("js");
// or
engine = new ScriptEngineManager(null).getEngineByName("nashorn");
String code = "var q = 0; q + 1";
Object o = engine.eval(code);
System.out.println(o);

Optional in Java is a concept that was added in Java 8 to handle possible null values.

Optional<String> text = Optional.empty();
text = Optional.of("123");
if(text.isEmpty()) {
    System.out.println(text.get());
}

In Kotlin, instead of Optional, the “Nullable” construct is used.

In Kotlin, Nullable is used to denote variables that can contain null values. Operations with Nullable variables must use safe navigation operators (safe call operator — ?. and elvis operator — ?:) to avoid exceptions when trying to access a null value.

Optional in Kotlin also has a special data type called “Optional” or “Maybe”, which can be used in some situations when you want to explicitly indicate that a variable can contain a null or non-null value. The Optional type is created using the “Nullable” keyword as a modifier.

override fun getData(): Optional<Data> {
  val file = getFile()
  return if (!file.exists() || !file.isFile) {
    Optional.empty()
  } else {
    Optional.of(file)
  }
}

In Java:

To write signed numbers in Java, two’s complement is used, where the most significant digit is the signed digit. If its value is 0, then the number is positive, and its binary representation is no different from the representation of an unsigned number. For example, 0000 0001 in the decimal system is 1. If the most significant digit is 1, then we are dealing with a negative number. For example, 1111 1111 in the decimal system represents -1. Accordingly, 1111 0011 represents -13.

byte b = 7; // 0000 0111
short s = 7; // 0000 0000 0000 0111

&:
logical multiplication

int a1 = 2; //010
int b1 = 5; //101
System.out.println(a1 & b1); // result 0
int a2 = 4; //100
int b2 = 5; //101
System.out.println(a2 & b2); // result 4

|:
logical addition

int a1 = 2; //010
int b1 = 5; //101
System.out.println(a1 | b1); // result 7–111
int a2 = 4; //100
int b2 = 5; //101
System.out.println(a2 | b2); // result 5–101

^:
logical exclusive OR

~:
logical negation

a<<b:
shifts the number a to the left by b places. For example, the expression 4<<1 shifts the number 4 (which is 100 in binary) by one place to the left, resulting in the number 1000 or the number 8 in decimal representation.

a>>b:
shifts the number a to the right by b places. For example, 16>>1 shifts the number 16 (which is 10000 in binary) by one place to the right, resulting in 1000 or the number 8 in decimal representation.

a>>>b:
unlike the previous types of shifts, this operation is an unsigned shift — it shifts the number a to the right by b places. For example, the expression -8>>>2 will be equal to 1073741822.

In Kotlin:

shl(bits): signed left shift (<< in Java) shr(bits): signed right shift (>> in Java)
ushr(bits): unsigned right shift (>>> in Java)
and(bits): bitwise AND
or(bits): bitwise OR
xor(bits): bitwise exclusive OR
inv(): bitwise negation

A class that allows you to write or read data from native memory (not heap, faster). This memory is not cleared by Garbage Collector and must be cleared manually.

ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // size in bytes
byteBuffer.put(new byte[]{1, 2, 3}); // write to memory
byteBuffer.flip(); // remove all unfilled (everything after 1 2 3)
byteBuffer.position(0); // move cursor to starting position
byte[] bytes = new byte[byteBuffer.remaining()]; // size == 3 after flip or == 1024 without it
byteBuffer.duplicate().get(bytes); // get contents
byteBuffer.clear(); // don't forget to clear
// now bytes == 1 2 3

Unsafe allows you to work with memory directly

public class Main {
    public static void main(String[] args) throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe"); // we get the field using reflection
        field.setAccessible(true); // allow access
        Unsafe unsafe = (Unsafe) field.get(null); // get the field and cast to the class
        // let's try to write int
        long startAddressInDDR = unsafe.allocateMemory(1024L); // allocate memory in ddr in bytes
        unsafe.putInt(startAddressInDDR, 123);
        System.out.println(unsafe.getInt(startAddressInDDR));
        unsafe.freeMemory(startAddressInDDR); // clear the address
        // then a new instance of the class))
        Example example = (Example) unsafe.allocateInstance(Example.class);
        System.out.println(example.name); // null although the class has a constructor where you need to pass the name, and the name is initialized
        example.name = "name";
        System.out.println(example.name); // name
    }
}
class Example{
    Example(String name){
        this.name = name;
    }
    public String name = "1";
}
// Kotlin
fun main(args: Array<String>) {
    val field: Field = Unsafe::class.java.getDeclaredField("theUnsafe")
    field.isAccessible = true
    val unsafe: Unsafe = field.get(null) as Unsafe
    val example: Example = unsafe.allocateInstance(Example::class.java) as Example
    val name: String = example.name // according to syntax there can't be null here
    println(name) // but here is null
    example.name = "name"
    println(example.name) // name
}
data class Example(var name: String = "1")

Represents a set of instructions that can be extracted into a separate variable and then called multiple times in different places in the program.
A lambda expression is not executed by itself, but forms an implementation of a method defined in an interface.
An interface must contain only one method without an implementation.

interface Interface {
        String method(String parameter);
    }
    public static void main(String[] args) {
        // executes a method "method" in an anonymous class that takes a parameter as input, prints it and returns another parameter
        Interface one = (String parameter) -> {
            System.out.print(parameter);
            return "2";
        };
        System.out.print(one.method("1 "));
    }
// 1 2

To load executable code, a dynamic class loading mechanism is used. For this, you can use the java.net.URLClassLoader class, which allows you to load classes from various sources, including files on the local file system and remote files by URL.

import java.net.URL;
import java.net.URLClassLoader;

public class RemoteClassLoader {
    public static void main(String[] args) throws Exception {
        URL url = new URL("http://example.com/classes/MyClass.class");
        URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
        // Loading a class from a remote source
        Class<?> clazz = classLoader.loadClass("MyClass");
        // Create an instance of the class
        Object obj = clazz.newInstance();
        // Method call
        clazz.getMethod("someMethod").invoke(obj);
    }
}

Here we create an instance of URLClassLoader with an array of URLs containing the path to the class to load. Then we load the class with the loadClass() method and create an instance of the class with the newInstance() method. We can then call methods of the loaded class using reflection.

class from the java.nio.channels package in Java NIO that provides an asynchronous way to work with server sockets. This class allows you to create server sockets that can accept connections from clients without blocking the execution thread.

Key features of AsynchronousServerSocketChannel:

Asynchronous:
The methods of this class do not block the execution thread, allowing you to efficiently manage a large number of connections.

Functional callbacks:
Used to handle the completion of operations asynchronously.

Channel support:
Integration with other channels and buffers from Java NIO.
AsynchronousServerSocketChannel example

Let’s look at an example of creating a simple asynchronous server that accepts connections and reads data from clients.

import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.AsynchronousServerSocketChannel
import java.nio.channels.AsynchronousSocketChannel
import java.nio.channels.CompletionHandler
import java.nio.charset.StandardCharsets
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit

fun main() {
    val serverAddress = InetSocketAddress("localhost", 8080)
    val serverChannel = AsynchronousServerSocketChannel.open().bind(serverAddress)
    println("Server listening on port 8080")
    serverChannel.accept(null, object : CompletionHandler<AsynchronousSocketChannel, Void?> {
        override fun completed(clientChannel: AsynchronousSocketChannel, attachment: Void?) {
            // Accept the next connection
            serverChannel.accept(null, this)
            // Process the current connection
            handleClient(clientChannel)
        }
        override fun failed(exc: Throwable, attachment: Void?) {
            println("Failed to accept a connection")
            exc.printStackTrace()
        }
    })
    // Block the main thread so that the server continues to run
    Thread.currentThread().join()
}

fun handleClient(clientChannel: AsynchronousSocketChannel) {
    val buffer = ByteBuffer.allocate(1024)
    clientChannel.read(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> {
        override fun completed(result: Int, attachment: ByteBuffer) {
            attachment.flip()
            val message = StandardCharsets.UTF_8.decode(attachment).toString()
            println("Received message: $message")
            attachment.clear()
            // Sending response to the client
            val response = ByteBuffer.wrap("Message received".toByteArray(StandardCharsets.UTF_8))
            clientChannel.write(response, response, object : CompletionHandler<Int, ByteBuffer> {
                override fun completed(result: Int, buffer: ByteBuffer) {
                    if (buffer.hasRemaining()) {
                        clientChannel.write(buffer, buffer, this)
                    } else {
                        buffer.clear()
                        clientChannel.read(buffer, buffer, this)
                    }
                }
                override fun failed(exc: Throwable, buffer: ByteBuffer) {
                    println("Failed to write to client")
                    exc.printStackTrace()
                    try {
                        clientChannel.close()
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            })
        }
        override fun failed(exc: Throwable, attachment: ByteBuffer) {
            println("Failed to read from client")
            exc.printStackTrace()
            try {
                clientChannel.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    })
}

Client handling:
The handleClient method reads data from the client and writes a response.
clientChannel.read(buffer, buffer, object : CompletionHandler { … }) starts reading data asynchronously.
completed is called when the data is successfully read.
failed is called when the reading fails.

Sending a response to the client:
After reading the data, the server sends a response to the client using clientChannel.write(response, response, object : CompletionHandler { … }).

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 instantiated without creating an instance of the outer class.

However, if an inner class is marked with the inner keyword, it becomes non-static, and then it has access to non-static members of the outer class and requires an instance of the outer class to be instantiated.
An 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 display "Accessing outer field: Outer field"
}

Kotlin has five Scope Functions that allow you to change the scope of variables, as well as make your code easier to read and less likely to make mistakes:

let:
allows you to execute a block of code on an object passed as an argument and return the result of that block. Inside 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 use a reference to the object via this.

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

with:
executes a block of code that passes 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 
}

A regular function:
has no state and always starts “from scratch” (unless, of course, it uses global variables)
must complete its execution before returning control to the code that called it

A coroutine:
has a state and can pause and resume its execution at certain points, thus returning control before it completes its execution.
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 a processor context switch, so they work faster.

There are two functions for starting 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:
tracks any coroutine created with launch or async (these are extension functions on CoroutineScope).
The current work (running coroutines) can be canceled by calling scope.cancel() at any time.

Job:
is the control element of the coroutine. For each coroutine created (with launch or async), it returns a Job instance that 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 properties of the Job: isActive, isCancelled, and isCompleted.

CoroutineContext:
is the set of elements that define the behavior of the coroutine.
It consists of:
Job: manages the lifecycle of the coroutine.
CoroutineDispatcher: dispatches work to the appropriate thread.
CoroutineName: name of the coroutine, useful for debugging.
CoroutineExceptionHandler: handles uncaught exceptions, which will be discussed in Part 3 of the coroutine series.
Coroutines have delay(1000L) – a 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 calling delay using the delay() function, this coroutine releases the thread in which it was executed and is saved in memory. And the released thread can be used for other tasks. And when the launched task (for example, execution of the delay() function) completes, the coroutine resumes its work in one of the free threads.

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
}

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 main 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 calculated once, on the first access
properties whose change events can be subscribed to (observable properties)
properties stored in an associative list, and not in separate fields
For such cases, Kotlin supports delegated properties.
The expression after by is a delegate: accesses (get(), set()) to the property will be processed by this expression. The delegate is not required 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 to me '${property.name}'!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value was assigned a 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"
}
// out 
// <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
))

Kotlin allows you to extend a class by adding new functionality.
Extensions do not actually modify the classes they extend. When you declare an extension, you create a new function, not a new class member. Such functions can be called using 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, not by the type of the expression computed at runtime, as with virtual functions.

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

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()
    }
}
var stringRepresentation: String
    get() = this.toString()
    set(value) { 
      setDataFromString(value) 
    }
var setterVisibility: String = "abc" 
    private set // setter has private access and default implementation

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.

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) {
	// pass to lambda and pass String to it, don't expect anything back (Unit)
    callback("1 ")
}
fun two(callback: (String) -> String) {
	// pass to lambda and pass String to it, expect String back
    callback("2 ")
    print(callback("3 "))
}
fun three(callback: () -> String) {
	// call the lambda but don't pass anything to it, 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

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

All function types have a list of parameter types enclosed in parentheses 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 may 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 a parameter B and a 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 parameter.

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

Parentheses can be used to combine function types: (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 function type an alternate name using type aliases:
typealias ClickHandler = (Button, ClickEvent) -> Unit

Creating a function type:
There are several ways to get an instance of a function type:
Using a code block inside a function literal of 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 the function type as an interface:

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

Constant time (O(1)):
operations are performed in constant time, independent of the collection size. Examples of operations with such complexity: adding and removing elements from a HashSet, getting an element by index from an Array.

Logarithmic time (O(log n)):
operations are performed in time logarithmically dependent on the collection size. Examples of operations with such complexity: adding and removing elements from a TreeSet, searching for an element in a TreeMap.

Linear time (O(n)):
operations are performed in time linearly dependent on the collection size. Examples of operations with such complexity: searching for an element in an ArrayList, deleting an element from a LinkedList.

Linearithmic time (O(n log n)):
operations are performed in time linearly multiplied by the logarithm of the collection size. Examples of operations with such complexity: sorting elements in a List using the Merge Sort algorithm.

Quadratic time (O(n²)):
operations are performed in time quadratic in the collection size. Examples of operations with this complexity estimate: sorting elements in a List using the Bubble Sort algorithm.

Exponential time (O(2^n)):
operations are performed in time exponential in the collection size. Examples of operations with this complexity estimate: computing all subsets of a set.

Singleton:
ensures that a class has only one instance and provides a global point of access to it.

public class Singleton {
    private static Singleton instance = null;
    private Singleton() { 
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
object Singleton {
    init {
        println("Singleton instance has been created.")
    }
    fun doSomething() {
        println("Singleton is doing something.")
    }
}

Observer:
establishes dependencies between objects in such a way that if one object changes, all its dependent objects are notified and automatically updated.

Builder:
allows you to create objects using a step-by-step process where you can specify various parameters, and ultimately get an object with certain properties.

class Person private constructor(
    val firstName: String?,
    val lastName: String?,
) {
    
    class Builder {
        private var firstName: String? = null
        private var lastName: String? = null
        fun firstName(firstName: String) = apply { this.firstName = firstName }
        fun lastName(lastName: String) = apply { this.lastName = lastName }
        fun build() = Person(firstName, lastName, age, city, country)
    }
}
val person = Person.Builder()
                .firstName("John")
                .lastName("Doe")
                .build()

Factory:
Provides a common interface for creating objects, but delegates the actual creation of objects to subclasses.

interface Transport {
    fun deliver(): String
}

class Car : Transport {
    override fun deliver() = "Delivering by car"
}

class Truck : Transport {
    override fun deliver() = "Delivering by truck"
}

enum class TransportType {
    CAR,
    TRUCK,
}

object TransportFactory {
    fun createTransport(transportType: TransportType): Transport {
        return when (transportType) {
            TransportType.CAR -> Car()
            TransportType.TRUCK -> Truck()
        }
    }
}

val car = TransportFactory.createTransport(TransportType.CAR)
println(car.deliver()) // "Delivering by car"

val truck = TransportFactory.createTransport(TransportType.TRUCK)
println(truck.deliver()) // "Delivering by truck"

Adapter:
Transforms the interface of one class into the interface expected by another class so that they can communicate with each other.

interface MediaPlayer {
    fun play(audioType: String, fileName: String)
}

interface AdvancedMediaPlayer {
    fun playVlc(fileName: String)
    fun playMp4(fileName: String)
}

class VlcPlayer : AdvancedMediaPlayer {
    override fun playVlc(fileName: String) {
        println("Playing vlc file. Name: $fileName")
    }
    override fun playMp4(fileName: String) {
        // do nothing
    }
}

class Mp4Player : AdvancedMediaPlayer {
    override fun playVlc(fileName: String) {
        // do nothing
    }
    override fun playMp4(fileName: String) {
        println("Playing mp4 file. Name: $fileName")
    }
}

class MediaAdapter(audioType: String) : MediaPlayer {
    private val advancedMediaPlayer: AdvancedMediaPlayer?
    init {
        when (audioType) {
            "vlc" -> advancedMediaPlayer = VlcPlayer()
            "mp4" -> advancedMediaPlayer = Mp4Player()
            else -> advancedMediaPlayer = null
        }
    }
    override fun play(audioType: String, fileName: String) {
        when (audioType) {
            "vlc" -> advancedMediaPlayer?.playVlc(fileName)
            "mp4" -> advancedMediaPlayer?.playMp4(fileName)
            else -> println("Invalid media. $audioType format not supported")
        }
    }
}

class AudioPlayer : MediaPlayer {
    private val mediaAdapter: MediaAdapter?
    override fun play(audioType: String, fileName: String) {
        when (audioType) {
            "mp3" -> println("Playing mp3 file. Name: $fileName")
            "vlc", "mp4" -> {
                mediaAdapter = MediaAdapter(audioType)
                mediaAdapter.play(audioType, fileName)
            }
            else -> println("Invalid media. $audioType format not supported")
        }
    }
}

Decorator:
dynamically adds new functionality to objects by wrapping them in other objects that have that functionality.

abstract class Beverage {
    abstract val description: String
    abstract fun cost(): Double
}

abstract class CondimentDecorator : Beverage()

class Milk(private val beverage: Beverage) : CondimentDecorator() {
    override val description = "${beverage.description}, Milk"
    override fun cost() = beverage.cost() + 0.1
}

val espresso = Espresso()
val latte = Milk(Whip(Mocha(espresso)))

Facade:
provides a unified interface to a group of interfaces in a subsystem, thus simplifying interaction with it.

class OrderProcessor {
    fun processOrder(order: Order): String {
        val warehouse = Warehouse()
        val paymentSystem = PaymentSystem()
        val warehouseResult = warehouse.checkInventory(order)
        if (warehouseResult == "available") {
            val paymentResult = paymentSystem.processPayment(order)
            if (paymentResult == "success") {
                warehouse.updateInventory(order)
                return "Order processed successfully"
            }
        }
        return "Order processing failed"
    }
}

class Warehouse {
    fun checkInventory(order: Order): String {
        // check inventory
        return "available"
    }
    fun updateInventory(order: Order) {
        // update inventory
    }
}

class PaymentSystem {
    fun processPayment(order: Order): String {
        // process payment
        return "success"
    }
}

class Order {
    // order details
}

class OrderFacade {
    fun processOrder(order: Order): String {
        val warehouseResult = checkInventory(order)
        if (warehouseResult == "available") {
            val paymentResult = processPayment(order)
            if (paymentResult == "success") {
                updateInventory(order)
                return "Order processed successfully"
            }
        }
        return "Order processing failed"
    }
    private fun checkInventory(order: Order): String {
        val warehouse = Warehouse()
        return warehouse.checkInventory(order)
    }
    private fun updateInventory(order: Order) {
        val warehouse = Warehouse()
        warehouse.updateInventory(order)
    }
    private fun processPayment(order: Order): String {
        val paymentSystem = PaymentSystem()
        return paymentSystem.processPayment(order)
    }
}

Template Method:
defines the core of an algorithm, but allows subclasses to override some steps of that algorithm without changing its overall structure.

abstract class Pizza {

    fun make() {
        prepareDough()
        addIngredients()
        bakePizza()
        cutPizza()
    }

    protected fun prepareDough() {
        println("Preparing pizza dough")
    }

    protected abstract fun addIngredients()

    protected fun bakePizza() {
        println("Baking pizza")
    }

    protected fun cutPizza() {
        println("Cutting pizza")
    }
}

class PepperoniPizza : Pizza() {

    override fun addIngredients() {
        println("Adding pepperoni to pizza")
    }
}

class MargheritaPizza : Pizza() {

    override fun addIngredients() {
        println("Adding mozzarella and basil to pizza")
    }
}

fun main() {
    val pepperoniPizza = PepperoniPizza()
    pepperoniPizza.make()

    val margheritaPizza = MargheritaPizza()
    margheritaPizza.make()
}

// Preparing pizza dough
// Adding pepperoni to pizza
// Baking pizza
// Cutting pizza
// Preparing pizza dough
// Adding mozzarella and basil to pizza
// Baking pizza
// Cutting pizza

Strategy:
When using the strategy pattern, we move some logic out of the main code into separate classes that implement a common interface. In the main code, we can then choose the right implementation depending on certain conditions, without having to use a lot of conditional statements.

interface PaymentStrategy {
    fun pay(amount: Double)
}

class CreditCardStrategy(private val cardNumber: String, private val cvv: String) : PaymentStrategy {
    override fun pay(amount: Double) {
        // logic for payment by credit card
    }
}

class PayPalStrategy(private val email: String, private val password: String) : PaymentStrategy {
    override fun pay(amount: Double) {
        // PayPal payment logic
    }
}

class PaymentProcessor(private val paymentStrategy: PaymentStrategy) {
    fun processPayment(amount: Double) {
        paymentStrategy.pay(amount)
    }
}

// usage
val paymentStrategy = if (useCreditCard) {
    CreditCardStrategy(cardNumber, cvv)
} else {
    PayPalStrategy(email, password)
}
val paymentProcessor = PaymentProcessor(paymentStrategy)
paymentProcessor.processPayment(amount)

Inheritance and composition are two different approaches to class design in object-oriented programming.

Inheritance:
is a mechanism that allows you to create a new class based on an existing class (base class or superclass). In this case, the new class (subclass or derived class) inherits all the properties and methods of the base class, and can also add its own properties and methods.

// Base class
open class Person(val name: String, val age: Int) {
    open fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

// Subclass with additional parameters in the constructor
class Employee(name: String, age: Int, val jobTitle: String) : Person(name, age) {
    override fun introduce() {
        println("Hi, my name is $name, I am $age years old and I work as a $jobTitle.")
    }
}

fun main() {
    val employee = Employee("John", 30, "Software Developer")
    employee.introduce()  // Output: Hi, my name is John, I am 30 years old and I work as a Software Developer.
}

Composition:
is a mechanism by which objects of one class use objects of other classes to perform their functions. The class that uses another class is called a composite (or component), and the class whose objects are used is called a composite. A composite class contains objects of other classes as its properties and uses their methods to implement its functions.

class Engine(val type: String, val horsepower: Int) {
    fun start() {
        println("Engine of type $type with $horsepower HP started.")
    }
}

// Class that uses composition
class Car(val model: String, val engine: Engine) {
    fun drive() {
        engine.start()
        println("Driving a $model.")
    }
}

fun main() {
    val engine = Engine("V8", 450)
    val car = Car("Mustang", engine)
    car.drive()  // Output: Engine of type V8 with 450 HP started.
                 //         Driving a Mustang.
}

Differences between inheritance and composition:

Relationship:
Inheritance is an “is-a” relationship, where a subclass extends a superclass. Composition is a “has-a” relationship, where an object of one class has a reference to an object of another class.

Flexibility:
Composition is more flexible than inheritance. In inheritance, a change in the superclass may affect all subclasses, which is not always desirable. In composition, objects can be easily replaced by other objects if they implement the required interface or abstract class.

Reusability:
Composition allows for a higher degree of code reuse, since objects can be used in different contexts. In inheritance, code reuse is only possible within the context of the inheritance hierarchy.

Liskov Substitution Principle:
If used incorrectly, inheritance can violate the Liskov substitution principle, which states that any instance of a class should be replaceable by any instance of its subclass without changing the correctness of the program. In composition, this principle is not violated, since objects of different classes can be replaced by objects implementing the same

Principles collected by Robert Martin, author of Clean Architecture, from which I will quote the answers

Single Responsibility Principle:
The principle of a single responsibility. A class should have only one reason to change.

A bad implementation option, since it has many reasons to change:

class Man {
    void work() {}
    void eat() {}
    void sleep() {}
}

The correct implementation, where each class has only one reason to change:

class Man {
}

class Work extends Man {
    void work() {}
}

class Eat extends Man {
    void eat() {}
}

class Sleep extends Man {
    void sleep() {}
}
// pattern Facade
class ManFacade {
    Work work = new Work();
    Eat eat = new Eat();
    Sleep sleep = new Sleep();
    void work() {
        work.work();
    }
    void eat() {
        eat.eat();
    }
    void sleep() {
        sleep.sleep();
    }
}

Open-Closed Principle:
open/closed principle, software entities should be open for extension and closed for modification

Bad implementation option, since the dependency direction is Main -> Toyota:

public class Main {
    public static void main(String[] args) {
        SportToyota sportToyota = new SportToyota();
        workInTaxi(sportToyota);
    }
    static void workInTaxi(Toyota toyota){
        if(toyota instanceof SportToyota){
            ((SportToyota) toyota).getOnePassenger();
        } else {
            toyota.getFourPassengers();
        }
    }
}
class Toyota {
    void getFourPassengers(){ }
}
class SportToyota extends Toyota {
    void getOnePassenger(){ }
}

The correct implementation option, since the direction of dependencies is Main -> Car <- Toyota:

public class Main {
    public static void main(String[] args) {
        SportToyota sportToyota = new SportToyota();
        workInTaxi(sportToyota);
    }
    static void workInTaxi(Car car){
        car.workInTaxi();
    }
}
interface Car {
    void workInTaxi();
}
class Toyota implements Car {
    void getFourPassengers(){ }
    @Override
    public void workInTaxi() {
        getFourPassengers();
    }
}
class SportToyota extends Toyota {
    void getOnePassenger(){ }
    @Override
    public void workInTaxi() {
        getOnePassenger();
    }
}

Liskov Substitution Principle:
Barbara Liskov’s substitution principle. To create software systems from interchangeable parts, these parts must conform to a contract that allows these parts to be replaced with each other. When inheriting, we must not affect the functionality of parent classes

A bad implementation, it changes the operation of the parent class methods:

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(10);
        rectangle.setWeight(12);
        rectangle.getSquare();
        rectangle = new Square();
        rectangle.setHeight(10);
        rectangle.setWeight(12);
        rectangle.getSquare();
        // there will be an error here
    }
}
class Rectangle {
    protected int weight;
    protected int height;
    void setWeight(int weight) {
        this.weight = weight;
    }
    void setHeight(int height) {
        this.height = height;
    }
    int getSquare() {
        return weight * height;
    }
}
class Square extends Rectangle {
    @Override
    void setWeight(int weight) {
        this.weight = weight;
        this.height = weight;
    }
    @Override
    void setHeight(int height) {
        this.height = height;
        this.weight = height;
    }
    @Override
    int getSquare() {
        return weight * height;
    }
}

The correct implementation option:

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(10);
        rectangle.setWeight(12);
        rectangle.getSquare();
        Square square = new Square();
        square.setSide(10);
        square.getSquare();
    }
}
interface Shape{
    int getSquare();
}
class Rectangle implements Shape {
    protected int weight;
    protected int height;
    void setWeight(int weight) {
        this.weight = weight;
    }
    void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int getSquare() {
        return weight * height;
    }
}
class Square implements Shape {
    int side;
    public void setSide(int side) {
        this.side = side;
    }
    @Override
    public int getSquare() {
        return side * side;
    }
}

Interface Segregation Principle:
Avoid depending on anything that is not used. There should be no situations where interface methods are not used in their implementation.

A bad implementation, in which the work method is not used for Intern:

interface Worker {
    void work();
    void eat();
}
class Man implements Worker {
    @Override
    public void work() {
        System.out.println("work");
    }
    @Override
    public void eat() {
        System.out.println("eat");
    }
}
class Intern implements Worker {
    @Override
    public void work() {
        // intern is study, not work
    }
    @Override
    public void eat() {
        System.out.println("eat");
    }
}

The correct implementation option is to split the interface into several:

interface Worker {
    void work();
}
interface Eater {
    void eat();
}
interface Person extends Worker, Eater {
}
class Man implements Person {
    @Override
    public void work() {
        System.out.println("work");
    }
    @Override
    public void eat() {
        System.out.println("eat");
    }
}
class Intern implements Eater {
    @Override
    public void eat() {
        System.out.println("eat");
    }
}

Dependency Inversion Principle:
The principle of dependency inversion. Code implementing high-level policy should not depend on code implementing low-level details. Instead, the details should depend on the policy. All dependencies in the source code cross this boundary in one direction, toward abstraction.
The abstract component contains all the high-level business rules of the application. The concrete component contains the implementation details of these rules.

Higher-order functions are functions that can take other functions as arguments or return functions as results. This means that higher-order functions can be considered first-class objects in a programming language.

In languages ​​that support higher-order functions, functions can be used as values ​​and passed to other functions. For example, a higher-order function can take another function as an argument and apply it to different data.

Higher-order functions can be used to create more abstract and flexible algorithms and APIs. They can simplify code, reduce duplication, and improve readability.

// example of a higher order function that takes a function as an argument
fun applyOperation(
  value: Int, 
  operation: (Int) -> Int
): Int {
    return operation(value)
}
// example of using higher order function
val result = applyOperation(5) { value -> 
    value * 2 
} // the result will be 10

In this example, the applyOperation function takes an argument operation, which is a function that takes one argument of type Int and returns a result of type Int. Inside the applyOperation function, operation is called with the argument value, and the result is returned from the applyOperation function.

There are 3 paradigms in programming, they were discovered between 1958 and 1968. Each of the paradigms imposes restrictions on the code – the paradigms tell us what we can’t do.

Structured programming imposes a restriction on direct control transfer (for example, using the goto / jump operator)

Object-oriented programming imposes a restriction on indirect control transfer

Functional programming imposes a restriction on assignment

See Robert Martin, Clean Architecture

JSON(JavaScript Object Notation):
was created as an alternative to more complex data interchange formats such as XML, and is intended to be easy to read and write for both humans and computers. The JSON format is a set of key-value pairs, where the key is a string, and the value can be any valid data type, including objects, arrays, numbers, strings, booleans, and null.

SOAP (Simple Object Access Protocol):
is based on XML (Extensible Markup Language) and defines the format of the message, as well as the rules for processing it.

Protobuf(Protocol Buffers):
a binary format that is not human-readable, unlike formats such as XML and JSON. Instead, it is a sequence of bytes that can only be parsed with a special tool. The protobuf protocol itself defines a data description language that is used to describe the structure of the data that will be transmitted over the network. This language has its own syntax that is used to describe the fields and their data types. Each field in the data structure has a unique identifier and data type. Allows compact and efficient serialization of data for transmission over a network, while providing high performance and compatibility between different versions.

SELECT field1, field2 FROM table name WHERE condition GROUP BY …
INSERT INTO table_name SET field1=value1, field2=value2, field3=value3
UPDATE table_name SET field1=value1, field2=value2, field3=value3 WHERE condition_by_which_rows_should_be_selected
DELETE FROM table_name WHERE condition
SELECT COUNT(field) FROM table_name WHERE condition

CopyOnWrite collections:
all operations on changing the collection (add, set, remove) result in the creation of a new copy of the internal array. This ensures that ConcurrentModificationException will not be thrown when iterating over the collection.
CopyOnWriteArrayList, CopyOnWriteArraySet

Scalable Maps:
improved implementations of HashMap, TreeMap with better support for multithreading and scalability.
ConcurrentMap, ConcurrentHashMap, ConcurrentNavigableMap, ConcurrentSkipListMap, ConcurrentSkipListSet

Non-Blocking Queues:
thread-safe and non-blocking implementations of Queue on linked nodes.
ConcurrentLinkedQueue, ConcurrentLinkedDeque Blocking Queues: BlockingQueue, ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue, BlockingDeque, LinkedBlockingDeque, TransferQueue, LinkedTransferQueue

The volatile keyword in Kotlin (and in Java) is used to declare variables whose values ​​can be changed by different threads. It ensures that when a variable is changed, the value is immediately written to and read from memory, and is not cached in processor registers, which can lead to synchronization and change visibility issues. The volatile keyword can be applied to variables of type Boolean, Byte, Char, Short, Int, Long, Float, Double, and reference types.

@Volatile
private var running: Boolean = false

In this example, the running variable will be updated in memory immediately after it is changed by any thread, and threads that use its value will see the most recent value in memory.

is a keyword in Kotlin (and in Java) that is used to synchronize access to shared resources in multithreaded applications. When multiple threads try to access a shared resource at the same time, problems can arise, such as ambiguity in the state of the resource or its corruption. synchronized avoids these problems by ensuring that only one thread at a time can access the shared resource.

synchronized(lock) {
    // block of code where the shared resource is accessed
}
@Synchronized
fun getCounter(): Int {
   return counter
}

Coroutines cannot be synchronized because synchronized is a keyword in Java that is used to synchronize access to shared resources between multiple threads. Instead, to synchronize access to shared resources between coroutines in Kotlin, you should use other synchronization mechanisms, such as atomic variables, locks, or mutexes. For example, you can use a mutex from the Kotlin standard library:

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex

val mutex = Mutex()

fun main() = runBlocking {
    launch {
        mutex.withLock {
            // code to be executed synchronously
        }
    }
}

This example uses the Kotlin standard library’s Mutex, which allows you to lock access to a shared resource inside a withLock block. This ensures that the code inside the withLock block is only executed by one coroutine at any given time.

Race condition:
a situation in which the outcome of a program’s execution depends on which threads execute first or last.

var count = 0

fun main() {
    Thread {
        for (i in 1..100000) {
            count++
        }
    }.start()
    Thread {
        for (i in 1..100000) {
            count++
        }
    }.start()
    Thread.sleep(1000)
    println("Count: $count")
}

This example creates two threads, each incrementing the count variable by 1 the same number of times. After these threads complete their execution, the main thread prints out the value of the count variable. However, since the two threads can execute in parallel, the result depends on which thread finishes first, and may be unexpected.

For example, the result may be 199836 on one run of the program, and 200000 on another. This is because the count variable is not used atomically (is not protected by synchronization mechanisms), and two threads can modify its value at the same time. As a result, the variable’s values ​​can be overwritten, and the final value may be smaller than expected.

To avoid race conditions in such cases, it is necessary to use synchronization and protection mechanisms such as locks and atomic variables, which can ensure correctness of operations and avoid unexpected results.

Deadlock:
occurs when two or more threads are blocked, waiting for each other to release resources needed to continue execution.

Incorrect use of synchronized: synchronized can be used incorrectly, which can lead to incorrect execution order or blocked threads.

To avoid these problems, Kotlin can use synchronization facilities such as mutex, lock, atomic variables, and others, as well as modern multithreaded programming practices such as using immutable data structures and limiting changes to shared data only inside critical sections.

class Resource(private val name: String) {
    @Synchronized fun checkResource(other: Resource) {
        println("$this: checking ${other.name}")
        Thread.sleep(1000)
        other.checkResource(this)
    }
}

fun main() {
    val resource1 = Resource("Resource 1")
    val resource2 = Resource("Resource 2")
    Thread {
        resource1.checkResource(resource2)
    }.start()
    Thread {
        resource2.checkResource(resource1)
    }.start()
}

In this example, two Resource objects are created, each synchronized using the @Synchronized annotation. Then, two threads are created, each calling the checkResource() method on different resources in different orders. So, if the first thread locks resource1 and the second thread locks resource2, both threads will wait for each other and will not be able to complete their execution, which will result in a Deadlock. Deadlock example in Kotlin can be avoided if the communication between threads occurs using shared locks, for example, synchronized or lock() from the Kotlin standard library. You can also use the wait() and notify() methods to manage threads and avoid deadlocks.

Livelock:
a situation in which two or more threads continue to perform actions to avoid a deadlock, but are unable to complete their work. As a result, they are in an infinite loop, consuming more and more resources without performing any useful work.

data class Person(val name: String, val isPolite: Boolean = true) {
    fun greet(other: Person) {
        while (true) {
            if (isPolite) {
                println("$name: After you, ${other.name}")
                Thread.sleep(1000)
                if (other.isPolite) {
                    break
                }
            } else {
                println("$name: No, please, after you, ${other.name}")
                Thread.sleep(1000)
                if (!other.isPolite) {
                    break
                }
            }
        }
    }
}

fun main() {
    val john = Person("John", true)
    val jane = Person("Jane", false)
    Thread {
        john.greet(jane)
    }.start()
    Thread {
        jane.greet(john)
    }.start()
}

This example creates two Person objects, each of which can be polite or impolite depending on the value of the isPolite property. It then creates two threads, each of which calls the greet() method on the other object. The greet() method uses a while loop to check whether the other object is polite and, depending on this, continue or stop.

If both objects are polite, they will alternately ask each other to leave first and will not be able to finish their conversation. If both objects are impolite, they will refuse to leave first and will also not be able to finish their conversation. Thus, both threads will continue their work in an infinite loop without performing any useful work, which is an example of a Livelock.

A Livelock example can be avoided, for example, by using timers and limiting the time of execution of operations, so that the threads can complete their work and avoid an infinite loop. Synchronization and locks can also be used to ensure correct communication between threads.

Lock:
is a synchronization mechanism that is used to prevent contention for access to shared resources between multiple threads in a multithreaded environment. When a thread uses a lock, it gets exclusive access to the resource associated with the lock, and other threads that attempt to access that resource are blocked until the first thread releases the lock.

ReentrantLock:
is an implementation of the Lock interface in Java that allows a thread to acquire and release a lock multiple times. It is called “reentrant” because it allows a thread to acquire the lock again if that thread already has access to the locked resource.
ReentrantLock allows you to control the locking mechanism more closely than with synchronized blocks, as it provides some additional features, such as the ability to suspend and resume threads that are waiting to access a locked resource, the ability to set a timeout on waiting for access to a resource, and the ability to use multiple condition variables to control access to a resource.

Buffer:
buffer all elements that have been sent until the consumer is ready to process them. This strategy can lead to out of memory if the producer generates elements too quickly or the consumer processes elements too slowly.

Drop:
drops elements that cannot be processed by the consumer. This strategy does not guarantee that all elements will be processed, but it avoids out of memory.

Latest:
keeps only the last element and discards all others. This strategy is suitable for scenarios where only the last element matters.

Error:
signals an error if the thread cannot process the data.

Missing:
if the thread cannot process the data, it will simply skip it without warning.

Cold Observable:
Does not broadcast objects until at least one subscriber has subscribed to it;
If an observable has multiple subscribers, it will broadcast the entire sequence of objects to each subscriber.

Hot Observable:
Broadcasts objects when they appear, regardless of whether there are subscribers;
Each new subscriber receives only new objects, not the entire sequence.

The volatile keyword and Atomic* classes in Java are used to ensure thread safety when accessing shared memory.

volatile:
ensures that the variable’s value will always be read from shared memory, not from the thread cache, which prevents synchronization errors. In addition, writing to a volatile variable is also written directly to shared memory, not to the thread cache.

Atomic classes:
ensure that operations with variables are atomic. That is, they guarantee that read and write operations are performed as a single, indivisible action. Atomic classes are implemented using hardware support mechanisms, so they can be more efficient in some cases than using volatile.

In general, if you only need to ensure thread safety when accessing shared memory, then volatile can be used. If you need to perform an atomic operation on a variable, then you should use Atomic* classes.

One of the mechanisms for working with asynchronous operations in Kotlin is using coroutines. They are objects that represent the result of an asynchronous operation.

Deferred coroutines are created using the async function and can be used to perform long operations in a background thread, while the main application thread remains free.
Once a deferred coroutine completes its work, its result can be obtained using the await() function. Unlike the join() function, which blocks the calling thread until the coroutine completes, await() does not block the calling thread, but returns a value only when it is ready.
Here is an example of using deferred coroutines in Kotlin:

fun loadDataAsync(): Deferred<List<Data>> = GlobalScope.async {
    // loading data from network or database in background thread
}
fun displayData() {
    GlobalScope.launch {
        // start a coroutine to load data
        val deferredData = loadDataAsync()
        // perform some actions in the main thread
		// get the result of executing the deferred coroutine
        val data = deferredData.await()
        // process data
    }
}

LAZY coroutines are a way to create deferred coroutines in Kotlin using the lazy function and async from the kotlinx.coroutines library.
The main difference between deferred coroutines created using the async function and LAZY coroutines is the moment of creation of the coroutine object. Deferred coroutines are created immediately when the async function is called, while LAZY coroutines are created only when they are called for the first time.
Typically, LAZY coroutines are used in cases where there is no need to immediately start executing the coroutine, for example, when we do not know whether its execution will be necessary at all. This allows us to avoid unnecessary resource costs and speed up the application.
Here is an example of creating a LAZY coroutine:

val lazyCoroutine: Lazy<Deferred<String>> = lazy { 
    GlobalScope.async { 
        // execute coroutine in background thread
        "Hello from coroutine!" 
    } 
}

fun main() { 
	// perform actions in the main thread
	// get the result of the coroutine execution
    val result = runBlocking { lazyCoroutine.value.await() } 
    // processing the result
    println(result) 
}

// or

fun main() = runBlocking<Unit> {
    val lazyCoroutine = launch(start = CoroutineStart.LAZY) {
        println("Coroutine is executing")
    }
    // perform other actions in the main thread
    lazyCoroutine.start() // start coroutine
    // perform other actions in the main thread
    lazyCoroutine.join() // wait for coroutine to complete
}

In this example, we create a deferred coroutine using the launch function and pass it the parameter start = CoroutineStart.LAZY. Then we do some other work on the main thread, and only after that we call the start() method of our deferred coroutine to start its execution.
It is important to understand that if we do not call the start() method of our deferred coroutine, it will never be executed.
We can also use the CoroutineStart.LAZY parameter with the async and withContext functions. In this case, a deferred coroutine is created that returns the result of the computation. We can call await() on this coroutine to get the result of the execution.

A component provided by Kotlin Coroutines that allows asynchronously passing values ​​between coroutines.
Channels are a higher-level abstraction than primitive synchronized objects such as locks and semaphores. They allow passing values ​​between coroutines while maintaining correct order and safety in a multi-threaded environment.

Kotlin Channels are similar to message queues and have the following main operations: send and receive. There is also an option to close a channel, which stops the data transfer.
In addition, Kotlin Channels provide various buffering operations such as limiting the buffer size and a timeout for receiving a message.

Overall, Kotlin Channels allow you to organize asynchronous data transfer in an application more efficiently and safely.

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
    }
    repeat(5) { println(channel.receive()) }
    println("Done!")
}
// Expected output: 1 4 9 16 25 Done!

In this example, we created a Channel using Channel(), which allows passing integers. Then, we started a coroutine that sends five messages to the channel using the send() method. In the main thread, we receive messages from the channel using the receive() method and print them to the console.
Here, we used the runBlocking function to start the main thread. This is necessary because the launch function starts the coroutine in a new thread.

Copyright: Roman Kryvolapov