Java – Paso a paso Tema 7

Written by lopezatienza on 18/08/2009 – 15:29 -

TEMA7. Objetos, clases y métodos

En esta Unidad Didáctica veremos como se implementan en Java los conceptos básicos del paradigma orientado a objetos, como son los propios objetos y las clases.

1. Objetos
2. Clases
3. Miembros estáticos
4. Métodos

1. Objetos

El objeto es la entidad fundamental del lenguaje Java. Aparte de los objetos, Java tiene los tipos de datos primitivos que ya se han visto.

En C por ejemplo si se quiere hacer un programa que calcule el área de un círculo habrá que crear un programa que tenga por un lado una variable radio, y por otro una función calculaArea() que haga el cálculo del área. Sería parecido al siguiente
programa C:

1. #include <stdio.h>
2.
3. //Datos: variables globales
4. float radio;
5. float area;
6.
7. //Operaciones
8. float calcula_area()
9. {
10. float a = 3.1416 * radio * radio;
11. return a;
12. }
13.
14. //Función principal
15. int main()
16. {
17. radio = 10.5;
18. area = calcula_area();
19. printf("area = %f", area);
20. }

En este programa se han utilizado variables globales, es decir variables que son visibles desde cualquier función del programa. El uso de variables globales tiene la ventaja de que los datos son visibles a lo largo de todo el programa facilitando la programación, ya que el número de argumentos de las funciones se reduce. Sin embargo presenta el gran inconveniente de que el programa es poco modular, difícil de modificar y de leer puesto que todas las funciones pueden potencialmente modificar los datos al ser excesivamente visibles. Si en vez de utilizar variables globales se hubieran utilizado variables locales el programa sería metodológicamente más correcto, aunque habría que modificar la función calcula_area() para que reciba el radio como argumento resultando el código modificado siguiente:

1. #include <stdio.h>
2.
3. float calcula_area(float r)
4. {
5. float a = 3.1416 * r * r;
6. return a;
7. }
8.
9. int main()
10. {
11. //Datos: variables locales
12. float radio = 10.5;
13. float area=calcula_area(radio);
14. printf("area = %f", area);
15. }

Como se ve, por un lado se tiene la parte de datos (las variables radio y area) y por otro la parte de operaciones (la función calcula_area()) sin que exista conexión entre ambos, aunque evidentemente existe ambas cosas están muy relacionadas. Java permite implementar el código anterior exactamente igual que en C (sin objetos) resultando el siguiente programa:

1. public class Circulo {
2.
3. public static float calcula_area(float r) {
4. float a = 3.1416 * r * r;
5. return a;
6. }
7.
8. public static void main(String[] _args) {
9. //Datos: variables locales
10. float radio = 10.5;
11. float area = calcula_area(radio);
12. System.out.println("area = " + area);
13. }
14.
15. }

Sin embargo con Java se pueden utilizar variables globales sin los inconvenientes de la programación estructurada. Esto se consigue restringiendo la visibilidad de los datos globales: los datos sólo serán accesibles únicamente por los
métodos que necesitan acceder a cada tipo de dato, y ambos (métodos y datos) se engloban en un nuevo prototipo de dato más complejo que los utilizados en la programación estructurada. Este nuevo tipo de dato es el objeto. De este modo en el
ejemplo anterior se puede crear un nuevo tipo de dato llamado Circulo el cual posea un dato llamado radio, y una operación llamada calculaArea() resultando la siguiente clase:

1. public class Circulo {
2.
3. float radio;
4.
5. public float calculaArea() {
6. float a = 3.1416 * radio * radio;
7. return a;
8. }
9.
10. }

En si esta clase no es un programa, es un tipo de dato “inteligente”: tiene un estado (el valor en cada momento del conjunto de sus variables) y un comportamiento (el conjunto de sus métodos). Este tipo de dato facilita la creación de un programa, ya que gran parte del trabajo lo realiza el propio dato.

Tanto a las variables (los datos) como a los métodos (las operaciones) se las denomina de manera genérica miembros de la clase, ya que son entidades que pertenecen a la clase al mismo nivel. Así un programa Java que calcule el área de un
círculo se reducirá a lo siguiente:

1. Se creará un objeto de tipo círculo
2. Se establecerá el radio de dicho objeto círculo
3. Se calculará el area de dicho objeto

El código Java que implementa los pasos anteriores serían los siguientes:

1. Circulo c = new Circulo();
2. c.radio = 10.5;
3. float area = c.calculaArea();

Como se ve tanto el dato (el radio) como la operación (calcular el área) pertenecen al objeto (objeto c), el cual es de tipo circulo (se crea a partir de la clase Circulo). Ni el dato ni la operación podrían ser utilizados sin la previa creación del
objeto. Las 3 líneas anteriores para que sean un programa Java deben ser incluidas en el método main de una clase. Para ello se podría crear una clase específica (la llamaríamos por ejemplo CalculoCirculo), o lo que es más usual, reutilizar la clase Circulo añadiéndole un método main al final de la misma sin que ello modifique la estructura del tipo de dato círculo que se ha creado. Normalmente en un programa el programador crea varios objetos a partir de las definiciones de sus clases. Una vez creados los objetos, estos se utilizan invocando sus métodos para realizar las distintas operaciones.

El acto de invocar un método es referido algunas veces como el envío de un mensaje al objeto, en el cual se requiere que el servicio (método) sea ejecutado. Una vez completado el trabajo del objeto, el espacio en memoria usado y los recursos utilizados pueden ser libreados, para que puedan ser usados por otros objetos del programa. El ciclo de vida de un objeto consta por lo tanto de las siguientes fases: creación, uso y destrucción.

Creación de objetos Al igual que ocurría con las variables de tipos de datos primitivos, las variables que almacenan objetos hay que declararlas en primer lugar. La declaración de un objeto consiste en la declaración de una variable cuyo tipo es la clase del objeto. Este tipo de variables, se denominan en Java referencias. La forma de declarar una referencia es igual que las variables de tipos primitivos:

NombreClase nombreVariable;

Por ejemplo, se la siguiente clase que representa un punto de dos dimensiones:

public class Punto {
double x = 0;
double y = 0;
}

Para declarar una variable de tipo Punto habría que escribir la siguiente línea de código: Punto pt; Al igual que ocurría con las variables de tipos primitivos, es conveniente inicializar las referencias. El valor análogo al 0 para las referencias es la
palabra reservada null: Punto pt = null; Aparte de la declaración, para poder usar las referencias, hay que realizar un paso más respecto a lo que se hacía con las variables de tipos primitivos. Antes de usar un objeto, y tras su declaración, hay que instanciarlo. La instanciación consiste en la creación en sí del objeto, es decir, ponerlo en memoria para su uso. Para instanciar un objeto se necesita llamar a un constructor de la clase mediante el operador new. El constructor es un método especial, que se llama igual que la clase, y que se ejecuta automáticamente al crear un objeto de dicha clase. En nuestro ejemplo, la declaración e instanciación de un objeto de tipo Punto se haría como sigue:

Punto pt; //declaración
pt = new Punto(); //instanciación

Aunque es más usual realizar las dos acciones anteriores en un solo paso:

Punto pt = new Punto();//declaración + instanciación

Uso de objetos Una vez que se ha creado un objeto se podrá utilizar de las siguientes formas: accediendo a sus variables de instancia o invocando a sus métodos.

Para referirse a los miembros (variables de instancia y métodos) de un objeto se utiliza el operador punto “.”:

referencia.miembro

Por ejemplo, para usar el objeto pt del ejemplo anterior, con el fin de establecer un valor para sus variables de instancia x e y, habría que escribir las siguientes sentencias:

Punto pt = new Punto();
pt.x = 5;
pt.x = 10;

Si un objeto no es instanciado no se podrá usar. Si se intenta usar se obtendrá la excepción en tiempo de ejecución NullPointerException y el programa finalizará:

Punto pt; pt.x = 7; //Error en ejecución!!!

Eliminación de objetos. Java nos libera de la tediosa tareade destruirlos cuando no sigan siendo necesarios. El entorno de ejecución de Java elimina los objetos cuando determina que no se van a utilizar más, es decir, que han dejado de ser necesarios. Este proceso es conocido como recolección de basura.

Un objeto puede ser elegido por el recolector de basura cuando no existen más referencias a él. Existen dos motivos por los cuales un objeto deja de estar referenciado. El primero es que la variable que mantiene la referencia salga de su ámbito. El segundo es cuando se borra explícitamente un objeto referencia mediante la selección de un valor cuyo tipo de dato es una referencia a null.

El recolector de basura de Java es un proceso que dinámicamente examina la memoria en busca de objetos. Marca todos aquellos que estén siendo referenciados y cuando termina, los que no están marcados los elimina.

