Los operadores son los verbos del lenguaje: lo que convierte datos inertes en cálculos. Ya has usado + y - de forma intuitiva, pero Java tiene un catálogo amplio y varias trampas que atrapan a todo el mundo al menos una vez: la división entre enteros que descarta los decimales, la diferencia entre i++ e ++i, la comparación de cadenas con == que a veces funciona y a veces no, y la precisión traicionera de double cuando hay dinero de por medio. Esta lección los recorre todos con calma, siempre sobre el mismo caso de BiblioTech: calcular la multa por retraso de un préstamo de la biblioteca de Nexus Software.

Contenido

  1. Operadores aritméticos y la trampa de la división entera
  2. El operador módulo
  3. Operadores de asignación compuesta
  4. Incremento y decremento: pre y post
  5. Operadores relacionales
  6. Operadores lógicos y evaluación de cortocircuito
  7. El operador ternario
  8. Operadores a nivel de bit y desplazamiento
  9. El operador instanceof
  10. Precedencia y asociatividad
  11. Comparar cadenas: == frente a equals
  12. La precisión de double y por qué existe BigDecimal
  13. Errores Comunes y Consejos
  14. Ejercicios

  1. Operadores aritméticos y la trampa de la división entera

Los cinco operadores aritméticos básicos:

Operador Nombre Ejemplo Resultado
+ Suma 12 + 3 15
- Resta 27 - 15 12
* Multiplicación 12 * 2 24
/ División 12 / 5 2 (¡atención!)
% Módulo (resto) 12 % 5 2

La trampa del 5 / 2

Es el error número uno de quien empieza en Java:

int total = 5;
int partes = 2;
System.out.println(total / partes);     // imprime 2, no 2.5

La regla es la siguiente: si ambos operandos son enteros, la división es entera y el resultado se trunca hacia cero, descartando los decimales. Java no redondea, corta.

Y esto no se arregla guardando el resultado en un double:

double resultado = total / partes;
System.out.println(resultado);          // 2.0, no 2.5

¿Por qué? Porque la operación total / partes se evalúa primero, entre enteros, dando 2; solo después ese 2 se convierte a double. La conversión llega demasiado tarde.

Las tres soluciones correctas:

// 1. Cast de uno de los operandos ANTES de dividir
double resultado = (double) total / partes;        // 2.5

// 2. Declarar las variables como double desde el principio
double totalD = 5;
double partesD = 2;
double resultado2 = totalD / partesD;              // 2.5

// 3. Multiplicar por 1.0 (truco menos legible, pero se ve en codigo real)
double resultado3 = total * 1.0 / partes;          // 2.5

Ojo con el orden en el cast: (double)(total / partes) no funciona. Los paréntesis hacen que se divida primero entre enteros y se convierta después: da 2.0. El cast tiene que aplicarse a uno de los operandos.

División por cero

Depende del tipo, y la diferencia es importante:

int a = 5 / 0;         // ArithmeticException: / by zero -> el programa se detiene
double b = 5.0 / 0;    // Infinity  -> el programa CONTINUA
double c = 0.0 / 0;    // NaN (Not a Number) -> el programa CONTINUA

La división entera por cero lanza un error de ejecución. La división decimal produce valores especiales (Infinity, NaN) que se propagan silenciosamente por todos los cálculos posteriores, lo que puede ser peor: acabas mostrando NaN EUR en un recibo sin haber visto ningún error.

Aplicado a BiblioTech

final int DIAS_PRESTAMO = 15;

int diasTranscurridos = 27;
int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;   // 12 dias de retraso

final double TARIFA_DIARIA = 0.25;
double multa = diasRetraso * TARIFA_DIARIA;            // 12 * 0.25 = 3.0

System.out.println("Dias de retraso: " + diasRetraso);
System.out.println("Multa: " + multa + " EUR");

Aquí diasRetraso es int y TARIFA_DIARIA es double: Java promociona el entero a decimal antes de multiplicar y el resultado es correcto. La promoción automática nos salva; la división es la única que hay que vigilar.

  1. El operador módulo

% devuelve el resto de una división entera. Es mucho más útil de lo que parece a primera vista.

System.out.println(17 % 5);     // 2  (17 = 5*3 + 2)
System.out.println(12 % 4);     // 0  (division exacta)
System.out.println(3 % 5);      // 3  (3 = 5*0 + 3)
System.out.println(-7 % 3);     // -1 (en Java el signo lo da el DIVIDENDO)
System.out.println(7.5 % 2);    // 1.5 (tambien funciona con decimales)

Usos habituales:

Objetivo Expresión Explicación
¿Es par? n % 2 == 0 Resto cero al dividir entre 2
¿Es múltiplo de 7? n % 7 == 0 Generalización del anterior
Último dígito n % 10 Resto de dividir entre 10
Ciclar en un rango 0..n−1 i % n Nunca se sale del rango
Descomponer tiempo minutos % 60 Minutos que sobran de las horas completas

