Classi astratte in Java

1. Panoramica

Ci sono molti casi in cui si implementa un contratto in cui si desidera posticipare alcune parti dell'implementazione per essere completate in seguito. Possiamo facilmente farlo in Java tramite classi astratte.

In questo tutorial impareremo le basi delle classi astratte in Java e in quali casi possono essere utili .

2. Concetti chiave per le classi astratte

Prima di approfondire quando utilizzare una classe astratta, diamo un'occhiata alle loro caratteristiche più rilevanti :

  • Definiamo una classe astratta con il modificatore abstract che precede la parola chiave class
  • Una classe astratta può essere sottoclasse, ma non può essere istanziata
  • Se una classe definisce uno o più metodi astratti , la classe stessa deve essere dichiarata astratta
  • Una classe astratta può dichiarare metodi sia astratti che concreti
  • Una sottoclasse derivata da una classe astratta deve implementare tutti i metodi astratti della classe base o essere astratta essa stessa

Per comprendere meglio questi concetti, creeremo un semplice esempio.

Facciamo in modo che la nostra classe astratta di base definisca l'API astratta di un gioco da tavolo:

public abstract class BoardGame { //... field declarations, constructors public abstract void play(); //... concrete methods }

Quindi, possiamo creare una sottoclasse che implementa il metodo di riproduzione :

public class Checkers extends BoardGame { public void play() { //... implementation } }

3. Quando utilizzare le classi astratte

Ora, analizziamo alcuni scenari tipici in cui dovremmo preferire classi astratte rispetto a interfacce e classi concrete:

  • Vogliamo incapsulare alcune funzionalità comuni in un unico luogo (riutilizzo del codice) che più sottoclassi correlate condivideranno
  • Dobbiamo definire parzialmente un'API che le nostre sottoclassi possano facilmente estendere e perfezionare
  • Le sottoclassi devono ereditare uno o più metodi o campi comuni con modificatori di accesso protetto

Teniamo presente che tutti questi scenari sono buoni esempi di piena adesione basata sull'ereditarietà al principio Aperto / Chiuso.

Inoltre, poiché l'uso di classi astratte si occupa implicitamente di tipi e sottotipi di base, stiamo anche sfruttando il polimorfismo.

Si noti che il riutilizzo del codice è un motivo molto convincente per utilizzare classi astratte, a condizione che la relazione "è-a" all'interno della gerarchia di classi sia preservata.

E Java 8 aggiunge un'altra ruga con i metodi predefiniti, che a volte possono sostituire la necessità di creare del tutto una classe astratta.

4. Una gerarchia di esempio di lettori di file

Per comprendere più chiaramente la funzionalità che le classi astratte portano in tavola, diamo un'occhiata a un altro esempio.

4.1. Definizione di una classe astratta di base

Quindi, se volessimo avere diversi tipi di lettori di file, potremmo creare una classe astratta che incapsula ciò che è comune alla lettura di file:

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); }

Nota che abbiamo protetto filePath in modo che le sottoclassi possano accedervi se necessario. Ancora più importante, abbiamo lasciato qualcosa di incompiuto: come analizzare effettivamente una riga di testo dal contenuto del file.

Il nostro piano è semplice: mentre le nostre classi concrete non hanno ognuna un modo speciale per memorizzare il percorso del file o attraversare il file, ognuna avrà un modo speciale per trasformare ogni riga.

A prima vista, BaseFileReader potrebbe sembrare inutile. Tuttavia, è la base di un design pulito e facilmente estendibile. Da esso, possiamo facilmente implementare diverse versioni di un lettore di file che possono concentrarsi sulla loro logica di business unica .

4.2. Definizione di sottoclassi

Un'implementazione naturale è probabilmente quella che converte il contenuto di un file in minuscolo:

public class LowercaseFileReader extends BaseFileReader { public LowercaseFileReader(Path filePath) { super(filePath); } @Override public String mapFileLine(String line) { return line.toLowerCase(); } }

Oppure un altro potrebbe essere quello che converte il contenuto di un file in maiuscolo:

public class UppercaseFileReader extends BaseFileReader { public UppercaseFileReader(Path filePath) { super(filePath); } @Override public String mapFileLine(String line) { return line.toUpperCase(); } }

Come possiamo vedere da questo semplice esempio, ogni sottoclasse può concentrarsi sul suo comportamento unico senza dover specificare altri aspetti della lettura del file.

4.3. Utilizzo di una sottoclasse

Infine, l'utilizzo di una classe che eredita da una astratta non è diverso da qualsiasi altra classe 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); }

Per semplicità, il file di destinazione si trova nella cartella src / main / resources / files . Quindi, abbiamo utilizzato un programma di caricamento classi dell'applicazione per ottenere il percorso del file di esempio. Sentiti libero di dare un'occhiata al nostro tutorial sui caricatori di classi in Java.

5. conclusione

In questo rapido articolo, abbiamo appreso le basi delle classi astratte in Java e quando usarle per ottenere l'astrazione e incapsulare l'implementazione comune in un unico posto .

Come al solito, tutti gli esempi di codice mostrati in questo tutorial sono disponibili su GitHub.