martes, 25 de octubre de 2011

2.6 Sobrecarga de operadores: Concepto y utilidad, operadores unarios y binarios

Los operadores de Java son muy parecidos en estilo y funcionamiento a los de C. Tanto C, como C++, como Java, proporcionan un conjunto de operadores para poder realizar acciones sobre uno o dos operandos. Un operador que actúa sobre un solo operando es un operador unario, y un operador que actúa sobre dos operandos es un operador binario.
Algunos operadores pueden funcionar como unarios y como binarios, el ejemplo más claro es el operador - (signo menos). Como operador binario, el signo menos, el cual hace que el operando de la derecha sea sustraido al operando de la izquierda; como operador unario hace que el signo algebraico del operando que se encuentre a su derecha sea cambiado.
En la siguiente tabla aparecen los operadores que se utilizan en Java, por orden de precedencia:
  .       []      ()
  ++      --
  !       ~       instanceof
  *       /       %
  +       -
  <<      >>      >>>
  <       >       <=       >=      ==      !=
  &       ^       |
  &&      ||
  ?  :
  =       op=     (*=      /=      %=     +=    -=   etc.) ,
Los operadores numéricos se comportan como esperamos:
    int + int = int
Los operadores relacionales devuelven un valor booleano.
Para las cadenas, se pueden utilizar los operadores relacionales para comparaciones además de + y += para la concatenación:
    String nombre = "nombre" + "Apellido";
El operador = siempre hace copias de objetos, marcando los antiguos para borrarlos, y ya se encargará el garbage collector de devolver al sistema la memoria ocupada por el objeto eliminado.
Java, a diferencia de C++, no soporta la sobrecarga de operadores. Esto significa que no es posible redefinir el entorno en el que actúa un operador con respecto a los objetos de un nuevo tipo que el programador haya definido como propios.
Un caso interesante, que se sale de la afirmación anterior, es el operador + (signo más), que se puede utilizar para realizar una suma aritmética o para concatenar cadenas (¡Java lo sobrecarga internamente!). Cuando el signo más se utiliza en esta última forma, el operando de la derecha se convierte automáticamente en una cadena de caracteres antes de ser concatenada con el operando que se encuentra a la izquierda del operador +. Esto asume que el compilador sabe que el operando de la derecha es capaz de soportar la conversión. Y ese conocimiento lo tiene porque comprueba todos los tipos primitivos y muchos de los tipos internos que se utilizan. Obviamente, el compilador no sabe absolutamente nada de los tipos que haya definido el programador.
A continuación se detallan algunos de los operadores que admite Java y que se suelen agrupar en operadores aritméticos, relacionales y condicionales, operadores lógicos y que actúan sobre bits y, finalmente, operadores de asignación.

Operadores Aritméticos

Java soporta varios operadores aritméticos que actúan sobre números enteros y números en coma flotante. Los operadores binarios soportados por Java son:
+
suma los operandos
-
resta el operando de la derecha al de la izquierda
*
multiplica los operandos
/
divide el operando de la izquierda entre el de la derecha
%
resto de la división del operando izquierdo entre el derecho
Como se ha indicado anteriormente, el operador más (+), se puede utilizar para concatenar cadenas, como se observa en el ejemplo siguiente:
"miVariable tiene el valor " + miVariable + " en este programa"
Esta operación hace que se tome la representación como cadena del valor de miVariable para su uso exclusivo en la expresión. De ninguna forma se altera el valor que contiene la variable.
En C++ no se soporta la concatenación de cadenas con el operador más (+); hay que utilizar la función strcat() para poder concatenar dos cadenas y el uso de esta función puede resultar peligroso porque no hay protección contra el traspaso de los límites del array que se utiliza como destino de la cadena concatenada.
El operador módulo (%), que devuelve el resto de una división, a diferencia de C++, en Java funciona con tipos en coma flotante además de con tipos enteros. Cuando se ejecuta el programa que se muestra en el ejemplo java401.java.
La salida que se obtiene por pantalla tras la ejecución del ejemplo es la que se reproduce a continuación:
%java java401
x mod 10 = 3
y mod 10 = 3.299999999999997
Los operadores unarios que soporta Java son:
+
indica un valor positivo
-
negativo, o cambia el signo algebraico
++
suma 1 al operando, como prefijo o sufijo
--
resta 1 al operando, como prefijo o sufijo
En los operadores de incremento (++) y decremento (--), en la versión prefijo, el operando aparece a la derecha del operador, ++x; mientras que en la versión sufijo, el operando aparece a la izquierda del operador, x++. La diferencia entre estas versiones es el momento en el tiempo en que se realiza la operación representada por el operador si éste y su operando aparecen en una expresión larga. Con la versión prefijo, la variable se incrementa (o decrementa) antes de que sea utilizada para evaluar la expresión en que se encuentre, mientras que en la versión sufijo, se utiliza la variable para realizar la evaluación de la expresión y luego se incrementa (o decrementa) en una unidad su valor.

