- Instrucciones simples de impresión - Utilización de los operadores Java - Precedencia - Asignación - Creación de alias en las llamadas a métodos - Operadores matemáticos - Operadores unarios mas y menos - Autoincremento y autodecremento - Operadores relacionales - Comprobación de la equivalencia de objetos - Operadores lógicos - Cortocircuitos - Literales - Notación exponencial - Operadores bit a bit - Operadores de desplazamiento - Operador ternario if - else - Operadores + y += para String - Errores comunes a la hora de utilizar operadores - Operadores de proyección - Truncamiento y redondeo - Promoción - Java no tiene operador "sizeof" - Compendio de operadores
En el nivel inferior, los datos en Java se manipulan utilizando operadores.
INSTRUCCIONES SIMPLES DE IMPRESIÓN
Si nos fijamos en la siguiente instrucción
System.out.println("Un montón de texto que escribir");
Es una instrucción bastante larga y que se repite mucho, por lo que el autor de este libro crea una biblioteca para simplificar la escritura de las instrucciones de impresión. Para poder utilizar esta biblioteca hay que descargarla de www.mindview.net, descomprimir el árbol de código y añadir el directorio raíz de dicho árbol de código a la variable de entorno CLASSPATH. Un ejemplo del uso de esa biblioteca es:
//: operators/HelloDate.java import java.util.*; import static net.mindview.util.Print.*; public class HelloDate { public static void main(String[] args) { print("Hello, it's: "); print(new Date()); } } /* Output: (55% match) Hello, it's: Wed Oct 05 14:39:05 MDT 2005 *///:~
En este ejemplo se utiliza la instrucción print en lugar de System.out.println. Esto es así porque estamos utilizando la biblioteca que importamos mediante:
import static net.mindview.util.Print.*;
Observa que se utiliza static.
Sin embargo, aunque se simplifican bastante los programas, si las instrucciones de impresión son pocas, es preferible seguir utilizando la forma habitual System.out.println en lugar de importar la biblioteca.
Personalmente yo no he descargado la biblioteca y en los ejercicios voy a utilizar la forma larga para imprimir.
Ejercicio 1. Escriba un programa que emplee tanto la forma "corta" como la normal de la instrucción de impresión.
UTILIZACIÓN DE LOS OPERADORES JAVA
En Java tenemos los operadores para: la suma, el operador más unario (+), la resta y el operador menos unario (-), la multiplicación (*), la división (/) y la asignación (=) y funcionan de manera similar a otros lenguajes de programación. Un operador toma uno o más argumentos y genera un nuevo valor.
Los operadores producen un valor a partir de sus operandos. Además, algunos operadores cambian el valor del operando, produciendo lo que se llama efecto colateral, por ejemplo, ++, --.
Casi todos los operadores funcionan únicamente con primitivas excepto '=', '==' y '!=' que funcionan con todos los objetos. String también soporta '+' y '+='.
PRECEDENCIA
La precedencia es el orden en el que se evalúa una expresión cuando hay varios operadores. En Java hay unas reglas que determinan el orden de evaluación. La multiplicación y la división se realizan antes que la suma y la resta. El resto de reglas suelen olvidarse, así pues se utilizan paréntesis para establecer el orden en el que se ha de evaluar una expresión. En el ejemplo:
//: operators/Precedence.java public class Precedence { public static void main(String[] args) { int x = 1, y = 2, z = 3; int a = x + y - 2/2 + z; // (1) int b = x + (y - 2)/(2 + z); // (2) System.out.println("a = " + a + " b = " + b); } } /* Output: a = 5 b = 1 *///:~
Si nos fijamos el resultado para a y b es diferente sin paréntesis y con paréntesis.
La expresión System.out.println() incluye el operador +, en este caso se utiliza para concatenar cadenas de caracteres. Cuando el compilador ve un objeto String seguido de '+' intenta convertirlo a un objeto String. En este caso convierte un int a String para a y b.
ASIGNACIÓN
La asignación se realiza con el operador =. Su significado es: toma el valor del lado derecho (rvalor) y cópialo en el lado izquierdo (lvalor). Un rvalor es cualquier constante, variable o expresión que genere un valor. Un lvalor es una variable determinada, designada mediante su nombre, debe haber un espacio físico para almacenar su valor. Podemos asignar un valor constante a una variable:
a=4;
Pero no podemos asignar nada a un valor constante, ya que una constante no puede ser un lvalor.
4=a;
La asignación de primitivas es sencilla. La primitiva almacena el valor real y no una referencia a cualquier objeto, cuando se asignan primitivas se asigna el contenido de un lugar a otro. Si tenemos a=b, el contenido de b se copia en a, si modificamos a, el valor de b no cambia. Esto es lo lógico.
Pero con los objetos la cosa cambia. Cuando manipulamos objetos en realidad estamos manipulando la referencia. Es decir, si tenemos la expresión c=d, tendremos a los objetos c y d apuntando a la misma referencia, al objeto al que sólo apuntaba d originalmente. Vamos a ver un ejemplo:
//: operators/Assignment.java // Assignment with objects is a bit tricky. import static net.mindview.util.Print.*; class Tank { int level; } public class Assignment { public static void main(String[] args) { Tank t1 = new Tank(); Tank t2 = new Tank(); t1.level = 9; t2.level = 47; print("1: t1.level: " + t1.level + ", t2.level: " + t2.level); t1 = t2; print("2: t1.level: " + t1.level + ", t2.level: " + t2.level); t1.level = 27; print("3: t1.level: " + t1.level + ", t2.level: " + t2.level); } } /* Output: 1: t1.level: 9, t2.level: 47 2: t1.level: 47, t2.level: 47 3: t1.level: 27, t2.level: 27 *///:~
Tenemos una clase Tank con un único campo level de tipo int. En la clase Assignment se crean dos objetos y dos instancias de la clase Tank, a cada campo level se le da un valor. Cuando se asigna t1=t2, t1.level y t2.level apuntan a la misma referencia y la referencia a la que apuntaba t1 se pierde, posteriormente será eliminada por el recolector de basura. Cuando se cambia t1.level=27 se podría pensar que t2.level=47, sin embargo esto no es así, t1.level=27 y t2.level=27, ya que ambos objetos apuntan a la misma referencia, por tanto, si cambiamos uno de ellos cambiamos otro.
Esto se llama creación de alias. ¿Qué hacemos si no queremos que las dos referencias apunten al mismo objeto? Podemos hacer la asignación a otro nivel:
t1.level=t2.level;
Así se mantienen independientes los dos objetos. Aunque en realidad esto resulta bastante confuso y va en contra de los principios de un buen diseño orientado a objetos.
Ejercicio 2. Cree una clase que contenga un valor float y utilícela para ilustrar el fenómeno de la creación de alias.
Creación de alias en las llamadas a métodos
La creación de alias también se manifiesta cuando se pasa un objeto a un método:
//: operators/PassObject.java // Passing objects to methods may not be // what you're used to. import static net.mindview.util.Print.*; class Letter { char c; } public class PassObject { static void f(Letter y) { y.c = 'z'; } public static void main(String[] args) { Letter x = new Letter(); x.c = 'a'; print("1: x.c: " + x.c); f(x); print("2: x.c: " + x.c); } } /* Output: 1: x.c: a 2: x.c: z *///:~
En otros lenguajes de programación, el método f(Letter y) haría una copia del argumento Letter dentro del ámbito del método, pero aquí se pasa una referencia, por tanto, la línea:
y.c='z';
cambia el objeto que está fuera de f(Letter y).
La creación de alias y su solución es un tema complejo, hay que tenerlo en cuenta para detectar posibles errores.
Ejercicio 3. Cree una clase que contenga un valor float y utilícela para ilustrar el fenómeno de la creación de alias durante las llamadas a métodos.
OPERADORES MATEMÁTICOS
Los operadores matemáticos básicos son: suma(+), resta(-), multiplicación(*), división(/) y módulo(%) que es el resto de una división entera. La división entera trunca en lugar de redondear.
Java también utiliza la notación abreviada que realiza una operación y una asignación al mismo tiempo. Se denota mediante un operador (+, -, *, /, %) seguido de un signo igual y se puede utilizar con todos los operadores allí donde tenga sentido. Por ejemplo, x+=4, sumaría 4 a la variable x y se asignaría el resultado a x. Veamos un ejemplo del uso de operadores matemáticos:
//: operators/MathOps.java // Demonstrates the mathematical operators. import java.util.*; import static net.mindview.util.Print.*; public class MathOps { public static void main(String[] args) { // Create a seeded random number generator: Random rand = new Random(47); int i, j, k; // Choose value from 1 to 100: j = rand.nextInt(100) + 1; print("j : " + j); k = rand.nextInt(100) + 1; print("k : " + k); i = j + k; print("j + k : " + i); i = j - k; print("j - k : " + i); i = k / j; print("k / j : " + i); i = k * j; print("k * j : " + i); i = k % j; print("k % j : " + i); j %= k; print("j %= k : " + j); // Floating-point number tests: float u, v, w; // Applies to doubles, too v = rand.nextFloat(); print("v : " + v); w = rand.nextFloat(); print("w : " + w); u = v + w; print("v + w : " + u); u = v - w; print("v - w : " + u); u = v * w; print("v * w : " + u); u = v / w; print("v / w : " + u); // The following also works for char, // byte, short, int, long, and double: u += v; print("u += v : " + u); u -= v; print("u -= v : " + u); u *= v; print("u *= v : " + u); u /= v; print("u /= v : " + u); } } /* Output: j : 59 k : 56 j + k : 115 j - k : 3 k / j : 0 k * j : 3304 k % j : 56 j %= k : 3 v : 0.5309454 w : 0.0534122 v + w : 0.5843576 v - w : 0.47753322 v * w : 0.028358962 v / w : 9.940527 u += v : 10.471473 u -= v : 9.940527 u *= v : 5.2778773 u /= v : 9.940527 *///:~
En este ejemplo se crea un objeto Random. Si se crea sin ningún argumento, Java usa la hora actual como semilla para generar números aleatorios y esto generaría una salida diferente en cada ejecución del programa. Para que la salida que se muestra al final de cada ejemplo sea lo más coherente posible, se proporciona una semilla (valor de inicialización para el generador de números aleatorios que siempre genera la misma salida para determinada semilla), al crear el objeto Random (Random rand = new Random(47);) siempre se generarán los mismos números aleatorios, así se podrá comprobar la salida del ejemplo.
Se generan varios números aleatorios con el objeto Random simplemente invocando los métodos nextInt() y nextFloat(), también se pueden invocar nextLong() o nextDouble(). El argumento de nextInt() establece la cota superior para el número generado. La cota inferior es cero, lo cual no es deseable debido a la posibilidad de división por 0, por eso se suma uno al resultado.
Ejercicio 4. Escriba un programa que calcule la velocidad utilizando una distancia constante y un tiempo constante.
Operadores unarios más y menos
El menos unario (-) y el más unario (+), son los mismos operadores que para la suma y la resta.
El menos unario invierte el signo de los datos y el más unario proporciona una simetría con respecto al más unario, pero no tiene ningún efecto. Algunos ejemplos:
x=a*(-b);
Los paréntesis se ponen por claridad.
x=-a;
Autoincremento y autodecremento
En Java existen una serie de abreviaturas para facilitar la escritura de código.
Los operadores de incremento y decremento son dos de ellas. El operador de incremento (++) aumenta en una unidad una variable (a++) y el de decremento (--) disminuye en una unidad la variable (a--).
Hay dos versiones de cada tipo de operador, llamadas prefija y postfija.
1. Prefija. Los operadores ++ y -- aparecen antes de la variable, (++a o --a). En este caso primero se realiza la operación y luego se genera el valor.
2. Postfija. Los operadores ++ y -- aparecen después de la variable, (a++ o a--). En este caso se genera primero el valor y después se realiza la operación.
Lo vemos con un ejemplo:
//: operators/AutoInc.java // Demonstrates the ++ and -- operators. import static net.mindview.util.Print.*; public class AutoInc { public static void main(String[] args) { int i = 1; print("i : " + i); print("++i : " + ++i); // Pre-increment print("i++ : " + i++); // Post-increment print("i : " + i); print("--i : " + --i); // Pre-decrement print("i-- : " + i--); // Post-decrement print("i : " + i); } } /* Output: i : 1 ++i : 2 i++ : 2 i : 3 --i : 2 i-- : 2 i : 1 *///:~
Estos son junto la asignación, los únicos operadores con efectos colaterales. Modifican el operando en lugar de utilizar su valor.
OPERADORES RELACIONALES
Los operadores relacionales generan un resultado de tipo boolean. Evalúan la relación existente entre los operandos, si la relación es cierta el resultado es true, en caso contrario es false. Los operadores relacionales son:
- Menor que: (<). - Mayor que: (>).
- Menor o igual que: (<=). - Mayor o igual que: (>=).
- Equivalente o igual: (==).
- No equivalente o diferente: (!=).
La equivalencia y no equivalencia funcionan con todas las primitivas, sin embargo, los otros operadores relacionales no funcionan con el tipo boolean ya que no tiene sentido.
Comprobación de la equivalencia de objetos
Los operadores relacionales == y != funcionan con todos los objetos, pero su significado puede confundir a los que comienzan a programar en Java. Veamos un ejemplo:
//: operators/Equivalence.java public class Equivalence { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1 == n2); System.out.println(n1 != n2); } } /* Output: false true *///:~
Si nos fijamos en System.out.println(n1==n2), podríamos pensar que el resultado es true y false en System.out.println(n1!=n2), sin embargo, esto no es así ya que aunque el contenido de los objetos es el mismo, las referencias no son iguales, digamos que cada objeto apunta a una referencia diferente. Estos operadores comparan referencias a objetos, por lo que la salida es false y luego true.
¿Cómo podemos saber si el contenido de los objetos es equivalente? Mediante un método especial equals(), disponible para todos los objetos, no para las primitivas. Veamos el siguiente ejemplo:
//: operators/EqualsMethod.java public class EqualsMethod { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1.equals(n2)); } } /* Output: true *///:~
El resultado es el que esperábamos. Sin embargo, la cosa no es tan sencilla, observa:
//: operators/EqualsMethod2.java // Default equals() does not compare contents. class Value { int i; } public class EqualsMethod2 { public static void main(String[] args) { Value v1 = new Value(); Value v2 = new Value(); v1.i = v2.i = 100; System.out.println(v1.equals(v2)); } } /* Output: false *///:~
El resultado es false. Esto es debido a que el comportamiento predeterminado de equals() consiste en comparar referencias. Obtendríamos el comportamiento esperado si sustituimos equals() en nuestra nueva clase. Pero hasta más adelante no vamos a ver la sustitución de unos métodos por otros y la forma adecuada de definir equals(). Sin embargo, el comportamiento de equal() nos va a ahorrar algún que otro quebradero de cabeza.
La mayoría de las clases de biblioteca Java implementan equals() de modo que compare el contenido de los objetos en lugar de sus referencias.
Ejercicio 5. Cree una clase denominada Dog (perro) que contenga dos objetos String: name (nombre) y says (ladrido). En main(), cree dos objetos perro con los nombres Spot (que ladre diciendo "Ruff!") y Scruffy (que ladre diciendo "Wurf!"). Después, muestre sus nombres y el sonido que hacen al ladrar.
Ejercicio 6. Continuando con el Ejercicio 5, cree una nueva referencia Dog y asígnela al objeto de nombre Spot. Realice una comparación utilizando == y equals() para todas las referencias.
OPERADORES LÓGICOS
Los operadores lógicos son AND (&&), OR (||) y NOT (!). Según la relación lógica de los operandos el resultado será igual a true o false.
- AND. Si todos los operandos son verdaderos el resultado es true, si un operando o más es falso el resultado es false.
- OR. Si un operando o más es verdadero el resultado es true, si todos los operandos son falsos el resultado es false.
- NOT. Si un operando es true NOT lo transforma en false, si es false lo transforma en true, pero el valor del operando no cambia, cambia sólo en la relación lógica.
//: operators/Bool.java // Relational and logical operators. import java.util.*; import static net.mindview.util.Print.*; public class Bool { public static void main(String[] args) { Random rand = new Random(47); int i = rand.nextInt(100); int j = rand.nextInt(100); print("i = " + i); print("j = " + j); print("i > j is " + (i > j)); print("i < j is " + (i < j)); print("i >= j is " + (i >= j)); print("i <= j is " + (i <= j)); print("i == j is " + (i == j)); print("i != j is " + (i != j)); // Treating an int as a boolean is not legal Java: //! print("i && j is " + (i && j)); //! print("i || j is " + (i || j)); //! print("!i is " + !i); print("(i < 10) && (j < 10) is " + ((i < 10) && (j < 10)) ); print("(i < 10) || (j < 10) is " + ((i < 10) || (j < 10)) ); } } /* Output: i = 58 j = 55 i > j is true i < j is false i >= j is true i <= j is false i == j is false i != j is true (i < 10) && (j < 10) is false (i < 10) || (j < 10) is false *///:~
Observa que se ha puesto entre comentarios (//!) i&&j, i||j y !i, esto es así porque los operadores relacionales sólo se pueden aplicar a valores de tipo boolean. En una expresión lógica no se puede emplear un valor que no sea booleano. El comentario (//!) permite la eliminación automática de comentarios para facilitar las pruebas.
Podemos ver que un valor boolean se convierte automáticamente a una forma de tipo texto apropiada cuando se usa en lugares donde lo que se espera es un valor de tipo String.
En este último ejemplo se pueden reemplazar los valores int por cualquier otro tipo de dato primitivo excepto boolean. Si se trata de números en coma flotante, es difícil que dos números sean iguales ya que si los valores difieren en un único número ya son diferentes y cualquier número situado por encima de cero por muy pequeño que sea sigue siendo distinto de cero.
Ejercicio 7. Escriba un programa que simule el proceso de lanzar una moneda al aire.
Cortocircuitos
Al tratar con operadores lógicos, nos encontramos con el fenómeno de los cortocircuitos. Cuando tenemos una expresión lógica, ésta se evalúa hasta que la veracidad o falsedad de la expresión es determinada de forma no ambigua, por tanto, partes de la expresión lógica puede que no lleguen a evaluarse. Veamos un ejemplo:
//: operators/ShortCircuit.java // Demonstrates short-circuiting behavior // with logical operators. import static net.mindview.util.Print.*; public class ShortCircuit { static boolean test1(int val) { print("test1(" + val + ")"); print("result: " + (val < 1)); return val < 1; } static boolean test2(int val) { print("test2(" + val + ")"); print("result: " + (val < 2)); return val < 2; } static boolean test3(int val) { print("test3(" + val + ")"); print("result: " + (val < 3)); return val < 3; } public static void main(String[] args) { boolean b = test1(0) && test2(2) && test3(2); print("expression is " + b); } } /* Output: test1(0) result: true test2(2) result: false expression is false *///:~
En esta clase tenemos tres métodos test1, test2 y test3 que nos devuelven true o false dependiendo de si el valor que se introduce como argumento es mayor que 1, 2 y 3 respectivamente. En main tenemos una variable b booleana que es el resultado de la expresión: test1(0) && test2(2) && test3(2), como el método test2(2) devuelve false, ni siquiera entramos al método test3(2) ya que sea cual sea el valor devuelto por este método la expresión va a ser false porque test2(2) ya ha devuelto false. De esta forma se mejora la velocidad del programa ya que no es necesario evaluar todas las partes de una expresión lógica.
Literales
Cuando insertamos un valor literal en un programa, el compilador sabe qué tipo asignarle. A veces puede que este tipo sea ambiguo. Cuando esto ocurre, hay que guiar al compilador añadiendo cierta información adicional en forma de caracteres asociados al valor literal. Veamos un ejemplo:
//: operators/Literals.java import static net.mindview.util.Print.*; public class Literals { public static void main(String[] args) { int i1 = 0x2f; // Hexadecimal (lowercase) print("i1: " + Integer.toBinaryString(i1)); int i2 = 0X2F; // Hexadecimal (uppercase) print("i2: " + Integer.toBinaryString(i2)); int i3 = 0177; // Octal (leading zero) print("i3: " + Integer.toBinaryString(i3)); char c = 0xffff; // max char hex value print("c: " + Integer.toBinaryString(c)); byte b = 0x7f; // max byte hex value print("b: " + Integer.toBinaryString(b)); short s = 0x7fff; // max short hex value print("s: " + Integer.toBinaryString(s)); long n1 = 200L; // long suffix long n2 = 200l; // long suffix (but can be confusing) long n3 = 200; float f1 = 1; float f2 = 1F; // float suffix float f3 = 1f; // float suffix double d1 = 1d; // double suffix double d2 = 1D; // double suffix // (Hex and Octal also work with long) } } /* Output: i1: 101111 i2: 101111 i3: 1111111 c: 1111111111111111 b: 1111111 s: 111111111111111 *///:~
Un carácter situado al final de un valor literal permite establecer su tipo. La L mayúscula o minúscula significa long (la minúscula suele ser confusa). Una F mayúscula o minúscula significa float. Una D mayúscula o minúscula significa double.
Los valores hexadecimales (base 16), que funcionan con todos los tipos de datos enteros, van precedidos de 0x o 0X seguido de 0-9 o a-f en mayúscula o minúscula. En el ejemplo anterior se ve el valor máximo para char, byte y short. Si nos excedemos del valor máximo que estos tipos pueden contener, el compilador transformará automáticamente el valor a int y nos dirá que necesitamos una proyección hacia abajo (esto lo veremos un poquito más adelante en este tema). Así sabremos que nos hemos pasado del límite permitido.
Los valores octales (base 8) van precedidos de un 0 y utilizando sólo los dígitos 0-7.
Aunque no existe una representación literal para los números binarios en Java, a veces resulta útil mostrar la forma binaria de los resultados. Esto lo podemos hacer con los métodos static toBinaryString() de las clases Integer y Long.Cuando se pasan tipos más pequeños a Integer.toBinaryString(), el tipo se convierte automáticamente a int.
Ejercicio 8. Demuestre que las notaciones hexadecimal y octal funcionan con los valores long. Utilice Long.toBinaryString() para mostrar los resultados.
Notación exponencial
Vamos a ver un ejemplo de notación exponencial:
//: operators/Exponents.java // " e="" means="" 10="" to="" the=""> public class Exponents { public static void main(String[] args) { // Uppercase and lowercase 'e' are the same: float expFloat = 1.39e-43f; expFloat = 1.39E-43f; System.out.println(expFloat); double expDouble = 47e47d; // 'd' is optional double expDouble2 = 47e47; // Automatically double System.out.println(expDouble); } } /* Output: 1.39E-43 4.7E48 *///:~
En el campo de las ciencias y la ingeniería, 'e' hace referencia a la base de los logaritmos naturales, que es aproximadamente 2,718 (en Java hay un 'e' double más preciso, Math.E). Esto se usa en expresiones de exponenciación, así 1,39*e-43 es 1,39*2,718e-43. Sin embargo, cuando se inventó FORTRAN se decidió que 'e' significaría "diez elevado a", lo cual es bastante extraño pero este valor ha ido pasando a C, C++ y Java, por tanto, 1,39e-43f es 1,39*10e-43.
No es necesario utilizar el carácter sufijo cuando el compilador puede deducir el tipo apropiado. Así:
long n3=200;
Aquí no hay ambigüedad, por lo que no es necesario poner la L después de 200. Sin embargo en:
float f4=1e-43f; // 10 elevado a
el compilador considera los números exponenciales como double, por tanto, si no ponemos la f final nos daría un error en el que nos indicaría que hay que usar una proyección para convertir el valor double a float.
Ejercicio 9. Visualice los números más grande y más pequeño que se pueden representar con la notación exponencial en el tipo float y en el tipo double.
Operadores bit a bit
Los operadores bit a bit permiten manipular bits individuales en un tipo de datos entero primitivo. Para obtener el resultado se realizan operaciones de álgebra booleana con los bits correspondientes de los dos argumentos. Estos operadores normalmente no los utilizaremos mucho en nuestros programas. Vamos a ver los operadores bit a bit que tenemos:
- AND (&). Genera un 1 si los dos bits de entrada son 1, en caso contrario genera un 0.
- OR (|). Genera un 1 si alguno de los bits de entrada es un 1, si los dos bits de entrada son 0 genera un 0.
- EXCLUSIVE OR o XOR (^). Genera un 1 si uno de los dos bits de entrada, pero no ambos, es 1, en caso contrario genera un cero.
- NOT (~). Es un operador unario que sólo admite un argumento. Genera un 1 si el bit de entrada es 0 y un 0 si el bit de entrada es 1, es decir el opuesto al bit de entrada.
Los operadores bit a bit y los lógicos utilizan los mismos símbolos para sus operaciones. Un truco para recordar cuál hay que utilizar para cada operación es: como los bits son pequeños para las operaciones bit a bit utilizamos sólo un símbolo y para las lógicas dos.
Los operadores bit a bit pueden combinarse con el símbolo igual: &=, |= y ^=.
El tipo boolean es diferente a los otros tipos primitivos pues se trata como un valor de un único bit. Se puede realizar una operación AND, OR y XOR bit a bit, pero no una operación NOT. Para los valores booleanos, los operadores bit a bit tienen el mismo efecto que los operadores lógicos, excepto que no se aplica la regla del cortocircuito. Además las operaciones bit a bit incluyen al operador lógico XOR, que no forma parte de los operadores lógicos.
Ejercicio 10. Escribe un programa con dos valores constantes, uno en el que haya unos y ceros binarios alternados, con un cero en el dígito menos significativo, y el segundo con un valor también alternado pero con un uno en el dígito menos significativo (consejo: lo mas fácil es usar constantes hexadecimales para esto). Tome estos dos valores y combínelos de todas las formas posibles utilizando los operadores bit a bit, y visualice los resultados utilizando Integer.toBinaryString().
Operadores de desplazamiento
Los operadores de desplazamiento también sirven para manipular bits. Sólo se pueden utilizar con tipos primitivos enteros. Los operadores de desplazamiento son:
- Operador de desplazamiento a la izquierda (<<). Genera como resultado el operando situado a la izquierda del operador después de desplazarlo hacia la izquierda el número de bits especificado a la derecha del operador (insertando 0 en los bits de menor peso).
- Operador de desplazamiento a la derecha con signo (>>). Genera como resultado el operando situado a la izquierda del operador después de desplazarlo hacia la derecha el número de bits especificado a la derecha del operador. Además utiliza lo que se llama extensión de signo: si el valor es positivo se insertan 0 en los bits de mayor peso, si es negativo se insertan 1 en los bits de mayor peso.
- Operador desplazamiento a la derecha sin signo (>>>). Utiliza lo que se denomina extensión con ceros, independientemente del signo, se insertan 0 en los bits de mayor peso.
Si se desplaza un valor de tipo char, byte o short, será convertido a int antes de que el desplazamiento tenga lugar y el resultado será de tipo int. Sólo se utilizarán los bits de menor peso del lado derecho, esto evita que se realicen desplazamientos con un número de posiciones superior al número de bits de un valor int. Si tenemos un valor long, se obtendrá un resultado de tipo long y sólo se emplearán los seis bits de menor peso del lado derecho, para así no desplazar más posiciones que el número de bits de un valor long.
Los desplazamientos se pueden combinar con el signo igual: <<= o >>= o >>>=. El lvalor se sustituye por el lvalor desplazado de acuerdo con lo que marque el rvalor. Existe un problema con el desplazamiento a la derecha sin signo combinado con la asignación. Si se usa con valores de tipo byte o short, no se obtienen los resultados correctos. En lugar de ello se transforman a int y luego se desplazan a la derecha, pero a continuación se truncan al volver a asignar los valores a sus variables, por lo que se obtiene -1 en esos casos. Vamos a verlo con un ejemplo:
//: operators/URShift.java // Test of unsigned right shift. import static net.mindview.util.Print.*; public class URShift { public static void main(String[] args) { int i = -1; print(Integer.toBinaryString(i)); i >>>= 10; print(Integer.toBinaryString(i)); long l = -1; print(Long.toBinaryString(l)); l >>>= 10; print(Long.toBinaryString(l)); short s = -1; print(Integer.toBinaryString(s)); s >>>= 10; print(Integer.toBinaryString(s)); byte b = -1; print(Integer.toBinaryString(b)); b >>>= 10; print(Integer.toBinaryString(b)); b = -1; print(Integer.toBinaryString(b)); print(Integer.toBinaryString(b>>>10)); } } /* Output: 11111111111111111111111111111111 1111111111111111111111 1111111111111111111111111111111111111111111111111111111111111111 111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 1111111111111111111111 *///:~
En el último desplazamiento, el valor resultante no se asigna de nuevo a b, sino que se imprime directamente obteniéndose el comportamiento deseado.
Vamos a ver un ejemplo con todos los operadores para el manejo de bits:
//: operators/BitManipulation.java // Using the bitwise operators. import java.util.*; import static net.mindview.util.Print.*; public class BitManipulation { public static void main(String[] args) { Random rand = new Random(47); int i = rand.nextInt(); int j = rand.nextInt(); printBinaryInt("-1", -1); printBinaryInt("+1", +1); int maxpos = 2147483647; printBinaryInt("maxpos", maxpos); int maxneg = -2147483648; printBinaryInt("maxneg", maxneg); printBinaryInt("i", i); printBinaryInt("~i", ~i); printBinaryInt("-i", -i); printBinaryInt("j", j); printBinaryInt("i & j", i & j); printBinaryInt("i | j", i | j); printBinaryInt("i ^ j", i ^ j); printBinaryInt("i << 5", i << 5); printBinaryInt("i >> 5", i >> 5); printBinaryInt("(~i) >> 5", (~i) >> 5); printBinaryInt("i >>> 5", i >>> 5); printBinaryInt("(~i) >>> 5", (~i) >>> 5); long l = rand.nextLong(); long m = rand.nextLong(); printBinaryLong("-1L", -1L); printBinaryLong("+1L", +1L); long ll = 9223372036854775807L; printBinaryLong("maxpos", ll); long lln = -9223372036854775808L; printBinaryLong("maxneg", lln); printBinaryLong("l", l); printBinaryLong("~l", ~l); printBinaryLong("-l", -l); printBinaryLong("m", m); printBinaryLong("l & m", l & m); printBinaryLong("l | m", l | m); printBinaryLong("l ^ m", l ^ m); printBinaryLong("l << 5", l << 5); printBinaryLong("l >> 5", l >> 5); printBinaryLong("(~l) >> 5", (~l) >> 5); printBinaryLong("l >>> 5", l >>> 5); printBinaryLong("(~l) >>> 5", (~l) >>> 5); } static void printBinaryInt(String s, int i) { print(s + ", int: " + i + ", binary:\n " + Integer.toBinaryString(i)); } static void printBinaryLong(String s, long l) { print(s + ", long: " + l + ", binary:\n " + Long.toBinaryString(l)); } } /* Output: -1, int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: 1 maxpos, int: 2147483647, binary: 1111111111111111111111111111111 maxneg, int: -2147483648, binary: 10000000000000000000000000000000 i, int: -1172028779, binary: 10111010001001000100001010010101 ~i, int: 1172028778, binary: 1000101110110111011110101101010 -i, int: 1172028779, binary: 1000101110110111011110101101011 j, int: 1717241110, binary: 1100110010110110000010100010110 i & j, int: 570425364, binary: 100010000000000000000000010100 i | j, int: -25213033, binary: 11111110011111110100011110010111 i ^ j, int: -595638397, binary: 11011100011111110100011110000011 i << 5, int: 1149784736, binary: 1000100100010000101001010100000 i >> 5, int: -36625900, binary: 11111101110100010010001000010100 (~i) >> 5, int: 36625899, binary: 10001011101101110111101011 i >>> 5, int: 97591828, binary: 101110100010010001000010100 (~i) >>> 5, int: 36625899, binary: 10001011101101110111101011 ... *///:~
Los dos métodos del final, printBinaryInt() y printBinaryLong(), toman un valor int o long respectivamente y lo imprimen en formato binario junto con una cadena de caracteres descriptiva. Además de mostrar el resultado de aplicar todos los operadores bit a bit para valores int y long, el ejemplo también muestra los valores mínimo, máximo, +1 y -1 para int y long para que vea el aspecto que tienen. El bit más alto representa el signo: 0 significa positivo y 1 negativo. En el ejemplo se muestra la salida de la parte correspondiente a los valores int.
La representación binaria de los número se denomina complemento a dos con signo.
Ejercicio 11. Comienza con un número que tenga un uno binario en la posición más significativa (consejo: utilice una constante hexadecimal). Emplea el operador de desplazamiento a la derecha con signo, desplaza el valor a través de todas sus posiciones binarias, mostrando cada vez el resultado con Integer.toBinaryString().
Ejercicio 12. Comienza con un número cuyos dígitos binarios sean todos iguales a uno. A continuación desplázalo a la izquierda y utiliza el operador de desplazamiento a la derecha sin signo para desplazarlo a través de todas sus posiciones binarias, visualizando los resultados con Integer.toBinaryString().
Ejercicio 13. Escribe un método que muestre valores char en formato binario. Ejecútelo utilizando varios caracteres diferentes.
Operador ternario if - else
El operador if - else tiene tres operandos. La expresión es de la forma:
expresión - booleana ? valor0 : valor1
Si expresión - booleana es true, se evalúa valor0, en caso de que sea false, se evalúa valor1.
Se podría utilizar una instrucción if - else normal, pero el operador if - else ternario es más compacto. Sin embargo hay que tener cuidado a la hora de utilizarlo ya que el código resultante puede ser poco legible.
Vamos a ver un ejemplo en el que se utiliza el operador ternario e if - else normal:
//: operators/TernaryIfElse.java import static net.mindview.util.Print.*; public class TernaryIfElse { static int ternary(int i) { return i < 10 ? i * 100 : i * 10; } static int standardIfElse(int i) { if(i < 10) return i * 100; else return i * 10; } public static void main(String[] args) { print(ternary(9)); print(ternary(10)); print(standardIfElse(9)); print(standardIfElse(10)); } } /* Output: 900 100 900 100 *///:~
El código de ternary() es más compacto mientras que el código de standardIfElse() es más fácil de comprender y hay que escribir más caracteres. Hay que asegurarse bien de cuándo utilizar el operador ternario, puede convenir cuando se quiera configurar una variable con uno de dos valores posibles.
Operadores + y += para String
Los operadores + y += pueden usarse para concatenar cadenas.
Esta sobrecarga de operadores se añadió en C++, para que los programadores pudieran añadir nuevos significados a casi cualquier operador. Sin embargo, en Java esta característica no existe, por tanto, no se pueden añadir más funcionalidades de las que tienen a los diferentes operadores, a diferencia de C++ y C#.
En Java si una expresión comienza con String, todos los operandos que siguen también tendrán que ser cadenas de caracteres, (el compilador transforma automáticamente a String toda secuencia de caracteres encerrada entre comillas dobles).
Veamos un ejemplo:
//: operators/StringOperators.java import static net.mindview.util.Print.*; public class StringOperators { public static void main(String[] args) { int x = 0, y = 1, z = 2; String s = "x, y, z "; print(s + x + y + z); print(x + " " + s); // Converts x to a String s += "(summed) = "; // Concatenation operator print(s + (x + y + z)); print("" + x); // Shorthand for Integer.toString() } } /* Output: x, y, z 012 0 x, y, z x, y, z (summed) = 3 0 *///:~
El resultado de la expresión print(s + x + y + z) es x, y, z 012, en lugar de x, y, z 3. Esto es porque el compilador convierte las variables x, y y z a String y concatena las cadenas de caracteres en lugar de calcular primero la suma. La segunda expresión de impresión print(x + " " + s) convierte la variable inicial x a String, por lo que la conversión a cadena no depende de qué es lo que haya primero. El operador += añade una cadena de caracteres a s, y s se transforma en x, y, z (summed) =. La expresión print(s + (x + y + z)) encierra entre paréntesis a los valores enteros por lo que se realiza la suma antes de convertir el resultado a String.
La última expresión print("" + x) realiza una conversión de entero a String de x sin necesidad de invocar al método Integer.toString().
Errores comunes a la hora de utilizar operadores
Uno de los errores más comunes es no incluir paréntesis cuando no se está seguro de cómo se evaluará una expresión.
En la siguiente expresión:
while (x=y){ //.... }
En lugar de utilizar el operador de equivalencia (==) se utiliza el de asignación (=). El compilador espera un valor boolean, pero el resultado de esta expresión no es de tipo boolean y no realizará ninguna conversión a partir de un valor int, por lo que dará un error en tiempo de compilación. Este error no es detectado por el compilador si x e y son de tipo boolean, en cuyo caso (x=y) sería legal, aunque su uso probablemente es un error.
En C y C++ los operadores bit a bit AND y OR utilizan los caracteres & y |, mientras que los operadores lógicos utilizan && y || respectivamente, por tanto es muy fácil equivocarse. En Java no se permite emplear un determinado tipo de datos en un lugar donde no sea correcto hacerlo.
Operadores de proyección
La palabra proyección (cast) hace referencia a la conversión explícita de datos de un tipo a otro. Java cambiará un tipo de datos a otro cada vez que sea necesario. Si se asigna a una variable de tipo float un valor entero, el compilador convertirá automáticamente el valor int a float. El mecanismo de conversión nos permite realizar esta conversión de manera explícita, o forzarla en situaciones donde no tenga lugar.
Para realizar una proyección se pone a la izquierda del valor que haya que convertir, entre paréntesis, el tipo de datos deseado. Lo vemos con un ejemplo:
//: operators/Casting.java public class Casting { public static void main(String[] args) { int i = 200; long lng = (long)i; lng = i; // "Widening," so cast not really required long lng2 = (long)200; lng2 = 200; // A "narrowing conversion": i = (int)lng2; // Cast required } } ///:~
Se puede aplicar una proyección tanto a los valores numéricos como a las variables. En el ejemplo vemos algunas proyecciones superfluas (long lng = (long)i; y long lng2 = (long)200;), ya que el compilador promocionará automáticamente un valor int a long cuando sea necesario. Sin embargo, estas proyecciones superfluas pueden resaltar la operación o clarificar el código. En otras situaciones (i = (int)lng2;) es necesaria la proyección para que el código se compile correctamente.
Cuando realizamos una conversión de estrechamiento (pasamos de un tipo de datos que puede albergar más información a otro que no permite tanta), corremos el riesgo de perder información. En estos casos el compilador nos obliga a realizar una proyección. En cuanto a la conversión de ensanchamiento (pasamos de un tipo de datos que puede albergar menos información a otro que puede albergar más), no hace falta una proyección explícita ya que el nuevo tipo albergará con creces la información del tipo anterior.
Java permite proyectar cualquier tipo primitivo a cualquier otro, excepto en el caso de boolean, que no permite ningún tipo de proyección. Los tipos de clase tampoco permiten proyecciones: para convertir uno de estos tipos en otro deben existir métodos especiales.
Truncamiento y redondeo
Cuando se realizan conversiones de estrechamiento hay que prestar atención al truncamiento y redondeo. Por ejemplo, si tenemos el valor en coma flotante 29,7 y lo proyectamos sobre un int, ¿el valor resultante será 30 o 29? Vamos a verlo en un ejemplo:
//: operators/CastingNumbers.java // What happens when you cast a float // or double to an integral value? import static net.mindview.util.Print.*; public class CastingNumbers { public static void main(String[] args) { double above = 0.7, below = 0.4; float fabove = 0.7f, fbelow = 0.4f; print("(int)above: " + (int)above); print("(int)below: " + (int)below); print("(int)fabove: " + (int)fabove); print("(int)fbelow: " + (int)fbelow); } } /* Output: (int)above: 0 (int)below: 0 (int)fabove: 0 (int)fbelow: 0 *///:~
Como vemos, cuando proyectamos un float o double a un valor entero, se trunca el número correspondiente. Si queremos que el resultado se redondee tenemos que utilizar los métodos round() de java.lang.Math.
//: operators/RoundingNumbers.java // Rounding floats and doubles. import static net.mindview.util.Print.*; public class RoundingNumbers { public static void main(String[] args) { double above = 0.7, below = 0.4; float fabove = 0.7f, fbelow = 0.4f; print("Math.round(above): " + Math.round(above)); print("Math.round(below): " + Math.round(below)); print("Math.round(fabove): " + Math.round(fabove)); print("Math.round(fbelow): " + Math.round(fbelow)); } } /* Output: Math.round(above): 1 Math.round(below): 0 Math.round(fabove): 1 Math.round(fbelow): 0 *///:~
Promoción
En Java si se hacen operaciones matemáticas o bit a bit sobre tipos de datos más pequeños que int (char, byte o short), dichos valores serán promocionados a int antes de realizar las operaciones y el resultado será un int. Por tanto, si se quiere asignar el resultado del nuevo tipo al más pequeño es necesario emplear una proyección, perdiendo de esta manera información. Generalmente, dentro de una expresión el tipo de datos de mayor tamaño es el que determina el tamaño del resultado de esa expresión, si multiplicamos un float por un double obtendremos un double, si sumamos un int y un long obtendremos un long, etc.
Java no tiene operador "sizeof"
En C y C++ el operador sizeof() nos dice el número de bytes asignado a un elemento de datos. La razón del uso de sizeof() es la portabilidad, ya que los diferentes tipos de datos pueden tener diferentes tamaños en distintas máquinas, así, el programador debe averiguar el tamaño de esos tipos a la hora de realizar operaciones que sean sensibles al tamaño. Una computadora puede almacenar los enteros en 32 bits, mientras que otra podría almacenarlos en 16 bits. La computadora de 32 bits podría así almacenar más valores que la de 16 bits.
Java no necesita un operador sizeof ya que los tipos de datos tienen el mismo tamaño en todas las computadoras.
Compendio de operadores
A continuación tenemos un ejemplo que muestra qué tipos de datos primitivos pueden utilizarse con determinados operadores concretos. Es el mismo ejemplo repetido una y otra vez pero empleando diferentes tipos de datos primitivos. Las líneas con errores están desactivadas con comentarios de tipo //!.
//: operators/AllOps.java // Tests all the operators on all the primitive data types // to show which ones are accepted by the Java compiler. public class AllOps { // To accept the results of a boolean test: void f(boolean b) {} void boolTest(boolean x, boolean y) { // Arithmetic operators: //! x = x * y; //! x = x / y; //! x = x % y; //! x = x + y; //! x = x - y; //! x++; //! x--; //! x = +y; //! x = -y; // Relational and logical: //! f(x > y); //! f(x >= y); //! f(x < y); //! f(x <= y); f(x == y); f(x != y); f(!y); x = x && y; x = x || y; // Bitwise operators: //! x = ~y; x = x & y; x = x | y; x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: //! x += y; //! x -= y; //! x *= y; //! x /= y; //! x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! char c = (char)x; //! byte b = (byte)x; //! short s = (short)x; //! int i = (int)x; //! long l = (long)x; //! float f = (float)x; //! double d = (double)x; } void charTest(char x, char y) { // Arithmetic operators: x = (char)(x * y); x = (char)(x / y); x = (char)(x % y); x = (char)(x + y); x = (char)(x - y); x++; x--; x = (char)+y; x = (char)-y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x= (char)~y; x = (char)(x & y); x = (char)(x | y); x = (char)(x ^ y); x = (char)(x << 1); x = (char)(x >> 1); x = (char)(x >>> 1); // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean bl = (boolean)x; byte b = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void byteTest(byte x, byte y) { // Arithmetic operators: x = (byte)(x* y); x = (byte)(x / y); x = (byte)(x % y); x = (byte)(x + y); x = (byte)(x - y); x++; x--; x = (byte)+ y; x = (byte)- y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = (byte)~y; x = (byte)(x & y); x = (byte)(x | y); x = (byte)(x ^ y); x = (byte)(x << 1); x = (byte)(x >> 1); x = (byte)(x >>> 1); // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void shortTest(short x, short y) { // Arithmetic operators: x = (short)(x * y); x = (short)(x / y); x = (short)(x % y); x = (short)(x + y); x = (short)(x - y); x++; x--; x = (short)+y; x = (short)-y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = (short)~y; x = (short)(x & y); x = (short)(x | y); x = (short)(x ^ y); x = (short)(x << 1); x = (short)(x >> 1); x = (short)(x >>> 1); // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; byte b = (byte)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void intTest(int x, int y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; byte b = (byte)x; short s = (short)x; long l = (long)x; float f = (float)x; double d = (double)x; } void longTest(long x, long y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; byte b = (byte)x; short s = (short)x; int i = (int)x; float f = (float)x; double d = (double)x; } void floatTest(float x, float y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = ~y; //! x = x & y; //! x = x | y; //! x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; //! x &= y; //! x ^= y; //! x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; byte b = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; double d = (double)x; } void doubleTest(double x, double y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = ~y; //! x = x & y; //! x = x | y; //! x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; //! x &= y; //! x ^= y; //! x |= y; // Casting: //! boolean bl = (boolean)x; char c = (char)x; byte b = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; } } ///:~
Si nos fijamos boolean es bastante limitado. Una variable de este tipo puede ser true y false, y se puede comprobar si el valor es verdadero o falso, pero no se pueden sumar valores booleanos ni realizar ninguna otra operación.
En char, byte y short se puede ver el efecto de la promoción con los operadores aritméticos. Toda operación aritmética entre cualquiera de estos tipos genera un resultado int que debe ser proyectado al tipo original para realizar la asignación a dicho tipo. Con los valores int no es necesaria ninguna proyección ya que todo es de tipo int. No todas las operaciones son seguras ya que si se multiplican dos int que sean suficientemente grandes, puede producirse un desbordamiento en el resultado. Lo vemos en el siguiente ejemplo:
//: operators/Overflow.java // Surprise! Java lets you overflow. public class Overflow { public static void main(String[] args) { int big = Integer.MAX_VALUE; System.out.println("big = " + big); int bigger = big * 4; System.out.println("bigger = " + bigger); } } /* Output: big = 2147483647 bigger = -4 *///:~
No se obtiene ningún error o advertencia del compilador, ni tampoco un error en tiempo de ejecución.
Las asignaciones compuestas (x+=y) no requieren proyecciones para char, byte o short, incluso cuando esté realizando promociones que provocan los mismos resultados que las operaciones aritméticas directas. Esto es algo sorprendente pero la posibilidad de no incluir la proyección simplifica el código.
Con la excepción de boolean, podemos proyectar cualquier tipo primitivo sobre cualquier otro tipo primitivo. Hay que tener en cuenta las conversiones de estrechamiento a la hora de realizar proyecciones sobre tipos de menor tamaño.
Ejercicio 14. Escribe un método que tome dos argumentos de tipo String y utilice todas las comparaciones boolean para comparar las dos cadenas de caracteres e imprimir los resultados. Para las comparaciones == y !=, realiza también la prueba con equals(). En main(), invoca el método que haya escrito, utilizando varios objetos String diferentes.
Hola, me ha gustado mucho tu texto, pero he de decirte que algunos conceptos que explicas están equivocados. Mira te pongo las líneas en las que te equivocaste y te muestro la corrección:
ResponderEliminartu escribes:
"Tenemos una clase Tank ..." (debajo del ejemplo).
Bueno aquí cometes un error de base, confundes mucho los objetos con sus referencias, así en ese párrafo llegas a decir que "AMBOS OBJETOS APUNTAN A LA MISMA REFERENCIA", y creas mucha confusión al lector. He de decirte que un objeto NUNCA apunta a una referencia, sino que es la VARIABLE REFERENCIA la que apunta al objeto. Te dejo una explicación alternativa a la que has expuesto tú para explicar el código, espero que te sirva:
"Lo correcto en este caso es que se crean dos objetos Tank, REFERENCIADOS por las variables t1 y t2. Cuando se realiza la asignación t1=t2, la VARIABLE REFERENCIA t1 pasa a apuntar al MISMO OBJETO que apunta t2, y el objeto que era referenciado anteriormente por t1 se convierte en un OBJETO INACCESIBLE(ó perdido) y será candidato a ser recolectado como basura. Cuando se ejecuta la instrucción t1.level=27, tendrá el mismo efecto que si ejecutamos t2.level=27, ya que a partir de la asignación t1=t2 ya sólo estamos trabajando con dos referencias que apuntan al mismo objeto".
No se si ves el error sutil que comentes, pero confundes un poco los terminos bastante, un objeto nunca apunta a una referencia.
Los mismo te vuelve a suceder en el epígrafe de Comprobacion de la equivalencia entre objetos, tu dices "digamos que cada objeto apunta a una referencia diferente", sustitúyelo por "cada referencia APUNTA a un objeto diferente".
Un Saludo!! Alfredo (ing. superior informatica, politéctica de Madrid).
Hola Alfredo:
EliminarPues tienes toda la razón del mundo. Tengo que corregirlo, es cierto que lo pongo mal. Es la variable referencia la que apunta al objeto.
Muchas gracias por tu aportación, muy interesante todo lo que has escrito.
Por cierto, si ves alguna cosa más que esté mal, dímelo y lo corrijo.
EliminarMuchas gracias de verdad.