Presentación del laboratorio de PRAP

Anuncio
Presentación del laboratorio de PRAP
El laboratorio de la asignatura Pràctiques de Programació (abreviadamente PRAP) tiene como
objetivo ejercitar los conocimientos sobre diseño de programas impartidos en dicha asignatura
y la precedente, Programació 1, y adquirir experiencia en la codificación y puesta a punto de
programas de tamaño y dificultad medios.
Un cuatrimestre de este laboratorio se divide en dos partes. La primera consta de una serie
de sesiones en las que se presentan las herramientas de programación necesarias para realizar
la práctica de la asignatura y se resuelven pequeños ejercicios. La segunda parte consiste en el
desarrollo de la práctica propiamente dicha.
Las mencionadas sesiones se describen en este documento, que es al mismo tiempo una guía
de referencia y un manual de ejercicios. Salvo en la sesión 1, que se basa mayoritariamente en
contenidos aparecidos en Programación 1, los elementos que presentaremos estarán muy relacionados con los conceptos que paralelamente se muestran en las clases de teoría. Terminadas las
sesiones, el alumno estará en condiciones de abordar la práctica, de forma individual. El tiempo
de clase se dedicará a la supervisión del trabajo del alumno por parte del profesor.
El entorno de trabajo sobre el que realizaremos las sesiones consta del sistema operativo
Linux y el lenguaje de programación Java. Supondremos que el alumno ya está familiarizado
con los conceptos básicos del lenguaje Java (instrucciones de control, tipos simples, arrays, etc)
y posee un mínimo dominio del sistema operativo Linux (editores, gestión de ficheros, etc).
De todas formas, existe un resumen de los comandos más importantes de Linux en la página
del LCFIB
http://www.fib.upc.es/ca/LCFIB/index.html
a partir del enlace Entorns de treball hasta Linux.
En cuanto al lenguaje Java, hay información muy completa en la misma página siguendo el
enlace FAQS.
Por último, toda la información sobre la asignatura, se encuentra en la página
http://www.lsi.upc.es/˜prap/prap.html
Dicha página incluye: un enlace a la guía docente, los apuntes de teoría, este manual, el
enunciado de la práctica, etc.
Sesión 1
Entorno de trabajo y repaso de Java
En esta primera sesión realizaremos algunos ejercicios para repasar la codificación de algoritmos
en Java. También presentaremos el paquete utilsPRAP, con métodos para facilitar la gestión de
la entrada y salida de nuestros programas. Para empezar, copiad todo el subdirectorio sesion1:
cp -r /assig/prap/sesion1 sesion1
1.1 Compilación de programas en Java
Para compilar un programa como PRAP1 basta con aplicar el comando
javac PRAP1.java
Si el programa no contiene errores, se generará el correspondiente PRAP1.class. Si dentro del
programa se definen otras clases, se generarán también los ficheros *.class de todas ellas.
1.2 Ejecución de programas en Java
Los programas que codificaremos en la asignatura no contendrán ningún elemento gráfico (ventanas, formularios, etc) para gestionar el flujo de información. Sólo trabajaremos con el canal
estándar de entrada/salida, en ocasiones redireccionado a ficheros de texto.
Por ello, no utilizaremos paquetes como java.awt, java.applet, etc. La entrada y salida
de los programas se gestionará con los métodos de System.in y System.out, o bien con los de
la clase InOut que presentaremos más adelante.
Las ejecuciones se realizarán desde la línea de comandos. Por ejemplo, si queremos ejecutar un programa, contenido en el fichero PRAP1.class, recibiendo sus datos por teclado y
escribiendo sus resultados en pantalla, escribimos
java PRAP1
2
1.3. ESTRUCTURA DE UN PROGRAMA EN JAVA
3
y a continuación escribimos los datos del programa respetando el formato esperado por éste. Tras
la ejecución, veremos escrita la salida del mismo.
Si deseamos ejecutar el mismo programa sobre los datos almacenados en el fichero PRAP1.dat,
pero queremos ver los resultados por pantalla redireccionaremos el canal estándar de entrada hacia ese fichero:
java PRAP1 < PRAP1.dat
Si además queremos que los resultados no se escriban en pantalla sino en otro fichero PRAP1.sal
escribiremos
java PRAP1 < PRAP1.dat > PRAP1.sal
Éstos son los mecanismos de redirección de los canales estándar de entrada y salida en Linux.
Puede usarse cualquier combinación de ellos cuando sea preciso.
1.3 Estructura de un programa en Java
La unidad básica de construcción de programas en Java es la clase. En la asignatura usaremos
clases para traducir módulos de datos y, por supuesto, programas principales. Cada programa
Java necesita una clase dotada de un método main que es la que se ejecuta en primera instancia
(corresponde al módulo principal). Ésta puede utilizar otras y así sucesivamente.
En un programa codificado en Java pueden aparecer una o varias clases, que podemos clasificar en los siguientes tipos:
las clases estándar del lenguaje
las clases definidas por el propio programador
las clases definidas por otros programadores
1.4 Visibilidad de clases
Para poder utilizar una clase, ésta ha de ser visible por el programa que la necesita. Esencialmente, una clase es visible desde el directorio de trabajo si ha sido declarada pública y si su
correspondiente fichero .class cumple alguna de estas propiedades:
está en el propio directorio de trabajo (en realidad, toda clase del directorio de trabajo es
pública, salvo si se ha declarado explícitamente como privada)
es del lenguaje, como Object o Exception
está en cualquiera de los directorios que aparecen en la definición de la variable de entorno
CLASSPATH
SESIÓN 1. ENTORNO DE TRABAJO Y REPASO DE JAVA
4
La variable CLASSPATH sirve para indicar al compilador de Java a dónde tiene que ir a buscar
las clases que utiliza el programa compilado. Está formada por una lista de directorios. Podéis
comprobar su valor en todo momento escribiendo
echo $CLASSPATH
El valor que veréis la primera vez que ejecutéis el comando será el predefinido por el sistema,
aunque cada usuario podrá modificarlo según su conveniencia. Usaremos esta posibilidad para
añadir la ruta del directorio de la asignatura /assig/prap/. Cread un fichero llamado .tcshrc
en vuestro directorio principal y escribid en él la siguiente línea1 (acabada en intro). Si el
fichero no existe, creadlo con el editor.
setenv CLASSPATH /assig/prap:.
Despues de salvar el fichero, ejecutad el comando
source .tcshrc
y comprobaréis si se ha añadido la mencionada ruta aplicando de nuevo el comando echo
$CLASSPATH.
1.5 El paquete utilsPRAP
Un paquete (o package, en Java) es un grupo de clases públicas que forman una unidad lógica.
Los paquetes se definen para facilitar el uso de sus clases a diferentes programas; ello se realiza
mediante la declaración import al pricipio de éstos. Las clases de un paquete han de estar
instaladas físicamente en un subdirectorio del directorio de trabajo o de uno que aparezca en la
definición de CLASSPATH. En cualquier caso, el paquete y su directorio asociado han de tener el
mismo nombre.
Con la definición anterior de CLASSPATH, las clases visibles que tendremos a nuestra disposición serán:
las del sistema,
las que están en el directorio de trabajo,
las del directorio /assig/prap,
las de los subdirectorios de ambos que hayan sido definidos como paquetes.
En esta asignatura se utilizarán dos paquetes: utilsPRAP, que pondrá a nuestra disposición
diversos métodos de lectura y escritura, y tadsPRAP, que contiene tipos de datos predefinidos.
Ambos paquetes están definidos como subdirectorios de /assig/prap.
1 Este
comando redefine la variable CLASSPATH, destruyendo su valor anterior. Si se desea conservar éste, ha
de escribirse setenv CLASSPATH "$CLASSPATH":/assig/prap:.
1.6. ACCIONES Y FUNCIONES; PASO DE PARÁMETROS
5
1.5.1 La clase InOut
Para simplificar la gestión de la entrada y salida de datos en nuestros programas hemos definido
la clase InOut dentro del paquete utilsPRAP. En ella se pueden encontrar métodos como
write
read
readint
Escribe cualquier clase de dato: números, caracteres, ...
Lee caracteres
Lee números enteros
Al tratarse de un paquete, para usar esas clases hay que comenzar nuestros programas con la
declaración
import utilsPRAP.*;
También se puede importar la clase directamente:
import utilsPRAP.InOut;
Los métodos de la clase InOut generan excepciones de la clase Exception. Por otra parte,
como InOut es una clase de datos, al usar dichos métodos hay que aplicarlos sobre un objeto de
la clase InOut previamente inicializado. Por lo tanto, habrá que comenzar los programas de la
siguiente forma:
import utilsPRAP.InOut;
class suma {
public static void main (String args []) throws Exception {
InOut io = new InOut();
...
y a partir de entonces ya podemos invocar dichos métodos:
c=io.read();
n=io.readint();
io.write(n+1);
El programa suma.java, que suma dos números enteros, emplea algunas instrucciones del
módulo InOut. Comprobad la necesidad de la importación para que el programa compile correctamente. Como ejercicio, modificad el programa para que sume una secuencia de enteros
terminada en 0. Podéis leer y sumar los números en el mismo método main. Usad el fichero
sumasec.dat como ejemplo de entrada.
1.6 Acciones y funciones; paso de parámetros
Una característica de la asignatura es que todas las variables que intervienen en las acciones
y funciones que diseñamos han de aparecer en la cabecera de éstas o bien ser locales (no se
SESIÓN 1. ENTORNO DE TRABAJO Y REPASO DE JAVA
6
permiten variables globales). Mantendremos esta línea al codificar nuestros algoritmos en Java,
si bien deberemos adaptarnos a las particularidades de dicho lenguaje.
Recordemos la clasificación de los parámetros de una acción (en una función todos los parámetros son de entrada):
De entrada: su valor inicial permanece inalterado tras la ejecución de la acción, aunque
durante ésta se haya modificado
De entrada/salida: su valor inicial es relevante para la ejecución de la acción pero puede
quedar modificado tras la misma
De salida: su valor inicial es irrelevante para la ejecución de la acción (muchas veces ni
siquiera se conoce) y su valor final es creado por la propia acción
En Java no se puede indicar a cual de estas categorías pertenece un parámetro. Simplemente
el lenguaje los trata de una determinada manera según si son de tipos básicos o son objetos de
una clase:
1. Tipos simples: si se modifican dentro del método, su valor no se ve alterado al salir de
la ejecución del mismo, por lo tanto siempre son de entrada. Para lograr que el cambio
de valor sea permanente, y convertirlos en parámetros de entrada/salida o salida, se necesita realizar una envoltura, es decir, introducirlos dentro de un objeto y usar éste como
parámetro.
2. Objetos, incluyendo los arrays: si se modifica alguna de sus componentes dentro del método, el cambio es permanente2 : son de entrada/salida o salida, según si su valor inicial es
relevante o no. Si esto no se desea, hay que obtener una copia del objeto y pasarla en su
lugar.
Ejemplos:
1. Cabecera de una acción que intercambia los valores de las variables enteras m y n.
accion intercambiar ent sal m n : entero Al pasar a Java, los enteros m y n serán tratados como parámetros de entrada por lo tanto,
sus valores no se verán modificados después de aplicar la acción. Por lo tanto, la traducción
que parecería inmediata no funciona:
public static void intercambiar (int m, int n)
2 Sin embargo,
el cambio no es permanente si se modifica un objeto entero mediante una asignación (se comporta
como si fuera de entrada)
1.6. ACCIONES Y FUNCIONES; PASO DE PARÁMETROS
7
Para conseguir el resultado deseado las dos variables enteras deben envolverse, es decir,
guardarse en sendos objetos de una clase y poder así ser tratados como de entrada/salida.
Supongamos que existe una clase INT que permite almacenar valores enteros. Entonces la
cabecera correcta sería
public static void intercambiar (INT m, INT n)
Más adelante veremos cómo se puede crear y manejar una clase de estas características.
2. Cabecera de una acción que intercambia los valores de las posiciones i y j de un vector v.
accion intercambiar_vect ent sal v : vector de enteros; ent i j : entero Claramente el vector es un parámetro de entrada/salida, mientras que las posiciones son
sólo de entrada: hay que mencionarlo en la notación algorítmica. La traducción a Java,
dentro de un programa principal, sería así
public static void intercambiar_vect (int[] v, int i, int j)
Por la mencionada característica de Java, contamos con que el vector será modificado de
forma permanente y los enteros no.
3. Cabecera de una función que busca un elemento x en un vector v y devuelve el resultado
en un booleano b.
funcion busqueda_lin v : vector de enteros; x : entero
retorna b : booleano;
Notad que, al tratarse de una función, los parámetros son de entrada y no hace falta mencionarlo. Esta cabecera podría traducirse a Java, dentro de un programa principal, así
public static boolean busqueda_lin (int[] v, int x) {
boolean b;
...
...
return b;
}
Observad que dicha función no debería modificar el vector, pero si lo hiciera, dicho cambio
sería permanente. Si deseamos evitar totalmente esa posibilidad, habría que pasar una
copia del vector.
La siguiente tabla resume las cuatro posibilidades que resultan al traducir a Java las diversas
categorías de parámetros:
SESIÓN 1. ENTORNO DE TRABAJO Y REPASO DE JAVA
8
tipo simple objeto
entrada
pasar
pasar
(cambios no permanentes)
tal cual
copia
entrada/salida o salida
pasar
pasar
(cambios permanentes)
envoltura tal cual
Por último, mencionemos que, si bien en la asignatura se establece claramente la diferencia
entre acciones y funciones, en ocasiones una función ha de convertirse en acción al codificarse
en Java, ya sea por conveniencia o por obligación. Para ello, sustituimos los resultados de la
misma por parámetros de salida.
1.6.1 Ejemplo de envolturas: intercambio de valores enteros
En el fichero intercambio.java tenéis un programa incorrecto para intercambiar los valores
de dos variables enteras, mediante una acción con parametros del tipo int que, como sabemos,
no son tratados como de entrada/salida. Vamos a arreglarlo para que funcione correctamente.
Tenemos dos opciones: envolver a los enteros mediante vectores de 1 elemento, cuyo uso es
bien conocido, o bien crear una clase envoltura nueva. Elegimos la segunda opción3, para lo cual
creamos la clase INT, mediante el siguiente código (ver fichero INT.java)
public class INT
{ int valor; }
De esta forma, si n es un objeto de la clase INT, podemos usarlo para envolver valores enteros
de la siguiente manera:
INT n = new INT(); /* declaración e inicialización */
n.valor = 0; /* asignación */
n.valor = n.valor+1; /* consulta + asignación */
La solución al problema consiste en escribir una acción que intercambie los valores de dos
objetos de la clase INT. Podemos modificar la típica operación para enteros:
public static void intercambiar_INT (INT m, INT n){
int aux;
aux=m.valor;
m.valor=n.valor;
n.valor=aux;
}
3 No debe recurrirse a las clases envoltura estándar de Java.
Para cada tipo simple, Java ofrece una clase envoltura
predefinida y métodos para manejarla: BOOLEAN, INTEGER, etc. Lamentablemente, éstas tienen la propiedad de que
una vez se le asigna un valor a uno de sus objetos, dicho valor no puede cambiar, por lo que no sirven para nuestros
actuales propósitos
1.6. ACCIONES Y FUNCIONES; PASO DE PARÁMETROS
9
Por último, en el programa principal, convertimos los enteros originales a INT y aplicamos la
nueva operación.
INT a = new INT();
INT b = new INT();
a.valor=x;
b.valor=y;
intercambiar_INT(a,b);
x=a.valor;
y=b.valor;
Juntad todos los elementos en un fichero llamado intercambio2.java y comprobad que
ahora el programa se comporta como deseábamos. Notad que el hecho de usar objetos no resuelve el problema por sí sólo, ya que si asignamos directamente los objetos (en lugar de sus campos)
tampoco obtendríamos el resultado deseado. De todas formas, es discutible si esta solución es
mejor que, por ejemplo, aplicar el intercambio directamente, sin la acción. Queda claro, por lo
tanto, que el no permitir que los tipos simples sean parámetros de entrada/salida es una de las
limitaciones más evidentes del lenguaje Java.
1.6.2 Ejercicio: Búsqueda lineal en un vector de enteros
En el fichero busquedalin.java tenéis un programa basado en el algoritmo de búsqueda lineal
sobre un vector de enteros. Modificadlo para obtener una función con dos resultados: un booleano que nos diga si el elemento buscado está en el vector y un índice que, en caso de éxito,
indique una posición en la que se encuentre dicho elemento.
El resultado esperado puede ser uno de los siguientes:
El elemento no esta en el vector
El elemento esta en el vector en la posicion <la que sea>
El algoritmo en el que nos basamos es una versión de la búsqueda lineal.
funcion busqueda_lin_2 v : vector; x : natural retorna b : booleano; i : natural
Pre: cierto i : 0;
b : falso;
Inv: b x está en v 1 i y i N y b v i x Cota: N i mientras i N b hacer
b : v i 1 x;
i : i 1
fmientras;
Post: b x está en v y b v i x SESIÓN 1. ENTORNO DE TRABAJO Y REPASO DE JAVA
10
Lo primero que debemos notar es que Java sólo permite escribir funciones con un resultado.
Por lo tanto, o bien convertimos nuestra función en acción y trabajamos con parámetros de salida
(que al ser de tipos simples, necesitarán una envoltura), o bien buscamos la manera de juntarlos
por medio de una clase auxiliar.
En el primer caso, los parámetros de salida pueden almacenarse en vectores de dimensión 1
o bien pueden envolverse mediante objetos de clases auxiliares como en el ejemplo anterior, ya
que igual que hemos usado la clase INT, podemos definir la clase BOOL.
En cuanto a la opción consistente en juntar los resultados en uno sólo, que permite implementar el algoritmo indistintamente como acción o como función, hay que definir una clase auxiliar
con dos atributos. El manejo de éstos es idéntico al del ejemplo anterior. La clase se define así
(ver fichero BOOLINT.java)
public class BOOLINT
{
boolean valbool;
int valint;
}
Notad que hemos definido las clases INT y BOOLINT de tal forma que su contenido es consultable y modificable directamente. Para clases más complejas, deberíamos definir su contenido
como privado y dotarles de operaciones de creación, modificación y consulta.
En resumen, tenemos cuatro soluciones para este problema:
1. una acción, convirtiendo los resultados en parámetros de salida, por separado, y envueltos
en vectores
2. una acción, convirtiendo los resultados en parámetros de salida, por separado, y envueltos
con INT y BOOL
3. una acción, convirtiendo los resultados en parámetros de salida, envueltos conjuntamente
mediante la clase BOOLINT
4. una función, cuyos resultados estén envueltos conjuntamente mediante la clase BOOLINT
Por último, hay dos soluciones más que no consideramos adecuadas. Una de ellas es tratar
uno de los resultados como tal y el otro como parámetro de salida. Lo que queda es un híbrido
entre acción y función al que no deseamos recurrir por ahora. Alternativamente, se puede intentar
agrupar los dos resultados en un mismo vector: al ser de tipos distintos, surgen problemas de
compatibilidad, que pueden esquivarse a cambio de complicar aún más la solución.
1.6.3 Ejercicio: Intercambio de dos posiciones de un vector
Escribir una acción que intercambie los valores de dos posiciones de un vector de enteros. Emplead la cabecera vista anteriormente en este capítulo. Completad el programa con un método
main que lea un vector y dos posiciones, realice el intercambio y escriba el nuevo vector.
Descargar