Aplicado a BiblioTech, para expresar el retraso en semanas y días sueltos:

int diasRetraso = 27;

int semanasRetraso = diasRetraso / 7;    // division entera: 3 semanas
int diasSueltos = diasRetraso % 7;       // resto: 6 dias

System.out.println("Retraso: " + semanasRetraso + " semanas y " + diasSueltos + " dias");
// Retraso: 3 semanas y 6 dias

Fíjate en cómo / y % trabajan en pareja: uno da el cociente, el otro lo que sobra. Es un patrón que reaparecerá muchas veces.

  1. Operadores de asignación compuesta

Son atajos para operar sobre una variable y guardar el resultado en ella misma:

Operador Equivalente a Ejemplo
+= x = x + y multa += 0.25;
-= x = x - y diasRetraso -= 3;
*= x = x * y multa *= 2;
/= x = x / y total /= 4;
%= x = x % y contador %= 7;
double multa = 3.0;
multa += 1.50;      // multa vale ahora 4.5
multa *= 2;         // multa vale ahora 9.0

Hay un detalle poco conocido: la asignación compuesta incluye un cast implícito. Esto compila:

int diasRetraso = 12;
diasRetraso += 0.75;                 // compila: equivale a diasRetraso = (int)(diasRetraso + 0.75)
System.out.println(diasRetraso);     // 12  <- el 0.75 se pierde en el truncamiento

mientras que la forma "larga" equivalente no compila:

diasRetraso = diasRetraso + 0.75;    // ERROR: incompatible types: possible lossy conversion

Es una asimetría del lenguaje que puede ocultar pérdidas de precisión sin ningún aviso. Merece la pena conocerla.

  1. Incremento y decremento: pre y post

++ suma 1 y -- resta 1. Lo interesante es que se pueden escribir antes o después de la variable, y eso cambia el valor de la expresión:

  • Post-incremento x++: usa el valor actual, y después incrementa.
  • Pre-incremento ++x: incrementa primero, y después usa el valor nuevo.
int a = 5;
int resultadoA = a++;     // resultadoA recibe 5, y luego a pasa a valer 6
System.out.println("a = " + a + ", resultadoA = " + resultadoA);
// a = 6, resultadoA = 5

int b = 5;
int resultadoB = ++b;     // b pasa a valer 6 primero, y resultadoB recibe 6
System.out.println("b = " + b + ", resultadoB = " + resultadoB);
// b = 6, resultadoB = 6

La tabla resume la diferencia partiendo siempre de x = 5:

Expresión Valor de la expresión Valor final de x
x++ 5 6
++x 6 6
x-- 5 4
--x 4 4

Cuándo importa de verdad: si el ++ está solo en su propia línea (contador++;), pre y post son idénticos y da igual cuál uses. La diferencia solo se manifiesta cuando el valor de la expresión se usa para algo, como en una asignación, un índice o un argumento.

Consejo práctico: no escribas expresiones que dependan de esa sutileza. Código como int x = i++ + ++i; es un acertijo, no un programa. Cuando importe, sepáralo en dos líneas claras.

  1. Operadores relacionales

Comparan dos valores y siempre producen un boolean:

Operador Significado Ejemplo Resultado
== Igual a diasRetraso == 0 false si hay retraso
!= Distinto de diasRetraso != 0 true si hay retraso
> Mayor que 27 > 15 true
< Menor que 12 < 15 true
>= Mayor o igual 15 >= 15 true
<= Menor o igual 20 <= 15 false
final int DIAS_PRESTAMO = 15;
int diasTranscurridos = 27;

boolean hayRetraso = diasTranscurridos > DIAS_PRESTAMO;
System.out.println("Hay retraso: " + hayRetraso);    // true

Repite la idea de la lección anterior: una comparación es una expresión que vale true o false por sí misma. No necesitas un if para obtenerla; el if (módulo 2) sirve para decidir qué hacer con ella.

Dos advertencias:

  • == compara valores en primitivos, pero referencias en objetos. Es el tema del apartado 11.
  • El error tipográfico más caro de la programación es escribir = (asignación) donde querías == (comparación). En Java, afortunadamente, if (x = 5) no compila salvo que x sea boolean, porque una asignación de int no produce un boolean. El compilador te protege.

  1. Operadores lógicos y evaluación de cortocircuito

Combinan valores booleanos:

Operador Nombre Resultado
&& Y lógico (AND) true solo si ambos son true
|| O lógico (OR) true si al menos uno es true
! Negación (NOT) Invierte el valor

Tablas de verdad:

a b a && b a || b !a
true true true true false
true false false true false
false true false true true
false false false false true

