Clases abstractas en Java
Define implementaciones parciales en Java con clases y métodos abstractos que las subclases deben completar.
Una clase abstracta es una clase que no puede ser instanciada directamente. Existe para ser extendida. Puede combinar métodos concretos (con cuerpo) y métodos abstractos (sin cuerpo, que la subclase debe implementar) — esa combinación es lo que la diferencia tanto de una clase regular como de una interfaz.
Esta página explica cómo declarar una clase abstracta, cómo las subclases la completan, por qué las clases abstractas pueden tener estado, el patrón de método plantilla y cómo elegir entre una clase abstracta y una interfaz.
Usa una clase abstracta cuando las subclases concretas necesiten compartir estado e infraestructura, no solo un contrato de API. Si solo necesitas un contrato, una interfaz es la mejor opción. Las clases abstractas se basan en la herencia y el polimorfismo, por lo que conviene estar familiarizado con estos conceptos primero.
Cómo declararla
Añade abstract al encabezado de la clase. Añade abstract a cualquier método que no tenga cuerpo:
public abstract class Shape {
protected final String name;
protected Shape(String name) { this.name = name; }
public abstract double area(); // no body — subclass must provide one
public String describe() { // concrete — inherited as-is
return name + " area=" + area();
}
}De esto se desprenden varias cosas:
new Shape("circle")es un error de compilación — las clases abstractas no pueden ser instanciadas.- Una subclase que no implementa todos los métodos abstractos heredados debe declararse también como
abstract. Pueden existir subclases abstractas de clases abstractas. - Una clase abstracta puede tener un constructor — las subclases lo invocan con
super(...)igual que con un padre regular.
Implementar los métodos abstractos
Una subclase concreta debe proporcionar un cuerpo para cada método abstracto heredado:
public class Circle extends Shape {
private final double r;
public Circle(double r) {
super("circle");
this.r = r;
}
@Override
public double area() { return Math.PI * r * r; }
}Ahora new Circle(2) funciona, y describe() (heredado de Shape) delega al area() de la subclase mediante despacho dinámico.
Las clases abstractas pueden tener estado
Esta es la principal razón para elegir una clase abstracta en lugar de una interfaz. El padre puede declarar campos, escribir un constructor que los inicialice y ofrecer métodos que operen sobre ese estado compartido:
public abstract class HttpHandler {
private final String path;
protected HttpHandler(String path) { this.path = path; }
public final String path() { return path; }
public abstract Response handle(Request r); // subclass-specific behavior
}Cada manejador concreto tiene un path; el padre lo almacena y lo expone; cada subclase solo escribe la lógica por petición. Las interfaces no pueden hacer esto por sí solas (no tienen campos de instancia).
Mezclar métodos abstractos y concretos — el método plantilla
Un patrón común: la clase abstracta implementa el flujo general como un método concreto y deja como abstractos los puntos de variación. Las subclases solo implementan las partes que difieren:
public abstract class Beverage {
// Template — the algorithm, written once.
public final void prepare() {
boilWater();
brew(); // varies
pourIntoCup();
addCondiments(); // varies
}
protected abstract void brew();
protected abstract void addCondiments();
private void boilWater() { System.out.println("boiling water"); }
private void pourIntoCup() { System.out.println("pouring into cup"); }
}
public class Tea extends Beverage {
protected void brew() { System.out.println("steeping tea"); }
protected void addCondiments() { System.out.println("adding lemon"); }
}Tea.prepare() ejecuta la plantilla del padre, que delega a brew y addCondiments de Tea mediante polimorfismo. Añadir una subclase Coffee solo requiere los dos métodos abstractos.
Este es el patrón de método plantilla y es la razón más habitual para elegir una clase abstracta.
Abstracto vs final
abstract y final son opuestos y el compilador lo hace cumplir:
abstract class— debe ser subclasificada.final class— no puede ser subclasificada.
Lo mismo aplica a los métodos: los métodos abstract deben ser sobreescritos; los métodos final no pueden serlo. Usar ambos a la vez es un error de compilación.
Abstracto vs interfaz
| Clase abstracta | Interfaz | |
|---|---|---|
| Constructores | Sí | No |
| Campos de instancia | Sí | No (solo constantes public static final) |
| Cuerpos de métodos | Sí (cualquier número) | Sí mediante default (con moderación) |
| Herencia | Simple — solo un padre | Múltiple — varias interfaces |
| Úsala cuando | Las subclases comparten estado e infraestructura | Las subclases solo comparten un contrato de API |
Una regla práctica: empieza con una interfaz. Pasa a (o añade) una clase abstracta solo si encuentras código compartido acumulándose entre implementaciones. Los métodos default de las interfaces en Java 8+ han ocupado parte del territorio que antes pertenecía a las clases abstractas, pero el caso de uso del "estado mutable compartido" sigue siendo territorio de las clases abstractas.
Los métodos abstractos no pueden ser private ni static
privateharía el método invisible para las subclases — no podrían sobreescribirlo, por lo que la abstracción sería inejecutable.- Los métodos
staticno se despachan dinámicamente — no pueden ser sobreescritos, solo ocultados — por lo que un método abstracto estático carecería de sentido.
Estas dos combinaciones son errores de compilación.
Errores comunes
- Olvidar
abstracten la clase. Un método sin cuerpo dentro de una clase no abstracta no compilará — el compilador exige que la clase que lo contiene también seaabstract. - Intentar instanciarla.
new Shape("x")es rechazado en tiempo de compilación. Instancia una subclase concreta en su lugar. - Dejar un método sin implementar en una subclase concreta. Si incluso un método abstracto heredado no tiene cuerpo, la subclase también debe declararse
abstract. - Esperar herencia múltiple. Una clase solo puede extender un padre (abstracto o no). Si necesitas combinar varios contratos, recurre a las interfaces.
Un ejemplo completo
Qué sigue
Ya has visto la versión de la abstracción que viene acompañada de estado y código compartido. La versión que prescinde de ambos — contrato puro, sin implementación — es la interfaz. Continúa con interfaces en Java.