Los cuatro pilares en Java
Herencia simple, encapsulamiento, polimorfismo y abstracción
Herencia simple
Java soporta herencia simple: una clase solo puede extender una clase padre (extends). Esto evita el problema del diamante de C++, pero limita la reutilización. Las interfaces compensan esta
restricción permitiendo implementación múltiple.
// Herencia: una clase extiende otra para reutilizar y especializar comportamiento
public class Animal {
protected String nombre;
public Animal(String nombre) {
this.nombre = nombre;
}
public void hacerSonido() {
System.out.println(nombre + " hace un sonido");
}
}
public class Perro extends Animal {
public Perro(String nombre) {
super(nombre); // llama al constructor de Animal
}
@Override
public void hacerSonido() {
System.out.println(nombre + " ladra"); // especialización
}
}
// Uso:
Animal a = new Perro("Rex");
a.hacerSonido(); // "Rex ladra" — polimorfismo en acción Encapsulamiento
Ocultar el estado interno de un objeto y exponer solo las operaciones válidas. El objetivo no es la seguridad sino mantener los invariantes de la clase: condiciones que siempre deben ser verdad sobre el estado interno.
// Encapsulamiento: ocultar el estado interno, exponer solo la interfaz pública
public class CuentaBancaria {
private double saldo; // privado: nadie puede modificarlo directamente
public CuentaBancaria(double saldoInicial) {
if (saldoInicial < 0) throw new IllegalArgumentException("Saldo negativo");
this.saldo = saldoInicial;
}
public void depositar(double importe) {
if (importe <= 0) throw new IllegalArgumentException("Importe inválido");
saldo += importe;
}
public boolean retirar(double importe) {
if (importe > saldo) return false;
saldo -= importe;
return true;
}
public double getSaldo() { return saldo; }
}
// El invariante (saldo >= 0) queda garantizado internamente Polimorfismo
Un objeto puede ser tratado como su tipo padre. En Java el polimorfismo de subtipo se implementa mediante dynamic dispatch: la JVM decide en tiempo de ejecución qué implementación de un método invocar según el tipo real del objeto, no el tipo declarado.
// Polimorfismo: mismo método, comportamiento distinto según el tipo real
public abstract class Forma {
public abstract double area();
}
public class Circulo extends Forma {
private double radio;
public Circulo(double radio) { this.radio = radio; }
@Override
public double area() { return Math.PI * radio * radio; }
}
public class Rectangulo extends Forma {
private double ancho, alto;
public Rectangulo(double ancho, double alto) {
this.ancho = ancho; this.alto = alto;
}
@Override
public double area() { return ancho * alto; }
}
// El llamador no necesita saber qué tipo concreto es
List<Forma> formas = Arrays.asList(new Circulo(5), new Rectangulo(3, 4));
for (Forma f : formas) {
System.out.println(f.area()); // dispatch en tiempo de ejecución
} Abstracción
Modelar un concepto por lo que hace (contrato), no por cómo lo hace (implementación). En Java se expresa mediante interfaces y clases abstractas. La abstracción es la base del principio de inversión de dependencias: el código de alto nivel depende de abstracciones, no de implementaciones concretas.
// Abstracción: exponer qué hace algo, ocultar cómo lo hace
public interface Repositorio<T> {
T buscarPorId(Long id);
void guardar(T entidad);
void eliminar(Long id);
}
// El llamador trabaja con la abstracción, no con la implementación concreta
public class ServicioUsuarios {
private final Repositorio<Usuario> repo; // depende de la abstracción
public ServicioUsuarios(Repositorio<Usuario> repo) {
this.repo = repo;
}
public Usuario obtenerUsuario(Long id) {
return repo.buscarPorId(id); // no sabe si es BBDD, memoria, API...
}
}