Entendiendo las Clases Abstractas en Java
Las clases abstractas se utilizan cuando quieres tener una base que implemente el comportamiento general de un grupo de clases, con el objetivo de no repetir la implementación en cada una de las clases pertenecientes al grupo, pero al mismo tiempo quieres dejar abiertas las partes que son específicas a cada clase.
Las clases abstractas son muy similares a las clases concretas de Java.
Para definir una clase abstracta, utilizamos la palabra reservada abstract
(Que en inglés significa abstracto o abstracta).
La forma más simple de una clase abstracta es:
abstract class ClaseAbstracta {
}
Aunque la clase anterior pasaría la compilación sin problemas, no es de mucha ayuda, dado que está vacía. Un ejemplo más práctico de una clase abstracta sería, por ejemplo, una clase que represente las figuras geométricas:
1. abstract class Figura {
2. private String nombreFigura;
3.
4. public void setNombre(String nombreFigura) {
5. this.nombreFigura = nombreFigura;
6. }
7.
8. public String getNombre() {
9. return this.nombreFigura;
10. }
11.
12. public abstract double calcularArea();
13. }
Como podrás ver en las líneas 4 a la 6, defino e implemento un método para especificar el nombre de la figura,
y en las líneas 8 a la 10, defino e implemento un método para obtener el nombre de la figura.
Estos métodos van a ser iguales en todas las clases de figuras geométricas que implementen la clase abstracta Figura
,
por lo que podemos implementarlos de una vez al nivel de la clase abstracta,
de esta forma nos ahorraremos implementarlos en todas las clases hijas.
Por otro lado, la forma en la que podemos calcular el área de una figura geométrica
depende completamente de la figura de la que estemos hablando,
por lo tanto, no podemos implementar el método al nivel de la clase abstracta,
por eso definimos el método calcularArea()
como tipo abstract
, lo puedes ver en la línea 12.
Cada clase concreta que extienda la clase Figura, estará obligada a ofrecer una implementación del método calcularArea(), veamos un ejemplo:
1. public class Circulo extends Figura {
2. private final double PI = 3.14159;
3. private final double radio;
4.
5. public Circulo(double radio) {
6. this.radio = radio;
7. }
8.
9. @Override
10. public double calcularArea() {
11. return PI * radio * radio;
12. }
13. }
Puedes ver en el ejemplo de aquí arriba, que la forma de heredar una clase abstracta
es igual que como lo hacemos con las clases normales.
A diferencia de las interfaces, utilizamos la palabra reservada extends
en lugar de implements
.
La diferencia más notable entre la herencia de una clase normal y la herencia una clase abstracta, es que normalmente vamos a tener métodos que son abstractos definidos en la clase abstracta, los cuales tenemos que sobrescribir en la clase hija, de hecho, estamos obligados a sobrescribir los métodos abstractos si la clase hija es una clase concreta.
También es importante notar que los métodos abstractos solo pueden ser definidos dentro de las clases abstractas, de lo contrario, Java nos daría un error de compilación, como en el siguiente ejemplo:
public class Auto {
// Error, sólo podemos definir métodos abstractos en clases abstractas
public abstract void acelerar();
}
Heredando una Clase Abstracta Desde otra Clase Abstracta
Las clases que heredan de las clases abstractas, no tienen que ser necesariamente clases concretas, también una clase abstracta puede heredar de otra clase abstracta:
abstract class ClasePadre {
public abstract void decirHola();
}
abstract class ClaseHija {
public abstract int hacerCalculo(int num);
}
Una clase abstracta que hereda de otra clase abstracta, no está obligada a implementar los métodos abstractos que hayamos definido en la clase abstracta padre, pero sí los hereda, de la misma forma que funciona la herencia de clases concretas.
Siguiendo el ejemplo anterior, si ahora utilizamos una clase concreta para heredar de la clase abstracta ClaseHija
,
deberemos implementar ambos métodos: decirHola()
y hacerCalculo(int num)
:
public class ClaseConcreta extends ClaseHija {
@Override
public void decirHola() {
System.out.println("¡Hola!");
}
@Override
public int hacerCalculo(int num) {
return num + 1;
}
}
Únicamente las clases concretas que hereden de clases abstractas, estarán obligadas a implementar los métodos abstractos definidos en la cadena de herencia.
Una clase abstracta también puede heredar de otra clase abstracta, si este es el caso, la clase hija no está obligada a implementar los métodos abstractos definidos en la clase padre.
Implementando Métodos Concretos en Clases Abstractas
Una clase abstracta también puede contener métodos concretos, el siguiente ejemplo es completamente válido para el compilador:
public abstract class MiClaseAbstracta {
public void decirAlgo() {
System.out.println("Hola");
}
}
Aunque podemos crear una clase como la de aquí arriba, no es de gran utilidad,
porque una clase abstracta no puede ser instanciada,
o sea, no podemos crear un objeto que sea de tipo MiClaseAbstracta
.
¿Por qué querríamos implementar métodos en una clase abstracta?, pues para agregar funciones que sabemos tienen una implementación muy definida, que no varía de clase a clase (como lo vimos anteriormente en este artículo), pero también queremos incluir métodos abstractos, donde el comportamiento sí varía.
Constructores en Clases Abstractas
Una clase abstracta puede incluir constructores definidos por el programador, de hecho, si no incluimos uno o más constructores manualmente, Java se encargará de incluir un constructor por defecto, tal cual lo hace en las clases concretas.
Considera el siguiente ejemplo de una clase abstracta:
public abstract class Mamifero {
public Mamifero() {
System.out.println("Me alimento de leche materna");
}
}
Luego, creamos una clase concreta que herede de la clase anterior:
public class Oso extends Mamifero {
public void pescar() {
System.out.println("Pescando un salmón");
}
}
Al momento en que instanciemos la clase Oso, como no definimos un constructor manualmente para la clase concreta, Java crea un constructor por defecto, que internamente llamará al constructor de la clase padre:
var osito = new Oso(); // Imprime: "Me alimento de leche materna"
Consideraciones con final
, private
, y static
al utilizar abstract
Cuando definimos un método abstracto, no tiene sentido que también lo especifiquemos como final
,
porque la intención detrás de un método abstracto es que sea implementado por alguna clase hija.
Por esto mismo, Java arroja un error de compilación si intentas marcar un método como abstract
y final
al mismo tiempo.
Algo similar ocurre cuando utilizas abstract
y private
,
porque cuando intentes implementar el método abstracto en alguna clase hija,
la clase hija no tiene visibilidad de los métodos privados de las clases padre en la jerarquía de clases.
Al igual que con la combinación de abstract
y final
,
Java arrojará un error de compilación cuando encuentre que se utilizan las palabras reservadas abstract
y private
al mismo tiempo.
Por último, también debes tener cuidado con la palabra reservada static
cuando estés definiendo un método abstracto,
como tal vez sabes, cuando defines un método estático en una clase,
le estás diciendo a Java que el método podrá ser invocado,
sin necesidad de crear una instancia de la clase que contiene el método estático.
Como ya vimos, por definición un método abstracto no contiene ninguna implementación,
y debido a esto no podríamos llamar al método utilizando el nombre de la clase abstracta.
Está de más decir que Java también arroja un error de compilación cuando encuentra que utilizas las palabras reservadas abstract
y static
al mismo tiempo.
A continuación, unos ejemplos donde Java nos daría error de compilación:
public abstract class Calculadora {
// Error, final y abstract no pueden ser usados al mismo tiempo
public final abstract long sumar(int a, int b);
// Error, private y abstract no pueden ser usados al mismo tiempo
private abstract long restar(int a, int b);
// Error, static y abstract no pueden ser usados al mismo tiempo
public static abstract long multiplicar(int a, int b);
}
Conclusión
Las clases abstractas en Java asumen un rol fundamental al abordar la necesidad de implementar comportamientos generales que aplican a grupos de clases interconectadas. A través de este artículo, hemos explorado de manera exhaustiva la estructura, la utilidad y la implementación de clases abstractas.
Cuando defines una clase abstracta, estableces una base sólida que te permite encapsular comportamientos compartidos entre clases. Esta técnica, particularmente valiosa en situaciones donde la repetición de la implementación se debe evitar, promueve la coherencia y la reutilización del código. La clave reside en la habilidad de establecer métodos concretos que mantienen una implementación constante y métodos abstractos que permanecen abiertos para personalización en clases hijas.
El ejemplo de las figuras geométricas demuestra con claridad cómo una clase abstracta puede ser diseñada
para contener métodos concretos compartidos entre las clases hijas.
La implementación del método calcularArea()
en cada figura específica, como el círculo,
ilustra la adaptabilidad que se logra al permitir a las subclases definir el comportamiento específico.
A través de este recorrido, hemos aprendido que las clases abstractas permiten una jerarquía de herencia más flexible. Las subclases pueden heredar métodos concretos y métodos abstractos, lo que proporciona un marco sólido para la personalización y la especialización de comportamientos.
No obstante, al explorar las posibles combinaciones de modificadores, como final
, private
y static
,
hemos comprendido que la coexistencia de ciertos modificadores puede generar errores de compilación.
El uso adecuado y coherente de estos modificadores en el contexto de clases abstractas
es esencial para un diseño eficiente y un código funcional.
En conclusión, la comprensión profunda de las clases abstractas en Java ofrece a los programadores una herramienta valiosa para crear jerarquías de herencia estructuradas, flexibles y eficaces. Al dominar el uso de las clases abstractas, los desarrolladores pueden construir sistemas modulares, reutilizables y altamente personalizables que cumplen con los rigurosos estándares de la programación orientada a objetos en Java.
Referencias
Selikoff, S. & Boyarsky, J. (2022). OCA: Oracle Certified Professional Java SE 17 Developer I Study Guide: Exam 1Z0-829. Hoboken.