Skip to content

Java / Kotlin Core – Theoretical Questions

Open all questions about Spring

Open all questions about Android

Content:

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

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

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

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

Quadratic time (O(n²)):
operations are performed in time quadratically dependent on the size of the collection. Examples of operations with such complexity assessment: sorting elements in a List using the Bubble Sort algorithm.

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

▲To the list of questions▲

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

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

Observer:
establishes a dependency between objects so that if one object changes, all its dependent objects are notified and automatically updated.

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

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

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

interface Transport {
    fun deliver(): String
}

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

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

enum class TransportType {
    CAR,
    TRUCK,
}

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

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

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

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

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

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

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

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

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

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

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

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

abstract class CondimentDecorator : Beverage()

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

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

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

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

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

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

class Order {
    // order details
}

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

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

abstract class Pizza {

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

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

    protected abstract fun addIngredients()

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

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

class PepperoniPizza : Pizza() {

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

class MargheritaPizza : Pizza() {

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

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

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

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

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

interface PaymentStrategy {
    fun pay(amount: Double)
}

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

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

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

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

▲To the list of questions▲

Inheritance and composition are two different approaches to designing classes in object-oriented programming.

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

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

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

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

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

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

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

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

Differences between inheritance and composition:

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

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

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

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

▲To the list of questions▲

The principles collected by Robert Martin, the author of Clean Architecture, I will quote the answers from it

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

This is a bad implementation because there are many reasons to change it:

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

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

class Man {
}

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

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

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

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

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

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

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

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

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

Bad implementation, it changes the way the parent class methods work:

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

The correct implementation option:

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

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

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

A bad implementation, it does not use the work method for Intern:

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

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

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

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

▲To the list of questions▲

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

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

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

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

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

▲To the list of questions▲

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

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

Object-oriented programming imposes a restriction on indirect transfer of control

Functional programming imposes a restriction on assignment

see Robert Martin, Clean Architecture

▲To the list of questions▲

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

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

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

▲To the list of questions▲

Copyright: Roman Kryvolapov