|
![]() |
Referencias |
Anterior | Siguiente |
Java se asemeja mucho a C y C++. Esta
similitud, evidentemente intencionada, es la mejor herramienta para los programadores, ya
que facilita en gran manera su transición a Java. Desafortunadamente, tantas similitudes
hacen que no se repare en algunas diferencias que son vitales. La terminología utilizada
en estos lenguajes, a veces es la misma, pero hay grandes diferencias subyacentes en su
significado, como se ha recalcado a lo largo de esta sección. C tiene tipos de datos básicos y punteros. C++ modifica un poco este panorama y le añade los tipos referencia. Java también especifica sus tipos primitivos, elimina cualquier tipo de punteros y tiene tipos referencia mucho más claros. Todo este maremágnum de terminología provoca cierta consternación, así que los párrafos que siguen intentan aclarar lo que realmente significan los términos que se utilizan. Se conocen ya ampliamente todos los tipos básicos de datos: datos base, integrados, primitivos e internos; que son muy semejantes en C, C++ y Java; aunque Java simplifica un poco su uso a los desarrolladores haciendo que el chequeo de tipos sea bastante más rígido. Además, Java añade los tipos boolean y hace imprescindible el uso de este tipo booleano en sentencias condicionales. C y C++ permiten la declaración y uso de punteros, que pueden ser utilizados en cualquier lugar. Esta tremenda flexibilidad resulta muy útil, pero también es la causa de que se pueda colgar todo el sistema. La intención principal en el uso de los punteros es comunicarse más directamente con el hardware, haciendo que el código se acelere. Desafortunadamente, este modelo de tan bajo nivel hace que se pierda robustez y seguridad en la programación y hace muy difíciles tareas como la liberación automática de memoria, la defragmentación de memoria, o realizar programación distribuida de forma clara y eficiente. Las referencias se incorporaron a C++ en un intento de manejar punteros de C de forma más limpia y segura. Sin embargo, como no elimina los punteros, la verdad es que su propósito lo consigue a medias. Es más, se podría decir que con las referencias C++, el lenguaje se vuelve más complicado y no es más poderoso que antes. Las referencias deben ser inicializadas cuando se declaran y no se pueden alterar posteriormente. Esto permite incrementar la eficiencia en tiempo de ejecución sobre la solución basada en punteros, pero es más por las deficiencias de los punteros que por las ventajas de las referencias. Las referencias en Java no son punteros ni referencias como en C++. Este hecho crea un poco de confusión entre los programadores que llegan por primera vez a Java. Las referencias en Java son identificadores de instancias de las clases Java. Una referencia dirige la atención a un objeto de un tipo específico. No hay por qué saber cómo lo hace ni se necesita saber qué hace ni, por supuesto, su implementación. Piénsese en una referencia como si se tratase de la llave electrónica de la habitación de un hotel. Vamos a utilizar precisamente este ejemplo del Hotel para demostrar el uso y la utilización que se puede hacer de las referencias en Java. Primero se crea la clase Habitacion, que está implementada en el fichero Habitacion.java, mediante instancias de la cual se levantará el Hotel: public class Habitacion { private int numHabitacion; private int numCamas; public Habitacion() { habitacion( 0 ); } public Habitacion( int numeroHab ) { habitacion( numeroHab ); } public Habitacion( int numeroHab,int camas ) { habitacion( numeroHab ); camas( camas ); } public synchornized int habitacion() { return( numHabitacion ); } public synchronized void habitacion( int numeroHab ) { numHabitacion = numeroHab; } public synchronized int camas() { return( camas ); } public syncronized void camas( int camas ) { numCamas = camas; } } El código anterior sería el corazón de la aplicación. Ahora se construye el Hotel creando Habitaciones y asignándole a cada una de ellas su llave electrónica; tal como muestra el código siguiente, Hotel1.java: public class Hotel1 { public static void main( String args[] ) { Habitacion llaveHab1; // paso 1 Habitacion llaveHab2; llaveHab1 = new Habitacion( 222 ); // pasos 2, 3, 4 y 5 llaveHab2 = new Habitacion( 1144,3 ); // ^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^ // A B y D C } } Para explicar el proceso, se dividen las acciones en los cinco pasos necesarios para poder entrar en la habitación del hotel. Aunque no se incluye, se puede también considerar el caso de la necesidad de un cerrajero, para que ante la pérdida de la llave se pueda abrir la puerta; y que, en el caso particular de este hotel virtual Java, sería el garbage collector, que recicla la habitación una vez que se hayan perdido todas las llaves. El primer paso es la creación de la llave, es decir, definir la variable referencia, por defecto nula. El resto de los pasos se agrupan en una sola sentencia Java. La parte B en el código anterior indica al gerente del Hotel que ya dispone de una nueva habitación. La parte C llama al decorador de interiores para que "vista" la habitación según un patrón determinado, para que no desentonen unas habitaciones con otras y no se pierdan las señas de identidad del hotel. El código electrónico que permitirá acceder a la habitación se genera en la parte D, una vez conocido el interior de la habitación y ya se programa en la llave en la parte A. Si se abandona el ejemplo real a un lado y se detiene uno en lo que ocurre en la ejecución del código, se observa que el operador new busca espacio para una instancia de un objeto de una clase determinada e inicializa la memoria a los valores adecuados. Luego invoca al método constructor de la clase, proporcionándole los argumentos adecuados. El operador new devuelve una referencia a sí mismo, que es inmediatamente asignada a la variable referencia. Se pueden tener múltiples llaves para una misma habitación: . . . Habitacion llaveHab3,llaveHab4; llaveHab3 = llaveHab1; llaveHab4 = llavehab2; De este modo se consiguen copias de las llaves. Las habitaciones en sí mismas no se han tocado en este proceso. Así que, ya hay dos llaves para la habitación 222 y otras dos para la habitación 1144. Una llave puede ser programada para que funcione solamente con una habitación en cualquier momento, pero se puede cambiar su código electrónico para que funcione con alguna otra habitación; por ejemplo, para cambiar una habitación anteriormente utilizada por un empedernido fumador por otra limpia de olores y con vistas al mar. Se cambiaría la llave duplicada de la habitación del fumador (222) por la habitación con olor a sal marina, 1144: . . . llaveHab3 = llaveHab2; Ahora hay una llave para la habitación 222 y tres para la habitación 1144; manteniendo una llave para cada habitación en la conserjería, para poder utilizarla como llave maestra, en el caso de que alguien pierda su llave propia. Alguien con la llave de una habitación puede hacer cambios en ella, y los compañeros que tengan llave de esa misma habitación, no tendrán conocimiento de esos cambios hasta que vuelvan a entrar en la habitación. Por ejemplo, si que quita una de las camas de la habitación, entrando en ella con la llave maestra: . . . llaveHab2.camas( 2 ); Ahora cuando los inquilinos entren en la habitación podrán comprobar el cambio realizado: . . . llaveHab4.printData(); Como en C y C++, Java dispone de arrays de tipos primitivos o de clases. Los arrays en C y C++ son básicamente un acompañante para los punteros. En Java, sin embargo, son ciudadanos de primera clase. Para expandir el hotel creando todo un ala de habitaciones, Hotel2.java, se creará un juego de llaves maestras y luego se construirán las habitaciones: public class Hotel2 { // Número de habitaciones por ala public static final int habPorAla = 12; public static void main( String args[] ) { Habitacion llavesMaestras[]; // paso 1 llavesMaestras = new Habitacion[ habPorAla ]; // pasos 2-5 // ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ // A B, C, D y E int numPiso = 1; for( int i=0; i < habPorAla; i++ ) // pasos 6-9 llavesMaestras[ i ] = new Habitacion( numPiso * 100 + i, ( 0 == (i%2)) ? 2 : 1 ); for( int i=0; i < habPorAla; i++ ) // pasos 10-11 llavesMaestras[i].printData(); } } Cada paso en el ejemplo es semejante al que ya se ha visto antes. El paso 1 especifica que el juego de llaves maestras es un grupo de llaves de habitaciones. Los pasos 2 a 5 son, en este caso, la parte principal. En lugar de crear una habitación, el gerente ordena construir un grupo contiguo de habitaciones. El número de llaves se especifica entre corchetes y todas se crean en blanco. Los pasos 6 a 9 son idénticos a los pasos 2 a 5 del ejemplo anterior, excepto en que en este caso todas las llaves pasan a formar parte del juego maestro. Los números de piso se dan en miles para que cuando se creen las habitaciones, todas tengan el mismo formato. También todas las habitaciones de número par tienen una sola cama, mientras que las habitaciones impares tendrán dos camas. Los pasos 10 y 11 permiten obtener información de cada una de las habitaciones del hotel. Hay gente que piensa que como Java no dispone de punteros, resulta demasiado complejo construir listas enlazadas, árboles binarios y grafos. Java dispone de estructuras de datos de esos tipos y además proporciona otras muchas como: mapas, conjuntos, tablas Hash, diccionarios, etc., que en el JDK 1.2 están muy bien diseñadas y siguen la nomenclatura al uso de la OOP. No obstante, en los párrafos siguientes se demuestra que quien así piensa está bastante equivocado, porque incluso de forma pedestre se pueden construir estas estructuras de datos sin demasiado esfuerzo. Hay que retomar el ejemplo de los arrays, y en vez de éstos utilizar una lista doblemente enlazada. El paquete de la lista simple se compone de dos clases. Cada elemento de la lista es un NodoListaEnlazada, NodoListaEnlazada.java: public class NodoListaEnlazada { private NodoListaEnlazada siguiente; private NodoListaEnlazada anterior; private Object datos; // . . . } Cada NodoListaEnlazada contiene una referencia a su nodo precedente en la lista y una referencia al nodo que le sigue. También contiene una referencia genérica a cualquier clase que se use para proporcionar acceso a los datos que el usuario proporcione. La lista enlazada, ListaEnlazada.java, contiene un nodo principio-fin y un contador para el número de nodos en la lista: public class ListaEnlazada { public NodoListaEnlazada PrincipioFin; private int numNodos; // . . . } El nodo especial PrincipioFin es sencillo, para simplificar el código. El contador se usa para optimizar los casos más habituales. Hay que revisar pues el código del Hotel, ahora Hotel3.java, que será prácticamente el mismo que en el caso de los arrays: public class Hotel3 { // Número de habitaciones por ala public static final int habPorAla = 12; public static void main( String args[] ) { ListaEnlazada llaveMaestra; // paso 1 llaveMaestra = new ListaEnlazada(); // pasos 2-5 int numPiso = 1; for( int i=0; i < habPorAla; i++ ) // pasos 6-9 llaveMaestra.insertAt( i, new Habitacion( numPiso * 100 + i, ( 0 == (i%2)) ? 2 : 1 ); for( int i=0; i < habPorAla; i++ ) // pasos 10-12 ( (Habitacion)llaveMaestra.getAt(i) ).printData(); } } El paso 1 es la llave maestra de la lista. Está representada por una lista generica; es decir, una lista de llaves que cumple la convención que se ha establecido. Se podría acelerar el tiempo de compilación metiendo la lista genérica ListaEnlazada dentro de una ListaEnlazadaHabitacion. Los pasos 2 a 5 son equivalentes a los del primer ejemplo. Se construye e inicializa una nueva ListaEnlazada, que se usará como juego de llaves maestras. Los pasos 6 a 9 son funcionalmente idénticos a los del ejemplo anterior con arrays, pero con diferente sintaxis. En Java, los arrays y el operador [] son internos del lenguaje. Como Java no soporta la sobrecarga de operadores por parte del usuario, solamente se puede utilizar en su forma normal. La ListaEnlazada proporciona el método insertAt() que coge el índice en la lista, donde el nuevo nodo ha de ser insertado, como primer argumento. El segundo argumento es el objeto que será almacenado en la lista. Obsérvese que no es necesario colocar moldeo alguno para hacer algo a una clase descendiente que depende de uno de sus padres. Los pasos 10 a 12 provocan la misma salida que los pasos 10 y 11 del ejemplo con arrays. El paso 10 coge la llave del juego que se indica en el método getAt(). En este momento, el sistema no sabe qué datos contiene la llave, porque el contenido de la habitación es genérico. Pero el programa sí sabe lo que hay en la lista, así que informa al sistema haciendo un moldeado a la llave de la habitación (este casting generará un chequeo en tiempo de ejecución por parte del compilador, para asegurarse de que se trata de una Habitacion). El paso 12 usa la llave para imprimir la información. Punteros C/C++ y Referencias Java Ahora que ya se conoce un poco más sobre las referencias en Java, es hora de compararlas con los punteros de C y C++. Los punteros en C y C++ están orientados hacia un modelo físico de funcionamiento. Es decir, que el modelo de punteros se mapea directamente sobre el modelo hardware. Este modelo asume cosas como el no movimiento, lo que hace que mecanismos como su liberación automática resulten mucho menos eficientes o simplemente imposibles. Cosas como la distribución en redes y la persistencia de objetos son muy difíciles de conseguir en C y C++. C y C++ permiten el uso de punteros de tal forma que se puede corromper el sistema, cosa que no puede suceder con las referencias en Java. Cualquier intento de hacer esto sería abortado por el compilador o por el sistema, en tiempo de ejecución (lanzando una excepción). C y C++ dejan la protección de memoria al sistema operativo, que solamente tiene el recurso de generar un error del sistema cuando un puntero accede a una posición no válida. Por el contrario, con el uso de las referencias, Java protege al programador contra sus propias tendencias autodestructivas. |
|