Saltar a contenido

4. La clase Math

Campo Contenido
Unidad UD3, Programación Orientada a Objetos con la API de Java
RA RA2, Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos
CE cubiertos CE 2.5 (se han escrito llamadas a métodos estáticos) y CE 2.6 (se han utilizado parámetros en la llamada a métodos) de forma directa, junto con CE 2.2 (se han escrito programas simples); refuerzo de CE 2.7 (se han incorporado y utilizado librerías de objetos) y CE 2.9 (se ha utilizado el entorno integrado de desarrollo)

En esta sección aprenderás:

  • Por qué Math es la clase de utilidad por excelencia de Java: cero objetos, cero new, cero estado — y un constructor que nadie puede usar.
  • Sus constantes de clase: PI, E y la moderna TAU (Java 19), y cómo presentarlas con las plantillas de la sección anterior.
  • El repertorio de cálculo (sqrt, pow, cbrt, abs, hypot), de redondeo (round, ceil, floor), de elección (min, max, clamp) y de trigonometría (sin, cos, tan — en radianes, que no se te olvide).
  • Math.clamp() (Java 21): limitar un valor a un rango en una sola llamada, con sus cuatro sobrecargas.
  • La aritmética segura: por qué 2147483647 + 1 produce -2147483648 sin que nadie se entere, y cómo la familia addExact, multiplyExact y compañía lo convierten en una ArithmeticException capturable.
  • Math.random(): el azar de andar por casa, su receta de escalado a enteros y por qué la sección siguiente lo sustituye por la clase Random.

Necesitas tener fresco (de unidades y secciones anteriores):

  • De la UD1: los ocho tipos primitivos y sus rangos (int, long, double); el casting explícito (int); la división entera frente a la real y el %; las constantes y su convención MAYUSCULAS_CON_BARRAS_BAJAS; Math.sqrt() e Integer.parseInt() como primeras llamadas estáticas.
  • De la UD2: try-catch y el manejo de excepciones predefinidas (ArithmeticException entre ellas); los bucles y los acumuladores.
  • De la sección 1: miembro de instancia frente a miembro estático, y la analogía del gremio de taxistas.
  • De la sección 2: la regla del receptor (Clase.metodo() para lo estático); que Math vive en java.lang — no en java.math, trampa de examen —; y el circuito de IntelliJ Ctrl+Space → Ctrl+P → Ctrl+Q.
  • De la sección 3: formatted con %f, %.2f y %d para presentar números; repeat para cuadros de texto; y que los doubles se imprimen con punto decimal (la coma solo aparece al formatear con locale español).

4.1 La clase que no se puede instanciar

La sección anterior la cerramos prometiendo el polo opuesto de String, y conviene empezar midiéndolo bien. String era el reino de la instancia: ochenta y tantos métodos que se piden a una cadena concreta, porque la respuesta depende de su contenido. Math es exactamente lo contrario: no declara ni un solo miembro de instancia. Todo lo suyo — noventa y nueve métodos públicos contando las sobrecargas, más tres constantes — es estático, se pide a la clase como receptor y su resultado depende únicamente de los argumentos que le entregues. La regla del receptor de la sección 2, aplicada a esta clase, no admite ni una excepción: siempre Math.loQueSea(...).

Vuelve a la analogía del gremio de taxistas de la sección 1 y llévala al extremo. String era un gremio con muchos coches: cada taxista (objeto) con su taxímetro (estado) y consultas que dependían de él. Math es un gremio sin coches: solo el listado de tarifas pegado en la pared del local. Nadie te va a dar una calculadora personal que puedas estropear; usas la del mostrador, que no recuerda nada de ti entre consulta y consulta. Por eso en esta sección no verás ni un new (salvo uno que vamos a intentar ejecutar para ver cómo fracasa) ni una variable guardando un objeto de esta clase: no existen tales objetos.

(Y una letra pequeña de rigor, porque la precisión también enseña: como toda clase de Java, Math hereda de Object nueve miembros de instancia universales — de equals a toString —. Sin ejemplares que los reciban, esa herencia es aquí letra muerta, y la Javadoc la aparta en su propia lista final, «Methods declared in class java.lang.Object». Esos nueve sí los abriremos de verdad en la UD4, cuando escribas tus propias clases y los sobrescribas.)

¿Y qué pasa si lo intentas? Escribe esto en IntelliJ y mira qué dice el compilador:

1
2
3
void main() {
    Math calculadora = new Math();   // intento de fabricar una Math propia
}

El compilador lo rechaza con un mensaje que ya sabes leer:

error: Math() has private access in Math

El constructor de Math es privado. private, el modificador de acceso de la UD4: nadie fuera de la propia clase puede llamarlo, así que ningún new Math() puede existir jamás. Es una decisión de diseño deliberada y muy fácil de entender una vez conoces la clase: como Math no tiene estado — nada que recordar entre llamadas —, fabricar objetos no aportaría nada; cada ejemplar sería idéntico al siguiente y consumiría memoria para nada. Cerrando el constructor, los diseñadores del API gritan su intención: esto es una caja de herramientas colgada en la pared, no una fábrica de herramientas. A las clases así — todo estático, constructor sellado, cero instancia — se las llama clases de utilidad (utility class), y Math es su ejemplo canónico.

El otro sello: final

Abre la Javadoc de Math (la conoces ya: Ctrl+Q sobre la palabra Math, o docs.oracle.com → Java 25 → java.lang.Math) y mira su declaración: public final class Math. Ese final de clase, el mismo que lucía String, significa que nadie puede heredar de ella. Sin herencia posible, sin instancias posibles y sin estado, la clase queda congelada: una pared de herramientas que nadie puede clonar ni decorar. ¿Recuerdas por qué String era final? Aquí la razón es la misma aplicada a lo colectivo: el gremio no admite filiales.

Un repaso de posición antes de entrar en materia, porque esta sección vuelve a moverte por el mapa del API que dibujó la sección 2. Math vive en java.lang, el paquete que se importa solo: por eso ningún programa de hoy lleva import, igual que ocurría con String. Y resiste la tentación del despiste clásico de examen que ya desenmascarlamos: Math no vive en java.math; ese paquete es el barrio de BigInteger y BigDecimal, la aritmética de precisión ilimitada. Hoy solo nos asomaremos a ese barrio una vez, al final, cuando el long se nos quede pequeño.

Para no perdernos entre noventa y nueve métodos — sí, has leído bien: la Javadoc de Math lista casi un centenar contando las sobrecargas, y hoy recorreremos en profundidad la treintena que cubre el 95 % del trabajo real de un programa normal —, vamos a organizarlos como hicimos con String: por verbos, que es como tu cabeza los buscará cuando programes. Este es el mapa del territorio de hoy:

Use mouse to pan and zoom
flowchart TD
    M["El repertorio de Math\nque veras hoy"]
    A["MEDIR\nconstantes de la pared\nPI · E · TAU"]
    B["CALCULAR\nraices, potencias y distancias\nsqrt · pow · cbrt · abs · hypot"]
    C["REDONDEAR\nde double a entero con criterio\nround · ceil · floor"]
    D["ELEGIR\nlimitar y escoger\nmin · max · clamp"]
    E["GIRAR\ntrigonometria en radianes\nsin · cos · tan · toRadians · toDegrees"]
    F["PROTEGER\naritmetica que avisa del desbordamiento\naddExact · multiplyExact · incrementExact ..."]
    G["SORTEAR\nel azar de bolsillo\nrandom"]
    M --> A
    M --> B
    M --> C
    M --> D
    M --> E
    M --> F
    M --> G
    style M fill:#2e7d32,color:#ffffff,stroke:#1b5e20
    style A fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style B fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style C fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style D fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style E fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style F fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style G fill:#c8e6c9,color:#000000,stroke:#2e7d32
Una hermana gemela: StrictMath

Si paseas por la Javadoc de java.lang, junto a Math vive StrictMath: la misma pared de herramientas, pero con una promesa extra — sus resultados son idénticos bit a bit en cualquier máquina y sistema operativo. Math, en cambio, permite que el procesador de cada ordenador use atajos más rápidos, con la consecuencia de que el último dígito de un cálculo puede variar entre dos equipos. Para tus programas da absolutamente igual (un seno con dieciséis cifras es igual de correcto con la última variando), pero hay dominios — finanzas, criptografía, cálculo científico que se debe reproducir al detalle — donde esa garantía importa, y ahí se usa StrictMath. Hoy la dejamos citada; en el resto del módulo no volveremos a cruzarnos con ella.

Con el mapa en la mano, empecemos por la pared donde cuelgan las herramientas más antiguas y más veneradas del gremio: sus constantes.

4.2 Las constantes de la pared: PI, E y TAU

Las conociste de pasada en la sección 2, cuando estudiamos los campos estáticos, y hoy las abrimos del todo. Math expone exactamente tres constantes públicas — ni una más, puedes contarlas en la Javadoc —, y las tres son double:

Constante Valor exacto que imprime Java Qué representa Desde
Math.PI 3.141592653589793 π, la relación entre la circunferencia de un círculo y su diámetro Java 1.0
Math.E 2.718281828459045 e, la base del logaritmo natural, la constante del crecimiento continuo Java 1.0
Math.TAU 6.283185307179586 τ (tau), una vuelta completa: el cociente entre circunferencia y radio, o lo que es lo mismo, 2π Java 19

Antes de nada, la lectura formal con las gafas de las secciones anteriores: son campos estáticos y finales de la clase — se leen con Math como receptor, sin new ninguno, y hay una única copia compartida por todo el programa. Sus nombres van en MAYUSCULAS_CON_BARRAS_BAJAS, la convención de constantes que fijamos en la UD1: una constante así nombrada le dice a quien lee el código «esto no cambia, no lo busques por ningún sitio porque no se mueve».

La novedad de la tabla es TAU, la más joven de las tres. Si π es media vuelta (180°), τ es la vuelta completa (360°): por eso, cuando un cálculo habla de círculos completos — ángulos que dan vueltas, fases de una onda que se repiten —, escribir TAU en vez de 2 * PI dice exactamente lo que significa. Los puristas de las matemáticas llevan años defendiendo que τ es la constante más natural; Java le dio casa en la versión 19 y ahí sigue en Java 25. Para tus programas de hoy, π sigue siendo la estrella; τ es la constante que te alegra el día cuando el problema es circular de verdad.

1
2
3
4
5
6
7
8
9
void main() {
    // Las tres constantes, tal cual salen de la pared del taller
    IO.println(Math.PI);    // 3.141592653589793
    IO.println(Math.E);     // 2.718281828459045
    IO.println(Math.TAU);   // 6.283185307179586

    // Y su parentesco: tau es exactamente dos pi (mismo double)
    IO.println(2 * Math.PI == Math.TAU);   // true
}

