¿Por qué debería usarse un HashMap (en funciones) para determinar qué valor devolver (para una clave) cuando una construcción if else puede hacer el trabajo en un mejor momento?

9

Mientras trabajaba recientemente en una gran empresa, noté que los programadores seguían este estilo de codificación:

Supongamos que tengo una función que devuelve 12 si la entrada es A, 21 si la entrada es B y 45 si la entrada es C.

Entonces puedo escribir la firma de la función como:

int foo(String s){
    if(s.equals("A"))      return 12;
    else if(s.equals("B")) return 21;
    else if(s.equals("C")) return 45;
    else throw new RuntimeException("Invalid input to function foo");
}

Pero en la revisión del código me pidieron que cambiara la función a la siguiente:

int foo(String s){
    HashMap<String, Integer> map = new HashMap<String, Integer>();
    map.put("A", 12);
    map.put("B", 21);
    map.put("C", 45);
    return map.get(s);
}

No puedo convencerme de por qué el segundo código es mejor que el primero. El segundo código definitivamente tomaría más tiempo en ejecutarse.

La única razón para usar el segundo código puede ser que ofrece una mejor legibilidad. Pero si la función se llama muchas veces, ¿la segunda función no ralentizaría el tiempo de ejecución de la utilidad que la llama?

¿Qué piensas sobre esto?

Nikunj Banka
fuente
44
Para tres valores, un mapa parece excesivo ( switchparece más apropiado que if-else). Pero en algún momento, se vuelve problemático. La principal ventaja de usar un Mapa es que puede cargarlo desde un archivo o una tabla, etc. Si está codificando la entrada al mapa, no veo mucho valor en un interruptor.
JimmyJames

Respuestas:

16

El punto es mover la creación del hashmap fuera de la función y hacerlo una vez (o solo menos veces que de otra manera).

private static final Map<String, Integer> map;
static{
    Map<String, Integer> temp = new HashMap<String, Integer>();
    temp.put("A", 12);
    temp.put("B", 21);
    temp.put("C", 45);
    map = Collections.unmodifiableMap(temp);//make immutable
}

int foo(String s){
    if(!map.containsKey(s))
        throw new RuntimeException("Invalid input to function foo");

    return map.get(s);
}

Sin embargo, java desde java7 ha podido tener cadenas (finales) en los conmutadores:

