Introducción
Con el fin de
proporcionar los conocimientos mínimos necesarios para realizar
implementaciones en una programación SIG vamos a realizar
una introducción orientada a objetos usando el lenguaje Java.
Estructura de un
programa
Un
programa (sea informático o no) está compuesto por cuatro partes bien diferenciadas:
Código. Es el conjunto de
instrucciones en sí. Normalmente, el código está escrito de forma que sea fácil
de entender y manipular por una persona.
Memoria. Ofrece un espacio al
programa para almacenar datos y recuperarlos más tarde.
Entrada. Es el conjunto de
datos que el programa recibe mientras se ejecuta y que condicionan las acciones
que éste realiza y, en consecuencia, los resultados que genera. Normalmente,
los datos proceden del usuario (pulsaciones del teclado, movimientos y
pulsaciones del ratón, etc.), pero también pueden venir de otros programas. En
este último caso tenemos, por ejemplo, los datos enviados por un servidor web a
nuestro navegador.
Salida. Es el conjunto
de datos generados en forma de resultado durante la ejecución del programa.
Estos datos pueden percibirse como acciones desde el punto de vista del
usuario. Los resultados pueden ser, pues, vario puntos: un número, una hoja de
papel impresa, una imagen en la pantalla, etc.
1. Fundamentos de programación
1.1. Declaración y
uso de variables
La memoria de un
ordenador permite almacenar y posteriormente recuperar gran cantidad de datos.
Cuando se programa con un lenguaje de alto nivel como Java, se utilizan nombres
para hacer referencia a los datos que hay en la memoria. En el argot del programador,
a estos datos con nombre se les llama variables.
Por definición, no puede haber dos variables con el mismo nombre.
Para garantizar una
cierta coherencia en el uso de las variables, éstas, además de tener nombre,
deben ser de un tipo. El tipo determina qué
datos puede almacenar una variable (texto, números, etc.) y qué operaciones
podemos realizar con ella. Empezaremos trabajando con los tipos siguientes:
int: valor entero entre –2147483648 y 2147483647.
No admite valores decimales.
Por ejemplo, 2001 ó
175000000.
double: valor real sin límite
de rango y de precisión arbitraria.
Admite valores
decimales. Por ejemplo, –0,075 ó 3,141592654.
String: texto de longitud
arbitraria. Por ejemplo, “La casa de María” u “¡Hola, mundo!”.
boolean: cierto o falso.
Para poder trabajar
con una variable, primero tenemos que declararla. Declarar una variable
consiste en mencionar su nombre y su tipo. En Java se escribe primero su tipo,
después su nombre y se termina la línea con un punto y coma. El punto y coma es
importante, porque indica el final de la declaración. Por ejemplo:
int contador;
declara una variable
llamada “contador” de tipo int. Como ya hemos visto anteriormente, en esta variable podremos
almacenar un valor entre –2147483648 y 2147483647.
Las líneas siguientes:
boolean esClientePreferente;
double capital, interés;
declaran tres
variables: “esClientePreferente”, de tipo boolean, y
“capital” e “interés”, ambas de tipo double. En
la primera podremos almacenar cierto o falso, lo que nos servirá, por
ejemplo, para almacenar si se trata de un cliente preferente o no. En las dos
últimas, almacenaremos el capital y el interés que estudiamos darle a ese mismo
cliente, que puede ser diferente en virtud de que sea cliente preferente o no.
Observad que podemos declarar dos o más variables en una misma línea separando
sus nombres por medio de la coma.
La línea siguiente:
int valores[];
declara un vector de
variables de tipo int llamado “valores”. Un
vector es un conjunto de variables del mismo tipo. Utilizaremos “valores[0]”
para referir- nos a la primera variable, “valores[1]” para referirnos a la
segunda, y así sucesivamente.
En
Java, se puede dar cualquier nombre a una variable. Sin embargo, se deben
respetar unas pocas reglas. Los nombres de las variables:
No
pueden comenzar por un carácter numérico. Por ejemplo, “12” o “1Valor” no son
nombres válidos.
No pueden llamarse
igual que los elementos del lenguaje. Por ejemplo, no podemos tener una
variable que se llame “int” o “double”.
No pueden contener
espacios.
Sin
embargo, a diferencia de otros lenguajes, Java sí que permite que el nombre de
una variable contenga caracteres acentuados.
Otro punto que se
debe tener en cuenta es que Java distingue entre mayúsculas y minúsculas. Esto
significa que las variables:
double capital, Capital;
son diferentes en
todos los sentidos.
1.2. El operador de
asignación
Una
vez declarada una variable, ésta tiene un valor indefinido. Para empezar a
trabajar con ella, se necesita asignarle un valor. La asignación de valores a variables
se lleva a cabo mediante el llamado operador de asignación: “=” (el signo de
igual). Así pues, las líneas siguientes:
int contador; contador = 0;
declaran la variable
“contador” y le asignan el valor 0.
Como podemos
observar, a la izquierda del operador de asignación se emplaza el nombre de la
variable, y a la derecha, el valor que se desea asignarle. La asignación (como
la declaración de variables) termina en punto y coma.
Veamos
más ejemplos de asignaciones:
int suma; suma = 3 + 2;
asigna
a la variable “suma” el resultado de sumar 3 y 2 (cinco).
int contador; contador = 0;
contador = contador + 1;
asigna, en primer
lugar, el valor 0 a la variable “contador”, y luego la incre- menta en una
unidad (1). Es importante destacar que cuando asignamos un valor a una variable
sobrescribimos su valor anterior. Así pues, al finalizar el código, la variable
“contador” valdrá 1.
double interés, capital,
tasaInterés, tiempo;
capital = 60000;
tasaInterés = 0.045;
tiempo = 6;
interés = capital * tasaInterés * tiempo;
asigna a la variable
“interés” los intereses generados por el préstamo de un capital de 60.000 euros
a una tasa del 4,5% (expresado en tanto por uno) a seis años. Según la fórmula
del interés simple, los intereses serán igual al producto de los tres factores.
Como puede apreciarse, el producto se representa median- te el carácter
asterisco (*). Al final de la ejecución, “interés” valdrá 16.200.
String nombre, frase; nombre = "María";
frase = "La casa de " + nombre;
asigna a la variable
“frase” el resultado de concatenar el texto “La casa de ” y la variable
“nombre”. Al finalizar la ejecución, “frase” será igual a “La casa de María”.
Debemos notar que el texto siempre se escribe encerrado entre comi- llas dobles
(“”).
boolean esPositivo;
int número;
número = –5;
es_positivo = número > 0;
almacena en la
variable “esPositivo” el resultado de evaluar si la variable “nú- mero” es
mayor que 0. Al finalizar el código, “esPositivo” valdrá false (falso).
Si
bien la flexibilidad que ofrece el operador de asignación es notable, para que
una asignación sea válida debe haber una cierta compatibilidad entre el tipo de
la variable y el tipo del valor que se le quiere asignar. Por ejemplo, la asignación
siguiente no es válida:
int peso; peso = 58.5;
pues
intenta asignar un valor decimal a una variable entera.
Tampoco
es válida la asignación:
String resultado; resultado = 25;
pues
intenta asignar un valor entero a una variable de tipo texto.
1.3. Expresiones
Como
hemos visto en los ejemplos anteriores, a una variable no sólo se le puede
asignar un valor literal o el valor de otra variable. Además, se le puede
asignar el valor resultante de una operación. Cuando nos encontramos con una
combinación de valores literales, variables y operadores, estamos ante lo que
se llama una expresión.
Las expresiones, que
siempre generan un valor, están sometidas a unas reglas de evaluación que
debemos conocer. Si tenemos, por ejemplo:
int cálculo;
cálculo = 2 + 3 * 5;
La expresión “2 + 3 *
5” se puede interpretar de dos formas: sumar 2 y 3, y el resultado (5)
multiplicarlo por 5 (25), o bien multiplicar 3 por 5, y al resultado (15)
sumarle 2 (17). Como podemos ver, de la forma de interpretar la expresión
depende el resultado.
El lenguaje Java,
como muchos otros lenguajes, define unas prioridades o precedencias entre
operadores que dictan el orden en el que se realizarán las operaciones. La
tabla “Precedencia de los operadores” muestra los operadores más comunes y su
precedencia. Cuanta mayor precedencia, antes se evaluará la operación.
Si aplicamos las reglas de precedencia de la
tabla al ejemplo anterior, tenemos que la multiplicación precederá a la suma.
En consecuencia, el lenguaje resol- verá la expresión multiplicando primero 3
por 5, y sumándole 2 al resultado. Así pues, se asignará el valor 17 a la
variable “cálculo”.
Como hemos visto, la
precedencia entre operadores permite al lenguaje determinar, de forma unívoca,
el valor de una expresión. Sin embargo, este mecanismo no es suficiente en el
caso de tener en una expresión dos o más operadores con la misma precedencia.
Cuando esto sucede, el lenguaje resuelve la expresión evaluando dichos
operadores de derecha a izquierda. Es decir, se operarán en el mismo sentido en
el que se leen. Por ejemplo, el código:
int cálculo;
cálculo = 25 % 7 * 3;
calculará primero el
residuo resultante de dividir 25 entre 7, y después multiplicará dicho residuo
por 3. El resultado será 12.
Supongamos
ahora que deseamos sumar 2 y 3, y multiplicar el resultado por 5. Ya hemos
visto que el código:
int cálculo;
cálculo = 2 + 3 * 5;
no responde a
nuestras necesidades, pues, por precedencia, se evaluará antes la
multiplicación que la suma. Para alterar el orden de evaluación de la expre-
sión, encerraremos la suma entre paréntesis:
int cálculo;
cálculo = (2 + 3) * 5;
Cuando encerramos
parte de una expresión entre paréntesis, esta parte se con- sidera una unidad
indivisible y se evalúa independientemente antes de ser operada. Así, volviendo
al ejemplo, la expresión se resolverá multiplicando el resultado de evaluar la
expresión “2 + 3” por 5. El resultado será 25.
Veamos más ejemplos
de expresiones. Las líneas siguientes:
double precioConDescuento,
precio, descuento; precio = 15.69; // en euros
descuento = 7; // tanto por ciento
precioConDescuento = precio – precio * descuento / 100;
Calculan el precio
rebajado de un producto. En primer lugar, por tener más precedencia, se hace la
multiplicación (precio * descuento), después se divide el resultado por cien, y
finalmente, se resta el resultado obtenido del valor de la variable “precio”.
El resultado será 14,5917.
Las líneas
siguientes:
int número;
boolean estáEntreCeroYDiez; número = –5;
estáEntreCeroYDiez = número > 0 && número < 10;
Determinan
si un número dado está entre 0 y 10. Para que esto se cumpla, el número debe
ser, a la vez, mayor que 0 y menor que 10. En primer lugar, se comprobará si el
número es mayor que 0 (número > 0), y después, si es menor que 10 (número
< 10), para, finalmente, mediante el operador && (y), ver si ambas
condiciones se cumplen. El resultado será igual a false (falso).
static boolean esPrimo(int número) { int divisor;
divisor = 2;
while (número % divisor != 0 && divisor < número) { divisor ++;
}
return divisor == // resultado: true o false
}
primo = esPrimo(19);
1.4. Estructura
básica de un programa
El
lenguaje Java, como la mayoría de lenguajes de programación, requiere una
estructura mínima que acoja el programa y sus componentes. En el caso concreto
de Java, a esta estructura mínima se le llama clase.
Aunque las clases se estudiarán en detalle en el capítulo 2, es necesario
conocer esta estructura para empezar a escribir programas funcionales. A
continuación, se muestra la estructura mínima de una clase ejecutable:
public class Programa {
public static void main(String [] args) {
// aquí va el código
}
}
Sin querer entrar en
demasiados detalles, el código define una clase (class) lla- mada “Programa” con
una función “main” que será el que se ejecutará al ini- ciar el programa. El
nombre de la clase ha sido escogido arbitrariamente (es decir, puede
cambiarse); no así el nombre de la función, que siempre deberá llamarse “main”.
Aunque las funciones se estudiarán en detalle en el apartado 1.7, es necesario
adelantar que son simples agrupaciones (funcionales) de código. Este programa
deberá guardarse en un fichero que se llame como la clase más el sufijo .java; en este caso,
“Programa.java”. El código del programa deberá ir entre las llaves de la
función “main”.
El
programa siguiente:
public class Programa {
public static void main(String [] args) { double distancia, velocidad, tiempo; velocidad =
120; // en km/h
tiempo = 2.5; // en horas
distancia = velocidad * tiempo; // 300 kmSystem.out.println(distancia); // escribe 300
}
}
Calcula la distancia
recorrida por un bólido a partir de su velocidad y el tiempo que ha estado
circulando. El texto precedido por doble barra (//) son comen- tarios. Los
comentarios son simples anotaciones que el programador hace en el código del
programa para facilitar su comprensión. Los comentarios no se ejecutan. La
última línea usa la función “System.out.println” para escribir el resultado en
la pantalla.
Como puede
apreciarse, todas las variables se han declarado al inicio del códi- go. A
diferencia de otros lenguajes, en Java las variables pueden declararse en
cualquier parte del código, siempre y cuando la declaración preceda a su uso.
Sin embargo, es una práctica habitual declararlas al principio del código para
mejorar su legibilidad.
1.5. Estructuras de
control de flujo
Supongamos que se
desea escribir un programa que calcule y muestre por pan- talla el resultado de
la suma de los números pares entre 0 y 10. El programa se podría escribir así:
public class SumaPares {
public static void main(String [] args) { int suma;
suma = 2 + 4 + 6 + 8; System.out.println(suma); // escribe 20
}
}
El programa,
efectivamente, escribirá “20”. Sin embargo, si se deseara conocer la suma de
los números pares entre 0 y 10.000, escribirlos todos sería inviable o, cuando
menos, demasiado costoso.
La solución a este
problema nos la dan las estructuras de control de flujo del programa, que nos
permiten repetir instrucciones o ejecutar unas u otras de- pendiendo de una
condición.
Para
sumar los números pares entre 0 y 10.000, en primer lugar, necesitamos generar
de alguna manera todos esos números pares. Una forma fácil de hacerlo es
partiendo del primer par, en este caso el 2, e irle sumando 2 una y otra vez:
númeroPar = 2;
númeroPar = númeroPar + 2; // númeroPar = 4númeroPar =
númeroPar + 2; // númeroPar = 6númeroPar =
númeroPar + 2; // númeroPar = 8
númeroPar = númeroPar + 2; // númeroPar = 9998
En total, 4.998
líneas de código, mientras “númeroPar” sea más pequeño que 10.000.
Afortunadamente, esto
se puede reducir a tres líneas usando una estructura
while (mientras):
númeroPar = 2;
while (númeroPar <
10000) { // mientras < 10000númeroPar
= númeroPar + 2; // siguiente número par
}
La estructura while repite
una serie de instrucciones mientras se cumpla una condición dada. Dicha
condición se cierra entre paréntesis a la derecha del while, mientras que las
instrucciones que se han de repetir se cierran entre lla- ves ({}). En el
ejemplo, se repetirá la suma mientras “númeroPar” sea más pe- queño que 10.000.
Volviendo a nuestro
empeño por conocer el valor de la suma de todos estos nú- meros, una vez
localizados tan sólo nos queda irlos sumando. Para hacer esto, de- clararemos
la variable “suma”, que irá acumulando el resultado. Sólo debemos tener la
precaución de ponerla a cero antes de empezar. El programa queda así:
public class SumaPares2 {
public static void main(String[] args) { int suma, númeroPar;
suma = 0;
númeroPar = 2; // el primer número
par
while (númeroPar <
10000) { // mientras < 10000 suma += númeroPar;
númeroPar += 2; // siguiente número
par
}
System.out.println(suma);
}
}
Al final del
programa, se mostrará el resultado por pantalla (24.995.000). He- mos de
destacar el uso del operador “+=”, que asigna a una variable el valor de ella
misma más el valor especificado. Es decir:
númeroPar += 2;
equivale
a:
númeroPar = númeroPar + 2;
Supongamos
ahora que queremos crear un programa que sume los múltiplos de un número dado
en un intervalo dado. Por ejemplo, los múltiplos de 122 entre 20.000 y 40.000.
El programa que suma los múltiplos de dos tiene una limitación: necesita
conocer el primer múltiplo del intervalo para poder generar los demás.
Reconocer el primer
número par de un intervalo es muy fácil, pero las cosas se complican bastante
cuando es cuestión de encontrar el primer múltiplo de 122.
El programa
“SumaPares2” construía cada número par sumando dos al inmediatamente anterior.
Una vez generado, lo acumulaba, y así hasta haber agotado todos los números
pares del intervalo. Sin embargo, ésta no era la única solución: se podría
haber optado por recorrer todos los números del intervalo, determinando cuáles
son pares y cuáles no. Si el número es par se acumula; si no, no. Esta
dicotomía (si se cumple condición, hacer A; si no, hacer B) se pue- de modelar
mediante la estructura if/else (si/si no).
Una forma de
determinar si un número dado es par o no, es dividiéndolo por dos y analizando
el residuo. Si el residuo es cero, el número es par. Teniendo en cuenta que en
Java el residuo de una división se obtiene mediante el operador “%” (por cien),
esta condición se podría escribir así:
if (número % 2 == 0) {
// código que hay que ejecutar si es par
}
else {
// código que hay que ejecutar si no es par
}
De modo similar a la
estructura while, la estructura if ejecuta
una serie de instrucciones si se cumple una determinada condición. Dicha
condición se cierra entre paréntesis a la derecha del if, mientras que las
instrucciones que se han de ejecutar se cierran entre llaves ({}).
Opcionalmente, la estructura if acepta
una extensiónelse, que especifica una serie de instrucciones que se han de ejecutar
en caso de que la condición no se cumpla.
Volviendo al
problema, si el número es par, debemos acumularlo, mientras que si no lo es,
simplemente lo ignoramos.Utilizando la estructura if, el código
quedaría así:
public class SumaPares3 {
public static void main(String [] args) { int número, suma;
número = 1; // primer número
del intervalo suma = 0;
while (número <= 9999) { // último número del intervalo if (número % 2 == 0) { // ¿es par?
suma += número; // sí, entonces lo
suma
}
número += 1; // siguiente número
del intervalo
}
System.out.println(suma);
}
Al igual que
“SumaPares2”, “SumaPares3” mostrará 24.995.000 por pantalla. Sin embargo,
modificar esta última versión para que opere en otro rango o para que sume los
múltiplos de un número diferente de 2, es inmediato. Siguiendo con el ejemplo,
si se desea sumar los múltiplos de 122 entre 20.000 y 40.000, basta con cambiar
sólo tres líneas:
public class SumaMultiples {
public static void main(String [] args) { int número, suma;
número = 20001; // primer número
del intervalo suma = 0;
while (número <=
39999) { // último número
if (número % 122 == 0)
{ // ¿es múltiplo de 122? suma += número; // sí, entonces lo suma
}
número += 1; // siguiente número
del intervalo
}
System.out.println(suma);
}
1.6. Esquemas de
recorrido y de búsqueda
Hemos visto que
cuando se trabaja con estructuras del tipo while, se
repiten una serie de instrucciones mientras se cumpla una determinada
condición. Generalmente, se recurre a las repeticiones con uno de estos dos
objetivos:
Recorrer todos los
elementos de un conjunto.
Buscar un elemento de
un conjunto.
Este hecho nos lleva
a hablar de esquemas de recorrido y de búsqueda. En el ejemplo “SumaPares3”, se
analizan los números entre 1 y 9.999. Se comprueba cada número para ver si es
par o no y, si lo es, se acumula. No se sale del bucle hasta que no se han analizado
todos y cada uno de los números del rango. Es- tamos pues ante un esquema de
recorrido.
Los
esquemas de recorrido tienen siempre la misma estructura:
while (haya_elementos)
{ hacer_acción(elemento_actual);
siguiente_elemento();
}
Por otro lado, podemos
tener repeticiones que tengan como objetivo buscar un determinado elemento
dentro de un conjunto. Sin apartarnos de los ejemplos de carácter numérico,
supongamos que se desea encontrar el primer múltiplo de 13 entre 789 y 800.
Está claro que se deben comprobar los números del rango hasta que se encuentre
el primer múltiplo de 13. El código siguiente podría resolver el problema:
int número;
número = 789;
while (número % 13 != 0)
{ número++;
}
Básicamente, empezando
por 789, se va incrementando la variable “número” (“número++” equivale a
“número = número + 1”) mientras “número” no sea múltiplo de 13. Sin embargo,
esta aproximación comete un error: supone que encontrará algún múltiplo dentro
del rango. Si este múltiplo no existiera, el buclewhile continuaría analizando números más allá del rango del
problema. Por esto, además de comprobar que no se ha encontrado el número que
se buscaba, también se debe comprobar que no se esté saliendo del rango de
análisis. El código siguiente tiene en cuenta ambas cosas:
int número;
número = 789;
while (número <= 800
&& número % 13 != 0) { número++;
}
Como se puede
observar, en primer lugar, se comprueba que “número” no sea mayor que 800 (es
decir, que sea más pequeño o igual a 800) y, en segundo lugar, que no sea
múltiplo de 13. Al haber dos condiciones que gobiernan el bucle (nú- mero <=
800 y número % 13 != 0), se puede salir de él por dos motivos:
1.-
Por haber agotado todos los números del rango (“número” ha alcanzado el valor
801) o
2.- Por haber
encontrado un múltiplo de 13.
Por esta razón,
debemos comprobar, al finalizar el bucle, si se ha salido de él por un motivo u
otro, pues de ello va a depender el comportamiento del resto del programa:
int número;
número = 789;
while (número <= 800
&& número % 13 != 0) { número++;
}
if (número % 13 != 0) { // se ha encontrado un múltiplo
System.out.println(número);
}
else { // no se ha
encontrado un múltiplo
System.out.println("No se ha
encontrado.");
}
Los esquemas de
búsqueda presentan la estructura siguiente:
while (haya_elementos && !
elemento_encontrado) { siguiente_elemento();
}
Es importante observar
que la condición “elemento_encontrado” siempre irá negada. Esto siempre será
así porque las instrucciones del bucle deben repetir- se mientras no se haya
encontrado el elemento que se busca.
static boolean esPrimo(int número) { int divisor;
divisor = 2;
while (número % divisor != 0 && divisor < número) { divisor ++;
}
return divisor == // resultado: true o false
}
primo = esPrimo(19);
1.7. Definición y uso
de funciones
En nuestro afán por
encontrar números de todo tipo, supongamos ahora que deseamos encontrar el
primer número primo de un intervalo dado. Hay que re- cordar que un número
primo es aquel que sólo se divide de forma entera por él mismo y por 1. El 13 o
el 19, por ejemplo, son números primos.
El
siguiente esquema de búsqueda podría resolver el problema:
encontrado
= es_primo el primer_número_del_intervalo; while(!encontrado && hay_mas_numeros_en_el_intervalo)
{encontrado = es_primo el siguiente_número;
}
if encontrado {
// número encontrado
}
else {
// número no encontrado
}
Probar que un número
es primo no es una tarea fácil. De hecho, dado un nú- mero, llamémosle N, la única forma de comprobar
que es primo es dividién- dolo entre todos los números primos entre 2 y N –
1, ambos incluidos*. Para que sea primo, los residuos de las divisiones deben
ser diferentes de 0. Si hay alguna división cuyo residuo sea 0, el número no es
primo.
Para
simplificar el problema, probaremos la primalidad de un número divi- diéndolo
entre todos los números menores que él, sean primos o no.
Vamos
a generar, en primer lugar, un código que compruebe si un número es pri- mo.
Concretamente, intentaremos demostrar que no lo es buscando un divisor entero
del mismo. Si fallamos en nuestro intento, el número será primo. Como ya hemos
visto, se trata de ir dividiendo el número en cuestión entre todos los nú-
meros menores que él hasta obtener una división cuyo residuo sea 0. Es decir,
de- bemos implementar un esquema de búsqueda del menor divisor entero:
int número, divisor;boolean esPrimo;
número = 19; // número que
queremos comprobar
divisor = 2;
while (número % divisor
!= 0 && divisor < número) {
divisor ++;
}
esPrimo = divisor == número; // resultado: true o
false
Como en todos los
recorridos de búsqueda, hay dos condiciones que gobier- nan elwhile: la que comprueba que
“divisor” no sea divisor entero de “núme- ro” (número % divisor != 0) y la que
comprueba que no se haya salido de rango (divisor < número).
Consecuentemente, al terminar la ejecución del bucle, “número” será primo si la
condición que ha “fallado” ha sido la segunda. En este caso, el número será
primo si “divisor” es igual a “número”.
Ahora
que ya disponemos de un código capaz de determinar si un número dado es primo,
vamos a construir el programa que busca el primer número primo de un intervalo.
Basándonos en el pseudocódigo propuesto anterior- mente, el código siguiente
busca el primer número primo entre 10.000 y 10.013:
public class BuscarPrimo {
public static void main(String[] args) {int inicio, fin; // inicio y fin del rango int número,
divisor;
boolean encontrado;
// inicio y fin del intervalo de búsqueda inicio = 10000;
fin = 10013;
System.out.println("Buscando el primer número
" +
" primo entre " + inicio + " y "
+ fin + "...");
//comprueba si el primer número del intervalo es
//primo
número = inicio; // número que hay
que comprobar divisor = 2;
while (número % divisor
!= 0 && divisor < número) { divisor ++;
}
encontrado = (divisor == número); // resultado
//mientras no se haya encontrado un número primo y
//haya más números en el intervalo
while (! encontrado
&& número < fin) {
// comprueba el siguiente número
número ++; divisor = 2;
while (número % divisor
!= 0 && divisor < número) { divisor ++;
}
encontrado = (divisor == número); // resultado
}
if (encontrado) {
//si ha encontrado un número primo lo muestra por
//pantalla
System.out.println(número);
}
else {
System.out.println("No hay ningún número primo
" + "en el intervalo");
}
}
}
Básicamente, el
código es un esquema de búsqueda del primer número primo, combinado con el
código que comprueba si un número es primo visto ante- riormente. Como se puede
apreciar, éste último, que hemos enmarcado para distinguirlo con facilidad, se
repite dos veces.
Repetir
bloques de código dos o más veces dentro de un programa tiene serios
inconvenientes:
Reduce
la legibilidad. Como se puede
apreciar en el programa, cuesta discernir entre el código que pertenece a la
búsqueda del número primo y el que comprueba si un número lo es. Las variables
se mezclan y las instrucciones también.
Dificulta la
mantenibilidad. Si se detecta un
error en la función que comprueba si un número es primo o se desea mejorarlo,
las modificaciones de- ben propagarse por todos y cada uno de los bloques
repetidos. Cuantas más repeticiones, más difícil será introducir cambios.
Es propenso a
errores. Ello es consecuencia
de la baja legibilidad y mantenibilidad. Una corrección o mejora que no se ha
propagado correctamente por todos los fragmentos repetidos puede generar nuevos
errores. La corrección de éstos, a su vez, puede generar otros nuevos, y así
indefinidamente.
Al final, la repetición
de bloques de código conduce a códigos inestables con un coste de mantenimiento
más elevado en cada revisión. Afortunada- mente, todos estos inconvenientes se
pueden salvar mediante el uso de funciones.
Si nos fijamos en el
pseudocódigo que hemos concebido inicialmente, vemos que, de forma natural,
hemos detectado la funcionalidad "comprobar si un número es primo",
que hemos expresado mediande la fórmula "es_primo". Más adelante,
cuando hemos implementado dicha funcionalidad, ha aparecido una variable que
podríamos considerar la entrada del subprograma: "número", y otra que
se podría considerar su resultado: "esPrimo". En la im- plementación
final, el programa principal ha hecho uso de ambas variables para comunicarse
con el subprograma, pasando el número a comprobar a través de la variable
"número" y obteniendo el resultado mediante la variable
"encontrado" (que es el nuevo nombre que, por razones de legibilidad,
he- mos dado a la variable "esPrimo").
De
todo esto, se derivan dos conclusiones importantes:
La
lógica del subprograma es completamente independiente de la del pro- grama
principal. Es decir, al programa principal poco le importa cómo se comprueba si
un número es primo, ni que variables se utilizan para hacer dicha comprobación.
Programa
y subpgorama se comunican mediante una interfaz basada en variables bien
definida.
Llegados
a este punto, podemos declarar lo que se llama una función. Una función no es más
que una agrupación de código que, a partir de uno o más valores de entrada, o,
valga la redundancia, en función de ellos, genera un resultado.
Veamos
pues la función “esPrimo”:
static boolean esPrimo(int número) { int divisor;
divisor = 2;
while (número % divisor
!= 0 && divisor < número) { divisor ++;
}
return divisor == número; // resultado: true o false
}
Al margen del uso del
modificador static, cuyo significado se estudiará en el capítulo 2, una función
se declara escribiendo el tipo de resultado que de- vuelve (en este caso boolean), su
nombre (“esPrimo”) y, encerrados entre paréntesis, sus parámetros de entrada
separados por comas (“número” de tipo int). Como se aprecia en el código, los parámetros de entrada se
definen de la misma forma que las variables: en primer lugar se escribe su tipo
y, luego,
su nombre. El código
de la función que sigue a este bloque declarativo va en- cerrado entre llaves.
Podemos
observar que al final de la función aparece una sentencia nueva: return(devolver). Cuando se
alcanza un return, termina la ejecución de
la función, y se establece como resultado de la misma el valor de la expresión
que sigue al return. La sentencia return siempre termina en punto
y coma.
Una
vez definida la función, podemos llamarla desde cualquier parte del código. Por
ejemplo, la línea siguiente:
primo = esPrimo(19);
determina si el número 19
es primo. Dicho de otra manera, asigna a la va- riable “primo” (de tipo boolean) el
resultado de la función “esPrimo” para el valor 19. La llamada a una función
también termina en punto y coma. Es importante observar que, al ejecutarse la
función, el parámetro “núme- ro” valdrá 19. De la misma forma, al terminarla,
se asignará a la variable “primo” el valor de la expresión que hay a la derecha
del return:
También es importante
destacar que cuando se llama a una función pasa a eje- cutarse el código de
ésta, y no se vuelve al código que la ha llamado hasta que no se alcanza un return.
static boolean esPrimo(int número) { int divisor;
divisor = 2;
while (número %
divisor != 0 && divisor < número) { divisor ++;
}
return divisor == //
resultado: true o false
}
primo = esPrimo(19);
Si rescribimos el
programa “BuscarPrimo” usando funciones, nos queda así:
public class BuscarPrimo2 {
static boolean esPrimo(int número) {
int divisor;
divisor = 2;
while (número % divisor
!= 0 && divisor <
número) { divisor ++;
}
return divisor == número; // resultado:
true o false
}
public
static void main(String[]
args) {int inicio, fin; // inicio y fin del rango int número;
boolean encontrado;
//
inicio y fin del intervalo de búsqueda inicio = 10000;
fin =
10013;
System.out.println("Buscando
el primer número " +
"
primo entre " + inicio + " y " + fin + "...");
//comprueba
si el primer número del intervalo es
//primo
número
= inicio; // número que hay que comprobar encontrado = esPrimo(número); //
resultado
//mientras
no se haya encontrado un número primo y
//haya
más números en el intervalo
while (! es_primo && número <= fin) {
//
comprueba el siguiente número número ++;
encontrado
= esPrimo(número); // resultado
}
if (encontrado) {
//si
ha encontrado un número primo lo muestra por
//pantalla
System.out.println(número");
}
else {
System.out.println("No
hay ningún número primo " + "en el intervalo");
}
}
}
Incluso
podemos simplificar el recorrido de búsqueda aún más:
número
= inicio;
while (número <= fin && !
esPrimo(número)) { número ++;
}
Cuando
se trabaja con funciones, deben tenerse en cuenta los aspectos si- guientes:
Los
parámetros de una función actúan a todos los efectos como variables. Podemos
leerlos y operar con ellos como si fueran variables.
Los
parámetros de una función son una copia de los parámetros de entra- da. Si
pasamos una variable como parámetro a una función, y dentro del código de la
función se modifica el parámetro, el valor de la variable no se verá alterado.
Al
espacio entre llaves que encierra el código de la función se le llama “ám- bito
de la función”. Las variables declaradas dentro de la función sólo pue- den
verse dentro del ámbito de la función. Es decir, no se puede acceder a ellas
desde fuera de la función. Lo mismo es aplicable a los parámetros.
Una
función se caracteriza a partir de su nombre y el tipo de los parámetros de
entrada. Si cambia alguno de ellos, estamos ante funciones diferentes. Esto
significa que puede haber funciones con el mismo nombre pero con parámetros
distintos. A esta característica, que no todos los lenguajes so- portan, se le
llama “sobrecarga de funciones”. A partir del número y el tipo de los
parámetros de entrada en una llamada, se averiguará la función a la que se
llama.
Una
función puede que no devuelva ningún resultado. En este caso, el tipo de la
función será void (vacío), y estaremos ante lo que se llama
un procedimiento. Un ejemplo de procedimiento és la función main, que es la primera función que se ejecuta al iniciar
un programa.
Bibliografía
Oracle, "The Java(tm) Tutorials", http://download.oracle.com/javase/tutorial/.
Oracle, "Java(tm) 2 Platform Standard Edition 5.0 API Specification", http:// download.oracle.com/javase/1.5.0/docs/api/.
Personalización
SIG, Albert Gavarró Rodríguez , Universidad Oberta de Catalunya, España 2011