Fíjate en esos dieciséis dígitos y en lo que significan: un double guarda unos 15-17 dígitos significativos en su representación interna de 64 bits, así que el π de Java no es π (un número infinito e irracional), sino su mejor recorte a 64 bits. Para cualquier cálculo de este mundo, dieciséis cifras bastan y sobran; pero conviene saber que están ahí recortadas, porque más adelante hoy veremos ese recorte asomar por los bordes.

Y una regla de presentación que ya conoces de la sección 3, ahora en su vertiente numérica: el IO.println crudo imprime siempre con punto decimal — así imprime Java cualquier double —, pero si el número va a salir en un informe para personas, lo suyo es formatted con %f:

1
2
3
4
5
6
7
8
9
void main() {
    double radio = 2.5;

    // Area del circulo: la formula clasica de la escuela
    double area = Math.PI * radio * radio;

    IO.println(area);                                              // 19.634954084936208
    IO.println("Area con dos decimales: %.2f".formatted(area));    // Area con dos decimales: 19,63
}

La misma operación, dos presentaciones: la cruda para depurar, la formateada para enseñar. Y si tu máquina tiene locale español, el %.2f pinta la coma decimal — 19,63 — exactamente como ya te ocurrió con la fortaleza del protocolo de registro de la sección 3. (¿El porqué? El mismo de entonces: %f respeta la configuración regional; el println crudo, no.)

Cierro la pared de constantes con el uso más realista que le vas a dar esta semana, porque combina constante, método de instancia y plantilla en cuatro líneas — un anticipo del tono de toda la sección:

void main() {
    // El grafico circular de una encuesta: tres respuestas, una vuelta completa
    double aFavor = 7;
    double enContra = 2;
    double sinDecidir = 1;
    double total = aFavor + enContra + sinDecidir;

    // Cada respuesta es una fraccion de la vuelta: porcentaje y angulo en radianes
    IO.println("A favor:     %.1f%%  (%.4f rad)".formatted(aFavor / total * 100,
                                                           aFavor / total * Math.TAU));
    IO.println("En contra:   %.1f%%  (%.4f rad)".formatted(enContra / total * 100,
                                                           enContra / total * Math.TAU));
    IO.println("Sin decidir: %.1f%%  (%.4f rad)".formatted(sinDecidir / total * 100,
                                                           sinDecidir / total * Math.TAU));
}
1
2
3
A favor:     70,0%  (4,3982 rad)
En contra:   20,0%  (1,2566 rad)
Sin decidir: 10,0%  (0,6283 rad)

(El %% para pintar el símbolo de porcentaje ya lo desactivamos como trampa en la sección 3; aquí solo confirma que sigue de servicio. Y fíjate de paso: el 10 % de la vuelta son 0,6283 radianes — exactamente TAU / 10, como debe ser.) Las constantes están listas. Pasamos a la primera estantería de herramientas de verdad: las que calculan.

4.3 El repertorio de cálculo: sqrt, pow, cbrt, hypot y abs

Estas cinco son el pan de cada día numérico, y todas comparten una firma mental que conviene fijar ya: entra un double, sale un double (el abs es la excepción: tiene versiones para cada tipo). Nada de enteros: aunque pidas sqrt(144) y la respuesta sea «12», Java te la entrega como 12.0, porque la raíz de un número podría tener decimales y el tipo de retorno es uno solo. Ya verás que eso tiene consecuencias.

sqrt: la raíz cuadrada, y el país de NaN

1
2
3
4
5
void main() {
    IO.println(Math.sqrt(2));     // 1.4142135623730951: la raiz de 2, recortada a 64 bits
    IO.println(Math.sqrt(144));   // 12.0: entera... pero double (observa el .0)
    IO.println(Math.sqrt(-1));    // NaN
}

La tercera línea es la que merece una pausa, porque es la primera gran diferencia de filosofía entre el mundo de los enteros y el mundo de los reales que cruzas en este curso. La raíz de un número negativo no existe — de los reales, que es el mundo del double —, y sin embargo el programa no revienta: imprime NaN y sigue su camino como si nada. NaN significa Not a Number y es el valor especial que el estándar de coma flotante (IEEE 754, el mismo que rige los double desde 1985) reserva para «esta operación no tiene respuesta». No es un error, no es una excepción, no es cero: es una respuesta que dice «imposible».

Y aquí viene lo contraintuitivo, que te lo adelanto porque es pregunta de examen y trampa de depuración en igual medida:

NaN no es igual a nada, ni siquiera a sí mismo. La expresión NaN == NaN devuelve false. Para detectarlo existe Double.isNaN(valor) — un adelanto de la sección 8, donde abriremos los wrappers a fondo.

1
2
3
4
5
6
void main() {
    double imposible = Math.sqrt(-4);

    IO.println(imposible == Double.NaN);     // false: jamas lo compares con ==
    IO.println(Double.isNaN(imposible));     // true: la forma correcta de preguntar
}

NaN tiene hermanos de show: Infinity y -Infinity, que aparecen cuando el cálculo se pasa de largo en vez de resultar imposible. Y aquí es donde conviene poner la tabla comparativa que desactiva la confusión clásica entre el comportamiento de enteros y de reales, porque acabas de ver en la UD2 que la división entera por cero sí revienta:

Expresión Resultado ¿El programa se detiene?
1 / 0 (enteros) ArithmeticException: / by zero Sí — excepción, la conociste en la UD2
1.0 / 0.0 Infinity No — sigue ejecutando
-1.0 / 0.0 -Infinity No
0.0 / 0.0 NaN No
Math.sqrt(-1) NaN No

Dos filosofías enfrentadas: los enteros de Java fallan ruidosamente cuando su aritmética no tiene sentido (división por cero: excepción; desbordamiento: ya veremos que es la gran excepción a este piano); los reales prefieren seguir corriendo y devolver valores especiales. Ninguna es «mejor»: la primera protege al distraído, la segunda evita que una simulación con millones de cálculos muera por un dato sucio. Tu trabajo es saber en cuál de los dos mundos estás parado en cada línea.

pow: potencias, con un as bajo la manga

Math.pow(base, exponente) eleva y devuelve siempre double. Dos argumentos, recuerda la lectura de firmas de la sección 2: potencia de la calculadora, con exponente negativo y fraccionario incluidos:

1
2
3
4
5
6
7
8
void main() {
    IO.println(Math.pow(2, 10));    // 1024.0: dos elevado a diez
    IO.println(Math.pow(2, -1));    // 0.5: exponente negativo = 1 / base
    IO.println(Math.pow(9, 0.5));   // 3.0: exponente 0.5 es la raiz cuadrada disfrazada
    IO.println(Math.pow(-8, 1 / 3));      // 1.0 (!): sigue leyendo
    IO.println(Math.pow(-8, 1.0 / 3));    // NaN: potencia de base negativa con exponente fraccionario
    IO.println(Math.cbrt(-8));      // -2.0: para raices cubicas, la herramienta dedicada
}

Las dos líneas del medio esconden dos trampas distintas, y las dos son de las que hacen perder tardes enteras:

  • La primera es la división entera de la UD1. 1 / 3 se calcula con dos int, así que vale 0 — no 0.333... — y la llamada se convierte en Math.pow(-8, 0), que es 1.0 para cualquier base. El programa compila, ejecuta y devuelve una respuesta plausible y falsa: la peor clase de error. La cura la aprendiste entonces: escribir 1.0 / 3 para forzar división real.
  • La segunda es una regla de pow. Base negativa con exponente fraccionario no tiene una única respuesta real (la raíz cúbica de -8 existe y es -2, pero las raíces pares de negativos no existen), así que pow se lava las manos y devuelve NaN. Si tu raíz es cúbica, usa la herramienta específica: Math.cbrt(-8) devuelve -2.0 sin drama. Y a diferencia de lo que hará abs con su trampa de rango (ahí sí existe una variante absExact que avisa), la raíz de un negativo no tiene versión vigilada: NaN es la respuesta prevista, y Double.isNaN el detector.

hypot: el teorema de Pitágoras, de una pieza

Adivina qué calcula hypot(x, y): exacto, la hipotenusa. Es sqrt(x² + y²) empaquetado, con una ventaja técnica que la Javadoc le atribuye y que algún día te ahorrará un disgusto: evita el desbordamiento intermedio (elevar al cuadrado números grandes puede desbordar el double antes de que la raíz lo devuelva a valores razonables). Hoy lo usamos porque hace legible el cálculo más clásico de la geometría:

void main() {
    // Una escalera apoyada en una pared: distancias horizontales y verticales medidas
    double catetoVertical = 1.5;
    double catetoHorizontal = 3.5;

    // Version artesanal: sqrt de la suma de cuadrados
    double artesanal = Math.sqrt(catetoVertical * catetoVertical
                               + catetoHorizontal * catetoHorizontal);

    // Version de gremio: hypot
    double deGremio = Math.hypot(catetoVertical, catetoHorizontal);

    IO.println(artesanal);   // 3.8078865529319543
    IO.println(deGremio);    // 3.8078865529319543: identica
}

Guárdate hypot para el día en que escribas distancias — distancia entre dos puntos, tamaño de un vector, radio desde coordenadas —: el patrón «distancia entre dos posiciones» volverá en la práctica guiada de hoy y en cualquier juego o mapa que programes.

abs: el valor absoluto... y su trampa histórica

Math.abs(x) devuelve x sin signo, y es el método más sobrecargado de la clase: cuatro versiones — int, long, float y double — que eligen solas según el tipo de su argumento (la sobrecarga, vista en la sección 2, trabajando para ti). Su uso normal no necesita presentación: distancias, desviaciones, «cuánto se aleja un valor de otro».

void main() {
    IO.println(Math.abs(-5));     // 5
    IO.println(Math.abs(-2.5));   // 2.5
    IO.println(Math.abs(7));      // 7: los positivos cruzan sin cambiar

    // La desviacion de una medida respecto a su objetivo
    double medida = 99.3;
    double objetivo = 100.0;
    IO.println(Math.abs(objetivo - medida));   // 0.7000000000000028
}

(Los 0.000... de la cola no son un error de abs: son el recorte de 64 bits de la sección anterior asomando de nuevo — la resta 100.0 - 99.3 ya nace así. Con %.2f en el informe se presenta como 0,70 y todo el mundo feliz.)

Y ahora, la trampa. Una sola, pero histórica: la firmó el propio rango asimétrico de int. Ejecuta esto:

1
2
3
4
void main() {
    IO.println(Math.abs(-2147483647));            // 2147483647: perfecto
    IO.println(Math.abs(Integer.MIN_VALUE));      // -2147483648: NEGATIVO
}

La segunda línea imprime un número negativo. El valor absoluto de Integer.MIN_VALUE... ¡es negativo! La explicación es pura aritmética de rangos: int va de -2147483648 a 2147483647, un rango asimétrico (hay un negativo más que positivos, porque el cero ocupa plaza). El «valor absoluto» de -2147483648 sería +2147483648... que no cabe en un int. Sin sitio donde ponerlo, Java hace lo único que puede con 32 bits: devolver el mismo número, signo incluido. ¿La cura? La misma filosofía de la familia Exact que abriremos en la subsección 4.8: existe Math.absExact(...), que en ese caso extremo lanza una ArithmeticException en vez de callarse. Para valores «normales» — los que comparas, mides o depuras —, abs es de fiar; solo recuerda que el extremo inferior del rango es una cueva con trampa.

El mismo desenlace en Long, y por que te lo cuento hoy

¿Recuerdas que Integer.MAX_VALUE (2147483647) y Integer.MIN_VALUE (-2147483648) te los presenté en la sección 2 como adelanto de la sección 8, donde abriremos los wrappers a fondo? Son campos estáticos de la clase Integer — int, pero empaquetado en clase — y los estamos usando como mapa del terreno: los límites del país de los enteros de 32 bits. Su primo Long.MIN_VALUE (-9223372036854775808) reproduce la misma trampa con abs, y por la misma razón: el rango de long también es asimétrico. Guárdate el patrón: el extremo negativo de un rango entero siempre es una cueva. Volveremos a necesitar esta idea dos subsecciones más abajo, cuando el desbordamiento entre en escena.

La tabla de la estantería de cálculo, para tu cuaderno:

Método Qué hace Ojo con
sqrt(x) Raíz cuadrada, double → double Negativos devuelven NaN, no excepción
pow(b, e) Potencia, siempre double 1 / 3 entero vale 0; base negativa con exponente fraccionario → NaN
cbrt(x) Raíz cúbica — admite negativos -2.0 para cbrt(-8), sin drama
hypot(x, y) sqrt(x² + y²) a prueba de desbordamientos La herramienta natural de distancias
abs(x) Valor absoluto (4 sobrecargas) abs(Integer.MIN_VALUE) devuelve negativo; absExact para el caso extremo

4.4 Redondear con criterio: round, ceil y floor

Aquí llega el bloque con más letra pequeña proporcional de todo el API — tres métodos diminutos, tres personalidades distintas — y también el que más sorpresas de examen esconde. El problema de fondo ya lo conoces de la UD1: un double lleva decimales y un int no, y el cast (int) los elimina truncando hacia el cero. Pero «cortar por lo que caiga» no siempre es el redondeo que un problema necesita, y de ahí la familia.

Las tres personalidades

  • round(x) — el redondeo de toda la vida: el entero más cercano, y si hay empate exacto a medias (un .5), hacia arriba — hacia el +∞, matiz importante con negativos.
  • ceil(x) — el techo: el entero más pequeño que es mayor o igual que x. Redondea hacia arriba, siempre, aunque el decimal sea ridículo.
  • floor(x) — el suelo: el entero más grande que es menor o igual que x. Redondea hacia abajo, siempre.
void main() {
    // round: al mas cercano; los empates, hacia arriba
    IO.println(Math.round(2.4));    // 2
    IO.println(Math.round(2.5));    // 3   (empate: gana "arriba")
    IO.println(Math.round(98.6));   // 99
    IO.println(Math.round(-2.5));   // -2  (empate: gana "arriba" = hacia el +infinito)
    IO.println(Math.round(-2.6));   // -3

    // ceil: el techo — sube siempre
    IO.println(Math.ceil(2.1));     // 3.0
    IO.println(Math.ceil(2.0));     // 2.0  (ya es entera: ni se inmuta)
    IO.println(Math.ceil(-2.1));    // -2.0 (subir, para un negativo, es acercarse a cero)

    // floor: el suelo — baja siempre
    IO.println(Math.floor(2.9));    // 2.0
    IO.println(Math.floor(-2.1));   // -3.0 (bajar, para un negativo, es alejarse de cero)
}

Ese Math.round(-2.5) que da -2 merece subrayado doble, porque es la trampa estrella: «los empates hacia arriba» no significa «hacia el número mayor» (-3), sino hacia el infinito positivo (-2). Si tu mente visualiza mejor con la recta numérica, aquí está la escena completa para un positivo y un negativo:

Use mouse to pan and zoom
flowchart TD
    V1["valor = 2.4"] --> C1["(int) 2.4  →  2\nhacia el CERO: trunca"]
    V1 --> R1["round(2.4)  →  2\nal MAS CERCANO"]
    V1 --> U1["ceil(2.4)  →  3.0\nal TECHO: siempre arriba"]
    V1 --> F1["floor(2.4)  →  2.0\nal SUELO: siempre abajo"]
    V2["valor = -2.1"] --> C2["(int) -2.1  →  -2\nhacia el CERO: trunca"]
    V2 --> R2["round(-2.1)  →  -2\nal MAS CERCANO"]
    V2 --> U2["ceil(-2.1)  →  -2.0\nal TECHO: arriba = acercarse a cero"]
    V2 --> F2["floor(-2.1)  →  -3.0\nal SUELO: abajo = alejarse de cero"]
    style V1 fill:#e3f2fd,color:#000000,stroke:#1565c0
    style V2 fill:#e3f2fd,color:#000000,stroke:#1565c0
    style C1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style C2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style R1 fill:#fff3e0,color:#000000,stroke:#e65100
    style R2 fill:#fff3e0,color:#000000,stroke:#e65100
    style U1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style U2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style F1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style F2 fill:#c8e6c9,color:#000000,stroke:#2e7d32

Y la tabla de consulta rápida, con el cast como columna de control — compárala con la salida real del programa cuando la escribas:

Valor (int) trunca round ceil floor
2.9 2 3 3.0 2.0
2.5 2 3 (empate arriba) 3.0 2.0
2.4 2 2 3.0 2.0
-2.1 -2 -2 -2.0 -3.0
-2.5 -2 -2 (empate hacia +∞) -2.0 -3.0
7.25 7 7 8.0 7.0
7.75 7 8 8.0 7.0

Dos detalles de firma que pillan a todos

Primero: ceil y floor devuelven double — mira la tabla: 3.0, no 3 — aunque su valor sea matemáticamente entero. La lógica es la misma de sqrt: entran y salen reales. Si necesitas el resultado como int, cast explícito: (int) Math.ceil(2.1). Y como el resultado de ceil ya es entero matemáticamente, el cast es seguro (no pierde nada).

Segundo, y este es el subrayado de examen: round aplicado a un double devuelve long, no int. La pareja de firmas es deliberada: round(float) devuelve int y round(double) devuelve long — al receptor de más bits le toca el retorno de más bits. (Y si el valor se sale del retorno, round no revienta: recorta al extremo del tipo — un round de un float gigante devuelve Integer.MAX_VALUE, lo documenta la Javadoc en sus special cases.) La consecuencia práctica la verás en cuanto escribas esto:

1
2
3
void main() {
    int nota = Math.round(6.5);   // pretendemos guardarlo en un int
}
error: incompatible types: possible lossy conversion from long to int

El compilador se niega: le estás metiendo un long en un int sin permiso — un narrowing de los de la UD1. La solución es la pareja que verás en todo código Java del mundo real:

1
2
3
4
void main() {
    int nota = (int) Math.round(6.5);   // 7: cast explicito, y a correr
    IO.println(nota);
}

Regla práctica de la subsección: (int) trunca hacia el cero; round va al más cercano (empates hacia +∞) y devuelve long; ceil y floor devuelven double aunque el resultado sea entero. Antes de asignar cualquiera de ellos a un int, piensa qué cast te toca poner.

Para qué elige cada uno un profesional

Cada personalidad tiene sus problemas por defecto — los que le caen solos en cuanto un programa los necesita — y saber emparejarlos es media destreza:

  • round: presentar. Notas medias, importes con céntimos, mediciones redondeadas al usuario. La media 7.5 de un expediente se presenta como 8 con round — o como 7 con floor, si la norma académica del centro favorece el recorte; el punto es que la elección de método es una decisión de negocio, no técnica.
  • ceil: dimensionar. «¿Cuántas cajas de 4 unidades necesito para 10 unidades?» — 10 / 4 es 2 (división entera), 10 / 4.0 es 2.5, y con ceil sale 3.0: tres cajas, porque media caja no existe. Páginas de un listado, autobuses para un grupo, baldosas para un pasillo: siempre ceil cuando el sobrante obliga a una unidad completa más.
  • floor: truncar por abajo con criterio. «Años cumplidos» a partir de la edad con decimales (a los 7.9 años sigues teniendo 7 cumplidos), o el índice de una casilla en una cuadrícula: floor reparte sin dejar que el borde se salte a la siguiente.
1
2
3
4
5
6
7
8
void main() {
    int unidades = 10;
    int porCaja = 4;

    // Cuantas cajas necesito: la division entera se queda corta
    IO.println(unidades / porCaja);                    // 2: dos cajas... y 2 unidades sueltas
    IO.println(Math.ceil(unidades / (double) porCaja)); // 3.0: las cajas que HACEN FALTA
}
Los primos enteros: floorDiv y floorMod

La filosofía de floor también existe para la división entera, y resuelve una rareza que quizá ya te cruzaste en la UD1: con negativos, / y % de Java redondean hacia el cero, no hacia abajo. Mira: -7 / 2 es -3 (y no -4) y -7 % 2 es -1 (y no 1). Para casi todo da igual... salvo que tu problema sea cíclico: repartir posiciones en un círculo, índices que dan la vuelta, el «hueco anterior» en una agenda. Ahí Java ofrece Math.floorDiv(-7, 2) → -4 y Math.floorMod(-7, 2) → 1, la pareja coherente con floor que garantiza que el resto salga siempre entre 0 y divisor - 1. Guárdalas para el día en que programes algo circular; y si te preguntas por un roundDiv... no existe: para redondear divisiones enteras se suman las mitades a mano o se pasa por double.

Y el redondeo del banquero: rint

En la Javadoc, junto a round, vive rint: igual que round va al entero más cercano, pero los empates los resuelve al par — rint(2.5) es 2.0 y rint(3.5) es 4.0. Es el redondeo del banquero (mitigar el sesgo acumulado de redondear siempre arriba) y te lo cuento solo para que no te pille desprevenido en una lectura de Javadoc; en este curso no lo usaremos.