Aplicado a BiblioTech:

boolean disponible = false;
int diasRetraso = 12;
boolean esEmpleadoActivo = true;

boolean estaPrestadoYConRetraso = !disponible && diasRetraso > 0;   // true
boolean necesitaAviso = diasRetraso > 0 || !disponible;             // true
boolean puedeReservar = disponible && esEmpleadoActivo;             // false

La evaluación de cortocircuito

Esta es la parte importante. && y || son perezosos: dejan de evaluar en cuanto el resultado ya está decidido.

  • En a && b, si a es false, el resultado es false pase lo que pase con b. b no se evalúa.
  • En a || b, si a es true, el resultado es true pase lo que pase con b. b no se evalúa.
flowchart TD
    A["evaluar a"] --> B{"¿qué operador?"}
    B -- "AND lógico" --> C{"¿a es false?"}
    C -- "Sí" --> D["resultado false<br/><b>b NO se evalúa</b>"]
    C -- "No" --> E["evaluar b<br/>resultado = b"]
    B -- "OR lógico" --> F{"¿a es true?"}
    F -- "Sí" --> G["resultado true<br/><b>b NO se evalúa</b>"]
    F -- "No" --> H["evaluar b<br/>resultado = b"]

¿Por qué importa? Porque permite proteger operaciones peligrosas colocando la comprobación de seguridad primero:

String empleado = null;

// Esto FALLA con NullPointerException: se intenta llamar a length() sobre null
boolean valido = empleado.length() > 0 && empleado != null;

// Esto FUNCIONA: si empleado es null, la primera condicion es false
// y la segunda ni siquiera se evalua
boolean valido = empleado != null && empleado.length() > 0;

El orden de las condiciones no es estético: es funcional. Recuérdalo, porque es un patrón que usarás constantemente a partir del módulo 6.

Las versiones sin cortocircuito: & y |

Existen & y | aplicados a booleanos, que siempre evalúan ambos lados:

boolean r1 = false && metodoQueImprime();   // el metodo NO se ejecuta
boolean r2 = false &  metodoQueImprime();   // el metodo SI se ejecuta

En la práctica se usan && y || casi siempre. Recuerda que & y | tienen además otro significado completamente distinto sobre números, que es el siguiente apartado.

  1. El operador ternario

Es el único operador de Java con tres operandos. Su forma es:

condicion ? valorSiVerdadero : valorSiFalso

Se lee: "si la condición es verdadera, el resultado es lo primero; si no, lo segundo".

int diasTranscurridos = 27;
final int DIAS_PRESTAMO = 15;

int diasRetraso = diasTranscurridos > DIAS_PRESTAMO
        ? diasTranscurridos - DIAS_PRESTAMO
        : 0;

System.out.println("Dias de retraso: " + diasRetraso);   // 12

Esta línea hace algo muy valioso para nosotros: evita que el retraso sea negativo. Si el libro se devuelve a los 10 días, 10 - 15 daría -5, y una multa negativa sería un absurdo (¡la biblioteca pagando al empleado!). El ternario nos permite corregirlo sin usar if, que todavía no hemos visto.

El uso más habitual es adaptar un texto:

double multa = diasRetraso * 0.25;
String estado = diasRetraso > 0 ? "CON RETRASO" : "EN PLAZO";
String moneda = multa == 1.0 ? "euro" : "euros";

System.out.println("Estado del prestamo: " + estado);
System.out.println("Importe: " + multa + " " + moneda);

Se pueden anidar, pero la legibilidad se degrada muy rápido:

// Legible al limite
String categoria = diasRetraso == 0 ? "PUNTUAL"
                 : diasRetraso <= 7 ? "LEVE"
                 : "GRAVE";

Regla práctica: el ternario es excelente para elegir un valor; en cuanto quieras ejecutar varias acciones distintas, necesitas un if, que llega en la lección 02-01.

  1. Operadores a nivel de bit y desplazamiento

Estos operadores trabajan sobre la representación binaria de los números enteros. Al principio parecen esotéricos, pero aparecen en permisos, banderas, criptografía, procesamiento de imágenes y protocolos de red, y en las entrevistas técnicas.

Recordatorio: un int son 32 bits. El número 12 es 00000000 00000000 00000000 00001100, y el 10 es ...00001010.

Los operadores bit a bit

Operador Nombre Regla por bit 12 op 10 Resultado
& AND 1 solo si ambos son 1 1100 & 1010 1000 = 8
| OR 1 si alguno es 1 1100 | 1010 1110 = 14
^ XOR 1 si son distintos 1100 ^ 1010 0110 = 6
~ NOT Invierte todos los bits ~12 -13
int a = 12;   // 1100
int b = 10;   // 1010

