Hytale Modding
Hytale Modding
Fundamentos do Java

08 — Encapsulamento e modificadores de acesso

Aprenda a proteger e controlar o acesso aos dados para as suas classes.

O encapsulamento consiste em ocultar os detalhes internos de uma classe e determinar como os seus dados podem ser acessados ou modificados. Isso evita que erros aconteçam e o seu código se torna mais simples de manter e atualizar.

O problema sem o encapsulamento

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;
        
        // Ops! Alguém pode quebrar as regras
        player.health = 500;      // Vida excedente!
        player.health = -50;      // Vida negativa!
        player.name = "";         // Sem nome!
    }
}

Sem proteção, qualquer um pode definir valores inválidos!

Modificadores de acesso

O Java tem palavras-chave que controlam quem pode acessar os membros da sua classe:

ModificadorClassePacoteSubclasseMundo
public
protected
(nenhum)
private

Por enquanto, concentre-se em:

  • public — Qualquer pessoa pode acessar
  • private — Apenas essa classe pode acessar

Tornando propriedades privadas

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;
    }
}

Não é mais possível acessar as propriedades diretamente:

Player player = new Player("Alice", 100);
player.health = 500;  // ❌ Erro! Health é privado

Métodos Getters e Setters

Para acessar propriedades privadas, crie os métodos getter e setter:

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;
    }
    
    // Getter – retorna o valor
    public int getHealth() {
        return health;
    }
    
    // Setter – define o valor com validação
    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;
    }
}

Agora você pode interagir com segurança com o objeto:

Player player = new Player("Alice", 100);

player.setHealth(150);  // Limitado automaticamente a 100
System.out.println(player.getHealth());  // 100

player.setHealth(-20);  // Definido automaticamente para 0
System.out.println(player.getHealth());  // 0
Nomeação Getter e Setter

Siga as convenções de nomenclatura do Java:

  • Getter: get + nome da propriedade (capitalizado);
  • Setter: set + nome da propriedade (capitalizado);
  • Boolean: is + nome da propriedade (capitalizado).
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) { }

Vantagens do encapsulamento

1. Validação

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. Propriedades apenas de leitura

Às vezes, você não quer um setter:

public class Monster {
    private String id;  // Deve permanecer inalterável
    private int health;
    
    public Monster(String id, int health) {
        this.id = id;
        this.health = health;
    }
    
    // Apenas getter, sem setter!
    public String getId() {
        return id;
    }
    
    public int getHealth() {
        return health;
    }
    
    public void setHealth(int health) {
        this.health = health;
    }
}

3. Propriedades calculadas

Os métodos Getters não precisam retornar um campo diretamente:

public class Player {
    private int health;
    private int maxHealth;
    
    public int getHealth() {
        return health;
    }
    
    // Propriedade calculada
    public double getHealthPercentage() {
        return (health * 100.0) / maxHealth;
    }
    
    // Propriedade calculada
    public boolean isLowHealth() {
        return getHealthPercentage() < 25;
    }
}

Exemplos práticos

Item com durabilidade

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 + " está quebrado(a)!");
            return;
        }
        
        durability--;
        System.out.println(name + " usado(a). Durabilidade: " + durability);
        
        if (durability <= 0) {
            broken = true;
            System.out.println(name + " quebrou!");
        }
    }
    
    public void repair() {
        durability = maxDurability;
        broken = false;
        System.out.println(name + " reparado(a)!");
    }
    
    // Getters
    public String getName() {
        return name;
    }
    
    public int getDurability() {
        return durability;
    }
    
    public boolean isBroken() {
        return broken;
    }
    
    public double getDurabilityPercentage() {
        return (durability * 100.0) / maxDurability;
    }
}

Exemplo de conta bancária

    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 + "x ouro(s) adicionado(s)");
        }
    }
    
    public boolean spendGold(int amount) {
        if (amount > gold) {
            System.out.println("Ouro insuficiente!");
            return false;
        }
        
        gold -= amount;
        System.out.println(amount + "x ouro(s) usado(s)");
        return true;
    }
    
    public int getGold() {
        return gold;
    }
    
    public int getTotalValue() {
        // 1 ouro = 100 pratas
        return gold * 100 + silver;
    }
}

Sistema de proteção de bloco

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("Bloco desbloqueado");
        } else {
            System.out.println("Você não é o dono deste bloco!");
        }
    }
    
    // Somente Getters — a posição e owner não devem mudar
    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;
    }
}