4.5 min y max: las decisiones de una línea

Si un concurso de «método más simple y más útil» existiera, lo ganarían estos dos. Math.min(a, b) y Math.max(a, b) devuelven el menor y el mayor de sus dos argumentos, y cada uno viene en las cuatro versiones de siempre — int, long, float, double —, así que la sobrecarga elige sola. Ya los has visto trabajar: Math.max(3, 9) protagonizó las demos de estático de las secciones 1 y 2. Lo que cambia hoy es preguntarse qué reemplazan.

void main() {
    int puntuacionA = 7;
    int puntuacionB = 9;

    // La version larga, con if: cuatro lineas para una decision
    int mejor;
    if (puntuacionA > puntuacionB) {
        mejor = puntuacionA;
    } else {
        mejor = puntuacionB;
    }

    // La version de gremio: la misma decision en una linea
    int mejorDelGremio = Math.max(puntuacionA, puntuacionB);

    IO.println(mejor == mejorDelGremio);   // true
}

No es solo estética. Un if de selección de máximo obliga a leer cuatro líneas para descubrir que hace algo de una línea; Math.max declara su intención en el propio nombre de la expresión. El lector ve Math.max(notas[i], mejorHastaAhora) y ya sabe, sin ejecutar nada mentalmente, qué papel juega cada valor. Por eso en código profesional estos dos métodos no se usan «cuando no queda otra», sino siempre que el problema sea escoger.

Dos matices de factura que te servirán en la práctica de hoy:

  • Mezclar tipos amplía el resultado. Math.max(3.5, 3) compila y devuelve un double (3.5): el int se amplía para encajar en la versión double del método — el widening de la UD1 decidiendo la sobrecarga por ti.
  • Se anidan con elegancia para tres o más. El máximo de tres valores es el máximo de dos pasos: Math.max(a, Math.max(b, c)). (Con cuatro ya empieza a leerse mal; para muchas cifras, la solución profesional es un bucle con acumulador — lo haremos en la práctica — o un stream, que llegarán en la UD5.)
void main() {
    // El panel de un sensor: tres lecturas, el informe quiere la peor y la mejor
    int[] lecturas = { 23, 29, 25 };

    int minima = lecturas[0];
    int maxima = lecturas[0];
    for (int i = 1; i < lecturas.length; i++) {
        minima = Math.min(minima, lecturas[i]);
        maxima = Math.max(maxima, lecturas[i]);
    }

    IO.println("Minima: " + minima);   // 23
    IO.println("Maxima: " + maxima);   // 29
}

Ese bucle con dos acumuladores es un patrón de por vida: recorrer y quedarse con los extremos. Fíjate en la inicialización: ambos acumuladores arrancan en el primer dato, no en cero — si minima arrancara en 0, un sensor que solo da temperaturas positivas reportaría una mínima de 0 que ningún sensor midió. (Error clásico de primer año; el patrón «inicializa con el primer elemento» lo desactiva para siempre.)

4.6 clamp: el guardián del rango (Java 21)

Y ahora, el flamante — la palabra que prometí en el cierre de la sección 3. Presente en Java desde la versión 21 y fichado en el mapa de características modernas del roadmap del módulo, Math.clamp(valor, min, max) resuelve uno de los problemas más recurrentes de la programación: mantener un número dentro de un rango legal.

El escenario siempre suena igual: una vida que no puede pasar de 100 ni bajar de 0; un volumen entre 0 y 100; un brillo entre 0 y 255; un porcentaje entre 0 y 1; un ángulo entre 0 y 360; una nota entre 0 y 10. El dato entra por cualquier vía — cálculo, usuario, red — y puede llegar sucio: por encima, por debajo o justo en el borde. La receta de clamp es de una simetría hermosa:

  • si valor < min → devuelve min
  • si valor > max → devuelve max
  • si está dentro → devuelve el valor tal cual, sin tocarlo
1
2
3
4
5
6
void main() {
    // El volumen de un reproductor: rango legal 0 a 100
    IO.println(Math.clamp(150, 0, 100));   // 100: por arriba, se queda en el borde
    IO.println(Math.clamp(-7, 0, 100));    // 0: por abajo, igual
    IO.println(Math.clamp(50, 0, 100));    // 50: dentro: ni lo toca
}

La alternativa «de antes» — es decir, la que escribieron los programadores durante los casi treinta años previos a Java 21 — combina min y max anidados en el intrincado «emparedado» que verás en código heredado:

void main() {
    int valor = 150;
    int min = 0;
    int max = 100;

    // El idiom clasico pre-Java-21: max de abajo, min de fuera
    int emparedado = Math.max(min, Math.min(valor, max));   // 100

    // Desde Java 21: lo mismo, legible y a la primera
    int moderno = Math.clamp(valor, min, max);              // 100

    IO.println(emparedado == moderno);   // true
}

(Comprueba el emparedado despacio y verás por qué funciona: el Math.min(valor, max) recorta por arriba y el Math.max(min, ...) recorta por abajo. Y luego dale gracias a Java 21 por haberte ahorrado el rompecabezas mental.)

Las cuatro sobrecargas, y la sorpresa de la versión int

Como toda la familia numérica, clamp llega en cuatro versiones — y esta vez hay una con un diseño digno de mirada despacio:

Firma Devuelve Comentario
clamp(long valor, int min, int max) int Acepta un long y lo encierra en un rango de int
clamp(long valor, long min, long max) long La versión grande
clamp(double valor, double min, double max) double La que usarás a diario
clamp(float valor, float min, float max) float La hermana menor

La primera es la sorpresa: no existe una versión con tres int — la firma recibe el valor como long a propósito, para que puedas encajar un número potencialmente enorme en un rango de int sin cast previo. ¿Un contador de megabytes que debe acabar en una escala 0-100? Math.clamp(3_000_000_000L, 0, 100) devuelve 100 limpiamente:

void main() {
    long bytes = 3_000_000_000L;   // tres gigabytes: no cabe en un int

    // El porcentaje de una cuota de 4 GB... calculado en long y presentado en int
    int porcentaje = Math.clamp(bytes * 100 / 4_000_000_000L, 0, 100);
    IO.println(porcentaje);        // 75

    // Y el guardian actuando solo cuando hace falta
    IO.println(Math.clamp(bytes, 0, 100));   // 100: el long gigante, recortado a int
}

Espera — ¿has visto lo que acaba de pasar en la segunda línea? El porcentaje ha calculado 3_000_000_000 * 100 / 4_000_000_000 en aritmética de long (porque sus operandos son long), y ese resultado — 75, perfectamente representable — ha entrado directamente en un int gracias a que clamp acepta long de entrada. Sin cast, sin peligro de desbordamiento en el camino. Ese es el nivel de detalle de diseño que esconde una firma aparentemente caprichosa; los diseñadores del API no eligen los tipos al azar.

Los límites, en orden... o enfadados

clamp confía en ti para una cosa: que min sea menor o igual que max. Si los cruzas — Math.clamp(5, 100, 0) — el método no devuelve nada raro ni se queda pensando: lanza una IllegalArgumentException con un mensaje que delata al culpable (100 > 0). Y en la versión double, unos límites que sean NaN provocan la misma protesta (min is NaN / max is NaN), aunque el valor sí puede ser NaN — en cuyo caso NaN sale, como buen ciudadano especial que es. La moraleja práctica: si tus límites vienen de datos calculados o introducidos, son parte de lo que hay que validar.

El diagrama del guardián, para la memoria visual:

Use mouse to pan and zoom
flowchart LR
    E1["valor = -7"] --> A1["por debajo de min"]
    A1 --> R1["resultado: min = 0"]
    E2["valor = 50"] --> A2["dentro de la pista"]
    A2 --> R2["resultado: 50\ntal cual"]
    E3["valor = 150"] --> A3["por encima de max"]
    A3 --> R3["resultado: max = 100"]
    style E1 fill:#e3f2fd,color:#000000,stroke:#1565c0
    style E2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style E3 fill:#e3f2fd,color:#000000,stroke:#1565c0
    style A1 fill:#fff3e0,color:#000000,stroke:#e65100
    style A2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style A3 fill:#fff3e0,color:#000000,stroke:#e65100
    style R1 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style R2 fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style R3 fill:#c8e6c9,color:#000000,stroke:#2e7d32

Regla práctica: «mantener X entre A y B» se escribe Math.clamp(x, a, b) — en Java 21 o superior, que ya es tu caso. Si lees código antiguo con el emparedado Math.max(a, Math.min(x, b)), ya sabes qué hace y por qué se escribía así. Y los límites cruzados o NaN no pasan: IllegalArgumentException al canto.

4.7 Trigonometría: el asunto de los radianes

Math.sin(x), Math.cos(x) y Math.tan(x) calculan seno, coseno y tangente... de un ángulo en radianes. Esa última palabra es el título de la subsección porque es la responsable del 90 % de los errores trigonométricos de programador novel — y porque suena a detalles de matemáticas de bachillerato cuando en realidad es una decisión de ingeniería con perfecta lógica.

Repaso relámpago de la equivalencia, por si el instituto quedó lejos: una vuelta completa son 360 grados y también son τ radianes (≈ 6,2832) — o media vuelta, 180°, son π radianes (≈ 3,1416). Los radianes son la medida «natural» del ángulo (la que hace que las fórmulas del cálculo no lleven constantes de conversion pegadas), y por eso las librerías matemáticas de todos los lenguajes los usan por dentro. Java ofrece el par de traductores:

1
2
3
4
void main() {
    IO.println(Math.toRadians(180));   // 3.141592653589793: 180 grados = PI rad
    IO.println(Math.toDegrees(Math.PI));   // 180.0: la vuelta de regreso
}

La regla de uso es maquinal: piensas en grados, programas en radianes, y toRadians es el traductor que escribes sin pensar — Math.sin(Math.toRadians(30)). ¿Y qué pasa si te olvidas? Nada que el compilador pueda detectar: Math.sin(90) compila perfectamente y calcula el seno de 90 radianes — que son casi catorce vueltas y media y dan 0.8939966636005579 en lugar del 1.0 que tu cabeza esperaba de «seno de 90 grados». Un resultado plausible, numérico, elegante... y disparatado: la clase más peligrosa de error, otra vez.

La sorpresa de los dieciséis dígitos

Escribe la tabla trigonométrica de la escuela — 30°, 45°, 60° — y prepárate para la segunda sorpresa de la subsección:

