Por qué java no soporta la sobre carga de operadores
1. ¿Por qué java no soporta la sobre carga de
operadores?
Java no soporta sobrecarga de operadores, aunque el sistema internamente la utiliza, pero está
ocultada al programador, por ejemplo si te fijas al hacer un
Int ejemplo=2+1;
En java eso es válido y le asigna un 3 a la variable ejemplo la funcionalidad fue sumar, sin embargo
en este:
String ejemplo="hola"+"mundo";
El operador + se utiliza con otra finalidad : Concatenar , lo ves? Java si usa la sobrecarga de
operadores, pero esta oculta para el programador, ósea no te permite usarla.
¿Por qué?
Básicamente para no llevar a confusión , imagina que defines una clase llamada "sobreEscribe" en
la cual sobrecargaras el operador suma y le asignaras tu propia forma de trabajar por ejemplo sumar
fracciones y más tarde defines la clase "fracciones" heredando la clase "sobreEscribe" el resultado
sería que en la clase "fracciones" el operador suma cambiara su comportamiento. Si llegase ahora
un programador externo y revisara la clase fracciones podría confundirse y creer que este operador
mantiene el comportamiento normal, para saber que tiene el comportamiento actual tendría que ir a
revisar "sobreEscribe", ahora imagina que esta herencia se realiza entre varias clases, sería muy
difícil saber si realmente se está cambiando el comportamiento del operador suma o no. Esta es una
de las razones más fuertes por las cuales Java no soporta la sobrecarga de operadores básicamente
para no llevar a confusión y al fin y al cabo no es necesaria, puedes utilizar tus propios métodos
¿Qué es la sobrecarga de operadores?
En C++ se pueden hacer operaciones con los tipos básicos del lenguaje: se pueden sumar enteros,
compararlos, etc. Lo siguiente es perfectamente válido en C++
int a=3;
int b=4;
int c;
c = a + b;
Si estos tipos no son los básicos del lenguaje, no se puede hacer sumas con ellos. Por ejemplo, si
ClaseC es una clase que tengo definida en C++, el siguiente código dará error.
ClaseC a;
ClaseC b;
ClaseC c;
2. c = a + b;
C++ permite que hagamos estas cosas si definimos en algún sitio cómo se suman esas clases.
Definiendo cómo se hacen las operaciones, podremos escribir las operaciones con nuestras clases
de la misma forma que si se trataran de tipos básicos del lenguaje.
De hecho podemos definir cualquier operador que nos dé C++, desde algunos muy normales como
suma, resta, multiplicación, mayor qué, etc, a otros un poco más raros como el operador () o el [], de
forma que ClaseC[i] o ClaseC(i,j) pueden devolver lo que nosostros queramos.
Sobrecarga del operador suma
Si tenemos una clase ComplejoC que representa un complejo, para poder sumar dos de estas
clases simplemente poniendo un +, como con cualquier tipo básico de C++, debemos sobrecargar
el operador +, darle una nueva funcionalidad.
La sobrecarga de un operador suma se hace de la siguiente manera
class ComplejoC
{
public:
// Permite sumar un ComplejoC con un double
ComplejoC operator + (double sumando);
// Permite sumar un ComplejoC con un array
ComplejoC operator + (const double sumando[]);
// Permite sumar dos ComplejoC entre sí.
ComplejoC operator + (const ComplejoC &sumando);
};
Aquí estamos redefiniendo tres operadores suma para que nos permita sumar a nuestra clase
ComplejoC cosas como un double, un array de double (que supondremos contiene dos double,
aunque no tenemos forma de comprobarlo) y otra clase ComplejoC .
En estas funciones hemos puesto const en los parámetros para no poder cambiarlos dentro del
código. En la tercera función pasamos ComplejoC por referencia (el &), para evitar que se hagan
copias innecesarias. En la primera función no es necesario nada de esto, puesto que es un simple
3. double. En la segunda, ponemos const para no poder modificar el array, pero no es necesaria la
referencia, puesto que los arrays son punteros.
A la hora de implementar debemos tener cuidado con los const que hemos puesto. Por ejemplo, en
el tercer operador +, recibimos un sumando que es const. El código que implementemos dentro no
puede modificar dicho sumando, ni puede llamar a ningún método de ese sumando que no esté
declarado como const. Si lo intentamos, el compilador dará error. Supongamos que ComplejoC
tiene un atributo double x (parte real) y un método dameX() para obtener dicho atributo, este método
debe estar declarado const para poder llamarlo desde nuestro operator +. Más o menos esto:
class ComplejoC
{
public:
// Atención al const del final. Sin el no podemos llamar a sumando.dameX().
double dameX() const;
};
De forma que en el operator + podemos llamar a sumando.dameX().
Una vez implementados estos métodos, podemos hacer operaciones como
ComplejoC a;
ComplejoC b;
// Aprovechando la primera sobrecarga
b = a + 1.0;
// Aprovechando la segunda sobrecarga
double c[] = {1.0, 2.0};
b = a + c;
// Aprovechando la tercera sobrecarga
b = a + b;
Cuando el compilador lee a + 1.0, lo interpreta como a.operator + (1.0), es decir, la llamada al
operador suma al que se le pasa como parámetro un double . De la misma forma sucede con los
otros dos operadores suma.
Sin embargo, esto nos da un pequeño problema. ¿Qué pasa si ponemos 1.0 + a?. Debería ser lo
mismo, pero al compilador le da un pequeño problema. Intenta llamar al método operator + de 1.0,
que no existe, puesto que 1.0 ni es una clase ni tiene métodos. Para solucionar este problema
tenemos la sobrecarga de operadores globales.
4. Sobrecarga de operadores suma globales
Un operador global es una función global que no pertenece a ninguna clase y que nos indica cómo
operar con uno o dos parámetros (depende del tipo de operador).
En nuestro ejemplo de las sumas, para poder poner los sumandos al revés, deberíamos definir las siguientes funciones globales:
ComplejoC operator + (double sumando1, const ComplejoC &sumando2);
ComplejoC operator + (double sumando1[], const ComplejoC &sumando2);
Estas funciones le indican al compilador cómo debe sumar los dos parámetros y qué devuelve. Con
ellas definidas e implementadas, ya podemos hacer
b = 1.0 + a;
// c era un array de double
b = c + a;
Esta sobrecarga es especialmente útil cuando tratamos con una clase ya hecha y que no podemos
modificar. Por ejemplo, cout es de la clase ostream y no podemos modificarla, sin embargo nos
sería de utilidad sobrecargar el operador << de ostream de forma que pueda escribir nuestros
números complejos. La siguiente llamada nos dará error mientras no redefinamos el operator <<
de ostream .
cout << a << endl;
// a es un ComplejoC
Con la sobrecarga de operadores globales podemos definir la función
ostream &operator << (ostream &salida, const ComplejoC &valor);
Con esta función definida, el complejo se escribirá en pantalla como indique dicha función. Esta
función deberá escribir la parte real e imaginaria del complejo en algún formato, utilizando algo
como
salida << valor.dameX() << " + " << valor.dameY() << "j";
El operador devuelve un ostream, que será un return cout. De esta forma se pueden encadenar
las llamadas a cout de la forma habitual.
cout << a << " + " << b << endl;
5. Primero se evalúa operator << (cout, a), que escribe a en pantalla y devuelve un cout, con lo que
la expresión anterior quedaría, después de evaluar esto
cout << " + " << b << endl;
Y así consecutivamente.
Hay que tener en cuenta que estos operadores globales no son de la clase, así que sólo pueden
acceder a métodos y atributos públicos de la misma.
El operador cast
Un operador interesante es el operador "cast". En C++, si tenemos dos tipos básicos distintos,
podemos pasar de uno a otro haciendo un cast (siempre que sean compatibles de alguna forma).
Por ejemplo
int a;
double b;
a = (int)b;
El cast consiste en poner delante, entre paréntesis, el tipo que queramos que sea. En algunos casos, como en el de este ejemplo,el
cast se hace automáticamente y no es necesario ponerlo. Puede que de un "warning" en el
compilador avisando de que perderemos los decimales.
En principio, con las clases no se puede hacer cast a otros tipos, pero es posible declarar operadores
que lo hagan. La sintaxis sería:
class ComplejoC
{
public:
// Permite hacer un cast de ComplejoC a double
operator double ();
}
Con este podemos hacer cast de nuestra clase a un double . Es nuestro problema decidir cómo se
hace ese cast. En el código de ejemplo que hay más abajo se ha definido como la operación módulo
del número complejo.
6. double a;
ComplejoC b;
a = (double)b;
En el operator cast se pone operator seguido del tipo al que se quiere hacer el cast. No se pone el
tipo del valor devuelto, puesto que ya está claro. Si ponemos operator double, hay que devolver un
double .
En el operator cast no se pone parámetro, puesto que el parámetro recibido será una instancia de
la clase.
Conviene tener cuidado con definir muchos operadores cast, puesto que el compilador los tendrá
todos presentes y será capaz, encadenando unos con otros, de hacer cast entre tipos que no tienen
nada que ver. Por ejemplo, si sumamos un ComplejoC con un DibujoC (que no tienen nada que ver)
y ambos tienen cast e int, es posible que el compilador los transforme ambos en int y luego los sume
como enteros.
¿Cómo hacemos un cast al revés?. Es decir, ¿Cómo podemos convertir un double a ComplejoC?.
El asunto es sencillo, basta hacer un constructor que admite un double como parámetro.
class ComplejoC
{
public:
ComplejoC (double valor);
};
Este constructor sirve para lo que ya sabemos
ComplejoC a(3.0);
o podemos usarlo para hacer un cast de double a ComplejoC
ComplejoC a;
a = (ComplejoC)3.0;
7. El operador igual
El operator = es como los demás. Simplemente un pequeño detalle. C++ por defecto tiene el
operador igual definido para clases del mismo tipo. Por ejemplo, sin necesidad de redefinir nada,
podemos hacer
ComplejoC a;
ComplejoC b;
a = b;
Este igual por defecto lo único que hace es copiar el contenido del uno sobre el otro, como si fueran
bytes, sin saber qué atributos está copiando ni qué significan. Para clases sencillas, que solo tienen
atributos que no son punteros, esto es más que suficiente.
Si embargo, si algún atributo es un puntero, tenemos que tener mucho cuidado con lo que hacemos.
Supongamos que ClaseC tiene un atributo Atributo que es un puntero. Supongamos también que
en el constructor de la clase, se hace new del puntero para que tenga algo y en el destructor se hace
el delete correspondiente.
ClaseC
{
public:
// Se crea un array de tres enteros
ClaseC () {
Atributo = new int[3];
}
// Se libera el array.
~ClaseC () {
delete [] Atributo;
}
protected:
int *Atributo;
};
Si ahora hacemos esto
// a tiene ahora un array de 3 enteros en su interior
ClaseC *a = new ClaseC();
// b tiena ahora otro array de 3 enteros en su interior
ClaseC *b = new ClaseC();
/* Se copia el Atributo de b sobre el de a, es decir, ahora a->Atributo apunta al mismo sitio que b-
8. >Atributo */
*a = *b;
// Ahora si que la hemos liado.
delete b;
Cuando hacemos a=b, con el operador igual por defecto de C++, se hace que el puntero Atributo
de a apunte al mismo sitio que el de b. El array original de a->Atributo lo hemos perdido, sigue
ocupando memoria y no tenemos ningún puntero a él para liberarlo.
Cuando hacemos delete b, el destructor de b se encarga de liberar su array. Sin embargo, al puntero
a->Atributo nadie le avisa de esta liberación, se queda apuntando a una zona de memoria que ya
no es válida. Cuando intentemos usar a->Atributo más adelante, puede pasar cualquier cosa
(cambios aleatorios de variables, caidas del programa, etc).
En el tema de punteros tienes un poco más detallado todo esto. Allí se habla de estructuras, pero
también se aplica a clases.
La forma de solucionar esto, es definiendo nosotros un operator = que haga una copia real del array,
liberando previamente el nuestro o copiando encima los datos.
ClaseC
{
public:
ClaseC &operator = (const ClaseC &original)
{
int i;
/* Damos por supuesto que ambos arrays existen y son de tamaño 3 */
for (i=0;i<3;i++)
Atributo[i] = original.Atributo[i];
}
};
Sobrecarga del operador new y delete
Otro operador interesante de sobrecargar es el new y el delete. Si los sobrecargamos dentro de la
clase, cada vez que hagamos un new a nuestra clase se llamará a nuestro operador.
Más interesante es la sobrecarga de los operadores new y delete globales. Sobrecargando estos
operadores se llamará a nuestras funciones cada vez que hagamos un new o un delete de cualquier
9. cosa (clases o variables). Esta característica nos permite hacer contabilidad de punteros, para ver si
liberamos todo lo que reservamos o liberamos lo mismo más veces de la cuenta.