Java / Kotlin Core – Теоретични въпроси
Какви нива на сложност има при колекциите
Constant time (O(1)):
операциите се изпълняват за постоянно време, което не зависи от размера на колекцията. Примери за операции с такава оценка на сложността: добавяне и премахване на елементи от HashSet, получаване на елемент по индекс от Array.
Logarithmic time (O(log n)):
операциите се изпълняват за време, което зависи логаритмично от размера на колекцията. Примери за операции с такава оценка на сложността: добавяне и премахване на елементи от TreeSet, търсене на елемент в TreeMap.
Linear time (O(n)):
операциите се изпълняват за време, което зависи линейно от размера на колекцията. Примери за операции с такава оценка на сложността: търсене на елемент в ArrayList, премахване на елемент от LinkedList.
Linearithmic time (O(n log n)):
операциите се изпълняват за време, линейно умножено по логаритъма от размера на колекцията. Примери за операции с такава оценка на сложността: сортиране на елементите в List с алгоритъма Merge Sort.
Quadratic time (O(n²)):
операциите се изпълняват за време, което зависи квадратично от размера на колекцията. Примери за операции с такава оценка на сложността: сортиране на елементите в List с алгоритъма Bubble Sort.
Exponential time (O(2^n)):
операциите се изпълняват за време, което зависи експоненциално от размера на колекцията. Примери за операции с такава оценка на сложността: изчисляване на всички подмножества на дадено множество.
Кои са най-често използваните шаблони
Singleton:
гарантира, че класът има само един екземпляр, и предоставя глобална точка за достъп до него.
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:
установява зависимост между обектите така, че когато един обект се промени, всички зависещи от него обекти получават уведомление за това и се обновяват автоматично.
Builder:
позволява да се създават обекти чрез поетапен процес, в който могат да се задават различни параметри, и накрая да се получи обект с определени свойства.
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) }}val person = Person.Builder() .firstName("John") .lastName("Doe") .build()Factory:
предоставя общ интерфейс за създаване на обекти, но делегира същинското създаване на обектите на подкласовете.
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:
преобразува интерфейса на един клас в интерфейс, който очаква друг клас, за да могат да взаимодействат помежду си.
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 var mediaAdapter: MediaAdapter? = null 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:
динамично добавя нова функционалност към обектите, като ги обвива в други обекти, които имат тази функционалност.
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:
предоставя унифициран интерфейс към група интерфейси в една подсистема и така опростява взаимодействието с нея.
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:
определя основата на един алгоритъм, но позволява на подкласовете да променят някои от стъпките му, без да се променя общата му структура.
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 pizzaStrategy:
При използването на шаблона „стратегия“ изнасяме част от логиката от основния код в отделни класове, които реализират общ интерфейс. След това в основния код можем да избираме нужната реализация в зависимост от определени условия, без да се налага да използваме множество условни оператори.
interface PaymentStrategy { fun pay(amount: Double)}
class CreditCardStrategy(private val cardNumber: String, private val cvv: String) : PaymentStrategy { override fun pay(amount: Double) { // логика за плащане с кредитна карта }}
class PayPalStrategy(private val email: String, private val password: String) : PaymentStrategy { override fun pay(amount: Double) { // логика за плащане с PayPal }}
class PaymentProcessor(private val paymentStrategy: PaymentStrategy) { fun processPayment(amount: Double) { paymentStrategy.pay(amount) }}
// използванеval paymentStrategy = if (useCreditCard) { CreditCardStrategy(cardNumber, cvv)} else { PayPalStrategy(email, password)}val paymentProcessor = PaymentProcessor(paymentStrategy)paymentProcessor.processPayment(amount)Каква е разликата между наследяването и композицията
Наследяването и композицията са два различни подхода към проектирането на класове в обектно-ориентираното програмиране.
Наследяване:
това е механизъм, който позволява да се създаде нов клас на базата на вече съществуващ клас (базов клас или суперклас). При това новият клас (подклас или производен клас) наследява всички свойства и методи на базовия клас, а също така може да добавя свои собствени свойства и методи.
// Базов класopen class Person(val name: String, val age: Int) { open fun introduce() { println("Hi, my name is $name and I am $age years old.") }}
// Клас наследник с допълнителни параметри в конструктора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.}Композиция:
това е механизъм, при който обектите на един клас използват обекти на други класове, за да изпълняват своите функции. Класът, който използва друг клас, се нарича съставен (или компонент), а класът, чиито обекти се използват, се нарича композит. Съставният клас съдържа обекти на други класове като свои свойства и използва техните методи, за да реализира своите функции.
// Клас, представляващ двигателclass Engine(val type: String, val horsepower: Int) { fun start() { println("Engine of type $type with $horsepower HP started.") }}
// Клас, използващ композиция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.}Разлики между наследяването и композицията:
Връзка:
Наследяването е отношение „е“, където подкласът е разширение на суперкласа. Композицията е отношение „има“, където обектът на един клас има референция към обект на друг клас.
Гъвкавост:
Композицията е по-гъвкава от наследяването. При наследяването промяната в суперкласа може да се отрази на всички подкласове, което не винаги е желано. При композицията обектите могат лесно да се заменят с други обекти, ако те реализират необходимия интерфейс или абстрактен клас.
Повторно използване:
Композицията осигурява по-висока степен на повторно използване на кода, тъй като обектите могат да се използват в различни контексти. При наследяването повторното използване на кода е възможно само в рамките на йерархията на наследяване.
Принцип за заместване на Барбара Лисков:
Ако наследяването се използва неправилно, то може да нарушава принципа за заместване на Барбара Лисков (Liskov substitution principle), който гласи, че всеки екземпляр на даден клас трябва да може да се замени с всеки екземпляр на неговия подклас, без да се наруши коректността на програмата. При композицията този принцип не се нарушава, тъй като обектите на различни класове могат да се заменят с обекти, които реализират един и същ интерфейс.
Какво е SOLID
Принципите са събрани от Робърт Мартин, автора на „Чиста архитектура“, от нея ще цитирам и отговорите
Single Responsibility Principle:
принцип за единствената отговорност. Класът трябва да има само една причина за промяна.
Лош вариант на реализация, тъй като в него има много причини за промяна:
class Man { void work() {} void eat() {} void sleep() {}}Правилен вариант на реализация, в който във всеки клас има само една причина за промяна:
class Man {}
class Work extends Man { void work() {}}
class Eat extends Man { void eat() {}}
class Sleep extends Man { void sleep() {}}// шаблон Фасада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:
принцип за отвореност/затвореност, софтуерните същности трябва да са отворени за разширение и затворени за промяна
Лош вариант на реализация, тъй като посоката на зависимостите е 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(){ }}Правилен вариант на реализация, тъй като посоката на зависимостите е 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:
принцип за заместване на Барбара Лисков. За да се създават софтуерни системи от взаимозаменяеми части, тези части трябва да отговарят на контракт, който позволява да се заменят една с друга. При наследяването не бива да засягаме функционалността на родителските класове
Лош вариант на реализация, в него се променя работата на методите на родителския клас:
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(); // тук резултатът ще е 144, а не 120 }}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; }}Правилен вариант на реализация:
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:
принцип за разделяне на интерфейсите. Да се избягва зависимост от всичко, което не се използва. Не бива да има ситуации, в които методите на интерфейсите не се използват при реализацията им.
Лош вариант на реализация, в него методът work не се използва за 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"); }}Правилен вариант на реализация, разбиваме интерфейса на няколко:
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:
принцип за инверсия на зависимостта. Кодът, който реализира политиката на високо ниво, не трябва да зависи от кода, който реализира детайлите на ниско ниво. Напротив, детайлите трябва да зависят от политиката. Всички зависимости в изходния код пресичат тази граница в една посока – към абстракцията.
Абстрактният компонент съдържа всички бизнес правила на високо ниво на приложението. Конкретният компонент съдържа детайлите на реализацията на тези правила.
Какво представляват функциите от по-висок ред
Функциите от по-висок ред са функции, които могат да приемат други функции като аргументи или да връщат функции като резултат. Това означава, че функциите от по-висок ред могат да се разглеждат като обекти от първи клас в езика за програмиране.
В езиците, които поддържат функции от по-висок ред, функциите могат да се използват като стойности и да се предават на други функции. Например функция от по-висок ред може да приеме друга функция като аргумент и да я приложи към други данни.
Функциите от по-висок ред могат да се използват за създаване на по-абстрактни и гъвкави алгоритми и API. Те могат да опростят кода, да намалят дублирането му и да подобрят четимостта му.
// пример за функция от по-висок ред, която приема функция като аргументfun applyOperation( value: Int, operation: (Int) -> Int): Int { return operation(value)}// пример за използване на функция от по-висок редval result = applyOperation(5) { value -> value * 2} // резултатът ще е 10В този пример функцията applyOperation приема аргумент operation, който е функция, приемаща един аргумент от тип Int и връщаща резултат от тип Int. Във функцията applyOperation се извиква operation с аргумента value, а резултатът се връща от функцията applyOperation.
Каква е разликата между структурната, функционалната и обектно-ориентираната парадигма в програмирането
Съществуват 3 парадигми в програмирането, открити са между 1958 и 1968 г. Всяка от парадигмите налага ограничения върху кода – парадигмите ни казват какво не бива да се прави.
Структурното програмиране налага ограничение върху директното предаване на управлението (например с оператора goto / jump)
Обектно-ориентираното програмиране налага ограничение върху индиректното предаване на управлението
Функционалното програмиране налага ограничение върху присвояването
вж. Робърт Мартин, „Чиста архитектура“
Какви формати за обмен на данни със сървъра съществуват
JSON(JavaScript Object Notation):
е създаден като алтернатива на по-сложните формати за обмен на данни, като XML, и е предназначен да бъде лесен за четене и писане както от човек, така и от компютър. Форматът JSON представлява набор от двойки „ключ-стойност“, където ключът е низ, а стойността може да бъде всеки допустим тип данни, включително обекти, масиви, числа, низове, булеви стойности и null.
SOAP (Simple Object Access Protocol):
основава се на XML (Extensible Markup Language) и определя формата на съобщението, както и правилата за неговата обработка.
Protobuf(Protocol Buffers):
двоичен формат, който не е четим от човек, за разлика от формати като XML и JSON. Вместо това той представлява последователност от байтове, които могат да се разчетат само със специален инструмент. Самият протокол protobuf определя език за описание на данни, който се използва за описание на структурата на данните, които ще се предават по мрежата. Този език има свой синтаксис, който се използва за описание на полетата и техните типове данни. Всяко поле в структурата от данни има уникален идентификатор и тип данни. Позволява компактно и ефективно сериализиране на данните за предаване по мрежата, като същевременно осигурява висока производителност и съвместимост между различни версии на приложенията.