Variables en Java — tipos primitivos, String, final y casting desde cero

Las variables en Java son el primer punto donde la diferencia con Python se hace realmente evidente. En Python una variable era simplemente un nombre que apuntaba a un valor, sin declarar nada, sin especificar tipos, sin límites de tamaño. En Java cada variable tiene un tipo fijo que se declara antes de usarla y que no puede cambiar nunca.

En este artículo vemos por qué es así, qué tipos existen, cómo convertir entre ellos y una de las trampas más clásicas de Java: la diferencia entre == y .equals().

Por qué en Java hay que declarar el tipo de cada variable

En Python las variables son dinámicas — pueden guardar cualquier tipo de dato y cambiar de tipo en cualquier momento:

# Python — sin tipos declarados
x = 5          # x es un int
x = "hola"     # ahora x es un String
x = 3.14       # ahora x es un float

Java es un lenguaje de tipado estático, el tipo de cada variable se fija en el momento de declararla y no puede cambiar nunca:

// Java — tipo declarado obligatorio
int x = 5;
x = "hola";   // error de compilación — x es int, no String
x = 3.14;     // error de compilación — x es int, no double

La razón es que Java es un lenguaje compilado. El compilador de Java necesita saber el tipo de cada variable antes de ejecutar el programa para verificar que todas las operaciones tienen sentido, que no estás sumando un texto con un número, que no estás llamando a un método que no existe en ese tipo, etc. Si hay un error de tipos, el compilador lo detecta antes de que ejecutes el programa, no en tiempo de ejecución como en Python.

La ventaja real es que los errores de tipo se detectan mucho antes y el código es más predecible. La desventaja es que hay que escribir más.

Cómo se declaran las variables en Java

La sintaxis es siempre: tipo nombre = valor;

tipo nombre = valor;
# Python — sin tipo
nombre = "Sergio"
edad = 22
nota = 7.5
aprobado = True
// Java — con tipo
String nombre = "Sergio";
int edad = 22;
double nota = 7.5;
boolean aprobado = true;

También puedes declarar sin inicializar, pero antes de usar la variable tienes que darle un valor, o el compilador da error:

int edad;          // declarada sin valor
edad = 22;         // ahora tiene valor
System.out.println(edad);    // correcto

int altura;
System.out.println(altura);  // error: variable might not have been initialized

Los tipos primitivos — qué son y por qué existen

En Java hay dos categorías de tipos: primitivos y objetos. Es una distinción fundamental que no existe en Python.

Los tipos primitivos son los tipos más básicos, guardan directamente el valor en memoria, sin ninguna envoltura. Son ocho:

byte     → entero muy pequeño         (8 bits)
short    → entero pequeño             (16 bits)
int      → entero estándar            (32 bits)  ← el más usado
long     → entero grande              (64 bits)
float    → decimal de precisión simple (32 bits)
double   → decimal de doble precisión  (64 bits)  ← el más usado
boolean  → verdadero o falso           (1 bit lógico)
char     → un carácter                 (16 bits)

En Python cuando escribías x = 5, Python creaba un objeto Integer completo con métodos y todo. En Java cuando escribes int x = 5, Java guarda solo el número 5 en bruto, nada más. Por eso los primitivos son más rápidos y ocupan menos memoria.

Los tipos enteros — byte, short, int y long

Los cuatro tipos enteros difieren solo en el tamaño, cuántos bits usan y por tanto qué rango de valores pueden guardar:

byte   → -128 a 127                        (8 bits)
short  → -32.768 a 32.767                  (16 bits)
int    → -2.147.483.648 a 2.147.483.647    (32 bits)
long   → -9.223.372.036.854.775.808
         a 9.223.372.036.854.775.807       (64 bits)
byte b = 100;
short s = 30000;
int i = 1000000;
long l = 9000000000L;    // L al final — indica que es long

¿Cuándo usar cada uno?

La regla práctica para FP2 es muy simple: usa int para casi todo. Es el tipo entero por defecto en Java y cubre todos los números que vas a usar en ejercicios académicos.