1
2
3
4
5
void main() {
    IO.println(Math.sin(Math.toRadians(30)));   // 0.49999999999999994 (!)
    IO.println(Math.cos(Math.toRadians(60)));   // 0.5000000000000001  (!)
    IO.println(Math.tan(Math.toRadians(45)));   // 0.9999999999999999  (!)
}

El seno de 30° «debería» ser exactamente 0.5, y sale 0.49999999999999994. ¿Está roto Java? No: están trabajando juntas las dos causas del recorte que ya has visto hoy. Primera: Math.PI y el resultado de toRadians ya son recortes de 64 bits de sus valores matemáticos, y el seno de un número ligeramente distinto de π/6 es ligeramente distinto de 0.5. Segunda: sin a su vez redondea su resultado al double más cercano. El recorte de entrada se multiplica por el de salida, y el cero perfecto se convierte en una cola de nueves. La propia Javadoc lo confiesa con una franqueza que merece cita: los usuarios no deben esperar que cos(toRadians(90.0)) sea exactamente 0.0.

La consecuencia práctica es una regla de oro del cálculo con reales:

Nunca compares resultados trigonométricos — ni de coma flotante en general — con ==. Compara «cercanía»: que la distancia entre ambos sea menor que una tolerancia diminuta. Y para medir distancias... abs, que acabas de desempolvar: Math.abs(resultado - esperado) < 1e-9.

void main() {
    double catetoOpuesto = Math.sin(Math.toRadians(30));

    // La comparacion ingenua: dice que NO son iguales
    IO.println(catetoOpuesto == 0.5);                          // false

    // La comparacion profesional: con tolerancia
    double tolerancia = 1e-9;   // 0.000000001: una milmillonesima
    IO.println(Math.abs(catetoOpuesto - 0.5) < tolerancia);    // true
}

(1e-9 es notación científica de literal — la conociste en la UD1 como compañera de los literales numéricos — y significa «uno por diez elevado a menos nueve». Dieciséis dígitos de precisión disponible, tolerancia en el noveno: margen de sobra.)

La escalera del fontanero: trigonometría de verdad

El problema clásico con el que se cierra cualquier curso de trigonometría aplicada, en su versión de obra: una escalera de 4 metros apoyada contra una pared formando un ángulo de 65° con el suelo. ¿A qué altura llega? ¿Cuánto se aleja su base de la pared? Seno y coseno, directamente:

void main() {
    double largoEscalera = 4.0;
    double anguloGrados = 65.0;

    // seno: cateto opuesto (la altura que alcanza)
    double altura = largoEscalera * Math.sin(Math.toRadians(anguloGrados));

    // coseno: cateto contiguo (lo que se aleja de la pared)
    double base = largoEscalera * Math.cos(Math.toRadians(anguloGrados));

    IO.println("Alcanza una altura de %.2f m".formatted(altura));   // Alcanza una altura de 3,63 m
    IO.println("Su base queda a %.2f m de la pared".formatted(base));   // Su base queda a 1,69 m de la pared
}

Los valores crudos son 3.6252311481465997 y 1.6904730469627978; el %.2f los presenta como 3,63 y 1,69 (con locale español). Ese par calcular crudo → presentar formateado es el mismo de las constantes al principio de la sección, y es el que debe quedar mecanizado: los cálculos viajan con toda su precisión; la precisión se recorta solo al mostrar.

La tangente de 90 grados: casi-infinito, sin excepcion

La tangente de 90° «es infinito» en la pizarra. En Java, Math.tan(Math.toRadians(90)) devuelve 1.633123935319537E16 — un número enorme pero finito: la E del formato es «elevado a», así que eso es 1,63 × 10¹⁶, dieciséis órdenes de magnitud. ¿Por qué no Infinity? Porque toRadians(90) no es exactamente π/2 — es su recorte de 64 bits —, y la tangente de un ángulo ligeramente distinto de π/2 es un número ligeramente distinto de infinito. La moraleja es la de siempre: el país de los reales no tiene excepciones, tiene valores especiales y colas de precisión. (Y si al calcular Math.sin(Math.PI) — el seno de π, que «debería» ser 0 — ves 1.2246467991473532E-16, es exactamente la misma historia en miniatura: una cantidad dieciséis órdenes de magnitud por debajo de 1.)

Las funciones inversas: asin, acos, atan y atan2

La Javadoc de Math lista también la familia inversa: asin y acos (de un valor entre -1 y 1 devuelven el ángulo en radianes — o NaN si les das algo fuera de rango, ya sabes cómo se portan) y atan / atan2. Esta última merece una chapa: dado un punto (x, y), atan2(y, x) devuelve el ángulo hasta ese punto teniendo en cuenta el cuadrante — lo que atan(y / x) no puede hacer solo, porque pierde el signo de cada eje. Es la herramienta estándar para «¿hacia dónde mira?» en juegos y robótica. Hoy quedan citadas; si un ejercicio futuro las necesita, ya sabes dónde viven.

4.8 Aritmética segura: la familia Exact

Llega la subsección que la sección 3 anunció como «la aritmética segura que detecta desbordamientos por ti», y para apreciarla hay que empezar por el delito que denuncia. Ejecuta esto con calma:

1
2
3
4
5
6
7
void main() {
    int maximo = Integer.MAX_VALUE;    // 2147483647: el mayor int que existe

    IO.println(maximo);                // 2147483647
    IO.println(maximo + 1);            // -2147483648  (!)
    IO.println(maximo * 2);            // -2  (!!)
}

2147483647 + 1 da -2147483648. Sin aviso, sin excepción, sin ruido: el número más grande que cabe en un int, sumándole uno, se convierte en el más pequeño. El programa compila, ejecuta y miente. Esto es el desbordamiento (overflow): la aritmética entera de Java trabaja con 32 bits en representación complemento a dos, y cuando el resultado se sale del rango, los bits que no caben se pierden y el signo queda del revés. No es un fallo de Java: C, C++ y la mayoría de los lenguajes hermanos hacen lo mismo (o algo aún más permisivo). Es el precio histórico de la velocidad: comprobar cada suma costaría tiempo en cada operación, y en 1995 se decidió que el programador vigilara sus rangos.

Visualízalo como lo que realmente es — una regla que se curva sobre sí misma hasta formar un círculo:

Use mouse to pan and zoom
flowchart LR
    A["... 2147483645"] --> B["2147483646"] --> C["2147483647\nel maximo"] -->|"sumar 1:\nla vuelta"| D["-2147483648\nel minimo"] --> E["-2147483647"] --> F["..."]
    style C fill:#fff3e0,color:#000000,stroke:#e65100
    style D fill:#fff3e0,color:#000000,stroke:#e65100
    style A fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style B fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style E fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style F fill:#c8e6c9,color:#000000,stroke:#2e7d32

Y ahora el dato que convierte este fenómeno de curiosidad en problema de clase mundial: el desbordamiento silencioso es positivo la mitad de las veces. Mira los factoriales — el ejemplo de manual, y por algo —, calculados en int con un bucle de la UD2:

1
2
3
4
5
6
7
void main() {
    int factorial = 1;
    for (int n = 1; n <= 13; n++) {
        factorial = factorial * n;
        IO.println(n + "! = " + factorial);
    }
}
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 1932053504

Doce pasos perfectamente razonables... y el decimotercero «funciona»: 13! muestra 1932053504, un número positivo y plausible, cuando el factorial real de 13 es 6227020800 — un valor que ya no cabe en un int y que la operación ha dejado caer en la zona envuelta del círculo. Si tu programa validara algo con ese resultado, validaría cualquier cosa. (En long el mismo bucle es fiable hasta 20! = 2432902008176640000; el 21! ya envuelve a negativo. Los rangos grandes solo retrasan el abismo, no lo eliminan.)

Otro clásico de producción, para que veas que esto no es cosa de factoriales de juguete: multiplicar tamaños. 100000 * 100000 en int produce 1410065408 — un millón por un millón son cien mil millones (100000000000, que no cabe), y los bits que sobreviven forman ese número impostor. Tamaños de fichero, contadores de milisegundos, píxeles de una imagen grande, dinero acumulado: el desbordamiento acecha exactamente donde los números se hacen grandes sin que nadie lo planee.

La familia Exact: sumar con cinturón de seguridad

La Javadoc de Math — ya sabes: párrafo de la descripción de la clase, antes de la lista de métodos — lo dice casi con estas palabras: cuando el tamaño sea int o long y haya que detectar el desbordamiento, usa los métodos cuyo nombre termina en Exact, que lanzan una ArithmeticException cuando el resultado se desborda. Esta es la familia, con su traducción:

Método Equivalente «peligroso» Qué hace
Math.addExact(x, y) x + y Suma; si desborda, lanza ArithmeticException
Math.subtractExact(x, y) x - y Resta con vigilancia
Math.multiplyExact(x, y) x * y Multiplicación con vigilancia
Math.incrementExact(x) x + 1 El ++ vigilado
Math.decrementExact(x) x - 1 El -- vigilado
Math.negateExact(x) -x Cambio de signo vigilado

Cada una existe en versión int y en versión long (la sobrecarga, de nuevo, eligiendo sola), y el mensaje de la excepción es de una honestidad reconfortante: integer overflow o long overflow, según el país donde ocurra. Y aquí conecta todo lo que ya sabes: en la UD2 aprendiste a capturar excepciones predefinidas; ArithmeticException era la de la división por cero. La familia Exact te da precisamente eso — un aviso capturable — para el desbordamiento:

void main() {
    int maximo = Integer.MAX_VALUE;

    try {
        // La suma vigilada: si el resultado no cabe, avisa en vez de mentir
        int suma = Math.addExact(maximo, 1);
        IO.println("Suma: " + suma);
    } catch (ArithmeticException ex) {
        IO.println("No cabe: " + ex.getMessage());   // No cabe: integer overflow
    }
}

Ejecuta ese programa y verás la línea del catch: integer overflow, el aviso que la suma normal jamás te daría. El ex.getMessage() de la UD2 extrayendo el texto oficial de la excepción. Este es el patrón completo de la aritmética segura: calcular con Exact y decidir en el catch qué hacer — rechazar el dato, pasar a long, cortar la operación — en vez de propagar en silencio un número corrupto.

Dos detalles de factura antes de seguir. Primero: negateExact(Integer.MIN_VALUE) lanza la excepción, exactamente por la cueva asimétrica que desactivamos con abs en la subsección 4.3 — el +2147483648 no existe, así que cambiarle el signo a -2147483648 es imposible, y esta familia lo confiesa a gritos en lugar de devolverte el mismo número con signo torcido. (¿Ves cómo las trampas de esta sección se sostienen entre sí? El rango asimétrico era la misma causa.) Segundo: la familia existe solo para int y long — los reales no la necesitan, porque un double desbordado se va a Infinity con toda naturalidad, como ya vimos en la tabla de la subsección 4.3. El círculo que envuelve es exclusivo del mundo entero.

