Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Tipos de Datos en Java

Universidad Nacional de Rio Negro - Sede Andina

En el capítulo anterior vimos que Java tiene un sistema de tipos estático y fuertemente tipado: los tipos se declaran explícitamente, se verifican en tiempo de compilación, y las conversiones con pérdida de información requieren casting explícito.

Este capítulo profundiza en los tipos de datos concretos que ofrece Java: los 8 tipos primitivos, sus rangos, cómo se representan en memoria, y todas las reglas de conversión entre ellos.

Declaración de Variables

Una variable es un espacio en memoria con un nombre que almacena un valor de un tipo específico. Pensá en una variable como una caja etiquetada: la etiqueta es el nombre, el tipo define qué puede contener la caja, y el valor es lo que está adentro.

La sintaxis para declarar variables es idéntica a C:

tipo nombreVariable;           // Declaración (reserva espacio, sin valor inicial)
tipo nombreVariable = valor;   // Declaración con inicialización (reserva y asigna)

Declaración de variables

¿Qué pasa en cada línea?

  1. Declaración sin inicialización: Le decís al compilador “necesito espacio para guardar un valor de este tipo, y lo voy a llamar así”. El espacio se reserva en memoria, pero no tiene un valor definido todavía.

  2. Declaración con inicialización: Además de reservar espacio, le asignás un valor inicial inmediatamente. Es lo más seguro porque garantiza que la variable siempre tiene un valor conocido.

Ejemplos de Declaración

int edad;                    // Declaración sin inicialización
edad = 25;                   // Asignación posterior

int cantidad = 100;          // Declaración con inicialización
double precio = 19.99;
char letra = 'A';
boolean activo = true;

// Declaración múltiple del mismo tipo (todas int)
int x, y, z;
int a = 1, b = 2, c = 3;

Declaración e inicialización de variables

Observá que la declaración múltiple (int x, y, z;) es exactamente igual que en C. Declara tres variables del tipo int en una sola línea.

Convención de Nombres

Java tiene convenciones de nombres que toda la comunidad sigue. Aunque el compilador no te obliga, seguir estas convenciones hace tu código más legible y profesional.

Java usa camelCase para nombres de variables y métodos. El nombre empieza en minúscula, y cada palabra nueva empieza con mayúscula (sin guiones ni guiones bajos):

// ✅ Correcto (camelCase para variables y métodos)
int edadUsuario;
double precioTotal;
boolean estaActivo;
String nombreCompleto;

// ❌ Incorrecto (estilos de otros lenguajes)
int edad_usuario;    // snake_case (estilo C/Python) - NO usar
int EdadUsuario;     // PascalCase - reservado para nombres de clases
int EDAD_USUARIO;    // SCREAMING_SNAKE_CASE - reservado para constantes
int edadusuario;     // Sin separación - difícil de leer

Convenciones de nomenclatura

¿Por qué importa esto?

Cuando ves EdadUsuario en código Java, inmediatamente sabés que es una clase, no una variable. Cuando ves MAX_INTENTOS, sabés que es una constante. Las convenciones comunican información sin necesidad de leer más código.

Tipos Primitivos

Un tipo primitivo es un tipo de dato básico provisto directamente por el lenguaje. No es un objeto, no tiene métodos, y se almacena directamente en la variable (no como referencia a otro lugar de memoria). Son los “átomos” con los que se construyen datos más complejos.

Java define exactamente 8 tipos primitivos. Este número es fijo —no se pueden crear nuevos tipos primitivos, solo usar estos ocho.

¿Qué significa “tipo primitivo”?

En Java hay dos categorías de tipos:

  1. Tipos primitivos: Almacenan valores directamente. Son los 8 que veremos acá.

  2. Tipos de referencia: Almacenan una “dirección” que apunta a un objeto en memoria (similar a punteros en C, pero manejados automáticamente).

Por ahora nos enfocamos en los primitivos. La diferencia práctica es que los primitivos son más eficientes (no hay indirección) y tienen tamaños garantizados.

Portabilidad: Tamaños Garantizados

Una diferencia fundamental con C es que Java garantiza el tamaño de cada tipo en todas las plataformas. No importa si compilás para Windows, Linux, Mac, un teléfono Android o un servidor —un int siempre ocupa 32 bits y tiene el mismo rango.

En C, el tamaño de int puede ser 16, 32 o 64 bits dependiendo del compilador y la arquitectura. Esto causaba (y causa) muchos problemas de portabilidad. Java eliminó este problema definiendo tamaños fijos.

Los 8 tipos primitivos de Java organizados por categoría: enteros, punto flotante, booleano y carácter.

Figure 1:Los 8 tipos primitivos de Java organizados por categoría: enteros, punto flotante, booleano y carácter.

Tabla de Tipos Primitivos

Table 1:Especificación técnica de tipos primitivos

TipoTamaño (bits)Rango de valoresEquivalente en CUso típico
byte8-128 a 127signed charDatos binarios, ahorro de memoria
short1632,768-32,768 a 32,76732,767shortPoco usado (histórico)
int32-231 a 23112^{31}-1int (32-bit)El tipo entero principal
long64-263 a 26312^{63}-1long longNúmeros muy grandes
float32±3.4×1038\pm 3.4 \times 10^{38}floatPrecisión simple (raro en Java)
double64±1.7×10308\pm 1.7 \times 10^{308}doubleEl tipo decimal principal
boolean1*true o false(no existe)Condiciones lógicas
char160 a 65,53565,535(16-bit Unicode)Caracteres individuales

*El tamaño exacto de boolean no está especificado por la JVM; típicamente usa 1 byte por eficiencia de acceso a memoria.

¿Por qué tantos tipos enteros?

Podrías preguntarte: ¿por qué no usar siempre long que es el más grande? La respuesta es eficiencia:

En la práctica, para la mayoría de los casos usamos int para enteros y double para decimales. Los otros tipos se usan en situaciones específicas.

Comparativa de Tamaños con C

AspectoCJava
Tamaño de intVaría (16, 32 o 64 bits según plataforma)Siempre 32 bits
Tamaño de longVaría (32 o 64 bits según plataforma)Siempre 64 bits
Tipo booleanoNo existe (usa int: 0=falso, ≠0=verdadero)boolean nativo (true/false)
Tamaño de char8 bits (ASCII)16 bits (Unicode UTF-16)
Tipos sin signounsigned int, unsigned char, etc.No existen (todos con signo, excepto char)

