Skip to content

3 Funciones

Introducción

Una subrutina o subprograma, como idea general, se presenta como un subalgoritmo que forma parte del algoritmo principal, el cual permite resolver una tarea específica.

Podemos distinguir tres términos que poseen diferencias:

  1. Función: conjunto de instrucciones que devuelven un resultado.
  2. Procedimiento: conjunto de instrucciones que se ejecutan sin devolver ningún resultado.
  3. Método: función o procedimiento que pertenece a un objeto.

Dichas funciones se pueden utilizar desde muchos sitios diferentes, por lo que de manera general, no se suelen poner mensajes en consola en las funciones ya que puede ser que dichos mensajes no interesen en todos los sitios donde se utilice dicha función. A no ser que la función se haya creado específicamente para dar mensajes informativos en consola.

Construcción

Una función se construye de la siguiente manera:

modificador_acceso tipo_resultado nombre_función (tipo_parámetro nombre_parámetro, ...){
    // instrucciones
    return expresión;
}
  • modificador_acceso: es la visibilidad que posee la función (Lo veremos más adelante. De momento, lo utilizaremos como public).
  • tipo_resultado: es el tipo del resultado que devuelve la función.
  • nombre_función: es el nombre que identifica a la función. Utiliza la notación lowerCamelCase. Ejemplo: imprimirResultadoDecimal.
  • tipo_parámetro nombre_parámetro, ...: puede ocurrir que la función necesite ciertos valores para efectuar la misión para la que ha sido creada. Por ejemplo, la función suma necesitaría los valores que tiene que sumar. En este caso, se deben indicar cada uno de dichos valores con sus tipos correspondientes. A estos valores se les conoce como parámetros de la función. Si la función no necesita parámetros, entonces solamente se ponen los paréntesis: nombre_función( ).

A todo esto se le conoce como firma (signature) de la función:

modificador_acceso tipo_resultado nombre_función (tipo_parámetro nombre_parámetro, ...)
  • instrucciones: instrucciones que conforman el algoritmo de la función, para que realice la misión para la que ha sido creada.
  • return expresión: con el return se termina la ejecución de la función y si va acompañado de una expresión, la función devuelve como resultado el valor de dicha expresión. Dicho valor tiene que ser del tipo_resultado indicado en la firma de la función.

Ejemplo de la función con dos parámetros:

public static int add(int sum1, int sum2){
    return sum1 + sum2;
}

Puede ser también que haya más de un return. En ese caso, el flujo de ejecución abandona la función en cuanto ejecute el primer return. Ejemplo:

public static boolean isPair(int n){
    if(n % 2 == 0){
        return true;
    } else {
        return false;
    }
}

En el caso de que estemos definiendo un procedimiento, no tendremos return expresión ya que no devuelve ningún resultado y el tipo_resultado es void. Como por ejemplo System.put.println, que escribe en pantalla lo que recibe por parámetro pero no devuelve nada.

Llamada a la función

Una función permite que reutilicemos un algoritmo ya que se puede utilizar cuando nos haga falta. Para ello, solamente tendremos que llamar a la función por su nombre y pasarle los parámetros en el mismo orden que se han definido y pertenecientes al mismo tipo de dato o compatible. En la llamada, dichos parámetros se llaman argumentos, es decir, los argumentos son los valores iniciales de los parámetros.

public class Functions1 {
    public static void main(String[] args) {
        boolean pair;
        int result;

        /*
         * Se llama a la función isPair con un valor de 5 en el argumento, es decir,
         * el valor inicial del parámetro n es 5:
         */
        pair = isPair(5);
        System.out.println(pair); // false

        /*
         * Se lla a la función isPair con un valor de 4 en el argumento, es decir,
         * ahora el valor inicial del parámetro n es 4.
         */
        pair = isPair(4);
        System.out.println(pair); // true

        /*
         * Se llama a la función add con los valores 5 y 2 en los argumentos, es decir,
         * los valores iniciales de los parámetros sum1 y sum2 son 5 y 2 respectivamente.
         */
        result = add(5,2);
        System.out.println(result); // 7
    }

    public static int add(int sum1, int sum2){
        return sum1 + sum2;
    }

    public static boolean isPair(int n){
        if(n % 2 == 0){
            return true;
        } else {
            return false;
        }
    }
}
Ejercicio 1

Realiza una función que reciba la base y el exponente y devuelva la potencia baseexponente sin utilizar Math.pow