System.out.println(a & b);    // 8   -> 1000
System.out.println(a | b);    // 14  -> 1110
System.out.println(a ^ b);    // 6   -> 0110
System.out.println(~a);       // -13

// Ver la representacion binaria es la mejor forma de entenderlo
System.out.println(Integer.toBinaryString(a));       // 1100
System.out.println(Integer.toBinaryString(a & b));   // 1000

El resultado de ~12 = -13 sorprende. La razón es que Java representa los enteros negativos en complemento a dos, donde ~x equivale siempre a -x - 1.

Los operadores de desplazamiento

Operador Nombre Qué hace Ejemplo Resultado
<< Desplazamiento a la izquierda Mueve los bits a la izquierda, rellena con ceros 12 << 2 48
>> Desplazamiento a la derecha con signo Mueve a la derecha, conserva el signo 12 >> 2 3
>>> Desplazamiento a la derecha sin signo Mueve a la derecha, rellena con ceros -12 >>> 28 15

La relación con la aritmética es directa y muy fácil de recordar:

  • x << n equivale a multiplicar por 2ⁿ
  • x >> n equivale a dividir entre 2ⁿ (división entera)
System.out.println(12 << 1);    // 24   (12 * 2)
System.out.println(12 << 2);    // 48   (12 * 4)
System.out.println(12 >> 1);    // 6    (12 / 2)
System.out.println(12 >> 2);    // 3    (12 / 4)
System.out.println(-12 >> 2);   // -3   conserva el signo
System.out.println(-12 >>> 2);  // 1073741821  rellena con ceros: el signo se pierde

Un uso realista: banderas de permisos

El caso donde estos operadores brillan de verdad es empaquetar varias opciones booleanas en un solo entero. En BiblioTech podríamos codificar los permisos de un empleado sobre el catálogo:

// Cada permiso ocupa un bit distinto
final int PERMISO_CONSULTAR = 1;    // 0001
final int PERMISO_PRESTAR   = 2;    // 0010
final int PERMISO_RESERVAR  = 4;    // 0100
final int PERMISO_ADMINISTRAR = 8;  // 1000

// Marta Ruiz puede consultar, tomar prestado y reservar
int permisosMarta = PERMISO_CONSULTAR | PERMISO_PRESTAR | PERMISO_RESERVAR;   // 0111 = 7

// Comprobar un permiso concreto: AND con la bandera
boolean puedeReservar    = (permisosMarta & PERMISO_RESERVAR) != 0;      // true
boolean puedeAdministrar = (permisosMarta & PERMISO_ADMINISTRAR) != 0;   // false

System.out.println("Permisos de Marta Ruiz: " + permisosMarta);
System.out.println("Puede reservar: " + puedeReservar);
System.out.println("Puede administrar: " + puedeAdministrar);

Cuatro booleanos en un solo int, con comprobaciones instantáneas. Es el mecanismo que usan por debajo los permisos de ficheros de Unix y muchísimas APIs.

  1. El operador instanceof

instanceof comprueba si un objeto es de un tipo determinado y devuelve un boolean:

String titulo = "Java Efectivo";
boolean esTexto = titulo instanceof String;   // true

Solo se menciona aquí para completar el catálogo de operadores. Su verdadera utilidad aparece cuando existen jerarquías de tipos y polimorfismo, es decir, a partir del módulo 3. Desde Java 16 admite además la coincidencia de patrones (if (obj instanceof String s)), que se estudia en el módulo 10.

  1. Precedencia y asociatividad

Cuando una expresión combina varios operadores, Java aplica un orden fijo. De mayor a menor prioridad:

Nivel Operadores Asociatividad
1 () [] . Izquierda a derecha
2 ++ -- (postfijos) Izquierda a derecha
3 ++ -- (prefijos), + - (unarios), !, ~, casts Derecha a izquierda
4 * / % Izquierda a derecha
5 + - Izquierda a derecha
6 << >> >>> Izquierda a derecha
7 < <= > >= instanceof Izquierda a derecha
8 == != Izquierda a derecha
9 & Izquierda a derecha
10 ^ Izquierda a derecha
11 | Izquierda a derecha
12 && Izquierda a derecha
13 || Izquierda a derecha
14 ? : (ternario) Derecha a izquierda
15 = += -= *= /= %= Derecha a izquierda

La asociatividad decide qué pasa cuando hay operadores del mismo nivel: 10 - 4 - 3 se agrupa como (10 - 4) - 3 = 3, no como 10 - (4 - 3) = 9.

Ejemplos donde la precedencia cambia el resultado:

System.out.println(2 + 3 * 4);        // 14, no 20: * va antes que +
System.out.println((2 + 3) * 4);      // 20

System.out.println(10 - 4 - 3);       // 3: asociativo por la izquierda

// Trampa clasica: & tiene MENOS prioridad que !=
// (a & b) != 0  es lo correcto; a & b != 0 se agrupa como a & (b != 0) y ni compila

