Perché le raccolte Java sono state implementate con "metodi opzionali" nell'interfaccia?


69

Durante la mia prima implementazione che ha esteso il framework di raccolta Java, sono rimasto piuttosto sorpreso nel vedere che l'interfaccia di raccolta contiene metodi dichiarati opzionali. L'implementatore dovrebbe generare UnsupportedOperationExceptions se non supportato. Questo mi ha subito colpito come una cattiva scelta di progettazione API.

Dopo aver letto gran parte dell'eccellente libro "L'efficace Java" di Joshua Bloch, e dopo aver appreso che potrebbe essere responsabile di queste decisioni, non sembrava gelare con i principi sposati nel libro. Penserei di dichiarare due interfacce: Collection e MutableCollection che estende Collection con i metodi "opzionali" avrebbe portato a un codice client molto più gestibile.

C'è un eccellente riassunto dei problemi qui .

C'è stata una buona ragione per cui sono stati scelti metodi opzionali anziché l'implementazione delle due interfacce?


IMO, non c'è una buona ragione. Il C ++ STL è stato creato da Stepanov nei primi anni '80. Sebbene la sua usabilità sia stata limitata dalla scomoda sintassi del modello C ++, è un esempio di coerenza e usabilità rispetto alle classi di raccolta Java.
Kevin Cline,

Esisteva un porting STL C ++ su Java. Ma era 5 volte più grande nel conteggio delle classi. Con questa mente non lo compro che quello più grande è più coerente e utilizzabile. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman

@ m3th0dman È molto più grande in Java perché Java non ha nulla di equivalente in potenza ai modelli C ++.
Kevin Cline,

Forse è solo uno stile strano che ho sviluppato, ma il mio codice tende a trattare tutte le raccolte come "sola lettura", (più precisamente, solo qualcosa su cui contate o su cui ripetete) tranne i pochi metodi che creano effettivamente le raccolte. Probabilmente buone pratiche comunque (specialmente per la concorrenza). E i metodi opzionali di cui molti si lamentano non sono mai stati un vero problema per me. Né gli incubi di Generics con "super" e "extends" non sono mai stati (molto) un problema. Ti stai solo chiedendo se gli altri usano questa pratica generale?
user949300,

Risposte:


28

Le FAQ forniscono la risposta. In breve, hanno visto una potenziale esplosione combinatoria di interfacce necessarie con vista modificabile, non modificabile, solo cancellazione, solo aggiunta, lunghezza fissa, immutabile (per il threading) e così via per ogni possibile serie di metodi di opzione implementati.


6
Tutto ciò sarebbe stato evitato se Java avesse una constparola chiave come C ++.
Etienne de Martel,

@Etienne: Meglio sarebbe metaclassi come Python. Quindi è possibile creare a livello di codice il numero combinatorio di interfacce. Il problema con const è che ti dà solo una o due dimensioni: vector<int>, const vector<int>, vector<const int>, const vector<const int>. Fin qui tutto bene, ma poi provi a implementare i grafici e vuoi rendere costante la struttura del grafico, ma gli attributi del nodo sono modificabili, ecc.
Neil G

2
@Etienne, un altro motivo per aggiungere "Learn Scala" alla mia lista di cose da fare!
glenviewjeff,

2
Quello che non capisco è, perché non hanno creato un canmetodo per verificare se un'operazione è possibile? Manterrebbe l'interfaccia semplice e veloce.
Mehrdad,

3
@Etienne de Martel Nonsense. Come sarebbe d'aiuto nell'esplosione combinatoria?
Tom Hawtin - tackline

10

Mi sembra che Interface Segregation Principlenon fosse così esplorato allora come lo è ora; quel modo di fare le cose (cioè la tua interfaccia include tutte le operazioni possibili e hai metodi "degenerati" che generano eccezioni per quelli che non ti servono) era popolare prima che SOLID e ISP diventassero lo standard di fatto per il codice di qualità.


2
Perché un downvote? A qualcuno non piace l'ISP?
Wayne Molina,

