Java Core – Основни въпроси
Какви типове данни има в Java и какъв е размерът им
Целочислени:
byte:
8 бита, от -128 до 127
short:
16 бита или 2 байта, от -32_768 до 32_767
int:
32 бита или 4 байта, от -2_147_483_648 до 2_147_483_647
long:
64 бита или 8 байта, от -9_223_372_036_854_775_808 до 9_223_372_036_854_775_807
ако стойността, записвана в long, надвишава максималната стойност за int, след числото трябва да се добави буквата l или L
С плаваща запетая:
float:
32 бита или 4 байта, от ~1,410 на степен -45 до ~3,410 на степен 38
double:
64 бита или 8 байта, от ~4,910 на степен -324 до ~1,810 на степен 308
Символ:
char:
16 бита или 2 байта, от ‘\u0000’ или 0 до ‘\uffff’ или 65_535
Логически:
boolean: 8 бита или 1 байт;
Трябва да помним също, че:
String не е примитивен тип данни
в Java примитивните типове не могат да бъдат null, а референтните типове могат да бъдат null
в Kotlin и примитивните, и референтните типове могат да бъдат null, ако са обявени като nullable, тоест като такива, които могат да бъдат null (var i: Int? = null). В Kotlin няма примитивни типове в явен вид, компилаторът сам решава дали да използва примитивен тип, или обвивка
Какви модификатори има в Java и Kotlin
В Java и Kotlin областите на видимост (модификаторите за достъп) определят кой може да получи достъп до класовете, методите, полетата и другите елементи на кода. Нека разгледаме основните области на видимост и в двата езика.
Модификатори за достъп в Java
В Java има 4 нива на видимост:
private:
Достъпен само вътре в класа, в който е обявен.
class MyClass { private int myVar = 10; // достъпен само вътре в MyClass}default (видимост на ниво пакет):
Ако модификаторът не е указан, се използва видимостта по подразбиране. Елементът е достъпен само вътре в пакета, но е недостъпен за класове от други пакети.
class MyClass { int myVar = 10; // достъпен вътре в текущия пакет}protected:
Достъпен вътре в същия пакет и за класовете-наследници, дори да се намират в друг пакет.
class MyClass { protected int myVar = 10; // достъпен в пакета и за наследниците}public:
Достъпен отвсякъде.
Модификатори за достъп в Kotlin
В Kotlin има 4 нива на видимост, но семантиката им е малко по-различна от тази в Java:
private:
За клас: достъпен само вътре в този клас.
За пакет: достъпен само вътре във файла, в който е обявен.
class MyClass { private val myVar = 10 // достъпен само в MyClass}internal:
Достъпен вътре в един модул. Това е нивото на видимост, което е уникално за Kotlin и ограничава достъпността на елементите до модула, а не до пакета.
internal class MyClass { internal val myVar = 10 // достъпен само вътре в модула}protected:
Достъпен в класа и в неговите подкласове. За разлика от Java, не е достъпен в рамките на пакета (освен ако класът не е наследник).
open class MyClass { protected val myVar = 10 // достъпен в класовете-наследници}public:
Достъпен навсякъде. Това е видимостта по подразбиране в Kotlin (ако модификаторът не е указан).
class MyClass { val myVarOne = 10 // достъпен за всички public val myVarTwo = 10 // достъпен за всички}Сравнение на Java и Kotlin
private: и в двата езика работи сходно, с изключение на това, че в Kotlin private на ниво пакет ограничава достъпа вътре във файла, а не в целия пакет.
default: в Java е аналогичен на internal в Kotlin, но видимостта в Java е ограничена до пакета, а в Kotlin – до модула.
protected: в Java е достъпен в пакета и за наследниците, а в Kotlin – само за наследниците.
public: еднакъв е в двата езика.
internal: модификатор, който го няма в Java. Той ограничава достъпа на ниво модул.
Всички модификатори
При методите и променливите в Java:
private:
достъпен вътре в класа
default (не е указан):
достъпен вътре в пакета
protected:
достъпен вътре в пакета или от класове-наследници
public:
достъпен навсякъде
static:
методът или променливата са статични
transient:
променливата не подлежи на сериализация и не трябва да участва в припокриването на equals() и hashCode()
final за метод:
методът не може да бъде припокрит
final за променлива:
променливата е константа
abstract:
методът на абстрактния клас е абстрактен и няма реализация
При класовете в Java:
default (не е указан):
достъпен вътре в пакета
public:
достъпен навсякъде
final:
от класа не може да се наследява
abstract:
класът е абстрактен и може да съдържа абстрактни методи
record:
това е специален вид клас, въведен в Java 14 (в предварителен вид) и окончателно включен в Java 16. Record предоставя кратък и изразителен начин за дефиниране на класове, които са прости контейнери за неизменяеми данни. Той значително опростява създаването на класове, които просто съхраняват данни, като автоматично генерира конструктори и методите equals(), hashCode() и toString().
При интерфейсите в Java:
default:
от Java 8 насам – обикновен метод вътре в интерфейса, предназначен да намали дублирането на код, ако във всички реализации на интерфейса има един и същ метод; при необходимост може да се припокрие в реализацията. Ако клас имплементира 2 интерфейса, в които има default методи с еднакви имена, ще възникне конфликт на имената и методът ще трябва да се припокрие.
static:
от Java 8 насам – статичен метод в интерфейса, който се извиква само от името на интерфейса, но не и от името на инстанция. Статичните методи не се наследяват от имплементиращия клас и не могат да се припокриват. От Java 9 насам може да бъде private, за да може да се извика от друг статичен метод.
В Kotlin има също:
internal:
достъпен вътре в модула
public (не е указан)
open за клас:
от класа може да се наследява, в противен случай не може,
open за метод:
методът може да бъде припокрит, в противен случай не може
object:
използва се за обявяване на singleton и за създаване на анонимни обекти, които заменят анонимните вътрешни класове в Java, или за вътрешни параметри
vararg:
позволява да се предава нефиксиран брой аргументи за един параметър
init:
блок за инициализация
lateinit:
свойство с късна инициализация
typealias:
предоставя алтернативни имена за съществуващи типове
::
референция към клас, функция или свойство
// референция към класval c = MyClass::class// референция към свойствоvar x = 1fun main(args: Array<String>) { println(::x.get()) // ще изведе "1" ::x.set(2) println(x) // ще изведе "2"}// референция към функцияfun isOdd(x: Int) = x % 2 != 0val numbers = listOf(1, 2, 3)println(numbers.filter(::isOdd)) // ще изведе [1, 3]// референция към конструкторclass Foofun function(factory : () -> Foo) { val x : Foo = factory()}value class:
инлайн клас, който отсъства в байткода – вместо него ще бъде неговата стойност; предназначен е за обвиване на данни и не води до спад на производителността
constructor:
допълнителен конструктор (secondary constructors)
inline:
модификатор на функция; използването на функции от по-висок ред (функция от по-висок ред е функция, която приема функции като параметри или връща функция като резултат.) води до намаляване на производителността. Модификаторът inline влияе и на функцията, и на ламбдата, предадена на нея: и двете ще бъдат вградени на мястото на извикването. Вграждането на функции може да увеличи количеството генериран код, но ако го правите в разумни граници (не вграждате големи функции), ще получите повишаване на производителността, особено при извикване на функции с параметри от различен тип в цикли.
noinline:
ако искате само някои от ламбдите, предадени на inline функцията, да бъдат вградени, трябва да отбележите с модификатора noinline онези функции-параметри, които няма да бъдат вградени
data class:
клас за данни; компилаторът автоматично формира следните членове на този клас от свойствата, обявени в основния конструктор: equals()/hashCode(), toString(), componentN(), copy()
inner class:
в Kotlin статичните вътрешни класове нямат никакъв модификатор освен class, а обикновените вътрешни класове, за достъп до които е необходима инстанция на външния клас, имат модификатор inner class
sealed class:
добавянето на модификатора sealed към суперкласа ограничава възможността за създаване на подкласове. Всички преки подкласове трябва да бъдат вложени в суперкласа. Запечатаният клас не може да има наследници, обявени извън класа. По подразбиране запечатаният клас е отворен и модификаторът open не е необходим. Запечатаните класове малко приличат на enum. В запечатания клас могат да се създадат толкова подкласове, колкото са нужни, за да се покрие всяка ситуация. Освен това всеки подклас може да има няколко инстанции, всяка от които ще носи собственото си състояние. Всеки подклас на запечатания клас има свой конструктор със свои индивидуални свойства. Запечатаните класове са enum със суперсила. Запечатаният клас може да има наследници, но всички те трябва да се намират в един и същ файл със запечатания клас. Класовете, които разширяват наследниците на запечатания клас, могат да бъдат където и да е.
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)Запечатаните класове са абстрактни и могат да съдържат абстрактни компоненти. Конструкторът на запечатания клас винаги е private и това не може да се промени. Запечатаните класове не могат да се инициализират. Наследниците на запечатания клас могат да бъдат класове от всякакъв вид: клас за данни, обект, обикновен клас или дори друг запечатан клас.
Трябва да помним също, че:
Всички променливи в интерфейсите по подразбиране са final
При наследяване и припокриване може да се разширява нивото на достъп default -> protected -> public
При наследяване и припокриване може да се стеснява нивото на грешките, например Exception -> RuntimeException -> ArithmeticException
Какво е Default method
Default method в Java
Default method (или метод по подразбиране) е метод с реализация, който може да бъде обявен в интерфейс. В Java възможността за създаване на методи по подразбиране се появи във версия Java 8.
Основни особености на default method:
Метод с реализация:
За разлика от обикновените методи на интерфейса, които трябва да бъдат реализирани в класовете, имплементиращи този интерфейс, default method вече съдържа своята реализация.
Не нарушава съвместимостта:
Добавянето на нови методи в интерфейсите преди Java 8 би довело до необходимост да се променят всички класове, които имплементират този интерфейс. Сега с помощта на методите по подразбиране могат да се добавят нови методи в интерфейса, без да се налага да се пренаписват всички съществуващи класове.
Наследяване:
Класовете, които имплементират интерфейса, могат да припокриват метода по подразбиране, ако е нужна специална логика. Ако не го правят, се използва реализацията по подразбиране, указана в интерфейса.
Повторно използване на код:
Общата функционалност може да се постави в метод по подразбиране, за да се избегне дублирането на код в различни класове.
Разширяване на функционалността:
Default methods позволяват да се разширяват интерфейсите чрез добавяне на нови методи, като същевременно се запазва възможността те да се използват в стария код.
interface MyInterface { // Обикновен абстрактен метод void regularMethod(); // Метод по подразбиране с реализация 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."); } // Методът по подразбиране може да се припокрие, ако е нужно @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. }}Default method в Kotlin
Концепцията за default methods в интерфейсите в Kotlin е реализирана малко по-различно, отколкото в Java, но функционално присъства чрез методи с реализация в интерфейсите.
В Kotlin интерфейсите могат да съдържат реализация на методи и класовете, които имплементират тези интерфейси, могат да използват тази реализация или да припокриват методите при необходимост. Това е аналогично на методите по подразбиране в Java.
interface MyInterface { // Обикновен метод, който трябва да се реализира в класа fun regularMethod() // Метод с реализация по подразбиране fun defaultMethod() { println("This is the default method implementation.") }}
class MyClass : MyInterface { override fun regularMethod() { println("Implemented regular method.") } // Методът по подразбиране може да се припокрие, ако е нужно override fun defaultMethod() { println("Overridden default method.") }}
fun main() { val myClass = MyClass() myClass.regularMethod() // Output: Implemented regular method. myClass.defaultMethod() // Output: Overridden default method.}default method в Java или методите с реализация в интерфейсите на Kotlin не могат да имат състояние директно, тъй като самите интерфейси не могат да съхраняват състояние.
Интерфейсите не могат да имат инстанционни променливи (полета), които биха могли да съхраняват състояние между извикванията на методите. Това е така, защото интерфейсите не са предназначени за съхраняване на състояние – основната им роля е да предоставят контракт (набор от методи), който класовете трябва да реализират.
Същевременно методите по подразбиране могат да достъпват статичните променливи на интерфейса или данни, предадени като аргументи. Но те не могат да имат собствени променливи на инстанцията, които да съхраняват състояние.
С какво Default method се различава от статичния метод:
Default method и статичният метод в интерфейсите на Java (и аналогично в Kotlin) се различават по няколко важни характеристики: приложението, контекста на извикване и взаимодействието с реализациите на интерфейса. Ето основните разлики:
Начин на извикване:
Default method:
Методът по подразбиране се извиква чрез инстанция на клас, който имплементира интерфейса.
Тоест може да бъде извикан върху обект, който имплементира интерфейса
Статичен метод:
Статичният метод се извиква не чрез обект, а чрез самия интерфейс. Той е привързан директно към интерфейса и не зависи от реализацията му в класовете.
Връзка с обекта:
Default method:
Привързан е към обекта на класа, който имплементира интерфейса.
Може да използва състоянието на обекта (например полетата на класа) или да извиква други методи на обекта.
Статичен метод:
Няма достъп до инстанцията на обекта и следователно не може да използва състоянието на обекта или да извиква нестатични методи.
Може да оперира само с данните, които са му предадени като параметри, или да работи със статични полета (ако има такива).
Наследяване и припокриване:
Default method:
Класът, който имплементира интерфейса, може да припокрие метода по подразбиране, ако трябва да предостави своя собствена реализация.
Статичен метод:
Статичните методи не могат да бъдат припокривани в класовете, които имплементират интерфейса. Те принадлежат на интерфейса и не се наследяват от класовете.
Ако в клас е обявен статичен метод със същото име, това е просто друг метод.
Достъп до статичните методи на интерфейса от неговата реализация:
Default method:
Методите по подразбиране могат да използват статичните методи на интерфейса, но за целта трябва явно да ги извикват чрез името на интерфейса.
Статичен метод:
Статичните методи могат да извикват само други статични методи на интерфейса или да работят с данни, предадени в параметрите.
Основно предназначение:
Default method:
Основната цел е да се даде възможност за добавяне на нови методи в интерфейса без нарушаване на обратната съвместимост. Тоест, ако в интерфейса бъде обявен нов метод по подразбиране, старите реализации на интерфейса няма да са задължени да го реализират.
Статичен метод:
Статичният метод най-често се използва за изпълнение на операции, които не зависят от инстанцията на класа. Това са помощни (utility) методи, които имат достъп само до статични данни и не изискват състояние на обекта.
С какво интерфейсът с Default method се различава от абстрактния клас:
Интерфейсите с default methods и абстрактните класове в Java/Kotlin имат много общо, тъй като и двата могат да предоставят частична реализация на методите. Между тях обаче има няколко ключови разлики, свързани с архитектурни и функционални аспекти. Нека разгледаме основните разлики между тях.
Наследяване и множествена имплементация:
Интерфейс с default methods:
В Java и Kotlin един клас може да имплементира няколко интерфейса. Това позволява на класа да наследява поведение от няколко източника.
Интерфейсът може да съдържа default 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 { // Разрешаваме конфликта между реализациите @Override public void printMessage() { InterfaceA.super.printMessage(); // Извикваме реализацията на InterfaceA }}В този пример класът MyClass може да имплементира едновременно два интерфейса и при необходимост да избере коя реализация да използва.
Абстрактен клас:
Един клас може да наследява само един абстрактен клас. Това се нарича единично наследяване и ограничава гъвкавостта в сравнение с интерфейсите.
Абстрактният клас може да съдържа както абстрактни методи (без реализация), така и конкретни методи (с реализация). За разлика от интерфейсите, абстрактните класове могат да имат полета и конструктори.
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"); }}Поддръжка на състояние:
Интерфейс с default methods:
Интерфейсът не може да съхранява състояние директно, тоест не може да има нестатични полета или конструктори. Всички променливи в интерфейса трябва да са статични и финални (константи).
Default methods могат да използват само методите и състоянието на класовете, които имплементират интерфейса. Това прави интерфейсите по-малко подходящи за случаите, когато е необходимо съхраняване на състояние.
interface MyInterface { default void defaultMethod() { System.out.println("Default method"); }}
class MyClass implements MyInterface { private int value = 10; // Състояние в класа
public void showValue() { System.out.println("Value: " + value); }}Абстрактен клас:
Абстрактните класове могат да съхраняват състояние – могат да съдържат полета, които могат да бъдат инициализирани чрез конструктора и да се използват в методите на класа. Това прави абстрактните класове по-гъвкави за създаване на класове с общо състояние и поведение.
Абстрактните класове могат да имат както статични, така и нестатични полета.
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); }}Множествено наследяване на поведение:
Интерфейс с default methods:
Интерфейсите с методи по подразбиране могат да са полезни за реализиране на множествено наследяване на поведение. Един клас може да имплементира няколко интерфейса, всеки от които предоставя default method. Това позволява да се избегнат проблемите, свързани с традиционното множествено наследяване (например „проблемът с диаманта“ в C++).
Ако два интерфейса имат методи с еднакви сигнатури, класът, който имплементира тези интерфейси, трябва явно да укаже коя реализация да се използва или да припокрие метода.
Абстрактен клас:
В Java абстрактният клас не поддържа множествено наследяване. Един клас може да наследява само един абстрактен клас. Това е едно от основните ограничения в сравнение с интерфейсите.
Конструктори и създаване на обекти:
Интерфейс с default methods:
Интерфейсите не могат да имат конструктори и следователно не могат да създават инстанции на самите себе си. Конструкторите са предназначени само за класове. Интерфейсът не може да капсулира логиката за създаване на обекти или инициализацията на състоянието.
Абстрактен клас:
Абстрактните класове могат да имат конструктори, които могат да се извикват от подкласовете при създаване на обект. Това позволява на абстрактните класове да предават състояние и логика за инициализация на своите подкласове.
abstract class AbstractClass { int value;
AbstractClass(int value) { this.value = value; }}
class MyClass extends AbstractClass { MyClass(int value) { super(value); }}Модификатори за достъп:
Интерфейс с default methods:
Методите в интерфейса могат да бъдат само public (по подразбиране), а също и private (от Java 9). Но интерфейсът не може да има методи с модификатор за достъп protected или default (package-private).
Полетата на интерфейса винаги са public, static и final.
Абстрактен клас:
Абстрактният клас може да съдържа методи с всякакви модификатори за достъп: public, protected, private и default (package-private). Това дава повече гъвкавост при управлението на достъпа до методите и състоянието.
Полетата в абстрактния клас също могат да имат всякакви модификатори за достъп.
Кога да използваме интерфейс с default methods и кога абстрактен клас:
Използвайте интерфейси с default methods, ако:
Трябва да предоставите контракт, който класовете трябва да реализират, с възможност за включване на базова реализация.
Трябва класът да може да имплементира няколко интерфейса едновременно.
Не е нужно да съхранявате състояние в интерфейса.
Използвайте абстрактен клас, ако:
Трябва да съхранявате състояние и логика за инициализация (чрез конструктори).
Нужна ви е обща реализация на методите и възможност тя да се наследява в подкласовете.
Трябва ви повече контрол върху модификаторите за достъп.
Какви Java Collection познавате
interface Collection:
базов интерфейс за колекциите и другите интерфейси на колекции.
interface List:
подреден списък, в който всеки елемент има индекс, допускат се дублиращи се стойности. Наследява Collection.
interface Set:
неподредено множество от уникални елементи. Наследява Collection
interface Map:
състои се от двойки „ключ-стойност“. Ключовете са уникални, а стойностите могат да се повтарят. Редът на елементите не е гарантиран. Map позволява да се търсят обекти (стойности) по ключ. Съдържа put вместо add. Съдържа методите entrySet за преобразуване на двойка ключ-стойност в Set, keySet за преобразуване на ключовете в Set и values за преобразуване на стойностите в Collection.
Не бъркайте интерфейса Collection и фреймуърка Collections. Map не наследява интерфейса Collection, не наследява нищо, но влиза в състава на фреймуърка Collections.
interface Queue:
опашка. В такъв списък елементите могат да се добавят само в края, а да се премахват – само от началото – така се реализира концепцията first in, first out. Наследява Collection
interface Deque:
може да действа и като опашка, и като стек. Това означава, че елементите могат да се добавят както в началото ѝ, така и в края. Същото важи и за премахването. Наследява Queue
class Stack:
наследява класа Vector, който от своя страна имплементира List и реализира прост механизъм от типа last in, first out.
interface Iterator:
позволява обхождане на колекциите с изключение на Map, съдържа методите hasNext, next, remove, а също и forEachRemaining
Класове:
List:
AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector
Set:
AbstractSet, ConcurrentHashMap.KeySetView, ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, JobStateReasons, LinkedHashSet, TreeSet
Map:
AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap
Queue:
AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, LinkedTransferQueue, PriorityBlockingQueue, PriorityQueue, SynchronousQueue
Deque:
ArrayDeque, ConcurrentLinkedDeque, LinkedBlockingDeque, LinkedList
Най-известните колекции:
ArrayList:
списък на базата на масив.
LinkedList:
свързан двупосочен списък.
Vector:
това е същото като ArrayList, но всички методи са синхронизирани
PriorityQueue:
опашка на базата на масив, сортирана с компаратор; за сложни класове може да се предаде компаратор в конструктора
SortedSet:
сортирани елементи
NavigableSet:
позволява да се извличат елементи въз основа на техните стойности
TreeSet:
всички обекти се съхраняват в сортиран вид във възходящ ред, работи на базата на TreeMap
HashSet:
същото като HashMap, но ключ е самият обект, работи на базата на HashMap
LinkedHashSet:
спазва реда на вмъкване на обектите, работи на базата на LinkedHashMap
HashMap:
масив ключ-стойност; до възникване на колизия представлява масив, след колизия – еднопосочен списък; при достигане на определен праг възелът от еднопосочния списък (съхранява следващия елемент) се трансформира в елемент на червено-черно дърво TreeNode
Hashtable:
синхронизиран вариант на HashMap
LinkedHashMap:
HashMap, който запомня реда на добавяне на елементите; той се запазва при извличане с помощта на entrySet или други методи. В Entry освен възела се съхранява референция към следващия и предишния елемент.
TreeMap:
сортиран map на базата на червено-черно дърво
Трябва да помним също, че:
В Kotlin колекциите се делят на променяеми (mutable) и непроменяеми (immutable).
Какви нива на сложност има при операциите с колекции
Списъци (Lists):
ArrayList:
Добавяне в края: O(1) амортизирано
Добавяне в средата или в началото: O(n)
Изтриване от края: O(1)
Изтриване от средата или от началото: O(n)
Достъп по индекс: O(1)
Търсене: O(n)
LinkedList:
Добавяне в началото или в края: O(1)
Добавяне в средата: O(1) (при наличие на референция към възела) или O(n) (търсене на възела)
Изтриване от началото или от края: O(1)
Изтриване от средата: O(1) (при наличие на референция към възела) или O(n) (търсене на възела)
Достъп по индекс: O(n)
Търсене: O(n)
Множества (Sets):
HashSet:
Добавяне: O(1) амортизирано
Изтриване: O(1) амортизирано
Търсене: O(1) амортизирано
LinkedHashSet:
Добавяне: O(1) амортизирано
Изтриване: O(1) амортизирано
Търсене: O(1) амортизирано
Итерация: O(n) (запазва реда на вмъкване)
TreeSet:
Добавяне: O(log n)
Изтриване: O(log n)
Търсене: O(log n)
Карти (Maps):
HashMap:
Добавяне: O(1) амортизирано
Изтриване: O(1) амортизирано
Търсене: O(1) амортизирано
LinkedHashMap:
Добавяне: O(1) амортизирано
Изтриване: O(1) амортизирано
Търсене: O(1) амортизирано
Итерация: O(n) (запазва реда на вмъкване)
TreeMap:
Добавяне: O(log n)
Изтриване: O(log n)
Търсене: O(log n)
Опашки (Queues и Deques):
PriorityQueue:
Добавяне: O(log n)
Изтриване: O(log n)
Търсене: O(n)
ArrayDeque:
Добавяне в началото или в края: O(1)
Изтриване от началото или от края: O(1)
Търсене: O(n)
LinkedList (използва се като опашка или дек):
Добавяне в началото или в края: O(1)
Изтриване от началото или от края: O(1)
Търсене: O(n)
Таблици (Tables):
ConcurrentHashMap:
Добавяне: O(1) амортизирано
Изтриване: O(1) амортизирано
Търсене: O(1) амортизирано
Какво е HashMap
HashMap използва хеш-таблица за съхраняване на картата, като осигурява бързо време за изпълнение на извикванията get() и put(). Хешовете се съхраняват в Bucket. Трябва да бъдат припокрити методите equals() и hashCode(). Hash представлява Integer, в който се преобразува ключът. Първо се сравнява хешът, ако е еднакъв, се сравняват ключовете, тъй като е възможна колизия, когато хешовете са еднакви, а ключовете – не.
DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16DEFAULT_LOAD_FACTOR = 0.75f;MAXIMUM_CAPACITY = 1 << 30 // 1_073_741_824 или половината от max intклас за съхраняване на данни
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; // тук се съхранява следващият възел, ако хешът съвпада Node<K,V> next;}Ако в
transient Node<K,V>[] table;няма елементи с предадения на метода
final V putVal( int hash, K key, V value, boolean onlyIfAbsent, boolean evict)хеш, се изпълнява
tab[i] = newNode(hash, key, value, null);а ако има – в Node<K,V> next на класа Node се поставя следващият елемент с помощта на
p.next = newNode(hash, key, value, null);Ако размерът не достига, се изпълнява
newCap = oldCap << 1 // или умножение по 2Полученият хеш код може да е огромна числова стойност, а първоначалният масив е условно изчислен само за 16 елемента. Затова хеш кодът трябва да се трансформира в стойности от 0 до 15 (ако размерът на масива е 16), за целта се използват допълнителни преобразувания.
В Java 8 след достигане на определен праг вместо свързани списъци се използват балансирани червено-черни дървета TreeNodes (работят аналогично на TreeMap).
Това означава, че HashMap в началото съхранява обектите в свързан списък, но след като броят на елементите в хеша достигне определен праг, се преминава към балансирани дървета, а след това може да се преобразува обратно в обикновен възел.
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;}В Kotlin също може да се задава с помощта на to
val map = mapOf("a" to 1, "b" to 2, "c" to 3)ако Hash в HashMap съвпадат, към елемента се добавя нов запис, сочещ към стария запис; за да се прегледат записите в този елемент, ще е необходимо да се мине през свързания списък със записи. Това се нарича Метод на веригите
Как да сортираме колекция, съдържаща сложен клас, Comparator и Comparable
Comparable:
класът трябва да имплементира интерфейса Comparable, да впише като генерик класа (implements Comparable), да реализира метода compareTo
Comparator:
Класът Comparator е функционален интерфейс, който се използва за сравняване на обекти. В езика Kotlin могат да се използват ламбда изрази за създаване на инстанции на класа Comparator.
Например, ако имаме клас Person със свойства name и age, можем да създадем Comparator за сортиране на списък от Person по възраст по следния начин:
Set set = new TreeSet<String>(new Comparator<String>() { public int compare(String i1, String i2) { return i2.compareTo(i1); }});// илиComparator<String> comparator = (o1, o2) -> o1.compareTo(o2);Set set = new TreeSet<String>(comparator);// или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)}Пример за сортиране за 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) // Резултат: 1println(setTwo.size) // Резултат: 2Поведение на BigDecimal:
Класът BigDecimal имплементира интерфейса Comparable<BigDecimal>.
BigDecimal(1) и BigDecimal("1.0") имат една и съща числова стойност (1), но различни представяния по отношение на точността:
BigDecimal(1) има точност 0 (цяло число).
BigDecimal("1.0") има точност 1 (с дробна част).
Тези два обекта се считат за различни при използване на методите equals() и hashCode(), тъй като те отчитат не само стойността, но и точността.
TreeSet:
TreeSet в Java/Kotlin е множество, което сортира елементите въз основа на естествения им ред или по зададен компаратор.
За класа BigDecimal редът се определя от метода compareTo(), който сравнява само числовата стойност и игнорира точността.
Тъй като числовата стойност на BigDecimal(1) и BigDecimal("1.0") е една и съща, методът compareTo() ще върне 0, което ще накара TreeSet да счита тези обекти за еднакви.
Затова в TreeSet ще бъде добавен само един елемент (или BigDecimal(1), или BigDecimal("1.0")).
HashSet:
HashSet използва методите equals() и hashCode() за сравняване на обекти.
Както вече обсъдихме, BigDecimal(1) и BigDecimal("1.0") се считат за различни обекти, защото имат една и съща числова стойност, но различна точност.
В резултат и двата обекта ще бъдат добавени в HashSet.
Каква е разликата между ArrayList и LinkedList
ArrayList:
списък на базата на масив.
LinkedList:
свързан двупосочен списък.
Ако често се добавят и премахват елементи, особено от средата или началото на списъка, по-добре е LinkedList, в останалите случаи – ArrayList
При създаване в ArrayList DEFAULT_CAPACITY = 10 или в конструктора може да се предаде количество; при достигане масивът се увеличава 1,5 пъти с помощта на метода grow
int newCapacity = oldCapacity + (oldCapacity >> 1)В LinkedList информацията се съхранява във вътрешния статичен клас Node. Имплементира Queue и е удобно да се използва като опашка, съответно има методи на опашки – peek (извличане без изтриване), poll (извличане с изтриване)
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:*
Когато започнете итерация по LinkedList, механизмът обхожда възлите на списъка един след друг, започвайки от главата (head) и движейки се към опашката (tail). Всеки възел съхранява референции към следващия и предишния възел, което позволява придвижване по веригата от възли.
ListIterator предоставя повече възможности, като например придвижване назад (към предишния възел).
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:
Итерацията по ArrayList с помощта на Iterator се основава на пряк достъп до елементите на масива по техния индекс. Обхождането на елементите се извършва линейно, но достъпът до всеки елемент има сложност O(1).
ListIterator предоставя допълнителни възможности, като придвижване по елементите както напред, така и назад, промяна на елементи, получаване на индекса на текущия елемент и т.н.
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(); } }}Как работи TreeMap
TreeMap в Java е реализация на интерфейса NavigableMap, която съхранява ключовете в сортиран ред с помощта на червено-черно дърво. Червено-черното дърво е самобалансиращо се двоично дърво за търсене, което поддържа подредеността на елементите и осигурява ефективно изпълнение на основните операции като добавяне, изтриване и търсене.
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;}Сортиран ред:
Всички елементи (двойки „ключ-стойност“) в TreeMap се съхраняват в сортиран ред по ключовете. Ключовете се сортират според естествения си ред (чрез интерфейса Comparable) или с помощта на външен компаратор (чрез интерфейса Comparator), ако е бил предаден при създаването на картата.
Самобалансиращо се дърво:
TreeMap използва червено-черно дърво за съхраняване на данните. Това е самобалансиращо се двоично дърво за търсене, което осигурява следните времеви характеристики на операциите:
Вмъкване (put): O(log n)
Изтриване (remove): O(log n)
Търсене (get): O(log n)
Уникални ключове:
Както и другите реализации на интерфейса Map, TreeMap не допуска дублиращи се ключове. Ако се опитате да вмъкнете елемент с ключ, който вече съществува в картата, новата стойност ще замени старата.
Вътрешно устройство на TreeMap:
Ключове и стойности: TreeMap съхранява двойки „ключ-стойност“ във възлите на червено-черното дърво. Всеки възел на дървото съдържа ключ, стойност, референция към лявото поддърво, референция към дясното поддърво и цвят на възела (червен или черен).
Интерфейси Comparable и Comparator:
Ако ключовете имплементират интерфейса Comparable, те ще се сравняват помежду си въз основа на естествения си ред (например за числата това е възходящ ред).
Можете също да предадете Comparator при създаването на TreeMap, за да контролирате реда на сортиране на ключовете. В този случай елементите ще бъдат сортирани по този компаратор.
Вмъкване на елементи (метод put):
Когато добавяте нова двойка „ключ-стойност“ в TreeMap, първо ключът се сравнява с другите ключове, които вече са в дървото.
Алгоритъмът за двоично търсене използва метода compareTo() (ако ключовете имплементират Comparable) или предадения компаратор (Comparator), за да определи къде да се вмъкне новият ключ.
Ако ключът вече съществува, стойността се обновява, но ключът остава на мястото си.
Търсене на елемент (метод get):
Търсенето на елемент по ключ се извършва с помощта на двоично търсене. Алгоритъмът върви по дървото, започвайки от корена, и на всеки възел решава дали да върви наляво (ако търсеният ключ е по-малък от текущия), или надясно (ако ключът е по-голям от текущия).
Операцията за търсене в TreeMap има сложност O(log n).
Изтриване на елементи (метод remove):
Изтриването на елемент от TreeMap също протича с балансиране на дървото. Ако изтриваният възел има два дъщерни възела, дървото ще намери най-малкия елемент в дясното поддърво и ще замени изтривания възел с него, за да се запази редът.
Операцията за изтриване също се изпълнява за O(log n).
Поддържане на баланса на дървото:
Червено-черното дърво поддържа баланс чрез преоцветяване на възлите и извършване на ротации (лява или дясна ротация) при вмъкване или изтриване на възли. Това гарантира, че височината на дървото винаги остава логаритмична спрямо броя на елементите, което позволява ефективно изпълнение на операциите.
Предимства и недостатъци на TreeMap:
Предимства:
Подреденост:
Всички ключове се съхраняват в сортиран ред.
Логаритмична сложност:
Вмъкването, изтриването и търсенето се изпълняват за O(log n), което прави TreeMap ефективен за съхраняване на големи обеми данни при необходимост от търсене в сортиран ред.
Поддръжка на диапазони:
TreeMap позволява лесно да се получават елементи в диапазон от ключове (например методът subMap).
Недостатъци:
По-бавен от HashMap:
Вмъкването и търсенето в TreeMap се изпълняват по-бавно, отколкото в HashMap, където тези операции имат амортизирана сложност O(1).
Големи режийни разходи за памет:
Червено-черното дърво използва допълнителни референции и полета за балансиране, което изисква повече памет, отколкото структурите на базата на хешове.
Какви са правилата за именуване в Java
Именуването е много важно за разбирането на кода, не пестете усилия да наименувате класовете, методите и променливите правилно, в съответствие с това какво означават и какво правят
Класове и интерфейси:
пишат се в CamelCase с главна буква
Методи и променливи:
пишат се в camelCase с малка буква, освен константите
Константи:
пишат се в SNAKE_CASE с главни букви
Променливата може да започва с _ $ или буква, но не може да започва с цифра, макар да може да съдържа цифри
Реализациите на интерфейси често се именуват Име на интерфейса + Impl, например InterfaceNameImpl
Имената на променливи, започващи с m, и на интерфейси, започващи с I, са остарели
Правилата за именуване са описани в Java Code Conventions.
За преобразуване на един case в друг в IDEA има удобен плъгин „String Manipulation“
Какво представляват статичните класове в Java
Статичен клас може да бъде само вътрешен клас, той може да се наследява, както и сам може да наследява всеки друг клас и да имплементира интерфейс.
Статичният вложен клас с нищо не се различава от обикновения вътрешен клас, освен че неговият обект не съдържа референция към създалия го обект на външния клас. За използване на статични методи/променливи/класове не ни е нужно да създаваме обект на дадения клас. В обикновените вътрешни класове без инстанция на външния клас не можем да създадем инстанция на вътрешния.
public class Vehicle { public static class Car { public int km; }}Vehicle.Car car = new Vehicle.Car();car.km = 90;Каква е разликата между StringBuffer и StringBuilder
StringBuffer и StringBuilder служат за операции с текстови данни, StringBuffer е синхронизиран и нишково безопасен
Какво е рефлексия в Java
Рефлексията (reflection) е механизъм, който позволява на програмите да изследват и променят своята структура и поведение по време на изпълнение.
С помощта на рефлексията може да се получи достъп до информация за класове, интерфейси, полета, методи и конструктори по време на изпълнение на програмата. Това може да е полезно в случаите, когато не знаете структурата на класа предварително, но искате да я обработите динамично.
Например с помощта на рефлексията могат да се създават нови обекти, да се извикват методи, да се задават стойности на полета и дори да се зареждат нови класове по време на изпълнение на програмата. Обаче трябва да сте внимателни при използването на рефлексията, тъй като това може да доведе до намаляване на производителността и влошаване на четимостта на кода.
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(){}}При какви ситуации възниква Memory leak
поради статични полета, чрез незатворени ресурси, неправилни реализации на equals() и hashCode(), вътрешни класове, които държат референция към външни класове, неправилно използване на lazy в Kotlin, изтичане на контекст и т.н.
Пример за код, в който може да възникне изтичане на памет:
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 { // Тук ламбдата захваща референция към data // и ще продължи да я използва дори след като // data е била унищожена. Toast.makeText(this, data, Toast.LENGTH_SHORT).show() } // Да предположим, че искаме да освободим data от паметта, // за да не възникне изтичане на памет. // Правим това, като зададем data на null. data = null // Това няма да премахне референцията към data от ламбдата. }}Какво е Generic (генерици)
С тяхна помощ могат да се обявяват класове, интерфейси и методи, в които типът данни е зададен във вид на параметър, например List<Integer>, в този случай не е нужно да се прави преобразуване на типове (Integer). Ако се използва като генерик Object в Java или Any в Kotlin, може да се използва всеки тип данни, но след това е необходимо да се прави каст до нужния с помощта на скоби в Java или „as“ в Kotlin.
В стария код на Java генериците не се използват.
Генериците се виждат само на етапа на компилация, по-нататък се извършва каст до типа, указан в генерика.
<? extends Object>:
Също така генериците могат да бъдат маски, например Collection или List
<T>:
Също така генерици могат да бъдат и букви. В дадения пример ограничихме T до наследниците на класа NewClass и интерфейса Comparable, съответно T ще има същите свойства.
class NewClass<T extends NewClass & Comparable> { T variable; NewClass method(T variable) { // метод на Comparable variable.compareTo(1); // метод на NewClass 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 има също:
Source<out T>:
указва, че типът T е ковариантен, тоест типът Source<Derived> е подтип на типа Source<Base>. Проекцията на типа позволява да се използва типът T само като тип на връщаната стойност на метод или като тип на стойността на свойство, но не и като тип на параметър на метод или тип на стойността на свойство.
interface Producer<out T> { fun produce(): T}Source<in T>:
указва, че типът T е контравариантен, тоест типът Source<Base> е подтип на типа Source<Derived>. Проекцията на типа позволява да се използва типът T само като параметър на метод или като тип на стойността на свойство, но не и като тип на връщаната стойност на метод или тип на стойността на свойство.
interface Consumer<in T> { fun consume(item: T)}<reified T>:
Обикновено по време на изпълнение на програмата информацията за типовете на аргументите се изтрива и не можете да получите достъп до типовете, използвани като аргументи на генерична функция. Обаче като използвате ключовата дума reified, можете да получите достъп до типа на аргумента по време на изпълнение.
inline fun <reified T> getTypeName() = T::class.simpleName
fun main() { println(getTypeName<String>()) // prints "String" println(getTypeName<Int>()) // prints "Int"}В този пример определихме генерична функция getTypeName(), която използва ключовата дума reified, за да получи името на класа на типа T по време на изпълнение. Извикваме тази функция с различни типове и тя връща името на класа за всеки тип.
Ключовата дума reified може да се използва само с генерични функции, не с класове или интерфейси. Освен това генеричният тип, обявен с ключовата дума reified, може да се използва само в контекст, където типът е указан явно, например при извикване на друга функция.
<*>:
нарича се Звездна проекция или Star projection
използва се за указване на неопределен тип на аргумента при използване на генерични типове.
Когато използвате генеричен тип в код на Kotlin, можете да укажете типа на аргумента в ъглови скоби. Но понякога може да поискате да използвате генеричен тип, без да указвате конкретен тип на аргумента, например ако искате да използвате тип, който може да съдържа обекти от различни типове.
В този случай можете да използвате символа <*> вместо типа на аргумента. Например:
val list: List<*> = listOf("foo", 42, Any())В този пример създадохме списък list от тип List<>, който може да съдържа обекти от всякакъв тип. Добавихме в списъка три обекта от различни типове: низ „foo“, число 42 и обект от тип Any(). Сега list съдържа и трите обекта. Символът <> може да се използва също вместо типа на аргумента при създаване на обект на генеричен клас, например:
val map: Map<String, *> = mapOf("foo" to 42, "bar" to "baz")Function<*, String>: означава Function<in Nothing, String>;Function<Int, *>: означава Function<Int, out Any?>;Function<*, *>: означава Function<in Nothing, out Any?>Какви методи има класът Object в Java и Any в Kotlin
wait(), notify(), notifyAll():
три метода от набора за многонишковост
getClass():
получаване на класа на обекта по време на изпълнение. Използва се основно за рефлексия.
clone():
получаване на точно копие на обекта. Не се препоръчва да се използва. По-често съветват да се използва конструктор за копиране.
equals():
сравнява два обекта.
hashcode():
числово представяне на обекта. Стойността по подразбиране е целочисленият адрес в паметта.
toString():
връща представяне на обекта във вид на низ. По подразбиране връща име_на_класа@hashcode в 16-ична система.
Ако hashcode не е припокрит, ще се върне стойността по подразбиране.
finalize():
Ако обектът взаимодейства с някакви ресурси, например отваря изходен поток и чете от него, такъв поток трябва да се затвори преди изтриването на обекта от паметта. За целта в езика Java е достатъчно да се припокрие методът finalize(), който се извиква в изпълнителната среда на Java непосредствено преди изтриването на обект от този клас. В тялото на метода finalize() трябва да се укажат действията, които трябва да бъдат изпълнени преди унищожаването на обекта. Методът finalize() се извиква само непосредствено преди събирането на „боклука“
Класът Any в Kotlin има методите:
equals(), hashCode(), toString()
Какво е Enum в Java
Специален клас, който представлява изброяване
В най-простата реализация изглежда така
enum Size { SMALL, MEDIUM, LARGE }функционалността му може да се разшири
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; }Какъв е приоритетът в Java при преобразуване на примитивни типове
byte ➜short ➜int ➜long ➜float ➜double ➜Byte() ➜Object() ➜Byte()… ➜byte… ➜short… ➜int… ➜long… ➜float… ➜double… ➜Object()…Какво е преобразуване на типове (type casting) / каст
Преобразуването може да бъде явно и неявно
Възможно е автоматично разширяващо преобразуване (widening):
byte -> short -> int -> long
int -> double
short -> float -> double
char -> int
Възможно е преобразуване със загуба на информация:
int -> float
long -> float
long -> double
Явно преобразуване със загуба на данни:
int a = 258;byte b = (byte) a; // 2
double a = 56.9898;int b = (int) a; // 56Преобразувания при операции:
ако един от операндите на операцията е от тип double, то и вторият операнд се преобразува към тип double
ако предишното условие не е изпълнено, а един от операндите на операцията е от тип float, то и вторият операнд се преобразува към тип float
ако предишните условия не са изпълнени, а един от операндите на операцията е от тип long, то и вторият операнд се преобразува към тип long
иначе всички операнди на операцията се преобразуват към тип int
В Kotlin има:
as:
обикновено небезопасно преобразуване на типове
as?:
безопасно преобразуване на типове, връща null при неуспех
is:
е от този тип
!is:
не е от този тип
За какво служат и как се създават анотациите в Java
Анотациите в Java са етикети в кода, които описват метаданни за функция, клас или пакет.
@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(); }}Според начина на съхранение анотациите са:
SOURCE: използва се само при писането на кода и се игнорира от компилатора
CLASS: запазва се след компилацията, но се игнорира от JVM
RUNTIME: запазва се след компилацията и се зарежда от JVM
Типът обект, над който се поставя:
ANNOTATION_TYPE: друга анотация
CONSTRUCTOR: конструктор на клас
FIELD: поле на клас
LOCAL_VARIABLE: локална променлива
METHOD: метод на клас
PACKAGE: описание на пакет package
PARAMETER: параметър на метод public void hello(@Annotation String param){}
TYPE: поставя се над клас
В Java SE 1.8 стандартната библиотека на езика ни предоставя редица анотации, основните от които са изброени по-долу
@Override
Retention: SOURCE; Target: METHOD.
Показва, че методът, над който е записана, е наследен от родителския клас.
@Deprecated
Retention: RUNTIME; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE.
Сочи методи, класове или променливи, които са „остарели“ и могат да бъдат премахнати в следващите версии на продукта.
@SuppressWarnings
Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
Изключва извеждането на предупрежденията на компилатора, които се отнасят до елемента, над който е записана. Това е SOURCE анотация, която се поставя над полета, методи и класове.
@Retention
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Задава „типа на съхранение“ на анотацията, над която е записана. Да, тази анотация се използва дори за самата себе си.
@Target
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Задава типа на обекта, над който може да се поставя създаваната от нас анотация. Да, и тя се използва за самата себе си.
В Java анотациите се създават с @interface, а в Kotlin с annotation class
Какво са Wrappers (обвивки) в Java
Обвивките (wrappers) в Java са класове, които обвиват примитивните типове данни, като int, char, boolean и т.н., за да могат да се използват като обекти. Тези класове се намират в пакета java.lang и имат следните имена:
Integer() за int
Character() за char
Boolean() за boolean
Short() за short
Long() за long
Double() за double
Float() за float
Byte() за byte
Всеки от тези класове съдържа методи и полета, които позволяват да се работи с примитивните типове данни като с обекти. Например класът Integer съдържа методи за преобразуване на числа в низове, за сравняване на числа и т.н.
Обвивките се използват и за подаване на примитивни типове данни като параметри на методи, които очакват обекти. Например ако един метод очаква параметър от тип Object, а вие искате да му подадете цяло число, можете да създадете обект от класа Integer, да обвиете числото в него и да подадете този обект на метода.
Друго предимство на обвивките е възможността да се използват null-стойности. Примитивните типове не могат да бъдат null, но обвивките могат. Ако не сте сигурни дали ще имате стойност за променливата, или не, можете да използвате обвивка, за да избегнете грешки.
В Kotlin обвивките не се използват, вместо това компилаторът сам решава кога да използва примитивен тип и кога референтен
Какви са операторите за преход
return:
по подразбиране излиза от най-близката обхващаща го функция или анонимна функция
break:
прекратява изпълнението на цикъла
continue:
продължава изпълнението на цикъла със следващата му стъпка, без да обработва останалия код на текущата итерация
Всеки израз в Kotlin може да бъде маркиран с етикет 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 // върни 1 в етикета @a, а не върни израза с етикет (@a 1)Какво са Stack (стек) и Heap в Java
За оптимална работа на приложението JVM разделя паметта на област на стека (stack) и област на heap. Стекът работи по схемата LIFO (последният влязъл излиза пръв). Всеки път, когато се извиква нов метод, който съдържа примитивни стойности или референции към обекти, на върха на стека за тях се заделя блок памет. Heap се използва за обекти и класове. Новите обекти винаги се създават в heap, а референциите към тях се пазят в стека. Примитивното поле на инстанция на клас се съхранява в heap. Heap се използва от всички части на приложението, докато стекът се използва само от една нишка за изпълнение на програмата.
Паметта на стека съдържа само локални променливи от примитивни типове и референции към обекти в heap. Обектите в heap са достъпни от всяка точка на програмата, докато паметта на стека не може да бъде достъпна за други нишки. Ако паметта на стека е напълно заета, Java Runtime хвърля java.lang.StackOverflowError, а ако паметта на heap е пълна, се хвърля изключението java.lang.OutOfMemoryError: Java Heap Space.
Всяка нишка, която работи във виртуалната машина на Java, има свой собствен стек. Стекът съдържа информация за това кои методи е извикала нишката. Щом нишката изпълни своя код, стекът на извикванията се променя. Стекът на нишката съдържа всички локални променливи на всеки изпълняван метод. Нишката има достъп само до своя стек.
Когато се извика метод, JVM създава в стека нов блок, наречен „frame“ или „stack frame“. В този блок се съхраняват локалните променливи на метода, както и референциите към обектите, подадени на метода като аргументи.
Всяко ново извикване на метод добавя нов frame в стека, а завършването на метода премахва последния frame от стека. Този механизъм се нарича „stack machine“.
Освен това стекът се използва за съхраняване на информация за връщането от методите. Когато се извика метод, адресът за връщане (return address) се поставя в стека, за да знае JVM къде да се върне след завършването на метода.
Стекът е ограничен по размер и може да предизвика грешка при препълване на стека (stack overflow), ако в него се създават твърде много frame-ове. Това може да се случи например ако метод рекурсивно извиква сам себе си твърде много пъти. В стандартната конфигурация на JVM размерът на стека може да бъде от няколко мегабайта до няколко десетки мегабайта в зависимост от версията на JVM и от операционната система. Размерът на стека обаче може да се промени при стартирането на JVM с параметъра -Xss. Например за да зададете размер на стека 1 мегабайт, трябва да стартирате JVM с параметъра -Xss1m.
В стандартната конфигурация на JVM размерът на heap може да бъде от няколкостотин мегабайта до няколко гигабайта в зависимост от версията на JVM и от операционната система. Размерът на heap обаче може да се промени при стартирането на JVM с параметрите -Xms и -Xmx. Параметърът -Xms задава началния размер на heap, а параметърът -Xmx задава максималния размер на heap. Например за да зададете начален размер на heap 256 мегабайта и максимален размер 1 гигабайт, трябва да стартирате JVM с параметрите -Xms256m -Xmx1g.
Локалните променливи са невидими за всички други нишки освен за нишката, която ги е създала. Дори две нишки да изпълняват един и същ код, те пак ще създават локалните променливи на този код в собствените си стекове. Така всяка нишка има своя версия на всяка локална променлива.
Всички локални променливи от примитивни типове (boolean, byte, short, char, int, long, float, double) се съхраняват изцяло в стека на нишката и не се виждат от другите нишки. Една нишка може да предаде копие на примитивна променлива на друга нишка, но не може да споделя примитивна локална променлива.
Heap съдържа всички обекти, създадени във вашето приложение, независимо от това коя нишка ги е създала. Това включва и версиите на примитивните типове като обекти (например Byte, Integer, Long и т.н.). Няма значение дали обектът е създаден и присвоен на локална променлива, или е създаден като променлива-член на друг обект – той се съхранява в heap.
Локалната променлива може да бъде от примитивен тип – в този случай тя се съхранява изцяло в стека на нишката.
Локалната променлива може да бъде и референция към обект. В този случай референцията (локалната променлива) се съхранява в стека на нишката, но самият обект се съхранява в heap.
Един обект може да съдържа методи, а тези методи могат да съдържат локални променливи. Тези локални променливи също се съхраняват в стека на нишката, дори обектът, на който принадлежи методът, да се съхранява в heap.
Променливите-членове на обекта се съхраняват в heap заедно със самия обект. Това важи както когато променливата-член е от примитивен тип, така и когато е референция към обект.
Статичните променливи на класа също се съхраняват в heap заедно с дефиницията на класа.
До обектите в heap имат достъп всички нишки, които притежават референция към обекта. Когато нишката има достъп до обекта, тя може да достъпва и променливите-членове на този обект. Ако две нишки извикат метод на един и същ обект едновременно, и двете ще имат достъп до променливите-членове на обекта, но всяка нишка ще има собствено копие на локалните променливи.
public static void main(String[] args) { int x = 10; // променлива от тип int се съхранява в стека String str = "Hello World!"; // обект от тип String се съхранява в heap System.out.println(str);}В този пример променливата x от тип int се съхранява в стека, тъй като е примитивен тип данни. Променливата str от тип String е обект и се съхранява в heap.
При изпълнението на програмата методът main се поставя в стека, създава се променливата x и ѝ се присвоява стойността 10. След това се създава обектът String, който съдържа низа „Hello World!“. Референцията към този обект се записва в променливата str. При извикването на метода println стойността на референцията към обекта str се подава на метода и той извежда низа „Hello World!“ на конзолата.
След изпълнението на метода main всички променливи се премахват от стека, а обектът String продължава да съществува в heap, докато има референции към него от други части на програмата.
Освен стека и heap, в Java има област за съхранение на постоянни данни (PermGen или Metaspace в зависимост от версията на Java). В тази област се поставят метаданните на класовете, информацията за методите, променливите и другите данни, свързани със самата структура на програмата. В Java 8 и по-нови версии PermGen е заменена с Metaspace.
Кодът на програмата се компилира в байткод, който се записва във файл .class. При стартирането на програмата байткодът се зарежда в паметта и се интерпретира от виртуалната машина на Java (JVM).
Какво са JDK, JRE и JVM
JDK (Java Development Kit):
това е набор от инструменти, които разработчиците използват за създаване на Java приложения. JDK включва компилатор за Java, библиотеки с класове на Java, помощни програми за разработка и дебъгване на Java приложения и други инструменти. JDK е пълната инсталация за разработка на Java приложения.
JRE (Java Runtime Environment):
това е средата за изпълнение на Java, която се използва за стартиране на Java приложения. JRE включва виртуалната машина на Java (JVM), библиотеките с класове на Java и други компоненти, необходими за изпълнението на Java приложения. JRE не съдържа инструменти за разработка на Java приложения.
JVM (Java Virtual Machine):
това е виртуална машина, която осигурява изпълнението на Java код на компютъра. JVM превежда байткода на Java в машинен код, който може да се изпълнява на конкретна платформа. JVM е ключов компонент на платформата Java, тъй като дава възможност да се напише Java код веднъж и да се стартира на всяка платформа, на която е инсталирана JVM.
Всеки компонент е важна част от платформата Java и те взаимодействат помежду си. Разработчиците използват JDK, за да създават Java приложения, а след това JRE се използва за стартирането на тези приложения, като JVM изпълнява Java кода.
С какви стойности се инициализират променливите по подразбиране
Стойността по подразбиране зависи от типа на променливата:
За числовите типове (byte, short, int, long, float, double) стойността по подразбиране е 0.
За типа char стойността по подразбиране е ‘\u0000’ (символът ‘NUL’).
За типа boolean стойността по подразбиране е false.
За референтните типове (всеки клас, интерфейс, масив) стойността по подразбиране е null.
Възможно ли е да се стесни нивото на достъп или типът на връщаната стойност при припокриване на метод
Нивото на достъп не може да се стеснява, защото това би довело до нарушаване на принципа за заместване на Барбара Лисков. Разширяването на нивото на достъп е възможно. Типът на връщаната стойност може да се стеснява: припокриващият метод може да връща подтип (ковариантен тип на връщаната стойност).
Какви типове референции съществуват в Java
Strong Reference:
това е референция към обект, която гарантира, че обектът няма да бъде премахнат от паметта, докато към него има поне една такава референция. Когато обектът е под управлението на strong reference, жизненият му цикъл се определя от жизнения цикъл на референцията. Ако референцията вече не е нужна, но все още се държи, обектът също не се премахва от паметта, което може да доведе до изтичане на памет.
String title = “hello”;SoftReference:
обектите, създадени чрез SoftReference, ще бъдат събрани, ако JVM има нужда от памет. Тоест има гаранция, че всички soft reference обекти ще бъдат събрани, преди JVM да хвърли OutOfMemoryError. SoftReference често се използва за кешове, които заемат голямо количество памет.
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 { // ...}В този пример SoftReference се използва за съхраняване на референция към обекта HeavyObject, който може да заема голям обем памет. В метода doSomething() се проверява дали вече съществува SoftReference и дали обектът HeavyObject е запазен. Ако обектът още не е бил създаден или вече е бил премахнат от паметта, се създава нов обект и референцията към него се запазва с помощта на SoftReference. В противен случай, ако обектът HeavyObject вече съществува в паметта и към него има референция чрез SoftReference, тази референция се връща за по-нататъшно използване.
Важно е да се отбележи, че SoftReference не гарантира, че обектът винаги ще бъде достъпен в паметта. Ако системата запълни паметта и не остане достатъчно място за съхраняване на HeavyObject, обектът може да бъде премахнат, а референцията чрез SoftReference ще бъде автоматично изчистена. При следващия опит за достъп до обекта ще се създаде нов обект и референцията към него ще се запази в SoftReference.
WeakReference:
по-слаба от SoftReference, не спасява обекта от финализация, дори когато има достатъчно свободна памет. Щом към обекта не останат strong и soft референции, той може да бъде финализиран. Използва се за кешове и за създаване на вериги от свързани помежду си обекти.
private var activityNavController: WeakReference<NavController>? = null// илиvar person: Person? = Person("John")val weakReference = WeakReference(person)println("Before garbage collection: $weakReference")person = nullSystem.gc()println("After garbage collection: $weakReference")В този пример се създава инстанция на класа Person и след това се създава слаба референция към нея с помощта на WeakReference. След това референцията към оригиналния обект се премахва и се извиква събиране на боклука с метода System.gc(). В края се извежда информация за референцията към обекта Person преди и след събирането на боклука.
Обърнете внимание, че след събирането на боклука референцията към обекта Person става null, тъй като оригиналният обект е бил премахнат от паметта. Референцията към обекта чрез WeakReference също става null, което означава, че обектът е бил премахнат.
PhantomReference:
обектите, създадени чрез PhantomReference, се унищожават, когато GC определи, че обектите, към които се сочи, могат да бъдат освободени. Този тип референции се използва като алтернатива на финализацията за по-гъвкаво освобождаване на ресурси.
class MyObject { // ресурси, свързани с обекта}val phantomReferenceQueue = ReferenceQueue<MyObject>()val myObject = MyObject()val phantomReference = PhantomReference(myObject, phantomReferenceQueue)// изпълняваме някакви действия// проверяваме дали обектът е премахнат от паметтаif (phantomReferenceQueue.poll() != null) { // освобождаваме ресурсите, свързани с обекта}Тук създадохме обекта MyObject и го поставихме в PhantomReference. След това изпълняваме някакви действия и ако обектът е бил премахнат от паметта, освобождаваме свързаните с него ресурси. Обърнете внимание, че създадохме ReferenceQueue, за да следим премахването на обекта.
Каква е разликата между map и flatMap
map:
това е функция, която приема колекция и функция, която се прилага към всеки елемент на колекцията. В резултат се създава нова колекция със същия брой елементи като в изходната колекция, но с променени стойности.
val numbers = listOf(1, 2, 3, 4)val doubledNumbers = numbers.map { it * 2 }println(doubledNumbers) // [2, 4, 6, 8]flatMap:
това е функция, която приема колекция и функция, която връща колекция. След това flatMap обединява всички получени колекции в една колекция.
val numbers = listOf(1, 2, 3)val nestedNumbers = numbers.flatMap { listOf(it, it * 2) }println(nestedNumbers) // [1, 2, 2, 4, 3, 6]Разликата между map и flatMap е, че map връща колекция от елементи, получени след прилагането на функцията към всеки елемент на изходната колекция, а flatMap връща колекция от елементи, получени след прилагането към всеки елемент на изходната колекция на функция, която връща колекция. Освен това flatMap може да бъде полезна, когато трябва да се „разгънат“ вложени колекции в една голяма колекция.
Приложение в Java Stream API:
map и flatMap често се използват в контекста на Stream API, който беше въведен в Java 8. Map преобразува елементите на колекцията, без да променя нивото на вложеност, докато flatMap обединява няколко потока в един плосък поток.
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] }}Приложение в Kotlin Flow:
Когато говорим за Kotlin Flow, map и flatMap имат сходни концепции, но се прилагат към потоци от данни.
Какви са типовете грешки в Java
Всички грешки се наследяват от класа Throwable
Exception:
грешки при изпълнението на програмата
Error:
системни грешки при работата на JVM, не трябва да се прихващат освен в редки случаи, тъй като това е сериозна грешка и ще доведе до срив на програмата
Exception са:
Checked:
задължително се прихващат и се проверяват от компилатора, причина за възникването – потенциални грешки в методите
примери: Throwable, Exception, IOException, ReflectiveOperationException
Unchecked:
не е задължително да се прихващат и не се проверяват от компилатора, причина за възникването – грешки в кода
примери: RuntimeException, IndexOutOfBoundsException
Как работи Garbage Collector и как е организирана паметта в JVM
Видовете събирачи на боклука са следните (в HotSpot Oracle JVM):
Serial Garbage Collection:
последователно събиране на младото и старото поколение.
Parallel Garbage Collection:
събирач по подразбиране в Java 8, работи също като Serial GC, но с използване на многонишковост
CMS Garbage Collection:
Concurrent Mark-and-Sweep. Прави две кратки паузи с пълно спиране на всички нишки, като тези паузи общо са по-кратки от целия цикъл на фоновото събиране. Когато е възможно, събира боклука във фонов режим. Първата пауза се нарича initial mark, в този момент се анализира stack, след което във фонов режим се обхожда heap и започва фазата mark. След това трябва отново да се спре приложението и да се извърши remark – да се уверим, че докато сме работили във фонов режим, нищо не се е променило. И чак след това във фонов режим се извършва sweep – изчистване на вече ненужните участъци.
G1 Garbage Collection:
събирач по подразбиране от Java 9 нататък, идеята в основата на G1 се нарича pause goal „желана пауза“. Този параметър показва за какво време програмата може да прекъсне работата си по време на изпълнение заради събиране на боклука, например за 20 ms веднъж на 5 минути. Събирачът на боклука не гарантира, че ще работи точно така, но ще се стреми да работи със зададените желани паузи. Това го отличава коренно от всички събирачи на боклука, с които сме се сблъсквали досега. Разработчикът може да контролира процеса на събиране на боклука много по-гъвкаво. G1 дели heap на области (региони) с еднакъв размер, например по 1 мегабайт. След това динамично се избира набор от такива региони, които се наричат младо поколение, като при това се запазват понятията Eden и Survivor. Но изборът се прави динамично.
Области на паметта:
Eden:
област от динамичната памет, в която първоначално се създават обектите. Много обекти никога не напускат тази област от паметта, защото бързо се превръщат в боклук.
Когато напишем нещо от рода на new Object(), създаваме обекта именно в Eden.
Принадлежи към young generation.
Survivor:
в паметта има две области на оцелелите. Или може да се счита, че областта на оцелелите обикновено се разделя наполовина. Именно в нея попадат обектите, преживели „изгонването от Едем“ (оттук идва и името ѝ). Понякога тези две пространства се наричат From Space и To Space. Едната от тези области винаги е празна, освен ако не протича процес на събиране.
От From Space обектите или се изтриват от GC, или се преместват в To Space – последното място, преди да остареят напълно и да преминат в Tenured.
Принадлежи към young generation.
Tenured:
област, в която попадат оцелелите обекти, които се смятат за „достатъчно стари“ (така те напускат областта Survivor).
Хранилището не се изчиства при събиране в младото поколение.
Обикновено по подразбиране тук се поставят обекти, преживели 8 събирания на боклука.
Принадлежи към old generation.
Постоянно поколение памет Metaspace и PermGen:
До Java 8 съществуваше специален раздел: PermGen, тук се заделяше място за вътрешни структури, например за дефиниции на класове. PermGen не влизаше в състава на динамичната памет, обикновени обекти тук никога не попадаха.
Тук се съхраняваха метаданни, класове, интернирани низове и т.н. – това беше специална област от паметта на JVM.
Тъй като е доста трудно да се определи необходимият размер на тази област, до Java 8 често можеше да се наблюдава грешката java.lang.OutOfMemoryError. Това ставаше, защото тази област се препълваше, освен ако не сте задали достатъчно памет за нея, а дали паметта е достатъчна, или не, можеше да се определи само чрез „научния метод“ на сляпото пробване.
Затова от Java 8 нататък беше решено тази област изобщо да се премахне и цялата информация, която се съхраняваше там, или се премества в heap, например интернираните низове, или се изнася в областта metaspace, в native memory. Максималният Metaspace по подразбиране не е ограничен от нищо освен от предела на обема на нативната памет. Но по желание може да се ограничи с параметъра MaxMetaspaceSize, който по същество е аналогичен на MaxPermSize при предшественика PermGen. Metaspace не се изчиства от GC и при необходимост може да се чисти ръчно.
String Pool:
област от паметта, в която се съхраняват низове, смисълът ѝ е, че низовете могат да се повтарят, а в този случай се записва един низ с различни референции към него.
String text = "text";// ще се създаде в String PoolString text = new String("text");// ще се създаде извън String Pool в HeapString text = new String("text").intern();// ще се създаде в String Pool благодарение на метода internСъществуват няколко типа събиране на боклука:
minor, major и full.
minor събиране на боклука:
изчистват се Eden и Survivor (young generation)
major събиране на боклука:
изчиства се old generation
full събиране на боклука:
изчиства се всичко
Данни в native memory може да се записват и четат с помощта на ByteBuffer и Unsafe.
Какво е Java NIO (New Input/Output)
Java NIO (New Input/Output) е набор от API, включен в Java от версия 1.4, който предоставя по-ефективни и мащабируеми методи за работа с вход/изход в сравнение с класическото IO (Input/Output) API. NIO API е предназначено за разработване на високопроизводителни приложения, които работят с големи обеми данни или обработват множество връзки едновременно.
Основни особености на Java NIO:
Канали (Channels):
Каналите представляват абстракция над потоците от данни. Те могат да бъдат асинхронни и неблокиращи.
Примери: FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel.
Буфери (Buffers):
Буферите се използват за съхраняване на данни, които се четат от канал или се записват в канал.
Примери: ByteBuffer, CharBuffer, IntBuffer, FloatBuffer.
Селектори (Selectors):
Селекторите позволяват на една нишка да управлява няколко канала. Това е полезно при разработването на сървъри, които обработват множество връзки едновременно.
Примери: Selector.
Неблокиращ режим (Non-blocking mode):
Каналите могат да работят в неблокиращ режим, което позволява нишката да не се блокира при извършване на операции за четене или запис.
Какъв е редът на изпълнение на Java клас
инициализация на статичните променливи на суперкласа (при първо обръщение към класа)
статичен инициализатор на суперкласа (при първо обръщение към класа)
инициализация на статичните променливи (при първо обръщение към класа)
статичен инициализатор (при първо обръщение към класа)
конструктор на суперкласа (ако няма суперклас, конструкторът на класа Object,
ако конструкторът на суперкласа има параметри, те трябва да се подадат при извикването на конструктора на извиквания клас с помощта на
super(параметри) )
обикновен инициализатор
инициализация на обикновените променливи
конструктор на класа
Може ли абстрактен клас да наследява обикновен
Да, също така може да припокрива методи, като ги прави абстрактни, интерфейсът не може
class One{ void one(){}}abstract class Two extends One { @Override abstract void one();}class Three extends Two { @Override void one() {}}Какви видове цикли има в Java и 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")}// проверката на елемент в колекцията ще изведе oneКакво е ClassLoader
В Java има три стандартни зареждача, всеки от които зарежда класове от определено място:
Bootstrap:
основен зареждач, нарича се още Primordial ClassLoader. Зарежда стандартните класове на JDK от архива rt.jar
Extension ClassLoader:
зареждач на разширения. Зарежда класовете на разширенията, които по подразбиране се намират в директорията jre/lib/ext, но могат да бъдат зададени със системното свойство java.ext.dirs
System ClassLoader:
системен зареждач. Зарежда класовете на приложението, определени в променливата на средата CLASSPATH
Всеки зареждач, с изключение на основния, е наследник на абстрактния клас java.lang.ClassLoader. Например имплементация на зареждача на разширения е класът sun.misc.Launcher$ExtClassLoader, а на системния зареждач – sun.misc.Launcher$AppClassLoader. Основният зареждач е нативен и имплементацията му е включена в JVM.
Може да се имплементира и собствен ClassLoader, за целта трябва да се наследи ClassLoader (extends) и да се припокрият методите му
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);}При стартиране класът преминава верификация, за да не може в байткода с помощта на hex редактор да се променят стойности и така да се наруши логиката.
Ако класът не премине верификация, се хвърля грешката java.lang.VerifyError
Какво е SecurityManager
Класът java.lang.SecurityManager позволява на приложенията да имплементират политика за сигурност. Без SecurityManager правата не са ограничени.
SecurityManager позволява на приложението, преди да изпълни евентуално опасна или чувствителна операция, да определи каква е тази операция и дали се предприема в контекст на сигурност, който позволява операцията да бъде изпълнена. Приложението може да разреши или да забрани операцията.
SecurityManager има много методи check… за проверка.
Например: checkAccept генерира изключението SecurityException, ако на извикващата нишка не е разрешено да приема сокет връзка от указания хост и номер на порт. Обикновено разрешенията (permissions) се намират във файла java.policy или default.policy
SecurityManager security = System.getSecurityManager();if (security != null) { security.checkAccept (String host, int port);}Можете да създадете свой файл с разрешения и след това или да запишете във vm options -Djava.security.policy=src/…/my.policy, или
System.setProperty("java.security.policy", "src/…/my.policy");System.setSecurityManager(new SecurityManager());Какво е JavaCompiler
Интерфейс, който позволява да се компилира код от програмата
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();int resultCode = javaCompiler.run(null, null, null ,”path/test.java”);Как да се стартира JavaScript от Java
Преди стартиране се проверява дали е инсталиран двигател за JavaScript
ScriptEngine engine = new ScriptEngineManager(null).getEngineByName("JavaScript");// илиengine = new ScriptEngineManager(null).getEngineByName("js");// илиengine = new ScriptEngineManager(null).getEngineByName("nashorn");String code = "var q = 0; q + 1";Object o = engine.eval(code);System.out.println(o);Какво е Optional в Java и Kotlin
Optional в Java е концепция, която беше добавена в Java 8 за обработка на възможни null стойности.
Optional<String> text = Optional.empty();text = Optional.of("123");if(text.isEmpty()) { System.out.println(text.get());}В Kotlin вместо Optional се използва конструкцията „Nullable“.
В Kotlin Nullable се използва за означаване на променливи, които могат да съдържат null стойности. Операциите с Nullable променливи трябва да използват оператори за безопасна навигация (safe call operator – ?. и elvis operator – ?:), за да се избегнат изключения при опит за достъп до null стойност.
В Kotlin няма отделен тип Optional: ролята му изпълняват nullable типовете (например String?). Ако обаче искате изрично да обозначите, че стойността може да липсва (например в код, който се използва и от Java), можете да вземете java.util.Optional – както в примера по-долу.
override fun getData(): Optional<Data> { val file = getFile() return if (!file.exists() || !file.isFile) { Optional.empty() } else { Optional.of(file) }}Какви побитови операции има в Java и Kotlin
В Java:
За записване на числа със знак в Java се използва допълнителен код (two’s complement), при който старшият разряд е знаков. Ако стойността му е 0, числото е положително и двоичното му представяне не се различава от представянето на число без знак. Например 0000 0001 в десетична система е 1. Ако старшият разряд е 1, имаме работа с отрицателно число. Например 1111 1111 в десетична система представлява -1. Съответно 1111 0011 представлява -13.
byte b = 7; // 0000 0111short s = 7; // 0000 0000 0000 0111&:
логическо умножение
int a1 = 2; //010int b1 = 5; //101System.out.println(a1 & b1); // резултат 0int a2 = 4; //100int b2 = 5; //101System.out.println(a2 & b2); // резултат 4|:
логическо събиране
int a1 = 2; //010int b1 = 5; //101System.out.println(a1 | b1); // резултат 7–111int a2 = 4; //100int b2 = 5; //101System.out.println(a2 | b2); // резултат 5–101^:
логическо изключващо ИЛИ
~:
логическо отрицание
a<<b:
измества числото a наляво с b разряда. Например изразът 4<<1 измества числото 4 (което в двоично представяне е 100) с един разряд наляво, в резултат се получава числото 1000 или числото 8 в десетично представяне.
a>>b:
измества числото a надясно с b разряда. Например 16>>1 измества числото 16 (което в двоична система е 10000) с един разряд надясно, тоест в крайна сметка се получава 1000 или числото 8 в десетично представяне.
a>>>b:
за разлика от предишните видове измествания, тази операция представлява изместване без знак – измества числото a надясно с b разряда. Например изразът -8>>>2 ще бъде равен на 1073741822.
В Kotlin:
shl(bits): изместване наляво със знак (<< в Java) shr(bits): изместване надясно със знак (>> в Java)
ushr(bits): изместване надясно без знак (>>> в Java)
and(bits): побитово И
or(bits): побитово ИЛИ
xor(bits): побитово изключващо ИЛИ
inv(): побитово отрицание
Какво са ByteBuffer и Unsafe
Клас, с помощта на който може да се записват или четат данни от нативната памет (не heap, по-бърза). Тази памет не се изчиства от Garbage Collector и трябва да се изчиства ръчно.
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // размер в байтовеbyteBuffer.put(new byte[]{1, 2, 3}); // запис в паметтаbyteBuffer.flip(); // премахва всички незапълнени (всичко след 1 2 3)byteBuffer.position(0); // премества курсора в началната позицияbyte[] bytes = new byte[byteBuffer.remaining()]; // размер == 3 след flip или == 1024 без негоbyteBuffer.duplicate().get(bytes); // получаване на съдържаниетоbyteBuffer.clear(); // не забравяйте да изчистите// сега bytes == 1 2 3Unsafe позволява да се работи с паметта директно
public class Main { public static void main(String[] args) throws Exception { Field field = Unsafe.class.getDeclaredField("theUnsafe"); // получаваме полето чрез рефлексия field.setAccessible(true); // разрешаваме достъп Unsafe unsafe = (Unsafe) field.get(null); // получаваме полето и го кастваме до класа // ще опитаме да запишем int long startAddressInDDR = unsafe.allocateMemory(1024L); // заделяме памет в ddr в байтове unsafe.putInt(startAddressInDDR, 123); System.out.println(unsafe.getInt(startAddressInDDR)); unsafe.freeMemory(startAddressInDDR); // освобождаваме адреса // по-нататък нова инстанция на класа)) Example example = (Example) unsafe.allocateInstance(Example.class); System.out.println(example.name); // null, въпреки че класът има конструктор, на който трябва да се подаде име, и името е инициализирано example.name = "name"; System.out.println(example.name); // name }}class Example{ Example(String name){ this.name = name; } public String name = "1";}// Kotlinfun 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 // по синтаксис тук не може да има null println(name) // но тук е null example.name = "name" println(example.name) // name}data class Example(var name: String = "1")Какво е ламбда (lambda) в Java
Представлява набор от инструкции, които могат да се изнесат в отделна променлива и след това да се извикват многократно на различни места в програмата.
Ламбда изразът не се изпълнява сам по себе си, а образува имплементация на метод, дефиниран в интерфейса.
Интерфейсът трябва да съдържа само един метод без имплементация.
interface Interface { String method(String parameter); } public static void main(String[] args) { // изпълнява в анонимен клас метода „method“, който приема на входа параметър, отпечатва го и връща друг параметър Interface one = (String parameter) -> { System.out.print(parameter); return "2"; }; System.out.print(one.method("1 ")); }// 1 2Как в Java да се зареди изпълним код от мрежата
За зареждане на изпълним код се използва механизмът за динамично зареждане на класове. За целта може да се използва класът java.net.URLClassLoader, който позволява да се зареждат класове от различни източници, включително файлове в локалната файлова система и отдалечени файлове по 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 }); // Зареждане на клас от отдалечен източник Class<?> clazz = classLoader.loadClass("MyClass"); // Създаване на инстанция на класа Object obj = clazz.newInstance(); // Извикване на метод clazz.getMethod("someMethod").invoke(obj); }}Тук създаваме инстанция на URLClassLoader с масив от URL адреси, съдържащи пътя до зареждания клас. След това зареждаме класа с метода loadClass() и създаваме инстанция на класа с метода newInstance(). Нататък можем да извикваме методите на заредения клас, като използваме рефлексия.
Какво е AsynchronousServerSocketChannel
клас от пакета java.nio.channels в Java NIO, който предоставя асинхронен начин за работа със сървърни сокети. Този клас позволява да се създават сървърни сокети, които могат да приемат връзки от клиенти, без да блокират изпълнението на нишката.
Основни особености на AsynchronousServerSocketChannel:
Асинхронност:
Методите на този клас не блокират нишката на изпълнение, което позволява ефективно да се управлява голям брой връзки.
Функционални колбеци:
Използват се за асинхронна обработка на завършването на операциите.
Поддръжка на канали:
Интеграция с други канали и буфери от Java NIO.
Пример за използване на AsynchronousServerSocketChannel
Ще разгледаме пример за създаване на прост асинхронен сървър, който приема връзки и чете данни от клиенти.
import java.net.InetSocketAddressimport java.nio.ByteBufferimport java.nio.channels.AsynchronousServerSocketChannelimport java.nio.channels.AsynchronousSocketChannelimport java.nio.channels.CompletionHandlerimport java.nio.charset.StandardCharsetsimport java.util.concurrent.Futureimport 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?) { // Приемаме следващата връзка serverChannel.accept(null, this) // Обработка на текущата връзка handleClient(clientChannel) } override fun failed(exc: Throwable, attachment: Void?) { println("Failed to accept a connection") exc.printStackTrace() } }) // Блокираме основната нишка, за да продължи сървърът да работи 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() // Изпращане на отговор на клиента 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() } } })}Обработка на клиента:
В метода handleClient се извършва четене на данни от клиента и запис на отговор.
clientChannel.read(buffer, buffer, object : CompletionHandler { … }) започва асинхронно четене на данни.
completed се извиква, когато данните са прочетени успешно.
failed се извиква, ако четенето не е успяло.
Изпращане на отговор на клиента:
След като прочете данните, сървърът изпраща отговор на клиента с помощта на clientChannel.write(response, response, object : CompletionHandler { … }).
Какво са функционалните интерфейси и анотацията @FunctionalInterface в Java
Анотацията @FunctionalInterface в Java се прилага към интерфейси и служи да покаже, че интерфейсът е функционален (може да има само един абстрактен метод).
Това означава, че интерфейсът е предназначен за имплементиране с помощта на ламбда изрази или на референции към методи.
За какво е нужна анотацията?
Явно указване:
Документира, че интерфейсът е функционален.
Проверка от компилатора:
Ако случайно се добави втори абстрактен метод, компилаторът ще изведе грешка.
Правила на функционалния интерфейс:
Трябва да съдържа точно един абстрактен метод.
Може да съдържа произволен брой методи по подразбиране (default) или статични методи (static).
Може да припокрива методи от Object (например equals()).
Пример за функционален интерфейс:
@FunctionalInterfaceinterface MyInterface {
// единственият абстрактен метод void doSomething();
// методите по подразбиране са разрешени default void defaultMethod() { System.out.println("Default method"); }
// статичните методи също са разрешени static void staticMethod() { System.out.println("Static method"); }
}
public class Main {
public static void main(String[] args) { // имплементация на функционалния интерфейс с помощта на ламбда израз MyInterface action = () -> System.out.println("Doing something..."); action.doSomething(); // Doing something...
action.defaultMethod(); // Default method MyInterface.staticMethod(); // Static method }
}Същият код на Kotlin
fun interface MyInterface {
// Единственият абстрактен метод fun doSomething()
// Методите по подразбиране са разрешени fun defaultMethod() { println("Default method") }
// Статичните методи в Kotlin обикновено се изнасят в companion object companion object { fun staticMethod() { println("Static method") } }}
fun main() {
// Използване на интерфейса чрез ламбда val action = MyInterface { println("Doing something...") }
action.doSomething() // Doing something... action.defaultMethod() // Default method MyInterface.staticMethod() // Static method
}Какво ще се случи, ако се наруши правилото:
Ако случайно добавите втори абстрактен метод, ще получите грешка на етапа на компилиране:
"Multiple non-overriding abstract methods found in interface BrokenInterface"
Какви вградени функционални интерфейси има в Java
Function<T, R>:
Приема един аргумент и връща резултат
Function<String, Integer> length = String::length;int len = length.apply("Hello"); // 5Predicate<T>:
Приема аргумент и връща boolean
Predicate<Integer> isPositive = n -> n > 0;boolean result = isPositive.test(10); // trueConsumer<T>:
Приема аргумент и не връща нищо
Consumer<String> print = System.out::println;print.accept("Hello!"); // Hello!Supplier<T>:
Не приема нищо, връща стойност
Supplier<Double> random = Math::random;double value = random.get(); // случайно числоUnaryOperator<T>:
Приема един аргумент от тип T и връща резултат от същия тип
UnaryOperator<Integer> square = x -> x * x;int result = square.apply(4); // 16BinaryOperator<T>:
Приема два аргумента от тип T, връща резултат от същия тип
BinaryOperator<Integer> sum = Integer::sum;int result = sum.apply(2, 3); // 5BiFunction<T, U, R>:
Приема два аргумента от различни типове и връща резултат
BiFunction<String, String, Integer> totalLength = (a, b) -> a.length() + b.length();int length = totalLength.apply("Hello", "Java"); // 9BiConsumer<T, U>:
Приема два аргумента, не връща нищо
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>:
Приема два аргумента и връща логическа стойност
BiPredicate<String, String> equalsIgnoreCase = String::equalsIgnoreCase;boolean res = equalsIgnoreCase.test("java", "JAVA"); // trueRunnable:
Не приема нищо и не връща нищо
Runnable task = () -> System.out.println("Running!");new Thread(task).start(); // Running!Callable<V>:
Не приема нищо, връща стойност от тип V, може да хвърли изключение.
Callable<Integer> callable = () -> { Thread.sleep(1000); return 42;};
ExecutorService executor = Executors.newSingleThreadExecutor();Future<Integer> future = executor.submit(callable);int result = future.get(); // 42 (след 1 секунда)executor.shutdown();Специализирани интерфейси за примитиви:
IntPredicate, LongPredicate, DoublePredicate
IntConsumer, LongConsumer, DoubleConsumer
IntSupplier, LongSupplier, DoubleSupplier
IntFunction, LongFunction, DoubleFunction
ToIntFunction, ToLongFunction, ToDoubleFunction
IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
Пример за използване на IntPredicate:
IntPredicate isEven = n -> n % 2 == 0;boolean result = isEven.test(10); // trueКакви типови параметри има в Java
Стандартни означения по конвенция:
<T>:
съкращение от Type (тип). Използва се за всеки тип данни
Примери:
List<T>
Optional<T>
<E>:
съкращение от Element (елемент). Обикновено се използва за елементите на колекции
Примери:
Collection<E>
Set<E>
<K>:
съкращение от Key (ключ). Използва се за ключове, например в структури от типа Map
Примери:
Map<K, V>
<V>:
съкращение от Value (стойност). Използва се за стойности в структури от типа Map
Примери:
Map<K, V>
<R>:
съкращение от Result (резултат). Обикновено се използва за връщани стойности
Примери:
Function<T, R>
<U>:
допълнителен втори типов параметър, ако е необходимо да се посочат повече от един общи типа
Примери:
BiFunction<T, U, R>
<N>:
съкращение от Number (число). Използва се рядко, но може да се използва за числови типове
Примери:
Comparator<N> (ако е нужен Comparator за числа)
<S>:
допълнителен типов параметър, рядко се използва. Обикновено за допълнителни помощни типове при множествена параметризация.
Какъв е механизмът на работа на volatile променливите в Java
От гледна точка на JVM и Java Memory Model (JMM) volatile задейства механизми на по-ниско ниво, като осигурява управление на паметта и бариери за видимост.
Memory Barriers (бариери на паметта):
При достъп до volatile променлива JVM вмъква memory barriers (бариери на паметта), които управляват реда на операциите за четене и запис. Това са:
LoadLoad barrier
LoadStore barrier
StoreLoad barrier
StoreStore barrier
volatile boolean flag = false;
При четене на flag:
JVM вмъква LoadLoad + LoadStore – всичко, което се случва след четенето, не може да бъде „пренесено“ преди него.
При запис във flag:
JVM вмъква StoreStore + StoreLoad – всички промени преди записа трябва да са завършени и никакви четения след него не могат да бъдат изпълнени по-рано.
На практика това означава, че volatile предотвратява агресивните оптимизации на компилатора и на CPU, които могат да нарушат реда на изпълнение на кода в многонишкови програми.
Работа с кешовете и регистрите на CPU:
Без volatile нишките могат да кешират стойността на променливата в регистри или в L1/L2 кешове. С volatile JVM гарантира, че:
Нишката винаги чете стойността от основната памет (main memory).
При запис стойността веднага се записва в основната памет.
Правилото Happens-before:
JMM определя отношения „happens-before“ между операциите. За volatile:
Записът във volatile променлива happens-before всяко четене на същата променлива от друга нишка.
Това е една от фундаменталните гаранции на JMM и основа на безопасната видимост при многонишковост.
JVM-инструкции:
На ниво байткод:
Четене на volatile → volatileLoad
Запис във volatile → volatileStore
JIT-компилаторът може също да вмъкне инструкции lock или mfence за x86 или dmb (data memory barrier) за ARM.
volatile в JVM:
Гарантира видимост между нишките чрез memory barriers.
Управлява реда на изпълнение на операциите.
Забранява пренареждането.
Поддържа отношението happens-before.
Използва специфични инструкции за flush/load от main memory.
Как работят в JVM стековете на нишките от гледна точка на взаимодействието с процесора
Какво е стек на нишка в JVM:
За всяка нишка JVM създава отделен стек (Thread Stack), който се използва за съхраняване на:
фреймове на извикванията на методи (Stack Frames)
локални променливи
операнди (за операциите на JVM инструкциите)
Всеки метод при извикване получава свой фрейм. След завършването на метода фреймът се премахва от стека.
Как това е свързано с процесора:
Процесорът сам по себе си оперира с регистри, кешове и оперативна памет. JVM работи в потребителското пространство и стекът на нишката в JVM се намира в оперативната памет (RAM).
На ниво взаимодействие:
Създаване на нишка:
JVM извиква през ОС създаване на нативна нишка (например чрез pthread_create в Linux).
ОС заделя блок памет за стека (обикновено 1–2 МБ по подразбиране).
JVM инициализира структурата на JVM стека във вътрешността на този блок памет.
Извикване на метод = работа със стека:
Когато нишката изпълнява метод, JVM добавя нов Stack Frame.
Всички изчисления (например a + b) се извършват в стека – операндите се поставят там, инструкциите на JVM работят с тях (през операндния стек на фрейма).
JVM сама интерпретира инструкциите или ги подава на JIT-компилиран код, който работи директно с регистрите и със стека на CPU.
JIT компилация и взаимодействие с CPU:
Когато HotSpot JIT компилира метод, той генерира машинен код, който:
използва регистрите на CPU (например за съхраняване на локални променливи, аргументи, временни стойности);
управлява собствения си стек на извикванията (в стека на процеса);
достъпва данните на JVM стека, ако е нужно, чрез указатели и отмествания.
Как JVM и стекът на нишката използват кеша:
сега ще добавим това към стека на нишката:
Работа с локални променливи и операнди:
Когато JVM (или JIT кодът) достъпва локални променливи или операнди в стека, той всъщност достъпва паметта.
Тази памет първо попада в кеша на CPU:
Ако данните са в кеш паметта (L1/L2), достъпът е много бърз (няколко наносекунди).
Ако не са – CPU прави кеш пропуск (cache miss) и дърпа данните от RAM.
Когато методът работи активно, данните, свързани със стека (например int a = 42), почти винаги попадат в кеша – това се нарича кеш локалност (cache locality).
Какво е кешът на CPU:
Кешът на процесора е многостепенна бърза памет, вградена в процесора. Задачата ѝ е да намали броя на обръщенията към оперативната памет (RAM), защото RAM работи бавно в сравнение с CPU.
Основни нива:
L1 (много бърз, малък – на ядро)
L2 (малко по-бавен, но по-голям – също на ядро)
L3 (още по-голям, по-бавен – общ за всички ядра)
L4 (рядко се използва, като правило в сървърни CPU)
JIT и оптимизация за кеша:
JIT компилаторът на HotSpot (C2) генерира машинен код, който:
Използва максимално регистрите на CPU за „горещите“ променливи (за да не се обръща изобщо към паметта).
Подрежда layout на променливите така, че често използваните данни да се намират наблизо в паметта → това повишава пространствената локалност, което помага на кеша.
Кешът и превключването на нишки:
При контекстно превключване кешовете могат да бъдат частично инвалидирани (например L1 кешът на друга нишка или ядро може да не съдържа нужните данни).
Това прави честите превключвания между нишки по-малко ефективни, ако те работят с различни части от паметта.
Затова е важно данните да се държат „близо“ – това е една от причините горещите функции при JIT интерпретацията да работят много по-бързо.
False sharing и кеш линиите:
Кешът работи с кеш линии (обикновено 64 байта).
Ако две нишки пишат в различни променливи, които се намират в една и съща кеш линия, възниква false sharing – и процесорът е принуден да синхронизира кешовете между ядрата.
JVM се стреми да избягва това, например чрез анотацията @Contended (вътрешна оптимизация за конкурентни структури).
Стек на нишката (локални променливи) -> Най-често в L1 кеша
Операнди на JVM -> Често се поместват в регистри/кеша
JIT код -> Генерира се с отчитане на кеш локалността
Често извиквани методи -> Кодът и данните им остават в кешовете
Превключване на нишки -> Може да предизвика кеш пропуски
Контекстно превключване:
Когато ОС превключва контекста (смяна на нишка), тя запазва регистрите на CPU, включително указателя на стека (stack pointer).
При връщане към нишката стекът продължава да се използва, сякаш нищо не се е случило.
Пример за визуализация (опростено):
fun foo(a: Int): Int { return bar(a * 2)}
fun bar(x: Int): Int { return x + 1}foo се извиква → стекът на JVM получава фрейма foo.
bar се извиква → стекът на JVM получава фрейма bar.
Изпълнява се x + 1 → в стека на bar се поставя x, след това резултатът.
bar завършва → фреймът му се премахва.
Връщаме се във foo, който също завършва → фреймът му се премахва.