Usa long cuando el número puede superar los 2.000 millones, como IDs de usuarios en aplicaciones reales, o cálculos astronómicos. El sufijo L al final del literal es obligatorio para que Java sepa que es un long y no un int que se ha desbordado.

byte y short se usan cuando necesitas ahorrar memoria con arrays enormes, situaciones que no vas a ver en FP2.

byte y char — la conexión con ASCII

byte y char tienen una relación especial con los códigos de caracteres.

Un byte puede guardar valores de -128 a 127, o de 0 a 255 si lo tratas como sin signo. Esto no es casualidad: la tabla ASCII estándar tiene 128 caracteres (0-127) y la tabla ASCII extendida llega a 255. Cada carácter de texto tiene un número en esa tabla:

'A' → 65
'a' → 97
'0' → 48
' ' → 32
'\n' → 10  (salto de línea)

Un char en Java guarda un único carácter Unicode codificado en 16 bits (valores 0-65535). Los primeros 128 valores coinciden exactamente con ASCII:

char letra = 'A';              // el carácter A
char letra2 = 65;              // también el carácter A (código ASCII 65)
int codigo = 'A';              // 65 — el código numérico de A

System.out.println(letra);     // → A
System.out.println(codigo);    // → 65
System.out.println((char)65);  // → A
System.out.println((int)'Z');  // → 90

Fíjate en la diferencia de comillas: los chars usan comillas simples 'A', los Strings usan comillas dobles "A". Esta distinción en Java es estricta, 'A' es un char, "A" es un String de un carácter. En Python no había distinción entre comillas simples y dobles.

char c = 'A';      // correcto — comillas simples
char c = "A";      // error — "A" es String, no char
String s = "A";    // correcto — comillas dobles
String s = 'A';    // error — 'A' es char, no String

Puedes hacer aritmética con chars porque internamente son números:

char letra = 'A';
System.out.println((char)(letra + 1));    // → B (A es 65, B es 66)
System.out.println((char)(letra + 25));   // → Z (A + 25 = 90 = Z)

// Recorrer el alfabeto
for (char c = 'a'; c <= 'z'; c++) {
    System.out.print(c + " ");
}
// → a b c d e f g h i j k l m n o p q r s t u v w x y z

Los tipos decimales — float y double

float  → precisión de ~7 dígitos decimales   (32 bits)
double → precisión de ~15 dígitos decimales  (64 bits)
float f = 3.14f;      // f al final — indica que es float
double d = 3.14;      // sin sufijo — los decimales son double por defecto
double d2 = 3.14d;    // d al final — también válido pero innecesario

¿Cuándo usar float y cuándo double?

La regla para FP2: usa siempre double. Es el tipo decimal por defecto en Java y tiene más precisión. float se usa en aplicaciones donde la memoria es crítica (gráficos 3D, sensores), situaciones que no verás en FP2.

Si olvidas la f al declarar un float, Java da error porque por defecto los literales decimales son double:

float f = 3.14;     // error: possible lossy conversion from double to float
float f = 3.14f;    // correcto
float f = (float)3.14;  // también correcto — casting explícito

boolean — verdadero o falso

En Python era True y False con mayúscula. En Java es true y false en minúscula:

# Python
aprobado = True
suspenso = False
// Java
boolean aprobado = true;
boolean suspenso = false;

Los booleanos en Java son estrictamente true o false, no hay conversión automática desde números como en Python:

# Python — 0 es falso, cualquier otro número es verdadero
if 1:
    print("verdadero")    # se ejecuta
// Java — un int nunca es un boolean
int x = 1;
if (x) {    // error de compilación — x es int, no boolean
    ...
}
if (x != 0) {    // correcto — condición booleana explícita
    ...
}

String — el que no es primitivo

String no es un tipo primitivo, es una clase. Esa distinción es importante y tiene consecuencias. Por eso se escribe con mayúscula (String), igual que todas las clases en Java, mientras que los primitivos van en minúscula (int, double, boolean).

