Literales, Asignaciones y Variables en Java

Buenos días, en esta entrada veremos profundamente el tema de los literales, las asignaciones y las variables.


  • Valores literales para todos los tipos primitivos

    Un literal primitivo es meramente una representación en código fuente de un tipo de dato primitivo. Lo siguiente son ejemplos de literales primitivos:

    			'b' 			// Literal para char
    			42				// Literal para int
    			false			// Literal para boolean
    			2546789.343		// Literal para double
    			
    • Literales Enteros

      Hay 3 maneras de representar números enteros en el lenguaje Java: decimal (base 10, octal (base 8), y base hexadecimal (base 16).

    • Literales Decimales

      Los enteros decimales no necesitan explicación: los hemos estado usando desde hace mucho tiempo. En el lenguaje Java, ellos son representados, sin prefijos de ningún tipo, como lo siguiente:

      					in length = 343;
      					
    • Literales Octales

      Los enteros octales solo usan dígitos del 0 al 7. En Java, representamos un entero en forma octal poniendo un 0 delante del número, como lo siguiente:

      					class Octal{
      						public static void main (String[] args){
      							int six = 06;		// Igual que el decimal 6
      							int seven = 07;		// Igual que el decimal 7
      							int eight = 010;	// Igual que el decimal 8
      							int nine = 011;		// Igual que el decimal 9
      							System.out.println("Octal 010 = " + eight);
      						}
      					}
      					

      Vemos que cuando pasamos el siete y no tenemos mas dígitos que usar (se nos permite solo usar dígitos del 0 al 7), volvemos de nuevo al 0, y lo agregamos al comienzo del número. Podemos tener hasta 21 dígitos en un número octal, sin incluir el 0 del comienzo.

    • Literales Hexadecimales

      Los números hexadecimales (hex para abreviar) son construidos usando 16 simbolos distintos. Ya que nunca hemos inventado ningún simbolo como dígito para los números entre el 10 y el 15, usaremos caracteres para representar estos dígitos. Contando desde 0 hasta 15 en hexadecimal es así:

      					0 1 2 3 4 5 6 7 8 9 a b c d e f
      					

      Java aceptará mayusculas o minusculas para los dígitos extra. Se nos permite tener hasta 16 dígitos en un número hexadecimal, sin incluir el prefijo 0x o la extensión del sufijo opcional L, el cual explicaremos luego. Todas las asignaciones para un número hexadecimal legales son las siguientes:

      					class HexTest{
      						public static void main (String[] args){
      							int x = 0X0001;
      							int y = 0X7fffffff;
      							int z = 0xDeadCafe;
      							System.out.println("x = " + x + " y = " + y + " z = " + z);
      						}
      					}
      					

      Si ejecutamos HexTest tendremos los siguientes resultados:

      					x = 1 y = 2147483647 z = -559035650
      					

      No debemos preocuparnos por las mayusculas o minusculas, en este caso OXCAFE y Oxcafe son ambos legales y contienen el mismo valor.

      Los 3 literales enteros (octal, decimal y hexadecimal están definidos como int por defecto, pero se les podría haber especificado tambien como long poniendo un sufijo L o l después del número:

      					long jo = 110599L;
      					long so = 0xFFFFl; 	// l en minusculas tambien es válido
      					
    • Literales Puntos Flotantes o Floating-Points

      Los números floating-point son definidos como un número, con símbolo decimal, y mas números representando la fracción.

      					double d = 11301874.9881024;
      					

      En el ejemplo anterior, el número 11301874.9881024 es el valor literal. Los literales Floating-point están definidos como double (64 bits) por defecto, por lo que si queremos asignar un literal floating-point a una variable de tipo float (32 bits), debemos añadir el sufijo F o f al número. Si no lo hacemos, el compilador nos avisará de una perdida de precisión, ya que estamos intentando meter un número en un “contenedor” menos preciso. El sufijo F nos dá una manera de decirle al compilador “Hey, Se lo que estoy haciendo, y tomaré el riesgo, gradias.”.

      					float f = 23.467890; // Error de compilador, posible perdida de precision
      					float g = 49837849.029847F; // Ok, tiene el sufijo "F"
      					

      Podemos tambien opcionalmente añadir una D o d a los literales double, pero no es necesario porque es un comportamiento por defecto:

      					double d = 110599.995011D; // Opcional, no se requiere
      					double g = 987.897; // No hay sufijo D, pero esta bien porque el literal es double por defecto
      					

      Vamos a ver un literal numérico que incluya una coma, por ejemplo:

      					int x = 25,343; // No compilará por la coma
      					
    • Literales Booleanos

      Los literales boolean son la representación en el código fuente para los valores boolean. Un valor booleano solo puede ser definido por true o false. Aunque en C (Y en otros lenguajes) es común usar números para representar true o false, esto no funciona así en Java. De nuevo, debemos recordar que “Java no es C++”.

      					boolean t = true;	// Legal
      					boolean f = 0;		// Error de compilador
      					

      Hay que estar alerta en preguntas en las que se usen números donde valores booleanos se requieren. Podemos ver un test if que use un número, como lo siguiente:

      					int x = 1;
      					if (x){ }	// Error de compilador
      					
    • Literales de Caracteres

      Un literal de un caracter está representado por un único caracter entre comillas simples.

      					char a = 'a';
      					char b = 'b';
      					

      Podemos tambien escribir el valor Unicode para el caracter, usando la notación Unicode con el prefijo del valor u como en el siguiente ejemplo:

      					char letterN = 'u004E';	// La letra N
      					

      Recordemos, los caracteres son enteros de 16-bit sin signo. Esto significa que podemos asignar un número literal, asumiendo que podremos meterlo en un rango de 16-bit sin signo /65535 o menos). Por ejemplo, lo siguiente es legal:

      [sourecode language=”java”]
      char a = 0x892; // Literal hexadecimal
      char b = 982; // Literal entero
      char c = (char) 70000; // El cast es requerido; 70000 está fuera del rango de los char
      char d = (char) -99; // Ridiculo, pero legal
      [/sourcecode]

      Y lo siguiente son ejemplos no legales que producirán un error de compilador:

      					char e = -29; // Posible perdida de precisión; necesita un cast
      					char f = 70000; // Posible perdida de precision; necesita un cast
      					

      Podemos tambien usar un caracter de escape si queremos representar un caracter que no se escribe como un literal, incluyendo caracteres para una nueva linea, tabulación horizontal, espacio y comillas simples:

      					char c = '"';		// Doble comilla
      					char d = 'n';		// Nueva linea
      					
    • Valores literales para Strings

      Un valor literal para String es la representación en código fuente de un valor para un Objeto String. Por ejemplo, lo siguiente es un ejemplo de 2 manera de representar un literal de String:

      					String s = "Bill Joy";
      					System.out.println(""Bill" + " Joy");
      					

      Aunque los Strings no son tipos de datos primitivos, están incluidos aquí porque pueden ser representados como literales, en otras palabras, escritos directamente en el código. Los únicos tipos de dato que no son primitivos que tienen una representación literal son los arrays, que veremos mas adelante.

  • Operadores de Asignación

    Asignar un valor a una variable parece facil; simplemente asignamos lo que hay en la derecha del = a la variable de la izquierda.

    Sin embargo, probaremos asignaciones mas complicadas que envuelven expresiones mas complejas y cast. Veremos tanto asignaciones de tipos de dato primitivos como asignaciones de variables de referencia,. Pero antes de empezar, vamos a ver de nuevo las variables. ¿Qué es una variable? ¿Como se representan las variables y su valor?.

    Las variables son simplemente “contenedores” de bit, con un tipo designado. Podemos tener un contenedor para un int, un contenedor para un double, un contenedor para un Button, e incluso un contenedor para String[]. Dentro de estos contenedores hay un montón de bits que representan el valor. Para los primitivos, estos bits representan un valor numérico. Un byte con un valor de 6, por ejemplo, significa que el patrón de bit en la variable es 00000110, representando 8 bits.

    Así que el valor de una variable primitiva es limpio, pero ¿Qué pasa dentro del contenedor de un Objeto?. Si decimos:

    				Button b = new Button();
    				

    ¿Qué hay dentro del contenedor de Button b?¿Es el objeto Button? ¡NO!. Una variable referente a un objeto es solo eso, una variable de referencia. Una variable de referencia contiene bits que representan la “manera de obtener el objeto”.” No sabemos que formato tiene. La manera en la cual la referencia al objeto se almacene es específica en la máquina virtual (Es un puntero a algo). Todo lo que podemos decir por seguro es que el valor de la variable no es el objeto, sino un valor que representa un objeto específico en el Heap. O null. Si la variable de referencia no tiene un valor asignado, o ha sido explícitamente asignado a un valor null, la variable que guarda los bits representa null. Podemos leer:

    				Button b = null;
    				

    como “La variable b de Button no se está refiriendo a ningún objeto.”

    Por lo que ahora lo que sabemos es que la variable es solo una pequeña caja de bits, podemos serguir con el trabajo de cambiar los bits. Veremos primero la asignación de valores a primitivos, y finalizaremos con asugnaciones a variables de referencia.

    • Asignaciones de Primitivos

      El signo (=) es usado para asignar un valor a una variable, y es llamado operador de asignación. Actualmente hay 12 operadores de asignación, pero vamos a ver los 5 mas comunes.

      Podemos asignar una variable primitiva usando un literal o el resultado de una expresión. Vamos a ver lo siguiente:

      						int x = 7;		// Asignamos un literal
      						int y = x + 2;	// Asignamos una expresión y un literal
      						int z = x * y;	// Asignamos una expresión
      						

      El punto mas importante que debemos recordar es que un entero literal es siempre un int de manera implícita. Lo siguiente es legal,

      						byte b = 27;
      						

      Pero el compilador hace mas estrecho el valor literal a byte de manera automática. En otras palabras, el compilador pone un cast. El código anterior es idéntico al siguiente:

      						byte b = (byte) 27; // Cast explícito de un literal entero a byte
      						

      Parece como si el compilador nos diera un descanso, y nos dejara coger un atajo con las asignaciones de variables enteras mas pequeñas que un int.

      Sabemos que un entero literal es siempre un int, pero mas importante, el resultado de una expresión que contenga cualquier cosa que sea int o mas pequeña siempre será un int. En otras palabras, si sumamos 2 byte entre ellos obtendremos un int. Multiplicando un int y un short nos dará un int. Dividir un short por un byte nos dará…un int. Bien, ahoar estamos en la parte extraña. Probemos esto:

      						byte a = 3;			// No hay problema, 3 entra en un byte
      						byte b = 8;			// No hay problema, el 8 entra en un byte
      						byte c = b + c;		// No debería ser un problema, la suma de 2 bytes cabe en un byte
      						

      Sin embargo, la última linea no compilará. Obtendremos un error de perdida de precisión.

      Hemos intentado asignar la suma de 2 bytes a una variable byte, el resultado de esto (11) era lo suficientemente pequeño para entrar en un byte, pero al compilador no le importa. Sabe la regla sobre int o expresiones mas pequeñas siempre resultan en un int. Compilaría si hacemos un cast explícito:

      						byte c = (byte) (a + b);
      						
    • Casting de primitivos

      El Casting nos permite convertir valores primitivos de un tipo a otro. Hemos mencionado ya con anterioridad los casting en tipos de dato primitivos, pero ahora vamos a mirarlo de forma mas profunda.

      Los Cast pueden ser explícitos o implícitos. Un cast implícito significa que que no tenemos que escribri el código para el cast, sino que la conversión se produce de manera automática. Típicamente, un cast implícito ocurre cuando estamos haciendo una conversión amplia. En otras palabras, poniendo una cosa pequeña (como un byte) en un contenedor mas grande (como un int). Recordemos la “posible pérdida de precisión” que el compilador nos lanza cuando veíamos conversiones en la parte anterior. Esto pasaba cuando intentabamos poner una cosa grande (como un long) en un contenedor mas pequeño (como un short). El valor grande en un contenedor pequeño requiere una conversión mas estrecha y por tanto requeriría un cast explícito, donde le diremos al compilador que estamos seguros del daño y que aceptamos la responsabilidad por completo. Vamos a ver primero un cast implícito:

      						int a = 100;
      						long b = a; 	// Cast implícito, un valor int siempre entra dentro de un long
      						

      Un cast explícito es así:

      						float a = 100.001f;
      						int b = (int) a; // Cast explícito, el float podría perder información
      						

      Los valores enteros pueden ser asignados a una variable double sin tener que hacer un cast explícito, ya que un valor entero puede entrar en un double de 64-bit. La siguiente línea demuestra esto:

      						double d = 100L; // Cast implícito
      						

      En la sentencia anterior, un double es inicializado con un valor long. No se necesita cast en este caso porque un double puede guardar cualquier pieza de información que un long puede guardar. Sin embargo, si queremos asignar un valor double a un tipo entero, estamos haciendo una conversión mas estrecha y el compilador lo sabe:

      						class Casting{
      							public static void main (String[] args){
      								int x = 3957.229;	// Ilegal
      							}
      						}
      						

      Si intentamos compilar, tendremos un error que nos dirá que es una declaración incompatible y se necesita un cast Explícito para convertir un double en un int.

      En el código anterior, un valor punto flotante está siendo asignado a una variable entera. Ya que el integer no es capaz de guardar decimales, ocurre un error. Para hacer que funcione, haremos un cast al valor floating-point para meterlo en un int:

      						class Casting{
      							public static void main (String[] args){
      								int x = (int) 3957.229;	// Cast Legal
      							}
      						}
      						

      Cuando hacemos un cast a un número flaoting-point a un tipo integer, el valor pierde todos los dígitos de decimal.

      También podemos hacer cast a un tipo de número mayor, como un long, a un tipo de numero mas pequeño, como un byte. Miremos lo siguiente:

      						class Casting{
      							public static void main (String[] args){
      								long l = 56L;
      								byte b = (byte)l;
      								System.out.println("El byte es " + b);
      							}
      						}
      						

      El código anterior compilará y se ejecutará bien. ¿Pero que pasa si el valor long fuera mas grande que 127 (El número mas grande que puede almacenar un byte)?. Vamos a modificar el código:

      						class Casting{
      							public static void main (String[] args){
      								long l = 130L;
      								byte b = (byte)l;
      								System.out.println("El byte es " + b);
      							}
      						}
      						

      El código compila bien, pero si ejecutamos obtendremos esta salida:

      						El byte es -126
      						

      No obtenemos un error en tiempo de ejecución, incluso si el valor está siendo convertido es muy grande para el tipo de dato. Los bits en la izquierda por debajo del 8…se pierden. Si el bit mas a la izquierda (el bit de signo) en el byte (o cualquier integer primitivo) ahora parece ser un 1, el primitivo tendrá valor negativo.

    • Asignando Números Floating-Point

      Los números floating-points tienen una ligera diferencia en el comportamiento de la asignación con los tipos integer. primero, debemos saber que todo literal floating-point es implícitamente un double (64 bits), no un float. Por lo que el literal 32.3, por ejemplo, es considerado un double. Si intentamos asignar un double a un float, el compilador sabe que no hay suficiente espacio en un contenedor float de 32-bit para mantener la precisión de un double de 64-bit, y te lo hará saber. El siguiente código parece que está bien, pero no compilará:

      						float f = 32.3;
      						

      Podemos ver que 32.3 debería entrar bien en una variable cuyo tamaño es float, pero el compilador no lo permitirá. Para asignar un literal floating-point a una variable float, debemos hacer cast al valor o añadir una f al final del literal. Las siguientes asignaciones compilarán:

      						float f = (float) 32.3;
      						float g = 32.3f;
      						float h = 32.3F;
      						
    • Asignar un Literal que es demasiado largo para una Variable

      Tambien obtendremos un error de compilación si intentamos asignar un valor literal que el compilador reconoce como muy grande si lo intentamos meter en una variable.

      byte a = 128; // Un byte solo puede guardar hasta 127

      El código anterior nos dará un error d eposible perdida de precision. Podemos arreglarlo con un cast:

      ç

      						byte a = (byte) 128;
      						

      ¿Pero entonces cual sería el resultado? Cuando intentamos estrechar un primitivo, Java simplemente trunca los valores mas grandes que no caben. En otras palabras, pierde todos los bits de la izquierda de los bits que se están estrechando. Vamos a echar un vistazo lo que pasa en el código anterior. Aquí, 128 es el patrón de bits 10000000. Se necesitan los 8 bits para representar el 128. Pero como el literal 128 es un int, actualmente coge 32bits, con los 128 residiendo en los 8 bits que están a la derecha. Por lo que el literal 128 es actualmente:

      						000000000000000000000010000000
      						

      Tenemos 32 bits aquí (Si no me he equivocado claro). Para estrechar los 32 bits que representan 128, Java simplemente corta los de mas a la izquierda, 24 bits. Nos deja solo 10000000. Pero recordamos que los byte tienen signo, con el bit de más a la izquierda representando el signo. Por lo que terminamos con un numero negativo (el 1 que representaba el 128 ahora representa el bit de signo negativo). Debemos usar un cast explícito para asignar 128 a byte, y la asignación nos dejará con el valor -128. Un cast no es nada mas que una manera de decirle al compilador “Confía en mi, sy un profesional. Obtengo toda la completa responsabilidad para todo lo extraño que pase.” Lo siguiente compilará:

      						byte b = 3;
      						b += 7;		// No hay problema - se añade 7 a b
      						

      Es equivalente a esto:

      						byte b = 3;
      						b = (byte) (b + 7);	// No compilará sin el cast, ya que b + 7 es un int
      						

      El operador compuesto de asignación += nos permite agregar al valor de b, sin tener que poner un cast explícito. De hecho, +=, -=, *= y /= pondrán un cast implícito.

    • Asignando una Variable Primitiva a otra Variable Primitiva

      Cuando asignamos una variable primitiva a otra, el contenido del lado derecho de la variable son copiadas. Por ejemplo:

      						int a = 6;
      						int b = a;
      						

      Este código puede ser leido como, “Asigno el patrón de bit para el número 6 a la variable int a. Entonces copio el patrón en a, y pongo la copia en la variable b.”. Por lo que ahora ambas variables guardan un patrón de bit para el 6, pero ambas variables no tienen ninguna otra relación. Hemos usado la variable solo para copiar su contenido. A y B tienen idénticos contenidos, pero si cambiamos el contenido de a o b, la otra variable no se verá afectada. Vamos a ver un ejemplo:

      						class ValueTest{
      							public static void main(String[] args){
      								int a = 10;
      								System.out.println("a = " + a);
      								int b = a;
      								b = 30;
      								System.out.println("a = " + a + " despues de cambiar a b");
      							}
      						}
      						

      La salida de este programa:

      						a = 10
      						a = 10 despues de cambiar a b
      						

      Vemos que el valor sigue siendo 10. El punto clave de esto es recordar que incluso despues de asignar a a b, a y b no están refiriendo al mismo lugar de la memoria. Las variables a y b no comparten ningún valor singular, solo tienen copias idénticas.

    • Asignación en Variables de Referencia

      Podemos asignar un objeto creado nuevo a una variable del objeto de referencia como en el siguiente ejemplo:

      						Button b = new Button();
      						

      Las 3 líneas anteriores tiene 3 cosas importantes:

      • Crea una variable de referencia llamada b, del tipo Button.
      • Crea un nuevo objeto Button en el heap.
      • Asigna el nuevo objeto Button creado a la variable de referencia b.

      Podemos asignar tambien null a una variable de referencia de un objeto, lo cual simplemente significa que la variable no está refiriendo a ningún objeto:

      						Button c = null;
      						

      La línea anterior crea un espacio para la variable de referencia Button, pero no crea el actual objeto Button.

      Como hemos discutido en el último capítulo, podemos tambien usar una variable de referencia para referirse a un objeto que es subclase del tipo de variable de referencia declarado, como el siguiente ejemplo:

      						class Foo{
      							public void doFooStuff(){ }
      						}
      						
      						class Bar extends Foo{
      							public void doBarStuff(){ }
      						}
      						
      						class test{
      							public static void main (String[] args){
      								Foo reallyABar = new Bar();		// Legal porque bar es subclase de Foo
      								Bar reallyAFoo = new Foo();		// Ilegal porque Foo no es subclase de Bar
      							}
      						}
      						

      La regla es que podemos asignar una subclase del tipo declarado, pero no una superclase del tipo declarado. Recordemos, que un objeto bar tiene la garantía de hacer todo lo que un Foo pueda hacer, por lo que cualquier con una referencia a Foo puede invocar los métodos de Foo incluso si el objeto es actualmente Bar.

      En el código anterior, hemos visto que Foo tiene un método doFooStuff() que alguien con una referencia a Foo puede intentar invocar. Si el objeto referenciado por la variable Foo es realmente un Foo, no hay problema. Tampoco habría problema si el objeto es Bar, ya que Bar ha heredado el método doFooStuff. No podemos hacer que funcione al revés, sin embargo. Si alguien tiene una referencia a Bar, van a invocar el método doBarStuff(), pero si el objeto es un Foo, no sabrá como responder.

    • Ämbito de Variable

      Una vez que hemos declarado e inicializado una variable, una pregunta natural es “¿Cuanto tiempo estará esta variable por aquí?”. Esta es una pregunta que tiene que ver con el ámbito de las variables. Y no solo el ámbito es una cosa importante a entender en general, tambien juega un gran papel en la programación. Vamos a ver este archivo de clase:

      						class Layout{					// Clase
      		
      							static int s = 343;			// Variable static
      							
      							int x;						// variable de Instancia
      							
      							{							// Bloque de Inicialización
      								x = 7;
      								int x2 = 5;
      							}
      							
      							Layout(){					// Cosntructor
      								x += 8;
      								int x3 = 6;
      							}
      							
      							void doStuff(){				// Método
      								
      								int y = 0;				// Variable Local
      								
      								for (int z = 0; z < 4; z++){	// código del bloque 'for'
      									y += z + x;
      								}
      							}
      							
      						}
      						

      Como las variables en todos los programas de Java, las variables en este programa ( s, x, x2, x3, y y z) todas tienen un ámbito.

      • s es una variable static.
      • x es una variable de instancia.
      • y es una variable local (Algunas veces llamada variable de “Método local”).
      • z es una variable de bloque.
      • x2 es una variable de inicio del bloque, con sabor a variable local
      • x3 es una variable del constructor, con sabor a variable local.

      Para los propósitos de la discusión de los ámbitos de variables, podemos decir que hay 4 ámbitos básicos:

      • Las variables static tienen el ámbito mas grande; son creadas cuando la clase es cargada, y sobreviven tanto como la clase esté cargada en la Máquina Virtual de Java (JVM).
      • Las variables de instancia son las segundas que mas viven, son creadas cuando una nueva instancia es creada, y sobreviven hasta que la instancia sea removida.
      • Las variables locales son las siguientes, vievn tanto como su método permanezca en el Stack. Como podremos ver, sin embargo, las variables locales pueden estar vivas, y mantenerse “Fuera del ámbito”.
      • Las variables de bloque viven hasta que su bloque finalice.

      La razón mas común para los errores de ámbitos de variables es cuando intentamos acceder a una variable que no está en el ámbito. Vamos a ver 3 ejemplos comunes de este tipo de error:

      • Intentamos acceder a una variable de instancia en un contexto static:
        								class ScopeErrors{
        									int x = 5;
        									public static void main (String[] args){
        										x++;	// No compilará pues x es una variable de instancia
        									}
        								}
        								
      • Intentar acceder a uan variable local desde un método anidado.

        Cuando un método, opr llamarlo de alguna manera, go(), invoca otro método, por ejemplo, go2(), go2() no puede tener acceso a las variables locales de go(). Mientras go2() se esté ejecutando, las variables locales de go() siguen mantniendose vivas, pero están fuera del ámbito. Cuando go2() se completa, es removido del Stack, y go() vuelve a su ejecución. En este punto, todo lo declarado previamente en go() vuelve a estar en el ámbito. Por ejemplo:

        								class ScopeErrors{
        									public static void main (String[] args){
        										ScopeErrors s = new ScopeErrors();
        										s.go();
        									}
        									
        									void go(){
        										int y = 5;
        										go2();
        										y++;		// Una vez que go2 se complete, y vuelve a estár en el ámbito
        									}
        									
        									void go2(){
        										y++;		// No compilará, y es local en go()
        									}
        								}
        								
      • Intentando usar una variable de bloque despues de que el código del bloque se haya completado.

        Es muy común declarar y usar una variable dentro de un bloque de código, pero hay que tener cuidado de no intentar usar la variable una vez que el bloque haya sido completado:

        								void go3(){
        									for (int z = 0; z < 5; z++){
        										boolean test = false;
        										if (z == 3){
        											test = true;
        											break;
        										}
        									}
        									System.out.println(test);	// test ya no existe en esta parte de código
        								}
        								
      • En los últimos 2 ejemplos, el compilador nos mostrará que no existen estas variables.

  • Usando una Variable o Elemento de un Array que no ha sido Inicializado y no Asignado

    Java nos dá la opción de inicializar y declarar variables o dejarlas sin inicializar. Cuando intentamos hacer uso de una variable que no ha sido inicializada, podemos obtener diferentes comportamiento según el tipo de variable o array estemos tratando (primitivos u objetos). El comportamiento tambien depende del nivel (ámbito) en el cual estemos declarando nuestra variable. Una variable de instancia es declarada dentro de la clase pero fuera de cualquier método o constructor, donde una variable local es declarada dentro de un método (o en la lista de argumentos del método).

    Las variables locales son normalmente llamadas stack, temporales, automáticas, o variables de método, pero las reglas para estas variables son las mismas sin importar como las llamemos. Aunque podemos dejar una variable local sin inicializar, el compilador nos advierte que si intentamos usar la variable local antes de inicializarla con un valor, como veremos.

    • Variables de instancia Primitivas u Objetos

      Las variables d einstancia (tambien llamadas variables miembro) son variables definidas a nivel de clase. Esto significa que la declaración de la variable no está dentro de ningún método, constructor, o cualquier otro bloque de inicialización. Las variables de instancia son inicializadas con un valor por defecto cada vez que una neuva instancia es creada, aunque pueden no tener valores explícitos despues de que el super constructor del objeto haya sido completado.

      Tipo de Variable Valor por defecto
      Referencia a Objeto null (no se refiere a ningún objeto)
      byte, short, int, long 0
      float, double 0.0
      boolean false
      char ‘u0000’
    • Variables de Instancia Primitivas

      En el siguiente ejemplo, el entero year está definido como miembro de la clase porque está dentro de las llaves iniciales de la clase y no dentro de las llaves de algún método:

      						public class BirthDate{
      							int year;
      							public static void main (String[] args){
      								BirthDate bd = new BirthDate();
      								bd.showYear();
      							}
      							public void showYear(){
      								System.out.println("El año es " + year);
      							}
      						}
      						

      Cuando el programa es ejecutado, se le da a la variable year el valor 0, el valor por defecto para los variables de instancia primitivas que sean números.

    • Variables de Instancia de referencia a un Objeto

      Cuando comparamos variables primitivas sin inicializar con variables de instancia de referencia a Objetos, las referencias a objeto que no son inicializadas son una historia completamente diferente. Vamos a ver siguiente código:

      						public class Book{
      							private String title;
      							public String getTItle(){
      								return title;
      							}
      							public static void main (String[] args){
      								Book b = new Book();
      								System.out.println("El titulo es " + b.getTItle());
      							}
      						}
      						

      EL código compila bien. Cuando lo ejecutamos obtendremos:

      						El titulo es null
      						

      La variable title no ha sido explícitamente inicializada con una asignación a String, por lo que la variable de instancia es null. Recordemos que null no es lo mismo que una String vacía (“”). Un valor null significa que la variable de referencia no se está refiriendo a ningún objeto en el Heao. La siguiente modificacion al código de Book se encuentra con problemas:

      						public class Book{
      							private String title;
      							public String getTItle(){
      								return title;
      							}
      							public static void main (String[] args){
      								Book b = new Book();
      								String s = b.getTItle();	// Compila y ejecuta
      								String t = s.toLowerCase();	// Excepción en tiempo de ejecución
      							}
      						}
      						

      Cuando intentemos ejecutar la clase Book, la JVM pdorucirá una excepción llamada NullPointerException.

      Obtenemos este error porque la variable de referencia title no apunta a un objeto. Podemos comprobar si un objeto ha sido instanciado usando la palabra clave null, como el siguiente código revisado muestra:

      						public class Book{
      							private String title;
      							public String getTItle(){
      								return title;
      							}
      							public static void main (String[] args){
      								Book b = new Book();
      								String s = b.getTItle();	// Compila y ejecuta
      								if (s != null){
      									String t = s.toLowerCase();
      								}
      							}
      						}
      						

      El código anterior comprueba que el objeto referenciado por la variable s no es null antes de intentar usarla. Podemos ver por ejemplo, que la declaración de la variable de instancia no hay inicialización explícita, reconociendo entonces que la variable title le será dada el valor por defecto de null, y entondes nos damos cuenta de que la variable s tambien tendrá el valor de null. Recordemos, el valor de s es uan copia del valor de title (como ha retornado en getTitle()), por lo que si title es referencia a null, s lo será tambien.

    • Variable de Instancia Array

      Vamos a ver la regla para los valores por defectos de los elementos de un array.

      Un array es un objeto, así, una variable de instancia array que es declarada pero no explícitamente inicializada tendrá el valor de null, como cualquier otra variable de instancia que se refiera a un objeto. Pero…si el array es inicializado, ¿Qué pasa con los elementos que contiene el array? A todos los elementos del array se les es dado su valor por defecto.

      Los elementos de un array, siempre, siempre, siempre se les dá su valor por defecto, sin importar donde sea declarado o instanciado.

      Si inicializamos un array, los elementos que refieren a un objeto serán null si no han sido inicializados individualmente con valores. En cambio, si el array contiene tipos de dato primitivos, se les dará su valor por defecto. Por ejemplo, en el siguiente código, el array year contendrá 100 enteros que serán iguales a 0:

      						public class BirthDays{
      							static int[] year = new int[100];
      							public static void main (String[] args){
      								for (int i = 0; i < 100; i++){
      									System.out.println("year[" + i + "] = " + year[i]);
      								}
      							}
      						}
      						

      Cuando ejecutamos el código anterior, la salida indica que hay 100 enteros en el array que son igual que 0.

  • Objetos y Primitivos locales (Stack, Automatic)

    Las variables locales don definidas dentro de un método, y esto incluye los parámetros de los métodos.

    • Primitivos locales

      En el siguiente simulador de viaje en el tiempo, el entero year es definido como una variable automática porque está dentro de las llaves del método.

      						public class TimeTravel{
      							public static void main (String[] args){
      								int year = 2050;
      								System.out.println("El año es " + year);
      							}
      						}
      						

      Las variables locales, incluyendo las primitivas, siempre, siembre deben ser inicializadas antes de intentar usarlas (Aunque no necesariamente en la misma linea de código). Java no da un valor por defecto a las variables loacles, debemos inicializarlas explícitamente con un valor, como en el ejemplo anterior. Si intentamos usar una variable local sin inicializar en nuestro código, obtendremos un error de compilación.

      						public class TimeTravel{
      							public static void main (String[] args){
      								int year;	// Variable local declarada pero no inicializada
      								System.out.println("El año es " + year); // Error de compilación
      							}
      						}
      						

      Para corregir nuestro código, debemos darle un valor entero a year. En este ejemplo actualizado, lo declaramos en una linea separada, la cual es perfectamente válida:

      						public class TimeTravel{
      							public static void main (String[] args){
      								int year;	// Variable local declarada pero no inicializada
      								int day;	// Variable local declarada pero no inicializada
      								System.out.println("Entramos en el portal.");
      								year = 2050;
      								System.out.println("El año es " + year); 
      							}
      						}
      						

      Vemos en el anterior eemplo que hemos declarado un integer llamado day que nunca es inicializado, aún así el código compila y se ejecuta bien. Legalmente, podemos declarar una variable sin inicializarla sin usarla, pero afrentemoslo, si la hemos declarado, es porque seguramente tenemos alguna razón.

    • Referencias de Objeto locales

      Las referencias a objeto, tambien, se comportan diferente cuando son decalradas dentro de un método en diferencia a las variables d einstancia. Con las referencias a objeto como variables de instancia, podemos no inicializarlos, ya que el código comprobará que la referencia no será null antes de usarla. Recordemos, para el compilador, null es un valor. No podemos usar el operador (.) en una referencia a null, porque no hay objeto al otro lado de el, pero una referencia a null no es lo mismo que una referencia sin inicializar. Las referencias declaradas localmente no pueden escaparse comprobando que es null antes de su uso, a no ser que explícitamente inicialicemos la variable local a null. El compilador se quejará en el siguiente código:

      						public class TimeTravel{
      							public static void main (String[] args){
      								Date date;
      								if(date == null)
      									System.out.println("date is null");
      							}
      						}
      						

      Si intentamos compilar este código resulta en un error que nos comunica que la variable date puede no haber sido inicializada.

      A las referencias a variables de instancia siempre se les dá el valor por defecto null, a no ser que las inicialicemos explícitamente a algo. Pero las referencias locales no se les dá un valor por defecto, en otras palabras, no son null. Si no inicializamos una variable de referencia local, entonces su valor por defecto es…ninguno. La siguiente variable local compilará apropiadamente:

      						Date date = null;
      						
    • Arrays Locales

      Al igual que otra referencia a objeto, las referencias a arrays declaradas dentro de un método deben ser asignados a un valor antes de ser usadas. Esto solo quiere decir que debemos declarar y construir el array. No tenemos que, sin embargo, necesidad de inicializar explícitamente los elementos de un array. Ya lo hemos dicho antes, pero es importante repetirlo: los elementos de un array tienene l valor por defecto (0, false, null, ‘u0000, etc) sin importar donde sea declarado el array, ya sea como instancia o como variable local. El objeto array por sí mismo, sin embargo, no será inicializado si ha sido declarado localmente. En otras palabras, debemos explícitamente inicializar una referencia a un array si ha sido declarado y usado dentro de un método, pero por el momento en el que construyamos el objeto array, todos sus elementos son asignados sus valores por defecto.

    • Asignando una Variable de Referencia a Otra

      Con variables primitivas, una asignación de una variable a otra significa que el contenido de una variable es copiada dentro de otra. Las variables de referencia aun objeto funcionanan de la misma manera. El contenido de una variable de referencia es un patrón de bits, por lo que si asignamos la variable de referenci aa a una variable de referencia b, el patrón de bits a es copiado y la nueva copia es colocada dentro de b. Si asignamos a una instancia existente de un objeto una nueva variable de referencia, entonces 2 variables de referencia guardará el mismo patrón de bit. Veamos el siguiente código:

      						class ReferenceTest{
      							public static void main (String[] args){
      								Dimension a = new Dimension (5, 10);
      								System.out.println("a.height = " + a.height);
      								Dimension b = a;
      								b.height = 30;
      								System.out.println("a.height = " + a.height + " despues de cambiar a b");
      							}
      						}
      						

      En el código anterior, un objeto Dimension es declarado e inicializado con una anchura de 5 y una altura de 10. Después, Se declara el objeto Dimension b, y es asignado el valor de a. En este punto, ambas variables (a y b) mantienen valores idénticos, porque el contenido de a fué copiado en b. Aún sigue existiendo solo un objeto Dimension. Finalmente, la propiedad altura o height es cambiado usando la referencia de b. Ahora pensemos por un minuto si esto va a cambiar la propiedad height de a tambien. Veamos lo que obtenemos por pantalla:

      						a.height = 10
      						a.height = 30 despues de cambiar a b
      						

      Con esta salida pro pantalla, podemos concluir que ambas variables se refieren a la misma instancia del objeto Dimension. Cuando ahcemos cambios en b, la propiedad height cambien es cambiada en a.

      Una excepcion en la manera de las referencias a objetos son asignadas es String. En java, los objetos String tienen un trato especial. Los objetos String son inmutables, no podemos cambiar el valor de un objeto String. Pero es seguro que se ve como si se puede. Examinemos el siguiente código:

      						class StringTest{
      							public static void main (String[] args){
      								String x = "Java";	//Asigna un valor a x
      								String y = x;		// Ahora y y x se refieren al mismo objeto Object
      								
      								System.out.println("y string = " + y); 
      								x = x + " Bean";		// Ahora modificamos el objeto usando la referencia a x
      								System.out.println("y string = " + y);
      							}
      						}
      						

      Podríamos pensar que la String y contendrá los caracteres Java bean despues de que al variable x es cambiada, porque los String son objetos. Vamos a ver cual sería la salida:

      						y string = Java
      						y string = Java
      						

      Como podemos ver, incluso si y es uan variable de referencia al mismo objeto al cual x se refiere, cuando cambiamos x, no cambia y. Para otro tipo de objeto, donde 2 referencias se refieren al mismi objeto, si alguna de esas referencias es usada para modificar el objeto, ambas referencias verán el cambio porque aún hay solo un mismo objeto.

      Siempre que hagamos un cambio o todo a un String, la VM actualizará la variable de referencia para que se refiera a un objeto diferente.

      El objeto diferente parece ser un nuevo objeto, o puede que no, pero definitivamente será un objeto diferente.

      Tenemso que entender que ocurre cuando usamos una variable de referencia String para modificar un String:

      • Un nuevo String es creado, dejando el objeto String original sin tocar.
      • La referencia usada para modificar el String es entonces asignada a la marca del nuevo objeto String.

      Por lo tanto cuando decimos:

      						String s = "Fred";
      						String t = s;
      						
      						t.toUpperCase();
      						

      No hemos cambiado el objeto String original creado en la linea 1. Cuando la linea 2 se completa, ambos t y s son referencias del mismo objeto String. Pero cuando la linea 3 se ejecuta, antes que modificar la referencia al objeto t, un nuevo objeto String es creado. Y entonces abandonado.

Hasta aquí el tema de Asignaciones, donde se han cubierto temas respecto a los tipos de dato primitivos, variables de referencia, ámbito de variables, inicialización de variables locales, ya sean primitivas o de referencia a un objeto.


Sin mas, cualquier aporte o corrección es bienvenido.

Saludos!!!