Hytale Modding
Conceptos básicos de Java

08 - Encapsulamiento y Modificadores de Acceso

Aprende a proteger y controlar el acceso a los datos de tu clase.

La encapsulación consiste en ocultar los detalles internos de una clase y controlar cómo se acceden y modifican sus datos. Esto previene errores y hace que tu código sea más fácil de mantener.

El Problema Sin Encapsulación

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;
        
        // Ups! Alguien podría romper las reglas
        player.health = 500;      // La vida sobrepasa el máximo!
        player.health = -50;      // Vida negativa!
        player.name = "";         // Nombre vacío!
    }
}

Sin la protección adecuada, cualquiera puede establecer valores inválidos.

Modificadores de Acceso

Java tiene palabras clave que controlan el acceso a los componentes de tu clase.

ModificadorClasePaqueteSubclaseMundo
public
protected
(none)
private

Por ahora, enfócate en:

  • public - Cualquiera puede acceder
  • private - Sólo la clase actual puede acceder

Privatizar Propiedades

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

Ahora no puedes acceder a estas propiedades directamente:

Player player = new Player("Alice", 100);
player.health = 500;  // ❌ Error! health es privado

Getters y Setters

Para acceder a propiedades privadas, se deben crear métodos getter y 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 el valor
    public int getHealth() {
        return health;
    }
    
    // Setter - establece el valor (con validación)
    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;
    }
}

Ahora puedes interactuar con el objeto de manera segura:

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

player.setHealth(150);  // Limitado automáticamente a 100
System.out.println(player.getHealth());  // 100

player.setHealth(-20);  // Se establece automáticamente a 0
System.out.println(player.getHealth());  // 0
Nomenclatura de los Getters y Setters

Se deben seguir las convenciones de Java:

  • Getter: get + nombre de la propiedad (con mayúscula inicial)
  • Setter: set + nombre de la propiedad (capitalizado)
  • Boolean: is + nombre de la propiedad (con mayúscula inicial)
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) { }

Beneficios de la Encapsulación

1. Validaciones

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. Propiedades de Solo Lectura (read-only)

A veces no quieres un setter.

public class Monster {
    private String id;  // Nunca debería cambiar
    private int health;
    
    public Monster(String id, int health) {
        this.id = id;
        this.health = health;
    }
    
    // Solo un getter - sin setter!
    public String getId() {
        return id;
    }
    
    public int getHealth() {
        return health;
    }
    
    public void setHealth(int health) {
        this.health = health;
    }
}

3. Propiedades Calculadas

Los getters pueden no devolver un campo directamente:

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

Ejemplos prácticos

Item con Durabilidad

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á roto!");
            return;
        }
        
        durability--;
        System.out.println(name + " fue utilizado. Durabilidad: " + durability);
        
        if (durability <= 0) {
            broken = true;
            System.out.println(name + " se rompió!");
        }
    }
    
    public void repair() {
        durability = maxDurability;
        broken = false;
        System.out.println(name + " fue reparado!");
    }
    
    // Getters
    public String getName() {
        return name;
    }
    
    public int getDurability() {
        return durability;
    }
    
    public boolean isBroken() {
        return broken;
    }
    
    public double getDurabilityPercentage() {
        return (durability * 100.0) / maxDurability;
    }

Ejemplo con Cuenta Bancaria

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("Se agregó " + amount + " de oro");
        }
    }
    
    public boolean spendGold(int amount) {
        if (amount > gold) {
            System.out.println("No hay suficiente oro!");
            return false;
        }
        
        gold -= amount;
        System.out.println("Se gastó " + amount + " de oro");
        return true;
    }
    
    public int getGold() {
        return gold;
    }
    
    public int getTotalValue() {
        // 1 de oro = 100 de plata
        return gold * 100 + silver;
    }
}

Sistema de Bloques Protegidos

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("Bloque desbloqueado");
        } else {
            System.out.println("No eres dueño de este bloque!");
        }
    }
    
    // Solo getters - la posición y el dueño no deberían cambiar
    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;
    }
}

Cuando usar Private vs Public

Reglas Generales

¡Hazlo privado por defecto! Haz las cosas públicas solo si necesitan ser accedidas desde fuera.

Private

  • Datos internos (salud, posición, inventario)
  • Métodos de ayuda usados solo dentro de la clase
  • Todo lo que necesite validación