String nombre = "Sergio";
String saludo = "Hola, " + nombre;
String vacio = "";
String nulo = null;    // un String puede ser null — sin valor asignado

Los Strings en Java son inmutables, igual que en Python. Una vez creado un String no puedes modificar sus caracteres. Cuando haces nombre = nombre + "!", no estás modificando el String original, estás creando uno nuevo y reasignando la variable.

String s = "Hola";
s = s + " mundo";    // crea un nuevo String "Hola mundo"
                     // el String "Hola" original sigue en memoria hasta que
                     // el garbage collector lo elimine

Primitivos vs objetos — heap y stack

Esta es la diferencia más importante entre primitivos y objetos, y la que explica por qué == se comporta diferente con Strings que con ints.

En Java la memoria se divide en dos zonas principales:

Stack (pila) — memoria rápida y ordenada. Aquí se guardan las variables locales y sus valores primitivos. Cuando termina un método, todo lo que estaba en el stack de ese método se libera automáticamente.

Heap (montón) — memoria más grande y flexible. Aquí se guardan los objetos, instancias de clases, arrays, Strings. El heap persiste más allá del método que creó el objeto.

STACK                    HEAP
┌─────────────────┐      ┌─────────────────────────┐
│ int edad = 22   │      │ String "Sergio"          │
│    edad → [22]  │      │ String "Hola"            │
│                 │      │ (otros objetos...)       │
│ String nombre ──┼──────► "Sergio"                 │
└─────────────────┘      └─────────────────────────┘

Cuando declaras int edad = 22, el valor 22 se guarda directamente en el stack, es el valor en sí.

Cuando declaras String nombre = "Sergio", en el stack se guarda una referencia, la dirección de memoria del heap donde está el String «Sergio». La variable nombre no contiene el texto, contiene la dirección donde está el texto.

int a = 5;
int b = a;    // copia el VALOR — b tiene su propio 5
b = 10;       // cambiar b no afecta a a
System.out.println(a);    // → 5 (no cambió)
System.out.println(b);    // → 10

String s1 = "Hola";
String s2 = s1;    // copia la REFERENCIA — s1 y s2 apuntan al mismo String

La diferencia entre == y .equals() — la trampa clásica de Java

Aquí está la consecuencia directa de todo lo anterior. El operador == en Java compara valores. Para primitivos eso funciona perfectamente:

int a = 5;
int b = 5;
System.out.println(a == b);    // → true — compara los valores 5 y 5

Pero para objetos (incluidos Strings), == compara referencias, es decir, compara si las dos variables apuntan exactamente al mismo objeto en el heap, no si tienen el mismo contenido:

String s1 = new String("Hola");    // crea un nuevo String en el heap
String s2 = new String("Hola");    // crea OTRO nuevo String en el heap

System.out.println(s1 == s2);         // → false — son objetos distintos en memoria
System.out.println(s1.equals(s2));    // → true  — tienen el mismo contenido
HEAP:
  dirección 0x1000: "Hola"   ← s1 apunta aquí
  dirección 0x2000: "Hola"   ← s2 apunta aquí

s1 == s2   → ¿0x1000 == 0x2000? → false
s1.equals(s2) → ¿"Hola".equals("Hola")? → true

La confusión surge porque Java tiene una optimización llamada String pool, cuando usas literales de String (sin new), Java reutiliza el mismo objeto si el contenido es igual:

String s1 = "Hola";    // Java mete "Hola" en el String pool
String s2 = "Hola";    // Java reutiliza el "Hola" del pool

System.out.println(s1 == s2);    // → true — ambas apuntan al mismo objeto del pool

Esto puede hacer que == parezca que funciona con Strings, pero solo por casualidad. Si en algún momento el String viene de una variable, de la entrada del usuario, de un método… ya no hay garantía:

String s1 = "Hola";
String s2 = "Ho" + "la";              // se resuelve en compilación → mismo objeto del pool
String s3 = new String("Hola");       // fuerza un nuevo objeto fuera del pool
String s4 = obtenerString();          // viene de un método — objeto nuevo