Y un caso real de BiblioTech donde faltan paréntesis:

int diasTranscurridos = 27;
final int DIAS_PRESTAMO = 15;
final double TARIFA_DIARIA = 0.25;

// MAL: se evalua (diasTranscurridos) - (DIAS_PRESTAMO * TARIFA_DIARIA)
double multa = diasTranscurridos - DIAS_PRESTAMO * TARIFA_DIARIA;   // 23.25, absurdo

// BIEN: los parentesis expresan la intencion
double multa = (diasTranscurridos - DIAS_PRESTAMO) * TARIFA_DIARIA; // 3.0

El consejo definitivo: no memorices la tabla. Usa paréntesis. Nadie ha perdido nunca una revisión de código por poner paréntesis de más, y sí muchas horas por confiar en la precedencia. Los paréntesis documentan tu intención.

  1. Comparar cadenas: == frente a equals

Es el error clásico por excelencia de Java, y lo peor es que a veces funciona, lo que retrasa el diagnóstico durante semanas.

String a = "Java Efectivo";
String b = "Java Efectivo";
System.out.println(a == b);         // true  (!)

String c = new String("Java Efectivo");
System.out.println(a == c);         // false (!)
System.out.println(a.equals(c));    // true

Por qué ocurre: el pool de cadenas

Recuerda de la lección anterior que una variable String no contiene el texto, sino una referencia a un objeto en el montículo. Y == sobre referencias compara si apuntan al mismo objeto, no si el contenido es igual.

La JVM mantiene un pool de cadenas (string pool): una zona especial donde guarda los literales. Cuando compila String a = "Java Efectivo"; y luego String b = "Java Efectivo";, ve que el literal es idéntico y hace que ambas variables apunten al mismo objeto del pool. Por eso a == b da true. Es una optimización de memoria, no una promesa del lenguaje.

Pero new String("Java Efectivo") fuerza la creación de un objeto nuevo, fuera del pool. Ahora hay dos objetos con el mismo texto en direcciones distintas, y == da false.

flowchart LR
    subgraph Pila
        A["a"]
        B["b"]
        C["c"]
    end
    subgraph Pool["String pool"]
        P["#p1<br/>'Java Efectivo'"]
    end
    subgraph Heap["Montículo"]
        H["#h9<br/>'Java Efectivo'"]
    end
    A --> P
    B --> P
    C --> H

a == b compara #p1 == #p1true. a == c compara #p1 == #h9false. En ambos casos el texto es el mismo.

La regla, sin excepciones

Para comparar el contenido de dos String, usa siempre equals(). Nunca ==.

String tituloIntroducido = "java efectivo";
String tituloCatalogo = "Java Efectivo";

System.out.println(tituloIntroducido.equals(tituloCatalogo));             // false
System.out.println(tituloIntroducido.equalsIgnoreCase(tituloCatalogo));   // true

Por qué el peligro es real: las cadenas que llegan de un Scanner, de un fichero o de la red no están en el pool. Así que tu comparación con == funcionará perfectamente en las pruebas con literales y fallará con datos reales.

Una técnica defensiva habitual es poner el literal a la izquierda, porque un literal nunca es null:

// Si isbn fuera null, esto lanzaria NullPointerException
if (isbn.equals("978-0000000001")) { ... }

// Esto es seguro incluso si isbn es null
if ("978-0000000001".equals(isbn)) { ... }

Resumen en tabla:

Comparación Con primitivos Con objetos (String)
== Compara valores. Correcto Compara referencias. Casi siempre incorrecto
equals() No existe (los primitivos no son objetos) Compara contenido. Correcto

  1. La precisión de double y por qué existe BigDecimal

Un último aviso, particularmente relevante porque BiblioTech maneja dinero:

System.out.println(0.1 + 0.2);              // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);       // false
System.out.println(1.03 - 0.42);            // 0.6100000000000001

Esto no es un fallo de Java: ocurre en Python, JavaScript, C y prácticamente en todos los lenguajes. La causa es que float y double siguen el estándar IEEE 754, que representa los números en binario. Del mismo modo que en decimal no puedes escribir 1/3 exactamente (0,3333…), en binario no puedes escribir 0,1 exactamente. El valor almacenado es una aproximación buenísima, pero aproximación.

Consecuencias prácticas:

  • Nunca compares dos double con ==. Compara si su diferencia es menor que una tolerancia: Math.abs(a - b) < 0.0001.
  • Nunca uses double para dinero en un sistema real. Los errores se acumulan a lo largo de miles de operaciones y acaban descuadrando la contabilidad.

La solución de Java es la clase BigDecimal, que representa decimales con precisión exacta y permite controlar el redondeo:

