¿Es mejor devolver un ImmutableMap o un Map?

90

Digamos que estoy escribiendo un método que debería devolver un mapa . Por ejemplo:

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

Después de pensarlo un rato, he decidido que no hay razón para modificar este mapa una vez creado. Por lo tanto, me gustaría devolver un ImmutableMap .

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

¿Debo dejar el tipo de devolución como un mapa genérico o debo especificar que estoy devolviendo un mapa inmutable?

Por un lado, esta es exactamente la razón por la que se crearon las interfaces; para ocultar los detalles de implementación.
Por otro lado, si lo dejo así, otros desarrolladores podrían perder el hecho de que este objeto es inmutable. Por lo tanto, no lograré un objetivo importante de objetos inmutables; para hacer el código más claro minimizando el número de objetos que pueden cambiar. Peor aún, después de un tiempo, alguien podría intentar cambiar este objeto, y esto resultará en un error de tiempo de ejecución (el compilador no lo advertirá).

AMT
fuente
2
Sospecho que alguien eventualmente etiquetará esta pregunta como demasiado abierta para discutir. ¿Puede hacer preguntas más específicas en lugar de "WDYT"? y "¿está bien ...?" Por ejemplo, "¿hay algún problema o consideración al devolver un mapa normal?" y "¿qué alternativas existen?"
ceniza
¿Qué pasa si luego decides que debería devolver un mapa mutable?
user253751
3
@immibis lol, o una lista para el caso?
djechlin
3
¿Realmente quieres hornear una dependencia de Guava en tu API? No podrá cambiar eso sin romper la compatibilidad con versiones anteriores.
user2357112 apoya a Monica el
1
Creo que la pregunta es demasiado esquemática y faltan muchos detalles importantes. Por ejemplo, si ya tiene Guava como una dependencia (visible) de todos modos. Incluso si ya lo tiene, los fragmentos de código son pesudocódigo y no transmiten lo que realmente desea lograr. Ciertamente no escribirías return ImmutableMap.of(somethingElse). En su lugar, almacenaría el ImmutableMap.of(somethingElse)como un campo y devolvería solo este campo. Todo esto influye en el diseño aquí.
Marco13

Respuestas:

70
  • Si está escribiendo una API de cara al público y esa inmutabilidad es un aspecto importante de su diseño, definitivamente lo haría explícito si el nombre del método denota claramente que el mapa devuelto será inmutable o devolviendo el tipo concreto de el mapa. En mi opinión, mencionarlo en el javadoc no es suficiente.

    Dado que aparentemente está utilizando la implementación de Guava, miré el documento y es una clase abstracta, por lo que le brinda un poco de flexibilidad en el tipo concreto real.

  • Si está escribiendo una herramienta / biblioteca interna, es mucho más aceptable devolver un archivo Map. Las personas conocerán los aspectos internos del código al que están llamando o, al menos, tendrán fácil acceso a él.

Mi conclusión sería que lo explícito es bueno, no dejes las cosas al azar.

Dici
fuente
1
Una interfaz adicional, una clase abstracta adicional y una clase que amplía esta clase abstracta. Esto es demasiado complicado para algo tan simple como lo que pregunta el OP, que es si el método debe devolver el Maptipo de interfaz o el ImmutableMaptipo de implementación.
fps
20
Si se refiere a Guava's ImmutableMap, el consejo del equipo de Guava es considerar ImmutableMapuna interfaz con garantías semánticas, y que debe devolver ese tipo directamente.
Louis Wasserman
1
@LouisWasserman mm, no vi que la pregunta estaba dando un enlace a la implementación de Guava. Eliminaré el fragmento entonces, gracias
Dici
3
Estoy de acuerdo con Louis, si su intención es devolver un mapa que sea un objeto inmutable, asegúrese de que el objeto de retorno sea de ese tipo preferiblemente. Si por alguna razón desea ocultar el hecho de que es un tipo inmutable, defina su propia interfaz. Dejarlo como mapa es engañoso.
WSimpson
1
@WSimpson Creo que eso es lo que dice mi respuesta. Louis estaba hablando de un fragmento que había agregado, pero lo eliminé.
Dici
38

Debería tener ImmutableMapcomo tipo de devolución. Mapcontiene métodos que no son compatibles con la implementación de ImmutableMap(eg put) y están marcados @deprecateden ImmutableMap.

El uso de métodos obsoletos dará como resultado una advertencia del compilador y la mayoría de los IDE advertirán cuando las personas intenten utilizar los métodos obsoletos.

