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, якщо вони оголошені як null не безпечні, тобто що вони можуть бути 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:
використовується для оголошення singletone і для створення анонімних об'єктів, які є заміною анонімних внутрішніх класів у 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)Ізольовані класи абстрактні і можуть містити в собі абстрактні компоненти. Конструктор ізольованого класу завжди приватний, і це не можна змінити. Ізольовані класи не можна ініціалізувати. Нащадки ізольованого класу можуть бути класами будь-якого типу: класом даних, об'єктом, звичайним класом або навіть іншим ізольованим класом.
Також потрібно пам'ятати що:
Усі змінні в інтерфейсах за замовчуванням є 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
Kotlin концепція default methods в інтерфейсах реалізована трохи інакше, ніж у 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:
Основна мета — надати можливість додавання нових методів в інтерфейс без порушення зворотної сумісності. Тобто, якщо в інтерфейсі оголошується новий метод за замовчуванням, старі реалізації інтерфейсу не будуть зобов'язані реалізувати його.
Статичний метод:
Статичний метод найчастіше використовується для виконання операцій, які не залежать від екземпляра класу. Це утилітарні методи, які мають доступ лише до статичних даних і не потребують стану об'єкта.
Чим інтерфейс з 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 vs абстрактний клас:
Використовуйте інтерфейси з 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:
відсортована мапа на основі червоно чорного дерева
Також потрібно пам'ятати що:
У 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 збігаються, r елементу додається новий запис, що посилається на старий запис, для перегляду записів у цьому елементі необхідно буде пройти зв'язаний список записів. Це називається Метод ланцюжків
Як відсортувати колекцію, що містить складний клас, 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 (витягти і не видалити), pool (витягти і видалити)
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 з великої літери
Методи і змінні:
пишуться в camesCase з маленької літери, крім констант
Константи:
пишуться в SNAKE_CASE великими літерами
Змінна може починатися на _ $ або літеру, але не може починатися з цифри, але може містити цифри
Реалізації інтерфейсів часто іменуються Ім'я інтерфейсу + Impl, наприклад InterfaceNameImpl
Назви змінних починаючи з m та інтерфейсів починаючи з I застаріли
Правила неймінгу описані в Java Code Conventions.
Для перетворення кейсів один в одного в 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 (Дженерики)
За їх допомогою можна оголошувати класи, інтерфейси та методи, де тип даних вказаний у вигляді параметра, наприклад , у цьому випадку не потрібно робити приведення типів (Integer). Якщо використовувати як дженерик Object у Java або Any у Kotlin, можна використовувати будь-який тип даних, але потім необхідно робити каст до потрібного за допомогою дужок у Java або “as” у Kotlin.
У старому коді Java дженерики не використовуються.
Дженерики видно лише на етапі компіляції, далі здійснюється каст до типу, вказаного в дженерику.
<? extends Object>:
Також дженерики можуть бути масками, наприклад Collection або List
<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 є коваріантним, тобто тип List є підтипом типу List. Проєкція типу дозволяє використовувати тип T лише як тип, що повертається методом, або тип значення властивості, але не як тип параметра методу або тип значення властивості.
interface Producer<out T> { fun produce(): T}Source<in T>:
вказує, що тип T є контраваріантним, тобто тип List є підтипом типу List. Проєкція типу дозволяє використовувати тип 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()…Що таке приведення типів / перетворення типів / каст
Буває явне і неявне перетворення
Можливе автоматичне розширювальне перетворення (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(@Annontation String param){}
TYPE: вказується над класом
Java SE 1.8 стандартна бібліотека мови надає нам 10 анотацій
@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 (останнім увійшов, першим вийшов). Щоразу, коли викликається новий метод, що містить примітивні значення або посилання на об'єкти, то на вершині стека під них виділяється блок пам'яті. Купа використовується для об'єктів і класів. Нові об'єкти завжди створюються в купі, а посилання на них зберігаються в стеку. Примітивне поле екземпляра класу зберігається в купі. Купа використовується всіма частинами застосунку, тоді як стек використовується лише одним потоком виконання програми.
Пам'ять стека містить лише локальні змінні примітивних типів і посилання на об'єкти в купі. Об'єкти в купі доступні з будь-якої точки програми, тоді як стекова пам'ять не може бути доступна для інших потоків. Якщо пам'ять стека повністю зайнята, то Java Runtime кидає java.lang.StackOverflowError, а якщо пам'ять купи заповнена, то кидається виняток 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 розмір купи може становити від кількох сотень мегабайтів до кількох гігабайтів залежно від версії JVM і операційної системи. Проте розмір купи можна змінити при запуску JVM за допомогою параметрів -Xms і -Xmx. Параметр -Xms встановлює початковий розмір купи, а параметр -Xmx встановлює максимальний розмір купи. Наприклад, щоб встановити початковий розмір купи в 256 мегабайтів і максимальний розмір купи в 1 гігабайт, необхідно запустити JVM з параметрами -Xms256m -Xmx1g.
Локальні змінні невидимі для всіх інших потоків, крім потоку, який їх створив. Навіть якщо два потоки виконують один і той самий код, вони все одно будуть створювати локальні змінні цього коду у своїх власних стеках. Таким чином, кожен потік має свою версію кожної локальної змінної.
Усі локальні змінні примітивних типів (boolean, byte, short, char, int, long, float, double) повністю зберігаються в стеку потоків і не видні іншим потокам. Один потік може передати копію примітивної змінної іншому потоку, але не може спільно використовувати примітивну локальну змінну.
Купа містить усі об'єкти, створені у вашому застосунку, незалежно від того, який потік створив об'єкт. До цього належать і версії об'єктів примітивних типів (наприклад, Byte, Integer, Long тощо). Неважливо, чи був об'єкт створений і присвоєний локальній змінній, чи створений як змінна-член іншого об'єкта, він зберігається в купі.
Локальна змінна може бути примітивного типу, у цьому випадку вона повністю зберігається в стеку потоку.
Локальна змінна також може бути посиланням на об'єкт. У цьому випадку посилання (локальна змінна) зберігається в стеку потоків, але сам об'єкт зберігається в купі.
Об'єкт може містити методи, і ці методи можуть містити локальні змінні. Ці локальні змінні також зберігаються в стеку потоків, навіть якщо об'єкт, якому належить метод, зберігається в купі.
Змінні-члени об'єкта зберігаються в купі разом із самим об'єктом. Це вірно як у випадку, коли змінна-член має примітивний тип, так і в тому випадку, якщо вона є посиланням на об'єкт.
Статичні змінні класу також зберігаються в купі разом з визначенням класу.
До об'єктів у купі можуть звертатися всі потоки, що мають посилання на об'єкт. Коли потік має доступ до об'єкта, він також може отримати доступ до змінних-членів цього об'єкта. Якщо два потоки викликають метод для одного й того самого об'єкта одночасно, вони обидва матимуть доступ до змінних-членів об'єкта, але кожен потік матиме свою власну копію локальних змінних.
public static void main(String[] args) { int x = 10; // змінна типу int зберігається у стеку String str = "Hello World!"; // об'єкт типу String зберігається у купі System.out.println(str);}У цьому прикладі змінна x типу int зберігається в стеку, оскільки це примітивний тип даних. Змінна str типу String є об'єктом і зберігається в купі.
При виконанні програми метод main поміщається в стек, створюється змінна x і їй присвоюється значення 10. Потім створюється об'єкт String, що містить рядок “Hello World!”. Посилання на цей об'єкт зберігається у змінній str. При виклику методу println значення посилання на об'єкт str передається в метод і він виводить рядок “Hello World!” у консоль.
Після виконання методу main усі змінні видаляються зі стека, а об'єкт String продовжує існувати в купі, поки на нього є посилання з інших частин програми.
Крім стека і купи, у 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 мс один раз на 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 і перевизначити його методи
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, якщо потоку, що викликає, не дозволено приймати сокет-з'єднання від вказаного хоста і номера порту. Зазвичай пермішени знаходяться у файлі 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 значення.
Optional у Kotlin має також спеціальний тип даних, що називається “Optional” або “Maybe”, який можна використовувати в деяких ситуаціях, коли ви хочете явно позначити, що змінна може містити null або не-null значення. Тип Optional створюється з використанням ключового слова “Nullable” як модифікатора
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, який теж завершений → його фрейм видаляється.