import java.math.BigDecimal;

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));      // 0.3 exacto

Tiene su coste: es más verboso (no se puede usar +, hay que llamar a add()) y más lento. Se estudia en el módulo 10; en este curso seguiremos usando double para las multas de BiblioTech porque el foco está en aprender el lenguaje, pero quedas advertido: en un sistema de facturación real, BigDecimal.

Errores Comunes y Consejos

  • 5 / 2 da 2. División entera. Aplica un cast a uno de los operandos: (double) 5 / 2.
  • (double)(5 / 2) sigue dando 2.0. El cast llega después de la división. Debe aplicarse a un operando, no al resultado.
  • Comparar String con ==. Funciona con literales y falla con datos de entrada. Usa equals() siempre.
  • Comparar double con ==. Usa una tolerancia.
  • Olvidar paréntesis en (a - b) * c. La multiplicación tiene más prioridad que la resta.
  • Confundir & con &&. Sobre booleanos, && evita evaluar el segundo operando; & no. Sobre enteros, & es una operación bit a bit completamente distinta.
  • Ordenar mal las condiciones protegidas. x != null && x.length() > 0, nunca al revés.
  • Escribir expresiones acrobáticas con ++. Si tienes que pararte a pensar, refactorízalo en dos líneas.
  • Consejo: usa paréntesis siempre que la expresión tenga más de dos operadores. No hay penalización de rendimiento y la ganancia en claridad es enorme.
  • Consejo: Integer.toBinaryString(n) es la mejor herramienta para entender los operadores de bits. Pruébalo en JShell.
  • Consejo: el ternario es perfecto para saturar valores a cero (x > 0 ? x : 0), un patrón que usarás mucho en BiblioTech para que los días de retraso nunca sean negativos.

Ejercicios

Ejercicio 1: Predecir la salida

Sin ejecutar el código, escribe la salida de cada línea y justifica el porqué.

public class PruebaOperadores {
    public static void main(String[] args) {
        System.out.println(7 / 2);
        System.out.println(7 % 2);
        System.out.println(7.0 / 2);
        System.out.println((double)(7 / 2));

        int x = 5;
        System.out.println(x++);
        System.out.println(x);
        System.out.println(++x);

        System.out.println(2 + 3 * 4);
        System.out.println(10 - 4 - 3);

        String a = "Refactorizacion";
        String b = "Refactorizacion";
        String c = new String("Refactorizacion");
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));

        System.out.println(0.1 + 0.2 == 0.3);
        System.out.println(12 & 10);
        System.out.println(12 << 2);
    }
}

Ejercicio 2: Calculadora de multa de BiblioTech

Escribe la clase CalculadoraMulta que, a partir de las siguientes constantes y datos, calcule y muestre la liquidación de un préstamo:

  • Constantes: DIAS_PRESTAMO = 15, TARIFA_DIARIA = 0.25, MULTA_MAXIMA = 20.0, RECARGO_GRAVE = 1.5 (multiplicador que se aplica si el retraso supera los 30 días).
  • Datos del préstamo: empleado "Diego Alonso", libro "Refactorizacion", diasTranscurridos = 52.

Debe calcular y mostrar:

  1. Los días de retraso, que nunca pueden ser negativos (usa el operador ternario).
  2. El retraso expresado en semanas completas y días sueltos (usa / y %).
  3. La multa base (diasRetraso * TARIFA_DIARIA).
  4. La multa con recargo si el retraso supera 30 días (usa el ternario).
  5. La multa final, sin superar nunca MULTA_MAXIMA (usa el ternario).
  6. Una etiqueta de gravedad: "EN PLAZO", "LEVE" (1-7 días) o "GRAVE" (más de 7), con ternarios anidados.

Todo sin usar if.

Ejercicio 3: Banderas de permisos

Usando los operadores a nivel de bit, escribe la clase PermisosBiblioTech que:

  1. Defina cuatro constantes de permiso con valores 1, 2, 4 y 8: consultar, prestar, reservar y administrar.
  2. Componga los permisos de tres empleados de Nexus Software: Marta Ruiz (consultar + prestar), Diego Alonso (consultar + prestar + reservar) y Nuria Vidal (los cuatro).
  3. Muestre para cada uno el valor entero de sus permisos, su representación binaria y si puede administrar.
  4. Añada el permiso de reservar a Marta Ruiz usando |= y muestre el resultado.

Soluciones

Solución 1

3
1
3.5
3.0
5
6
7
14
3
true
false
true
false
8
48