Vale anche la pena notare che nei framework che supportano la varianza c'è un ENORME vantaggio nel separare quegli aspetti di un'interfaccia che possono essere covarianti o contraddittori da quelli che sono fondamentalmente invarianti. Anche in assenza di tale supporto, nei framework che non utilizzano la cancellazione del tipo, sarebbe utile segregare gli aspetti di un'interfaccia che sono indipendenti dal tipo (ad es. Consentire di ottenere la Countraccolta senza doversi preoccupare dei tipi di elementi che contiene), ma in framework basati sulla cancellazione di tipi come Java non è un problema del genere.
supercat

4

Mentre alcune persone potrebbero detestare i "metodi opzionali", in molti casi possono offrire una semantica migliore rispetto alle interfacce altamente segregate. Tra le altre cose, consentono le possibilità che un oggetto possa acquisire abilità o caratteristiche durante la sua vita, o che un oggetto (specialmente un oggetto wrapper) potrebbe non sapere quando è costruito quali abilità esatte dovrebbe riferire.

Mentre difficilmente chiamerò le classi di raccolta Java paragoni di buon design, suggerirei che un buon framework di raccolte dovrebbe includere alla sua base un gran numero di metodi opzionali insieme a modi di chiedere a una collezione le sue caratteristiche e abilità . Un tale design consentirà di utilizzare una singola classe wrapper con una grande varietà di collezioni senza oscurare accidentalmente le capacità che la collezione sottostante potrebbe possedere. Se i metodi non fossero facoltativi, sarebbe necessario disporre di una classe wrapper diversa per ogni combinazione di funzionalità che le raccolte potrebbero supportare, altrimenti alcune wrapper potrebbero essere inutilizzabili in alcune situazioni.

Ad esempio, se una raccolta supporta la scrittura di un elemento per indice o l'aggiunta di elementi alla fine, ma non supporta l'inserimento di elementi nel mezzo, il codice che desidera incapsularlo in un wrapper che registra tutte le azioni eseguite su di esso richiederebbe una versione del wrapper di logging che prevedeva la combinazione esatta di abilità supportate, o se nessuna fosse disponibile avrebbe dovuto usare un wrapper che supportava append o write-by-index ma non entrambi. Se, tuttavia, un'interfaccia di raccolta unificata forniva tutti e tre i metodi come "opzionali", ma includeva quindi metodi per indicare quale dei metodi opzionali sarebbe utilizzabile, una singola classe wrapper poteva gestire raccolte che implementano qualsiasi combinazione di caratteristiche. Quando viene chiesto quali funzioni supporta, un wrapper può semplicemente segnalare qualsiasi supporto della raccolta incapsulata.

Si noti che l'esistenza di "abilità opzionali" può in alcuni casi consentire alle raccolte aggregate di implementare determinate funzioni in modi molto più efficienti di quanto sarebbe possibile se le abilità fossero definite dall'esistenza di implementazioni. Ad esempio, supponiamo concatenateche sia stato usato un metodo per formare una raccolta composita da altri due, il primo dei quali era una ArrayList con 1.000.000 di elementi e l'ultimo dei quali era una raccolta di venti elementi che poteva essere ripetuta dall'inizio. Se fosse richiesta la raccolta composita per il 1.000.013 ° elemento (indice 1.000.012), si potrebbe chiedere all'ArrayList quanti elementi conteneva (cioè 1.000.000), sottrarre quello dall'indice richiesto (producendo 12), leggere e saltare dodici elementi dal secondo raccolta e quindi restituisce l'elemento successivo.

In una situazione del genere, anche se la raccolta composita non avrebbe un modo istantaneo di restituire un articolo per indice, chiedere alla raccolta composita per il 1.000.013 ° articolo sarebbe comunque molto più veloce che leggere 1.000.013 articoli da esso singolarmente e ignorare tutto tranne l'ultimo uno.


@kevincline: non sei d'accordo con la frase citata? Vorrei considerare la progettazione dei mezzi con cui le implementazioni dell'interfaccia possono descrivere le loro caratteristiche e capacità essenziali come uno degli aspetti più importanti della progettazione dell'interfaccia, anche se spesso non riceve molta attenzione. Se diverse implementazioni di un'interfaccia avranno diversi modi ottimali per eseguire determinate attività comuni, ci dovrebbero essere dei mezzi per i clienti che si preoccupano delle prestazioni per selezionare l'approccio migliore negli scenari in cui sarebbe importante.
supercat,

