¿Cómo puedo evitar repetir el código inicializando un hashmap de hashmap?

27

Cada cliente tiene un id, y muchas facturas, con fechas, almacenadas como Hashmap de clientes por id, de un hashmap de facturas por fecha:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if(allInvoices!=null){
    allInvoices.put(date, invoice);      //<---REPEATED CODE
}else{
    allInvoices = new HashMap<>();
    allInvoices.put(date, invoice);      //<---REPEATED CODE
    allInvoicesAllClients.put(id, allInvoices);
}

La solución de Java parece ser usar getOrDefault:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.getOrDefault(
    id,
    new HashMap<LocalDateTime, Invoice> (){{  put(date, invoice); }}
);

Pero si get no es nulo, todavía quiero que se ejecute put (fecha, factura), y también es necesario agregar datos a "allInvoicesAllClients". Por lo tanto, no parece ayudar mucho.

Hernán Eche
fuente
Si no puede garantizar la unicidad de la clave, lo mejor es que el mapa secundario tenga el valor de Lista <Factura> en lugar de solo Factura.
Ryan

Respuestas:

39

Este es un excelente caso de uso para Map#computeIfAbsent. Su fragmento es esencialmente equivalente a:

allInvoicesAllClients.computeIfAbsent(id, key -> new HashMap<>()).put(date, invoice);

Si idno está presente como clave allInvoicesAllClients, creará una asignación de iduna nueva HashMapy devolverá la nueva HashMap. Si idestá presente como una clave, devolverá la existente HashMap.

Jacob G.
fuente
1
computeIfAbsent, hace un get (id) (o un put seguido por un get (id)), por lo que el siguiente put se realiza para corregir el put del elemento (fecha), respuesta correcta.
Hernán Eche
allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Alexander - Restablece a Monica el
1
@ Alexander-ReinstateMonica Map.ofcrea un inmodificable Map, que no estoy seguro de que el OP quiera.
Jacob G.
¿Sería este código menos eficiente que el OP originalmente? Preguntando esto porque no estoy familiarizado con cómo Java maneja las funciones lambda.
Zecong Hu
16

computeIfAbsentEs una gran solución para este caso particular. En general, me gustaría señalar lo siguiente, ya que nadie lo mencionó todavía:

El hashmap "externo" solo almacena una referencia al hashmap "interno", por lo que puede reordenar las operaciones para evitar la duplicación de código:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if (allInvoices == null) {           
    allInvoices = new HashMap<>();
    allInvoicesAllClients.put(id, allInvoices);
}

allInvoices.put(date, invoice);      // <--- no longer repeated
Heinzi
fuente
¡Así es como lo hicimos durante décadas antes de que Java 8 apareciera con su computeIfAbsent()método elegante !
Neil Bartlett
1
Todavía uso este enfoque hoy en idiomas en los que la implementación del mapa no proporciona un solo método de obtener o poner y devolver si está ausente. Vale la pena mencionar que esta puede ser la mejor solución en otros idiomas, aunque esta pregunta está etiquetada específicamente para Java 8.
Quinn Mortimer
11

Casi nunca deberías usar la inicialización del mapa de "doble paréntesis".

{{  put(date, invoice); }}

En este caso, debes usar computeIfAbsent

allInvoicesAllClients.computeIfAbsent(id, (k) -> new HashMap<>())
                     .put(date, allInvoices);

Si no hay un mapa para esta ID, insertará uno. El resultado será el mapa existente o calculado. A continuación, puede putelementos en ese mapa con la garantía de que no será nulo.

Miguel
fuente
1
No sé quién votó negativamente, no yo, tal vez el código de una sola línea está confundiendo todas las facturas que ponen todos los clientes, porque usas id en lugar de fecha, lo editaré
Hernán Eche
1
@ HernánEche Ah. Mi error. Gracias. Sí, el puesto idestá hecho también. Puede pensar computeIfAbsentcomo una opción condicional si lo desea. Y también devuelve el valor
Michael
" Casi nunca deberías usar la inicialización del mapa" doble paréntesis ". " ¿Por qué? (No dudo que tengas razón; estoy preguntando por genuina curiosidad.)
Heinzi
1
@Heinzi Porque crea una clase interna anónima. Esto contiene una referencia a la clase que lo declaró, que si expone el mapa (por ejemplo, a través de un captador) evitará que se recoja basura en la clase adjunta. Además, creo que puede ser confuso para las personas menos familiarizadas con Java; Los bloques de inicializador casi nunca se usan, y escribirlo de esta manera hace que parezca que {{ }}tiene un significado especial, que no es así.
Michael
1
@Michael: Tiene sentido, gracias. Olvidé por completo que las clases internas anónimas siempre son no estáticas (incluso si no es necesario que lo sean).
Heinzi
5

Esto es más largo que las otras respuestas, pero en mi opinión, mucho más legible:

if(!allInvoicesAllClients.containsKey(id))
    allInvoicesAllClients.put(id, new HashMap<LocalDateTime, Invoice>());

allInvoicesAllClients.get(id).put(date, invoice);
Lobo
fuente
3
Esto puede funcionar para un HashMap, pero el enfoque general no es óptimo. Si se tratara de ConcurrentHashMaps, estas operaciones no son atómicas. En tal caso, comprobar-luego-actuar conducirá a condiciones de carrera. Votaron de todos modos, los que odian.
Michael
0

Aquí está haciendo dos cosas separadas: asegurarse de que HashMapexista y agregarle la nueva entrada.

El código existente se asegura de insertar el nuevo elemento primero antes de registrar el mapa hash, pero eso no es necesario, ya HashMapque no importa el orden aquí. Ninguna de las variantes es segura para subprocesos, por lo que no está perdiendo nada.

Entonces, como sugirió @Heinzi, puede dividir estos dos pasos.

Lo que también haría es descargar la creación de la HashMapal allInvoicesAllClientsobjeto, por lo que el getmétodo no puede volver null.

Esto también reduce la posibilidad de carreras entre hilos separados que podrían obtener nullpunteros gety luego decidir putuna nueva HashMapcon una sola entrada: la segunda putprobablemente descartaría a la primera, perdiendo el Invoiceobjeto.

Simon Richter
fuente