Hytale Modding
Основи Java

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;
        // ... складна ініціалізація, яку неможливо виконати одним виразом
    }
}

Практичні вправи

  1. Створіть клас BankAccount:

    • Приватні властивості: accountNumber, balance
    • Конструктор для встановлення номера рахунку
    • Методи: deposit(), withdraw(), getBalance()
    • Перевірка: не можна зняти більше, ніж є на рахунку
    • Номер рахунку повинен бути тільки для читання
  2. Створіть клас Door:

    • Приватні властивості: isLocked, keyCode
    • Конструктор для встановлення пароля
    • Методи: lock(), unlock(String code), isLocked()
    • unlock() працює тільки з правильним паролем
    • Пароль повинен бути приватним (не розголошуйте його!)
  3. Створіть клас PlayerStats:

    • Приватні властивості: strength, defense, speed
    • Конструктор для встановлення всіх показників
    • Методи-отримувачі для всіх показників
    • Метод: getPowerLevel(), що повертає strength + defense + speed
    • Показники не можуть бути від'ємними або перевищувати 100
  4. Модифікуйте клас: Візьміть один із класів з попереднього уроку та додайте належну інкапсуляцію:

    • Зробіть всі властивості приватними
    • Додайте відповідні методи-отримувачі та методи-встановлювачі
    • Додайте перевірку там, де це необхідно