Tabla de Contenidos

06 - 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.

Las funciones tienen dos partes: la cabecera (definición de la función) y el cuerpo (implementación de la función).

Cabecera

La cabecera de una función consiste en un nombre definido por el programador y unos parámetros de entrada. Además, ciertos lenguajes (como Java) permiten definir el tipo de parámetro que devolverá esa función. Por ejemplo, la siguiente función recibe dos variables de tipo int como parámetros de entrada y devuelve un entero:

    int add(int number1, int number2)

Además del nombre, los parámetros de entrada y el de salida, en Java también podemos poner la visibilidad de la función y palabras claves (como static) que modifican el comportamiento de la función. Fíjate por ejemplo en la cabecera del método (en la programación orientada a objetos las funciones se llaman métodos, como veremos más adelante) main:

public static void main(String[] args)

Esto es porque Java es un lenguaje totalmente orientado a objectos. Cuando lleguemos a la POO, ya entenderás todos estos conceptos.

De hecho, nosotros, por ahora, vamos a definir todas nuestras funciones con la palabra reservada static para poder utilizarlas desde nuestro método main, ya que de lo contrario nos saltará un error (de nuevo, esto tiene que ver con la POO). Aunque esto no es aconsejable, cuando lleguemos al tema de POO ya entenderéis el porqué.

Las funciones pueden devolver cualquier tipo de parámetro (no solo los tipos primitivos), por ejemplo un String:

static String getHellWorld()

Existe un tipo de funciones donde no se devuelve ningún parámetro (a menudo llamadas procedimientos). Para estos casos, en Java se utiliza la palabra void para indicar que esa función no devuelve nada (ten en cuenta que, al contrario que otros lenguajes, en Java es obligatorio poner el tipo de parámetro que devuelve una función):

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.

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. A partir de definir la cabecera y el cuerpo de una función, ya podremos reusar dicho código llamando a la función en diferentes partes del programa.

    static int add(int number1, int number2) {
        return number1 + number2;
    }
    
    static String getHellWorld() {
        return "Hola mundo";
    }    

En nuestro ejemplo anterior, la primera función devuelve la suma de dos números enteros, mientras que la segunda devuelve la frase “Hola mundo”.

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. Es decir, si intento poner dos return seguidos, el compilador de Java me mostrará un error avisándome con el mensaje “Unreachable code”, ya que el segundo return nunca se podrá alcanzar:

        return number1 + number2;
        return number1 - number2;

Eso no quiere decir que no pueda haber dos return definidos en el cuerpo de una función. Por ejemplo, mira el siguiente código:

    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;
    }

Ámbito de las variables

Copia el siguiente ejemplo e intenta compilarlo:

class AmbitoVariables {

    static int add() {
        return resultado + 5;
    }
    
    public static void main(String[] args) {
        int resultado = 10;
        add();
    }
}

AmbitoVariables.java:4: error: cannot find symbol
        return resultado + 5;
               ^
  symbol:   variable resultado
  location: class AmbitoVariables
1 error

¿Qué está pasando? Definimos la variable resultado en el método main con un valor de 10. A continuación, llamamos al método add que le suma 5 al valor de resultado y lo devuelve. El código es muy sencillo, ¿por qué da un error? La respuesta está en donde definimos la variable y donde la utilizamos.

Cuando definimos una variable, ésta solo existe en el bloque de código donde la hemos definido (en nuestro ejemplo, en el método main). Si intentamos acceder a ella fuera de ese método, Java nos dará un error indicándonos que la variable no existe.

En realidad, esto no tiene que ver solo con funciones. Mira el siguiente trozo de código:

        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }

        i = 15;

AmbitoVariables.java:15: error: cannot find symbol
        i = 15;
        ^
  symbol:   variable i
  location: class AmbitoVariables
1 error

Aquí está ocurriendo lo mismo. La variable i la hemos definido en el bloque de código for, con lo que solo existe en ese bloque. Si la intentamos utilizar fuera, nos da un error.

¿Como podemos definir variables que sean accesibles en todo nuestro código? En la mayoría de lenguajes de programación, podemos definir variables globales, las cuales son accesibles desde cualquier punto de nuestro programa. En Java, al ser totalmente orientado a objetos, lo que definimos son propiedades de la clase, que es un concepto muy parecido. En este caso, si definimos este tipo de variables, podremos acceder a ellas desde cualquier punto de nuestra clase (como por ahora solo estamos utilizando una clase, podemos tomas las propiedades de la clase como variables globales).

