Skip to content

8.4 Clases abstractas

En determinadas ocasiones, es posible que necesites definir una clase que represente un concepto lo suficientemente abstracto como para que nunca vayan a existir instancias de ella (objetos). ¿Tendría eso sentido? ¿Qué utilidad podría tener?

Imagina una aplicación para un centro educativo que utilice las clases de ejemplo Alumno y Profesor, ambas subclases de Persona. Es más que probable que esa aplicación nunca llegue a necesitar objetos de la clase Persona, pues serían demasiado genéricos como para poder ser utilizados (no contendrían suficiente información específica). Podrías llegar entonces a la conclusión de que la clase Persona ha resultado de utilidad como clase base para construir otras clases que hereden de ella, pero no como una clase instanciable de la cual vayan a existir objetos. A este tipo de clases se les llama clases abstractas.

A tener en cuenta

En algunos casos puede resultar útil disponer de clases que nunca serán instanciadas, sino que proporcionan un marco o modelo a seguir por sus clases derivadas dentro de una jerarquía de herencia. Son las clases abstractas.

La posibilidad de declarar clases abstractas es una de las características más útiles de los lenguajes orientados a objetos, pues permiten dar unas líneas generales de cómo es una clase sin tener que implementar todos sus métodos o implementando solamente algunos de ellos. Esto resulta especialmente útil cuando las distintas clases derivadas deban proporcionar los mismos métodos indicados en la clase base abstracta, pero su implementación sea específica para cada subclase.

Imagina que estás trabajando en un entorno de manipulación de objetos gráficos y necesitas trabajar con líneas, círculos, rectángulos, etc. Estos objetos tendrán en común algunos atributos que representen su estado (ubicación, color del contorno, color de relleno, etc.) y algunos métodos que modelen su comportamiento (dibujar, rellenar con un color, escalar, desplazar, rotar, etc.). Algunos de ellos serán comunes para todos ellos (por ejemplo la ubicación o el desplazamiento) y sin embargo otros (como por ejemplo dibujar) necesitarán una implementación específica dependiendo del tipo de objeto. Pero, en cualquier caso, todos ellos necesitan esos métodos (tanto un círculo como un rectángulo necesitan el método dibujar, aunque se lleven a cabo de manera diferente). En este caso resultaría muy útil disponer de una clase abstracta objeto gráfico donde se definirían las líneas generales (algunos atributos concretos comunes, algunos métodos concretos comunes implementados y algunos métodos genéricos comunes sin implementar) de un objeto gráfico y más adelante, según se vayan definiendo clases especializadas (líneas, círculos, rectángulos), se irán concretando en cada subclase aquellos métodos que se dejaron sin implementar en la clase abstracta.

1. Declaración de una clase abstracta

Ya has visto que una clase abstracta es una clase que no se puede instanciar, es decir, que no se pueden crear objetos a partir de ella. La idea es permitir que otras clases deriven de ella, proporcionando un modelo genérico y algunos métodos de utilidad general. Las clases abstractas se declaran mediante el modificador abstract:

1
2
3
[modificador_acceso] abstract class nombreClase [herencia] [interfaces] {
  ...
}

A tener en cuenta

Una clase puede contener en su interior métodos declarados como abstract (métodos para los cuales sólo se indica la cabecera, pero no se proporciona su implementación). En tal caso, la clase tendrá que ser necesariamente también abstract. Esos métodos tendrán que ser posteriormente implementados en sus clases derivadas.

Por otro lado, una clase también puede contener métodos totalmente implementados (no abstractos), los cuales serán heredados por sus clases derivadas y podrán ser utilizados sin necesidad de definirlos (pues ya están implementados).

Cuando trabajes con clases abstractas debes tener en cuenta:

  • Una clase abstracta sólo puede usarse para crear nuevas clases derivadas. No se puede hacer un new de una clase abstracta. Se produciría un error de compilación.
  • Una clase abstracta puede contener métodos totalmente definidos (no abstractos) y métodos sin definir (métodos abstractos).
Ejemplo 4.01: Revisa con cuidado el siguiente ejemplo

Basándote en la jerarquía de clases de ejemplo (Persona, Alumno, Profesor), que ya has utilizado en otras ocasiones, modifica lo que consideres oportuno para que Persona sea, a partir de ahora, una clase abstracta (no instanciable) y las otras dos clases sigan siendo clases derivadas de ella, pero sí instanciables.

Solución
En este caso lo único que habría que hacer es añadir el modificador abstract a la clase Persona. El resto de la clase permanecería igual y las clases Alumno y Profesor no tendrían porqué sufrir ninguna modificación.

1
2
3
4
5
6
public abstract class Persona {
    protected String nombre;
    protected String apellidos;
    protected LocalDate fechaNacimiento;
    ...
}

A partir de ahora no podrán existir objetos de la clase Persona. El compilador generaría un error.

Clases abstractas en la API

Localiza en la API de Java algún ejemplo de clase abstracta.