Tipos Enteros en Detalle

Los tipos enteros almacenan números sin parte decimal. Java tiene cuatro tipos enteros, cada uno con diferente tamaño y rango. Todos usan representación en complemento a dos para números negativos (igual que en C).

byte (8 bits)

El tipo entero más pequeño. Almacena valores de -128 a 127.

¿Cuándo usar byte?

byte edad = 25;          // OK: 25 está en el rango
byte temperatura = -10;  // OK: valores negativos permitidos
byte maximo = 127;       // OK: valor máximo
byte minimo = -128;      // OK: valor mínimo

// byte overflow = 128;  // ❌ ERROR de compilación: 128 fuera de rango

// ¿Qué pasa con overflow en tiempo de ejecución?
byte b = 127;
b = (byte)(b + 1);       // b = -128 (overflow, da la vuelta)

Uso de byte

short (16 bits)

Entero de 16 bits. Rango: -32,768 a 32,767.

¿Cuándo usar short?

short poblacion = 30000;   // OK para ciudades pequeñas
short altitud = -500;      // Metros bajo el nivel del mar
short temperatura = 32767; // Máximo valor

// En la práctica, casi siempre es mejor usar int
int poblacionMejor = 30000;  // Más idiomático en Java

Uso de short

int (32 bits) — El Tipo Entero Principal

El tipo entero más común y el tipo por defecto para literales enteros. Cuando escribís 42 en tu código, Java lo interpreta como int.

Rango: aproximadamente ±2.1 mil millones (-231 a 23112^{31}-1)

¿Cuándo usar int?

int cantidad = 1000000;          // Un millón
int saldo = -50000;              // Puede ser negativo
int poblacionCiudad = 45000000;  // Buenos Aires

// Constantes útiles de la clase Integer
int maximo = Integer.MAX_VALUE;  // 2147483647
int minimo = Integer.MIN_VALUE;  // -2147483648

// El literal 42 es automáticamente int
int respuesta = 42;

Uso de int

long (64 bits)

Para valores que exceden el rango de int. Rango: aproximadamente ±9.2 quintillones.

¿Cuándo usar long?

long poblacionMundial = 8_000_000_000L;  // 8 mil millones
long distanciaLuna = 384_400_000L;       // metros
long timestampActual = System.currentTimeMillis();  // Milisegundos desde 1970

// Constantes útiles
long maxLong = Long.MAX_VALUE;   // 9223372036854775807
long minLong = Long.MIN_VALUE;   // -9223372036854775808

Uso de long

Literales Enteros en Diferentes Bases

Un literal es un valor escrito directamente en el código (como 42 o "hola"). Al igual que en C, Java permite escribir números enteros en diferentes bases numéricas:

int decimal = 42;        // Base 10 (decimal) - sin prefijo
int binario = 0b101010;  // Base 2 (binario) - prefijo 0b o 0B
int octal = 052;         // Base 8 (octal) - prefijo 0
int hexadecimal = 0x2A;  // Base 16 (hexadecimal) - prefijo 0x o 0X

// Todos representan el mismo valor: 42
System.out.println(decimal);      // 42
System.out.println(binario);      // 42
System.out.println(octal);        // 42
System.out.println(hexadecimal);  // 42

Literales en diferentes bases

¿Por qué usar diferentes bases?

Separador de Dígitos (Guión Bajo)

Desde Java 7, podés usar guiones bajos (_) dentro de los números para mejorar la legibilidad. El compilador los ignora —son puramente visuales:

// Sin separadores (difícil de leer)
long poblacion = 8000000000L;
int binario = 0b11110000111100001111000011110000;

// Con separadores (mucho más claro)
long poblacionClara = 8_000_000_000L;            // 8 mil millones
int binarioClaro = 0b1111_0000_1111_0000_1111_0000_1111_0000;
double precio = 1_234_567.89;
long telefono = 011_4444_5555L;

// Reglas: no pueden estar al inicio, al final, ni junto al punto decimal
// int invalido1 = _1000;     // ❌ ERROR
// int invalido2 = 1000_;     // ❌ ERROR
// double invalido3 = 1_.5;   // ❌ ERROR

Guiones bajos como separadores visuales

Tip: Usá separadores para agrupar de a 3 dígitos (como los miles) o de a 4 bits en binario/hexadecimal.

Tipos de Punto Flotante

Los tipos de punto flotante almacenan números con parte decimal (también llamados “números reales” en matemática, aunque la representación no es exacta). Java tiene dos tipos: float (precisión simple) y double (precisión doble).

¿Qué significa “punto flotante”?

El nombre viene de cómo se almacenan estos números: el “punto decimal” puede “flotar” a diferentes posiciones usando un exponente. Es similar a la notación científica:

3.14159=3.14159×1003.14159 = 3.14159 \times 10^0
0.000314159=3.14159×1040.000314159 = 3.14159 \times 10^{-4}
314159=3.14159×105314159 = 3.14159 \times 10^{5}

El mismo conjunto de dígitos (3.14159) representa diferentes magnitudes moviendo el punto.

float (32 bits) — Precisión Simple

Almacena aproximadamente 7 dígitos significativos de precisión. Los literales deben terminar con F o f.

¿Cuándo usar float?

float temperatura = 36.5f;   // La f es OBLIGATORIA
float pi = 3.14159f;
float pequeno = 1.5e-10f;    // Notación científica: 1.5 × 10⁻¹⁰

// Sin la f, Java interpreta el literal como double
// float error = 36.5;   // ❌ ERROR: incompatible types (double to float)
float correcto = 36.5f;     // ✅ Con sufijo f
float tambienOk = (float)36.5;  // ✅ Con casting explícito

Uso de float

double (64 bits) — Precisión Doble — El Tipo Decimal Principal

Almacena aproximadamente 15-16 dígitos significativos de precisión. Es el tipo por defecto para literales decimales —cuando escribís 3.14 sin sufijo, Java lo interpreta como double.

¿Cuándo usar double?

double precio = 19.99;       // No necesita sufijo (es el tipo por defecto)
double pi = 3.141592653589793;
double avogadro = 6.022e23;  // Notación científica: 6.022 × 10²³
double pequeno = 1.5e-300;   // Número muy pequeño

// Constantes útiles de la clase Double
double maxDouble = Double.MAX_VALUE;  // ≈1.7 × 10³⁰⁸
double minPositivo = Double.MIN_VALUE;  // ≈4.9 × 10⁻³²⁴ (mínimo positivo)

