Métodos y Clases en Java: Parte II
Continuamos en este artículo la discusión sobre aspectos más avanzados de la POO en Java.
Introducción: Polimorfismo en Java
Esta palabra que significa "muchas formas", es una característica del lenguaje Java que permite a una interface
ser usada por una clase general de acciones. La acción concreta a llevar a cabo se determina por la naturaleza
específica de la situación.
En términos más generales, el concepto de polimorfismo a menudo se expresa por la frase "una interfaz, múltiples
métodos". Esto significa que es posible diseñar una interfaz genérica para un grupo de actividades relacionadas.
Es evidente que esta forma de trabajar ayuda a reducir la complejidad del diseño, pues permite usar una misma
interfaz para especificar un conjunto de acciones similares. Será el compilador el que tendrá que seleccionar
la acción concreta (esto es, el método) para aplicar en cada situación. Como programadores, nosotros solo
tenemos que conocer la interfaz general. En el siguiente apartado se relata un caso práctico que ayudará a entender los
beneficios del polimorfismo.
Sobrecarga de métodos
Otra de las ventajas de este lenguaje de programación es que nos permite definir dos o más métodos
dentro de la misma clase con el mismo nombre, siempre que la declaración de sus parámetros sea diferente.
En este caso se dice que el método está
sobrecargado y el proceso de definir un método así se
conoce como
sobrecarga del método. La sobrecarga de métodos es una de las maneras en que Java implementa el polimorfismo.
Cuando se llama a un método sobrecargado, el compilador actúa justo sobre la versión cuyo tipo de parámetros coincida
con los de la llamada. Así, se podría definir la siguiente clase "SumaGenerica" que aglutinara las sumas de todos los tipos primitivos:
class SumaGenerica {
int suma (int a, int b) {
return a+b;
}
double suma (double a, double b) {
return a+b;
}
...
}
NOTA: Realmente no sería necesario definir el método suma() para todos los tipos de datos, pues aquí también interviene
el casting implícito que hace Java. Por ejemplo, una suma de float, llamaría automáticamente al método que devuelve double siempre y cuando
no esté definido el método que devuelve float, claro está.
No existe una regla exacta par saber si un método se debe sobrecargar o no. Realmente, la idea es aprovechar las ventajas que
ofrece esta forma de polimorfismo, así que lo normal es sobrecargar aquellos métodos que estén intrínsicamente relacionados, como es el
caso del ejemplo anterior pero no nos debemos confundir. Por ejemplo, el método sqrt(), aunque se llama igual, calcula
de forma totalmente diferente la raiz cuadrada de un número entero que la de uno en punto flotante. Aquí, aunque aplicaramos
sobrecarga al método, realmente no estaríamos respetando el propósito para el que se creó el polimorfismo.
Sobrecarga de Constructores
Es aquí donde realmente se aprecia los beneficios del polimorfismo. Como sabemos, el constructor de una clase es el que inicializa los valores que el programador crea conveniente cuando
ésta se instancia. Pues bien, sobrecargando el constructor conseguimos dotar a la clase de flexibilidad. Por ejemplo, como mínimo se debería tener en cuenta
que podría no pasarsele parámetros al constructor, cuando éste lo espera, debido a un fallo en alguna otra parte de la aplicación
(me refiero a cualquier otra clase que llame a ésta).
Es por ello que siempre es recomendable definir al menos dos constructores: el específico de la aplicación que estemos diseñando y el "estándar". El siguiente ejemplo te lo va a dejar mucho más claro:
class Box {
double width;
double height;
double depth;
//El siguiente es el constructor específico
Box(double w, double h, double d) {
width = w; height = h; depth = d;
}
//pero podría ser que no le llegarán parámetros
// por fallar la otra clase (método) que lo invoque
Box () {
width = height = depth = -1 //-1 indica volumen no existente
}
//e incluso podemos pensar que se quiere construir un cubo,
//entonces, por qué introducir 3 valores? ;)
Box (double valor) {
width = height = depth = valor;
}
double volume() {
return width * height * depth;
}
Como ves, de cara a flexibilizar una clase es fundamental el polimorfismo a la hora de implementar los constructores.
Objetos como parámetros
A parte de usar los tipos primitivos como parámetros, en Java es perfectamente posible pasarle a un método un objeto como parámetro.
class Comparar {
int a, b;
Comparar (int i, int j) {
a = i;
b = j;
}
boolean equals (Comparar c) {
if (c.a == a && c.b == b) return true;
else return false;
}
}
Como se puede apreciar, el método equals() de Comparar comprueba si dos objetos son idénticos
. Esto es, compara el objeto que invoca el método con el que se le pasa al método como argumento.
Acabo de liarte? ;), pues fíjate en el siguiente trozo de código que se explica mucho mejor.
class PasaObjeto {
public static void main(string args[]) {
Comparar objeto1 = new Test(2,3);
Comparar objeto2 = new Test(2,3);
System.out.println(objeto1.equals(objeto2));
}
}
Como ves,
objeto1 es el que invoca el método y
objeto2 es
el que se pasa como argumento al método equals() de objeto1.
Observa también que el parámetro c en equals() especifica Comparar como tipo.
Aunque Comparar es un tipo de clase creado por el programa, éste se usa de la misma manera
que los tipos primitivos que Java nos proporciona.
Una mirada más cercana al paso de argumentos
Como sabemos, existen dos formas de pasar un argumento a una rutina:
- Por valor: El método copia el valor de un argumento en el parámetro formal de la rutina.
- Por referencia: Se pasa al parámetro de la rutina la referencia al argumento (no su valor).
En el primer caso, al ser solo una copia, cualquier modificación dentro de la rutina de ese valor no tendrá
efecto una vez fuera de éste mientras que si es por referencia, sí persistirá la modificación
hecha una vez salgamos de la rutina.
Pues bien, cuando en Java se pasan argumentos de tipo simple, estos siempre se hacen por valor. Entonces, cómo puedo pasar un
parámetro por referencia para modificarlo?. Pues muy fácil, pasando el objeto. Esto ocurre porque cuando tu creas una variable de un tipo de clase,
tu solo creas una referencia al objeto. Así, cuando pasas esta referencia a un método, el parámetro
que recibe éste se refiere al mismo objeto que el referido por el argumento. Por tanto, los cambios que se hagan en la referencia de
la rutina afectarán también al objeto pasado como argumento.
El Operador static
Hay veces que se desea querer definir una clase miembro para ser usada
independientemente de cualquier objeto de esa clase. Normalmente a una clase miembro se accede
solo si hemos instanciado un objeto de dicha clase. No obstante, es posible crear un miembro
que pueda ser usado por si mismo, sin necesidad de referenciar a una instancia específica. Para crear tales
tipos de miembros se emplea el operador
static.
Cuando un miembro se declara con esta palabra reservada, se puede acceder a él
antes de que cualquier objeto de su clase sea creado, y sin referenciar a ningún objeto. Puedes declarar tanto los métodos
como las variables como static. El ejemplo más claro de un miembro static es el main(). Se declara de esta manera
porque se debe llamar antes de que cualquier objeto sea declarado.
Las variables static
Las variables de instancia declaradas como static (también llamadas "
de clase") son, esencialmente, variables globales. Cuando creamos objetos específicos
de esa clase no se hace ninguna copia de las variables static. Lo que ocurre es que todas las instancias de esa clase
comparten la misma variable static. Estas variables mayormente tienen sentido cuando varias instancias de la misma clase necesitan
llevar el control o estado del valor de un dato. También se podrían utilizar para definir constantes, pero cuando menos es aconsejable
que también se emplee el operador que se explica en la siguiente sección.
Para llamar a este tipo de variables se suele utilizar el nombre de la clase
(no es imprescindible, pues se puede utilizar también el nombre de cualquier objeto), porque de esta forma queda más claro, seguida de un "." y
la variable, por ejemplo, Estatica.b
Las variables miembro static se crean en el momento en que pueden ser necesarias: cuando se va
a crear el primer objeto de la clase, en cuanto se llama a un método static o en cuanto se utiliza una variable static de dicha clase. Lo
importante es que las variables miembro static se inicializan siempre antes que cualquier objeto de la clase.
Los métodos static
También llamados "
de clase", pueden recibir objetos de su clase como argumentos explícitos, pero no tienen argumento implícito ni
pueden utilizar la referencia this. Para llamar a estos métodos se suele emplear el nombre de la clase, en vez del nombre de un objeto de la clase. Los métodos y variables de clase
son lo más parecido que Java tiene a las funciones y variables globales de C/C++.
Las restricciones que tiene un método static son:
- Solo pueden llamar otro método static
- Solo deben acceder a datos static
- No se pueden referir a this o super de ninguna manera
Si se necesita hacer algún tipo de computación para inicializar las variables static, se puede declarar también un bloque static el cual se ejecutará
solo una vez, cuando se carga.
class Estatica {
static int a = 3;
static int b;
static void show(int x) {
System.out.println("x = " + x);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
static {
System.out.println("Bloque estático inicializado");
b = a * 4; //vemos que, aunque declarada como static
//b se inicializa en tiempo de ejecución
}
public static void main(String args[]) {
show(15);
}
}
Tan pronto como Estatica se carga, todas las sentencias se ejecutan.
Primero, a se inicializa a 3, luego se ejecuta el bloque estático y, por último,
b se inicializa al valor asignado.
El operador final
Una variable que se declara como final, se previene que su contenido sea modificado. Esto significa
que debes inicializar una variable como final cuando la declaras. No obstante, Java permite separar la
definición de la inicialización. Esta última se puede hacer más tarde, en tiempo de ejecución,
llamando a métodos o en función de otros datos. La variable final así definida es constante pero no tiene
porqué tener el mismo valor en todas las ejecuciones del programa, pues depende de cómo haya sido inicializada.
Esto es lo más parecido a las constantes de otros lenguajes de programación. Por ejemplo:
final float PI = 3.1416;//Según la convención de código las variables "constantes"
//se ponen en mayúsculas.
//Observa que ahora podrás usar esta variable sin miedo a
//ser modificada, esto es, tienes una constante.
Aunque básicamente este operador se emplea para crear constantes en Java, también podemos definir una clase
o un método como final. En el primer caso, no puede tener clases derivadas. Esto se hace por motivos de seguridad y de eficiencia, porque cuando el compilador
sabe que los métodos no van a ser redefinidos puede hacer optimizaciones adicionales.
En el segundo, un método declarado como final no puede ser redefinido por una clase que derive
de su propia clase.
En el próximo capítulo acabaremos de ver otros conceptos de la POO y algunos aspectos del uso de clases con los operadores explicados. También haremos un
repaso de lo que son las clases internas y las clases anidadas.
Leo Suarez es un Ingeniero Superior de Telecomunicaciones por la Universidad de Las Palmas de Gran Canaria con todas sus letras, que no son pocas, y trabaja como tal en Santa Cruz de Tenerife. Cuando no está saltando de isla en isla o escribiendo para javaHispano aprovecha para disfrutar con la novia y los amigos del estupendo clima de las islas afortunadas.
Para cualquier duda o tirón de orejas, e-mail a: leo_ARROBA_javahispano.com
|
|