08 - Інкапсуляція та модифікатори доступу
Дізнайтеся, як захистити та контролювати доступ до даних вашого класу.
Інкапсуляція полягає у приховуванні внутрішніх деталей класу та контролі доступу до його даних і їх модифікації. Це запобігає появі помилок і робить ваш код більш зручним для обслуговування.
Проблема без інкапсуляції
public class Player {
public String name;
public int health;
public int maxHealth;
}
public class Main {
public static void main(String[] args) {
Player player = new Player();
player.health = 100;
player.maxHealth = 100;
// Ой! Хтось може порушити правила
player.health = 500; // Здоров'я перевищує максимум!
player.health = -50; // Негативне здоров'я!
player.name = ""; // Порожнє ім'я!
}
}Без захисту будь-хто може встановити недопустимі значення!
Модифікатори доступу
Java має ключові слова, які контролюють, хто може отримати доступ до членів вашого класу:
| Модифікатор | Клас | Пакет | Підклас | Світ |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (none) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Наразі зосередьтеся на:
public- доступ для всіхprivate- доступ тільки для цього класу
Робимо властивості приватними
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
}Тепер ви не можете отримати прямий доступ до властивостей:
Player player = new Player("Анна", 100);
player.health = 500; // ❌ Помилка! health є приватною властивістюМетоди-отримувачі та методи-встановлювачі
Щоб отримати доступ до приватних властивостей, створіть методи-отримувачі та методи-встановлювачі:
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
// Метод-отримувач - повертає значення
public int getHealth() {
return health;
}
// Метод-встановлювач - встановлює значення з перевіркою
public void setHealth(int health) {
if (health < 0) {
this.health = 0;
} else if (health > maxHealth) {
this.health = maxHealth;
} else {
this.health = health;
}
}
public String getName() {
return name;
}
public int getMaxHealth() {
return maxHealth;
}
}Тепер ви можете безпечно взаємодіяти з об'єктом:
Player player = new Player("Анна", 100);
player.setHealth(150); // Автоматично обмежується 100
System.out.println(player.getHealth()); // 100
player.setHealth(-20); // Автоматично встановлюється на 0
System.out.println(player.getHealth()); // 0Дотримуйтесь правил іменування Java:
- Метод-отримувач:
get+ ім'я властивості (з великої літери) - Метод-встановлювач:
set+ ім'я властивості (з великої літери) - Логічний (boolean):
is+ ім'я властивості (з великої літери)
private int health;
public int getHealth() { }
public void setHealth(int health) { }
private boolean alive;
public boolean isAlive() { }
public void setAlive(boolean alive) { }
private String name;
public String getName() { }
public void setName(String name) { }Переваги інкапсуляції
1. Перевірка
public class Item {
private int durability;
private int maxDurability;
public void setDurability(int durability) {
if (durability < 0) {
this.durability = 0;
} else if (durability > maxDurability) {
this.durability = maxDurability;
} else {
this.durability = durability;
}
}
public boolean isBroken() {
return durability <= 0;
}
}2. Властивості тільки для читання
Іноді вам не потрібен метод-встановлювач:
public class Monster {
private String id; // Ніколи не повинен змінюватися
private int health;
public Monster(String id, int health) {
this.id = id;
this.health = health;
}
// Тільки метод-отримувач - без метода-встановлювача!
public String getId() {
return id;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
}3. Обчислювані властивості
Методи-отримувачі не обов'язково повинні повертати поле напряму:
public class Player {
private int health;
private int maxHealth;
public int getHealth() {
return health;
}
// Обчислювана властивість
public double getHealthPercentage() {
return (health * 100.0) / maxHealth;
}
// Обчислювана властивість
public boolean isLowHealth() {
return getHealthPercentage() < 25;
}
}Практичні приклади
Предмет із міцністю
public class Tool {
private String name;
private int durability;
private int maxDurability;
private boolean broken;
public Tool(String name, int maxDurability) {
this.name = name;
this.durability = maxDurability;
this.maxDurability = maxDurability;
this.broken = false;
}
public void use() {
if (broken) {
System.out.println(name + " зламаний!");
return;
}
durability--;
System.out.println(name + " використано. Міцність: " + durability);
if (durability <= 0) {
broken = true;
System.out.println(name + " зламався!");
}
}
public void repair() {
durability = maxDurability;
broken = false;
System.out.println(name + " відремонтований!");
}
// Методи-отримувачі
public String getName() {
return name;
}
public int getDurability() {
return durability;
}
public boolean isBroken() {
return broken;
}
public double getDurabilityPercentage() {
return (durability * 100.0) / maxDurability;
}
}Приклад банківського рахунку
public class PlayerWallet {
private int gold;
private int silver;
public PlayerWallet() {
this.gold = 0;
this.silver = 0;
}
public void addGold(int amount) {
if (amount > 0) {
gold += amount;
System.out.println("Додано " + amount + " золота");
}
}
public boolean spendGold(int amount) {
if (amount > gold) {
System.out.println("Недостатньо золота!");
return false;
}
gold -= amount;
System.out.println("Витрачено " + amount + " золота");
return true;
}
public int getGold() {
return gold;
}
public int getTotalValue() {
// 1 gold = 100 silver
return gold * 100 + silver;
}
}Система захищених блоків
public class ProtectedBlock {
private int x, y, z;
private String type;
private String owner;
private boolean locked;
public ProtectedBlock(int x, int y, int z, String type, String owner) {
this.x = x;
this.y = y;
this.z = z;
this.type = type;
this.owner = owner;
this.locked = true;
}
public boolean canBreak(String playerName) {
if (!locked) {
return true;
}
return playerName.equals(owner);
}
public void unlock(String playerName) {
if (playerName.equals(owner)) {
locked = false;
System.out.println("Блок доступний");
} else {
System.out.println("Ви не володієте цим блоком!");
}
}
// Тільки методи-отримувачі - позиція і власник не повинні змінюватися
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public String getOwner() {
return owner;
}
public boolean isLocked() {
return locked;
}
}Коли використовувати приватний, а коли публічний
За замовчуванням робіть все приватним! Робіть публічним тільки те, до чого потрібен доступ ззовні.
Приватний:
- Внутрішні дані (стан, положення, інвентар)
- Допоміжні методи, що використовуються тільки в класі
- Все, що потребує перевірки
Публічний:
- Методи, що визначають поведінку класу
- Конструктор
- Методи, які повинні викликати інші класи
public class Example {
// Приватний - внутрішні дані
private int internalCounter;
private String secretKey;
// Публічний - частина інтерфейсу
public void doSomething() {
// Використовує приватний допоміжний метод
validateData();
}
// Приватний - внутрішній допоміжний
private void validateData() {
// ...
}
}Ключове слово final
final означає, що змінну не можна змінити після її встановлення:
public class Player {
private final String id; // Не можна змінити після створення
private String name; // Можна змінювати
private int health; // Можна змінювати
public Player(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
// Немає setId() - це final!
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}Ключове слово static
Статичні компоненти
Клас може визначати два типи компонентів:
-
Компоненти екземпляру - належать кожному об'єкту (кожен екземпляр має свою власну копію).
-
Статичні компоненти - належать класу (одна спільна копія для всього типу).
Простіше кажучи: компоненти екземпляру належать об'єктам; статичні компоненти належать самому класу і є спільними для всіх об'єктів цього типу.
Оголошення
/* (модифікатор доступу) */ static ... memberName; Приклад
class Data {
public int x; // Компонент екземпляру
public static int y = 1000; // Статичний компонент
// Компонент екземпляру:
// може отримати доступ як до статичних, так і до нестатичних компонентів
public void foo() {
x = 100; // OK - те саме, що this.x = 100;
y = 100; // OK - те саме, що Data.y = 200;
}
// Статичний компонент:
// не може отримати доступ до нестатичних змінних
public static void bar() {
x = 100; // Помилка: нестатична змінна x не може бути посиланням зі статичного контексту
y = 100; // OK
}
}Доступ до статичних компонентів
Data data = new Data();
data.x = 1000; // OK
data.y = 1000; // майже OK - не дуже рекомендується; краще використовувати Data.y
Data.y = 1000; // OK - найкраща практика
Data.x = 1000; // Помилка: неможливо отримати доступ до змінних екземпляру в статичному контекстіСтатичні поля
Статичне поле представляє компонент даних, що належить типу класу, а не об'єкту. Статичні поля також зберігаються в певному місці пам'яті, яке є спільним для всіх створених екземплярів об'єкта.
Воно оголошується наступним чином:
/* (модифікатор доступу) (опціонально) */ static /* final/volatile (опціонально) */ fieldName;Візьмемо той самий приклад класу Data і додамо цей конструктор:
public Data() {
y++; // пам'ятайте, що це те саме, що Data.y++;
}// Кожен екземпляр Data матиме приватну копію компоненту екземпляра x
// Однак він буде вказувати на те саме місце в пам'яті для компоненту y
Data d1 = new Data(); // y = 1001
d1.x = 5;
Data d2 = new Data(); // y = 1002
d2.x = 25;
Data d3 = new Data(); // y = 1003
// ... і так даліСтатичні методи
Статичні методи по суті представляють собою функціональний компонент певного типу класу
З класу Data згадайте функцію (метод екземпляра) foo і (статичний метод) bar
Доступ до цих методів можна отримати за допомогою:
Data d1 = new Data();
d1.foo(); // Метод екземпляра: доступний ТІЛЬКИ об'єкту
Data.bar(); // Статичний метод: доступний без об'єктаСтатичний ініціалізатор
Використовуйте статичний ініціалізатор для виконання логіки ініціалізації під час першого завантаження класу:
class OtherData {
private static int a = 12;
private static int b;
private static String msg;
static {
msg = "Ініціалізація..."
System.out.println(msg);
b = 4;
// ... складна ініціалізація, яку неможливо виконати одним виразом
}
}Практичні вправи
-
Створіть клас
BankAccount:- Приватні властивості: accountNumber, balance
- Конструктор для встановлення номера рахунку
- Методи: deposit(), withdraw(), getBalance()
- Перевірка: не можна зняти більше, ніж є на рахунку
- Номер рахунку повинен бути тільки для читання
-
Створіть клас
Door:- Приватні властивості: isLocked, keyCode
- Конструктор для встановлення пароля
- Методи: lock(), unlock(String code), isLocked()
- unlock() працює тільки з правильним паролем
- Пароль повинен бути приватним (не розголошуйте його!)
-
Створіть клас
PlayerStats:- Приватні властивості: strength, defense, speed
- Конструктор для встановлення всіх показників
- Методи-отримувачі для всіх показників
- Метод: getPowerLevel(), що повертає strength + defense + speed
- Показники не можуть бути від'ємними або перевищувати 100
-
Модифікуйте клас: Візьміть один із класів з попереднього уроку та додайте належну інкапсуляцію:
- Зробіть всі властивості приватними
- Додайте відповідні методи-отримувачі та методи-встановлювачі
- Додайте перевірку там, де це необхідно