É melhor retornar um ImmutableMap ou um Mapa?

90

Digamos que estou escrevendo um método que deve retornar um Mapa . Por exemplo:

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

Depois de pensar um pouco, decidi que não há razão para modificar este mapa depois de criado. Portanto, gostaria de retornar um ImmutableMap .

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

Devo deixar o tipo de retorno como um Mapa genérico ou devo especificar que estou retornando um ImmutableMap?

De um lado, é exatamente para isso que as interfaces foram criadas; para ocultar os detalhes de implementação.
Por outro lado, se eu deixar assim, outros desenvolvedores podem perder o fato de que esse objeto é imutável. Portanto, não alcançarei o objetivo principal de objetos imutáveis; para tornar o código mais claro, minimizando o número de objetos que podem ser alterados. Pior ainda, depois de um tempo, alguém pode tentar alterar este objeto, e isso resultará em um erro de execução (o compilador não avisará sobre isso).

AMT
fonte
2
Suspeito que alguém acabará por marcar esta questão como muito aberta para argumentos. Você pode colocar perguntas mais específicas em vez de "WDYT?" e "está tudo bem ...?" Por exemplo, "há algum problema ou consideração em retornar um mapa normal?" e "quais alternativas existem?"
cinza
E se mais tarde você decidir que ele deve realmente retornar um mapa mutável?
user253751
3
@immibis lol, ou uma lista para esse assunto?
Djechlin
3
Você realmente deseja assar uma dependência Guava em sua API? Você não será capaz de mudar isso sem quebrar a compatibilidade com versões anteriores.
user2357112 suporta Monica
1
Acho que a pergunta é muito superficial e muitos detalhes importantes estão faltando. Por exemplo, se você já tem Guava como uma dependência (visível) de qualquer maneira. Mesmo que você já o tenha, os snippets de código são pesudocode e não transmitem o que você realmente deseja alcançar. Você certamente não escreveria return ImmutableMap.of(somethingElse). Em vez disso, você armazenaria o ImmutableMap.of(somethingElse)como um campo e retornaria apenas este campo. Tudo isso influencia o design aqui.
Marco13

Respostas:

70
  • Se você está escrevendo uma API voltada para o público e essa imutabilidade é um aspecto importante do seu design, eu definitivamente tornaria isso explícito fazendo com que o nome do método denote claramente que o mapa retornado será imutável ou retornando o tipo concreto de o mapa. Mencioná-lo no javadoc não é suficiente em minha opinião.

    Visto que você aparentemente está usando a implementação Guava, olhei para o documento e é uma classe abstrata, portanto, oferece um pouco de flexibilidade no tipo real e concreto.

  • Se você está escrevendo uma ferramenta / biblioteca interna, torna-se muito mais aceitável apenas retornar um plano Map. As pessoas saberão sobre os detalhes internos do código para o qual estão ligando ou pelo menos terão fácil acesso a ele.

Minha conclusão seria que explícito é bom, não deixe as coisas ao acaso.

Dici
fonte
1
Uma interface extra, uma classe abstrata extra e uma classe que estende esta classe abstrata. Isso é muito incômodo para uma coisa tão simples como o OP está pedindo, que é se o método deve retornar o Maptipo de interface ou ImmutableMaptipo de implementação.
fps
20
Se você está se referindo ao Guava's ImmutableMap, o conselho da equipe do Guava é considerar ImmutableMapuma interface com garantias semânticas, e que você deve retornar esse tipo diretamente.
Louis Wasserman
1
@LouisWasserman mm, não vi que questão estava dando um link para a implementação do Guava. Vou remover o snippet então, obrigado
Dici
3
Concordo com o Louis, se sua intenção é retornar um mapa que seja um objeto Imutável, certifique-se de que o objeto de retorno seja desse tipo de preferência. Se por algum motivo você quiser obscurecer o fato de que é um tipo imutável, defina sua própria interface. Deixá-lo como mapa é enganoso.
WSimpson
1
@WSimpson, acredito que é isso que minha resposta está dizendo. Louis estava falando sobre algum trecho que eu adicionei, mas eu o removi.
Dici
38

Você deve ter ImmutableMapcomo seu tipo de retorno. Mapcontém métodos que não são suportados pela implementação de ImmutableMap(por exemplo put) e são marcados @deprecatedcom ImmutableMap.

O uso de métodos obsoletos resultará em um aviso do compilador e a maioria dos IDEs avisará quando as pessoas tentarem usar os métodos obsoletos.