Uso de double

Notación Científica

Para números muy grandes o muy pequeños, usá notación científica con e (o E):

// numeroEexponente significa numero × 10^exponente
double grande = 1.5e10;    // 1.5 × 10¹⁰ = 15,000,000,000
double pequeno = 2.5e-8;   // 2.5 × 10⁻⁸ = 0.000000025
double velocidadLuz = 3e8; // 3 × 10⁸ m/s

// También funciona con float (agregá la f)
float grandeF = 1.5e10f;

Notación científica

Representación IEEE 754 — Cómo se almacenan los decimales

Los tipos float y double siguen el estándar internacional IEEE 754, igual que en C y prácticamente todos los lenguajes modernos. Entender este estándar te ayuda a comprender por qué los números decimales a veces se comportan de formas “extrañas”.

Un número de punto flotante se descompone en tres partes:

La fórmula para reconstruir el número es:

x=(1)s×1.m×2esesgox = (-1)^s \times 1.m \times 2^{e-sesgo}

Donde:

Representación IEEE 754 de un número double de 64 bits, mostrando la distribución de bits entre signo, exponente y mantisa.

Figure 2:Representación IEEE 754 de un número double de 64 bits, mostrando la distribución de bits entre signo, exponente y mantisa.

¿Por qué importa esto?

La cantidad de bits en la mantisa limita la precisión (cuántos dígitos significativos podés representar). Los bits del exponente limitan el rango (cuán grande o pequeño puede ser el número).

TipoBits mantisaDígitos significativosRango aproximado
float23~710-38 a 1038
double52~15-1610-308 a 10308

El Problema de la Precisión — ¡Importante!

Aquí viene un concepto fundamental que causa confusión a muchos programadores: la mayoría de los números decimales no tienen representación binaria exacta.

Esto no es un bug de Java —es una limitación matemática de representar fracciones decimales en binario. El número 0.1 en decimal es una fracción infinita en binario, igual que 1/3 = 0.333... es infinito en decimal.

double resultado = 0.1 + 0.2;
System.out.println(resultado);  // Imprime: 0.30000000000000004 (¡no 0.3!)

// ¿Por qué? 0.1 y 0.2 no tienen representación binaria exacta
// La suma acumula pequeños errores de redondeo

// Otro ejemplo clásico
double suma = 0.0;
for (int i = 0; i < 10; i = i + 1) {
    suma = suma + 0.1;
}
System.out.println(suma);  // Imprime: 0.9999999999999999 (¡no 1.0!)

Errores de precisión de punto flotante

Este comportamiento es idéntico en C y en casi todos los lenguajes. No es un defecto de Java.

Valores Especiales de Punto Flotante

IEEE 754 define valores especiales para representar situaciones matemáticas excepcionales. Java los expone como constantes:

// Infinitos (cuando el resultado es demasiado grande)
double infinito = Double.POSITIVE_INFINITY;     // +∞
double negInfinito = Double.NEGATIVE_INFINITY;  // -∞

// Not a Number (resultado indefinido o inválido)
double noEsNumero = Double.NaN;  // "Not a Number"

// ¿Cómo se generan estos valores?
double divisionPorCero = 1.0 / 0.0;    // POSITIVE_INFINITY (¡no es error!)
double divNegativa = -1.0 / 0.0;       // NEGATIVE_INFINITY
double overflow = 1e308 * 10;          // POSITIVE_INFINITY (overflow)
double indeterminado = 0.0 / 0.0;      // NaN (0/0 es indefinido)
double raizNegativa = Math.sqrt(-1);   // NaN (raíz de negativo)

// Comparación con Double.POSITIVE_INFINITY funciona normalmente
System.out.println(divisionPorCero == Double.POSITIVE_INFINITY);  // true

Valores especiales de punto flotante

Diferencia con C: En C, dividir un entero por cero causa un error fatal (crash). En Java con punto flotante, obtenés infinito o NaN sin que el programa falle. Sin embargo, dividir enteros por cero en Java sí lanza una excepción (ArithmeticException).

double d = 1.0 / 0.0;  // OK, d = Infinity
int i = 1 / 0;         // ❌ ArithmeticException: / by zero

El Tipo boolean — Verdadero o Falso

El tipo boolean representa valores lógicos: solo puede ser true (verdadero) o false (falso). No hay otros valores posibles.

Diferencia Crítica con C

Esta es una de las diferencias más importantes entre Java y C:

AspectoCJava
Tipo booleanoNo existe nativamenteboolean nativo
Valores “verdaderos”Cualquier valor ≠ 0Solo true
Valores “falsos”El valor 0Solo false
En condiciones (if, while)Acepta cualquier expresión numéricaSolo expresiones boolean

En C, 0 es “falso” y cualquier otro número es “verdadero”. Esto permite código como:

// En C: Esto compila y funciona (aunque es confuso)
int x = 5;
if (x) {
    printf("x es distinto de cero\n");
}

int ptr = NULL;
if (!ptr) {
    printf("ptr es nulo\n");
}

Java rechaza esto completamente:

boolean activo = true;
boolean encontrado = false;

// ✅ Correcto en Java: condiciones booleanas explícitas
if (activo) {
    System.out.println("Está activo");
}

int x = 5;
// ❌ En Java esto NO compila
// if (x) { ... }           // ERROR: int no es boolean

// ✅ Correcto: comparación explícita
if (x != 0) {
    System.out.println("x es distinto de cero");
}

// ❌ No se puede asignar int a boolean
// boolean b = 1;           // ERROR: incompatible types
// boolean b = 0;           // ERROR: incompatible types

// ✅ Correcto
boolean b = true;
boolean c = (x > 0);  // Resultado de comparación es boolean

Uso de boolean

¿Por qué Java es tan estricto?

Esta restricción previene una clase muy común de bugs en C: confundir asignación (=) con comparación (==):

// En C: BUG clásico — esto compila pero es un error lógico
int x = 0;
if (x = 5) {    // ¡Asignación, no comparación!
    // Siempre se ejecuta porque x=5 retorna 5, que es "verdadero"
}
// En Java: El compilador detecta el error
int x = 0;
// if (x = 5) {  // ❌ ERROR de compilación: int cannot be converted to boolean
// }

if (x == 5) {   // ✅ Correcto: comparación
    // ...
}

El tipado estricto de Java convierte un bug silencioso en un error de compilación.