Ámbito de vida de los parámetros

A nivel de visibilidad y de ámbito de vida, los parámetros funcionan como las variables locales (Ver tema 1.3 Variables y constantes 4. Ámbito de vida de las variables), por lo tanto el ámbito de vida de los parámetros es el bloque donde han sido definidos, es decir, la propia función. Cada vez que se llame a la función, los parámetros nacen, se ejecuta la función y una vez que la función ha terminado de ejecutarse, los parámetros mueren.

public class Functions2 {
    public static void main(String[] args) {
        boolean pair;
        int result;

        pair = isPair(5); //(1)!

        //(2)!

        System.out.println(pair);
        pair = isPair(4); //(3)!

        //(4)!

        System.out.println(pair);

        result = add(5, 2); //(5)!

        //(6)!

        System.out.println(result);
    }

    public static int add(int sum1, int sum2){ //(7)!
        return sum1 + sum2;
    } //(8)!

    public static boolean isPair(int n){ //(9)!
        if(n % 2 == 0){
            return true;
        } else {
            return false;
        }
    } //(10)!
}
  1. Nace el parámetro n con el valor 5
  2. Aquí n ya no existe porque la función isPair ya ha terminado de ejecutarse
  3. Vuelve a nacer n pero esta vez con un valor de 4
  4. Aquí n ya no existe porque la función isPair ya ha terminado de ejecutarse
  5. Nacen los parámetros sum1 y sum2 con los valores 5 y 2 respectivamente
  6. Aquí sum1 y sum2 ya no existen porque la función add ha terminado de ejecutarse
  7. Comienzo del ámbito de vida de los parámetros sum1 y sum2
  8. Fin del ámbito de vida de los parámetros sum1 y sum2
  9. Comienzo del ámbito de vida del parámetro n
  10. Fin del ámbito de vida del parámetro n
Ejercicio 2

Realiza una función que reciba 3 parámetros: dos de tipo entero y uno de tipo carácter. La función deberá sumar, restar, multiplicar o dividir los valores de los dos primeros parámetros dependiendo de la operación indicada en el tercer parámetro, y devolver el resultado

Ejercicio 3

Sobrecarga la función del ejercicio anterior para que se pueda operar con enteros y con decimales. Haz un programa que utilice las dos funciones, con enteros y con decimales

Ejemplo de función: el factorial de un número

El factorial de un entero positivo n, también indicado como n!, se define como el producto de todos los números enteros positivos desde 1 hasta n. Por ejemplo:

5! = 1 x 2 x 3 x 4 x 5 = 120

La operación de factorial aparece en muchas áreas de las matemáticas, particularmente en combinatoria y análisis matemático. De manera fundamental, el factorial de n representa el número de formas distintas de ordenar n objetos distintos (elementos sin repetición). Este hecho ha sido conocido desde hace varios siglos, en el siglo XII, por los hindúes.

Veamos cómo se programaría dicha función factorial y las llamadas con distintos tipos de argumentos:

import java.util.Scanner;

public class Factorial {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        int n, variable;

        System.out.println("Llamada a la función con argumentos literales: ");
        System.out.printf("El factorial de 5 es %d\n", factorial(5));

        System.out.println("Llamada a la función usando una variable como argumento: ");
        variable = 5;
        System.out.printf("El factorial de %d es %d\n", variable, factorial(variable));

        System.out.println("Llamada a la función usando una expresión como argumento: ");
        variable = 3;
        System.out.printf("El factorial de %d es %d\n", variable + 2, factorial(variable + 2));

        System.out.println("Llamada a la función con argumentos introducidos por el usuario: ");
        do {
            System.out.println("Introduzca un número entero positivo: ");
            n = keyboard.nextInt();
        } while (n <= 0);
        System.out.printf("El factorial de %d es %d", n, factorial(n));
    }

    public static int factorial(int n) {
        int result = 1;
        for (int i = 2; i <= n; i++) {
            result *= i;
        }
        return result;
    }
}

El concepto del factorial se aplica a los números enteros positivos, pero como los int admiten números negativos, se podría llamar a la función con un número negativo aunque no tenga sentido:

int result = factorial(-5); //(1)!
System.out.println("El factorial de %d es %d\n", -5, result); //(2)!
  1. No tiene mucho sentido porque el factorial se aplica a números positivos
  2. El factorial de -5 es 1

