Métodos en Java¶
Un método en Java es la unidad fundamental de comportamiento y abstracción. Permite agrupar un conjunto de instrucciones bajo un nombre, de manera que puedan ejecutarse múltiples veces sin repetir el código. Si ya se trabajó con funciones en C, la transición a métodos en Java resulta bastante natural: la sintaxis es similar y los conceptos fundamentales —parámetros, retorno, variables locales— funcionan de forma análoga.
La diferencia principal es que en Java todo método debe estar dentro de una clase. Por ahora, no es necesario profundizar en qué significa “clase” desde el punto de vista de la programación orientada a objetos; basta con entenderlo como el contenedor obligatorio donde se escriben los métodos. Más adelante en el curso se explorará este concepto en detalle.
¿Por qué usar métodos?¶
Antes de entrar en la sintaxis, vale la pena entender las razones por las que se organizan los programas en métodos:
Reutilización: Si una operación se necesita en varios lugares del programa, escribirla una sola vez en un método evita duplicar código. En C, esto ya se conoce: una función
calcular_promedio()se escribe una vez y se llama donde haga falta.Abstracción: Un método oculta los detalles de cómo se hace algo. Quien lo usa solo necesita saber qué hace. Por ejemplo, al llamar a
Math.sqrt(25), no importa qué algoritmo interno se usa para calcular la raíz cuadrada; solo importa que devuelve5.0.Modularidad: Dividir un programa grande en métodos pequeños facilita entender, probar y modificar cada parte de forma independiente. Un error en un método no debería afectar a otros si cada uno tiene una responsabilidad clara.
Legibilidad: Un método con un nombre descriptivo hace que el código sea más fácil de leer. Comparar
if (esPrimo(n))conif (n > 1 && ...)seguido de un lazo complejo muestra claramente la ventaja.
Anatomía de un Método¶
Sintaxis General¶
[modificadores] tipoRetorno nombreMetodo([parámetros]) [throws excepciones] {
// cuerpo del método
[return valor;]
}Estructura completa de un método
Cada parte de esta estructura tiene un propósito específico:
modificadores (opcional): Palabras clave que alteran el comportamiento o visibilidad del método. Los más comunes son
public(accesible desde cualquier lugar),private(solo accesible dentro de la misma clase) ystatic(pertenece a la clase, no a una instancia). Por ahora, todos los métodos llevaránpublic static.tipoRetorno (obligatorio): El tipo de dato que el método devuelve al terminar. Puede ser cualquier tipo primitivo (
int,double,boolean, etc.), cualquier tipo de referencia (String, arreglos, objetos), o la palabra especialvoidque indica que el método no devuelve nada. En C, esto es idéntico:int funcion()devuelve un entero,void funcion()no devuelve nada.nombreMetodo (obligatorio): El identificador que se usará para invocar al método. Por convención en Java, se usa camelCase: la primera palabra en minúscula y las siguientes con inicial mayúscula (
calcularPromedio,esPrimo,obtenerMaximo). Esto difiere de C, donde se suele usar snake_case (calcular_promedio).parámetros (opcional): Lista de variables que el método recibe como entrada, separadas por comas. Cada parámetro requiere su tipo:
(int a, double b, String nombre). Si el método no necesita datos de entrada, los paréntesis quedan vacíos:().throws (opcional): Declaración de excepciones que el método puede lanzar. Se verá en detalle en el capítulo de excepciones; por ahora puede ignorarse.
cuerpo: Las instrucciones que se ejecutan cuando se invoca el método, encerradas entre llaves
{}. Aquí va la lógica del método.return (obligatorio si no es
void): La sentencia que finaliza la ejecución del método y devuelve un valor al código que lo llamó. El tipo del valor devuelto debe coincidir con el tipoRetorno declarado.
Comparación con C¶
Para quien viene de programar en C, la siguiente tabla muestra las equivalencias:
| Aspecto | C | Java |
|---|---|---|
| Declaración | int sumar(int a, int b) | public static int sumar(int a, int b) |
| Sin retorno | void imprimir(char* msg) | public static void imprimir(String msg) |
| Ubicación | Archivo .c o .h | Dentro de una clase |
| Convención de nombres | calcular_promedio | calcularPromedio |
| Prototipo | Necesario antes de usar | No necesario (el compilador analiza toda la clase) |
La diferencia más notable es que en Java no existen los prototipos (forward declarations). En C, si main() llama a sumar() pero sumar() está definida después, se necesita declarar el prototipo antes. En Java, los métodos pueden estar en cualquier orden dentro de la clase y el compilador los encuentra sin problema.
Ejemplos de Declaración¶
public static void saludar() {
System.out.println("¡Hola!");
}Método sin parámetros ni retorno
public static int sumar(int a, int b) {
int resultado = a + b;
return resultado;
}Método con parámetros y retorno
public static double calcularCuadrado(double numero) {
return numero * numero;
}Método con un solo parámetro
public static boolean esPar(int numero) {
return numero % 2 == 0;
}Método que retorna boolean
Este último ejemplo muestra un patrón muy común: métodos que verifican una condición y devuelven true o false. Por convención, estos métodos suelen nombrarse con prefijos como es, tiene, puede (esPar, tieneElementos, puedeAcceder). En C, esto se haría con int retornando 0 o 1, ya que C89 no tiene tipo booleano nativo.
Invocación de Métodos¶
Para ejecutar un método, se lo invoca (o llama) usando su nombre seguido de paréntesis con los argumentos necesarios. La invocación puede hacerse de varias formas según el contexto.
Invocación Básica¶
// Método sin retorno - se ejecuta por su efecto
saludar();
// Método con retorno - guardar resultado en variable
int suma = sumar(5, 3); // suma = 8
// Método con retorno - usar directamente en otra llamada
System.out.println(sumar(10, 20)); // Imprime: 30
// Usar retorno en expresión aritmética
double area = calcularCuadrado(5.0) * 3.14159;
// Usar retorno boolean en condición
if (esPar(numero)) {
System.out.println("Es par");
}Invocación de métodos
El Flujo de Ejecución Durante una Llamada¶
Cuando se invoca un método, ocurre lo siguiente:
Se evalúan los argumentos: Si los argumentos son expresiones (como
sumar(2+3, 4*2)), primero se calculan sus valores (5y8).Se transfiere el control: La ejecución “salta” desde el punto de llamada hacia la primera línea del cuerpo del método.
Se ejecuta el cuerpo: Las instrucciones del método se ejecutan secuencialmente.
Se retorna: Al encontrar
return(o al llegar al final si esvoid), el control vuelve al punto de llamada y, si hay valor de retorno, este reemplaza a la expresión de llamada.
Tipos de Retorno¶
Métodos void¶
Un método void no devuelve ningún valor. Se usa para acciones que producen un efecto (imprimir en pantalla, modificar estado) pero no generan un resultado que el código llamante necesite usar.
public static void imprimirLinea(int cantidad) {
for (int i = 0; i < cantidad; i = i + 1) {
System.out.print("-");
}
System.out.println();
}Método void
En C, void funciona exactamente igual. La función printf() de C técnicamente retorna un int (la cantidad de caracteres impresos), pero casi siempre se ignora y se la usa como si fuera void. En Java, System.out.println() es genuinamente void.
Métodos con Retorno¶
Un método con tipo de retorno debe devolver un valor de ese tipo en todos los caminos de ejecución posibles. Esto significa que sin importar qué rama de un if se tome o cuántas vueltas dé un lazo, eventualmente debe ejecutarse un return con un valor del tipo correcto.
public static String clasificarNota(int nota) {
String clasificacion;
if (nota >= 90) {
clasificacion = "Excelente";
} else if (nota >= 70) {
clasificacion = "Bueno";
} else if (nota >= 50) {
clasificacion = "Regular";
} else {
clasificacion = "Insuficiente";
}
return clasificacion;
}Retorno en todos los caminos
En este ejemplo, la variable clasificacion siempre recibe un valor porque el else final cubre todos los casos restantes. Una forma equivalente y más directa:
public static String clasificarNota(int nota) {
if (nota >= 90) {
return "Excelente";
} else if (nota >= 70) {
return "Bueno";
} else if (nota >= 50) {
return "Regular";
} else {
return "Insuficiente";
}
}Return directo en cada rama
Ambas versiones son correctas. La segunda es más compacta, pero puede resultar menos clara cuando la lógica es más compleja.
Parámetros y Argumentos¶
Parámetros Múltiples¶
Un método puede recibir cualquier cantidad de parámetros, separados por comas. Cada parámetro necesita su propio tipo declarado, incluso si varios parámetros tienen el mismo tipo:
public static double calcularIMC(double peso, double altura) {
double imc = peso / (altura * altura);
return imc;
}
// Invocación
double miIMC = calcularIMC(70.5, 1.75);Método con múltiples parámetros
Orden de los Argumentos¶
El orden de los argumentos debe coincidir exactamente con el orden de los parámetros. Java asocia el primer argumento con el primer parámetro, el segundo con el segundo, y así sucesivamente:
public static void mostrarDatos(String nombre, int edad, double salario) {
System.out.printf("%s tiene %d años y gana $%.2f%n", nombre, edad, salario);
}
// Correcto: "Ana" → nombre, 30 → edad, 50000.0 → salario
mostrarDatos("Ana", 30, 50000.0);
// Incorrecto - error de compilación por tipos incompatibles
// mostrarDatos(30, "Ana", 50000.0); // Error: int no es compatible con StringImportancia del orden de argumentos
Si los tipos coincidieran por casualidad (por ejemplo, tres parámetros int), el compilador no detectaría el error pero el programa produciría resultados incorrectos. Por eso es importante elegir nombres de parámetros claros y verificar el orden al invocar.
Compatibilidad de Tipos y Promoción Automática¶
Java permite pasar argumentos de tipos “más pequeños” cuando el parámetro espera un tipo “más grande”. Esto se llama promoción automática o widening conversion. Es el mismo mecanismo que existe en C.
public static double dividir(double dividendo, double divisor) {
return dividendo / divisor;
}
// Todas estas llamadas son válidas
double r1 = dividir(10.0, 3.0); // double, double - coincidencia exacta
double r2 = dividir(10, 3.0); // int se promueve a double
double r3 = dividir(10.0, 3); // int se promueve a double
double r4 = dividir(10, 3); // ambos int se promueven a doublePromoción automática de tipos
La promoción sigue una jerarquía de tipos numéricos:
byte → short → int → long → float → doubleUn tipo puede promoverse a cualquier tipo que esté a su derecha en esta jerarquía. Por ejemplo:
bytepuede promoverse ashort,int,long,floatodoubleintpuede promoverse along,floatodoubledoubleno puede promoverse a nada (ya es el más “amplio”)
Firma de un Método y Sobrecarga¶
¿Qué es la Firma de un Método?¶
La firma (signature) de un método es su identificador único para el compilador. Permite distinguir un método de otro, incluso si tienen el mismo nombre. Se compone estrictamente de:
El nombre del método.
El número, tipo y orden de sus parámetros.
Elementos que no forman parte de la firma:
El tipo de retorno
Los modificadores de acceso (
public,private, etc.)Los nombres de los parámetros (solo importan los tipos)
Las excepciones declaradas con
throws
Esto significa que dos métodos con el mismo nombre y parámetros pero diferente tipo de retorno son considerados el mismo método por el compilador, lo cual genera un error.
Figure 1:Sobrecarga de métodos: ejemplos válidos e inválidos basados en la firma.
¿Qué es la Sobrecarga?¶
La sobrecarga (overloading) es la capacidad de definir múltiples métodos con el mismo nombre pero con firmas diferentes. Esto permite que un mismo nombre represente operaciones conceptualmente similares pero que trabajan con distintos tipos o cantidades de datos.
Ejemplos de Sobrecarga¶
// Todas estas son firmas diferentes porque varían en parámetros
// Firma: sumar(int, int)
public static int sumar(int a, int b) {
return a + b;
}
// Firma: sumar(int, int, int) - diferente cantidad de parámetros
public static int sumar(int a, int b, int c) {
return a + b + c;
}
// Firma: sumar(double, double) - diferentes tipos de parámetros
public static double sumar(double a, double b) {
return a + b;
}
// Firma: sumar(String, String) - diferentes tipos de parámetros
public static String sumar(String a, String b) {
return a + b; // Concatenación de cadenas
}Métodos sobrecargados válidos
La sobrecarga es útil porque permite usar nombres intuitivos. Sin ella, tendríamos que inventar nombres como sumarEnteros, sumarTresEnteros, sumarDobles, concatenarStrings, etc.
// ERROR DE COMPILACIÓN: tienen la misma firma
public static int calcular(int x) {
return x * 2;
}
public static double calcular(int x) { // Solo difiere en retorno
return x * 2.0;
}
// Error: method calcular(int) is already definedSobrecarga inválida - mismo nombre y parámetros
El compilador no puede distinguir cuál método llamar basándose solo en el tipo de retorno. ¿Qué debería hacer con calcular(5) si se usa sin asignar a ninguna variable?
Resolución de Sobrecarga (Overload Resolution)¶
Cuando invocás un método sobrecargado, el compilador debe determinar cuál de las versiones ejecutar. Este proceso se llama resolución de sobrecarga y sigue un orden de prioridad:
Coincidencia exacta de tipos: Si existe un método cuyos parámetros coinciden exactamente con los tipos de los argumentos, se elige ese.
Promoción de primitivos: Si no hay coincidencia exacta, el compilador intenta promociones automáticas (
int→long→float→double).Autoboxing/Unboxing: Si aún no hay coincidencia, intenta convertir entre tipos primitivos y sus clases envolventes (
int↔Integer,double↔Double). Esto se verá más adelante en el curso.Varargs: Como último recurso, busca coincidencias con parámetros variables.
public static void mostrar(int x) {
System.out.println("int: " + x);
}
public static void mostrar(double x) {
System.out.println("double: " + x);
}
public static void mostrar(String x) {
System.out.println("String: " + x);
}
// ¿Cuál se invoca?
mostrar(5); // int: 5 (coincidencia exacta con int)
mostrar(5.0); // double: 5.0 (coincidencia exacta con double)
mostrar("Hola"); // String: Hola (coincidencia exacta con String)
mostrar(5L); // double: 5.0 (long se promueve a double, no hay mostrar(long))
mostrar('A'); // int: 65 (char se promueve a int)Resolución de sobrecarga en acción
El último caso es interesante: 'A' es un char, pero como no hay mostrar(char), se promueve a int (el valor ASCII de ‘A’ es 65).
Mecanismo de Pasaje de Parámetros¶
Entender cómo se pasan los datos a los métodos es fundamental para evitar errores sutiles. En Java existe una regla simple pero que genera confusión: Java siempre pasa por valor.
¿Qué significa “pasar por valor”? Que cuando se invoca un método, se crea una copia del dato que se pasa como argumento. El método trabaja con esa copia, no con el original.
Sin embargo, hay un matiz importante que depende de si el dato es un tipo primitivo o una referencia.
Figure 2:Diferencia entre pasaje de primitivos (copia de valor) y referencias (copia de dirección).
Pasaje de Tipos Primitivos¶
Cuando se pasa un tipo primitivo (int, double, boolean, etc.), se copia el valor numérico. Cualquier modificación dentro del método afecta solo a la copia local, no a la variable original.
public static void duplicar(int numero) {
numero = numero * 2; // Modifica solo la copia local
System.out.println("Dentro del método: " + numero); // Imprime: 20
}
public static void main(String[] args) {
int valor = 10;
duplicar(valor);
System.out.println("Fuera del método: " + valor); // Imprime: 10 (sin cambios)
}Pasaje por valor con primitivos
Esto es idéntico a lo que ocurre en C con parámetros no-puntero:
// Equivalente en C - mismo comportamiento
void duplicar(int numero) {
numero = numero * 2;
printf("Dentro: %d\n", numero); // 20
}
int main() {
int valor = 10;
duplicar(valor);
printf("Fuera: %d\n", valor); // 10
}Pasaje de Referencias (Tipos no Primitivos)¶
Cuando se pasa un objeto (un tipo no primitivo como String, arreglos, o cualquier instancia de clase), lo que se pasa es una copia de la referencia. La referencia es esencialmente la dirección de memoria donde está el objeto.
Esto es análogo a pasar un puntero por valor en C: el puntero se copia, pero ambas copias apuntan a la misma zona de memoria.
Las consecuencias son dos:
Si se modifica el estado interno del objeto (llamando a sus métodos o modificando sus campos), el cambio sí es visible fuera del método, porque ambas referencias apuntan al mismo objeto.
Si se reasigna la referencia a un nuevo objeto, esto no afecta a la variable original, porque solo se modifica la copia local de la referencia.
public static void modificarContenido(StringBuilder sb) {
sb.append(" modificado"); // Cambia el objeto al que apunta sb
}
public static void reasignarReferencia(StringBuilder sb) {
sb = new StringBuilder("nuevo"); // sb ahora apunta a otro objeto
// La variable original sigue apuntando al objeto anterior
}
public static void main(String[] args) {
StringBuilder texto = new StringBuilder("original");
modificarContenido(texto);
System.out.println(texto); // Imprime: "original modificado"
// El cambio persiste porque modificamos el objeto, no la referencia
reasignarReferencia(texto);
System.out.println(texto); // Imprime: "original modificado"
// No cambió porque reasignar sb no afecta a texto
}Modificar estado vs reasignar referencia
Resumen del Pasaje de Parámetros¶
| Tipo de Dato | ¿Qué se Copia? | ¿Modificar afecta el original? |
|---|---|---|
Primitivo (int, double, etc.) | El valor numérico | No |
| Referencia (objetos, arreglos) | La dirección de memoria | Depende: modificar el objeto sí, reasignar la referencia no |
Alcance de Variables (Scope)¶
El alcance (o scope) de una variable determina en qué parte del código esa variable existe y puede ser accedida. Este concepto funciona de manera casi idéntica en C y Java.
Variables Locales¶
Las variables declaradas dentro de un método son locales a ese método. Existen desde el momento en que se declaran hasta que el método termina. Fuera del método, esas variables no existen.
public static void metodo1() {
int x = 10; // x solo existe en metodo1
System.out.println(x); // OK
}
public static void metodo2() {
// System.out.println(x); // ERROR: x no existe en este método
int x = 20; // Esta es una variable DIFERENTE, también llamada x
System.out.println(x); // OK, imprime 20
}Alcance de variables locales
Las dos variables x son completamente independientes. Que tengan el mismo nombre no las relaciona de ninguna manera. Esto es igual que en C.
Variables de Bloque¶
Dentro de un método, cada par de llaves {} define un bloque. Las variables declaradas dentro de un bloque solo existen dentro de ese bloque y sus bloques internos.
public static void ejemplo() {
int a = 1; // 'a' visible en todo el método
if (a > 0) {
int b = 2; // 'b' solo visible dentro del if
System.out.println(a + b); // OK: 'a' viene del bloque externo
}
// System.out.println(b); // ERROR: 'b' ya no existe
for (int i = 0; i < 5; i = i + 1) {
int c = i * 2; // 'c' solo visible dentro del for
System.out.println(c);
}
// System.out.println(i); // ERROR: 'i' solo existía en el for
// System.out.println(c); // ERROR: 'c' solo existía en el for
}Alcance dentro de bloques
Una variable declarada en un bloque externo es visible en los bloques internos, pero no al revés. Esta regla se conoce como anidamiento léxico y es idéntica en C.
Parámetros como Variables Locales¶
Los parámetros de un método se comportan exactamente como variables locales que ya vienen inicializadas con los valores de los argumentos. Existen durante toda la ejecución del método y desaparecen cuando el método termina.
public static int calcular(int valor) {
// 'valor' es una variable local inicializada con el argumento
valor = valor + 10; // Modifica solo la copia local
return valor;
}
public static void main(String[] args) {
int x = 5;
int resultado = calcular(x);
System.out.println(x); // 5 (no cambió)
System.out.println(resultado); // 15
}Parámetros como variables locales
Esto refuerza el concepto de pasaje por valor: modificar un parámetro es modificar una variable local, no el argumento original.
Sombreado de Variables (Shadowing)¶
Java no permite declarar una variable local con el mismo nombre que un parámetro, pero sí permite que una variable local “sombree” (shadow) a una variable de un ámbito externo como un campo de clase, los campos de clase, son similares a las variables globales en el hecho que es un valor compartido, pero son mucho, pero mucho, más, ya que son la base de la programación orientada a objetos, que veremos mas adelante.
public class Ejemplo {
static int valor = 100; // Variable de clase
public static void metodo(int valor) { // Parámetro sombrea a la de clase
// Aquí "valor" se refiere al parámetro, no al campo de clase
System.out.println(valor); // Imprime el parámetro
System.out.println(Ejemplo.valor); // Para acceder al campo de clase
}
}Sombreado (se evitará, por ahora, por claridad)
El sombreado puede generar confusión, por lo que es mejor evitarlo usando nombres distintos.
Gestión de la Pila: Stack Frames¶
Cuando un programa ejecuta métodos, la JVM (Java Virtual Machine) utiliza una estructura de datos llamada pila de llamadas (call stack) para gestionar la ejecución. Esta pila funciona exactamente igual que en C: cada vez que se invoca un método, se crea un nuevo “marco” (frame) en la pila; cuando el método termina, su marco se destruye.
¿Qué Contiene un Stack Frame?¶
Cada stack frame (marco de pila) almacena toda la información necesaria para ejecutar un método:
Variables Locales: Todas las variables declaradas dentro del método, incluyendo los parámetros recibidos. Estas se almacenan en un arreglo interno de posiciones numeradas.
Pila de Operandos: Una pila auxiliar donde la JVM realiza los cálculos intermedios. Por ejemplo, para calcular
a + b * c, primero se apilab, luegoc, se multiplican (el resultado queda en la pila), luego se apilaay se suma.Dirección de Retorno: La posición en el código donde debe continuar la ejecución cuando el método termine. Así el programa “sabe” a dónde volver.
Referencia al pool de constantes: Información de la clase que permite resolver nombres de métodos y campos.
Figure 3:Stack de llamadas mostrando los frames de métodos anidados.
El Ciclo de Vida de un Frame¶
Creación: Cuando se invoca un método, se reserva espacio en el tope de la pila para su frame.
Ejecución: El método trabaja con sus variables locales y operandos.
Retorno: Cuando el método ejecuta
returno llega al final de su cuerpo (si esvoid), su frame se destruye.Continuación: El control vuelve al frame anterior, que estaba “esperando” debajo en la pila.
Este mecanismo de pila tiene una propiedad importante: los métodos se completan en orden inverso al que fueron llamados (el último en entrar es el primero en salir, LIFO).
Ejemplo de Flujo de Ejecución¶
El siguiente ejemplo muestra paso a paso cómo se construye y destruye la pila de llamadas:
public static void main(String[] args) {
int resultado = metodoA(5); // 1. Se crea frame para main
System.out.println(resultado); // 5. Continúa main con resultado = 22
} // 6. Se destruye frame de main
public static int metodoA(int x) {
int y = metodoB(x + 1); // 2. Se crea frame para metodoA (x=5)
return y * 2; // 4. metodoA calcula 11*2=22 y retorna
} // Su frame se destruye
public static int metodoB(int n) {
return n + 10; // 3. metodoB calcula 6+10=16 y retorna
} // Su frame se destruye inmediatamenteSeguimiento del stack de llamadas
El estado de la pila en cada momento:
| Paso | Acción | Estado de la Pila (tope → base) |
|---|---|---|
| 1 | main llama a metodoA(5) | metodoA ← main |
| 2 | metodoA llama a metodoB(6) | metodoB ← metodoA ← main |
| 3 | metodoB retorna 16 | metodoA ← main |
| 4 | metodoA retorna 22 | main |
| 5 | main imprime 22 | main |
| 6 | main termina | (pila vacía) |
Recursión y StackOverflowError¶
Un método puede llamarse a sí mismo; esto se llama recursión. Cada llamada recursiva crea un nuevo frame en la pila, con sus propias copias de las variables locales. Es fundamental que exista un caso base que detenga la recursión; de lo contrario, la pila crecerá indefinidamente hasta agotar la memoria asignada.
public static long factorial(int n) {
// Caso base: detiene la recursión
if (n <= 1) {
return 1;
}
// Caso recursivo: n! = n * (n-1)!
return n * factorial(n - 1);
}Método recursivo para calcular factorial
La definición matemática del factorial es naturalmente recursiva:
Para factorial(4), la pila crece así:
factorial(4)espera el resultado defactorial(3)factorial(3)espera el resultado defactorial(2)factorial(2)espera el resultado defactorial(1)factorial(1)retorna 1 (caso base)factorial(2)retorna 2 * 1 = 2factorial(3)retorna 3 * 2 = 6factorial(4)retorna 4 * 6 = 24
public static void infinito() {
infinito(); // Sin caso base, nunca termina
// Eventualmente: java.lang.StackOverflowError
}Recursión sin caso base - StackOverflowError
El StackOverflowError ocurre cuando la pila de llamadas crece más allá del límite de memoria asignado. En Java, el tamaño de la pila es configurable pero finito. La recursión muy profunda puede causar este error incluso con un caso base correcto, simplemente porque los datos de entrada requieren demasiados niveles.
Métodos Estáticos vs. de Instancia¶
Los métodos (y variables) estáticos pertenecen a la clase y se cargan cuando la JVM carga la clase por primera vez, mientras que lo que no es static, pertenece a los objetos creados a partir de esa clase.
Este es otro de los temas que veremos en la parte de Orientación a Objetos más adelante.
De momento, lo que tenemos que saber sobre este calificador es que:
Se puede invocar sin crear un objeto: Basta con usar el nombre de la clase seguido de un punto y el nombre del método:
NombreClase.metodo().Solo puede acceder a otros miembros estáticos: No puede usar variables de instancia ni llamar a métodos de instancia directamente, ya que tendría que crear un objeto primero.
Comparación Rápida¶
| Aspecto | Método Estático | Método de Instancia |
|---|---|---|
| Palabra clave | Incluye static | No incluye static |
| Invocación | Clase.metodo() | objeto.metodo() |
Acceso a this | No | Sí |
| Típico uso | Utilidades, funciones matemáticas | Operaciones sobre datos del objeto |
Por ahora, todos los métodos que escribamos serán public static para poder llamarlos desde main. La distinción con métodos de instancia se explorará más adelante.
Ejercicios¶
Los siguientes ejercicios permiten practicar los conceptos vistos. Se recomienda intentar resolverlos antes de ver las soluciones.
Solution to Exercise 1
El programa imprime 10 AB.
Análisis paso a paso:
En
main, se declarax = 10ysapunta a unStringBuildercon contenido “A”.Se llama a
modificar(x, s):Se crea una copia de
x(el valor 10) en el parámetronSe crea una copia de
s(la referencia al StringBuilder) en el parámetrosbAhora
sbapunta al mismo objeto StringBuilder ques
Dentro de
modificar:n = 20: Modifica solo la copia local.xen main sigue siendo 10.sb.append("B"): Modifica el objeto StringBuilder. Comosbysapuntan al mismo objeto, el cambio es visible desde ambas referencias. El contenido ahora es “AB”.sb = new StringBuilder("C"): Reasignasba un nuevo objeto. Esto no afecta asporque solo cambia la copia local de la referencia.
Al volver a
main:xsigue siendo 10 (nunca se modificó)ssigue apuntando al StringBuilder original, cuyo contenido es “AB”
Solution to Exercise 2
Se invocará procesar(int x) y se imprimirá “int”.
El proceso de resolución de sobrecarga sigue estas prioridades:
Coincidencia exacta: El literal
5es de tipoint. Existe un métodoprocesar(int x), por lo tanto hay coincidencia exacta.Si no hubiera
procesar(int x), el compilador buscaría promociones:int→long: Usaríaprocesar(long x)Si tampoco existiera,
int→double: Usaríaprocesar(double x)
La regla general es que el compilador siempre prefiere la coincidencia más específica (que requiera menos conversiones).
Solution to Exercise 3
El orden de ejecución es: 1 → 3 → 5 → 4 → 2
Desglose detallado:
Línea 1:
maincomienza a ejecutar. Al evaluarmetodoA(), se crea un frame parametodoAy se transfiere el control.Pila:
metodoA←main
Línea 3: Dentro de
metodoA, al evaluarmetodoB(), se crea un frame parametodoB.Pila:
metodoB←metodoA←main
Línea 5:
metodoBcalcula y retorna 10. Su frame se destruye.Pila:
metodoA←main
Línea 4:
metodoArecibe el valor 10 enb, calcula10 + 1 = 11y retorna. Su frame se destruye.Pila:
main
Línea 2:
mainrecibe el valor 11 enay lo imprime.Pila:
main
El resultado impreso es 11.
Solution to Exercise 4
public static boolean esPrimo(int numero) {
// El 1 no es primo por definición
if (numero <= 1) {
return false;
}
// El 2 es el único primo par
if (numero == 2) {
return true;
}
// Los demás números pares no son primos
if (numero % 2 == 0) {
return false;
}
// Verificar divisores impares hasta la raíz cuadrada
// Si n tiene un divisor mayor que √n, también tiene uno menor
boolean esPrimo = true;
int divisor = 3;
while (divisor * divisor <= numero && esPrimo) {
if (numero % divisor == 0) {
esPrimo = false; // Encontramos un divisor, no es primo
}
divisor = divisor + 2; // Solo probamos impares
}
return esPrimo;
}Explicación de la optimización:
Solo verificamos hasta la raíz cuadrada porque si
n = a × bcona ≤ √n, entoncesb ≥ √n. Si no encontramos ningún divisor hasta√n, no habrá ninguno mayor.Solo probamos divisores impares (excepto el 2) porque todos los números pares mayores que 2 ya fueron descartados.
Solution to Exercise 5
public static long potencia(int base, int exponente) {
// Caso base: cualquier número elevado a 0 es 1
if (exponente == 0) {
return 1;
}
// Caso recursivo: b^n = b * b^(n-1)
return base * potencia(base, exponente - 1);
}Ejemplo de ejecución para potencia(2, 4):
potencia(2, 4)→ 2 ×potencia(2, 3)potencia(2, 3)→ 2 ×potencia(2, 2)potencia(2, 2)→ 2 ×potencia(2, 1)potencia(2, 1)→ 2 ×potencia(2, 0)potencia(2, 0)→ 1 (caso base)Volviendo: 2×1=2, 2×2=4, 2×4=8, 2×8=16
Resultado: 16
Nota: Esta implementación es simple pero no es la más eficiente. Existe una versión que usa cuando n es par, reduciendo la cantidad de llamadas recursivas de O(n) a O(log n).
Buenas Prácticas para Métodos¶
Las siguientes recomendaciones ayudan a escribir métodos claros, mantenibles y menos propensos a errores.
Nombres descriptivos¶
El nombre del método debe indicar claramente qué hace. La convención en Java es usar verbos o frases verbales en camelCase.
// Buenos nombres - indican claramente la acción
public static double calcularPromedio(int[] numeros) { ... }
public static boolean esEmailValido(String email) { ... }
public static void imprimirReporte(String datos) { ... }
public static int contarPalabras(String texto) { ... }
public static String obtenerNombreCompleto(String nombre, String apellido) { ... }
// Nombres poco claros - evitar
public static double cp(int[] n) { ... } // Abreviatura críptica
public static boolean validar(String s) { ... } // ¿Qué valida?
public static void proceso1(String d) { ... } // Completamente opaco
public static int f(int x) { ... } // Sin significadoBuenos nombres de métodos
Para métodos que devuelven boolean, se suelen usar prefijos como:
es/is:esValido(),esPar(),isEmpty()tiene/has:tieneElementos(),hasNext()puede/can:puedeAcceder(),canRead()
Métodos Cortos y Enfocados (Principio de Responsabilidad Única)¶
Cada método debe hacer una sola cosa y hacerla bien. Si un método hace demasiadas cosas, se vuelve difícil de entender, probar y modificar.
Un buen indicador de que un método hace demasiado es si cuesta resumir qué hace en una oración simple.
// Problema: este método hace cuatro cosas diferentes
public static void procesarDatos(int[] datos) {
// Lee datos del usuario
Scanner sc = new Scanner(System.in);
for (int i = 0; i < datos.length; i++) {
datos[i] = sc.nextInt();
}
// Valida que no haya negativos
for (int d : datos) {
if (d < 0) {
System.out.println("Error: dato negativo");
return;
}
}
// Calcula el promedio
int suma = 0;
for (int d : datos) {
suma = suma + d;
}
double promedio = (double) suma / datos.length;
// Imprime el resultado
System.out.println("Promedio: " + promedio);
}Un método haciendo demasiadas cosas
public static void leerDatos(int[] datos) {
Scanner sc = new Scanner(System.in);
for (int i = 0; i < datos.length; i++) {
datos[i] = sc.nextInt();
}
}
public static boolean sonDatosValidos(int[] datos) {
for (int d : datos) {
if (d < 0) {
return false;
}
}
return true;
}
public static double calcularPromedio(int[] datos) {
int suma = 0;
for (int d : datos) {
suma = suma + d;
}
return (double) suma / datos.length;
}
public static void imprimirResultado(double promedio) {
System.out.println("Promedio: " + promedio);
}
// El método principal coordina, pero no hace el trabajo
public static void procesarDatos(int[] datos) {
leerDatos(datos);
if (sonDatosValidos(datos)) {
double promedio = calcularPromedio(datos);
imprimirResultado(promedio);
} else {
System.out.println("Error: datos inválidos");
}
}Responsabilidades separadas en métodos pequeños
La segunda versión es más larga en total, pero cada método es simple, fácil de entender y fácil de probar de forma independiente.
Evitar Efectos Secundarios Inesperados¶
Un efecto secundario es cualquier cambio de estado observable fuera del método: imprimir en pantalla, modificar variables globales, escribir en archivos, etc. No todos los efectos secundarios son malos, pero deberían ser esperables según el nombre del método.
Un método llamado calcularSuma debería calcular y retornar una suma, no imprimir mensajes ni modificar variables externas. Si lo hace, sorprende al programador que lo usa.
// Problema: el nombre sugiere que solo calcula, pero también imprime
public static int sumar(int a, int b) {
int resultado = a + b;
System.out.println("Calculando suma..."); // Efecto inesperado
return resultado;
}
// ¿Por qué es malo? Imaginar que se usa así:
// int total = sumar(x, y) + sumar(z, w);
// Esto imprimirá "Calculando suma..." DOS veces, lo cual es confusoEfecto secundario inesperado
// Mejor: hace exactamente lo que el nombre indica
public static int sumar(int a, int b) {
return a + b;
}
// Si se necesita imprimir, hacerlo en un método cuyo nombre lo indique
public static void imprimirSuma(int a, int b) {
System.out.println("La suma es: " + (a + b));
}Sin efectos secundarios inesperados
Regla práctica: Un método con nombre que empiece con “calcular”, “obtener”, “es”, “tiene” no debería tener efectos secundarios. Un método con nombre que empiece con “imprimir”, “guardar”, “enviar” claramente tiene efectos y eso está bien.
Documentación con Javadoc¶
Los métodos públicos deberían documentarse con Javadoc, un formato especial de comentario que comienza con /** y puede incluir etiquetas estructuradas. Las herramientas de Java pueden extraer estos comentarios y generar documentación HTML automáticamente.
/**
* Calcula el factorial de un número entero no negativo.
*
* El factorial de n (escrito n!) es el producto de todos los
* enteros positivos menores o iguales a n. Por ejemplo,
* factorial(5) = 5 × 4 × 3 × 2 × 1 = 120.
*
* @param n Número del cual calcular el factorial. Debe ser >= 0.
* @return El factorial de n (n!)
* @throws IllegalArgumentException si n es negativo
*/
public static long factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("n debe ser no negativo");
}
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}Documentación Javadoc completa
Las etiquetas más comunes son:
@param nombreParametro descripción: Describe un parámetro de entrada.@return descripción: Describe qué devuelve el método.@throws NombreExcepcion condición: Describe cuándo se lanza una excepción.
Para métodos privados o muy simples, un comentario breve o ninguno puede ser suficiente si el código es autoexplicativo. La documentación excesiva de lo obvio puede ser contraproducente.
Limitar la Cantidad de Parámetros¶
Un método con muchos parámetros (más de 3 o 4) se vuelve difícil de usar correctamente. Es fácil equivocarse en el orden de los argumentos.
// Problema: ¿cuál va primero, ancho o alto? ¿y los colores?
public static void dibujarRectangulo(int x, int y, int ancho, int alto,
int colorBorde, int colorRelleno, boolean rellenar) { ... }
// Al llamarlo, es fácil equivocarse:
dibujarRectangulo(10, 20, 100, 50, 0xFF0000, 0x00FF00, true);
// ¿O era así?
dibujarRectangulo(10, 20, 50, 100, 0x00FF00, 0xFF0000, false);Demasiados parámetros
Soluciones posibles:
Dividir el método en varios más específicos
Agrupar parámetros relacionados en una estructura (cuando se vean clases)
Usar patrones como Builder (tema avanzado)
Referencias Bibliográficas¶
Bloch, J. (2018). Effective Java (3ra ed.). Addison-Wesley Professional.
Liang, Y. D. (2017). Introduction to Java Programming and Data Structures (11va ed.). Pearson.
Schildt, H. (2022). Java: A Beginner’s Guide (9na ed.). McGraw Hill.
Gosling, J., et al. (2015). The Java Language Specification. Oracle.