Introducción A Ciencias de La Computación I - Manual de Prácticas de Laboratorio
Introducción A Ciencias de La Computación I - Manual de Prácticas de Laboratorio
Introducción A Ciencias de La Computación I - Manual de Prácticas de Laboratorio
discussions, stats, and author profiles for this publication at: https://www.researchgate.net/publication/43041588
CITATIONS READS
0 577
2 authors, including:
Canek Pelez
Universidad Nacional Autnoma de Mxico
8 PUBLICATIONS 7 CITATIONS
SEE PROFILE
Some of the authors of this publication are also working on these related projects:
All content following this page was uploaded by Canek Pelez on 25 August 2016.
Se ha tratado de utilizar una serie de convenciones coherentes para poder distinguir los
diferentes contextos en los que aparece codigo en el texto.
Todo el codigo fue insertado en el texto utilizando el paquete listings, de Carsten Heinz, a
menos que el paquete fuera incapaz de manejar el contexto. De cualquier forma, si no se poda
usar el paquete listings, se trato de emular su funcionamiento.
El codigo que aparece dentro de bloques con lneas numeradas es codigo que esta pensado
para que se escriba directamente en un archivo o que esta sacado de algun archivo ya existente.
Por ejemplo
5 public c l a s s U s o M a t r i z 2 x 2 {
6
El codigo que aparece en cajas y sin numeracion de lneas es codigo pensado para ejemplos
cortos, no necesariamente existente o atado a alguna funcion en particular. Por ejemplo
c . i m p r i m e l n ( "hola mundo" ) ;
Los comandos pensados para que el alumno los teclee en su interprete de comandos estan
escritos utilizando el tipo typewriter, y se utiliza el caracter # para representar el prompt del
interprete. Por ejemplo
Cada vez que aparece un nuevo concepto en el texto, se resalta utilizando el tipo slanted. Por
ejemplo: Generalmente diremos que la clase principal o clase de uso es la clase que mandamos
ejecutar desde el interprete de comandos con java.
Cuando un nombre o nombres aparezca entre < y >, significa que puede ser reemplazado
por cadenas proporcionadas por el alumno. Por ejemplo
# java <NombreDeClase>
significa que <NombreDeClase> puede ser reemplazado por cualquier nombre de clase. Tambien
significara que puede ser reemplazado por mas de un termino, si el contexto lo permite. Por
ejemplo en
i
# javac <VariosArchivosDeClases>
ii
Indice general
Convenciones I
iii
3.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.5. Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6. Herencia 73
6.1. Meta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
6.2. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
6.3. Desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
6.3.1. Heredar y abstraer clases . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.3.2. El acceso en la herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.3.3. La jerarqua de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.3.4. Tipos basicos como objetos . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.3.5. Conversion explcita de tipos . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.3.6. Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.3.7. La herencia en el diseno . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.5. Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
7. Entrada/Salida y Arreglos 87
7.1. Meta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.2. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.3. Desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.3.1. Entrada y Salida (E/S) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.3.2. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
iv
7.5. Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
8. Recursion 101
8.1. Meta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8.2. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8.3. Desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8.3.1. Un mal ejemplo: factorial . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8.3.2. Un buen ejemplo: las Torres de Hanoi . . . . . . . . . . . . . . . . . . . . 103
8.3.3. Listas y recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
8.3.4. Arreglos y recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
8.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
8.5. Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
v
12. Hilos de Ejecucion y Enchufes
(Opcional) 161
12.1. Meta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
12.2. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
12.3. Desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
12.3.1. Hilos de Ejecucion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
12.3.2. Programacion en red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
12.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
12.5. Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
vi
Figuras
3.1. Consola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2. Resultado de imprime en la consola . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.3. Resultado de imprimeln en la consola . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4. Tipos basicos de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.1. Jerarqua de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.1. Torres de Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
10.1. Dialogo de leeString . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
10.2. Jerarqua de componentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
10.3. Jerarqua de componentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
12.1. Ejecucion de un hilo de ejecucion . . . . . . . . . . . . . . . . . . . . . . . . . . 165
vii
viii
Practica 1
Ant y el compilador de Java
1.1. Meta
Que el alumno comience a utilizar Ant para compilar, detectar errores de sintaxis y semanti-
cos, y generar bytecode ejecutable.
1.2. Desarrollo
Java es un lenguaje compilado, lo que quiere decir que un compilador se encarga de trans-
formar las instrucciones de alto nivel de un programa, en codigo que la Maquina Virtual de
Java (Java Virtual Machine o JVM) puede ejecutar.
Actividad 1.1 Invoca al compilador de Java sin ningun argumento, con la siguiente lnea
de comandos:
# javac
(Nota que solo debes escribir javac y despues teclear Enter ; el # solo representa el prompt
de tu interprete de comandos).
Anota todas las opciones que se pueden pasar al compilador.
El compilador de Java no solo genera el codigo ejecutable por la JVM, tambien detecta
errores de sintaxis y semanticos, mostrando en que lnea ocurren.
1
Ademas, el compilador tiene lo que es conocido como recuperacion; al encontrar el primer
error no se detiene, trata de continuar tanto como sea posible y seguir encontrando errores. Esto
ultimo es importante porque le permite al programador corregir un mayor numero de errores
por cada compilacion, en lugar de tener que compilar cada vez que quiere encontrar el siguiente
error.
1.2.1. Ant
Es posible compilar cualquier proyecto de Java utilizando el compilador desde la lnea de
comandos. Sin embargo, conforme un proyecto crece en tamano y complejidad, la lnea de
comandos va siendo cada vez mas restrictiva.
Para ayudarnos con la compilacion de proyectos en Java, utilizaremos Ant. Ant es un progra-
ma (escrito en Java, por cierto), que lee un archivo de configuracion en XML, y de ah obtiene
la informacion necesaria para compilar un proyecto.
http://ant.apache.org/
# cd practica1
# ant compile
Cuantos errores marca? Los entiendes? Pregunta al ayudante el significado de cada error.
El compilador de Java, a traves de Ant, muestra en que lnea de un programa ocurre el error
(si encuentra alguno). Si se utiliza un editor como XEmacs, y si esta configurado de manera
adecuada, se puede compilar dentro de XEmacs y saltar directamente a la lnea en donde ocurre
el error.
2
Actividad 1.4 Abre el archivo UsoReloj.java en XEmacs (esta en el directorio
icc1/practica1), y compila haciendo C-c C-v C-b , tecleando compile y dando Enter
despues.
Debe abrirse un segundo buffer en XEmacs donde se muestran los errores encontrados por
el compilador. Cambiate a ese buffer haciendo C-x o y teclea Enter en la primera lnea
que marque error. Que sucede?
En caso de que no funcione, puedes intentar cargar el archivo de proyecto que utiliza la
practica. Para esto, en XEmacs haz click en el menu JDE Project Project File
Load. Si no existe ningun menu JDE, tu XEmacs no esta configurado como es debido.
Una vez que un programa esta libre de errores, el compilador genera un archivo en bytecode.
La JVM ejecuta bytecode, un formato binario desarrollado para que los ejecutables de Java
puedan ser utilizados en varias plataformas sin necesidad de recompilar.
El bytecode puede copiarse directamente a cualquier maquina que tenga una JVM, y ejecu-
tarse sin cambios desde ah. A eso se le conoce como portabilidad a nivel binario. Muchos otros
lenguajes de programacion tienen portabilidad a nivel de codigo, y otros no tienen ningun tipo
de portabilidad.
http://www.w3c.org
Para motivos de esta practica, solo es necesario saber que un documento XML tiene etiquetas
(tags), que cada etiqueta tiene una etiqueta de inicio (del tipo <etiqueta>) y una etiqueta final
(del tipo </etiqueta>), y que estas etiquetas estan anidadas:
1 <e t i q u e t a 1 >
2 <e t i q u e t a 2 >
3 ...
4 </ e t i q u e t a 2 >
5 <e t i q u e t a 3 >
6 <e t i q u e t a 2 > . . . </ e t i q u e t a 2 >
7 </ e t i q u e t a 3 >
8 ...
9 </ e t i q u e t a 1 >
3
<e t i q u e t a 1 ><e t i q u e t a 2 > . . . </ e t i q u e t a 1 ></ e t i q u e t a 2 >
La sintaxis del archivo build.xml la iremos analizando en las siguientes practicas del curso.
1.3. Ejercicios
1. Corrige los errores que aparecen en la practica, hasta que el programa compile y genere
el bytecode. Utiliza los mensajes del compilador1 para determinar la lnea del error y
en que consiste. Todos los errores estan en la clase UsoReloj, que esta en el archivo
UsoReloj.java (las clases de Java generalmente estan en un archivo llamado como la
clase, con extension .java). No toques los demas archivos.
(Puedes darle una mirada a los demas archivos de la practica; sin embargo, el fun-
cionamiento de estos archivos sera discutido posteriormente.)
1
S, los mensajes estan en ingles. Ni modo.
4
2. Una vez que el programa compile, ejecutalo con la siguiente lnea de comandos (estando
en el directorio practica1):
# ant run
# ant clean
# ant
y ve cuantos archivos dice ant que va a compilar (al momento de correr Ant con el objetivo
compile, dice cuantos archivos se prepara a compilar).
Una vez que termine de compilar, ejecuta el comando
# touch src/icc1/practica1/UsoReloj.java
1.4. Preguntas
1. Que errores encontraste al compilar esta practica? Explica en que consisten.
2. Los errores que encontraste, de que tipo crees que sean, sintacticos o semanticos? Justifica
tu respuesta.
4. Cual crees que sea la explicacion del comportamiento de Ant despues de hacer el ejercicio
3? Justifica tu respuesta.
5
6
Practica 2
Usar y modificar clases
2.1. Meta
Que el alumno aprenda como funcionan las clases, como se instancian, que son metodos y
variables de clase, y como se invocan las funciones de una clase.
2.2. Objetivos
Al finalizar la practica el alumno sera capaz de:
hacer que el objeto llame a las funciones de su clase con el operador . (punto),
modificar las funciones de una clase para que sus objetos tengan un comportamiento
distinto, y
generar la documentacion de una clase para saber que funciones provee sin necesidad de
leer el codigo directamente.
7
2.3. Desarrollo
En la practica pasada, aprendimos como utilizar ant y el compilador de Java para generar
bytecode ejecutable, y ejecutamos un programa que mostraba un reloj en la pantalla.
En esta practica analizaremos mas detenidamente la clase UsoReloj para ver como funciona
el programa, y modificaremos ciertas partes de la clase ClaseReloj.
2.3.1. Clases
Las clases de la practica pasada son ClaseReloj, VistaRelojAnalogico y UsoReloj. Ademas
tenemos dos interfaces: Reloj y VistaReloj. Veremos las interfaces en detalle mas adelante en
esta misma practica.
Los objetos de la clase ClaseReloj son como la maquinaria de un reloj de verdad. La maquina-
ria es la que se encarga de que el reloj funcione de cierta manera (de que avance o retroceda,
etc.)
Los objetos de la clase VistaRelojAnalogico son como la parte externa de un reloj de verdad.
Piensen en el Big Ben; los objetos de la clase VistaRelojAnalogico son la torre y las manecillas
y la caratula con los numeros. En otras palabras, los objetos de la clase VistaRelojAnalogico
no saben como funciona un reloj (ni tienen por que saberlo). Solo se encargan de mostrar la
informacion del reloj al mundo exterior, poniendo las manecillas donde deben estar.
La clase UsoReloj es como el dueno de un reloj de verdad. Posee al reloj, lo pone a la hora
requerida y en general hace con el todo lo que se puede hacer con un reloj.
La clase principal del programa es UsoReloj, en el sentido de que es en esta clase donde
se utilizan las otras dos (ClaseReloj y VistaRelojAnalogico). Generalmente diremos que la clase
principal o clase de uso es la clase que mandamos ejecutar desde el interprete de comandos con
java o con ant run.
La clase principal siempre tendra el punto de entrada a nuestros programas; este punto de
entrada es la funcion main.1
8
1 <t a r g e t name="compile">
2 <mkdir d i r="build"/>
3 <j a v a c s r c d i r="src" d e s t d i r="build" debug="true"
4 d e b u g l e v e l="source" />
5 </ t a r g e t >
Lo primero que hace este codigo es crear un directorio (dentro del directorio donde se
encuentra el archivo build.xml). Este directorio (build) sirve para que ah se guarde el bytecode
que se genera al compilar nuestro codigo. El tener dos directorios separados para nuestro codigo
fuente (src, por source, fuente en ingles), y otro para nuestros archivos construidos (build, por
construir en ingles), nos permite una mas facil organizacion de nuestro codigo. En particular,
limpiar nuestro directorio de trabajo se limita a borrar el directorio build.xml.
Para crear el directorio, utilizamos la tarea mkdir (las tareas son etiquetas que ant provee
para hacer cosas):
1 <mkdir d i r="build"/>
Noten que la etiqueta termina con / >, lo que quiere decir que no tiene cuerpo.
Despues de crear el directorio build, mandamos llamar al compilador de Java, javac,
diciendole donde esta nuestro codigo fuente (srcdir), donde queremos que se guarden los binarios
(destdir), que queremos que el codigo tenga smbolos de depuracion (debug), y el nivel de
depuracion que queremos (debuglevel). Para esto, utilizamos la tarea javac:
Tambien es una etiqueta sin cuerpo. Con esta informacion, ant busca los archivos fuente
(.java) en el directorio de fuentes, o en sus subdirectorios, y los compila en el directorio build.
Para ejecutar nuestro programa utilizamos
# ant run
El objetivo tiene un atributo, depends, cuyo valor es compile. Esto quiere decir que el
objetivo run depende del objetivo compile. En otras palabras, el objetivo run no puede ser
llamado si antes no ha sido llamado el objetivo compile. Si llamamos al objetivo run sin haber
llamado al objetivo compile, ant automaticamente lo llamara.
Para ejecutar el programa, utilizamos la tarea java:
9
1 <j a v a c l a s s n a m e="icc1.practica2.UsoReloj">
2 <c l a s s p a t h >
3 <p a t h e l e m e n t p a t h="build" />
4 </ c l a s s p a t h >
5 </ j a v a >
1 <c l a s s p a t h >
2 <p a t h e l e m e n t p a t h="build" />
3 </ c l a s s p a t h >
El cuerpo es una estructura de tipo ruta (path en ingles). En otras palabras, le dice a Java
donde buscar las clases necesarias para la ejecucion de un programa. Esta estructura a su vez
tiene un cuerpo que esta formado por la etiqueta pathelement, que en su atributo path dice
que ruta queremos utilizar. En este caso es el directorio build, donde estan nuestras clases
compiladas. La etiqueta classpath puede aceptar varios directorios en su cuerpo.
Noten que todas las partes de una clase de Java estan formadas a partir de bloques, anidados
unos dentro de otros, y todos adentro del bloque mas grande, que es el de la clase misma. Un
bloque comienza con un {, y termina con un }. Si descontamos el bloque de la clase, todos los
bloques son de la forma:
{
<e x p r e s i o n 1 >;
<e x p r e s i o n 2 >;
10
...
<e x p r e s i o n N>;
}
Esto es un bloque de ejecucion. En el, las expresiones del bloque se ejecutan en el orden
en que aparecen. A un bloque tambien se les suele llamar cuerpo. Cada vez que un bloque
comienza, la indentacion del programa debe aumentar; esto facilita la lectura de codigo.
14 Reloj r ;
14 ClaseReloj r ;
pero queremos utilizar la interfaz Reloj. Ahorita veremos en que consisten las interfaces.
As queda declarado un objeto que implementa la interfaz Reloj, y el nombre con el que
haremos referencia al objeto sera r. Declarar un objeto es como cuando llenamos un catalogo
para hacer compras por correo o por Internet; hacemos explcito que queremos (en este caso un
objeto que inmplementa la interfaz Reloj), pero aun no lo tenemos. Para poseerlo, necesitamos
crearlo.
Para crear o instanciar un objeto, se utiliza el operador new. En la clase UsoReloj lo hacemos
de la siguiente manera:
17 r = new C l a s e R e l o j ( ) ;
El operador new llama al constructor de la clase ClaseReloj. Veremos con mas detalle a los
constructores en la siguiente practica.
Es hasta el momento de instanciar el objeto, cuando este comienza a existir propiamente;
ahora podemos comenzar a utilizarlo.
11
Para poner a la maquinaria la caratula y las manecillas, necesitamos un objeto que imple-
mente la interfaz VistaReloj.
De igual manera, necesitamos declarar al objeto e instanciarlo. En la clase UsoReloj hacemos
esto:
V i s t a R e l o j rep ;
...
r e p = new V i s t a R e l o j A n a l o g i c o ( r ) ;
22 r . avanzaSegundo ( ) ;
Lo que hace entonces el operador . (punto) es ver de que clase es el objeto (un objeto
siempre sabe de que clase es instancia), buscar la funcion por su nombre, y ejecutarla pasandole
los parametros.
2
Aqu algunos de ustedes estaran pensando que no tiene mucho sentido separar la maquinaria de la caja;
despues de todo, cuando uno compra un reloj lo hace con la maquinaria y la caja juntas (aunque muchos relojes
finos vienen con varios extensibles intercambiables, por ejemplo).
Esta es una de las caractersticas del paradigma Orientado a Objetos. Al tener separadas la parte que muestra
al reloj, de la parte que maneja su funcionamiento, es posible cambiar la representacion del reloj sin tener
que reescribir todo el programa (podramos escribir una clase llamada VistaRelojDigital, que en lugar de usar
manecillas utilizara el formato HH:MM:SS para representar el reloj, y ya no tenemos que preocuparnos del
funcionamiento interno del reloj, que lo maneja la clase ClaseReloj).
12
Actividad 2.4 En el archivo UsoReloj.java, busca donde se llama a las funciones avan-
zaSegundo, avanzaMinuto y avanzaHora y defineTiempo de la clase ClaseReloj.
Estos metodos hacen lo que su nombre indica; avanzaSegundo hace que el segundero se
mueva hacia adelante un segundo. Las otras dos (avanzaMinuto y avanzaHora) hacen lo mismo
con el minutero y las horas respectivamente.
El metodo defineTiempo cambia el valor del segundero, del minutero y de las horas de un
solo golpe. Recibe tres enteros como parametro, as que para poner el reloj a las 7 con 22
minutos y 34 segundos tenemos que hacer esto:
23 rep . a c t u a l i z a ( ) ;
13
20 private int h o r a ;
21 private int minuto ;
22 private int segundo ;
Tambien hora, minuto y segundo son variables. Que diferencia tienen de r y rep?
La primera diferencia es obvia; la declaracion de hora, minuto y segundo esta precedida por
un private. La palabra clave private es un modificador de acceso; los veremos con mas detalle
en la practica 3.
La segunda diferencia tambien es muy facil de distinguir: r y rep estan declaradas dentro de
una funcion (main en este caso), mientras que hora, minuto y segundo estan declaradas dentro
de la clase.
La diferencia es importante; las variables declaradas dentro de una funcion son variables
locales, mientras que las variables declaradas dentro de una clase son variables de clase o
variables miembro.
Desde que vieron la declaracion de hora, minuto y segundo, debio ser obvio que sirven para
que guardemos los valores del mismo nombre de nuestros objetos de la clase Reloj.
Las variables de clase sirven justamente para eso; para guardar el estado de un objeto. Al
valor que tienen en un instante dado las variables de clase de un objeto se le llama el estado de
un objeto.
Veremos mas acerca de las variables en las siguientes practicas.
Que hace el metodo? En primer lugar, no recibe ningun parametro. Esto tiene sentido, ya
que la funcion siempre hara lo mismo (avanzar una hora), y por lo tanto no necesita ninguna
informacion externa para hacerlo. Despues, esta la lnea:
3
Algunas veces tambien se utilizan funciones que no cambian el estado de un objeto, ni tampoco lo obtienen;
el metodo espera es un ejemplo.
14
152 h o r a++
El operador ++ le suma un 1 a una variable. Veremos este y mas operadores en las siguientes
practicas.
Sumarle un 1 a hora es justamente lo que queremos (que las horas avancen en uno). Pero,
que ocurre cuando son las 12 y le sumamos 1? En un reloj de verdad, la hora da la vuelta
y se pone de nuevo en 1. En particular en nuestra clase ClaseReloj estamos asumiendo que 12
es igual a cero (como ocurre con muchos relojes digitales), y entonces cuando hora vale 0, la
manecilla de las horas apunta hacia el 12. As que cuando hora valga 11 y se llame a avanzaHora,
nuestro reloj debera poner hora en 0. Eso es justamente lo que hacen las lneas
2.3.8. Interfaces
Hemos estado trabajando con la clase ClaseRejoj; sin embargo, el objeto que declaramos fue
de la interfaz Reloj. Que es una interfaz?
Si examinamos el archivo Reloj.java, notaremos que una interfaz es una especie de clase
vaca. Sus metodos no tienen cuerpo; no hacen nada. Para que nos sirve entonces?
Una interfaz es un contrato. Especifica que ofrece una clase; pero no dice como lo hace. La
clase ClaseReloj implementa (implements) la interfaz Reloj. Con eso, se compromete a cumplir
el contrato; a encontrar un modo de hacer que funcionen los metodos que declara la interfaz.
Veremos con mas detalle a las interfaces cuando veamos herencia.
15
Los comentarios que empiezan con / son por bloque. El compilador ignora absolutamente
todo lo que encuentra a partir de /, y lo sigue haciendo hasta que encuentre un /. No se vale
tener comentarios de este tipo anidados; esto es invalido:
/
/ Comentario i n v a l i d o ( e s t a anidado ) . /
/
2.3.10. JavaDoc
Como podemos saber que metodos nos ofrece una clase?
La manera mas sencilla es abrir el archivo de la clase y leer el codigo. Sin embargo, esto
puede resultar tedioso, si la clase es larga y compleja.
Java ofrece un pequeno programa que nos permite observar los interiores de una clase de
manera ordenada y elegante.
El programa se llama JavaDoc, y lee programas escritos en Java, con lo que genera paginas
HTML que podemos ver utilizando cualquier navegador, como Firefox o el Internet Explorer.
Como casi todo lo que tiene que ver con desarrollo en Java, Ant tiene una tarea para manejar
a JavaDoc, y sorprendentemente se llama javadoc. En nuestro build.xml lo mandamos llamar
dentro del objetivo docs:
1 <t a r g e t name="docs">
2 <j a v a d o c s o u r c e p a t h="src" d e s t d i r="docs "
3 packagenames="icc1.practica2" />
4 </ t a r g e t >
Lo unico que le decimos a la tarea javadoc es donde estan los archivos fuente, en que direc-
torio queremos que genere la documentacion, y de que paquetes queremos que la genere (vimos
un poco arriba que el paquete que estamos utilizando es icc1.practica2.
# javadoc
# ant docs
16
La documentacion generada muestra, entre otras cosas, los metodos y constructores de las
clases, incluyendo los parametros que reciben.
Mas adelante veremos como utilizar los comentarios de Java para ayudar a JavaDoc a
generar informacion.
2.4. Ejercicios
1. Modifica la funcion avanzaMinuto de la clase ClaseReloj para que cada vez que sea llamada,
en lugar de avanzar un minuto avance diez. Recuerda que la clase ClaseReloj esta en el
archivo ClaseReloj.java.
Compila y ejecuta de nuevo el programa para ver si tus cambios son correctos.
2. Con la documentacion que generaste, lee que parametros recibe el segundo constructor de
la clase VistaRelojAnalogico, y utilzalo en el metodo main de la clase UsoReloj, para que
al ejecutar el programa la ventana del reloj sea mas grande o pequena. Juega con varios
valores de x y y, compilando y ejecutando cada vez para comprobar tus resultados.
2.5. Preguntas
1. Entiendes como funciona el metodo avanzaHora? Justifica tu respuesta.
17
18
Practica 3
Variables, tipos, y operadores
3.1. Meta
Que el alumno aprenda a utilizar los tipos basicos de Java y sus operadores, y que entienda
el concepto de referencia.
3.2. Objetivos
Al finalizar la practica el alumno sera capaz de:
3.3. Desarrollo
Un programa (en Java o cualquier lenguaje de programacion), esta escrito para resolver
algun problema, realizando una o mas tareas.
En el caso particular de Java, se utiliza la Orientacion a Objetos para resolver estos prob-
lemas. Utilizar la Orientacion a Objetos quiere decir, entre otras cosas, abstraer el mundo en
objetos, y a estos objetos agruparlos en clases. En la practica anterior, por ejemplo, utilizamos
las clases ClaseReloj y VistaRelogAnalogico, que abstraan el funcionamiento de un reloj y su
representacion externa al implementar las interfaces Reloj y VistaReloj.
19
Vamos a comenzar a resolver problemas utilizando Orientacion a Objetos. Esto quedara divi-
dido en dos partes fundamentales: la primera sera identificar los objetos que representan nuestro
problema, agruparlos en clases y definir precisamente el comportamiento (las funciones) que
tendran para que resuelvan nuestro problema.
La segunda parte es un poco mas detallada. Una vez definido el comportamiento de una
clase, hay que sentarse a escribirlo. Esto se traduce en implementar los metodos de la clase. Para
implementarlos, es necesario manejar las partes basicas o atomicas del lenguaje: las variables,
sus tipos, y sus operadores.
En esta practica jugaremos con esta parte fundamental del lenguaje Java; en la siguiente
utilizaremos lo que aprendamos aqu para comenzar a escribir nuestras clases.
Para jugar con variables, vamos a necesitar un bloque de ejecucion donde ponerlas, o sea un
metodo, y ya que necesitamos un metodo, vamos a necesitar tambien una clase donde ponerlo.
Un problema es que no sabemos hacer todava metodos, as que utilizaremos el metodo
main, que ya utilizamos la clase pasada. Creemos pues una clase llamada Prueba, y utilicemos
el metodo main de ella.
Nuestra clase Prueba quedara como sigue:
1 package i c c 1 . p r a c t i c a 3 ;
2
3 public c l a s s Prueba {
4
7 }
8 }
Aun no hemos visto paquetes, as que por ahora no explicaremos que significa la lnea
1 package i c c 1 . p r a c t i c a 3 ;
Por ahora solo necesitamos saber que eso significa que el archivo Prueba.java debe estar
en un directorio llamado practica3, y que este a su vez debe estar en un directorio llamado
icc1.
20
Actividad 3.1 Baja el archivo practica3.tar.gz, y descomprmelo como lo hemos hecho en
practicas pasadas.
El archivo contiene ya un build.xml para esta practica, y la estructura de directorios nece-
saria (o sea src/icc1/practica3). Dentro de ese directorio crea tu archivo Prueba.java
para tu clase Prueba.
Si estas usando XEmacs (y este esta bien configurado), y pones esto:
/ jde /
en la primera lnea, cada vez que abras el archivo, XEmacs hara disponibles varias fun-
cionalidades especiales para editar archivos de Java. Entre otras cosas, coloreara de forma
especial la sintaxis del programa, y te permitira llamar a ant con C-c C-v C-b .
Como recordaras de la practica anterior, utilizamos el directorio src para poner nuestro
codigo fuente (y as tambien lo especifiacmos en el build.xml). Y como nuestro paquete
es icc1.practica3, dentro de src creamos el directorio icc1, y dentro de este otro llamado
practica3.
Con el build.xml proporcionado podemos compilar y ejecutar el programa como venamos
haciendolo hasta ahora. Podemos comprobar (viendo el archivo build.xml) que ahora la clase
que ejecutamos en el objetivo run es icc1.practica3.Prueba. Esto es la clase Prueba del
paquete icc1.practica3.
Varios deben estar ahora haciendose varias preguntas: Que quiere decir exactamente public
class Prueba? Para que es el static? De donde salio el constructor de Prueba?
Todas esas preguntas las vamos a contestar en la siguiente practica; en esta nos vamos a
centrar en variables, tipos y operadores.
La clase Prueba se ve un poco vaca; sin embargo, es una clase completamente funcional y
puede ser compilada y ejecutada.
Actividad 3.2 Compila y ejecuta la clase (desde el directorio donde esta el archivo
build.xml):
# ant compile
# ant run
No va a pasar nada interesante (de hecho no va a pasar nada), pero con esto puedes com-
probar que es una clase valida.
Ahora, queremos jugar con variables, pero no va a servir de mucho si no vemos los resultados
de lo que estamos haciendo. Para poder ver los resultados de lo que hagamos, vamos a utilizar
a la clase Consola.
21
Actividad 3.3 Con la ayuda de tu ayudante, baja el archivo icc1.jar, y ponlo en el
directorio de la practica 3.
Como vamos a utilizar la clase Consola dentro de nuestra clase Prueba? Primero, debemos
decirle a nuestra clase acerca de la clase Consola. Esto lo logramos escribiendo
import i c c 1 . i n t e r f a z . C o n s o l a ;
por
El classpath es donde Java (el compilador y la maquina virtual) buscan clases extras.
En ultimo lugar, cuando ejecutemos de nuevo tenemos que decirle a la JVM donde esta nues-
tra clase Consola; para esto tambien modificamos nuestro build.xml.
22
Igual; estamos anadiendo el archivo icc1.jar al classpath para que la JVM Java lo encuentre
al ejecutar el programa.
Que podemos hacer con la clase Consola? De hecho, podemos hacer muchas cosas, que
iremos descubriendo poco a poco conforme avancemos en el curso; pero por ahora, nos confor-
maremos con utilizarla para mostrar los valores de nuestras variables.
Para mostrar informacion con la consola, necesitamos crear un objeto de la clase consola y
con el llamar a los metodos imprime o imprimeln. Por ejemplo, modifica el metodo main de la
clase Prueba de la siguiente manera:1
1 package i c c 1 . p r a c t i c a 3 ;
2
3 import i c c 1 . i n t e r f a z . C o n s o l a ;
4
5 public c l a s s Prueba {
6
23
11 Consola c ;
En esta lnea instanciamos a c. El constructor que usamos recibe como parametro una
cadena, que es el ttulo de la ventana de nuestra consola. Al construir el objeto, la ventana con
nuestra consola se abre automaticamente.
El metodo imprime de la clase Consola hace lo que su nombre indica; imprime en la consola
la cadena que recibe como parametro, que en nuestro ejemplo es "Holat mundo.".
Dijimos que podamos utilizar los metodos imprime e imprimeln para mostrar informacion
en la consola. Para ver la diferencia, imprime otra cadena en la consola:
11 Consola c ;
12 c = new C o n s o l a ( " Valores de variables" ) ;
13 c . imprime ( "Hola mundo." ) ;
14 c . imprime ( "Adios mundo." ) ;
13 c . i m p r i m e l n ( "Hola mundo." ) ;
14 c . i m p r i m e l n ( "Adios mundo." ) ;
24
Figura 3.3: Resultado de imprimeln en la consola
La diferencia entre imprime e imprimeln es que la segunda imprime un salto de lnea des-
pues de haber impreso la cadena que le pasamos como parametro. El salto de lnea en Java
esta representado por el caracter \n. Otra forma de conseguir el mismo resultado es hacer
o incluso
La clase Consola ofrece muchos metodos, que iremos utilizando a lo largo de las demas
practicas. En esta solo veremos imprime e imprimeln.
Actividad 3.7 Crea la clase Prueba en el archivo Prueba.java y realiza (valga la redundan-
cia) pruebas con imprime e imprimeln.
Reloj r elo j ;
...
VistaReloj vista ;
25
Las dos son variables locales, pero tienen una diferencia muy importante: son de tipos
diferentes. Java es un lenguaje de programacion fuertemente tipificado, lo que significa que una
variable tiene un unico tipo durante toda su vida, y que por lo tanto nunca puede cambiar de
tipo.
Esto quiere decir que lo siguiente sera invalido:
v i s t a = new C l a s e R e l o j ( ) ;
Si intentan compilar algo as, javac se va a negar a compilar la clase; les va a decir que
vista no es de tipo ClaseReloj.
Vamos a empezar a declarar variables. Para declarar una variable, tenemos que poner el
tipo de la variable, seguido del nombre de la variable:
int a ;
Si queremos usar varias variables de un solo tipo, podemos declararlas todas juntas:
int a , b , c , d ;
Con eso declaramos una variable de cierto tipo, pero, que son los tipos? Para empezar a
ver los tipos de Java, declaremos un entero llamado a en el metodo main.
11 // Declaramos un e n t e r o a ;
12 int a ;
13 }
La variable a es de tipo entero (int). Java tiene algunos tipos especiales que se llaman tipos
basicos. Los tipos basicos difieren de todos los otros tipos en que NO son objetos; no tienen
metodos ni variables de clase, y no tienen constructores. Para inicializar (que no es igual que
instanciar) una variable de tipo basico, sencillamente se le asigna el valor correspondiente, por
ejemplo:
a = 5;
Java tiene ocho tipos basicos, que puedes consultar en la figura 3.4.
26
Nombre Descripcion
byte Enteros con signo de 8 bits en complemento a dos
short Enteros con signo de 16 bits en complemento a dos
int Enteros con signo de 32 bits en complemento a dos
long Enteros con signo de 64 bits en complemento a dos
float Punto flotante de 32 bits de precision simple
double Punto flotante de 64 bits de precision doble
char Caracter de 16 bits
boolean Valor booleano (verdadero o falso)
int entero ;
double doble ;
char caracter ;
boolean booleano ;
entero = 1234;
doble = 13.745;
caracter = a ;
booleano = true ;
En general, a cualquier serie de numeros sin punto decimal Java lo considera una literal de
tipo int, y a cualquier serie de numeros con punto decimal Java lo considera una literal de tipo
double. Si se quiere una literal de tipo long se tiene que poner una L al final, como en 1234L, y
si se quiere un flotante hay que poner una F al final, como en 13.745F. No hay literales explcitas
para byte ni para short.
Las unicas literales de tipo boolean son true y false. Las literales de caracter son a, Z,
+, etc. Para los caracteres como el salto de lnea o el tabulador, las literales son especiales,
como \n o \t. Si se necesita la literal del caracter \, se utiliza \\. Si se necesita un
caracter internacional, se puede utilizar su codigo Unicode2 , por ejemplo \u0041 representa
el caracter A (el entero que sigue a \u esta escrito en hexadecimal, y deben escribirse los
cuatro dgitos).
La fuerte tipificacion de Java se aplica tambien a sus tipos basicos:
int a = 1 3 ; // Codigo v a l i d o
int b = true ; // C o d i g o i n v a l i d o !
2
Unicode es un codigo de caracteres pensado para reemplazar a ASCII. ASCII solo tena (originalmente)
127 caracteres, y despues fue extendido a 255. Eso basta para el alfabeto latino, con acentos incluidos, y
algunos caracteres griegos; pero es completamente inutil para lenguas como la china o japonesa. Unicode tiene
capacidad para 65,535 caracteres, lo que es suficiente para manejar estos lenguajes. Java es el primer lenguaje
de programacion en soportar nativamente Unicode.
27
Sin embargo, el siguiente es codigo valido tambien:
float x = 12;
byte b = 5 ;
int a = b;
Si somos precisos, la literal 12 es de tipo int (como dijimos arriba); y sin embargo se lo
asignamos a una variable de tipo float. De igual forma, b es de tipo byte, pero le asignamos
su valor a a, una variable de tipo int. Dejaremos la explicacion formal de por que esto no
viola la fuerte tipificacion hasta que veamos conversion explcita de datos; por ahora, basta
con entender que un entero con signo que cabe en 8 bits (los de tipo byte), no tiene ninguna
dificultad hacer que quepa en un entero con signo de 32 bits, y que todo entero con signo que
quepa en 32 bits, puede ser representado por un punto flotante de 32 bits.
Por la misma razon, el siguiente codigo s es invalido:
int a = 1 . 2 ;
int b = 5 ;
byte c = b ;
El primero debe ser obvio; no podemos hacer que un punto flotante sea representado por
un entero. Sin embargo, por que no podemos asignarle a c el valor b, cuando 5 s cabe en un
entero de 8 bits con signo? No podemos porque al compilador de Java no le interesa que valor
en particular tenga la variable b. Lo unico que le importa es que es de tipo int, y un int es muy
probable que no quepa en un byte. El compilador de Java juega siempre a la segura.
Actividad 3.8 Prueba los pequenos ejemplos que se han visto, tratando de compilar todos
(utiliza el metodo main para tus ejemplos). Ve que errores manda el compilador en cada
caso, si es que manda.
Como vamos a imprimir en nuestra consola nuestros tipos basicos? Pues igual que nuestras
cadenas:
int a = 5 ;
c . imprimeln ( a ) ;
Los metodos imprime e imprimeln de la clase Consola soportan todos los tipos de Java.
Una ultima observacion respecto a variables locales (sean estas de tipo basico u objetos).
Es obvio que para usar una variable local en un metodo, tiene que estar declarada antes de
ser usada. Esto es obvio por que no podemos usar algo antes de tenerlo. Empero, hay otra
restriccion: no podemos usar una variable antes de inicializarla; el siguiente codigo no compila:
int a ;
int b = a ; // No podemos usar l a v a r i a b l e a t o d a v a . . .
28
Estamos tratando de usar a sin que haya sido inicializada. Esto es invalido para el compilador
de Java.
Dijimos que esto se aplica a tipos basicos y objetos, y mas arriba dijimos que inicializar
no era lo mismo que instanciar. Esto es porque tambien podemos inicializar variables que
sean objetos sin necesariamente instanciar nada. Para entender esto, necesitamos comprender
que son las referencias.
3.3.4. Referencias
Cuando hacemos
Consola c ;
c = new C o n s o l a ( ) ;
lo que hace new es pedirle a la JVM que asigne memoria para poder guardar en ella al nuevo
objeto de la clase Consola. Entonces new regresa la direccion en memoria del nuevo objeto, y
se guarda en la variable c.
Esto es importante; las variables de referencia a algun objeto tienen valores que son di-
recciones de memoria, as como las variables de tipo entero tienen valores que son enteros en
complemento a dos, o las variables de tipo booleano tienen valores que son true o false.
Si despues de instanciar p hacemos
C o n s o l a c2 ;
c2 = c ;
29
3.3.5. Operadores
Sabemos ya declarar todas las posibles variables de Java. Una vez que tengamos variables,
podemos guardar valores en ellas con el operador de asignacion =; pero ademas debemos poder
hacer cosas interesantes con los valores que puede tomar una variable.
Para hacer cosas interesantes con nuestras variables, es necesario utilizar operadores. Los
operadores de Java aparecen en la tabla 3.1.
30
Cuadro 3.1: Operadores de Java. . . . . . (continua)
int a = 3 + 2 ;
entonces a toma el valor 5. Todos los operadores tienen un dominio especfico; cosas como
true + f a l s e ;
no tienen sentido. Cuando se opera con tipos distintos, pero compatibles, se asume el tipo mas
general. Por ejemplo, si tenemos
int a = 2 ;
float x = 1.5;
31
entonces a+x es de tipo float.
Los operadores funcionan de acuerdo al tipo de los operandos. Si se dividen dos enteros con
el operador /, se hace division entera; el resultado se trunca al mayor entero menor o igual al
resultado de la division. Si se dividen flotantes o dobles, el resultado es la division real (o lo
mas cercano, de acuerdo a la precision simple o doble de cada tipo).
Hay varios operadores en la tabla que funcionan sobre bits. Esto quiere decir que operan
sobre un valor de tipo entero, pero fijandose en los bits, mas que en el valor en s.
Veamos un ejemplo
int a = 1 3 ;
int b = 1 9 ;
int c = a | b ;
a = 00000000000000000000000000001101
b = 00000000000000000000000000010011
(recordemos que los enteros tienen 32 bits). Para no tener que escribir tanto, usamos hexadec-
imal para representar los valores y podemos verlos como 0x0000000D y 0x00000013.
Cual es valor de la variable c entonces? Estamos usando el OR para bits; se aplica un OR
a cada uno de los bits del entero, y el resultado es
c = 00000000000000000000000000011111
o bien 0x0000001F, que a su vez es el valor 31 en base 10. Siempre hay que tener en cuenta que
los tipos enteros de Java (byte, short, int, long) tienen signo; eso quiere decir que el bit mas
significativo en estos tipos guarda el signo.
La tabla 3.1 esta ordenada en orden de precedencia. La precedencia de los operadores
determina el orden en que seran aplicados. Por ejemplo en
int a = 3 + 2 5 ;
int a = ( 3 + 2 ) 5 ;
Pueden utilizar la tabla de operadores para saber que operador tiene precedencia sobre
cuales; sin embargo, en general es bastante intuitivo. En caso de duda, poner parentesis nunca
hace dano (pero hace que el codigo se vea horrible; los parentesis son malos MR ).4
Los operadores relacionales (<, >, <=, >=) funcionan sobre tipos numericos, y regresan un
valor booleano. La igualdad y la desigualdad (==, !=) funcionan sobre todos los tipos de Java.
En el caso de las referencias, == regresara true si y solo si las referencias apuntan a la misma
posicion en la memoria; al mismo objeto.
4
Parenthesis are evilTM .
32
Hay que notar que la asignacion (=) es muy diferente a la igualdad (==). La asignacion
toma el valor de la derecha (se hacen todos los computos que sean necesarios para obtener ese
valor), y lo asigna en la variable de la izquierda. El valor debe ser de un tipo compatible al de
la variable. Tambien hay algunos operadores de conveniencia, los llamados auto operadores.
Son +=, =, =, /=, %=, >>=, <<=, >>>=, &=, =, y |=. La idea es que si tenemos una
variable entera llamada a, entonces
a += 10;
es exactamente igual a
a = a + 10;
E igual con cada uno de los auto operadores. Como el caso de sumarle (o restarle) un uno a
una variable es muy comun en muchos algoritmos, los operadores ++ y hacen exactamente
eso. Mas hay dos maneras de aplicarlos: prefijo y post fijo. Si tenemos
int a = 5 ;
int b = a++;
entonces a termina valiendo 6, pero b termina valiendo 5. Esto es porque el operador regresa
el valor de la variable antes de sumarle un uno, y hasta despues le suma el uno a la variable.
En cambio en
int a = 5 ;
int b = ++a ;
las dos variables terminan valiendo 6, porque el operador primero le suma un uno a a, y despues
regresa el valor de la variable.
Los operadores logicos && (AND) y || (OR) funcionan como se espera que funcionen (p q
es verdadero si y solo si p y q son verdaderos, y p q es verdadero si y solo si p es verdadero o
q es verdadero), con una pequena diferencia: funcionan en corto circuito. Esto quiere decir que
si hacemos
y a < b es falso, entonces el && ya no comprueba su segundo operando. Ya sabe que el resultado
va a dar falso, as que ya no se molesta en hacer los calculos necesarios para ver si b < c
es verdadero o falso. Lo mismo pasa si el primer operando de un || resulta verdadero. Es
importante tener en cuenta esto, porque aunque en Matematicas Discretas nos dijeron que
p q q p, a la hora de programar esto no es 100 % cierto.
Un operador bastante util es la condicional aritmetica ( ?: ). Funciona de la siguiente manera:
int z = ( a < b ) ? 1 : 0 ;
33
int b = ( a < 0) ? a : a ;
Hay varios operadores que funcionan con referencias. Algunos funcionan con referencias y
con tipos basicos al mismo tiempo (como = y ==, por ejemplo); pero otros son exclusivos de
las referencias.
Los operadores mas importantes que tienen las referencias son new, que las inicializa, y .
(punto), que nos permite interactuar con las partes de un objeto.
Hay otros operadores para los objetos, y otros para tipos basicos, que iremos estudiando en
las practicas que siguen.
3.3.6. Expresiones
Una vez que tenemos variables (tipos basicos u objetos), literales y operadores, obtenemos
el siguiente nivel en las construcciones de Java. Al combinar una o mas variables o literales con
un operador, tenemos una expresion.
Las expresiones no son atomicas; pueden partirse hasta que lleguemos a variables o literales
y operadores. Podemos construir expresiones muy rapido:
En general, todas las expresiones regresan un valor. No siempre es as, sin embargo, como
veremos en la siguiente practica.
Podemos hacer expresiones tan complejas como queramos (como muestra el ultimo de nue-
stros ejemplos arriba), siempre y cuando los tipos de cada una de sus partes sean compatibles.
Si no lo son, el compilador de Java se va a negar a compilar esa clase.
El siguiente nivel en las construcciones de Java lo mencionamos en la practica pasada: son
los bloques. La proxima practica analizaremos nivel de construccion mas importante de Java:
las clases.
3.4. Ejercicios
Todas estas pruebas las tienes que realizar dentro del metodo main de tu clase Prueba.
2. Declara un float llamado x, y asgnale la literal 1F. Declara un float llamado y y asgnale
la literal 0.00000001F. Declara un tercer float llamado z y asgnale el valor que resulta de
restarle y a x. Imprime z en tu consola.
34
4. Declara dos variables booleanas p y q.
Asgnales a cada una un valor (true o false, tu decide), e imprime el valor de la expresion
(p q) en sintaxis de Java. Despues imprime el valor de la expresion p q utilizando
la consola.
Cambia los valores de p y q para hacer las cuatro combinaciones posibles, imprimiendo
siempre el resultado de las dos expresiones. Con esto demostraras DeMorgan caso por
caso.
3.5. Preguntas
1. Crees que sea posible asignar el valor de un flotante a un entero? Como crees que
funcionara?
int a = 1 ;
int b = 2 ;
int c = 3 ;
35
36
Practica 4
Interfaces y clases por dentro
4.1. Meta
Que el alumno aprenda como escribir clases.
4.2. Objetivos
Al finalizar la practica el alumno sera capaz de:
entender como esta formada una clase,
manejar cadenas, y
escribir clases y clases de uso.
4.3. Desarrollo
En la practica anterior, dijimos que usar el paradigma Orientado a Objetos consista en
abstraer en objetos el problema que queremos resolver. Abstraer en objetos significa identificar
los elementos principales de nuestro problema, crear una clase que abarque todas sus instancias,
y escribir los metodos necesarios de cada clase para que resuelvan nuestro problema.
Por que decimos crear una clase que abarque todas sus instancias? Un problema, en
ciencias de la computacion, abarca muchos posibles (generalmente una infinidad) de casos
concretos. No escribimos una solucion para sumar 3 y 5; escribimos una solucion para sumar
dos numeros a y b, para todos los posibles valores de a y b.1
Esa siempre ha sido la idea de las ciencias de la computacion, desde el inicio cuando las com-
putadoras utilizaban bulbos, y los programas se guardaban en tarjetas perforadas. La diferencia
1
A veces, sin embargo, resolver todos los casos es demasiado costoso, y restringimos el dominio de nuestro
problema. Sin embargo, nunca escribimos una solucion que solo sirva con uno o dos casos concretos; siempre se
trata de abarcar el mayor numero de casos posibles.
37
es como escribimos esta solucion que abarque tantos casos como podamos.
Hace algunos anos se ideo el paradigma Orientado a Objetos como un metodo para escribir
las soluciones a nuestros problemas. No es el unico paradigma; pero muchos pensamos que es
el mejor y mas general.
La idea es que tomemos nuestro problema y definamos los componentes que lo forman.
Cada uno de estos componentes sera un objeto de nuestro problema. Pero no escribimos un
solo objeto. Escribimos una clase que abarca a todas las encarnaciones posibles de nuestros
objetos. Esto tiene muchsimas ventajas:
Cada componente es una entidad independiente. Esto quiere decir que podemos modificar
cada uno de ellos (hacerlo mas rapido, mas entendible, que abarque mas casos), y los demas
componentes no tendran que ser modificados para ello. A esto se le llama encapsulamiento.
Cuando resolvamos otro problema distinto, es posible que al definir sus componentes
descubramos que uno de ellos ya lo habamos definido para un problema anterior. Entonces
solo utilizamos esa clase que ya habamos escrito. A esto se le llama reutilizacion de codigo.
Tenemos un ejemplo de eso ya en las manos. La clase Consola fue escrita para resolver el
problema de como mostrar informacion al usuario (tambien de como obtener informacion
del usuario; veremos eso mas adelante en esta practica). El problema ya esta resuelto; la
clase Consola ya esta escrita, y ahora la utilizaremos casi todo el curso, sin necesidad de
modificarla o escribirla toda cada vez que queramos mostrar informacion al usuario.
Hay muchas mas ventajas; pero para entenderlas necesitamos estudiar mas conceptos. La
ventaja mas importante, sin embargo, es que nuestro problema puede quedar formalmente divi-
dido en una serie de componentes bien definidos. Con esto dividimos al problema en problemas
mas pequenos. Divide y venceras.
<v a r i a b l e D e C l a s e 1 >
<v a r i a b l e D e C l a s e 2 >
...
<v a r i a b l e D e C l a s e M >
<metodo1>
<metodo2>
38
...
<metodoN>
}
(Ya sabemos que todo esto debe estar en un archivo que se llame como la clase, o sea
<NombreDeClase>.java).
Pusimos todas las variables de clase juntas al inicio, y todos los metodos juntos al final; pero
realmente pueden ir en el orden que se quiera. Sin embargo, en estas practicas sera requisito
que las variables de clase esten todas juntas al inicio.
Las interfaces son muy parecidas a las clases, pero en lugar de class utilizan interface, sus
metodos son vacos (no tienen cuerpo), y no declaran variables de clase:
i n t e r f a c e <N o m b r e D e I n t e r f a z > {
<metodoVaco1 >
<metodoVaco2 >
...
<metodoVacoN>
}
1 public i n t e r f a c e M a t r i z C u a d r a d a {
2
3 public M a t r i z C u a d r a d a suma ( M a t r i z C u a d r a d a m) ;
4
5 public M a t r i z C u a d r a d a r e s t a ( M a t r i z C u a d r a d a m) ;
6
7 public M a t r i z C u a d r a d a m u l t i p l i c a ( M a t r i z C u a d r a d a m) ;
8
9 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) ;
10
11 public double g e t D e t e r m i n a n t e ( ) ;
12
13 }
Un poco mas adelante veremos porque la definicion incluye public, porque hay dos metodos
multiplica, y porque el ultimo metodo se llama getDeterminante y no dameDeterminante.
Lo que tenemos aqu es, como hemos venido diciendo, un contrato. Cualquier clase que
implemente a MatrizCuadrada esta obligado a implementar los metodos suma, resta, multiplica
(los dos), y getDeterminante.
Declararemos nuestra clase Matriz2x2 de la siguiente manera:
39
1 c l a s s M a t r i z 2 x 2 implements M a t r i z C u a d r a d a {
2 }
No sabemos todava declarar metodos ni variables de clase, por lo que la dejaremos as por
el momento. La clase no puede compilar (intentenlo), porque no implementa los metodos que
declara MatrizCuadrada.
Antes de seguir con la clase Matriz2x2, veamos las partes de una clase en Java en general.
Variables de clase
Las variables de clase se declaran de forma casi identica a las variables locales:
class AlgunaClase {
int a ;
}
Pueden utilizarse todos los tipos de Java (referencias incluidas), y son variables que se
comportan exactamente igual que sus equivalentes locales. Lo que distingue a una variable de
clase de una variable local (tecnicamente), es su alcance y su vida.
El alcance de una variable de clase es toda la clase. La variable puede ser vista en casi todos
los metodos de la clase (en un momento veremos en cuales no), y utilizada o modificada dentro
de todos ellos.
La vida de una variable es el tiempo en que la variable existe. En el caso de las variables
locales, viven (o existen) desde el momento en que son declaradas, hasta que el metodo donde
esten termine. Las variables de clase viven desde que el objeto es creado con new hasta que el
objeto deja de existir.
Esto es importante; una variable de clase se crea al mismo tiempo que el objeto al que
pertenece, y sigue siendo la misma variable aunque sean llamados varios metodos por el objeto.
En cambio, una variable local existe dentro del metodo y es distinta para cada llamada del
metodo. Cada vez que sea llamado un metodo, todas las variables locales declaradas dentro de
el son creadas de nuevo. No son las mismas a traves de distintas llamadas al metodo.
Aunque tecnicamente hay pocas diferencias entre una variable local y una variable de clase,
conceptualmente la diferencia es enorme en Orientacion a Objetos. Las variables locales solo
sirven para hacer cuentas, u operaciones en general. Las variables de clase en cambio son
fundamentales; son las que guardan el estado de un objeto, las que mapean el componente del
mundo real al objeto con el que queremos representarlo en nuestro programa.
Vamos a anadirle variables de clase a nuestra clase Matriz2x2. Asumamos que los compo-
nentes de nuestra matriz estan dispuestos de la siguiente manera:
a b
.
c d
40
Entonces nuestra clase necesita cuatro valores en punto flotante para ser representada.
Utilicemos el tipo double, para tener mas precision en nuestras operaciones. Y solo para hacerlo
mas interesante, anadamos otro doble para que represente el determinante de nuestra matriz.
1 c l a s s M a t r i z 2 x 2 implements M a t r i z C u a d r a d a {
2 double a ;
3 double b ;
4 double c ;
5 double d ;
6 double d e t e r m i n a n t e ;
7 }
Podramos declararlas todas en una sola lnea con double a,b,c,d,determinante; pero acostum-
braremos declarar as las variables de clase, una por renglon.
Las variables de clase son unicas para cada objeto. Esto quiere decir que si tenemos dos
objetos de nuestra clase Matriz2x2, digamos m1 y m2, entonces las variables a, b, c, d y determi-
nante del objeto m1 pueden tener valores totalmente distintos a las variables correspondientes
del objeto m2. Los estados de los objetos (en otras palabras, los valores de sus variables de
clase) son independientes entre ellos. Si empezamos a modificar los valores de las variables del
objeto m1, esto no afectara a las variables del objeto m2.
Metodos
En los metodos no pasa como con las variables, que hay locales y de clase. Todas las funciones
(o metodos, o procedimientos) son de clase. No hay metodos locales.
Ya hemos visto varios metodos implementados (main principalmente), y tambien varios
declarados en interfaces. Ahora vamos a explicar como se implementan y por que se declaran
como se declaran.
Como nuestra clase Matrix2x2 implementa a la interfaz MatrizCuadrada, estamos obligados
a implementar sus cinco metodos. Comencemos con la multiplicacion con un escalar.Como ya
llevamos Algebra Superior I, sabemos que
a b xa xb
x = .
c d xc xd
La unica diferencia (y muy importanta) entre declarar un metodo en una interfaz y en una
clase, es que en la interfaz no hay cuerpo para el metodo; en otras palabras es:
41
<t i p o D e R e g r e s o > <nombreDelMetodo > ( < parametro1 >,
<parametro2 >,
...
<parametroN >);
En las interfaces, los metodos nos dicen que hace la interfaz. En las clases, los metodos nos
dicen que hace la clase, y ademas como lo hace.
Cada parametro es de la forma <tipo> <nombre>, donde el tipo es el tipo del parametro, y
el nombre es el nombre que tendra dentro de la funcion.
A veces nuestros metodos no van a hacer ningun calculo, sino que van a realizar acciones; en
esos casos se pone que el tipo de regreso del metodo es void. Las expresiones que consisten de
un objeto llamando a un metodo cuyo tipo de regreso es void no tienen valor. Son expresiones
que no pueden usarse del lado derecho de una asignacion, o para pasarlas como parametros a
una funcion.
Estamos implementando el metodo multiplica de nuestra interfaz MatrizCuadrada, que recibe
un doble (nuestro escalar), y que regresa una MatrizCuadrada. Noten que no regresa un objeto
de la clase Matriz2x2; si lo hiciera, la firma del metodo ya no sera igual a la de la interfaz,
y nuestra clase no compilara (necesitamos implementar todos los metodos declarados en la
interfaz, exactamente como esten en la interfaz):
5 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
6 }
Lo unico que diferencia este metodo del de la interfaz es que tiene un bloque. El bloque
esta vaco, pero ya tiene un bloque.
Varios de ustedes deben haber observado algo raro. Vamos a multiplicar una matriz y un
escalar; entonces por que solo le pasamos un escalar a la funcion, y no le pasamos una matriz
tambien?
La respuesta es que dentro de la funcion multiplica, ya hay una matriz; la que manda llamar
el metodo.
Cuando queramos usar a la funcion multiplica, necesitaremos un objeto de la clase Matriz2x2
para que la llame. Supongamos que ya existe este objeto y que se llama m. Para llamar al
metodo, tendremos que hacer:
m. m u l t i p l i c a ( 2 . 5 ) ;
Entonces, dentro del metodo tendremos el double x (con valor 2.5), y a m, que es pasada
implcitamente porque es el objeto que mando llamar a la funcion. Dentro del metodo, m se va
a llamar this; este, en ingles, lo que hace enfasis en que este mando llamar la funcion.
La fuerte tipificacion de Java que vimos la practica pasada se aplica tambien a los metodos.
El tipo de un metodo esta determinado por el tipo del valor que regresa, y el tipo de cada uno
de sus parametros.
Por lo tanto, lo siguiente es ilegal:
m. m u l t i p l i c a ( true ) ;
42
Sin embargo, todo esto s es legal:
double y = 3 . 7 ;
float x = 23.14;
short s = 5 ;
m. multiplica (y);
m. multiplica (x);
m. multiplica ( s );
m. multiplica (1.3);
m. multiplica (4);
por lo que dijimos la practica anterior de que hay tipos que s caben dentro de otros tipos. Lo
siguiente en cambio es ilegal:
char c = m. m u l t i p l i c a ( 2 . 9 ) ;
El metodo multiplica regresa un valor de tipo referencia a un objeto cuya clase implementa
la interfaz MatrizCuadrada. No podemos asignarselo a una variable de tipo char. Esto s es
correcto:
M a t r i z C u a d r a d a m2 ;
double w = 3 . 8 ;
m2 = m. m u l t i p l i c a ( w ) ;
y de hecho as sera usado casi siempre. En la variable m2 quedara guardada la matriz resultante
de multiplicar m con 3.8 (cuando escribamos el cuerpo del metodo).
En los ejemplos vimos que le podemos pasar tanto literales como variables al metodo. Esto
es importante; las funciones en Java reciben sus parametros por valor. Esto quiere decir que
cuando la funcion es llamada, no importa que haya dentro de los parentesis (una variable, una
literal, cualquier expresion), el valor de lo que haya se copia, y se le asigna al parametro dentro
de la funcion (en nuestro caso, x). Por supuesto, ese valor debe ser de un tipo compatible al del
parametro.
Dentro de la funcion, x se comporta de manera identica a una variable local; pero es ini-
cializada cuando se manda llamar el metodo, con el valor que le pasamos. Lo mismo pasa con
todos los parametros de cualquier funcion.
Supongamos que mandamos llamar a multiplica as:
double x = 1 4 . 7 5 ;
m. m u l t i p l i c a ( x ) ;
5 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
6 x = x / 5;
7 return null ;
8 }
43
La variable x que le pasamos al metodo cuando lo llamamos, es totalmente independiente
del parametro x del metodo. La variable x no se ve afectada cuando le hacemos x = x / 5 al
parametro x dentro del metodo. La variable x seguira valiendo 14.75;
Escribamos el cuerpo del metodo multiplica
5 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
6 double a , b , c , d ;
7
8 a = x this . a ;
9 b = x this . b ;
10 c = x this . c ;
11 d = x this . d ;
12 }
Noten que declaramos cuatro variables locales que tienen el mismo tipo y se llaman igual
que variables de clase existentes. No importa, porque usamos this para distinguir las variables
de clase de las locales.
Con esto, ya tenemos los cuatro componentes necesarios (a, b, c, d) para que hagamos una
nueva matriz. El unico problema es que no sabemos hacer matrices.
Para hacer una matriz, vamos a necesitar un constructor.
Constructores
Conceptualmente, los constructores sirven para que definamos el estado cero de nuestros
objetos; el estado que tendran al ser creados. Para motivos practicos, los constructores son
sencillamente metodos que no tienen tipo de regreso, y que solo podemos llamar a traves del
operador new.
Como los metodos, pueden recibir parametros (por valor tambien), y puede hacerse dentro
de ellos cualquier cosa que podamos hacer dentro de un metodo, excepto regresar un valor.
Un constructor obvio para nuestras matrices sera as:
(En tu archivo Matriz2x2.java, y en todas las clases que escribas, pon siempre los con-
structores antes que los metodos).
Observen que el constructor recibe parametros que se llaman igual y que tienen el mismo
tipo de las variables de clase. No importa, porque usamos this para tener acceso a las variables
de clase, y as ya no hay confusion. En los constructores, this hace referencia al objeto que
44
esta siendo construido.2
En este caso el constructor inicializa las variables de clase con los
valores
que recibe, y
a b
calcula el determinante de la matriz (que bueno que recordamos que det = ad bc).
c d
Ahora ya podemos terminar nuestro metodo multiplica:
15 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
16 double a , b , c , d ;
17
18 a = x this . a ;
19 b = x this . b ;
20 c = x this . c ;
21 d = x this . d ;
22
23 MatrizCuadrada r e s u l t a d o ;
24 r e s u l t a d o = new M a t r i z 2 x 2 ( a , b , c , d ) ;
25 return r e s u l t a d o ;
26 }
MatrizCuadrada r e s u l t a d o ;
r e s u l t a d o = new M a t r i z 2 x 2 ( a , b , c , d ) ;
Reloj r elo j ;
r e l o j = new C l a s e R e l o j ( ) ;
Polimorfismo
Tenemos que implementar el otro metodo multiplica (esta en nuestra interfaz). La multipli-
cacion de matrices cuadradas da como resultado una matriz cuadrada; en particular, multiplicar
una matriz de dos por dos nos devuelve una matriz de dos por dos:
a b x y ax + bz cx + dz
= .
c d z w ay + bw cy + dw
El metodo va a recibir una matriz como parametro (la otra matriz sera la que llame la
funcion), y va a regresar otra matriz (as esta declarado).
2
Con nuestra clase Matriz2x2 hemos estado usando mucho this para no confundirnos con los nombres de las
variables. Pero cuando no haya conflicto entre variables de clase y variables locales, podemos usar los nombres
de las variables de clase sin el this y el compilador de Java sabra que hacemos referencia a las variables de
clase.
45
Ahora, el orden importa en la multiplicacion de matrices (no es conmutativa), y alguna de
las matrices que vayamos a multiplicar tendra que llamar al metodo, as que debemos definir
cual sera; si el operando derecho o el izquierdo.
Como va a ser usado as el metodo:
M a t r i z C u a d r a d a m1 , m2 ;
m1 = new M a t r i z 2 x 2 ( 1 , 2 , 3 , 4 ) ;
m2 = new M a t r i z 2 x 2 ( 5 , 6 , 7 , 8 ) ;
M a t r i z C u a d r a d a mult ;
mult = m1 . m u l t i p l i c a ( m2 ) ; // M u l t i p l i c a m o s m1 por m2 .
vamos a definir que la matriz que llame la funcion sea el operando izquierdo.
Aqu algunos estaran exclamando (y desde la declaracion de la interfaz de hecho): hey,
ya tenamos un metodo que se llama multiplica!. Es verdad; pero en Java podemos repetir el
nombre de un metodo, siempre y cuando el numero o tipo de los parametros no sea el mismo (si
no, vean en la documentacion generada de la clase Consola y busquen el metodo imprime. . . mejor
dicho los metodos imprime). A esta caracterstica se le llama polimorfismo.
Nuestro (segundo) metodo multiplica quedara as:
30 public M a t r i z C u a d r a d a m u l t i p l i c a ( M a t r i z C u a d r a d a m) {
31 double a , b , c , d ;
32 M a t r i z 2 x 2 m2x2 = ( M a t r i z 2 x 2 )m;
33
39 MatrizCuadrada r e s u l t a d o ;
40 r e s u l t a d o = new M a t r i z 2 x 2 ( a , b , c , d ) ;
41 return r e s u l t a d o ;
42 }
M a t r i z 2 x 2 m2x2 = ( M a t r i z 2 x 2 )m;
Por que estamos haciendo eso? El metodo recibe una MatrizCuadrada (tiene que ser as porque
as se declaro en la interfaz). Sin embargo, una MatrizCuadrada es un ente abstracto. Represen-
ta a todas las matrices cuadradas (de dos por dos, de tres por tres, de n por n). Nosotros
necesitamos una matriz de dos por dos (porque nuestra multiplicacion en matrices de dos por
dos solo esta definida para matrices de dos por dos). Eso que hicimos (el (Matriz2x2)) se llama
casting. Es sencillamente ver a nuestro objeto m, como objeto de la clase Matriz2x2. Es como
ponerle una mascara.
Como el objeto m2x2 (que es identico al objeto m, pero con mascara de Matriz2x2), ya
podemos tener acceso a sus variables de clase (que son las que nos importan).
46
Noten que usamos al operador . (punto) para tener acceso a las variables de clase del objeto
m2x2, que es el parametro que recibe el metodo, con mascara de Matriz2x2.
Una nota respecto al parametro m. Como todos los parametros, es una copia del valor
que le pasaron. Pero los valores de las referencias son direcciones de memoria. As que aunque
esta direccion de memoria sea una copia de la que le pasaron, la direccion en s es la misma.
Hace referencia al mismo objeto (a la misma matriz) que se le paso al metodo; por lo tanto, si
modificamos la matriz dentro de la funcion, s se afecta al objeto con el que se llamo la funcion.
Hay que tener eso en cuenta.
El polimorfismo tambien se aplica a los constructores. Podemos tener tantos constructores
como queramos, siempre que reciban un numero de parametros distintos o que sus tipos sean
diferentes. Por ejemplo, podemos tener un constructor para nuestra matriz cero:
15 public M a t r i z 2 x 2 ( ) {
16 this . a = 0;
17 this . b = 0;
18 this . c = 0;
19 this . d = 0;
20 this . determinante = 0;
21 }
Si solo vamos a tener un constructor, y este solo va a poner en cero las variables (o en null,
o en false), podemos no hacerlo. Java proporciona un constructor por omision, que no recibe
parametros, y que inicializa todas las variables de clase como cero, si son numericas, como false,
si son booleanas, como \u0000 si son caracteres, y como null si son referencias. Sin embargo,
este constructor solo esta disponible si no hay ningun otro constructor en la clase.
Ademas, en estas practicas siempre haremos constructores para nuestras clases, aunque
hagan lo mismo que el constructor por omision, es una buena practica de programacion.
Con todos estos conocimientos, hacer el resto de las operaciones para matrices de dos por
dos es trivial.
Actividad 4.2 Haz los metodos suma, resta e inversa para tu clase Matriz2x2. Las primeras
dos son triviales (puedes basarte en el metodo para multiplicar matrices).
Para obtener la matriz inversa, primero comprueba que exista, o sea que el determinante
sea distinto de cero (utiliza un if ), si no, una division por cero hara que tu programa no
funcione.
1 public double g e t D e t e r m i n a n t e ( ) {
2 return d e t e r m i n a n t e ;
3 }
47
Porque llamarlo getDeterminante y no dameDeterminante? Porque en Java existe la conven-
cion de que si tenemos una variable llamada x, entonces el metodo para obtenerla se llamara
getX y el metodo para cambiarle el valor sera setX (recuerdan el metodo setHora?). Solo se
esta siguiendo la convencion de Java.
Clases de uso
Para probar nuestra interfaz MatrizCuadrada y nuestra clase Matriz2x2 podramos sencilla-
mente colocarle un metodo main a esta ultima, como los que hemos usado. Empero, aunque
esto es util para probar inicialmente una clase, siempre hay que probarla desde otra clase.
La razon es que siempre que hagamos una clase, debemos hacerla pensando que es un
componente mas de algun problema (y si tenemos suerte, de varios problemas). Para que un
componente sea realmente util, debe poder incluirse en cualquier proyecto sin que haya que
hacerle modificaciones, y si solo probamos una clase dentro de ella misma, lo mas seguro es que
esta modularidad no sea bien comprobada.
Para probar nuestras clases, y para hacer nuestros programas, siempre utilizaremos clases de
uso. Las clases de uso nunca tendran variables de clase ni constructores. Muchas veces incluso
consistiran solo de una funcion main, como la clase UsoReloj de las primeras dos practicas (para
distinguirlas de nuestras clases normales, a nuestras clases de uso les vamos a anadir el prefijo
Uso).
Nuestra clase UsoMatrizCuadrada quedara as:
1 c l a s s U s o M a t r i z C u a d r ad a {
2 public s t a t i c void main ( S t r i n g [ ] a r g s ) {
3 M a t r i z C u a d r a d a m1 , m2 ;
4 m1 = new M a t r i z 2 x 2 ( 1 , 2 , 3 , 4 ) ;
5 m2 = new M a t r i z 2 x 2 ( 5 , 6 , 7 , 8 ) ;
6
7 M a t r i z C u a d r a d a mult ;
8 mult = m1 . m u l t i p l i c a ( m2 ) ;
9 }
10 }
Pueden compilarla y ejecutarla (si ya acabaron la clase Matriz2x2. Parecera que no hace
nada, pero s hace; lo que pasa es que no hemos creado una interfaz para mostrar nuestras
matrices en la consola (y de hecho, ni siquiera declaramos una consola en nuestra clase de uso).
Para mostrar nuestras matrices en la consola, podramos crear el metodo imprime en la clase
Matriz2x2:
48
Tendramos por supuesto que anadir import icc1 1. interfaz .Consola; a la clase Matriz2x2 para
que compilara. Sin embargo, esto es un error.
Si metemos el metodo imprime en nuestra clase Matriz2x2, entonces ya no sera una clase
independiente; dependera de la clase Consola. A cualquier lugar que llevaramos nuestra clase
Matriz2x2, tendramos que llevar tambien a la clase Consola.
A veces no podra evitarse eso; una clase dependera de otras muchas. Pero en este caso en
particular, podemos hacer independiente a la clase Matriz2x2 de la clase Consola. Para ello, en
lugar de pasarle toda una consola a la clase Matriz2x2 para que se imprima, mejor hagamos que
la clase Matriz2x2 pueda ser representada en una cadena para que la imprimamos con la clase
Consola. Esto lo logramos implementando el metodo toString:
50 public S t r i n g t o S t r i n g ( ) {
51 return t h i s . a + "\t" + t h i s . b + "\n" +
52 t h i s . c + "\t" + t h i s . d ;
53 }
El public es obligatorio. Por que debe llamarse exactamente toString lo explicaremos cuando
veamos herencia, y en un momento veremos por que estamos sumando dobles con cadenas. Por
ahora, con este metodo en la clase, podemos imprimir nuestras matrices haciendo sencillamente:
Consola c ;
c = new C o n s o l a ( " Matrices" ) ;
M a t r i z C u a d r a d a m;
m = new M a t r i z 2 x 2 ( 1 , 2 , 3 , 4 ) ;
c . i m p r i m e l n (m ) ; // Podemos p a s a r a l a m a t r i z d i r e c t a m e n t e .
1 import i c c 1 1 . i n t e r f a z . C o n s o l a ;
2
3 c l a s s U s o M a t r i z C u a d r ad a {
4 public s t a t i c void main ( S t r i n g [ ] a r g s ) {
5 Consola c ;
6 c = new C o n s o l a ( "Matrices" ) ;
7
8 M a t r i z C u a d r a d a m1 , m2 ;
9 m1 = new M a t r i z 2 x 2 ( 1 , 2 , 3 , 4 ) ;
10 m2 = new M a t r i z 2 x 2 ( 5 , 6 , 7 , 8 ) ;
11
12 c . i m p r i m e l n ( m1 ) ;
13 c . i m p r i m e l n ( m2 ) ;
14
15 M a t r i z C u a d r a d a mult ;
16 mult = m1 . m u l t i p l i c a ( m2 ) ;
17
18 c . i m p r i m e l n ( mult ) ;
49
19 }
20 }
Actividad 4.3 Basandote en las practicas anteriores, crea tu propio archivo build.xml.
Pon el codigo fuente en un directorio llamado src.
No estamos usando paquetes, as que la tarea java sera as:
Compila y ejecuta usando tu build.xml. Si quieres compilar dentro de XEmacs, utiliza uno
de los archivos pjr.el de las practicas anteriores, y ponlo junto al archivo build.xml.
Modificadores de acceso
Que pasa si hacemos lo siguiente en el metodo main de nuestra clase de uso?
8 M a t r i z C u a d r a d a m;
9 m = new M a t r i z 2 x 2 ( 1 , 2 , 3 , 4 ) ;
10
11 m. a = 1 0 ;
12 c . i m p r i m e l n (m. g e t D e t e r m i n a n t e ( ) ) ;
13 }
14 }
El valor que se imprima en la consola sera -2, aunque el determinante de nuestra matriz
(despues de la modificacion que le hicimos en la lnea 11) sea 34, porque el determinante es
calculado cuando el objeto es creado.
Lo que pasa aqu es que el estado de nuestro objeto queda inconsistente. Y eso pasa porque
modificamos directamente una variable de clase de nuestro objeto m al hacer
m. a = 1 0 ;
Podramos hacer la solemne promesa de nunca modificar directamente una variable de clase;
pero eso no basta. Tenemos que garantizar que eso no ocurra. No debe ocurrir nunca.
A esto se le llama el acceso de una variable de clase (que no es lo mismo que el alcance).
Una variable puede tener acceso publico, de paquete, protegido y privado. Las variables locales
no tienen modificadores de acceso ya que solo existen dentro de un metodo.
50
Cuando una variable de clase tiene acceso publico, en cualquier metodo de cualquier clase
puede ser vista y modificada. Cuando tiene acceso privado, solo dentro de los metodos y con-
structores de la clase a la que pertenece puede ser vista y modificada. Los accesos de paquete
y protegidos los veremos cuando veamos paquetes y herencia, respectivamente.
Restringir el acceso a una variable de clase nos permite controlar como y cuando sera mod-
ificado el estado de un objeto. Con esto garantizamos la consistencia del estado, pero tambien
nos permite cambiar las variables privadas de un objeto, y que este siga siendo valido para
otras clases que lo usen; como nunca ven a las variables privadas, entonces no importa que las
quitemos, les cambiemos el nombre o tipo o le anadamos alguna. El acceso es el principal factor
del encapsulamiento de datos.
Por todo esto, siempre es bueno tener las variables de clase con un acceso privado. Vamos
a cambiarle el acceso a las variables de clase de Matriz2x2. Nuestras variables no tenan ningun
acceso especificado, por lo que Java les asigna por omision el acceso de paquete; para motivos
practicos y con lo que hemos visto, el acceso es publico.
Para cambiarselo a privado, solo tenemos que hacer
3 c l a s s M a t r i z 2 x 2 implements M a t r i z C u a d r a d a {
4 private double a ;
5 private double b ;
6 private double c ;
7 private double d ;
8 private double d e t e r m i n a n t e ;
Los modificadores para publico, protegido y de paquete son public, protected y package
respectivamente. Aunque no es necesario poner package para que una variable de clase tenga
acceso de paquete, en estas practicas sera requisito que el acceso de los metodos y las variables
sea explcito.
Los metodos tambien tienen acceso, y tambien es de paquete por omision. Hemos usado
public porque queramos tener completa nuestra interfaz antes de implementar nuestra clase.
El acceso en los metodos tiene el mismo significado que en las variables de clase; si un metodo
es privado, no podemos usarlo mas que dentro de los metodos de la clase, y si un metodo es
publico, se puede usar en cualquier metodo de cualquier clase.
Los metodos privados son generalmente funciones auxiliares de los metodos publicos y de
paquete, y al hacerlos privados podemos agregar mas o quitarlos cuando queramos, y las clases
que usen a nuestra clase no se veran afectadas. El encapsulamiento de datos facilita la modu-
larizacion de codigo.
Tambien sera requisito que todos los metodos tengan el acceso explcito, aunque sea de
paquete.
Los constructores tambien tienen acceso, y como en muchas otras cosas funcionan de manera
identica que para metodos. Sin embargo, si tenemos un constructor privado, significa que no
podemos construir objetos de esa clase fuera de la misma clase; no podramos construir objetos
de la clase en una clase de uso, por ejemplo. Veremos en un momento por que querramos hacer
eso.
No solo las variables de clase, los metodos y constructores tienen acceso. Las mismas clases
tienen acceso, y tambien por omision es de paquete. Una clase privada parece no tener sentido;
y no obstante lo tiene. Veremos mas adelante cuando podemos hacer privado el acceso a una
51
clase; por ahora, cambiemos el acceso de nuestra clase a publico:
3 public c l a s s M a t r i z 2 x 2 implements M a t r i z C u a d r a d a {
4 ...
Ya que nuestras variables de clase son privadas, como hacemos para poder verlas o mod-
ificarlas fuera de la clase? Como dijimos arriba, Java tiene la convencion de utilizar metodos
set/get para esto. Los metodos set cambiaran el valor de una variable, y los get lo obtendran.
Por ejemplo, para la variable a:
79 ...
Puede parecer engorroso; pero la justificacion se hace evidente en esta ultima funcion. Cada
vez que modifiquemos un componente de la matriz, actualizaremos automaticamente el valor
del determinante. Con esto dejamos bien definido como ver y como modificar el estado de un
objeto, y garantizamos que sea consistente siempre.
El acceso es parte fundamental del diseno de una clase. Si hacemos un metodo o variable
de clase publico, habra que cargar con el siempre, ya que si lo quitamos o lo hacemos privado,
es posible que existan aplicaciones que lo utilizaran, y entonces estas ya no serviran.
Actividad 4.4 Implementa los metodos getA, getB, getC, getD y setA, setB, setC y setD.
Puedes observar que no habra metodo setDeterminante, ya que el determinante depende de
las componentes de la matriz.
Otros modificadores
Hay dos modificadores mas para las variables y metodos de Java. El primero de ellos es
final . Una variable final solo puede ser inicializada una vez; y generalmente sera al momento de
ser declarada. Una vez inicializada, ya no cambia de valor nunca. Su valor es final. Las variables
finales pueden ser de clase o bien locales.
Cuando tengamos una literal que utilizamos en muchas partes del codigo, y que dentro de
la ejecucion del programa nunca cambia (por ejemplo, el IVA en un programa de contabilidad),
siempre es mejor declarar una variable final para definirlo, y as si por alguna razon nos vuelven
a subir el IVA,3 en lugar de cambiar todos los lugares donde aparezca la literal, solo cambiamos
el valor que le asignamos a la variable final. Tambien facilita la lectura del codigo.
3
O mas raro; que lo bajen.
52
Otros lenguajes de programacion tienen constantes. Las variables finales de Java no son
constantes exactamente; pero pueden emular a las constantes de otros lenguajes. Hay metodos
finales tambien, pero su significado lo veremos cuando veamos herencia.
El otro modificador es static. Una variable de clase estatica es la misma para todos los
objetos de una clase. No es como las variables de clase normales, que cada objeto de la clase
tiene su propia copia; las variables estaticas son compartidas por todos los objetos de la clase,
y si un objeto la modifica, todos los demas objetos son afectados tambien. No hay variables
locales estaticas (no tendran sentido).
Tambien hay metodos estaticos. Un metodo estatico no puede tener acceso a las variables
de clase no estaticas; no puede hacer this. a ninguna variable. El metodo main por ejemplo
siempre es estatico.
Para tener acceso a una variable estatica o a un metodo estatico se puede utilizar cualquier
objeto de la clase, o la clase misma. Si la clase Matriz2x2 tuviera una variable estatica var
por ejemplo, podramos hacer Matriz2x2.var para tener acceso a ella (si var fuera publica, por
supuesto).
Hay clases cuyos objetos necesitan pasar por un proceso de construccion mas estricto que las
clases normales. Para este tipo de clases, que suelen llamarse clases fabrica (o factory classes
en ingles), lo que se hace es que tienen constructores privados, y entonces se usan metodos
estaticos para que creen los objetos de la clase usando las restricciones que sean necesarias.
Si queremos una variable privada, que ademas sea estatica, y ademas final, hay que hacer
4.3.2. Cadenas
La practica anterior vimos tipos basicos, pero varios de ustedes han de haber observado que
constantemente mencionabamos cadenas sin nunca definirlas. Por ejemplo, al constructor de
la clase Consola le pasamos una cadena:4
Consola c ;
c = new C o n s o l a ( "Ttulo de mi consola" ) ;
Las cadenas (strings en ingles) son objetos de Java; pero son los unicos objetos que tienen
literales distintas de null y definido el operador +. Ademas, son los unicos objetos que podemos
crear sin utilizar new. Las cadenas son consideradas por todo esto un tipo basico; pero no por
ello dejan de ser objetos, con todos los atributos y propiedades de los objetos.
Las literales de las cadenas las hemos usado a lo largo de estas practicas; son todos los
caracteres que estan entre comillas:
4
Observa que estamos utilizando t para representar los espacios dentro de las cadenas.
53
"hola mundo" ;
"adios..." ;
"En un lugar de la Mancha de cuyo nombre..." ;
Para declarar una variable de tipo cadena, declaramos un objeto de la clase String:
String a ;
pero no es necesario instanciarlo con new; el asignarle una literal de cadena basta. Las dos
instanciaciones de abajo son equivalentes
a = "hola mundo" ;
a = new S t r i n g ( "hola mundo" ) ;
Esta es una comodidad que Java otorga; pero en el fondo las dos instanciaciones son exac-
tamente lo mismo (en ambas la JVM asigna memoria para el objeto), aunque en la primera no
tengamos que escribir el new String ( ).
Ademas de tener literales e instanciacion implcita, los cadenas son el unico objeto en Java
que tiene definido el operador +, y sirve para concatenar cadenas:
S t r i n g h , m, a ;
h = "hola " ;
m = "mundo" ;
a = h + m;
En este ejemplo, la variable a termina valiendo "holat mundo". Lo bonito del caso es que no
solo podemos concatenar cadenas; podemos concatenar cadenas con cualquier tipo de Java. Si
hacemos
int edad = 1 5 ;
String a ;
a = "Edad : " + 1 5 ;
la cadena resultante es "Edad:t 15". Y funciona para enteros, flotantes, booleanos caracteres e
incluso referencias. Por eso nuestro metodo toString poda sumar dobles con cadenas; real-
mente estaba concatenando cadenas.
Por dentro, una cadena es una secuencia de caracteres uno detras del otro. La cadena a que
vimos arriba tiene esta forma por dentro:
h o l a t m u n d o
0 1 2 3 4 5 6 7 8 9
Las cadenas son objetos de solo lectura. Esto quiere decir que podemos tener acceso al
caracter m de la cadena "holat mundo", pero no podemos cambiar la m por t para que la
cadena sea "holat tundo".
54
Sin embargo, podemos crear un nuevo objeto cadena que diga "holat tundo" a partir de
"holat mundo", y asignarlo de nuevo a la variable:
String a ;
a = "hola mundo" ;
a = a . s u b s t r i n g ( 0 , 5 ) + "t" + a . s u b s t r i n g ( 6 , 1 0 ) ;
Ahora a representa a "holat tundo"; pero hay que entender que a a se le asigno un nuevo
objeto (construido a partir del original), no que se modifico el anterior (y ademas, utiliza tres
objetos temporales para construir el nuevo objeto; el primero a. substring (0,5) , el segundo "t",
y el tercero a. substring (6,10) ).
Las cadenas son parte fundamental de la aritmetica de Java; ofrecen muchas funciones y
utilidades que en muchos otros lenguajes de programacion es necesario implementar a pie.
Regresando a la cadena "holat tundo", su longitud es 10 (tiene 10 caracteres), y siempre
puede saberse la longitud de una cadena haciendo
int l o n g i t u d = a . l e n g t h ( ) ;
String b ;
b = a . substring (5 ,10);
De nuevo; esto regresa un nuevo objeto sin modificar el anterior. La cadena a seguira siendo
"holat tundo".
Hay que notar que substring regresa todos los caracteres entre el ndice izquierdo y el ndice
derecho menos uno. Esto es para que la longitud de la subcadena sea el ndice derecho menos
el ndice izquierdo.
Puede que solo deseemos un caracter, entonces podemos usar
char c = a . c h a r A t ( 5 ) ;
Y esto regresa el caracter (tipo char) m. Si tratamos de leer un caracter en una posi-
cion invalida (menor que cero o mayor o igual que la longitud de la cadena), el programa
terminara con un error.
Las variables de la clase String son referencias, como todos los objetos. Eso quiere decir que
cuando hagamos
S t r i n g a = "hola mundo" ;
S t r i n g b = "hola mundo" ;
boolean c = ( a == b ) ;
el boolean c tendra como valor false. Las variables a y b son distintas; apuntan a direcciones
distintas en la memoria. Que en esas direcciones haya cadenas identicas no le importa a Java; lo
55
importante es que son dos referencias distintas. Si queremos comparar dos cadenas lexicografi-
camente, debemos usar el metodo equals de la clase String:
c = a . equals ( b ) ;
La clase String de Java ofrece muchas otras funciones utiles, que iremos aprendiendo a usar
a lo largo de estas practicas.
Actividad 4.5 Con la ayuda del ayudante, consulta la documentacion de la clase String.
Necesitaras estarla consultando para los ejercicios de esta practica.
String a ;
a = "hola mundo" ;
a = a . s u b s t r i n g ( 0 , 5 ) + "t" + a . s u b s t r i n g ( 6 , 1 0 ) ;
estabamos usando tres objetos temporales (las tres subcadenas que forman "hola t tundo").
Que pasa con esas subcadenas? Cuando deja de existir un objeto? Si new hace que la
JVM asigne memoria, cuando es liberada esta memoria?
La respuesta corta es no hay que preocuparse por ello. La larga es as: imagnense que cada
objeto tiene un contador interno que se llama contador de referencias. Cuando un objeto es
instanciado con new (aunque sea implcitamente, como las cadenas), ese contador se pone en
cero. En cuanto una referencia comienza a apuntar hacia el objeto, el contador aumenta en uno.
Por cada referencia distinta que empiece a apuntar hacia el objeto, el contador se aumentara en
uno. Por ejemplo
String a ;
a = "cadena" ;
String b , c , d , e ;
56
b = c = d = e = a;
En este ejemplo, al objeto de la clase cadena que instanciamos como "cadena", su contador
de referencias vale 5, ya que a, b, c, d y e, apuntan hacia el.
Cada vez que una referencia deje de apuntar al objeto, el contador disminuye en 1. Si ahora
hacemos
a = b = c = d = e = null ;
con eso las cinco referencias dejan de apuntar al objeto, y entonces su contador de referencias
llega a cero.
Cuando el contador de referencias llega a cero, el objeto es marcado como removible. Un
programa especial que corre dentro de la JVM (el recolector de basura) se encarga cada cierto
tiempo de buscar objetos marcados como removibles, y regresar al sistema la memoria que
utilizaban.
Debe quedar claro que este es un ejemplo simplificado de como ocurre realmente la recolec-
cion de basura; en el fondo es un proceso mucho mas complejo y el algoritmo para marcar un
objeto como removible es mucho mas inteligente. Pero la idea central es esa; si un objeto ya no
es utilizado, que el recolector se lo lleve.
La respuesta corta es suficiente sin embargo. La idea del recolector de basura es que los
programadores (ustedes) no se preocupen de liberar la memoria que asignan a sus objetos. En
otros lenguajes eso tiene mucha importancia; mas por suerte Java lo hace por nosotros.
Puedes ver que la documentacion generada es bastante explcita por s misma. Nos dice el
tipo de regreso de los metodos, as como sus parametros, y que constructores tiene la clase.
Con esa informacion cualquiera que sepa lo que nosotros sabemos puede empezar a trabajar
con una clase.
Sin embargo, podemos hacer todava mas para que la documentacion sea aun mas explcita.
Podemos utilizar comentarios especiales para que JavaDoc haga la documentacion mas profun-
da.
Para que JavaDoc reconozca que un comentario va dirigido hacia el, necesitamos comenzarlos
con / en lugar de solo /. Despues de eso, solo necesitamos poner el comentario inmediata-
57
mente antes de la declaracion de un metodo, un constructor o de una clase para que JavaDoc
lo reconozca. Modifica as la declaracion de la clase Matriz2x2
3 /
4 C l a s e para r e p r e s e n t a r m a t r i c e s de dos r e n g l o n e s por dos columnas ,
5 que implementa l a i n t e r f a z MatrizCuadrada .
6 /
7 public c l a s s M a t r i z 2 x 2 implements M a t r i z C u a d r a d a {
8 ...
y modifica as la declaracion del metodo multiplica (el que multiplica por escalares):
15 /
16 C a l c u l a e l r e s u l t a d o de m u l t i p l i c a r l a m a t r i z por un e s c a l a r .
17 /
18 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
19 ...
Vuelve a generar la documentacion de la clase y consultala para que veas los resultados.
Para metodos, podemos mejorar todava mas la documentacion utilizando unas etiquetas
especiales. Vuelve a modificar el comentario del metodo multiplica, pero ahora as:
15 /
16 C a l c u l a e l r e s u l t a d o de m u l t i p l i c a r l a m a t r i z por un e s c a l a r .
17 @param x e l e s c a l a r por e l que s e m u l t i p l i c a r a l a m a t r i z .
18 @return l a m a t r i z r e s u l t a n t e de m u l t i p l i c a r e s t a m a t r i z por
19 el escalar x .
20 /
21 public M a t r i z C u a d r a d a m u l t i p l i c a ( double x ) {
22 ...
4.4. Ejercicios
Entre las aplicaciones mas utilizadas en el mundo de la computacion estan las Bases de
Datos. A partir de esta practica, iremos recolectando todo lo que aprendamos para construir
poco a poco una Bases de Datos para una agenda.
Una Base de Datos tiene registros, que a su vez estan compuestos de campos. Los campos
de nuestra Base de Datos para una agenda seran nombre, direccion y telefono.
58
Por ahora, empezaremos con una Base de Datos ya llena, lo que quiere decir que los datos
ya estan dados y que no necesitaras introducirlos tu mismo.
Aunque hay muchas formas de implementar Bases de Datos, una de las mas comunes es
utilizar una tabla; los renglones son los registros, y las columnas campos, como en
Para representar a todos los datos, utilizaremos una sola variable estatica de tipo cadena,
llamada tabla. Los registros (o renglones) son de tamano fijo, as que si queremos el primer
registro solo necesitamos hacer
t a b l a . s u b s t r i n g ( 0 , TAM REGISTRO ) ;
Donde TAM REGISTRO es otra variable, estatica y final, que tiene el tamano del registro. Si
necesitamos el segundo registro, tenemos que hacer
Para obtener los campos se necesita algo similar usando los tamanos de cada campo. Nota
que estamos usando TAM REGISTRO solo, sin hacer this.TAM REGISTRO o BaseDeDatosAgen-
da.TAM REGISTRO. Esto es porque estamos dentro de la clase BaseDeDatosAgenda; cuando
usamos TAM REGISTRO el compilador asume que hablamos de las variables de la clase donde
estamos.
Por ahora, asumiremos que todos los campos son unicos; o sea que no se repiten nombres,
ni direcciones ni telefonos.
1. Por ahora, nuestra Base de Datos de agenda sera solo de lectura, ya que no insertaremos
nuevos registros ni borraremos ninguno.
Sin embargo s leeremos datos, y de hecho podremos hacer busquedas.
Abre el archivo BaseDeDatosAgenda.java donde esta el esqueleto de lo que hemos es-
tado discutiendo. Por ahora solo tiene dos funciones (vacas); dameRegistroPorNombre y
59
dameRegistroPorTelefono. La primera recibe una cadena con el nombre a buscar, y la se-
gunda recibe un entero con el telefono. Ambas regresan una cadena que es el registro que
caza la busqueda, o null si el nombre o telefono no estan en la Base de Datos.
Por ejemplo, si hacemos
BaseDeDatosAgenda bdda ;
S t r i n g busqueda ;
Y lo mismo si hacemos
busqueda = bdda . d a m e R e g i s t r o P o r T e l e f o n o ( 5 4 4 7 1 4 9 9 ) ;
4.5. Preguntas
1. Que otras funciones debe implementar una Base de Datos de agenda? Explica.
2. Podramos implementarlas con lo que sabemos hasta ahora? Justifica.
3. Que crees que necesitaramos? Explica.
5
S; la documentacion esta en ingles. Vayanse acostumbrando.
60
Practica 5
Estructuras de control y listas
5.1. Meta
Que el alumno aprenda a utilizar condicionales, iteraciones y listas.
5.2. Objetivos
Al finalizar la practica el alumno sera capaz de:
utilizar condicionales (switch, if ),
utilizar iteraciones (while, do ... while, for),
utilizar listas, y
comprender que son los paquetes en Java.
5.3. Desarrollo
Un metodo no es solo una serie de instrucciones ejecutandose una por una. Un metodo tiene
que tomar decisiones a partir de los parametros que reciba. Hemos utilizado el if para hacer
esto; pero no lo hemos definido formalmente.
Ademas de tomar decisiones, un metodo puede necesitar repetir una tarea un determinado
numero de veces; o mientras cierta condicion se cumpla.
En esta practica, aprenderemos como nuestros metodos pueden tomar decisiones (varias
decisiones a la vez, de hecho), y como repetir tareas, utilizando condicionales e iteraciones
(tambien conocidas como ciclos).
Con estos nuevos conocimientos podremos comenzar a utilizar una de las estructuras mas
poderosas que hay en computacion (independientemente del lenguaje): las listas simplemente
ligadas.
61
5.3.1. Condicionales
En la practica anterior, cuando una de nuestras busquedas fallaba, el metodo deba regresar
null.
Muchas funciones (y muchas mas que escribiremos) regresaran un valor especial cuando
algo no funcione exactamente como esperabamos. Hay que saber cuando son regresados estos
valores especiales y como tratarlos.
Ese es un ejemplo particular. En general, un metodo debe ser capaz de tomar decisiones
de acuerdo a los parametros que reciba. Para tomar decisiones, Java ofrece dos condicionales:
switch e if .
La condicional switch
La condicional switch nos sirve para tomar multiples decisiones a partir de una expresion
de tipo int, short, byte o char.
Supongamos que tenemos una clase Prueba con las siguientes variables de clase:
Con esto podemos clasificar de manera legible los distintos estados civiles. Y podemos usar
un switch para tomar decisiones (si asumimos que el metodo dameEstadoCivil de alguna manera
obtiene el estado civil):
Consola c ;
c = new C o n s o l a ( ) ;
Prueba p ;
p = new Prueba ( ) ;
int e s t a d o C i v i l = p . d a m e E s t a d o C i v i l ( ) ;
switch ( e s t a d o C i v i l ) {
case Prueba . SOLTERO:
c . i m p r i m e l n ( "Este s es listo." ) ;
break ;
case Prueba .CASADO:
c . i m p r i m e l n ( "A este ya lo agarraron." ) ;
break ;
case Prueba . DIVORCIADO :
c . i m p r i m e l n ( "Este recapacito." ) ;
break ;
case Prueba . VIUDO :
c . i m p r i m e l n ( "Este tomo decisiones extremas." ) ;
62
break ;
default :
c . i m p r i m e l n ( "Este no quiere decir." ) ;
}
La condicional switch solo recibe expresiones de alguno de los tipos que dijimos arriba; no
funciona con expresiones de punto flotante, booleanas o siquiera de tipo long.
Una vez que recibe la expresion, calcula su valor y comprueba si algun case caza con ese
valor. Si caza, se ejecuta el codigo a partir de los dos puntos, y hasta el final del switch. Esto
es importante; se ejecuta desde el case que caza con el valor recibido, y se siguen ejecutando
todos los case que le siguen. Para evitar que se sigan ejecutando todos los case, ponemos el
break. El break (como su nombre indica) rompe la ejecucion del bloque donde estemos.
Si solo queremos que se ejecute uno de los case, siempre hay que poner break; pero a veces
querremos que se ejecute mas de un case.
Si ningun case caza con el valor recibido, se ejecuta el default. No es obligatorio que exista
un default, pero se recomienda para que absolutamente todos los casos queden cubiertos. Si no
hay default, sencillamente el programa continua con la ejecucion del enunciado que le siga al
switch.
En este ejemplo, usamos variables finales para etiquetar los case. Es el unico tipo de variables
que podemos usar: un case solo puede ser etiquetado por una variable final o una literal (de
los tipos que mencionamos arriba).
La condicional switch nos permite tomar decisiones con varias opciones. Muchas veces, sin
embargo, vamos a necesitar decidir solo entre s y no. Para eso esta el if .
String reg ;
r e g = bdd . dameRegistroPorNombre ( "Pedrito" ) ;
i f ( r e g == null ) {
c . i m p r i m e l n ( "No existe Pedrito en la Base de Datos." ) ;
} else {
c . i m p r i m e l n ( "El registro es:\n"+r e g ) ;
}
La condicional if recibe una expresion booleana. No recibe ningun otro tipo de expresion;
ni enteros ni flotantes ni caracteres. Solo recibe booleanos. Si el valor de la expresion que recibe
es true, se ejecuta el bloque. Si es false, se ejecuta el bloque del else.
Un if puede existir sin else, por ejemplo (estamos asumiendo ahora otro metodo que regresa
una calificacion):
float c a l i f i c a c i o n = p . dameCalificacion ( ) ;
c . i m p r i m e l n ( "Tu calificacion es: "+ c a l i f i c a c i o n +"." ) ;
i f ( c a l i f i c a c i o n >= 8) {
63
c . i m p r i m e l n ( " Felicidades." ) ;
}
Consola c ;
c = new C o n s o l a ( ) ;
Prueba p ;
p = new Prueba ( ) ;
int e s t a d o C i v i l = p . d a m e E s t a d o C i v i l ( ) ;
i f ( e s t a d o C i v i l == Prueba . SOLTERO) {
c . i m p r i m e l n ( "Este s es listo." ) ;
} e l s e i f ( e s t a d o C i v i l == Prueba . CASADO) {
c . i m p r i m e l n ( "A este ya lo agarraron." ) ;
} e l s e i f ( e s t a d o C i v i l == Prueba . DIVORCIADO ) {
c . i m p r i m e l n ( "Este recapacito." ) ;
} e l s e i f ( e s t a d o C i v i l == Prueba . VIUDO ) {
c . i m p r i m e l n ( "Este tomo decisiones extremas." ) ;
} else {
c . i m p r i m e l n ( "Este no quiere decir." ) ;
}
En este ejemplo, XEmacs (y cualquier otro editor de codigo que se respete) indentara as los
if s. Pero hay que entender que el segundo if esta anidado dentro del primero, y el tercero
dentro del segundo, etc. Es por eso que el switch es mucho mejor para este tipo de casos. Pero
existen ambas versiones para que se pueda utilizar la que mas nos convenga, dependiendo de
nuestro problema.
5.3.2. Iteraciones
Que pasa si queremos imprimir los nombres de todos los registros en nuestra base de datos?
Podemos hacerlo uno por uno, si suponemos que los conocemos todos a priori. O podemos
utilizar iteraciones.
Las iteraciones nos sirven para repetir tareas un determinado numero de veces, o hasta que
alguna condicion falle.
64
int i = 0 ;
while ( r e g ! = null ) {
r e g = bdd . d a m e R e g i s t r o ( i ) ;
nombre = bdd . dameNombreDeRegistro ( r e g ) ;
c . i m p r i m e l n ( "Nombre: "+nombre ) ;
i ++;
}
int i = 0 ;
String reg ;
S t r i n g nombre ;
do {
r e g = bdd . d a m e R e g i s t r o ( i ) ;
nombre = bdd . dameNombreDeRegistro ( r e g ) ;
c . i m p r i m e l n ( "Nombre: "+nombre ) ;
i ++;
} while ( r e g ! = null ) ;
La unica diferencia es que while comprueba primero la condicion, y luego ejecuta el cuerpo.
En cambio do ... while primero ejecuta el cuerpo y luego comprueba la condicion; esto quiere
decir que el do ... while ejecuta siempre su cuerpo al menos una vez.
El while y el do ... while son especialmente utiles cuando no sabemos de antemano cuantas
veces vamos a iterar. Sin embargo, a veces s sabemos exactamente cuantas veces vamos a iterar,
y en estos casos se suele usar el for.
La iteracion for
El for esta pensado para iterar sobre rangos. Por ejemplo, para imprimir todos los nombres
de nuestra Base de Datos para agenda haramos:
int i ;
// Con e s t o , c a l c u l a m o s e l numero de r e g i s t r o s .
int n u m e r o D e R e g i s t r o s = t a b l a . l e n g t h ( ) / TAM REGISTRO ;
String reg ;
S t r i n g nombre ;
65
for ( i = 0 ; i < n u m e r o D e R e g i s t r o s ; i ++) {
r e g = bdd . d a m e R e g i s t r o ( i ) ;
nombre = bdd . dameNombreDeRegistro ( r e g ) ;
c . i m p r i m e l n ( "Nombre: "+nombre ) ;
}
La iteracion for recibe tres expresiones. La primera se ejecuta incondicionalmente, pero solo
una vez. Generalmente se utiliza para inicializar variables que se usaran para recorrer algun
rango. Esta primera expresion no necesita regresar valor.
La segunda expresion es booleana. Mientras esta expresion se cumpla (o sea, regrese true),
el cuerpo del for se ejecutara. Tiene que regresar obligatoriamente un valor de tipo booleano.
La tercera expresion se utiliza generalmente para actualizar las variables inicializadas en la
primera expresion. Tampoco necesita regresar ningun valor.
Al entrar al for, se ejecuta primero la inicializacion, y se comprueba que la condicion
booleana sea verdadera. Si no es verdadera, no se ejecuta el cuerpo nunca.
Si s es verdadera, se ejecuta el cuerpo del for, y al final se ejecuta el tercer argumento. Des-
pues vuelve a comprobar la condicion booleana (la inicializacion ya no vuelve a considerarse),
y si es verdadera se repite todo.
El for, el while y do ... while son exactamente lo mismo. Un while puede escribirse con un
for, y un for con un while; y si solo se tuviera while o do ... while se podra emular al otro.
De hecho, hay lenguajes de programacion donde solo se da while o do ... while, no ambos.
Tambien hay lenguajes donde no se da el for.
Tener los tres nos permite mayor versatilidad al escribir codigo, y es bueno utilizar el que
mas se acomode en cada caso.
La instruccion break nos sirve para romper la ejecucion de un switch, como ya vimos. Pero
sirve tambien para romper cualquier bloque o cuerpo de un ciclo. Con el break podemos romper
la ejecucion de un switch, de un while, de un do .. while y de un for. La instruccion continue
en cambio se salta lo que reste de la vuelta actual, para empezar inmediatamente la siguiente.
Habra veces dentro de un metodo en que no solo querremos salir de un switch o un ciclo,
sino que querremos salir de toda la funcion. Para tales casos, hay que usar la instruccion return.
Podemos usar la instruccion return en cualquier metodo, inclusive aquellos que tienen tipo de
regreso void; en estos casos se utiliza el return solo, sin ninguna expresion a la derecha.
5.3.4. Listas
Las condicionales e iteraciones nos permiten tomar decisiones dentro de nuestro programa,
lo que hace que el flujo de ejecucion sea tan complejo como deseemos.
Ahora que las tenemos, es posible utilizar clases que nos permiten representar de forma mas
clara la informacion de un programa.
Las listas son un tipo abstracto de datos que nos permite tener un numero arbitrario de
algun tipo de elementos. Definiremos las listas como sigue
66
Una lista es
La definicion es recursiva; hace referencia a lo que esta definiendo. Piensen en los numeros
naturales y los axiomas de Peano; un numero natural es 1, o su antecesor (que es un numero
natural) mas 1.1
Por su misma definicion, las listas son divertidas para trabajar. Si una lista no tiene elemen-
tos, entonces decimos que es vaca y sera sencillamente null. Si no es vaca, entonces tiene al
menos un elemento, y ademas a su primer elemento le sigue una lista (con cero o mas elementos).
Mas adelante trabajaremos con listas de muchos tipos; por ahora utilizaremos solo listas de
cadenas utilizando la clase ListaDeCadena.
Si una lista no es vaca, tiene dos partes: su elemento, que llamaremos cabeza y al que
podremos tener acceso con el metodo getElemento; y otra lista, a la cual le llamaremos la
siguiente lista, y a la que podremos tener acceso con el metodo getSiguiente:
ListaDeCadena a ;
a = p . l l e n a L a L i s t a ( ) ; // Llena l a l i s t a de a l g u n a manera .
i f ( a ! = null ) {
c . i m p r i m e l n ( "Este es el primer elemento: "+a . g e t E l e m e n t o ( ) ) ;
a = a. getSiguiente ();
i f ( a ! = null ) {
c . i m p r i m e l n ( "Este es el siguiente elemento: " +
a . getElemento ( ) ) ;
}
}
a = a . getSiguiente ();
la unica referencia que apuntaba al primer elemento de la lista (a), hacemos ahora que apunte
al siguiente elemento. Ya no hay referencias apuntando al primer elemento, ya no podemos
usarlo. Eso significa que queda marcado como removible, y que el recolector de basura se lo
llevara. Y ya no hay manera de recuperarlo.
Con las listas, si tenemos a un elemento, podemos tener el que le sigue, pero no al anterior.
Esto es importante; no pierdan nunca la cabeza (de las listas, por supuesto).
Cuantos elementos tiene una lista? Es muy facil saberlo:
ListaDeCadena a ;
a = p . l l e n a L a L i s t a ( ) ; // Asumimos e s t e metodo . . .
L i s t a D e C a d e n a b = a ; // Para no p e r d e r l a c a b e z a . . .
int c o n t a d o r = 0 ;
1
S nos acordamos de los axioma de Peano, verdad?
67
while ( b ! = null ) {
b = b. getSiguiente ();
c o n t a d o r++;
}
ListaDeCadena a ;
a = new L i s t a D e C a d e n a ( "una cadena" ) ;
Noten que el constructor recibe una cadena, que es justamente el elemento de nuestra lista.
As construida, la lista que le sigue a a es null.
Ahora, como una lista tiene otra lista (la siguiente), tambien podremos construir listas a
partir de listas:
ListaDeCadena a ;
a = new L i s t a D e C a d e n a ( "una cadena" ) ;
ListaDeCadena b ;
b = new L i s t a D e C a d e n a ( "otra cadena" , a ) ;
Esto quiere decir que tenemos dos constructores; uno recibe una cadena, y el otro recibe
una cadena y una lista. El ejemplo de arriba crea una lista de la forma:
b a
De esta forma, ahora a es el elemento siguiente de b. Noten que de esta manera las listas
se construyen al reves; el ultimo elemento en crearse se vuelve la cabeza. Para que no sea
68
as, tendremos la funcion agrega, de la clase ListaDeCadena que hara justamente eso, agregar
elementos al final de la lista:
ListaDeCadena a ;
a = new L i s t a D e C a d e n a ( "una cadena" ) ;
a . a g r e g a ( "otra cadena" ) ;
a . a g r e g a ( "una tercera" ) ;
a . a g r e g a ( "y otra" ) ;
5.3.5. Paquetes
Hemos estado usando paquetes, aunque sin decir que son exactamente. Hemos usado los
paquetes icc1.interfaz para nuestra consola, e icc1.util para nuestras listas. Ademas, generalmente
en las practicas las clases han estado en los paquetes icc1.practica<n>, donde <n> es el numero
de la practica.
Los paquetes son la solucion de Java a problemas relacionados con espacios de nombres
(namespaces en ingles).
Si por alguna razon quisieramos crear una clase llamada Consola (que no estuviera rela-
cionada para nada con la que hemos estado usando), no podramos hacerlo sin usar paquetes,
porque ya existe una clase Consola. Los paquetes nos permiten tener clases con nombres identi-
cos, siempre y cuando esten en paquetes distintos.
Para especificar el paquete de una clase, se utiliza
package mi . p a q u e t e ;
al inicio de la clase.
La clase Consola, que es del paquete icc1.interfaz, por ejemplo, su nombre calificado es
icc1.interfaz.Consola. Igual, el nombre calificado de la clase ListaDeCadena es icc1.util.ListaDeCadena.
Ya hemos usado nombres calificados; cuando en el objetivo run de nuestro build.xml espe-
cificabamos que clase iba a ser ejecutada, siempre ponamos el nombre calificado de la clase
(recuerden icc1.practica1.UsoReloj).
Si no utilizaramos la palabra clave import en nuestras clases, tendramos que usar el
nombre calificado de la clase. Al usar import icc1.interfaz .Consola; , el compilador ya sabe
que cuando hacemos referencia a una clase Consola, realmente estamos haciendo referencia
a icc1.interfaz.Consola.
Los paquetes nos dan entonces una division conceptual de nuestras clases. Pero tambien nos
dan una division fsica: si la clase Consola es del paquete icc1.interfaz, eso quiere decir que, en el
disco duro de la computadora donde se compilo, el archivo Consola.java esta en un directorio
llamado interfaz, el cual a su vez esta en un directorio icc1.
Cuando se les dieron clases a ustedes que ya estaban en algun paquete, estas estaban en un
directorio que se llamaban como el paquete. Y todos esos directorios estan debajo del directorio
src, que es el que especificamos en el build.xml.
69
Cuando una clase no tiene un paquete especificado, esta en el paquete por omision. Todas
las clases que no tienen paquete especificado, estan en el mismo paquete anonimo.
Hay dos ocasiones cuando no es necesario hacer import de una clase; una es cuando las
clases son del paquete java.lang. A las clases de ese paquete no es necesario que les hagamos
import. La clase String es de ese paquete, por ejemplo.
Ademas, no es necesario hacer import nunca de una clase que este en el mismo paquete de
la clase con la que estemos trabajando. Por ejemplo, UsoReloj no hace import de VistaReloj ni
de Reloj, porque todas estaban en icc1.practica1.
A partir de esta practica, tus clases deben estar en un paquete llamado icc1.practica<n>,
donde <n> es el numero de la practica.
5.4. Ejercicios
1. Nuestra Base de Datos para agenda tiene una fuerte limitante: es estatica. Para acentuar
esto, la variable tabla de la clase es final; siempre es la misma. No podemos borrar o
agregar registros.
Las listas parecen un buen candidato para reemplazar a nuestra cadenota (principalmente
por el hecho de que no hemos visto ninguna otra cosa).
Haz que la clase BaseDeDatosAgenda utilice una ListaDeCadena en lugar de una cadena
enorme. Cada registro sera igual que antes; una cadena de tamano TAM REGISTRO.
Queremos que nuestra Base de Datos tenga los mismos registros de la clase anterior,
as que tendras que crear un constructor para anadir los primeros registros (que puedes
copiar de la practica anterior). Nuestra antigua tabla debe quedar ahora como la lista:
Ya tienes el archivo icc1.jar. A estos archivos se les conoce como archivos Jar, o jarfiles,
y sirven para guardar dentro de ellos clases y paquetes. Dentro de icc1.jar ya esta la clase
ListaDeCadena. Para compilar tu programa, puedes utilizar el mismo objetivo compile
de los build.xml anteriores. Igual para ejecutarlo, utiliza el objetivo run que ya hemos
usado.
La clase ListaDeCadena esta en un paquete llamado icc1.util, as que para utilizarla debes
poner la siguiente lnea en las clases donde uses listas de cadenas:
import i c c 1 . u t i l . L i s t a D e C a d e n a ;
Tu clases deben estar en el paquete icc1.practica5. Eso quiere decir que tienen que tener
la lnea
package i c c 1 . p r a c t i c a 5 ;
70
y estar en un directorio llamado icc1/practica5 dentro de tu directorio src. A partir
de esta practica, tus clases deben estar en un paquete llamado icc1.practica<n>, donde
<n> es el numero de la practica.
Aun si utlizas los mismos archivos de practicas anteriores para hacer tus practicas,
asegurate de que el paquete refleje de que practica son.
3. Implementa la funcion publica agregaRegistro, que recibe una cadena con un registro, y
regresa void.
La funcion debe comprobar que el registro sea valido (i.e. que tenga longitud TAM REGISTRO)
y, si es as, anadirlo a la Base de Datos (o sea, a la lista). Comenta la funcion para JavaDoc.
Ten en cuenta de que por ahora estamos poniendo dentro del constructor de la clase
varios registros; entonces cuando sea llamado este metodo, la variable de clase lista ya no
sera vaca. Aun as, tienes que cubrir dentro del metodo el caso cuando la variable lista
sea vaca (o sea, null).
5.5. Preguntas
1. Ahora que podemos agregar registros a nuestra Base de Datos de agenda, que otras cosas
crees que le hagan falta? Justifica.
3. Crees que podras escribir la clase MiListaDeCadena, e implementar las funciones getEle-
mento, getSiguiente y agrega? Justifica tu respuesta
71
72
Practica 6
Herencia
6.1. Meta
Que el alumno aprenda como funciona la herencia en Java.
6.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
6.3. Desarrollo
La herencia es una de las herramientas mas poderosas de la Orientacion a Objetos. Nos
permite reutilizar trabajo ya hecho, cambiar las interfaces de una clase sin por ello inutilizar
otras clases que las usan, resolver varios problemas a la vez, y muchas cosas mas.
La herencia es fundamental en el diseno Orientado a Objetos. Si el lenguaje otorga la
facilidad de herencia y no se utiliza, se esta desperdiciando gran parte del poder que se obtiene
al utilizar un lenguaje Orientado a Objetos.
73
6.3.1. Heredar y abstraer clases
Hemos trabajado con la clase BaseDeDatosAgenda, que nos sirve para guardar registros de
una agenda. Que sucede si nos piden ahora una Base de Datos para una nomina, o una librera?
Una vez que hayamos terminado la clase BaseDeDatosAgenda, nos sera muy sencillo hacer la
clase BaseDeDatosLibreria, que hara en esencia exactamente lo mismo que la clase BaseDeDatos-
Agenda, pero en lugar de usar registros con nombres, direcciones y telefonos, utilizara registros
con ttulos, autores y numeros ISBN.
Sin embargo, esto nos llevara a repetir mucho codigo ya existente, y hemos visto que una
de las metas de la Orientacion a Objetos es justamente reutilizar la mayor cantidad de codigo
posible.
La herencia es una de las soluciones para la reutilizacion de codigo. Cuando uno hereda una
clase A en una clase B, los metodos y variables de clase no privados de la clase A pueden ser
usados automaticamente por los objetos de la clase B. Los metodos y variables privados no se
heredan; todos los demas s.
Las interfaces solo nos dan el contrato de la clase; que metodos deben tener las clases que
implementen una interfaz. La herencia nos da mucho mas; nos da variables que ya no hay
necesidad de declarar (si no son privadas), y nos da codigo ya escrito. Con la herencia no solo
nos dan el contrato; nos dan codigo ya escrito.
Una vez que la clase B se haya heredado de la clase A, puede definir metodos y variables de
clase propios. Entonces los objetos de la clase B podran hacer uso de los metodos y variables
de clase de la clase A, y ademas los que se definan en la clase B. Por eso tambien se dice que
heredar una clase es extenderla, y ambos terminos seran equivalentes en estas practicas.
Ademas de definir metodos propios, la clase B puede redefinir o sobrecargar los metodos de
la clase A. Veremos un poco mas de eso mas adelante.
Si asumimos que tenemos una clase Vehiculo, y quisieramos extenderla con la clase Bicicleta,
entonces solo tenemos que hacer
public c l a s s B i c i c l e t a extends V e h i c u l o {
...
Casi todas las clases de Java se pueden extender (en un momento veremos cuales no).
Podemos extender nuestra clase Matriz2x2, o nuestra clase Consola, por ejemplo.
Sin embargo, habra veces que escribiremos una clase con el unico proposito de extenderla.
En otras palabras, en ocasiones escribiremos clases de las cuales no pensamos instanciar objetos,
sino heredarlas para instanciar objetos de las clases herederas. A estas clases las llamaremos
abstractas, ya que no se concretaran en objetos. Para que una clase sea abstracta, solo tenemos
que agregar abstract a la declaracion de la clase:
public abstract c l a s s V e h i c u l o {
...
Cualquier clase puede ser abstracta; la palabra clave abstract solo hace que sea imposible
instanciar objetos de esa clase. Sin embargo, cuando hagamos clases abstractas sera general-
mente porque tendran metodos abstractos, metodos que no tienen definido todava un compor-
tamiento. Si una clase tiene un metodo abstracto, la clase misma tiene que ser abstracta, de
otra forma no va a compilar.
74
Un metodo abstracto no tiene cuerpo. Por ejemplo, el siguiente metodo podra estar definido
dentro de la clase Vehiculo que acabamos de mostrar
(No se pongan quisquillosos con la aparicion espontanea de clases como Paquete; esto es un
ejemplo sencillo). Noten que, si una clase es completamente abstracta, se asemeja muchsimo
a una interfaz. Sin embargo, no son iguales; las clases abstractas pueden declarar variables de
clase, que heredaran las clases que la extiendan.
Ya hemos visto, con las interfaces, la razon para tener un cuerpo vaco. La clase Vehiculo
tiene un metodo abstracto llamado transporta; esto significa que todas las clases que extiendan
a Vehiculo tienen que implementar el metodo transporta (a menos que sean abstractas tambien).
Con esto forzamos el comportamiento de todas las clases herederas de la clase Vehiculo
(igual que con las interfaces). Si una clase extiende a Vehiculo (y no es abstracta), entonces
podemos garantizar que tiene un metodo que se llama transporta, que regresa un booleano, y
que recibe un objeto de la clase Paquete como parametro.
Dijimos que las clases, aunque sean abstractas, pueden tener cuerpo en sus metodos, a
diferencia de las interfaces. Entonces, por que no escribimos el cuerpo de transporta? Si todas
las clases que extiendan a Vehiculo heredan todos sus metodos no privados, entonces por que no
de una vez escribimos transporta? La respuesta es que una bicicleta transporta paquetes muy
distinto a como lo hace un automovil.
La idea de tener un metodo abstracto (o en una interfaz), es que todas las clases herederas
tendran al metodo, pero como funcionara va a depender de la clase que hereda. El ejemplo
de una clase vehculo es muy simple, pero sirve para entender este punto. Tenemos toda la
gama posible de vehculos (bicicletas, automoviles, patines, helicopteros), y todos ellos pueden
transportar paquetes; pero como lo hacen funciona de manera distinta en todos. Entonces
declaramos un metodo transporta abstracto en la clase abstracta Vehiculo, y dejemos que cada
clase que la extienda (Bicicleta, Automovil, Patines, Helicoptero, etc.) la implemente de acuerdo
a como funcione la clase.
En nuestro ejemplo de la clase Bicicleta, como la clase no es abstracta, tiene que implementar
al metodo transporta. Si fuera abstracta podra no hacerlo; pero como no lo es esta obligada a
implementarlo. Para que la clase Bicicleta implemente el metodo transporta, debe usar la misma
declaracion que en su clase padre (la clase Vehiculo), pero sin abstract:
La declaracion debe ser exactamente igual que la de la clase padre (como en las interfaces);
debe llamarse igual el metodo, debe regresar el mismo tipo, y debe tener el mismo numero de
parametros y con el mismo tipo y orden en que aparecen (el nombre de los parametros puede
cambiar). El acceso al metodo debe ser el mismo, aunque un cambio esta permitido. Veremos
mas del acceso al heredar un poco mas adelante. Al nombre de un metodo, a su tipo de regreso
y a los tipos y orden de los parametros se le conoce en conjunto como la firma de un metodo.
Para que una clase heredera implemente una version propia de algun metodo de su clase
padre, no es absolutamente necesario que el metodo sea abstracto. A esto se le llama redefinir
75
o sobrecargar un metodo (overloading es el termino usado en la literatura en ingles). Incluso
se puede utilizar el metodo que queremos sobrecargar dentro del metodo sobrecargado, usando
super:
La referencia super se utiliza cuando queremos hacer referencia a la clase padre. Esto
funciona porque los objetos de una clase heredera pueden comportarse como objetos de la clase
padre. Hay que recordar eso siempre.
Si tenemos una clase, y no queremos que sus herederas puedan sobrecargar algun metodo,
entonces podemos definir al metodo como final:
Habamos dejado pendiente la explicacion de que era un metodo final desde la practica 4. El
significado es muy distinto a las variables finales; esta unicamente relacionado con la herencia.
Si un metodo es final, ya no puede redefinirse o sobrecargarse: sera el mismo para todas las
clases herederas.
Dijimos un poco mas arriba que casi todas las clases podan extenderse. Podemos evitar
que una clase sea heredada si la hacemos final:
public f i n a l c l a s s C l a s e N o H e r e d a b l e {
...
Funciona de la misma manera que con los metodos finales; si una clase es final, sencillamente
no puede ser heredada o extendida.
Hay que tener en cuenta una ultima cosa al heredar una clase, los constructores nunca se
heredan. Un constructor no puede heredarse porque un constructor define cierto estado inicial,
y este siempre es concreto. Por lo mismo, no tiene sentido un constructor abstracto.
Sin embargo, s podemos llamar a los constructores de nuestra clase padre dentro de los
constructores de la clase heredera, utilizando super:
Por supuesto, los parametros de super deben coincidir con los parametros de algun cons-
tructor de la clase padre. Solo podemos usar super para llamar a los constructores de la clase
padre dentro de los constructores de la clase heredera.
76
6.3.2. El acceso en la herencia
Java provee un acceso menos restrictivo que private, pero no tan abierto como public para
clases que heredan a otras clases. El acceso se llama protegido (protected). Permite restringir
el acceso a metodos y variables a solo miembros de la familia, o sea, unicamente a clases que
hereden.
Esto es muy util ya que para clases que no sean herederas el acceso es para motivos practicos
privado, preservando el encapsulamiento de datos y la modularidad de componentes. Ademas,
permite a clases emparentadas compartir variables y metodos que no queremos mostrar a
nadie mas.
Cuando uno sobrecarga un metodo, o implementa un metodo abstracto, debe preservar
el acceso tal y como lo defina la clase padre. La unica excepcion a esta regla, es cuando el
metodo tiene acceso protegido. Si el metodo tiene acceso protegido en la clase padre, la clase
que extiende puede cambiarle el acceso a publico; pero es el unico cambio permitido. Al reves
es ilegal; no se puede transformar un metodo publico en uno protegido. Y todas las otras
combinaciones tambien son ilegales.
Vehiculo
Bicicleta
Automovil
Patines
Helicoptero
La jerarqua de clases tiene una gran importancia en Java; ordena a las clases en terminos
de comportamiento y caractersticas. La idea fundamental es que todas las clases de Java esten
relacionadas en esta jerarqua, que toda clase sea heredera de alguna otra clase. Para garantizar
esto, Java tiene una clase fundamental que se llama Object.
La clase Object es La Superclase (con mayusculas). Todas las clases de Java son herederas
de la clase Object en algun grado (lo que quiere decir que si no son herederas directas, su
clase padre s lo es, o si no la clase padre de la clase padre, etc.) Si una clase no extiende
explcitamente a alguna otra clase, por omision extiende a la clase Object. La clase Vehiculo es
heredera directa de la clase Object por ejemplo.
Una de las principales ventajas que ofrece la herencia es que un objeto de una clase heredera
garantiza que se puede comportar como un objeto de su clase padre. Un objeto de la clase Bici-
cleta se puede comportar igual que un objeto de la clase Vehiculo, porque tiene que implementar
sus metodos abstractos, y porque hereda todos los metodos no privados (que son los que definen
el comportamiento). Esto permite cosas como el siguiente ejemplo (es una clase de uso):
77
public s t a t i c void main ( S t r i n g [ ] a r g s ) {
Vehiculo v ;
v = i n s t a n c i a ( o b t e n T i p o ( ) ) ; // Obtenemos a l g u n t i p o . . .
i f ( v ! = null ) {
Paquete p ;
p = o b t e n P a q u e t e ( ) ; // Obtenemos un p a q u e t e . . .
v . transporta ( p );
}
}
public s t a t i c V e h i c u l o i n s t a n c i a ( int t i p o ) {
switch ( t i p o ) {
case TODOS:
return new H e l i c o p t e r o ( ) ;
case AUTOMOVIL:
return new A u t o m o v i l ( ) ;
case BICICLETA :
return new B i c i c l e t a ( ) ;
case PATINES :
return new P a t i n e s ( ) ;
default :
return null ;
}
}
78
pero en otros casos hay relaciones mas fuertes.
Todo esto nos lleva a que absolutamente todos los objetos de Java pueden comportarse como
objetos de la clase Object, y por lo tanto utilizar sus metodos.
Los 11 metodos de la clase Object tienen un significado especial cuando usamos Java. En
particular, el metodo toString es el que utiliza Java para obtener la representacion como cadena
de un objeto.
La clase Object ofrece el metodo toString, por lo que todos los objetos de Java pueden
llamarlo. En la clase Object el metodo toString esta implementado de tal forma que regresa
el nombre de la clase concatenado con un "@", concatenado con un entero que Java utiliza
internamente para identificar unvocamente al objeto (pueden pensar que es la direccion de
memoria donde vive el objeto, pero no siempre es as).
Por supuesto, podemos sobrecargar el metodo. En la clase Matriz2x2, por ejemplo, lo sobre-
cargamos para que pudieramos pasar los objetos de la clase como cadenas al metodo imprime
de la clase Consola.
Como todos los objetos de Java se pueden comportar como objetos de la clase Object,
podemos hacer ahora una clase Lista cuyos elementos no sean cadenas, sino objetos de la clase
Object. Con esto automaticamente ya tenemos listas que pueden contener objetos de todas las
clases de Java.
La clase Lista puede tener como elementos objetos de cualquier clase. Esto en particular
significa que puede tener como elemento un objeto de la clase Lista. O sea, podemos tener listas
de listas. Y listas de listas de listas.
Se puede complicar tanto como queramos.
79
Integer i ;
i = new I n t e g e r ( 5 ) ;
Lista l ;
l = new L i s t a ( i ) ;
Para obtener el valor entero de este objeto, utilizamos el metodo intValue de la clase Integer:
int j = i . i n t V a l u e ( ) ;
Debe ser obvio que la funcion equivalente para la clase Float es floatValue. Estas clases
no solo envuelven a los tipos basicos de Java para que sean tratados como objetos; tambien
ofrecen funciones que resultan utiles para el tipo basico al que representan y variables que nos
dan informacion sobre el tipo (como MAX VALUE de la clase Integer, que contiene un valor de
tipo int que corresponde al entero de mayor tamano que podemos representar con 32 bits).
Actividad 6.3 Con el apoyo de tu ayudante, consulta la documentacion de las clases Byte,
Short, Integer, Long, Float, Double, Character y Boolean.
Lista lis ta ;
l i s t a = new L i s t a ( i ) ;
El constructor de la clase Lista recibe un objeto de la clase Object. Ya vimos que esto es
valido porque al ser Integer una clase heredera de la clase Object, entonces puede comportarse
como un objeto de la misma. La fuerte tipificacion de Java no se rompe cuando vamos de una
clase mas particular a una mas general. Sin embargo, al reves no funciona.
Cuando queramos recuperar el objeto de la clase Integer de la lista, no podremos hacer
Integer k ;
k = l i s t a . dameElemento ( ) ;
80
Pero nosotros s sabemos que es de la clase Integer. Para poder hacer una conversion explcita
de tipos (o casting en la jerga computacional), precedemos a la expresion que deseamos convertir
con el tipo que queremos entre parentesis
Integer k ;
k = ( I n t e g e r ) l i s t a . dameElemento ( ) ;
Con esto le ponemos mascara de Integer al objeto de la clase Object. En el ejemplo que
estamos manejando va a funcionar porque nosotros sabemos de que tipo es el objeto que nos
regresa la lista; pero si le hacemos una conversion a un objeto que no es del tipo al que estamos
convirtiendo, la JVM marcara un error y terminara la ejecucion del programa.
Como vimos hace algunas practicas, la conversion explcita de datos no solo funciona con
objetos; si queremos asignarle a un int el valor de un float hacemos
float f = 1.2F ;
int i = ( int ) f ;
Habra veces en que no sabremos con tanta seguridad de que clase es un objeto. Por suerte,
los objetos siempre recuerdan de que tipo son, y Java ofrece un operador para comprobar la clase
de una referencia; instanceof, un operador binario no conmutativo cuyo operando izquierdo es
una variable de referencia y el derecho el nombre de una clase:
Consola c ;
c = new C o n s o l a ( " Transporte de paquetes" ) ;
i f ( v instanceof B i c i c l e t a ) {
c . i m p r i m e l n ( "Usamos una bicicleta para transportar." ) ;
} e l s e i f ( v instanceof A u t o m o v i l ) {
c . i m p r i m e l n ( "Usamos un automovil para transportar." ) ;
}
...
6.3.6. Interfaces
Que pasa si queremos una clase que combine el comportamiento de otras dos, o tres clases?
Lo logico sera pensar en heredar todas las clases que queramos en nuestra clase. A esto se le
llama herencia multiple.
Sin embargo, Java no soporta herencia multiple. Lo que Java ofrece a cambio son interfaces.
Las interfaces son clases que no pueden implementar metodos; solo pueden declararlos. Una
interfaz es de esta forma
81
public i n t e r f a c e E j e r c i t a d o r {
public void q u e m a C a l o r i a s ( int kg ) ;
}
El acceso de la interfaz tiene que ser publico; no se admite ningun otro. Lo mismo ocurre
con los metodos y las variables de clase. Y por supuesto, lo mas importante es que los metodos
no tienen cuerpo.
Hemos trabajado con interfaces desde el inicio de las practicas, pero vamos a recapitular lo
que sabemos de ellas, y a extender esta informacion un poco mas.
No podemos instanciar objetos de una interfaz. Las interfaces sirven para definir el compor-
tamiento de una clase, sin implementar ninguno de sus metodos. Pero la principal ventaja es
que una clase puede heredar varias interfaces al mismo tiempo.
Pusimos heredar (entre comillas) porque no es exactamente herencia, y de hecho tiene
un termino propio: implementar. Una clase puede implementar tantas interfaces como quiera,
ademas de que puede extender a alguna otra clase (a lo mas una). Si quisieramos una clase que
extendiera a la clase Vehiculo e implementara a la clase Ejercitador, haramos:
public c l a s s B i c i c l e t a extends V e h i c u l o
implements E j e r c i t a d o r {
...
Si la clase Bicicleta esta declarada as, entonces debe implementar los metodos abstractos de
la clase Vehiculo, y ademas tiene que implementar todos los metodos declarados en la interfaz
Ejercitador.
Las interfaces solo pueden tener variables de clase que sean publicas y finales. Todas las
variables de clase de una interfaz son estaticas; incluso aunque no usemos static. El compilador
las compila como si fueran estaticas de cualquier forma. Tiene esto sentido ya que una interfaz
nunca va a tener objetos, entonces una variable no estatica no tiene sentido.
Una clase puede implementar tantas interfaces como quiera:
public c l a s s Z implements A , B , C , D , E {
...
Las interfaces son la alternativa de Java a la herencia multiple. Se puede hacer conversion
de tipos hacia una interfaz, y pueden ser recibidas como parametros y regresadas por metodos.
Java en particular utiliza mucho las interfaces para senalar que una clase es capaz de hacer
ciertas cosas.
1. Disenar desde el principio una jerarqua de herencia con clases abstractas e interfaces,
extendiendolas e implementandolas de manera que resuelvan el problema.
82
2. Disenar una solucion sin herencia y, a la mitad, darnos cuenta de que ciertas clases
comparten varias caractersticas y que podemos crear una o varias clases abstractas que
al heredarlas nos daran una jerarqua de clases que nos resolvera el problema.
Por supuesto, la idea es que en algun punto ustedes sean capaces de disenar un programa
utilizando el primer metodo, aunque no es sencillo. Lo natural es utilizar el segundo metodo.
Lo usual sera casi siempre que al estar a la mitad de su analisis se percaten de que hay
posibilidad de utilizar la herencia en ciertas partes del problema, y entonces tendran que ver
que tanto conviene hacerlo. Casi siempre convendra.
El primer metodo requiere tener ya experiencia programando, para que seamos capaces de
detectar patrones de problemas, muchos de los cuales tendran una metodologa para resolverlos
que involucrara herencia. No es sencillo llegar a ese nivel; e incluso habiendo llegado a el, la
mayora de las veces ocurrira que a la mitad del camino descubramos una nueva manera de
utilizar la herencia en nuestro problema.
El diseno es la parte mas importante cuando resolvemos un problema. Sin embargo, no es
algo intocable; la principal directiva del diseno Orientado a Objetos es que una vez completada
gran parte de la implementacion de nuestro problema, habra que regresar a ver que partes
del diseno estan mal y cambiarlas de acuerdo a ello. Habra incluso ocasiones en que todo un
problema tendra que redisenarse desde el principio (y en esas ocasiones lo mejor es detectarlo
lo mas pronto que se pueda).
Por supuesto, hay que disenar tratando de evitar que eso ocurra. Pero si el introducir la
herencia a algun diseno va a facilitarnos mucho la vida, aun a costa de redisenar casi todo de
nuevo, no hay que dudar y hay que introducir la herencia. Casi siempre valdra la pena.
6.4. Ejercicios
Como ya sabemos utilizar la herencia, es hora de redisenar nuestra base de datos para
aprovecharla. Ya que vamos a redisenar, veamos que otras cosas estan mal en nuestra Base de
Datos:
Tenemos que cambiar de utilizar la clase ListaDeCadena a utilizar la clase Lista, para que
dejemos de representar a nuestros registros como cadenas.
Ya que podemos meter cualquier clase en nuestras listas, vamos a utilizar una clase para
representar a nuestros registros.
83
Solo se te daran los archivos necesarios. Recuerda que tu tienes que escribir tu propio
build .xml, y hacer tus directorios.
Consola c ;
c = new C o n s o l a ( "Base de Datos" ) ;
S t r i n g nombre ;
84
nombre = c . l e e S t r i n g ( "Dame una cadena:" ) ;
Esto abre una ventana de dialogo donde el usuario puede escribir su cadena. Con el
auxilio de tu ayudante, consulta la documentacion de la clase Consola y consulta todos
los metodos lee.
4. Todas las clases y metodos que implementes deben estar comentados para la docu-
mentacion de JavaDoc. Debe quedar claro en la documentacion que devuelve una funcion
o para que es cada parametro.
6.5. Preguntas
1. Las funciones agregaRegistro y quitaRegistro implementadas en el archivo BaseDeDatos.java
utilizan solo cosas que hemos visto en esta y practicas anteriores. Hay algo que no en-
tiendas? Elabora detalladamente tu respuesta (sea afirmativa o negatica).
2. Que te parece el diseno que esta tomando la base de datos? Crees que hay algun error
en el? Explica.
3. Te habras dado cuenta de que para crear una Base de Datos de una librera (o de lo
que fuera), casi solo se necesitara extender la clase Registro a una nueva clase. Como lo
haras tu? Explica a detalle.
85
86
Practica 7
Entrada/Salida y Arreglos
7.1. Meta
Que el alumno aprenda como funcionan la entrada/salida y los arreglos en Java.
7.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
utilizar las clases del paquete icc1.es para escribir y leer datos en disco, y
7.3. Desarrollo
7.3.1. Entrada y Salida (E/S)
Un programa (a menos que sea puramente teorico) recibe una cierta entrada. Nuestros
programas hasta ahora han recibido su entrada de distintas maneras.
Al inicio utilizabamos datos estaticos. Si queramos buscar el nombre Juan Perez en
nuestra Base de Datos, tenamos que compilarlo estaticamente en la funcion main
87
. . . // Llenamos l a Base de Datos .
Consola c ;
c = new C o n s o l a ( "Base de Datos de Agenda" ) ;
Registro r ;
r = bd . dameRegistroPorNombre ( "Juan Perez" ) ;
i f ( r == null ) {
c . i m p r i m e l n ( "El nombre \" Juan Perez\" no existe." ) ;
} else {
c . imprimeln ( r ) ;
}
}
El siguiente paso fue leerlo por teclado, haciendolo dinamico. Para esto, utilizamos las
funciones leeXXX de la clase Consola.
Consola c ;
c = new C o n s o l a ( "Base de Datos de Agenda" ) ;
Registro r ;
r = bd . dameRegistroPorNombre ( nombre ) ;
i f ( r == null ) {
c . i m p r i m e l n ( "El nombre \""+nombre+"\" no existe." ) ;
} else {
c . imprimeln ( r ) ;
}
}
Para la salida hemos utilizado hasta ahora las funciones imprime e imprimeln de la clase
Consola.
La entrada y la salida son aspectos fundamentales de la programacion, y la mayor parte de
los problemas que uno encuentra en el momento de hacer un programa medianamente complejo,
tienen que ver con la comunicacion entre el usuario y la computadora (teclado/monitor); con la
comunicacion entre el programa y algun dispositivo externo (disco, cinta, CD-ROM), o con la
comunicacion entre el programa y algun otro programa, posiblemente en otra maquina (red).
En esta practica cubriremos la entrada y la salida con mas detalle de lo que lo hemos hecho
hasta ahora.
88
Flujos
Por detras de los telones, la clase Consola utiliza cadenas para comunicarse con el usuario;
las funciones imprime e imprimeln transforman cualquier tipo que se les pase en cadena y lo
imprimen, y las funciones leeXXX en realidad leen una cadena que luego se trata de convertir
al tipo deseado (por ejemplo leeInteger utiliza la funcion estatica parseInt de la clase Integer).
Esto es porque la clase Consola esta disenada para crear una comunicacion con el usuario
sencilla y rapida. Pero la mayor parte de la comunicacion que hay en un programa (esto es, su
entrada y salida), es sencillamente el transporte de bytes de un lugar para otro. As sea leer
datos de un archivo, escuchar un puerto de la red, o esperar entrada a traves del teclado, todo
se reduce a una serie de bytes uno detras del otro.
A esta serie de bytes se le suele denominar flujo (stream en ingles), haciendo referencia a un
flujo de agua. Hay flujos de los que podemos determinar facilmente donde empiezan y donde
terminan (como los archivos en un disco duro o un CD-ROM) y hay flujos que no tienen un
principio o un fin determinados (como las estaciones de radio en Internet, que mandan bytes
ininterrumpidamente con la senal de la estacion).
Viendolos como flujos, hay flujos de entrada, es decir, de los que recibimos bytes, y flujos
de salida, o sea a los que les mandamos bytes. Un archivo en un CD-ROM es un flujo de
entrada (no podemos mandarle informacion, solo obtenerla). Cuando guardamos un archivo en
XEmacs, utilizamos un flujo de salida al mandar informacion al disco duro. Pine o GNUS, o en
general cualquier lector de correo electronico utiliza flujos de entrada para recibir correos de su
servidor, y flujos de salida para mandarle al servidor de correo.
Todos los flujos funcionan de la siguiente manera:
1. Se crea el flujo (se abre el archivo en disco, se hace la conexion en red, etc.)
Hay flujos que pueden abrirse simultaneamente para lectura y escritura. Sin embargo, esto
no necesariamente representa una gran ventaja, y no veremos ese tipo de flujos en la practica.
Filtros
Podemos conectar los flujos con programas o funciones, de tal manera que se reciban bytes
por un flujo de entrada, el programa o funcion le haga algo a estos bytes, y se vuelvan a enviar
89
a algun otro lado utilizando un flujo de salida. A estos programas o funciones se les denomina
filtros.
Un filtro muy comun, por ejemplo, es uno que tome los bytes de entrada y le aplique algun
algoritmo que los comprima para mandarlos como salida. Otro filtro puede descomprimir los
bytes. Un tercero puede utilizar cifrado de datos para proteger la informacion; un cuarto puede
descifrar esos datos para poder leerla.
Si subimos el nivel de abstraccion, y ocultamos con ello el hecho de que trabajamos en
el fondo con bytes, todo programa es un filtro. Todo programa recibe cierta entrada que en
el fondo son bytes, pero que nosotros abstraemos en un nivel mas alto utilizando para ello el
lenguaje de programacion. Manejamos esos bytes como cadenas, enteros, flotantes, clases. El
programa manipula de cierta manera la entrada, y nos regresa una salida que de nuevo en el
fondo son bytes, pero que con ayuda del lenguaje manipulamos como entidades menos crudas
que los bytes.
En el ejemplo que hemos utilizado, la entrada es un nombre, que es una cadena, y la salida es
un registro, que es una clase, que tambien podemos representar como cadena gracias al metodo
toString.
Java es de los lenguajes de programacion que existen mas poderosos para el manejo de
entrada y salida. Tiene un conjunto de clases enorme y facilidades de abstraccion de datos que
nos permiten manejar la entrada y la salida en un nivel muy alto de abstraccion. Pero tambien
permite la manipulacion directa de bytes desde su mas bajo nivel hasta conversion de clases
en bytes y viceversa. Las clases que se encargan de manejar flujos en Java estan en el paquete
java.io.
Actividad 7.2 Con asistencia de tu ayudante, consulta la documentacion del paquete ja-
va.io.
Manejo de archivos
Trabajar con bytes es tedioso; un trabajo de bajo nivel. Un byte puede representar cualquier
cosa; puede ser una variable del tipo byte de Java, o parte del tipo char, que utiliza dos bytes,
que a su vez puede ser parte de una cadena, como tambien puede ser parte de un float, que
utiliza cuatro bytes.
Cuando se trabaja a bajo nivel se pueden hacer muchas cosas, pero en general son mas
difciles de hacer y hay que tener mas cuidado.
1
Con computadoras cada vez mas veloces, y compiladores cada vez mas inteligentes, esto es cada vez menos
perceptible.
90
Cuando se trabaja a bajo nivel, si queremos guardar cadenas en un flujo de salida (un
archivo en disco duro por ejemplo), tenemos que convertirlas a bytes y entonces mandarla por
el flujo. Si queremos guardar un int, tenemos que hacer lo mismo. Y si luego queremos utilizar
un flujo de entrada para leer los datos, tenemos que hacer el camino a la inversa.
Lo que nosotros quisieramos, son clases a las que les dijeramos guarda esta cadena, y que
despues solo necesitaramos decirles dame la cadena que guarde.
Tales clases existen, y estan en el paquete icc1.es, y se llaman Entrada y Salida. Son abstrac-
tas, para que otras clases concretas implementen las funciones de acuerdo al dispositivo que se
use. En particular, las clases EntradaDeDisco y SalidaADisco son para escribir y leer datos en
disco duro.
Actividad 7.3 Con auxilio de tu ayudante, consulta la documentacion de las clases del
paquete icc1.es, especialmente la de las clases EntradaDeDisco y SalidaADisco.
El paquete icc1.es esta en el mismo archivo Jar. icc1.jar.
Las clases EntradaDeDisco y SalidaADisco estan hechas pensando en facilidad de uso antes
que cualquier otra cosa. La clase SalidaADisco escribe sobre archivos de texto. Si uno hace
SalidaADisco sad ;
s a d = new S a l i d a A D i s c o ( "archivo.txt" ) ;
sad . c i e r r a ( ) ;
obtendra un archivo de texto llamado archivo.txt que puede ser modificado usando cualquier
editor como XEmacs. Pueden comprobarlo haciendo lo siguiente
# cat archivo.txt
12
1.4
hola mundo
true
#
Los metodos que leen de la clase EntradaDeDisco leen una lnea del archivo que abren, y
tratan de convertirla al tipo solicitado. Para leer el contenido del archivo archivo.txt solo
habra que hacer
E n t r a d a D e D i s c o edd ;
edd = new E n t r a d a D e D i s c o ( "archivo.txt" ) ;
int i = edd . l e e I n t e g e r ( ) ;
91
float f = edd . l e e F l o a t ();
S t r i n g s = edd . l e e S t r i n g ();
boolean b = edd . l e e B o o l e a n ( ) ;
edd . c i e r r a ( ) ;
Como pueden ver, el orden importa: si lo primero que escriben es un double asegurense de
que un double sea lo primero que lean.
Las clases son muy sencillas de usar; cualquier error grave que ocurra (que no haya permisos
para leer o escribir un archivo, por ejemplo), resultara en que el programa termine con un
mensaje de error.
Actividad 7.4 Aun cuando las clases del paquete icc1.es son de alto nivel, el uso de entrada
y salida siempre es complicado. Para comprender mejor su funcionamiento, baja el archivo
ejemploes.tar.gz, complalo y ejecutalo:
Examina el codigo de la clase UsoEntradaSalidaDeDisco para que veas como funcionan las
clases EntradaDeDisco y SalidaADisco.
7.3.2. Arreglos
Cuando hablamos de flujos, dijimos que se trataban de sucesiones de bytes, uno detras del
otro. Las listas son sucesiones de objetos.
Sin embargo, si solo tenemos el primer elemento de una lista, o sea la cabeza, no podemos
saber directamente, sin recorrer la lista, donde esta el n-esimo.
En Java hay otra estructura que es una sucesion de objetos, o de tipos basicos de Java,
pero con la ventaja de que estan ocupando posiciones contiguas en la memoria. Como ocupan
posiciones contiguas, si sabemos donde esta el primero podemos saber donde esta el segundo,
y el tercero, y el n-esimo. Podemos tener acceso directo a los objetos o tipos basicos. Esta
estructura son los arreglos.
Las listas son una estructura de tamano arbitrario. De entrada no sabemos cuantos elementos
en total tiene una lista.
Esto es dinamico y muy provechoso; pero tiene la gran desventaja de que al momento de
buscar un elemento, tenemos que recorrer gran parte de la lista para encontrarlo. En el peor de
los casos (cuando el elemento que buscamos esta al final de la lista) la recorremos toda.
A veces sabemos que vamos a necesitar a lo mas k elementos. Para estos casos son los
arreglos.
Los arreglos son sucesiones de objetos o tipos basicos de Java, ordenados uno despues de
otro, y a los que podemos tener acceso de forma directa gracias justamente a que estan en
posiciones contiguas en memoria (al contrario de las listas).
92
Para declarar un arreglo (por ejemplo de enteros), hacemos
int [ ] a r r e g l o ;
Los arreglos son objetos calificados de Java. Heredan a la clase Object y tienen acceso a
todos los metodos de la clase. Ya que son objetos, hay que instanciarlos para poder usarlos.
Los arreglos se instancian as:
a r r e g l o = new int [ 5 ] ;
Aqu instanciamos el arreglo para que tenga 5 elementos; pero el numero de elementos
puede ser arbitrariamente grande. Dentro de los corchetes en la instanciacion podemos poner
cualquier expresion de tipo int; pero si es un entero negativo, la JVM terminara la ejecucion
del programa al tratar de instanciar el arreglo.
Una vez definido cuantos elementos tendra un arreglo, no podemos hacer que tenga mas
(contrario a las listas). Podemos crear otro arreglo mas grande y copiar los elementos que el
primero tena, pero no podemos agrandar un arreglo.
Para tener acceso al n-esimo elemento de un arreglo, solo ponemos el nombre del arreglo, y
entre corchetes el ndice del elemento que queramos. Por ejemplo, para tener acceso al tercer
elemento del arreglo de enteros que declaramos arriba hacemos:
arreglo [ 2] ;
Ese es el tercer elemento, ya que comenzamos a contar desde el cero. Por tanto, los elementos
de un arreglo tienen ndices que van desde 0 hasta el numero de elementos menos uno. El
elemento arreglo [2] es una variable de tipo int y podemos utilizarla como cualquier otra variable
de tipo int:
Cuando instanciamos al arreglo con new, el arreglo tiene todos sus elementos sin inicializar,
pero Java les asigna 0 por omision. Si fuera un arreglo de objetos, los inicializara como null;
si fuera de booleanos como false, etc.
Podemos pasar arreglos como parametros:
y regresarlos tambien:
public int [ ] g e t A r r e g l o ( ) {
...
En estos casos necesitamos saber el tamano del arreglo que nos pasan o que nos regresan.
Todos los arreglos (recuerden que son objetos) tienen una variable publica y final llamada length
que nos dice cuantos elementos tiene el arreglo:
93
int tam = a r r e g l o . l e n g t h ;
c . i m p r i m e l n ( "El arreglo tiene "+tam+" elementos." ) ;
RegistroAgenda [ ] r e g i s t r o s ;
r e g i s t r o s = new R e g i s t r o A g e n d a [ 1 0 0 0 ] ;
Es importante recordar que registros [0] , registros [1] , registros [2] , . . . , registros [999] son
referencias inicializadas con null, o sea, registros [0] es igual a null, registros [1] es igual a null,
etc.
Cuando instanciamos con new un arreglo, si es de objetos hay que instanciar a pie cada
uno de los elementos del arreglo:
Varias dimensiones
A veces una simple sucesion de objetos no nos basta. Por ejemplo, si queremos representar
matrices, necesitamos arreglos de dos dimensiones.
Para esto hacemos
int [ ] [ ] t h e M a t r i x ;
t h e M a t r i x = new int [ 7 ] [ 5 ] ;
Con esto creamos una matriz de 7 por 5. De aqu es evidente como crear arreglos de tres
dimensiones:
int [ ] [ ] [ ] theCube ;
theCube = new int [ 7 ] [ 5 ] [ 6 ] ;
94
Arreglos al vuelo
Si queremos un arreglo con los cinco primeros numeros primos, tendramos que hacer
int [ ] p r i m o s ;
p r i m o s = new int [ 5 ] ;
primos [ 0 ] = 2;
primos [ 1 ] = 3;
primos [ 2 ] = 5;
primos [ 3 ] = 7;
primos [ 4 ] = 11;
Esto es tedioso. Si al momento de compilar sabemos que valores tendra un arreglo, podemos
crearlo al vuelo
int [ ] p r i m o s = { 2 , 3 , 5 , 7 , 1 1 } ;
con lo que nos ahorramos mucho codigo. Por supuesto, se puede hacer esto con cualquier tipo
de Java.
RegistroAgenda ra1 =
new R e g i s t r o A g e n d a ( "Jose Arcadio Buenda" , "Macondo" , 0 0 0 0 0 0 0 1 ) ;
RegistroAgenda ra2 =
new R e g i s t r o A g e n d a ( " Ursula Buenda" , "Macondo" , 0 0 0 0 0 0 0 2 ) ;
RegistroAgenda ra3 =
new R e g i s t r o A g e n d a ( "Aureliano Buenda" , "Macondo" , 0 0 0 0 0 0 0 3 ) ;
R e g i s t r o A g e n d a [ ] r e g i s t r o s = { ra1 , ra2 , r a 3 } ;
Esta forma es muy comoda, mas si el contenido o tamano de un arreglo solo se puede saber
en tiempo de ejecucion, tenemos que utilizar la primera forma de instanciacion.
El metodo main tiene que ser declarado as. Si no es publico, o no es estatico, o no se llama
main, o no recibe un arreglo de cadenas como parametro, entonces ese no es el metodo main,
el punto de entrada a nuestro programa.
Con arreglos ya podemos entender completamente al metodo main; es un metodo publico
(porque tiene que llamarse desde fuera de la clase, lo llama la JVM), es estatico, porque no
95
puede ser llamado por ningun objeto (es el primer metodo que se llama, de donde podramos
sacar un objeto para llamarlo?), no regresa nada (su tipo de regreso es void; ningun otro es
permitido), por supuesto se llama main, y recibe un arreglo de cadenas como parametro.
Por que se le pasa un arreglo de cadenas a main? La razon es para pasarle argumentos a
un programa. Por ejemplo, si queremos pasarle argumentos a UsoBaseDeDatosAgenda, hacemos
lo siguiente con el objetivo run de nuestro build.xml:
Que estamos haciendo aqu? La etiqueta arg (de argument o argumento) nos permite
pasarle argumentos al programa. En este caso, "12.5", "123" y "Hola mundo" seran los argu-
mentos del programa. Dentro de main, tendramos acceso a ellos con args [0] , args [1] y args [2]
respectivamente.
Hay que recordar que son cadenas; el primer argumento se interpreta como la cadena "12.5",
no como el flotante 12.5; y el segundo argumento se interpreta como la cadena "123", no como
el entero 123. Esta es la unica manera de pasarle parametros al metodo main.
7.4. Ejercicios
Nuestra Base de Datos de agenda tiene un grave problema, los registros que damos de
alta durante la ejecucion del programa dejan de existir cuando el programa termina. En ese
momento desaparecen.
Los ejercicios de esta practica consistiran en que la Base de Datos de agenda tenga memoria
permanente, o sea, que sea capaz de guardar su estado en disco duro.
Para escribir en disco duro utilizaremos la clase SalidaADisco, heredera de la clase Salida.
La documentacion de la clase puedes consultarla con el auxilio del ayudante.
Utiliza la clase UsoEntradaSalidaDisco para que veas un ejemplo de como utilizar la clase
SalidaADisco.
Como puedes ver, la funcion es concreta. Con esto hacemos que todas las Bases de Datos
que extiendan a BaseDeDatos puedan guardar sus registros. En particular, si el metodo
96
funciona correctamente, nuestra clase BaseDeDatosAgenda podra guardar sus registros sin
necesidad de hacerle nada.
El metodo debe guardar el numero de registros que hay en la Base de Datos: la longitud
de la lista. Despues debe guardar los registros.
Para guardar los registros, asume que la clase Registro tiene un metodo que se llama
guardaRegistro que recibe un objeto heredero de la clase Salida. Gracias a ello, para guardar
los registros solo tendremos que recorrer la lista y pedirle a cada registro que se guarde.
Si asumimos el metodo guardaRegistro, podemos hacer el metodo guardaBaseDeDatos en
la clase abstracta, ya que no tenemos que saber nada de los registros para pedirles que se
guarden. Solo necesitamos saber que se pueden guardar.
El objeto heredero de la clase Salida que recibe nuestro metodo como parametro es un
objeto prestado. No le corresponde al metodo guardaBaseDeDatos crearlo; lo recibe ya
creado. Lo usamos para guardar el numero de registros, y luego se lo pasamos al metodo
guardaRegistro cuando guardemos cada uno de los registros, pero no lo construimos ni lo
cerramos.
El crear el objeto y cerrar el flujo se realizara afuera de este metodo.
2. En la clase Registro declara la funcion abstracta guardaRegistro de la siguiente manera
Con esto forzamos que todos los objetos de alguna clase heredera de Registro tendran un
metodo guardaRegistro. El metodo del primer ejercicio necesita eso.
Ahora implementa el metodo concreto guardaRegistro en la clase RegistroAgenda (tienes
que hacerlo; si no lo haces la clase RegistroAgenda dejara de compilar).
Igual que el metodo guardaBaseDeDatos, guardaRegistro recibe prestado el objeto heredero
de la clase Salida que le pasamos como parametro. No crearemos dentro del metodo al
objeto, ni invocaremos su metodo cierra.
Dentro de guardaRegistro solo guardaremos los campos del registro, usando para ello al
objeto heredero de la clase Salida. Eso es lo unico que hace el metodo.
3. Implementa en la clase Registro el metodo abstracto recuperaRegistro con la siguiente
firma:
public abstract R e g i s t r o r e c u p e r a R e g i s t r o ( E n t r a d a e n t ) ;
Igual que en el ejercicio anterior, con esto forzamos a todas las clases herederas de Registro
a que tengan un metodo recuperaRegistro para que el registro recupere otro registro del
disco duro.
Hay que implementar el metodo recuperaRegistro en la clase RegistroAgenda (si no, no
compilara):
public R e g i s t r o r e c u p e r a R e g i s t r o ( E n t r a d a e n t ) {
...
97
Este metodo es la razon por la que en la practica pasada hicimos un constructor sin
parametros a la clase RegistroAgenda. Cuando creemos un registro sin campos
R e g i s t r o A g e n d a r = new R e g i s t r o A g e n d a ( ) ;
R e g i s t r o A g e n d a nuevo ;
nuevo = ( R e g i s t r o A g e n d a ) r . r e c u p e r a R e g i s t r o ( edd ) ;
El objeto ra solo lo usamos para que la clase BaseDeDatos sepa como leer del disco duro
los registros.
98
5. Has usado los metodos leeString, leeInteger, leeFloat, etc. para obtener entrada del usuario
por el teclado. Sin embargo es incomodo para el usuario tener que estar escribiendo
respuestas una por una. Lo ideal sera que pudiera contestar varias preguntas de una vez.
Para esto, la clase Consola ofrece las funciones hazPreguntas y dameRespuestaXXX. La
primera recibe un arreglo de preguntas
con lo que una ventana de dialogo aparece donde se permite contestar todas las preguntas
que esten en el arreglo. La ventana tiene un boton de Aceptar, para que se guarden
en memoria las respuestas del usuario, y otro boton de Cancelar para cancelar las
preguntas. hazPreguntas regresa true si el primer boton es presionado, y false si se presiona
el segundo o se cierra la ventana.
Para obtener las respuestas, se utilizan las funciones dameRespuestaXXX, donde XXX es
Integer, String, etc. Si se quiere obtener la primer respuesta como una cadena y la segunda
como un entero se hace
99
i ) Pedir un nombre y buscarlo en la nueva Base de Datos recuperada.
Por su puesto, todo esto podra hacerse de una manera mucho menos rebuscada, pero
queremos
6. Por supuesto todos los metodos deben tener sus correspondientes comentarios para JavaDoc.
7.5. Preguntas
1. En esta practica para guardar la Base de Datos solo guardamos el numero de registros
y despues los registros. Se te ocurre una mejor manera de guardar la Base de Datos?
Justifica.
2. Crees que sera mejor si la Base de Datos utilizara un arreglo en lugar de una lista?
Justifica.
100
Practica 8
Recursion
8.1. Meta
Que el alumno aprenda a usar recursion.
8.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
8.3. Desarrollo
Podemos hacer llamadas de funciones dentro de otras funciones. Dentro del metodo main
en nuestras clases de uso llamamos a varias funciones.
Dentro de esas mismas funciones podemos llamar a otras funciones. El metodo guard-
aBaseDeDatos de la clase BaseDeDatos llama al metodo guardaRegistro de la clase Registro.
Si podemos llamar funciones desde funciones, esto lleva a una pregunta casi inmediata;
que pasa si llamamos a una funcion dentro de ella misma? Y la respuesta es la recursion.
101
El factorial de un numero n N {0}, que denotaremos como n!, es
1 si n = 0
int f a c t o r i a l ( int n ) {
// C l a u s u l a de e s c a p e .
i f ( n == 0) {
return 1 ;
}
return n f a c t o r i a l ( n 1);
}
Lo que hace entonces factorial es llamarse a s misma con un valor distinto cada vez; esa
es la idea basica de la recursion. La comprobacion que aparece en el metodo debe aparecer en
toda funcion recursiva; se le denomina clausula de escape o caso base, y nos permite escapar
de la funcion.
Si una funcion recursiva no tiene clausula de escape o esta mal definida, nunca podremos
salir de ella. El programa quedara atrapado para siempre en esa funcion, y eso es algo que
nunca debe ocurrir. A quedar atrapado en una funcion recursiva se le llama muchas veces caer
en loop.
Dijimos que la funcion factorial es un mal ejemplo de recursion. Esto es porque podemos
hacer factorial iterativamente muy facil
int f a c t o r i a l ( int n ) {
int r ;
for ( r = 1 ; n > 0 ; n) {
r = r n ;
}
return r ;
}
En Java, una funcion recursiva sera siempre (o en la mayora de los casos) mas cara (en
recursos de la computadora) que una funcion no recursiva. La recursion afecta tanto en velocidad
como en memoria.1 Sin embargo, el enfoque recursivo es elegante en extremo, y puede simplificar
el codigo de manera significativa.
En el caso de factorial, no tiene sentido hacerla recursiva. La version iterativa es facil de
entender y no complica el algoritmo. Pero hay algoritmos en los que la version iterativa es
1
Otra vez: el aumento en la velocidad de las computadoras y la existencia de compiladores cada vez mas
inteligentes hacen esta diferencia cada vez menos perceptible. Mas la diferencia esta ah.
102
extremadamente compleja, y en estos casos siempre es mejor utilizar la recursion, como veremos
enseguida.
El reto es relativamente sencillo; tenemos que mover los discos del primer poste al tercero,
con la siguiente restriccion: no podemos poner nunca un disco sobre otro de diametro menor.
Como movemos los n discos del primer al segundo poste? Recursivamente es sencillo;
nuestro caso base (el que nos da la pauta para la clausula de escape), es cuando solo tenemos
un disco. En este caso, solamente hay que mover el disco del primer poste al tercero.
Ahora solo falta la recursion, que es como mover n discos.
La recursion es un poco mas complicada, ya que tenemos la restriccion de que no podemos
poner un disco sobre otro de menor diametro. Pero como comenzamos con los discos ordenados,
solo hay que encontrar como no romper el orden.
Esto resulta sencillo ya que tenemos tres postes; entonces para mover n discos del primer
poste al segundo poste, primero movemos n1 discos del primer poste al segundo, ayudandonos
con el tercero, movemos el disco mas grande del primer al tercer poste (ya sabemos como mover
un solo disco), y despues movemos de nuevo los n 1 discos del segundo poste al tercero,
ayudandonos con el primero.
Y eso resuelve el problema:
/
Mueve un d i s c o d e l p o s t e p1 a l p o s t e p2 .
/
public void mueveDisco ( P o s t e p1 , P o s t e p2 ) {
// Movemos e l d i s c o de h a s t a a r r i b a d e l p o s t e p1 a l p o s t e p2 .
}
/
Pasa n d i s c o s d e l p o s t e p1 a l p o s t e p2 , ayudandose
con e l p o s t e p3 .
/
public void m u e v e D i s c o s ( P o s t e p1 , P o s t e p2 , P o s t e p3 , int n ) {
103
i f ( n == 1) {
mueveDisco ( p1 , p2 ) ;
} else {
m u e v e D i s c o s ( p1 , p3 , p2 , n 1);
mueveDisco ( p1 , p2 ) ;
m u e v e D i s c o s ( p3 , p2 , p1 , n 1);
}
}
Actividad 8.1 Con la asistencia de tu ayudante, baja el archivo Jar hanoi.jar. El archivo
contiene una muestra grafica de las Torres de Hanoi. Ejecutala con la siguiente lnea de
comandos:
El parametro 5 es el numero de discos a utilizar. Prueba con mas discos, si no tienes nada
mejor que hacer.
Puedes comprobar que el programa de ejemplo utiliza el algoritmo de arriba, paso por paso.
La recursion tiene mucho que ver con la induccion matematica. En la induccion matematica,
solo hay que ver un caso base, asumirlo para n = k y demostrarlo para n = k + 1. El mismo
principio se aplica aqu; decimos como manejar nuestro caso base (la clausula de escape),
asumimos que funciona para n 1 y lo hacemos para n.
Las Torres de Hanoi es un algoritmo doblemente recursivo; hace dos llamadas a s misma
dentro de la funcion. El algoritmo iterativo de las Torres de Hanoi ocupa varias paginas de
codigo, en comparacion de las menos de quince lneas que utilizamos.
Hay una leyenda oriental que dice que si algun da alguien termina de jugar las Torres de
Hanoi con 64 discos, el Universo dejara de existir. Sin embargo no hay que preocuparse; si
se moviera una pieza por segundo, uno tardara mas de quinientos mil millones de anos en
terminar de jugar.
Las Torres de Hanoi es uno de los ejemplos de problemas que tienen solucion, que la solucion
puede ser llevada a cabo por una computadora, y que sin embargo con una entrada no muy
grande (64 en este caso) tardara mucho tiempo en dar el resultado, haciendolos para todo caso
practico intratables.
104
int l o n g i t u d ( L i s t a l i s t a ) {
i f ( l i s t a == null ) {
return 0 ;
}
return 1+ l o n g i t u d ( l i s t a . s i g u i e n t e ( ) ) ;
}
Cuando usemos listas con recursion, generalmente la clausula de escape sera comprobar si
la lista es nula.
Hasta ahora, habamos utilizado iteraciones para tratar con las listas. A partir de ahora,
trataremos de usar recursion siempre que sea posible, ya que es la forma natural de tratarlas
dada su definicion. Ademas, hara mas legible nuestro codigo y nos dara mas herramientas al
momento de atacar problemas.
Sin embargo, debera quedar claro que las listas pueden tratarse recursiva o iterativamente.
i f ( i >= m i A r r e g l o . l e n g t h ) {
return ;
}
De hecho, habra ocasiones en que tendra mas sentido que en el ejercicio de arriba.
8.4. Ejercicios
1. Cambia las funciones agregaRegistro y quitaRegistro de la clase BaseDeDatos para que
funcionen recursivamente.
Debes probar que los metodos funcionen igual que antes en la clase de uso. Es posible
que necesites una funcion auxiliar (privada seguramente) para que esa sea sobre la que
realmente se haga la recursion.
La recursion puede ser enganosa; presta atencion a los parametros y a los valores de
regreso. Y sobre todo no pierdas nunca de vista la clausula de escape.
105
2. Cambia los metodos guardaBaseDeDatos y recuperaBaseDeDatos de la clase BaseDeDatos
para que funcionen recursivamente.
Date cuenta que de nuevo no debemos cambiar los comentarios de JavaDoc; las funciones
siguen haciendo lo mismo, aunque ya no lo hacen de la misma forma. Por esto documentamos
que hace una funcion, no como.
8.5. Preguntas
1. Como te parece mejor que funcionan los metodos de la clase BaseDeDatos, iterativa o
recursivamente? Justifica en detalle.
2. Si te das cuenta, hemos hasta ahora detenido el diseno de la Base de Datos, ya solo
hemos cambiado como se hacen por dentro las cosas. Como crees que podramos mejorar
el diseno de la Base de Datos?
106
Practica 9
Manejo de Excepciones
9.1. Meta
Que el alumno aprenda a manejar y crear excepciones.
9.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
manejar excepciones, y
9.3. Desarrollo
Cuando en nuestra Base de Datos buscamos un registro y este no existe, regresamos null.
Esta es una practica muy comun en computacion: regresar un valor especial en caso de error o
de que algo extrano haya ocurrido. Valores como null, -1, o false son muy usados para reportar
una situacion extraordinaria.
Empero, al complicar mas un programa, el numero de errores que podemos detectar va
creciendo, y estar asignando un valor distinto de retorno a cada tipo de error puede resultar
complicado (tenemos un monton de numeros negativos, pero solo un null).
Ademas, si estamos dentro de una funcion recursiva, es posible que tengamos que escapar
de la recursion tan rapido como sea posible, y eso muchas veces significa saltarse de alguna
manera la clausula de escape, y descargar la pila de ejecucion del metodo.
Para todo esto surgio la idea de las excepciones.
107
9.3.1. Uso de excepciones
La idea detras de las excepciones es justamente eso: situaciones de excepcion en las que el
programa debe de alguna manera detenerse a pensar que esta ocurriendo, ver si puede manejar
la situacion, o morir graciosamente.
Un programa nunca debe explotarle en la cara a un usuario. Si un error ocurre debe entonces
tratar de encontrar la manera de continuar la ejecucion del programa, o terminar con un mensaje
de error claro y conciso.
La idea es sencilla: cuando haya metodos que se prestan a situaciones de excepcion, tendran
la capacidad de lanzar excepciones (throw en ingles).
Si existe la posibilidad de que un metodo usado por el programa lance una o mas excepciones,
primero debera intentarse (try en ingles) ejecutar el metodo. Si la situacion extraordinaria
ocurre en el intento, la funcion lanzara una excepcion (solo una), que debe ser atrapada (catch
en ingles).
Por ultimo (finally), habra cosas que hacer despues de ejecutar la funcion, independiente-
mente de si algo malo ocurrio en el intento.
Todo esto se logra con los bloques try ... catch ... finally .
try {
// I n v o c a c i o n a metodos p o t e n c i a l m e n t e p e l i g r o s a s .
} catch ( A l g u n a E x c e p c i o n e1 ) {
// Manejo de e r r o r ( e x c e p t i o n h a n d l e r ) .
} catch ( O t r a E x c e p c i o n e2 ) {
// Manejo de e r r o r ( e x c e p t i o n h a n d l e r ) .
} catch ( UnaExcepcionMas e3 ) {
// Manejo de e r r o r ( e x c e p t i o n h a n d l e r ) .
...
} catch ( U n a U l t i m a E x c e p c i o n eN ) {
// Manejo de e r r o r ( e x c e p t i o n h a n d l e r ) .
} finally {
// Limpieza .
}
Dentro del bloque try se ejecutan funciones peligrosas. Con peligrosas queremos decir que
mientras la funcion se ejecute pueden ocurrir situaciones de excepcion, que dentro de la misma
funcion no hay manera de manejar.1
En estas practicas ya hemos visto clases en las que dentro de las funciones pueden ocurrir
muchsimas excepciones; las clases del paquete icc1.es. Cuando hacemos entrada y salida, hay
muchos errores potenciales, como por ejemplo:
108
que no podamos conectarnos a una maquina del otro lado del mundo,
Podemos seguir con muchas otras, pero lo importante es que al efectuar entrada y salida no
podemos asegurar ninguna de las operaciones; abrir, escribir/leer, cerrar.
Dentro del bloque try envolvemos entonces a las posibles situaciones de excepcion
try {
/ Abrimos e l a r c h i v o . /
B u f f e r e d R e a d e r i n = null ;
i n = new B u f f e r e d R e a d e r ( new F i l e R e a d e r ( "datos.txt" ) ) ;
nombre = in . readLine ( ) ;
direccion = in . readLine ( ) ;
S t r i n g tmp = i n . r e a d L i n e ( ) ;
t e l e f o n o = I n t e g e r . p a r s e I n t ( tmp ) ; // Convertimos a e n t e r o .
in . close ( ) ;
}
Por supuesto, dentro del bloque try puede haber instrucciones inofensivas; la idea es agrupar
a un conjunto de instrucciones peligrosas en un solo bloque try (aunque se mezclen instrucciones
inofensivas), para no tener que utilizar un try para cada instruccion peligrosa.
En el momento en que una de las funciones lance una excepcion, la ejecucion del try se
detendra y se pasara a los bloques catch, tambien llamados manejadores de excepcion. Es
importante entender que la ejecucion se detendra en el momento en que la excepcion sea lanzada.
Esto quiere decir que cuando ejecutemos las lneas
B u f f e r e d R e a d e r i n = null ;
i n = new B u f f e r e d R e a d e r ( new F i l e R e a d e r ( "datos.txt" ) ) ;
109
catch ( F i l e N o t F o u n d E x c e p t i o n f n f e ) {
c . p r i n t l n ( "El archivo \" datos.txt \" no existe." ) ;
} catch ( I O E x c e p t i o n i o e ) {
c . p r i n t l n ( "No se pudo leer el archivo." ) ;
}
Por ultimo, la ejecucion pasa al bloque finally (si existe), que se ejecuta haya o no habido
una excepcion. Es para realizar operaciones crticas, sin importar si hubo o no errores en el
bloque try, o el tipo de estos errores, si es que hubo.
finally {
terminaTareas ( ) ;
}
try {
/ Abrimos e l a r c h i v o . /
B u f f e r e d R e a d e r i n = null ;
i n = new B u f f e r e d R e a d e r ( new F i l e R e a d e r ( "datos.txt" ) ) ;
nombre = in . readLine ( ) ;
direccion = in . readLine ( ) ;
S t r i n g tmp = i n . r e a d L i n e ( ) ;
t e l e f o n o = I n t e g e r . p a r s e I n t ( tmp ) ; // Convertimos a e n t e r o .
in . close ( ) ;
}
} catch ( F i l e N o t F o u n d E x c e p t i o n f n f e ) {
c . p r i n t l n ( "El archivo \" datos.txt \" no existe." ) ;
} catch ( I O E x c e p t i o n i o e ) {
c . p r i n t l n ( "No se pudo leer el archivo." ) ;
} finally {
terminaTareas ( ) ;
}
En este ejemplo hay que notar que dentro del try se llevan a cabo todas las operaciones
relacionadas con leer de disco.
El bloque finally es opcional; no es obligatorio que aparezca. Pero debe quedar en claro que
cuando aparece, el bloque finally se ejecuta incondicionalmente, se lance o no se lance ninguna
excepcion. La unica manera de impedir que un finally se ejecute es terminar la ejecucion del
programa dentro del catch (lo que no tiene mucho sentido si existe un bloque finally ).
La parte mas importante al momento de manejar excepciones es, justamente, los mane-
jadores de excepciones. Es ah donde determinamos que hacer con la excepcion.
Para utilizar los manejadores de excepciones, necesitamos utilizar a las clases herederas de
Throwable.
110
Actividad 9.1 Con el apoyo de tu ayudante, consulta la documentacion de la clase Throw-
able.
La clase Throwable
La sintaxis de un manejador de excepcion es
try {
// Cosas p e l i g r o s a s que l a n z a n c h o r r o c i e n t a s e x c e p c i o n e s
// d i s t i n t a s .
} catch ( E x c e p t i o n e ) {
c . i m p r i m e l n ( "Algo malo ocurrio." ) .
}
Sin embargo es considerada una mala practica, ya que perdemos todo el fino control que
nos dan las excepciones (no distinguimos que excepcion estamos atrapando), y sera prohibido
su uso en estas practicas. Empero, atrapar a la clase Exception estara permitido si es al final,
cuando ya se ha escrito el manejador de todas las excepciones posibles que pueden lanzarse.
Lanzamiento de excepciones
Hay dos maneras de manejar las excepciones dentro de nuestros metodos. La primera es
la que ya vimos, atrapar las excepciones en un bloque try ... catch ... finally . La segunda es
encadenar el lanzamiento.
111
Encadenar el lanzamiento significa que asumimos que nuestro metodo no es el responsable
de manejar la excepcion, sino que debe ser responsabilidad de quien sea que llame a nuestro
metodo. Entonces, nuestro metodo en lugar de atrapar la excepcion volvera a lanzarla.
Piensen en la situacion de excepcion como en una papa caliente, y en los metodos como una
fila de personas. La primera persona (o sea metodo) que de repente tenga la papa caliente en
las manos, tiene la opcion de manejar la papa (resolver el problema, manejar la excepcion), o
pasarselo a la persona que sigue, lavandose las manos. La papa (o excepcion) puede entonces
ir recorriendo personas (metodos) en la fila (en la pila de ejecucion) hasta que algun metodo
maneje la excepcion en lugar de lanzarla de nuevo.
Para lanzar de nuevo la excepcion, tenemos que hacer especfico que nuestro metodo puede
lanzar esa excepcion utilizando la clausula throws en el encabezado, por ejemplo:
Una vez que nuestro metodo miFuncionLanzadora declara que puede lanzar a la excepcion
AlgunaExcepcion, ya no es necesario que envolvamos con un try a ninguna funcion que pudiera
lanzar tambien a la excepcion AlgunaExcepcion. El lanzamiento queda encadenado automatica-
mente.
Por supuesto, podemos manejar distintos tipos de papas calientes; nada mas debemos es-
pecificarlo en el encabezado
Un metodo puede ser capaz de lanzar un numero arbitrario de excepciones, y cuando sea
llamado el metodo debera ser dentro de un bloque try (a menos que la funcion que llame al
metodo tambien lance las mismas excepciones).
Si llamamos a un metodo que puede lanzar excepciones, y el metodo desde donde lo llamamos
no lanza las mismas excepciones, entonces hay que envolver la llamada con un try; de otra
manera, el compilador protestara.
El metodo main puede lanzar excepciones, que son atrapadas por la JVM. Es una forma de
probar metodos que lanzan excepciones sin necesitar escribir los bloques try ... catch ... finally
(sencillamente encadenamos main al lanzamiento de las excepciones).
112
public c l a s s R e g i s t r o D u p l i c a d o extends E x c e p t i o n { }
Se daran cuenta de que lanzar las excepciones es algo que se decide en tiempo de ejecucion
(por eso los if s). No tiene sentido hacer una funcion que siempre lance una excepcion. Las
excepciones son as, singulares, y deben ser lanzadas despues de hacer una evaluacion acerca
del estado del programa (como ver que un nuevo registro es identico a uno ya existente).
113
9.3.3. El flujo de ejecucion
Al momento de hacer un throw, la ejecucion de la funcion termina, y la excepcion recorre to-
das las funciones que han sido llamadas, deteniendolas, hasta que encuentra un bloque catch que
lo atrape. Eso rompe cualquier algoritmo recursivo, cualquier iteracion y en general cualquier
flujo de ejecucion, lo que hace al throw mucho mas poderoso que el return.
Dado que se pueden descartar muchas llamadas a funciones que se hayan hecho (desmontandolas
en la pila de ejecucion), una excepcion en general debe tratar de manejarse mas que de repararse.
Repararla requerira que se volvieran a hacer las llamadas detenidas por el lanzamiento de la
excepcion, con el posible riesgo de que el error ocurra de nuevo.
Lo sensato es dar un mensaje al usuario de que lo que pidio no puede realizarse, y regresar
a un punto de ejecucion seguro en el programa (el menu inicial o la pantalla de inicio).
114
9.4. El paquete java.io
Hasta ahora, las excepciones de nuestro archivo Jar icc1.jar haban sido suprimidas. Para
que se den una idea, las excepciones se manejaban as
try {
r = Integer . parseInt ( s );
} catch ( NumberFormatException n f e ) {
System . e r r . p r i n t l n ( "*** ERROR ***: \""+s+
"\" no es un byte valido." ) ;
System . e r r . p r i n t l n ( "*** ERROR ***: Ejecucion "+
"del programa detenida." ) ;
System . e x i t ( 1 ) ;
}
El metodo exit de la clase System (como ya habran adivinado) obliga a que un programa
termine.
El manejo de excepciones se suprimio para que no tuvieran la necesidad de preocuparse por
ellas y se pudieran concentrar en resolver los problemas que se les planteaban.
Mas las excepciones son de las caractersticas que hacen de Java un buen lenguaje de
programacion, y su uso debe ser fomentado y diseminado.
A partir de esta practica tendran que lidiar con las excepciones. Recibiran nuevas versiones
de los paquetes icc1.interfaz e icc1.util que lanzaran algunas excepciones, aunque todas son
herederas de RuntimeException (lo que quiere decir que no es necesario que las envuelvan en un
try con su correspondiente catch).
Sin embargo, el paquete java.es es especial. La entrada y salida tal y como la maneja Java
deberan ser capaces de manejarla sin muchos problemas; excepto por el uso de excepciones.
Como la entrada y salida es una de las fuentes mas amplia de excepciones, el unico motivo para
la existencia de java.es era que ustedes no tuvieran que lidiar con ellas.
Por lo tanto, el paquete java.es desaparecera; para manejar entrada y salida deberan utilizar
las clases del paquete java.io, con sus correspondientes excepciones. Deberan ser capaces de
entender estas clases consultando solo la documentacion de este paquete.
Sin embargo, para facilitarles algo la vida, todo el codigo fuente de los paquetes que se han
visto en el curso, incluyendo los ejemplos, estara disponible para que lo consulten. Entre el
codigo se encuentra un ejemplo de entrada/salida usando las clases de Java; lo necesitaran para
que sus clases compilen utilizando las nuevas bibliotecas.
Actividad 9.2 Preguntale a tu ayudante de donde bajar las nuevas bibliotecas que se
utilizaran en el curso; el archivo se llama icc1.jar tambien.
Tambien baja los archivos noexcepciones.tar.gz, excepciones.tar.gz, y
ejemplos.tar.gz. El primero tiene las bibliotecas que utilizamos en el curso, las
que no usan excepciones. El segundo tiene el codigo fuente de las nuevas bibliotecas. El
tercero tiene el codigo fuente de los ejemplos vistos en el curso. En particular, revisa la
clase UsoJavaIO, porque muestra como utilizar las clases de Java para escribir y leer del
disco duro.
115
9.5. Ejercicios
Baja las nuevas versiones de los paquetes que se han utilizado. El nuevo archivo se llama
tambien icc1.jar; asegurate de que sea el correcto, porque tienen que compilar tus clases con
las nuevas versiones.
Los paquetes y clases se llaman igual; icc1.util.Lista, icc1.interfaz.Consola, etc. Nada mas ya
no existe el paquete icc1.es, y tampoco la clase icc1.util.ListaDeCadena. Todo el codigo de todas
las clases esta disponible para que lo consultes.
Comprueba que realmente estes usando la biblioteca correcta; el ayudante comprobara que
el codigo compile con la que debe.
1. Haz que tu Base de Datos (tal y como la dejaste la practica pasada) compile con los
nuevos paquetes.
Lo mas obvio sera que el compilador se quejara de que no existe ninguna clase del paquete
icc1.es. Reemplaza su uso con clases del paquete java.io. Utiliza el ejemplo proporcionado
para ver que tienes que cambiar.
Vas a tener que utilizar bloques try ... catch ... finally para manejar las excepciones que
lanzan los nuevos paquetes. Puedes hacer lo que creas conveniente en los manejadores de
excepcion, pero trata de hacer algo. No siempre se puede; en particular, si algo sale mal
al leer o escribir en disco, realmente no hay mucho que puedas hacer. Eso s, continua la
ejecucion del programa (no uses el metodo exit de la clase System).
Recuerda: las excepciones son clases. Para utilizar las excepciones de un paquete, debes
importar la excepcion al inicio de la clase. Por ejemplo, para atrapar la excepcion FileNot-
FoundException, del paquete java.io, debes poner al inicio de tu clase
import j a v a . i o . F i l e N o t F o u n d E x c e p t i o n ;
Tienes que crear las clases de las excepciones. Llamalas como tu quieras y utiliza la
jerarqua de clases que consideres necesaria.
Una excepcion muy usada en la biblioteca de clases de Java es la excepcion NullPoint-
erException, que es lanzada cada vez que se trata de usar una referencia nula (null) en un
contexto en el que esta prohibido. Esta excepcion es heredera de la clase RuntimeException,
as que no es obligatorio atraparla.
Haz que tus metodos lancen tambien la excepcion NullPointerException cuando el parametro
que reciban sea null. No es necesario usar ningun import para usar la clase NullPoint-
erException, ya que esta en el paquete java.lang.
Una vez modificadas las funciones, tendras que modificar la clase de uso, ya que se nece-
sitara atrapar las excepciones para que compile la clase de nuevo.
116
3. Cuando modifiques las funciones agregaRegistro y borraRegistro, modifica los comentarios
de JavaDoc utilizando la etiqueta @throws para especificar que excepciones lanza y cuando
las lanza.
Tambien comenta las clases de tus excepciones.
9.6. Preguntas
1. Crees que es util el manejo de excepciones? Justifica a detalle.
2. Que otras excepciones crees que podran crearse para nuestra Base de Datos? Explica.
117
118
Practica 10
Interfaces Graficas
10.1. Meta
Que el alumno aprenda a crear interfaces graficas.
10.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
10.3. Desarrollo
La mayor parte del mundo utiliza la computadora solo como una herramienta. Escriben
trabajos en ellas, calculan sus impuestos, organizan su agenda, platican con alguien del otro
lado del mundo, le mandan un correo electronico al hijo que esta estudiando lejos, etc. Para
hacer todo eso necesitan aplicaciones sencillas de usar, que les permita realizar su trabajo y
pasatiempos sin muchas complicaciones.
Cuando se dice que estas aplicaciones deben ser sencillas de usar, muchos coinciden en que
la mejor manera de facilitarle la vida al usuario es que el programa se comunique con el a traves
de Interfaces Graficas de Usuario, IGUs, o GUIs por las mismas siglas en ingles.
(El hecho de que las interfaces graficas nos compliquen la vida a los programadores no parece
importarle mucho a los usuarios. . . )
A lo largo de estas practicas hemos utilizado interfaces graficas sencillas, aunque ustedes
mismos no las han programado. En esta practica veremos como se programan las interfaces
119
graficas, y discutiremos la caracterstica mas importante de ellas, y que se ha evitado inten-
cionalmente hasta ahora. Nos referimos al manejo de eventos.
Las primeras versiones de Java utilizaban las clases del paquete java.awt para crear interfaces
graficas. Las ultimas versiones utilizan las clases del paquete javax.swing, que sera el definitivo
una vez que las aplicaciones escritas con AWT sean convertidas a Swing. En esta practica
utilizaremos Swing.
Actividad 10.1 Con el apoyo de tu ayudante, ve las clases del paquete javax.swing y ja-
va.awt.
10.3.1. Componentes
Una interfaz grafica en Java esta compuesta de componentes alojados en un contenedor.
Veamos una pequena interfaz grafica que se ha usado a lo largo del curso, la pequena ventana
de dialogo que aparece cuando hacemos
Un componente de la clase JLabel, una etiqueta. Las etiquetas generalmente seran solo
usadas para mostrar algun tipo de texto, aunque pueden mostrar imagenes (pixmaps).
120
JDialog
JPanel
JLabel
JTextField
El dialogo, que en este sencillo ejemplo es la ventana principal, sirve mas que nada para
poner juntos a los demas componentes. Es un contenedor de primer nivel.
El panel es un contenedor intermedio. Su proposito en la vida es ayudarnos a acomodar
otros componentes.
La etiqueta y la caja de texto son componentes atomicos, componentes que no contienen a
otros componentes, sino que por s mismos muestran o reciben informacion del usuario.
En el pequeno diagrama de la jerarqua de componentes nos saltamos algunos componentes
que hay entre el dialogo y el panel. Sin embargo, la mayor parte de las veces no habra que
preocuparse de esos componentes.
Para crear esa interfaz grafica, el codigo fundamental es
p a n e l . add ( e t i q u e t a ) ;
p a n e l . add ( c a j a T e x t o ) ;
d i a l o g . g e t C o n t e n t P a n e ( ) . add ( p a n e l ) ;
d i a l o g . pack ( ) ;
d i a l o g . s e t V i s i b l e ( true ) ;
121
10.3.3. Eventos
Ya sabemos como se ponen algunos componentes en una pequena ventana. Ahora, como
hacemos para que la interfaz reaccione a lo que el usuario haga? Por ejemplo, la caja de texto
de nuestro dialogo debe cerrar el dialogo cuando el usuario presione la tecla ENTER en ella.
Esto se logra con el manejo de eventos. Un evento ocurre cuando el usuario genera algun
cambio en el estado de un componente: teclear en una caja de texto, hacer click con el raton
en un boton, cerrar una ventana.
Si existe un escucha (listener en ingles) para el objeto que emitio el evento, entonces po-
dra ejecutar su manejador del evento. Ademas, un objeto puede tener mas de un escucha, cada
uno de los cuales ejecutara su manejador del evento si el evento ocurre.
Entender como se emiten los eventos y como se manejan es lo mas complicado de ha-
cer interfaces graficas. Lo demas es seguir algunas recetas para utilizar los distintos tipos de
componentes, que son muchos, pero que todos funcionan de manera similar.
Escuchas
Los escuchas son interfaces; son como clases vacas que nos dicen que tipos de eventos se
pueden emitir, pero que nos dejan la libertad de implementar que hacer cuando los eventos
sucedan.
Hagamos un ejemplo completo: una pequena ventana con una etiqueta para mensajes y un
boton. Primero hagamoslo sin escuchas
1 package i c c 1 . e j e m p l o s ;
2
3 import j a v a x . s w i n g . JFrame ;
4 import j a v a x . s w i n g . JButton ;
5 import j a v a x . swing . JLabel ;
6 import j a v a x . swing . JPanel ;
7 import j a v a x . swing . BorderFactory ;
8 import j a v a . awt . B o r d e r L a y o u t ;
9 import j a v a . awt . G r i d L a y o u t ;
10 import j a v a . awt . C o n t a i n e r ;
11
12 public c l a s s EjemploBoton {
13 public s t a t i c void main ( S t r i n g [ ] a r g s ) {
14 JFrame f r a m e = new JFrame ( "Ejemplo de Boton" ) ;
15 JButton boton = new JButton ( "Haz click" ) ;
16 J L a b e l e t i q u e t a = new J L a b e l ( "Esto es una etiqueta" ) ;
17
18 J P a n e l p a n e l = new J P a n e l ( ) ;
19 panel . setBorder ( BorderFactory . createEmptyBorder (5 ,5 ,5 ,5));
20 p a n e l . s e t L a y o u t (new G r i d L a y o u t ( 0 , 1 ) ) ;
21
22 p a n e l . add ( boton ) ;
23 p a n e l . add ( e t i q u e t a ) ;
24
122
26 c . add ( p a n e l , B o r d e r L a y o u t . CENTER ) ;
27
28 f r a m e . pack ( ) ;
29 f r a m e . s e t V i s i b l e ( true ) ;
30 }
31 }
Que hace el programa? En las lneas 14, 15 y 16 crea los tres principales componentes
de nuestra aplicacion: nuestra ventana principal (JFrame), nuestro boton (JButton), y nuestra
etiqueta (JLabel). Esos son los componentes que el usuario vera directamente.
Lo siguiente que hace el programa es crear un panel (lnea 18). El unico proposito del panel
es agrupar a nuestro boton y a nuestra etiqueta en la ventana principal. Lo primero que hace
es ponerse un borde de 5 pixeles alrededor [19], y definirse un administrador de trazado [20]; en
este caso es de la clase GridLayout, lo que significa que sera una cuadrcula. Ya que le pasamos
como parametros 0 y 1, se entiende que tendra un numero no determinado de renglones (de
ah el 0), y una sola columna. Lo ultimo que hace el panel es anadirse el boton y la etiqueta
[22,23].
El programa despues obtiene la ventana contenedora de la ventana principal [25] (todos los
contenedores de primer nivel tienen una ventana contenedora), y a aquel le anadimos el panel
[26]. Para terminar, la ventana principal se empaca y se hace visible. Durante el proceso de
empacado se calculan los tamanos de los componentes, para que automaticamente se defina el
tamano de la ventana principal.
El programa no hace nada interesante (ni siquiera termina cuando se cierra la ventana). El
boton puede ser presionado cuantas veces queramos; pero nada ocurre porque no le anadimos
ningun escucha. Cuando el usuario hace click en el boton se dispara un evento; pero no hay
ningun manejador de evento que se encargue de el.
Vamos a escribir un escucha para nuestro ejemplo. Ya que el evento que nos interesa es el
click del boton, utilizaremos un escucha de raton, o mouse listener, que es una interfaz que
esta declarada como sigue
1 package j a v a . awt . e v e n t ;
2 public i n t e r f a c e M o u s e L i s t e n e r extends E v e n t L i s t e n e r {
3 public void m o u s e C l i c k e d ( MouseEvent e ) ;
4 public void mouseEntered ( MouseEvent e ) ;
5 public void m o u s e E x i t e d ( MouseEvent e ) ;
6 public void m o u s e P r e s s e d ( MouseEvent e ) ;
7 public void m o u s e R e l e a s e d ( MouseEvent e ) ;
8 }
(Aunque se pueden escribir escuchas propios, Java provee ya los escuchas mas utilizados
para distintos eventos, por lo que no hay necesidad de hacerlos.)
123
Esto nos dice que los escuchas de raton ofrecen metodos para cuando el raton haga click sobre
el componente (mouseClicked), para cuando el raton se posa sobre el componente (mouseEntered),
para cuando el raton deja de estar sobre el componente (mouseExited), para cuando se presiona
el boton del raton sin soltarlo (mousePressed), y para cuando se suelta el boton del raton despues
de presionarlo (mouseRelease).
Si queremos hacer un escucha para nuestro boton, debemos crear una clase que implemente
la interfaz MouseListener, diciendo que debe hacerse en cada caso:
1 package i c c 1 . e j e m p l o s ;
2
3 import j a v a . awt . e v e n t . M o u s e L i s t e n e r ;
4 import j a v a . awt . e v e n t . MouseEvent ;
5 import j a v a x . s w i n g . J L a b e l ;
6
9 private int c o n t a d o r ;
10 private J L a b e l e t i q u e t a ;
11
12 public MiEscuchaDeRaton ( J L a b e l e t i q u e t a ) {
13 this . e t i q u e t a = e t i q u e t a ;
14 contador = 0;
15 }
16
Realmente solo implementamos la funcion mouseClicked; las otras no nos interesan. Pero
debemos darles una implementacion a todas (aunque sea como en este caso una implementacion
vaca), porque si no el compilador se quejara de que no estamos implementando todas las
funciones de la interfaz MouseListener.
Que hace este manejador de evento? Solo se hace cargo del evento mouseClicked; cuando el
usuario haga click sobre el boton, la etiqueta cambiara su mensaje a Clicks y el numero de
clicks que se hayan hecho.
Para que nuestro boton utilice este escucha solo tenemos que agregarlo (modificando la clase
EjemploBoton)
124
18 MiEscuchaDeRaton e s c u c h a = new MiEscuchaDeRaton ( e t i q u e t a ) ;
19 boton . a d d M o u s e L i s t e n e r ( e s c u c h a ) ;
Adaptadores
Como ocurrio arriba, muchas veces no querremos implementar todas las funciones que ofrece
un escucha. La mayor parte de las veces solo nos interesara el evento de click sobre un boton,
y no nos importara el resto.
Para esto estan los adaptadores. Los adaptadores son clases concretas que implementan a
las interfaces de un escucha, pero que no hacen nada en sus funciones; de esta forma, uno solo
hereda al adaptador y sobrecarga la funcion que le interese. Por ejemplo, el adaptador de raton
(mouse adapter ) es
1 package j a v a . awt . e v e n t ;
2 public c l a s s MouseAdapter implements M o u s e L i s t e n e r {
3 public void m o u s e C l i c k e d ( MouseEvent e ) { }
4 public void mouseEntered ( MouseEvent e ) { }
5 public void m o u s e E x i t e d ( MouseEvent e ) { }
6 public void m o u s e P r e s s e d ( MouseEvent e ) { }
7 public void m o u s e R e l e a s e d ( MouseEvent e ) { }
8 }
No hacen nada sus funciones; pero si unicamente nos interesa el evento del click del raton
solo hay que implementar mouseClicked. Nuestra clase MiEscuchaDeRaton se reducira a
1 package i c c 1 . e j e m p l o s ;
2
12 public MiEscuchaDeRaton ( J L a b e l e t i q u e t a ) {
13 this . e t i q u e t a = e t i q u e t a ;
14 contador = 0;
15 }
16
125
17 public void m o u s e C l i c k e d ( MouseEvent e ) {
18 c o n t a d o r++;
19 e t i q u e t a . s e t T e x t ( "Clicks: "+c o n t a d o r+"." ) ;
20 }
21 }
Como los otros eventos no nos interesan, los ignoramos. No hacemos nada si ocurren. Todos
los escuchas de Java tienen un adaptador disponible para facilitarnos la vida.1
public c l a s s A {
...
class B {
...
}
}
La clase B es clase interna de A; solo puede ser vista dentro de A. Ademas, los metodos de
la clase B pueden hacer referencia a las variables de clase de la clase A, incluso a las privadas.
Esto es posible ya que la clase B es parte de hecho de la clase A.
Las clases internas son muy utiles con los escuchas y adaptadores, ya que nada mas nos
interesa que los vean dentro de la clase donde estamos creando nuestra interfaz grafica.
Cuando compilemos el archivo A.java, que es donde viven las clases A y B, se generaran
dos archivos: A.class, y A$B.class; este ultimo quiere decir que B es clase interna de A.
Podemos entonces crear nuestros escuchas y adaptadores dentro de la clase donde creemos
nuestra interfaz grafica. Pero aun podemos hacer mas: podemos utilizar clases anonimas.
Las clases anonimas son clases creadas al vuelo, que no tienen nombre. Por ejemplo, para
nuestro boton podramos anadirle nuestro escucha de raton al vuelo, de esta manera
126
22 e t i q u e t a . s e t T e x t ( "Clicks: "+c o n t a d o r . i n t V a l u e ()+"." ) ;
23 }
24 });
1 package i c c 1 . e j e m p l o s ;
2
3 import j a v a x . s w i n g . JFrame ;
4 import j a v a x . s w i n g . JButton ;
5 import j a v a x . swing . JLabel ;
6 import j a v a x . swing . JPanel ;
7 import j a v a x . swing . BorderFactory ;
8 import j a v a . awt . B o r d e r L a y o u t ;
9 import j a v a . awt . G r i d L a y o u t ;
10 import j a v a . awt . C o n t a i n e r ;
11 import j a v a . awt . e v e n t . MouseAdapter ;
12 import j a v a . awt . e v e n t . MouseEvent ;
13
14 public c l a s s EjemploBoton {
15 public s t a t i c void main ( S t r i n g [ ] a r g s ) {
16 JFrame f r a m e = new JFrame ( "Ejemplo de Boton" ) ;
17 JButton boton = new JButton ( "Haz click" ) ;
18 f i n a l J L a b e l e t i q u e t a = new J L a b e l ( "Esto es una etiqueta" ) ;
19 f i n a l int c o n t a d o r = { 0 } ;
20
28 J P a n e l p a n e l = new J P a n e l ( ) ;
29 panel . setBorder ( BorderFactory . createEmptyBorder (5 ,5 ,5 ,5));
30 p a n e l . s e t L a y o u t (new G r i d L a y o u t ( 0 , 1 ) ) ;
31
32 p a n e l . add ( boton ) ;
33 p a n e l . add ( e t i q u e t a ) ;
34
38 f r a m e . pack ( ) ;
39 f r a m e . s e t V i s i b l e ( true ) ;
40 }
41 }
127
Actividad 10.5 Vuelve a modificar el archivo EjemploBoton.java para que utilice la clase
anonima. Complalo y ejecutalo de nuevo.
1 package i c c 1 . e j e m p l o s ;
2
16 public c l a s s EjemploBoton {
17
18 private int c o n t a d o r ;
19
20 public EjemploBoton ( ) {
21 contador = 0;
22 JFrame f r a m e = new JFrame ( "Ejemplo de Boton" ) ;
23 JButton boton = new JButton ( "Haz click" ) ;
24 f i n a l J L a b e l e t i q u e t a = new J L a b e l ( "Esto es una etiqueta" ) ;
25
26 f r a m e . a d d W i n d o w L i s t e n e r (new WindowAdapter ( ) {
128
27 public void w i n d o w C l o s i n g ( WindowEvent e v e n t o ) {
28 System . e x i t ( 0 ) ;
29 }
30 });
31
39 J P a n e l p a n e l = new J P a n e l ( ) ;
40 panel . setBorder ( BorderFactory . createEmptyBorder (5 ,5 ,5 ,5));
41 p a n e l . s e t L a y o u t (new G r i d L a y o u t ( 0 , 1 ) ) ;
42
43 p a n e l . add ( boton ) ;
44 p a n e l . add ( e t i q u e t a ) ;
45
49 f r a m e . pack ( ) ;
50 f r a m e . s e t V i s i b l e ( true ) ;
51 }
52
(Anadimos tambien un escucha a la ventana, en las lneas 26-30, para que cuando el usuario
la cierre, el programa termine).
Las clases internas y anonimas fueron pensadas mayormente para interfaces graficas, pero
podemos usarlas cuando y donde queramos. Sin embargo nosotros solo las utilizaremos para
interfaces graficas (y de hecho es lo recomendable).
En los ejercicios de la practica podras utilizar clases normales, internas o anonimas, depen-
diendo de como lo prefieras.
129
Actividad 10.6 Con el apoyo de tu ayudante, consulta la documentacion de la clase JCom-
ponent.
JFrame. (Marco) Son ventanas autonomas y que no dependen de ninguna otra ventana.
Generalmente seran la ventana principal de nuestras aplicaciones.
JDialog. (Dialogo) Son ventanas mas limitadas que los objetos de la clase JFrame. Depen-
den de otra ventana, que es la ventana padre (parent window ) del dialogo. Si la ventana
padre es minimizada, sus dialogos tambien lo hacen. Si la ventana padre es cerrada, se
cierran tambien los dialogos.
Applet. (No tiene sentido en espanol) Emulan a una ventana para correr aplicaciones de
Java dentro de un navegador de la WWW. No los veremos.
Para manejar los eventos de un contenedor de primer nivel, se utiliza un escucha de ventana
o window listener.
Contenedores intermedios
Los contenedores intermedios nos sirven para agrupar y presentar de cierta manera distintos
componentes. Mientras que se pueden realizar varias cosas con ellos, aqu solo daremos una breve
descripcion de cada uno de ellos, y nos concentraremos en los objetos de la clase JPanel en el
ejemplo mas abajo.
130
Ventana corrediza. Provee barras de desplazamiento alrededor de un componente grande
o que puede aumentar mucho de tamano. Se implementa en la clase JScrollPane.
Ventana de carpeta. Contiene multiples componentes, pero solo muestra uno a la vez. El
usuario puede cambiar facilmente entre componentes. Se implementa en la clase JTabbed-
Pane.
Marco interno. Es como un marco, y tiene casi los mismos metodos, pero debe aparecer
dentro de otra ventana. Se implementa en la clase JInternalFrame.
Ventana en capas. Provee una tercera dimension, profundidad, para acomodar compo-
nentes. Se debe especificar la posicion y tamano para cada componente. Se implementa
en la clase LayeredPane.
Ventana raz. Provee soporte detras de los telones para los contenedores de primer nivel.
Se implementa en la clase JRootPane.
Componentes atomicos
Los componentes atomicos sirven para presentar y/o recibir informacion del usuario. Son
los eventos generados por estos componentes los que mas nos interesaran al crear nuestras
interfaces graficas.
Los siguientes son componentes que sirven para recibir informacion del usuario:
Boton, Boton de dos estados, Radio boton. Proveen botones de uso simple.
Los botones normales estan implementados en la clase JButton.
Los botones de dos estados son botones que cambian de estado cuando se les hace click.
Como su nombre lo indica, tienen dos estados: activado y desactivado. Estan implemen-
tados en la clase JCheckBox.
Los radio botones son un grupo de botones de dos estados en los cuales solo uno de ellos
puede estar activado a la vez. Estan implementados en la clase JRadioButton.
Caja de combinaciones. Son botones que al hacer click en ellos ofrecen un menu de
opciones. Estan implementados en la clase JComboBox.
Listas. Muestran un grupo de elementos que el usuario puede escoger. Estan implemen-
tadas en la clase JList.
131
Menus. Permiten hacer menus. Estan implementados en las clase JMenuBar, JMenu y
JMenuItem.
Rangos. Permiten escoger un valor numerico que este en cierto rango. Estan implemen-
tados en la clase JSlider.
Campo de texto. Permiten al usuario escribir una sola lnea de texto. Estan implemen-
tados en la clase JTextField.
Los siguientes son componentes que solo muestran informacion al usuario, sin recibir ninguna
entrada de el:
Pistas. Muestran una pequena ventana con informacion de algun otro componente. Im-
plementadas en la clase JToolTip.
Los siguientes son componentes que presentan informacion con formato, y una manera de
editar esa informacion:
Selector de color. Una interfaz para seleccionar colores. Implementada en la clase JCol-
orChooser.
Soporte para texto. Una serie de componentes para manejo de texto, a distintos niveles
y con distintos grados de complejidad. Esta en las clases JTextArea, JTextComponent,
JTextField, y JTextPane.
132
Diseno de un editor de texto sencillo
Queremos hacer un editor de texto sencillo en una clase llamada Editor. Podra cargar archivos
de texto del disco duro, editarlos y salvarlos de nuevo.
La mayor parte de la interfaz grafica sera un componente que nos permita mostrar y editar
texto. Ademas, nuestra interfaz debera tener un menu de archivo con opciones para abrir,
guardar, salir y crear un nuevo archivo, y otro menu de ayuda con informacion del programa.
Tambien deberemos tener una etiqueta que le diga al usuario si el archivo actual ha sido o no
modificado.
El componente que nos permite mostrar y editar texto es un objeto de la clase JTextArea,
y nuestra ventana principal es un objeto de la clase JFrame. Para acomodar los componentes
usaremos un objeto de la clase JPanel.
Ya con esto, nuestra jerarqua de componentes sera como se ve en la figura 10.3.
JFrame
JMenuBar
Men: Archivo
JMenuItem: Nuevo archivo
JMenuItem: Abrir archivo
JMenuItem: Guardar archivo
JMenuItem: Guardar archivo como...
JMenuItem: Quitar programa
Men: Ayuda
JMenuItem: Acerca de...
JPanel
JTextArea
JLabel
4 package i c c 1 . e j e m p l o s ;
5
133
13 import j a v a x . s w i n g . JMenuItem ; // Elemento de menu .
14 import j a v a x . s w i n g . J F i l e C h o o s e r ; // S e l e c c i o n a d o r de a r c h i v o s . .
15 import j a v a x . s w i n g . JOptionPane ; // D i a l o g o s .
16 import j a v a x . s w i n g . BoxLayout ; // Trazado en c a j a .
17 import j a v a x . swing . KeyStroke ; // T e c l a z o .
18
Que variables de clase vamos a necesitar? En primer lugar, nuestro componente de texto
debera ser una variable de clase para que podamos modificarlo de manera sencilla en todos
nuestros metodos. La etiqueta para decirle al usuario si se ha modificado el texto tambien nos
conviene hacerla una variable de clase. Y siempre es conveniente hacer nuestra ventana principal
una variable de clase.
Usaremos una cadena para saber el nombre del archivo sobre el que estamos trabajando,
y un booleano para saber si el texto ha sido modificado. Vamos ademas a anadir otra cadena
para guardar el nombre del archivo (vamos a necesitarlo despues), y definamos constantes para
el ancho y alto de nuestra ventana:
47 public c l a s s E d i t o r {
48
53 private S t r i n g a r c h i v o O r i g i n a l ; // El nombre d e l a r c h i v o o r i g i n a l .
134
54 private S t r i n g archivo ; // El nombre d e l a r c h i v o a c t u a l .
55 private boolean modificado ;
56
La variable estatica BLOQUE [57], la vamos a usar para leer y escribir del disco duro.
Constructores
Tendremos dos constructores; uno sin parametros, y otro con una cadena como parametro,
que sera el nombre de un archivo a abrir. As podremos iniciar un editor vaco, sin ningun texto,
y otro que abra un archivo y muestre su contenido.
65 public E d i t o r ( ) {
66 t h i s ( null ) ;
67 }
75 public E d i t o r ( S t r i n g a r c h i v o ) {
76 modificado = false ;
77 a r c h i v o O r i g i n a l = this . a r c h i v o = a r c h i v o ;
78 creaVentanaPrincipal ();
79 i f ( a r c h i v o ! = null ) {
80 abrirArchivoDeDisco ( ) ;
81 }
82 marco . s e t V i s i b l e ( true ) ;
83 }
Los dos constructores funcionan de manera identica; solo que uno ademas de inicializar todo,
manda abrir un archivo de texto. Para evitar codigo duplicado, hacemos que el constructor que
no recibe parametros mande llamar al constructor que recibe una cadena [66], y le pase null
para distinguir cuando abrimos el archivo.
El constructor inicializa en false la variable que nos dice si el archivo esta modificado [76],
inicializa las variables con el nombre del archivo [77] (que puede ser null), y manda crear la
ventana principal [78]. Para esto asumimos que tendremos una funcion abrirArchivoDeDisco que
abrira el archivo especificado por nuestra variable de clase archivo y mostrara su contenido en
nuestro componente de texto. Siempre es bueno imaginar que tenemos funciones que hacen
ciertas cosas, porque as vamos dividiendo el trabajo en tareas cada vez mas pequenas. Si
queremos probar que el programa compila, solo hay que implementar las funciones vacas.
Si el nombre de archivo que nos pasaron no es null [79], mandamos abrir el archivo en
disco [80]. Asumimos otro metodo llamado abrirArchivoDeDisco que (contrario a lo que pudiera
pensarse) abrira el archivo del disco.
Por ultimo, hacemos visible la ventana principal de nuestro programa [82].
135
Creacion de la ventana principal
Veamos ahora como crearemos la ventana principal con el metodo creaVentanaPrincipal:
90 public void c r e a V e n t a n a P r i n c i p a l ( ) {
91 marco = new JFrame ( "Editor -- <ArchivoNuevo >" ) ;
92
93 t e x t o = creaAreaDeTexto ( ) ;
94 JMenuBar b a r r a = creaBarraDeMenu ( ) ;
95 e s t a d o = new J L a b e l ( " " ) ;
96
97 J P a n e l p a n e l = new J P a n e l ( ) ;
98 J S c r o l l P a n e s c r o l l P a n e = new J S c r o l l P a n e ( t e x t o ) ;
99 s c r o l l P a n e . s e t P r e f e r r e d S i z e (new Dimension (ANCHO, ALTO ) ) ;
100
105 C o n t a i n e r c = marco . g e t C o n t e n t P a n e ( ) ;
106 c . add ( p a n e l ) ;
107
111 t e x t o . s e t R e q u e s t F o c u s E n a b l e d ( true ) ;
112 texto . requestFocus ( ) ;
113
120 R e c t a n g l e medidas = o b t e n M e d i d a s P a n t a l l a ( ) ;
121 marco . s e t L o c a t i o n ( ( medidas . widthANCHO) / 2 , ( medidas . h e i g h t ALTO) / 2 ) ;
122 }
136
se ajusta el tamano de los componentes.
Queremos que cuando el programa empiece el usuario pueda escribir inmediatamente, as que
le pasamos el foco a nuestro componente de texto [111,112]. Por ultimo le anadimos un escucha
a la ventana para que cuando el evento de cerrar ocurra (widowClosing), entonces el programa
termine [114-118]. Asumimos una nueva funcion: menuQuitarPrograma. El prefijo menu es porque
querremos que el evento de cerrar la ventana funcione igual que cuando seleccionemos la opcion
de quitar el programa del menu; para ambas usaremos este metodo.
Las dos ultimas lneas del metodo [120,121] obtienen las dimensiones de la pantalla donde
este corriendo el programa, para poder colocar la ventana de forma centrada. Esto es un adorno
y es absolutamente prescindible, pero es el tipo de adornos que hacen agradables a los progra-
mas. Ah nos creamos otro metodo: obtenMedidasPantalla. El metodo es el que sigue:
128 public R e c t a n g l e o b t e n M e d i d a s P a n t a l l a ( ) {
129 R e c t a n g l e v i r t u a l B o u n d s = new R e c t a n g l e ( ) ;
130 G r a p h i c s E n v i r o n m e n t ge ;
131 ge = G r a p h i c s E n v i r o n m e n t . g e t L o c a l G r a p h i c s E n v i r o n m e n t ( ) ;
132 G r a p h i c s D e v i c e [ ] g s = ge . g e t S c r e e n D e v i c e s ( ) ;
133 for ( int j = 0 ; j < g s . l e n g t h ; j ++) {
134 G r a p h i c s D e v i c e gd = g s [ j ] ;
135 G r a p h i c s C o n f i g u r a t i o n [ ] gc = gd . g e t C o n f i g u r a t i o n s ( ) ;
136 for ( int i = 0 ; i < gc . l e n g t h ; i ++) {
137 v i r t u a l B o u n d s = v i r t u a l B o u n d s . u n i o n ( gc [ i ] . getBounds ( ) ) ;
138 }
139 }
140 return v i r t u a l B o u n d s ;
141 }
Esta copiado tal cual de la clase GraphicsConfiguration, pero ah va una explicacion rapida: se
crea un rectangulo [129], obtenemos el ambiente grafico [131], del cual sacamos los dispositivos
graficos disponibles [132], a cada uno de los cuales les sacamos la configuracion [135], y de
todas las configuraciones hacemos una union [137], para al final regresar el rectangulo [140]. Y
as obtenemos las medidas de la pantalla.
148 public J T e x t A r e a c r e a A r e a D e T e x t o ( ) {
149 J T e x t A r e a t e x t o = new J T e x t A r e a ( ) ;
150 t e x t o . s e t E d i t a b l e ( true ) ;
151 t e x t o . s e t M a r g i n (new I n s e t s ( 5 , 5 , 5 , 5 ) ) ;
152 t e x t o . s e t F o n t (new Font ( "Monospaced" , Font . PLAIN , 1 4 ) ) ;
153 t e x t o . getDocument ( ) . a d d D o c u m e n t L i s t e n e r (new D o c u m e n t L i s t e n e r ( ) {
154 public void changedUpdate ( DocumentEvent e ) {
155 m o d i f i c a d o = true ;
156 e s t a d o . s e t T e x t ( " Modificado" ) ;
157 }
158 public void i n s e r t U p d a t e ( DocumentEvent e ) {
137
159 m o d i f i c a d o = true ;
160 e s t a d o . s e t T e x t ( " Modificado" ) ;
161 }
162 public void removeUpdate ( DocumentEvent e ) {
163 m o d i f i c a d o = true ;
164 e s t a d o . s e t T e x t ( " Modificado" ) ;
165 }
166 });
167 return t e x t o ;
168 }
138
200 menu . add ( e l ) ;
201 e l = new JMenuItem ( "Salvar archivo" ) ;
202 e l . s e t A c c e l e r a t o r ( K e y S t r o k e . g e t K e y S t r o k e ( KeyEvent . VK S ,
203 A c t i o n E v e n t . CTRL MASK ) ) ;
204 e l . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) {
205 public void a c t i o n P e r f o r m e d ( A c t i o n E v e n t e ) {
206 menuSalvarArchivo ( ) ;
207 }
208 });
209 menu . add ( e l ) ;
210 e l = new JMenuItem ( "Salvar archivo como ..." ) ;
211 e l . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) {
212 public void a c t i o n P e r f o r m e d ( A c t i o n E v e n t e ) {
213 menuSalvarArchivoComo ( ) ;
214 }
215 });
216 menu . add ( e l ) ;
217 menu . a d d S e p a r a t o r ( ) ;
218 e l = new JMenuItem ( "Quitar programa" ) ;
219 e l . s e t A c c e l e r a t o r ( K e y S t r o k e . g e t K e y S t r o k e ( KeyEvent . VK Q ,
220 A c t i o n E v e n t . CTRL MASK ) ) ;
221 e l . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) {
222 public void a c t i o n P e r f o r m e d ( A c t i o n E v e n t e ) {
223 menuQuitarPrograma ( ) ;
224 }
225 });
226 menu . add ( e l ) ;
227
240 return b a r r a ;
241 }
El metodo es muy largo; pero hace casi lo mismo seis veces. Primero creamos la barra de
menu [175], y despues declaramos un menu [176] y un elemento de menu [177]. Vamos a usar
dos menus (Archivo y Ayuda) y seis elementos de menu (Nuevo archivo, Abrir archivo,
Guardar archivo, Guardar archivo como. . . , Quitar programa y Acerca de. . . ); as que
139
para no declarar ocho variables, declaramos solo dos y las usamos varias veces. 2
Primero creamos el menu Archivo [179], y le declaramos una tecla mnemonica [180]. La
tecla que elegimos es la tecla a (por eso el KeyEvent.VK A), y significa que cuando hagamos
M-a (o Alt-a en notacion no-XEmacs), el menu se activara. Y anadimos el menu a la barra
[181].
Despues creamos el elemento de menu Nuevo archivo [183], y le ponemos un acelerador
[184,185]. El acelerador es parecido a la tecla mnemonica; cuando lo presionemos, se activara la
opcion de menu. El acelerador que escogimos es C-n (la tecla es KeyEvent.VK N y el mod-
ificador es ActionEvent.CTRL MASK, o sea Control ). De una vez le ponemos un escucha al
elemento del menu [186-190]. El escucha solo manda llamar al metodo (que asumimos) me-
nuNuevoArchivo. Por ultimo, anadimos el elemento de menu al menu [191].
Las lneas [192-226] son una repeticion de esto ultimo; creamos los demas elementos de
menu, les ponemos aceleradores (no a todos), y les ponemos escuchas que solamente mandan
llamar a algun metodo, ninguno de los cuales hemos hecho. La unica lnea diferente es la [217],
en la que anadimos un separador al menu (un separador es una lnea nada mas).
Y por ultimo, en las lneas [228-238] creamos el menu Ayuda de forma casi identica al
menu Archivo. Lo ultimo que hace el metodo es regresar la barra de menu [240].
menuNuevoArchivo
Lo primero que hace el metodo es verificar que el archivo en la ventana no este modificado
[249]. Si esta modificado, entonces hay que confirmar si se deja o no el archivo [250]; para
eso asumimos que tenemos un metodo confirmarDejarArchivo.
2
Usar una misma variable para distintos objetos es considerado una mala practica de programacion; pero no
es un pecado capital, y puede ser utilizado en casos semi triviales, como este.
140
Despues limpiamos el texto del componente de texto [252], y dejamos el estado del editor
como no modificado [253,254]. Ponemos el nombre del archivo en null [255] (porque es
nuevo), y cambiamos el ttulo de la pantalla al nuevo estado.
Noten que la variable archivoOriginal la dejamos igual que a archivo.
menuAbrirArchivo
269 J F i l e C h o o s e r f c = null ;
270 try {
271 S t r i n g d i r = System . g e t P r o p e r t y ( "user.dir" ) ;
272 f c = new J F i l e C h o o s e r ( d i r ) ;
273 } catch ( S e c u r i t y E x c e p t i o n s e ) {
274 f c = new J F i l e C h o o s e r ( ) ;
275 }
276
141
Si el usuario presiono la opcion Aceptar [279], obtenemos el archivo del dialogo [280], y
comprobamos que exista [281]. Si el archivo no existe, mostramos un mensaje diciendolo
[282-287] y salimos de la funcion [288]. Si no entramos al cuerpo del if (el segundo if ,
anidado dentro del primero), entonces el archivo existe, as que obtenemos el nombre [291]
y lo abrimos con la funcion abrirArchivoDeDisco [292], que aun no tenemos.
Si el usuario no presiono Aceptar, entonces presiono Cancelar, y ya no hacemos nada.
menuSalvarArchivo
Con este metodo ocurre lo contrario a los primeros dos; si el archivo no ha sido modificado
[303] entonces se sale del metodo [304] (para que lo salvamos si no ha habido cambios?).
Despues comprueba si el nombre del archivo es null [306]. Si lo es significa que el archivo
es nuevo, y entonces hay que hacer lo que hace el metodo menuSalvarArchivoComo, y por
lo tanto lo manda llamar [307]. Si no, manda salvar el archivo en disco [309], con el metodo
salvarArchivoEnDisco.
menuSalvarArchivoComo
142
331 i f ( r == J F i l e C h o o s e r . APPROVE OPTION ) {
332 if ( f . exists ()) {
333 int r 2 ;
334 r 2 = JOptionPane . s h o w C o n f i r m D i a l o g
335 ( marco ,
336 "El archivo \""+f+
337 "\" existe.\n"+
338 ">Desea sobreescribirlo ?" ,
339 "El archivo ya existe" ,
340 JOptionPane . YES NO OPTION ) ;
341 i f ( r 2 ! = JOptionPane . YES OPTION )
342 return ;
343 }
344 archivo = f . toString ();
345 salvarArchivoEnDisco ( ) ;
346 }
347 }
Si el archivo esta modificado [355], pedimos confirmacion para dejarlo [356], y despues
salimos del programa [358].
acercaDe
143
Este es el metodo mas idiota de todos los tiempos; solo muestra un dialogo con informacion
del programa (que por cierto no tiene ninguna informacion util).
abrirArchivoDeDisco
El metodo primero crea un arreglo de caracteres [381], del tamano que definimos en la
variable BLOQUES, y declara un entero para saber cuantos caracteres leemos en cada
pasada [382].
Tambien declara una cadena variable (StringBuffer) [383]. Las cadenas variables son como
las cadenas: con la ventaja de que pueden aumentar y disminuir de tamano, editarse los
caracteres que tienen dentro, etc.
144
Despues, dentro de un bloque try [384] abrimos el archivo de texto para lectura [385], y
en un ciclo [386-389] leemos todos los caracteres del archivo y los metemos en la cadena
variable. Cerramos el archivo [390], ponemos todos esos caracteres como el texto de nuestro
componente de texto [391], y hacemos que lo que se vea del texto sean los primeros
caracteres [292].
Si algo sale mal en el try [393], asumimos que no pudimos leer el archivo, se lo informamos
al usuario [394-399], regresamos el nombre del archivo al original [400] (si no hacemos esto
podemos perderlo), y nos salimos de la funcion [401].
Si salimos airosos del try, actualizamos la variable archivoOriginal [403], y ponemos el
estado del editor en no modificado [404-406], lo que tiene sentido pues acabamos de abrir
el archivo.
salvarArchivoEnDisco
Dentro de un try [417], abrimos el archivo para escritura [418], le escribimos todo el texto
de nuestro componente de texto [419], y cerramos el archivo [420].
Si algo sale mal [421], le decimos al usuario que no pudimos salvar su archivo [422-427],
regresamos el nombre del archivo al original [428] y salimos del programa [429].
145
Si salimos bien del try, actualizamos la variable archivoOriginal [431], y ponemos el estado
del editor en no modificado [432-435], lo que tiene sentido pues acabamos de salvar el
archivo.
confirmarDejarArchivo
El metodo main
Por ultimo, hay que ver el metodo main:
Lo que hace main es comprobar que a lo mas se llamo al programa con un parametro [460-
463]. Despues, define el local del programa para que hable espanol de Mexico [464], declara un
editor [465], y llama al constructor apropiado [466-470].
146
Actividad 10.11 Consulta la documentacion de la clase Locale.
Observaciones finales
Hay que reflexionar un poco acerca del diseno de nuestro editor.
Si se dieron cuenta, dentro de los manejadores de eventos (todos en clases internas anonimas)
se intento utilizar el menor codigo posible. Poner mucho codigo en los manejadores de eventos
solo nos complica la vida y hace el codigo mas feo. En general, se encapsula lo que hay que
hacer en un manejador de eventos en un solo metodo, y solo se invoca.
El diseno se hizo de forma descendiente (top-down); comenzamos haciendo los metodos mas
generales para despues hacer los mas particulares. Java se presta mucho para trabajar de esta
forma, y en el caso de interfaces graficas nos facilita la vida ya que es muy sencillo pensar en
una ventana principal, y que va a ocurrir cuando elijamos un menu o presionemos un boton.
Fjense como todos los metodos son compactos. Cada metodo hace una unica cosa, y se
asegura de hacerla bien, manejando las excepciones de acuerdo. Ningun metodo es realmente
largo; casi todos caben en una pantalla de texto normal, y los que no es porque tienen mucho
codigo de interfaces graficas (como la creacion de la barra de menu). Ningun metodo por
s mismo debe ser difcil de entender. Noten tambien que casi no hay repeticion de codigo
excepto para cosas triviales.
Es necesario explicar por que todo el programa consiste de una clase. En el caso de este
problema (hacer un editor de texto), todas las clases necesarias las provee Java: toda la parte
de interfaces graficas (Swing), y toda la parte de entrada/salida (las clases del paquete java.io).
Por lo tanto, solo tenamos que hacer la clase de uso, que es nuestro programa.
Una ultima cosa respecto a la clase Locale. Es importante que se acostumbren a tratar de
usar el idioma nativo del usuario para un programa. Todas las cadenas que aparecen en clases
como JFileChooser o JMessageDialog estan por omision en ingles. Java por suerte proporciona la
habilidad de cambiar dinamicamente esas cadenas, de acuerdo a la lengua que queremos usar.
Java maneja el concepto de locales, que es la manera en que determina como presentar cierta
informacion al usuario. Esto no solo se aplica a en que idioma imprimira "S" o "Archivo",
sino a muchas diferencias que hay entre idiomas, y aun entre idiomas iguales en distintos pases.
Por ejemplo, en Mexico escribimos 1,000.00 para representar un millar con dos cifras decimales;
pero en Espana escriben 1.000,00 para representar lo mismo. Y en ambos pases se habla espanol
(o eso dicen en Espana). Los locales de Java manejan todo este tipo de cosas.
La lnea
define como local por omision al que habla espanol ("es"), en Mexico ("MX"). Pueden comentar
esa lnea y volver a compilar y ejecutar el programa para que vean la diferencia. Fjense en
particular cuando usamos las clases JFileChooser y JOptionPane.
147
Los codigos de lenguaje y pas usados para determinar los locales estan definidos en el
estandar ISO-639 e ISO-3166 respectivamente. El primero lo pueden consultar en
<http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt>
y el segundo en
<http://www.chemie.fu-berlin.de/diverse/doc/ISO 3166.html>
10.4. Ejercicios
1. Haz una interfaz grafica para la Base de Datos.
La ventana principal de la aplicacion mostrara tres campos vacos, que representaran el
nombre, la direccion y el telefono; usa tres etiquetas para senalizar cual es cual. El usuario
debe poder introducir cadenas en estos campos. Ademas, la ventana tendra tres botones;
uno que dira Agregar, otro que dira Borrar, y otro que dira Buscar.
En caso de que el usuario haga click en el boton Agregar, se debera comprobar que el
usuario haya introducido datos en los campos, y que sean correctos (o sea, que el telefono
sea un entero valido), y de ser as, crear un RegistroAgenda y agregarlo a la base de datos.
Despues de hacerlo, debera volver a dejar vacos los campos. Si el registro es repetido,
debe utilizar un dialogo para avisarle al usuario que no pudo agregar el registro porque
ya exista uno igual.
Si alguno de los campos esta vaco cuando el usuario haga click en Agregar, un dialogo
debera informarle que no puede agregar un registro incompleto.
Si el usuario hace click en Buscar, se debera comprobar que solo el campo del nombre
o el del telefono tenga informacion. De ser as, se realizara una busqueda por nombre o
por telefono (dependiendo de cual de los dos tenga informacion). Debe comprobarse que
si es el campo del telefono el que tiene informacion, que esta sea un entero valido.
Si el registro es encontrado, los campos deben ser actualizados con los datos del registro.
Si no es encontrado, un dialogo debera decirle al usuario que el nombre (o telefono) que
busco no existen.
Si se hace click en Buscar sin que haya informacion en el campo del nombre o el del
telefono, un dialogo debera decirle al usuario que necesita uno de ambos para buscar un
registro.
Al momento en que el usuario haga click en Borrar, debe haber informacion en los tres
campos. De no ser as, un dialogo debe informar al usuario que no se puede borrar un
registro del que no se tiene toda la informacion.
Aqu se le deja al programador una de varias opciones. La primera es que el usuario
pueda borrar cualquier registro si de memoria teclea los datos que le corresponden y
despues hace click en Borrar. En este caso el programador tiene que hacer primero una
busqueda, despues una comprobacion con el metodo equals, y en caso de que exista el
registro, borrarlo y si no existe avisarle al usuario que esta tratando de borrar un registro
inexistente.
148
La segunda es que el usuario solo pueda borrar un registro que haya buscado antes. Esta
estrategia tiene la ventaja de que se garantiza que el registro exista, y se evita la necesidad
de buscarlo. Pero hay que tener siempre una referencia al ultimo registro buscado.
Tambien es posible implementar ambas; aunque es particularmente engorroso. Se deja a
criterio del programador cual debe de implementar.
Ademas de todo esto, el programa debe tener una barra de menu con dos entradas: Base
de Datos y Ayuda. En la entrada Base de Datos deben existir (al menos) las opciones
Nueva Base de Datos, Guardar Base de Datos, Recuperar Base de Datos y Salir
del programa. Cada una de las opciones hace lo que su nombre indica. Pon lo que quieras
en la entrada de Ayuda; pero no la dejes vaca.
Varias cosas que el programa debe cumplir:
Nunca debe tronar. El programa debe ser indestructible. No importa lo que pase, el
programa debe poder continuar corriendo a menos que el usuario cierre la ventana
o seleccione la opcion Salir del programa del menu Base de Datos. O que caiga
un meteoro en la computadora.
Esto quiere decir, en primer lugar, que todas las excepciones deben manejarse de
manera inteligente. Nunca debe terminar el programa, aunque no pueda leer o es-
cribir la base de datos. Tampoco debe de terminar por alguna excepcion del tipo de
NullPointerException. Asegurate de que todos tus metodos funcionen con el caso de
que la lista de registros sea null.
Si algo sale mal, siempre debe ser avisado el usuario, con un mensaje claro y com-
pacto.
Se probaran todos los metodos que se han visto en el curso. Se probara el poder
buscar y agregar registros, as como el poder guardar y el poder recuperar la base
de datos. Se probaran todos estos metodos tambien cuando la lista de registros sea
null. Debe funcionar el programa en todos estos casos.
El programa no debe permitir el perder informacion sin avisar al usuario. Si algun
registro ha sido anadido o borrado de la base de datos, y el usuario quiere salir del
programa, debe preguntarsele primero si quiere guardar la base de datos en disco
duro. Para esto, el programa debe estar siempre consciente de si la base de datos ha
sufrido alguna modificacion.
10.5. Preguntas
1. Hay alguna parte del editor que no comprendas?
149
2. Que se te antoja hacer con interfaces graficas? Explcate.
150
Practica 11
Ant y archivos Jar
11.1. Meta
Que el alumno profundice sus conocimientos de Ant, y que aprenda a utilizar archivos Jar.
11.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
11.3. Desarrollo
Hemos usado Ant a lo largo de esta practicas sin hacer mucho enfasis en como funciona y
que es exactamente lo que hace. Tambien hemos usado archivos Jar durante casi todo el curso,
sin decir como se crean o que son.
En esta practica nos enfocaremos a ver exactamente como funcionan estas dos herramientas
de Java.
11.3.1. Ant
No es posible entender del todo a Ant si no explicamos primero las herramientas de Java, de
las cuales las mas importantes son el compilador y la herramienta de JavaDoc, aunque tambien
se incluyen muchas otras en el Java Developers Kit (JDK) de Java.
151
El compilador de Java
Hasta esta practica, hemos compilado nuestros programas haciendo:
# ant compile
u otro objetivo de nuestro archivo build.xml. Que es lo que ocurre realmente cuando hacemos
esto? El objetivo compile generalmente ha tenido esta forma:
<t a r g e t name="compile">
<mkdir d i r="build" />
<j a v a c s r c d i r="src" d e s t d i r="build" />
</ t a r g e t >
Desde la practica 2 vimos que <mkdir... /> y <javac... /> son tareas (tasks) de Ant. La
tarea mkdir es obvia; crea el directorio build, a menos que ya exista. La tarea javac se ha
hecho evidente a lo largo de las practicas: compila todos los archivos .java que hay debajo
del directorio src/, y coloca los correspondientes archivos .class en el directorio build/, si
compilan claro. Pero, como hace esto?
Como dijimos en la practica 1, Ant realmente no hace mucho; recarga casi todo el trabajo
en el compilador de Java, javac.
El compilador de Java puede ser mas que suficiente si estamos compilando una clase sola:
digamos que la clase Matriz2x2 y la interfaz MatrizCuadrada estuvieran en un mismo directorio,
y que no pertenecieran a ningun paquete. Entonces podramos compilarlas con javac directa-
mente sin ningun problema, haciendo lo siguiente en el directorio donde estuvieran los archivos
Matriz2x2.java y MatrizCuadrada.java:
# javac Matriz2x2.java
El compilador de Java es lo suficientemente listo como para descubrir por s mismo que para
poder compilar la clase Matriz2x2, necesitara a la interfaz MatrizCuadrada, porque Matriz2x2 la
implementa. Los archivos .class quedaran en el mismo directorio donde estan los .java.
Para programas pequensimos de una o dos clases, javac es mas que sugiciente. El problema
es que generalmente Java no se utiliza para programas pequensismos de una o dos clases.
Con la clase Matriz2x2 ocurre ademas que esta en el paquete icc1.practica4, entonces no
podramos compilarla como lo hicimos arriba. El porque de esto es algo complicado, y lo veremos
en un momento.
Para compilarla podramos hacer lo siguiente: en el directorio padre del directorio icc1 (que
tiene que existir porque la clase Matriz2x2 esta en el paquete icc1.practica4), hacemos:
Los archivos .class tambien terminaran en el mismo directorio de los archivos .java.
Aqu es donde las cosas comienzan a ponerse divertidas. Si tenemos muchos archivos1 , la
cosa puede alcanzar grados ridculos facilmente. Por supuesto, si tuvieramos muchas clases en
un solo paquete, las cosas podran simplificarse:
1
Y con Java parece que siempre terminan existiendo muchos archivos.
152
javac icc1/practica4/*.java
Pero esto solo nos sirve si tenemos solo un paquete2 . Para compilar todas nuestras practicas,
habra que hacer
javac icc1/practica1/*.java icc1/practica2/*.java ... icc1/practica10/*.java
Por supuesto, en estas practicas solo han necesitado las clases de los paquetes icc1.*, pero
desde proyectos medianamente complejos, se terminan utilizando muchas mas clases que estan
en distintas bibliotecas, y seguramente en varios directorios. Y tambien tenemos la ventaja de
que solo queremos compilar clases del paquete icc1.practica4. Pero, que pasara si quisieramos
tambien compilar clases de otros paquetes, y que no necesariamente tuvieran todos como pa-
quete raz a icc1?
Ocurrira que nuestra lnea de compilacion sera algo como lo siguiente (la lista de la ruta
de clases utiliza dos puntos : para separar sus distintos elementos)
2
Y el problema es que, ademas de muchas clases, tambien siempre parecen terminar habiendo muchos pa-
quetes.
153
# javac -d build \
-classpath /usr/lib/bibl1:/usr/lib/bibl2:... \
icc1/practica4/*.java \
pak1/subpak1/*.java ...
Y rapidamente comienza a salirse de control el asunto. Con Java no es raro tener un proyecto
que consiste en cientos de clases, repartidos en decenas de paquetes, y utilizando multiples
bibliotecas.
Aqu es donde Ant nos facilita la vida.
La ayuda de Ant
La lnea de compilacion de arriba:
# javac -d build \
-classpath /usr/lib/bibl1:/usr/lib/bibl2:... \
icc1/practica4/*.java \
pak1/subpak1/*.java ...
queda reducida con Ant a un objetivo as (moviendo todos los paquetes con nuestro codigo
fuente a un directorio src):
<t a r g e t name="compile">
<mkdir d i r="build" />
<j a v a c s r c d i r="src" d e s t d i r="build">
<c l a s s p a t h >
<p a t h e l e m e n t="/usr/lib/bibl1" />
<p a t h e l e m e n t="/usr/lib/bibl2" />
...
</ c l a s s p a t h >
</ j a v a c >
</ t a r g e t >
Y van a decir, que ventaja tiene eso? Incluso se termina escribiendo mas. La cosa es que
solo hay que escribirlo una unica vez, y si hay que modificarlo, solo hay que hacerlo en un sitio
(el archivo build.xml).
Pero ademas, podemos meter cuantos paquetes querramos en el directorio src, y estos seran
automaticamente compilados, no importa cuantas clases tenga cada uno.
Por supuesto, conforme crece el tamano de nuestros proyectos, la ayuda de Ant se hace mas
y mas obvia. Supongamos que en nuestro proyecto tenemos distintos modulos, que no queremos
compilar juntos siempre, pero que necesitan la misma ruta de clases para compilarse. Entonces
podemos hacer esto:
15 <t a r g e t name="compile.module1">
154
16 <mkdir d i r="module1/build" />
17 <j a v a c s r c d i r="module1/src" d e s t d i r="module1/build">
18 <c l a s s p a t h r e f i d ="compile.classpath" />
19 </ j a v a c >
20 </ t a r g e t >
21
22 <t a r g e t name="compile.module2">
23 <mkdir d i r="module2/build" />
24 <j a v a c s r c d i r="module2/src" d e s t d i r="module2/build">
25 <c l a s s p a t h r e f i d ="compile.classpath" />
26 </ j a v a c >
27 </ t a r g e t >
Cada modulo tendra su propio directorio (modulo1 y modulo2). Y si alguna vez hay que
modificar la ruta de clases, solo hay que hacerlo en un lugar.
Pero ademas Ant nos permite tener dependencias entre objetivos. Supongamos que el modu-
lo 1 siempre tuviera que compilarse antes que el modulo 2; entonces podramos modificar a este
ultimo 2 as:
y con ello, si tratamos de compilar el modulo 2 sin haber compilado el modulo 1, Ant au-
tomaticamente lo hara por nosotros.
Ant tiene tareas para manejar no solo el compilador, sino tambien JavaDoc, el depurador
de Java, y muchas mas, entre ellas la misma maquina virtual, lo que nos permite poder correr
nuestros programas a traves de Ant. Ademas, ofrece una biblioteca (en Java, por supuesto),
para que uno pueda crear tareas que hagan casi cualquier cosa.
A lo largo de estas practicas se han dado archivos build.xml que permiten manejar pequenos
proyectos (como lo han sido las praticas). Ant es una herramienta increblemente poderosa, y
no podemos cubrir todas sus funciones en una practica. Sin embargo, con lo que se les ha dado
es mas que suficiente para que puedan comenzar a utilizar Ant, y estudiar por su cuenta las
tareas que ofrece y como hacer ustedes sus propias tareas de Ant.
<http://ant.apache.org>.
155
11.3.2. Archivos Jar
Supongamos ahora que ya tenemos un programa enorme con cientos de clases, dividido en
varios paquetes, que ademas utiliza otras varias decenas de clases distribuidas en otras varias
decenas de paquetes.
Nuestro programa ya compila y corre y, orgullosos, queremos decirle al mundo de el y
presumirlo con pompa y circunstancia. As que ponemos una pagina en la WWW y pedimos a
la gente que baje y pruebe nuestro programa.
No podemos decirles que bajen todos nuestros archivos .class, y que despues los acomoden
en la jerarqua de directorios necesaria (de hecho s podramos decirles eso, otra cosa es que
lo hicieran). Tenemos que encontrar la manera de distribuir nuestras clases y paquetes de una
manera sencilla y eficiente.
De la misma forma, muchas veces querremos usar bibliotecas, y al compilar no necesaria-
mente tener que pasarle a la ruta de clases un directorio. Nos gustara poder pasarle solo un
archivo por biblioteca, por ejemplo.
Y para todo esto estan los archivos Jar.
Los archivos Jar (jarfiles en ingles), se llaman as por Java Archives, o archivos de Java.
Aqu se entiende archivo como fichero en el sentido bibliografico, no como archivo en disco
duro.
si tuvieramos los archivos Jar bibl1.jar y bibl2.jar en un directorio lib. As no parece que
haya mucha ganancia; pero podramos mejorarlo as:
Con eso, cualquier archivo Jar que pongamos en el directorio lib quedara automatica-
mente incluido en la ruta de clases. Lo cual es muy conveniente si para compilar un proyecto
156
necesitamos varios archivos Jar.
Supongamos que queremos crear un archivo Jar para la pratica 10. Solo nos ponemos en el
directorio build (despues de compilar, claro) y hacemos
# man jar
En pocas palabras, la c es para decirle que cree el archivo Jar, y la f es para decirle
como se llamara el archivo. Java tiene un paquete especializado para tratar con archivos Jar
programaticamente (esto es, dentro de programas escritos en Java). Es el paquete java.jar.
Tambien podemos crear archivos Jar desde nuestro build.xml. Solo utilizamos la tarea
<jar/>; por ejemplo, igual para nuestra practica 10:
Con esto creamos el archivo Jar utilizando como base nuestro directorio build. Hacemos
que el objetivo dependa de la compilacion, para solo crear el archivo Jar si la practica ya fue
compilada.
(Necesitamos pasarle tambien el archivo Jar icc1.jar a la ruta de clases para que el pro-
grama corra).
Pero esto es feo. Recordemos que el programa de las Torres de Hanoi les fue proporcionado
en un archivo Jar, y que lo unico que tuvieron que hacer para correrlo fue
157
La opcion -jar le dice a la maquina virtual que hay que ejecutar la clase principal del
archivo Jar. As que, como le especificamos la clase principal a un archivo Jar?
La respuesta es sencilla; el archivo Jar debe incluir un archivo llamado MANIFEST en su raz.
Por ejemplo en nuestro ejemplo de la practica 10, el archivo debera estar en el directorio build
antes de crear el archivo Jar.
En el archivo manifiesto (el archivo MANIFEST) pueden ir varias cosas, pero en particular
podemos crearlo as:
Main-Class: icc1.practica10.UsoBaseDeDatosAgenda
Si un archivo MANIFEST con ese contenido esta en el directorio build al momento de crear
el archivo Jar, podremos ejecutar nuestra practica 10 de la siguiente manera:
# java -jar practica10.jar
11.4. Ejercicios
1. Como vimos, para hacer un archivo Jar ejecutable directamente por la maquina virtual,
necesitamos incluirle un archivo manifiesto. Vimos que lo mas sencillo es crear el archivo
en el directorio build.
Sin embargo, nuestro objetivo clean siempre ha borrado ese directorio, as que perderamos
nuestro archivo manifiesto siempre que limpiaramos.
En tu build.xml Crea un objetivo llamado jar, que al ser invocado de la siguiente manera
ant jar
Con este ejercicio terminamos con nuestra Base de Datos en el curso. Por supuesto, no es
una Base de Datos en todo el sentido del termino (seguramente llevaran un curso completo de
Bases de Datos mas adelante en la carrera), pero varias de las cosas que hemos visto se aplican.
Solo queremos especificar algo respecto a las Bases de Datos reales que ignoramos a lo largo
de todas estas practicas, porque s es un concepto importante. Durante todo el curso manejamos
la idea de que una Base de Datos es una tabla. En la practica 5 cambiamos la representacion
interna con una lista, pero conceptualmente podamos seguirla viendo como una tabla (cada
registro como un renglon, cada campo como una columna).
Las Bases de Datos reales son un conjunto de tablas, organizadas alrededor de relaciones
entre las columnas de las tablas.
158
11.5. Preguntas
1. Al momento de compilar, cual es la diferencia entre especificar un archivo Jar o un
directorio con una jerarqua de paquetes en la ruta de clases?
159
160
Practica 12
Hilos de Ejecucion y Enchufes
(Opcional)
12.1. Meta
Que el alumno aprenda a utilizar hilos de ejecucion y a programar en red con enchufes.
12.2. Objetivos
Al finalizar esta practica el alumno sera capaz de:
12.3. Desarrollo
A lo largo de las practicas se ha cubierto el material para Java de un primer curso de
programacion en Ciencias de Computacion.
Queremos sin embargo cubrir dos puntos que, aunque probablemente algo avanzados para
un primer curso de programacion, creemos es necesario mencionarlos y discutirlos, ya que en
la actualidad son conocimientos obligatorios para cualquier computologo.
El proposito de esta ultima practica es mencionar los hilos de ejecucion y la programacion
en red con enchufes.
161
clases. Dentro de cada uno de esos metodos se poda a la vez llamar a mas metodos (y en la
practica 8 vimos lo que ocurra cuando un metodo se llamaba a s mismo). Y desde la practica
5 sabemos que dentro de un metodo puede haber uno o varios ciclos dando vueltas. Incluso
podemos tener ciclos dentro de ciclos.
Pero al fin y al cabo, si nuestras funciones recursivas estan bien hechas y nuestros ciclos
terminan algun da, el programa continuara su ejecucion tranquilamente, una instruccion a la
vez.
Las aplicaciones modernas dejaron de funcionar as hace ya mucho tiempo. Uno tiene edi-
tores como XEmacs donde se puede compilar un programa en un buffer mientras se edita un
archivo en otro buffer al mismo tiempo. O navegadores como Firefox que pueden abrir varias
paginas de la red al mismo tiempo, y algunas de esas paginas pueden reproducir musica o video,
y ademas el navegador puede estar bajando varios archivos a la red, todo al mismo tiempo.
Por supuesto, para que todo esto funcione, el sistema operativo debe proveer de la fun-
cionalidad necesaria. Para esto, hace ya casi cuarenta anos surgio el concepto de multiproceso,
que es lo que permite que hagamos varias cosas en la computadora al mismo tiempo.
A pesar de que en la actualidad ya no es tan raro encontrar computadoras personales con
dos o incluso cuatro procesadores, la gran mayora de las computadoras de escritorio siguen
contando con un unico procesador. Un procesador solo puede ejecutar una instruccion a la vez.
Los procesadores actuales pueden ejecutar varios millones de instrucciones en un segundo, pero
una a una. Incluso en las computadoras con multiples procesadores, cada procesador solo puede
ejecutar una instruccion a la vez.
Los sistemas operativos reparten el procesador entre varios procesos (aunque algunos lo
hacen mucho peor que otros). Como los procesadores son muy rapidos (y cada vez lo son mas),
no se nota que en cada instante dado, el procesador solo se hace cargo de un proceso.
Gracias a la capacidad multiproceso de los sistemas operativos, los lenguajes de progra-
macion implementan un comando para que el proceso de un programa pueda dividirse en dos
procesos (proceso padre y proceso hijo), cada uno de los cuales puede seguir caminos muy
distintos. La instruccion suele llamarse fork.
El problema de dividir procesos es que cuando se hace, toda la imagen del proceso padre en
memoria se copia para el proceso hijo. Ademas, son procesos completamente independientes; si
el proceso padre abre archivos por ejemplo, no puede compartirlos con el proceso hijo.
Imagnense un programa como Firefox o el Internet Explorer. Son programas grandes y
ocupan mucho espacio en memoria. Cuando el usuario solicita que el navegador comience a bajar
un archivo de la red, queremos que pueda seguir navegando mientras el archivo se descarga. Si
dividimos el proceso, toda la imagen en memoria de Firefox debe copiarse, cuando lo unico que
queremos es la parte del programa que se encarga de bajar archivos. Y toda esa memoria tiene
que liberarse una vez que el archivo se haya descargado. Ademas, imaginen que al usuario se le
ocurre bajar siete archivos al mismo tiempo (a algun usuario se le va a ocurrir que eso es una
buena idea. . . ).
En vista de lo costoso que resulta dividir procesos, surgio el concepto de procesos ligeros o
hilos de ejecucion (threads en ingles).1
Un hilo de ejecucion es como un proceso hijo; pero se ejecuta en el contexto de su proceso
1
Los hilos de ejecucion ya existan en Algol extendido, lenguaje de programacion que usaban las maquinas
Burroughs (hoy Unisys) en 1970. Todos los sistemas Unix, que estan basados en el estandar POSIX, utilizan
los Posix Threads implementados en el leguaje de programacion C, desde hace ya varios anos. El concepto no
es nuevo, pero comenzo a popularizarse hasta hace relativamente pocos anos.
162
padre. Pueden compartir variables, intercambiar informacion, y lo que muchos consideran lo
mas importante: no se copia toda la imagen en memoria del proceso padre.
Aunque es posible que no se hayan dado cuenta, nosotros ya hemos usado varios hilos de
ejecucion. Al momento de empezar a utilizar eventos, comenzamos a utilizar varios hilos de
ejecucion en un programa.
Cuando tenemos un programa orientado a eventos (como los son la gran mayora de los
programas que utilizan una interfaz grafica para comunicarse con el usuario), la parte que
espera que los eventos ocurran se ejecuta en un hilo de ejecucion distinto al del programa
principal.
Fjense en el metodo main de la clase Editor (o el de su propia interfaz grafica para su Base
de Datos). Despues de crear el objeto de la clase Editor con new, el programa principal termina.
Ya no se ejecuta ningun metodo o instruccion; main sencillamente acaba. Ese hilo de ejecucion
termina (porque aunque usemos un unico hilo de ejecucion en un programa, este sigue siendo
un hilo de ejecucion).
La ejecucion del programa continua porque hicimos visible un objeto de la clase JFrame con
el metodo setVisible. En ese momento comienza a ejecutarse otro hilo de ejecucion que pueden
imaginarse como el siguiente codigo:
Event e ;
do {
e = eventoActual ( ) ;
i f ( e ! = null ) {
/ E j e c u t a t o d o s l o s manejadores d e l e v e n t o /
}
} while ( true ) ;
163
{
// I n s t r u c c i o n 1
B ( ) ; // Llamamos a l metodo B.
// I n s t r u c c i o n 2
}
1. Se ejecutaba la instruccion 1.
2. Se llamaba al metodo B (ejecutandose todas las instrucciones, ciclos, recursiones, etc. del
metodo).
3. Se ejecutaba la instruccion 2.
Con un hilo de ejecucion, en cambio, tenemos esto (si suponemos que tenemos un hilo de
ejecucion llamado t):
{
// I n s t r u c c i o n 1
t . s t a r t ( ) ; // Llamamos a l metodo s t a r t d e l h i l o de e j e c u c i o n .
// I n s t r u c c i o n 2
}
En este caso, el metodo start se ejecuta al mismo tiempo que la instruccion 2. El metodo
start regresa inmediatamente despues de haber sido llamado, y ahora la ejecucion del programa
corre en dos hilos de ejecucion paralelos.
En los dos hilos de ejecucion podemos hacer cosas, y los hilos de ejecucion pueden verse y
hablarse (pasarse objetos y tipos basicos).
Despues de todo lo que se dijo al inicio de la seccion, ustedes saben que los dos hilos de
ejecucion no se ejecutan al mismo tiempo exactamente. Pero la JVM, con la ayuda del sistema
operativo, se encarga de que parezca que s se ejecutan al mismo tiempo.
Dentro de la clase MiProceso lo que hacemos es sobrecargar el metodo run, que no recibe
ningun parametro y tiene tipo de regreso void:
public void r u n ( ) {
// Aqu implementamos n u e s t r o h i l o de e j e c u c i o n .
}
164
Despues, cuando queramos ejecutar el hilo de ejecucion, llamamos al metodo start que se
hereda de la clase Thread, y start a su vez ejecuta el metodo run.
public c l a s s UsoMiProceso {
public s t a t i c void main ( S t r i n g [ ] a r g s ) {
MiProceso mp = . . . // I n s t a n c i a m o s e l h i l o de e j e c u c i o n .
... // Hacemos mas c o s a s .
mp . s t a r t ( ) ; // Ejecutamos e l h i l o de e j e c u c i o n .
... // Hacemos t o d a v a mas c o s a s .
Mtodo main
(otras expresiones)
Noten que no es obligatorio que el metodo start sea llamado desde main; puede ser llamado
desde cualquier lugar en el programa (por eso ponemos que hay otras posibles expresiones desde
que entramos a main hasta que llamamos a start).
Extender a la clase Thread siempre que queramos un hilo de ejecucion independiente puede
resultar restrictivo. Tal vez en el diseno de nuestro problema nos encontremos con una clase
que esta dentro de una jerarqua de herencia especfica, y que ademas tiene que ejecutarse en
su propio hilo de ejecucion.
Para esto esta la interfaz Runnable. Cuando una clase que hagamos implemente la interfaz
Runnable, tiene que definir el metodo run (que es el unico metodo declarado en la interfaz):
165
public c l a s s MiProceso extends A l g u n a C l a s e implements Runnable {
public void r u n ( ) {
...
}
...
Y para ejecutar nuestro hilo de ejecucion, creamos un objeto de la clase Thread utilizando
el objeto de nuestra clase. Con este nuevo objeto podemos ya llamar al metod start:
public c l a s s UsoMiProceso {
public s t a t i c void main ( S t r i n g [ ] a r g s ) {
MiProceso mp = . . . // I n s t a n c i a m o s e l o b j e t o r u n n a b l e .
... // Hacemos mas c o s a s .
Thread t ; // Declaramos un h i l o de e j e c u c i o n , y
t = new Thread ( mp ) ; // l o i n s t a n c i a m o s con n u e s t r o o b j e t o .
t . start (); // Ejecutamos e l h i l o de e j e c u c i o n .
... // Hacemos t o d a v a mas c o s a s .
En el ejemplo pusimos que la clase MiProceso extiende a la clase AlgunaClase. Esto es porque
solo hay que implementar la interfaz Runnable si nuestra clase tiene que heredar a alguna otra
que no sea Thread. Si nuestra clase no necesita extender a ninguna otra, lo correcto es que
extienda a Thread.
try {
166
Thread . s l e e p ( 1 0 0 0 ) ;
} catch ( I n t e r r u p t e d E x c e p t i o n e ){
// No podemos dormir ahora , a s que seguimos .
}
El metodo estatico sleep de la clase Thread tratara de poner a dormir el hilo de ejecucion
donde se manda llamar el metodo (por eso es estatico). El metodo espera de la clase Graficos-
DeReloj de las primeras dos practicas es lo que haca.
Puede ocurrir que la JVM decida que el hilo de ejecucion no puede dormirse en ese momento,
por lo que el metodo lanza la excepcion InterruptedException cuando eso ocurre. Realmente no
hay mucho que hacer en esos casos; sencillamente el hilo de ejecucion continuara su ejecucion
sin dormirse.
Otra cosa que podemos hacer es preguntar si un hilo de ejecucion esta vivo o no:
i f ( mp . i s A l i v e ( ) ) {
// El h i l o de e j e c u c i o n e s t a v i v o .
}
Este metodo se invoca desde afuera del hilo de ejecucion que nos interesa, no adentro. Si
un hilo de ejecucion esta vivo quiere decir que ya fue llamado su metodo run, y que este no ha
terminado.
Ademas de esto, podemos cambiar la prioridad de un hilo de ejecucion. La prioridad de
un hilo de ejecucion es la importancia que le da la JVM para ejecutarlo. Como discutimos
arriba, una computadora generalmente tiene un procesador, y en algunos casos dos o cuatro.
La JVM (junto con el sistema operativo) tiene que repartir el procesador entre todos los hilos
de ejecucion que existan. En principio, trata de ser lo mas justa que puede, asignandole a cada
hilo de ejecucion relativamente la misma cantidad de tiempo en el procesador.
Podemos cambiar eso con el metodo setPriority. Sin embargo, no es un metodo confiable en el
sentido de que no va a funcionar igual entre arquitecturas distintas, o entre sistemas operativos
diferentes. Pueden realizar experimentos con el metodo, pero realmente se recomienda no basar
el funcionamiento de un programa en las prioridades de sus hilos de ejecucion.
167
El ejemplo es de sistemas operativos, ya que sirve para ejemplificar de manera muy sencilla
lo que pasa cuando varios procesos quieren usar el procesador (o la memoria, o el disco duro).
El caso en que el sistema se traba porque todos los procesos quieren usar el mismo recurso al
mismo tiempo (como cuando los filosofos se quedaban cada uno con nada mas un palillo) se le
llama abrazo mortal o deadlock.
Para evitar los abrazos mortales, lo primero que debemos hacer es evitar que dos hilos de
ejecucion distintos traten de llamar al mismo metodo al mismo tiempo. Para esto hacemos que
el metodo este sincronizado, lo que se logra agregando la palabra clave synchronized en la
definicion del metodo:
Esto se hace en los metodos de los objetos que vayan a ser compartidos por los hilos de
ejecucion, no en los metodos de los hilos de ejecucion mismos (se puede hacer tambien, pero
realmente no tiene mucho sentido). Casi todas las clases de Java tienen sincronizados sus meto-
dos.
Ademas de sincronizar metodos completos, podemos sincronizar un bloque respecto a una
o varias variables (si asumimos que queremos sincronizar respecto al objeto obj):
synchronized ( o b j ) {
o b j . metodo ( ) ;
obj . d ef ine Var iable ( 5 ) ;
}
Cuando el hilo de ejecucion llegue a esa parte del codigo, ningun otro hilo de ejecucion
podra hacer uso del objeto obj.
La segunda cosa que debemos hacer para evitar abrazos mortales es hacer que un hilo de
ejecucion espere hasta que le avisen que ya puede tener acceso a algun recurso u objeto. Por
ejemplo, en nuestro ejemplo de los filosofos podemos hacer que los filosofos 2 y 4 esperen a que
los filosofos 1 y 3 hayan usado los palillos para que ellos los usen, y que el filosofo 5 espere a
los filosofos 2 y 4, y por ultimo que los filosofos 1 y 3 esperen a que el 5 acabe para continuar. 2
Para que un hilo de ejecucion espere hasta que le avisen que ya puede seguir su ejecucion,
esta el metodo wait:
try {
// espearamos a que nos a v i s e n que podemos c o n t i n u a r .
wait ( ) ;
} catch ( I n t e r r u p t e d E x c e p t i o n e ) {
}
El metodo wait esta definido en la clase Object, as que todos los objetos de Java pueden lla-
marlo. El metodo lanza la excepcion InterruptedException si por algun motivo no puede esperar.
Ademas, en la clase Object estan definidas otras dos versiones del metodo wait, que sirven para
2
Esta es una solucion que garantiza que todos los filosofos comen, pero es ineficiente porque mientras el
filosofo 5 come podra hacerlo tambien algun otro filosofo. Hay muchas soluciones distintas para el problema de
los filosofos y los palillos.
168
que el hilo de ejecucion solo espere un determinado numero de milisegundos (o nanosegundos,
si el sistema operativo puede manejarlos).
El metodo notifyAll (tambien definido en la clase Object) hace que todos los hilos de ejecucion
que esten detenidos por haber llamado al metodo wait continuen su ejecucion.
Con estos dos metodos podemos hacer que los hilos de ejecucion se pongan de acuerdo en
cuando deben tener acceso a algun recurso u objeto, para que no traten de hacerlo todos al
mismo tiempo.
169
de correo electronico para trabajar.
La programacion en red, tambien llamada de diseno cliente/servidor, es un tema en s mismo
amplio y fuera del alcance de estas practicas. Sin embargo, se les dara una pequena muestra de
como funciona para que vean los fundamentos basicos de este tipo de programas.
Para crear programas que funcionen sobre la red usando Java hay varios metodos, de los
cuales los mas conocidos son:
Servlets y JSP. JSP significa Java Servlet Page, y funciona muy similarmente a PHP o
las paginas ASP de Microsoft (PHP significa PHP: Hypertext Preprocessor mientras que
ASP se entiende por ActiveX Server Page). Los servlets son programas de Java normales
que se ejecutan en una maquina que funciona como servidor de WWW, y que generan
paginas dinamicamente. Los JSP, as como PHP y las ASP funcionan similarmente; pero
en lugar de ser programas normales, es codigo incrustado dentro de una pagina HTML.
Todos estos metodos son utilizados para crear programas cuyos clientes necesitan un
navegador para ejecutarse, como Firefox o el Internet Explorer.
En esta practica veremos enchufes, ya que existen en casi todos los lenguajes de progra-
macion del universo y porque son relativamente sencillos de utilizar, al precio de no proveer
tanta funcionalidad o nivel de abstraccion como el RMI, los Servlets/JSP o CORBA.
170
Enchufes
Los enchufes, contrario a lo que pudiera pensarse, funcionan como enchufes. Piensen en
el enchufe telefonico; es un punto de entrada/salida al exterior. Pueden recibir informacion a
traves de el (cuando escuchan), y mandar informacion a traves de el (cuando hablan). De hecho,
pueden hablar y or al mismo tiempo; pueden recibir y enviar informacion al mismo tiempo.
La idea de los enchufes es crear puntos de entrada/salida entre dos computadoras; una vez
creados, las computadoras podran enviarse bytes mutuamente a traves de ellos. Es importante
senalar que la comunicacion se reduce a bytes, por lo que son de muy bajo nivel.
Para establecer la conexion entre dos enchufes, se necesitan dos programas. Se podra hacer
la conexion con un solo programa utilizando hilos de ejecucion, pero no tiene sentido ya que lo
que queremos es comunicar dos maquinas.
El primer programa es llamado servidor, y lo que hace es estar escuchando en un puerto
de comunicacion de la maquina donde esta corriendo, esperando por una solicitud de conexion.
Cuando la recibe crea un enchufe, y si la solicitud es valida, la comunicacion queda establecida.
El segundo programa es llamado cliente, y lo que hace es crear un enchufe con la direccion
de la maquina y el puerto donde esta escuchando el servidor. Si el servidor esta en esa maquina
escuchando en ese puerto, la conexion queda establecida.
Una vez que la conexion ha sido establecida, cada enchufe dispone de un objeto de la clase
InputStream y de otro objeto de la clase OutputStream. Cuando un enchufe manda bytes a su
OutputStream, el otro los recibe por su InputStream, y viceversa. El control de los enchufes
queda totalmente en manos del programador, y de acuerdo a la aplicacion se vera como cada
enchufe controla los mensajes que manda y recibe.
El objeto de la clase InputStream de los enchufes tiene implentado el metodo available, por
lo que siempre podemos saber si hay algo que leer de el. Si el metodo available regresa un entero
mayor que cero, entonces hay algo que leer. Si regresa cero, no hay nada que leer.
El servidor
Lo que hace un servidor se resume en las siguientes lneas:
try {
int p u e r t o = 1 0 0 0 0 ;
S e r v e r S o c k e t s e r v i d o r = new S e r v e r S o c k e t ( p u e r t o ) ;
S o c k e t c l i e n t e = s e r v i d o r . a c c e p t ( ) ; // Lo ponemos a e s c u c h a r .
manejaCliente ( c l i e n t e ) ;
} catch ( E x c e p t i o n e ) {
/ Crear un e n c h u f e de s e r v i d o r y p o n e r l o a e s c u c h a r e s
p o t e n c i a l m e n t e p e l i g r o s o y puede r e s u l t a r en que sean
l a n z a d a s v a r i a s e x c e p c i o n e s . /
}
171
informacion, o tal vez solo la reciba. Lo mas comun, sin embargo, es que haga ambas cosas
constantemente.
Si se quiere hacer un servidor para multiples clientes, se hace algo de este estilo:
try {
int p u e r t o = 1 0 0 0 0 ;
S e r v e r S o c k e t s e r v i d o r = new S e r v e r S o c k e t ( p u e r t o ) ;
while ( true ) {
// Nos ponemos a e s c u c h a r .
Socket c l i e n t e = s e r v i d o r . accept ( ) ;
// Creamos un h i l o de e j e c u c i o n para que maneje a l e n c h u f e .
MiThread p r o c e s o C l i e n t e = new MiThread ( c l i e n t e ) ;
// Disparamos a l h i l o de e j e c u c i o n .
procesoCliente . start ();
}
} catch ( E x c e p t i o n e ) {
}
De esta manera el servidor escucha eternamente por el puerto; cuando una conexion es
recibida, dispara un hilo de ejecucion que maneja al enchufe de la misma manera que lo hara
manejaCliente, y vuelve a esperar por otra conexion. Esta es la manera en que funcionan casi
todos los servidores en la red (HTTP, FTP, TELNET, SSH, etc.)
Si se dan cuenta, un servidor as implementado es un programa orientado a eventos, aunque
no tenga interfaz grafica. Los eventos en este caso son las solicitudes de conexion que recibe el
servidor.
Un mismo servidor puede tener un numero potencialmente infinito de enchufes conectados
a un mismo puerto; sin embargo siempre se limita a un numero fijo las conexiones concurrentes
posibles.3
El cliente
El cliente es todava mas sencillo. Para crear el enchufe solo se necesita la direccion del
servidor y el puerto donde esta escuchando:
try {
int p u e r t o = 1 0 0 0 0 ;
// Puede u t i l i z a r s e un IP numerico , como 1 3 2 . 2 4 8 . 2 8 . 6 0 .
S t r i n g d i r e c c i o n = "abulafia.fciencias.unam.mx" ;
S o c k e t s e r v i d o r = new S o c k e t ( d i r e c c i o n , p u e r t o ) ;
manejaServidor ( se r vi d o r ) ;
} catch ( E x c e p t i o n e ) {
3
El ancho de banda no es gratuito.
172
/ Crear un e n c h u f e de c l i e n t e tambien genera v a r i a s p o s i b l e s
e x c e p c i o n e s . /
}
Analisis de un chat
173
Actividad 12.5 Compila las clases Servidor y Cliente. El build.xml incluido con los
archivos genera los archivos Jar servidor.jar y cliente.jar. Para correr el servidor,
una vez compilado, ejecuta
Ahora ejecuta el cliente (en otra maquina de ser posible) con la siguiente lnea de comandos:
Necesitaras pasarle la direccion de la maquina donde este el servidor, con la opcion -s. Si
el servidor utiliza el puerto por omision (1234), puedes omitir la opcion -p.
12.4. Ejercicios
1. Basandote (si quieres) en el servidor que solo recibe una conexion, utiliza hilos de ejecucion
para que pueda recibir varias conexiones.
12.5. Preguntas
1. Piensa en todos los programas que conoces, ya sea que funcionen en Unix/Linux o en
cualquier otro sistema operativo. Crees poder hacerte una idea de como estan progra-
mados? Justifica ampliamente.
174
Apendice A
El resto de las leyes
175