Es decir, cuando programamos una función, no podemos dar por hecho que el programador que la vaya a utilizar lo haga de manera adecuada con la lógica que representa su funcionalidad. Así que siempre que programemos una función, debemos asegurarnos que va a funcionar correctamente para todos los valores posibles del parámetro. En nuestro caso, no podemos dar un resultado coherente para los números negativos puesto que no tiene sentido matemáticamente el factorial de un número negativo, así que lo más conveniente es lanzar un error cuando llamen a la función con un número negativo.

import java.util.Scanner;

public class Factorial {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        int n, variable;

        System.out.println("Llamada a la función con argumentos literales: ");
        System.out.printf("El factorial de 5 es %d\n", factorial(5));

        System.out.println("Llamada a la función usando una variable como argumento: ");
        variable = 5;
        System.out.printf("El factorial de %d es %d\n", variable, factorial(variable));

        System.out.println("Llamada a la función usando una expresión como argumento: ");
        variable = 3;
        System.out.printf("El factorial de %d es %d\n", variable + 2, factorial(variable + 2));

        System.out.println("Llamada a la función con argumentos introducidos por el usuario: ");
        do {
            System.out.println("Introduzca un número entero positivo: ");
            n = keyboard.nextInt();
        } while (n <= 0);
        System.out.printf("El factorial de %d es %d", n, factorial(n));

        System.out.println("Llamada a la función con argumentos negativos");
        variable = - 5;
        System.out.printf("El factorial %d es %d\n", variable, factorial(variable));
    }

    public static int factorial(int n) {
        int result = 1;

        if(n < 0){
            throw new IllegalArgumentException("El factorial se aplica a números positivos");
        }

        for (int i = 2; i <= n; i++) {
            result *= i;
        }
        return result;
    }
}

Salida por consola:

Exception in thread "main" java.lang.IllegalArgumentException: El factorial se 
aplica a números positivos
    at tema2_3_Funciones.Factorial3.factorial(Factorial3.java:19)
    at tema2_3_Funciones.Factorial3.main(Factorial3.java:9)
Ejercicio 4

Realiza una función que encuentre el primer valor N para el que la suma 1 + 2 + 3 + .. + N exceda a un valor M que se introduce por parámetro. Es decir, si M vale:

  • 1: devuelve 2
  • 3: devuelve 3
  • 7: devuelve 4
  • 10: devuelve 5
  • 15: devuelve 6
Ejercicio 5

El máximo común divisor de dos enteros es el entero más grande que es divisor exacto de los dos números. Realiza una función que devuelva el máximo común divisor de dos enteros. Por ejemplo, 12 es el mcd de 36 y 60.

Ejemplo de Procedimiento

No tendremos return expresión ya que no devuelve ningún resultado y el tipo_resultado es void:

import java.util.Scanner;

import static examples.tema_01.Colors.GREEN;
import static examples.tema_01.Colors.RESET;

public class Procedure {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        String string;

        System.out.print("Introduce una cadena: ");
        string = keyboard.nextLine();
        paintGreen(string);
    }

    private static void paintGreen(String string) {
        System.out.printf("La cadena que has introducido en verde: %s", GREEN + string + RESET);
    }
}

Resultado de las funciones

En las llamadas a funciones, no hay que obligatoriamente utilizar el valor devuelto:

import java.util.Scanner;

import static examples.tema_01.Colors.GREEN;
import static examples.tema_01.Colors.RESET;

public class Result {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        String string, stringGreen;

        System.out.print("Introduce una cadena: ");
        string = keyboard.nextLine();

        /*
         * En la llamada a la función turnGreen, no estamos utilizando el valor devuelto
         */
        turnGreen(string);

        /*
         * En la siguiente llamada, sí lo vamos a utilizar
         */
        stringGreen = turnGreen(string);
        System.out.printf("La cadena %s convertida a verde: %s", string, stringGreen);
    }

    private static String turnGreen(String string) {
        String result = String.format("%s", GREEN + string + RESET);
        System.out.println(result);
        return result;
    }
}
Ejercicio 6

Se dice que un número entero es primo si sólo es divisible entre 1 y entre sí mismo. Por ejemplo, 2, 3, 5 y 7 son primos, pero 4, 6, 8 y 9 no lo son.

  1. Realiza una función que determine si un número es primo o no.
  2. Realiza una función que muestre todos los números primos comprendidos entre 1 y 10.000
  3. Realiza una función que descomponga un número en factores primos. Ejemplo:
    • 18 = 2 x 3 x 3
    • 11 = 11
    • 35 = 5 x 7
    • 40 = 2 x 2 x 2 x 5