Justificaciones:

  • 7 / 2 → 3. Ambos operandos son int: división entera truncada.
  • 7 % 2 → 1. El resto de dividir 7 entre 2.
  • 7.0 / 2 → 3.5. Al ser 7.0 un double, el 2 se promociona y la división es decimal.
  • (double)(7 / 2) → 3.0. Los paréntesis fuerzan la división entera primero (3) y el cast llega después.
  • x++ → 5, y a continuación x vale 6. El post-incremento devuelve el valor anterior.
  • x → 6. Confirma el incremento previo.
  • ++x → 7. El pre-incremento incrementa primero (7) y devuelve el valor nuevo.
  • 2 + 3 * 4 → 14. * tiene más precedencia que +: se calcula 3*4=12 y luego 2+12.
  • 10 - 4 - 3 → 3. Asociatividad por la izquierda: (10-4)-3.
  • a == b → true. Ambos son literales idénticos, así que la JVM los comparte en el pool de cadenas y las dos referencias apuntan al mismo objeto.
  • a == c → false. new String(...) crea un objeto distinto fuera del pool; las referencias difieren aunque el texto sea igual.
  • a.equals(c) → true. equals compara el contenido, que sí es idéntico.
  • 0.1 + 0.2 == 0.3 → false. La representación IEEE 754 de 0.1 y 0.2 es aproximada; la suma da 0.30000000000000004.
  • 12 & 10 → 8. En binario, 1100 & 1010 = 1000, que es 8.
  • 12 << 2 → 48. Desplazar 2 bits a la izquierda equivale a multiplicar por 2² = 4.

Solución 2

public class CalculadoraMulta {

    public static void main(String[] args) {

        // === Reglas de negocio de BiblioTech (Nexus Software) ===
        final int DIAS_PRESTAMO = 15;        // dias de prestamo sin recargo
        final double TARIFA_DIARIA = 0.25;   // euros por dia de retraso
        final double MULTA_MAXIMA = 20.0;    // tope absoluto por prestamo
        final double RECARGO_GRAVE = 1.5;    // multiplicador si el retraso pasa de 30 dias
        final int UMBRAL_RECARGO = 30;
        final int UMBRAL_LEVE = 7;

        // === Datos del prestamo ===
        String empleado = "Diego Alonso";
        String titulo = "Refactorizacion";
        String isbn = "978-0000000003";
        int diasTranscurridos = 52;

        // 1. Dias de retraso, saturado a cero con el operador ternario.
        //    Si se devolviera antes de tiempo, la resta seria negativa y
        //    produciria una multa negativa, que carece de sentido.
        int diasRetraso = diasTranscurridos > DIAS_PRESTAMO
                ? diasTranscurridos - DIAS_PRESTAMO
                : 0;                                          // 52 - 15 = 37

        // 2. Descomposicion en semanas y dias: / da el cociente, % el resto.
        int semanas = diasRetraso / 7;                        // 5
        int diasSueltos = diasRetraso % 7;                    // 2

        // 3. Multa base. int * double -> Java promociona el int a double.
        double multaBase = diasRetraso * TARIFA_DIARIA;       // 37 * 0.25 = 9.25

        // 4. Recargo por retraso grave, de nuevo con el ternario.
        double multaConRecargo = diasRetraso > UMBRAL_RECARGO
                ? multaBase * RECARGO_GRAVE
                : multaBase;                                  // 9.25 * 1.5 = 13.875

        // 5. Tope: la multa nunca supera MULTA_MAXIMA.
        double multaFinal = multaConRecargo > MULTA_MAXIMA
                ? MULTA_MAXIMA
                : multaConRecargo;                            // 13.875 < 20 -> 13.875

        // 6. Etiqueta de gravedad con ternarios anidados.
        //    Se evaluan en cadena, de arriba abajo, como una escalera.
        String gravedad = diasRetraso == 0 ? "EN PLAZO"
                        : diasRetraso <= UMBRAL_LEVE ? "LEVE"
                        : "GRAVE";

        // === Salida ===
        System.out.println("=== BiblioTech - Liquidacion de prestamo ===");
        System.out.println("Empleado:        " + empleado);
        System.out.println("Libro:           " + titulo);
        System.out.println("ISBN:            " + isbn);
        System.out.println("Dias transcurridos: " + diasTranscurridos
                + " (prestamo estandar: " + DIAS_PRESTAMO + " dias)");
        System.out.println();
        System.out.println("Dias de retraso: " + diasRetraso
                + " (" + semanas + " semanas y " + diasSueltos + " dias)");
        System.out.println("Gravedad:        " + gravedad);
        System.out.println("Multa base:      " + multaBase + " EUR");
        System.out.println("Con recargo:     " + multaConRecargo + " EUR");
        System.out.println("MULTA FINAL:     " + multaFinal + " EUR");
    }
}

Salida:

=== BiblioTech - Liquidacion de prestamo ===
Empleado:        Diego Alonso
Libro:           Refactorizacion
ISBN:            978-0000000003
Dias transcurridos: 52 (prestamo estandar: 15 dias)