Operadores Relacionales y Condicionales


Aunque Java y C++ soportan los mismos operadores relacionales, devuelven valores diferentes. Los operadores relacionales en Java devuelven un tipo booleano, true o false; mientras que en C++ devuelven un valor entero, en donde se puede considerar al valor cero como false y a un valor no-cero como true.
el operando izquierdo es mayor que el derecho
>=
el operando izquierdo es mayor o igual que el derecho
el operando izquierdo es menor que el derecho
<=
el operando izquierdo es menor o igual que el derecho
==
el operando izquierdo es igual que el derecho
!=
el operando izquierdo es distinto del derecho
Los operadores relacionales combinados con los operadores condicionales (o lógicos en C++), se utilizan para obtener expresiones más complejas. Los operadores condicionales que soporta Java son:
&&
expresiones izquierda y derecha son true
||
o la expresión izquierda o al expresión de la derecha son true
!
la expresión de la derecha es false


Operadores a Nivel de Bits

Java y C, C++ comparten un conjunto de operadores que realizan operaciones sobre un solo bit cada vez. Java también soporta el operador >>>, que no existe en C o C++ y, además, el entorno en el que se realizan algunas de las operaciones no siempre es el mismo en Java que en los otros lenguajes.
Los operadores de bits que soporta Java son los que aparecen en la siguiente tablita:

Operador
Uso
Operación
>> 
Operando >> Despl
Desplaza bits del operando hacia la derecha las posiciones indicadas (con signo)
<< 
Operando << Despl
Desplaza bits del operando hacia la izquierda las posiciones indicadas
>>> 
Operando >>> Despl
Desplaza bits del operando hacia la derecha las posiciones indicadas (sin signo)
&
Operando & Operando
Realiza una operación AND lógiga entre los dos operandos
|
Operando | Operando
Realiza una operación OR lógica entre los dos operandos
^
Operando ^ Operando
Realiza una operación lógica OR Exclusiva entre los dos operandos
~
~Operando
Complementario del operando (unario)

En Java, el operador de desplazamiento hacia la derecha sin signo, rellena los bits que pueden quedar vacíos con ceros. Los bits que son desplazados fuera del entorno se pierden.
En el desplazamiento a la izquierda, hay que ser precavidos cuando se trata de desplazar enteros positivos pequeños, porque el desplazamiento a la izquierda tiene el efecto de multiplicar por 2 para cada posición de bit que se desplace; y esto es peligroso porque si se desplaza un bit 1 a la posición más alta, el bit 31 o el 63, el valor se convertirá en negativo.

Operadores de Asignación

El operador = es un operador binario de asignación de valores. El valor almacenado en la memoria y representado por el operando situado a la derecha del operador es copiado en la memoria indicada por el operando de la izquierda. Al contrario que en C++, el operador de asignación (ni ningún otro) no se puede sobrecargar en Java.
Java soporta toda la panoplia de operadores de asignación que se componen con otros operadores para realizar la operación que indique ese operador y luego asignar el valor obtenido al operando situado a la izquierda del operador de asignación. De este modo se pueden realizar dos operaciones con un solo operador.
+=   -=   *=   /=   %=   &=   |=   ^=   <<=
>>=  >>>=
Por ejemplo, las dos sentencias que siguen realizan la misma función:
    x += y;
    x = x + y;
Y las otras comprobaciones siguen el mismo patrón. C++ no soporta el operador >>>= porque tampoco soporta el operador a nivel de bits de desplazamiento sin signo (>>>).

Operadores Ternario if-then-else

Java, lo mismo que C y C++, soporta este operador ternario. No obstante, la construcción utilizada por este operador es lago confusa, aunque se puede llegar a entender perfectamente cuando uno lee en el pensamiento lo que está escrito en el código. La forma general del operador es:
    expresion ? sentencia1 : sentencia2
