1. Información general
Hay muchos casos al implementar un contrato en los que queremos posponer algunas partes de la implementación para completarlas más tarde. Podemos lograr esto fácilmente en Java a través de clases abstractas.
En este tutorial, aprenderemos los conceptos básicos de las clases abstractas en Java y en qué casos pueden ser útiles .
2. Conceptos clave para clases abstractas
Antes de sumergirnos en cuándo usar una clase abstracta, veamos sus características más relevantes :
- Definimos una clase abstracta con el modificador abstracto antes de la palabra clave de clase.
- Una clase abstracta se puede subclasificar, pero no se puede crear una instancia
- Si una clase define uno o más métodos abstractos , entonces la clase en sí debe declararse abstracta
- Una clase abstracta puede declarar métodos tanto abstractos como concretos
- Una subclase derivada de una clase abstracta debe implementar todos los métodos abstractos de la clase base o ser abstracta ella misma
Para comprender mejor estos conceptos, crearemos un ejemplo simple.
Hagamos que nuestra clase abstracta base defina la API abstracta de un juego de mesa:
public abstract class BoardGame { //... field declarations, constructors public abstract void play(); //... concrete methods }
Luego, podemos crear una subclase que implemente el método play :
public class Checkers extends BoardGame { public void play() { //... implementation } }
3. Cuándo utilizar clases abstractas
Ahora, analicemos algunos escenarios típicos en los que deberíamos preferir clases abstractas sobre interfaces y clases concretas:
- Queremos encapsular algunas funciones comunes en un solo lugar (reutilización de código) que compartirán múltiples subclases relacionadas.
- Necesitamos definir parcialmente una API que nuestras subclases puedan extender y refinar fácilmente
- Las subclases deben heredar uno o más métodos o campos comunes con modificadores de acceso protegidos
Tengamos en cuenta que todos estos escenarios son buenos ejemplos de adherencia total, basada en herencia, al principio Abierto / Cerrado.
Además, dado que el uso de clases abstractas trata implícitamente con tipos y subtipos básicos, también estamos aprovechando el polimorfismo.
Tenga en cuenta que la reutilización de código es una razón muy convincente para usar clases abstractas, siempre que se mantenga la relación "es-a" dentro de la jerarquía de clases.
Y Java 8 agrega otra arruga con los métodos predeterminados, que a veces pueden reemplazar la necesidad de crear una clase abstracta por completo.
4. Una jerarquía de muestra de lectores de archivos
Para comprender más claramente la funcionalidad que aportan las clases abstractas, veamos otro ejemplo.
4.1. Definición de una clase abstracta base
Entonces, si quisiéramos tener varios tipos de lectores de archivos, podríamos crear una clase abstracta que encapsule lo que es común a la lectura de archivos:
public abstract class BaseFileReader { protected Path filePath; protected BaseFileReader(Path filePath) { this.filePath = filePath; } public Path getFilePath() { return filePath; } public List readFile() throws IOException { return Files.lines(filePath) .map(this::mapFileLine).collect(Collectors.toList()); } protected abstract String mapFileLine(String line); }
Tenga en cuenta que hemos hecho que filePath esté protegido para que las subclases puedan acceder a él si es necesario. Más importante aún, hemos dejado algo sin hacer: cómo analizar realmente una línea de texto del contenido del archivo.
Nuestro plan es simple: si bien nuestras clases concretas no tienen una forma especial de almacenar la ruta del archivo o recorrer el archivo, cada una tendrá una forma especial de transformar cada línea.
A primera vista, BaseFileReader puede parecer innecesario. Sin embargo, es la base de un diseño limpio y fácilmente ampliable. A partir de él, podemos implementar fácilmente diferentes versiones de un lector de archivos que pueden enfocarse en su lógica comercial única .
4.2. Definición de subclases
Una implementación natural es probablemente aquella que convierte el contenido de un archivo a minúsculas:
public class LowercaseFileReader extends BaseFileReader { public LowercaseFileReader(Path filePath) { super(filePath); } @Override public String mapFileLine(String line) { return line.toLowerCase(); } }
U otro podría ser uno que convierta el contenido de un archivo a mayúsculas:
public class UppercaseFileReader extends BaseFileReader { public UppercaseFileReader(Path filePath) { super(filePath); } @Override public String mapFileLine(String line) { return line.toUpperCase(); } }
Como podemos ver en este ejemplo simple, cada subclase puede enfocarse en su comportamiento único sin necesidad de especificar otros aspectos de la lectura de archivos.
4.3. Usando una subclase
Finalmente, usar una clase que hereda de una abstracta no es diferente a cualquier otra clase concreta:
@Test public void givenLowercaseFileReaderInstance_whenCalledreadFile_thenCorrect() throws Exception { URL location = getClass().getClassLoader().getResource("files/test.txt") Path path = Paths.get(location.toURI()); BaseFileReader lowercaseFileReader = new LowercaseFileReader(path); assertThat(lowercaseFileReader.readFile()).isInstanceOf(List.class); }
En aras de la simplicidad, el archivo de destino se encuentra en la carpeta src / main / resources / files . Por lo tanto, usamos un cargador de clases de aplicaciones para obtener la ruta del archivo de ejemplo. No dude en consultar nuestro tutorial sobre cargadores de clases en Java.
5. Conclusión
En este artículo rápido, aprendimos los conceptos básicos de las clases abstractas en Java y cuándo usarlas para lograr la abstracción y encapsular la implementación común en un solo lugar .
Como de costumbre, todos los ejemplos de código que se muestran en este tutorial están disponibles en GitHub.