Para definir una variable global, las declaramos al principio de nuestro código, justo después de la definición de la clase (de nuevo, tenemos que usar, por ahora, la palabra reservada static):

class AmbitoVariables {

    static int resultado = 10;

    static int add() {
        return resultado + 5;
    }
    
    public static void main(String[] args) {
        add();
    }
}

Ahora, al compilar el código ya no nos saltará ningún error.

Llamada a una función

Para llamar a una función que hemos definido previamente, lo único que tenemos que hacer es invocarla mediante su nombre y pasarle los parámetros adecuados separados por comas (,):

class LlamadaFuncion {

    static int add(int number1, int number2) {
        return number1 + number2;
    }

    public static void main(String[] args) {
        int a = 2;
        int b = 3;
        int result;

        result = add(a, b);
        
    }

}

Paso de parámetros

Cuando llamamos a una función y le pasamos parámetros, en realidad le estamos pasando una copia de esas variables, no la variable en sí. Por ejemplo, si ejecutamos el siguiente código:

class LlamadaFuncion {

    static void changeNumber(int number1) {
        number1 = 10;
    }

    public static void main(String[] args) {
        int a = 2;

        System.out.println(a);
        changeNumber(a);
        System.out.println(a);
        
    }

}

2
2

Vemos que no se modifica el valor de la variable a.

En los lenguajes de programación existen dos tipos de paso de parámetros a una función:

En Java, todos los tipos primitivos se pasan por valor. No existe el paso por referencia (al contrario que otros lenguajes de programación).

Otra cuestión es el orden en el que pasamos los parámetros. Como antes, cada lenguaje de programación tiene sus reglas, pero, en general, existen dos aproximaciones:

Para entender esto, primero vamos a ver una distinción entre parámetros formales y parámetros reales o actuales. Los parámetros formales son las variables que recibe la función, se crean al definir la función. Su contenido lo recibe al realizar la llamada a la función de los parámetro reales. Los parámetros formales son variables locales dentro de la función.

Por el contrario, los parámetro reales son la expresiones que se utilizan en la llamada de la función. Sus valores se copiarán en los parámetros formales.

Por ejemplo, en el siguiente código:

    static int add(int number1, int number2) {
        return number1 + number2;
    }
    
    public static void main(String[] args) {
        int a = 2;
        int b = 3;
        int result;

        result = add(a, b);        
    }

Las variables a y b del método main son los parámetros reales, mientras que las variables number1 y number2 del método add son parámetros formales (son una copia de a y b que sólo existen en el ámbito de la función add).

Como hemos dicho antes, el paso de parámetros reales a formales se puede hacer por posición (el primer parámetro real se convertirá en el primer parámetro formal, el segundo real en el segundo formal…) o por nombre, especificando qué parámetro real corresponde a qué parámetro formal.

En Java, los parámetros siempre se pasan por posición.

Parámetros de longitud variable

En ocasiones, es útil no definir exactamente el número de parámetros de entrada que tiene una función. Por ejemplo, si queremos hacer un método que devuelva la suma de varios números, no sabemos, a priori, cuantos números le vamos a pasar a esa función. Podemos hacer varios métodos que reciban cada uno un número diferente de parámetros, pero ésto no sería demasiado eficiente.

Una solución, es poder definir en la cabecera de los métodos parámetros de longitud variable. Para hacerlo, tenemos que agregar tres puntos (…) después de declarar el tipo de parámetro:

static void myFunction(int... numbers)

De esta forma, indicamos que el método myFunction puede recibir 0 o más parámetros de tipo int (en realidad, lo que le pasa es un array de enteros). Para leer los parámetros, podemos utilizar un bucle for:

class ParametrosLongitudVariable {
    
    static void myFunction(int... numbers) {
        for (int i = 0; i < numbers.length; i++) {
            System.out.println(numbers[i]);
        }
    }

    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 3;
        int d = 4;

        myFunction(a, b);
        myFunction(a, b, c);
        myFunction(a, b, c, d);
    }
}