en donde expresion puede ser cualquier expresión de la que se obtenga como resultado un valor booleano, porque en Java todas las expresiones condicionales se evalúan a booleano; y si es true entonces se ejecuta la sentencia1 y en caso contrario se ejecuta la sentencia2. La limitación que impone el operador es que sentencia1 y sentencia2 deben devolver el mismo tipo, y éste no puede ser void.
Puede resultar útil para evaluar algún valor que seleccione una expresión a utilizar, como en la siguiente sentencia:
    cociente = denominador == 0 ? 0 : numerador / denominador
Cuando Java evalúa la asignación, primero mira la expresión que está a la izquierda del interrogante. Si denominador es cero, entonces evalúa la expresión que está entre el interrogante y los dos puntos, y se utiliza como valor de la expresión completa. Si denominador no es cero, entonces evalúa la expresión que está después de los dos puntos y se utiliza el resultado como valor de la expresión completa, que se asigna a la variable que está a la izquierda del operador de asignación, cociente.
En el ejemplo java402.java se utiliza este operador para comprobar que el denominador no es cero antes de evaluar la expresión que divide por él, devolviendo un cero en caso contrario. Hay dos expresiones, una que tiene un denominador cero y otra que no.
class java402 {
    public static void main( String args[] ) {
        int a = 28;
        int b = 4;
        int c = 45;
        int d = 0;
   
        // Utilizamos el operador ternario para asignar valores a
        // las dos variables e y f, que son resultado de la
        // evaluación realizada por el operador
        int e = (b == 0) ? 0 : (a / b);
        int f = (d == 0) ? 0 : (c / d);
        // int f = c / d;
   
        System.out.println( "a = " + a );
        System.out.println( "b = " + b );
        System.out.println( "c = " + c );
        System.out.println( "d = " + d );
        System.out.println();
        System.out.println( "a / b = " + e );
        System.out.println( "c / d = " + f );
        }
    }
El programa se ejecuta sin errores, y la salida que genera por pantalla es la que se reproduce:
%java java402
a = 28
b = 4
c = 45
d = 0
   
a / b = 7
c / d = 0
Si ahora cambiamos la línea que asigna un valor a la variable f, y quitamos este operador, dejándola como:
    int f = c / d;
se producirá una excepción en tiempo de ejecución de división por cero, deteniéndose la ejecución del programa en esa línea.
%java java402
java.lang.ArithmeticException: / by zero at java402.main(java402.java:40)


sábado, 22 de octubre de 2011

Sobrecarga de métodos


Un método sobrecargado se utiliza para reutilizar el nombre de un método pero con diferentes argumentos (opcionalmente un tipo diferente de retorno). Las reglas para sobrecargar un método son las siguientes:

+ Los métodos sobrecargados debe de cambiar la lista de argumentos.
+ Pueden cambiar el tipo de retorno.
+ Pueden cambiar el modificador de acceso.
+ Pueden declarar nuevas o más amplias excepciones.
+ Un método puede ser sobrecargado
en la misma clase o en una subclase.

Veamos un método que se desea sobrecargar:
public void cambiarTamano(int tamano, String nombre, float patron){ }

Los siguientes métodos son sobrecargas legales del método cambiarTamaño():

public void cambiarTamano(int tamano, String nombre){}
public int cambiarTamano(int tamano, float patron){}
public void cambiarTamano(float patron, String nombre) throws IOException{}


Cómo invocar un método sobrecargado::

Lo que define qué método es el que se va a llamar son los argumentos que se envían al mismo durante la llamada. Si se invoca a un método con un String como argumento, se ejecutará el método que tome un String como argumento, si se manda a llamar al mismo método pero con un float como argumento, se ejecutará el método que tome un float como argumento y así sucesivamente. Si se invoca a un método con un argumento que no es definido en ninguna de las versiones sobrecargadas entonces el compilador arrojará un mensaje de error.

Ejemplo de una clase con un método sobrecargado:

public class Sobrecarga {
public void Numeros(int x, int y){
System.out.println("Método que recibe enteros.");
}
public void Numeros(double x, double y){
System.out.println("Método que recibe flotantes.");
}
public void Numeros(String cadena){
System.out.println("Método que recibe una cadena: "+ cadena);
}
public static void main (String... args){
Sobrecarga s = new Sobrecarga();
int a = 1;
int b = 2;
s.Numeros(a,b);
s.Numeros(3.2,5.7);
s.Numeros("Monillo007");
}
}


Al ejecutar el código anterior obtendremos lo siguiente:

Método que recibe enteros.
Método que recibe flotantes.
Método que recibe una cadena: Monillo007


