02.2 - Funciones
En programación, el concepto de reutilización de código es fundamental. Una de las herramientas que nos ayuda a conseguir esa reutilización son las funciones. Las funciones empaquetan y aíslan bloques de código que realiza una tarea determinada.
Una función se compone de dos partes fundamentales:
- Cabecera: definición de la función.
- Cuerpo: implementación de la función.
1. Cabecera
La cabecera de una función incluye un nombre definido por el programador y los parámetros de entrada. Además, lenguajes con tipado fuerte como Java requieren definir el tipo de dato que devolverá.
int add(int number1, int number2)
static) que modifican el comportamiento de la función. De hecho, hasta que lleguemos a POO, definiremos todas nuestras funciones como static.
Las funciones pueden devolver cualquier tipo de parámetro (no solo los tipos primitivos), por ejemplo un String:
static String getHelloWorld()
Existe un tipo de funciones donde no se devuelve ningún parámetro. Para estos casos, en Java se utiliza la palabra void para indicar que esa función no devuelve nada:
static void myFunction()
En cuanto a los parámetros de entrada, tenemos que indicar cuántos son y de qué tipo es cada uno. Por ejemplo, la primera función de ejemplo (add) recibe dos parámetros de tipos int (number1 y number2). Una vez definidos los parámetros de entrada, ya podemos utilizarlos dentro de la función como si fueran variables.
2. Cuerpo
El cuerpo de la función es el bloque de código que indica qué (y cómo) realiza esa función. El cuerpo está encerrado entre llaves ({ }), y es el código que queremos encapsular (ocultar) al resto de la aplicación.
static int add(int number1, int number2) {
return number1 + number2;
}
static String getHelloWorld() {
return "Hola mundo";
}
3. Devolución de valores
Dentro del cuerpo de una función, si queremos que ésta devuelva un valor lo tenemos que indicar con la palabra reservada return:
static int add(int number1, int number2) {
return number1 + number2;
}
Ten en cuenta que cuando se ejecuta la orden return se sale de la función, con lo que sólo puede haber un return en cada flujo de código de una función.
Eso no quiere decir que no pueda haber dos return definidos en el cuerpo de una función:
static int myFunction(int option) {
if (option > 10) {
return "Mayor que 10";
} else {
return "Menor o igual a 10"
}
}
En este caso, Java no muestra ningún error, ya que dependiendo del valor de option se ejecutará un return u otro, pero nunca los dos a la vez.
Una función puede devolver un valor, una variable, el resultado de una operación… La única condición es que coincida con el tipo de parámetro de salida que hemos definido en la cabecera.
static int myFunction() {
return 3;
}
static int myFunction() {
int result = 10
return result;
}
static int myFunction() {
int a = 5;
int b = 2;
return a + b;
}
4. Ámbito de las variables
Las variables solo existen dentro del bloque de código ({}) donde han sido declaradas.
class AmbitoVariables {
static void main() {
int resultado = 10;
add();
}
static int add() {
return resultado + 5; // ❌ Error: no encuentra la variable 'resultado'
}
}
for: fuera de sus llaves dejan de existir.
4.1 Variables de clase (Variables globales)
Para definir variables que sean accesibles desde cualquier punto de nuestra clase, las declaramos fuera de los métodos con la palabra reservada static:
class AmbitoVariables {
static int resultado = 10; // Variable accesible en toda la clase
static void main() {
add();
}
static int add() {
return resultado + 5; // Funciona correctamente
}
5. Llamada a una función
Para llamar a una función, la invocamos por su nombre pasándole los argumentos requeridos entre paréntesis:
class LlamadaFuncion {
static void main() {
int a = 2, b = 3;
int result = add(a, b);
}
static int add(int number1, int number2) {
return number1 + number2;
}
}
5.1 Paso de parámetros: Por valor
En Java, todos los parámetros de tipo primitivo se pasan por valor. Esto significa que al llamar a una función se envía una copia del dato, por lo que cualquier modificación dentro del método no afectará a la variable original.
static void main() {
int a = 2;
IO.println(a); // Muestra 2
changeNumber(a);
IO.println(a); // Muestra 2 (no ha cambiado)
}
static void changeNumber(int number1) {
number1 = 10; // Modifica únicamente la copia local
}
Paso de arrays a funciones
A diferencia de los tipos primitivos, los arrays son objetos. Al pasarlos como parámetro a un método, se copia la referencia a la memoria original. Por tanto, cualquier cambio dentro del método modificará el array original.
static void main() {
int[] numbers = {1, 2, 3};
updateFirst(numbers);
IO.println(numbers[0]); // Muestra 99
}
static void updateFirst(int[] data) {
data[0] = 99; // Modifica el array original
}
5.2 Parámetros reales y formales
A la hora de trabajar con funciones, distinguimos dos tipos de parámetros:
- Parámetros reales (o argumentos): Son las variables o valores reales que se envían desde la llamada a la función.
- Parámetros formales: Son las variables locales declaradas en la cabecera de la función que reciben la copia de los valores.
static void main() {
int a = 2, b = 3;
// 'a' y 'b' son los parámetros reales
int result = add(a, b);
}
//'number1' y 'number2' son los parámetros formales
static int add(int number1, int number2) {
return number1 + number2;
}
5.3 Parámetros de longitud variable (Varargs)
Si no sabemos a priori cuántos parámetros pasaremos a una función, podemos utilizar tres puntos (…) tras el tipo de dato en la cabecera. Internamente Java tratará esos datos como un array.
static void main() {
int a = 1, b = 2, c = 3, d = 4;
myFunction(a, b); // Pasa 2 argumentos
myFunction(a, b, c, d); // Pasa 4 argumentos
}
static void myFunction(int... numbers) {
for (int i = 0; i < numbers.length; i++) {
IO.println(numbers[i]);
}
}
6. Sobrecarga de métodos
La sobrecarga consiste en definir varias funciones con el mismo nombre, pero con diferente cantidad o tipo de parámetros. Java ejecutará una u otra versión automáticamente según los argumentos enviados.
static void main() {
myFunction(1); // Llama a int
myFunction(3.0f); // Llama a float
myFunction(1, 2); // Llama a int, int
}
static void myFunction(int a) {
IO.println("Parámetro int");
}
static void myFunction(float a) {
IO.println("Parámetro float");
}
static void myFunction(int a, int b) {
IO.println("Dos parámetros int");
}
A la sobrecarga de métodos también se la conoce como polimorfismo estático o polimorfismo en tiempo de compilación.
Se le llama “estático” porque el compilador de Java determina exactamente qué versión del método se va a ejecutar antes de arrancar el programa, analizando el número y tipo de los argumentos pasados en la llamada.
Más adelante, en el tema de Programación Orientada a Objetos, estudiaremos el polimorfismo dinámico, el cual se resuelve durante la ejecución del programa mediante la herencia y la sobrescritura de métodos.
7. Recursividad
La recursividad es la técnica por la cual una función se llama a sí misma para resolver un problema.
Ejemplo: Cálculo del factorial de un número.
7.1 Enfoque Iterativo
static int factorialIterativo(int number) {
int factorial = 1;
for (int i = 2; i <= number; i++) {
factorial *= i;
}
return factorial;
}
7.2 Enfoque Recursivo
static int factorialRecursivo(int number) {
if (number == 1) { // Caso base
return 1;
} else { // Caso recursivo
return number * factorialRecursivo(number - 1);
}
}
Para factorialRecursivo(3), la traza de ejecución es:
3 * factorialRecursivo(2) -> 3 * (2 * factorialRecursivo(1)) -> 3 * (2 * 1) = 6
Ejercicios
Ejercicio 16.a
Escribe un programa que calcula la suma, resta, multiplicación y división de dos números enteros (introducidos por teclado) mediante funciones. En el caso de la división, el resultado deberá ser de tipo float.
Ejercicio 16.b
Haz que la aplicación anterior muestre la frase “Error: División por 0” cuando el segundo número sea $0$ en la división.
Ejercicio 17.a
Crea el menú del ejercicio 15.a del tema anterior mediante una función.
Ejercicio 17.b
Crea el menú del ejercicio 15.b del tema anterior mediante una función.
Ejercicio 17.c
Haz que la opción del ejercicio anterior sea una variable global.
Ejercicio 18.a
Escribe una función que acepte dos números como parámetros de entrada y devuelva el mayor de ellos. Haz un programa que utilice esa función para mostrar por pantalla el mayor de dos números introducidos por teclado.
Ejercicio 18.b
Cambia la función anterior para que la función devuelva $1$ si el primer número es mayor que el segundo, $-1$ si el segundo es el mayor o $0$ si son iguales.
Ejercicio 18.c
Haz que las variables de entrada del ejercicio anterior (los dos números introducidos por teclado) sean variables globales de la aplicación.
Ejercicio 19.a
Crea una función que reciba un número variable de enteros y devuelva la suma de todos ellos.
Ejercicio 19.b
Modifica la función anterior para que sume sólo los números impares.
Ejercicio 20.a
Crea un programa que calcule el área de un cuadrado y de un triángulo. Utiliza sobrecarga de métodos para calcular ambas áreas.
Ejercicio 20.b
Modifica el programa anterior para que la aplicación muestre un menú donde el usuario podrá elegir entre calcular el área de un cuadrado o de un triángulo con los métodos anteriores. La aplicación deberá pedir los datos correspondientes (lado o altura y base) según la opción escogida.
Ejercicio 21.a
Haz una función recursiva que sume los números naturales (enteros positivos sin contar el $0$) hasta un número introducido por pantalla.
Ejercicio 21.b
Haz que a la función anterior se le pueda pasar un número negativo, con lo que sumará desde el $0$ hasta ese número negativo (la función tiene que seguir siendo recursiva). También se le podrá pasar el $0$, con lo que el resultado devuelto debería ser $0$.