¿Cuándo usar cuál? La tabla de decisiones

Situación Herramienta Por qué
Índices, contadores de bucle, valores acotados por diseño Operadores normales + - * El rango lo controla tu lógica; la vigilancia sería ruido
Datos que vienen de fuera (usuario, fichero, red) o productos de crecimiento imprevisible addExact y familia Un dato sucio no debe corromper el cálculo en silencio
Rango justito pero previsible (segundos, identificadores) Cambiar a long El margen de un long (hasta ±9,2 trillones y pico) suele bastar de por vida
Dinero, criptografía, cifras sin límite conocido BigInteger (java.math) Precisión arbitraria — pincelada, abajo

Y la pincelada prometida: cuando ni long llega — factoriales más allá de 20, dinero con decimales exactos, claves criptográficas de cientos de dígitos — el barrio correcto es java.math con BigInteger y BigDecimal, aquel paquete que la sección 2 presentó como «el de la aritmética de precisión» y que el roadmap del módulo deja citado para pinceladas puntuales. No lo usaremos en el itinerario, pero ahora sabes exactamente cuándo alguien debería: cuando el círculo de 64 bits se quede pequeño para el problema.

toIntExact: el portero del paso long a int

La familia tiene un primo suelto que te salvará en una situación muy concreta: has calculado en long y necesitas el resultado como int (porque un método lo pide, porque un array lo indexa). El cast (int) miLong trunca los bits sin preguntar — el desbordamiento de narrowing, primo del de arriba —. Math.toIntExact(miLong) hace el cast solo si cabe, y si no, lanza su ArithmeticException con el mensaje honesto de siempre (integer overflow). Guarda su nombre: el día que conviertas System.currentTimeMillis() a int para algo, querrás tenerlo delante.

4.9 Math.random(): el azar de bolsillo

Cerramos el repertorio con la herramienta más juguetona de la pared: Math.random() devuelve un double mayor o igual que 0.0 y menor que 1.0. Ese intervalo se escribe [0.0, 1.0) — corchete cerrado a la izquierda, abierto a la derecha — y el matiz del corchete abierto es la mitad de la subsección, así que no lo pases por alto.

1
2
3
4
5
void main() {
    IO.println(Math.random());   // 0.3795441200715002 (en esta ejecucion; en la tuya, otro)
    IO.println(Math.random());   // 0.9766597516554214
    IO.println(Math.random());   // 0.0056548727448151315
}

Cada llamada devuelve un número nuevo e impredecible; los de arriba son de una ejecución real y en la tuya serán otros. (Un detalle que te hará sonreír si piensas en la analogía del gremio: random() es un método estático... que esconde un objeto por dentro. Si, con las gafas de la sección 2, te preguntas dónde está el estado — porque un azar necesita memoria de lo ya sorteado —, la respuesta oficial de la Javadoc es que la primera llamada crea un único generador compartido de la clase java.util.Random... la clase que la sección 5 abre a fondo, y de la que la sección 2 ya te enseñó a leer sus constructores. Hoy, Math.random() es un panel público con una manivela; la manivela y su maquinaria son el tema de la próxima sección.)

La receta de escalado: del corchete abierto al dado

Un double entre 0 y 1 es útil para probabilidades — «¿ocurre algo con un 20 % de chances?» es literalmente Math.random() < 0.2 —, pero la mayoría de los problemas quiere enteros en un rango: tirar un dado, repartir cartas, elegir un índice. Ahí entra la receta clásica de escalado, que se deduce del corchete abierto:

void main() {
    // El dado: la receta completa en dos pasos
    IO.println((int) (Math.random() * 6) + 1);   // 1, 2, 3, 4, 5 o 6

    // Despiece paso a paso de una tirada concreta (supongamos random() = 0.3795441...)
    double r = Math.random();        // r esta en [0.0, 1.0): nunca llega a 1
    IO.println(r * 6);              // 2.2772...: esta en [0.0, 6.0): nunca llega a 6
    IO.println((int) (r * 6));      // 2: el cast trunca, y como nunca hubo 6, salen 0..5
    IO.println((int) (r * 6) + 1);  // 3: el +1 desplaza el rango a 1..6: el dado
}

¿Por qué funciona con tanta exactitud? Sigue el corchete abierto con atención:

Use mouse to pan and zoom
flowchart LR
    R["random()\nen [0.0, 1.0)"] -->|"x 6"| S["en [0.0, 6.0)"] -->|"(int) trunca"| T["0, 1, 2, 3, 4 o 5"] -->|"+ 1"| D["1, 2, 3, 4, 5 o 6"]
    style R fill:#e3f2fd,color:#000000,stroke:#1565c0
    style S fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style T fill:#fff3e0,color:#000000,stroke:#e65100
    style D fill:#c8e6c9,color:#000000,stroke:#2e7d32

La multiplicación preserva el «menor que» del corchete abierto (lo que era menor que 1 pasa a ser menor que 6), y el cast (int) trunca hacia el cero — la personalidad que le conocimos en la subsección de redondeo —, regalándote solo los enteros 0 a 5. El + 1 final desplaza la ventana. Nunca sale un 7 y nunca sale un 0, con la misma garantía matemática. Generalizado, el molde de moldes:

Dado un rango de enteros [min, max] (ambos incluidos): min + (int) (Math.random() * (max - min + 1)). Para el dado, min = 1 y max = 6 — y el paréntesis (max - min + 1) cuenta cuántos valores tiene el rango.

1
2
3
4
5
6
7
8
void main() {
    // Simulacion: 12 tiradas del dado de la subseccion anterior
    for (int tirada = 1; tirada <= 12; tirada++) {
        IO.print((int) (Math.random() * 6) + 1 + " ");
    }
    IO.println();
    // (una ejecucion real: 1 3 3 3 3 5 3 4 5 4 1 1 — la tuya sera distinta)
}

Fíjate en el detalle de factura del IO.print con el espacio final y el IO.println() de cierre — pequeño arreglo de presentación que ya usaste en la UD2. Y una curiosidad estadística que conviene haber visto una vez: como random() nunca devuelve exactamente 1.0, el resultado de la receta es uniforme — los seis valores del dado salen con la misma frecuencia a largo plazo. En un millón de tiradas, cada cara cae unas 166 666 veces, con las lógicas fluctuaciones de todo azar real.

Lo que random() no sabe hacer (el spoiler oficial)

El roadmap avisaba de que esta subsección era el spoiler de la siguiente, así que cumpla su papel de promesa de tráiler. Math.random() es perfecto para scripts, dados y probabilidades sueltas... y se queda corto en cuanto el azar se toma en serio:

  • No puedes repetir una ejecución. Los resultados de Random con semilla son reproducibles — misma semilla, misma secuencia, lo que convierte un bug aleatorio en una prueba determinista. Con Math.random() no hay semilla que elegir. (La sección 2 ya te dejó verlo con new Random(42) y sus apodos idénticos entre ejecuciones.)
  • No hay variedad de tipos. La clase Random ofrece nextInt(n), nextLong(), nextBoolean(), nextDouble()...: el método que va con el tipo de dato que necesites, sin recetas de escalado a mano.
  • No hay chorros de azar. Random.ints(cantidad, min, max) fabrica un stream de enteros aleatorios — el mismo adelanto de la Streams API que te esperó en chars() durante la sección 3.
  • Y en programas con hilos — adelanto lejano de las unidades de concurrencia —, la recomendación moderna es ThreadLocalRandom.current(), por rendimiento.
Math.random() new Random() — sección 5
Qué devuelve double en [0.0, 1.0), con conversiones a mano Enteros, reales, booleanos... a la carta
Reproducible No Sí, fijando la semilla en el constructor
Herramientas Una sola manivela Un repertorio con Javadoc propia
Apto para hilos Compartido (cuello de botella potencial) ThreadLocalRandom para cada hilo

Si esta tabla te ha dejado con ganas de más, perfecto: es exactamente el estado de ánimo con el que la sección 5 arranca. Por hoy, Math.random() y su receta de escalado te hacen autónomo para dados, muestras aleatorias y probabilidades — y la práctica guiada que empieza ahora los usa todos.

4.10 Práctica guiada: la arena de combate

Toca poner a trabajar toda la sección, y el escenario de hoy es un clásico del sector: el motor de combate de un minijuego. Construiremos «La arena de combate»: un programa con dos personajes situados en un mapa, que calcula la distancia entre ellos, resuelve sus golpes y emite el informe de la partida. En una sola práctica caben Pitágoras (hypot), degradación del ataque con la distancia (max y round), curas que no se pasan (clamp), golpes críticos (random), tiro con arco (pow, sin, toRadians) y un contador de prestigio que desborda (addExact y try-catch). Es decir: el temario entero de hoy, jugando.

Como en la sección anterior, el código base te lo doy entero y las cinco misiones son tuyas — con la Javadoc y el autocompletado como brújula, e intentando resolverlas antes de abrir la solución plegable. Escribe el programa en un fichero nuevo, Arena.java:

void main() {
    // Los combatientes y su posicion en el mapa (en metros)
    String heroe = "Ada";
    String enemigo = "Bug";

    double heroeX = 3.0, heroeY = 4.0;     // Ada esta en (3, 4)
    double enemigoX = 0.0, enemigoY = 0.0;  // Bug esta en (0, 0)

    int vidaEnemigo = 100;
    int ataqueBase = 16;

    // La distancia entre ambos: Pitagoras con la herramienta de hoy
    double distancia = Math.hypot(heroeX - enemigoX, heroeY - enemigoY);

    // El golpe pierde fuerza con la distancia: a 10 m o menos, potencia
    // completa; mas alla, se degrada en proporcion a la distancia
    int dano = (int) Math.round(ataqueBase * 10.0 / Math.max(distancia, 10.0));

    vidaEnemigo -= dano;

    IO.println("Distancia entre combatientes: " + distancia + " m");
    IO.println("El golpe de " + heroe + " causa " + dano + " puntos de dano");
    IO.println("Vida restante de " + enemigo + ": " + vidaEnemigo);
}

Ejecútalo y comprueba la salida — que además es un guiño geométrico deliberado, porque una escalera de hipotenusa 5.0 ya apareció hoy:

1
2
3
Distancia entre combatientes: 5.0 m
El golpe de Ada causa 16 puntos de dano
Vida restante de Bug: 84