Al utilizar objetos en lugar de tipos primitivos o cadenas se vuelve más interesante. Veamos lo que sucede cuando se tiene un método sobrecargado que en una de sus versiones toma un Animal como parámetro y en otra un Caballo.

class Animal { }

class Caballo extends Animal{ }

class Animales{
public void MetodoSobrecargado(Animal a){
System.out.println("Método de parámetro Animal...");
}
public void MetodoSobrecargado(Caballo c){
System.out.println("Método de parámetro Caballo...");
}
public static void main(String... args){
Animales as = new Animales();
Animal objetoAnimal = new Animal();
Caballo objetoCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimal);
as.MetodoSobrecargado(objetoCaballo);
}
}


Al ejecutar el código anterior obtenemos:

Método de parámetro Animal...
Método de parámetro Caballo...


Como era de esperarse, cada objeto manda a llamar al método que le corresponde de acuerdo a su tipo, pero qué pasa si creamos una referencia Animal sobre un objeto Caballo...

Animal objetoAnimalRefCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimalRefCaballo);


El resultado es:

Método de parámetro Animal...

Aunque en tiempo de ejecución el objeto es un Caballo y no un Animal, la elección de qué método sobrecargado invocar no se realiza dinámicamente en tiempo de ejecución, el tipo de referencia, no el objeto actual, es el que determina qué método es el que se ejecutará.

Reglas de la sobrecarga y sobreescritura de métodos::

Ahora que hemos visto ambas formas de reescribir métodos, revisemos las reglas y diferencias entre ambos tipos de reescritura:

+Argumentos: En un método sobrecargado los argumentos deben de cambiar mientras que en un método sobreescrito NO deben cambiar.

+ El tipo de retorno: En un método sobrecargado el tipo de retorno puede cambiar, en un método sobreescrito NO puede cambiar, excepto por subtipos del tipo declarado originalmente.

+ Excepciones: En un método sobrecargado las excepciones pueden cambiar, en un método sobreescrito pueden reducirse o eliminarse pero NO deben de arrojarse excepciones nuevas o más amplias.

+ Acceso: En un método sobrecargado puede cambiar, en un método sobreescrito el acceso NO debe de hacerse más restrictivo(puede ser menos restrictivo).

+ Al invocar: En un método sobrecargado los argumentos son los que determinan qué método es el que se invocará, en un método sobreescrito el tipo de objeto determina qué método es elegido.

jueves, 20 de octubre de 2011

2.5 Constructores y destructores: declaración, uso y aplicaciones.

Constructores y destructores

Los constructores y destructores son dos tipos de métodos especiales que se ejecutan, respectivamente, al crear un nuevo objeto y cuando el recolector de basura detecta que ya no lo estamos utilizando y es posible eliminarlo de la memoria. Hasta ahora no hemos hecho uso nunca ni de los constructores ni de los destructores puesto que los ejemplos que hemos venido utilizando eran bastante simples, pero a partir de ahora vamos a empezar a utilizarlos bastante a menudo. Sobre todo en el caso de los constructores, ya que los destructores no se suelen usar más que en contadas ocasiones.

Para crear un objeto se necesita reservar suficiente espacio en memoria e inicializar los valores de los campos que representan el estado del objeto.
Este trabajo es realizado por un tipo especial de método denominado constructor.

Constructor
Un método constructor de una clase es un método especial que:
  • tiene el mismo nombre que la clase y
  • no tiene tipo de retorno.
  • inicializa el estado de un objeto.
La sintaxis para la declaración de un método constructor es:
[atributos] [modificadores] <identificador> ( [parámetros] ) [inicializador]
{
// Cuerpo del constructor.
}
Donde:
atributos (opcional) es información declarativa adicional.
modificadores (opcional) se restringen a extern y a los modificadores de acceso.
identificador es el nombre del método constructor (igual al nombre de la clase).
parámetros (opcional) es la lista de parámetros pasados al constructor.
inicializador (opcional). Con el inicializador, el constructor invoca previamente a otro constructor.
El inicializador puede ser uno de los siguientes:
·         base([listaDeParámetros])
·         this([listaDeParámetros])
Cuerpo del constructor es el bloque de programa que contiene las instrucciones para inicializar la instancia de clase (objeto).

