Aprende la escancia del manejo de Objetos en Java y diferéncialos de los tipos Nativos. También te explicaremos el uso de Variables y Constantes y la diferencia que existe entre el paso por valor y el paso por referencia.

Si quieres aprender sobre estructura de datos comienza aquí.

1. Referencias, Wrappers y Tipos Nativos Código Java

Cuando aprendes a programar un punto inicial son los Tipos Nativos Objetos Variables y Constantes en Java y es clave poder diferenciarlos, aquí se mostrará todo el detalle necesario para comprender esas diferencias.

1.1. Tipos de Datos Definidos por el Usuario

Aunque Java puede tener muchas clases, que en general se conocen en el mundo de la programación como tipos de datos definidos por el usuario, en realidad Java solo tiene dos tipos de datos: tipos nativos y referencias.

Los tipos nativos son aquellos que almacenan el valor en la misma posición de memoria a la cual se refieren, mientras que las referencias almacenan la dirección de memoria donde reside el objeto.

1.2. Tipos nativos en Java

Para facilitar la comprensión de este concepto vamos a comenzar con los tipos de datos nativos los cuales se pueden ver en la siguiente tabla junto con la cantidad de memoria que ocupan y el rango de valores que admiten, note que un tipo nativo no permiten que se les asigne el valor null.

Tipo de datoTamaño
boolean8 bit (false, true)
byte8 bit (-128 a 127)
short16 bit (-2^15 a 2^15-1)
int32 bit (-2^31 a 2^31-1)
long64 bit (-2^63 a 2^63-1)
float32 bit (±3.4×10^-38 a ± 3.4×10^38)
double64 bit (±1.8×10^-308 a ± 1.8×10^308)
char16 bit (Valores Unicode)
Tabla 2. Tipos de Dato Nativo

En el caso de las referencias, el tamaño que ocupa depende de la arquitectura de la JVM si es de 32 bit o de 64 bit.

Los tipos nativos se caracterizan a su vez ser palabras reservadas, es importante comprender que estos tipos nativos no representan clases u objetos y que Java a incluido versiones objetuales de los tipos nativos, así por ejemplo existe Double e Integer.

2. Constantes en Java

Tipos Nativos Objetos Variables y Constantes en Java tienen una importancia crucial en la codificación en Java en esta sección aprenderás sobre el manejo de las constantes.

2.1. Constantes numéricas