Esta advertencia avanzada es preferible a tener excepciones en tiempo de ejecución como primer indicio de que algo anda mal.

Synesso
fuente
1
En mi opinión, esta es la respuesta más sensata. La interfaz de Map es un contrato e ImmutableMap claramente está incumpliendo una parte importante. Usar Map como un tipo de retorno en este caso no tiene sentido.
TonioElGringo
@TonioElGringo ¿y Collections.immutableMapentonces?
OrangeDog
@TonioElGringo tenga en cuenta que los métodos que ImmutableMap no implementa están explícitamente marcados como opcionales en la documentación de la interfaz docs.oracle.com/javase/7/docs/api/java/util/… . Por lo tanto, creo que todavía puede tener sentido devolver un mapa (con los javadocs adecuados). Aunque, elijo devolver el ImmutableMap explícitamente, debido a las razones discutidas anteriormente.
AMT
3
@TonioElGringo no está violando nada, esos métodos son opcionales. Al igual que en List, no hay garantía de que List::addsea ​​válida. Prueba Arrays.asList("a", "b").add("c");. No hay ninguna indicación de que fallará en tiempo de ejecución, pero lo hace. Al menos Guava te avisa.
njzk2
14

Por otro lado, si lo dejo así, otros desarrolladores podrían perder el hecho de que este objeto es inmutable.

Deberías mencionar eso en los javadocs. Los desarrolladores los leen, ya sabes.

Por lo tanto, no lograré un objetivo importante de objetos inmutables; para hacer el código más claro minimizando el número de objetos que pueden cambiar. Peor aún, después de un tiempo, alguien podría intentar cambiar este objeto, y esto resultará en un error de tiempo de ejecución (el compilador no lo advertirá).

Ningún desarrollador publica su código sin probarlo. Y cuando lo prueba, recibe una excepción en la que no solo ve el motivo, sino también el archivo y la línea donde intentó escribir en un mapa inmutable.

Sin embargo, tenga en cuenta que solo el Mapmismo será inmutable, no los objetos que contiene.

tkausl
fuente
10
"Ningún desarrollador publica su código sin probar". ¿Quieres apostar por esto? Habría muchos menos (mi presunción) errores en el software público, si esta oración fuera cierta. Ni siquiera estaría seguro de que "la mayoría de los desarrolladores ..." fuera cierto. Y sí, eso es decepcionante.
Tom
1
De acuerdo, Tom tiene razón, no estoy seguro de lo que intentas decir, pero lo que escribiste es claramente falso. (También: empujón para la inclusión de género)
djechlin
@Tom Supongo que te perdiste el sarcasmo en esta respuesta
Capitán Fogetti
10

si lo dejo así, otros desarrolladores podrían perder el hecho de que este objeto es inmutable

Eso es cierto, pero otros desarrolladores deberían probar su código y asegurarse de que esté cubierto.

Sin embargo, tienes 2 opciones más para solucionar esto:

  • Utilice Javadoc

    @return a immutable map
  • Elija un nombre de método descriptivo

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

    Para un caso de uso concreto, incluso puede nombrar mejor los métodos. P.ej

    public Map<String, Integer> getUnmodifiableCountByWords()

¡¿Qué más puedes hacer?!

Puede devolver un

  • Copiar

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

    Este enfoque debe usarse si espera que muchos clientes modifiquen el mapa y siempre que el mapa solo contenga algunas entradas.

  • CopyOnWriteMap

    Las colecciones de copia en escritura se utilizan generalmente cuando tienes que lidiar con
    concurrencia. Pero el concepto también le ayudará en su situación, ya que CopyOnWriteMap crea una copia de la estructura de datos interna en una operación mutativa (por ejemplo, agregar, eliminar).

    En este caso, necesita un envoltorio delgado alrededor de su mapa que delegue todas las invocaciones de métodos al mapa subyacente, excepto las operaciones mutativas. Si se invoca una operación mutativa, se crea una copia del mapa subyacente y todas las invocaciones posteriores se delegarán en esta copia.

    Este enfoque debe utilizarse si espera que algunos clientes modifiquen el mapa.

    Lamentablemente, Java no tiene tal CopyOnWriteMap. Pero puede encontrar un tercero o implementarlo usted mismo.

Por último, debe tener en cuenta que los elementos de su mapa aún pueden ser mutables.

René Link
fuente
8

Definitivamente devuelve un ImmutableMap, la justificación es:

  • La firma del método (incluido el tipo de retorno) debe ser autodocumentada. Los comentarios son como el servicio al cliente: si sus clientes necesitan confiar en ellos, entonces su producto principal es defectuoso.
  • Si algo es una interfaz o una clase, solo es relevante cuando se extiende o se implementa. Dada una instancia (objeto), el 99% de las veces el código del cliente no sabrá o no le importará si algo es una interfaz o una clase. Al principio asumí que ImmutableMap era una interfaz. Solo después de hacer clic en el enlace me di cuenta de que es una clase.
Benito Ciaro
fuente
5

Depende de la clase en sí. Guava's ImmutableMapno pretende ser una vista inmutable en una clase mutable. Si su clase es inmutable y tiene una estructura que es básicamente an ImmutableMap, entonces haga el tipo de retorno ImmutableMap. Sin embargo, si tu clase es mutable, no lo hagas. Si tienes esto:

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

Guava copiará el mapa cada vez. Eso es lento. Pero si internalMapya era un ImmutableMap, entonces está totalmente bien.

Si no restringe su clase para que regrese ImmutableMap, entonces podría regresar Collections.unmodifiableMapasí:

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

Tenga en cuenta que esta es una vista inmutable del mapa. Si internalMapcambia, también lo hará una copia en caché de Collections.unmodifiableMap(internalMap). Sin embargo, todavía lo prefiero para los getters.

Justin
fuente
1
Estos son buenos puntos, pero para completar, me gusta esta respuesta que explica que el unmodifiableMaptitular de la referencia solo no puede modificarlo: es una vista y el titular del mapa de respaldo puede modificar el mapa de respaldo y luego la vista cambia. Entonces esa es una consideración importante.
Davidbak
@Como comentó Davidback, tenga mucho cuidado al usar Collections.unmodifiableMap. Ese método devuelve una vista del mapa original en lugar de crear un nuevo objeto de mapa distinto e independiente. Entonces, cualquier otro código que haga referencia al mapa original puede modificarlo, y luego el poseedor del mapa supuestamente no modificable aprende por las malas que el mapa puede modificarse desde debajo de ellos. A mí me ha mordido ese hecho. Aprendí a devolver una copia del mapa o usar Guava ImmutableMap.
Basil Bourque
5

Esto no responde a la pregunta exacta, pero vale la pena considerar si se debe devolver un mapa. Si el mapa es inmutable, entonces el método principal que se proporcionará se basa en get (clave):

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

Esto hace que la API sea mucho más estricta. Si realmente se requiere un mapa, esto podría dejarse en manos del cliente de la API proporcionando un flujo de entradas:

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

Luego, el cliente puede hacer su propio mapa inmutable o mutable según lo necesite, o agregar las entradas a su propio mapa. Si el cliente necesita saber si el valor existe, se puede devolver opcional en su lugar:

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

Immutable Map es un tipo de mapa. Entonces, dejar el tipo de retorno de Map está bien.

Para asegurarse de que los usuarios no modifiquen el objeto devuelto, la documentación del método puede describir las características del objeto devuelto.

toro
fuente
-1

Podría decirse que esto es una cuestión de opinión, pero la mejor idea aquí es usar una interfaz para la clase de mapa. Esta interfaz no necesita decir explícitamente que es inmutable, pero el mensaje será el mismo si no expone ninguno de los métodos de establecimiento de la clase principal en la interfaz.

Eche un vistazo al siguiente artículo:

Andy Gibson

WSimpson
fuente
1
Envoltorios innecesarios considerados dañinos.
djechlin
Tiene razón, yo tampoco usaría un contenedor aquí, ya que la interfaz es suficiente.
WSimpson
Tengo la sensación de que si esto fuera lo correcto, ImmutableMap ya implementaría una ImmutableMapInterface, pero técnicamente no entiendo esto.
djechlin
El punto no es lo que pretendían los desarrolladores de Guava, es el hecho de que a este desarrollador le gustaría encapsular la arquitectura y no exponer el uso de ImmutableMap. Una forma de hacerlo sería utilizar una interfaz. No es necesario usar una envoltura, como señaló, y, por lo tanto, no se recomienda. Devolver el mapa no es deseable, ya que es engañoso. Entonces parece que si el objetivo es la encapsulación, entonces una interfaz es la mejor opción.
WSimpson
La desventaja de devolver algo que no extiende (o implementa) la Mapinterfaz es que no puedes pasarlo a ninguna función de API esperando un Mapsin emitirlo. Esto incluye todo tipo de constructores de copias de otros tipos de mapas. Además de eso, si la mutabilidad sólo está "oculta" por la interfaz, sus usuarios siempre pueden solucionar esto enviando y rompiendo su código de esa manera.
Hulk