Ejercicio 7

Se dice que un número entero es un número perfecto si la suma de sus divisores propios (incluyendo el 1 y sin incluirse él mismo) da como resultado el mismo número. Por ejemplo, 6 es un número perfecto, porque sus divisores propios son 1, 2 y 3; y 6 = 1 + 2 + 3. Los siguientes números perfectos son 28, 496 y 8128.

  1. Realiza una función que determine si el parámetro es perfecto o no.
  2. Realiza una función que dado un número perfecto, imprima los divisores para confirmar que el número es perfecto. Si no lo es, que no haga nada.
  3. Realiza una función que muestre todos los números perfectos entre 1 y 10.000 con sus correspondientes factores.
Ejercicio 8

La serie Fibonacci se define mediante: a0 = 0 a1 = 1 an = an-1 + an-2, es decir, la serie Fibonacci sería la siguiente 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

  1. Realiza una función que devuelva el elemento enésimo de la serie Fibonacci. Es decir, si recibe:
    • 0: devuelve 0
    • 1: devuelve 1
    • 4: devuelve 3
    • 7: devuelve 13
  2. Realiza una función que muestre los 30 primeros números de la serie de Fibonacci
  3. Realiza una función que calcule el primer elemento de la serie Fibonacci que se mayor o igual que un valor introducido por parámetro. Por ejemplo, si recibe 20, devolverá 21, ya que es el primer elemento de la serie mayor o igual que 20.
Ejercicio 9

Realiza una función que reciba un número entero positivo de n cifras y devuelva el número con sus cifras en orden inverso. No utilizar String ni calcular previamente el número de cifras. Ej: 24.321 debe devolver 12.345

Recursividad

La recursividad es una técnica de escritura de funciones pensada para problemas complejos. La idea parte de que una función pueda invocarse a sí misma.

Esta técnica es peligrosa ya que se pueden generar fácilmente llamadas infinitas (la función se llama a sí misma, tras la llamada se vuelve a llamar a sí misma, y así sucesivamente sin freno ni control). Por lo tanto, es muy importante tener en cuenta cuándo la función debe dejar de llamarse.

Hay que ser muy cauteloso cuando se utiliza la recursividad, pero permite soluciones muy originales y abre la posibilidad de solucionar problemas muy complejos.

Veamos como ejemplo la versión recursiva del factorial:

import java.util.Scanner;

public class RecursiveFactorial {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        int n;

        do{
            System.out.println("Introduce un número entero positivo: ");
            n = keyboard.nextInt();
        } while (n <= 0);

        System.out.printf("El factorial de %d es %d\n", factorial(n));
    }

    private static int factorial(int n) {
        int result;
        if(n == 1){ // Caso base: devuelve 1
            result = 1;
        } else { // Caso recursivo
            result = n * factorial(n-1);
        }

        /*
         * Mensaje intermedio para comprobar
         * como funciona
         */
        System.out.printf("Factorial de %d  Resultado: %d\n", n, result);

        return result;
    }
}

¿Recursividad o iteración? Hay otra versión del factorial resuelto mediante un bucle for (solución iterativa) en lugar de utilizar la recursividad. La cuestión es ¿cuál es mejor? Ambas implican sentencias repetitivas hasta llegar a una determinada condición, por lo que ambas pueden generar programas que no finalizan si la condición nunca se cumple. En el caso de la iteración es una condición la que permite determinar el final, la recursividad lo que hace es ir simplificando el problema hasta generar una llamada a la función que devuelva un valor y no se vuelva a llamar. Para un ordenador es más costosa la recursividad ya que implicar realizar muchas llamadas a funciones, es decir, es más rápida la solución iterativa. Entonces, ¿por qué elegir recursividad? La recursividad se utiliza sólo sí:

  • No encontramos la solución iterativa a un problema.
  • El código es mucho más claro en su versión recursiva.
Ejercicio 10

Realiza el ejercicio 1, haciendo uso de la recursividad

Ejercicio 11

El máximo común divisor de los enteros a y b es el entero más grande que es divisor exacto de a y de b. Escribe una función recursiva llamada gcd que devuelva el máximo común divisor de a y b. El máximo común divisor de a y b se define recursivamente como sigue:

  • si b = 0 → gcd(a, b) = a
  • si b ≠ 0 → gcd(a, b) = gcd(b, a % b)
