📦 ¿Qué es una variable en Java?
Una variable en Java es un espacio con nombre en la memoria del ordenador que almacena un valor de un tipo determinado. Puedes imaginarla como una caja etiquetada: la etiqueta es el nombre de la variable, el tamaño de la caja viene definido por el tipo de dato, y dentro se guarda el valor que le asignes.
Java es un lenguaje de tipado estático y fuerte, lo que significa dos cosas fundamentales: primero, toda variable debe declararse con un tipo de dato concreto antes de poder usarla; segundo, el compilador verifica que todas las operaciones y asignaciones sean compatibles con el tipo declarado. Esta característica es una de las razones por las que Java produce código robusto y con menos errores en tiempo de ejecución que los lenguajes de tipado dinámico.
Toda variable en Java se define mediante tres elementos esenciales: un tipo de dato (que determina qué valores puede almacenar y cuánta memoria ocupa), un nombre identificador (que permite referirse a ella en el código) y, opcionalmente, un valor inicial.
// Tipo Nombre Valor inicial int edad = 25; String nombre = "María"; boolean activo = true;
✏️ Declaración e inicialización de variables
Declarar una variable significa informar al compilador de que existe un nombre asociado a un tipo de dato. Inicializar significa asignarle un valor por primera vez. Ambas operaciones pueden realizarse por separado o en una sola sentencia.
▶️ Declaración sin inicialización
int edad; // Declaración: reserva espacio para un entero edad = 25; // Inicialización: asigna el valor 25 String ciudad; // Declaración de una referencia a String ciudad = "Madrid"; // Inicialización con un literal de cadena
▶️ Declaración con inicialización simultánea
int edad = 25; double salario = 2850.75; boolean esActivo = true; char inicial = 'J'; String nombre = "Ana García";
▶️ Declaración múltiple en una línea
Java permite declarar varias variables del mismo tipo separándolas con comas, aunque se recomienda una variable por línea para mayor legibilidad:
int x = 0, y = 0, z = 0; // Tres enteros en una línea String nombre, apellido; // Dos Strings sin inicializar
🔢 Tipos de datos primitivos
Java define exactamente ocho tipos primitivos. Son los bloques fundamentales del lenguaje: almacenan valores directamente en la pila (stack), no son objetos, y su tamaño en memoria es fijo e independiente de la plataforma. Esta es una de las garantías de portabilidad de Java.
| Tipo | Tamaño | Rango de valores | Valor por defecto | Ejemplo |
|---|---|---|---|---|
byte |
8 bits | -128 a 127 | 0 |
byte edad = 25; |
short |
16 bits | -32.768 a 32.767 | 0 |
short puerto = 8080; |
int |
32 bits | -2.147.483.648 a 2.147.483.647 | 0 |
int poblacion = 47000000; |
long |
64 bits | -9,2 × 1018 a 9,2 × 1018 | 0L |
long distancia = 149597870700L; |
float |
32 bits | ±3,4 × 1038 (7 dígitos decimales) | 0.0f |
float pi = 3.1416f; |
double |
64 bits | ±1,7 × 10308 (15 dígitos decimales) | 0.0 |
double precio = 29.99; |
char |
16 bits | Carácter Unicode ('\u0000' a '\uffff') | '\u0000' |
char letra = 'A'; |
boolean |
1 bit* | true o false |
false |
boolean activo = true; |
* El tamaño real de boolean depende de la JVM; típicamente ocupa 1 byte en la práctica, aunque solo necesita 1 bit de información.
🔹 Literales numéricos con formato legible (Java 7+)
Desde Java 7, los literales numéricos pueden incluir guiones bajos como separadores visuales, lo que mejora enormemente la legibilidad de números grandes:
int poblacion = 47_000_000; // Más legible que 47000000 long presupuesto = 1_500_000_000L; // Mil quinientos millones double pi = 3.141_592_653_589_793; // Pi con alta precisión int binario = 0b1010_1100; // Literal binario: 172 int hex = 0xFF_EC_DE_5E; // Literal hexadecimal
🔹 Sufijos obligatorios en literales
Algunos tipos requieren un sufijo en sus literales para distinguirlos del tipo por defecto:
long distanciaLuna = 384_400_000L; // L obligatoria para long float temperatura = 36.6f; // f obligatoria para float double gravedad = 9.81; // Sin sufijo: double por defecto // Sin sufijo, un entero literal es int y un decimal es double
L en literales long que exceden el rango de int causa un error de compilación: long x = 3000000000; falla porque el literal se interpreta como int. La solución es long x = 3_000_000_000L;
📚 Tipos de referencia
Los tipos de referencia incluyen todo lo que no es un tipo primitivo: clases, interfaces, arrays y enumeraciones. A diferencia de los primitivos, las variables de referencia no almacenan el valor directamente sino la dirección de memoria donde reside el objeto en el heap.
// Clase String (la más usada)
String nombre = "María López";
// Arrays
int[] calificaciones = {9, 8, 10, 7, 9};
String[] ciudades = new String[3];
// Clases envoltorio (wrapper classes)
Integer edad = 25; // Autoboxing: int → Integer
Double salario = 2500.50; // Autoboxing: double → Double
// Colecciones (requieren tipos de referencia)
ArrayList<String> alumnos = new ArrayList<>();
HashMap<String, Integer> notas = new HashMap<>();
🔹 Primitivos vs. referencia: diferencias clave
| Característica | Tipo primitivo | Tipo de referencia |
|---|---|---|
| Almacenamiento | Valor directo en el stack | Referencia en stack, objeto en heap |
Puede ser null |
❌ No | ✅ Sí |
| Valor por defecto | 0, false, '\u0000' | null |
| Tiene métodos | ❌ No | ✅ Sí |
| Uso en genéricos | ❌ No (ArrayList<int> no compila) |
✅ Sí (ArrayList<Integer>) |
Comparación con == |
Compara valores | Compara referencias (usar .equals()) |
== en objetos: new String("hola") == new String("hola") devuelve false porque compara direcciones de memoria, no contenido. Usa siempre .equals() para comparar el contenido de objetos.
🏗️ Tipos de variables: locales, de instancia y estáticas
Independientemente de su tipo de dato, las variables en Java se clasifican en tres categorías según dónde se declaran y a quién pertenecen:
🔹 Variables locales
Se declaran dentro de un método, constructor o bloque. Solo existen durante la ejecución de ese bloque y no reciben valor por defecto — el compilador exige que se inicialicen antes de usarlas.
public double calcularDescuento(double precio, int porcentaje) {
// 'descuento' es una variable local
double descuento = precio * porcentaje / 100.0;
// 'precioFinal' también es local
double precioFinal = precio - descuento;
return precioFinal;
}
// Aquí 'descuento' y 'precioFinal' ya no existen
🔹 Variables de instancia (atributos de objeto)
Se declaran dentro de la clase pero fuera de cualquier método. Cada objeto creado de esa clase tiene su propia copia independiente. Reciben valores por defecto y pueden tener modificadores de acceso (private, protected, public).
public class Empleado {
// Variables de instancia: cada objeto tiene su propia copia
private String nombre;
private double salario;
private int antiguedad;
public Empleado(String nombre, double salario) {
this.nombre = nombre; // this diferencia instancia de local
this.salario = salario;
this.antiguedad = 0; // Valor inicial explícito
}
}
🔹 Variables de clase (estáticas)
Se declaran con el modificador static. Pertenecen a la clase, no a ningún objeto concreto, y todas las instancias comparten la misma copia. Se accede a ellas con el nombre de la clase: NombreClase.variable.
public class Empleado {
// Variable estática: compartida por TODOS los objetos
private static int contadorEmpleados = 0;
// Constante estática: convenio en MAYÚSCULAS
public static final double SALARIO_MINIMO = 1134.0;
private String nombre;
public Empleado(String nombre) {
this.nombre = nombre;
contadorEmpleados++; // Se incrementa por cada nuevo empleado
}
public static int getTotal() {
return contadorEmpleados;
}
}
// Uso desde fuera de la clase:
System.out.println(Empleado.SALARIO_MINIMO); // 1134.0
System.out.println(Empleado.getTotal()); // Número de empleados creados
🔹 Tabla comparativa de los tres tipos
| Característica | Local | De instancia | Estática |
|---|---|---|---|
| Lugar de declaración | Dentro de un método/bloque | En la clase, fuera de métodos | En la clase, con static |
| Pertenece a | El método | Cada objeto | La clase |
| Valor por defecto | ❌ Ninguno | ✅ Sí (0, null, false) | ✅ Sí (0, null, false) |
| Vida útil | Mientras se ejecuta el bloque | Mientras vive el objeto | Mientras la clase esté cargada |
| Modificadores de acceso | ❌ No permitidos | ✅ private, public, protected | ✅ private, public, protected |
🎯 Alcance (scope) de las variables
El alcance o scope de una variable es la región del código donde puede ser accedida. Java usa alcance léxico (o estático): el ámbito de una variable se determina por su posición en el código fuente, no por el flujo de ejecución.
public class EjemploAlcance {
// Alcance de CLASE: accesible en toda la clase
private int valorClase = 100;
public void metodoEjemplo() {
// Alcance de MÉTODO: accesible en todo el método
int valorMetodo = 50;
for (int i = 0; i < 10; i++) {
// Alcance de BLOQUE: 'i' solo existe dentro del for
int temporal = valorMetodo + i;
System.out.println(temporal);
}
// ❌ ERROR: 'i' y 'temporal' ya no existen aquí
// System.out.println(i); // No compila
// System.out.println(temporal); // No compila
// ✅ CORRECTO: 'valorMetodo' sigue vivo
System.out.println(valorMetodo);
}
}
🔹 Shadowing (ensombrecimiento) de variables
Cuando una variable local tiene el mismo nombre que una variable de instancia, la local «ensombrace» a la de instancia dentro de su ámbito. Para acceder a la variable de instancia se usa this:
public class Producto {
private String nombre; // Variable de instancia
private double precio; // Variable de instancia
public Producto(String nombre, double precio) {
// Los parámetros 'nombre' y 'precio' ensombrecen a los atributos
this.nombre = nombre; // this.nombre → atributo de instancia
this.precio = precio; // nombre (sin this) → parámetro local
}
}
🔒 Constantes con final
La palabra clave final indica que una variable no puede cambiar de valor después de su inicialización. Se puede aplicar a variables locales, parámetros, variables de instancia y variables estáticas.
public class Configuracion {
// Constante de clase: static + final, nombre en MAYÚSCULAS
public static final double IVA = 0.21;
public static final int MAX_INTENTOS = 3;
public static final String EMPRESA = "Ciberaula S.L.";
// Variable de instancia final: se asigna una vez (en constructor)
private final String id;
public Configuracion(String id) {
this.id = id; // Solo se puede asignar una vez
}
public void procesar() {
// Variable local final
final int limite = 100;
// limite = 200; // ❌ ERROR de compilación
// Parámetro final (no se puede reasignar)
calcular(limite);
}
private void calcular(final int valor) {
// valor = 50; // ❌ ERROR: parámetro final
System.out.println("Calculando con: " + valor);
}
}
static final se escriben en MAYÚSCULAS_CON_GUIONES_BAJOS por convención universal en Java. Ejemplos: MAX_SIZE, DEFAULT_TIMEOUT, PI.
final en una referencia a objeto impide reasignar la referencia, pero no impide modificar el contenido del objeto. Por ejemplo, final ArrayList<String> lista = new ArrayList<>(); permite hacer lista.add("elemento"), pero no lista = new ArrayList<>();.
🔮 Inferencia de tipos con var (Java 10+)
Desde Java 10, la palabra reservada var permite declarar variables locales sin especificar explícitamente su tipo: el compilador lo infiere del valor asignado. Es importante entender que no convierte a Java en un lenguaje dinámico — el tipo se fija en compilación y no cambia.
// Con tipo explícito String mensaje = "Hola, Java"; ArrayList<String> nombres = new ArrayList<>(); HashMap<String, List<Integer>> mapa = new HashMap<>(); // Con var (equivalente exacto) var mensaje = "Hola, Java"; // Inferido: String var nombres = new ArrayList<String>(); // Inferido: ArrayList<String> var mapa = new HashMap<String, List<Integer>>(); // Inferido // Especialmente útil con tipos genéricos largos var iterador = nombres.iterator(); // Inferido: Iterator<String>
🔹 Restricciones de var
| Uso | ¿Permitido? | Ejemplo |
|---|---|---|
| Variable local con inicialización | ✅ Sí | var x = 10; |
| Variable en bucle for mejorado | ✅ Sí | for (var item : lista) |
| Variable en bucle for clásico | ✅ Sí | for (var i = 0; i < 10; i++) |
| Variable de instancia o estática | ❌ No | var nombre = "Ana"; en la clase |
| Parámetro de método | ❌ No | void metodo(var x) |
| Sin inicialización | ❌ No | var x; (no hay tipo que inferir) |
Inicializado con null |
❌ No | var x = null; (tipo ambiguo) |
var lista = new ArrayList<String>()) o cuando el tipo es excesivamente largo. Evítalo cuando el tipo no sea evidente y la legibilidad se resienta.
🔄 Conversión de tipos y casting
Java realiza automáticamente la conversión implícita (widening) cuando se asigna un valor de un tipo más pequeño a uno más grande, sin pérdida de información. Para la dirección contraria se necesita un casting explícito (narrowing), que puede provocar pérdida de datos.
▶️ Conversión implícita (widening)
byte b = 42; int i = b; // byte → int: automático long l = i; // int → long: automático float f = l; // long → float: automático double d = f; // float → double: automático // Cadena de promoción: byte → short → int → long → float → double
▶️ Casting explícito (narrowing)
double precio = 29.99; int precioEntero = (int) precio; // 29 (se pierde la parte decimal) long grande = 130; byte pequeno = (byte) grande; // -126 (overflow: 130 excede rango byte) float decimal = 3.14f; int entero = (int) decimal; // 3 (truncamiento, no redondeo) // Para redondear correctamente: int redondeado = (int) Math.round(decimal); // 3 (Math.round devuelve long)
▶️ Autoboxing y unboxing
Java convierte automáticamente entre tipos primitivos y sus clases envoltorio:
// Autoboxing: primitivo → objeto (automático) Integer edadObj = 25; // int → Integer Double salarioObj = 2500.50; // double → Double // Unboxing: objeto → primitivo (automático) int edad = edadObj; // Integer → int double salario = salarioObj; // Double → double // Cuidado con null en unboxing: Integer valor = null; // int x = valor; // ❌ NullPointerException en tiempo de ejecución
📋 Valores por defecto
Las variables de instancia y las estáticas que no se inicializan explícitamente reciben un valor por defecto. Las variables locales no tienen valor por defecto y el compilador exige su inicialización.
| Tipo de dato | Valor por defecto |
|---|---|
byte, short, int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | '\u0000' (carácter nulo) |
boolean | false |
| Cualquier referencia (String, arrays, objetos) | null |
public class ValoresPorDefecto {
// Variables de instancia: tienen valor por defecto
int numero;
boolean activo;
String texto;
double[] datos;
public void mostrar() {
System.out.println("int: " + numero); // 0
System.out.println("boolean: " + activo); // false
System.out.println("String: " + texto); // null
System.out.println("double[]:" + datos); // null
}
public void errorLocal() {
int local;
// System.out.println(local); // ❌ ERROR: variable might not have been initialized
}
}
int: 0 boolean: false String: null double[]:null
📐 Convenciones de nomenclatura
Java tiene reglas estrictas sobre los identificadores y convenciones ampliamente aceptadas. Seguirlas hace que el código sea legible para cualquier desarrollador Java del mundo.
🔹 Reglas del lenguaje (obligatorias)
Un identificador en Java puede contener letras, dígitos, guiones bajos (_) y el signo de dólar ($). No puede empezar por un dígito ni coincidir con una palabra reservada del lenguaje (class, int, if, for, etc.). Java distingue mayúsculas de minúsculas: edad, Edad y EDAD son tres variables distintas.
🔹 Convenciones de estilo (mejores prácticas)
| Tipo de identificador | Convención | Ejemplo |
|---|---|---|
| Variables y parámetros | camelCase |
nombreCompleto, precioUnitario |
Constantes (static final) |
MAYUSCULAS_CON_GUIONES |
MAX_INTENTOS, IVA_GENERAL |
| Clases e interfaces | PascalCase |
Empleado, CuentaBancaria |
| Métodos | camelCase (verbo) |
calcularTotal(), getNombre() |
| Paquetes | todo en minúsculas | com.ciberaula.modelo |
precioConIva a p, y indiceActual a i (excepto en bucles cortos). Un buen nombre elimina la necesidad de comentarios.
💼 Ejemplo integrador: sistema de registro de empleados
Este ejemplo combina todos los conceptos vistos: variables locales, de instancia y estáticas; tipos primitivos y de referencia; constantes; conversión de tipos; y buenas prácticas de nomenclatura.
public class RegistroEmpleados {
// ── Constantes de clase (static final) ──────────────────────
public static final double SALARIO_MINIMO = 1134.0;
public static final int MAX_EMPLEADOS = 500;
public static final String EMPRESA = "Ciberaula S.L.";
// ── Variable estática: compartida por todas las instancias ──
private static int totalEmpleados = 0;
// ── Variables de instancia: propias de cada objeto ──────────
private final int id; // final: no cambia después del constructor
private String nombre;
private String departamento;
private double salarioBruto;
private boolean activo;
// ── Constructor ─────────────────────────────────────────────
public RegistroEmpleados(String nombre, String departamento, double salarioBruto) {
// Shadowing: parámetros 'nombre', 'departamento', 'salarioBruto'
// ensombrecen a las variables de instancia
this.id = ++totalEmpleados;
this.nombre = nombre;
this.departamento = departamento;
this.salarioBruto = Math.max(salarioBruto, SALARIO_MINIMO);
this.activo = true;
}
// ── Método con variables locales ────────────────────────────
public double calcularSalarioNeto() {
// Variables locales: solo existen dentro de este método
final double IRPF = 0.15;
final double SEGURIDAD_SOCIAL = 0.0635;
double descuentoIrpf = salarioBruto * IRPF;
double descuentoSS = salarioBruto * SEGURIDAD_SOCIAL;
double salarioNeto = salarioBruto - descuentoIrpf - descuentoSS;
return Math.round(salarioNeto * 100.0) / 100.0; // Redondeo a 2 decimales
}
// ── Método con conversión de tipos ──────────────────────────
public String generarFicha() {
// Conversión implícita: int → String (concatenación)
var ficha = new StringBuilder();
ficha.append("╔══════════════════════════════════════╗\n");
ficha.append(" FICHA DE EMPLEADO - ").append(EMPRESA).append("\n");
ficha.append(" ID: ").append(id).append("\n");
ficha.append(" Nombre: ").append(nombre).append("\n");
ficha.append(" Departamento: ").append(departamento).append("\n");
ficha.append(" Salario bruto: ").append(String.format("%.2f €", salarioBruto)).append("\n");
ficha.append(" Salario neto: ").append(String.format("%.2f €", calcularSalarioNeto())).append("\n");
ficha.append(" Estado: ").append(activo ? "Activo" : "Inactivo").append("\n");
ficha.append("╚══════════════════════════════════════╝");
return ficha.toString();
}
// ── Getters ─────────────────────────────────────────────────
public int getId() { return id; }
public String getNombre() { return nombre; }
public boolean isActivo() { return activo; }
public static int getTotalEmpleados() { return totalEmpleados; }
// ── Método main: punto de entrada ───────────────────────────
public static void main(String[] args) {
// Variables locales en main
var emp1 = new RegistroEmpleados("Ana García", "Desarrollo", 3200.0);
var emp2 = new RegistroEmpleados("Carlos López", "Marketing", 2800.0);
var emp3 = new RegistroEmpleados("María Torres", "RRHH", 900.0); // Se ajusta al mínimo
System.out.println(emp1.generarFicha());
System.out.println();
System.out.println(emp2.generarFicha());
System.out.println();
System.out.println(emp3.generarFicha());
System.out.println("\n📊 Total empleados registrados: " + RegistroEmpleados.getTotalEmpleados());
System.out.println("💰 Salario mínimo vigente: " + RegistroEmpleados.SALARIO_MINIMO + " €");
}
}
╔══════════════════════════════════════╗ FICHA DE EMPLEADO - Ciberaula S.L. ID: 1 Nombre: Ana García Departamento: Desarrollo Salario bruto: 3200.00 € Salario neto: 2516.80 € Estado: Activo ╚══════════════════════════════════════╝ ╔══════════════════════════════════════╗ FICHA DE EMPLEADO - Ciberaula S.L. ID: 2 Nombre: Carlos López Departamento: Marketing Salario bruto: 2800.00 € Salario neto: 2202.20 € Estado: Activo ╚══════════════════════════════════════╝ ╔══════════════════════════════════════╗ FICHA DE EMPLEADO - Ciberaula S.L. ID: 3 Nombre: María Torres Departamento: RRHH Salario bruto: 1134.00 € Salario neto: 891.42 € Estado: Activo ╚══════════════════════════════════════╝ 📊 Total empleados registrados: 3 💰 Salario mínimo vigente: 1134.0 €
🚫 Errores frecuentes con variables
❌ Error 1: Usar una variable local sin inicializar
public void ejemplo() {
int resultado;
// ❌ ERROR de compilación:
System.out.println(resultado); // "variable resultado might not have been initialized"
// ✅ CORRECTO:
int resultado2 = 0;
System.out.println(resultado2); // 0
}
❌ Error 2: Comparar Strings con == en lugar de .equals()
String a = new String("hola");
String b = new String("hola");
// ❌ INCORRECTO: compara referencias (direcciones de memoria)
if (a == b) {
System.out.println("Iguales"); // No se ejecuta
}
// ✅ CORRECTO: compara contenido
if (a.equals(b)) {
System.out.println("Iguales"); // Sí se ejecuta
}
❌ Error 3: Pérdida de precisión por casting inadvertido
// ❌ INCORRECTO: división entre enteros → resultado entero int aprobados = 7; int total = 10; double porcentaje = aprobados / total; // 0.0 (¡no 0.7!) // ✅ CORRECTO: forzar al menos un operando a double double porcentaje2 = (double) aprobados / total; // 0.7 double porcentaje3 = aprobados / (double) total; // 0.7 double porcentaje4 = aprobados * 1.0 / total; // 0.7
❌ Error 4: NullPointerException por unboxing de null
// ❌ PELIGROSO: unboxing de null lanza NullPointerException Integer edadObjeto = null; int edad = edadObjeto; // NullPointerException en tiempo de ejecución // ✅ SEGURO: verificar null antes de unboxing Integer edadObjeto2 = obtenerEdad(); // Puede devolver null int edad2 = (edadObjeto2 != null) ? edadObjeto2 : 0;
❌ Error 5: Acceder a variable fuera de su alcance
// ❌ INCORRECTO: declarar dentro del bloque y usar fuera
if (condicion) {
String mensaje = "Aprobado";
}
System.out.println(mensaje); // ERROR: "cannot find symbol"
// ✅ CORRECTO: declarar antes del bloque
String mensaje;
if (condicion) {
mensaje = "Aprobado";
} else {
mensaje = "Suspendido";
}
System.out.println(mensaje); // OK
🧪 Ejercicios prácticos
📝 Ejercicio 1: Calculadora de IMC
Crea una clase CalculadoraIMC con variables de instancia para el nombre, peso (kg) y altura (m) de una persona. Incluye un método calcularIMC() que devuelva el índice de masa corporal (peso / altura²) y un método clasificar() que devuelva una cadena indicando si es bajo peso (<18.5), normal (18.5-24.9), sobrepeso (25-29.9) u obesidad (≥30). Usa constantes para los umbrales.
📝 Ejercicio 2: Conversor de temperaturas
Escribe una clase con métodos estáticos para convertir temperaturas entre Celsius, Fahrenheit y Kelvin. Usa constantes para los factores de conversión. El método main debe demostrar todas las conversiones con variables locales, usando var donde sea apropiado.
📝 Ejercicio 3: Inventario de productos con tipos mixtos
Crea una clase Producto que combine todos los tipos de variables vistos: una constante estática para el IVA (21%), un contador estático de productos, atributos de instancia para id, nombre, precio y stock, y un método calcularPrecioFinal() con variables locales. Incluye un método estático resumen() que reciba un array de productos y calcule el valor total del inventario.
📎 Artículos relacionados
❓ Preguntas frecuentes sobre Variables en Java: declaración, tipos y alcance
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Variables en Java: declaración, tipos y alcance? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!