Operaciones con boolean

Los valores boolean solo participan en operaciones lógicas:

boolean a = true;
boolean b = false;

boolean and = a && b;    // false (AND lógico)
boolean or = a || b;     // true (OR lógico)
boolean not = !a;        // false (NOT lógico)

// Comparaciones producen boolean
int x = 10;
int y = 20;
boolean mayor = x > y;      // false
boolean igual = x == y;     // false
boolean diferente = x != y; // true

Operaciones booleanas

El Tipo char — Caracteres Unicode

El tipo char almacena un carácter individual. Pero hay una diferencia importante con C:

AspectoCJava
Tamaño8 bits16 bits
CodificaciónASCII (128 caracteres)Unicode UTF-16 (65,536 caracteres)
Caracteres especialesSolo inglés básicoAcentos, ñ, cirílico, griego, chino, emoji, etc.

¿Por qué 16 bits?

ASCII (7-8 bits) fue diseñado para inglés y solo tiene 128 caracteres. No incluye acentos, ni la ñ, ni caracteres de otros idiomas.

Unicode es un estándar internacional que asigna un número único a cada carácter de (casi) todos los idiomas del mundo. Java adoptó Unicode desde su primera versión (1995), lo que lo hizo verdaderamente internacional.

char letra = 'A';          // Carácter ASCII básico
char digito = '7';         // Los dígitos también son caracteres
char simbolo = '@';
char enie = 'ñ';           // ¡Directamente soportado!
char omega = 'Ω';          // Letra griega
char corazon = '♥';        // Símbolo especial

// Secuencias de escape para caracteres especiales
char nuevaLinea = '\n';    // Salto de línea
char tabulacion = '\t';    // Tabulación
char comillaSimple = '\''; // Comilla simple (escapada)
char barraInvertida = '\\';// Barra invertida (escapada)

// Caracteres Unicode por código hexadecimal
char omegaPorCodigo = '\u03A9';    // Ω (U+03A9)
char corazonPorCodigo = '\u2665';  // ♥ (U+2665)

Uso de char

Secuencias de Escape

Las secuencias de escape permiten representar caracteres que no se pueden escribir directamente:

SecuenciaSignificadoCódigo ASCII
\nNueva línea (line feed)10
\tTabulación horizontal9
\rRetorno de carro (carriage return)13
\\Barra invertida literal92
\'Comilla simple literal39
\"Comilla doble literal34
\0Carácter nulo0
\uXXXXCarácter Unicode (XXXX en hexadecimal)
// Imprimir texto con formato
System.out.println("Línea 1\nLínea 2\nLínea 3");
// Salida:
// Línea 1
// Línea 2
// Línea 3

System.out.println("Columna1\tColumna2\tColumna3");
// Salida:
// Columna1    Columna2    Columna3

System.out.println("Ella dijo: \"Hola\"");
// Salida:
// Ella dijo: "Hola"

System.out.println("Ruta: C:\\Users\\Juan");
// Salida:
// Ruta: C:\Users\Juan

Secuencias de escape en acción

char como Valor Numérico

Al igual que en C, char es técnicamente un tipo numérico. Internamente almacena el código Unicode del carácter (un número entre 0 y 65535). Esto permite hacer aritmética con caracteres:

char letra = 'A';
int codigo = letra;              // codigo = 65 (código ASCII/Unicode de 'A')
System.out.println(codigo);      // Imprime: 65

// Obtener la siguiente letra
char siguiente = (char)(letra + 1);  // siguiente = 'B' (código 66)
System.out.println(siguiente);   // Imprime: B

// Convertir minúscula a mayúscula (los códigos difieren en 32)
char minuscula = 'a';            // código 97
char mayuscula = (char)(minuscula - 32);  // código 65 = 'A'
// O más legible:
mayuscula = (char)(minuscula - 'a' + 'A');

// Iterar sobre todas las letras del alfabeto
for (char c = 'a'; c <= 'z'; c = (char)(c + 1)) {
    System.out.print(c);  // Imprime: abcdefghijklmnopqrstuvwxyz
}
System.out.println();

// Verificar si un char es un dígito
char caracter = '7';
boolean esDigito = (caracter >= '0' && caracter <= '9');  // true

// Convertir char dígito a su valor numérico
int valor = caracter - '0';  // valor = 7 (no 55 que es el código de '7')

char como valor numérico

Diferencia con C: char es sin signo

En Java, char es el único tipo primitivo sin signo. Su rango es 0 a 65535 (no tiene valores negativos). En C, char puede ser con signo o sin signo dependiendo del compilador.

Constantes con final

Una constante es una variable cuyo valor no puede cambiar después de la inicialización. En Java, se declaran con la palabra clave final.

¿Para qué sirven las constantes?

  1. Evitar “números mágicos”: En vez de escribir 7 por todos lados, usás DIAS_POR_SEMANA. Más legible y si cambia, lo modificás en un solo lugar.

  2. Prevenir errores: El compilador impide que modifiques el valor accidentalmente.

  3. Documentación implícita: El nombre de la constante explica qué significa el valor.

final int MAX_INTENTOS = 3;
final double PI = 3.14159265359;
final char SEPARADOR = ',';
final String MENSAJE_ERROR = "Operación inválida";

// Convención: SCREAMING_SNAKE_CASE para constantes
final int DIAS_POR_SEMANA = 7;
final double GRAVEDAD = 9.81;
final int PUERTO_HTTP = 80;

// Una vez asignado, no se puede cambiar
// MAX_INTENTOS = 5;  // ❌ ERROR: cannot assign a value to final variable

Declaración de constantes

Comparativa con C

CJava
#define MAX 100 (preprocesador, sin tipo)final int MAX = 100; (con tipo)
const int MAX = 100; (variable constante)final int MAX = 100;

La diferencia es que #define en C es una sustitución textual del preprocesador (sin verificación de tipos), mientras que final en Java crea una variable real con tipo verificado.

Constantes locales vs constantes de clase

Las constantes pueden ser locales (dentro de un método) o de clase. Las constantes de clase se verán más adelante; por ahora, usá final para valores que no deben cambiar dentro de tu método:

public static void main(String[] args) {
    final double TASA_IVA = 0.21;
    final int DESCUENTO_MAXIMO = 50;
    
    double precio = 100.0;
    double precioConIva = precio * (1 + TASA_IVA);
    
    System.out.println(precioConIva);  // 121.0
}