En el siguiente tema veremos arrays y listas, con lo que ya aprenderás como bien como recorrer y leer elementos de un array

Sobrecarga de métodos

En Java no podemos definir dos funciones con el mismo nombre y el mismo número y tipo de parámetros, pero podemos definir varios métodos con el mismo nombre con diferentes número y/o tipos de parámetros. Esto se conoce como sobrecarga de métodos (o funciones).

Si utilizamos la sobrecarga de métodos, Java se encargará de llamar a uno u otro en función del tipo y número de parámetros que le pasemos:

class Sobrecarga {

    static void myFunction(int a) {
        System.out.println("Llamada al método con un parámetro de tipo entero");
    }

    static void myFunction(float a) {
        System.out.println("Llamada al método con un parámetro de tipo float");
    }

    static void myFunction(int a, int b) {
        System.out.println("Llamada al método con dos parámetros de tipo entero");
    }

    static int myFunction() {
        System.out.println("Llamada al método sin parámetros. devolvemos el número 5");
        return 5;
    }


    public static void main(String[] args) {
        int a=1, b=2;
        float c=3.0f;

        myFunction(a);
        myFunction(c);
        myFunction(a, b);
        myFunction();
    }
}

Llamada al método con un parámetro de tipo entero
Llamada al método con un parámetro de tipo float
Llamada al método con dos parámetros de tipo entero
Llamada al método sin parámetros. devolvemos el número 5

La sobrecarga de métodos es una herramienta que nos puede facilitar la implementación de los métodos. Si no tuviésemos esa opción, deberíamos definir métodos diferentes o un método con parámetros de longitud variable y comprobar el número (y tipo) de parámetros pasados para hacer una u otra cosa.

Recursividad

En Java (igual que en otros lenguajes) un método se puede llamar a sí mismo. Este proceso se llama recursividad, y los métodos o funciones se llaman funciones recursivas.

Por ejemplo, para calcular el número factorial de un entero, podemos construir un método iterativo parecido a éste:

class Recursividad {

    static int factorialIterativo(int number) {
        int factorial = 1;
        for (int i = 2; i <= number; i++) {
            factorial *= i;
        }
        return factorial;
    }
    
    public static void main(String[] args) {
        int a = 5;

        System.out.println(factorialIterativo(a));
    }
}

120

Podemos hacer otro método que calcule los mismo (el factorial de un número entero) con un enfoque recursivo:

    static int factorialRecursivo(int number) {
        if (number == 1) {
            return 1;
        } else {
            return (number * factorialRecursivo(number-1));
        }
    }

120

Cuando se llama a factorialRecursivo() con un parámetro igual a 1, el método devuelve 1; de lo contrario, devuelve el producto de number * factorialRecursivo(number - 1).

Para evaluar esta expresión, se llama a factorialRecursivo() con n-1. Este proceso se repite hasta que n es igual a 1 y las llamadas al método comienzan a devolver un resultado. Por ejemplo, si le pasamos el número 3 a la función recursiva, el cálculo que realizará es el siguiente:

3 * factorialRecursivo(2) = 3 * (2 * facturialRecursivo(1)) = 3 * (2 * 1) = 6. 

En los métodos recursivos hay que estar atentos a la devolución simple del método, es decir, la condición para que el método devuelva un resultado y no se vuelva a llamar a sí mismo. En nuestro ejemplo, cuando number vale 1 la función devuelve 1 y empieza a calcular resultados con las devoluciones sucesivas de las llamadas recursivas al método. Si nos equivocamos y esta devolución simple nunca se alcanza (por ejemplo, si hubiésemos puesto if (number>100), entraríamos en un bucle de llamadas recursivas infinito.

Ejercicios

Ejercicio 1.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 1.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 2.a

Crea el menú del ejercicio 9.a del tema anterior mediante una función.

Ejercicio 2.b

Crea el menú del ejercicio 9.b del tema anterior mediante una función.

Ejercicio 2.c

Haz que la opción del ejercicio anterior sea una variable global.

Ejercicio 3.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 3.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 3.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 4.a

Crea una función que reciba un número variable de enteros y devuelva la suma de todos ellos.

Ejercicio 4.b

Modifica la función anterior para que sume sólo los números impares.

Ejercicio 5.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 5.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 6.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 6.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.