int foo(String s){
    switch(s){
    case "A":
        return 12;
    case "B": 
        return 21;
    case "C": 
        return 45;
    default: throw new RuntimeException("Invalid input to function foo");
}
monstruo de trinquete
fuente
1
No veo cómo esto responde la pregunta de los OP, así que -1 allí. Pero +1 por sugerir un cambio.
user949300
Muestra cómo implementar correctamente el estilo de codificación para tener sentido y posiblemente mejorar el rendimiento. Todavía no tiene sentido para 3 opciones, pero el código original probablemente fue mucho más largo.
Florian F
12

En su segundo ejemplo, Mapdebe ser un miembro estático privado para evitar la sobrecarga de inicialización redundante.

Para grandes cantidades de valores, el mapa funcionará mejor. Usando una tabla hash, uno puede buscar la respuesta en tiempo constante. La construcción de if múltiple tiene que comparar la entrada con cada una de las posibilidades hasta que encuentre la respuesta correcta.

En otras palabras, la búsqueda del mapa es O (1) mientras que ifs son O (n) donde n es el número de entradas posibles.

La creación del mapa es O (n), pero solo se realiza una vez si es un estado constante estático. Para una búsqueda realizada con frecuencia, el mapa superará a las ifdeclaraciones a largo plazo, a costa de un poco más de tiempo cuando se inicia el programa (o la clase se carga, según el idioma).

Dicho esto, el mapa no siempre es la herramienta adecuada para este trabajo. Es bueno cuando hay muchos valores, o los valores deben ser configurables a través de un archivo de texto, entrada del usuario o base de datos (en cuyo caso el mapa actúa como caché).


fuente
Sí, para grandes cantidades de valores, el mapa funcionará mejor. Pero la cantidad de valores es fija y son tres.
RemcoGerlich
la creación del mapa es O (N) solo la búsqueda en él es O (1).
Pieter B
Buenos puntos, aclaré mi respuesta.
El mapa también requiere unboxing automático que afecta el rendimiento muy ligeramente.
user949300
@ user949300 en Java, sí, y el código en la pregunta parece ser Java. Sin embargo, no está etiquetado con ningún lenguaje, y el enfoque funciona en varios idiomas (incluidos C # y C ++, ninguno de los cuales requiere boxeo).
3

Hay dos velocidades en el software: el tiempo que lleva escribir / leer / depurar el código; y el tiempo que lleva ejecutar el código.

Si puede convencerme (y a sus revisores de código) de que la función hashmap es realmente más lenta que if / then / else (después de refactorizar para hacer un hashmap estático) Y puede convencerme / revisores de que se ha llamado suficientes veces para hacer un real diferencia, luego continúe y reemplace el hashmap con el if / else.

De lo contrario, el código hashmap es legible por completo; y (probablemente) libre de errores; puedes determinar eso rápidamente con solo mirarlo. Realmente no se puede decir lo mismo sobre el if / else sin realmente estudiarlo; la diferencia es aún más exagerada cuando hay cientos de opciones.

Robert Hanson
fuente
3
Bueno, esa objeción se desmorona cuando uno se compara con un interruptor en su lugar ...
Deduplicator
También se desmorona si escribe las declaraciones if en una sola línea.
gnasher729
2
Al colocar la creación de hashmap en otro lugar, solo hace que sea más difícil descubrir qué sucede realmente. Necesita ver esas teclas y valores para saber cuál es el efecto real de la función.
RemcoGerlich
2

Prefiero la respuesta de estilo HashMap.

Hay una métrica para esto

Hay una métrica de calidad de código llamada Complejidad ciclomática . Esta métrica básicamente cuenta el número de rutas diferentes a través del código ( cómo calcular la Complejidad Ciclomática ).

Para cada ruta de ejecución posible, un método se vuelve cada vez más difícil de comprender Y probar completamente para su corrección.

Se reduce al hecho de que "palabras clave de control" como: ifs, elses, whiles, etc ... aprovechan las pruebas booleanas que pueden estar equivocadas. El uso repetido de "palabras clave de control" produce código frágil.

Beneficios adicionales

Además, el "enfoque basado en mapas" alienta a los desarrolladores a pensar en los pares de entrada-salida como un conjunto de datos que se puede extraer, reutilizar, manipular en tiempo de ejecución, probar y verificar. Por ejemplo, a continuación reescribí "foo" para que no estemos permanentemente encerrados en "A-> 12, B-> 21, C-> 45":

int foo(String s){
    HashMap<String, Integer> map = getCurrentMapping();
    return map.get(s);
}

rachet_freak menciona este tipo de refactor en su respuesta, argumenta a favor de la velocidad y la reutilización, estoy defendiendo la flexibilidad en el tiempo de ejecución (aunque usar una colección inmutable puede tener enormes beneficios dependiendo de la situación)

Ivan
fuente
1
Agregar esa flexibilidad en el tiempo de ejecución es una gran idea de futuro o un sobreenfriamiento innecesario que hace que sea mucho más difícil darse cuenta de lo que está sucediendo. :-).
user949300
@JimmyJames El enlace funciona para mí: es: leepoint.net/principles_and_practices/complexity/…
Ivan
1
@ user949300 El punto es que los datos clave / valor que respaldan el método "foo" es un concepto separado que podría merecer alguna forma de claridad. La cantidad de código que desea escribir para aislar el Mapa dependerá en gran medida de cuántos elementos contiene el Mapa y con qué frecuencia esos elementos pueden necesitar cambiar.
Ivan
1
@ user949300 Parte del motivo por el que sugiero retirar la cadena if-else es que he visto que existen cadenas if-else en grupos. Como cucarachas similares, si hay un método basado en una cadena if-else, es probable que haya otro método en otra parte de la base de código con una cadena if-else similar / idéntica. Extraer el mapa paga dividendos si suponemos que podría haber otros métodos que usan una estructura lógica similar de búsqueda / cambio.
Ivan
Yo también he visto bloques duplicados, casi idénticos si / si no dispersos por todo el lugar. Bien dicho
Jon Chesterfield
1

Los datos son mejores que el código. No menos importante porque es demasiado tentador agregar otra rama al código, pero agregar una fila a una tabla es difícil de equivocar. Esta pregunta es una pequeña instancia de esto. Estás escribiendo una tabla de búsqueda. Escriba una implementación, complete con lógica y documentación condicional, o escriba la tabla y luego búsquela.

Una tabla de datos es siempre una mejor representación de algunos datos que el código, pasa la optimización del módulo. Lo difícil que es expresar la tabla puede depender del lenguaje: no conozco Java, pero espero que pueda implementar una tabla de búsqueda más simple que el ejemplo en el OP.

Esta es una tabla de búsqueda en python. Si esto se considera un conflicto atractivo, tenga en cuenta que la pregunta no está etiquetada como Java, la refactorización es independiente del lenguaje y que la mayoría de las personas no conocen Java.

def foo(s):
    return {
               "A" : 12,
               "B" : 21,
               "C" : 45,
           }[s]

La idea de reestructurar el código para reducir el tiempo de ejecución tiene mérito, pero prefiero usar un compilador que incorpore la configuración común que hacerlo yo mismo.

Jon Chesterfield
fuente
-1 no es una respuesta a la pregunta.
Pieter B
¿En qué sentido? Le habría pedido al autor que reemplazara la cadena if else con un mapa sobre la base de que los datos y el código deberían estar separados. Esta es una razón clara por la cual el segundo código es mejor que el primero, aunque todas las llamadas a map.put son desafortunadas.
Jon Chesterfield
2
@ JonChesterfield Esta respuesta es básicamente "usar un mejor lenguaje", lo cual rara vez es útil.
walpen
@walpen fair point. Ivan ha hecho más o menos la misma observación a través de Java, por lo que claramente no necesité ir a Python. Veré si puedo limpiarlo un poco
Jon Chesterfield
En algunos códigos antiguos de Java, necesitaba algunos mapas para algo muy similar, así que escribí una pequeña utilidad para convertir matrices N-dimensionales de matrices 2-D en un mapa. Un poco hacky pero funcionó bien. Esta respuesta señala el poder de Python / JS para admitir la notación JSONy de manera tan simple y fácil.
user949300