È meglio restituire una ImmutableMap o una Map?


90

Diciamo che sto scrivendo un metodo che dovrebbe restituire una mappa . Per esempio:

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}

Dopo averci pensato per un po ', ho deciso che non c'è motivo di modificare questa mappa una volta creata. Pertanto, vorrei restituire una ImmutableMap .

public Map<String, Integer> foo() {
  return ImmutableMap.of();
}

Devo lasciare il tipo restituito come una mappa generica o devo specificare che sto restituendo una mappa immutabile?

Da un lato, questo è esattamente il motivo per cui sono state create le interfacce; per nascondere i dettagli di implementazione.
D'altra parte, se lo lascio così, altri sviluppatori potrebbero perdere il fatto che questo oggetto è immutabile. Pertanto, non raggiungerò l'obiettivo principale di oggetti immutabili; per rendere il codice più chiaro riducendo al minimo il numero di oggetti che possono cambiare. Ancora peggio, dopo un po ', qualcuno potrebbe provare a cambiare questo oggetto, e questo risulterà in un errore di runtime (il compilatore non lo avviserà).


2
Sospetto che qualcuno alla fine etichetterà questa domanda come troppo aperta per gli argomenti. Puoi inserire domande più specifiche invece di "WDYT?" e "va bene ...?" Ad esempio, "ci sono problemi o considerazioni con la restituzione di una mappa normale?" e "quali alternative esistono?"
cenere

E se in seguito decidi che dovrebbe effettivamente restituire una mappa modificabile?
user253751

3
@immibis lol, o un elenco per quella materia?
Djechlin

3
Vuoi davvero inserire una dipendenza Guava nella tua API? Non sarai in grado di cambiarlo senza interrompere la compatibilità con le versioni precedenti.
user2357112 supporta Monica

1
Penso che la domanda sia troppo approssimativa e mancano molti dettagli importanti. Ad esempio, se hai già Guava come dipendenza (visibile) comunque. Anche se lo hai già, gli snippet di codice sono pesudocodici e non comunicano ciò che desideri effettivamente ottenere. Certamente non scriveresti return ImmutableMap.of(somethingElse). Invece, memorizzerai il ImmutableMap.of(somethingElse)come campo e restituirai solo questo campo. Tutto questo influenza il design qui.
Marco13

Risposte:


70
  • Se stai scrivendo un'API rivolta al pubblico e l'immutabilità è un aspetto importante del tuo progetto, lo renderei sicuramente esplicito se il nome del metodo indica chiaramente che la mappa restituita sarà immutabile o restituendo il tipo concreto di la mappa. Menzionarlo nel javadoc non è sufficiente a mio parere.

    Dato che apparentemente stai usando l'implementazione Guava, ho guardato il documento ed è una classe astratta, quindi ti dà un po 'di flessibilità sul tipo reale e concreto.

  • Se stai scrivendo uno strumento / libreria interna, diventa molto più accettabile restituire solo un semplice Map. Le persone conosceranno gli interni del codice che stanno chiamando o almeno avranno un facile accesso ad esso.

La mia conclusione sarebbe che esplicito è buono, non lasciare le cose al caso.


1
Un'interfaccia extra, una classe astratta extra e una classe che estende questa classe astratta. Questo è troppo fastidioso per una cosa così semplice come chiede l'OP, ovvero se il metodo deve restituire il Maptipo di interfaccia o il ImmutableMaptipo di implementazione.
fps

20
Se ti riferisci a Guava ImmutableMap, il consiglio del team di Guava è di considerare ImmutableMapun'interfaccia con garanzie semantiche e di restituire direttamente quel tipo.
Louis Wasserman,

1
@LouisWasserman mm, non ho visto che la domanda stava fornendo un collegamento all'implementazione di Guava. Quindi rimuoverò lo snippet, grazie
Dici

3
Sono d'accordo con Louis, se intendi restituire una mappa che è un oggetto immutabile, assicurati che l'oggetto restituito sia preferibilmente di quel tipo. Se per qualche motivo vuoi oscurare il fatto che si tratta di un tipo immutabile, definisci la tua interfaccia. Lasciarlo come mappa è fuorviante.
WSimpson

1
@WSimpson, credo che sia quello che dice la mia risposta. Louis stava parlando di uno snippet che avevo aggiunto, ma l'ho rimosso.
Dici

38

Dovresti avere ImmutableMapcome tipo di ritorno. Mapcontiene metodi che non sono supportati dall'implementazione di ImmutableMap(eg put) e sono contrassegnati @deprecatedin ImmutableMap.

L'uso di metodi deprecati risulterà in un avviso del compilatore e la maggior parte degli IDE avviserà quando le persone tenteranno di utilizzare i metodi deprecati.

Questo avviso avanzato è preferibile rispetto alle eccezioni di runtime come primo suggerimento che qualcosa non va.


1
Questa è secondo me la risposta più sensata. L'interfaccia Map è un contratto e ImmutableMap ne sta chiaramente violando una parte importante. L'utilizzo di Map come tipo restituito in questo caso non ha senso.
TonioElGringo

@TonioElGringo e Collections.immutableMapallora?
OrangeDog

@TonioElGringo per favore nota che i metodi che ImmutableMap non implementano sono esplicitamente contrassegnati come opzionali nella documentazione dell'interfaccia docs.oracle.com/javase/7/docs/api/java/util/… . Quindi, penso che possa ancora avere senso restituire una mappa (con javadoc appropriati). Anche se, scelgo di restituire la ImmutableMap esplicitamente, a causa dei motivi discussi sopra.
AMT

3
@TonioElGringo non sta violando nulla, questi metodi sono opzionali. Come in List, non vi è alcuna garanzia che List::addsia valida. Prova Arrays.asList("a", "b").add("c");. Non vi è alcuna indicazione che fallirà in fase di esecuzione, eppure lo fa. Almeno Guava ti dà un avvertimento.
njzk2

14

D'altra parte, se lo lascio così, altri sviluppatori potrebbero perdere il fatto che questo oggetto è immutabile.

Dovresti menzionarlo nel file javadocs. Gli sviluppatori li leggono, sai.

Pertanto, non raggiungerò l'obiettivo principale di oggetti immutabili; per rendere il codice più chiaro riducendo al minimo il numero di oggetti che possono cambiare. Peggio ancora, dopo un po 'qualcuno potrebbe provare a cambiare questo oggetto, e questo si tradurrà in un errore di runtime (il compilatore non lo avviserà).

Nessuno sviluppatore pubblica il suo codice non testato. E quando lo prova, riceve un'eccezione in cui non solo vede il motivo, ma anche il file e la riga in cui ha provato a scrivere su una mappa immutabile.

Nota però, solo lo Mapstesso sarà immutabile, non gli oggetti che contiene.


10
"Nessuno sviluppatore pubblica il suo codice non testato." Ci vuoi scommettere? Ci sarebbero molti meno (presumo) bug nel software pubblico, se questa frase fosse vera. Non sarei nemmeno sicuro che "la maggior parte degli sviluppatori ..." sarebbe vero. E sì, è deludente.
Tom,

1
Ok, Tom ha ragione, non sono sicuro di quello che stai cercando di dire, ma quello che hai scritto in realtà è chiaramente falso. (Anche: spinta per l'inclusione di genere)
djechlin

@ Tom Immagino che ti sei perso il sarcasmo in questa risposta
Capitano Fogetti

10

se lo lascio così, altri sviluppatori potrebbero perdere il fatto che questo oggetto è immutabile

È vero, ma altri sviluppatori dovrebbero testare il loro codice e assicurarsi che sia coperto.

Tuttavia hai altre 2 opzioni per risolvere questo problema:

  • Usa Javadoc

    @return a immutable map
  • Scegli un nome di metodo descrittivo

    public Map<String, Integer> getImmutableMap()
    public Map<String, Integer> getUnmodifiableEntries()

    Per un caso d'uso concreto puoi persino nominare meglio i metodi. Per esempio

    public Map<String, Integer> getUnmodifiableCountByWords()

Cos'altro puoi fare ?!

Puoi restituire un file

  • copia

    private Map<String, Integer> myMap;
    
    public Map<String, Integer> foo() {
      return new HashMap<String, Integer>(myMap);
    }

    Questo approccio dovrebbe essere utilizzato se ti aspetti che molti client modificheranno la mappa e fintanto che la mappa contiene solo poche voci.

  • CopyOnWriteMap

    le raccolte copy on write vengono solitamente utilizzate quando si ha a che fare
    concorrenza. Ma il concetto ti aiuterà anche nella tua situazione, poiché CopyOnWriteMap crea una copia della struttura dati interna su un'operazione mutativa (ad es. Aggiungi, rimuovi).

    In questo caso è necessario un sottile involucro attorno alla mappa che delega tutte le chiamate di metodo alla mappa sottostante, tranne le operazioni mutative. Se viene invocata un'operazione mutativa, viene creata una copia della mappa sottostante e tutte le ulteriori chiamate verranno delegate a questa copia.

    Questo approccio dovrebbe essere utilizzato se si prevede che alcuni client modificheranno la mappa.

    Purtroppo java non ha un tale file CopyOnWriteMap. Ma potresti trovare una terza parte o implementarla da solo.

Infine dovresti tenere a mente che gli elementi nella tua mappa potrebbero essere ancora mutabili.


8

Sicuramente restituire una ImmutableMap, giustificazione essendo:

  • La firma del metodo (incluso il tipo restituito) dovrebbe essere auto-documentante. I commenti sono come il servizio clienti: se i tuoi clienti devono fare affidamento su di loro, il tuo prodotto principale è difettoso.
  • Se qualcosa è un'interfaccia o una classe è rilevante solo quando la si estende o la implementa. Data un'istanza (oggetto), il 99% delle volte il codice client non saprà o non si preoccuperà se qualcosa è un'interfaccia o una classe. All'inizio ho pensato che ImmutableMap fosse un'interfaccia. Solo dopo aver cliccato sul link mi sono reso conto che si tratta di una classe.

5

Dipende dalla classe stessa. Guava's ImmutableMapnon vuole essere una vista immutabile in una classe mutevole. Se la tua classe è immutabile e ha una struttura che è fondamentalmente an ImmutableMap, crea il tipo restituito ImmutableMap. Tuttavia, se la tua classe è mutabile, non farlo. Se hai questo:

public ImmutableMap<String, Integer> foo() {
    return ImmutableMap.copyOf(internalMap);
}

Guava copierà la mappa ogni volta. È lento. Ma se internalMapera già un fileImmutableMap , allora va benissimo.

Se non limiti la tua classe al ritorno ImmutableMap, potresti invece tornare in questo Collections.unmodifiableMapmodo:

public Map<String, Integer> foo() {
    return Collections.unmodifiableMap(internalMap);
}

Nota che questa è una visualizzazione immutabile nella mappa. In caso di internalMapmodifiche, anche una copia cache di Collections.unmodifiableMap(internalMap). Lo preferisco comunque per i getter, comunque.


1
Questi sono buoni punti, ma per completezza, mi piace questa risposta che spiega che non unmodifiableMapè modificabile solo dal titolare del riferimento: è una vista e il titolare della mappa di supporto può modificare la mappa di supporto e quindi la visualizzazione cambia. Quindi questa è una considerazione importante.
davidbak

@Come ha commentato Davidback, stai molto attento all'uso Collections.unmodifiableMap. Questo metodo restituisce una vista sulla mappa originale invece di creare un nuovo oggetto mappa distinto e separato. Quindi qualsiasi altro codice che fa riferimento alla mappa originale può modificarla, e quindi il detentore della mappa presumibilmente non modificabile impara a proprie spese che la mappa può effettivamente essere modificata da sotto di loro. Sono stato morso da questo fatto anch'io. Ho imparato a restituire una copia della mappa o a usare Guava ImmutableMap.
Basil Bourque

5

Questo non risponde alla domanda esatta, ma vale comunque la pena considerare se una mappa debba essere restituita. Se la mappa è immutabile, il metodo principale da fornire è basato sul get (key):

public Integer fooOf(String key) {
    return map.get(key);
}

Questo rende l'API molto più stretta. Se una mappa è effettivamente richiesta, questa potrebbe essere lasciata al client dell'API fornendo un flusso di voci:

public Stream<Map.Entry<String, Integer>> foos() {
    map.entrySet().stream()
}

Quindi il client può creare la propria mappa immutabile o modificabile a seconda delle necessità, oppure aggiungere le voci alla propria mappa. Se il client ha bisogno di sapere se il valore esiste, può essere invece restituito opzionale:

public Optional<Integer> fooOf(String key) {
    return Optional.ofNullable(map.get(key));
}

-1

Immutable Map è un tipo di mappa. Quindi lasciare il tipo di restituzione di Map va bene.

Per garantire che gli utenti non modifichino l'oggetto restituito, la documentazione del metodo può descrivere le caratteristiche dell'oggetto restituito.


-1

Questa è probabilmente una questione di opinione, ma l'idea migliore qui è usare un'interfaccia per la classe map. Questa interfaccia non ha bisogno di dire esplicitamente che è immutabile, ma il messaggio sarà lo stesso se non esponi nessuno dei metodi setter della classe genitore nell'interfaccia.

Dai un'occhiata al seguente articolo:

andy gibson


1
Involucri non necessari considerati dannosi.
Djechlin

Hai ragione, nemmeno qui userei un wrapper, poiché l'interfaccia è sufficiente.
WSimpson

Ho la sensazione che se questa fosse la cosa giusta da fare, ImmutableMap implementerebbe già ImmutableMapInterface, ma non lo capisco tecnicamente.
Djechlin

Il punto non è quello che intendevano gli sviluppatori Guava, è il fatto che questo sviluppatore vorrebbe incapsulare l'architettura e non esporre l'uso di ImmutableMap. Un modo per farlo sarebbe usare un'interfaccia. L'uso di un wrapper come hai sottolineato non è necessario e quindi non è consigliato. La restituzione della mappa è indesiderabile in quanto fuorviante. Quindi sembra che se l'obiettivo è l'incapsulamento, allora un'interfaccia è l'opzione migliore.
WSimpson

Lo svantaggio di restituire qualcosa che non estende (o implementa) l' Mapinterfaccia è che non puoi passarlo a nessuna funzione API che si aspetta un Mapsenza eseguirne il cast. Ciò include tutti i tipi di costruttori di copie di altri tipi di mappe. Inoltre, se la mutabilità è solo "nascosta" dall'interfaccia, i tuoi utenti possono sempre aggirare il problema trasmettendo e rompendo il codice in questo modo.
Hulk
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.