Ejemplo:
class Producto
{
private int clave;
private double precio;
public Producto( int c, double p)
{
clave = c;
precio = p;
}
public double daPrecio( )
{
return precio;
}

Destructor
En contraposición al constructor, el destructor elimina el vínculo y libera el espacio de memoria de un objeto, para que pueda ser ocupado nuevamente.

La sintaxis para declarar un destructor es:
[atributos] ~ <identificador> ( )
{
// Cuerpo del destructor.
}

Notas:
  • Una clase solamente puede tener un destructor.
  • Los destructores no pueden heredarse o sobrecargarse.
  • Los destructores no pueden invocarse, sino que son invocados automáticamente.
  • Un destructor no acepta modificadores ni parámetros. Por ejemplo, la siguiente es una declaración de un destructor para la clase Figura:
~ Figura()
{
// Instrucciones para limpiar.
}

El destructor llama implícitamente al método Object.Finalize( ) en la clase base object. Por lo tanto, el código destructor precedente es traducido automáticamente a:
protected override void Finalize( )
{
try
{
// Instrucciones para limpiar.
}
finally
{
base.Finalize( ) ;
}

Ejemplo:
// Destructores.cs : Maneja tres destructores de clases encadenadas.
using System;
using C = System.Console;
class Primera
{
~ Primera( )
{
C.WriteLine("Se invocó al destructor de Primera...");
}
}
class Segunda : Primera
{
~ Segunda( )
{
C.WriteLine("Se invocó al destructor de Segunda...");
}
}
class Tercera : Segunda
{
~ Tercera( )
{
C.WriteLine("Se invocó al destructor de Tercera...");
}
}
public class Principal
{
public static void Main( )
{
Tercera t = new Tercera ( );
}


Aplicaciones de constructores y destructores

En esta sección se presenta una serie de ejemplos donde se implementarán:
1.- clases que sólo poseen el constructor predeterminado ,
2.- clases con un constructor definido por el programador y
3.- clases con un constructor definido por el programador y el predefinido (que deberá ser reescrito por el programador).
El constructor predeterminado es incluído automáticamente.
Cuando el programador define un constructor, el predeterminado se anula. En caso de requerir el constructor predeterminado junto con un constructor definido por el programador, deberá volverse a escribir el constructor predefinido, de acuerdo al siguiente formato :
public <nombreClase>( )
{

Ejemplo 1
// Uso del constructor predeterminado.(No se define otro constructor)
using System;
using C = System.Console;
class Persona
{
private string nombre;
private int edad;
public void asignaNombre( string n)
{
nombre = n;
}
public void asignaEdad( int e)
{
edad = e;
}
public string daNombre( )
{
return nombre;
}
public int daEdad( )
{
return edad ;
}
}
public class Principal
{
public static void Main( )
{
Persona p = new Persona( ) ; // Se invoca al constructor
//predeterminado.
p.asignaNombre("Luis");
p.asignaEdad(25);
C.WriteLine("Nombre: {0} , Edad: {1}", p.daNombre(),
p.daEdad());
}


Ejemplo 2
// Uso de un constructor con dos parámetros.(No se requiere un
constructor predefinido)
using System;
using C = System.Console;
class Persona
{
private string nombre;
private int edad;
public Persona( string n, int e)
{
nombre = n;
edad = e;
}
public string daNombre( )
{
return nombre;
}
public int daEdad( )
{
return edad ;
}
}
public class Principal
{
public static void Main( )
{
Persona p = new Persona( "Luis", 25) ;
C.WriteLine("Nombre: {0} , Edad: {1}", p.daNombre(),
p.daEdad());
}

Ejemplo 3
// Uso de un constructor con dos parámetros y el constructor predefinido.
using System;
using C = System.Console;
class Persona
{
private string nombre;
private int edad;
public Persona(string n, int e)
{
nombre = n;
edad = e;
}
public Persona() // Constructor predeterminado, vuelto a escribir.
{
}
public string daNombre()
{
,.....return nombre;
}
public int daEdad()
{
.....return edad;
}
public void asignaNombre(string n)
{
.....nombre = n;
}
public void asignaEdad(int e)
{
...edad = e;
}
}
public class Principal
{
public static void Main()
{
Persona pa = new Persona("Luis", 25);
Persona pb = new Persona(); // Se invoca al constructor
................................................// predeterminado
pb.asignaNombre("Pepe");
pb.asignaEdad(19);
C.WriteLine("Nombre: {0} , Edad: {1}", pa.daNombre(),
pa.daEdad());
C.WriteLine("Nombre: {0} , Edad: {1}", pb.daNombre(),
pb.daEdad());
C.Read();
}
}