Java
Encapsulación: getters, setters y modificadores de acceso
Por qué los atributos se declaran private, cómo se escriben getters y setters de verdad, qué significa cada modificador de acceso y cuándo NO hace falta un setter.
El problema: un objeto que nadie protege
En Clases y objetos la primera versión de Alumno dejaba sus atributos sin ningún modificador:
public class Alumno {
String nombre;
double nota;
}Con eso, cualquier línea de cualquier parte del programa puede escribir:
Alumno a = new Alumno();
a.nota = -50;
a.nota = 12345;
a.nombre = null;Nada de eso tiene sentido para un alumno real, y la clase Alumno no puede hacer absolutamente nada al respecto. El objeto queda en un estado inválido y el error se descubre mucho después, en otro archivo, cuando alguien intenta calcular un promedio y le sale un número absurdo.
La encapsulación resuelve esto invirtiendo quién manda: en vez de que el resto del programa escriba directamente en los atributos, la clase expone un conjunto controlado de métodos y se guarda para sí la única llave que toca los datos.
“Encapsular” es literalmente “meter en una cápsula”. La imagen es la de un medicamento: la cápsula tiene una superficie lisa y predecible con la que interactúas, y el contenido queda adentro, sin que puedas manipularlo directamente.
Los cuatro modificadores de acceso
Java tiene cuatro niveles de visibilidad. Se escriben delante del atributo o del método:
| Modificador | Quién puede acceder | Cuándo lo usarías |
|---|---|---|
private | Solo la propia clase | Casi todos los atributos. Es el que deberías usar por defecto. |
| (nada) | Las clases del mismo paquete (package-private) | Rara vez a propósito; suele ser un private que alguien olvidó escribir. |
protected | El mismo paquete y las subclases | Cosas que una clase hija legítimamente necesita — ver Herencia. |
public | Cualquier clase, de cualquier paquete | Los métodos que forman la interfaz de uso de la clase. |
La regla práctica cabe en una línea: atributos private, métodos que quieres que se usen desde afuera public.
public class Alumno {
private String nombre;
private double nota;
}A partir de ese private, la línea a.nota = -50; escrita desde main ya no compila. El compilador dice algo como nota has private access in Alumno — y eso es una victoria, no un obstáculo: el error apareció al compilar, en la línea exacta que lo causa, en vez de convertirse en un número raro tres pantallas más adelante.
Getters: leer sin poder escribir
Un getter es un método public que devuelve el valor de un atributo private. La convención de nombres en Java es get + el nombre del atributo en CamelCase:
public class Alumno {
private String nombre;
private double nota;
public String getNombre() {
return nombre;
}
public double getNota() {
return nota;
}
}Cada getter no recibe parámetros (() vacío), devuelve el tipo del atributo, y su cuerpo es un único return. Desde afuera se usa así:
Alumno a = new Alumno("Ana", 8.5);
System.out.println(a.getNombre() + " sacó " + a.getNota());Para un atributo boolean la convención cambia el prefijo: se usa is en vez de get, porque hace que la condición se lea como una frase. if (alumno.isAprobado()) se lee mucho mejor que if (alumno.getAprobado()).
Setters: escribir bajo condiciones
Un setter es el camino de vuelta: un método public que recibe un valor nuevo y lo asigna al atributo, pero solo si pasa las validaciones que la clase decida. La convención es set + el nombre, no devuelve nada (void) y recibe exactamente un parámetro del tipo del atributo:
public void setNota(double nuevaNota) {
if (nuevaNota < 0 || nuevaNota > 10) {
System.out.println("Nota inválida: " + nuevaNota);
return;
}
this.nota = nuevaNota;
}Aquí está la diferencia real con un atributo público: a.nota = -50 no tenía forma de fallar; a.setNota(-50) tiene un lugar donde poner el if. Ese if es toda la razón de existir del setter.
Un setter que solo hace this.x = x; sin validar nada no protege nada: es un atributo público con dos líneas más de código. No está mal escribirlo — a veces se hace porque una librería o un framework espera encontrar esa forma — pero no confundas escribir el setter con haber encapsulado algo.
La clase completa
public class Alumno {
private String nombre;
private double nota;
public Alumno(String nombre, double nota) {
this.nombre = nombre;
setNota(nota);
}
public String getNombre() {
return nombre;
}
public double getNota() {
return nota;
}
public void setNota(double nuevaNota) {
if (nuevaNota >= 0 && nuevaNota <= 10) {
this.nota = nuevaNota;
}
}
public boolean isAprobado() {
return nota >= 6;
}
}Fíjate en dos detalles que suelen pasarse por alto:
- El constructor llama a
setNota(nota)en vez de asignar directo. Si la validación vive en el setter, dejar que el constructor la esquive abre justamente el agujero que estabas tapando:new Alumno("Ana", -50)volvería a crear un objeto inválido. isAprobado()no tiene ningún atributo detrás. No todo métodopublices el getter de algo: este calcula un dato a partir del estado interno. Guardar un atributoaprobadoademás denotasería peor, porque habría que acordarse de mantenerlo sincronizado cada vez que la nota cambie.
No todo atributo necesita los dos
Escribir mecánicamente un getter y un setter para cada atributo es el error más común al aprender encapsulación. La pregunta correcta es “¿qué necesita el resto del programa hacer con este dato?”:
| Situación | Qué exponer |
|---|---|
| El valor se lee mucho y no debe cambiar nunca después de crear el objeto | Solo getter (por ejemplo, el DNI o el legajo) |
| El valor se lee y se modifica durante la vida del objeto | Getter + setter con validación |
| El valor es un detalle interno de cómo la clase hace su trabajo | Ninguno de los dos |
| El valor solo se configura, nunca se consulta desde afuera | Solo setter (poco frecuente, pero existe) |
Empieza siempre por lo más cerrado: todo private, ningún método público. Después abre lo que el programa realmente necesita. Es fácil agregar un getter después; sacar uno que media docena de archivos ya usa es mucho más caro.
Pon a prueba
public class Cuenta {
private String titular;
private double saldo;
public Cuenta(String titular, double saldo) {
this.titular = titular;
this.saldo = saldo;
}
public String getTitular() {
return titular;
}
public double getSaldo() {
return saldo;
}
public void setSaldo(double nuevoSaldo) {
if (nuevoSaldo >= 0) {
this.saldo = nuevoSaldo;
}
}
}La clase Producto guarda un precio que nunca puede ser negativo. Escribe el modificador de acceso del atributo, el nombre de cada método según la convención de Java, y la palabra que distingue el atributo del parámetro.
public class Producto {
double precio;
public double () {
return precio;
}
public setPrecio(double precio) {
if (precio >= 0) {
.precio = precio;
}
}
}Con la clase Alumno completa de más arriba (la que llama a setNota desde el constructor), ¿qué imprime esto?
Alumno a = new Alumno("Ana", -3);
System.out.println(a.getNota());
System.out.println(a.isAprobado());
a.setNota(7.5);
System.out.println(a.isAprobado());Imprime 0.0, después false, y después true.
La primera línea es la que sorprende: new Alumno("Ana", -3) llama a setNota(-3), que rechaza el valor y no asigna nada. Como nota es un double que nunca llegó a asignarse, se queda con su valor por defecto, que en Java es 0.0 — los atributos, a diferencia de las variables locales, se inicializan solos.
Con nota en 0.0, isAprobado() devuelve false. Después setNota(7.5) sí pasa la validación, la nota queda en 7.5, y isAprobado() pasa a true.
Esto muestra el límite de validar en silencio: el objeto quedó válido, pero con un valor que nadie pidió y sin que nadie se enterara del rechazo. Un diseño más estricto cortaría ahí mismo lanzando una excepción — ver Excepciones.
Ordena las líneas para escribir una clase Termostato con un atributo privado y un setter que limite la temperatura entre 15 y 30.
- if (nueva >= 15 && nueva <= 30) {
- }
- public int getTemperatura() {
- }
- }
- public class Termostato {
- return temperatura;
- this.temperatura = nueva;
- private int temperatura;
- }
- public void setTemperatura(int nueva) {
Siguiente paso
Ya sabes construir una clase que protege sus propios datos. El paso siguiente es que una clase pueda partir de otra en vez de escribirse desde cero, reutilizando sus atributos y métodos y cambiando solo lo que la diferencia — ver Herencia y @Override.