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().
Tabla de Contenidos
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.
Java variables — primitive types, String, final and casting from scratch
Java variables are where the difference from Python becomes really clear. In Python a variable was just a name pointing to a value — no declarations, no types, no size limits. In Java every variable has a fixed type declared before use that can never change.
Why Java requires declaring the type of every variable
Java is statically typed, the type of every variable is fixed at declaration and cannot change:
# Python — no types x = 5 x = "hello" # fine in Python
// Java — type declaration required int x = 5; x = "hello"; // compile error — x is int, not String
The compiler checks all type operations before running the program, errors are caught at compile time, not runtime.
Variable declaration syntax
// type name = value; String name = "Sergio"; int age = 22; double grade = 7.5; boolean passed = true;
The eight primitive types
byte → tiny integer (8 bits, -128 to 127) short → small integer (16 bits, -32768 to 32767) int → standard integer (32 bits) ← most used long → large integer (64 bits) float → single precision decimal (32 bits) double → double precision decimal (64 bits) ← most used boolean → true or false char → single character (16 bits)
When to use each:
FP2 rule: use int for integers and double for decimals. That covers 95% of cases. Use long when numbers exceed ~2 billion (add L suffix). Use float only when explicitly needed (add f suffix).
int i = 1000000; long l = 9000000000L; // L suffix required double d = 3.14; float f = 3.14f; // f suffix required boolean b = true; // lowercase — not True like Python char c = 'A'; // single quotes — not double
byte, char and ASCII
byte holds values 0-255 (unsigned), matching the ASCII table range. char holds a single Unicode character (16 bits, values 0-65535). The first 128 values match ASCII exactly:
char letter = 'A'; // the character A
int code = 'A'; // 65 — ASCII code of A
char next = (char)('A'+1); // 'B'
System.out.println((int)'Z'); // → 90
System.out.println((char)65); // → A
// Iterate through alphabet
for (char c = 'a'; c <= 'z'; c++) {
System.out.print(c + " ");
}
// → a b c d e f ... z
Single quotes for char, double quotes for String, strict rule in Java:
char c = 'A'; // correct char c = "A"; // error — "A" is a String String s = "A"; // correct String s = 'A'; // error — 'A' is a char
String — not a primitive
String is a class, not a primitive, hence the capital letter. Strings are immutable (like Python).
Heap and Stack
Java memory has two main zones:
Stack — fast, ordered. Stores local variables and primitive values. Freed automatically when a method finishes.
Heap — larger, flexible. Stores objects. Persists beyond the method that created them.
STACK HEAP int age = 22 → [22] String "Sergio" (at address 0x1000) String name ─────────────► "Sergio"
Primitives store their value directly in the stack. Objects store a reference (memory address) in the stack pointing to the actual data in the heap.
== vs .equals() — Java’s classic trap
== compares values for primitives, works correctly:
int a = 5, b = 5; System.out.println(a == b); // → true
For objects, == compares references, whether two variables point to the exact same object in the heap:
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // → false — different objects in heap
System.out.println(s1.equals(s2)); // → true — same content
The String pool creates confusion, Java reuses String objects for literals:
String s1 = "Hello"; String s2 = "Hello"; System.out.println(s1 == s2); // → true (same pool object — coincidence)
But once a String comes from a method, user input, or new, == fails. Golden rule: always use .equals() to compare Strings. Never ==.
if (name.equals("Sergio")) { ... } // correct
if (name == "Sergio") { ... } // wrong — could fail
final — Java’s constant
# Python — convention only, not enforced PI = 3.14159 PI = 5 # Python allows this
// Java — enforced by compiler final double PI = 3.14159; PI = 5; // compile error: cannot assign a value to final variable final int MAX_ATTEMPTS = 3; // UPPERCASE_WITH_UNDERSCORES convention final String APP_NAME = "FP2";
Casting — type conversion
Implicit (widening): smaller to larger type. Java does it automatically:
int i = 100; double d = i; // automatic — int fits in double long l = i; // automatic // Hierarchy: byte → short → int → long → float → double
Explicit (narrowing): larger to smaller. May lose data, must be explicit:
double d = 9.99; int i = (int) d; // → 9 (truncates decimals) char c = (char) 66; // → 'B' int code = 'A'; // → 65 (automatic char → int)
String ↔ number conversion:
int n = Integer.parseInt("42");
double x = Double.parseDouble("3.14");
String s = String.valueOf(42); // → "42"
String s = "" + 42; // → "42" (concatenation)
Wrapper classes
int → Integer double → Double boolean → Boolean char → Character
Java does autoboxing/unboxing automatically. Always use .equals() with wrapper objects, not ==.
Quick summary
// PRIMITIVES
int i = 5; // standard integer ← most used
long l = 5L; // large integer — L suffix
double d = 3.14; // decimal ← most used
float f = 3.14f; // single precision — f suffix
boolean b = true; // lowercase true/false
char c = 'A'; // single quotes
// char and ASCII
int code = 'A'; // → 65
char letter = (char)66; // → 'B'
// String — object, not primitive
String s = "text"; // double quotes
s.equals("other") // ALWAYS .equals() for objects — never ==
// final — constant
final int MAX = 100; // cannot change
// CASTING
int i = (int) 9.99; // → 9 (truncates)
double d = 5; // automatic (int → double)
// String ↔ number
int n = Integer.parseInt("42");
String s = String.valueOf(42);
// HEAP vs STACK
// Primitives: value stored in stack
// Objects: reference in stack → value in heap
// That's why == with objects compares addresses, not content
// GOLDEN RULES
// 1. int for integers, double for decimals in FP2
// 2. Always .equals() for Strings — never ==
// 3. Single quotes for char, double quotes for String
// 4. L suffix for long, f suffix for float
// 5. (type) for explicit cast when data loss is possible