Pausa para leerlo con las gafas de la sección. La distancia sale de hypot sobre las diferencias de coordenadas (la fórmula de Pitágoras en su forma de programador: raíz de la suma de los cuadrados de las deltas). El daño combina tres herramientas en una línea: Math.max(distancia, 10.0) fija el suelo de distancia a 10 metros (más cerca no pegas más fuerte, el max «protege» el mínimo), la división degrada la potencia más allá, y round con su cast (int) — el de la subsección 4.4 — convierte el double final en un daño entero. Todo lo demás es la lógica de la UD1. Las misiones van ahora, en orden y sin saltarse la parte incómoda.

Misión 1 — La poción que no cura de más. Añade una variable vidaHeroe para Ada (arranca en 65: llegó tocada al combate) y simula dos eventos: se bebe una poción de +35 puntos... y acto seguido recibe el martillo del ogro, que quita 140. La vida de Ada debe quedar siempre entre 0 y 100, sin if ninguno.

Solución a la misión 1

El guardián del rango de la subsección 4.6, en su hábitat natural: los valores de vida de un personaje. Sumar y restar sin miedo, porque clamp recorta en la frontera:

1
2
3
4
5
6
7
8
9
int vidaHeroe = 65;

// La pocion: +35... pero la vida no pasa de 100
vidaHeroe = Math.clamp(vidaHeroe + 35, 0, 100);   // 100

// El martillo del ogro: -140... y la vida no baja de 0
vidaHeroe = Math.clamp(vidaHeroe - 140, 0, 100);  // 0

IO.println("Vida de " + heroe + ": " + vidaHeroe);   // 0: Ada cae (heroicamente)

La primera operación interna da 100 (justo el máximo: el clamp no interviene) y la segunda da -40, que el guardián deja en 0 — una muerte limpia, sin vidas negativas que avergüencen al informe final. Cuenta las líneas que te has ahorrado frente a la versión con dos if y agradécele a Java 21 el clamp: antes de existir, cada borde del rango era un if y cada olvido, un bug.

Misión 2 — El golpe crítico. Los combates aburren si todos los golpes son iguales: añade un 20 % de probabilidad de golpe crítico, que duplica el daño de ese asalto. Simula cinco ataques seguidos y cuenta cuántos críticos han salido.

Solución a la misión 2

La prueba de probabilidad de la subsección 4.9 (Math.random() < 0.2) decidida en cada vuelta del bucle, con un acumulador de críticos al lado:

int criticos = 0;

for (int asalto = 1; asalto <= 5; asalto++) {
    int golpe = ataqueBase;

    if (Math.random() < 0.2) {        // 20% de probabilidad: sale o no sale
        golpe = golpe * 2;            // critico: el doble de dano
        criticos++;
    }

    IO.println("Asalto " + asalto + ": " + golpe + " puntos");
}

IO.println("Criticos acertados: " + criticos + " de 5");

Una ejecución posible: Asalto 1: 16 puntos, Asalto 2: 32 puntos, Asalto 3: 16 puntos, Asalto 4: 32 puntos, Asalto 5: 16 puntos — dos críticos de cinco, que es un día de suerte; en tu máquina la secuencia será otra, que es la gracia. ¿Quieres comprobar que el 20 % es justo? Ejecuta el programa muchas veces y anota la media de críticos: se acercará a uno de cada cinco. (Y si quieres la versión científica de ese experimento — repetible, medible, con semilla —, es exactamente lo que la sección 5 pone sobre la mesa con la clase Random.)

Misión 3 — El arco de la reina. Ada cambia de táctica y dispara una flecha con velocidad inicial de 25 m/s y ángulo de lanzamiento de 40°. La física del instituto dice que el alcance de un proyectil sobre suelo llano es v² · sen(2α) / g, con g = 9.8 m/s². Calcula cuántos metros vuela la flecha y preséntalo con un decimal.

Solución a la misión 3

Toda la subsección 4.7 en tres líneas: pow para el cuadrado, toRadians como traductor y sin del ángulo doble. Con formatted para la presentación digna:

1
2
3
4
5
6
7
8
9
double velocidad = 25.0;    // m/s
double angulo = 40.0;       // grados
double gravedad = 9.8;      // m/s al cuadrado

// Alcance del proyectil: v^2 * sen(2 * angulo) / g
double alcance = Math.pow(velocidad, 2)
               * Math.sin(Math.toRadians(2 * angulo)) / gravedad;

IO.println("La flecha vuela %.1f metros".formatted(alcance));

La salida (con locale español): La flecha vuela 62,8 metros. Dos detalles finos. Primero, el ángulo entra en grados (piensas en grados) y toRadians(2 * angulo) lo traduce dentro de la fórmula — el hábito anti-error de la subsección 4.7. Segundo, ¿por qué sen(2α) y no sen(α)? Porque es la forma que hace visible el máximo a 45°: la fórmula con el ángulo doble vale lo máximo cuando 2α = 90°. Compruébalo tú: cambia el ángulo a 45 y el alcance sube a 63,8 metros; bájalo a 30 y cae a 55,2. Tres ejecuciones, una parábola, y la física del instituto verificada con Math y un bucle for si te apetece barrer todos los ángulos de 5 en 5.

Misión 4 — El contador de prestigio. Cada victoria en la arena otorga 150 000 000 puntos de experiencia. Ada llega con 1 800 000 000 acumulados y juega una tanda de tres victorias. Simula el contador dos veces: primero con la suma normal (experiencia = experiencia + premio), y después con la familia Exact, capturando la excepción con el try-catch de la UD2. Compara lo que ocurre en cada versión.

Solución a la misión 4

La primera versión es el drama silencioso de la subsección 4.8 en directo:

1
2
3
4
5
6
7
8
int experiencia = 1_800_000_000;
int premioPorVictoria = 150_000_000;

// Version ingenua: la suma que miente
for (int victoria = 1; victoria <= 3; victoria++) {
    experiencia = experiencia + premioPorVictoria;
    IO.println("Victoria " + victoria + ": experiencia = " + experiencia);
}
1
2
3
Victoria 1: experiencia = 1950000000
Victoria 2: experiencia = 2100000000
Victoria 3: experiencia = -2044967296

Dos victorias razonables... y la tercera convierte a la heroína más prestigiosa del reino en la más novata, con un contador negativo que ningún jugador perdonaría. La segunda versión es la misma tanda con cinturón de seguridad:

experiencia = 1_800_000_000;   // se reinicia para repetir la tanda

// Version segura: la familia Exact avisa en vez de mentir
for (int victoria = 1; victoria <= 3; victoria++) {
    try {
        experiencia = Math.addExact(experiencia, premioPorVictoria);
        IO.println("Victoria " + victoria + ": experiencia = " + experiencia);
    } catch (ArithmeticException ex) {
        IO.println("Victoria " + victoria + ": el contador no admite tanto prestigio");
        IO.println("Motivo: " + ex.getMessage());   // integer overflow
    }
}
1
2
3
4
Victoria 1: experiencia = 1950000000
Victoria 2: experiencia = 2100000000
Victoria 3: el contador no admite tanto prestigio
Motivo: integer overflow

La moraleja práctica de la subsección 4.8 hecha partida: la suma normal corrompe en silencio; la vigilada lanza una ArithmeticException que tu catch convierte en un mensaje digno — y en una decisión de diseño (rechazar la victoria, migrar el contador a long, usar BigInteger). Fíjate además en que el programa no se cae: la excepción capturada deja el contador en su último valor válido, que es exactamente el comportamiento que un juego real querría.

Misión 5 — El marcador final. Remata la arena con un combate de cinco asaltos: cada asalto golpea con ataqueBase + un bono aleatorio de 0 a 5 (así que los golpes van de 16 a 21). El informe final debe mostrar el mejor y el peor golpe (max y min), el golpe medio redondeado (round), la vida final de Bug tras los cinco asaltos — sin poder quedar negativa — y una barra de vida de 20 huecos, donde cada hueco representa 5 puntos.

Solución a la misión 5

Reunión de toda la sección en un solo bloque: la receta de escalado del random para el bono, el bucle con acumuladores de la subsección 4.5 — inicializados con el primer golpe real, como manda el patrón «recorrer y quedarse con los extremos» —, el round para la media, el clamp para la vida, y repeat con formatted para la barra (el mismo matrimonio que cerró el informe de la sección 3):

// El primer asalto, fuera del bucle: inicializa mejor y peor con datos reales
int golpe = ataqueBase + (int) (Math.random() * 6);   // bono de 0 a 5
int suma = golpe;
int mejor = golpe;
int peor = golpe;
IO.println("Asalto 1: " + golpe + " puntos");

for (int asalto = 2; asalto <= 5; asalto++) {
    golpe = ataqueBase + (int) (Math.random() * 6);
    suma += golpe;
    mejor = Math.max(mejor, golpe);
    peor = Math.min(peor, golpe);
    IO.println("Asalto " + asalto + ": " + golpe + " puntos");
}

// La vida de Bug tras el combate: nunca negativa (clamp), y su barra
int vidaFinal = Math.clamp(100 - suma, 0, 100);
int huecos = vidaFinal / 5;      // division entera: cada hueco son 5 puntos

String informe = """
        ==================================
           ARENA DE COMBATE - INFORME
        ==================================
        Mejor golpe: %d
        Peor golpe: %d
        Golpe medio: %d
        Vida final de Bug: %d
        Bug: %s%s
        ==================================
        """.formatted(mejor, peor, Math.round(suma / 5.0), vidaFinal,
                      "#".repeat(huecos), "-".repeat(20 - huecos));

IO.print(informe);

Una ejecución posible — la tuya sacará otros bonos, que para eso son aleatorios:

Asalto 1: 19 puntos
Asalto 2: 16 puntos
Asalto 3: 21 puntos
Asalto 4: 17 puntos
Asalto 5: 19 puntos
==================================
   ARENA DE COMBATE - INFORME
==================================
Mejor golpe: 21
Peor golpe: 16
Golpe medio: 18
Vida final de Bug: 8
Bug: #-------------------
==================================

Comprueba la coherencia de esa ejecución de ejemplo como entrenamiento: los golpes fueron 19+16+21+17+19, que suman 92; la media 92/5.0 es 18.4, que round lleva a 18; la vida queda en 100 - 92 = 8; y 8 entre 5 da 1 hueco entero (división entera de la UD1), así que la barra muestra un # y diecinueve -. ¿Y por qué el clamp de la vida, si 92 es menos de 100? Porque los cinco golpes máximos posibles suman 105: en la ejecución más brutal del azar, 100 - 105 es -5... y el guardián la deja en 0, con la barra vacía y sin vergüenzas negativas en el informe. Diseñar para el peor caso es la diferencia entre un demo y un producto.