Quando usar Private e Public

Regras gerais

Make it private by default! Only make things public if they need to be accessed from outside.

Private:

  • Dados internos (health, position, inventory);
  • Métodos de ajuda usados apenas na classe;
  • Tudo que precise de validação.

Public:

  • Métodos que definem o comportamento da classe;
  • Construtor;
  • Métodos que outras classes precisam utilizar.
public class Example {
    // Private - internal data
    private int internalCounter;
    private String secretKey;
    
    // Public - part of the interface
    public void doSomething() {
        // Uses private helper method
        validateData();
    }
    
    // Private - internal helper
    private void validateData() {
        // ...
    }
}

A palavra-chave final

final means a variable can't be changed after it's set:

public class Player {
    private final String id;  // Deve permanecer inalterado após ser criado
    private String name;      // Alterável
    private int health;       // Alterável
    
    public Player(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getId() {
        return id;
    }
    
    // Não há setId() — termina aqui!
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

A palavra-chave static

Membros estáticos

A class can define two kinds of members:

  • Instance members — owned by each object (each instance has its own copy).

  • Static members — owned by the class (one shared copy for the entire type).

Put simply: instance members belong to objects; static members belong to the class itself and are shared by all objects of that type.

Declaração

    /* (modificador de acesso) */ static ... memberName; 

Exemplo

class Data {
    public int x; // Instanced member
    public static int y = 1000; // Static member

    // Instanced member:
    // can access to both static and non-static members
    public void foo() {
        x = 100; // OK - same as this.x = 100;
        y = 100; // OK - same as Data.y = 200;
    }

    // Static member:
    // cannot access to non-static variables
    public static void bar() {
        x = 100; // Error: non-static variable x cannot be renference from a static context
        y = 100; // OK
    }
}

Accessing static members

Data data = new Data();
data.x = 1000; // OK

data.y = 1000; // OK-ish - not really suggested; it's better to use Data.y
Data.y = 1000; // OK - best practice

Data.x = 1000; // Error: cannot access instanced variables in a static context

Campos estáticos

A static field represents a data member owned by the class type rather then the object. Static fields are also stored in a specific memory location that's been shared between all the object instances that are created.

É declarado o seguinte:

/* (modificador de acesso) (opcional) */ static /* final/volatile (opcional) */ fieldName;

Let's take the same Data class example and add this constructor:

public Data() {
    y++; // remember that's the same as Data.y++;
}
// Every instance of Data will have a private copy of the instanced member x
// However it will point to the same location in memory for the member 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
// ... and so on

Métodos estáticos

Static methods essentially represent a function member of a certain class type

From the Data class remember the function (instanced method) foo and (static method) bar

One can access those methods via:

Data d1 = new Data();

d1.foo(); // Instanced method: Accessible ONLY by an object

Data.bar(); // Static method: accessible without an object

Inicializador estático

Use a static initializer block to run initialization logic when the class is first loaded:

class OtherData {
    private static int a = 12;
    private static int b;
    private static String msg;

    static {
        msg = "Inicializando..."
        System.out.println(msg);
        b = 4;
        // ... inicialização complexa que não pode ser feita em uma única expressão
    }
}

Exercícios práticos

  1. Crie uma classe BankAccount:

    • Propriedades privadas: accountNumber, balance;
    • Construtor para definir o número da conta;
    • Métodos: deposit(), withdraw(), getBalance();
    • Validação: não é possível sacar mais do que o saldo;
    • O número da conta deve ser apenas leitura.
  2. Crie uma classe Door:

    • Propriedades privadas: isLocked, keyCode;
    • Construtor para definir o código-chave;
    • Métodos: lock(), unlock(String code), isLocked();
    • O método unlock() funciona apenas com o código correto;
    • O código deve ser private (não o exponha!).
  3. Crie uma classe PlayerStats:

    • Propriedades privadas: strength, defense, speed;
    • Construtor para definir todas as estatísticas;
    • Getters para todas as estatísticas;
    • Método: getPowerLevel() que retorna strength + defense + speed;
    • As estatísticas não podem ser negativas ou superiores a 100.
  4. Refatore uma classe: pegue uma de suas classes da lição anterior e adicione o encapsulamento adequado:

    • Prive todas as propriedades;
    • Adicionar getters e setters apropriados;
    • Adicione a validação quando for necessário.