System.out.println(s1 == s2);         // → true  (por el pool)
System.out.println(s1 == s3);         // → false (new fuerza objeto nuevo)
System.out.println(s1 == s4);         // → false (objeto nuevo)
System.out.println(s1.equals(s4));    // → true  (mismo contenido)

La regla de oro: para comparar Strings (y cualquier objeto) usa siempre .equals(), nunca ==.

// MAL — trampa clásica de Java
if (nombre == "Sergio") { ... }      // puede fallar

// BIEN — siempre .equals() para objetos
if (nombre.equals("Sergio")) { ... } // siempre correcto

final — la constante de Java

final en Java es el equivalente de las constantes, una variable declarada final no puede cambiar su valor después de la asignación inicial. Es el equivalente aproximado de las constantes en Python, aunque Python no las enforce realmente:

# Python — convención de constante (mayúsculas), pero nada impide cambiarla
PI = 3.14159
PI = 5    # Python no da error aunque viole la convención
// Java — final obliga a que no cambie
final double PI = 3.14159;
PI = 5;    // error de compilación: cannot assign a value to final variable PI

final int MAX_INTENTOS = 3;
final String NOMBRE_APP = "SergioLearns";

Por convención, las variables final en Java se escriben en MAYÚSCULAS con guiones bajos, igual que las constantes en Python. No es obligatorio pero es el estilo estándar.

final int MAX = 100;              // constante entera
final double GRAVEDAD = 9.8;      // constante decimal
final String TITULO = "FP2";      // constante String

final también puede aplicarse a variables que se inicializan más tarde, pero solo pueden recibir valor una vez:

final int x;
x = 5;         // primera y única asignación — correcto
x = 10;        // error — ya tiene valor

Conversión entre tipos — casting

Java no convierte tipos automáticamente en todos los casos. Hay dos tipos de conversión:

Conversión implícita (widening): de un tipo más pequeño a uno más grande. Java la hace automáticamente porque no hay riesgo de perder datos:

int i = 100;
long l = i;        // int → long — automático, sin pérdida
double d = i;      // int → double — automático, sin pérdida
float f = i;       // int → float — automático

// Jerarquía: byte → short → int → long → float → double
// A la derecha siempre caben los de la izquierda

Conversión explícita (narrowing): de un tipo más grande a uno más pequeño. Puede perder datos, así que Java la exige explícita con casting:

double d = 9.99;
int i = d;           // error — posible pérdida de datos
int i = (int) d;     // correcto — casting explícito → i = 9 (trunca decimales)

long l = 1000000L;
int i = (int) l;     // correcto — casting explícito → i = 1000000 (cabe)

long l = 3000000000L;  // mayor que Integer.MAX_VALUE (2.147.483.647)
int i = (int) l;       // correcto sintácticamente pero da resultado incorrecto
                       // — los bits se truncan → resultado imprevisible
// Casting con char e int
char c = 'A';
int codigo = c;          // char → int — automático → 65
char c2 = (char) 66;     // int → char — casting explícito → 'B'

System.out.println(codigo);    // → 65
System.out.println(c2);        // → B

Conversión entre String y números

Convertir entre String y tipos numéricos no es casting, son métodos:

// String → número
int i = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
long l = Long.parseLong("9000000000");

// número → String
String s1 = String.valueOf(42);       // → "42"
String s2 = String.valueOf(3.14);     // → "3.14"
String s3 = Integer.toString(42);     // → "42" (también válido)
String s4 = "" + 42;                  // → "42" (concatenación — menos formal)

Si el String no es un número válido, parseInt lanza NumberFormatException:

int i = Integer.parseInt("hola");    // NumberFormatException — "hola" no es un int
int i = Integer.parseInt("3.14");    // NumberFormatException — tiene decimales

Las clases envoltorio — Integer, Double, Boolean…

Cada tipo primitivo tiene una clase envoltorio (wrapper class) que lo convierte en objeto:

int      → Integer
double   → Double
float    → Float
long     → Long
boolean  → Boolean
char     → Character
byte     → Byte
short    → Short

Las necesitarás cuando Java exija un objeto en vez de un primitivo, por ejemplo en colecciones como ArrayList. Java hace la conversión automáticamente en la mayoría de casos (autoboxing/unboxing):

Integer objetoInt = 42;       // autoboxing — int → Integer automático
int primitivo = objetoInt;    // unboxing — Integer → int automático

Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // → true (Java cachea Integer de -128 a 127)

Integer c = 200;
Integer d = 200;
System.out.println(c == d);   // → false (fuera del caché — objetos distintos)
System.out.println(c.equals(d)); // → true — siempre usa equals con objetos

También tienen métodos útiles:

int max = Integer.MAX_VALUE;    // → 2147483647
int min = Integer.MIN_VALUE;    // → -2147483648
System.out.println(Integer.toBinaryString(10));    // → 1010
System.out.println(Integer.toHexString(255));      // → ff
System.out.println(Double.isNaN(0.0/0.0));         // → true

Declaración múltiple y var

Puedes declarar varias variables del mismo tipo en una línea:

int a = 1, b = 2, c = 3;
double x = 0.0, y = 0.0;

Desde Java 10 puedes usar var para que Java infiera el tipo automáticamente, como Python, pero solo para variables locales:

var nombre = "Sergio";    // Java infiere String
var edad = 22;            // Java infiere int
var nota = 8.5;           // Java infiere double

var no significa que la variable no tenga tipo, significa que el compilador lo deduce del valor. Una vez asignado el tipo no puede cambiar:

var x = 5;
x = "hola";    // error — x es int, no String

En FP2 verás más el estilo explícito (int x = 5) que var, porque los apuntes y los exámenes usan la forma tradicional. Pero var existe y es válido.

Resumen rápido

// TIPOS PRIMITIVOS
byte b    = 100;           // -128 a 127
short s   = 30000;         // -32768 a 32767
int i     = 1000000;       // ← más usado — entero estándar
long l    = 9000000000L;   // L obligatorio — entero grande
float f   = 3.14f;         // f obligatorio — decimal simple precisión
double d  = 3.14;          // ← más usado — decimal doble precisión
boolean b = true;          // true o false (minúscula, no True/False)
char c    = 'A';           // comillas simples — un carácter

// char y ASCII
char c = 'A';              // 'A' = 65 en ASCII
int codigo = 'A';          // → 65
char letra = (char) 66;    // → 'B'

// String — NO es primitivo, es objeto
String nombre = "Sergio";  // comillas dobles
// NUNCA == para comparar Strings
nombre.equals("Sergio")    // ← siempre .equals() para objetos

// final — constante
final int MAX = 100;       // no puede cambiar después

// CASTING
double d = 9.99;
int i = (int) d;           // → 9 (trunca decimales) — casting explícito

// conversión implícita (sin pérdida)
int i = 5;
double d = i;              // automático — int cabe en double

// String ↔ número
int n = Integer.parseInt("42");
double x = Double.parseDouble("3.14");
String s = String.valueOf(42);

// CLASES ENVOLTORIO
Integer, Double, Boolean, Character, Long, Float, Byte, Short

// HEAP vs STACK
// Primitivos → valor en el stack
// Objetos    → referencia en el stack, valor en el heap
// Por eso == con objetos compara referencias, no contenido

// REGLAS DE ORO
// 1. Usa int para enteros y double para decimales en FP2
// 2. Siempre .equals() para comparar Strings — nunca ==
// 3. Comillas simples para char, dobles para String
// 4. Sufijo L para long, f para float
// 5. (tipo) para casting explícito cuando puedes perder datos

En el próximo artículo vemos la entrada de datos con Scanner y el control de flujo, if/else, switch y cómo usar las variables que acabas de aprender.

Siguiente paso: prácticaVariables en Java — práctica con los errores que todo el mundo comete

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *