Skip to content

Java Core – Basic Questions

Open all questions about Spring

Open all questions about Android

Content:

➤ 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 difficulty levels for collection operations?
➤ What is 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/casting
➤ What are annotations in Java for and how to create them
➤ 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 Garbage Collector works and how memory is organized in 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 class?
➤ What types of cycles are there 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
➤ What are functional interfaces and @FunctionalInterface annotation in Java
➤ What are the built-in functional interfaces in Java
➤ What are the typical parameters in Java
➤ What is the mechanism of volatile variables in Java?
➤ How thread stacks work in the JVM, in terms of interaction with the processor

➤ What are the data types in Java and what is their size

Integers:

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 38th power

double:
64 bits or 8 bytes, from ~4.910 to the -324th power to ~1.810 to the 308th power

Symbol:

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

Logical:

boolean: 8 bits or 1 byte;

It is also important to 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

▲To the list of questions▲

➤ What are the modifiers in Java and Kotlin

In Java and Kotlin, visibility zones (access modifiers) determine who can access classes, methods, fields, and other code elements. Let's look at the main visibility zones for both languages.

There are 4 visibility levels in Java:

private:
Accessible only within the class in which it is declared.

class MyClass {
    private int myVar = 10; // only available within MyClass
}

default (package visibility level):
If no modifier is specified, the default visibility is used. The element is accessible only within the package, but is not accessible to classes from other packages.

class MyClass {
    int myVar = 10; // available within the current package
}

protected:
Available within the same package and to descendant classes, even if they are in a different package.

class MyClass {
    protected int myVar = 10; // available in package and to heirs
}

public:
Accessible from anywhere.

Kotlin has 4 visibility levels, but their semantics are slightly different from Java:

private:
For the class: only available within this class.
For the package: accessible only within the file in which it is declared.

class MyClass {
    private val myVar = 10 // only available in MyClass
}

internal:
Available within a single module. This is a unique visibility level for Kotlin, which limits the element's availability to a module rather than a package.

internal class MyClass {
    internal val myVar = 10 // available only within the module
}

protected:
Available in the class and its subclasses. Unlike Java, not available within a package (unless the class is an inheritor).

open class MyClass {
    protected val myVar = 10 // available in successor classes
}

public:
Available everywhere. This is the default visibility in Kotlin (if no modifier is specified).

class MyClass {
    val myVarOne = 10 // accessible to all
    public val myVarTwo = 10 // accessible to all
}

private: in both languages it works similarly, except that in Kotlin private at the package level restricts access within the file, not the entire package.

default: in Java it is similar to internal in Kotlin, but Java visibility is limited to the package, while Kotlin visibility is limited to the module.

protected: In Java it is available in the package and its descendants, and in Kotlin only to its descendants.

public: is the same in both languages.

internal: a modifier that does not exist in Java. It restricts access at the module level.

Java methods and variables:

private:
accessible within the class

default (not specified):
available inside the package

protected:
accessible within a package or from descendant classes

public:
available 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 the method:
method cannot be overwritten

final for variable:
the variable is a constant

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

For Java classes:

default (not specified):
available inside the package

public:
available everywhere

final:
class cannot be inherited from

abstract:
the class is abstract and can contain abstract methods

record:
is a special type of class introduced in Java 14 (in preview form) and finally included in Java 16. Record provides a concise and 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.

Java interfaces:

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

static:
Since Java 8, a static method in an interface is 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, they can be private so that they can be called from another static method.

Kotlin also has:

internal:
available inside the module
public (not specified)

open for class:
you can inherit from a class, otherwise you can't,

open for the method:
the method can be overwritten, otherwise it is not possible

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

vararg:
allows passing a non-fixed number of arguments for a parameter

heat:
initialization block

Latinit:
late initialized property

typealias:
provides alternative names for existing types

::
reference to a class, function or property

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

value class:
inline class, absent in byte code – its value will be instead, intended for wrapping data, does not lead to a drop in performance

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.) has a performance penalty. The inline modifier affects both the function and the lambda passed to it: both will be inlined at the call site. Inlining functions can increase the amount of code generated, but if you do it in moderation (don't inline large functions), you'll get a performance boost, especially when calling functions with different parameter types inside loops.

noinline:
in case you want only some lambdas passed to an inline function to be inlined, you need to mark those function parameters that will not be inlined with the noinline modifier

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 have no modifier other than class, while regular inner classes that require an instance of the outer class to be accessed 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 of a sealed class as you need to cover every situation. In addition, each subclass can have multiple instances, each with its own state. Each subclass of a sealed class has its own constructor with its own individual properties. Sealed classes are enums with superpowers. A sealed class can have descendants, but they must all be in the same file as the sealed class. Classes that extend descendants of a sealed 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 cannot be changed. Sealed classes cannot be initialized. The descendants of a sealed class can be any type of class: a data class, an object, a regular class, or even another sealed class.

It is also important to remember that:

All variables in interfaces are final by default.

When inheriting and overriding, you can extend the access level default -> protected -> public

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

▲To the list of questions▲

➤ What is Default method

A default method is a method with an implementation that can be declared in an interface. In Java, the ability to create default methods has been available since Java 8.

Main features of default method:

Method with implementation:
Unlike regular interface methods, which must be implemented in classes implementing that interface, the default method already contains its implementation.

Does not violate compatibility:
Adding new methods to interfaces before Java 8 would require changing all classes that implement that interface. Now, with default methods, you can add new methods to an interface without having to rewrite all existing classes.

Inheritance:
Classes that implement an interface can override the default method if special logic is required. If they do not, the default implementation specified in the interface is used.

Reusing code:
Common functionality can be placed in a default method to avoid duplicating code across different classes.

Expanded functionality:
Default methods allow you to extend interfaces by adding new methods while still being able to use them in old code.

interface MyInterface {
    // A regular abstract method
    void regularMethod();
    // Default method with implementation
    default void defaultMethod() {
        System.out.println("This is the default method implementation.");
    }
}

class MyClass implements MyInterface {
    @Override
    public void regularMethod() {
        System.out.println("Implemented regular method.");
    }
    // You can override the default method if you need
    @Override
    public void defaultMethod() {
        System.out.println("Overridden default method.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj.regularMethod(); // Output: Implemented regular method.
        obj.defaultMethod(); // Output: Overridden default method.
    }
}

Kotlin implements the concept of default methods in interfaces a little differently than in Java, but functionally it is present through methods with implementation in interfaces.
In Kotlin, interfaces can contain implementations of methods, and classes implementing those interfaces can use that implementation or override the methods as needed. This is similar to default methods in Java.

interface MyInterface {
    // A common method that needs to be implemented in a class
    fun regularMethod()
    // Method with default implementation
    fun defaultMethod() {
        println("This is the default method implementation.")
    }
}

class MyClass : MyInterface {
    override fun regularMethod() {
        println("Implemented regular method.")
    }
    // You can override the default method if you need
    override fun defaultMethod() {
        println("Overridden default method.")
    }
}

fun main() {
    val myClass = MyClass()
    myClass.regularMethod()    // Output: Implemented regular method.
    myClass.defaultMethod()    // Output: Overridden default method.
}

Java's default method or methods with implementation in Kotlin interfaces cannot have state directly, since interfaces themselves cannot store state.

Interfaces cannot have instance variables (fields) that could store state between method calls. This is because interfaces are not intended to store state—their primary role is to provide a contract (a set of methods) that classes must implement.

However, default methods can access static interface variables or data passed as arguments, but they cannot have their own instance variables that could store state.

How does a Default method differ from a static method:

Default methods and static methods in Java interfaces (and similarly in Kotlin) differ in several important ways: their usage, invocation context, and interaction with interface implementations. Here are the main differences:

Call method:

Default method:
The default method is called through an instance of a class that implements the interface.
That is, it can be called on an object that implements the interface.

Static method:
A static method is called not through an object, but through the interface itself. It is tied directly to the interface and does not depend on its implementation in classes.

Binding to object:

Default method:
Binds to an object of a class that implements the interface.
It can use the object's state (such as class fields) or call other methods of the object.

Static method:
Does not have access to the object instance and therefore cannot use the object's state or call non-static methods.
It can only operate on data that is passed to it as parameters, or work with static fields (if any).

Inheritance and overriding:

Default method:
A class implementing an interface can override the default method if it needs to provide its own implementation.

Static method:
Static methods cannot be overridden in classes that implement an interface. They belong to the interface and are not inherited by classes.
If a static method with the same name is declared in a class, it is just another method.

Accessing static methods of an interface from its implementation:

Default method:
Default methods can use static interface methods, but to do so, they must be explicitly called via the interface name.

Static method:
Static methods can only call other static interface methods or operate on data passed in parameters.

Main purpose:

Default method:
The main goal is to allow adding new methods to an interface without breaking backward compatibility. That is, if an interface declares a new default method, older implementations of the interface will not be required to implement it.

Static method:
A static method is most often used to perform operations that do not depend on the class instance. These are utility methods that only have access to static data and do not require the state of the object.

How does an interface with a Default method differ from an abstract class:

Interfaces with default methods and abstract classes in Java/Kotlin have a lot in common, as both can provide partial implementation of methods. However, there are several key differences between them, which are related to architectural and functional aspects. Let's look at the main differences between them.

Inheritance and multiple implementation:

Interface with default methods:
In Java and Kotlin, a class can implement multiple interfaces. This allows a class to inherit behavior from multiple sources.
An interface may contain default methods – methods with a default implementation that classes can override if necessary.
An interface can define a contract (a set of methods) that a class must implement and provide a common implementation of some methods.

interface InterfaceA {
    default void printMessage() {
        System.out.println("Message from InterfaceA");
    }
}

interface InterfaceB {
    default void printMessage() {
        System.out.println("Message from InterfaceB");
    }
}

class MyClass implements InterfaceA, InterfaceB {
    // Resolving conflicts between implementations
    @Override
    public void printMessage() {
        InterfaceA.super.printMessage();  // Call the implementation of InterfaceA
    }
}

In this example, the MyClass class can implement both interfaces at once and choose which implementation to use as needed.

Abstract class:
A class can only inherit from one abstract class. This is called single-level inheritance, and it limits flexibility compared to interfaces.
An abstract class can contain both abstract methods (without implementation) and concrete methods (with implementation). Unlike interfaces, abstract classes can have fields and constructors.

abstract class AbstractClass {
    abstract void printMessage();

    void showMessage() {
        System.out.println("Message from AbstractClass");
    }
}

class MyClass extends AbstractClass {
    @Override
    void printMessage() {
        System.out.println("Overridden message");
    }
}

State support:

Interface with default methods:
An interface cannot store state directly, meaning it cannot have non-static fields or constructors. All variables in an interface must be static and final (constants).
Default methods can only use methods and state from classes that implement the interface. This makes interfaces less suitable for use cases where state is required.

interface MyInterface {
    default void defaultMethod() {
        System.out.println("Default method");
    }
}

class MyClass implements MyInterface {
    private int value = 10;  // Status in the classroom

    public void showValue() {
        System.out.println("Value: " + value);
    }
}

Abstract class:
Abstract classes can store state – they can contain fields that can be initialized via a constructor and used in class methods. This makes abstract classes more flexible for creating classes with shared state and behavior.
Abstract classes can have both static and non-static fields.

abstract class AbstractClass {
    protected int value;

    AbstractClass(int value) {
        this.value = value;
    }

    abstract void printValue();
}

class MyClass extends AbstractClass {
    MyClass(int value) {
        super(value);
    }

    @Override
    void printValue() {
        System.out.println("Value: " + value);
    }
}

Multiple inheritance of behavior:

Interface with default methods:
Interfaces with default methods can be useful for implementing multiple inheritance of behavior. A class can implement multiple interfaces, each of which provides a default method. This avoids problems associated with traditional multiple inheritance (such as the "diamond problem" in C++).
If two interfaces have methods with the same signature, the class that implements those interfaces must explicitly specify which implementation to use or override the method.

Abstract class:
In Java, an abstract class does not support multiple inheritance. A class can only inherit from one abstract class. This is one of the main limitations compared to interfaces.

Constructors and object creation:

Interface with default methods:
Interfaces cannot have constructors and therefore cannot create instances of themselves. Constructors are only for classes. An interface cannot encapsulate object creation logic or state initialization.

Abstract class:
Abstract classes can have constructors that can be called by subclasses when creating an object. This allows abstract classes to pass state and initialization logic to their subclasses.

abstract class AbstractClass {
    int value;

    AbstractClass(int value) {
        this.value = value;
    }
}

class MyClass extends AbstractClass {
    MyClass(int value) {
        super(value);
    }
}

Access modifiers:

Interface with default methods:
Methods in an interface can only be public (by default) and private (since Java 9). But an interface cannot have methods with the protected or default (package-private) access modifier.
Interface fields are always public, static, and final.

Abstract class:
An abstract class can contain methods with any access modifiers: public, protected, private, and default (package-private). This gives more flexibility in managing access to methods and state.
Fields in an abstract class can also have any access modifiers.

When to use an interface with default methods vs an abstract class:

Use interfaces with default methods if:
You need to provide a contract that classes must implement, with the option to include a base implementation.
You want a class to be able to implement multiple interfaces at the same time.
You don't need to store state in the interface.

Use an abstract class if:
You need to store state and initialization logic (via constructors).
You need a common implementation of methods and the ability to inherit it in subclasses.
You need more control over access modifiers.

➤ What Java Collections do you know?

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

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

interface Set:
an unordered set of unique elements. Inherits from Collection

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

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

interface:
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. Inherited from Queue

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

interface Iterator:
allows you to iterate over 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

And about:
ArrayDeque, ConcurrentLinkedDeque, LinkedBlockingDeque, LinkedList

The most famous collections:

ArrayList:
array based list.

LinkedList:
doubly linked list.

Vector:
it's the same as ArrayList but all methods are synchronized

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

SortedSet:
sorted items

NavigableSet:
allows you to extract elements based on their values

TreeSet:
all objects are stored in sorted ascending order, works on the basis of TreeMap

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

LinkedHashSet:
respects the order of insertion of objects, works on the basis of LinkedHashMap

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

Hashtable:
synchronized version of HashMap

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

TreeMap:
sorted mapa based on red black wood

It is also important to remember that:

In Kotlin, collections are divided into mutable and immutable.

▲To the list of questions▲

➤ What are the difficulty levels for collection operations?

Lists:

ArrayList:
Append to end: O(1) amortized
Adding to the middle or beginning: O(n)
End Removal: O(1)
Removing from the middle or beginning: O(n)
Index access: O(1)
Search: O(n)

LinkedList:
Append to beginning or end: O(1)
Adding to the middle: O(1) (if there is a reference to the node) or O(n) (finding the node)
Deleting from start or end: O(1)
Removing from the middle: O(1) (if there is a reference to the node) or O(n) (finding the node)
Access by index: O(n)
Search: O(n)

Sets:

HashSet:
Addition: O(1) amortized
Removal: O(1) amortized
Search: O(1) amortized

LinkedHashSet:
Addition: O(1) amortized
Removal: O(1) amortized
Search: O(1) amortized
Iteration: O(n) (preserves insertion order)

TreeSet:
Addition: O(log n)
Deletion: O(log n)
Search: O(log n)

Maps:

HashMap:
Addition: O(1) amortized
Removal: O(1) amortized
Search: O(1) amortized

LinkedHashMap:
Addition: O(1) amortized
Removal: O(1) amortized
Search: O(1) amortized
Iteration: O(n) (preserves insertion order)

TreeMap:
Addition: O(log n)
Deletion: O(log n)
Search: O(log n)

Queues and Deques:

PriorityQueue:
Addition: O(log n)
Deletion: O(log n)
Search: O(n)

ArrayDeque:
Append to beginning or end: O(1)
Deleting from start or end: O(1)
Search: O(n)

LinkedList (used as a queue or deck):
Append to beginning or end: O(1)
Deleting from start or end: O(1)
Search: O(n)

Tables:

ConcurrentHashMap:
Addition: O(1) amortized
Removal: O(1) amortized
Search: O(1) amortized

▲To the list of questions▲

➤ 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 multiplication by 2

The resulting hash code can be a huge numerical 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 uses balanced red-black trees TreeNodes instead of linked lists after a certain threshold is reached (works similar 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, it switches to balanced trees and can then be converted back to 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

▲To the list of questions▲

➤ How to sort a collection containing a complex class, Comparator and Comparable

Comparable:
the class must implement the Comparable interface, write 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 the list of Persons by age like this:

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)
}

Example of sorting for BigDecimal:

val one = BigDecimal(1)
val two = BigDecimal(1.0)

val setOne = TreeSet<BigDecimal>()
val setTwo = HashSet<BigDecimal>()

setOne.add(one)
setOne.add(two)

setTwo.add(one)
setTwo.add(two)

println(setOne.size)  // Result: 1
println(setTwo.size)  // Result: 2

BigDecimal behavior:
The BigDecimal class implements the Comparable<BigDecimal> interface.
BigDecimal(1) and BigDecimal(1.0) have the same numeric value (1), but different representations in terms of precision:
BigDecimal(1) has a precision of 0 (integer).
BigDecimal(1.0) has a precision of 1 (with fractional part).
These two objects are considered different when using the equals() and hashCode() methods, since they take into account not only the value but also the precision.

TreeSet:
TreeSet in Java/Kotlin is a set that sorts elements based on their natural order or by a given comparator.
For the BigDecimal class, the order is determined by the method compareTo(), which compares only the numeric value and ignores precision.
Since the numeric value of BigDecimal(1) and BigDecimal(1.0) is the same, the compareTo() method will return 0, which will cause TreeSet to consider these objects to be the same.
Therefore, only one element (either BigDecimal(1) or BigDecimal(1.0)) will be added to the TreeSet.

HashSet:
HashSet uses methods equals() And hashCode() to compare objects.
As we have already discussed, BigDecimal(1) and BigDecimal(1.0) are considered different objects because they have the same numeric value but different precision.
As a result, both objects will be added to the HashSet.

▲To the list of questions▲

➤ 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 a 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;
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

LinkedList iteration mechanism:
When you start iterating over a LinkedList, it goes through the nodes of the list one by one, starting at the head and moving towards the tail. Each node stores references to the next and previous nodes, allowing you to move along the chain of nodes.
ListIterator provides more features, such as moving backwards (to the previous node).

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    ListItr(int index) {
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext()) throw new NoSuchElementException();
        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }

    public boolean hasPrevious() {
        return nextIndex > 0;
    }

    public E previous() {
        checkForComodification();
        if (!hasPrevious()) throw new NoSuchElementException();
        lastReturned = next = (next == null) ? last : next.prev;
        nextIndex--;
        return lastReturned.item;
    }

    public int nextIndex() {
        return nextIndex;
    }

    public int previousIndex() {
        return nextIndex - 1;
    }

    public void remove() {
        checkForComodification();
        if (lastReturned == null) throw new IllegalStateException();
        Node<E> lastNext = lastReturned.next;
        unlink(lastReturned);
        if (next == lastReturned) next = lastNext;
        else nextIndex--;
        lastReturned = null;
        expectedModCount++;
    }

    public void set(E e) {
        if (lastReturned == null) throw new IllegalStateException();
        checkForComodification();
        lastReturned.item = e;
    }

    public void add(E e) {
        checkForComodification();
        lastReturned = null;
        if (next == null) linkLast(e);
        else linkBefore(e, next);
        nextIndex++;
        expectedModCount++;
    }
    // ...
}

ArrayList iteration mechanism:
Iterating over an ArrayList using Iterator is based on direct access to the array elements by their index. Iterating over the elements is linear, but accessing each element has O(1) complexity.
ListIterator provides additional capabilities such as moving forward and backward through elements, modifying elements, getting the index of the current element, etc.

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)  throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0) throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    // ...
}

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        checkForComodification();
        int i = cursor - 1;
        if (i < 0) throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) throw new ConcurrentModificationException();
        cursor = i;
        return (E) elementData[lastRet = i];
    }

    public void set(E e) {
        if (lastRet < 0) throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();
        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

▲To the list of questions▲

➤ How TreeMap Works

TreeMap in Java is an implementation of the NavigableMap interface that stores keys in sorted order using a red-black tree. A red-black tree is a self-balancing binary search tree that maintains the order of elements and provides efficient execution of basic operations such as insertion, deletion, and search.

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;
}

Sorted order:
All elements (key-value pairs) in a TreeMap are stored in sorted order by keys. The keys are sorted based on their natural order (via the Comparable interface) or by an external comparator (via the Comparator interface), if one was passed when the map was created.

Self-balancing tree:
TreeMap uses a red-black tree to store data. It is a self-balancing binary search tree that provides the following timing characteristics for operations:
Insert (put): O(log n)
Remove: O(log n)
Search (get): O(log n)

Unique keys:
Like other implementations of the Map interface, TreeMap does not allow duplicate keys. If you try to insert an element with a key that already exists in the map, the new value will replace the old one.

Internal structure of TreeMap:
Keys and values: TreeMap stores key-value pairs in nodes of a red-black tree. Each tree node contains a key, a value, a reference to the left subtree, a reference to the right subtree, and the color of the node (red or black).

Comparable and Comparator interfaces:
If keys implement the Comparable interface, they will be compared to each other based on their natural order (for example, for numbers, this is ascending order).
You can also pass a Comparator when creating a TreeMap to control the sort order of the keys. In this case, the elements will be sorted by this comparator.

Inserting elements (put method):
When you add a new key-value pair to a TreeMap, the key is first compared to other keys that are already in the tree.
The binary search algorithm uses the compareTo() method (if the keys implement Comparable) or the passed Comparator to determine where to insert the new key.
If the key already exists, the value is updated but the key remains in place.

Finding an element (get method):
Finding an element by key is done using binary search. The algorithm goes up the tree, starting from the root, and at each node decides whether to go left (if the search key is less than the current one) or right (if the key is greater than the current one).
The search operation in TreeMap has complexity O(log n).

Removing elements (remove method):
Removing an element from a TreeMap also balances the tree. If the node being removed has two children, the tree will find the smallest element in the right subtree and replace the node being removed with it to preserve order.
The delete operation also takes O(log n).

Maintaining the balance of the tree:
A red-black tree maintains balance by recoloring nodes and performing rotations (left or right rotation) when inserting or deleting nodes. This ensures that the height of the tree always remains logarithmic in the number of elements, allowing for efficient operations.

Advantages and disadvantages of TreeMap:

Advantages:

Orderliness:
All keys are stored in sorted order.

Logarithmic complexity:
Insertion, deletion, and lookup are all performed in O(log n), making TreeMap efficient for storing large amounts of data that need to be looked up in sorted order.

Range support:
TreeMap allows you to easily retrieve elements in a range of keys (e.g. subMap method).

Flaws:

Slower than HashMap:
Insertion and lookup in TreeMap are slower than in HashMap, where these operations have amortized complexity O(1).

Large memory overhead:
A red-black tree uses extra links and fields for balancing, which requires more memory than hash-based structures.

▲To the list of questions▲

➤ 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 camelsCase with a lowercase letter, except for constants

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

Interface implementations are often named InterfaceName + Impl, e.g. InterfaceNameImpl

Variable names starting with m and interface names starting with I are deprecated.

Naming rules are described in Java Code Conventions.
To translate cases into other cases in IDEA there is a convenient plugin “String Manipulation”

▲To the list of questions▲

➤ What are static classes in Java

Only an inner class can be a static class, it can be inherited, just like 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;

▲To the list of questions▲

➤ What is the difference between StringBuffer and StringBuilder

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

▲To the list of questions▲

➤ What is reflection in Java

Reflection is a mechanism that allows programs to examine and modify their structure and behavior at run time.
Reflection allows you to access information about classes, interfaces, fields, methods, and constructors at runtime. This can be useful in cases where 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 during program execution. However, you should be careful when using reflection, as it can lead to decreased 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(){}
}

▲To the list of questions▲

➤ 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 that may leak memory:

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
            // how the data was destroyed.
            Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
        }
        // Let's assume we want to free data from memory,
        // to prevent memory leaks.
        // We do this by setting data to null.
        data = null // This will not remove the reference to data from the lambda.
    }
}

▲To the list of questions▲

➤ 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 which 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.

Old Java code does not use generics.

Generics are visible only at the compilation stage, 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 have limited T to inherit from 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) {
        // Comparable method
        variable.compareTo(1);
        // NewClass method
        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. In addition, 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
used to indicate 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 without specifying 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 list contains all three objects. The < symbol> can also be used instead 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?>

▲To the list of questions▲

➤ 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. Mainly used for reflection.

clone():
get an exact copy of an object. Not recommended. It is more common to recommend using the copy constructor.

equals():
compares two objects.

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

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

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

finalize():
If an object interacts with some resources, for example, opens an output stream and reads from it, then such a stream must be closed before deleting the object from memory. To do this in the Java language, 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 must specify the actions that must be performed before destroying the object. The finalize() method is called only immediately before garbage collection

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

▲To the list of questions▲

➤ 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;
    }

▲To the list of questions▲

➤ 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()…

▲To the list of questions▲

➤ What is type casting/type conversion/casting

There is an explicit and implicit transformation

Automatic widening transformation is possible:
byte -> short -> int -> long
int -> double
short -> float -> double
char -> int

Conversion with loss of information is possible:
int -> float
long -> float
long -> double

Explicit lossy transformation:

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

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

Transformations during operations:
if one of the operands of the operation is of type double, then the second operand is also 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 also 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:
normal unsafe type casting

as?:
safe type cast, returns null on failure

is:
is this type

!is:
is not this type

▲To the list of questions▲

➤ 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: persists after compilation, but is ignored by the JVM
RUNTIME: saved after compilation and loaded by the JVM

The type of object over which it is indicated:

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

Java SE 1.8 standard language library provides us with 10 annotations

@Override
Retention: SOURCE; Target: METHOD.
Shows that the method it is written over 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 "deprecated" and may be removed in future versions of the product.

@SuppressWarnings
Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
Disables the output of 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 it is specified above. 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

▲To the list of questions▲

➤ 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 for converting numbers to strings, comparing numbers, and so on.
Wrappers are also used to pass primitive data types as parameters to methods that expect objects. For example, if a method expects a parameter of type Object, and you want to pass it an integer, you can create an object of class Integer, wrap the number in it, and pass that object to the method.
Another benefit of wrappers is the ability to handle null values. Primitive types cannot be null, but wrappers can. If you are unsure 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.

▲To the list of questions▲

➤ 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 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 at label @a, not return expression with label (@a 1)

▲To the list of questions▲

➤ 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 in the heap, and references to them are stored in the stack. A primitive field of a class instance is stored in the heap. The heap is used by all parts of the application, while the stack is used only by one thread of program execution.

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 java.lang.StackOverflowError, and if the heap memory is full, the exception java.lang.OutOfMemoryError: Java Heap Space 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 the thread executes its code, the call stack changes. The thread stack contains all the local variables for each method 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 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”.

The stack is also 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 standard JVM configuration, the stack size can range from a few megabytes to several tens of megabytes, depending on the JVM version and operating system. However, the stack size can be changed when starting the JVM using the -Xss parameter. For example, to set the stack size to 1 megabyte, you must start the JVM with the -Xss1m parameter.

In a standard JVM configuration, the heap size can range from a few hundred megabytes to several gigabytes, depending on the JVM version and operating system. However, the heap size can be changed when the JVM starts 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 cannot share a primitive local variable.

The heap contains all objects created in your application, regardless of which thread created the object. This includes primitive type versions of objects (e.g., Byte, Integer, Long, etc.). It doesn't matter whether the object was 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 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.

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

Static class variables 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 variable x of type int is stored on the stack because it is a primitive data type. The variable str of type String is an object and is stored on the heap.

When the program is executed, the main method is pushed onto the stack, the variable x is created and assigned the value 10. 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 str object reference is passed to the method, and it prints the string “Hello World!” to the console.

After the main method executes, 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 heap, Java has a persistent data storage area (PermGen or Metaspace, depending on the Java version). This area stores class metadata, information about methods, variables, and other data related to the program structure itself. In Java 8 and higher, PermGen is 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).

▲To the list of questions▲

➤ What is JDK, JRE and JVM

JDK (Java Development Kit):
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):
is a Java runtime 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):
is a virtual machine that enables Java code to run on a computer. JVM translates Java bytecode into machine code that can be executed on a specific platform. JVM is a key component of the Java platform because it enables Java code to be written once and run on any platform where 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.

▲To the list of questions▲

➤ 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.

▲To the list of questions▲

➤ 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.

▲To the list of questions▲

➤ What are the types of references in Java

Strong Reference:
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 take up a large amount of memory. The doSomething() method checks whether the SoftReference already exists and stores the HeavyObject. If the object has not yet been created or has already been removed from memory, a new object is created and a reference to it is stored using the SoftReference. Otherwise, if the HeavyObject already exists in memory and is referenced via the SoftReference, the reference to it is returned for further use.
It is important to note that 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 SoftReference will be automatically cleared. The next time you try to access the object, a new object will be created and a reference to it will be stored in SoftReference.

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

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 the System.gc() method. 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 has been removed from memory. The reference to the object via WeakReference also becomes null, indicating that the object has been removed.

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)
// we perform some actions
// check if the object has been deleted from memory
if (phantomReferenceQueue.poll() != null) {
    // freeing resources associated with the object
}

Here we create an object MyObject and put it in 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.

▲To the list of questions▲

➤ 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 original collection, while flatMap returns a collection of elements obtained by applying a function that returns a collection to each element of the original collection. Also, flatMap can be useful when you need to "unfold" nested collections into one 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, whereas 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.

▲To the list of questions▲

➤ What are the types of errors in Java

All errors are inherited from the Throwable class.

Exception:
errors when executing a program

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

Exceptions can be:

Checked:
they are necessarily caught and checked by the compiler, the reason for their occurrence is potential errors in methods
Examples: Throwable, Exception, IOException, ReflectiveOperationException

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

▲To the list of questions▲

➤ How Garbage Collector works and how memory is organized in JVM

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

Serial Garbage Collection:
sequential assembly of the young and old generations.

Parallel Garbage Collection:
the default collector in Java 8, works the same 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 total background collection cycle. If possible, it performs garbage collection in the background. The first pause is called initial mark, during which 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 nothing has changed while we were doing it in the background. 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 “desired pause”. 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 this way, but it will try to work with the specified desired pauses. This is fundamentally different from all garbage collectors that 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 each. 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:
the area of dynamic memory in which objects are initially created. Many objects never leave this area of memory because they quickly become garbage.
When we write something in the form of new Object() we create an object in Eden.
Refers to the young generation.

Survivor:
There are two areas of survivors in the memory. Or you can think of the survivor area as usually being divided in half. It is where the 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 there is a collection process going on.
From From Space objects are either removed by GC or moved to To Space – the last place before they become too old and become Tenured.
Refers to the young generation.

Tenured:
the area where surviving objects that are deemed "old enough" end up (thus leaving the Survivor area).
The repository is not cleared during a young build.
Typically, by default, objects that have survived 8 garbage collections are placed here.
Belongs to the old generation.

Metaspace and PermGen Permanent Memory Generation:
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, and regular objects never got here.
This is where metadata, classes, interned strings, etc. were stored – it was a special area of memory in the JVM.
Since it is quite difficult to know the required size of this area, before Java 8 you could often see the error java.lang.OutOfMemoryError. This happened because this area overflowed unless you set enough memory for it, and it was only possible to determine whether there was enough memory or not 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, for example, interned strings, or moved to the metaspace area, to native memory. The maximum Metaspace by default is not limited by anything except the native memory limit. But it can be optionally limited by the MaxMetaspaceSize parameter, which is essentially similar to MaxPermSize in the PermGen ancestor. Metaspace is not cleared by GC and, if necessary, can be cleared manually.

String Pool:
the area of memory where strings are stored, its meaning 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 the String Pool in the Heap
String text = new String("text").intern(); 
// will be created in String Pool by the intern method

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

minor garbage collection:
Eden and Survivor (young generation) are cleared

major garbage collection:
old generation is cleared

full garbage collection:
everything is cleared

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

▲To the list of questions▲

➤ 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 intended for developing high-performance applications that work with large amounts of data or handle many connections simultaneously.

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 not to be blocked when performing read or write operations.

▲To the list of questions▲

➤ 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 when calling the called class constructor using
super(parameters) )
normal initializer
initialization of normal variables
class constructor

▲To the list of questions▲

➤ Can an abstract class inherit from a regular class?

Yes, it can also overwrite methods by making them abstract, an interface can't

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

▲To the list of questions▲

➤ 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

▲To the list of questions▲

➤ What is ClassLoader

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

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

Extension ClassLoader:
extension 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:
system loader. Loads application classes defined in the CLASSPATH environment variable

Each loader except the base loader is a descendant of the abstract class java.lang.ClassLoader. For example, the extension loader implementation is sun.misc.Launcher$ExtClassLoader, and the system loader is 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 launched, it undergoes verification so that the values in the bytecode cannot be changed using a hex editor, thereby breaking the logic.
If the class fails verification, a java.lang.VerifyError error is thrown.

▲To the list of questions▲

➤ What is SecurityManager

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

The 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 to be performed. The application can allow or deny the operation.

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());

▲To the list of questions▲

➤ 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”);

▲To the list of questions▲

➤ 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);

▲To the list of questions▲

➤ 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 hold null values. Operations on Nullable variables must use safe navigation operators (safe call operator — ?. and elvis operator — ?:) to avoid exceptions when attempting 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 where 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)
  }
}

▲To the list of questions▲

➤ What are the bitwise operations in Java and Kotlin

In Java:

Signed numbers in Java are written using two's complement, where the most significant digit is the signed digit. If its value is 0, 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 a number a to the left by b places. For example, the expression 4<<1 shifts the number 4 (which is 100 in binary) one place to the left, resulting in the number 1000, or the number 8 in decimal.

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) one place to the right, resulting in 1000, or the number 8 in decimal notation.

a>>>b:
Unlike the previous types of shifts, this operation is an unsigned shift — it shifts the number a to the right by b digits. 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)
tenth(bits): unsigned right shift (>>> in Java)
and(bits): bitwise AND
or(bits): bitwise OR
xor(bits): bitwise exclusive OR
inv(): bitwise negation

▲To the list of questions▲

➤ 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}); // writing to memory
byteBuffer.flip(); // delete all unfilled (everything after 1 2 3)
byteBuffer.position(0); // move cursor to start position
byte[] bytes = new byte[byteBuffer.remaining()]; // size == 3 then flip or == 1024 without it
byteBuffer.duplicate().get(bytes); // get contents
byteBuffer.clear(); // don't forget to clean up
// 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 it with the help of field reflection
        field.setAccessible(true); // allow access
        Unsafe unsafe = (Unsafe) field.get(null); // we get the field and cast it 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); // clearing 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 the 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")

▲To the list of questions▲

➤ What is lambda in Java

Represents a set of instructions that can be isolated 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 with no implementation.

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

▲To the list of questions▲

➤ 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");
        // Creating an instance of a 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. We then 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.

▲To the list of questions▲

➤ 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 of a thread.

Key features of AsynchronousServerSocketChannel:

Asynchrony:
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.
Example of using AsynchronousServerSocketChannel

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?) {
            // We accept the following connection
            serverChannel.accept(null, this)
            // Processing 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 a 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()
            }
        }
    })
}

Customer processing:
In the handleClient method, data is read from the client and the response is written.
clientChannel.read(buffer, buffer, object : CompletionHandler { … }) starts reading data asynchronously.
completed is called when the data has been read successfully.
failed is called if the read 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 { … }).

▲To the list of questions▲

➤ What are functional interfaces and @FunctionalInterface annotation in Java

Annotation @FunctionalInterface In Java, it is applied to interfaces and is used to indicate that the interface is functional (can have only one abstract method).

This means that the interface is intended to be implemented using lambda expressions or method references.

Why do you need an annotation?

Explicit indication:
Documents that the interface is functional.

Compiler check:
If you accidentally add a second abstract method, the compiler will generate an error.

Functional interface rules:
Must contain exactly one abstract method.
May contain any number of default or static methods.
Can override methods from Object (e.g. equals()).

Example of a functional interface:

@FunctionalInterface
interface MyInterface {

    // single abstract method
    void doSomething();

    // default methods are allowed
    default void defaultMethod() {
        System.out.println("Default method");
    }

    // static methods are also allowed
    static void staticMethod() {
        System.out.println("Static method");
    }

}

public class Main {
  
    public static void main(String[] args) {
        // implementing a functional interface using a lambda expression
        MyInterface action = () -> System.out.println("Doing something...");
        action.doSomething(); // Doing something...

        action.defaultMethod(); // Default method
        MyInterface.staticMethod(); // Static method
    }
  
}

The same code in Kotlin

fun interface MyInterface {
  
    // The only abstract method
    fun doSomething()

    // Default methods are allowed
    fun defaultMethod() {
        println("Default method")
    }

    // Static methods in Kotlin are usually moved to a companion object
    companion object {
        fun staticMethod() {
            println("Static method")
        }
    }
}

fun main() {
  
    // Using an interface via lambda
    val action = MyInterface {
        println("Doing something...")
    }

    action.doSomething()        // Doing something...
    action.defaultMethod()      // Default method
    MyInterface.staticMethod()  // Static method
    
}

What happens if you break the rule:
If you accidentally add a second abstract method, you will get a compile-time error:
“Multiple non-overriding abstract methods found in interface BrokenInterface”

▲To the list of questions▲

➤ What are the built-in functional interfaces in Java

Function<T, R>:
Takes one argument and returns a result.

Function<String, Integer> length = String::length;
int len = length.apply("Hello"); // 5

Predicate<T>:
Takes an argument and returns a boolean

Predicate<Integer> isPositive = n -> n > 0;
boolean result = isPositive.test(10); // true

Consumer<T>:
Takes an argument and returns nothing.

Consumer<String> print = System.out::println;
print.accept("Hello!"); // Hello!

Supplier<T>:
Accepts nothing, returns a value

Supplier<Double> random = Math::random;
double value = random.get(); // random number

UnaryOperator<T>:
Takes one argument of type T and returns a result of the same type.

UnaryOperator<Integer> square = x -> x * x;
int result = square.apply(4); // 16

BinaryOperator<T>:
Takes two arguments of type T, returns a result of the same type

BinaryOperator<Integer> sum = Integer::sum;
int result = sum.apply(2, 3); // 5

BiFunction<T, U, R>:
Takes two arguments of different types and returns the result

BiFunction<String, String, Integer> totalLength = (a, b) -> a.length() + b.length();
int length = totalLength.apply("Hello", "Java"); // 9

BiConsumer<T, U>:
Takes two arguments, returns nothing.

BiConsumer<String, Integer> printNameAge = (name, age) ->
    System.out.println(name + " is " + age + " years old.");

printNameAge.accept("Alice", 30); // Alice is 30 years old.

BiPredicate<T, U>:
Takes two arguments and returns a boolean value.

BiPredicate<String, String> equalsIgnoreCase = String::equalsIgnoreCase;
boolean res = equalsIgnoreCase.test("java", "JAVA"); // true

Runnable:
Does not accept or return anything

Runnable task = () -> System.out.println("Running!");
new Thread(task).start(); // Running!

Callable<V>:
Accepts nothing, returns a value of type V, may throw an exception.

Callable<Integer> callable = () -> {
    Thread.sleep(1000);
    return 42;
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(callable);
int result = future.get(); // 42 (in 1 second)
executor.shutdown();

Specialized interfaces for primitives:
IntPredicate, LongPredicate, DoublePredicate
IntConsumer, LongConsumer, DoubleConsumer
IntSupplier, LongSupplier, DoubleSupplier
IntFunction, LongFunction, DoubleFunction
ToIntFunction, ToLongFunction, ToDoubleFunction
IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator

Example of using IntPredicate:

IntPredicate isEven = n -> n % 2 == 0;
boolean result = isEven.test(10); // true

▲To the list of questions▲

➤ What are the typical parameters in Java

Standard notations by convention:

<T>:
Abbreviation for Type. Used for any type of data.

Examples:
List<T>
Optional<T>

<E>:
short for Element. Typically used for collection items.

Examples:
Collection<E>
Set<E>

<K>:
short for Key. Used for keys, such as in Map structures.

Examples:
Map<K, V>

<V>:
short for Value. Used for values in Map type structures.

Examples:
Map<K, V>

<R>:
short for Result. Typically used for return values.

Examples:
Function<T, R>

<U>:
an optional second generic parameter if more than one generic type needs to be specified

Examples:
BiFunction<T, U, R>

<N>:
Abbreviation for Number. Rarely used, but can be used for numeric types.

Examples:
Comparator<N> (if you need a Comparator for numbers)

<S>:
additional type parameter, rarely used. Usually for additional auxiliary types in multiple parameterization.

▲To the list of questions▲

➤ What is the mechanism of volatile variables in Java?

From the perspective of the JVM and the Java Memory Model (JMM), volatile leverages lower-level mechanisms to provide memory management and visibility barriers.

Memory Barriers:

When accessing a volatile variable, the JVM inserts memory barriers that control the order of read/write operations. These are:

LoadLoad barrier
LoadStore barrier
StoreLoad barrier
StoreStore barrier

volatile boolean flag = false;

When reading flag:
JVM inserts LoadLoad + LoadStore – anything that happens after the read cannot be "carried" before it.

When writing to flag:
The JVM inserts StoreStore + StoreLoad – all modifications before the write must be completed, and no reads after it can be performed before then.

In practice, this means that volatile prevents aggressive compiler and CPU optimizations that could disrupt the order of code execution in multithreaded situations.

Working with CPU caches and registers:

Without volatile, threads can cache the variable's value in registers or L1/L2 caches. With volatile, the JVM guarantees that:
A thread always reads a value from main memory.
When writing, the value is immediately reset to the main memory.

Happens-before rule:

JMM defines "happens-before" relationships between operations. For volatile:
Writing to a volatile variable happens-before any reading of the same variable from another thread.
This is one of the fundamental guarantees of the JMM and the basis for visibility safety in multithreading.

JVM instructions:

At the bytecode level:
Reading volatile → volatileLoad
Entry in volatile → volatileStore
The JIT compiler may also insert lock or mfence instructions for x86, or dmb (data memory barrier) for ARM.

volatile in JVM:
Ensures visibility between threads through memory barriers.
Controls the order in which operations are executed.
Prevents reordering.
Supports a happens-before relationship.
Uses specific instructions to flush/load from main memory.

▲To the list of questions▲

➤ How thread stacks work in the JVM, in terms of interaction with the processor

What is thread stack in JVM:
For each thread, the JVM creates a separate stack (Thread Stack), which is used to store:
method call frames (Stack Frames)
local variables
operands (for JVM instruction operations)

Each method receives its own frame when called. After the method completes, the frame is removed from the stack.

How does this relate to the processor:
The processor itself operates on registers, caches, and RAM. The JVM runs in user space, and the JVM thread stack resides in RAM.

At the interaction level:

Creating a stream:
The JVM calls the OS to create a native thread (e.g. via pthread_create on Linux).
The OS allocates a block of memory for the stack (usually 1–2 MB by default).
The JVM initializes the JVM stack structure inside this memory block.

Method call = stack manipulation:
When a thread executes a method, the JVM adds a new Stack Frame.
All calculations (e.g. a + b) happen on the stack – operands are put there, JVM instructions operate on them (via the frame's operand stack).
The JVM itself interprets the instructions or passes them to JIT-compiled code that works directly with the CPU register and stack.

JIT compilation and CPU interaction:
When HotSpot JIT-compiles a method, it generates machine code that:
uses CPU registers (e.g. to store local variables, arguments, temporary values);
manages its call stack (in the process stack);
accesses JVM stack data via pointers and offsets if needed.

How the JVM and thread stack use the cache:
now let's add this to our thread stack:

Working with local variables and operands:
When the JVM (or JIT code) accesses local variables or operands on the stack, it is actually accessing memory.

This memory first goes to the CPU cache:
If the data is in cache memory (L1/L2), access is very fast (a few nanoseconds).
If not, the CPU makes a cache miss and pulls data from RAM.
When a method is actively running, data associated with the stack (for example, int a = 42) almost always ends up in the cache – this is called cache locality.

What is CPU cache:
The processor cache is a multi-level fast memory built into the processor. Its purpose is to reduce the number of accesses to random access memory (RAM), because RAM is slow compared to the CPU.

Main levels:
L1 (very fast, small – per core)
L2 (slightly slower, but larger – also per core)
L3 (even larger, slower – shared across all cores)
L4 (rarely used, usually in server CPUs)

JIT and cache optimization:
The HotSpot JIT compiler (C2) generates machine code that:
Uses CPU registers as much as possible for "hot" variables (so as not to access memory at all).
Layouts variables so that frequently used data is close together in memory → this improves spatial locality, which helps the cache.

Cache and thread switching:
During context switching, caches may be partially invalidated (for example, the L1 cache of another thread/core may not contain the required data).
This makes frequent switching between threads less efficient if they operate on different parts of memory.
So it's important to keep data "close" – this is one of the reasons why hot functions are much faster in JIT interpretation.

False sharing and cache lines:
The cache works with cache lines (usually 64 bytes).
If two threads write to different variables that are in the same cache line, false sharing occurs – and the processor is forced to synchronize the caches between cores.
JVM tries to avoid this, for example by using @Contended annotation (internal optimization for concurrent structures).

Thread stack (local variables) -> Often in L1 cache
JVM operands -> Often placed in registers/cache
JIT code -> Generated with cache locality in mind
Frequently called methods -> Their code and data remain in caches
Thread switching -> May cause cache misses

Context switching:
When the OS switches context (thread changes), it saves the CPU registers, including the stack pointer.
When you return to the thread, the stack continues to be used as if nothing had happened.

Visualization example (simplified):

fun foo(a: Int): Int {
    return bar(a * 2)
}

fun bar(x: Int): Int {
    return x + 1
}

foo is called → JVM stack gets foo's frame.
bar is called → JVM stack gets frame bar.
x + 1 is executed → x is pushed onto the stack bar, then the result.
bar is complete → its frame is removed.
We return to foo, which is also completed → its frame is deleted.

▲To the list of questions▲

Copyright: Roman Kryvolapov