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
Java
➤ What are the data types in Java and what is their size
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
➤ What are the modifiers in Java and Kotlin
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
➤ What Java Collections do you know?
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.
➤ What are the difficulty levels for collection operations?
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
➤ What is HashMap?
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
➤ How to sort a collection containing a complex class, Comparable and Comparable
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) }
➤ What is the difference between ArrayList and LinkedList
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; }
➤ How TreeMap Works
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; }
➤ What are the naming rules in Java
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”
➤ What are static classes in Java
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;
➤ What is the difference between StringBuffer and StringBuilder
StringBuffer and StringBuilder are used for operations with text data, StringBuffer is synchronized and thread-safe
➤ What is reflection in Java
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(){} }
➤ In what situations does Memory leak occur?
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. } }
➤ What is Generic?
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?>
➤ What methods does the Object class have in Java and Any in Kotlin
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()
➤ What is Enum in Java
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; }
➤ What is the priority in Java when converting primitive types
byte ➜ short ➜ int ➜ long ➜ float ➜ double ➜ Byte() ➜ Object() ➜ Byte()… ➜ byte… ➜ short… ➜ int… ➜ long… ➜ float… ➜ double… ➜ Object()…
➤ What is type casting/type conversion/casting
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
➤ What are annotations in Java for and how to create them
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
➤ What are Wrappers in Java
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
➤ What are the transition operators?
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)
➤ What is Stack and Heap in Java
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).
➤ What is JDK, JRE and 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.
➤ What values are variables initialized with by default?
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.
➤ Is it possible to narrow the access level/return type when overriding a method?
No, because this would violate Barbara Liskov’s substitution principle. Access level expansion is possible.
➤ What are the types of references in Java
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.
➤ What is the difference between map and flatMap
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.
➤ What are the types of errors in Java
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
➤ How Garbage Collector works and how memory is organized in JVM
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.
➤ What is Java NIO (New Input/Output)
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.
➤ What is the execution order of a Java class
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
➤ Can an abstract class inherit from a regular class?
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() {} }
➤ What types of cycles are there in Java and Kotlin
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
➤ What is ClassLoader
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.
➤ What is SecurityManager
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());
➤ What is JavaCompiler
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”);
➤ How to Run JavaScript from 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);
➤ What is Optional in Java and Kotlin
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) } }
➤ What are the bitwise operations in Java and Kotlin
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
➤ What is ByteBuffer and Unsafe
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")
➤ What is lambda in Java
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
➤ How to download executable code from the network in Java
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.
➤ What is AsynchronousServerSocketChannel
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 { … }).
Kotlin
➤ What are Inner Classes in Kotlin
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" }
➤ What are Scope Functions in Kotlin
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 }
➤ How does coroutine work and what is suspend
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.
➤ What is the difference between coroutine launch method launch and async
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 }
➤ What are Delegates in Kotlin
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 ))
➤ What is Extension in Kotlin
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() }
➤ What is a companion object in Kotlin
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() } }
➤ How to use getters and setters in Kotlin
var stringRepresentation: String get() = this.toString() set(value) { setDataFromString(value) } var setterVisibility: String = "abc" private set // setter has private access and default implementation
➤ What is @JvmStatic annotation
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.
➤ What is a callback, functional types and Unit in Kotlin and how is it used
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()
Theory
➤ What are the levels of difficulty for collections?
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.
➤ What are the most commonly used patterns?
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)
➤ What is the difference between inheritance and composition
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
➤ What is SOLID
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.
➤ What are higher order functions
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.
➤ What is the difference between structural, functional and object-oriented paradigms in programming
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
➤ What formats of data exchange with the server exist?
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.
➤ What is the SQL database query syntax
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
Multithreading
➤ What are synchronized collections and how do they work?
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
➤ What does volatile mean
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.
➤ What does synchronized mean
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.
➤ What problems can there be in multithreading in Java
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.
➤ What is Lock / ReentrantLock
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.
➤ What are the Backpressure strategies?
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.
➤ What is the difference between hot and cold Observables
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.
➤ What is the difference between volatile and atomic
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.
➤ What are Deferred Coroutines in Kotlin
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 } }
➤ What are lazy coroutines in Kotlin
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.
➤ What are Kotlin Channels
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.