Promoción y casting de datos
Cuando se asigna una variable o una expresión a otra variable, puede ser que los tipos
de datos del resultado obtenido y de la variable que ha de contenerlo no coincidan. Si el
compilador detecta esta situación generará un error y en caso de que no lo haga los resultados
en tiempo de ejecución serán incorrectos.
Para evitar esto, las variables y las expresiones podrán ser promovidas a un tamaño
mayor; la promoción podrá ser manual (realizada por el programador) o automática
(realizada por el compilador. La otra opción es ajustar el tamaño de las expresiones para
coincidir con la variable; a este método de ajuste se le conoce formalmente como type
casting.
Promoción:
Supongamos el siguiente caso:
int num1 = 53; int num2 = 47; byte num3;
num3 = num1 + num2;
Aparentemente esto debería de funcionar, dado que un dato de tipo byte es capaz de
contener el valor del resultado; sin embargo el compilador arrojará el siguiente error:
possible loss of precision
found: int
required: byte
Esto sucede dado que un dato byte es menor que un dato int; para esta situación la
promoción manual es la solución más sencilla, dado que sólo habría que redefinir el tipo de
dato de num3:
int num 3;
Sin embargo en algunos casos (si la variable es de un tipo mayor al de la expresión que
se le pretende asignar) el compilador realiza la promoción de la expresión de manera
automática; esto sucede dado que se asume que no existirá pérdida de datos. Suponga por
ejemplo la siguiente asignación directa:
long bigNumber = 6;
Como recordará, las literales numéricas enteras se consideran por omisión de tipo int.
Siendo este el caso, se realizará una promoción automática a tipo long al momento de la
compilación, como si se le hubiese adicionado el posfijo L al número seis. Lo mismo sucede
cuando pretendemos asignar un dato de tipo entero (char, byte, int o long) a uno de
tipo real (float o double). Dado que no hay posiciones decimales en los enteros, no hay
datos que perder.
NOTA IMPORTANTE
Cuando se resuelve una ecuación y antes de asignarla a una variable, se
almacena el resultado en memoria de forma temporal. Este espacio temporal
creado será siempre igual al tamaño que posea el operando más grande. Si por
ejemplo se multiplican dos datos de tipo int, el espacio temporal se creará de
32 bits.
Por ello, tenga cuidado al multiplicar operandos que puedan sobrepasar el
ámbito temporal puesto que de lo contrario habrá pérdida de información. Por
ejemplo:
int num1 = 55555;
int num2 = 66666;
long num3;
num3 = num1 * num2;
Lo anterior es incorrecto aunque num3 sea long, dado que el resultado de
la operación intermedia (3,703,629,630) rebasa por mucho el ámbito de un tipo
int. Para evitar este problema, establezca al menos un término a tipo long
asegurando así el mayor espacio temporal posible. Lo mismo si los valores fuesen
literales; por ejemplo:
long num3;
num3 = 55555 * 66666L;
Casting:
Ahora bien, cuando no es deseable cambiar el tipo de dato de la variable que habrá de
contener nuestra asignación, será necesario ajustar el tamaño de esta última por medio de un
casting. Un casting literalmente corta el resultado a un tipo de dato menor y requiere de la
siguiente sintaxis:
identificador = (tipo_de_destino) expresión
En donde:
identificador es el nombre de la variable que almacenará el resultado.
tipo_de_destino es el tipo al cual se realizará la reducción.
expresión será el valor literal, variable, operación matemática o expresión
java que se pretende ajustar. Si la expresión es compleja deberá encerrarse entre
paréntesis.
Por ejemplo, supongamos el siguiente caso:
int num1 = 53;
int num2 = 47;
byte num3;
num3 = (byte) (num1 + num2);
La expresión num1 + num2 será ajustada a tipo byte y posteriormente asignada a
la variable num3, sin causar error al momento de la compilación.
NOTA: Emplee el casting con cuidado cuando se trate de datos reales que tengan una
parte fraccional, puesto que esta será truncada por completo (a menos que eso sea lo
que usted pretende). De igual manera tome en cuenta que los datos de tipo entero serán
recortados si se les ajusta a un tipo menor dentro de su categoría.
Cuidados en el manejo de datos enteros y reales
El compilador de Java realiza ciertas suposiciones al evaluar datos primitivos en
expresiones matemáticas y de asignación; se recomienda que dichas suposiciones sean
tomadas en cuenta al realizar un casting:
Cuando los datos primitivos son enteros se convierten a tipo int (a menos que
específicamente se diga que son de tipo long) antes de realizarse cualquier operación
matemática; suponga por ejemplo:
short a, b, c;
a = 1; b = 2;
c = a + b;
Lo anterior causa un error dado que aunque a y b se declararon como short, los
valores 1 y 2 son convertidos a int antes de sumarse. Para librar el problema podríamos:
Declarar a c como un int:
int c;
Aplicar un cast a a + b antes de asignarlo a c:
c = (short) (a+b);
Por otra parte, cuando los datos primitivos son de tipo real se convierten a tipo double
(a menos que específicamente se diga que son de tipo float). La siguiente línea por ejemplo
causaría un error de compilación, dado que por omisión 27.9 es considerado un double:
float float1 = 27.9;
Aquí tendríamos dos opciones:
Agregar el posfijo F a la literal 27.9:
float float1 = 27.9F;
Aplicar un cast a tipo float a la literal 27.9:
float float1 = (float) 27.9;
Ejercicio 7
Diseñe una clase llamada Primo.java que contenga un método capaz de
definir si un número de tipo long es o no primo. El número a definir deberá
almacenarse en el atributo público number, al cual usted asignará un valor
desde la clase principal PrimoTest.java.
Diseñe la clase principal de tal forma que pruebe la clase Primo al menos con
tres valores diferentes.
Almacenamiento de variables, referencias y objetos en memoria
Cuando en un programa se emplean valores literales o se crean variables y constantes
para asignarles un valor, estos son almacenados en la memoria de la computadora. La
siguiente figura ilustra que las variables locales y los atributos se almacenan en partes
separadas:
Variables y constantes Objetos con sus
declaradas dentro de atributos
un método
Memoria Memoria
Stack Heap
Los objetos con sus atributos son almacenados en la memoria Heap, la cual es
administrada de manera dinámica por el programa. Por otra parte, las referencias a los objetos
y las variables de sus métodos son almacenadas en la memoria Stack, dado que sólo son
empleadas por un breve periodo de tiempo. Mientras que las variables de tipo primitivo
almacenan valores, las referencias a objetos almacenan la ubicación en memoria (dirección)
de los mismos.
Dichas ubicaciones, generalmente escritas en notación hexadecimal, son únicas para
cada objeto instanciado y se crean dinámicamente en tiempo de ejecución. Considere el
siguiente código:
public static void main(String args[]){
int counter = 10;
Shirt myShirt = new Shirt();
}
Esto quedaría ordenado en memoria de la siguiente manera:
0 shirtID
0.0 price
U colorCode
counter 10
0x034009
myShirt 0x034009
Memoria Stack Memoria Heap
Por lo anterior, tome en cuenta que cuando usted asigna el valor de una referencia a
objeto a otra no estará pasando los valores de los atributos de un objeto a otro. Lo que en
realidad sucederá es que ambas referencias contendrán ahora la misma dirección (esto es,
apuntarán al mismo objeto). Por ejemplo:
Shirt myShirt = new Shirt();
Shirt yourShirt = new Shirt();
myShirt = yourShirt;
Esta asignación dejaría a las referencias como se ve en el siguiente esquema:
0 shirtID
0.0 price
U colorCode
counter 10
0x034009
0x034009 0 shirtID
myShirt
0x99F311
0.0 price
yourShirt 0x99F311 U colorCode
0x99F311
Memoria Stack Memoria Heap
Dado que el objeto myShirt original ya no es referenciado por ningún identificador,
no tiene razón de existir en memoria. Para eliminar o destruir todos aquellos objetos en
memoria Heap que ya no son referenciados, Java implementa una herramienta de limpieza
automática conocida como garbage collector (recolector de basura) que periódicamente se
encarga de realizar dicha tarea.
Uso de la clase String
La clase String es una de las muchas clases incluidas en el API de Java y provee la
capacidad de almacenar una serie de caracteres bajo un identificador de manera sencilla.
Aunque en ejercicios pasados hemos tratado a las cadenas como un tipo de dato primitivo,
son en realidad un objeto; lo peculiar del caso es que no es necesario emplear la palabra clave
new para construirlas.
La sintaxis general para declarar e inicializar una cadena es la siguiente:
String identificador = “cadena_literal”;
Lo cual sería lo mismo a escribir:
String identificador = new String(“cadena_literal”);
La primera forma es por supuesto más común, dada su sencillez y similitud con las
declaraciones de variables simples. Como cualquier objeto, las cadenas hacen uso de espacio
en la memoria stack para almacenar la referencia y de la memoria heap para almacenar su
contenido; sin embargo, cuando son creadas sin la palabra clave new, las cadenas se
almacenan en una parte especial del heap conocida como el pool de literales (literal pool).
Esta área reservada tiene la característica de no permitir la duplicación de cadenas idénticas.
Suponga que se redacta el siguiente código:
String myString = “Luis Muslera”;
Esto generaría lo siguiente en la memoria:
myString
0xDEF 0x0011F [C Value
0x2244C Comparator
0xDEF
Luis Muslera
0x0011F
Memoria Stack Memoria Heap
La referencia en el stack contiene la dirección del objeto String en el heap, quien a
su vez contiene un valor conocido como [C Value que guarda la dirección en el pool de
literales de la cadena almacenada.
Por último, tome en cuenta que en Java las cadenas son objetos inmutables (esto es, no
se pueden modificar). Una vez creado un objeto String no se pueden alterar los caracteres
que lo conforman. Cuando usted iguala un identificador a una cadena diferente Java crea un
objeto nuevo en el Heap y modifica la referencia empleada, dejando en manos del recolector
de basura la destrucción de la cadena original.
Paso de argumentos de tipo objeto
Hasta el momento, hemos usado valores literales y nombres de variables para pasar
argumentos a nuestros métodos. A esta forma de paso de argumentos se le conoce como
llamada por valor, dado que lo que en realidad estamos haciendo es almacenar el valor literal
o copiar el valor de nuestra variable en el argumento. Por tal motivo, los cambios hechos al
parámetro dentro del método no afectan en lo absoluto el valor original de nuestra variable.
Considere por ejemplo el siguiente código:
public class Test{
void method(int a, int b){
a *= 2;
b *=2;
}
}
public class TestTest{
public static void main(String args[]){
Test myTest = new Test();
int a = 10, b = 10;
System.out.println(“Antes: “ + a + “ “ + b);
myTest.method(a , b);
System.out.println(“Despues: “ + a + “ “ + b);
}
}
La salida del programa sería:
Antes: 10 10
Despues: 10 10
Lo anterior comprueba entonces que los cambios que hagamos a las variables a y b
dentro del método method del objeto myTest no afectan en lo absoluto a las variables a y
b del método main, puesto que lo único que estamos haciendo es copiar el valor de las
variables en los argumentos del método; dichos parámetros se crean al invocar al método y
se destruyen al salir de él.
Con los objetos, sin embargo, la situación es completamente diferente, ya que el
nombre de un objeto es en realidad una referencia al mismo; por ello, cuando se pasa un
objeto a un método se dice que se está realizando un llamado por referencia. Como es de
suponerse, los cambios que realicemos en el objeto dentro del método, afectarán al objeto
original, puesto que ahora el argumento está haciendo referencia al mismo objeto.
Observe el siguiente código:
public class Test2{
int a, b;
Test2(int i, int j){
a = i;
b = j;
}
void method(Test2 ref){
ref.a *= 2;
ref.b *= 2;
}
}
public class Test2Test{
public static void main(String args[]){
Test2 myTest2 = new Test2(10,10);
System.out.println(“Antes: “ + myTest2.a + “ “ +
myTest2.b);
myTest2.method(myTest2);
System.out.println(“Despues: “ + myTest2.a + “ “ +
myTest2.b);
}
}
La salida del programa sería:
Antes: 10 10
Despues: 20 20
En este caso, las acciones efectuadas a los miembros del objeto referenciado por ref
dentro del método method de la clase Test2 afectan a los miembros del objeto
referenciado por myTest2 dado que en realidad se trata de dos referencias diferentes que
apuntan al mismo objeto y por lo tanto a los mismos miembros.
Retorno de valores de tipo objeto
Un método puede devolver cualquier tipo de dato, objetos inclusive. Suponga el
siguiente caso:
public class Test3{
int a;
Test3(int i){
a = i;
}
Test3 method(){
Test3 temp = new Test3(a+10);
return temp;
}
}
public class Test3Test{
public static void main(String args[]){
Test3 myTest3_1 = new Test3(10);
Test3 myTest3_2;
myTest3_2 = myTest3_1.method();
System.out.println(“myTest3_2.a = “ + myTest3_2.a);
myTest3_2 = myTest3_2.method();
System.out.println(“myTest3_2.a = “ + myTest3_2.a);
}
}
Como puede apreciarse, cada vez que se invoca al método method, se crea un nuevo
objeto de tipo Test3 con un incremento de diez a su atributo a. El valor de retorno es la
referencia al objeto creado, la cual se almacena en myTest3_2 permitiendo que el objeto
no sea recogido por el recolector de basura.
Clase STRING
Un String en Java representa una cadena de caracteres no modificable.
Todos los literales de la forma "cualquier texto", es decir, literales entre comillas dobles, que
aparecen en un programa java se implementan como objetos de la clase String.
El String “En Java, los String son objetos”. automáticamente se convierte en un objeto String
por Java.
Se puede construir un String igual que construye cualquier otro tipo de objeto: utilizando new
y llamando al constructor String. Por ejemplo:
Esto crea un objeto String llamado str que contiene la cadena de caracteres “Hola”.
String str = new String("Hola");
Utilizando una cadena de caracteres entre comillas:
String str = "Hola ¿cómo estás?";
Utilizando operador de concatenación +con dos o más objetos String:
String s2 = s1 + "ghij" s2 contiene "abcdefghij"
String s3 = s1 + s2 + "klm" s3 contiene " abcdefabcdefghijklm"
Una vez que haya creado un objeto String, puede usarlo en cualquier lugar que permita una
cadena entrecomillada. Por ejemplo, puede usar un objeto String como argumento para
println(), como se muestra en este ejemplo:
// Uso de String
class DemoString
{
public static void main(String args[])
{
//Declaración de String de diferentes maneras
String str1=new String("En Java, los String son objetos");
String str2=new String("Se construyen de varias maneras");
String str3=new String(str2);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
}
}
Salida del programa:
En Java, los String son objetos
Se construyen de varias maneras
Se construyen de varias maneras
Operando con Métodos de la clase String
La clase String contiene varios métodos que operan en cadenas. Aquí se detallan todos
los métodos:
int length(): Devuelve la cantidad de caracteres del String.
"Javadesdecero.es".length(); // retorna 16
Char charAt(int i): Devuelve el carácter en el índice i.
System.out.println("Javadesdecero.es".charAt(3)); // retorna 'a'
String substring (int i): Devuelve la subcadena del i-ésimo carácter de índice
al final.
"Javadesdecero.es".substring(4); // retorna desdecero.es
String substring (int i, int j): Devuelve la subcadena del índice i a j-1.
"Javadesdecero.es".substring(4,9); // retorna desde
String concat( String str): Concatena la cadena especificada al final de esta
cadena.
String s1 = "Java";
String s2 = "desdeCero”;
String salida = s1.concat(s2); // retorna "JavadesdeCero"
int indexOf (String s): Devuelve el índice dentro de la cadena de la primera
aparición de la cadena especificada.
String s = "Java desde Cero";
int salida = s.indexOf("Cero"); // retorna 11
int indexOf (String s, int i): Devuelve el índice dentro de la cadena de la
primera aparición de la cadena especificada, comenzando en el índice especificado.
String s = "Java desde Cero";
int salida = s.indexOf('a',3); //retorna 3
Int lastIndexOf (int ch): Devuelve el índice dentro de la cadena de la última
aparición de la cadena especificada.
String s = "Java desde Cero";
int salida = s.lastIndexOf('a'); // retorna 3
boolean equals (Objeto otroObjeto): Compara este String con el objeto
especificado.
Boolean salida = "Java".equals("Java"); // retorna true
Boolean salida = "Java".equals("java"); // retorna false
boolean equalsIgnoreCase (String otroString): Compares string to another
string, ignoring case considerations.
Boolean salida= "Java".equalsIgnoreCase("Java"); // retorna true
Boolean salida = "Java".equalsIgnoreCase("java"); // retorna true
int compareTo (String otroString): Compara dos cadenas
lexicográficamente.
int salida = s1.compareTo(s2); // donde s1 y s2 son // strings que se comparan
Esto devuelve la diferencia s1-s2. Si :
salida < 0 // s1 es menor que s2
salida = 0 // s1 y s2 son iguales
salida > 0 // s1 es mayor que s2
String toLowerCase(): Convierte todos los caracteres de String a minúsculas.
String palabra1 = "HoLa";
String palabra2 = palabra1.toLowerCase(); // retorna "hola"
String toUpperCase(): Convierte todos los caracteres de String a mayúsculas.
String palabra1 = "HoLa";
String palabra2 = palabra1.toUpperCase(); // retorna "HOLA"
String trim(): Devuelve la copia de la cadena, eliminando espacios en blanco
en ambos extremos. No afecta los espacios en blanco en el medio.
String palabra1 = " Java desde Cero ";
String palabra2 = palabra1.trim(); // retorna "Java desde Cero"
String replace (char oldChar, char newChar): Devuelve una nueva cadena al
reemplazar todas las ocurrencias de oldChar con newChar.
String palabra1 = "yavadesdecero";
String palabra2 = palabra1.replace('y' ,'j'); //retorna javadesdecero