1. Definición de la encapsulación
Encapsulación es uno de los principios fundamentales de la programación orientada a objetos (POO). En pocas palabras, la encapsulación es la capacidad de ocultar las entrañas del objeto y dar acceso a ellas solo a través de «puertas» especialmente previstas: los métodos públicos.
Imagina una cafetera moderna. El usuario solo ve los botones y la pantalla: no necesita saber cómo están hechos la caldera, la bomba y los tubos por dentro. Pulsa «Capuchino» y obtiene el resultado. Todo lo interno está oculto. ¡Eso es encapsulación!
En Java (y en otros lenguajes de POO) la encapsulación se consigue gracias a:
- Ocultación de datos — los campos de la clase se declaran como private (o al menos no public).
- Interfaz pública — solo se «exponen» hacia fuera los métodos que realmente necesita el usuario del objeto.
Esquema: cómo se ve la encapsulación
+-------------------------------+
| Clase Student |
|-------------------------------|
| - name: String | // campo privado
| - age: int | // campo privado
|-------------------------------|
| + getName(): String | // método público
| + setName(String): void | // método público
| + getAge(): int | // método público
| + setAge(int): void | // método público
+-------------------------------+
Aquí el signo - significa private (oculto), y el + — public (accesible desde fuera).
¿Qué son los getters y setters?
Antes de entrar en por qué hace falta la encapsulación, conozcamos rápidamente los getters y setters: son métodos especiales que nos ayudan a «comunicarnos» con los campos privados de la clase.
Getter (getter) — método que obtiene el valor de un campo privado. Suele llamarse getFieldName().
Setter (setter) — método que establece el valor de un campo privado. Suele llamarse setImyaPolya(value).
Ejemplo sencillo:
public class Student {
private String name; // campo privado: no es visible desde fuera
// Getter - "dame el nombre del estudiante"
public String getName() {
return name;
}
// Setter - "establece el nombre del estudiante"
public void setName(String name) {
this.name = name;
}
}
Cómo funciona:
Student student = new Student();
student.setName("John"); // establecemos el nombre a través del setter
String name = student.getName(); // obtenemos el nombre a través del getter
Piensa en los getters y setters como «peticiones educadas» al objeto: en lugar de meterle la mano en el bolsillo (student.name = "John"), le pedimos amablemente: «Por favor, establece el nombre» (student.setName("John")).
Un adelanto: dentro de un par de lecciones estudiaremos en detalle los getters y setters, conoceremos sus secretos y aprenderemos a aprovecharlos al máximo. Por ahora basta con entender la idea principal.
2. ¿Para qué sirve la encapsulación?
Protección de datos frente a un uso incorrecto
Si todos los campos de la clase fueran públicos (public), cualquier código externo podría cambiar sus valores directamente como quisiera:
Student s = new Student();
s.age = -1000; // ¡Ups, un estudiante vampiro!
¡Esto es peligroso! Tu programa puede empezar a comportarse de forma impredecible y los bugs aparecerán en los lugares más inesperados.
Posibilidad de cambiar la implementación interna sin afectar al código externo
La encapsulación te permite cambiar la estructura interna de la clase sin romper el código que la utiliza. Por ejemplo, puedes cambiar la forma de almacenar los datos o añadir validación en los métodos, y los usuarios de la clase ni se enterarán: siguen invocando los mismos métodos.
Mejora de la legibilidad y del mantenimiento del código
Cuando todos los detalles internos están ocultos, la interfaz externa se vuelve más limpia y comprensible. El programador que usa tu clase no necesita saber cómo funciona por dentro: le basta con conocer qué métodos están disponibles y qué hacen.
Ejemplo de la vida real
Piensa en cómo usas tu smartphone. No te paras a pensar cómo se procesan exactamente las pulsaciones en la pantalla, cómo es la batería por dentro o cómo funciona el módulo de comunicaciones. Simplemente invocas las funciones que necesitas mediante una interfaz comprensible (iconos, botones). Si el fabricante cambia la implementación interna, ni lo notarás.
Ejemplo real en código
Imagina que tenemos una clase BankAccount. En una versión antigua del programa, el saldo se almacenaba como una cadena con puntos separadores, por ejemplo "1.000.50". Después los programadores decidieron almacenar el saldo como un número double. Si el campo fuera público, todo el código antiguo que accedía directamente a account.balance se rompería.
Pero si usamos encapsulación y ocultamos el campo, proporcionando solo los métodos deposit() y getBalance(), el código externo ni se enterará de los cambios:
public class BankAccount {
private double balance; // campo oculto
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
Ahora, si mañana queremos almacenar el saldo, por ejemplo, en céntimos (long), nos basta con cambiar la implementación interna de la clase, y todo el código restante que llama a deposit() y getBalance() seguirá funcionando como antes.
3. Ejemplos de mala y buena encapsulación
Mal ejemplo: campos públicos
public class Student {
public String name;
public int age;
}
Problemas de este enfoque:
- Cualquier código puede asignar a los campos cualquier valor, incluso valores incorrectos.
- No hay posibilidad de añadir validación de datos.
- Si decides cambiar el tipo o la estructura del campo, tendrás que modificar todo el código que lo usa.
Buen ejemplo: campos privados y métodos públicos
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// ¡Se puede añadir una validación!
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("El nombre no puede estar vacío");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// Comprobamos que la edad no sea negativa
if (age < 0) {
throw new IllegalArgumentException("La edad no puede ser negativa");
}
this.age = age;
}
}
Ventajas:
- El código externo no puede cambiar los campos directamente: solo a través de métodos.
- Se pueden añadir comprobaciones, logs, acciones automáticas (por ejemplo, actualización de estadísticas).
- Si la representación interna cambia (por ejemplo, si la edad pasa a almacenarse en otro formato), la interfaz externa permanecerá igual.
Cómo se ve en uso
Student s = new Student();
s.setName("Alice");
s.setAge(20);
System.out.println(s.getName() + ", edad: " + s.getAge());
Intenta asignar una edad negativa: ¡recibirás un error ya en tiempo de ejecución! Tu programa queda protegido contra disparates.
4. Relación con otros principios de la POO
La encapsulación es la «madre» de los demás principios de la POO. Sin ella no existirían ni la herencia, ni el polimorfismo, ni la abstracción. Los estudiaremos más adelante, pero por ahora los mencionamos brevemente:
- Herencia (extends) permite crear nuevas clases a partir de clases existentes, ampliando o modificando su comportamiento. Si las tripas de la clase estuvieran abiertas, una subclase podría romper sin querer algo importante.
- Polimorfismo (la capacidad de objetos de distintas clases de reaccionar de forma diferente al mismo mensaje) es imposible sin una separación clara entre la implementación interna y la interfaz externa.
- Abstracción — es destacar solo las características esenciales de un objeto y ocultar los detalles. La encapsulación ayuda a implementar la abstracción en la práctica.
Analogía
Imagina un automóvil. El conductor solo tiene acceso al volante, los pedales y las palancas: esa es la interfaz. Todo lo demás (motor, caja de cambios, electrónica) está oculto bajo el capó. Si el conductor pudiera manejar directamente cada tornillo del motor, ¡los accidentes serían mucho más frecuentes!
5. Ejemplo práctico: encapsulación en nuestra aplicación
Sigamos desarrollando nuestra aplicación de aprendizaje — por ejemplo, una «Agenda de contactos». Supongamos que tenemos una clase Contact que guarda el nombre y el teléfono.
Sin encapsulación (anti–ejemplo):
public class Contact {
public String name;
public String phone;
}
Uso:
Contact contact = new Contact();
contact.name = ""; // ¡Uy! El nombre está vacío
contact.phone = null; // Teléfono no establecido
Con encapsulación (enfoque correcto):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("El nombre del contacto no puede estar vacío");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("El teléfono no puede estar vacío");
}
this.phone = phone;
}
}
Ahora el código externo no podrá dejar el nombre o el teléfono vacíos:
Contact contact = new Contact();
contact.setName("John");
contact.setPhone("+1-999-123-45-67");
Si intentas establecer un nombre vacío, el programa lanzará un error.
6. Matices útiles
Encapsulación y mantenimiento a largo plazo del código
Cuando trabajas en un proyecto pequeño de aprendizaje, parece que puedes hacerlo todo «de buena fe»: ¿quién va a asignar una edad negativa o un nombre vacío? Pero en cuanto el proyecto crece, aparecen otros desarrolladores, e incluso tú mismo olvidas los detalles de la implementación al cabo de un par de meses — ahí es donde la encapsulación te salva del caos.
- Es fácil cambiar las tripas de la clase — si hace falta almacenar el teléfono como un objeto de tipo PhoneNumber y no como cadena, solo cambias la implementación y no tocas el código externo.
- Más fácil de probar — si todos los cambios se realizan solo a través de métodos, es fácil rastrear qué datos cambian y cuándo.
- Menos bugs — protección frente a valores incorrectos y cambios accidentales.
Pregunta: ¿siempre hacen falta getters y setters?
A menudo los principiantes piensan: «Si encapsulación significa campos privados y getters/setters públicos, ¡entonces hay que hacer getter y setter para cada campo!». No es exactamente así.
- A veces un campo debe ser solo de lectura (por ejemplo, un identificador único del objeto). En ese caso, haz solo el getter.
- A veces un campo no necesita «exponerse hacia fuera» — entonces no hagas ni getter ni setter.
- El setter puede ser privado si el valor del campo solo puede cambiarse dentro de la propia clase.
Regla de oro: expón solo los datos y métodos que realmente necesita el código externo.
Visualización: comparación de enfoques
| Enfoque | Ejemplo de acceso al campo | Posibilidad de control | Seguridad |
|---|---|---|---|
| campos public | |
No | Baja |
| campos private + métodos | |
Sí | Alta |
7. Errores comunes al trabajar con la encapsulación
Error n.º 1: Todos los campos de la clase están declarados como public. Es el error más común entre principiantes. Este código se vuelve incontrolable rápidamente: cualquiera puede cambiar cualquier dato sin que te enteres. ¡No lo hagas, aunque tengas muchas ganas de ahorrar tiempo!
Error n.º 2: Getters y setters sin validación ni lógica. Si creas métodos de acceso, úsalos para validar: no permitas asignar valores incorrectos. Simplemente «copiar» el valor del parámetro al campo no siempre es la mejor opción.
Error n.º 3: Revelar prematuramente la estructura interna. Si creas getters/setters para todos los campos «por si acaso», corres el riesgo de exponer demasiados detalles que luego serán difíciles de cambiar.
Error n.º 4: Devolver objetos mutables directamente. Si un campo es un objeto mutable (por ejemplo, una lista), no lo devuelvas directamente mediante el getter. Es mejor devolver una copia o hacerlo inmutable.
GO TO FULL VERSION