4 de diciembre de 2020

Comparación de objetos (equals vs Comparable y Comparator)

Vimos que equals es un miembro de Object. Sirve para comparar dos objetos y obtener si son iguales/equivalentes.

Por otra parte existen las interfaces Comparable y Comparator que sirven también para comparar dos objetos, pero las usaremos para ordenar pues nos pueden decir cuan iguales/distintos son dos objetos midiéndolo con un entero (int). Las diferencias entre estas dos formas de comparar las podemos resumir en:
  1. Comparable/Comparator es una interfaz mientras que equals es un miembro de todos los objetos (todos tienen una implementación).
  2. Todos los objetos pueden comparar su igualdad (usar equals), pero sólo los objetos con Comparable implementada tienen lo que se llama un orden natural con el que ordenarse por defecto.
  3. Comparable/Comparator admiten un tipo variable T (el tipo que pueden comparar). La definición de este tipo variable afectara al parámetro de su único método a implementar: compareTo(T o)/compare(T o1, T o2).
La diferencia fundamental entre Comparable y Comparator es el dónde y el para qué de su funcionalidad:
  • Dónde:
    • Comparable necesita implementar el método compareTo(T o) como miembro del objeto que se va a comparar con el parámetro o, y son del mismo tipo T (teniendo en cuenta que los subtipos son del tipo de su supertipo).
    • Comparator necesita implementar el método compare(T o1, T o2) como miembro del objeto comparador que se encargará de comparar o1 con o2: el comparador es un objeto independiente que no tiene relación con o1 ni o2.
  • Para qué: Comparable va a establecer un orden natural para el tipo mientras que Comparator se puede usar para hacer una ordenación puntual, pudiendo tener varios comparadores para ordenar por distintos criterios.

equals nos dice si dos valores son iguales, Comparable/Comparator nos dice cuan iguales son y nos permite ordenar


¿Cómo funcionan?


El único método que debe implementar cada una debe devolvernos:
  1. un número negativo si o1 es menor que o2,
  2. cero si son iguales (no implica que o1.equals(o2) sea true) o
  3. positivo si es mayor.
De esta forma podemos medir cuan distintos son dos objetos y por tanto ordenarlos, lo cual es imposible con equals que devuelve un boolean.

Se recomienda encarecidamente que ambos métodos sean coherentes entendido como: o1.compareTo(o2)/compare(o1, o2) == 0 debería dar el mismo resultado que o1.equals(o2).

¿Cuándo usamos cada una?

Desde mi experiencia yo tiendo a generar comparadores antes que a implementar Comparable. Mis motivos son:
  1. Puedo ordenar un tipo incluso si no tengo acceso al código para implementar Comparable
  2. Puedo disponer de varios comparadores como constantes (static final) en un tipo para poder ordenarlos fácilmente, mientras que sólo puedo tener una implementación para Comparable
  3. Comparator es una interfaz funcional y puedes instanciarlos con lambdas desde Java 8. El código queda muy legible y compacto con lo que no es un problema crearlos como antiguamente que había que hacer un tipo sólo para eso.
Sólo se puede implementar Comparable una vez, si un supertipo ya lo tiene implementado habrá que sobrescribir el método compareTo si debe ser distinta implementación y las comparaciones dos-a-dos podrían salir inconsistentes (ver type erasure cuando se hable de genéricos).

El consejo definitivo es: haz un comparador a menos que el diseño de un código en concreto te obligue a implementar Comparable (por ejemplo que tenga que usarse el tipo como clave de un mapa ordenado o que un tipo genérico imponga la interface Comparable).

Si no estás obligado a usar Comparable mejor hazte un Comparator para tener el código de ordenamiento separado

Para ver el uso de Comparator termino la entrada añadiendo éste código al de la entrada anterior que ordena los vehículos por su color:

System.out.println("\nLista ordenada (por color):");
vehiculos.sort((v1, v2) -> v1.getColor().compareTo(v2.getColor()));
vehiculos.forEach(System.out::println);
Así queda más limpio y separado el código de vehículos y el de ordenación para un caso concreto. Si te fijas en los métodos de Comparator verás cómo han potenciado la interfaz desde Java 8 y lo fácil que es tener un comparador que ordene por orden inverso, usando una clave específica (Comparable) o que trate los valores null de forma distinta. Todo son facilidades.

Compárteme

Entradas populares