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.
| Modificador | Clase | Paquete | Subclase | Mundo |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (none) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Por ahora, enfócate en:
public- Cualquiera puede accederprivate- 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 privadoGetters 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()); // 0Se 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
¡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áticoCampos 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í sucesivamenteMé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
-
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 accountNumberdebe ser de solo lectura
- Propiedades privadas:
-
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!)
- Propiedades privadas:
-
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 retornestrength + defense + speed - Las estadísticas no pueden ser negativas o superiores a 100
- Propiedades privadas:
-
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