Ejercicio 12

Realiza el ejercicio 4 haciendo uso de la recursividad

Ejercicio 13

Realiza el ejercicio 8 haciendo uso de la recursividad

La pila

Una pila(stack) es una lista ordenada o estructura de datos que permite almacenar y recuperar datos, el modo de acceso a sus elementos es de tipo LIFO (del inglés Last In, First Out, último en entrar, primero en salir) de supuestos en el área de informática debido a su simplicidad y capacidad de dar respuesta a numerosos procesos.

Para el manejo de los datos cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, desapilar (pop), que retira el último elemento apilado.

En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último objeto apilado (denominado TOS, Top of Stack). La operación desapilar permite la obtención de este elemento, que es retirado de la pila permitiendo el acceso al anterior (apilado con anterioridad), que pasa a ser el último, el nuevo TOS.

Para las llamadas entre funciones, se utiliza una estructura de tipo pila: supongamos que se está procesando una función y en su interior llama a otra función. La función se abandona para procesar la función de la llamada, pero antes se almacena en una pila la dirección que apunta a la función. Ahora supongamos que esa nueva función llama a su vez a otra función. Igualmente, se almacena su dirección, se abandona y se atiende la petición. Así en tantos casos como existan peticiones. La ventaja de la pila es que no requiere definir ninguna estructura de control ni conocer las veces que el programa estará saltando entre funciones para después retomarlas, con la única limitación de la capacidad de almacenamiento de la pila. Conforme se van cerrando las funciones, se van rescatando las funciones precedentes mediante sus direcciones almacenadas en la pila y se va concluyendo su proceso, esto hasta llegar a la primera.

En el caso de una función recursiva, esto es posible implementarlo con sencillez mediante una pila. La función se llama a sí misma tantas veces como sea necesario hasta que el resultado de la función cumpla la condición de retorno; entonces, todas las funciones abiertas van completando su proceso en cascada. No se necesita saber cuantas veces se anidará y, por tanto, tampoco cuando se cumplirá la condición, con la única limitación de la capacidad de la pila. De sobrepasarse ese límite, normalmente porque se entra en un bucle sin final, se produce el error de desbordamiento de la pila (stack overflow).

Ejercicio 15

Realiza una función recursiva que invierta los caracteres de una cadena. Por ejemplo, si la función recibe "Hola a todos", devuelve "sodot a aloH".

Ejercicio 14

Realiza un programa para resolver el juego de las Torres de Hanoi. El juego consiste en tres varillas verticales. En una de las varillas se apila un número indeterminado de discos. Los discos se apilan sobre una varilla en tamaño decreciente. No hay dos discos iguales, y todos ellos están apilados de mayor a menor radio en una de las varillas, quedando las otras dos varillas vacantes. El juego consiste en pasar todos los discos de la varilla ocupada a una de las otras varillas vacantes. Para realizar este objetivo, es necesario seguir estas simples reglas:

  • Solo se puede mover un disco cada vez.
  • Un disco de mayor tamaño no puede descansar sobre uno más pequeño que él mismo.
  • Solo puedes desplazar el disco que se encuentre arriba en cada varilla.

El movimiento de n discos se puede visualizar en términos de mover sólo n-1 discos (y de ahí la recursividad) como sigue:

  1. Pasar n-1 discos de la varilla 1 a la 2, usando la varilla 3 como área de retención temporal.
  2. Pasar el último disco (el más grande) de la varilla 1 a la 3.
  3. Pasar los n-1 discos de la varilla 2 a la 3, empleando la varilla 1 como área de retención temporal.

El proceso termina cuando la última tarea implica pasar n=1 disco, esto es, el caso base. Esto se logra transfiriendo el disco sin necesidad de un área de retención temporal. Escribe una función recursiva con cuatro parámetros:

  1. El número de discos por transferir.
  2. La varilla en la que están colocados inicialmente esos discos.
  3. La varilla a la que debe pasarse esa pila de discos.
  4. La varilla que se usará como área de retención temporal.

El programa deberá imprimir las instrucciones precisas requeridas para pasar los discos de la varilla inicial a la varilla de destino. Por ejemplo, para pasar una pila de tres discos de la varilla 1 a la varilla 3, el programa deberá imprimir la siguiente serie de movimientos:

1  3
1  2
3  2
1  3
2  1
2  3
1  3