Existen una gran cantidad de clases abstractas en la API de Java. Aquí tienes un par de ejemplos:

  • La clase AbstractList:
    1
    2
    3
    4
    5
    6
    7
    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
        // ...
    
        // Métodos abstractos
        public abstract E get(int index);
        public abstract int size();
    }
    
    De la que heredan clases instanciable como Vector o ArrayList.

  • La clase AbstractSequentialList:
    1
    2
    3
    4
    5
    6
    public abstract class AbstractSequentialList<E> extends AbstractList<E>{
        // ...
    
        // Métodos abstractos
        public abstract ListIterator<E> listIterator(int index);
    }
    
    Esta clase hereda de AbstractList, y de esta hereda la clase LinkedList.

2. Métodos abstractos

Un método abstracto es un método declarado en una clase para el cual esa clase no proporciona la implementación.

Si una clase dispone de, al menos, un método abstracto se dice que es una clase abstracta.

Implementar métodos abstractos heredados

Toda clase que herede (sea subclase) de una clase abstracta debe implementar todos los métodos abstractos de su superclase o bien volverlos a declarar como abstractos (y por tanto también sería abstracta).

Para declarar un método abstracto en Java se utiliza el modificador abstract. Es un método cuya implementación no se define, sino que se declara únicamente su interfaz (cabecera) para que su cuerpo sea implementado más adelante en una clase derivada.

Un método se declara como abstracto mediante el uso del modificador abstract (como en las clases abstractas):

[modificador_acceso] abstract <tipo> <nombreMetodo> ([parámetros]) [excepciones];

A tener en cuenta

Cuando una clase contiene un método abstracto tiene que declararse como abstracta obligatoriamente.

Imagina que tienes una clase Empleado genérica para diversos tipos de empleado y tres clases derivadas: EmpleadoFijo (tiene un salario fijo más ciertos complementos), EmpleadoTemporal (salario fijo más otros complementos diferentes) y EmpleadoComercial (una parte de salario fijo y unas comisiones por cada operación). La clase Empleado podría contener un método abstracto calcularNomina, pues sabes que se método será necesario para cualquier tipo de empleado (todo empleado cobra una nómina). Sin embargo el cálculo en sí de la nómina será diferente si se trata de un empleado fijo, un empleado temporal o un empleado comercial, y será dentro de las clases especializadas de Empleado (EmpleadoFijo ̧ EmpleadoTemporal, EmpleadoComercial) donde se implementen de manera específica el cálculo de las mismas.

Debes tener en cuenta al trabajar con métodos abstractos:

  • Un método abstracto implica que la clase a la que pertenece tiene que ser abstracta, pero eso no significa que todos los métodos de esa clase tengan que ser abstractos.
  • Un método abstracto no puede ser privado (no se podría implementar, dado que las clases derivadas no tendrían acceso a él).
  • Los métodos abstractos no pueden ser estáticos, pues los métodos estáticos no pueden ser redefinidos (y los métodos abstractos necesitan ser redefinidos).
Ejemplo 4.02: Revisa con cuidado el siguiente ejemplo

Basándote en la jerarquía de clases Persona, Alumno, Profesor, crea un método abstracto llamado mostrarDatos para la clase Persona. Dependiendo del tipo de persona (alumno o profesor) el método mostrarDatos tendrá que mostrar unos u otros datos personales (habrá que hacer implementaciones específicas en cada clase derivada).

Una vez hecho esto, implementa completamente las tres clases (con todos sus atributos y métodos) y utilízalas en un pequeño programa de ejemplo que cree un objeto de tipo Alumno y otro de tipo Profesor, los rellene con información y muestre esa información en la pantalla a través del método mostrar.

Solución
Dado que el método mostrarDatos no va a ser implementado en la clase Persona, será declarado como abstracto y no se incluirá su implementación:

protected abstract void mostrarDatos ();

Recuerda que el simple hecho de que la clase Persona contenga un método abstracto hace que la clase sea abstracta (y deberá indicarse como tal en su declaración):

public abstract class Persona {
...

En el caso de la clase Alumno habrá que hacer una implementación específica del método mostrarDatos y lo mismo para el caso de la clase Profesor.

1) Método mostrarDatos para la clase Alumno:

@Override
public void mostrarDatos() {
    DateTimeFormatter formatoFecha = DateTimeFormatter.ofPattern("dd/MM/yyyy");
    String stringFecha = formatoFecha.format(this.fechaNacimiento);

    System.out.printf ("%-18s%s\n", "Nombre:", this.nombre);
    System.out.printf ("%-18s%s\n", "Apellidos:", this.apellidos);
    System.out.printf ("%-18s%s\n", "Fecha nacimiento:", stringFecha);
    // A continuación mostramos la información "especializada" de esta subclase
    System.out.printf ("%-18s%s\n", "Grupo:", this.grupo);
    System.out.printf ("%-18s%-5.2f\n", "Nota media:", this.notaMedia);    
}

2) Método mostrarDatos para la clase Profesor:

