Tipos, variables y métodos

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
Esto no lanza ningún error: da la vuelta en silencio. Es la causa de una familia entera de fallos difíciles de encontrar, sobre todo al multiplicar milisegundos o bytes. Si el cálculo puede desbordar, está 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 €
La regla práctica sobre el dinero. Para calcular con dinero, 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:

  • duplicar no cambia nada: recibió una copia del entero.
  • duplicarTodo sí cambia el array: recibió una copia de la referencia, que apunta al mismo array.
  • reasignar no cambia nada: apuntó su copia de la referencia a otro objeto. El original ni se enteró.
  • modificar sí cambia: usó la referencia para tocar el objeto de verdad.
La frase exacta es: Java siempre pasa por valor. Lo que pasa es que, con los objetos, el valor que se copia es la referencia. Por eso puedes modificar el objeto que te pasan, pero no puedes sustituirlo desde dentro del método.

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.