Constantes locales en un método

Operadores

Los operadores son símbolos que realizan operaciones sobre valores (operandos). Java tiene operadores muy similares a C, con algunas diferencias importantes.

Operadores Aritméticos

Realizan operaciones matemáticas básicas. Idénticos a C:

OperadorOperaciónEjemploResultado
+Suma5 + 38
-Resta5 - 32
*Multiplicación5 * 315
/División5 / 31 (entera) o 1.666... (flotante)
%Módulo (resto)5 % 32
int a = 17;
int b = 5;

int suma = a + b;       // 22
int resta = a - b;      // 12
int producto = a * b;   // 85
int cociente = a / b;   // 3 (división ENTERA, trunca el decimal)
int resto = a % b;      // 2 (17 = 5×3 + 2)

// El operador - también es unario (negación)
int negativo = -a;      // -17

Operadores aritméticos

División Entera vs División Real — ¡Cuidado!

Este es un punto que causa muchos errores. La división entre enteros siempre produce un entero, truncando (no redondeando) la parte decimal:

// División entre enteros → resultado entero (truncado)
int resultado1 = 7 / 2;     // 3 (no 3.5, no 4)
int resultado2 = 1 / 2;     // 0 (no 0.5)
int resultado3 = -7 / 2;    // -3 (trunca hacia cero)

// Para obtener decimal, al menos UN operando debe ser double/float
double decimal1 = 7.0 / 2;      // 3.5 (7.0 es double)
double decimal2 = 7 / 2.0;      // 3.5 (2.0 es double)
double decimal3 = (double)7 / 2; // 3.5 (casting a double)
double decimal4 = 7 / (double)2; // 3.5

// ❌ ERROR COMÚN: asignar división entera a double
double mal = 7 / 2;  // mal = 3.0 (primero divide enteros → 3, luego convierte a double)

División entera vs división real

Este comportamiento es idéntico en C. La regla es: si ambos operandos son enteros, el resultado es entero.

Operador Módulo (Resto)

El operador % devuelve el resto de la división entera. Muy útil para:

// Verificar si un número es par o impar
int numero = 17;
boolean esPar = (numero % 2 == 0);  // false (17 es impar)

// Obtener el último dígito de un número
int n = 12345;
int ultimoDigito = n % 10;  // 5

// Ciclar valores (ej: horas del reloj)
int hora = 25;
int horaReal = hora % 24;  // 1 (25 horas = 1 hora del día siguiente)

// Verificar divisibilidad
boolean divisiblePor5 = (numero % 5 == 0);  // false

Usos del operador módulo

Operadores de Asignación Compuesta — Prohibidos en el Curso

Referencia: Estos son los operadores que existen pero no debés usar en el curso:

OperadorEquivalente explícito
a += ba = a + b
a -= ba = a - b
a *= ba = a * b
a /= ba = a / b
a %= ba = a % b
a++ o ++aa = a + 1
a-- o --aa = a - 1

Operadores Relacionales (Comparación)

Comparan dos valores y devuelven un boolean (true o false). Idénticos a C:

OperadorSignificadoEjemploResultado
==Igual a5 == 5true
!=Distinto de5 != 3true
<Menor que3 < 5true
>Mayor que3 > 5false
<=Menor o igual5 <= 5true
>=Mayor o igual3 >= 5false
int edad = 18;

boolean esMayor = edad >= 18;       // true
boolean esExacto = edad == 18;      // true
boolean esDiferente = edad != 21;   // true
boolean esMenor = edad < 18;        // false

// Pueden usarse directamente en condiciones
if (edad >= 18) {
    System.out.println("Es mayor de edad");
}

Operadores relacionales

Operadores Lógicos

Operan sobre valores boolean y devuelven boolean. Son fundamentales para construir condiciones complejas:

OperadorSignificadoResultado
&&AND lógicotrue solo si ambos son true
||OR lógicotrue si al menos uno es true
!NOT lógicoInvierte el valor

Tablas de verdad:

ABA && BA || B
truetruetruetrue
truefalsefalsetrue
falsetruefalsetrue
falsefalsefalsefalse
A!A
truefalse
falsetrue
boolean a = true;
boolean b = false;

boolean and = a && b;  // false (true Y false = false)
boolean or = a || b;   // true (true O false = true)
boolean not = !a;      // false (NO true = false)

// Ejemplos prácticos
int edad = 25;
boolean tieneLicencia = true;

// Puede manejar si es mayor de 18 Y tiene licencia
boolean puedeManejar = (edad >= 18) && tieneLicencia;  // true

// Descuento si es estudiante O es jubilado
boolean esEstudiante = true;
boolean esJubilado = false;
boolean tieneDescuento = esEstudiante || esJubilado;   // true

// Entrada gratuita si NO es mayor de 12
int edadVisitante = 10;
boolean entradaGratis = !(edadVisitante > 12);  // true (10 no es > 12)
// Equivalente más claro:
entradaGratis = edadVisitante <= 12;

Operadores lógicos

Evaluación en Cortocircuito — Muy Importante

Los operadores && y || usan evaluación en cortocircuito (short-circuit evaluation): si el resultado se puede determinar con el primer operando, el segundo no se evalúa.

// && (AND): si el primero es false, el resultado ya es false
// → no evalúa el segundo
boolean resultado1 = false && metodoQueNuncaSeEjecuta();  // false, no llama al método

// || (OR): si el primero es true, el resultado ya es true
// → no evalúa el segundo
boolean resultado2 = true || metodoQueNuncaSeEjecuta();   // true, no llama al método

Cortocircuito en operadores lógicos

¿Para qué sirve? El cortocircuito es muy útil para evitar errores:

// Evitar división por cero
int divisor = 0;
// Si divisor es 0, la primera condición es false y NO evalúa la división
if (divisor != 0 && numero / divisor > 10) {
    // ...
}

// Evitar NullPointerException (más adelante verás referencias)
String texto = null;
// Si texto es null, no intenta llamar .length() (que fallaría)
if (texto != null && texto.length() > 5) {
    // ...
}

// Sin cortocircuito, esto fallaría si texto es null

Uso práctico del cortocircuito

Operadores de Bits

Operan sobre la representación binaria de los números, bit a bit. Idénticos a C (con una adición):

