Fundamentos
Operadores y expresiones
Aritmética, comparación y lógica, con la precedencia y el desbordamiento a la vista.
Desde Java 8
Este capítulo cubre Java 8. Estás leyendo en .
Una expresión devuelve un valor
Un operador combina valores y produce otro valor. Casi todo lo que escribes en Java es una
expresión: 2 + 3, edad >= 18, nombre.equals("Ada"). La excepción son las sentencias,
que hacen algo y no devuelven nada (if, for, una asignación como sentencia).
Dos reglas gobiernan el resto de este capítulo:
- El tipo del resultado no siempre es el tipo de los operandos, y Java lo decide con
conversión numérica:
byteyshortse promocionan aintantes de operar. - La evaluación va de izquierda a derecha dentro de una misma precedencia, y un operando se evalúa antes que el operador que lo usa.
Aritmética entera: dos trampas propias
La división entre enteros trunca hacia cero, no hacia abajo:
System.out.println(7 / 2); // 3
System.out.println(-7 / 2); // -3
System.out.println(7 % 2); // 1
System.out.println(-7 % 2); // -1El resto tiene el signo del dividendo, no el del divisor. La especificación lo dice así: el
resultado de % puede ser negativo solo si el dividendo lo es, y su magnitud siempre es
menor que la del divisor. Si vienes de Python, donde -7 % 2 da 1, esto te muerde la
primera vez que calculas una vuelta de array.
Para el resto con signo del divisor está Math.floorMod:
System.out.println(-7 % 2); // -1
System.out.println(Math.floorMod(-7, 2)); // 1La segunda trampa es el desbordamiento. Los enteros no avisan: dan la vuelta.
Desbordamiento de int
-
01
Techo
2_147_483_647Integer.MAX_VALUE. -
02
Sumas uno
+ 1No hay sitio para el bit de signo. -
03
Suelo
-2_147_483_648Integer.MIN_VALUE, sin excepción.
int techo = Integer.MAX_VALUE;
System.out.println(techo + 1); // -2147483648
System.out.println(Math.addExact(techo, 1)); // ArithmeticExceptionMath.addExact, subtractExact y multiplyExact existen justo para esto: si el resultado
no cabe, lanzan ArithmeticException en vez de mentir. Un contador de dinero, de
identificadores o de tiempo va con long o con estas variantes, nunca con int a secas.
Cast: bajar de tamaño se avisa, subir no
Pasar de un tipo menor a uno mayor es automático. Al contrario hay que decirlo:
int grande = 300;
byte pequeno = (byte) grande; // casting explícito: 44
long ancho = grande; // conversión automática300 en binario ocupa nueve bits; byte guarda ocho, así que se queda con los bits bajos y
el valor se convierte en 44. Con double a int la regla es otra: se trunca hacia cero.
System.out.println((int) 3.9); // 3
System.out.println((int) -3.9); // -3
System.out.println(Math.round(3.9)); // 4char es un caso aparte: es numérico, y promociona a int en cualquier cuenta.
char letra = 'a';
System.out.println(letra + 1); // 98, un int
System.out.println((char) (letra + 1)); // bAsignación compuesta: el cast que va escondido
x += 1 no es lo mismo que x = x + 1. La forma compuesta equivale a un cast al tipo de la
izquierda, y por eso compila donde la otra falla:
short contador = 0;
contador += 1; // bien: equivale a contador = (short) (contador + 1)
contador = contador + 1; // no compila: el resultado de la suma es intEs la razón de que byte b = 0; b++; funcione y b = b + 1 no. Útil para acumuladores
pequeños, peligroso cuando el cast silencioso tapa un desbordamiento.
Incremento: prefijo y sufijo no son lo mismo
int i = 0;
int a = i++; // a vale 0, i vale 1
int b = ++i; // b vale 2, i vale 2En una sentencia suelta da igual cuál uses. Dentro de una expresión, el sufijo devuelve el
valor viejo y el prefijo el nuevo, y ahí nacen los bugs de un solo carácter. Ni una de las
dos formas es atómica entre hilos: para concurrencia, AtomicInteger.
Comparar: == es identidad
Con primitivos, == compara valores. Con referencias, compara direcciones.
int x = 5, y = 5;
System.out.println(x == y); // true
String saludo = new String("hola");
System.out.println(saludo == "hola"); // false
System.out.println(saludo.equals("hola")); // trueEl capítulo de Strings entra en el pool de literales y en por qué "hola" == "hola" sí da
true. La regla práctica no cambia: == para saber si es el mismo objeto, equals para
saber si valen lo mismo.
Con coma flotante hay dos rarezas más, herencia de IEEE 754:
System.out.println(0.1 + 0.2 == 0.3); // false
System.out.println(Double.NaN == Double.NaN); // false
System.out.println(Double.compare(Double.NaN, Double.NaN)); // 0Ningún NaN es igual a sí mismo, ni siquiera consigo mismo, así que x == Double.NaN
siempre es false. Para comprobar si un double es NaN se usa Double.isNaN(x), y para
comparar números de coma flotante con orden total, Double.compare.
&& y || se saltan la mitad
&& y || son de cortocircuito: si el resultado ya se sabe con el operando izquierdo, el
derecho no se evalúa. & y | sobre booleanos evalúan siempre los dos.
String texto = null;
if (texto != null && texto.isEmpty()) { // seguro: isEmpty no se llama
System.out.println("vacío");
}Ese patrón es la guarda más común de Java. Con & en lugar de && sería un
NullPointerException garantizado.
Bits y desplazamientos
&, |, ^ y ~ operan bit a bit sobre enteros; con boolean hacen lo mismo pero sin
cortocircuito. Los desplazamientos mueven los bits:
int mascara = 0b1010;
System.out.println(mascara & 0b0010); // 2
System.out.println(mascara | 0b0001); // 11
System.out.println(mascara ^ 0b1111); // 5
System.out.println(1 << 10); // 1024
System.out.println(-1 >> 28); // -1, conserva el signo
System.out.println(-1 >>> 28); // 15, rellena con ceros>> es desplazamiento aritmético (copia el bit de signo), >>> es lógico (mete ceros).
La trampa: la distancia de desplazamiento se enmascara. Para int se usan solo los cinco
bits más bajos, y para long los seis, así que 1 << 32 no es cero ni un error: es 1,
porque la distancia efectiva es 32 & 0x1f, es decir 0.
System.out.println(1 << 32); // 1
System.out.println(1L << 64); // 1Ternario: una expresión con dos ramas
int edad = 20;
String tipo = edad >= 18 ? "adulto" : "menor";Exige que las dos ramas produzcan un tipo compatible. Y cuidado con las cajas: si una rama es
Integer y la otra int, el resultado es int y la caja se desboxa, con
NullPointerException si venía null.
Integer cantidad = null;
int total = cantidad != null ? cantidad : 0; // correcto, pero solo porque compruebas antes
int malo = true ? cantidad : 0; // NullPointerExceptionPara elegir entre dos valores es perfecto. Para tres o más, un switch o un if se lee
mejor: el ternario anidado es siempre peor de lo que parece.
instanceof con patrón (Java 16)
Desde Java 16, instanceof puede declarar la variable que ya sabe que es de ese tipo:
Object dato = "hola";
if (dato instanceof String texto && texto.length() > 3) {
System.out.println(texto.toUpperCase()); // HOLA
}Sin patrón harías un cast a mano justo después de la comprobación, que es donde se cuelan
los ClassCastException. El patrón es definitivo desde Java 21, y en 17 ya estaba.
Dos detalles: null instanceof String es false sin excepción, y el patrón solo funciona con
tipos referencia, nunca con primitivos.
Precedencia: el orden que sí importa
La tabla completa tiene una docena de niveles, pero en la práctica solo discuten cuatro.
| Nivel | Operadores | Ejemplo |
|---|---|---|
| Más fuerte | () . [] ++ -- postfijos | lista.get(0) |
| Unarios y cast | ! ~ ++ -- prefijos, (tipo) | (int) x |
| Multiplicativo | * / % | a * b |
| Aditivo | + - | a + b |
| Desplazamiento | << >> >>> | a << 2 |
| Relacional | < > <= >= instanceof | a < b |
| Igualdad | == != | a == b |
| AND, XOR, OR de bits | & ^ ` | ` |
| AND, OR lógicos | && ` | |
| Ternario | ?: | a ? b : c |
| Más débil | = += -= y el resto de asignaciones | a = b |
Los dos errores que la tabla evita: & tiene más precedencia que | (no es lo que la
mayoría supone) y && va por debajo de los operadores de bits, así que
a & b == 0 se lee como a & (b == 0). Ante la duda, paréntesis.
Qué te llevas
- La división entera trunca hacia cero y el resto lleva el signo del dividendo.
- El desbordamiento da la vuelta en silencio; usa
Math.addExactolong. - Bajar de tamaño exige cast, y la asignación compuesta lo esconde dentro.
==compara identidad: valores en primitivos, referencias en objetos.&&y||protegen delnull;&y|no.- La distancia de un desplazamiento se enmascara a 5 o 6 bits.
- El patrón de
instanceofelimina el cast manual posterior. - Ante precedencia dudosa, paréntesis: cuestan menos que un bug.