@Override
public void mostrarDatos() {
    DateTimeFormatter formatoFecha = DateTimeFormatter.ofPattern("dd/MM/yyyy");
    String stringFecha = formatoFecha.format(this.fechaNacimiento);

    System.out.printf ("%-18s%s\n", "Nombre:", this.nombre);
    System.out.printf ("%-18s%s\n", "Apellidos:", this.apellidos);
    System.out.printf ("%-18s%s\n", "Fecha nacimiento:", stringFecha);
    // A continuación mostramos la información "especializada" de esta subclase
    System.out.printf ("%-18s%s\n", "Especialidad:", this.especialidad);
    System.out.printf ("%-18s%-7.2f €\n", "Salario:", this.salario);
}

3) Un pequeño programa de ejemplo de uso del método mostrar en estas dos clases podría ser:

import java.time.LocalDate;

public class EjemploUso {

    public static void main(String[] args) {
        // Declaración de objetos
        Alumno alumno;
        Profesor profesor;

        // Creación de objetos (llamada a constructores)
        alumno = new Alumno("Juan", "Torres", LocalDate.of(1990, 10, 6), "1DAW", 7.5);

        profesor = new Profesor("Antonio", "Campos", LocalDate.of(1970, 8, 15), "Informatica", 1750);

        // Utilización del método mostrar
        alumno.mostrarDatos();
        System.out.println();
        profesor.mostrarDatos();
    }
}

La salida debe ser algo parecido a esto:

Nombre:           Juan
Apellidos:        Torres
Fecha nacimiento: 6/10/1990
Grupo:            1DAW
Nota media:       7,50

Nombre:           Antonio
Apellidos:        Campos
Fecha nacimiento: 15/08/1970
Especialidad:     Informatica
Salario:          1750,00 

3. Clases y métodos finales

En unidades anteriores has visto el modificador final, aunque sólo lo has utilizado por ahora para atributos y variables (por ejemplo para declarar atributos constantes, que una vez que toman un valor ya no pueden ser modificados). Pero este modificador también puede ser utilizado con clases y con métodos (con un comportamiento que no es exactamente igual, aunque puede encontrarse cierta analogía: no se permite heredar o no se permite redefinir).

3.1. Clases final

Una clase declarada como final no puede ser heredada, es decir, no puede tener clases derivadas. La jerarquía de clases a la que pertenece acaba en ella (no tendrá clases hijas):

[modificador_acceso] final class nombreClase [herencia] [interfaces]

3.2. Métodos final

Un método también puede ser declarado como final, en tal caso, ese método no podrá ser redefinido en una clase derivada:

[modificador_acceso] final <tipo> <nombreMetodo> ([parámetros]) [excepciones]

Si intentas redefinir un método final en una subclase se producirá un error de compilación.

Distintos contextos en los que puede aparecer el modificador final:

Lugar Función
Como modificador de clase. La clase no puede tener subclases.
Como modificador de atributo. El atributo no podrá ser modificado una vez que tome un valor. Sirve para definir constantes.
Como modificador al declarar un método El método no podrá ser redefinido en una clase derivada.
Como modificador al declarar una variable referencia. Una vez que la variable tome un valor referencia (un objeto), no se podrá cambiar. La variable siempre apuntará al mismo objeto, lo cual no quiere decir que ese objeto no pueda ser modificado internamente a través de sus métodos. Pero la variable no podrá apuntar a otro objeto diferente.
Como modificador en un parámetro de un método El valor del parámetro (ya sea un tipo primitivo o una referencia) no podrá modificarse dentro del código del método.

Veamos un ejemplo de cada posibilidad:

Ejemplo 4.03: Modificador de una clase
1
2
3
public final class ClaseSinDescendencia { // Clase "no heredable"
...
}
Ejemplo 4.04: Modificador de un atributo
1
2
3
public class ClaseEjemplo {
// Valor constante conocido en tiempo de compilación
final double PI = 3.14159265;

// Valor constante conocido solamente en tiempo de ejecución final int SEMILLA = (int) Math.random()*10+1; ... } ```

Ejemplo 4.05: Modificador de un método
1
2
3
public final metodoNoRedefinible (int parametro1) { // Método "no redefinible"
    ...
} 
Ejemplo 4.06: Modificador en una variable referencia
1
2
3
// Referencia constante: siempre se apuntará al mismo objeto Alumno
// recién creado, aunque este objeto pueda sufrir modificaciones.
final Alumno PRIMER_ALUMNO = new Alumno ("Pepe", "Torres", 9.55);

// Si la variable no es una referencia (tipo primitivo), // sería una constante más (como un atributo constante). final int NUMERO_DIEZ = 10; // Valor constante (dentro del ámbito de vida de la variable) ```

Ejemplo 4.07: Modificador en un parámetro de un método
1
2
3
4
5
void metodoConParametrosFijos (final int par1, final int par2) {
    // Los parámetros "par1" y "par2" no podrán
    // sufrir modificaciones aquí dentro
    ...
}