- 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 - Yerros 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 próximo instrucción System.out.println("Un montón de texto que escribir"); Es una instrucción bastante larga y que se reitera 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 agregar el directorio raíz de dicho árbol de código a la variable de ámbito 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 emplea la instrucción print en espacio de System.out.println. Esto es así porque estamos empleando la biblioteca que importamos mediante: import static net.mindview.util.Print.*; Observa que se emplea static. Sin embargo, aunque se simplifican bastante los programas, si las instrucciones de impresión son pocas, es preferible seguir utilizando la manera habitual System.out.println en espacio de importar la biblioteca. Personalmente yo no he descargado la biblioteca y en los ejercicios voy a utilizar la manera larga para imprimir. Ejercicio 1. Escriba un proyecto que emplee tanto la manera "corta" como la usual de la instrucción de impresión. UTILIZACIÓN DE LOS OPERADORES JAVA En Java poseemos 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 forma parecida a otros lenguajes de programación. Un operador coge uno o más argumentos y genera un nuevo valor. Los operadores producen un valor a dividir de sus operandos. Además, algunos operadores varían el valor del operando, produciendo lo que se llama resultado 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 normas que definen el orden de evaluación. La multiplicación y la división se realizan antes que la suma y la resta. El resto de normas suelen olvidarse, así pues se emplean paréntesis para establecer el orden en el que se ha de valorar 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 fruto para a y b es distinto sin paréntesis y con paréntesis. La expresión System.out.println() incluye el operador +, en este caso se emplea 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: coge 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 lugar físico para Guardar 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 espacio a otro. Si poseemos 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 verdad estamos manipulando la referencia. Es decir, si poseemos 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 tema level de tipo int. En la clase Assignment se crean dos objetos y dos instancias de la clase Tank, a cada tema 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 varía 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 varíamos uno de ellos varíamos otro. Esto se llama creación de alias. ¿Qué hacemos si no deseamos que las dos referencias apunten al mismo objeto? Podemos realizar la asignación a otro nivel: t1.level=t2.level; Así se mantienen independientes los dos objetos. Aunque en verdad esto resulta bastante confuso y va en contra de los comienzos 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 entorno del método, pero aquí se pasa una referencia, por tanto, la línea: y.c='z'; varía el objeto que está afuera de f(Letter y). La creación de alias y su solución es un asunto complejo, hay que tenerlo en cuenta para detectar probables 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 mientras 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 espacio de redondear. Java también emplea la notación resumida que realiza una operación y una asignación al mismo tiempo. Se denota mediante un operador (+, -, *, /, %) seguido de un signo idéntico y se puede emplear con todos los operadores allí donde tenga sentido. Por ejemplo, x+=4, sumaría 4 a la variable x y se asignaría el fruto 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 distinto en cada ejecución del programa. Para que la salida que se presenta al final de cada ejemplo sea lo más coherente posible, se ofrece una semilla (valor de inicialización para el generador de números aleatorios que siempre genera la misma salida para alguna semilla), al crear el objeto Random (Random rand = new Random(47);) siempre se generarán los mismos números aleatorios, así se podrá verificar 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() constituye la cota sobresaliente para el número generado. La cota inferior es cero, lo cual no es deseable debido a la probabilidad de división por 0, por eso se suma uno al resultado. Ejercicio 4. Escriba un proyecto 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 ofrece 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 tienen lugar una serie de abreviaturas para facilitar la escritura de código. Los operadores de aumento y decremento son dos de ellas. El operador de aumento (++) crece 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 -- surgen antes de la variable, (++a o --a). En este caso primero se realiza la operación y despues se genera el valor. 2. Postfija. Los operadores ++ y -- surgen 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 espacio de utilizar su valor. OPERADORES RELACIONALES Los operadores relacionales generan un fruto de tipo boolean. Evalúan la relación existente entre los operandos, si la relación es alguna el fruto es true, en caso opuesto es false. Los operadores relacionales son: - Menor que: (). - Mayor que: (>). - Menor o idéntico que: (=). - Mayor o idéntico 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 fruto 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 despues 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 próximo 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 fruto 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 fruto 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 vuestra nueva clase. Pero hasta más adelante no vamos a ver la sustitución de unos métodos por otros y la manera apropiada de definir equals(). Sin embargo, el comportamiento de equal() nos va a economizar determinado 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 espacio 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 fruto será idéntico a true o false. - AND. Si todos los operandos son verdaderos el fruto es true, si un operando o más es falso el fruto es false. - OR. Si un operando o más es verdadero el fruto es true, si todos los operandos son falsos el fruto 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 varía, varía 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 ya que 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 contratar un valor que no sea booleano. El comentario (//!) faculta la eliminación automática de comentarios para facilitar las pruebas. Podemos ver que un valor boolean se convierte automáticamente a una manera de tipo texto adecuada cuando se usa en espacios donde lo que se aguarda 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 ubicado por arriba de cero por muy chico que sea Seguid siendo diferente de cero. Ejercicio 7. Escriba un proyecto que simule el proceso de arrojar una moneda al aire. Cortocircuitos Al tratar con operadores lógicos, nos encontramos con el fenómeno de los cortocircuitos. Cuando poseemos una expresión lógica, ésta se evalúa hasta que la veracidad o falsedad de la expresión es alguna de manera no ambigua, por tanto, fracciónes 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 poseemos tres métodos test1, test2 y test3 que nos devuelven true o false dependiendo de si el valor que se introduce como argumento es gran que 1, 2 y 3 respectivamente. En main poseemos una variable b booleana que es el fruto 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 manera se mejora la velocidad del proyecto ya que no es indispensable valorar todas las fracciónes 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 alguna inmaneración adicional en manera de caracteres afiliados 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 ubicado al final de un valor literal faculta establecer su tipo. La L mayúscula o minúscula implica long (la minúscula suele ser confusa). Una F mayúscula o minúscula implica float. Una D mayúscula o minúscula implica double. Los valores hexadecimales (base 16), que funcionan con todos los tipos de datos enteros, van antecedidos de 0x o 0X seguido de 0-9 o a-f en mayúscula o minúscula. En el ejemplo previo 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 bajo (esto lo veremos un poco más adelante en este tema). Así sabremos que nos hemos pasado del límite permitido. Los valores octales (base 8) van antecedidos de un 0 y utilizando sólo los dígitos 0-7. Aunque no tiene lugar una representación literal para los números binarios en Java, a veces resulta provechoso presentar la manera binaria de los resultados. Esto lo podemos realizar 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 presentar 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 tema de las ciencias y la ingeniería, 'e' hace referencia a la fundamento 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 raro pero este valor ha ido pasando a C, C++ y Java, por tanto, 1,39e-43f es 1,39*10e-43. No es indispensable 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 indispensable colocar la L después de 200. Sin embargo en: float f4=1e-43f; // 10 elevado a el compilador estima los números exponenciales como double, por tanto, si no ponemos la f final nos daría un yerro 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 chico 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 facultan manipular bits individuales en un tipo de datos entero primitivo. Para conseguir el fruto se realizan operaciones de álgebra booleana con los bits correspondientes de los dos argumentos. Estos operadores usualmente no los utilizaremos mucho en vuestros programas. Vamos a ver los operadores bit a bit que tenemos: - AND (&). Genera un 1 si los dos bits de acceso son 1, en caso opuesto genera un 0. - OR (|). Genera un 1 si sdeterminados de los bits de acceso es un 1, si los dos bits de acceso 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 opuesto genera un cero. - NOT (~). Es un operador unario que sólo admite un argumento. Genera un 1 si el bit de acceso es 0 y un 0 si el bit de acceso es 1, es decir el contrario al bit de acceso. Los operadores bit a bit y los lógicos emplean 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 distinto a los otros tipos primitivos pues se trata como un valor de un único bit. Se puede hacer 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 resultado que los operadores lógicos, excepto que no se aplica la norma del cortocircuito. Además las operaciones bit a bit incluyen al operador lógico XOR, que no manera fracción de los operadores lógicos. Ejercicio 10. Escribe un proyecto con dos valores persistentes, uno en el que haya unos y ceros binarios alternados, con un cero en el dígito menos importante, y el segundo con un valor también alternado pero con un uno en el dígito menos importante (consejo: lo mas sencillo es usar persistentes hexadecimales para esto). Tome estos dos valores y combínelos de todas las maneras probables utilizando los operadores bit a bit, y visualice los frutos 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 fruto el operando ubicado 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 fruto el operando ubicado 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 emplea lo que se llama expansión de signo: si el valor es efectivo se insertan 0 en los bits de gran peso, si es negativo se insertan 1 en los bits de gran peso. - Operador desplazamiento a la derecha sin signo (>>>). Emplea lo que se designa expansión con ceros, independientemente del signo, se insertan 0 en los bits de gran peso. Si se desplaza un valor de tipo char, byte o short, será convertido a int antes de que el desplazamiento tenga espacio y el fruto 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 sobresaliente al número de bits de un valor int. Si poseemos un valor long, se obtendrá un fruto 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 asunto 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 frutos correctos. En espacio de ello se transforman a int y despues se desplazan a la derecha, pero a continuación se truncan al regresar 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 empleo 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(), cogen un valor int o long respectivamente y lo imprimen en formato binario junto con una cadena de caracteres descriptiva. Además de presentar el fruto de aplicar todos los operadores bit a bit para valores int y long, el ejemplo también presenta los valores mínimo, máximo, +1 y -1 para int y long para que vea el apariencia que tienen. El bit más alto representa el signo: 0 implica efectivo y 1 negativo. En el ejemplo se presenta la salida de la fracción correspondiente a los valores int. La representación binaria de los número se designa 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). Utiliza el operador de desplazamiento a la derecha con signo, desplaza el valor a través de todas sus posiciones binarias, mostrando cada vez el fruto 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 emplea el operador de desplazamiento a la derecha sin signo para desplazarlo a través de todas sus posiciones binarias, visualizando los frutos 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 usarlo ya que el código resultante puede ser escaso legible. Vamos a ver un ejemplo en el que se emplea 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 entretanto que el código de standardIfElse() es más sencillo de entender 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 agregar nuevos significados a casi cualquier operador. Sin embargo, en Java esta característica no existe, por tanto, no se pueden agregar más funcionalidades de las que tienen a los distintos operadores, a diferencia de C++ y C#. En Java si una expresión inicia 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 fruto de la expresión print(s + x + y + z) es x, y, z 012, en espacio 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 espacio 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 += agrega 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 fruto 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(). Yerros comunes a la hora de utilizar operadores Uno de los yerros más comunes es no incluir paréntesis cuando no se está seguro de cómo se evaluará una expresión. En la próximo expresión: while (x=y){ //.... En espacio de emplear el operador de equivalencia (==) se emplea el de asignación (=). El compilador aguarda un valor boolean, pero el fruto de esta expresión no es de tipo boolean y no realizará ninguna conversión a dividir de un valor int, por lo que dará un yerro en tiempo de compilación. Este yerro 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 yerro. En C y C++ los operadores bit a bit AND y OR emplean los caracteres & y |, entretanto que los operadores lógicos emplean && y || respectivamente, por tanto es muy sencillo equivocarse. En Java no se faculta contratar un algun tipo de datos en un espacio donde no sea adecuado 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 faculta hacer esta conversión de forma explícita, o forzarla en situaciones donde no tenga lugar. Para hacer 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 determinadas 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 destacar 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 faculta tanta), corremos el riesgo de perder información. En estos casos el compilador nos obliga a hacer 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 faculta proyectar cualquier tipo primitivo a cualquier otro, excepto en el caso de boolean, que no faculta ningún tipo de proyección. Los tipos de clase tampoco facultan 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 vigilancia al truncamiento y redondeo. Por ejemplo, si poseemos 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 deseamos que el fruto se redondee poseemos 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 chicos que int (char, byte o short), dichos valores serán promocionados a int antes de hacer las operaciones y el fruto será un int. Por tanto, si se desea asignar el fruto del nuevo tipo al más chico es indispensable contratar una proyección, perdiendo de esta forma información. Generalmente, dentro de una expresión el tipo de datos de gran dimensión es el que decide el dimensión del fruto 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 fundamento de datos. La razón del uso de sizeof() es la portabilidad, ya que los distintos tipos de datos pueden tener distintos dimensiónes en diferentes máquinas, así, el programador debe averiguar el dimensión de esos tipos a la hora de hacer operaciones que sean sensibles al dimensión. Una computadora puede Guardar los enteros en 32 bits, entretanto que otra podría Guardarlos en 16 bits. La computadora de 32 bits podría así Guardar más valores que la de 16 bits. Java no requiere un operador sizeof ya que los tipos de datos tienen el mismo dimensión en todas las computadoras. Compendio de operadores A continuación poseemos un ejemplo que presenta qué tipos de datos primitivos pueden utilizarse con determinados operadores concretos. Es el mismo ejemplo repetido una y otra vez pero empleando distintos tipos de datos primitivos. Las líneas con yerros 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 verificar si el valor es verdadero o falso, pero no se pueden sumar valores booleanos ni hacer ninguna otra operación. En char, byte y short se puede ver el fruto de la promoción con los operadores aritméticos. Toda operación aritmética entre cualquiera de estos tipos genera un fruto int que debe ser proyectado al tipo original para hacer 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 fruto. Lo vemos en el próximo 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 yerro o advertencia del compilador, ni tampoco un yerro en tiempo de ejecución. Las asignaciones compuestas (x+=y) no necesitan proyecciones para char, byte o short, inclusive cuando esté realizando promociones que provocan los mismos frutos que las operaciones aritméticas directas. Esto es algo admirable pero la probabilidad 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 hacer 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 distintos