Si has completado las cinco misiones, mira atrás: has medido distancias con hypot, degradado fuerzas con max, redondeado con round y su cast, curado con clamp, sorteado críticos con random, volado flechas con pow-sin-toRadians, y salvado un contador con addExact y un catch oportuno. Eso es el temario completo de la sección resolviendo un combate, y el examen de la unidad se escribe con estas mismas piezas. Una reflexión final de profesor: hoy casi ninguna herramienta ha sido «nueva» en lo conceptual — todas eran llamadas estáticas con parámetros, la mecánica que dominas desde la sección 2 —; lo nuevo ha sido el criterio: qué herramienta elige un profesional para cada redondeo, cada rango y cada desbordamiento. Ese criterio es exactamente lo que evalúa el quiz que viene.

4.11 Resumen, glosario y checklist

Si tuvieras que quedarte con diez ideas de esta sección, que fueran estas:

  1. Math es la clase de utilidad canónica: vive en java.lang (no en java.math), es final, no declara ni un miembro de instancia y su constructor es privado — new Math() no compila. Todo su repertorio (99 métodos públicos con las sobrecargas, 3 constantes) se pide a la clase como receptor.
  2. Sus tres constantes públicas son PI, E y TAU (Java 19): campos static final de tipo double, con unos 15-17 dígitos significativos. Se presentan con %.2f y compañía; el println crudo siempre pinta punto decimal.
  3. sqrt, pow, cbrt e hypot entran y salen como double: sqrt(144) es 12.0. sqrt(-1) devuelve NaN — no lanza nada —, pow con base negativa y exponente fraccionario también, y cbrt es la herramienta que sí admite negativos.
  4. NaN no es igual a nada (ni a sí mismo): se detecta con Double.isNaN. Los reales responden a lo imposible con valores especiales (NaN, Infinity); los enteros, con excepciones (/ by zero) o con silencio (el desbordamiento).
  5. abs(Integer.MIN_VALUE) devuelve negativo — el rango de int es asimétrico y +2147483648 no cabe —; absExact lanza ArithmeticException en ese caso extremo.
  6. Redondeo: el cast (int) trunca hacia el cero; round va al más cercano con empates hacia +∞ (round(-2.5) es -2) y devuelve long; ceil y floor devuelven double. Presentar → round; dimensionar → ceil; truncar por abajo → floor.
  7. min y max convierten decisiones en expresiones; el patrón «recorrer y quedarse con los extremos» inicializa los acumuladores con el primer dato, nunca con cero.
  8. clamp(valor, min, max) (Java 21) mantiene un número en su rango legal; cuatro sobrecargas — la de int acepta el valor como long — y límites cruzados o NaN en los bordes provocan IllegalArgumentException.
  9. Trigonometría en radianes: toRadians es el traductor de tus grados. Los resultados llegan con colas de precisión (0.49999999999999994), así que nunca se comparan con ==: tolerancia y abs.
  10. El desbordamiento entero es silencioso (MAX_VALUE + 1 da MIN_VALUE, y el 13! sale positivo); la familia Exact (addExact, multiplyExact, incrementExact...) lo convierte en una ArithmeticException capturable. Math.random() devuelve un double en [0.0, 1.0) y el dado se escribe min + (int) (Math.random() * (max - min + 1)).

Glosario de la sección

Término Definición en una línea
Clase de utilidad Clase con solo miembros estáticos y constructor privado: se usa, no se instancia (Math)
Constructor privado Constructor inaccesible desde fuera: sella la clase contra new
NaN Valor especial «no es un número»: respuesta de los reales a operaciones imposibles; no es igual a nada
Infinity / -Infinity Valores especiales de los reales para resultados que se pasan de largo (1.0 / 0.0)
IEEE 754 El estándar de coma flotante que rige float y double (y sus valores especiales) desde 1985
Desbordamiento (overflow) Resultado entero fuera de rango: los bits que no caben se pierden y el signo se tuerce, sin aviso
Complemento a dos Representación binaria de los enteros con signo que hace del rango un círculo cerrado
Familia Exact Métodos addExact, multiplyExact... que lanzan ArithmeticException al desbordar
Rango asimétrico int y long tienen un negativo más que positivos: el extremo negativo es «una cueva»
Empate Valor exactamente a medio camino (2.5): round lo resuelve hacia el infinito positivo
Truncar hacia el cero Lo que hace el cast (int): descarta los decimales acercándose a cero (-2.9 → -2)
Tolerancia Comparación de reales por cercanía (Math.abs(a - b) < 1e-9) en lugar de por igualdad exacta
Radián Medida natural del ángulo: una vuelta completa son TAU (o 360°); lo que exigen sin, cos y tan
Intervalo [0.0, 1.0) Corchete cerrado, corchete abierto: el 1.0 no está incluido — el matiz del que nace el dado
Receta de escalado min + (int) (Math.random() * (max - min + 1)): convertir el [0, 1) en enteros de un rango
Semilla Estado inicial de un generador aleatorio que fija toda su secuencia (estrella de la sección 5)
Locale Configuración regional: decide que %f pinte la coma decimal (repaso de la sección 3)
Notación científica Formato 1.22E-16 del println para números muy grandes o pequeños: «elevado a»
StrictMath Hermana de Math con resultados idénticos bit a bit entre máquinas (citada, sin uso en el módulo)
Guardián del rango Apodo de trabajo de clamp: lo que sale de la pista vuelve al borde más cercano

Checklist: al terminar, debes ser capaz de...

  • [ ] Explicar por qué new Math() no compila y qué tiene que ver con ser una clase de utilidad sin estado (CE 2.5, base de CE 2.1).
  • [ ] Decir en qué paquete vive Math — y por qué ningún programa de esta sección llevaba import — sin mirar apuntes (CE 2.7).
  • [ ] Predecir el resultado de sqrt(-25), 0.0 / 0.0 y 1.0 / 0.0, y detectar un NaN con Double.isNaN en vez de con == (CE 2.2).
  • [ ] Explicar la doble trampa de Math.pow(-8, 1 / 3) — división entera y base negativa — y elegir cbrt cuando toca (CE 2.2, CE 2.6).
  • [ ] Explicar por qué Math.abs(Integer.MIN_VALUE) sale negativo y qué alternativa avisa del problema (CE 2.2).
  • [ ] Predecir round(2.5), round(-2.5), ceil(-2.1) y floor(-2.1), junto con el tipo exacto de cada resultado (CE 2.2).
  • [ ] Corregir un int nota = Math.round(media); que no compila, con el cast adecuado (CE 2.2).
  • [ ] Elegir entre (int), round, ceil y floor para un problema dado: notas medias, cajas necesarias, años cumplidos (CE 2.2, CE 2.6).
  • [ ] Escribir Math.clamp con la sobrecarga correcta, y predecir la IllegalArgumentException de los límites cruzados (CE 2.5, CE 2.6).
  • [ ] Escribir el bucle de extremos (mínimo y máximo de un array) inicializando los acumuladores con el primer elemento (CE 2.2).
  • [ ] Convertir Math.random() a un rango [min, max] de enteros y explicar por qué el corchete abierto garantiza que no sobra ningún valor (CE 2.2, CE 2.6).
  • [ ] Demostrar el desbordamiento silencioso con Integer.MAX_VALUE + 1 y blindarlo con Math.addExact dentro de un try-catch (CE 2.2).
  • [ ] Leer la ficha Javadoc de un método de Math que no hayamos visto en clase y deducir parámetros, retorno y casos especiales (CE 2.7, CE 2.9).

Lo que hoy quedó citado, y dónde vive

Tema mencionado hoy Dónde se desarrolla
Random, semillas, nextInt, ThreadLocalRandom, chorros de aleatorios Sección 5 de esta unidad
java.time y sus fábricas (LocalDate.now(), of) Sección 6 de esta unidad
Concatenación eficiente en bucles (StringBuilder) Sección 7 de esta unidad
Wrappers: Integer.MAX_VALUE a fondo, Double.isNaN, autoboxing Sección 8 de esta unidad
Maven y las dependencias externas Sección 10 de esta unidad
chars() y Random.ints(): los streams UD5 (teaser ya cobrado en la sección 3)
Escribir tu propia clase de utilidad con static final UD4, sección 6
StrictMath Citada hoy; sin desarrollo en el itinerario del módulo
BigInteger y BigDecimal (java.math) Pinceladas puntuales; fuera del itinerario principal

4.12 Autoevaluación de la sección

¿Esto no era la sección 11?

Una vez más: el test oficial de repaso de la unidad vive en la sección 11, dividido por secciones para el repaso de conjunto. Este bloque es una autoevaluación rápida para hacer justo después de leer, sin material y sin presión: si fallas algo, vuelve a la subsección correspondiente antes de continuar. El orden de las preguntas sigue el orden de la sección.

#

Pregunta 1

¿Por qué no compila la línea Math calculadora = new Math();?

#

Pregunta 2

¿Qué devuelve Math.sqrt(-25)?

#

Pregunta 3

¿Qué devuelve Math.pow(-8, 1 / 3)?

#

Pregunta 4

¿Qué imprime IO.println(Math.abs(Integer.MIN_VALUE));?

#

Pregunta 5

¿Qué devuelve Math.round(-2.5)?

#

Pregunta 6

¿Qué tipos devuelven Math.round(7.5), Math.ceil(7.5) y Math.floor(7.5)?

#

Pregunta 7

¿Qué ocurre al ejecutar Math.clamp(5, 100, 0)?

#

Pregunta 8

Un compañero escribe Math.sin(90) esperando el seno de 90 grados y obtiene 0.8939966636005579. ¿Qué ha pasado?

#

Pregunta 9

¿Qué imprime IO.println(Integer.MAX_VALUE + 1); y cómo se blinda esa operación?

#

Pregunta 10

¿Qué valores puede devolver la expresión (int) (Math.random() * 6) + 1?

Cuando las diez preguntas te salgan sin dudar, has cerrado la sección de la clase de utilidad: dominas el repertorio estático más transitado del API, sus trampas desactivadas y el criterio profesional para elegir herramienta. Estás listo para la sección 5 — La clase Random y ThreadLocalRandom, donde el azar deja de ser de bolsillo: instanciarás tu propio generador con new Random(), aprenderás a fijar la semilla que convierte una partida aleatoria en un experimento repetible, pedirás enteros, reales y booleanos con nextInt, nextDouble y nextBoolean, y conocerás el ThreadLocalRandom que usa el código moderno — con el mismo espíritu con el que hoy has pasado de usar la pared de herramientas de Math a entender por qué está colgada así.