Java antes de que el objeto sea eliminado definitivamente, el recolector de basura le da una oportunidad para limpiarse él mismo mediante la llamada al método finalize() del propio objeto. Durante el proceso de finalización,un objeto podría liberar los recursos que tenga en uso, como pueden ser los ficheros abiertos, conexiones a bases de datos, registros de auditoría, etc... y así poder liberar referencias a otros objetos para que puedan ser seleccionados también por el recolector.

El método finalize() es un miembro de la clase java.lang.Object y cualquier clase Java que lo necesite debe sobreescribirlo e indicar en él las acciones necesarias a realizar cuando se eliminen los objetos de ese tipo.

2. Clases

Una clase es un prototipo o plantilla a partir de la cual se crean objetos por lo que a los objetos también se les suele llamar instancias de una clase. Una clase no tiene entidad física, es algo abstracto que no ocupa memoria. Un objeto sin embargo si que tiene entidad real. Cada objeto tiene su propio espacio en memoria para almacenar sus datos, y sus métodos actuarán sobre estos datos. Así por ejemplo, si añadimos a la clase Punto un método que imprima las variables de instancia:

public class Punto {
double x = 0;
double y = 0;
void coordenadas() {
System.out.println(“[x,y]=[” + x + “,” + y + “]”);
}
}

Si creamos dos objetos de tipo Punto con coordenadas diferentes, al invocar al método coordenadas() sobre cada uno de los objetos, se obtendrán respuestas diferentes:

1. Punto p1 = new Punto();
2. Punto p2 = new Punto();
3. p1.x = 1;
4. p1.y = 5;
5. p2.x = 30;
6. p2.y = 10;
7. p1.coordenadas();
8. p2.coordenadas();

Al ejecutar el código anterior se obtendría el siguiente resultado:

[x,y] = [1,5]
[x,y] = [30,10]

Creación de clases Una clase está formada por dos secciones bien diferenciadas: las declaraciones de las variables que serán almacenadas en cada objeto (los datos) y las declaraciones de los métodos que pueden ser invocados usando el objeto (las operaciones). El convenio para dar nombre a las clases en Java es:

1. La primera letra de la clase va en MAYÚSCULAS y el resto de letras en minúsculas. Ejemplo: Calendario.

2. Cuando hay más de una palabra se sigue este mismo convenio pero separando las palabras que forman el nombre de la clase poniendo la primera letra de cada palabra en mayúsculas. Ejemplo:
CalendarioLunar.

En un fichero de código fuente Java pueden definirse varias clases, pero sólo una de ellas debe ser public, la cual además será la que le de nombre al fichero .java.

3. Miembros estáticos

Se ha visto que los objetos son un tipo de variable especial que posee internamente sus propios datos y además tiene métodos que actúan sobre esos datos de forma exclusiva. Con el tipo de dato objeto se logra que las variables de instancia del objeto sean variables globales a nivel de objeto, es decir que cualquier método del objeto podrá acceder directamente a dichas variables sin necesidad de pasarlas mediante argumentos. Un buen lenguaje de programación orientado a objetos debería tener además un tipo de variable global a nivel de clase, es decir un tipo de variable a la cual puedan acceder todos los objetos de una misma clase. Estas variables pueden ser vistas como una zona de memoria global que se dedicará a almacenar fundamentalmente datos que sean de gran utilidad como por ejemplo constantes matemáticas, rutas de ficheros, mensajes de error, etc. Igualmente también existen métodos a nivel de clase que contendrán rutinas de gran utilidad para todo el programa como por ejemplo cálculos matemáticos, o accesos a variables globales a nivel de clase. Java
permite declarar variables y métodos globales de este tipo mediante el modificador de acceso static. denominándose por este motivo variables y métodos estáticos.

Por ejemplo, volviendo a la clase Circulo, la constante matemática ? (3.1416) es una candidata perfecta a convertirse en variable estática. En un primer lugar se podría declarar como variable de instancia de la clase:

1. public class Circulo {
2.
3. float radio;
4. float PI = 3.1416;
5.
6. public float calculaArea() {
7. float a = PI * radio * radio;
8. return a;
9. }
10.
11. }

En este caso cada vez que se instancie un objeto de tipo círculo, la variable PI estará incluida en la zona de memoria asociada a cada objeto, de modo que por cada objeto que se cree se estará desperdiciando memoria, ya que no tiene sentido almacenar el valor 3.1416 a nivel de objeto.

