Al venir de C++ a Java, la pregunta obvia sin respuesta es ¿por qué Java no incluyó la sobrecarga de operadores?
¿No es Complex a, b, c; a = b + c;
mucho más simple que Complex a, b, c; a = b.add(c);
?
¿Existe una razón conocida para esto, argumentos válidos para no permitiendo la sobrecarga de operadores? ¿Es la razón arbitraria, o se pierde en el tiempo?
Suponiendo que desea sobrescribir el valor anterior del objeto al que hace referencia a
, se deberá invocar una función miembro.
Complex a, b, c;
// ...
a = b.add(c);
En C++, esta expresión le dice al compilador que cree tres (3) objetos en la pila, realice una adición y copie el valor resultante del objeto temporal en el objeto existente a
.
Sin embargo, en Java, operator=
no realiza una copia de valor para los tipos de referencia y los usuarios solo pueden crear nuevos tipos de referencia, no tipos de valor. Entonces, para un tipo definido por el usuario llamado Complex
, la asignación significa copiar una referencia a un valor existente.
Considere en su lugar:
b.set(1, 0); // initialize to real number '1'
a = b;
b.set(2, 0);
assert( !a.equals(b) ); // this assertion will fail
En C++, esto copia el valor, por lo que la comparación resultará no igual. En Java, operator=
realiza una copia de referencia, por lo que a
y b
ahora se refieren al mismo valor. Como resultado, la comparación producirá "igual", ya que el objeto se comparará igual a sí mismo.
La diferencia entre copias y referencias solo aumenta la confusión de la sobrecarga del operador. Como mencionó @Sebastian, Java y C # tienen que lidiar con el valor y la igualdad de referencia por separado: operator+
probablemente tratará con valores y objetos, pero operator=
ya está implementado para tratar con las referencias.
En C++, solo debe tratar con un tipo de comparación a la vez, por lo que puede ser menos confuso. Por ejemplo, en Complex
, operator=
y operator==
están trabajando en valores, copiando valores y comparando valores respectivamente.
Hay una gran cantidad de publicaciones quejándose de la sobrecarga del operador.
Sentí que tenía que aclarar los conceptos de "sobrecarga del operador", ofreciendo un punto de vista alternativo sobre este concepto.
Este argumento es una falacia.
Es tan fácil ofuscar código en C o Java a través de funciones/métodos como en C++ a través de sobrecargas de operadores:
// C++
T operator + (const T & a, const T & b) // add ?
{
T c ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
// Java
static T add (T a, T b) // add ?
{
T c = new T() ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
/* C */
T add (T a, T b) /* add ? */
{
T c ;
c.value = a.value - b.value ; /* subtract !!! */
return c ;
}
Para otro ejemplo, veamos la interfaz Cloneable
en Java:
Se supone que debes clonar el objeto implementando esta interfaz. Pero podrías mentir. Y crea un objeto diferente. De hecho, esta interfaz es tan débil que podría devolver otro tipo de objeto por completo, solo por diversión:
class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}
Como la interfaz Cloneable
puede ser objeto de abuso/ofuscación, ¿debería prohibirse por la misma razón que se supone que la sobrecarga del operador de C++ es?
Podríamos sobrecargar el método toString()
de una clase MyComplexNumber
para que devuelva la hora del día. ¿Debería prohibirse también la sobrecarga toString()
? Podríamos sabotear MyComplexNumber.equals
para que devuelva un valor aleatorio, modifique los operandos ... etc. etc. etc.
En Java, como en C++, o cualquier otro lenguaje, el programador debe respetar un mínimo de semántica al escribir código. Esto significa implementar una función add
que agregue, y un método de implementación Cloneable
que clone, y un operador ++
que incrementos.
Ahora que sabemos que el código puede ser saboteado incluso a través de los métodos Java prístinos, podemos preguntarnos sobre el uso real de la sobrecarga de operadores en C++.
A continuación, compararemos, para diferentes casos, el "mismo" código en Java y C++, para tener una idea de qué tipo de estilo de codificación es más claro.
// C++ comparison for built-ins and user-defined types
bool isEqual = A == B ;
bool isNotEqual = A != B ;
bool isLesser = A < B ;
bool isLesserOrEqual = A <= B ;
// Java comparison for user-defined types
boolean isEqual = A.equals(B) ;
boolean isNotEqual = ! A.equals(B) ;
boolean isLesser = A.comparesTo(B) < 0 ;
boolean isLesserOrEqual = A.comparesTo(B) <= 0 ;
Tenga en cuenta que A y B pueden ser de cualquier tipo en C++, siempre y cuando se proporcionen las sobrecargas del operador. En Java, cuando A y B no son primitivos, el código puede llegar a ser muy confuso, incluso para objetos de tipo primitivo (BigInteger, etc.) ...
// C++ container accessors, more natural
value = myArray[25] ; // subscript operator
value = myVector[25] ; // subscript operator
value = myString[25] ; // subscript operator
value = myMap["25"] ; // subscript operator
myArray[25] = value ; // subscript operator
myVector[25] = value ; // subscript operator
myString[25] = value ; // subscript operator
myMap["25"] = value ; // subscript operator
// Java container accessors, each one has its special notation
value = myArray[25] ; // subscript operator
value = myVector.get(25) ; // method get
value = myString.charAt(25) ; // method charAt
value = myMap.get("25") ; // method get
myArray[25] = value ; // subscript operator
myVector.set(25, value) ; // method set
myMap.put("25", value) ; // method put
En Java, vemos que para cada contenedor hacer lo mismo (acceder a su contenido a través de un índice o identificador), tenemos una forma diferente de hacerlo, lo cual es confuso.
En C++, cada contenedor utiliza la misma forma de acceder a su contenido, gracias a la sobrecarga del operador.
Los siguientes ejemplos utilizan un objeto Matrix
, que se encuentra utilizando los primeros enlaces encontrados en Google para " Java Matrix object " y " c ++ Matrix object ":
// C++ YMatrix matrix implementation on CodeProject
// http://www.codeproject.com/KB/architecture/ymatrix.aspx
// A, B, C, D, E, F are Matrix objects;
E = A * (B / 2) ;
E += (A - B) * (C + D) ;
F = E ; // deep copy of the matrix
// Java JAMA matrix implementation (seriously...)
// http://math.nist.gov/javanumerics/jama/doc/
// A, B, C, D, E, F are Matrix objects;
E = A.times(B.times(0.5)) ;
E.plusEquals(A.minus(B).times(C.plus(D))) ;
F = E.copy() ; // deep copy of the matrix
Y esto no se limita a las matrices. Las clases BigInteger
y BigDecimal
de Java sufren de la misma verbosidad confusa, mientras que sus equivalentes en C++ son tan claros como los tipos incorporados.
// C++ Random Access iterators
++it ; // move to the next item
--it ; // move to the previous item
it += 5 ; // move to the next 5th item (random access)
value = *it ; // gets the value of the current item
*it = 3.1415 ; // sets the value 3.1415 to the current item
(*it).foo() ; // call method foo() of the current item
// Java ListIterator<E> "bi-directional" iterators
value = it.next() ; // move to the next item & return the value
value = it.previous() ; // move to the previous item & return the value
it.set(3.1415) ; // sets the value 3.1415 to the current item
// C++ Functors
myFunctorObject("Hello World", 42) ;
// Java Functors ???
myFunctorObject.execute("Hello World", 42) ;
// C++ stream handling (with the << operator)
stringStream << "Hello " << 25 << " World" ;
fileStream << "Hello " << 25 << " World" ;
outputStream << "Hello " << 25 << " World" ;
networkStream << "Hello " << 25 << " World" ;
anythingThatOverloadsShiftOperator << "Hello " << 25 << " World" ;
// Java concatenation
myStringBuffer.append("Hello ").append(25).append(" World") ;
Bien, en Java también puede usar MyString = "Hello " + 25 + " World" ;
... Pero espere un segundo: esto es una sobrecarga de operadores, ¿no es así? ¿No es trampa?
:-RE
Los mismos operandos modificadores de código genérico deben ser utilizables tanto para elementos integrados/primitivos (que no tienen interfaces en Java), objetos estándar (que no podrían tener la interfaz correcta) como objetos definidos por el usuario.
Por ejemplo, calculando el valor promedio de dos valores de tipos arbitrarios:
// C++ primitive/advanced types
template<typename T>
T getAverage(const T & p_lhs, const T & p_rhs)
{
return (p_lhs + p_rhs) / 2 ;
}
int intValue = getAverage(25, 42) ;
double doubleValue = getAverage(25.25, 42.42) ;
complex complexValue = getAverage(cA, cB) ; // cA, cB are complex
Matrix matrixValue = getAverage(mA, mB) ; // mA, mB are Matrix
// Java primitive/advanced types
// It won't really work in Java, even with generics. Sorry.
Ahora que hemos visto comparaciones justas entre el código C++ que usa la sobrecarga de operadores y el mismo código en Java, ahora podemos hablar de la "sobrecarga de operadores" como un concepto.
Incluso fuera de la informática, existe una sobrecarga de operadores: por ejemplo, en matemáticas, los operadores como +
, -
, *
, etc. están sobrecargados.
De hecho, la significación de +
, -
, *
, etc. cambia dependiendo de los tipos de los operandos (números, vectores, funciones de onda cuántica, matrices, etc.).
La mayoría de nosotros, como parte de nuestros cursos de ciencias, aprendimos múltiples significados para los operadores, según los tipos de operandos. ¿Los encontramos confusos, ellos?
Esta es la parte más importante de la sobrecarga de operadores: como en matemáticas o en física, la operación depende de los tipos de sus operandos.
Entonces, conozca el tipo de operando, y usted sabrá el efecto de la operación.
En C, el comportamiento real de un operador cambiará de acuerdo con sus operandos. Por ejemplo, agregar dos enteros es diferente a agregar dos dobles, o incluso un entero y uno doble. Incluso existe todo el dominio aritmético del puntero (sin la conversión, puede agregar a un puntero un entero, pero no puede agregar dos punteros ...).
En Java, no hay aritmética de punteros, pero alguien todavía encontró que la concatenación de cadenas sin el operador +
sería lo suficientemente ridícula como para justificar una excepción en el credo de "la sobrecarga de operadores es mala".
Es solo que usted, como C (por razones históricas) o Java (por razones personales, ver más abajo) codificador, no puede proporcionar el suyo propio.
En C++, la sobrecarga de operadores para los tipos incorporados no es posible (y esto es algo bueno), pero usuario definido tipos pueden tener usuario definido Sobrecargas del operador.
Como ya se dijo anteriormente, en C++, y al contrario que en Java, los tipos de usuarios no se consideran ciudadanos de segunda clase del lenguaje, en comparación con los tipos incorporados. Por lo tanto, si los tipos incorporados tienen operadores, los tipos de usuario también deberían poder tenerlos.
La verdad es que, como los métodos toString()
, clone()
, equals()
son para Java (es decir, casi estándar), La sobrecarga de operadores de C++ es una parte tan importante de C++ que se vuelve tan natural como los operadores de C originales o los métodos de Java mencionados anteriormente.
Combinada con la programación de plantillas, la sobrecarga del operador se convierte en un patrón de diseño bien conocido. De hecho, no puede ir muy lejos en STL sin utilizar operadores sobrecargados, y sobrecargar operadores para su propia clase.
La sobrecarga del operador debe esforzarse por respetar la semántica del operador. No reste en un operador +
(como en "no restar en una función add
", o "devuelva la basura en un método clone
").
La sobrecarga de fundición puede ser muy peligrosa porque puede llevar a ambigüedades. Así que realmente deberían estar reservados para casos bien definidos. En cuanto a &&
y ||
, no los sobrecargue a menos que realmente sepa lo que está haciendo, ya que perderá la evaluación de cortocircuito que disfrutan los operadores nativos &&
y ||
.
Porque James Gosling lo dijo así:
Dejé fuera la sobrecarga del operador como elección bastante personal porque había visto demasiada gente abusar de él en C++.
James gosling Fuente: http://www.gotw.ca/publications/c_family_interview.htm
Por favor compare el texto de Gosling anterior con el de Stroustrup a continuación:
Muchas decisiones de diseño de C++ tienen sus raíces en mi disgusto por forzar a las personas a hacer las cosas de alguna manera en particular. A menudo, [...] tuve la tentación de prohibir una característica que personalmente no me gustaba, me abstuve de hacerlo porque No pensé que tuviera derecho a forzar mi opinión sobre otros.
Bjarne Stroustrup. Fuente: El diseño y la evolución de C++ (1.3 Antecedentes generales)
Algunos objetos se beneficiarían enormemente de la sobrecarga del operador (tipos concretos o numéricos, como BigDecimal, números complejos, matrices, contenedores, iteradores, comparadores, analizadores, etc.).
En C++, puede beneficiarse de este beneficio debido a la humildad de Stroustrup. En Java, simplemente estás jodido debido a Gosling decisión personal.
Las razones para no agregar la sobrecarga de operadores ahora en Java podrían ser una mezcla de políticas internas, alergia a la función, desconfianza de los desarrolladores (ya saben, los saboteadores que parecen acosar a los equipos de Java ...), compatibilidad con las JVM anteriores, tiempo para escribir una especificación correcta, etc.
Así que no aguantes la respiración esperando esta característica ...
Sí...
Si bien esto está lejos de ser la única diferencia entre los dos idiomas, este nunca deja de divertirme.
Al parecer, la gente de C #, con su "cada primitiva es una struct
, y una struct
deriva del Objeto", lo tengo justo en el primer intento.
A pesar de toda la FUD contra la sobrecarga del operador definido usado, los siguientes idiomas lo soportan: Scala , Dart , Python , F # , C # , D , ALGOL 68 , Smalltalk , Groovy , Perl 6 , C++, Ruby , Haskell , MATLAB , Eiffel , Lua , Clojure , Fortran 90 , Swift , Ada , Delphi 2005 ...
Tantos lenguajes, con tantas filosofías diferentes (ya veces opuestas), y sin embargo todos están de acuerdo en ese punto.
Comida para el pensamiento...
James Gosling comparó el diseño de Java con lo siguiente:
"Hay un principio sobre la mudanza, cuando te mudas de un apartamento a otro. Un experimento interesante es empacar tu apartamento y poner todo en cajas, luego pasar al siguiente apartamento y no desempacar nada hasta que lo necesites. Así que ' está haciendo su primera comida y está sacando algo de una caja. Luego, después de un mes o más, lo ha usado para averiguar qué cosas en su vida realmente necesita, y luego toma el resto de la Cosas: olvida lo mucho que le gusta o lo genial que es, y simplemente lo tira. Es increíble cómo eso simplifica su vida, y puede usar ese principio en todo tipo de problemas de diseño: no haga las cosas solo porque son geniales o simplemente porque son interesantes ".
Puede leer el contexto de la cita aquí
Básicamente, la sobrecarga del operador es excelente para una clase que modela algún tipo de punto, moneda o número complejo. Pero después de eso, te quedas sin ejemplos rápidamente.
Otro factor fue el abuso de la función en C++ por parte de los desarrolladores que sobrecargan a los operadores como '&&', '||', los operadores de reparto y, por supuesto, 'nuevo'. La complejidad que resulta de combinar esto con el paso por valor y las excepciones está bien cubierta en Exceptional C++ book.
Echa un vistazo a Boost.Units: enlace de texto
Proporciona un análisis dimensional de cero sobrecarga a través de la sobrecarga del operador. ¿Cuánto más claro puede conseguir esto?
quantity<force> F = 2.0*newton;
quantity<length> dx = 2.0*meter;
quantity<energy> E = F * dx;
std::cout << "Energy = " << E << endl;
en realidad daría como resultado "Energía = 4 J" que es correcto.
Los diseñadores de Java decidieron que la sobrecarga del operador era más problemática de lo que valía. Simple como eso.
En un lenguaje donde cada variable de objeto es en realidad una referencia, la sobrecarga del operador tiene el riesgo adicional de ser bastante ilógica, al menos para un programador de C++. Compare la situación con la sobrecarga del operador == de C # y Object.Equals
y Object.ReferenceEquals
(o como se llame).
Groovy tiene sobrecarga de operadores y se ejecuta en la JVM. Si no te importa el impacto de rendimiento (que se reduce cada día). Es automático basado en nombres de métodos. por ejemplo, '+' llama al método 'más (argumento)'.
Creo que esta puede haber sido una elección de diseño consciente para obligar a los desarrolladores a crear funciones cuyos nombres comuniquen claramente sus intenciones. En C++, los desarrolladores sobrecargarían a los operadores con una funcionalidad que a menudo no tendría relación con la naturaleza comúnmente aceptada del operador dado, haciendo que sea casi imposible determinar qué hace una pieza de código sin mirar la definición del operador.
Bueno, realmente puedes dispararte en el pie con la sobrecarga del operador. Es como con los punteros las personas cometen errores estúpidos con ellos y así se decidió quitar las tijeras.
Al menos creo que esa es la razón. Estoy de tu lado de todos modos. :)
Algunas personas dicen que la sobrecarga de operadores en Java llevaría a la ofuscación. ¿Alguna vez se detuvo esa gente para ver un código Java que hace algunos cálculos básicos, como aumentar un valor financiero en un porcentaje utilizando BigDecimal? .... La verbosidad de tal ejercicio se convierte en su propia demostración de la ofuscación. Irónicamente, agregar una sobrecarga de operadores a Java nos permitiría crear nuestra propia clase de Moneda que haría que este código matemático fuera elegante y simple (menos confuso).
Técnicamente, existe una sobrecarga de operadores en cada lenguaje de programación que puede manejar diferentes tipos de números, por ejemplo. Números enteros y reales. Explicación: el término sobrecarga significa que simplemente hay varias implementaciones para una función. En la mayoría de los lenguajes de programación, se proporcionan diferentes implementaciones para el operador +, una para los enteros, una para los reales, esto se denomina sobrecarga del operador.
Ahora, a muchas personas les resulta extraño que Java tenga una sobrecarga de operadores para el operador + para agregar cadenas, y desde un punto de vista matemático, esto sería realmente extraño, pero visto desde el punto de vista del desarrollador de un lenguaje de programación, no hay nada de malo en agregar la sobrecarga incorporada del operador. para el operador + para otras clases por ejemplo Cuerda. Sin embargo, la mayoría de la gente está de acuerdo en que una vez que agregue la sobrecarga incorporada para + para String, entonces generalmente es una buena idea proporcionar esta funcionalidad también para el desarrollador.
Está completamente en desacuerdo con la falacia de que la sobrecarga del operador oculta el código, ya que esto queda para que el desarrollador lo decida. Esto es ingenuo pensar, y para ser honesto, está envejeciendo.
+1 para agregar la sobrecarga de operadores en Java 8.
Decir que la sobrecarga del operador conduce a errores lógicos de tipo que el operador no coincide con la lógica de operación, es como no decir nada. El mismo tipo de error ocurrirá si el nombre de la función no es apropiado para la lógica de operación. ¿Cuál es la solución: eliminar la capacidad de uso de la función? Esta es una respuesta cómica: "Inapropiado para la lógica de operación", cada nombre de parámetro, cada clase, función o lo que sea lógicamente inapropiado. Creo que esta opción debería estar disponible en un lenguaje de programación respetable, y aquellos que piensan que es inseguro. Oye, ambos dicen que tienes que usarlo. Vamos a tomar el C #. Inclinaron los punteros pero, por su cuenta y riesgo, hay una declaración de "código no seguro" que usted desea.
Suponiendo que Java sea el lenguaje de implementación, entonces a, b y c serían referencias al tipo Complejo con valores iniciales de nulo. También asumiendo que el Complejo es inmutable como el mencionado BigInteger y similar inmutable BigDecimal , Creo que te refieres a lo siguiente, ya que estás asignando la referencia al Complejo devuelto al agregar b y c y no comparando esta referencia con una.
No es
Complex a, b, c; a = b + c;
mucho más simple que:
Complex a, b, c; a = b.add(c);
A veces sería bueno tener sobrecarga de operadores, clases de amigos y herencia múltiple.
Sin embargo sigo pensando que fue una buena decisión. Si Java hubiera tenido una sobrecarga de operadores, nunca podríamos estar seguros de los significados de los operadores sin revisar el código fuente. En la actualidad eso no es necesario. Y creo que su ejemplo de uso de métodos en lugar de sobrecarga de operadores también es bastante legible. Si desea aclarar las cosas, siempre puede agregar un comentario sobre las frases peludas.
// a = b + c
Complex a, b, c; a = b.add(c);
Alternativas a la compatibilidad nativa de la sobrecarga del operador Java
Como Java no tiene una sobrecarga de operadores, aquí hay algunas alternativas que puede considerar:
Si alguien está al tanto de otros, por favor comente, y lo agregaré a esta lista.
Esta no es una buena razón para rechazarla, sino una práctica:
La gente no siempre lo usa responsablemente. Mira este ejemplo de la biblioteca de Python scapy:
>>> IP()
<IP |>
>>> IP()/TCP()
<IP frag=0 proto=TCP |<TCP |>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP frag=0 proto=TCP |<TCP |>>>
>>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"
<IP frag=0 proto=TCP |<TCP |<Raw load='GET / HTTP/1.0\r\n\r\n' |>>>
>>> Ether()/IP()/IP()/UDP()
<Ether type=0x800 |<IP frag=0 proto=IP |<IP frag=0 proto=UDP |<UDP |>>>>
>>> IP(proto=55)/TCP()
<IP frag=0 proto=55 |<TCP |>>
Aquí está la explicación:
El operador/se ha utilizado como operador de composición entre dos capas. Al hacerlo, la capa inferior puede tener uno o más de sus campos predeterminados sobrecargados de acuerdo con la capa superior. (Todavía puedes dar el valor que quieras). Una cadena se puede utilizar como una capa en bruto.