Public:

  • Métodos que definen el comportamiento de la clase
  • Constructor
  • Métodos que otras clases necesiten llamar
public class Example {
    // Private - datos internos
    private int internalCounter;
    private String secretKey;
    
    // Public - parte de la interfaz
    public void doSomething() {
        // Utiliza el metodo de ayuda privado
        validateData();
    }
    
    // Private - metodo de ayuda privado
    private void validateData() {
        // ...
    }
}

La palabra clave final

final significa que una variable no puede ser cambiada después de ser creada:

public class Player {
    private final String id;   // Su valor no puede cambiar luego de crearse
    private String name;     // Puede cambiar
    private int health;          // Puede cambiar
    
    public Player(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getId() {
        return id;
    }
    
    // No hay setter setId() - id es final!
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

La palabra clave estático

Miembros estáticos

Una clase puede definir dos tipos de miembros:

  • Miembros de instancia — pertenecen a cada objeto (cada instancia tiene su propia copia).

  • Miembros estáticos — pertenecen a la clase (una sola copia compartida para todo el tipo).

En pocas palabras: los miembros de instancia pertenecen a los objetos; los miembros estáticos pertenecen a la propia clase y son compartidos por todos los objetos de ese tipo.

Declaración

/* (modificador de acceso) */ estático ... Nombre Del Miembro;

Ejemplo

class Data {
    public int x; // Miembro de instancia
    public static int y = 1000; // Miembro estático

    // Miembro de instancia:
    // puede acceder tanto a miembros estáticos como no estáticos
    public void foo() {
        x = 100; // OK - igual que this.x = 100;
        y = 100; // OK - igual que Data.y = 100;
    }

    // Miembro estático:
    // no puede acceder a variables no estáticas
    public static void bar() {
        x = 100; // Error: la variable no estática x no puede referenciarse desde un contexto estático
        y = 100; // OK
    }
}

Accediendo a miembros estáticos

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

data.y = 1000; // Más o menos OK - no se recomienda; es mejor usar Data.y
Data.y = 1000; // OK - mejor práctica

Data.x = 1000; // Error: no se puede acceder a variables de instancia desde un contexto estático

Campos estáticos

Un campo estático representa un miembro de datos que pertenece al tipo de clase en lugar de al objeto. Los campos estáticos también se almacenan en una ubicación de memoria específica que es compartida entre todas las instancias de objetos que se crean.

Se declara de la siguiente manera:

/* (modificador de acceso) (opcional) */ estático /* final/volatile (opcional) */ nombre Del Campo;

Tomemos el mismo ejemplo de la clase Data y añadamos este constructor:

Publicó Data() {
    y++; // recuerda que esto es lo mismo que Data.y++;
}
// Cada instancia de Data tendrá una copia privada del miembro de instancia x
// Sin embargo, todas apuntarán a la misma ubicación en memoria para el miembro 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
// ... y así sucesivamente

Métodos estáticos

Los métodos estáticos representan esencialmente un miembro funcional de un determinado tipo de clase.

En la clase Data, recuerda la función (método de instancia) fue y él (método estático) bar.

Se puede acceder a esos métodos mediante:

Static Initializer

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 = "Initialization..."
        System.out.println(msg);
        b = 4;
        // ... complex initialization that can't be done in a single expression
    }
}

Ejercicios de Práctica

  1. Crea una clase BankAccount:

    • Propiedades privadas: accountNumber, balance
    • Constructor para establecer accountNumber
    • Métodos deposit(), withdraw(), getBalance()
    • Validación: no se puede retirar más que balance
    • accountNumber debe ser de solo lectura
  2. Crea una Clase Door:

    • Propiedades privadas: isLocked, keyCode
    • Constructor para establecer keyCode
    • Métodos: lock(), unlock(String code), isLocked()
    • unlock() solo funciona con código correcto
    • ¡El código debe ser private (no exponerlo!)
  3. Crea una clase PlayerStats:

    • Propiedades privadas: strength, defense, speed
    • Constructor para establecer todas las estadísticas
    • Getters para todas las estadísticas
    • Método: getPowerLever() que retorne strength + defense + speed
    • Las estadísticas no pueden ser negativas o superiores a 100
  4. Refactorizar una clase: Toma una de tus clases de la lección anterior y añade una encapsulación adecuada:

    • Hacer todas las propiedades privadas
    • Añadir getters y setters apropiados
    • Añadir validación cuando sea necesario