1
scusa, commento incompleto. Stavo per dire che "'modi di chiedere una raccolta' ..." sposta quello che dovrebbe essere un controllo in fase di compilazione in fase di esecuzione.
Kevin Cline,

@kevincline: nei casi in cui un cliente deve avere una certa abilità e non può vivere senza di essa, i controlli in fase di compilazione possono essere utili. D'altra parte, ci sono molte situazioni in cui le raccolte potrebbero avere abilità oltre a quelle che possono essere garantite in tempo di compilazione. In alcuni casi, può avere senso avere un'interfaccia derivata il cui contratto specifica che tutte le implementazioni legittime dell'interfaccia derivata devono supportare metodi particolari che sono facoltativi nell'interfaccia di base, ma nei casi in cui il codice sarà in grado di gestire qualcosa indipendentemente dal fatto ha qualche caratteristica ...
supercat

... ma trarrà vantaggio dalla sua funzionalità, è meglio che il codice chieda all'oggetto se supporta la funzione piuttosto che chiedere al sistema di tipi se il tipo di oggetto promette di supportare la funzione. Se una particolare interfaccia "specifica" sarà ampiamente utilizzata, si potrebbe includere un AsXXXmetodo nell'interfaccia di base che restituirà l'oggetto su cui viene invocato se implementa tale interfaccia, restituisce un oggetto wrapper che supporta tale interfaccia, se possibile, o lancia un'eccezione in caso contrario. Ad esempio, ImmutableCollectionun'interfaccia potrebbe richiedere per contratto ...
supercat

2
C'è un problema con la tua proposta: un modello "chiedi quindi fai" ha un problema di concorrenza se le capacità di un oggetto possono cambiare durante il suo ciclo di vita. (Se hai intenzione di rischiare comunque un'eccezione, potresti non preoccuparti di chiedere ...)
jhominal

-1

Lo attribuirei agli sviluppatori originali semplicemente non sapendo meglio allora. Abbiamo fatto molta strada nella progettazione di OO dal 1998 circa, quando Java 2 e le collezioni sono state rilasciate per la prima volta. Ciò che sembra evidentemente un cattivo design ora non era così evidente nei primi giorni di OOP.

Ma potrebbe essere stato fatto per evitare un casting extra. Se fosse una seconda interfaccia, dovresti trasmettere le istanze delle tue raccolte per chiamare quei metodi opzionali che sono anche un po 'brutti. Così com'è ora acquisiresti subito UnsupportedOperationException e aggiusterai il tuo codice. Ma se ci fossero due interfacce dovresti usare instanceof e lanciare ovunque. Forse hanno considerato questo un compromesso valido. Anche nei primi 2 giorni di Java, instanceof era fortemente malvisto a causa delle sue prestazioni lente, avrebbero potuto cercare di impedire un uso eccessivo di esso.

Naturalmente questa è tutta una speculazione selvaggia, dubito che potremmo mai rispondere di sicuro a meno che uno degli architetti delle collezioni originali non intervenga.


7
Quale casting? Se un metodo ti restituisce un Collectione non un MutableCollection, è un chiaro segno che non è pensato per essere modificato. Non so perché qualcuno dovrebbe lanciarli. Avere interfacce separate significa che otterrai questo tipo di errori in fase di compilazione invece di ottenere un'eccezione in fase di esecuzione. Prima ricevi l'errore, meglio è.
Etienne de Martel,

1
Poiché è molto meno flessibile, uno dei maggiori vantaggi della libreria di raccolta è che puoi restituire le interfacce di alto livello ovunque e non preoccuparti dell'implementazione effettiva. Se stai usando due interfacce, ora sei più strettamente accoppiato. Nella maggior parte dei casi si vorrebbe semplicemente restituire Elenco e non ImmutableList perché di solito si desidera lasciarlo alla classe chiamante per determinare.
Jberg,

6
Se ti do una raccolta di sola lettura, è perché non voglio che venga modificata. Generare un'eccezione e fare affidamento sulla documentazione sembra un inferno come una patch. In C ++, vorrei semplicemente restituire un constoggetto, dicendo immediatamente all'utente che l'oggetto non può essere modificato.
Etienne de Martel,

7
Il 1998 non fu "i primi giorni della progettazione di OO".
quant_dev,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.