Sería por tanto mejor solución declarar dicha variable como estática:

1. public class Circulo {
2.
3. static float PI = 3.1416;
4. float radio;
5.
6. public float calculaArea() {
7. float a = Circulo.PI * radio * radio;
8. return a;
9. }
10.
11. }

Al declarar PI como variable estática, se está promocionándola a un nivel superior al del objeto: PI ya no pertenece a ningún objeto y por tanto no necesita de un objeto para existir, y es por ello por lo que hay que utilizar el nombre genérico de la clase (y no un objeto en particular) para acceder a ella (Circulo.PI). De hecho se puede acceder a la variable PI sin que se haya instanciado ni un sólo objeto previamente.

A una variable estática se puede acceder tanto desde un método de instancia como desde un método estático. La elección del tipo de método a utilizar dependerá del caso concreto aunque lo normal es que si desde el método sólo se va a
acceder a variables estáticas el método sea también estático. Por ejemplo, supóngase que se quiere mantener un contador del número de objetos círculo que se crean. Para ello se necesita una variable que llamaremos contador que esté compartida por todos los objetos de tipo círculo. de modo que deberá definirse como variable estática. Para leer y
modificar dicha variable se crearán dos métodos: un método incrementarContador() que será llamado tras la creación de un objeto, y un método leerContador() que será llamado cada vez que se quiera conocer el número de objetos círculo creados. Estos dos métodos únicamente accederán a la variable estática contador, por lo que normalmente se
declararán como estáticas:

1. public class Circulo {
2.
3. static float PI = 3.1416;
4. static int contador = 0;
5. float radio;
6.
7. public static void incrementarContador() {
8. contador++;
9. }
10.
11. public static int leerContador() {
12. return contador;
13. }
14.
15. public float calculaArea() {
16. float a = Circulo.PI * radio * radio;
17. return a;
18. }
19.
20. }

Esta clase podría ser utilizada desde el método main de un programa como sigue:

1. Circulo c1 = new Circulo();
2. Circulo.incrementarContador();
3. Circulo c2 = new Circulo();
4. Circulo.incrementarContador();
5. System.out.println(“contador=”+Circulo.leerContador());

Como los métodos estáticos existen sin necesidad de ningún objeto, no se puede acceder desde un método estático a las variables de instancia de un objeto ya que los métodos estáticos pueden ser ejecutados incluso sin que exista ni un sólo objeto creado.

Además aunque existieran objetos creados, los métodos estáticos operan en un contexto diferente a los objetos. Recuérdese que el método main de un programa Java es estático. Los diseñadores del lenguaje lo han definido de este modo para que no haya que crear un objeto a la hora de ejecutar un programa, y de este modo ahorrar recursos ya que al ejecutar el programa sólo se cargarán en memoria las variables estáticas.

4. Métodos

Un método es un conjunto de instrucciones que realizan una tarea concreta y que se agrupan bajo un identificador. La utilidad principal del uso de métodos es la reutilización de código: un método puede ser utilizado muchas veces desde un mismo programa o desde otros programas sin tener que volver a escribir el código contenido en el método. Otra utilidad es mejorar la legibilidad del código: aunque no sea necesario crear un método para agrupar un conjunto de instrucciones, puede ayudar a hacer el programa más legible por terceras personas. El concepto de método es análogo al de función o procedimiento de los lenguajes de programación procedurales. Un método puede ser visto como una caja negra, con una serie de entradas sobre las que realiza una serie de operaciones y genera unas salidas. Un método desde el punto de vista de su programación es un bloque de código que tiene nombre, tipo de acceso, tipo de retorno y una lista de argumentos (también llamados parámetros). Toda definición de método consta de dos partes: la cabecera y el cuerpo. La sintaxis de la declaración de un método es la siguiente ([ x ] = opcional):

