1. Властивості: гетер і сетер
Коли великий проєкт розробляють десятки програмістів одночасно, нерідко виникають проблеми з тим, що програмісти по-різному поводяться з даними, які зберігаються в полях класу.
Ніхто докладно не вивчає документацію щодо класів, або в ній може бути описано не всі випадки, тому часто трапляються ситуації, коли дані всередині об'єкта «псуються» і об'єкт стає неприпустимим.
Щоб уникнути таких ситуацій, у Java прийнято всі поля класу робити приватними (private). Тільки методи класу можуть змінювати змінні класу, і ніякі методи з інших класів не мають безпосереднього доступу до змінних класу. Отак.
Якщо ви хочете, щоб інші класи могли отримувати або змінювати дані всередині об'єктів вашого класу, потрібно додати в код вашого класу два методи — get-метод і set-метод. Приклад:
Код | Примітка |
---|---|
|
private -поле nameІніціалізація поля за допомогою конструктора getName() — метод повертає значення поля namesetName() — метод змінює значення поля name |
Жоден інший клас не зможе безпосередньо змінити значення поля name. Якщо комусь потрібно отримати значення поля name, доведеться викликати метод getName()
для об'єкта типу Person
. Якщо хочеться поміняти в коді значення поля name, потрібно буде викликати метод setName()
для об'єкта типу Person
.
Метод getName()
інакше називають «гетер поля name», а метод setName()
— «сетер поля name».
Це дуже поширений підхід. У 80–90 % всього Java-коду ви ніколи не побачите публічних змінних класу. Натомість вони будуть оголошені private
(або protected
), і кожна змінна матиме публічні гетери й сетери.
З таким підходом код стає довшим, але надійнішим.
Безпосереднє звернення до змінної класу — це як поворот через подвійну суцільну лінію: простіше та швидше, але якщо так робитимуть усі, то всім буде від цього тільки гірше.
Припустімо, ви хочете створити клас, який описує точку з координатами x
і y
на площині. Отак це зробив би програміст-початківець:
class Point
{
public int x;
public int y;
}
А отак це зробив би досвідчений Java-програміст:
Код |
---|
|
Код став довшим? Безумовно.
Натомість у сетери й гетери можна додати валідацію параметрів. Наприклад, можна слідкувати за тим, щоб x
та y
завжди були більшими за нуль (або не меншими за нуль). Приклад:
Код | Примітка |
---|---|
|
2. Тривалість життя об'єкта
Ви вже знаєте, що об'єкти створюють за допомогою оператора new
, а от як їх видаляють? Не існують же вони вічно — для цього ніякої пам'яті не вистачить.
У багатьох мовах програмування, як-от у С++, для видалення об'єкта є спеціальний оператор delete
. А як вирішується це питання в Java?
У мові Java все влаштовано трохи по-іншому, і оператора delete у Java немає. Чи означає це, що об'єкти в Java не видаляються? Ні, звичайно ж, видаляються. Інакше в Java-програмах швидко закінчилася би пам'ять, і про місяці безперервної роботи навіть і не йшлося б.
У мові Java процес видалення об'єктів повністю автоматизовано — видаленням об'єктів займається сама Java-машина. Такий процес називають збиранням сміття (garbage collecting), а механізм, який збирає сміття, — збирачем сміття — Garbage Collector або скорочено GC.
Тож як Java-машина дізнається, що якийсь об'єкт потрібно видалити й коли?
Усі об'єкти збирач сміття розділяє на досяжні й недосяжні. Якщо на об'єкт є щонайменше одне посилання, він вважається досяжним. Якщо немає жодної змінної, що посилається на об'єкт, такий об'єкт вважається недосяжним і оголошується сміттям: значить, його можна видаляти.
У Java не можна взяти й створити посилання на наявний об'єкт: його можна тільки присвоїти. Якщо ми видалили всі посилання на об'єкт, його втрачено назавжди.
Циклічні посилання
Описана вище логіка звучить чудово, доки ми не придумаємо простий контрприклад: у нас є два об'єкти, що посилаються один на одного (зберігають посилання один на одного). Більше ніхто жодного посилання на ці об'єкти не зберігає.
До цих об'єктів не можна звернутися з іншого коду, проте посилання на них усе ж таки існують.
Саме тому збирач сміття розділяє об'єкти не на «об'єкти з посиланнями» і «об'єкти без посилань», а на досяжні й недосяжні.
Досяжні об'єкти
Спочатку до списку досяжних додаються об'єкти, які на 100 % «живі». Наприклад, поточна нитка (Thread.current()
) або Консоль (System.in
).
Відтак список досяжних об'єктів поповнюють ті, на які посилаються перші досяжні об'єкти. Потім ті, на які посилаються другі і т. д.
Отже, якщо є якась група об'єктів, що посилаються тільки один на одного, але досяжні об'єкти до них жодним чином дістатися не можуть, такі об'єкти будуть вважатися сміттям і будуть видалені.
3. Збирання сміття
Фрагментація пам'яті
Ще один важливий момент, пов'язаний з видаленням об'єктів, — фрагментація пам'яті. Якщо постійно створювати й видаляти об'єкти, скоро в пам'яті настане розгардіяш: заповнені ділянки пам'яті будуть увесь час перемежатися з вільними.
І запросто може трапитися ситуація, коли ми не зможемо створити великий об'єкт (наприклад, масив на мільйон елементів) через відсутність великого шматка вільної пам'яті. Тобто вільна пам'ять нібито і є, і її багато, а от великого цільного шматка вільної пам'яті може й не бути.
Оптимізація (дефрагментація) пам'яті
Java-машина вирішує цю проблему специфічним чином. Приблизно отак:
Пам'ять розділяється на дві частини. Усі об'єкти створюються (і видаляються) лише в одній її половині. Коли настає час усунути «дірки» в пам'яті, всі об'єкти з першої половини копіюються у другу половину. Однак копіюються вони вже впритул один до одного, щоб «дірок» не було.
Цей процес має приблизно такий вигляд:
Етап 1: Після створення об'єктів
Етап 2: Виникнення «дірок»
Етап 3: Усунення «дірок»
Таким чином, навіть не потрібно видаляти об'єкти. Java-машина просто копіює всі досяжні об'єкти в нове місце, а всю ділянку пам'яті зі старими об'єктами оголошує вільною.