Las constantes numéricas que aparecen en el código son tratadas con int si estas no tienen un punto, en caso que lo tengan son tratadas como constantes numéricas de tipo Double, así mismo se puede forzar a interpretar una constante entera como long si la constante tiene el sufijo L o l (se aconseja utilizar el primero ya que el segundo se asemeja mucho a un carácter 1, en el caso de las constantes numéricas de punto flotante se puede forzar a interpretarlas como float si se utiliza como sufijo la letra F o f y finalmente el sufijo para double es d o D, note que todos estos sufijos hacen referencia a tipos nativos y no a los Wrapper correspondientes. Para el caso del tipo de dato char es importante reconocer que los caracteres individuales se encierran entre comillas dobles mientas que las cadenas de caracteres se encierran en comillas dobles.

2.2. Literales

Las constantes numéricas, es decir, valores fijos numéricos que aparecen el código se denominan literales (también aplica para valores de texto) y Java automáticamente los trata como int si los mismos no tienen punto decimal, en caso que tengan punto decimal se tratan como double, así mismo los literales pueden usar el carácter _ como separador, el prefijo 0 para indicar base octal, el predijo 0x o 0X para base hexadecimal y el prefijo 0b o 0B para base binaria. Algunos ejemplos de literales se muestran a continuación.

long max = 1234567890123; // No permitido, el valor literal supera el tamaño del int
double d1 = _1000.00; // El separador no es permitido al principio
double d2 = 1000.00_; // El separador no es permitido al final
double d3 = 1000_.00; //El separador no es permitido al lado del punto decimal
double d4 = 1_00_0.0_0; // Válido

3. Creación de Objetos en Java

Tipos Nativos Objetos Variables y Constantes en Java tiene una similitud, esta sección explica los objetos y es algo que normalmente los programadores incluso los experimentados no diferencian con claridad, a continuación aprenderás uno de los grandes secretos: las referencias.

En la creación de un objeto es necesario comprender que realmente se crean dos espacios de memoria diferentes, uno para la referencia y otro para el objeto.

Cuando un objeto se queda sin referencias es habilitado para que el recolector de basura libere la memoria asociada a dicho objeto automáticamente, y lo anterior tiene sentido porque si un objeto se queda sin referencias no es posible acceder desde ningún punto del código, el gráfico a continuación ilustra la relación entre referencia y objeto para el siguiente código .

Person p1 = new Person()
Relación entre objetos y referencias
Figura 1. Relación objetos y referencias

Algo importante para notar es que una referencia se crea cuando se declara en cambio el objeto se crea cuando se usa el operador new, por lo tanto se debe comprender que la referencia almacena la dirección de memoria de donde se encuentra el objeto, realmente en Java todas las variables son referencias, es decir, almacenan direcciones de memoria, excepto para los tipos nativos.

4. Paso por valor y paso por referencia

Lo anterior nos lleva a explorar lo que sucede cuando pasamos parámetros a métodos ya que existen dos formas: paso por valor que aplica para atributos nativos y paso por referencia, que aplica para los objetos, a fin de explorar este concepto consideremos el siguiente código.

public static void main(String... args) {
    int a1 = 1;
    m1(a1);
    System.out.println("a: " + a1);
    Integer i = new Integer(5);
    m2(i);
    System.out.println("Integer i: " + i);
    String s = "hi";
    m3(s);
    System.out.println("s: " + s);
    StringBuffer sb = new StringBuffer("abc");
    m4(sb);
    System.out.println("sb: " + sb);
}
public static void m1(int a) {
    a = a + 1;
}
public static void m2(Integer i) {
    i = i + 1;
}
public static void m3(String s) {
    s = s + "1";
}
public static void m4(StringBuffer s) {
    s.append("d");
}

La pregunta es ¿qué se imprime? Lo ideal es revisar con calma antes de contestar. Ahora vamos con cada impresión a revisar el fondo.

4.1. Paso por valor

  • Primera impresión: la primera impresión trata la pregunta si el método m1, es decir, si este método afecta la variable externa, la respuesta es no, debido a que la variable a se pasa por valor al ser de un tipo nativo, termina sucediendo que la variable a del main contiene un valor independiente de la variable a del método m1, por tanto la modificación de la variable a no afecta la variable externa, de hecho el parámetro del método m1 podría incluso tener otro nombre.
  • Segunda impresión: este segundo caso tiene un poco más de truco. Si nos basamos en la teoría aprendida pensaríamos que si se modifica puesto que la variable no hace referencia a un tipo nativo sino a un wrapper, sin embargo en el momento dela suma, la variable i debe ser convertida de Integer a int para poder realizar la suma. Esta característica de Java se llama autoboxing y le permite al lenguaje escribir expresiones con tipos nativos o wrappers de forma equivalente para el programador.
  • Tercera impresión: esta impresión también tiene un pequeño truco, puesto que al igual que el caso anterior pensaríamos que se ve afectada la variable externa, sin embargo, esto no sucede ya que String es una clase con comportamientos muy diferentes al resto de clases de Java. Los String en Java son inmutables y se almacenan en una parte especial de la memoria, es decir, una vez definido un String este no cambiará jamás, por lo tanto el append que se observa en el método m3 no modifica el String y por esta razón el valor externo no cambia.

4.2. Paso por referencia

  • Cuarta impresión: esta impresión asociada al método m4 es el único que en realidad es un paso por referencia de manera que lo que hagamos dentro de este método afectará la variable externa ya que ambas variables sb y s referencia el mismo espacio de memoria, esto también se puede notar ya que solamente existe un new, es decir un objeto tipo StringBuffer en este código.

5. Manejo de Strings

Ahora que conoces los Tipos Nativos Objetos Variables y Constantes en Java es hora de entender una de las clases más importantes en Java y de hecho en cualquier lenguaje, los String que permiten manejar cadenas de texto.

Como particularidad de los String cabe anotar que los objetos de esta clase se pueden representar directamente en el código, por ejemplo "hola".length() note que en este caso no fue necesario hacer un nuevo objeto tipo String para invocar el método length().

5.1. Referencias tipo String

A fin de tener presente los puntos más importantes acerca del manejo de referencias, el programador de Java debe recordar que las referencias de tipos Wrappers y de tipo String tienen un manejo diferente al resto de referencias, las primeras por que les aplica el concepto de autoboxing y las segundas por que las cadenas de texto son almacenadas de forma inmutable, es decir, una vez definido un String este no puede cambiar durante la ejecución del programa, para ejemplificar este último concepto de la inmutabilidad de los String considere el siguiente código.

public static void main(String... args) {
    String str1 = "hola";
    String str2 = "hola";
    String str3 = "hola".trim();
    String str4 = "hola   ".trim();
    System.out.println(str1 == str2);
    System.out.println(str1 == str3);
    System.out.println(str1 == str4);
}

Uno pensaría que la primera impresión es true debido a que son referencias diferentes, pero dado que Java almacena los String de forma especial y en una zona especial de la memoria, la asignación de str1 y str2 terminan apuntando a la misma dirección de memoria.

La segunda impresión sigue siendo verdadero debido a que la función trim() que recorta los espacios en blanco al principio y final de una cadena, no realiza en realidad ninguna operación, debido a que no hay nada que recortar, pero algo curioso sucede con la tercera impresión y en este caso arroja false indicando que la referencia str4 termina apuntando a un lugar diferente de memoria, y por lo tanto terminan existiendo en memoria dos objetos String con la misma información hola, note que esto sucede porque trim() no puede modificar el objeto original y cuando lo modifica deja el resultado en una posición de memoria nueva y para ello no determina si ese objeto String ya existia.

5.2. Errores comunes con Strings

Note que debido a estas características de Java, un programa puede proliferar rápidamente la cantidad de String que tiene creados. Una nota final de los String y esto solamente le sucede a esta clase, y es que no requiere un new, no en el código anterior que se realizaron asignaciones sin necesidad de crear un objeto nuevo y esto es porque Java es capaz de tratar como objetos a las cadenas de texto directamente sobre el código, por lo mismo códigos como el siguiente funcionan correctamente: "hola".trim(). Respecto a la primera impresión del código anterior, que sucedería si en lugar de ser String fueran Integer? y para ello considere el siguiente código.

public static void main(String... args) {
    Integer i1 = 10;
    Integer i2 = 10;
    Integer i3 = new Integer(10);
    Integer i4 = new Integer(10);
    System.out.println(i1 == i2);
    System.out.println(i3 == i4);
}

Este código realmente puede ser muy desconcertante, ¿qué imprimirá?

La respuesta es que la primera impresión es true y la segunda es false, la respuesta es la forma en que se crearon los objetos, el primero se hizo usando la característica de autoboxing (recuerde que esta es la habilidad que tiene Java automáticamente de pasar del tipo nativo al Wrapper según se necesite), por lo tanto en la primera ocasión los objetos apuntan a la misma dirección, este caso funciona de forma similar a los String mientras que en la segunda forma los Integer son creados como objetos primero (lo cual hace que ambas referencias i3 e i4 apunten a direcciones diferentes) y luego se inicializan los valores de estas referencias respectivamente.

6. Destrucción de objetos

Los objetos son puestos en un lugar de memoria llamado Memory Heap. Java tiene un hilo de baja prioridad denominado Garbage Collector que elimina objetos que ya no son referenciados.

Se puede invocar manualmente usando System.gc() aunque nada garantiza que se libere la memoria en un momento dado del tiempo, llamar a este método es meramente una sugerencia para Java. Similar a como los constructores crean objetos los destructores se encargan de eliminarlos, el siguiente método se invoca cuando un objeto es elegido por el recolector y puede personalizar la liberación de recursos. En realidad no es muy usado, sin embargo recuerde que este método nunca se llama dos veces: protected void finalize();

7. Artículos de Interés