Java tiene dos clases de tipos y se comportan distinto. Esta lección va de los ocho primitivos, de cuándo el compilador deja que pierdas datos sin avisar, y de qué ocurre exactamente cuando le pasas algo a un método: la respuesta no es la que parece.
Dos mundos: primitivos y objetos
En la lección anterior quedó la idea: los primitivos guardan el valor, todo lo demás guarda una referencia. Primitivos hay ocho, y se acabó; no se pueden crear más. Conviene saber qué aguanta cada uno:
IO.println("byte " + Byte.MIN_VALUE + " .. " + Byte.MAX_VALUE);
IO.println("int " + Integer.MIN_VALUE + " .. " + Integer.MAX_VALUE);
IO.println("long " + Long.MIN_VALUE + " .. " + Long.MAX_VALUE);
IO.println("double " + Double.MAX_VALUE);
salidabyte -128 .. 127 short -32768 .. 32767 int -2147483648 .. 2147483647 long -9223372036854775808 .. 9223372036854775807 float 3.4028235E38 double 1.7976931348623157E308 char 0 .. 65535 boolean true / false
En la práctica se usan cuatro: int para contar, long cuando el int se queda corto, double para decimales y boolean para condiciones. byte y short aparecen al manejar datos binarios; float, casi nunca.
El techo no avisa
int techo = Integer.MAX_VALUE; IO.println(techo + " + 1 = " + (techo + 1));
salida2147483647 + 1 = -2147483648
Math.addExact y sus hermanos, que sí avisan:
Math.addExact(techo, 1)java.lang.ArithmeticException: integer overflow
Dividir enteros no da decimales
IO.println("7 / 2 = " + (7 / 2));
IO.println("7 % 2 = " + (7 % 2));
IO.println("7 / 2.0 = " + (7 / 2.0));
IO.println("7.0 / 0 = " + (7.0 / 0));
salida7 / 2 = 3 7 % 2 = 1 7 / 2.0 = 3.5 7.0 / 0 = Infinity
Dos cosas que sorprenden. La primera: 7 / 2 da 3, no 3,5, porque los dos son enteros y el resultado también lo es. Basta con que uno sea decimal para que cambie todo. La segunda: dividir un double entre cero no revienta, devuelve Infinity. Dividir enteros entre cero sí:
7 / 0java.lang.ArithmeticException: / by zero
Decimales, dinero y cómo mostrarlos
IO.println("0.1 + 0.2 = " + (0.1 + 0.2));
IO.println("con BigDecimal = " + new BigDecimal("0.1").add(new BigDecimal("0.2")));
double total = 3 * 19.90;
IO.println("crudo = " + total);
IO.println("formateado = " + String.format("%.2f", total));
System.out.printf("printf = %,.2f €%n", 1234567.891);
salida0.1 + 0.2 = 0.30000000000000004 con BigDecimal = 0.3 crudo = 59.699999999999996 formateado = 59.70 printf = 1,234,567.89 €
BigDecimal y construido desde una cadena: new BigDecimal("0.1"). Si le pasas un double arrastras el error desde el principio. Para presentar un resultado por pantalla, String.format o printf bastan. Y %n no es un capricho: es el salto de línea del sistema, al contrario que \n.Convertir de un tipo a otro
int n = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
De texto a número se usan los métodos parseX. Son estrictos:
Integer.parseInt("42 ")java.lang.NumberFormatException: For input string: "42 "
Un espacio sobra. Por eso casi siempre se escribe Integer.parseInt(texto.trim()).
Entre números, la conversión se fuerza con un cast, y ahí hay dos trampas:
double precio = 19.99;
int entero = (int) precio; // trunca, no redondea
IO.println("(int) 19.99 = " + entero + " Math.round = " + Math.round(precio));
int grande = 300;
byte b = (byte) grande;
IO.println("(byte) 300 = " + b);
salida(int) 19.99 = 19 Math.round = 20 (byte) 300 = 44
El cast a int trunca: no redondea. Y el cast a un tipo más pequeño se queda con los bits que caben, de ahí ese 44 que no se parece a 300. El compilador te deja hacerlo porque lo has pedido explícitamente; es tu palabra de que sabes lo que haces.
var, y lo que no es
var mensaje = "el tipo se deduce y ya no cambia";
var cuantos = 3;
IO.println("mensaje es un " + mensaje.getClass().getSimpleName());
salidamensaje es un String
var no convierte Java en un lenguaje dinámico: el compilador deduce el tipo una vez, al declarar, y a partir de ahí es tan rígido como si lo hubieras escrito a mano. Solo vale para variables locales con valor inicial; no para campos, ni parámetros, ni valores de retorno.
| Úsalo cuando… | Evítalo cuando… |
|---|---|
var lista = new ArrayList<String>(); — el tipo ya está a la derecha |
var x = obtener(); — nadie sabe qué devuelve eso |
| El tipo es largo y se repite | El tipo concreto es la información importante |
Métodos
Un método es un bloque con nombre, parámetros y un tipo de retorno. void significa que no devuelve nada.
String describir(int n) { return "un int: " + n; }
String describir(double d) { return "un double: " + d; }
String describir(String s) { return "un String: " + s; }
String describir(int a, int b) { return "dos int: " + (a + b); }
salidaun int: 3 un double: 3.0 un String: tres dos int: 7
Cuatro métodos con el mismo nombre. Eso es sobrecarga, y el compilador elige según los tipos de los argumentos. Lo que no se puede es sobrecargar cambiando solo el tipo de retorno: por ahí no hay forma de distinguirlos.
Lo que de verdad pasa al pasar un argumento
Esta es la parte que más confusión genera, y se resuelve con un experimento.
void duplicar(int n) { n = n * 2; }
void duplicarTodo(int[] a) { for (int i = 0; i < a.length; i++) a[i] *= 2; }
void reasignar(StringBuilder s){ s = new StringBuilder("otra cosa"); }
void modificar(StringBuilder s){ s.append(" mundo"); }
salidaprimitivo tras duplicar(x): 5 array tras duplicarTodo: 2, 4, 6 tras reasignar(sb): hola tras modificar(sb): hola mundo
Tres resultados distintos que parecen contradictorios, y no lo son:
duplicarno cambia nada: recibió una copia del entero.duplicarTodosí cambia el array: recibió una copia de la referencia, que apunta al mismo array.reasignarno cambia nada: apuntó su copia de la referencia a otro objeto. El original ni se enteró.modificarsí cambia: usó la referencia para tocar el objeto de verdad.
final
final int MAXIMO = 100;
// MAXIMO = 200; -> error: cannot assign a value to final variable MAXIMO
final int[] datos = {1, 2, 3};
datos[0] = 99; // esto SÍ vale: cambia el contenido, no la referencia
final congela la variable, no el objeto. Con un primitivo equivale a una constante; con un objeto, solo impide que la variable apunte a otra cosa. El contenido sigue siendo modificable, como enseña el array de ahí arriba.
Lo esencial
| Si quieres… | Usa |
|---|---|
| Contar cosas | int; long si puede pasar de 2 100 millones |
| Decimales para mostrar | double + String.format("%.2f", x) |
| Decimales para dinero | BigDecimal, creado desde una cadena |
| Texto a número | Integer.parseInt(s.trim()) |
| Redondear | Math.round; el cast a int trunca |
| Detectar desbordamiento | Math.addExact, multiplyExact |
| Ahorrar escritura | var, solo si el tipo se ve a la derecha |
Todo el código de esta lección se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicarla; las salidas y los errores del compilador están copiados de esa ejecución.