OperadorOperaciónEjemplo
&AND bit a bit0b1010 & 0b11000b1000
|OR bit a bit0b1010 | 0b11000b1110
^XOR bit a bit0b1010 ^ 0b11000b0110
~Complemento (NOT)~0b10100b...0101
<<Desplazamiento izquierda0b0001 << 20b0100
>>Desplazamiento derecha (con signo)0b1000 >> 20b0010
>>>Desplazamiento derecha (sin signo)Solo en Java
int a = 0b1010;  // 10 en decimal
int b = 0b1100;  // 12 en decimal

int and = a & b;   // 0b1000 = 8   (bits en común)
int or = a | b;    // 0b1110 = 14  (bits de alguno)
int xor = a ^ b;   // 0b0110 = 6   (bits diferentes)
int not = ~a;      // Complemento a uno (invierte todos los bits)

// Desplazamientos
int izq = a << 2;  // 0b101000 = 40 (equivale a multiplicar por 4)
int der = a >> 1;  // 0b0101 = 5    (equivale a dividir por 2)

Operadores de bits

¿Para qué sirven? Los operadores de bits se usan para:

// Verificar si un número es par (bit menos significativo = 0)
int numero = 42;
boolean esPar = (numero & 1) == 0;  // true (42 en binario termina en 0)

// Multiplicar por 2, 4, 8... (desplazar a la izquierda)
int x = 5;
int porDos = x << 1;    // 10
int porCuatro = x << 2; // 20
int porOcho = x << 3;   // 40

// Dividir por 2, 4, 8... (desplazar a la derecha)
int y = 40;
int entreDos = y >> 1;    // 20
int entreCuatro = y >> 2; // 10

// Extraer el byte menos significativo de un int
int valor = 0x12345678;
int byteMenor = valor & 0xFF;  // 0x78 = 120

// Intercambiar dos valores sin variable temporal (truco clásico)
int p = 5, q = 3;
p = p ^ q;  // p = 6
q = p ^ q;  // q = 5
p = p ^ q;  // p = 3
// Ahora p = 3, q = 5

Ejemplos prácticos de operadores de bits

Operador >>> — Exclusivo de Java

Java tiene un operador de desplazamiento a la derecha sin signo (>>>), que no existe en C. La diferencia:

int negativo = -8;  // En binario: 11111111111111111111111111111000

int conSigno = negativo >> 2;   // -2 (rellena con 1s, preserva signo)
int sinSigno = negativo >>> 2;  // 1073741822 (rellena con 0s)

// Con números positivos, ambos dan el mismo resultado
int positivo = 8;
int a = positivo >> 2;   // 2
int b = positivo >>> 2;  // 2

Diferencia entre >> y >>>

Precedencia de Operadores

Cuando una expresión tiene múltiples operadores, Java los evalúa en un orden específico llamado precedencia. Los operadores con mayor precedencia se evalúan primero.

PrecedenciaOperadoresAsociatividad
1 (mayor)() (paréntesis)
2! ~ - (unarios)Derecha a izquierda
3* / %Izquierda a derecha
4+ -Izquierda a derecha
5<< >> >>>Izquierda a derecha
6< <= > >=Izquierda a derecha
7== !=Izquierda a derecha
8&Izquierda a derecha
9^Izquierda a derecha
10|Izquierda a derecha
11&&Izquierda a derecha
12||Izquierda a derecha
13 (menor)= (asignación)Derecha a izquierda
// Multiplicación antes que suma
int a = 2 + 3 * 4;      // 2 + 12 = 14, no 20

// Paréntesis fuerzan orden
int b = (2 + 3) * 4;    // 5 * 4 = 20

// Comparación antes que lógicos
boolean c = 5 > 3 && 2 < 4;  // (5 > 3) && (2 < 4) = true && true = true

// AND antes que OR
boolean d = true || false && false;  // true || (false && false) = true || false = true
boolean e = (true || false) && false; // true && false = false

Ejemplos de precedencia

Conversiones de Tipo (Casting)

En ocasiones necesitás convertir un valor de un tipo a otro. Java tiene dos tipos de conversiones:

  1. Implícitas (automáticas): Java las hace sin que lo pidas

  2. Explícitas (casting): Tenés que indicarlas con (tipo)

Conversiones Implícitas (Promoción Automática / Widening)

Java promueve automáticamente tipos más pequeños a más grandes cuando no hay riesgo de perder información. Esto se llama widening (ensanchamiento) porque el tipo destino es “más ancho” (tiene más bits).

La regla general es: si el tipo destino puede contener todos los valores posibles del tipo origen, la conversión es automática.

Jerarquía de promoción automática entre tipos primitivos. Las flechas indican conversiones implícitas permitidas.

Figure 3:Jerarquía de promoción automática entre tipos primitivos. Las flechas indican conversiones implícitas permitidas.

Cadena de promoción:

byte → short → int → long → float → double
         ↑
        char
byte b = 10;
short s = b;     // ✅ OK: byte cabe en short
int i = s;       // ✅ OK: short cabe en int
long l = i;      // ✅ OK: int cabe en long
float f = l;     // ✅ OK: long cabe en float (pero puede perder precisión)
double d = f;    // ✅ OK: float cabe en double

// char también se promueve a int
char c = 'A';
int codigo = c;  // ✅ OK: codigo = 65

Promoción automática (widening)

Conversiones Explícitas (Casting / Narrowing)

Cuando querés convertir de un tipo más grande a uno más pequeño, hay riesgo de perder información. Java no hace esto automáticamente —tenés que indicarlo explícitamente con un cast: (tipo)valor.

Esto se llama narrowing (estrechamiento) porque el tipo destino es “más estrecho”.

double d = 9.99;
int i = (int) d;       // i = 9 (trunca la parte decimal, NO redondea)

long l = 1000L;
int j = (int) l;       // ✅ OK si el valor cabe en int

float f = 3.7f;
int k = (int) f;       // k = 3 (trunca)

// Casting entre enteros
int grande = 300;
byte b = (byte) grande;  // b = 44 (overflow, 300 no cabe en byte)

Casting explícito (narrowing)

¿Qué pasa con el overflow en casting?

Cuando el valor no cabe en el tipo destino, Java trunca los bits más significativos. El resultado puede ser muy diferente al original:

// int usa 32 bits, byte usa 8 bits
// Solo se mantienen los 8 bits menos significativos

int valor = 300;           // En binario: 00000000 00000000 00000001 00101100
byte b = (byte) valor;     // Solo toma:                         00101100 = 44