Dias de retraso: 37 (5 semanas y 2 dias)
Gravedad:        GRAVE
Multa base:      9.25 EUR
Con recargo:     13.875 EUR
MULTA FINAL:     13.875 EUR

Observa el 13.875: tres decimales para un importe en euros. Es exactamente el tipo de resultado que en un sistema real exigiría BigDecimal o, como mínimo, un formateo a dos decimales. Eso último lo resolveremos en la lección siguiente con printf.

Solución 3

public class PermisosBiblioTech {

    public static void main(String[] args) {

        // 1. Cada permiso ocupa UN bit distinto: potencias de 2.
        //    Asi ninguno interfiere con los demas al combinarlos.
        final int CONSULTAR    = 1;   // 0001
        final int PRESTAR      = 2;   // 0010
        final int RESERVAR     = 4;   // 0100
        final int ADMINISTRAR  = 8;   // 1000

        // 2. Se combinan con OR: activa los bits de ambos operandos.
        int permisosMarta = CONSULTAR | PRESTAR;                          // 0011 = 3
        int permisosDiego = CONSULTAR | PRESTAR | RESERVAR;               // 0111 = 7
        int permisosNuria = CONSULTAR | PRESTAR | RESERVAR | ADMINISTRAR; // 1111 = 15

        // 3. Se comprueba con AND: si el bit esta activo, el resultado no es cero.
        boolean martaAdmin = (permisosMarta & ADMINISTRAR) != 0;   // false
        boolean diegoAdmin = (permisosDiego & ADMINISTRAR) != 0;   // false
        boolean nuriaAdmin = (permisosNuria & ADMINISTRAR) != 0;   // true

        System.out.println("=== BiblioTech - Permisos de catalogo ===");
        System.out.println("Marta Ruiz:   " + permisosMarta
                + " (" + Integer.toBinaryString(permisosMarta) + ")"
                + " admin=" + martaAdmin);
        System.out.println("Diego Alonso: " + permisosDiego
                + " (" + Integer.toBinaryString(permisosDiego) + ")"
                + " admin=" + diegoAdmin);
        System.out.println("Nuria Vidal:  " + permisosNuria
                + " (" + Integer.toBinaryString(permisosNuria) + ")"
                + " admin=" + nuriaAdmin);

        // 4. Anadir un permiso con |= : enciende ese bit sin tocar los demas.
        permisosMarta |= RESERVAR;                                 // 0011 | 0100 = 0111 = 7

        System.out.println();
        System.out.println("Tras conceder RESERVAR a Marta Ruiz:");
        System.out.println("Marta Ruiz:   " + permisosMarta
                + " (" + Integer.toBinaryString(permisosMarta) + ")"
                + " puede reservar=" + ((permisosMarta & RESERVAR) != 0));
    }
}

Salida:

=== BiblioTech - Permisos de catalogo ===
Marta Ruiz:   3 (11) admin=false
Diego Alonso: 7 (111) admin=false
Nuria Vidal:  15 (1111) admin=true

Tras conceder RESERVAR a Marta Ruiz:
Marta Ruiz:   7 (111) puede reservar=true

Dos detalles que merecen atención:

  • Los paréntesis en (permisosMarta & ADMINISTRAR) != 0 son obligatorios, no opcionales. & tiene menos precedencia que !=, así que sin ellos Java intentaría evaluar permisosMarta & (ADMINISTRAR != 0), lo que ni siquiera compila por mezclar int y boolean.
  • Integer.toBinaryString no muestra los ceros a la izquierda: el 3 aparece como 11, no como 0011. Es normal; representa el mismo valor.

Conclusión

Ya dominas el catálogo completo de operadores de Java: la aritmética con su trampa de la división entera, el módulo y su pareja natural con /, la asignación compuesta con su cast implícito oculto, el incremento pre y post y cuándo la diferencia importa, las comparaciones que producen booleanos, los operadores lógicos con la evaluación de cortocircuito que te permite proteger expresiones peligrosas, el ternario para elegir valores sin if, las operaciones a nivel de bit aplicadas a banderas de permisos, la precedencia y el consejo de usar paréntesis, la diferencia crítica entre == y equals explicada por el pool de cadenas, y las limitaciones de precisión de double que un día te llevarán a BigDecimal. Con eso, BiblioTech ya sabe calcular una multa completa: días de retraso saturados a cero, recargo por gravedad y tope máximo.

En la siguiente lección, Entrada y Salida por Consola, resolveremos lo que ha quedado feo en estos ejemplos: dejaremos de escribir importes como 13.875 y aprenderemos a formatear la salida con printf (%.2f, anchos, alineación), y sobre todo dejaremos de tener los datos escritos a fuego en el código para pedírselos al usuario con Scanner. Es el último ingrediente antes de construir la primera versión completa de BiblioTechApp.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados