Perché molte classi Collection in Java estendono la class astratta e implementano anche l’interfaccia?

Perché molte classi Collection in Java estendono la class Abstract e implementano anche l’interfaccia (che viene implementata anche dalla class astratta fornita)?

Ad esempio, la class HashSet estende AbstractSet e implementa anche Set , ma AbstractSet implementa già Set .

È un modo per ricordare che questa class implementa davvero quell’interfaccia.
Non avrà alcun effetto negativo e può aiutare a capire il codice senza passare attraverso la gerarchia completa della class data.

Dal punto di vista del sistema di tipi le classi non sarebbero diverse se non implementassero nuovamente l’interfaccia, poiché le classi di base astratte le implementano già.

Questo è vero.

La ragione per cui lo implementano comunque è (probabilmente) principalmente documentazione: un HashSet è un Set . E ciò è reso esplicito dall’aggiunta di implements Set alla fine, sebbene non sia strettamente necessario.

Si noti che la differenza è effettivamente osservabile usando il reflection, ma sarei costretto a produrre codice che si interromperebbe se HashSet non implementasse Set direttamente.

Ciò potrebbe non avere molta importanza nella pratica, ma volevo chiarire che implementare esplicitamente un’interfaccia non è esattamente la stessa cosa che implementarla per via ereditaria. La differenza è presente nei file di class compilati e visibile tramite riflessione. Per esempio,

 for (Class c : ArrayList.class.getInterfaces()) System.out.println(c); 

L’output mostra solo le interfacce esplicitamente implementate da ArrayList , nell’ordine in cui sono state scritte nell’origine, che [sulla mia versione Java] è:

 interface java.util.List interface java.util.RandomAccess interface java.lang.Cloneable interface java.io.Serializable 

L’output non include le interfacce implementate dalle superclassi o le interfacce che sono superinterfacce di quelle che sono incluse. In particolare, Iterable e Collection mancano di quanto sopra, anche se ArrayList li implementa implicitamente. Per trovarli devi ripetere in modo ricorsivo la gerarchia delle classi.

Sarebbe sfortunato se qualche codice là fuori usasse la riflessione e dipendesse dall’interpretazione esplicita delle interfacce, ma è ansible, così i manutentori della libreria delle collezioni potrebbero essere riluttanti a cambiarlo ora, anche se lo volessero. (C’è un’osservazione chiamata la legge di Hyrum : “Con un numero sufficiente di utenti di un’API, non importa ciò che prometti nel contratto, tutti i comportamenti osservabili del tuo sistema dipenderanno da qualcuno”).

Fortunatamente questa differenza non influenza il sistema di tipi. Le espressioni new ArrayList<>() instanceof Iterable e Iterable.class.isAssignableFrom(ArrayList.class) ancora true .

A differenza di Colin Hebert , non comprendo che le persone che scrivevano tenessero alla leggibilità. (Tutti quelli che pensano che le librerie Java standard siano state scritte da dei impeccabili, dovrebbero dare un’occhiata alle loro fonti: la prima volta che l’ho fatto sono rimasto inorridito dalla formattazione del codice e da numerosi blocchi copiati).

La mia scommessa è che era tardi, erano stanchi e non importava in nessun modo.

Da “Effective Java” di Joshua Bloch:

È ansible combinare i vantaggi delle interfacce e delle classi astratte aggiungendo una class di implementazione scheletrica astratta per andare con un’interfaccia.

L’interfaccia definisce il tipo, fornendo forse alcuni metodi predefiniti, mentre la class scheletrica implementa i restanti metodi di interfaccia non primitivi in ​​cima ai metodi di interfaccia primitivi. L’estensione di un’implementazione scheletrica richiede la maggior parte del lavoro dall’implementazione di un’interfaccia. Questo è il modello Metodo del modello.

Per convenzione, le classi di implementazione scheletriche sono chiamate AbstractInterface dove Interface è il nome dell’interfaccia che implementano. Per esempio:

 AbstractCollection AbstractSet AbstractList AbstractMap 

Credo anche che sia per chiarezza. Il framework di collezioni Java ha una gerarchia di interfacce che definisce i diversi tipi di raccolta. Inizia con l’interfaccia Collection, quindi estesa da tre sottointerfacce principali Set, List e Queue. Esiste anche SortedSet che estende Set e BlockingQueue Estending Queue.

Ora, le classi concrete che li implementano sono più comprensibili se affermano esplicitamente quale interfaccia nella gerarchia sta implementando, anche se a volte può sembrare ridondante. Come hai detto, una class come HashSet implementa Set ma una class come TreeSet sebbene estenda anche AbstractSet implementa SortedSet invece che è più specifico del solo Set. HashSet può sembrare ridondante ma TreeSet non lo è perché richiede l’implementazione di SortedSet. Tuttavia, entrambe le classi sono implementazioni concrete e sarebbero più comprensibili se entrambe seguissero certe convenzioni nella loro dichiarazione.

Esistono anche classi che implementano più di un tipo di raccolta come LinkedList che implementa sia la lista che la coda. Tuttavia, esiste almeno una class che è un po ‘”non convenzionale”, il PriorityQueue. Estende AbstractQueue ma non implementa esplicitamente la coda. Non chiedermi il perché. 🙂

(il riferimento è da API Java 5)

Dal mio punto di vista, quando una class implementa un’interfaccia, deve implementare tutti i metodi presenti in essa (come di default sono metodi pubblici e astratti in un’interfaccia).

Se non vogliamo implementare tutti i metodi di interfaccia, deve essere una class astratta.

Quindi, se alcuni metodi sono già stati implementati in alcune classi astratte che implementano un’interfaccia particolare e dobbiamo estendere la funzionalità per altri metodi che non sono stati implementati, avremo bisogno di implementare l’interfaccia originale nella nostra class per ottenere quei restanti set di metodi. nel mantenimento delle regole contrattuali stabilite da un’interfaccia.

Risulterà in una rilavorazione se dovessimo implementare solo l’interfaccia e di nuovo sovrascrivere tutti i metodi con le definizioni dei metodi nella nostra class.

Troppo tardi per la risposta?

Sto facendo una supposizione per convalidare la mia risposta. Assumi il seguente codice

HashMap extends AbstractMap (non implementa Map)

AbstractMap implements Map

Ora immagina che sia arrivato un tizio a caso, ha cambiato la mappa delle implementazioni con alcuni java.util.Map1 con lo stesso identico insieme di metodi di Map

In questa situazione non ci saranno errori di compilazione e jdk verrà compilato (il test fuori rotta fallirà e prenderà questo).

Ora qualsiasi client che utilizza HashMap come Map m = new HashMap () inizierà a fallire. Questo è molto a valle.

Dal momento che sia AbstractMap, Map etc proviene dallo stesso prodotto, quindi questo argomento appare infantile (che con ogni probabilità è o potrebbe non esserlo), ma pensate a un progetto in cui la class base proviene da un altro jar / libreria di terze parti ecc. Quindi terze parti / team diversi possono cambiare la loro implementazione di base.

Implementando “l’interfaccia” nella class Child, anche gli sviluppatori cercano di rendere la class autosufficiente, a prova di rottura dell’API.

Suppongo che ci possa essere un modo diverso di gestire i membri del set, l’interfaccia, anche se fornire l’implementazione delle operazioni di default, non serve come soluzione unica. Una coda circolare rispetto alla coda LIFO potrebbe implementare la stessa interfaccia, ma le loro operazioni specifiche saranno implementate in modo diverso, giusto?

Se tu avessi solo una class astratta non potresti creare una tua class che erediti anche da un’altra class.