int negativo = -1;         // En binario: 11111111 11111111 11111111 11111111
byte bn = (byte) negativo; // Solo toma:                         11111111 = -1

int grande = 128;          // En binario: 00000000 00000000 00000000 10000000
byte bg = (byte) grande;   // Solo toma:                         10000000 = -128 (!)

Overflow en casting

Truncamiento vs Redondeo

El casting de double/float a entero trunca (elimina la parte decimal), no redondea:

double d1 = 9.1;
double d2 = 9.9;
double d3 = -9.9;

int i1 = (int) d1;  // 9 (no 9)
int i2 = (int) d2;  // 9 (no 10 — trunca, no redondea)
int i3 = (int) d3;  // -9 (trunca hacia cero, no hacia -10)

// Si querés redondear, usá Math.round()
long redondeado = Math.round(9.5);  // 10
int redondeadoInt = (int) Math.round(9.5);  // 10

Truncamiento en casting

Promoción Automática en Expresiones

Cuando operás con valores de diferentes tipos en una expresión, Java promueve automáticamente al tipo “más grande” antes de operar. Esta regla es idéntica a C:

Reglas de promoción en expresiones:

  1. Si hay un double, todo se convierte a double

  2. Si no hay double pero hay float, todo se convierte a float

  3. Si no hay flotantes pero hay long, todo se convierte a long

  4. En cualquier otro caso, todo se convierte a int (incluyendo byte, short, char)

int entero = 5;
double decimal = 2.0;
double resultado = entero / decimal;  // entero se promueve a double → 2.5

// Regla importante: byte, short y char se promueven a int en CUALQUIER operación
byte x = 10;
byte y = 20;
// byte z = x + y;  // ❌ ERROR: x + y es int, no byte
int z = x + y;      // ✅ OK: el resultado es int
byte z2 = (byte)(x + y);  // ✅ OK con casting explícito

// Esto también aplica a operaciones unarias (excepto asignación)
short s = 100;
// short s2 = -s;   // ❌ ERROR: -s es int
int s2 = -s;        // ✅ OK

Promoción en expresiones

¿Por qué byte + byte da int?

Esta regla existe para evitar overflows silenciosos. Si byte x = 100 y byte y = 100, entonces x + y = 200, que no cabe en byte (-128 a 127). Java promueve a int para que el cálculo intermedio no haga overflow.

byte a = 100;
byte b = 100;

// Sin promoción automática (hipotético), esto daría overflow:
// byte c = a + b;  // 200 no cabe en byte → resultado incorrecto

// Con promoción automática:
int c = a + b;      // c = 200 (correcto, cabe en int)

// Si realmente querés un byte (sabiendo que puede haber overflow):
byte d = (byte)(a + b);  // d = -56 (overflow intencional)

Ejemplo de por qué se promueve a int

Resumen de Conversiones

ConversiónTipoSintaxis¿Pérdida de información?
byteintImplícitaint i = b;No
intlongImplícitalong l = i;No
intdoubleImplícitadouble d = i;No
longfloatImplícitafloat f = l;Posible (precisión)
doubleintExplícitaint i = (int) d;Sí (trunca)
longintExplícitaint i = (int) l;Posible (overflow)
intbyteExplícitabyte b = (byte) i;Posible (overflow)

Conversiones entre String y Tipos Primitivos

El casting no funciona entre String y tipos primitivos porque son categorías completamente diferentes. Para estas conversiones, Java provee métodos específicos.

De String a tipos primitivos (parsing)

Para convertir texto a número, usás los métodos parse de las clases envolventes (wrapper classes):

String textoEntero = "100";
int numero = Integer.parseInt(textoEntero);     // 100
System.out.println(numero + 50);                // 150

String textoDecimal = "19.99";
double precio = Double.parseDouble(textoDecimal);  // 19.99

String textoLargo = "9876543210";
long grande = Long.parseLong(textoLargo);       // 9876543210L

String textoByte = "42";
byte pequeño = Byte.parseByte(textoByte);       // 42

// Para boolean
String textoBoolean = "true";
boolean activo = Boolean.parseBoolean(textoBoolean);  // true
// Nota: cualquier valor distinto de "true" (ignorando mayúsculas) da false

Parsing de String a tipos numéricos

De tipos primitivos a String

Para convertir en la dirección opuesta, tenés varias opciones:

int numero = 42;
double precio = 19.99;
boolean activo = true;

// Opción 1: String.valueOf() — la más explícita
String s1 = String.valueOf(numero);   // "42"
String s2 = String.valueOf(precio);   // "19.99"
String s3 = String.valueOf(activo);   // "true"

// Opción 2: concatenación con String vacío — común pero menos clara
String s4 = "" + numero;              // "42"
String s5 = "" + precio;              // "19.99"

// Opción 3: métodos toString() de las clases wrapper
String s6 = Integer.toString(numero);      // "42"
String s7 = Double.toString(precio);       // "19.99"

// Opción 4: con formato específico
String s8 = String.format("%d", numero);   // "42"
String s9 = String.format("%.2f", precio); // "19.99" (2 decimales)

Conversión de primitivos a String

Comparación con Python

Python, al igual que Java, requiere conversiones explícitas entre tipos incompatibles (tipado fuerte). La diferencia clave es cuándo se detectan los errores:

Python (tipado dinámico y fuerte):

print("5" + str(3))   # "53" - hay que convertir explícitamente
print("5" + 3)        # ❌ TypeError: can only concatenate str (not "int") to str
print("5" - 3)        # ❌ TypeError: unsupported operand type(s)
print("5" * 3)        # "555" - repetición de string (válido en Python)
print(int("5") * 3)   # 15 - conversión explícita a entero

Python rechaza mezclar tipos incompatibles, pero el error ocurre en tiempo de ejecución. Si el código problemático está en una rama que no se ejecuta, el programa no falla.

Java (tipado estático y fuerte):

System.out.println("5" + 3);   // "53" (concatenación, única excepción permitida)
// System.out.println("5" - 3);  // ❌ Error de compilación
// System.out.println("5" * 3);  // ❌ Error de compilación

Java detecta estos errores en tiempo de compilación, antes de ejecutar. Incluso si el código estuviera dentro de un if (false) que nunca se ejecutaría, el compilador lo rechaza.

Ejemplo práctico de la diferencia:

(en-python-funciona-hasta-que-se-ejecuta-la-rama-problematica)=
# En Python: funciona hasta que se ejecuta la rama problemática
def calcular(x, es_texto):
    if es_texto:
        return x.upper()
    else:
        return x + "!"  # Si x es int, falla aquí (solo al ejecutar)
// En Java: el compilador detecta problemas de tipos aunque no se ejecuten
public static String calcular(Object x, boolean esTexto) {
    if (esTexto) {
        return ((String) x).toUpperCase();
    } else {
        return x + "!";  // Compila: + con cualquier Object concatena a String
    }
}

Ejercicios de Aplicación

Dado el siguiente código en C, escribí el equivalente en Java indicando las diferencias principales:

int main() {
    int x = 10;
    long y = 1000000000000;
    char c = 'A';
    float f = 3.14;
    if (x) {
        printf("x es verdadero\n");
    }
    return 0;
}
Solution to Exercise 1
public static void main(String[] args) {
    int x = 10;
    long y = 1000000000000L;     // Necesita sufijo L
    char c = 'A';
    float f = 3.14f;            // Necesita sufijo f
    if (x != 0) {               // No puede usar int como boolean
        System.out.println("x es verdadero");
    }
}

Diferencias:

  1. long necesita sufijo L para literales grandes

  2. float necesita sufijo f

  3. La condición if debe ser boolean, no int

  4. printf se reemplaza por System.out.println

Solution to Exercise 2

Aunque ambos ocupan 64 bits, el long dedica todos sus bits (salvo el de signo) a la magnitud entera. En cambio, el double reparte sus 64 bits entre el signo (1), el exponente (11) y la mantisa (52). Una vez que un número entero supera la capacidad de la mantisa (253), el double debe empezar a “saltar” valores (aproximar) usando el exponente, perdiendo la precisión de la unidad.

Solution to Exercise 3
  • a = 3 — División entera, trunca el decimal

  • b = 2 — Resto de la división (17 = 5×3 + 2)

  • c = 3.0 — 17/5 se evalúa como división entera (3), luego se promueve a double

  • d = 3.4 — Al menos un operando es double, se hace división real

Solution to Exercise 4

No compila. El error es: “incompatible types: possible lossy conversion from int to byte”.

En Java, cuando se suman dos byte, el resultado se promueve automáticamente a int para evitar overflow durante el cálculo. Por eso, no se puede asignar directamente a un byte.

Corrección con casting:

byte a = 10; byte b = 20; byte c = (byte)(a + b); // Casting explícito a byte
Solution to Exercise 5
  • letra = 'D' — El carácter D

  • codigo = 68 — El código ASCII/Unicode de ‘D’ es 68

  • anterior = 'C' — El carácter anterior a ‘D’ (código 67)

  • mayuscula = 'A' — Convertimos ‘a’ (código 97) restando 32 para obtener ‘A’ (código 65)

  • esDigito = false — ‘D’ no está en el rango ‘0’-‘9’

Explicación: Los caracteres en Java son valores numéricos (su código Unicode). Podemos hacer aritmética con ellos porque internamente son números.

Solution to Exercise 6

¿Por qué no son iguales?

Los números 0.1, 0.2 y 0.3 no tienen representación binaria exacta en IEEE 754. Son fracciones infinitas en binario, igual que 1/3 = 0.333... es infinito en decimal.

  • a = 0.1 + 0.2 resulta en 0.30000000000000004

  • b = 0.3 resulta en 0.29999999999999999

Son números muy cercanos, pero no exactamente iguales.

Solución: usar tolerancia (epsilon)

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.0000001;  // Tolerancia aceptable

if (Math.abs(a - b) < epsilon) {
    System.out.println("Son iguales (dentro de tolerancia)");
} else {
    System.out.println("No son iguales");
}

Esto compara si la diferencia absoluta es menor que un valor pequeño, lo cual es la forma correcta de comparar números de punto flotante.

Solution to Exercise 7
Línea¿Compila?Explicación
int a = 1000000000000;El literal es muy grande para int (máx ~2.1 mil millones)
long b = 1000000000000;Sin sufijo L, Java interpreta el literal como int, que es muy grande
long c = 1000000000000L;Con sufijo L, el literal es long
float d = 3.14;3.14 es double, no se puede asignar a float sin cast
float e = 3.14f;Con sufijo f, el literal es float
double f = 3.14;3.14 es double por defecto
byte g = 200;200 está fuera del rango de byte (-128 a 127)
byte h = 100;100 está dentro del rango de byte
Solution to Exercise 8

Salida:

Condición falsa
Programa terminó normalmente

Explicación paso a paso:

  1. Se evalúa x != 00 != 0false

  2. Como el operador es && (AND) y el primer operando es false, el resultado ya es false sin importar el segundo operando

  3. Cortocircuito: Java NO evalúa y / x > 1 porque ya sabe que el resultado es false

  4. Esto evita la división por cero (y / x con x = 0 causaría ArithmeticException)

  5. Se ejecuta el bloque else

Si no existiera el cortocircuito, el programa fallaría con una excepción de división por cero. El cortocircuito permite usar este patrón común de “verificar antes de operar”.

Solution to Exercise 9
VariableValorExplicación
a14* tiene mayor precedencia: 2 + (3 * 4) = 2 + 12
b20Paréntesis fuerzan orden: (2 + 3) * 4 = 5 * 4
cfalse(5 > 3) = true, (2 < 1) = false, true && false = false
dtrue(5 > 3) = true, (2 < 1) = false, true || false = true
efalse(5 > 3) = true, !true = false
f9División entera: 10 / 3 = 3, luego 3 * 3 = 9
g10.0División real: 10.0 / 33.333..., luego 3.333... * 3 = 10.0

Nota sobre f y g: Esto ilustra cómo la división entera puede causar pérdida de información. Aunque matemáticamente 10 / 3 * 3 = 10, con enteros obtenemos 9.

Solution to Exercise 10
VariableValorExplicación
b1-128Overflow: 127 + 1 = 128, pero 128 no cabe en byte. “Da la vuelta” a -128 (complemento a dos)
i2-2147483648Overflow: Integer.MAX_VALUE (2147483647) + 1 “da la vuelta” a Integer.MIN_VALUE
i3-12949672963 mil millones no cabe en int (máx ~2.1 mil millones). Se truncan bits y el resultado es negativo

Lección: Java no detecta overflow en tiempo de ejecución para tipos primitivos. Es responsabilidad del programador verificar que los valores estén dentro del rango esperado.

Referencias Bibliográficas