java c# comparator comparable

java - ¿Por qué compareTo devuelve un entero?



c# comparator (5)

Recientemente vi una discusión en un chat SO pero sin conclusiones claras, así que terminé preguntando allí.

¿Es esto por razones históricas o coherencia con otros idiomas? Al mirar las firmas de compareTo de varios idiomas, devuelve un int .

Por qué no devuelve una enumeración en su lugar. Por ejemplo en C # podríamos hacer:

enum CompareResult {LessThan, Equals, GreaterThan};

y:

public CompareResult CompareTo(Employee other) { if (this.Salary < other.Salary) { return CompareResult.LessThan; } if (this.Salary == other.Salary){ return CompareResult.Equals; } return CompareResult.GreaterThan; }

En Java, los enums se introdujeron después de este concepto (no recuerdo acerca de C #), pero podría haber sido resuelto por una clase extra como:

public final class CompareResult { public static final CompareResult LESS_THAN = new Compare(); public static final CompareResult EQUALS = new Compare(); public static final CompareResult GREATER_THAN = new Compare(); private CompareResult() {} }

y

interface Comparable<T> { Compare compareTo(T obj); }

Lo pregunto porque no creo que un int represente bien la semántica de los datos.

Por ejemplo en C #,

l.Sort(delegate(int x, int y) { return Math.Min(x, y); });

y su gemelo en Java 8,

l.sort(Integer::min);

compila ambos porque Min/min respeta los contratos de la interfaz del comparador (tome dos ints y devuelva un int).

Obviamente los resultados en ambos casos no son los esperados. Si el tipo de devolución era Compare , habría causado un error de compilación, lo que obligaría a implementar un comportamiento "correcto" (o al menos es consciente de lo que está haciendo).

Se pierde una gran cantidad de semántica con este tipo de retorno (y potencialmente puede causar algunos errores difíciles de encontrar), así que ¿por qué diseñarlo de esta manera?


Esta práctica proviene de comparar enteros de esta manera y usar una resta entre los primeros caracteres no coincidentes de una cadena.

Tenga en cuenta que esta práctica es peligrosa con cosas que son parcialmente comparables al usar un -1 para significar que un par de cosas era incomparable. Esto se debe a que podría crear una situación de a <b y b <a (que la aplicación podría usar para definir "incomparable"). Tal situación puede llevar a bucles que no terminan correctamente.

Una enumeración con valores {lt, eq, gt, incomparable} sería más correcta.


Mi entendimiento es que esto se hace porque puede ordenar los resultados (es decir, la operación es reflexiva y transitiva). Por ejemplo, si tiene tres objetos (A, B, C) puede comparar A-> B y B-> C, y usar los valores resultantes para ordenarlos correctamente. Hay una suposición implícita de que si A.compareTo (B) == A.compareTo (C) entonces B == C.

Consulte la documentación del comparator de java.


Responder esto se debe a razones de rendimiento. Si necesita comparar int, como sucede a menudo, puede devolver lo siguiente:

La comparación de hechos a menudo se devuelven como sustracciones.

Como ejemplo

public class MyComparable implements Comparable<MyComparable> { public int num; public int compareTo(MyComparable x) { return num - x.num; } }


[Esta respuesta es para C #, pero probablemente también sea Apple para Java en cierta medida.]

Esto es por razones históricas, de rendimiento y de legibilidad. Potencialmente aumenta el rendimiento en dos lugares:

  1. Donde se implementa la comparación. A menudo, simplemente puede devolver "(lhs - rhs)" (si los valores son tipos numéricos). Pero esto puede ser peligroso: ver más abajo!
  2. El código de llamada puede usar <= y >= para representar naturalmente la comparación correspondiente. Esto utilizará una única instrucción de IL (y, por tanto, de procesador) en comparación con el uso de la enumeración (aunque hay una manera de evitar la sobrecarga de la enumeración, como se describe a continuación)

Por ejemplo, podemos verificar si un valor de lhs es menor o igual a un valor de rhs de la siguiente manera:

if (lhs.CompareTo(rhs) <= 0) ...

Usando una enumeración, se vería así:

if (lhs.CompareTo(rhs) == CompareResult.LessThan || lhs.CompareTo(rhs) == CompareResult.Equals) ...

Eso es claramente menos legible y también es ineficiente ya que está haciendo la comparación dos veces. Puede corregir la ineficiencia utilizando un resultado temporal:

var compareResult = lhs.CompareTo(rhs); if (compareResult == CompareResult.LessThan || compareResult == CompareResult.Equals) ...

Aún es mucho menos legible que la OMI, y aún es menos eficiente, ya que realiza dos operaciones de comparación en lugar de una (aunque admito libremente que es probable que esa diferencia de rendimiento rara vez sea importante).

Como lo señala raznagul a continuación, puedes hacerlo con una sola comparación:

if (lhs.CompareTo(rhs) != CompareResult.GreaterThan) ...

Así que puedes hacerlo bastante eficiente, pero, por supuesto, la legibilidad aún sufre. ... != GreaterThan no es tan claro como ... <=

(Y si usa la enumeración, no puede evitar la sobrecarga de convertir el resultado de una comparación en un valor de enumeración, por supuesto).

Así que esto se hace principalmente por razones de legibilidad, pero también en cierta medida por razones de eficiencia.

Finalmente, como otros han mencionado, esto también se hace por razones históricas. Las funciones como strcmp() y memcmp() C siempre han devuelto ints.

Las instrucciones de comparación de ensambladores también tienden a usarse de manera similar.

Por ejemplo, para comparar dos enteros en el ensamblador x86, puedes hacer algo como esto:

CMP AX, BX ; JLE lessThanOrEqual ; jump to lessThanOrEqual if AX <= BX

o

CMP AX, BX JG greaterThan ; jump to greaterThan if AX > BX

o

CMP AX, BX JE equal ; jump to equal if AX == BX

Puede ver las comparaciones obvias con el valor de retorno de CompareTo ().

Apéndice:

Aquí hay un ejemplo que muestra que no siempre es seguro usar el truco de restar los rhs de las lhs para obtener el resultado de la comparación:

int lhs = int.MaxValue - 10; int rhs = int.MinValue + 10; // Since lhs > rhs, we expect (lhs-rhs) to be +ve, but: Console.WriteLine(lhs - rhs); // Prints -21: WRONG!

Obviamente esto se debe a que la aritmética se ha desbordado. Si hubiera activado la compilación activada, el código anterior de hecho arrojaría una excepción.

Por esta razón, es mejor evitar la optimización de la utilización de la resta para implementar la comparación. (Vea los comentarios de Eric Lippert a continuación.)


Mantengámonos al tanto de los hechos, con un mínimo absoluto de handwaving y / o detalles innecesarios / irrelevantes / dependientes de la implementación.

Como ya te Since: JDK1.0 cuenta, compareTo es tan antiguo como Java ( Since: JDK1.0 desde Integer JavaDoc ); Java 1.0 fue diseñado para ser familiar para los desarrolladores de C / C ++ e imitó muchas de sus opciones de diseño, para bien o para mal. Además, Java tiene una política de compatibilidad con versiones anteriores , por lo tanto, una vez implementado en la biblioteca central, el método está casi obligado a permanecer en él para siempre.

En cuanto a C / C ++ - strcmp / memcmp , que existió durante tanto tiempo como string.h, esencialmente en tanto que en la biblioteca estándar de C, devuelve exactamente los mismos valores (o más bien, compareTo devuelve los mismos valores que strcmp / memcmp ) - vea por ejemplo, C ref - strcmp . En el momento de la creación de Java, ir de esa manera era lo lógico. No había enums en Java en ese momento, no genéricos, etc. (todo lo que vino en> = 1.5)

La decisión de los valores de retorno de strcmp es bastante obvia: en primer lugar, puede obtener 3 resultados básicos en comparación, por lo que seleccionar +1 para "más grande", -1 para "más pequeño" y 0 para "igual" fue lo lógico que hacer. Además, como se señaló, puede obtener el valor fácilmente por sustracción, y devolver int permite usarlo fácilmente en otros cálculos (de una forma tradicional insegura de tipo C), al mismo tiempo que permite una implementación eficiente de operación única.

Si necesita o desea utilizar su interfaz de comparación de seguros de tipos basada en enum , puede hacerlo, pero como la convención de strcmp devuelve +1 / 0 / -1 es tan antigua como la programación contemporánea, en realidad transmite un significado semántico. de la misma manera, null puede interpretarse como unknown/invalid value o un valor int fuera de límites (por ejemplo, un número negativo suministrado para calidad solo positiva) puede interpretarse como un código de error. Tal vez no sea la mejor práctica de codificación, pero ciertamente tiene sus ventajas, y aún se usa comúnmente, por ejemplo, en C.

Por otra parte, preguntar por qué la biblioteca estándar de lenguaje XYZ se ajusta a los estándares heredados del lenguaje ABC es en sí mismo discutible, ya que solo puede ser respondido con precisión por el mismo lenguaje diseñado que lo implementó.

TL; DR es así principalmente porque se hizo así en versiones heredadas por razones heredadas y POLA para programadores en C, y se mantiene así para compatibilidad con versiones anteriores y POLA, nuevamente.

Como nota al margen, considero que esta pregunta (en su forma actual) es demasiado amplia para ser respondida de manera precisa, altamente basada en la opinión y fuera del límite del tema debido a las preguntas directas sobre los patrones de diseño y la arquitectura del lenguaje .