Este aviso avançado é preferível a ter exceções de tempo de execução como sua primeira dica de que algo está errado.

Synesso
fonte
1
Esta é, em minha opinião, a resposta mais sensata. A interface do Map é um contrato e ImmutableMap está claramente violando uma parte importante dele. Usar Map como um tipo de retorno neste caso não faz sentido.
TonioElGringo
@TonioElGringo e Collections.immutableMapentão?
OrangeDog
@TonioElGringo observe que os métodos que o ImmutableMap não implementa são explicitamente marcados como opcionais na documentação da interface docs.oracle.com/javase/7/docs/api/java/util/… . Portanto, acho que ainda pode fazer sentido retornar um Mapa (com javadocs adequados). Mesmo assim, eu escolho retornar o ImmutableMap explicitamente, por causa dos motivos discutidos acima.
AMT de
3
@TonioElGringo não está violando nada, esses métodos são opcionais. Como em List, não há garantia de que List::addseja válido. Experimente Arrays.asList("a", "b").add("c");. Não há nenhuma indicação de que ele falhará em tempo de execução, mas isso ocorre. Pelo menos Goiaba avisa.
njzk2
14

Por outro lado, se eu deixar assim, outros desenvolvedores podem perder o fato de que esse objeto é imutável.

Você deve mencionar isso nos javadocs. Os desenvolvedores os leem, você sabe.

Portanto, não alcançarei o objetivo principal de objetos imutáveis; para tornar o código mais claro, minimizando o número de objetos que podem ser alterados. Pior ainda, depois de um tempo, alguém pode tentar alterar este objeto, e isso resultará em um erro de execução (o compilador não avisará sobre isso).

Nenhum desenvolvedor publica seu código não testado. E quando ele o testa, ele recebe uma Exceção onde ele não apenas vê o motivo, mas também o arquivo e a linha onde ele tentou gravar em um mapa imutável.

Observe, porém, que apenas o Mappróprio será imutável, não os objetos que ele contém.

tkausl
fonte
10
"Nenhum desenvolvedor publica seu código não testado." Você quer apostar nisso? Haveria muito menos (minha presunção) bugs no software público, se esta frase fosse verdadeira. Eu nem teria certeza de que "a maioria dos desenvolvedores ..." seria verdade. E sim, isso é decepcionante.
Tom
1
Ok, Tom está certo, não tenho certeza do que você está tentando dizer, mas o que você realmente escreveu é claramente falso. (Também: nudge para inclusão de gênero)
Djechlin
@Tom Eu acho que você perdeu o sarcasmo nesta resposta
Capitão Fogetti
10

se eu deixar assim, outros desenvolvedores podem perder o fato de que este objeto é imutável

Isso é verdade, mas outros desenvolvedores devem testar seu código e garantir que ele seja coberto.

No entanto, você tem mais 2 opções para resolver isso:

  • Use Javadoc

    @return a immutable map
  • Escolha um nome descritivo para o método

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

    Para um caso de uso concreto, você pode até nomear melhor os métodos. Por exemplo

    public Map<String, Integer> getUnmodifiableCountByWords()

O que mais você pode fazer?!

Você pode devolver um

  • cópia de

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

    Essa abordagem deve ser usada se você espera que muitos clientes modifiquem o mapa e desde que o mapa contenha apenas algumas entradas.

  • CopyOnWriteMap

    As coleções de cópia na gravação geralmente são usadas quando você precisa lidar com a
    simultaneidade. Mas o conceito também o ajudará em sua situação, já que um CopyOnWriteMap cria uma cópia da estrutura de dados interna em uma operação mutativa (por exemplo, adicionar, remover).

    Nesse caso, você precisa de um wrapper fino em torno de seu mapa que delega todas as invocações de método para o mapa subjacente, exceto as operações mutativas. Se uma operação mutativa for chamada, ela criará uma cópia do mapa subjacente e todas as outras chamadas serão delegadas a essa cópia.

    Essa abordagem deve ser usada se você espera que alguns clientes modifiquem o mapa.

    Infelizmente, o java não tem tal CopyOnWriteMap. Mas você pode encontrar um terceiro ou implementá-lo sozinho.

Por fim, você deve ter em mente que os elementos em seu mapa ainda podem ser mutáveis.

René Link
fonte
8

Definitivamente, retorne um ImmutableMap, com a justificativa:

  • A assinatura do método (incluindo o tipo de retorno) deve ser autodocumentada. Comentários são como atendimento ao cliente: se seus clientes precisam confiar neles, seu produto principal está com defeito.
  • Se algo é uma interface ou uma classe, só é relevante ao estender ou implementar. Dada uma instância (objeto), 99% das vezes o código do cliente não saberá ou se importará se algo é uma interface ou uma classe. Presumi a princípio que ImmutableMap era uma interface. Só depois de clicar no link é que percebi que é uma aula.
Benito Ciaro
fonte
5

Depende da própria classe. O Guava ImmutableMapnão pretende ser uma visão imutável em uma classe mutável. Se sua classe for imutável e tiver alguma estrutura que seja basicamente um ImmutableMap, faça o tipo de retorno ImmutableMap. No entanto, se sua classe for mutável, não faça isso. Se você tem este:

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

O Guava irá copiar o mapa todas as vezes. Isso é lento. Mas se internalMapjá era um ImmutableMap, então está tudo bem.

Se você não restringir sua classe para retornar ImmutableMap, então você pode retornar desta Collections.unmodifiableMapforma:

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

Observe que esta é uma visão imutável no mapa. Se internalMapmudar, uma cópia em cache de Collections.unmodifiableMap(internalMap). Eu ainda prefiro para getters, no entanto.

Justin
fonte
1
Esses são bons pontos, mas para ser completo, eu gosto desta resposta, que explica que o unmodifiableMapsó não pode ser modificado pelo titular da referência - é uma visualização e o titular do mapa de apoio pode modificar o mapa de apoio e então a visualização muda. Portanto, essa é uma consideração importante.
davidbak
@Como Davidback comentou, tenha muito cuidado ao usar Collections.unmodifiableMap. Esse método retorna um visualização do Mapa original em vez de criar um novo objeto de mapa distinto separado. Portanto, qualquer outro código que faça referência ao Mapa original pode modificá-lo, e então o detentor do mapa supostamente não modificável aprende da maneira mais difícil que o mapa pode realmente ser modificado por baixo deles. Eu mesmo fui mordido por esse fato. Aprendi a devolver uma cópia do Mapa ou a usar Goiaba ImmutableMap.
Basil Bourque
5

Isso não está respondendo à pergunta exata, mas ainda vale a pena considerar se um mapa deve ser devolvido. Se o mapa for imutável, o método principal a ser fornecido será baseado em get (key):

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

Isso torna a API muito mais rígida. Se um mapa for realmente necessário, isso pode ser deixado para o cliente da API, fornecendo um fluxo de entradas:

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

Em seguida, o cliente pode fazer seu próprio mapa imutável ou mutável conforme necessário ou adicionar as entradas a seu próprio mapa. Se o cliente precisa saber se o valor existe, opcional pode ser retornado em seu lugar:

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

Mapa imutável é um tipo de mapa. Portanto, não há problema em deixar o tipo de retorno de Mapa.

Para garantir que os usuários não modifiquem o objeto retornado, a documentação do método pode descrever as características do objeto retornado.

toro
fonte
-1

Isso é indiscutivelmente uma questão de opinião, mas a melhor ideia aqui é usar uma interface para a classe de mapa. Esta interface não precisa dizer explicitamente que é imutável, mas a mensagem será a mesma se você não expor nenhum dos métodos setter da classe pai na interface.

Dê uma olhada no seguinte artigo:

Andy Gibson

WSimpson
fonte
1
Wrappers desnecessários considerados prejudiciais.
Djechlin
Você tem razão, eu também não usaria um wrapper aqui, pois a interface é suficiente.
WSimpson
Tenho a sensação de que se isso fosse a coisa certa a fazer, ImmutableMap já implementaria um ImmutableMapInterface, mas não entendo isso tecnicamente.
Djechlin
A questão não é o que os desenvolvedores do Guava pretendiam, é o fato de que esse desenvolvedor gostaria de encapsular a arquitetura e não expor o uso do ImmutableMap. Uma maneira de fazer isso seria usar uma interface. Usar um invólucro como você apontou não é necessário e, portanto, não é recomendado. Retornar mapa é indesejável, pois é enganoso. Portanto, parece que se o objetivo é o encapsulamento, uma interface é a melhor opção.
WSimpson
A desvantagem de retornar algo que não estende (ou implementa) a Mapinterface é que você não pode passá-lo para nenhuma função da API esperando umMap sem lançá-lo. Isso inclui todos os tipos de construtores de cópias de outros tipos de mapas. Além disso, se a mutabilidade estiver apenas "oculta" pela interface, seus usuários sempre podem contornar isso lançando e quebrando seu código dessa maneira.
Hulk