[visibilidad] [static] retorno nombre ([parámetros]) { //cuerpo del método }

donde:

- [visibilidad]: es el nivel de accesibilidad del método. Es un parámetro opcional que en caso de incluirlo debe ser una de las siguientes palabras reservadas: public, private o protected.
- [static]: parámetro opcional que al incluirse en la definición del método indica que el método es estático, es decir global a nivel de clase. Si no se especifica nada el método será de instancia, es decir, global a nivel de objeto.
- retorno: Es un campo obligatorio que especifica el tipo de dato que devuelve el método a quien lo ha invocado tras finalizar su ejecución. Es válido cualquier tipo de dato (tipos primitivos y tipos objeto). En caso de que el método no devuelva nada se escribirá la palabra reservada void.
- nombre: Es el campo obligatorio para indicar como se llama el método.
Es válido cualquier identificador Java válido.
- [parámetros]: parámetro opcional formado por la lista de los argumentos en formato tipo nombre separados por comas, donde tipo es el tipo de dato del argumento y nombre es el nombre del parámetro formal (se verá más adelante). En caso de no recibir argumentos el método es obligatorio dejar los paréntesis vacíos.

Todos los métodos deben definirse al mismo nivel, es decir, no se puede definir un método dentro de otro. Es un error muy frecuente al comenzar a programar el incluir los nuevos métodos que se van creando dentro del método main. Por otro lado recuérdese también que todas las definiciones de métodos deben escribirse dentro del cuerpo de una clase.

La sentencia return Cuando se pretende devolver un valor calculado dentro del método (en su cuerpo), hay que escribir la instrucción return seguida de una expresión o variable con el valor a devolver. Este valor retornado debe ser del mismo tipo que el especificado en la definición del método. Se pueden escribir varias sentencias return a lo largo de un método y en distintas posiciones aunque hay que tener cuidado para no dejar algún fragmento de código inalcanzable ya que la ejecución de la sentencia return finaliza el método aunque llegado al final de éste. El siguiente ejemplo muestra un
método estático suma() que realiza la suma de dos valores recibidos como argumentos y devuelve el valor de la suma en una variable:

public static int suma(int op1, int op2)
{
int resultado = op1 + op2;
return resultado;
}

También se podía haber escrito de manera simplificada devolviendo el valor de la suma en una expresión ahorrándose de este modo memoria:

public static int suma(int op1, int op2) { return op1 + op2; }

Llamada a un método Cuando definimos un método, establecemos qué tarea va a realizar y, en caso de que devuelva un valor, cual será la expresión a retornar. Para hacer uso del método en el código fuente se ha de escribir como una sentencia más el nombre del método seguido de (entre paréntesis) tantos parámetros y del mismo tipo como los que se especifiquen en la definición del método. Si no hay lista de parámetros, se hará la llamada escribiendo el nombre del método seguido por un paréntesis de apertura y uno de cierre. Esa es la forma de realizar una “llamada (o invocación) a un método”. En el momento de la llamada al método el flujo de ejecución del programa pasa de la sentencia de llamada, al interior del método, y éste retiene el flujo hasta que se produce el retorno mediante una instrucción return o hasta que termina el cuerpo del método. Tras esto, el flujo de ejecución retorna de nuevo a la línea siguiente desde la cual se llamó al método, continuándose la ejecución del programa. Analicemos la ejecución del siguiente programa:

1. public class Metodos {
2.
3. public static void saludo(String msg) {
4. System.out.println(msg);
5. }
6.
7. public static void main(String[] args) {
8. System.out.println(“Antes de saludo()...”);
9. saludo(“Hola”);
10. System.out.println(“...después de saludo()”);
11. }
12.
13. }

Este programa comienza (como cualquier programa Java) a partir del método main (línea 7). Tras ello el flujo de ejecución pasa a la línea 8 que se ejecuta imprimiendo por pantalla el siguiente mensaje: Antes de saludo()...

La siguiente línea a ejecutar (línea 9) es una llamada al método saludo(), al cual se le pasa como argumento el mensaje a imprimir. El método no sabe qué debe imprimir por pantalla hasta que alguien lo invoque y le pase una cadena con el mensaje a imprimir. La llamada al método hace que el flujo del programa pase desde la línea 9 a la línea 3. En este salto el argumento msg (llamado parámetro formal) toma el valor pasado como argumento pasado desde la línea 9 (llamado parámetro actual). Tras esto se ejecuta la sentencia de la línea 4 mostrando por pantalla el mensaje recibido: Hola

La línea 5 supone el fin del método saludo() de modo que el flujo de ejecución retorna al método main continuándose la ejecución a partir de la línea 10, la cual imprimirá el último mensaje del programa: ...después de saludo()

El método anterior saludo() es de tipo void, es decir no devuelve ningún valor a quién lo llama, por lo que para invocarlo únicamente hay que poner su nombre y lista de parámetros actuales en caso de que los tenga: saludo(); Cuando el método devuelva algún valor podría llamarse como en el caso anterior, pero si así se hiciera se estaría perdiendo el valor retornado por el método. Por ello para llamar a un método con valor de retorno hay que capturar dicho valor devuelto en una variable que sea del mismo tipo que la devuelta por el método. Supongamos que el método saludo() no imprimiera el
mensaje, sino que devolviera el mensaje a imprimir para que lo muestre por pantalla el método que lo invoque.

La línea 9 del listado anterior deberá sustituirse por la siguiente: String msg = saludo();

El listado total del programa modificado sería el siguiente:

1. public class Metodos {
2.
3. public static String saludo() {
4. return “Hola”;
5. }
6.
7. public static void main(String[] args) {
8. System.out.println(“Antes de saludo()...”);
9. String msg = saludo();
10. System.out.println(msg);
11. System.out.println(“...después de saludo()”);
12. }
13.
14. }

Por simplicidad se puede utilizar la llamada a un método con valor de retorno directamente una expresión o incluido en la llamada a otros métodos como si de una variable más se tratara tal y como puede verse en el ejemplo anterior modificado:

1. public class Metodos {
2.
3. public static String saludo() {
4. return “Hola”;
5. }
6.
7. public static void main(String[] args) {
8. System.out.println(“Antes de saludo()...”);
9. System.out.println(saludo());
10. System.out.println(“...después de saludo()”);
11. }
12.
13. }

Paso de parámetros Se han definido el apartado anterior los conceptos de parámetros formales como los argumentos tal y como se definen en la declaración del método, y parámetros actuales como los argumentos reales pasados a un método desde fuera de él. Los parámetros formales son por tanto internos al método y los actuales son externos al método. Los parámetros actuales se utilizan en la llamada al método y los formales en la recepción (tras el salto del flujo de ejecución).

Es un error muy frecuente en los programadores noveles confundir estos dos tipos de parámetros. Hay que tener muy presente que estos dos tipos de parámetros se almacenan en variables diferentes, y por ello normalmente se utilizan en su declaración nombres de variable diferentes para no confundirlas. La única relación entre los dos tipos de variables reside en el hecho de que el valor de la variable real se copia en la variable formal. El hecho de que ambas variables contengan el mismo valor es el origen de la confusión entre ellas.

Algunos programadores suelen utilizar el mimo nombre en las variables formales y actuales, pero añaden un carácter de subrayado “_” como prefijo de las variables formales para diferenciarlas de las actuales. Ambos tipos de variables se
encuentran en un ámbito (contexto) diferente, de modo que el acceso desde un ámbito al otro en general no es posible. El acceso desde el ámbito de una variable actual (externa al método) a una variable formal (interna al método) no es posible, ya que las variables declaradas en el interior de un método no tienen existencia hasta que el flujo de ejecución entra en el método y dejan de existir en el momento en el que el flujo sale de él. Es el caso del siguiente ejemplo:

1. public static double raizCuadrada(double num) {
2. if (num >= 0) {
3. return Math.sqrt(num);
4. }
5. System.out.println(“Error: número negativo”);
6. return -1;
7. }
8.
9. public static void main(String[] _args) {
10. double numero = 100;
11. double raiz = raizCuadrada(numero);
12. num = 20;
13. }

En el listado anterior la línea 12 dará error ya que no se puede acceder desde fuera a una variable declarada en el interior de un método.

El proceso contrario, esto es, el acceso desde dentro del método (contexto de las variables formales) a una variable externa (contexto de variables actuales) tampoco es posible tal y como puede verse en el siguiente ejemplo:

1. public static double raizCuadrada(double num) {
2. num = --numero;
3. if (num >= 0) {
4. return Math.sqrt(num);
5. }
6. System.out.println(“Error: número negativo”);
7. return -1;
8. }
9.
10. public static void main(String[] _args) {
11. double numero = 100;
12. double raiz = raizCuadrada(numero);
13. }

En este listado la línea 2 dará error ya que no se puede acceder desde dentro de un método a una variable declarada dentro de otro método diferente. La única excepción a esto es cuando la variable externa no se declara dentro del método, sino dentro de la clase, es decir si se trata de una variable de instancia o estática. Veamos cual sería el valor de la variable raiz al llegar el flujo de programa a la línea 15 del siguiente programa:

1. static double numero = 0;
2.
3. public static double raizCuadrada(double num) {
4. numero = --num;
5. if (num >= 0) {
6. return Math.sqrt(num);
7. }
8. System.out.println(“Error: número negativo”);
9. return -1;
10. }
11.
12. public static void main(String[] _args) {
13. numero = 100;
14. double raiz = raizCuadrada(numero);
15. }

En la línea 4 se está accediendo desde el interior de un método a una variable externa (numero), lo cual en este caso es posible por tratarse una de una variable de clase (en particular se ve que es una variable estática).

En la línea 13 se da el valor 100 a dicha variable (la cual ya no está declarada dentro del método main). Tras ello en la línea 14 se realiza la llamada al método raizCuadrada() y en el salto del método main a dicho método se copia el valor 100 desde el parámetro actual numero al parámetro formal num declarado en la cabecera del método (línea 3).

La línea 4 ya dentro del método llamado equivale a las dos siguientes y en ese mismo orden:

num = num – 1; numero = num;

De modo que tras ejecutarse dicha línea tanto la variable interna num como la variable externa numero tomarán ambas el valor 99. Por esta razón el valor devuelto en la línea 6 por el método será 99 el mismo que se guardará finalmente en la variable raiz tras la ejecución completa de la línea 14.

Paso por valor y por referencia JAVA admite dos formas de paso de parámetros en las llamadas a métodos dependiendo del tipo de los parámetros usados:

- Cuando se pasan tipos de datos primitivos (int, double, ...) el proceso consiste en pasar una copia del valor de los parámetros actuales dentro de los parámetros formales. La información estará por tanto duplicada en memoria. Hemos visto que las modificaciones realizadas dentro de los métodos sobre los parámetros formales no se reflejará en el valor de los parámetros actuales puesto que estaríamos modificando copias. Esto se conoce como paso de parámetros por valor.

- En el caso de que los parámetros pasados sean de tipo objeto (por ejemplo variables de tipo String, arrays, etc.), lo que se pasa es una copia del valor almacenado por la referencia que apunta al objeto. La referencia a un objeto es una variable que contiene la dirección de memoria del objeto, es decir puede verse como una llave para controlar el objeto de manera total. De esa forma, si pasamos una copia de la llave podremos “abrir la puerta” del objeto, es decir modificarlo, ya que ambas variables (actual y formal) apuntarán a la misma dirección de memoria en la cual está almacenado el objeto. La modificación del objeto utilizando la copia de la referencia dentro del procedimiento afecta pues de manera irreversible al objeto. Este tipo de paso de parámetro se denomina paso de parámetros por referencia.

Como ejemplo de paso por valor analicemos la salida del siguiente fragmento de código:

1. public static void main(String _args[]) {
2. int i = 0;
3.System.out.println(“main(A): i=”+i);
4. valor(i);
5. System.out.println(“main(D): i=”+i);
6. }
7.
8. public static void valor(int _i) {
9. System.out.println(“valor(A): _i=”+_i);
10. _i++;
11. System.out.println(“valor(D): _i=”+_i);
12. }

En este caso en la línea 4 se está llamando al método valor() pasando como argumento un dato de tipo primitivo (int) de modo que el paso del parámetro será por valor. Cualquier modificación dentro del método no afectará a la variable original i, ya que los cambios se hacen sobre una copia de dicha variable. La copia es la variable formal _i. La salida del fragmento anterior será entonces la siguiente:

main(A): i=0 valor(A): _i=0 valor(D): _i=1 main(D): i=0

Para ver un ejemplo análogo de paso por referencia necesitamos un objeto en vez de una variable de tipo primitivo. Crearemos a tal efecto una tipo de dato objeto cuyo único objetivo será almacenar un dato de tipo entero. Será la versión objeto del tipo de dato int. La clase será la siguiente:

public class Int { int dato = 0; }

A partir del tipo recién creado considérese el fragmento de código siguiente análogo al del ejemplo anterior:

1. public static void main(String _args[]) {
2. Int i = new Int();
3. System.out.println(“main(A): i.dato=”+i.dato);
4. referencia(i);
5. System.out.println(“main(D): i.dato=”+i.dato);
6. }
7.
8. public static void valor(Int _i) {
9. System.out.println(“referencia(A):_i.dato=”+_i.dato);
10. _i.dato++;
11. System.out.println(“referencia(D):_i.dato=”+_i.dato);
12. }

En este ejemplo en la línea 4 se está llamando al método referencia() pasándole un tipo de dato objeto, el cual en su interior tiene una variable de instancia de tipo int llamada dato. Al llamar al método se está copiando la referencia actual i en la referencia formal _i, de modo que ambas referencias apuntan al mismo objeto almacenado en memoria. Al modificar el interior de dicho objeto en la línea 10 el cambio permanecerá tras finalizar el método por lo que la salida del programa seria la siguiente:

main(A): i.dato=0 referencia(A): _i.dato=0 referencia(D): _i.dato=1 main(D):i.dato=1

Constructores Como se ha visto anteriormente para instanciar un objeto es necesario invocar a su constructor. El constructor es un método especial que es automáticamente invocado al crear el objeto. Sin embargo hasta el momento este
método no ha sido necesario definirlo ni implementarlo, sino que únicamente se ha utilizado tras el operador new y sin pasarle argumentos.

Este constructor que hemos estado utilizando se llama constructor por defecto, y Java no obliga a definirlo dentro de las clases. El constructor por defecto no recibe argumentos de modo que todos los objetos creados con él son iguales, por lo que para inicializar las variables de instancia del objeto creado hay que darles valor explícitamente tras crearlo:

Punto pt = new Punto(); pt.x=10; pt.y=20;

Sin embargo lo usual es que cuando se crea una clase se defina en su interior uno o varios constructores con argumentos que permitan inicializar las variables de instancia en el mismo momento de la creación del objeto.

Si la clase Punto dispusiera de un constructor de este tipo las coordenadas x e y se podrían establecer en la propia instanciación del siguiente modo:

Punto pt = new Punto(10,20);

Esto, además de reducir el número de líneas de código, evita errores comunes de programación por olvido de inicialización de los objetos. En el ejemplo anterior se ha utilizado un constructor que no es el de por defecto, de modo que habrá que definirlo e implementarlo dentro de la clase. En concreto el constructor usado tendría la siguiente implementación:

1. public Punto(double _x, double _y){ 2. x = _x; 3. y = _y; 4. }

Nótese que el constructor no tiene tipo de retorno (ni siquiera void). Los argumentos que recibe se utilizan para inicializar las variables de instancia x e y. Para mayor comprensión los argumentos recibidos en el constructor suelen llamarse igual que las variables de instancia a las que inicializan, pero con el carácter de subrayado “_” usado como prefijo. No obstante, y al igual que el resto de los métodos, puede usarse como nombre de variable cualquier identificador Java válido. Si la clase
estuviera encapsulada, en el constructor no se deberían establecer el valor de las variables de instancia directamente, sino a través de los métodos accesores set, de modo que se preserve el encapsulamiento:

1. public Punto(double _x, double _y){
2. x = setCoordenadaX(_x);
3. y = setCoordenadaY(_y);
4. }

En resumen, las características de los constructores son las siguientes:

1. Son métodos no estáticos.
2. Tiene el mismo nombre de la clase.
3. No deben devolver ningún valor, ni siquiera void.
4. Deben ser públicos.
5. Son llamados automáticamente al crear el objeto con el operador new. En una clase se pueden especificar tantos constructores como sea necesario diferenciándose entre sí por el número, orden y tipo de sus argumentos: Por ejemplo los siguientes 5 son constructores válidos de una clase Empleado:

1. Prueba();
2. Prueba(int edad);
3. Prueba(long edad);
4. Prueba(String nombre);
5. Prueba(int a);
6. Prueba(String nombre, int edad);

Sin embargo, los constructores 2 y 5 son el mismo, ya que su única diferencia es el nombre la de variable que representa al parámetro formal, lo cual no es motivo de diferencia entre dos constructores.

Es curioso el comportamiento que los diseñadores de Java han establecido para cuando se defina un constructor con argumentos, ya que en ese caso no podrá usarse el constructor por defecto, sino que habría que definirlo explícitamente dentro de la clase si se quisiera dotar a la clase de un constructor sin argumentos junto al otro constructor que si los tiene. Es convenio de programación el definir los constructores de una clase como los primeros métodos, justo después de las variables de instancia.

<< Volver al tema anterior  || >> Ir al tema siguiente

 

Referencias:

 

http://www.jmordax.com/

 


Autor: Antonio Lopez Atienza


Tags:
Posted in Java | No Comments »

Leave a Comment

 

RSS
MCC D5E