LibroJava PDF
LibroJava PDF
Computacion I
(con Java)
Elisa Viso G.
Canek Pelaez V.
Facultad de Ciencias, UNAM
Indice general
1. Introduccion 1
1.1. Conceptos generales . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Historia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Sistemas numericos . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4. La Arquitectura de Von Neumann . . . . . . . . . . . . . . . . . . 13
1.5. Ejecucion de programas . . . . . . . . . . . . . . . . . . . . . . . 25
1.5.1. Filosofas de programacion . . . . . . . . . . . . . . . . . . 26
1.6. Caractersticas de Java . . . . . . . . . . . . . . . . . . . . . . . . 27
3. Clases y Objetos 55
3.1. Tarjetas de responsabilidades . . . . . . . . . . . . . . . . . . . . 55
3.2. Programacion en Java . . . . . . . . . . . . . . . . . . . . . . . . 62
3.2.1. Declaraciones en Java . . . . . . . . . . . . . . . . . . . . 63
3.2.2. Alcance de los identificadores . . . . . . . . . . . . . . . . 81
3.2.3. Implementacion de metodos en Java . . . . . . . . . . . . 83
3.3. Expresiones en Java . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.3.1. Declaracion y definicion simultaneas . . . . . . . . . . . . 98
6. Herencia 171
6.1. Extension de clases . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.2. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
6.2.1. Arreglos de una dimension . . . . . . . . . . . . . . . . . . 175
6.2.2. Iteracion enumerativa . . . . . . . . . . . . . . . . . . . . . 183
6.2.3. Arreglos de mas de una dimension . . . . . . . . . . . . . . 187
6.2.4. Los arreglos como parametros y valor de regreso . . . . . . 190
6.3. Aspectos principales de la herencia . . . . . . . . . . . . . . . . . 193
6.3.1. Super y subclases . . . . . . . . . . . . . . . . . . . . . . . 193
6.4. Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
6.5. Clases abstractas . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
6.6. Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
5.10. Recorrido de una lista para contar sus registros. (ListaCurso) . . 154
5.11. Modifica el numero de grupo. (ListaCurso) . . . . . . . 155
5.12. Agregar un registro al principio de la lista. (ListaCurso) . . . 157
5.13. Agregar un registro al final de la lista. (ListaCurso) . . . . . 158
5.14. Imprimiendo toda la lista. (ListaCurso) . . . . . . . . 159
5.15. Imprimiendo registros seleccionados. (ListaCurso) . . . . . 160
5.16. Metodo que busca un registro. (ListaCurso) . . . . . . . 162
5.17. Eliminacion de un registro en una lista. (ListaCurso) . . . . 165
5.18. Menu para el manejo de la lista. (MenuLista) 1/5 . . . 166
5.18. Menu para el manejo de la lista. (MenuLista) 2/5 . . . . . 167
5.18. Menu para el manejo de la lista. (MenuLista) 3/5 . . . . . 168
5.18. Menu para el manejo de la lista. (MenuLista) 4/5 . . . . . 169
5.18. Menu para el manejo de la lista. (MenuLista) 5/5 . . . . . 170
6.1. Superclase EstudianteBasico. 1/2 . . . . . . . . . 172
6.1. Superclase EstudianteBasico. 2/2 . . . . . . . . . 173
6.2. Encabezado para la subclase EstudianteCurso . . . . . . . . . . . . . 174
6.3. Calculo de n! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
6.4. Codificacion de iteracion for con while . . . . . . . . . . . . . . . 186
6.5. Construccion de un triangulo de numeros . . . . . . . . . . . . . . 187
6.6. Arreglos como parametros y valor de regreso de una funcion 1/2 191
6.6. Arreglos como parametros y valor de regreso de una funcion 2/2 192
6.7. Campos y constructor de EstudianteCurso . . . . . . . . . . . . . 193
6.8. Metodos nuevos para la subclase EstudianteCurso . . . . . . . . . 194
6.9. Redefinicion del metodo getRegistro() . . . . . . . . . . . . . . . . . 195
6.10. Registros en un arreglo. . . . . . . . . . . . . . . . . . . . . . . . . 196
6.11. Otra version del metodo getRegistro . . . . . . . . . . . . . . . . . 197
6.12. Clases abstractas y concretas . . . . . . . . . . . . . . . . . . . . . 199
6.13. Interfaz para manejar una lista. . . . . . . . . . . . . . . . . . . . . 201
6.14. Herencia con una interfaz. . . . . . . . . . . . . . . . . . . . . . . . 201
7.1. Clase que ilustra el anidamiento dinamico . . . . . . . . . . . . . . . 206
7.2. La funcion factorial. . . . . . . . . . . . . . . . . . . . . . . . . . . 229
7.3. Factorial calculado iterativamente . . . . . . . . . . . . . . . . . . 237
7.4. Metodos para las torres de Hanoi . . . . . . . . . . . . . . . . . . 239
8.1. Superclase con la informacion basica de los estudiantes(InfoEstudian-
te)1/3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
8.1. Superclase con la informacion basica de los estudiantes. (InfoEstu-
diante)2/3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
8.1. Superclase con la informacion basica de los estudiantes. (InfoEstu-
diante)3/3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
8.2. Extendiendo la clase InfoEstudiante. (EstudianteLista) . . . 249
INDICE DE ALGORITMOS Y LISTADOS xiv
a todo lo que tiene que ver con computacion, nosotros lo entendemos mas bien
como refiriendose a aquellos aspectos de la computacion que tienen que ver con la
administracion de la informacion (sistemas de informacion, bases de datos, etc.).
Al igual que la programacion, la informatica la podemos considerar contenida
propiamente en la computacion.
El termino cibernetica es un termino forjado por los sovieticos en los anos
cincuenta. Sus races vienen de combinar aspectos biologicos de los seres vivos
con ingeniera mecanica, como es el caso de los robots, la percepcion remota, la
simulacion de funciones del cuerpo, etc. A pesar de que se utiliza muchas veces en
un sentido mas general, no lo haremos as en estas notas.
Definicion 1.1 Un algoritmo es un metodo de solucion para un problema que cumple con:
1. Trabaja a partir de 0 o mas datos (entrada).
2. Produce al menos un resultado (salida).
3. Esta especificado mediante un numero finito de pasos (finitud).
4. Cada paso es susceptible de ser realizado por una persona con papel y lapiz
(definicion).
5. El seguir el algoritmo (la ejecucion del algoritmo) lleva un tiempo finito
(terminacion).
Estamos entonces preocupados en ciencias de la computacion por resolver pro-
blemas; pero no cualquier problema, sino unicamente aquellos para los que po-
damos proporcionar un metodo de solucion que sea un algoritmo mas adelante
en la carrera demostraran ustedes que hay mas problemas que soluciones, ya no
digamos soluciones algortmicas.
La segunda parte importante de nuestra disciplina es la implementacion de
algoritmos. Con esto queremos decir el poder llevar a cabo un algoritmo dado (o
disenado) de manera automatica, en la medida de lo posible.
3 Introduccion
La computacion es una disciplina mucho muy antigua. Tiene dos races fun-
damentales:
tiguo. Tambien los mayas tenan un smbolo asociado al cero. Este concepto es
muy importante para poder utilizar notacion posicional. La notacion posicional
es lo que usamos hoy en da, y consiste en que cada smbolo tiene dos valores
asociados: el peso y la posicion. Por ejemplo, el numero 327.15 se puede presentar
de la siguiente manera:
EJEMPLO 1.3.2
ii. Los ceros antes del primer dgito distinto de cero, desde la izquierda, no apor-
tan nada al numero.
iii. Los ceros a la derecha del punto decimal y antes de un dgito distinto de cero
s cuentan. No es lo mismo .75 que .00075.
iv. Los ceros a la derecha del ultimo dgito distinto de cero despues del punto
decimal no cuentan.
1.3 Sistemas numericos 8
vi. Cada dgito aporta su valor especfico multiplicado por el peso de la posicion
que ocupa.
Sabemos todos trabajar en otras bases para la notacion posicional. Por ejem-
plo, base 8 (mejor conocida como octal ) sera de la siguiente manera:
EJEMPLO 1.3.3
num10
0
d b i
i
i n
d 2
correspondiente.
0
1010002 i
i
i 5
1 25 0 24 1 23 0 22 0 21 0 20
32 0 8 0 0 0
4010
Para pasar de base 10 a cualquier otra base, se utiliza el algoritmo 1.1.
Hay que notar que en la lnea 7 se esta concatenando un smbolo, por lo que
si la base es mayor a 10, se tendran que utilizar smbolos para los dgitos mayores
que 9. As, para pasar de base 10 a base 16 el numero 857510 el algoritmo se
ejecutara de la siguiente manera:
Trabajemos con los dos numeros en base 10 para corroborar que el algoritmo
trabajo bien:
75358 7 83 5 82 3 81 5 80
7 512 5 64 3 8 5 1
3584 320 24 5
393310
301136 3 64 0 63 1 62 1 61 3 60
3 1296 0 216 1 36 1 6 3 1
3888 0 36 6 3
393310
En general, para pasar de una base B a otra base b, lo que tenemos que hacer
es expresar b en base B, y despues llevar a cabo el algoritmo en base B. Cada vez
que tengamos un residuo, lo vamos a tener en base B y hay que pasarlo a base b.
Esto ultimo es sencillo, pues el residuo es, forzosamente, un numero entre 0 y b.
Cuando una de las bases es potencia de la otra, el pasar de una base a la otra es
todava mas sencillo. Por ejemplo, si queremos pasar de base 8 a base 2 (binario),
observamos que 8 23 . Esto nos indica que cada posicion octal se convertira a
exactamente a tres posiciones binarias. Lo unico que tenemos que hacer es, cada
dgito octal, pasarlo a su representacion binaria:
75358 7 5 3 5
111 101 011 101 1111010111012
Algo similar se hace para pasar de base 16 a base 2, aunque tomando para
cada dgito base 16 cuatro dgitos base 2.
El proceso inverso, para pasar de base 2, por ejemplo, a base 16, como 16 24
deberemos tomar 4 dgitos binarios por cada dgito hexadecimal:
Las computadoras actuales son, en su inmensa mayora, digitales, esto es, que
representan su informacion de manera discreta, con dgitos. Operan en base 2
1.3 Sistemas numericos 12
Cuales son los distintos elementos que requerimos para poder implementar un
algoritmo en una computadora digital?
Como se representan en binario esos distintos elementos?
Procesador
central
Unidad de control
Unidad aritmetica
Unidad logica
Cache
comparar y decidir si un numero es mayor que otro, etc. En realidad son instruc-
ciones que hacen muy pocas cosas y relativamente sencillas. Recuerdese que se
hace todo en sistema binario.
Lenguajes de programacion
(4)
Programa
objeto
(binario)
(3)
Unidad
Datos para de control
el programa (5)
Programa a
ejecutar Resultados
Programa (2) MEMORIA (6)
ensamblador
(binario) (1)
1.4 La Arquitectura de Von Neumann 16
x1 b b
2a
4ac def a 102 mul ac a c
def b 104 mul ac 4 ac
def c 106 mul a2 2 a
def ac 108 add rad ac b2
def a2 110 sqrt rad rad
def b2 112 sub x1 rad b
def rad 114 div x1 x1 a2
das.
Estos lenguajes tenan un formato tambien mas o menos estricto, en el sentido
de que las columnas de las tarjetas perforadas estaban perfectamente asignadas,
y cada elemento del lenguaje tena una posicion, como en lenguaje ensamblador.
El proceso por el que tiene que pasar un programa en alguno de estos lenguajes
de programacion para ser ejecutado, es muy similar al de un programa escrito
en lenguaje ensamblador, donde cada enunciado en lenguaje de alto nivel se
traduce a varios enunciados en lenguaje de maquina (por eso el calificativo de
alto nivel, alto nivel de informacion por enunciado), que es el unico que puede
ser ejecutado por la computadora.
Hacia finales de los anos 50 se diseno un lenguaje, ALGOL ALGorithmic
Oriented Language que resulto ser el modelo de desarrollo de practicamente
todos los lenguajes orientados a algoritmos de hoy en da, como Pascal, C, C++,
Java y muchos mas. No se pretende ser exhaustivo en la lista de lenguajes, bas-
te mencionar que tambien en los anos 50 surgio el lenguaje LISP, orientado a
inteligencia artificial; en los anos 60 surgio BASIC, orientado a hacer mas facil
el acercamiento a las computadoras de personas no forzosamente con anteceden-
tes cientficos. En los anos 60 surgio el primer lenguaje que se puede considerar
orientado a objetos, SIMULA, que era una extension de ALGOL. En los aproxi-
madamente 50 anos que tienen en uso las computadoras, se han disenado y usado
mas de 1,000 lenguajes de programacion, por lo que pretender mencionar siquiera
a los mas importantes es una tarea titanica, y no el objetivo de estas notas.
1.4 La Arquitectura de Von Neumann 18
Representacion de la informacion
Caracteres
Numeros enteros
sin embargo, que el resultado no sea valido. Por ejemplo, si sumamos dos numeros
positivos y el resultado es mayor que la capacidad, tendremos acarreo sobre el
bit 15, dando aparentemente un numero negativo. Sabemos que el resultado es
invalido porque si en los dos sumandos el bit 15 estaba apagado, tiene que estar
apagado en el resultado. Algo similar sucede si se suman dos numeros negativos
y el resultado ya no cabe en 16 bits ver figura 1.6.
0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 25
+ 1 1 1 1 1 1 0 0 0 1 0 1 0 1 0 1 939
1 1 1 1 1 1 0 0 0 1 1 0 1 1 1 0 914
1.4 La Arquitectura de Von Neumann 22
Pueden verificar, sumando las potencias de 2 donde hay un 1, que las sumas
en la figura 1.5 se hicieron directamente y que el resultado es el correcto.
La desventaja del complemento a 2 es que se pueden presentar errores sin que
nos demos cuenta de ello. Por ejemplo, si le sumamos 1 al maximo entero positivo
(una palabra con 0 en el bit 15 y 1s en el resto de los bits) el resultado resulta
ser un numero negativo, aquel que tiene 1s en todas las posiciones de la palabra
ver figura 1.7.
Numeros reales
27 26 25 24 23 22 21 20 27 26 25 24 23 22 21 20
0 1 0 0 1 1 0 1 0 1 0 0 0 0 1 1 77,67
1 0 0 1 1 0 1 1 0 1 0 1 0 1 1 0 100,162
Una de las ventajas de este tipo de notacion es que es muy sencillo hacer
operaciones aritmeticas, pues se usa a toda la palabra como si fuera un entero
y el proceso de colocar el punto decimal se hace al final. Sin embargo, tiene
una gran desventaja que es la poca flexibilidad para representar numeros
que tengan muchos dgitos en la fraccion, o muy pocos.
1,32456E6 1324560
1,32456E 6 ,00000132456
1,32456E3 1324,56
1.4 La Arquitectura de Von Neumann 24
Definicion 1.4 Un interprete es un programa que una vez cargado en la memoria de una
computadora, y al ejecutarse, procede como sigue:
Repite estas dos acciones hasta que alguna instruccion le indique que pare,
o bien tenga un error fatal en la ejecucion.
A los interpretes se les conoce tambien como maquinas virtuales, porque una
vez que estan cargados en una maquina, se comportan como si fueran otra compu-
tadora, aquella cuyo lenguaje de maquina es el que se esta traduciendo y ejecu-
tando.
Especificacion
Analisis y Diseno
Implementacion
Validacion
Mantenimiento
Refinamiento y extension
d) Alta cohesion, que se refiere al hecho de que todo lo que este relacionado
(funciones, datos) se encuentren juntos, para que sean faciles de localizar,
entender y modificar.
Especificacion
Una buena especificacion, sea formal o no, hace hincapie, antes que nada, en
cual es el resultado que se desea obtener. Este resultado puede tomar muy distintas
formas. Digamos que el computo corresponde a un modelo (la instrumentacion o
implementacion del modelo).
Podemos entonces hablar de los estados por los que pasa ese modelo, donde
un estado corresponde a los valores posibles que toman las variables. Por ejemplo,
si tenemos las variables x, y, z, un posible estado sera:
t x5 y 7 z 9 u
Si tenemos la especificacion de un programa (rutina) que intercambie los va-
lores de dos variables x, y, podemos pensarlo as:
t x K1 y K2 u
es el estado inicial (con los valores que empieza el proceso), mientras que
t x K2 y K1 u
corresponde al estado final deseado. Podemos adelantar que una manera de lograr
que nuestro modelo pase del estado inicial al estado final es si a x le damos el
2.1 Que es la programacion? 34
valor de K2 (el valor que tiene y al empezar) y a y le damos el valor que tena x.
Podemos representar este proceso de la siguiente manera:
Analisis y diseno
Podemos decir, sin temor a equivocarnos, que la etapa de diseno es la mas
importante del proceso. Si esta se lleva a cabo adecuadamente, las otras etapas
se simplifican notoriamente. La parte difcil en la elaboracion de un programa
de computadora es el analisis del problema (definir exactamente que se desea) y
el diseno de la solucion (plantear como vamos a obtener lo que deseamos). Para
esta actividad se requiere de creatividad, inteligencia y paciencia. La experiencia
juega un papel muy importante en el analisis y diseno. Dado que la experiencia
se debe adquirir, es conveniente contar con una metodologa que nos permita ir
construyendo esa experiencia.
35 El proceso del software
Mantenimiento
Porque se trata de un curso, no nos veremos expuestos a darle mantenimien-
to a nuestros programas. En las sesiones de laboratorio, sin embargo, tendran
que trabajar con programas ya hechos y extenderlos, lo que tiene que ver con el
mantenimiento.
2.1 Que es la programacion? 36
Quedan agrupados en una misma clase aquellos objetos que presentan las
mismas caractersticas y funcionan de la misma manera.
En el diseno orientado a objetos, entonces, lo primero que tenemos que hacer
es clasificar nuestro problema: encontrar las distintas clases involucradas en el
mismo.
Las clases nos proporcionan un patron de comportamiento: nos dicen que y
como se vale hacer con los datos de la clase. Es como el guion de una obra de teatro,
ya que la obra no se nos presenta hasta en tanto no haya actores. Los actores son
los objetos, que son ejemplares o instancias de las clases (representantes de las
clases).
Al analizar un problema deberemos tratar de identificar a los objetos invo-
lucrados. Una vez que tengamos una lista de los objetos (agrupando datos que
tienen propositos similares, por ejemplo), deberemos abstraer, encontrando carac-
tersticas comunes, para definir las clases que requerimos.
Distinguimos a un objeto de otro de la misma clase por su nombre identidad
o identificador. Nos interesa de un objeto dado:
Esto se logra mediante reglas de acceso, que pueden ser de alguno de los tipos
que siguen:
Mensajes (messages)
Metodos (methods)
Clases (classes)
Ejemplares (instances)
variables). Sin embargo, hay variables entre las que corresponden a un objeto, que
si bien cambian de un objeto a otro (de una instancia a otra), una vez definidas
en el objeto particular ya no cambian, son invariantes. Por ejemplo, el color de
los ojos, el sexo, la forma de la nariz.
Elementos geometricos
Herencia (inheritance)
Polimorfismo (polymorphism)
Dado que tenemos la posibilidad de agrupar a las clases por familias, que-
remos la posibilidad de que, dependiendo de cual de los herederos se trate, una
funcion determinada se lleve a cabo de manera personal a la clase. Por ejemplo,
si tuvieramos una familia, la funcion de arreglarse se debera llevar a cabo de
distinta manera para la abuela que para la nieta. Pero la funcion se llama igual:
arreglarse. De la misma manera, en orientacion a objetos, conforme definimos he-
rencia podemos modificar el comportamiento de un cierto metodo, para que tome
en consideracion los atributos adicionales de la clase que hereda. A esto, el que
el mismo nombre de metodo pueda tener un significado distinto dependiendo de
la clase a la que pertenece el objeto particular que lo invoca, es a lo que se llama
polimorfismo tomar varias formas.
En resumen, presentados ante un problema, estos son los pasos que deberemos
realizar:
1. Escribir de manera clara los requisitos y las especificaciones del problema.
2. Identificar las distintas clases que colaboran en la solucion del problema y la
relacion entre ellas; asignar a cada clase las responsabilidades correspondien-
tes en cuanto a informacion y proceso (atributos y metodos respectivamen-
te); identificar las interacciones necesarias entre los objetos de las distintas
clases (diseno).
3. Codificar el diseno en un lenguaje de programacion, en este caso Java.
4. Compilar y depurar el programa.
5. Probarlo con distintos juegos de datos.
De lo que hemos visto, la parte mas importante del proceso va a ser el analisis
y el diseno, as que vamos a hablar de el con mas detalle.
3. Determina las formas en las que los objetos colaboran con otros objetos para
delegar sus responsabilidades.
Una vez que se tiene este esquema, conviene tratar de establecer una jerarqua
entre las clases que definimos. Esta jerarqua establece las relaciones de herencia
entre las clases. Dependiendo de la complejidad del diseno, podemos tener anida-
dos varios niveles de encapsulamiento. Si nos encontramos con varias clases a las
que conceptualizamos muy relacionadas, podemos hablar entonces de subsiste-
mas. Un subsistema, desde el exterior, es visto de la misma manera que una clase.
Desde adentro, es un programa en miniatura, que presenta su propia clasificacion
y estructura. Las clases proporcionan mecanismos para estructurar la aplicacion
de tal manera que sea reutilizable.
Si bien suena sencillo eso de determina las clases en tu aplicacion, en la vida
real este es un proceso no tan directo como pudiera parecer. Veamos con un poco
de mas detalle estos subprocesos:
Que es lo que el objeto tiene que saber de tal manera que pueda
realizar las tareas que tiene encomendadas?
Cuales son los pasos, en la direccion del objetivo final del sistema, en
los que participa el objeto?
Con que otros objetos tiene que colaborar para poder cumplir con sus
responsabilidades (a quien le puede delegar)?
Que objetos en el sistema poseen informacion que necesita o sabe como
llevar a cabo alguna operacion que requiere?
En que consiste exactamente la colaboracion entre objetos?
No se hacerlo, no lo reconozco.
Un valor o dato que posee.
La realizacion de un proceso
45 El proceso del software
Como ya mencionamos antes, para disenar cada uno de los metodos o funciones
propias de un sistema debemos recurrir a otro tipo de analisis que el orientado
a objetos. Esto se debe fundamentalmente a que dentro de un metodo debemos
llevar a cabo un algoritmo que nos lleve desde un estado inicial a otro final, pero
donde no existe colaboracion o responsabilidades, sino simplemente una serie de
tareas a ejecutar en un cierto orden.
Tenemos cuatro maneras de organizar a los enunciados o lneas de un algoritmo:
Secuencial, donde la ejecucion prosigue, en orden, con cada lnea, una despues
de la otra y siguiendo la organizacion fsica. Por ejemplo:
1: pone 1 en x
2: suma 2 a x
3: copia x a y
2: Repite 10 veces:
Toda solucion algortmica que demos, sobre todo si seguimos un diseno es-
tructurado, debera estar dado en terminos de estas estructuras de control. El
problema central en diseno consiste en decidir cuales de estas estructuras utili-
zar, como agrupar enunciados y como organizar, en general los enunciados del
programa.
Una parte importante de todo tipo de diseno es la notacion que se utiliza
para expresar los resultados o modelos. Al describir las estructuras de control
utilizamos lo que se conoce como pseudocodigo, pues escribimos en un lenguaje
parecido al espanol las acciones a realizar. Esta notacion, si bien es clara, resulta
facil una vez que tenemos definidas ya nuestras estructuras de control, pero no
nos ayuda realmente a disenar. Para disenar utilizaremos lo que se conoce como
la metodologa de Warnier-Orr, cuya caracterstica principal es que es un diseno
controlado por los datos, i.e. que las estructuras de control estan dadas por las
estructuras que guardan los datos. Ademas, el diseno parte siempre desde el estado
final del problema (que es lo que queremos obtener) y va definiendo pequenos pasos
que van transformando a los datos hacia el estado inicial del problema (que es lo
que sabemos antes de empezar a ejecutar el proceso).
Empecemos por ver la notacion que utiliza el metodo de Warnier-Orr, y dado
que es un metodo dirigido por los datos, veamos la notacion para representar
2.3 Diseno Estructurado 48
grupos de datos, que al igual que los enunciados, tienen las mismas 4 formas
de organizarse: secuencial, iterativa, condicional o recursiva . Por supuesto que
tambien debemos denotar la jerarqua de la informacion, donde este concepto se
refiere a la relacion que guarda la informacion entre s. Representa a los datos
con una notacion muy parecida a la de teora de conjuntos, utilizando llaves para
denotar a los conjuntos de datos (o enunciados). Sin embargo, cada conjunto puede
ser refinado con una ampliacion de su descripcion, que se encuentra siempre
a la derecha de la llave. Otro aspecto muy importante es que en el caso de los
conjuntos de Warnier-Orr el orden es muy importante. La manera en que el
metodo de Warnier-Orr representa estos conceptos se explica a continuacion:
Jerarqua
Abre llaves. El nombre de la llave es el objeto de mayor jerarqua e identifica
al subconjunto de datos que se encuentran a la derecha de la llave. Decimos
entonces que lo que se encuentra a la derecha de la llave refina (explica con
mayor detalle) al nombre de la llave. Veamos la figura 2.3.
nombre
'
'
'
'
'
'
' ...
'
'
'
'
'
' #
'
'
'
'
%digito 2 92
' ...
...
Veamos como quedaran representados los pequenos procesos que dimos arriba
en terminos de la notacion de Warnier-Orr en las figuras 2.6 y 2.7, donde el smbolo
significa obten el valor de lo que esta a la derecha y coloca ese valor en la
variable que esta a la izquierda.
2.3 Diseno Estructurado 50
Para el caso que nos ocupa, determinar los factores primos de un entero, el
diagrama con el que empezamos lo podemos ver en la figura 2.10 en la pagina
anterior. En el primer momento, todo lo que sabemos es que es lo que tenemos de
datos (n) y que esperamos de resultado (una lista con todos los valores de k para
los cuales k es primo y k divide a n). Para el principio y final de nuestros procesos
podemos observar que lo ultimo que vamos a hacer es proporcionar o escribir la
lista de factores primos del numero n. Esto corresponde al final de nuestro proceso.
Sabemos, porque algo de?matematicas manejamos, que deberemos verificar todos
los enteros k entre 2 y n, y que durante este proceso es cuando se debe cons-
truir la lista de primos divisores de n. Tambien sabemos, porque corresponde a
nuestros datos, que al principio de nuestro proceso deberemos obtener n. Con esta
informacion, podemos producir el diagrama inicial que, como ya mencionamos, se
encuentra en la figura 2.10 en la pagina anterior.
$ !
'
' .P rincipio n el numero a f actorizar
'
'
' $ $ !
'
$ esP rimo #true
'
' '
' '
'
' .P rincipio
'
'
' '
'
' '
'
'
' '
' '
'
' '
' i es factor esP rimo f also
'
'
' '
'
' '
' '
'
'
' '
' '
' '
'
' in
'
' '
' '
'
'
V erif ica si '
'
de k
'
'
' '
'
' '
' &
'
' '
' '
'
'
i divide a k
'
'
' '
'
' & pi 2, '
'
' '
' k es factor ? '
'
' #
'
'
' V erif ica si k '
'
' ' q '
'
'
' '
' de n '
'
. . . , k '
'
'
i es factor
'
& divide a '
& '
'
' %
'
'
' ! de k
OF P
' n
' '
'
' esP rimo true Agrega k a la lista
'
'
' pk ? 2 . . . '
'
' '
'
'
' '
' '
'
'
' . . . nq '
' '
'
' !
'
'
' '
'
' '
%
'
' '
' esP rimo true
'
'
' '
'
'
'
'
' '
'
'
'
'
' '
'
'
'
'
' '
'
'
'
'
' '
'
'
'
' '
' !
k es factor
'
'
' % de n
'
'
' !
%.F inal escribe lista construida
Podemos ver que el objeto reloj, posee dos objetos que corresponde cada
uno de ellos a una manecilla. Cada manecilla posee un objeto valor y un objeto
lmite. El valor concreto de cada manecilla es suficientemente simple como para
que se lleve en un entero, lo mismo que el lmite; excepto que este ultimo debe ser
constante porque una vez que se fije ya no debera cambiar. Las horas y los minutos
57 Clases y objetos
Manecilla
Datos: valor
lmite
Funciones: constructores: Poner el lmite
incrementa
pon valor
muestra
Reloj
Datos: horas Una manecilla con lmite 12
minutos Una manecilla con lmite 60
Funciones: constructores
incrementa
pon valor
muestra
1
Usaremos la palabra get en estos casos, en lugar del termino en espanol da o dame, ya que
en Java existe la convencion de que los metodos de acceso a los atributos de una clase sean de
la forma get seguidos del identificador del atributo empezado con mayuscula. Similarmente en
los metodos de modificacion o actualizacion de los valores de un atributo la convencion es usar
set seguido del nombre del atributo escrito con mayuscula.
3.1 Tarjetas de responsabilidades 58
Si1 completamos estos esquemas con las responsabilidades de cada quien, van
a quedar como se muestra en la figura 3.2 para la clase Reloj y en la figura 3.3 en
la pagina opuesta para la clase Manecilla.
En forma esquematica las tarjetas quedan como se muestran en las figuras 3.4 y 3.5
en la pagina opuesta.
Sintaxis:
x declaracion de interfazy ::=
xaccesoy interface xidentificadory {
xencabezado de metodoy;
(xencabezado de metodoy;)*
}
Semantica:
Se declara una interfaz en un archivo. El nombre del archivo debe tener
como extension .java y coincide con el nombre que se le de a la interfaz.
Una interfaz, en general, no tiene declaraciones de atributos, sino unica-
mente de metodos, de los cuales unicamente se da el encabezado. Los en-
cabezados de los distintos metodos se separan entre s por un ; (punto y
coma). El que unicamente contenga encabezados se debe a que una interfaz
no dice como se hacen las cosas, sino unicamente cuales cosas sabe hacer.
Sintaxis:
xacceso y ::= public | private | protected | package |
Semantica:
El acceso a una clase determina quien la puede usar:
public La puede usar todo mundo.
private No tiene sentido para una clase ya que delimita a usar la clase a la
misma clase: no se conoce fuera de la clase.
protected Solo la pueden ver las clases que extienden a esta. No tiene
sentido para clases.
package Solo la pueden ver clases que se encuentren declaradas en el mismo
subdirectorio (paquete). Es el valor por omision.
Puede no haber regla de acceso, en cuyo caso el valor por omision es package.
En el caso de las interfaces, el acceso solo puede ser de paquete o publico, ya
que el concepto de interfaz tiene que ver con anunciar servicios disponibles.
65 Clases y objetos
Sintaxis:
xidentificador y ::= (xletra y | )(xletray | xdgitoy | )*
Semantica:
Los identificadores deben ser nemonicos, esto es, que su nombre ayude a la
memoria para recordar que es lo que representan. No pueden tener blancos
insertados. Algunas reglas no obligatorias (aunque exigidas en este curso)
son:
Clases: Empiezan con mayuscula y consiste de una palabra descriptiva,
como Reloj, Manecilla.
Metodos: Empiezan con minusculas y se componen de un verbo da,
calcula, mueve, copia seguido de uno o mas sustantivos. Cada uno
de los sustantivos empieza con mayuscula.
Variables: Nombres sugerentes con minusculas.
Constantes: Nombres sugerentes con mayusculas.
Hay que notar que en Java los identificadores pueden tener tantos caracteres
como se desee. El lenguaje, ademas, distingue entre mayusculas y minuscu-
las no es lo mismo carta que Carta.
Una interfaz puede servir de contrato para mas de una clase (que se llamen
distinto). Es la clase la que tiene que indicar si es que va a cumplir con algun
contrato, indicando que va a implementar a cierta interfaz.
El acceso a los metodos de una interfaz es siempre publico o de paquete. Esto
se debe a que una interfaz anuncia los servicios que da, por lo que no tendra
sentido que los anunciara sin que estuvieran disponibles.
Siempre es conveniente poder escribir comentarios en los programas, para que
nos recuerden en que estabamos pensando al escribir el codigo. Tenemos tres tipos
de comentarios:
Empiezan con // y terminan al final de la lnea.
Todo lo que se escriba entre /* y */. Puede empezar en cualquier lado y
terminar en cualquier otro. Funcionan como separador.
Todo lo que se escriba entre /** y */. Estos comentarios son para JavaDoc,
de tal manera que nuestros comentarios contribuyan a la documentacion del
programa.
3.2 Programacion en Java 66
Sintaxis:
xdeclaracion de clasey ::= xaccesoy class xidentificadory
( |p implements (xidentificadory)+)
( | extends xidentificadory) {
xdeclaracionesy
( | xmetodo mainy)
}
Semantica:
Se declara una clase en un archivo. El nombre del archivo debe tener como
extension .java y, en general, coincide con el nombre que se le de a la
clase. Una clase debe tener xdeclaracionesy y puede o no tener xmetodo
mainy. La clase puede o no adoptar una interfaz para implementar. Si lo
hace, lo indica mediante la frase implements e indicando a cual o cuales
interfaces implementa. Las xdeclaracionesy corresponden a los ingredientes o
variables y a los metodos que vamos a utilizar. Una xvariabley corresponde a
una localidad (cajita, celda) de memoria donde se va a almacenar un valor.
El xmetodo mainy se usa para poder invocar a la clase desde el sistema
operativo. Si la clase va a ser invocada desde otras clases, no tiene sentido
que tenga este metodo. Sin embargo, muchas veces para probar que la clase
funciona se le escribe un metodo main. En Java todo identificador tiene que
estar declarado para poder ser usado.
terminar con un punto. Despues del punto se puede dar una explicacion mas am-
plia. A continuacion debera aparecer la descripcion de los parametros, cada uno
en al menos un renglon precedido por @param y el nombre del parametro, con una
breve explicacion del papel que juega en el metodo. Finalmente se procedera a
informar del valor que regresa el metodo, precedido de @returns y que consiste de
una breve explicacion de que es lo que calcula o modifica el metodo.
Metodos de acceso
Los metodos de acceso los tenemos para que nos informen del estado de un
objeto, esto es, del valor de alguno de los atributos del objeto. Por ello, la firma
del metodo debe tener informacion respecto al tipo del atributo que queremos
observar. La sintaxis se puede ver en la figura 3.10, donde las definiciones de xtipo
y, xaccesoy e xidentificadory son como se dieron antes.
Figura 3.10 Encabezado para los metodos de acceso.
Sintaxis:
x encabezado demetodo de acceso y ::=
xaccesoy xtipoy xidentificadory ( xParametros y)
Semantica:
La declaracion de un metodo de acceso consiste del tipo de valor que desea-
mos ver, ya que nos va a regresar un valor de ese tipo, seguido de la firma
del metodo, que incluye a los xParametrosy. El identificador del metodo es
arbitrario, pero se recomienda algo del estilo getAtributo, que consista
de un verbo que indica lo que se va a hacer, y un sustantivo que indique el
atributo que se busca o el valor que se desea calcular.
Los tipos que se manejan en Java pueden ser primitivos o de clase. Un tipo
primitivo es aquel cuyas variables no son objetos y son atomicos, esto es, no se
subdividen en otros campos o atributos. En la tabla 3.3 en la siguiente pagina se
encuentra una lista, con los tipos primitivos y sus rangos.
Otro tipo de dato que vamos a usar mucho, pero que corresponde a una clase
y no un dato primitivo como en otros lenguajes, son las cadenas, sucesiones de
caracteres. La clase se llama String. Las cadenas (String) son cualquier sucesion
de caracteres, menos el de fin de lnea, entre comillas. Los siguientes son objetos
tipo String:
"Esta es una cadena 1 2 3 "
""
3.2 Programacion en Java 70
Identificador Capacidad
boolean true o false
char 16 bits, Unicode 2.0
byte 8 bits con signo en complemento a 2
short 16 bits con signo en complemento a 2
int 32 bits con signo en complemento a 2
long 64 bits con signo en complemento a 2
float 32 bits de acuerdo al estandar IEEE 754-1985
double 64 bits de acuerdo al estandar IEEE 754-1985
"a"+"b"+"c" "abc"
"Esta cadena es"+"muy bonita " "Esta cadena esmuy bonita "
Sintaxis:
xParametrosy::= |
xparametroy(, xparametroy)*
xparametroy ::= xtipoy xidentificadory
Semantica:
Los parametros pueden estar ausentes o bien consistir de una lista de
parametros separados entre s por coma (,). Un parametro marca lugar
y tipo para la informacion que se le de al metodo. Lo que le interesa al
compilador es la lista de tipos (sin identificadores) para identificar a un
metodo dado, ya que se permite mas de un metodo con el mismo nombre,
pero con distinta firma.
Por ejemplo, los metodos de una Manecilla que dan los valores de los atributos
privados tienen firmas como se muestra en el listado 3.3. En general, podemos
pedirle a cualquier metodo que regrese un valor, y tendra entonces la sintaxis de
los metodos de acceso. Como lo que queremos del Reloj es que se muestre, no que
nos diga que valor tiene, no tenemos ningun metodo de acceso para esta clase.
Codigo 3.3 Metodos de acceso para los atributos privados de Manecilla (ServiciosManecilla)
interface ServiciosManecilla {
...
public int getValor ( ) ;
public int getLimite ( ) ;
...
} // S e r v i c i o s M a n e c i l l a
3.2 Programacion en Java 72
Metodos de implementacion
Estos metodos son los que dan los servicios. Por ello, el metodo muestra cuya
firma aparece en el listado 3.4 es de este tipo. Es comun que este tipo de metodos
regresen un valor que indiquen algun resultado de lo que hicieron, o bien que
simplemente avisen si pudieron o no hacer lo que se les pidio, regresando un
valor booleano. En el caso de que sea seguro que el metodo va a poder hacer lo
que se le pide, sin contratiempos ni cortapisas, se indica que no regresa ningun
valor, poniendo en lugar de xtipo yla palabra void. Por ejemplo, el encabezado del
metodo que muestra la Manecilla queda como se muestra en el listado 3.4. Tambien
en el listado 3.5 mostramos el metodo de implementacion muestra para la interfaz
ServiciosReloj.
Como pueden ver, ninguno de estos dos metodos regresa un valor, ya que
simplemente hace lo que tiene que hacer y ya. Tampoco tienen ningun parametro,
ya que toda la informacion que requerira es el estado del objeto, al que tienen
acceso por ser metodos de la clase.
Metodos de manipulacion
Los metodos de manipulacion son, como ya mencionamos, aquellos que cam-
bian el estado de un objeto. Generalmente tienen parametros, pues requieren in-
formacion de como modificar el estado del objeto. Los metodos que incrementan
y que asignan un valor son de este tipo, aunque el metodo que incrementa no re-
quiere de parametro ya que el valor que va a usar es el 1. Muchas veces queremos
que tambien nos proporcionen alguna informacion respecto al cambio de estado,
73 Clases y objetos
como pudiera ser un valor anterior o el mismo resultado; tambien podramos que-
rer saber si el cambio de estado procedio sin problemas. En estos casos el metodo
tendra valor de regreso, mientras que si no nos proporciona informacion sera un
metodo de tipo void. Por ejemplo, el metodo que incrementa a la Manecilla nos
interesa saber si al incrementar llego a su lmite. Por ello conviene que regrese un
valor de 0 si no llego al lmite, y de 1 si es que llego (dio toda una vuelta). La
firma de este metodo se muestra en los listados 3.6 y 3.7.
porque este es el objetivo principal del programa. Veamos como queda lo que lle-
vamos del programa en el listado 3.8 (omitimos los comentarios de JavaDoc para
75 Clases y objetos
ahorrar algo de espacio). Como declaramos que nuestras clases Reloj y Manecilla
implementan, respectivamente, a las interfaces ServiciosReloj y ServiciosManecilla,
estas clases tendran que proporcionar las implementaciones para los metodos que
listamos en las interfaces correspondientes. El esqueleto construido hasta ahora
se puede ver en el listado 3.8. Como a la clase Manecilla unicamente la vamos a
utilizar desde la clase Reloj no le damos un archivo fuente independiente.
De las cinco variedades de metodos que listamos, nos falta revisar a los metodos
constructores y a los metodos auxiliares, que tienen sentido solo en el contexto de
la definicion de clases.
Metodos auxiliares
Estos metodos son aquellos que auxilian a los objetos para llenar las solicitudes
que se les hacen. Pueden o no regresar un valor, y pueden o no tener parametros:
depende de para que se vayan a usar. Dado que el problema que estamos atacando
por el momento es relativamente simple, no se requieren metodos auxiliares para
las clases.
Metodos constructores
Una clase es un patron (descripcion, modelo, plano) para la construccion de
objetos que sean ejemplares (instances) de esa clase. Por ello, las clases s tie-
nen constructores que determinan el estado inicial de los objetos construidos de
acuerdo a esa clase.
Sintaxis:
xconstructory ::=xaccesoy xidentificador de Clasey ( xParametrosy ) {
ximplementaciony
}
xParametrosy ::=xparametroy(, xparametroy) |
xparametroy ::=xtipoy xidentificadory
Semantica:
Los constructores de una clase son metodos que consisten en un acceso
que puede ser cualquiera de los dados anteriormente seguido del nombre
de la clase y entre parentesis los xParametrosy del metodo. Un parametro
corresponde a un dato que el metodo tiene que conocer (o va a modificar).
Cada parametro debera tener especificado su tipo. Los nombres dados a ca-
da parametro pueden ser arbitrarios, aunque se recomienda, como siempre,
que sean nemonicos y no se pueden repetir.
/ C o n s t r u c t o r que e s t a b l e c e l a h o r a a c t u a l .
@param l i m Cota s u p e r i o r p a r a e l v a l o r de l a m a n e c i l l a .
/
M a n e c i l l a ( i n t lim , i n t v a l )\ {
/ C o n s t r u c t o r : pone v a l o r maximo y v a l o r i n i c i a l /
// xImplementacion y
} // Firma : M a n e c i l l a ( i n t , i n t )
...
} // M a n e c i l l a
Toda clase tiene un constructor por omision, sin parametros, que puede ser in-
vocado, siempre y cuando no se haya declarado ningun constructor para la clase.
Esto es, si se declaro, por ejemplo, un constructor con un parametro, el construc-
tor sin parametros ya no esta accesible. Por supuesto que el programador puede
declarar un constructor sin parametros que sustituya al que proporciona Java por
omision.
Atributos
Sintaxis:
xdeclaracion de atributoy ::=xaccesoy xmodificadory xtipo y
xidentificador y(,xidentificadory)*;
xmodificador y ::=final | static |
xtipo y ::=xtipo primitivoy | xidentificador de clase y
Semantica:
Todo identificador que se declara, como con el nombre de las clases, se le
debe dar el xaccesoy y si es constante (final) o no. Por el momento no ha-
blaremos de static. Tambien se debe decir su tipo, que es de alguno de los
tipos primitivos que tiene Java, o bien, de alguna clase a la que se tenga
acceso; lo ultimo que se da es el identificador. Se puede asociar una lista de
identificadores separados entre s por una coma, con una combinacion de
acceso, modificador y tipo, y todas las variables de la lista tendran las mis-
mas caractersticas. Al declararse un atributo, el sistema de la maquina le
asigna una localidad, esto es, un espacio en memoria donde guardar valores
del tipo especificado. La cantidad de espacio depende del tipo. A los atri-
butos que se refieren a una clase se les reserva espacio para una referencia,
que es la posicion en el heap donde quedara el objeto que se asocie a esa
variable3 .
Las declaraciones de las lneas 5:, 7:, 13: y 15: son declaraciones de atributos del
tipo que precede al identificador. En la lnea 5: se estan declarando dos atributos
de tipo Manecilla y acceso privado, mientras que en la lnea 13: se esta declarando
un atributo de tipo entero y acceso privado. En la lnea 15: aparece el modificador
final, que indica que a este atributo, una vez asignado un valor por primera vez,
este valor ya no podra ser modificado. Siguiendo las reglas de etiqueta de Java,
el identificador tiene unicamente mayusculas. En el caso de los atributos de tipo
Manecilla, debemos tener claro que nada mas estamos declarando un atributo, no
el objeto. Esto quiere decir que cuando se construya el objeto de tipo Manecilla,
la variable horas se referira a este objeto, esto es, contendra una referencia a un
objeto de tipo Manecilla. Como los objetos pueden tener muy distintos tamanos
sera difcil acomodarlos en el espacio de ejecucion del programa, por lo que se
construyen siempre en un espacio de memoria destinado a objetos, que se llama
heap 4 , y la variable asociada a ese objeto nos dira la direccion del mismo en el heap.
4
El valor de una referencia es una direccion del heap. En esa direccion se encuentra el objeto
construido.
3.2 Programacion en Java 80
El metodo main
El metodo main corresponde a la colaboracion que queremos se de entre clases.
En el se define la logica de ejecucion. No toda clase tiene un metodo main, ya que
no toda clase va a definir una ejecucion. A veces pudiera ser nada mas un recurso
(como es el caso de la clase Manecilla). El sistema operativo (la maquina virtual
de Java) reconoce al metodo main y si se invoca a una clase procede a ejecutar
ese metodo. El encabezado para este metodo se encuentra en el listado 3.13.
Sintaxis:
xReferencia a atributo o metodoy::=
(xreferencia de objeto o clasey.) (xid de atributoy |
xinvocacion a metodoy)
Semantica:
El operador . es el de selector, y asocia de izquierda a derecha. Lo usamos
para identificar, el identificador que se encuentra a su derecha, de que objeto
forma parte. Tambien podemos usarlo para identificar a alguna clase que
pertenezca a un paquete. En el caso de un identificador de metodo, este
debera presentarse con los argumentos correspondientes entre parentesis. la
xreferencia de objetoy puede aparecer en una variable o como resultado de
una funcion que regrese como valor una referencia, que se encuentre en el
alcance de este enunciado. Podemos pensar en el . como un operador del
tipo referencia.
La pista mas importante para esto son las parejas de llaves que abren y cierran.
Para las que corresponden a la clase, todos los nombres que se encuentran en las
declaraciones dentro de la clase son accesibles desde cualquier metodo de la misma
clase. Adicionalmente, los nombres que tengan acceso publico o de paquete son
accesibles tambien desde fuera de la clase.
Sin embargo, hemos dicho que una clase es nada mas una plantilla para cons-
truir objetos, y que cada objeto que se construya va a ser construido de acuerdo
a esa plantilla. Esto quiere decir que, por ejemplo en el caso de la clase Manecilla,
cada objeto que se construya va a tener su atributo valor y su atributo LIM. Si
este es el caso, como hacemos desde fuera de la clase para saber de cual objeto
estamos hablando? Muy facil: anteponiendo el nombre del objeto al del atributo,
separados por un punto. Veamos la forma precisa en la figura 3.14.
Si tenemos en la clase Reloj dos objetos que se llaman horas y minutos, podremos
3.2 Programacion en Java 82
acceder a sus metodos publicos, como por ejemplo incrementa como se muestra en
el listado 3.14.
Es claro que para que se puedan invocar estos metodos desde la clase Reloj
deben tener acceso publico o de paquete. Tambien los objetos horas y minutos
tienen que ser conocidos dentro de la clase Reloj.
Sin embargo, cuando estamos escribiendo la implementacion de algun metodo,
al referirnos, por ejemplo, al atributo valor no podemos saber de cual objeto,
porque el metodo va a poder ser invocado desde cualquier objeto de esa clase.
Pero estamos asumiendo que se invoca, forzosamente, con algun objeto. Entonces,
para aclarar que es el atributo valor del objeto con el que se esta invocando,
identificamos a este objeto con this. Cuando no aparece un identificador de objeto
para calificar a un atributo, dentro de los metodos de la clase se asume entonces
al objeto this. En el codigo que sigue las dos columnas son equivalentes para
referirnos a un atributo dentro de un metodo de la clase.
this.incrementa() incrementa()
this.valor valor
this.horas.LIM horas.LIM
En todo lo que llevamos hasta ahora simplemente hemos descrito los ingredien-
tes de las clases y no hemos todava manejado nada de como hacen los metodos
lo que tienen que hacer. En general un metodo va a consistir de su encabezado
y una lista de enunciados entre llaves, como se puede ver en la figura 3.15 en la
pagina anterior.
Las declaraciones
Cuando estamos en la implementacion de un metodo es posible que el metodo
requiera de objetos o datos primitivos auxiliares dentro del metodo. Estas varia-
bles auxiliares se tienen que declarar para poder ser usadas. El alcance de estas
variables es unicamente entre las llaves que corresponden al metodo. Ninguna va-
riable se puede llamar igual que alguno de los parametros del metodo, ya que si
as fuera, como los parametros son locales se estara repitiendo un identificador en
el mismo alcance. La sintaxis para una declaracion se puede ver en la figura 3.16.
Figura 3.16 Declaracion de variables locales
Sintaxis:
xdeclaracion de variable localy ::= xtipoy xLista de identificadoresy;
Semantica:
La declaracion de variables locales es muy similar a la de parametros for-
males, excepto que en este caso s podemos declarar el tipo de varios iden-
tificadores en un solo enunciado. La xLista de identificadoresy es, como su
nombre lo indica, una sucesion de identificadores separados entre s por una
coma (,).
100: } // m u e s t r a
101: ...
102: } // c l a s s R e l o j
El enunciado return
Cuando un metodo esta marcado para regresar un valor, en cuyo caso el tipo
del metodo es distinto de void, el metodo debe tener entre sus enunciados a return
xexpresiony. En el punto donde este enunciado aparezca, el metodo suspende su
funcionamiento y regresa el valor de la xexpresiony al punto donde aparecio su
invocacion. Cuando un metodo tiene tipo void, vamos a utilizar el enunciado re-
turn para salir del metodo justo en el punto donde aparezca este enunciado. Por
ejemplo, los metodos de acceso lo unico que hacen es regresar el valor del atributo,
87 Clases y objetos
El enunciado de asignacion
Sintaxis:
xenunciado de asignaciony::=| xvariabley = xexpresiony
xexpresiony ::= xvariabley| xconstantey
| new xconstructory | ( xexpresiony )
| xoperador unarioy xexpresiony
| xexpresiony xoperador binario y xexpresiony
| xmetodo que regresa valor y
| xenunciado de asignaciony
Semantica:
Podemos hablar de que el xenunciado de asignaciony consiste de dos partes,
lo que se encuentra a la izquierda de la asignacion (=) y lo que se encuentra
a la derecha. A la izquierda tiene que haber una variable, pues es donde
vamos a guardar, copiar, colocar un valor. Este valor puede ser, como
en el caso del operador new, una referencia a un objeto en el heap, o un
valor. El; valor puede ser de alguno de los tipos primitivos o de alguna de
las clases accesibles. La expresion de la derecha se evalua (se ejecuta) y el
valor que se obtiene se coloca en la variable de la izquierda. Si la expresion
no es del mismo tipo que la variable, se presenta un error de sintaxis. Toda
expresion tiene que regresar un valor.
3.2 Programacion en Java 88
Sintaxis:
xconstruccion de objetoy ::=new xinvocacion metodo constructory
Semantica:
Para construir un objeto se utiliza el operador new y se escribe a conti-
nuacion de el (dejando al menos un espacio) el nombre de alguno de los
constructores que hayamos declarado para la clase, junto con sus argumen-
tos. El objeto queda construido en el heap y tiene todos los elementos que
vienen descritos en la clase.
Sintaxis:
xinvocacion de metodoy ::=xnombre del metodoy(xArgumentosy )
xArgumentosy ::=xargumentoy (,xargumentoy)* |
xargumentoy ::=expresion
Semantica:
Los xArgumentosy tienen que coincidir en numero, tipo y orden con los
xParametrosy que aparecen en la declaracion del metodo. La sintaxis indi-
ca que si la declaracion no tiene parametros, la invocacion no debe tener
argumentos.
Si el metodo regresa algun valor, entonces la invocacion podra aparecer
en una expresion. Si su tipo es void tendra que aparecer como enunciado
simple.
89 Clases y objetos
El operador new nos regresa una direccion en el heap donde quedo construido
el objeto (donde se encuentran las variables del objeto). Tengo que guardar esa
referencia en alguna variable del tipo del objeto para que lo pueda usar. Si nos
lanzamos a programar los constructores de la clase Reloj, lo hacemos instanciando
a las manecillas correspondientes. La implementacion de estos constructores se
pueden ver en el listado 3.19.
Una expresion en Java es cualquier enunciado que nos regresa un valor. Por
ejemplo, new Manecilla(limH) es una expresion, puesto que nos regresa un obje-
to de la clase Reloj. Podemos clasificar a las expresiones de acuerdo al tipo del
valor que regresen. Si regresan un valor numerico entonces tenemos una expre-
sion aritmetica; si regresan falso o verdadero tenemos una expresion booleana; si
regresan una cadena de caracteres tenemos una expresion tipo String. Tambien
podemos hacer que las expresiones se evaluen a un objeto de determinada clase.
Cuando escribimos una expresion aritmetica tenemos, en general, dos dimen-
siones en las cuales movernos: una vertical y otra horizontal. Por ejemplo, en la
formula que da la solucion de la ecuacion de segundo grado
?2
x1 b b
2a
4ac
x{px 1q
x
x 1
x{x
x
1 1
x
Como se puede deducir del ejemplo anterior, la division tiene mayor prece-
dencia (se hace antes) que la suma, por lo que en ausencia de parentesis se
evalua como en el segundo ejemplo. Con los parentesis estamos obligando a
que primero se evalue la suma, para que pase a formar el segundo operando
de la division, como se muestra en el primer ejemplo.
4ac 4ac
3px 2y q 3 px 2 y q
Finalmente, son pocos los lenguajes de programacion que tienen como opera-
dor la exponenciacion, por lo que expresiones como b2 se tendran que expresar en
terminos de la multiplicacion de b por s misma, o bien usar algun metodo (como
el que usamos para raz cuadrada) que proporcione el lenguaje o alguna de sus
bibliotecas. La famosa formula para la solucion de una ecuacion de segundo
grado quedara entonces
?2
x1 b b
2a
4ac x1 pb M ath.sqrtppb bq p4 a cqqq{p2 aq
Hay que recordar que estos metodos, por ser constructores, de hecho regresan
95 Clases y objetos
Tenemos ya las clases terminadas. Ahora tendramos que tener un usuario que
comprara uno de nuestros relojes. Hagamos una clase cuya unica funcion sea
probar el Reloj. La llamaremos UsoReloj. Se encuentra en el listado 3.27.
/ M a n i p u l a c i o n d e l r e l o j i t o /
r e l o j i t o . incrementa ( ) ;
r e l o j i t o . muestra ( c o n s o l i t a ) ;
r e l o j i t o . incrementa ( ) ;
r e l o j i t o . muestra ( c o n s o l i t a ) ;
r e l o j i t o . setValor (10 ,59);
r e l o j i t o . muestra ( c o n s o l i t a ) ;
r e l o j i t o . incrementa ( ) ;
r e l o j i t o . muestra ( ) ;
} // main
} // U s o R e l o j
No hemos mencionado que en Java se permite asignar valor inicial a los atri-
butos y a las variables locales en el momento en que se declaran. Esto se consigue
3.3 Expresiones en Java 98
Uno de los ingredientes que mas comunmente vamos a usar en nuestros pro-
gramas son las expresiones. Por ello, dedicaremos este captulo a ellas.
cadenas la sintaxis de Java es mucho mas flexible para la creacion de cadenas que
de objetos en general y nos permite cualquiera de los siguientes formatos:
ii. En una asignacion. Se asigna una cadena a una variable tipo String:
S t r i n g cadenota ;
c a d e n o t a = "Una cadena "+ " muy larga " ;
iii. Al vuelo. Se construye una cadena como una expresion, ya sea directamente
o mediante funciones de cadenas:
Es importante mencionar que las cadenas, una vez creadas, no pueden ser
modificadas. Si se desea modificar una cadena lo que se debe hacer es construir
una nueva con las modificaciones, y, en todo caso, reasignar la nueva. Por ejemplo,
si queremos pasar a mayusculas una cadena, podramos tener la siguiente sucesion
de enunciados:
Descripcion:
Cada objeto del curso consiste del numero del grupo (una cadena), la lista de
alumnos y el numero de alumnos. La lista de alumnos consiste de alumnos,
donde para cada alumno tenemos su nombre completo, su numero de cuenta,
la carrera en la que estan inscritos y su clave de acceso a la red.
Las operaciones que queremos se puedan realizar son:
En la interfaz que acabamos de dar, casi todos los metodos que hacen la con-
sulta trabajan a partir de saber la posicion relativa del registro que queremos. Sin
embargo, una forma comun de interrogar a una base de datos es proporcionandole
informacion parcial, como pudiera ser alguno de los apellidos, por lo que conviene
agregar un metodo al que le proporcionamos esta informacion y nos debera de-
cir la posicion relativa del registro que contiene esa informacion. Este metodo lo
podemos ver en el Listado 4.2.
Sin embargo, pudieramos buscar una porcion del registro que se repite mas de
una vez, y quisieramos que al interrogar a la base de datos, esta nos diera, uno tras
4.2 Implementacion de una base de datos 108
otro, todos los registros que tienen esa subcadena. Queremos que cada vez que le
pidamos no vuelva a empezar desde el principio, porque entonces nunca pasara
del primero. Le agregamos entonces un nuevo parametro para que la busqueda
sea a partir de un posicion. El encabezado de este metodo se puede ver en el
Listado 4.3.
private String
lista =
" Aguilar Sols Aries Olaf " + "Mate" + " 975412191 " + " aguilarS " +
"Cruz Cruz Gil Noe " + "Comp" + " 990363584 " + " cruzCruz " +
" Garca Villafuerte Israel " + "Comp" + " 025986583 " + " garciaV " +
" Hubard Escalera Alfredo " + "Comp" + " 002762387 " + " hubardE " +
" Tapia Vazquez Rogelio " + "Actu" + " 026393668 " + " tapiaV " ;
Habamos comentado que queremos dos constructores, uno que trabaje a partir
de una lista que de el usuario, y otro que inicie con una lista vaca y vaya agregando
113 Manejo de cadenas y expresiones
nombres conforma el usuario los va dando. El codigo para ambos casos se puede
ver en el listado 4.5.
Codigo 4.5 Constructores para la clase Curso (Curso)
24: /
25: C o n s t r u y e una b a s e de d a t o s a p a r t i r de l o s d a t o s que de e l
26: usuario .
27: @param g r u p o La c l a v e d e l g r u p o
28: @param l i s t a La l i s t a b i e n armada d e l g r u p o
29: @param c u a n t o s E l numero de r e g i s t r o s que s e r e g i s t r a n
30: /
31: p u b l i c C u r s o ( S t r i n g grupo , S t r i n g l i s t a ) {
32: t h i s . l i s t a = l i s t a == n u l l
33: ? ""
34: : lista ;
35: t h i s . g r u p o = g r u p o == n u l l
36: ? "????"
37: : grupo ;
38: numRegs = l i s t a . l e n g t h ()==0
39: ?0
40: : l i s t a . l e n g t h ( ) /TAM REG + 1 ;
41: }
42: /
43: C o n s t r u y e una b a s e de d a t o s v a c a p e r o con numero de g r u p o
44: @param g r u p o Numero de g r u p o
45: /
46: public Curso ( S t r i n g grupo ) {
47: t h i s . l i s t a = "" ;
48: t h i s . g r u p o = g r u p o == n u l l
49: ? "????"
50: : grupo ;
51: numRegs = 0 ;
52: }
Como siempre nos vamos a estar moviendo al inicio del i-esimo registro vamos
a elaborar un metodo, privado, que me de el caracter en el que empieza el i-esimo
registro, mediante la formula
posicion pi 1q T AM REG.
Hay que tomar en cuenta aca al usuario, que generalmente va a numerar los
registros empezando desde el 1 (uno), no desde el 0 (cero). Con esto en mente y
de acuerdo a lo que es la que discutimos al inicio de este tema, el metodo queda
como se puede observar en el listado 4.7.
Los metodos que regresan un campo siguen todos el patron dado en la figura 4.4
y su implementacion se puede ver en el listado 4.8 en la pagina opuesta.
115 Manejo de cadenas y expresiones
una posicion inicial a partir de donde buscar. Le damos al metodo daPosicion otra
firma que tome en cuenta este parametro adicional. La programacion se encuentra
tambien en los listados 4.9 en la pagina opuesta y 4.10.
El unico metodo que nos falta de los que trabajan con un registro particular
es el que arma un registro para mostrarlo. El algoritmo es sumamente sencillo, y
lo mostramos en la figura 4.6. Lo unico relevante es preguntar si el registro que
nos piden existe o no.
De las funciones mas comunes a hacer con una lista de un curso es listar toda
la lista completa. Sabemos cuantos registros tenemos, todo lo que tenemos que
hacer es recorrer la lista e ir mostrando uno por uno. A esto le llamamos iterar
sobre la lista. El algoritmo podra ser el que se ve en la figura 4.7.
Sintaxis:
xenunciado compuesto whiley::=
while ( xexpresion booleanay ) {
xenunciado simple o compuestoy
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
}
Semantica:
Lo primero que hace el programa es evaluar la xexpresion booleanay. Si esta
se evalua a verdadero, entonces se ejecutan los enunciados que estan entre
las llaves, y regresa a evaluar la xexpresion booleanay. Si se evalua a falso,
brinca todo el bloque y sigue con el enunciado que sigue al while.
1
Figura 4.9 Encontrar el lmite de 2n , dado
$ $
'
'
' '
& = .001
'
'
'
' Inicializar double f Actl 1{2
'
'
' '
%
'
' double f Ant 0
'
'
&
Calcular lmite de #
1
'
' Calcular siguiente termino f Ant f Actl
2n '
'
'
'
' (mientras |fant factl | ) f Actl f Ant{2
'
'
'
'
'
' !
'
% Final Reporta f Actl
Como se puede ver, esta iteracion es ideal cuando no tenemos claro el numero
de iteraciones que vamos a llevar a cabo y deseamos tener la posibilidad de no
ejecutar el cuerpo ni siquiera una vez. Por ejemplo, si el valor de que nos pasaran
como parametro fuera mayor que 1{2, la iteracion no se llevara a cabo ni una vez.
Hay ocasiones en que deseamos que un cierto enunciado se ejecute al menos
una vez. Supongamos, por ejemplo, que vamos a sumar numeros que nos den desde
la consola hasta que nos den un 1. El algoritmo se puede ver en la figura 4.10.
121 Manejo de cadenas y expresiones
Sintaxis:
xenunciado compuesto do. . . whiley::=
do {
xenunciado simple o compuestoy
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
} while ( xexpresion booleanay );
Semantica:
Lo primero que hace el enunciado al ejecutarse es ejecutar los enunciados
que se encuentran entre el do y el while. Es necesario aclarar que estos enun-
ciados no tienen que estar forzosamente entre llaves (ser un bloque) pero las
llaves me permiten hacer declaraciones dentro del enunciado, mientras que
sin las llaves, como no tengo un bloque, no puedo tener declaraciones loca-
les al bloque. Una vez ejecutado el bloque procede a evaluar la xexpresion
booleanay. Si esta se evalua a verdadero, la ejecucion continua en el primer
enunciado del bloque; si es falsa, sale de la iteracion y sigue adelante con el
enunciado que sigue al do . . . while.
Es claro que lo que se puede hacer con un tipo de iteracion se puede hacer con la
otra. Si queremos que el bloque se ejecute al menos una vez usando un while, lo que
hacemos es colocar el bloque inmediatamente antes de entrar a la iteracion. Esto
nos va a repetir el codigo, pero el resultado de la ejecucion va a ser exactamente
el mismo. Por otro lado, si queremos usar un do. . . while pero queremos tener
la posibilidad de no ejecutar ni una vez, al principio del bloque ponemos una
condicional que pruebe la condicion, y como cuerpo de la condicional colocamos el
bloque original. De esta manera si la condicion no se cumple al principio el bloque
no se ejecuta. Esto quiere decir que si un lenguaje de programacion unicamente
cuenta con una de estas dos iteraciones, sigue teniendo todo lo necesario para
elaborar metodos pensados para la otra iteracion.
Tenemos una tercera iteracion conocida como for, que resulta ser el canon
de las iteraciones, en el sentido de que en un solo enunciado inicializa, evalua
una expresion booleana para saber si entra a ejecutar el enunciado compuesto e
incrementa al final de la ejecucion del enunciado compuesto. Lo veremos cuando
sea propicio su uso. Por lo pronto volveremos a nuestro problema de manejar una
pequena base de datos.
La lista del curso debemos mostrarla en algun medio. Para usar dispositivos
de entrada y salida utilizaremos por lo pronto una clase construida especialmente
para ello, se llama Consola y se encuentra en el paquete icc1.interfaz. Tenemos que
crear un objeto de tipo Consola, y a partir de ese momento usarlo para mostrar
y/o recibir informacion del usuario. Veamos los principales metodos que vamos a
usar por el momento en la tabla 4.3 en la pagina opuesta.
123 Manejo de cadenas y expresiones
Para ajustar los tamanos de las cadenas, primero les agregamos al final un
monton de blancos, para luego truncarla en el tamano que debe tener. La progra-
macion se encuentra en el listado 4.15 en la pagina opuesta.
125 Manejo de cadenas y expresiones
Caso 1
loooooooooooooooooooooooooooooooooooooooooooooomoooooooooooooooooooooooooooooooooooooooooooooon
loooooooooooooooooooooooooooooooooooooooooooooomoooooooooooooooooooooooooooooooooooooooooooooon Caso 3
4.2 Implementacion de una base de datos 126
Conocemos de cual de estas tres situaciones se trata (no hay otra posibilidad)
dependiendo de la posicion del registro:
Es el primero si i vale 1.
Es el ultimo si i vale NUM REGS.
Esta en medio si 1 i NUM REGS.
Para este tipo de enunciado es conveniente que introduzcamos el xenunciado
compuesto condicionaly cuya sintaxis y semantica se encuentra en la figura 4.15 en
la pagina opuesta. Con esto podemos ya pasar a programar el metodo que elimina
al i-esimo registro de nuestra lista, en el listado 4.16 en la pagina opuesta.
El unico metodo que nos falta, para terminar esta seccion, es el que arma una
lista con todos los registros que contienen una subcadena. En este caso todo lo
que tenemos que hacer es, mientras encontremos la subcadena buscada, seguimos
buscando, pero a partir del siguiente registro. El algoritmos lo podemos ver en la
figura 4.16 en la pagina 128.
127 Manejo de cadenas y expresiones
Sintaxis:
xenunciado compuesto condicionaly::=
if ( xexpresion booleanay ) {
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
} else {
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
}
Semantica:
Lo primero que hace el programa es evaluar la xexpresion booleanay. Si
esta se evalua a verdadero, entonces se ejecutan los enunciados que estan
entre las primeras llaves y si se evalua a falso se ejecutan los enunciados
que estan a continuacion del else. Pudieramos en ambos casos tener un
unico enunciado, en cuyo caso se pueden eliminar las llaves correspondien-
tes. Tambien podemos tener que no aparezca una clausula else, en cuyo
caso si la expresion booleana se evalua a falso, simplemente se continua la
ejecucion con el enunciado que sigue al if.
Figura 4.16 Metodo que encuentra TODOS los que contienen a una subcadena.
$ $
'
'
' '
'
'
' '
'
'
'
'
' '
'
' donde Primer registro que caza.
'
'
' &
'
'
' P rincipio
'
'
' '
' cazaCon (cadena vaca)
'
' '
'
'
'
'
' '
'
'
' '
%
'
&
Arma cadena $
'
'
'
'
' '
'
'
'
' '
'
' '
'
'cazaCon cazaCon
'
'
' '
& armaRegistropdondeq
Arma siguiente
'
'
'
'
'
'
(mientras hayas
'
'
' '
'
'
'
'
'
encontrado) '
' donde Siguiente registro que caza
'
' '
'
%
%
Lo unico importante en este caso es darnos cuenta que ya tenemos dos ver-
siones que nos dan la posicion de una subcadena, as que la programacion es
practicamente directa. La podemos ver en el listado 4.17 en la pagina opuesta.
129 Manejo de cadenas y expresiones
Codigo 4.17 Metodo que lista a los que cazan con . . . (Curso)
211: /
212: C o n s t r u y e una l i s t a p a r a m o s t r a r con t o d o s l o s r e g i s t r o s que
213: t i e n e n como s u b c a d e n a a l p a r a m e t r o .
214: @param s u b c a d Lo que s e b u s c a en cada r e g i s t r o
215: @ r e t u r n Una c a d e n a que c o n t i e n e a l o s r e g i s t r o s que c a z a n
216: /
217: p u b l i c S t r i n g losQueCazanCon ( S t r i n g s u b c a d ) {
218: S t r i n g cazanCon = "" ;
219: i n t donde = d a P o s i c i o n ( s u b c a d ) ;
220: w h i l e ( donde != 1) {
221: cazanCon = cazanCon . c o n c a t ( a r m a R e g i s t r o ( donde)+"\n" ) ;
222: donde = d a P o s i c i o n ( subcad , donde ) ;
223: } // w h i l e e n c u e n t r e s
224: r e t u r n cazanCon ;
225: }
Contar con una base de datos que tiene las funcionalidades usuales no es
suficiente. Debemos contar con algun medio de comunicacion con la misma, como
pudiera ser un lenguaje de consulta (query language) o, simplemente, un menu que
nos de acceso a las distintas operaciones que podemos realizar sobre la base de
datos. Construiremos un menu para tal efecto.
$ $
'
'
' '
'
' Muestra el menu
'
'
' '
'
'
'
'
' '
'
' !
Pide opcion al usuario
'
'
' '
'
' opcion == Termina Salir
'
'
' '
'
'
'
'
' '
'
'
'
' '
' #
'
'
' '
'
' Pide registro
'
'
' '
'
'
opcion == Agrega
'
'
' '
'
'
Agrega registro
'
'
' '
'
'
'
'
' '
'
' $
'
'
' '
'
' '
' Pregunta a quien
'
' '
' '
'
'
'
' '
' '
'
'
'
' '
'
' &Buscalo !
'
'
' '
'
' opcion == Quita Encontrado Qutalo
'
'
' '
'
' '
'
'
Menu '
'
& Maneja Menu '
'
& '
'
' !
'
%
para (mientras desee Encontrado Reporta No encontrado
'
'
' '
'
'
Cursos '
'
'
el usuario) '
'
' $
'
'
' '
'
' '
'
' '
' '
'
'
Pregunta a quien
'
'
' '
'
' '
'
'
' '
' '
&Buscalo !
'
'
' '
'
'
'
'
' '
'
'
opcion == Busca Encontrado Reportalo
'
'
' '
' '
'
'
'
'
' '
'
' '
' !
'
' '
' '
%
'
'
' '
'
'
Encontrado Reporta No encontrado
'
' '
'
'
'
' '
'
' !
'
'
' '
'
'
'
'
' '
'
'
opcion == Lista Lista todo el curso
'
' '
'
'
'
' '
'
' #
'
'
' '
'
'
'
'
% '
'
%
opcion == Todos los Pregunta subcadena
que cazan Arma la lista
El menu que requerimos corresponde a una clase que va a hacer uso de la clase
Curso que ya disenamos. Por lo tanto, podemos empezar ya su codificacion. El
hacerlo en una clase aparte nos permitira, en un futuro, tal vez manejar nuestra
base de datos con un lenguaje de consulta o hasta, posiblemente, con un lenguaje
de programacion.
131 Manejo de cadenas y expresiones
Sintaxis:
switch( xexpresiony ) {
case xvalor1 y:
xlista de enunciados simples o compuestosy;
case xvalor2 y:
xlista de enunciados simples o compuestosy;
case . . .
xlista de enunciados simples o compuestosy;
default:
xlista de enunciados simples o compuestosy;
}
Semantica:
Los valores valor1 , . . . deben ser del mismo tipo que la expresion, a la que
llamamos la selectora del switch y deben ser constantes de ese tipo. El
enunciado switch elige un punto de entrada al arreglo de enunciados de la
lista. Evalua la expresion y va comparando contra los valores que aparecen
frente a la palabra case. El primero que coincide, a partir de ese punto
ejecuta todos los enunciados desde ah hasta que se encuentre un enunciado
break, o el final del switch, lo que suceda antes. Se acostumbra colocar un
enunciado break al final de cada opcion para que unicamente se ejecuten los
enunciados relativos a esa opcion. El switch tiene una clausula de escape,
que puede o no aparecer. Si aparece y la expresion no toma ninguno de
los valores explcitamente listados, se ejecuta el bloque correspondiente a
default. Si no hay clausula de escape y el valor de la expresion no caza con
ninguno de los valores en los cases, entonces el programa abortara dando
un error de ejecucion.
Sintaxis:
xenunciado breaky ::= break
Semantica:
Hace que el hilo de ejecucion del programa no siga con el siguiente enun-
ciado, sino que salga del enunciado compuesto en el que esta, en este caso
el switch.
boolean e s t a = i != 1;
switch ( e s t a ) {
case f a l s e :
c o n s o l a . i m p r i m e l n ( "NO esta " ) ;
break ;
case t r u e :
c o n s o l a . i m p r i m e l n ( "SI esta" ) ;
}
Como las unicas dos posibilidades para el selector del switch, una expresion
booleana, es falso o verdadero, no hay necesidad de poner una clausula de escape,
ya que no podra tomar ningun otro valor. Este ejemplo es realmente un enunciado
if disfrazado, ya que si la expresion se evalua a verdadero (true) se ejecuta lo que
corresponde al caso etiquetado con true, mientras que si se evalua a falso, se ejecuta
el caso etiquetado con false. Programado con ifs queda de la siguiente manera:
boolean e s t a = i != 1;
i f ( esta )
c o n s o l a . i m p r i m e l n ( "SI esta" ) ;
else
c o n s o l a . i m p r i m e l n ( "SI esta" ) ;
Otro ejemplo mas apropiado, ya que tiene mas de dos opciones, es el siguiente:
supongamos que tenemos una clave que nos dice el estado civil de una persona y
que queremos convertirlo a la cadena correspondiente. Las claves son:
s: soltero
c: casado
d: divorciado
v: viudo
u: union libre
En una variable de tipo caracter tenemos una de estas opciones, y deberemos
133 Manejo de cadenas y expresiones
Codigo 4.19 Encabezado de la clase bfseries Menu y el metodo daMenu (MenuCurso) 2/2
46: else {
47: r e p o r t a N o ( cons , nombre ) ;
48: }
49: return 2;
50: case 3 : // Busca s u b c a d e n a
51: s u b c a d = c o n s . l e e S t r i n g ( "Dame la subcadena a "
52: + " buscar : " ) ;
53: donde = miCurso . d a P o s i c i o n ( s u b c a d ) ;
54: i f ( donde != 1) {
55: c o n s . i m p r i m e l n ( miCurso . a r m a R e g i s t r o ( donde ) ) ;
56: }
57: else {
58: r e p o r t a N o ( cons , s u b c a d ) ;
59: }
60: return 3;
61: case 4 : // L i s t a t o d o s
62: miCurso . l i s t a C u r s o ( c o n s ) ;
63: return 4;
64: case 5 : // L i s t a con c r i t e r i o
65: s u b c a d = c o n s . l e e S t r i n g ( "Da la subcadena que " +
66: " quieres contengan los " +
67: " registros :" ) ;
68: r e g i s t r o s = miCurso . losQueCazanCon ( s u b c a d ) ;
69: i f ( r e g i s t r o s . e q u a l s ( "" ) ) {
70: c o n s . i m p r i m e l n ( "No hubo ningun registro con " +
71: "este criterio " ) ;
72: }
73: else {
74: cons . imprimeln ( r e g i s t r o s ) ;
75: }
76: return 5;
77: d e f a u l t : // E r r o r , v u e l v e a p e d i r
78: c o n s . i m p r i m e l n ( "No diste una opcion valida .\n" +
79: "Por favor vuelve a elegir ." ) ;
80: return 0;
81: }
82: }
Para el menu construimos una cadena que se muestra separada por renglones,
como se observa en las lneas68 a 73, y le pedimos al usuario que elija un nume-
ro de opcion, en la lnea 76. En esta lnea aparece algo nuevo, ya que estamos
interaccionando con el usuario. La manera de hacerlo es a traves de una consola
(por eso aparece un objeto de esta clase como parametro). Por lo pronto vamos
a leer cadenas, y tenemos la opcion de pedirle o no que escriba una cadena como
4.3 Una clase Menu 136
ttulo de la pequena pantalla en la que solicita el dato. Las dos posible firmas del
metodo de lectura de cadenas de la clase consola son
String leeString ()
String leeString ( String )
4.3.1. Salir
En esta opcion se emite un mensaje para el usuario, avisandole del final del
proceso, y se regresa un valor de -1 para que el programa principal ya no siga
mostrando el menu y termine.
Esta opcion tiene que funcionar como una interface entre el metodo de la
base de datos y el usuario, para agregar de manera correcta a un estudiante. Por
ello, deberemos primero llenar cada uno de los campos del registro (sera absurdo
pedirle al usuario que conociera como esta implementada nuestra base de datos).
137 Manejo de cadenas y expresiones
Por ello se procede a solicitarle al usuario cada uno de los campos involucrados.
Elegimos hacer un metodo distinto para cada campo, para poder indicarle al
usuario que tipo de cadena estamos esperando. La codificacion de cada uno de
estos metodos se encuentran en el listado 4.20. Algunos de estos metodos los
volveremos a usar, ya que nos proporcionan, por parte del usuario, informacion.
Cada uno de estos metodos simplemente le dice al usuario que es lo que espera y
recibe una cadena, que, idealmente, debera ser lo que el usuario espera.
Codigo 4.20 Metodos para agregar un estudiante a la base de datos (MenuCurso) 2/2
113: /
114: P i d e en e l i n p u t s t r e a m e l numero de c u e n t a d e l alumno .
115: @param c o n s e l I n p u t S t r e a m
116: @ r e t u r n E l numero de c u e n t a l e d o
117: /
118: private S t r i n g pideCuenta ( Consola cons ) {
119: S t r i n g cuenta =
120: c o n s . l e e S t r i n g ( "Dame el numero de cuenta del "
121: + " estudiante , de 9 dgitos " ) ;
122: return cuenta ;
123: }
Tambien realiza su tarea usando metodos que ya explicamos. Al igual que las
otras opciones, verifica que las operaciones sobre la base de datos se realicen de
manera adecuada.
En este caso, regresaremos un valor que permita al usuario saber que no dio
una opcion correcta y que debe volver a elegir.
Con esto damos por terminada nuestra primera aproximacion a una base de
datos. En lo que sigue haremos mas eficiente y flexible esta implementacion, bus-
cando que se acerque mas a lo que es una verdadera base de datos.
Datos estructurados
5
La implementacion que dimos en el captulo anterior a nuestra base de datos
es demasiado alejada de como abstraemos la lista del grupo. Realmente, cuando
pensamos en una lista es una cierta coleccion de registros, donde cada registro
tiene uno o mas campos. Tratemos de acercarnos un poco mas a esta abstraccion.
El tipo de coleccion que usaremos en esta ocasion es una lista. La definicion
de una lista es la siguiente:
Por ejemplo, si una lista nada mas tiene un elemento, esta consiste del primer
elemento de una lista, seguido de una lista vaca.
Toda lista es una referencia a objetos de cierto tipo que contienen determinada
informacion y donde al menos uno de sus campos es una referencia, para acomodar
all a la lista que le sigue. En Java, si una lista es la lista vaca tendra el valor de null,
que corresponde a una referencia nula. Generalmente representamos las listas como
se muestra en la figura 5.1 en la siguiente pagina, con la letra r representando
un campo para guardar all una referencia, y el smbolo representando que no
sigue nadie (una referencia nula).
Como la definicion de la lista es recursiva, debemos siempre tener anclado
5.1 La clase para cada registro 142
1
Del ingles, chain.
143 Datos estructurados
Una vez que tenemos los constructores, tenemos que proveer, para cada campo
al que queramos que se tenga acceso, un metodo de consulta y uno de actuali-
zacion. La programacion de estos metodos se encuentra en el listado 5.3 en la
siguiente pagina.
5.1 La clase para cada registro 144
37: /
38: R e g r e s a e l c o n t e n i d o d e l campo nombre .
39: @return String
40: /
41: p u b l i c S t r i n g getNombre ( ) {
42: r e t u r n nombre ;
43: }
44: /
45: A c t u a l i z a e l campo nombre .
46: @param S t r i n g e l v a l o r que s e va a a s i g n a r .
47: /
48: p u b l i c v o i d setNombre ( S t r i n g nombre ) {
49: t h i s . nombre = nombre ;
50: }
51: /
52: R e g r e s a e l c o n t e n i d o d e l campo c a r r e r a .
53: @return String
54: /
55: public String getCarrera () {
56: return c a r r e r a ;
57: }
58: /
59: A c t u a l i z a e l campo c a r r e r a .
60: @param S t r i n g e l v a l o r que s e va a a s i g n a r .
61: /
62: public void s e t C a r r e r a ( S t r i n g c a r r e ) {
63: carrera = carre ;
64: }
65: /
66: R e g r e s a e l c o n t e n i d o d e l campo c u e n t a .
67: @return String
68: /
69: public S t r i n g getCuenta () {
70: return cuenta ;
71: }
72: /
73: A c t u a l i z a e l campo c u e n t a .
74: @param S t r i n g e l v a l o r que s e va a a s i g n a r .
75: /
76: public void setCuenta ( S t r i n g cnta ) {
77: cuenta = cnta ;
78: }
145 Datos estructurados
Podemos tambien poner metodos mas generales, dado que todos los atribu-
tos son del mismo tipo, que seleccionen un campo a modificar o a regresar. Los
podemos ver en el codigo 5.4.
106: c l a s s E s t u d i a n t e {
107: /
108: R e g r e s a e l campo s o l i c i t a d o .
109: @param c u a l i d e n t i f i c a d o r d e l campo s o l i c i t a d o
110: @ r e t u r n La c a d e n a con e l campo s o l i c i t a d o
111: /
112: p u b l i c S t r i n g getCampo ( i n t c u a l ) {
113: switch ( c u a l ) {
114: case NOMBRE: r e t u r n getNombre ( ) ;
115: case CUENTA : return getCuenta ( ) ;
5.1 La clase para cada registro 146
Codigo 5.4 Metodos que arman y actualizan registro completo (Estudiante) 2/2
Finalmente, de esta clase unicamente nos faltan dos metodos, uno que regrese
todo el registro armado, listo para impresion, y uno que actualice todos los campos
del objeto. Podemos ver su implementacion en el listado 5.5.
Codigo 5.5 Metodos que arman y actualizan registro completo (2) (Estudiante)1/2
Codigo 5.5 Metodos que arman y actualizan registro completo (2) (Estudiante)2/2
149: / A c t u a l i z a t o d o e l r e g i s t r o de un j a l o n .
150: @param S t r i n g e l nombre ,
151: S t r i n g cuenta
152: String carrera
153: String clave .
154: /
155: p u b l i c v o i d s e t R e g i s t r o ( S t r i n g nmbre , S t r i n g c n ta ,
156: String clve , String crrera ) {
157: nombre = nmbre . t r i m ( ) ;
158: cuenta = cnta . trim ( ) ;
159: clave = clve . trim ( ) ;
160: carrera = c r r e r a . trim ( ) ;
161: }
lista, una referencia al primer elemento. Por supuesto que esta lista estara vaca
en tanto no le agreguemos ningun objeto del tipo Estudiante. Por lo tanto, la unica
diferencia entre las declaraciones de nuestra implementacion anterior y esta es el
tipo de la lista, quedando las primeras lneas de la clase como se puede apreciar
en el listado 5.6.
Al igual que con la clase Curso, tenemos dos constructores, uno que unicamente
coloca el numero del grupo y otro inicializa la lista y coloca el numero de grupo.
Debemos recordar que lo primero que hace todo constructor es poner los valores
de los atributos que corresponden a referencias en null y los que corresponden a
valores numericos en 0. Los constructores se encuentran en el listado 5.7.
$ $ !
'
'
' '
' inicia contador cuantos 0
'
' '
'
'
'
'
' '
&
'
' $
'
'
' Inicio '
&
'
' '
'
'
Colocate al
'
'
' '
'
' inicio de actual lista
'
' % '
%
'
'
' la lista
'
'
'
'
'
' $ $
'
'
& '
' '
&
Cuenta registros '
'
' incrementa
'
'
' cuantos
en una lista '
'
' '
' contador '
%
'
' Cuenta el '
&
'
'
'
'
'
'
registro actual
' $
'
' '
'
' '
'
'
'
(mientras haya) '
' &
'
' '
'
'
pasa al
actual toma el siguiente
'
'
' '
' '
'
' %siguiente %
'
'
'
'
'
'
'
' !
% F inal Entrega el contador
Codigo 5.9 Recorrido de una lista para contar sus registros (ListaCurso)
43: / C a l c u l a e l numero de r e g i s t r o s en l a l i s t a .
44: @ r e t u r n i n t E l numero de r e g i s t r o s
45: /
46: p u b l i c i n t getNumRegs ( ) {
47: int cuantos = 0;
48: Estudiante actual = l i s t a ;
49: w h i l e ( a c t u a l != n u l l ) {
50: c u a n t o s ++;
51: actual = actual . getSiguiente ();
52: }
53: return cuantos ;
54: }
151 Datos estructurados
Este metodo nos da el patron que vamos a usar casi siempre para recorrer una
lista, usando para ello la referencia que tiene cada registro al registro que le sigue.
El patron general es como se muestra en la figura 5.3.
Junto con los metodos de acceso, debemos tener metodos que alteren o asig-
nen valores a los atributos. Sin embargo, tanto el atributo numRegs como lista
deberan ser modificados por las operaciones de la base de datos, y no directa-
mente. En cambio, podemos querer cambiarle el numero de grupo a una lista. Lo
hacemos simplemente indicando cual es el nuevo numero de grupo. Podemos ver
este metodo en el listado 5.10.
decir que la nueva lista consiste de este primer registro, seguido de la vieja lista.
Veamos en la figura 5.4 que es lo que queremos decir. El diagrama de Warnier-Orr
para hacer esto esta en la figura 5.5 y la programacion del metodo se encuentra
en el listado 5.11.
$
'
&Pon a nuevo a apuntar a lista
Agrega registro
al principio '
% Pon a lista a apuntar a nuevo
Antes:
r lista
r Inf o r
nuevo
Despues:
r lista
r Inf o r
nuevo
153 Datos estructurados
r lista
r r
Info
nuevo
Como se puede ver, seguimos el mismo patron que recorre la lista, pero nuestra
155 Datos estructurados
Para el metodo que escribe todos los registros que cazan con una cierta subca-
dena tambien vamos a usar este patron, pues queremos revisar a todos los registros
y, conforme los vamos revisando, decidir para cada uno de ellos si se elige (im-
prime) o no. El algoritmo se encuentra en la figura 5.9 y la programacion en el
listado 5.14.
lista
r Eliminacion entre dos registros
En el primer caso tenemos que redirigir la referencia lista a que ahora apunte
hacia el que era el segundo registro. En el segundo caso tenemos que conservar la
5.2 La lista de registros 160
informacion del registro anterior al que queremos eliminar, para poder modificar
su referencia al siguiente a que sea a la que apuntaba el registro a eliminar. El
algoritmo para eliminar un registro se encuentra en la figura 5.12.
La programacion del menu para tener acceso a la base de datos del curso es
sumamente similar al caso en que los registros eran cadenas, excepto que ahora,
para agregar a cualquier estudiante hay que construir un objeto de la clase Estu-
diante. El algoritmo es el mismo, por lo que ya no lo mostramos. La programacion
de encuentra en el listado 5.17.
Es en esta clase donde realmente se van a crear objetos nuevos para poderlos ir
enlazando en la lista. Por ejemplo, para agregar un estudiante, una vez que tene-
mos los datos (lneas 95: en la pagina 164- 98: en la pagina 164 en el listado 5.17),
procedemos a invocar al metodo agrega de la clase ListaCurso. Este metodo tiene
como argumento un objeto, que es creado en el momento de invocar a agrega
ver lnea 99: en la pagina 164 del listado 5.17. Este es el unico metodo en el que
se crean objetos, y esto tiene sentido, ya que se requiere crear objetos solo cuando
se desea agregar a un estudiante.
Para el caso de los metodos de la clase ListaCurso que regresan una referencia a
un objeto de tipo estudiante, es para lo que se declaro la variable donde lnea 76:
en la pagina 164 del listado 5.17. En estos casos el objeto ya existe, y lo unico que
hay que pasar o recibir son las variables que contienen la referencia.
Con esto damos por terminado este captulo. Se dejan como ejercicios las
siguientes mejoras:
subclase hereda los atributos y metodos de la superclase. Que herede quiere decir
que, por el hecho de extender a la superclase, tiene al menos todos los atributos
y metodos de la superclase.
Si regresamos por unos momentos a nuestro ejemplo de la base de datos para
listas de cursos, veremos que hay muchas otras listas que se nos ocurre hacer y
que tendran la misma informacion basica. Quitemos la clave de usuario, porque
esa informacion no la necesitan mas que en el centro de computo, y entonces
nuestra clase EstudianteBasico quedara definida como se muestra en el listado 6.1.
Omitimos los comentarios para tener un codigo mas fluido.
Se nos ocurren distintos tipos de listas que tomen como base a EstudianteBasico.
Por ejemplo, un listado que le pudiera servir a una biblioteca tendra, ademas de la
informacion de EstudianteBasico, los libros en prestamo. Un listado para la Division
de Estudios Profesionales debera tener una historia academica y el primer ano
de inscripcion a la carrera. Un listado para calificar a un grupo debera tener un
cierto numero de campos para las calificaciones. Por ultimo, un listado para las
salas de computo debera contar con la clave de acceso.
En todos los casos que listamos, la informacion original debe seguir estando
presente, as como los metodos que tiene la superclase. Debemos indicar, entonces,
que la clase que se esta definiendo extiende a la superclase, heredando por lo tanto
todos los metodos y atributos de la superclase. La sintaxis en Java se muestra a
continuacion.
Sintaxis:
xsubclase que hereday ::= class xsubclasey extends xsuperclasey {
xdeclaraciones de campos y metodosy
}
Semantica:
La xsubclasey es una nueva declaracion, que incluye (hereda) a todos los
campos de la superclase, junto con todos sus metodos. Las xdeclaraciones
de campos y metodosy se refiere a lo que se desea agregar a la definicion de
la superclase en esta subclase.
6.2 Arreglos
donde estamos asumiendo que cada estudiante tiene lugar para, a lo mas, 15
calificaciones (del 0 al 14). Se ve, asimismo, muy util el poder manejar a cada una
de las calificaciones refiriendonos a su subndice, en lugar de declarar calif0, calif1,
etc., imitando el manejo que damos a los vectores en matematicas.
6.2 Arreglos 172
Es importante notar que lo que tenemos es una coleccion de datos del mismo
tipo que se distinguen uno de otro por el lugar que ocupan. A estas colecciones les
llamamos en programacion arreglos. La declaracion de arreglos tiene la siguiente
sintaxis:
Sintaxis:
xdeclaracion de arregloy ::= xtipoy[ ] xidentificadory;
Semantica:
Se declara una variable que va a ser una referencia a un arreglo de objetos
o datos primitivos del tipo dado.
Veamos algunos ejemplos:
int [ ] arregloDeEnteros ; // Arreglo de enteros
EstudianteBasico [ ] estudiantes ; // Arreglo de objetos del tipo
// EstudianteBasico.
float [ ] vector ; // Arreglo de reales.
String [ ] cadenas; // Arreglo de cadenas
Sintaxis:
xdeclaracion de arreglos con inicializaciony ::=
xtipoy[ ] xidentificadory = { xlistay };
Semantica:
Para inicializar el arreglo se dan, entre llaves, valores del tipo del arreglo,
separados entre s por coma. El arreglo tendra tantos elementos como apa-
rezcan en la lista, y cada elemento estara creado de acuerdo a la lista. En
el caso de un arreglo de objetos, la lista debera contener objetos que ya
existen, o la creacion de nuevos mediante el operador new.
E s t u d i a n t e B a s i c o paco =
new E s t u d i a n t e ( "Paco" ,
" 095376383 " ,
"Compu" ) ;
EstudianteBasico [ ] estudiantes =
{new E s t u d i a n t e ( ) ,
paco ,
new E s t u d i a n t e ( )
};
Veamos en las figuras 6.1 a 6.3 a continuacion como se crea el espacio para los
arreglos que se mostraron arriba. En los esquemas marcamos las localidades de
memoria que contienen una referencia con una @en la esquina superior izquier-
da. Esto quiere decir que su contenido es una direccion en el heap. Las localidades
estan identificadas con rectangulos. El numero que se encuentra ya sea inmedia-
tamente a la izquierda o encima del rectangulo corresponde a la direccion en el
heap.
6.2 Arreglos 174
MEMORIA HEAP
MEM HEAP
175 Herencia
1020
@ [0]
1500
@ 2054 2060 2066
1026 2054 [1] @4020 @5100 @3200
1032
@ [2]
1820
3200
Compu
4020
estudiantes @1020
Paco
5100
095376383
looooooooooooooooooooooooooooooooooooooomooooooooooooooooooooooooooooooooooooooon
MEM HEAP
EstudianteBasico [ ] estudiantes = {%
new E s t u d i a n t e ( ) ,
new E s t u d i a n t e ( "Paco" , " 095376383 " , "Compu" ) ,
new E s t u d i a n t e ( ) } ;
Sintaxis:
xdeclaracion de un arreglo con tamano dadoy ::=
xtipoy xidentificadory = new xtipoy[xexpresion enteray];
Semantica:
Se inicializa la referencia a un arreglo de referencias en el caso de objetos,
o de datos en el caso de tipos primitivos.
Si los ejemplos que dimos antes los hubieramos hecho sin la inicializacion seran:
1: i n t [ ] p r i m o s = new i n t [ 5 ] ;
2: E s t u d i a n t e B a s i c o [ ] e s t u d i a n t e s = new E s t u d i a n t e B a s i c o [ 3 ] ;
3: f l o a t [ ] v e c t o r = new f l o a t [ 3 ] ;
4: S t r i n g [ ] c a d e n a s = new S t r i n g [ 2 ] ;
MEMORIA HEAP
177 Herencia
27844 27850
cadenas @ 27844 @null @null
r0s r1s
MEMORIA HEAP
Sintaxis:
xseleccion de un elemento de un arregloy ::=
xid. de arregloy [ xexpresion enteray ]
Semantica:
El operador r s es el de mayor precedencia de entre los operadores de Java.
Eso indica que evaluara la expresion dentro de ella antes que cualquier otra
operacion (en la ausencia de parentesis). Una vez obtenido el entero corres-
pondiente a la xexpresion enteray que pudiera ser una constante entera
procedera a elegir al elemento con ese ndice en el arreglo. El resultado
de esta operacion es del tipo de los elementos del arreglo. De no existir el
elemento al que corresponde el ndice calculado, el programa abortara con
el mensaje ArrayIndexOutOfBoundsException. Al primer elemento del arre-
glo le corresponde siempre el ndice 0 (cero) y al ultimo elemento el ndice
n 1, donde el arreglo se creo con n elementos.
Es importante apreciar que el tamano de un arreglo no forma parte del tipo.
Lo que forma parte del tipo es el numero de dimensiones (vector, matriz, cubo,
. . . ) y el tipo de sus elementos.
p5q
1260 1264 1268 1272 1276
enteros @ 1260
2 4 6 8 10
[0] [1] [2] [3] [4]
p6q
enteros @ 2580
2580 2584 2588 2592 2596 2600 2604 2608 2612 2616
r0s 0 0 0 0 0 0 0 0
[0] [1] [2] [3] [4] [5] [6] [7] [8]
Como se puede observar en esta figura, el arreglo que se crea en la lnea 3 del
codigo no es el mismo que el que se crea en la lnea 7: no se encuentran en la
misma direccion del heap, y no contienen lo mismo. Insistimos: no es que haya
cambiado el tamano del arreglo, sino que se creo un arreglo nuevo.
Sintaxis:
xenunciado de iteracion enumerativay ::=
for ( xenunciado de inicializaciony ;
xexpresion booleanay ;
xlista de enunciadosy )
xenunciado simple o compuestoy
Semantica:
En la ejecucion va a suceder lo siguiente:
1. Se ejecuta el xenunciado de inicializaciony.
2. Se evalua la xexpresion booleanay.
a) Si es verdadera, se continua en el paso 3.
b) Si es falsa, se sale de la iteracion.
3. Se ejecuta el xenunciado simple o compuestoy.
4. Se ejecuta la xlista de enunciadosy.
5. Se regresa al paso 2.
Cualquiera de las tres partes puede estar vaca, aunque el ; s tiene que
aparecer. En el caso de la primera y tercera parte, el que este vaco indica
que no se hace nada ni al inicio del enunciado ni al final de cada iteracion.
En el caso de una xexpresion booleanay, se interpreta como la constante
true.
Vimos ya un ejemplo sencillo del uso de un while para recorrer un arreglo uni-
dimensional. Sin embargo, para este tipo de tareas el for es el enunciado indicado.
La misma iteracion quedara de la siguiente forma:
1: int [ ] enteros ;
2: ...
3: e n t e r o s = new i n t [ 5 ] ;
4: f o r ( i n t i = 0 ; i < 5 ; i++ ) {
5: e n t e r o s [ i ] = ( i + 1) 2 ;
6: }
Hay una pequena diferencia entre las dos versiones. En el caso del for, la varia-
ble i es local a el, mientras que en while tuvimos que declararla fuera. En ambos
casos, sin embargo, esto se hace una unica vez, que es el paso de inicializacion.
Otra manera de hacer esto con un for, usando a dos variables enumerativas,
una para i y otra para i + 1, pudiera ser como sigue:
181 Herencia
1: int [ ] enteros ;
2: ...
3: e n t e r o s = new i n t [ 5 ] ;
4: f o r ( i n t i = 0 , j = 1 ; i < 5 ; i ++, j ++) {
5: enteros [ i ] = j 2;
6: }
Del ejemplo anterior hay que notar que tanto i como j son variables locales al
for; para que esto suceda se requiere que la primera variable en una lista de este
estilo aparezca declarada con su tipo, aunque la segunda (tercera, etc.) variable no
debe aparecer como declaracion. Si alguna de las variables esta declarada antes y
fuera del for aparecera el mensaje de que se esta repitiendo la declaracion, aunque
esto no es correcto. Lo que s es valido es tener varios fors, cada uno con una
variable local i.
Por supuesto que el xenunciado simple o compuestoy puede contener o consistir
de, a su vez, algun otro enunciado for o while o lo que queramos. La ejecucion va
a seguir el patron dado arriba, terminando la ejecucion de los ciclos de adentro
hacia afuera.
EJEMPLO 6.2.7
Supongamos que queremos calcular el factorial de un entero n que nos pa-
san como parametro. El metodo podra estar codificado como se muestra en el
codigo 6.3.
EJEMPLO 6.2.8
Supongamos ahora que queremos tener dos variables para controlar la itera-
cion, donde la primera nos va a decir en cual iteracion va (se incrementa de 1 en
6.2 Arreglos 182
i= 1 j= 1
i= 2 j= 3
i= 3 j= 7
i= 4 j= 15
i= 5 j= 31
i= 6 j= 63
i= 7 j= 127
EJEMPLO 6.2.9
Podemos tener iteraciones anidadas, como lo dice la descripcion de la sintaxis.
Por ejemplo, queremos producir un triangulo con la siguiente forma:
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
1 2 3 4 5 6
1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9
En este caso debemos recorrer renglon por renglon, y en cada renglon reco-
rrer tantas columnas como renglones llevamos en ese momento. El codigo para
conseguir escribir esto se encuentra en el listado 6.5.
Es obvio que podemos necesitar arreglos de mas de una dimension, como por
ejemplo matrices. Para Java los arreglos de dos dimensiones son arreglos de arre-
6.2 Arreglos 184
i n t [ ] [ ] [ ] cubos ;
c u b o s = new i n t [ 3 ] [ ] [ ] ;
c u b o s [ 0 ] = new i n t [ 6 ] [ ] ;
c u b o s [ 1 ] = new i n t [ 3 ] [ 3 ] ;
c u b o s [ 2 ] = new i n t [ 2 ] [ ] ;
Codigo 6.6 Arreglos como parametros y valor de regreso de una funcion 1/2
1: p u b l i c c l a s s A r r e g l o s {
2: /
3: Suma d os a r r e g l o s de do s d i m e n s i o n e s .
4: @param Los a r r e g l o s
5: @ r e t u r n e l a r r e g l o que c o n t i e n e a l a suma
6: /
7: p u b l i c i n t [ ] [ ] suma ( i n t [ ] [ ] A , i n t [ ] [ ] B) {
8: i n t min1 = Math . min (A . l e n g t h , B . l e n g t h ) ;
9: i n t [ ] [ ] laSuma = new i n t [ min1 ] [ ] ;
10: / I n v o c a m o s a q u i e n s a b e sumar a r r e g l o s de una d i m e n s i o n /
11: f o r ( i n t i =0; i <min1 ; i ++) {
12: laSuma [ i ] = suma (A [ i ] , B [ i ] ) ;
13: } // end o f f o r ( ( i n t i =0; i <min1 ; i ++)
14:
15: r e t u r n laSuma ;
16: } // f i n suma ( i n t [ ] [ ] , i n t [ ] [ ] )
17: /
18: Suma d o s a r r e g l o s de una d i m e n s i o n
19: @param Dos a r r e g l o s de una d i m e n s i o n
20: @ r e t u r n Un a r r e g l o con l a suma
21: /
22: p u b l i c i n t [ ] suma ( i n t [ ] A , i n t [ ] B) {
23: i n t tam = Math . min (A . l e n g t h , B . l e n g t h ) ;
24:
25: i n t [ ] r e s u l t = new i n t [ tam ] ;
26: f o r ( i n t i =0; i <tam ; i ++) {
27: r e s u l t [ i ] = A[ i ] + B[ i ] ;
28: }
29: return r e s u l t ;
30: } // f i n suma ( i n t [ ] , i n t [ ] )
31: p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
32: C o n s o l a c o n s = new C o n s o l a ( ) ;
33: / Los d o s a r r e g l o s a sumar /
34: i n t [ ] [ ] uno = { { 3 , 4 , 5 } , { 2 , 8 , 7 , 5 } , { 3 , 4 , 7 } } ;
35: i n t [ ] [ ] dos = { { 8 , 4 , 2 , 1 } , { 8 , 7 , 2 , 3 } , { 4 , 3 , 5 , 1 } } ;
36: / A r r e g l o p a r a g u a r d a r l a suma /
37: i n t [ ] [ ] miSuma ;
6.3 Aspectos principales de la herencia 188
Codigo 6.6 Arreglos como parametros y valor de regreso de una funcion 2/2
38: / O b j e t o p a r a p o d e r u s a r l o s metodos /
39: A r r e g l o s p r u e b i t a = new A r r e g l o s ( ) ;
40: / I n v o c a c i o n a l a suma de d os m a t r i c e s /
41: miSuma = p r u e b i t a . suma ( uno , do s ) ;
42: / I m p r e s i o n de l o s r e s u l t a d o s /
43: f o r ( i n t i =0; i <miSuma . l e n g t h ; i ++) {
44: f o r ( i n t j =0; j <miSuma [ i ] . l e n g t h ; j ++) {
45: c o n s . i m p r i m e ( miSuma [ i ] [ j ]+"\t" ) ;
46: } // end o f f o r ( i n t j =0; j <miSuma [ i ] . l e n g t h ; j ++)
47: cons . imprimeln ( ) ;
48: } // end o f f o r ( i n t i =0; i <miSuma . l e n g t h ; i ++)
49: } // f i n main ( )
50: }
6.4 Polimorfismo
Una de las grandes ventajas que nos ofrece la herencia es el poder manipu-
lar subclases a traves de las superclases, sin que sepamos concretamente de cual
subclase se trata. Supongamos, para poner un ejemplo, que declaramos otra sub-
clase de EstudianteBasico que se llama EstudianteBiblio y que de manera similar
a como lo hicimos con EstudianteCurso extendemos adecuadamente y redefinimos
nuevamente el metodo getRegistro(). Podramos tener el codigo que sigue:
1: EstudianteBasico [ ] estudiantes = {
2: new E s t u d i a n t e C u r s o ( " Pedro ,. . . )
3: new EstudianteBiblio (. . . )
4: };
En estas lneas declaramos un arreglo con dos elementos del tipo Estudiante-
Basico, donde el primero contiene a un EstudianteCurso mientras que el segundo
contiene a un EstudianteBiblio. Como las subclases contienen todo lo que contiene
la superclase, y como lo que guardamos son referencias, podemos guardar en un
arreglo de la superclase elementos de las subclases. Es mas; si hacemos la siguiente
solicitud
S t r i n g cadena = e s t u d i a n t e s [ 0 ] . g e t R e g i s t r o ( ) ;
6.4 Polimorfismo 192
se da cuenta de que lo que tiene ah es una referencia a algo del tipo EstudianteCur-
so, por lo que utilizara la implementacion dada en esa subclase para dar respuesta
a esta solicitud. Decimos que gobierna el tipo de la referencia, no el tipo de la
declaracion. La decision de a cual de las implementaciones de getRegistro() debe
ser invocada se toma en ejecucion, ya que depende de la secuencia de ejecucion
el tipo de la referencia guardada en la localidad del arreglo. A esta capacidad de
los lenguajes orientados a objetos de resolver dinamicamente el significado de un
nombre de metodo es a lo que se llama polimorfismo, ya que el mismo nombre (de
hecho, la misma firma) puede tomar significados distintos dependiendo del estado
del programa.
El operador instanceof
Supongamos que estamos en la seccion escolar de la Facultad, donde tienen
registros de alumnos de distintos tipos, y que le piden al coordinador que por
favor le entregue una lista de todos los registros que tiene para la biblioteca. El
coordinador debera tener un programa que identifique de que clase es cada uno de
los objetos que se encuentran en la lista (arreglo, lista ligada, etc.). El operador
instanceof hace exactamente esto. Es un operador binario, donde las expresiones
que lo usan toman la siguiente forma:
xexpresion de objetoy instanceof xidentificador de clasey
y regresa el valor booleano verdadero si, en efecto, el objeto dado en la expresion
de la izquierda es un ejemplar de la clase dada a la derecha; y falso si no. Para
un proceso como el que acabamos de describir tendramos un codigo como el que
sigue, suponiendo que tenemos los registros en una lista, como se puede observar
en el listado 6.10.
Supongamos que tenemos una jerarqua de clases para las figuras geometricas,
que se podra ver como en la figura 6.12.
Figura geometrica
Sintaxis:
xencabezado de clase abstractay ::= abstract class xidentificadory
Semantica:
Le estamos indicando al compilador dos cosas:
No pueden crearse objetos de esta clase.
Contiene al menos un metodo cuya implementacion no esta definida.
No hay la obligacion de que todos los metodos en una clase abstracta sean
abstractos. Dependera del diseno y de las posibilidades que tenga la superclase
para definir algunos metodos para aquellas clases que hereden. Aun cuando la
clase no tenga metodos abstractos, si queremos que no se creen objetos de esa
clase la declaramos como abstracta.
6.6 Interfaces
La herencia en Java es simple, esto es, cada clase puede heredar de a lo mas
una superclase. Pero muchas veces necesitamos que hereden de mas de una clase.
Las interfaces corresponden a un tipo, como las clases, que definen exclusivamente
comportamiento. Lo unico que pueden tener las interfaces son constantes estaticas
y metodos abstractos, por lo que definen el contrato que se establece con aquellas
clases que implementen esa interfaz. Se dice que una clase implementa una interfaz,
porque cuando una clase hereda de una interfaz debe dar la implementacion de los
metodos declarados en la interfaz. La sintaxis para la declaracion de una interfaz
es:
Sintaxis:
xencabezado de interfazy ::= interface xidentificadory
...
Semantica:
Se declara un tipo que corresponde a constantes y encabezados de metodos.
Como todo lo que se declare dentro de una interfaz es publico, pues corresponde
siempre a lo que puede hacer un objeto, no se usa el calificativo public en los
enunciados dentro de la declaracion de la interfaz. Como forzosamente todos los
metodos son abstractos, ya que no tienen implementacion, tampoco se pone este
calificativo frente a cada metodo. Y como solo se permiten constantes estaticas, los
calificativos de static y final se omiten en las declaraciones de constantes dentro
de una interfaz. Por lo tanto, las constantes y metodos en una interfaz seran
declarados nada mas con el tipo de la constante o el tipo de valor que regrese el
metodo, los identificadores y, en el caso de las constantes el valor; en el caso de
los metodos, los parametros.
Pensemos en el comportamiento de una lista, como las que hemos estado vien-
197 Herencia
do. Sabemos que no importa de que sea la lista, tiene que tener un metodo que
agregue, uno que busque, etc. Dependiendo de los objetos en la lista y de la imple-
mentacion particular, la implementacion de cada uno de los metodos puede variar.
Tenemos aca un caso perfecto para una interfaz. En el listado 6.13 podemos ver
la declaracion de una interfaz de este tipo.
Dado que las interfaces corresponden a tipos, igual que las clases, podemos
declarar variables de estos tipos. Por ejemplo,
Lista miLista ;
y usarse en cualquier lugar en que se pueda usar un objeto de una clase que imple-
menta a la interfaz Lista, de la misma manera que se puede usar una variable de
la clase Object en cualquier lugar de cualquier objeto. Sin embargo, si deseamos
6.6 Interfaces 198
que el objeto sea visto como la subclase, tendremos que aplicar lo que se cono-
ce como casting, que obliga al objeto a comportarse como de la subclase. Se
logra poniendo el tipo al que deseamos conformar al objeto de tipo Object entre
parentesis, precediendo a la variable:
E s t u d i a n t e C u r s o nuevo = ( E s t u d i a n t e C u r s o ) b u s c a ( 1 , " Pedro " ) ;
El metodo busca nos regresa un objeto de tipo Object (la referencia), pero
sabemos, por la implementacion de busca que en realidad nos va a regresar un
una referencia a un objeto de tipo EstudianteCurso, por lo que podemos aplicar el
casting.
Una clase dada puede extender a una sola superclase, pero puede implementar
a tantas interfaces como queramos. Como no tenemos la implementacion de los
metodos en las interfaces, aun cuando una misma firma aparezca en mas de una
interfaz (o inclusive en la superclase) la implementacion que se va a elegir es la
que aparezca en la clase, por lo que no se presentan conflictos.
Las interfaces tambien pueden extender a una o mas interfaces de la misma
manera que subclases extienden a superclases. La sintaxis es la misma que con la
extension de clases:
Sintaxis:
interface xidentif1 y extends xidentif2 y, . . . , xidentifn y
Semantica:
De la misma manera que con las clases, la subinterfaz hereda todos
los metodos de las superinterfaces.
Clase
atributos
metodo
metodo
bloque
bloque
metodo
bloque
metodo
bloque
bloque
1
Sin embargo, si existe alguna declaracion de una variable con el mismo nombre fuera del
bloque y que la precede, el compilador dara error de sintaxis por identificador ya declarado.
2
No pueden tener el mismo identificador que una declaracion previa y fuera del bloque.
201 Administracion de la memoria durante ejecucion
Para los metodos estaticos, como es el caso del metodo main de las clases esto
funciona un poco distinto, ya que este tipo de metodos no tiene acceso mas que a
los atributos o metodos estaticos de la misma clase, y a los metodos o atributos
publicos o de paquete de las clases a las que se tenga acceso. Olvidandonos un poco
de los metodos estaticos (de clase) podemos decir que la estructura de bloques
nos da lo que conocemos como el rango de una variable, que se refiere a aquellos
puntos de la clase donde la variable puede ser utilizada. Si regresamos a nuestro
esquema de los espejos/cristales, tenemos que desde dentro de un bloque podemos
ver hacia afuera: desde el nivel local tenemos acceso a las variables de la clase - el
rango de las variables de la clase es toda la clase. Desde dentro de un metodo, sin
embargo, no podemos ver lo que esta declarado dentro de otro metodo o bloques.
Pero, que pasa cuando el nombre de una variable de clase se repite dentro de
un bloque como parametro o variable local? En este caso, decimos que la variable
local bloquea a la variable global: si existe una variable local con el mismo nombre,
todas las referencias a esa variable en ese bloque se refieren a la variable local. El
compilador utiliza a la variable que le queda mas cerca, siempre y cuando tenga
acceso a ella, la pueda ver.
El rango de una variable es estatico, pues esta definido por la estructura del
programa: de ver el listado podemos decir cual es el rango de una variable dada, y
definirlo como publico (global, pero atado a un objeto) o local (declarado dentro
de un metodo o como privado para una clase). Es el compilador el que se encarga
de resolver todo lo relacionado con el rango de las variables, siguiendo la estructura
de bloques.
Durante la ejecucion del programa, esta estructura presenta un cierto anida-
miento, distinto del anidamiento sintactico o lexicografico, donde desde dentro
de un metodo se puede llamar a cualquiera de los que esta en rango, o sea, cual-
quiera de los metodos declarados en la clase o que sean publicos de otras clases.
Se lleva a cabo una anidamiento dinamico durante ejecucion, donde se establece
una cadena de llamadas tal que el ultimo metodo que se llamo es el primero que
se va a abandonar.
Supongamos que tenemos la clase del listado 7.1.
En el esquema de la figura 7.2 en la pagina 203 tenemos los bloques en el orden
en que son invocados, mostrando el anidamiento en las llamadas. El nombre del
metodo junto con el valor de sus parametros se encuentra sobre la lnea que lo
demarca. En la esquina superior derecha de cada bloque se encuentra el nivel de
anidamiento dinamico que tiene cada metodo. El valor de las variables, tanto de los
atributos como de las variables locales, dependera del anidamiento lexicografico.
Por ejemplo, dentro del metodo B en el que se le asigna valor a una variable
entera a, esta es una variable local, por lo que el valor del atributo a no se va a
ver modificado. Por ello, en la llamada de la lnea 29: al metodo B, los valores con
7.1 El stack y el heap 202
A(10) [2]
B(10,3) [3]
C( ) [4]
B(3,2) [2]
C( ) [3]
C( ) [2]
$
'
'
'
'
' $
Construccion de objeto
'
'
' '
' ...
'
' '
'
' $
'
'
' '
& '
&. . .
'
'
'
'
'
'
Llamada a A(10) Llamada a B(10,3) Llamada a C()
' '
& '
'
' %. . .
Secuencia de '
'
%. . .
Ejecucion ''
'
' $
'
'
' '
&. . .
'
'
'
'
'
'
Llamada a B(3,2) Llamada a C()
'
'
' %. . .
'
'
' !
'
%Llamada a C() . . .
Lo que me indican los dos diagramas anteriores, es que la ejecucion del pro-
grama debe proseguir de la siguiente manera:
7.1 El stack y el heap 204
Para poder hacer esto, la ejecucion del programa se lleva a cabo en la memoria
de la maquina, organizada esta como un stack, que es una estructura de datos con
las siguientes caractersticas:
a) Respecto a su estructura:
La estructura es lineal, esto es, podemos pensarla con sus elementos for-
mados uno detras del otro.
Es una estructura homogenea, donde todos sus elementos son del mismo
tipo.
Es una estructura dinamica, esto es, crece y se achica durante ejecucion.
Tiene asociado un tope, que corresponde al ultimo elemento que se coloco en
el stack.
b) Respecto a su uso:
Un stack empieza siempre vaco, sin elementos.
Conforme progresa la ejecucion, se van colocando elementos en el stack
y se van quitando elementos del stack, siguiendo siempre esta regla: los
elementos se colocan siempre en el tope del stack y cuando se remueven,
se hace tambien del tope del stack.
Veamos un esquema de un stack en la figura 7.4.
tope
... ...
crece ... ...
hacia
... ...
arriba
... ...
... ...
stack o pila
$ $
'
'
' '
'
' Toma la siguiente instruccion
'
'
' '
'
'
'
'
' '
'
'
Obten los operandos
'
'
' '
'
' Incrementa el contador$del programa
'
'
' '
'
'
'
' '
' '
'
' Suma
'
& Ejecuta instrucciones '
& '
'
Ejecuta '
'
'
programa '
(Hasta que encuentres
' '
'
'
'
'
' '
'
' &Resta
'
'
'
la de parar) '
'
' Ejecuta la instruccion
'
'
' '
'
' '
'
'
' '
' '
'
' .
'
'
' '
'
' '
'
'
' '
' '
'
' .
'
% '
% '
% .
se desmonta del stack al mismo. Para montar un metodo al stack hay que
construir lo que se conoce como su registro de activacion, que es una tabla en
la que hay lugar para los parametros y las variables locales del metodo, de tal
manera que durante la ejecucion se encuentren siempre en la parte superior del
stack.
Al invocar un metodo (para transferirse a ejecutarlo), el sistema debe realizar
los siguientes pasos:
1. Localizar la marca del stack mas cercana al tope, la ultima que se coloco.
2. Colocar en el contador del programa la direccion de regreso que se encuentra
en esa marca.
3. Si el enunciado que causa la terminacion de la rutina es un return, colocar
el valor en el lugar inmediatamente abajo de la marca del stack correspon-
diente.
4. Quitar del stack todo lo que se encuentra a partir de la marca, incluyendola.
Se quita algo del stack simplemente bajando el apuntador al tope del
stack a que apunte al ultimo registro que se quito (recordar que lo ultimo
que se coloco es lo primero que se quita). No hay necesidad de borrar la
informacion pues la ejecucion solo va a tomar en cuenta aquella informacion
que se encuentre antes del tope del stack.
5. Continua la ejecucion en el lugar del codigo al que apunta el contador del
programa.
Para ilustrar estos pasos, vamos a seguir el programa que escribimos, y que
tiene los renglones numerados. Por supuesto que la ejecucion del programa no
se lleva a cabo directamente sobre el texto fuente. El compilador y ligador del
programa producen un programa en binario (lenguaje de maquina) que se coloca
en un cierto segmento de la memoria. El contador del programa va apuntando a
direcciones de este segmento de memoria y en cada momento apunta a una ins-
truccion de maquina. Es suficiente para nuestros propositos manejar el programa
a nivel de enunciado. En los esquemas del stack que presentamos a continuacion,
lo que corresponde a direcciones en memoria de datos (el stack) se preceden con
un * mientras que lo que corresponde a memoria de programa se precede con
un #. El contador del programa apunta a la siguiente instruccion a ejecutarse.
El tope del stack apunta al primer lugar vaco en el stack (si el tope del stack
contiene un 0, quiere decir que no hay nadie en el stack).
Al ejecutar el paso 0, se cargan al stack todos los atributos y nombres de
metodos de la clase3 , quedando el stack como se observa en la figura 7.6.
El sistema operativo sabe que el primer metodo a ejecutarse es main, por lo
que inicia la ejecucion con el. Sigamos los pasos, uno a uno, para ver como lo hace:
d.r. Sistema
Operativo 25:
main
xvoidy main #25: Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
direccion de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
#5:
xinty i *10
direccion de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
#5:
En la lnea #8: tenemos una llamada al metodo B(i,a), por lo que nuevamente
marcamos el stack, copiamos la direccion del PC a la marca, armamos el registro
de activacion para B y lo montamos en el stack, colocamos la direccion donde
empieza B a ejecutarse en el PC y proseguimos la ejecucion en ese punto. En el
momento inmediato anterior a que se ejecute B, el stack se presenta como se puede
observar en la figura 7.12.
Al llegar a la lnea de codigo #16: hay una llamada desde B al metodo C, por
lo que nuevamente se marca el stack, se actualiza el contador del programa y se
monta en el stack el registro de activacion de C(). El resultado de estas acciones
se pueden ver en la figura 7.13 en la pagina 214.
Se ejecuta el metodo C() y al llegar al final del mismo el sistema desmonta
el registro de activacion de C() del stack, coloca en el contador del programa
la direccion que se encuentra en la marca y elimina la marca puesta por esta
invocacion. El stack se ve como en la figura 7.14 en la pagina opuesta.
Se termina la ejecucion de B(10,3) en la lnea #18:, por lo que se desmonta el
registro de activacion de b(10,3) del stack, se copia la direccion de regreso de la
marca al PC y se quita la marca del stack, quedando el stack como se muestra en
la figura 7.15.
213 Administracion de la memoria durante ejecucion
xinty a *13
xinty j *3
xinty i *10
direccion de
regreso: #9:
B(10,3)
xinty i *10
direccion de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xStringr sy args #heap
d.r. Sistema
Operativo #14:
main
xvoidy main #25:
Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
7.1 El stack y el heap 214
xinty k *6
direccion de
regreso: #17:
C()
xinty a *13
xinty j *3
xinty i *10
direccion de
regreso: #9:
B(10,3)
xinty i *10
direccion de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xStringr sy args #heap
d.r. Sistema
Operativo #19:
main
xvoidy main #25:
Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
215 Administracion de la memoria durante ejecucion
xinty a *13
xinty j *3
xinty i *10
direccion de
regreso: #9:
B(10,3)
xinty i *10
direccion de
regreso: #29:
A(10)
xCualquieray objeto # heap
xinty n *5 #17:
xinty m *10
Contador
xString[ ]y args # heap del
direccion de Programa
regreso: #29:
main
xinty i *10
direccion de
regreso: #29:
A(10)
xCualquieray objeto # heap
xinty n *5
xinty m *10
xString[ ]y args # heap #9:
d. r. Sistema Contador
Operativo del
main Programa
7.1 El stack y el heap 216
Se llega al final del metodo A(10), por lo que se desmonta el registro de ac-
tivacion de A(10), se copia la direccion de regreso de la marca al contador del
programa y quita la marca del stack. Podemos observar el estado del stack en este
momento en la figura 7.16.
Al llegar la ejecucion del programa a la lnea 29: se encuentra con otra invoca-
cion a B(3,2), que son los campos de la clase. Se coloca la marca en el stack con
direccion de regreso 30:, se actualiza el PC para que marque el inicio del metodo
B y se monta al stack el registro de activacion de B(3,2). Los resultados de estas
acciones se muestran en la figura 7.17.
217 Administracion de la memoria durante ejecucion
xinty a *5
xinty j *2
xinty i *3
direccion de
regreso: #29:
B(3,2)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xStringr sy args #heap
d.r. Sistema
Operativo #12:
main
xvoidy main #25:
Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
xinty k *6
direccion de
regreso: #17:
C()
xinty a *5
xinty j *2
xinty i *3
direccion de
regreso: #29:
B(3,2)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xStringr sy args #heap
d.r. Sistema
Operativo #22:
main
xvoidy main #25:
Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
xinty a *5
xinty j *2
xinty i *3
direccion de
regreso: #29:
B(3,2)
xCualquieray objeto # heap
xinty n *5
xinty m *10
xString[ ]y args # heap #14:
d. r. Sistema Contador
Operativo del
main Programa
En la lnea 30: nuevamente se hace una llamada al metodo C(), por lo que se
marca el stack y se monta su registro de activacion. El resultado se puede ver en
la figura 7.21 en la pagina opuesta.
xinty k *6
direccion de
regreso: #31:
C()
xCualquieray objeto #heap
xinty n *5
xinty m *10
xStringr sy args #heap
d.r. Sistema
Operativo #14:
main
xvoidy main #25:
Contador del
xvoidy C #19: Programa
xvoidy B #12:
xvoidy A #5:
xinty b *2
xinty a *3
d.r. Sistema
Operativo
clase Cualquiera
Como la lnea 31: es la que termina main, se descarga del stack el registro de
activacion de este metodo, se copia al PC la direccion de regreso de la marca y se
quita la marca. En ese momento termina la ejecucion del programa, por lo que se
libera el stack y el PC.
En todo momento durante la ejecucion, el sistema puede utilizar lo que se
encuentre en el bloque global, mas aquello que se encuentre por encima de la ultima
marca en el stack y hasta inmediatamente antes de la celda antes de la ultima
marca que se puso. De esta manera, Cada metodo crea su propio ambiente de
ejecucion.
Resumiendo, el stack se utiliza para la administracion de la memoria en eje-
cucion. Cada vez que se invoca una rutina o metodo, se construye el registro de
activacion de la misma y se coloca en el stack. Cada vez que se sale de un metodo,
se quita del stack el registro de activacion de la rutina que esta en el tope y la
ejecucion continua en la direccion de regreso desde la que se invoco a esa instancia
7.1 El stack y el heap 222
del metodo.
Durante la ejecucion de un programa, el sistema trabaja con dos variables, el
tope del stack, que indica cual es la siguiente celda en la que se va a colocar in-
formacion, y el contador del programa, que indica cual es la siguiente instruccion
que se va a ejecutar. En ambos casos, decimos que las variables son apuntadores,
pues el tope del stack apunta a una celda en el stack (contiene una direccion del
stack) y el contador del programa apunta a una direccion de memoria del progra-
ma donde se encuentra almacenado el codigo del programa.
En el stack se le da lugar a:
miembros de una clase que han sido ocultados por declaraciones locales utilizando
el identificador de objeto this seguido del operador . y a continuacion el nombre
del atributo. Debo insistir en que el bloque o registro de activacion en el que se
encuentra la variable debe ser visible desde el punto de ejecucion y unicamente
se aplica a variables que hayan sido ocultadas por una reutilizacion del nombre.
Si la variable se encuentra en un registro de activacion inaccesible, entonces el
compilador emitira un mensaje de error.
7.2 Recursividad
long f a c t o r i a l ( i n t n ) {
i f ( n <= 0 ) {
r e t u r n 1;
}
i f ( n > 1) {
r e t u r n ( f a c t o r i a l ( n 1) n ) ;
}
else {
return 1;
}
}
xFactorialy f # heap
xConsolay cons # heap
d.r. Sistema
Operativo
main
xvoidy main #13: #16:
Numeramos las lneas del programa para poder hacer referencia a ellas en
225 Administracion de la memoria durante ejecucion
la ejecucion, que empieza en la lnea 13:. En las lneas 13: y 15: tenemos las
declaraciones e inicializaciones de variables locales a main, y en las lneas 16: y 17:
esta el unico enunciado realmente de ejecucion de main. La primera llamada de
factorial desde main deja el stack como se ve en la figura 7.24
Figura 7.24 Estado del stack al iniciarse la llamada de factorial desde main.
xinty n *4
direccion de
regreso #17:
factorial(4)
xlongy valor de regreso
xFactorialy f # heap
xConsolay cons # heap
d.r. Sistema
Operativo
main
xvoidy main #13: #3:
Figura 7.25 Estado del stack al iniciarse la llamada de factorial desde factorial.
xinty n *3
direccion de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
direccion de
regreso #17:
factorial(4)
xlongy valor de regreso
xFactorialy f # heap
xConsolay cons # heap
d.r. Sistema
Operativo
main
xvoidy main #13: #3:
Figura 7.26 Estado del stack al iniciarse la llamada de factorial desde factorial.
xinty n *2
direccion de
regreso #7:
factorial(2)
xlongy valor de regreso
xinty n *3
direccion de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
direccion de
regreso #17:
factorial(4)
xlongy valor de regreso
xFactorialy f # heap
xConsolay cons # heap
d.r. Sistema
Operativo
main
xvoidy main #13: #3:
Figura 7.27 Estado del stack al iniciarse la llamada de factorial desde factorial.
xinty n *1
direccion de
regreso #7:
factorial(1)
xlongy valor de regreso
xinty n *2
direccion de
regreso #7:
factorial(2)
xlongy valor de regreso
xinty n *3
direccion de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
direccion de
regreso #17:
factorial(4)
xlongy valor de regreso
xFactorialy f # heap
xConsolay cons # heap
d.r. Sistema
Operativo
main
xvoidy main #13: #3:
Figura 7.31 Estado del stack al terminarse la llamada de factorial desde main.
Lo que me dice esta estrategia es que si solo tenemos dos fichas las sabemos
mover a pie. Para el caso de que tenga mas de dos fichas (n 2), suponemos
que pudimos mover las n 1 fichas que estan en el tope del poste al poste auxiliar,
siguiendo las reglas del juego, despues movimos una sola ficha al poste definitivo, y
para terminar movimos las n 1 fichas del poste auxiliar al definitivo. Como en el
caso del calculo de factorial con recursividad, se entra al metodo decrementando la
7.2 Recursividad 234
n en 1 hasta que tengamos que mover una sola ficha; en cuanto la movemos, pasa-
mos a trabajar con el resto de las fichas. Hay que aclarar que esto funciona porque
se van intercambiando los postes 1, 2 y 3. El codigo (de manera esquematica) se
puede ver en el listado 7.4.
/
Mueve n f i c h a s d e l poste1 a l poste2 , usando e l
poste3 como p o s t e de t r a b a j o .
@param n e l numero de f i c h a s a mover .
@param p o s t e 1 e l p o s t e d e s d e e l c u a l s e mueven .
@param p o s t e 2 e l p o s t e d e s t i n o .
@param p o s t e 3 e l p o s t e de t r a b a j o .
/
p u b l i c v o i d mueveN ( i n t n , i n t p o s t e 1 , i n t p o s t e 2 , i n t p o s t e 3 ) {
i f ( n == 2 ) {
mueveUno ( p o s t e 1 , p o s t e 3 ) ;
mueveUno ( p o s t e 1 , p o s t e 2 ) ;
mueveUno ( p o s t e 3 , p o s t e 2 ) ;
}
else {
mueveN ( n 1, p o s t e 1 , p o s t e 3 , p o s t e 2 ) ;
mueveUno ( p o s t e 1 , p o s t e 2 ) ;
mueveN ( n 1, p o s t e 3 , p o s t e 2 , p o s t e 1 ) ;
}
}
mueveN(4,1,2,3)
4 2?
mueveN(3,1,3,2)
3 2?
mueveN(2,1,2,3)
2 2?
mueveUno(1,3) /* 1 */
mueveUno(1,2) /* 2 */
mueveUno(3,2) /* 3 */
mueveUno(1,3) /* 4 */
mueveN(2,2,3,1)
2 2?
mueveUno(2,1) /* 5 */
mueveUno(2,3) /* 6 */
mueveUno(1,3) /* 7 */
mueveUno(1,2) /* 8 */
mueveN(3,3,2,1)
3 2?
mueveN(2,3,1,2)
2 2?
mueveUno(3,2) /* 9 */
mueveUno(3,1) /* 10 */
mueveUno(2,1) /* 11 */
mueveUno(3,2) /* 12 */
mueveN(2,1,2,3)
2 2?
mueveUno(1,3) /* 13 */
mueveUno(1,2) /* 14 */
mueveUno(3,2) /* 15 */
7.2 Recursividad 236
Comprobemos que este algoritmo trabaja viendo una visualizacion con cuatro
fichas. En cada figura mostraremos los movimientos que se hicieron mediante
flechas desde el poste en el que estaba la ficha al poste en el que se coloco. Las
reglas exigen que cada vez que se mueve una ficha, esta sea la que se encuentra
hasta arriba.
/* 2 */
/* 3 */
/* 6 */
/* 5 */
/* 11 */ /* 9 */
7.2 Recursividad 238
/* 12 */
/* 14 */ /* 15 */
Como se puede ver del ejercicio con las torres de Hanoi, 4 fichas provocan
15 movimientos. Podramos comprobar que 5 fichas generan 31 movimientos. Es-
to se debe a la recursividad, que se encuentra escondida en la simplicidad del
algoritmo. Aunque definitivamente es mas facil expresarlo as, que ocupa aproxi-
madamente 10 lneas, que dar las reglas con las que se mueven las fichas de dos
en dos.
Entre otros ejemplos que ya no veremos por el momento, donde la solucion
recursiva es elegante y mucho mas clara que la iterativa se encuentra el recorrido
de arboles, las busquedas binarias y algunos ordenamientos como el de mezcla y
el de Quick.
Con esto damos por terminado una descripcion somera sobre como se com-
porta la memoria durante la ejecucion de un programa, en particular el stack de
ejecucion. Esta descripcion no pretende ser exhaustiva, sino unicamente propor-
cionar una idea de como identifica la ejecucion los puntos de entrada, de regreso
y parametros a una funcion.
Ordenamientos
usando estructuras 8
de datos
Habiendo ya visto arreglos, se nos ocurre que puede resultar mas facil guardar
nuestras listas de cursos en un arreglo, en lugar de tenerlo en una lista ligada. Todo
lo que tenemos que hacer es pensar en cual es el tamano maximo de un grupo y
reservar ese numero de localidades en un arreglo. La superclase para el registro
con la informacion del estudiante queda casi exactamente igual al que utilizamos
como EstudianteBasico, excepto que como ahora la relacion de quien sigue a quien
va a estar dada por la posicion en el arreglo, no necesitamos ya la referencia al
siguiente estudiante. Todos los metodos quedan exactamente igual, excepto que
todo lo relacionado con el campo siguiente ya no aparece ver listado 8.1 en la
siguiente pagina.
8.1 Base de datos en un arreglo 240
Como se puede observar en el listado 8.1 en la pagina 240, todo quedo practi-
camente igual, excepto que quitamos todo lo relacionado con la referencia al si-
guiente. En su lugar agregamos un campo para que el registro se identifique
a s mismo en cuanto a la posicion que ocupa en el arreglo, pos y los metodos
correspondientes. Este campo se vera actualizado cuando se entregue la referencia
del registro sin especificar su posicion.
La clase as definida puede ser usada, por ejemplo, para cuando queramos
una lista de estudiantes que tengan esta informacion basica incluida. Un posible
ejemplo se muestra en el listado 8.2. Esta jerarqua se puede extender tanto como
queramos. Si pensamos en estudiantes para listas usamos InfoEstudiante para he-
redar, agregando simplemente campos necesarios para mantener la lista, como en
el caso EstudianteLista del Listado 8.2.
que se quitan, para que en todo momento se tenga claro el numero de elementos
que tenemos en un arreglo. En el listado 8.4 podemos ver lo relacionado con el
cambio de estructura de datos de una lista a un arreglo.
Hay algunas operaciones basicas que vamos a necesitar al trabajar con arreglos.
Por ejemplo, para agregar a un elemento en medio de los elementos del arreglo
(o al principio) necesitamos recorrer a la derecha a todos los elementos que se
encuentren a partir de la posicion que queremos que ocupe el nuevo elemento.
Esto lo tendremos que hacer si queremos agregar a los elementos y mantenerlos
en orden conforme los vamos agregando.
Similarmente, si queremos eliminar a alguno de los registros del arreglo, tene-
mos que recorrer a los que esten mas alla del espacio que se desocupa para que se
ocupe el lugar desocupado. En ambos casos tenemos que recorrer a los elementos
uno por uno y deberemos tener mucho cuidado en el orden en que recorramos a
los elementos. Al recorrer a la derecha deberemos recorrer desde el final del arre-
glo hacia la primera posicion que se desea mover. Si no se hace en este orden se
tendra como resultado el valor del primer registro que se desea mover copiado a
todos los registros a su derecha. El metodo para recorrer hacia la derecha se en-
cuentra en el listado 8.5 en la siguiente pagina. El metodo nos tiene que regresar
si pudo o no pudo recorrer a los elementos. En el caso de que no haya suficiente
lugar a la derecha, nos respondera falso, y nos respondera verdadero si es que
pudo recorrer.
Si deseamos regresar un lugar a la izquierda, el procedimiento es similar, ex-
cepto que tenemos que mover desde el primero hacia el ultimo, para no acabar
con una repeticion de lo mismo.
8.1 Base de datos en un arreglo 250
tando los que se quitan, con los registros activos ocupando posiciones consecutivas
Por ultimo, si se desea mantener ordenados los registros con un orden lexi-
cografico, primero tenemos que localizar el lugar que le toca. Una vez hecho esto
se recorren todos los registros a partir de ah un lugar a la derecha, y se coloca en
el lugar desocupado al nuevo registro.
En los tres casos, antes de agregar algun registro deberemos verificar que to-
dava hay lugar en el arreglo, ya que el arreglo tiene una capacidad fija dada en
el momento en que se crea. Como no siempre vamos a poder agregar registros
(algo que no suceda cuando tenamos una lista), todos estos metodos tienen que
decirnos de regreso si pudieron o no.
Para localizar el lugar que le toca a un registro nuevo lo vamos comparando
con los registros en el arreglo, hasta que encontremos el primero mayor que el.
Como el orden esta dado por el nombre, que es una cadena, tenemos que comparar
cadenas. El metodo compareTo de la clase String nos sirve para saber la relacion
entre dos cadenas, de la siguiente forma:
$
&1 Si s1 s2
'
s1.compareTo(String s2) 0 Si s1 == s2
'
% 1 Si s1 s2
en las lneas 149: 152: del listado 8.8 en la pagina 252. Nos colocamos al principio
del vector, poniendo el ndice que vamos a usar para recorrerlo en 0 lnea 149:. A
continuacion, mientras no se nos acaben los registros del arreglo y estemos viendo
registros lexicograficamente menores al que buscamos condicionales en lneas
150: y 151: incrementamos el ndice, esto es, pasamos al siguiente.
Podemos salir de la iteracion porque se deje de cumplir cualquiera de las dos
condiciones: que ya no haya elementos en el arreglo o que ya estemos entre uno
menor o igual y uno mayor. En el primer caso habremos salido porque el ndice
llego al numero de registros almacenados actual == numRegs en cuyo caso
simplemente colocamos al nuevo registro en el primer lugar sin ocupar del arreglo.
No hay peligro en esto pues al entrar al metodo verificamos que todava hubiera
lugares disponibles.
En el caso de que haya encontrado un lugar entre dos elementos del arreglo,
tenemos que recorrer a todos los que son mayores que el para hacer lugar. Esto
se hace en la lnea 158:, donde de paso preguntamos si lo pudimos hacer segu-
ramente s porque ya habamos verificado que hubiera lugar. Una vez recorridos
los elementos del arreglo, colocamos el nuevo elemento en el lugar que se desocu-
po gracias al corrimiento, y avisamos que todo estuvo bien lneas 161: y 162:
no sin antes incrementar el contador de registros.
lo siguiente: localiza al registro que contenga al nombre completo, que llega como
parametro lneas 174: a 178: del listado 8.9 en la pagina opuesta.
Una vez que encontro el registro en el que esta ese nombre, se procede a
desaparecerlo, recorriendo a los registros que estan despues que el un lugar a
la izquierda, encimandose en el registro que se esta quitando; esto se hace con la
llamada a regresa(actual,1) en la lnea 181: del listado 8.9 en la pagina opuesta. Al
terminar de recorrer a los registros hacia la izquierda, se decrementa el contador
de registros numRegs. La ubicacion de este decremento en el metodo regresa lnea
85: del listado 8.5 en la pagina 250 se justifica bajo la optica de que numRegs
indica la posicion del primer registro vaco; otra opcion hubiera sido poner el
decremento en quitaEst, que estara justificado bajo la optica de que numRegs
cuenta el numero de estudiantes en la base de datos. Como numRegs juega ambos
papeles la respuesta a donde poner el decremento no es unica. El diagrama de
Warnier correspondiente se encuentra en la Figura 8.1.
Codigo 8.10 Busqueda de una subcadena en algun campo del arreglo (CursoEnVector)
184: / Busca a l r e g i s t r o que c o n t e n g a a l a s u b c a d e n a .
185: @param i n t c u a l C u a l e s e l campo que s e va a c o m pa r ar .
186: @param S t r i n g s u b c a d La c a d e n a que s e e s t a b u s c a n d o .
187: @ r e t u r n s i n t E l r e g i s t r o d e s e a d o o 1. /
188: public I n f o E s t u d i a n t e buscaSubcad ( i n t cual , S t r i n g subcad ) {
189: int actual ;
190: subcad = subcad . trim ( ) . toLowerCase ( ) ;
191: actual = 0;
192: w h i l e ( a c t u a l < numRegs && ( l i s t a [ a c t u a l ] . daCampo ( c u a l ) .
193: i n d e x O f ( s u b c a d . t o L o w e r C a s e ( ) ) ) == 1)
194: a c t u a l ++;
195: i f ( a c t u a l < numRegs )
196: return l i s t a [ a c t u a l ] ;
197: else
198: return n u l l ;
199: }
las listas, y esto es algo deseable. De esa manera podemos decir que la clase Me-
nuVector no tiene que saber como estan implementadas las estructuras de datos
o los metodos de la clase VectorLista, sino simplemente saber usarlos y saber que
le tiene que pasar como parametro y que espera como resultado. Excepto por
los metodos que agregan y quitan estudiantes, que los volvimos booleanos para
que informen si pudieron o no, todos los demas metodos mantienen la firma que
tenan en la implementacion con listas ligadas. Vale la pena decir que podramos
modificar los metodos de las listas ligadas a que tambien contestaran si pudieron
o no, excepto que en el caso de las listas ligadas siempre podran.
Nos falta revisar nada mas dos metodos: el que lista todo el contenido de la base
de datos y el que lista solamente los que cazan con cierto criterio. Para el primer
metodo nuevamente se aplica la transformacion de que colocarse al principio de la
lista implica poner al ndice que se va a usar para recorrerla en 0 lnea 207: en el
listado 8.11. Nuevamente nos movemos por los registros incrementando el ndice
en 1, y verificamos al salir de la iteracion si encontramos lo que buscabamos o no.
En el caso del metodo que lista a los que cazan con cierto criterio listado 8.12
en la siguiente pagina nuevamente se recorre el arreglo de la manera que ya vimos,
excepto que cada uno que contiene a la subcadena es listado. Para saber si se listo o
no a alguno, se cuentan los que se van listando lnea 223: en el listado 8.12 en la
siguiente pagina. Si no se encontro ningun registro que satisficiera las condiciones
dadas, se da un mensaje de error manifestandolo.
8.2 Manteniendo el orden con listas ligadas 258
Codigo 8.12 Listando los que cumplan con algun criterio (CursoEnVector)
214: /
215: Imprim e l o s r e g i s t r o s que c a z a n con un c i e r t o p a t r o n .
216:
217: @param C o n s o l a c o n s D i s p o s i t i v o en e l que s e va a e s c r i b i r .
218: @param i n t c u a l Con c u a l campo s e d e s e a c o m pa r ar .
219: @param S t r i n g s u b c a d Con e l que queremos que c a c e .
220: /
221: p u b l i c v o i d losQueCazanCon ( C o n s o l a cons , i n t c u a l ,
222: S t r i n g subcad ) {
223: int i = 0;
224: subcad = subcad . toLowerCase ( ) ;
225: int actual ;
226:
227: / R e c o r r e m o s b u s c a n d o e l r e g s i t r o /
228: f o r ( a c t u a l = 0 ; a c t u a l < numRegs ; a c t u a l ++) {
229: i f ( l i s t a [ a c t u a l ] . daCampo ( c u a l ) . i n d e x O f ( s u b c a d ) !=
230: 1) {
231: i ++;
232: cons . imprimeln ( l i s t a [ a c t u a l ] . d a R e g i s t r o ( ) ) ;
233: }
234: }
235:
236: / S i no s e e n c o n t r o n i n g u n r e g i s t r o /
237: i f ( i == 0 ) {
238: c o n s . i m p r i m e l n ( "No se encontro ningun registro " +
239: "que cazara " ) ;
240: }
241: }
Tenemos ya una clase que maneja a la base de datos en una lista ligada
(ListaCurso). Podemos modificar levemente ese programa para beneficiarnos de
la herencia y hacer que Estudiante herede de la clase InfoEstudiante, y de esa ma-
nera reutilizar directamente el codigo que ya tenemos para InfoEstudiante. Todo
lo que tenemos que hacer es agregarle los campos que InfoEstudiante no tiene y
los metodos de acceso y manipulacion para esos campos. La programacion de la
259 Ordenamientos usando estructuras de datos
Codigo 8.13 Definicion de la clase Estudiante para los registros (Estudiante) 1/3
1: import i c c 1 . i n t e r f a z . C o n s o l a ;
2: /
3: Base de d a t o s , a b a s e de l i s t a s de r e g i s t r o s , que emula l a l i s t a
4: de un c u r s o de l i c e n c i a t u r a . T i e n e l a s o p c i o n e s n o r m a l e s de una
5: b a s e de d a t o s y f u n c i o n a m e d i a n t e un Menu
6: /
7: c l a s s E s t u d i a n t e extends I n f o E s t u d i a n t e {
8: protected E s t u d i a n t e s i g u i e n t e ;
9: protected S t r i n g c l a v e ;
10: p u b l i c s t a t i c f i n a l i n t CLAVE = 4 ;
11: / C o n s t r u c t o r s i n p a r a m e t r o s . /
12: public Estudiante () {
13: super ( ) ;
14: clave = null ;
15: siguiente = null ;
16: }
17: /
18: C o n s t r u c t o r a p a r t i r de d a t o s de un e s t u d i a n t e .
19: Los campos v i e n e n s e p a r a d o s e n t r e s p o r comas , m i e n t r a s
20: que l o s r e g i s t r o s v i e n e n s e p a r a d o s e n t r e s p o r punto
21: y coma .
22: @param S t r i n g , S t r i n g , S t r i n g , S t r i n g l o s v a l o r e s p a r a
23: cada uno de l o s campos que s e van a l l e n a r .
24: @ r e t u r n E s t u d i a n t e una r e f e r e n c i a a una l i s t a
25: /
26: p u b l i c E s t u d i a n t e ( S t r i n g nmbre , S t r i n g c nt a , S t r i n g c l v e ,
27: String crrera ) {
28: super ( nmbre , c nt a , c r r e r a ) ;
29: clave = clve . trim ( ) ;
30: siguiente = null ;
31: }
32: /
33: R e g r e s a e l c o n t e n i d o d e l campo c l a v e .
34: /
35: public String getClave () {
36: return c l a v e ;
37: }
38: /
39: A c t u a l i z a e l campo c l a v e con e l v a l o r que p a s a como
40: p a r a m e t r o .
41: /
42: public void s e t C l a v e ( S t r i n g c l v e ) {
43: clave = clve ;
44: }
8.2 Manteniendo el orden con listas ligadas 260
Hay que notar que lo que programamos como de acceso privado cuando no
tomabamos en consideracion la herencia, ahora se convierte en acceso protegido,
para poder extender estas clases.
Algunos de los metodos que enunciamos en esta clase son, simplemente, meto-
dos nuevos para los campos nuevos. Tal es el caso de los que tienen que ver con
clave y siguiente. El metodo getCampo se redefine en esta clase, ya que ahora tiene
que considerar mas posibilidades. Tambien el metodo getRegistro es una redefini-
cion, aunque usa a la definicion de la superclase para que haga lo que corresponda
a la superclase.
Los constructores tambien son interesantes. Cada uno de los constructores,
tanto el que tiene parametros como el que no, llaman al correspondiente cons-
tructor de la superclase, para que inicialice los campos que tiene en comun con la
superclase.
La palabra super se esta utilizando de dos maneras distintas. Una de ellas,
en el constructor, estamos llamando al constructor de la superclase usando una
notacion con argumentos. En cambio, en el metodo getRegistro se usa igual que
cualquier otro objeto, con la notacion punto. En este segundo caso nos referimos
al super-objeto de this, a aquel definido por la superclase.
lista
nuevo Alberto
nuevo Ricardo
Debemos insistir en que se debe tener cuidado el orden en que se cambian las
referencias. Las referencias que vamos a modificar son la de nuevo y la siguiente
en anterior, que es la misma que tenemos almacenada en actual. Por ello, el orden
para cambiar las referencias podra haber sido
138: a n t e r i o r . p o n S i g u i e n t e ( nuevo ) ;
139: nuevo . p o n S i g u i e n t e ( a c t u a l ) ;
265 Ordenamientos usando estructuras de datos
Lo que debe quedar claro es que una vez modificado anterior.siguiente, esta refe-
rencia ya no se puede usar para colocarla en nuevo.siguiente.
$
'
&Un nodoque no tiene hijos
Un arbol n-ario es
'
% Un nodo que tiene como hijos a n arboles
raz
Info
......
Arbol Arbol
Arbol
En estos momentos revisaremos unicamente a los arboles binarios, por ser estos
un mecanismo ideal para organizar a un conjunto de datos de manera ordenada.
Un arbol binario, entonces, es un arbol donde el maximo numero de hijos para
cada nodo es dos. Si lo utilizamos para organizar cadenas, podemos pensar que
dado un arbol, cada nodo contiene una cierta cadena. Todas las cadenas que se
encuentran en el subarbol izquierdo son menores a la cadena que se encuentra en
la raz. Todas las cadenas que se encuentran en el subarbol derecho, son mayores
267 Ordenamientos usando estructuras de datos
A P
E M Y
C F N R
B D G T
Cuando todos los nodos, excepto por las hojas del ultimo nivel, tienen exac-
tamente el mismo numero de hijos, decimos que el arbol esta completo. Si la pro-
fundidad del subarbol izquierdo es la misma que la del subarbol derecho, decimos
que el arbol esta equilibrado 2 . El arbol del esquema anterior no esta ni completo
ni equilibrado.
Para el caso que nos ocupa, la clase correspondientes a cada Estudiante vuelve
a extender a la clase InfoEstudiante, agregando las referencias para el subarbol
izquierdo y derecho, y los metodos de acceso y manipulacion de estos campos. La
programacion se puede ver en el listado 8.15 en la siguiente pagina.
Como se ve de la declaracion del registro ArbolEstudiante, tenemos una es-
tructura recursiva, donde un registro de estudiante es la informacion, con dos
referencias a registros de estudiantes.
2
En ingles, balanced
8.3 *Ordenamiento usando arboles 268
8.3.3. Insercion
raz
Manuel
Anita Octavio
Como podemos ver en este esquema, para listar el contenido de los nodos en
orden tenemos que recorrer el arbol de la siguiente manera:
los arboles son estructuras recursivas, el hijo izquierdo es, a su vez, un arbol (lo
mismo el hijo derecho). Por ello, si pensamos en el caso mas general, en que el
hijo izquierdo pueda ser un arbol tan complicado como sea y el hijo derecho lo
mismo, nuestro algoritmo para recorrer un arbol binario arbitrario quedara como
se muestra en el esquema de la figura 8.10.
En nuestro caso, el proceso del nodo raz de ese subarbol en particular consiste
en escribirlo. El metodo publico que muestra toda la lista queda con la firma como
la tena en las otras dos versiones que hicimos, y programamos un metodo privado
que se encargue ya propiamente del recorrido recursivo, cuya firma sera
p r i v a t e v o i d l i s t a A r b o l ( C o n s o l a cons , A r b o l E s t u d i a n t e r a i z )
Quisieramos guardar el contenido del arbol en disco, para la proxima vez em-
pezar a partir de lo que ya tenemos. Si lo guardamos en orden, cuando lo volvamos
a cargar va a producir un arbol degenerado, pues ya vimos que lo peor que nos
puede pasar cuando estamos cargando un arbol binario es que los datos vengan en
orden (ya sea ascendente o descendente). Por ello, tal vez sera mas conveniente
recorrer el arbol de alguna otra manera. Se nos ocurre que si lo recorremos en
preorden, vamos a producir una distribucion adecuada para cuando volvamos a
cargar el directorio. Esta distribucion va a ser equivalente a la original, por lo que
estamos introduciendo un cierto factor aleatorio. El algoritmo es igual de sencillo
que el que recorre en orden simetrico y se muestra en la figura 8.11 en la siguiente
pagina.
8.3 *Ordenamiento usando arboles 276
8.3.6. Busquedas
Codigo 8.21 Listado de registros que contienen a una subcadena (ArbolOrden) 1/2
227: / Imprim e l o s r e g i s t r o s que c a z a n con un c i e r t o p a t r o n .
228: @param C o n s o l a c o n s D i s p o s i t i v o en e l que s e va a e s c r i b i r .
229: @param i n t c u a l Con c u a l campo s e d e s e a c o m pa r ar .
230: @param S t r i n g s u b c a d Con e l que queremos que c a c e .
231: /
232: p u b l i c v o i d losQueCazanCon ( C o n s o l a cons , i n t c u a l ,
233: S t r i n g subcad ) {
234: subcad = subcad . trim ( ) . toLowerCase ( ) ;
235: int cuantos = 0;
236: i f ( r a i z == n u l l )
237: c o n s . i m p r i m e l n ( "No hay registros en la base de"
238: + " datos " ) ;
239: else
240: c u a n t o s = b u s c a C a z a n ( cons , c u a l , subcad , r a i z ) ;
241: i f ( c u a n t o s == 0 )
242: c o n s . i m p r i m e l n ( "No hay registros que cacen ." ) ;
243: }
281 Ordenamientos usando estructuras de datos
Como dijimos, una vez que se tiene al padre de una hoja, todo lo que hay que
hacer es identificar si el nodo es hijo izquierdo o derecho y poner el apuntador
correspondiente en null.
Resuelta la eliminacion de una hoja, pasemos a ver la parte mas complicada,
que es la eliminacion de un nodo intermedio. Veamos, por ejemplo, el arbol de la
figura 8.6 en la pagina 267 y supongamos que deseamos eliminar el nodo etiquetado
con E. Como reacomodamos el arbol de tal manera que se conserve el orden
correcto? La respuesta es que tenemos que intercambiar a ese nodo por el nodo
menor de su subarbol derecho. Por que? Si colocamos al nodo menor del subarbol
derecho en lugar del que deseamos eliminar, se sigue cumpliendo que todos los que
esten en el subarbol izquierdo son menores que el, mientras que todos los que esten
en el subarbol derecho son mayores o iguales que el:
Para localizar el elemento menor del subarbol derecho simplemente bajamos a
la raz del subarbol derecho y de ah en adelante seguimos bajando por las ramas
izquierdas hasta que ya no haya ramas izquierdas.El algoritmo para encontrar el
elemento menor de un subarbol se encuentra en la figura 8.15 en la pagina opuesta.
La programacion de este metodo se muestra en el listado 8.23.
283 Ordenamientos usando estructuras de datos
$ $ !
'
'
' '
' El nodo es hoja
'
' '
'
'
Elimina a la raz
'
'
' '
'
'
' '
'
' #
'
'
' '
'
'
' '
'
'
El nodo tiene solo Pon al subarbol derecho
'
'
' '
'
'
' '
'
'
subarbol derecho
en la raz
'
'
' '
&
'
' #
'
'
'
nodo == raz El nodo tiene solo
'
' '
'
'
Pon al subarbol izquierdo
'
'
' '
'
'
' '
'
'
subarbol izquierdo
en la raz
'
'
' '
'
'
' '
'
' $
'
'
' '
' '
&Localiza menor en subarbol derecho
'
' '
'
' El nodo tiene
'
'
' '
' Intercambia a menor y nodo
'
' '
% ambos hijos '
%
'
& Elimina a quien quedo en menor
Elimina
nodo ' ' $ !
'
'
' '
' El nodo es hoja
'
' '
'
'
Anula el apuntador del padre
'
'
' '
'
'
' '
'
' #
'
'
' '
'
'
' '
'
'
El nodo tiene solo Sube al subarbol derecho
'
'
' '
'
'
' '
'
'
subarbol derecho
al lugar del nodo
'
'
' '
&
'
'
' #
'
'
'
nodo == raz El nodo tiene solo
' Sube al subarbol izquierdo
'
' '
'
'
'
'
' '
' subarbol izquierdo al lugar del nodo
'
' '
'
'
'
'
' '
'
'
' '
' $
'
' '
'
' '
&Localiza menor en subarbol derecho
'
'
' '
' El nodo tiene
'
' '
'
' Intercambia a menor y nodo
'
% % ambos hijos '
% Elimina a quien quedo en menor
285 Ordenamientos usando estructuras de datos
Por ultimo, el mecanismo para modificar algun registro no puede ser, sim-
plemente, modificar la informacion, pues pudiera ser que la llave cambiara y el
registro quedara fuera de orden. La estrategia que vamos a utilizar para modificar
la informacion de un registro es primero borrarlo y luego reinsertarlo, para evitar
que se modifique la llave y se desacomode el arbol.
Todos hemos padecido en algun momento errores de ejecucion. Java tiene me-
canismos muy poderosos para detectar errores de ejecucion de todo tipo, a los que
llama excepciones. Por ejemplo, si se trata de usar una referencia nula, Java ter-
minara (abortara) el programa con un mensaje de error. Generalmente el mensaje
tiene la forma:
En el primer renglon del mensaje Java nos dice el tipo de excepcion que causo que
el programa abortara, que en este caso es NullPointerException, y a partir del
segundo renglon aparece la historia de la ejecucion del programa, esto es, los
registros de activacion montados en el stack de ejecucion en el momento en que
sucede el error. Hay muchos errores a los que Java va a reaccionar de esta manera,
como por ejemplo usar como ndice a un arreglo un entero menor que cero o ma-
yor o igual al tamano declarado (ArrayIndexOutOfBoundsException) o una division
entre 0 (ArithmeticException).
9.1 Tipos de errores 290
La clase Exception es una clase muy sencilla que tiene, realmente, muy po-
cos metodos. De hecho, unicamente tiene dos constructores que se pueden usar
en aquellas clases que hereden a Exception. La clase Throwable, superclase de Ex-
ception, es la que cuenta con algunos metodos mas que se pueden invocar desde
cualquier excepcion, ya sea esta de Java o del programador. Veamos primero los
dos constructores de Exception.
public Exception() Es el constructor por omision. La unica informacion que pro-
porciona es el nombre de la excepcion.
public Exception(String msg) Construye el objeto pasandole una cadena, que pro-
porciona informacion adicional a la del nombre de la clase.
3
en adelante JVM.
295 Manejo de errores en ejecucion
Constructores:
public Throwable() Es el constructor por omision.
public Throwable(String msg) Da la oportunidad de construir el objeto con infor-
macion que se transmite en la cadena.
Metodo descriptivo:
Regresa en una cadena el nombre de la clase a la que per-
public String toString()
tenece y la cadena con la que fue creada, en su caso.
Sintaxis:
try {
xenunciados donde puede ser lanzada la excepciony
} catch ( xtipo de excepciony xidentify ) {
xEnunciados que reaccionan frente a la excepciony
}
Semantica:
Se pueden agrupar tantos enunciados como se desee en la clausula try, por
lo que al final de ella se puede estar reaccionando a distintas excepciones.
Se elige uno y solo un manejador de excepciones, utilizando la primera
excepcion que califique con ser del tipo especificado en las clausulas catch.
de cada clase de excepcion que se pudiera presentar en el cuerpo del try. El tipo
de excepcion puede ser Exception, en cuyo caso cualquier tipo de excepcion va
a ser cachada y manejada dentro de ese bloque. En general, una excepcion que
extiende a otra puede ser cachada por cualquier manejador para cualquiera de sus
superclases. Una vez que se lista el manejador para alguna superclase, no tiene
sentido listar manejadores para las subclases.
La manera como se ejecuta un bloque try en el que se pudiera lanzar una
excepcion es la siguiente:
La JVM entra a ejecutar el bloque correspondiente.
Como cualquier bloque en Java, todas las declaraciones que se hagan dentro
del bloque son visibles unicamente dentro del bloque.
Se ejecuta el bloque enunciado por enunciado.
En el momento en que se presenta una excepcion, la ejecucion del bloque se
interrumpe y la ejecucion prosigue buscando a un manejador de la excepcion.
La ejecucion verifica los manejadores, uno por uno y en orden, hasta que
encuentre el primero que pueda manejar la excepcion. Si ninguno de los
manejadores puede manejar la excepcion que se presento, sale de la manera
que indicamos hasta que encuentra un manejador, o bien la JVM aborta el
programa.
Si encuentra un manejador adecuado, se ejecuta el bloque correspondiente
al manejador.
Si no se vuelve a lanzar otra excepcion, la ejecucion continua en el enunciado
que sigue al ultimo manejador.
El programa que se encuentra en el listado 9.4 cacha las excepciones posibles
en el bloque del try mediante una clausula catch para la superclase Exception. Una
vez dentro del manejador, averigua cual fue realmente la excepcion que se disparo,
la reporta y sigue adelante con la ejecucion del programa.
javac DivPorCeroUso.java
DivPorCeroUso.java:7: unreported exception DivPorCeroException;
must be caught or declared to be thrown
throw new DivPorCeroException("Se pide raz de numero
negativo!");
^
1 error
Una vez corregido esto, la llamada al metodo sqrt tiene que aparecer dentro
de un bloque try que se encargue de detectar la excepcion que lanza el metodo.
Estos cambios se pueden ver en los listados 9.8 en la siguiente pagina y 9.9 en la
siguiente pagina.
9.3 Como detectar y cachar una excepcion 302
en este caso es main, la excepcion no se propaga hacia afuera de main, por lo que
el metodo no tiene que avisar que pudiera lanzar una excepcion.
Como ya mencionamos antes, las excepciones se pueden lanzar en cualquier
momento: sin simplemente un enunciado mas. Por supuesto que un uso racional
de ellas nos indica que las deberemos asociar a situaciones no comunes o crticas,
pero esto ultimo tiene que ver con la semantica de las excepciones, no con la
sintaxis.
Tal vez el ejemplo del listado 9.9 en la pagina opuesta no muestre lo util que
pueden ser las excepciones, porque redefinen de alguna manera una excepcion
que la JVM lanzara de todos modos. Pero supongamos que estamos tratando de
armar una agenda telefonica, donde cada individuo puede aparecer unicamente
una vez, aunque tenga mas de un telefono. Nuestros metodos de entrada, al tratar
de meter un nombre, detecta que ese nombre con la direccion ya esta registrado.
En terminos generales, esto no constituye un error para la JVM, pero si para el
contexto de nuestra aplicacion. Una manera elegante de manejarlo es a traves de
excepciones, como se muestra en los listados 9.10 a 9.12 en la siguiente pagina.
El listado 9.12 en la pagina opuesta hace uso del hecho de que cuando en un
bloque se presenta una excepcion, la ejecucion salta a buscar el manejador de la
excepcion y deja de ejecutar todo lo que este entre el punto donde se lanzo la
excepcion y el manejador seleccionado. Como tanto agrega como elimina lanzan
excepciones, su invocacion tiene que estar dentro de un bloque try que va de la
lnea 24: a la lnea 35:. Si es que estos metodos lanzan la excepcion, ya sea en la
lnea 26: o 29:, ya no se ejecuta la lnea 27: en el primer caso y la lnea 30: en el
segundo. Por lo tanto se esta dando un control adecuado del flujo del programa,
utilizando para ello excepciones.
Otra caracterstica que tiene este segmento de aplicacion es que como el bloque
try esta dentro de una iteracion, y si es que se hubiere lanzado una excepcion en
alguno de los metodos invocados, una vez que se llego al final del bloque try y
habiendose o no ejecutado alguno de los manejadores de excepciones asociados al
bloque try, la ejecucion regresa a verificar la condicion del ciclo, logrando de hecho
que el programa no termine por causa de las excepciones. Esta forma de hacer las
cosas es muy comun. Supongamos que le pedimos al usuario que teclee un numero
entero y se equivoca. Lo mas sensato es volverle a pedir el dato al usuario para
trabajar con datos adecuados, en lugar de abortar el programa.
camente podemos usar sus constructores por omision, los que no tienen ningun
parametro.
El constructor por omision siempre nos va a informar del tipo de excepcion que
fue lanzado (la clase a la que pertenece), por lo que el constructor de Exception
que tiene como parametro una cadena no siempre resulta muy util. Sin embargo,
podemos definir una clase tan compleja como queramos, con los parametros que
queramos en los constructores. Unicamente hay que recordar que si definimos
alguno de los constructores con parametros, automaticamente perdemos acceso
al constructor por omision. Claro que siempre podemos invocar a super() en los
constructores definidos en las subclases.
Podemos, en las clases de excepciones creadas por el usuario, tener mas meto-
dos o informacion que la que nos provee la clase Exception o su superclase Th-
rowable. Por ejemplo, la clase RegNoEncontradoException que disenamos para la
base de datos pudiera proporcionar mas informacion al usuario que simplemente el
mensaje de que no encontro al registro solicitado; podra proporcionar los registros
inmediato anterior e inmediato posterior al usuario. En ese caso, debera poder
armar estos dos registros. Para ello, podramos agregar a la clase dos campos, uno
para cada registro, y dos metodos, el que localiza al elemento inmediato anterior
en la lista y el que localiza al inmediato posterior. En los listados 9.13 y 9.14 en
la pagina opuesta podemos ver un bosquejo de como se lograra esto.
excepcion, y que son llenados por el constructor, para proveer mas informacion al
usuario de la situacion presente en el momento de la excepcion. Haciendolo de esta
manera, en el momento de lanzar la excepcion se puede invocar un constructor
que recoja toda la informacion posible del contexto en el que es lanzada, para
reportar despues en el manejador.
Unicamente hay que recordar que todas aquellas variables que sean declaradas
en el bloque try no son accesibles desde fuera de este bloque, incluyendo a los
manejadores de excepciones. Insistimos: si se desea pasar informacion desde el
punto donde se lanza la excepcion al punto donde se maneja, lo mejor es pasarla
en la excepcion misma. Esto ultimo se consigue redefiniendo y extendiendo a la
clase Exception.
Ademas de la informacion que logremos guardar en la excepcion, tenemos
tambien los metodos de Throwable, como el que muestra el estado de los registros
de activacion en el stack, o el que llena este stack en el momento inmediato anterior
a lanzar la excepcion. Todos estos metodos se pueden usar en las excepciones
creadas por el programador.
Veamos en los listados 9.15 y 9.16 otro ejemplo de declaracion y uso de ex-
cepciones creadas por el programador. En este ejemplo se agrega un constructor
y un atributo que permiten a la aplicacion recoger informacion respecto al orden
en que se lanzan las excepciones y el contexto en el que esto sucede.
finally funciona como una tarea que sirve para dar una ultima pasada al codigo,
de tal manera de garantizar que todo quede en un estado estable. No siempre es
necesario, ya que Java cuenta con recoleccion automatica de basura y destructores
de objetos tambien automaticos. Sin embargo, se puede usar para agrupar tareas
que se desean hacer, por ejemplo en un sistema guiado por excepciones, ya sea que
se presente un tipo de excepcion o no. Veamos un ejemplo con unos interruptores
electricos en el listado 9.19.
iii. Calcular algun resultado alternativo en lugar del que el metodo se supone que
deba haber calculado.
iv. Hacer lo que se pueda en el contexto actual y relanzar la excepcion para que
sea manejada en un contexto superior.
viii. Hacer una aplicacion (o biblioteca) mas segura (se refleja a corto plazo en la
depuracion y a largo plazo en la robustez de la aplicacion).
Con esto damos por terminado este tema, aunque lo usaremos extensivamente
en los captulos que siguen.
Entrada y salida
10
10.1 Conceptos generales
Uno de los problemas que hemos tenido hasta el momento es que las bases
de datos que hemos estado construyendo no tienen persistencia, esto es, una vez
que se descarga la aplicacion de la maquina virtual (que termina) la informacion
que generamos no vive mas alla. No tenemos manera de almacenar lo que cons-
truimos en una sesion para que, en la siguiente sesion, empecemos a partir de
donde nos quedamos. Practicamente en cualquier aplicacion que programemos y
usemos vamos a requerir de mecanismos que proporcionen persistencia a nuestra
informacion.
En los lenguajes de programacion, y en particular en Java, esto se logra me-
diante archivos 1 , que son conjuntos de datos guardados en un medio de almace-
namiento externo. Los archivos sirven de puente entre la aplicacion y el medio
exterior, ya sea para comunicarse con el usuario o para, como acabamos de men-
cionar, darle persistencia a nuestras aplicaciones.
Hasta ahora hemos usado extensamente la clase Consola, que es una clase
programada por nosotros. Tambien hemos usado en algunos ejemplos del captulo
anterior dos archivos (objetos) que estan dados en Java y que son System.out
y System.err. Ambos archivos son de salida; el primero es para salida normal en
consola y el segundo para salida, tambien en consola, pero de errores. Por ejemplo,
cuando un programa que aborta reporta donde se lanzo la excepcion, el reporte
10.1 Conceptos generales 322
lo hace a System.err.
La razon por la que usamos nuestra propia clase hasta el momento es que en
Java practicamente toda la entrada y salida puede lanzar excepciones; eso implica
que cada vez que usemos un archivo para leer, escribir, crearlo, eliminarlo, y en
general cualquier operacion que tenga que ver con archivos, esta operacion tiene
que ser vigilada en un bloque try, con el manejo correspondiente de las excep-
ciones que se pudieran lanzar. Lo que hace nuestro paquete de entrada y salida
es absorber todas las excepciones lanzadas para que cuando usan los metodos de
estas clases ya no haya que vigilar las excepciones.
El disenar los metodos de entrada y salida para que lancen excepciones en
caso de error es no solo conveniente, sino necesario, pues es en la interaccion con
un usuario cuando la aplicacion puede verse en una situacion no prevista, como
datos erroneos, un archivo que no existe o falta de espacio en disco para crear un
archivo nuevo.
Un concepto muy importante en la entrada y salida de Java es el de flujos de
datos. Java maneja su entrada y salida como flujos de caracteres (ya sea de 8 o
16 bits). En el caso de los flujos de entrada, estos proporcionan caracteres, uno
detras de otro en forma secuencial, para que el programa los vaya consumiendo
y procesando. Los flujos de salida funcionan de manera similar, excepto que es
el programa el que proporciona los caracteres para que sean proporcionados al
mundo exterior, tambien de manera secuencial.
En las figuras 10.1 y 10.2 en la pagina opuesta vemos los algoritmos generales
para lectura y escritura, no nada mas para Java, sino que para cualquier lenguaje
de programacion.
Dispositivo D
A
T Leer
O S Aplicacion
T
O S Dispositivo
DataInput
InputStream ObjectInput
ObjectInputStream
LineNumberInputStream
SequenceInputStream
DataInputStream
ByteArrayInputStream
BufferedInputStream
FilterInputStream
PushBackInputStreeam
FileInputStream
CheckedInputSteam
PipedInputStream
CipherInputStream
StringBufferInputStream
DigestInputStream
InflaterInputStream
ProgressMonitorInputStream
Con sus marcadas excepciones, por el uso que se le pueda dar, hay una correspon-
dencia entre ambas jerarquas.
DataOutput
OutputStream ObjectOutput
ObjectOutputStream
ByteArrayOutputStream
PipedOutputStream
FileOutputStream
FilterOutputStream
PrintStream
BufferedOutputStream
DataOutputStream
BufferedWriter
CharArrayWriter
FilterWriter
Writer PrintWriter
PipedWriter
StringWriter
OutputStreamWriter FileWriter
10.4 Entrada y salida de caracteres 330
StringReader
CharArrayReader
PipedReader
Reader
BufferedReader LineNumberReader
FilterReader PushbackReader
InputStreamReader FileReader
Filtro
Origen Destino
Tambien estas jerarquas corren paralelas a las que trabajan con bytes, por lo
que no daremos una nueva explicacion de cada una de ellas. Se aplica la misma
331 Entrada y salida
explicacion, excepto que donde dice byte hay que sustituir por caracter. Uni-
camente explicaremos aquellas clases que no tienen contra parte en bytes.
Vale la pena hacer la aclaracion que en este caso los flujos que leen de y escriben
a archivos en disco extienden a las clases InputStreamReader y OutputStreamWriter
respectivamente, ya que la unidad de trabajo en los archivos es el byte (8 bits) y
no el caracter (16 bits). Por lo demas funcionan igual que sus contra partes en los
flujos de bytes.
Es conveniente mencionar que las versiones actuales de Java indican que las
clases que se deben usar son las que derivan de Reader y Writer y no las que son
subclases de InputStream y OutputStream. Ambas jerarquas (las de bytes y las
de caracteres) definen practicamente los mismos metodos para bytes y caracteres,
pero para fomentar la portabilidad de las aplicaciones se ha optado por soportar
de mejor manera las clases relativas a caracteres.
Sin embargo, como ya mencionamos, la entrada y salida estandar de Java es
a traves de clases que pertenecen a la jerarqua de bytes (System.in, System.out y
System.err).
Lo primero que queremos poder hacer es leer desde el teclado y escribir a
pantalla. Esto lo necesitamos para la clase que maneja el menu y de esta manera
ir abriendo las cajas negras que nos proporcionaba la clase Consola para este fin.
Por ser objetos estaticos de la clase se pueden usar sin construirlos. Todo
programa en ejecucion cuenta con ellos, por lo que los puede usar, simplemente
refiriendose a ellos a traves de la clase System.
El primero de ellos es un archivo al que dirigiremos los mensajes que se refieran
a errores, y que no queramos mezclar con la salida normal. El segundo objeto es
para leer de teclado (con eco en la pantalla) y el tercero para escribir en la pantalla.
Las dos clases mencionadas son clases concretas que aparecen en la jerarqua de
clases que mostramos en las figuras 10.5 en la pagina 325 y 10.6 en la pagina 328.
Si bien la clase PrintStream se va a comportar exactamente igual a Consola,
en cuanto a que interpreta enteros, cadenas, flotantes, etc. para mostrarlos con
formato adecuado, esto no sucede con la clase InputStream que opera de manera
muy primitiva, leyendo byte por byte, y dejandole al usuario la tarea de pegar los
bytes para interpretarlos. Mas adelante revisaremos con cuidado todos los meto-
dos de esta clase. Por el momento unicamente revisaremos los metodos que leen
byte por byte, y que son:
Como podemos ver de los metodos de la clase InputStream, son muy primitivos
y difciles de usar. Por ello, como primer paso en la inclusion de entrada y salida
completa en nuestra aplicacion, para entrada utilizaremos una subclase de Reader,
BufferedReader, mas actual y mejor soportada.
Esta es una clase abstracta que deja sin implementar uno de sus metodos. El
constructor y los metodos se listan a continuacion:
public void write (byte[] b, int off , int len ) throws IOException
Escribe en el flujo de salida los bytes desde b[off] hasta b[off+len-1]. Si
hay un error en el ndice o si b es null, lanza la excepcion correspondiente
(como son ArithmeticException ambas no hay que vigilarlas). Si hay algun
error de I/O se lanza la excepcion correspondiente.
public void flush () throws IOException
Evacua el flujo, obligando a que los bytes que esten todava en el buffer
sean escritos al medio fsico.
public void close () throws IOException
Cierra el flujo y libera los recursos del sistema asociados al flujo. Una vez
cerrado el flujo, cualquier otro intento de escribir en el va a causar una
excepcion. Lanza una excepcion (IOException) si se intenta reabrir. En
realidad no hace nada, sino que se tiene que reprogramar para que haga
lo que tiene que hacer.
Antes:
import i c c 1 . i n t e r f a z . C o n s o l a ;
Despues:
import j a v a . i o . ;
Dejamos de importar nuestra clase de entrada y salida e importamos el paquete
io de Java. Si bien no lo necesitamos todava, IOException esta en este paquete
y pudieramos necesitarla.
Antes:
p r i v a t e void r e p o r t a N o ( C o n s o l a cons , S t r i n g nombre ) {
c o n s . i m p r i m e l n ( "El estudiante : z n z t"
+ nombre
+ " z n No esta en el grupo" ) ;
}
Despues:
p r i v a t e void r e p o r t a N o ( P r i n t S t r e a m out ,
S t r i n g nombre ) {
o u t . p r i n t l n ( "El estudiante : z n z t" + nombre
+ " z n no esta en el grupo" ) ;
}
Consola antes se usaba para entrada y salida, pero ahora tenemos que tener un
flujo de entrada y dos de salida. Este metodo unicamente maneja el de salida,
por lo cambiamos su parametros a que sea un flujo de salida del mismo tipo
que son out y err.
10.5 El manejo del menu de la aplicacion 338
Antes:
S t r i n g nombre = c o n s . l e e S t r i n g ( "Dame el nombre del "
+ " estudiante empezando por "
+ " apellido paterno:" ) ;
Despues:
S t r i n g nombre ;
System . o u t . p r i n t ( "Dame el nombre del estudiante ,"
+ " empezando por apellido paterno:" ) ;
// Aca va l a l e c t u r a
Antes:
c o n s . i m p r i m e l n ( menu ) ;
S t r i n g s o p c i o n = c o n s . l e e S t r i n g ( "Elige una opcion --> " ) ;
Despues:
System . o u t . p r i n t l n ( menu ) ;
System . o u t . p r i n t ( "Elige una opcion --> " ) ;
// Aca v i e n e l o de l a l e c t u r a de l a o p c i o n
Antes:
case 0 : // S a l i r
c o n s . i m p r i m e l n ( "Espero haberte servido. z n"
+ "Hasta pronto ..." ) ;
Despues:
case 0 : // S a l i r
System . o u t . p r i n t l n ( "Espero haberte servido. z n"
+ "Hasta pronto ..." ) ;
339 Entrada y salida
Antes:
c o n s . i m p r i m e l n ( "El estudiante : z n z t"
+ nombre + " z n Ha sido eliminado " ) ;
}
e l s e r e p o r t a N o ( cons , nombre ) ;
Despues:
System . o u t . p r i n t l n ( "El estudiante : z n z t"
+ nombre + " z n Ha sido eliminado " ) ;
}
e l s e r e p o r t a N o ( System . e r r , nombre ) ;
Antes:
s u b c a d = c o n s . l e e S t r i n g ( "Dame la subcadena a "+
"buscar: " ) ;
do {
s c u a l = c o n s . l e e S t r i n g ( "Ahora dime de cual campo :"
+ "1: Nombre , 2: Cuenta ,"
+ " 3: Carrera , 4: Clave" ) ;
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
c o n s . i m p r i m e l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
Despues:
try {
System . o u t . p r i n t ( "Dame la subcadena a "
+ "buscar: " ) ;
// L e e r s u b c a d e n a
do {
System . o u t . p r i n t ( "Ahora dime de cual campo :"
+ "1: Nombre 2: Cuenta 3: Carrera 4: Clave" ) ;
// L e e r o p c i o n
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
System . o u t . p r i n t l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
10.5 El manejo del menu de la aplicacion 340
donde = ( E s t u d i a n t e ) miCurso . b u s c a S u b c a d ( c u a l , s u b c a d ) ;
i f ( donde != n u l l )
System . o u t . p r i n t l n ( donde . d a R e g i s t r o ( ) ) ;
e l s e r e p o r t a N o ( System . e r r , s u b c a d ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "Error al dar los datos"
+ " para buscar" ) ;
} // end o f t r y c a t c h
Dejamos sin llenar las lecturas, aunque las colocamos en un bloque try. . . catch
porque las lecturas pueden lanzar excepciones.
Antes:
case 4 : // L i s t a t o d o s
miCurso . l i s t a T o d o s ( c o n s ) ;
Despues:
case 4 : // L i s t a t o d o s
miCurso . l i s t a T o d o s ( System . o u t ) ;
Simplemente pasamos como parametro el archivo de la consola.
Antes:
d e f a u l t : // E r r o r , v u e l v e a p e d i r
c o n s . i m p r i m e l n ( "No diste una opcion valida .\n" +
"Por favor vuelve a elegir." ) ;
return 0;
Despues:
d e f a u l t : // E r r o r , v u e l v e a p e d i r
System . o u t . p r i n t l n ( "No diste una opcion valida .\n"
+ "Por favor vuelve a elegir." ) ;
Antes:
import i c c 1 . i n t e r f a z . C o n s o l a ;
Despues:
import j a v a . i o . ;
Antes:
p u b l i c void l i s t a T o d o s ( C o n s o l a c o n s ) {
...
cons . imprimeln ( a c t u a l . d a R e g i s t r o ( ) ) ;
...
c o n s . i m p r i m e l n ( "No hay registros en la base de datos" ) ;
Despues:
p u b l i c void l i s t a T o d o s ( P r i n t S t r e a m c o n s ) {
...
cons . p r i n t l n ( a c t u a l . d a R e g i s t r o ( ) ) ;
...
c o n s . p r i n t l n ( "No hay registros en la base de datos" ) ;
Antes:
p u b l i c void losQueCazanCon ( C o n s o l a cons , i n t c u a l ,
S t r i n g subcad ) {
...
cons . imprimeln ( a c t u a l . d a R e g i s t r o ( ) ) ;
...
c o n s . i m p r i m e l n ( "No se encontro ningun registro "
+ "que cazara" ) ;
Despues:
p u b l i c void losQueCazanCon ( P r i n t S t r e a m cons , i n t c u a l ,
S t r i n g subcad ) {
...
cons . p r i n t l n ( a c t u a l . d a R e g i s t r o ( ) ) ;
...
c o n s . p r i n t l n ( "No se encontro ningun registro "
+ "que cazara" ) ;
10.5 El manejo del menu de la aplicacion 342
Antes:
p u b l i c void l i s t a T o d o s ( C o n s o l a c o n s ) ;
p u b l i c void losQueCazanCon ( C o n s o l a cons , i n t c u a l ,
S t r i n g subcad ) ;
Despues:
p u b l i c void l i s t a T o d o s ( P r i n t S t r e a m c o n s ) ;
p u b l i c void losQueCazanCon ( P r i n t S t r e a m cons , i n t c u a l ,
S t r i n g subcad ) ;
3
Estamos trabajando con JDK Standard Edition 5.0 o posteriores
10.5 El manejo del menu de la aplicacion 344
Constructor:
protected FilterInputStream(InputStream in)
Construye el flujo con in como flujo subyacente.
Metodos:
Hereda los metodos ya implementados de InputStream e implementa los
declarados como abstractos en la superclase. Como los encabezados y su
significado se mantiene, no vemos el caso de repetir esta informacion.
No existe una clase paralela a esta en la jerarqua de Reader. Sin embargo, al re-
visar la documentacion vemos que nuestro metodo favorito, readLine(), esta anun-
ciado como descontinuado (deprecated ), lo que quiere decir que no lo mantienen al
da y lo usaramos arriesgando problemas. Pero tambien en la documentacion vie-
ne la sugerencia de usar BufferedReader, subclase de Reader, por lo que volteamos
hacia ella para resolver nuestros problemas. Presentamos nada mas lo agregado
en esta clase, ya que los metodos que hereda los [presentamos en la clase Reader
antes. Como su nombre lo indica, esta clase trabaja con un buffer de lectura.
No listaremos los metodos que sobreescribe (overrides) ya que tienen los mismos
parametros y el mismo resultado, as que los pensamos como heredados aunque
esten redefinidos en esta clase.
Como se puede ver, tampoco esta clase es muy versatil, pero como lo unico que
347 Entrada y salida
Antes:
p r i v a t e S t r i n g pideNombre ( C o n s o l a c o n s ) {
S t r i n g nombre ;
System . o u t . p r i n t ( "Dame el nombre del estudiante ,"
+ " empezando por apellido paterno:" ) ;
nombre=c o n s . l e e S t r i n g ( ) ;
r e t u r n nombre ;
}
Despues:
p r i v a t e S t r i n g pideNombre ( B u f f e r e d R e a d e r c o n s )
throws I O E x c e p t i o n {
S t r i n g nombre ;
System . o u t . p r i n t ( "Dame el nombre del estudiante ,"
+ " empezando por apellido paterno:" ) ;
try {
nombre=c o n s . r e a d L i n e ( ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "Error al leer nombre" ) ;
throw e ;
} // end o f t r y c a t c h
r e t u r n nombre ;
}
Antes:
s o p c i o n = c o n s . l e e S t r i n g ( "Elige una opcion --> " ) ;
10.5 El manejo del menu de la aplicacion 348
Despues:
System . o u t . p r i n t ( "Elige una opcion --> " ) ;
try {
sopcion = cons . readLine ( ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "Error al leer opcion" ) ;
throw new I O E x c e p t i o n ( "Favor de repetir eleccion " ) ;
} // end o f t r y c a t c h
Antes:
case AGREGA : // Agrega E s t u d i a n t e
nombre = pideNombre ( c o n s ) ;
cuenta = pideCuenta ( cons ) ;
c a r r e r a = p i d e C a r r e r a ( cons ) ;
c l a v e = pide Clave ( cons ) ;
miCurso . a g r e g a E s t O r d e n
(new E s t u d i a n t e ( nombre , c u e n t a , c l a v e , c a r r e r a ) ) ;
Despues:
case AGREGA : // Agrega E s t u d i a n t e
try {
nombre = pideNombre ( c o n s ) ;
cuenta = pideCuenta ( cons ) ;
c a r r e r a = p i d e C a r r e r a ( cons ) ;
c l a v e = pide Clave ( cons ) ;
miCurso . a g r e g a E s t O r d e n
(new E s t u d i a n t e ( nombre , c u e n t a , c l a v e , c a r r e r a ) ) ;
} catch ( I O E x c e p t i o n e ) {
System . o u t . p r i n t l n ( "Error al leer datos del "
+ " estudiante .\ nNo se pudo agregar" ) ;
} // end o f t r y c a t c h
Antes:
case QUITA : // Q u i t a e s t u d i a n t e
nombre = pideNombre ( c o n s ) ;
donde = ( E s t u d i a n t e ) miCurso . b u s c a S u b c a d
( E s t u d i a n t e .NOMBRE, nombre ) ;
349 Entrada y salida
i f ( donde != n u l l ) {
nombre = donde . daNombre ( ) ;
miCurso . q u i t a E s t ( nombre ) ;
System . o u t . p r i n t l n ( "El estudiante :\n\t"
+ nombre + "\n Ha sido eliminado " ) ;
}
e l s e r e p o r t a N o ( nombre ) ;
Despues:
case QUITA : // Q u i t a e s t u d i a n t e
try {
nombre = pideNombre ( c o n s ) ;
donde = ( E s t u d i a n t e ) miCurso . b u s c a S u b c a d
( E s t u d i a n t e .NOMBRE, nombre ) ;
i f ( donde != n u l l ) {
nombre = donde . daNombre ( ) ;
miCurso . q u i t a E s t ( nombre ) ;
System . o u t . p r i n t l n ( "El estudiante :\n\t"
+ nombre + "\nHa sido eliminado " ) ;
}
e l s e r e p o r t a N o ( nombre ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "No se proporcionaron bien"
+ " los datos .\ nNo se pudo eliminar." ) ;
} // end o f t r y c a t c h
Antes:
case BUSCA : // Busca s u b c a d e n a
s u b c a d = c o n s . l e e S t r i n g ( "Dame la subcadena a "
+ "buscar: " ) ;
do {
s c u a l = c o n s . l e e S t r i n g ( "Ahora dime de cual "
+ "campo : 1: Nombre 2: Cuenta , 3: Carrera"
+ ", 4: Clave -->" ) ;
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
System . o u t . p r i n t l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
10.5 El manejo del menu de la aplicacion 350
Antes: (continua. . . )
donde = ( E s t u d i a n t e ) miCurso .
buscaSubcad ( cual , subcad ) ;
i f ( donde != n u l l )
System . o u t . p r i n t l n ( donde . d a R e g i s t r o ( ) ) ;
else reportaNo ( subcad ) ;
Despues:
case BUSCA : // Busca s u b c a d e n a
try {
System . o u t . p r i n t ( "Dame la subcadena a "+
"buscar: " ) ;
subcad = cons . r e a d L i n e ( ) ;
do {
System . o u t . p r i n t ( "Ahora dime de cual "
+ "campo : 1: Nombre 2: Cuenta"
+ " 3: Carrera 4: Clave -->" ) ;
s c u a l = cons . readLine ( ) ;
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
System . o u t . p r i n t l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
donde = ( E s t u d i a n t e ) miCurso .
buscaSubcad ( cual , subcad ) ;
i f ( donde != n u l l )
System . o u t . p r i n t l n ( donde . d a R e g i s t r o ( ) ) ;
else reportaNo ( subcad ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "Error al dar los "
+ "datos para buscar" ) ;
} // end o f t r y c a t c h
Antes:
case LISTAALGUNOS : // L i s t a con c r i t e r i o
s u b c a d = c o n s . l e e S t r i n g ( "Da la subcadena que "
+ " quieres contengan los "
+ " registros :" ) ;
do {
s c u a l = c o n s . l e e S t r i n g ( "Ahora dime de cual campo:"
+ " 1: Nombre 2: Cuenta 3: Carrera 4: Clave -->" ) ;
351 Entrada y salida
Antes: (continua. . . )
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
System . o u t . p r i n t l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
miCurso . losQueCazanCon ( System . out , c u a l , s u b c a d ) ;
Despues:
case LISTAALGUNOS : // L i s t a con c r i t e r i o
try {
System . o u t . p r i n t l n ( "Da la subcadena que " +
" quieres contengan los " +
" registros :" ) ;
subcad = cons . r e a d L i n e ( ) ;
do {
System . o u t . p r i n t ( "Ahora dime de cual campo:"
+ "1: Nombre 2: Cuenta 3: Carrera 4: Clave -->" ) ;
s c u a l = cons . readLine ( ) ;
c u a l = "01234" . i n d e x O f ( s c u a l ) ;
i f ( c u a l < 1)
System . o u t . p r i n t l n ( "Opcion no valida" ) ;
} while ( c u a l < 1 ) ;
miCurso . losQueCazanCon ( System . out , c u a l , s u b c a d ) ;
} catch ( I O E x c e p t i o n e ) {
System . e r r . p r i n t l n ( "Error al dar los datos"
+ "para listar" ) ;
} // end o f t r y c a t c h
Antes:
p u b l i c s t a t i c void main ( S t r i n g [ ] a r g s ) {
...
C o n s o l a c o n s o l a = new C o n s o l a ( ) ;
Despues:
p u b l i c s t a t i c void main ( S t r i n g [ ] a r g s ) {
...
B u f f e r e d R e a d e r c o n s o l a = new B u f f e r e d R e a d e r
(new I n p u t S t r e a m R e a d e r ( System . i n ) ) ;
10.6 Redireccionamiento de in, out y err 352
Antes:
Despues:
w h i l e ( o p c i o n != 1) {
try {
o p c i o n = miMenu . daMenu ( c o n s o l a , miCurso ) ;
} catch ( I O E x c e p t i o n e ) {
System . o u t . p r i n t l n ( "Opcion mal elegida" ) ;
o p c i o n =0;
} // end o f t r y c a t c h
} // w h i l e
p u b l i c f i n a l s t a t i c v o i d s e t I n ( I n p u t S t r e a m newIn )
p u b l i c f i n a l s t a t i c v o i d s e t O u t ( OutputStream newOut )
p u b l i c f i n a l s t a t i c v o i d s e t E r r ( P r i n t S t r e a m newErr )
Hasta ahora unicamente hemos trabajado con la consola o bien con redirec-
cionamiento de la consola, pero no hemos entrado a la motivacion principal de
este captulo y que consiste en lograr guardar el estado de nuestra base de datos
para que pueda ser utilizado posteriormente como punto de partida en la siguiente
ejecucion.
Podemos almacenar, en primera instancia, la base de datos como un conjun-
to de cadenas, y para ello podemos volver a utilizar a los flujos BufferedReader
y BufferedWriter que ya conocemos, pero en esta ocasion queremos que el flujo
subyacente sea un archivo en disco y no un flujo estandar. Revisemos entonces la
clase FileReader y FileWriter que me van a dar esa facilidad. Estos flujos extienden,
respectivamente, a InputStreamReader y OutputStreamWriter, que a su vez here-
dan, respectivamente, de Reader y Writer. De esta jerarqua unicamente hemos
revisado la clase Reader, as que procedemos a ver las otras clases de la jerarqua
que vamos a requerir.
Realmente el unico metodo con el que hay que tener cuidado es el que escribe
un caracter (entero), porque el resto de los metodos se construyen simplemente
invocando a este.
Las clases que heredan directamente de Reader y Writer son, respectivamente,
355 Entrada y salida
Ahora s ya podemos pasar a revisar las clases FileReader y FileWriter que here-
dan respectivamente de InputStreamReader y OutputStreamWriter. Empezaremos
por el flujo de entrada. En esta subclase unicamente se definen los constructores,
ya que se heredan precisa y exactamente los metodos implementados en InputS-
treamReader.
Para los flujos de salida que escriben a disco tenemos una situacion similar a
la de archivos de entrada, pues lo unico que se define para la subclase FileWri-
ter son los constructores. Para el resto de los metodos y campos se heredan las
implementaciones dadas por OutputStreamWriter. Veamos la definicion.
10.7 Persistencia de la base de datos 358
Hay que recordar que la herencia permite que donde quiera que aparezca una
clase como parametro, los argumentos pueden ser objetos de cualquiera de sus sub-
clases. Con esto en mente pasamos a implementar las opciones en el menu de leer
de un archivo en disco o escribir a un archivo en disco para guardar la informacion
generada en una sesion dada.
359 Entrada y salida
nicarnos con el usuario. Queremos que el mensaje sea preciso respecto a que vamos
a hacer con el archivo, pero como usamos el mismo metodo simplemente le pasa-
mos de cual caso se trata caso para que pueda armar el mensaje correspondiente
(lneas 59: a 64:). Despues entramos a un bloque try. . . catch en el que vamos a
leer del usuario el nombre del archivo. El metodo, como lo indica su encabezado,
exporta la excepcion que pudiera lanzarse al leer el nombre del archivo. Estamos
listos ya para programar el algoritmo de la figura 10.10 en la pagina opuesta. El
codigo lo podemos ver en el listado 10.2.
En las lneas 202: y 203: solicitamos el nombre del archivo a usar y procedemos
a abrir el archivo. En este punto la unica excepcion que pudo haber sido lanzada
es en la interaccion con el usuario, ya que la apertura de un archivo en disco
difcilmente va a lanzar una excepcion.
El algoritmo para leer registros de una archivo en disco es la imagen del proceso
para guardar. Este se puede ver en la figura 10.11.
Para identificar el archivo del que vamos a leer usamos el mismo metodo,
excepto que con un mensaje apropiado. Al abrir el archivo automaticamente nos
encontraremos frente al primer registro. A partir de ah, suponemos que el archivo
esta correcto y que hay cuatro cadenas sucesivas para cada registro que vamos a
leer. El codigo que corresponde a esta opcion se encuentra en el listado 10.3 en la
pagina opuesta.
363 Entrada y salida
$ $
'
' '
' Identificar archivo
'
' '
&
'
' Abrir el archivo
'
' Inicio
'
' '
'
' '
'
Colocarse al principio
'
' %
'
' del archivo
Leer registros &
a la Base de Datos # #
'
'
desde un archivo en disco '
' Procesar registro Leer registro de disco
'
' P roceso
'
' (mientras haya) Pasar al siguiente
'
'
'
'
'
'
'
' !
%F inal Cerrar el archivo
Codigo 10.3 Opcion para leer registros desde disco (MenuListaIO) 1/2
234: case LEER : // L e e r de d i s c o
235: try {
236: s A r c h i v o = pideNombreArch ( cons , LEER ) ;
237: a r c h i v o 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 ( s A r c h i v o ) ) ;
238:
239: w h i l e ( ( nombre = a r c h i v o I n . r e a d L i n e ( ) ) != n u l l ) {
240: cuenta = a r c h i v o I n . readLine ( ) ;
241: carrera = archivoIn . readLine ( ) ;
242: clave = archivoIn . readLine ( ) ;
243:
244: miCurso . a g r e g a E s t F i n a l
245: ( new E s t u d i a n t e ( nombre , c u e n t a , c a r r e r a , c l a v e ) ) ;
246: } // end o f w h i l e ( ( nombre = a r c h i v o I n . r e a d L i n e ( ) ) != n u l l )
247: } catch ( F i l e N o t F o u n d E x c e p t i o n e ) {
248: System . o u t . p r i n t l n ( "El archivo " + s A r c h i v o
249: + " no existe ." ) ;
250: throw e ;
251: } catch ( I O E x c e p t i o n e ) {
252: System . e r r . p r i n t l n ( "No pude abrir archivo " ) ;
253: } catch ( E x c e p t i o n e ) {
254: System . o u t . p r i n t l n ( "NO alcanzaron los datos " ) ;
255: i f ( c a r r e r a == n u l l ) {
256: c a r r e r a = "????" ;
257: System . o u t . p r i n t l n ( "No hubo carrera " ) ;
258: } // end o f i f ( c a r r e r a == n u l l )
10.7 Persistencia de la base de datos 364
Codigo 10.3 Opcion para leer registros desde disco (MenuListaIO) 2/2
234: i f ( c u e n t a == n u l l ) {
235: c u e n t a = " 000000000 " ;
236: System . o u t . p r i n t l n ( "No hubo cuenta " ) ;
237: } // end o f i f ( c u e n t a == n u l l )
238: i f ( c l a v e == n u l l ) {
239: c l a v e = "????" ;
240: System . o u t . p r i n t l n ( "No hubo clave " ) ;
241: } // end o f i f ( c l a v e == n u l l )
242: } // end o f c a t c h
243: finally {
244: i f ( a r c h i v o I n != n u l l ) {
245: try {
246: archivoIn . close ();
247: } catch ( I O E x c e p t i o n e ) {
248: System . e r r . p r i n t l n ( "No pude cerrar el"
249: + " archivo de lectura " ) ;
250: } // end o f t r y c a t c h
251: } // end o f i f ( a r c h i v o I n != n u l l )
252: } // end o f f i n a l l y
253: r e t u r n LEER ;
En las lneas 303: y 304:, donde se usa otro constructor para el flujo, el que
permite indicar si se usa un archivo ya existente para agregar a el.
En las lneas 307: a 313: se usa el metodo append en lugar del metodo write,
ya que deseamos seguir agregando al final del archivo.
0 0 0 4 0 0 2 8 0 0 0 9 0 0 0 4 0 0 1 4 4 3 7 2 6 9 7A . .... .
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 . . . . . . 49
Lo que conviene es que la clase para cada registro nos entregue el tamano de
cada campo. Podemos suponer que esto es as, agregando a la clase InfoEstudiante
un arreglo con esta informacion:
s h o r t [ ] tamanhos = { 4 , 4 0 , 9 , 4 , 2 0 } ;
quedando en tamanhos[0] el numero de campos, en tamanhos[1] el tamano del primer
campo y as sucesivamente. Agregamos a la clase un metodo
p u b l i c s h o r t getTamanho ( i n t campo ) {
r e t u r n tamanhos [ campo ] ;
}
que simplemente regresa el tamano del campo solicitado.
Podemos pensar en un archivo que es heterogeneo, en el sentido de que lo que
llamamos el encabezado del mismo no tiene la forma que el resto de los elementos;
estos se componen de n campos la n viene en los primeros dos bytes del archivo
con formato binario de un entero corto (short) con un total de k bytes que
corresponde a la suma de los n enteros cortos que aparecen a partir del byte 2 del
archivo. El encabezado del archivo consiste de 2pn 1q bytes. Una vez procesados
estos n 1 enteros cortos, el resto del archivo lo podemos ver como un arreglo
unidimensional de bytes (similarmente a como manejamos la base de datos en
cadenas al principio).
Deseamos insistir en lo que dijimos al principio de esta seccion: todos los
archivos en disco se componen de bytes; la manera de agrupar los bytes para
obtener informacion que tenga sentido depende del software que se use para verlo,
de las mascaras que le apliquemos al archivo. Una vez que terminemos de armar
nuestro archivo con el formato que acabamos de ver, podran observar el archivo
con alguno de los visores de su sistema operativo y veran que tambien los primeros
2pn 1q bytes podran tratar de interpretarlos como caracteres ASCII, no como
variables de Java; por supuesto que si hacen esto la mayora de estos caracteres
no se podran ver en pantalla (por ejemplo, el 0 binario) o apareceran caracteres
que no guardan ninguna relacion con lo que ustedes esperaran ver.
369 Entrada y salida
......
15: LEERREGS = 9 ,
16: GUARDARREGS = 1 0 ;
......
128: p u b l i c i n t daMenu ( B u f f e r e d R e a d e r cons , L i s t a C u r s o miCurso )
129: throws I O E x c e p t i o n {
......
141: DataInputStream a r c h i v o R e g s I n = n u l l ;
142: DataOutputStream a r c h i v o R e g s O u t = n u l l ;
......
156: + "(9)\ tLeer de archivo binario \n"
157: + "(A)\ tGuardar en archivo binario \n"
......
170: o p c i o n = " 0123456789 AB" . i n d e x O f ( s o p c i o n ) ;
......
Nuevamente optamos por declarar los flujos necesarios dentro del metodo que
maneja el menu. La razon de esto es que estas opciones se pueden elegir en cual-
quier momento y mas de una vez, en cada ocasion con flujos fsicos distintos, por lo
que hacerlos globales a la clase o, peor aun, al uso de la clase, amarrara a utilizar
unicamente el flujo determinado antes de empezar, cuando existe la posibilidad
de que no se elija esta opcion o que, como ya mencionamos, se desee hacer varias
copias de l;a informacion. En las lneas 15:, 16: 156: a 170: simplemente agrega-
mos dos opciones al menu, y los mecanismos para manejarlas posponemos por el
momento el desarrollo de la opcion correspondiente dentro del switch . La decla-
10.8 Escritura y lectura de campos que no son cadenas 372
racion de los flujos la hacemos en las lneas 141: y 142:. Tanto en este caso como en
el los flujos BufferedReader y BufferedWriter podramos haberlos declarado como
objetos de las superclases correspondiente:
134: Reader a r c h i v o I n = n u l l ;
135: Writer archivoOut = null ;
Como se puede observar, las dos opciones son practicamente paralelas, excepto
que cuando una escribe la otra lee. Pasemos a revisar primero la opcion de escri-
tura, que sera el orden en que programaramos para probar nuestra aplicacion.
375 Entrada y salida
En las lneas 443: a 450: se verifica que la lista en memoria no este vaca. De
ser as se procede a obtener el descriptor de los campos (el encabezado del archivo
binario) en un arreglo de enteros pequenos (short); si la lista esta vaca se sale del
menu lanzando una excepcion, que es atrapada desde el metodo principal (main)
de la aplicacion.
Se procede a escribir el encabezado del archivo binario en las lneas 452: a 455:
como lo indica el diagrama del algoritmo, utilizando para ello el metodo writeShort
de la clase DataOutputStream ver documentacion de la clase en las paginas 10.8
y 10.8 . Una vez hecho esto se procede a escribir cada uno de los registros de la
base de datos, como un arreglo de bytes, con cada campo ocupando el numero de
bytes que indica el encabezado del archivo en las lneas 459: y 460: se ajusta el
campo a su tamano agregando blancos y en la lnea 463: se escribe utilizando el
metodo writeBytes de la clase DataOutputStream, que convierte una cadena a un
arreglo de bytes . Para escribir toda la lista seguimos nuestro algoritmo usual
que recorre listas.
Comparemos ahora el algoritmo con el codigo del listado 10.6. La parte que
corresponde a abrir el archivo y localizarlo se encuentra en las lneas 380: a 389:.
En esta opcion s procesamos por separado la excepcion que nos pueda dar la
localizacion del archivo binario del que el usuario solicita leer porque es posible que
no exista dicho archivo. Por eso, en lugar de la tradicional excepcion IOException,
aca tratamos de atrapar una excepcion que nos indica que no se encontro el
archivo. Igual que en el caso anterior, sin embargo, salimos con una excepcion del
metodo, pues tenemos que regresar a solicitar otra opcion.
De manera similar a como lo hicimos para escribir, construimos un flujo de la
10.8 Escritura y lectura de campos que no son cadenas 378
que el acceso al flujo sigue siendo secuencial, as que los bytes que saltemos en
la lectura ya no los podemos regresar5 . n el listado 10.7 agregamos una opcion al
menu para poder acceder al i-esimo registro, siempre y cuando se haga al principio
del proceso. Esto pudieramos usarlo para descartar un cierto numero de registros
y leer unicamente a partir de cierto punto.
domAccessFile. Hay que tener presente que aunque esta clase maneja archivos en
disco no hereda de ningun flujo (stream) o de lector/escritor (Reader/Writer), sino
que hereda directamente de Object, aunque s se encuentra en el paquete java.io.
Los ejemplares de esta clase proveen tanto lectura como escritura en el mis-
mo objeto. en general podemos pensar en un archivo de acceso directo como un
arreglo en disco, donde cada elemento del arreglo es un registro y al cual quere-
mos tener acceso directamente a cualquiera de los registros sin seguir un orden
predeterminado.
Con un archivo de este tipo tenemos siempre asociado un apuntador de archivo
(en adelante simplemente apuntador), que se encarga de indicar en cada momento
a partir de donde se va a realizar la siguiente lectura/escritura. Si el apuntador
esta al final del archivo y viene una orden de escritura, el archivo simplemente
se extiende (el arreglo crece); si estando al final del archivo viene una orden de
lectura, la maquina virtual lanzara una excepcion EOFException. Si se intenta
realizar alguna operacion despues de que el archivo fue cerrado, se lanzara una
IOException. Se tiene un metodo que se encarga de mover al apuntador, lo que
consigue que la siguen te lectura/escritura se lleve a cabo a partir de la posicion a
la que se movio el apuntador. Estas posiciones son absolutas en terminos de bytes.
Este tipo de archivos se pueden usar para lectura, escritura o lectura/escritura,
dependiendo de que se indique al construir los ejemplares.
Si bien no hereda de ninguna de las clases Stream, como ya dijimos, implemen-
ta a las interfaces DataOutput, DataInput y Closeable. Como se pueden imaginar,
las dos primeras tambien son implementadas por DataOutputStream y DataInputS-
tream, por lo que tendremos practicamente los mismos metodos que ya conocemos
de estas dos ultimas clases, todos en una unica clase. Revisaremos unicamente
aquellos metodos que no conocemos todava, dando por sentado que contamos
con los metodos para leer y escribir de DataInputStream y DataOutputStream.
Con esto ya tenemos las herramientas necesarias para acceder al disco con
acceso directo.
Figura 10.15 Algoritmo para agregar registros desde archivo de acceso directo
$
'
' Pedir nombre de archivo
'
'
'
' Abrir archivo para#lectura
'
'
'
'
'
'
'
Leer numero de tamanhos[0] Primer short
'
'
'
' campos del archivo
'
'
'
'
'
' $ $
'
' ' '
'
' & &Leer siguiente
'
' Calcular tamano Sumar tamanhos[i]
'
' short
'
' de registro '
% i=1,. . . , tamanhos[0] ' %
'
' Sumarlo
'
'
'
'
'
'
'
' $ $
'
' ' 'Leer cadena de
'
' '
' Obtener del usuario el ' '
'
' '
' &
'
' '
' consola
'
' '
' numero del registro
'
' '
' '
' Procesar dgito
'
' '
' solicitado '
%
Agregar registros '
' '
' mientras haya
& '
'
'
'
desde archivo de '
'
'
' '
' $
acceso directo ' '
'
'
'
'
'
'
'
&pos
'
'
'
'
'
'
'
'
'
'
Calcular posicion en
'
tamR numR
'
'
'
'
'
'
'
'
'
'
bytes %
|
encabezado|
'
' &
'
'
Procesar registro Colocar el apuntador en esa posicion
'
'
'
' (mientras se den) ''
'
'
' ' $
'
' '
' ' Leer nombre
'
' '
' '
'
'
' '
' &
'
' 'Leer registro desde
' Leer cuenta
'
' '
'
'
' '
' disco '
' Leer carrera
'
' '
' '
%
'
' '
'
'
' '
' Leer clave
'
' '
'
'
' '
'
'
' '
' $
'
' '
' '
'
' '
'Agregar a lista &Construir registro
'
' '
'
'
' '
' nuevo
'
% '
% registro ledo '
%
Agregar a lista
10.8 Escritura y lectura de campos que no son cadenas 386
Codigo 10.8 Lectura del nombre del archivo y apertura del mismo (case LEERDIRECTO) 1/2
603: case LEERDIRECTO :
604: // P e d i r e l nombre d e l f l u j o
605: try {
606: s A r c h i v o = pideNombreArch ( cons , o p c i o n ) ;
607: archivoRndm = new R a n d o m A c c e s s F i l e ( s A r c h i v o , "r" ) ;
608: } catch ( F i l e N o t F o u n d E x c e p t i o n e ) {
387 Entrada y salida
Codigo 10.8 Lectura del nombre del archivo y apertura del mismo (case LEERDIRECTO) 2/2
609: System . e r r . p r i n t l n ( "el archivo de entrada "
610: + ( s A r c h i v o != n u l l ? s A r c h i v o : "nulo" )
611: + " no existe " ) ;
612: r e t u r n LEERDIRECTO ;
613: } catch ( I O E x c e p t i o n e ) {
614: throw e ;
615: } // end o f t r y c a t c h
Una vez que calculamos el tamano del encabezado (numBytes), el tamano del
registro (tamR) y el numero de registros logicos en el archivo (fileSize) procedemos
a iterar, pidiendole al usuario el numero de registro, tantos como desee, del registro
que desea leer y agregar a la lista en memoria. Esto se lleva a cabo en las lneas
642: a 667: en el listado 10.10 en la siguiente pagina.
10.8 Escritura y lectura de campos que no son cadenas 388
Elegimos leer del usuario una cadena, para evitar errores de lectura en caso
de que el usuario no proporcione un entero lnea 650:. Calculamos el entero
correspondiente usando la regla de Horner, que proporciona una manera sencilla,
de izquierda a derecha, de calcular un polinomio. Supongamos que tenemos un
polinomio
cn xn cn1 xn1 . . . c0
Podemos pensar en un numero en base b como un polinomio donde x b y
tenemos la restriccion de que 0 ci b. En este caso para saber el valor en base
10 evaluamos el polinomio. La manera facil y costosa de hacerlo es calculando
cada una de las potencias de b para proceder despues a multiplicar ci por bi .
n
P pxq ci xi
i 0
Pero como acabamos de mencionar, esta es una manera costosa y poco elegante
389 Entrada y salida
lo que nos permite evaluar el polinomio sin calcular previamente las potencias de
b. Por ejemplo, el numero base 10 8725 lo podemos expresar como el polinomio
Pero como tenemos que calcular de adentro hacia afuera, el orden de las opera-
ciones es el siguiente:
ppp8 10q 7q 10 2q 10 5
8 10 80 7
87 10 870 2
872 10 8720 5
8725
lo que permite leer los dgitos de izquierda a derecha e ir realizando las multipli-
caciones y sumas necesarias. La ventaja de esta regla es que cuando leemos el 8,
por ejemplo, no tenemos que saber la posicion que ocupa, sino simplemente que
es el que esta mas a la izquierda. Dependiendo de cuantos dgitos se encuentren a
su derecha va a ser el numero de veces que multipliquemos por 10, y por lo tanto
la potencia de 10 que le corresponde. Este algoritmo se encuentra codificado en
las lneas 655: a 658:.
En las lneas 651: a 654: verificamos que el usuario no este proporcionando un
numero negativo (que empieza con -). Si es as, damos por terminada la sucesion
de enteros para elegir registros.
Quisieramos insistir en que no importa si el flujo es secuencial o de acceso
directo, una lectura se hace siempre a partir de la posicion en la que se encuentra
el flujo. Si se acaba de abrir esta posicion es la primera la cero (0) . Conforme se
hacen lecturas o escrituras el flujo o archivo va avanzando; en los flujos secuenciales
de entrada, mediante el metodo skip se puede avanzar sin usar los bytes saltados,
pero siempre hacia adelante. En cambio, en los archivos de acceso directo se cuenta
10.8 Escritura y lectura de campos que no son cadenas 390
con el comando seek que es capaz de ubicar la siguiente lectura o escritura a partir
de la posicion dada como argumento, colocando el apuntador de archivo en esa
posicion.
En el listado 10.11 se encuentra el codigo que corresponde a la ubicacion del
apuntador del archivo, frente al primer byte del registro solicitado por el usuario
en la iteracion actual.
Codigo 10.11 Posicionamiento del apuntador del archivo y lectura (case LEERDIRECTO)
687: try {
688: // S a l t a r e l numero de b y t e s r e q u e r i d o s p a r a u b i c a r s e
689: // en e l r e g i s t r o s o l i c i t a d o .
690: a S a l t a r = numRtamR + numBytes ;
691: archivoRndm . s e e k ( a S a l t a r ) ;
692: bCadena = new byte [ tamR ] ;
693: // Leemos e l r e g i s t r o s o l i c i t a d o .
694: archivoRndm . r e a d ( bCadena , 0 , tamanhos [ 1 ] ) ;
695: nombre = new S t r i n g ( bCadena , 0 , tamanhos [ 1 ] ) ;
696: f o r ( i n t i = 2 ; i <= tamanhos [ 0 ] ; i ++) {
697: archivoRndm . r e a d ( bCadena , 0 , tamanhos [ i ] ) ;
698: switch ( i ) {
699: case 2 :
700: c u e n t a = new S t r i n g ( bCadena , 0 , tamanhos [ i ] ) ;
701: break ;
702: case 3 :
703: c a r r e r a = new S t r i n g ( bCadena , 0 , tamanhos [ i ] ) ;
704: break ;
705: case 4 :
706: c l a v e = new S t r i n g ( bCadena , 0 , tamanhos [ i ] ) ;
707: break ;
708: default :
709: break ;
710: } // end o f s w i t c h ( i )
711: } // end o f f o r ( i n t i = 1 ;
712: // Se arma un o b j e t o de l a c l a s e E s t u d i a n t e
713: E s t u d i a n t e nuevo = new E s t u d i a n t e
714: ( nombre , c u e n t a , c a r r e r a , c l a v e ) ;
715: i f ( miCurso == n u l l ) {
716: System . o u t . p r i n t l n ( "No existe Curso " ) ;
717: throw new N u l l P o i n t e r E x c e p t i o n ( " Uuups " ) ;
718: } // end o f i f ( miCurso == n u l l )
719: miCurso . a g r e g a E s t F i n a l
720: ( new E s t u d i a n t e ( nombre , c u e n t a , c a r r e r a , c l a v e ) ) ;
721: } catch ( I O E x c e p t i o n e ) {
722: System . o u t . p r i n t l n ( "Hubo error en LEERDIRECTO " ) ;
723: } // end o f t r y c a t c h
724: } // end w h i l e s e p i d a n r e g i s t r o s
391 Entrada y salida
La lista que acabamos de dar dista mucho de ser exhaustiva, pero contiene la
suficiente informacion para poder cumplir con nuestro objetivo, que consiste en
serializar y deserializar la base de datos. Terminemos de reunir las herramientas
que requerimos mostrando la definicion de la clase ObjectOutputStream.
8
Reescribimos estas dos clases agregando al nombre Serial para mantener intacto el trabajo
que realizamos con anterioridad.
403 Entrada y salida
......
En las lneas 9:, 13:, 30:, 50: y 56: se encuentran las consecuencias de haber
cambiado el nombre a ambas clases de las que hablamos. En la lnea 10: se en-
cuentra la declaracion de que esta clase implementa a la interfaz Serializable, lo
que la hace susceptible de ser escrita o leda de un flujo de objetos.
Dado que la base de datos esta compuesta por objetos de la clase Estudiante
y eso no lo queremos modificar, tenemos que dar metodos que conviertan de
EstudianteSerial a Estudiante y viceversa en la clase EstudianteSerial. Son metodos
sencillos que unicamente copian los campos. El codigo se puede ver en el listado
10.15.
......
219: case LEEROBJETOS :
220: m e n s a j e += "de donde vas a leer objetos " ;
221: break ;
222: case GUARDAROBJETOS :
223: m e n s a j e += "en donde vas a escribir objetos " ;
224: break ;
......
10.9 Lectura y escritura de objetos 406
Con esto ya estamos listos para llenar los casos que nos ocupan. El algoritmo
para la lectura de los registros se muestra en la figura 10.17 y como se puede ob-
servar es practicamente identico al de leer registros de un archivo directo, excepto
que por el hecho de leer objetos la maquina virtual se encarga de interpretarlos.
$ #
'
' Abrir el archivo
'
' Inicio
'
' Colocarse al principio de la lista
'
'
'
'
'
'
'
& $
'
' Convertir al estudiante actual
Escritura de Objetos '
&
'
' Tomar registro en estudiante serial
'
'
'
' (mientras haya) ' Escribirlo al flujo de objetos
'
' '
'
'
' %
'
' Tomar como actual al siguiente
'
%
Cerrar el archivo
407 Entrada y salida
cion de entrada y salida que se va a alcanzar cuando ya no pueda leer del flujo.
Por ello, colocamos estas lneas en un bloque trycatch 981: a 986: que atrape
la excepcion y simplemente prenda una variable para avisar que ya no hay datos.
En las lneas 987: a 989: se detecta que ya se alcanzo el fin de archivo (o que no
se pudo leer) y se regresa al encabezado del while, para evitar tratar de procesar
datos erroneos.
Finalmente, en las lneas 990: y 991: se obtiene un objeto Estudiante a partir
del que se leyo y se agrega a la lista.
Las clausulas catch externas a la lectura y que corresponden unicamente al caso
de lectura lneas 994: a 1013: simplemente proporcionan un mensaje de error
de que es lo que sucedio, lo mismo que las clausulas catch del caso de escritura a
flujo de objetos lneas 1044: a 1049: .
En ambos casos que nos ocupan tenemos una clausula finally que se encarga de
cerrar el archivo, preguntando antes si es que el archivo fue abierto adecuadamente
lneas 1015: a 1022: en lectura y 1050: a 1058: en escritura que ademas tiene
que estar, nuevamente, en un bloque try-catch para el caso en que se lance una
excepcion.
Es interesante ver con un visor de texto en ASCII un archivo creado como
flujo de objetos ya que se ve, de alguna manera, la gran cantidad de informacion
que se encuentra adicional a los datos mismos. Esto se debe a que con cada
objeto se tiene que describir al mismo, lo que sucede ya que en un mismo flujo de
objetos podemos escribir objetos de distintas clases y al leerlos, automaticamente
se carga la descripcion que trae consigo el objeto. Es por eso que la lectura siempre
es de un Object y se tiene que hacer un cast para obtener el objeto que creemos
estamos leyendo. Para decodificar un objeto de un flujo del que no sabemos a
que clase pertenece, podemos utilizar la clase Class que proporciona toda clase de
herramientas para ello.
10.10 Colofon
La clase PingPong podra no tener metodo main. Lo ponemos para hacer una
demostracion de esta clase. En la lnea 22: nuevamente invocamos a los metodos
11.2 La clase Thread 414
currentThread y getName para que nos indiquen el nombre del hilo donde aparece
esa solicitud. A continuacion creamos dos objetos anonimos y les solicitamos que
cada uno de ellos inicie su hilo de ejecucion con el metodo start().
La ejecucion del metodo main de esta clase produce la salida que se observa
en la figura 11.1 en la pagina anterior.
Como se puede observar en la figura 11.1 en la pagina anterior, mientras que
el nombre del proceso que esta ejecutandose inmediatamente antes de lanzar los
hilos de ejecucion se llama main, que es el que tiene, los hilos de ejecucion lanzados
tienen nombres asignados por el sistema. Podamos haberles asignado nosotros un
nombre al construir a los objetos. Para eso debemos modificar al constructor de la
clase PingPong e invocar a un constructor de Thread que acepte como argumento
a una cadena, para que esta sea el nombre del hilo de ejecucion. Los cambios a
realizar se pueden observar en el listado 11.2. La salida que produce se puede ver
en la figura 11.2 en la pagina opuesta.
ejecucion.
Debe quedar claro que en el caso de hilos de ejecucion, cuando uno termina su
ejecucion no es que regrese al lugar desde el que se le lanzo, sino que simplemente
se acaba. El hilo de ejecucion, una vez lanzado, adquiere una vida independiente,
con acceso a los recursos del objeto desde el cual fue lanzado.
Hasta ahora hemos visto cuatro constructores para objetos de la clase Thread:
public Thread()
Es el constructor por omision. Construye un hilo de ejecucion anonimo, al
que el sistema le asignara nombre. Cuando una clase que extiende a Thread
no invoca a ningun constructor de la superclase, este es el constructor que
se ejecuta.
public Thread(String name)
Este construye un hilo de ejecucion y la asigna el nombre dado como argu-
mento.
11.4 Sincronizacion de hilos de ejecucion 418
cronizados o no, sobre el mismo objeto procedera sin problemas, ya que el hilo
de ejecucion posee el candado respectivo. El candado se libera cuando el hilo de
ejecucion termina de ejecutar el primer metodo sincronizado que invoco, que es el
que le proporciono el candado. Si esto no se hiciera as, cualquier metodo recursivo
o de herencia, por ejemplo, bloqueara la ejecucion llegando a una situacion en la
que el hilo de ejecucion no puede continuar porque el candado esta comprometi-
do (aunque sea consigo mismo), y no puede liberar el candado porque no puede
continuar.
El candado se libera automaticamente en cualquiera de estos tres casos:
Sintaxis:
xenunciado de sincronizaciony::= synchronized( xexpry) {
xenunciadosy
}
Semantica:
La xexpry tiene que regresar una referencia a un objeto. Mientras se ejecu-
tan los xenunciadosy, el objeto referido en la xexpry queda sincronizado. Si
se desea sincronizar mas de un objeto, se recurrira a enunciados de estos
anidados. El candado quedara liberado cuando se alcance el final de los
enunciados, o si se lanza alguna excepcion dentro del grupo de enunciados.
La forma general para que un proceso espere a que una condicion se cumpla es
s y n c h r o n i z e d v o i d doWhenCondition ( ) {
while ( ! c o n d i c i o n )
wait ( ) ;
// . . . Se h a c e l o que s e t i e n e que h a c e r cuando l a
// c o n d i c i o n e s v e r d a d e r a
}
Cuando se usa el enunciado wait se deben considerar los siguientes puntos:
Como hay varios hilos de ejecucion, la condicion puede cambiar de estado
aun cuando este hilo de ejecucion no este haciendo nada. Sin embargo, es
importante que estos enunciados esten sincronizados. Si estos enunciados no
estan sincronizados, pudiera suceder que el estado de la condicion se hiciera
verdadero, pero entre que este codigo sigue adelante, algun otro proceso hace
que vuelva a cambiar el estado de la condicion. Esto hara que este proceso
trabajara sin que la condicion sea verdadera.
La definicion de wait pide que la suspension del hilo de ejecucion y la libera-
cion del candado sea una operacion atomica, esto es, que nada pueda suceder
ni ejecutarse entre estas dos operaciones. Si no fuera as podra suceder que
hubiera un cambio de estado entre que se libera el candado y se suspende
el proceso, pero como el proceso ya no tiene el candado, la notificacion se
podra perder.
La condicion que se esta probando debe estar siempre en una iteracion, para
no correr el riesgo de que entre que se prueba la condicion una unica vez y
se decide que hacer, la condicion podra cambiar otra vez.
Por otro lado, la notificacion de que se cambio el estado para una condicion
generalmente toma la siguiente forma:
synchronized v o i f changeCondition ( ) {
// . . . Cambia a l g u n v a l o r que s e u s a en l a p r u e b a
// de l a c o n d i c i o n
notifyAll (); // o b i e n n o t i f y ( )
}
Si se usa notifyAll() todos los procesos que estan en estado de espera que eje-
cutaron un wait() se van a despertar, mientras que notify() unicamente despierta
a un hilo de ejecucion. Si los procesos estan esperando distintas condiciones se
debe usar notifyAll(), ya que si se usa notify() se corre el riesgo de que despierte un
proceso que esta esperando otra condicion. En cambio, si todos los procesos estan
esperando la misma condicion y solo uno de ellos puede reiniciar su ejecucion, no
es importante cual de ellos se despierta.
11.5 Comunicacion entre hilos de ejecucion 426
Como sabemos a estas alturas, en los sistemas Unix las impresoras estan conec-
tadas a los usuarios a traves de un servidor de impresion. Cada vez que un usuario
solicita la impresion de algun trabajo, lo que en realidad sucede es que se arma la
solicitud del usuario y se forma en una cola de impresion, que es atendida uno a la
vez. En una estructura de datos tipo cola, el primero que llega es el primero que
es atendido. Por supuesto que el manejo de la cola de impresion se tiene que hacer
en al menos dos hilos de ejecucion: uno que forma a los trabajos que solicitan ser
impresos y otra que los toma de la cola y los imprime. En el listado 11.7 vemos
la implementacion de una cola generica o abstracta (sus elementos son del tipo
Object) usando para ello listas ligadas. Como los elementos son objetos, se puede
meter a la cola cualquier tipo de objeto, ya que todos heredan de Object. Se usa
para la implementacion listas ligadas, ya que siendo generica no hay una idea de
cuantos elementos podran introducirse a la cola. Cuando se introduce algun ele-
mento a la cola en el metodo add, se notifica que la cola ya no esta vaca. Cuando
se intenta sacar a un elemento de la cola, si la cola esta vaca, el metodo take entra
a una espera, que rompe hasta que es notificado de que la cola ya no esta vaca.
Con todas las clases que usa el servidor de impresion ya definidas, en el lista-
do 11.10 mostramos el servidor de impresion propiamente dicho.
Codigo 11.10 Servidor de impresion que corre en un hilo propio de ejecucion
1: import j a v a . u t i l . ;
2:
3: c l a s s P r i n t S e r v e r implements R u n n a b l e {
4: p r i v a t e Queue r e q u e s t s = new Queue ( ) ;
5: public PrintServer () {
6: new Thread ( t h i s ) . s t a r t ( ) ;
7: }
8: public void p r i n t ( PrintJob job ) {
9: r e q u e s t s . add ( j o b ) ;
10: }
11: public void run ( ) {
12: try {
13: for ( ; ; )
14: r e a l P r i n t (( PrintJob ) requests . take ( ) ) ;
15: } catch ( I n t e r r u p t e d E x c e p t i o n e ) {
16: System . o u t . p r i n t l n ( "La impresora quedo fuera de lnea " ) ;
17: System . e x i t ( 0 ) ;
18: }
19: }
20: private void r e a l P r i n t ( PrintJob job ) {
21: System . o u t . p r i n t l n ( " Imprimiendo " + j o b ) ;
22: }
23: }
429 Hilos de ejecucion
Una ejecucion breve de este programa produce se puede ver en la figura 11.4
en la siguiente pagina.
El hilo de control en el que aparece el wait se duerme hasta que sucede una
de cuatro cosas:
11.5 Comunicacion entre hilos de ejecucion 430
Equivalente a wait(0).
Metodos de notificacion:
public f i n a l void n o t i f y A l l ( )
Notifica a todos los procesos que estan en estado de espera que alguna con-
dicion cambio. Despertaran los procesos que puedan readquirir el candado
que estan esperando.
public f i n a l void n o t i f y ( )
Notifica a lo mas a un hilo esperando notificacion, pero pudiera suceder que
el hilo notificado no estuviera esperando por la condicion que cambio de
estado. Esta forma de notificacion solo se debe usar cuando hay seguridad
de quien esta esperando notificacion y por que.
......
tema que del tipo de proceso. En general es aceptable asumir que el sistema le
dara preferencia a los procesos con mas alta prioridad y que esten en estado de
poder ser ejecutados, pero no hay ninguna garanta al respecto. La unica manera
de modificar estas polticas de desalojo es mediante la comunicacion de procesos.
La prioridad de un hilo de ejecucion es, en principio, la misma que la del
proceso que lo creo. Esta prioridad se puede cambiar con el metodo public final
void setPriority(int newPriority) que asigna una nueva prioridad al hilo de ejecucion.
Esta prioridad es un valor entero tal que cumple
MIN PRIORITY newPriority MAX PRIORITY
La prioridad de un hilo que se esta ejecutando puede cambiarse en cualquier
momento. En general, aquellas partes que se ejecutan continuamente en un sistema
deben correr con prioridad menor que los que detectan situaciones mas raras,
como la alimentacion de datos. Por ejemplo, cuando un usuario oprime el boton
de cancelar para un proceso, quiere que este termine lo antes posible. Sin embargo,
si se estan ejecutando con la misma prioridad, puede pasar mucho tiempo antes
de que el proceso que se encarga de cancelar tome el control.
En general es preferible usar prioridades cercanas a NORM PRIORITY para
evitar comportamientos extremos en un sistema. Si un proceso tiene la prioridad
mas alta posible, puede suceder que evite que cualquier otro proceso se ejecute,
una situacion que se conoce como hambruna (starvation).
Podemos ver dos ejecuciones con lneas de comandos distintas, una que provoca
el desalojo y la otra no. Sin embargo, en el sistema en que probamos este pequeno
programa, el resultado es el mismo. Deberamos ejecutarlo en varios sistemas para
ver si el resultado cambia de alguna manera. Veamos los resultados en la figura 11.5
en la pagina opuesta.
435 Hilos de ejecucion
Siempre que se tienen dos hilos de ejecucion y dos objetos con candados se
puede llegar a una situacion conocida como abrazo mortal, en la cual cada proceso
posee el candado de uno de los objetos y esta esperando adquirir el candado del
otro proceso. Si el objeto X tiene un metodo sincronizado que invoca a un metodo
sincronizado del objeto Y, quien a su vez tiene a un metodo sincronizado invocando
a un metodo sincronizado de X, cada proceso se encontrara esperando a que el
otro termine para poder continuar, y ninguno de los dos va a poder hacerlo.
En el listado 11.14 mostramos una clase Apapachosa en la cual un amigo, al
ser apapachado, insiste en apapachar de regreso a su companero.
Codigo 11.14 Posibilidad de abrazo mortal 1/2
1: c l a s s Apapachosa {
2: p r i v a t e Apapachosa amigo ;
3: p r i v a t e S t r i n g nombre ;
4: p u b l i c Apapachosa ( S t r i n g nombre ) {
5: t h i s . nombre = nombre ;
6: }
7: p u b l i c s y n c h r o n i z e d v o i d apapacha ( ) {
8: System . o u t . p r i n t l n ( Thread . c u r r e n t T h r e a d ( ) . getName ( ) +
9: " en " + nombre + ". apapacha () tratando de " +
10: " invocar a " + amigo . nombre + ". reApapacha ()" ) ;
11: amigo . r e A p a p a c h a ( ) ;
12: }
11.7 Abrazo mortal (deadlock ) 436
En las corridas que hicimos en nuestra maquina siempre pudo Lupe terminar
su intercambio antes de que Juana intentara el suyo. Sin embargo, pudiera suceder
437 Hilos de ejecucion
que en otro sistema, o si la JVM tuviera que atender a mas hilos de ejecucion, ese
programita bloqueara el sistema cayendo en abrazo mortal.
Introduciendo un enunciado wait dentro del metodo reaApapacha conseguimos
que el proceso caiga en un abrazo mortal. Se lanza el hilo de ejecucion con lupe,
pero como tiene que esperar cuando llega a reApapacha(), esto permite que tambien
el hilo de ejecucion con juana se inicie. Esto hace que ambos procesos se queden
esperando a que el otro libere el candado del objeto, pero para liberarlo tienen
que terminar la ejecucion de reApapacha, lo ninguno de los dos puede hacer cada
uno esta esperando a que termine el otro. La aplicacion modificada se muestra
en el listado 11.15 y en la figura 11.7 en la siguiente pagina se muestra como se
queda pasmado el programa, sin avanzar ni terminar, hasta que se teclea un C.
Como se puede ver en las lneas 22, 23 y 26, lo unico que se le agrego a esta clase
es que una vez dentro del metodo reApapacha espere 3 milisegundos, liberando el
candado. Este es un tiempo suficiente para que el otro hilo de ejecucion se apodere
del candado. Aca es cuando se da el abrazo mortal, porque el primer hilo no puede
terminar ya que esta esperando a que se libere el candado, pero el segundo hilo
tampoco puede continuar porque el primero no ha terminado.
elisa@lambda ...ICC1/progs/threads %
Si bien nos costo trabajo lograr el abrazo mortal, en un entorno donde se estan
ejecutando multiples aplicaciones a la vez no podramos predecir que el segundo
hilo de ejecucion no se apoderara del candado del objeto antes de que el primer
hilo lograra llegar a ejecutar el metodo reApapacha, por lo que tendramos que
hacer algo al respecto.
Es responsabilidad del cliente evitar que haya abrazos mortales, asegurandose
del orden en que se ejecutan los distintos metodos y procesos. Esto lo conseguira el
programador usando enunciados wait, yield, notify, notifyAll y sincronizando alre-
dedor de uno o mas objetos, para conseguir que un proceso espere siempre a otro.
439 Hilos de ejecucion
vivo: Un metodo esta vivo desde el momento que es iniciado con start() y hasta
que termina su ejecucion.
ejecutable: Na hay ningun obstaculo para que sea ejecutado, pero el procesador
no lo ha atendido.
Un metodo deja de estar vivo, como ya dijimos, por cualquiera de las causas
que siguen:
Es posible que se detecte que algo mal esta sucediendo en cierto proceso y se
desee terminar su ejecucion. Sin embargo, destroy debera ser un ultimo recurso, ya
que si bien se consigue que el proceso termine, pudiera suceder que no se liberen
los candados que posee el proceso en cuestion y se queden en el sistema otros
procesos bloqueados para siempre esperando candados que ya nunca van a poder
ser liberados. Es tan drastico y peligroso este metodo para el sistema en general
que muchas maquinas virtuales han decidido no implementarlo y simplemente
lanzan una excepcion NoSuchMethodError, que termina la ejecucion del hilo. Esto
sucede si modificamos el metodo apapacha como se muestra en el listado 11.16 en
la siguiente pagina.
11.8 Como se termina la ejecucion de un proceso 440
// En e l h i l o 1
thread2 . i n t e r r u p t ()
desde un metodo distinto que el que se desea interrumpir. En el metodo por in-
terrumpir deberemos tener una iteracion que en cada vuelta este preguntando si
ha sido o no interrumpido:
// En e l h i l o 2
while ( ! i n t e r r u p t e d ( ) ) {
// haz e l t r a b a j o p l a n e a d o
}
Una de las razones que puede tener un programa para lanzar un proceso para-
lelo es la necesidad de hacer calculos complejos, que pudieran hacerse simultanea-
mente. Sin embargo, muchas veces no se sabe cual de los dos calculos se va a
tardar mas, por lo que el proceso principal tendra que esperar a que termine, en
su caso, el proceso que lanzo antes de poder utilizar los resultados del mismo.
Java tiene el metodo join() de la clase Thread que espera a que el proceso con
el que se invoca el metodo termine antes de que se proceda a ejecutar la siguiente
lnea del programa. Veamos en los listados 11.19 y 11.20 en la pagina opuesta un
ejemplo en el quesupuestamente se desean hacer dos calculos que pueden llevarse
a cabo de manera simultanea. El metodo principal (main) crea un proceso paralelo
para que se efectue uno de los calculos mientras el ejecuta el otro. Una vez que
termina de ejecutar su propio calculo, se sienta a esperar hasta que el proceso
que lanzo termine.
vez hecha la tarea que se poda realizar de manera simultanea, se espera para
garantizar que el coproceso termino. Si esto no se hace as pudiera suceder que
el valor que se desea calcule el coproceso no estuviera listo al terminar el proceso
principal con su propio trabajo.
Que un proceso no este ejecutandose no quiere decir que el proceso no este vivo.
Un proceso esta vivo si esta en espera, ya sea de alguna notificacion, de que
transcurra algun tiempo, o de que la JVM continue ejecutandolo. Por lo tanto, aun
cuando un proceso sea interrumpido por alguna de las condiciones que acabamos
de mencionar, el proceso seguira vivo mientras no termine su ejecucion.
Nos surge una pregunta natural respecto a la relacion que existe entre los
procesos lanzados desde una aplicacion, y el proceso que corresponde a la aplica-
cion misma: que pasa si el proceso principal termina antes de que terminen los
procesos que fueron lanzados por el?
La respuesta es mas simple de lo que se piensa: el proceso principal no termina
hasta que hayan terminado todos los procesos que fueron lanzados por el. Bajo
terminar nos referimos a dejar de estar vivo, no forzosamente a estar ejecutando
algo. Esta situacion, pensandolo bien, es lo natural. El proceso principal es aquel
cuyo metodo main se invoco. El resto de los procesos fueron invocados utilizando
start(). Esa es la unica diferencia que hay entre ellos. Al lanzar procesos se genera
una estructura como de cacto, donde cada proceso tiene su stack de ejecucion, que
es una continuacion del stack de ejecucion del proceso que lo lanzo. Por lo tanto,
447 Hilos de ejecucion
no puede terminar un proceso hasta en tanto todos los procesos que dependen de
el (que montaron su stack de ejecucion encima) hayan terminado.
En Java hay otro tipo de procesos llamados demonios. Un demonio es un
proceso lanzado desde otro y cuya tarea es realizar acciones que no forzosamente
tienen mucho que ver con el proceso que los lanzo. En el caso de los demonios,
cuando termina el proceso que los lanzo, en ese momento y abruptamente se
interrumpen todos los demonios lanzados por ese proceso. Es como si aplicaramos
un destroy() a todos los demonios lanzados por el proceso que termina.
Resumiendo, hay dos tipos de procesos que se pueden lanzar desde otro proceso
cualquiera: los hilos de ejecucion normales y los demonios. El proceso lanzador
no puede terminar hasta en tanto los hilos de ejecucion iniciados por el no ter-
minen, mientras que los demonios se suspenden abruptamente cuando el proceso
que los lanzo termina.
Un proceso se puede hacer demonio simplemente aplicandole el metodo setDae-
mon(true) al hilo de ejecucion antes de que este inicie su ejecucion. Los procesos
iniciados por un demonio son, a su vez, demonios. Veamos un ejemplo de la dife-
rencia entre procesos normales y demonios en el listado 11.22.
Para ver los distintos efectos que tiene el que los procesos lanzados sean o
no demonios, vamos a ejecutar la clase Daemon como esta. La teora es que al
terminar de ejecutarse el metodo main de esta clase, los demonios suspenderan
tambien su funcionamiento. Y en efecto as es. La ejecucion la podemos ver en la
figura 11.10 en la pagina opuesta.
Mostramos dos ejecuciones de la misma aplicacion. En la primera ejecucion,
se alcanzo a lanzar 5 demonios antes de que el proceso principal terminara. En la
segunda ejecucion, unicamente se alcanzaron a lanzar 2 demonios. El numero de
demonios que se logre lanzar dependera de las polticas de atencion de la JVM.
Como el proceso Daemon es, a su vez, un demonio, en cuanto llega el proceso
principal al final, se suspende abruptamente la ejecucion de todos los demonios
que se hayan iniciado desde el, o desde procesos o demonios iniciados por el.
Si cambiamos la lnea 8: de esta aplicacion para que Daemon sea un proceso
comun y corriente con la lnea
8: setDaemon ( f a l s e ) ;
entre las lneas 37: y 38: de la clase DaemonSpawn, la ejecucion de Daemon tiene
que terminar antes de que la aplicacion pueda hacerlo, por lo que se alcanzan a
lanzar todos los demonios. Como no hay ninguna manera de que termine el proceso
Daemon, tambien el proceso principal nunca termina, hasta que tecleamos ctrl-C.
Si en las lneas 18: y 19: de la clase Daemon quitamos el ciclo y unicamente tenemos
yield(), el proceso termina normalmente en el momento en que termina el proceso
de la clase Daemon. Veamos la salida en la figura 11.11 en la siguiente pagina.
Dependiendo de la programacion particular que de la JVM, podra suceder
que alguno de los hilos de ejecucion causara una excepcion, o que al suspenderse
alguno de los demonios abruptamente lanzara una excepcion. Esto resulta en
que ejecuciones de la misma aplicacion puedan dar distintos resultados cuando
estamos trabajando con hilos de ejecucion, sobre todo si no se ejerce ninguna
sincronizacion, como es el caso del ejemplo con el que estamos trabajando.
Java proporciona dos metodos a utilizar cuando hay problemas con la ejecucion
de coprocesos. Estos son:
11.11 Otros temas relacionados con hilos de ejecucion 450
Entre los temas que ya no veremos en este captulo por considerarlos mas
apropiados para cursos posteriores, podemos mencionar:
Esperamos haber proporcionado una vision del potencial que tienen los hilos
de ejecucion en Java. Una vez que esten realizando programacion en serio, y en el
ambiente actual de procesos en red, tendran que usarlos.
Bibliografa
[1] Ken Arnold and James Gosling. The Java Programming Language Third Edi-
tion. Addison-Wesley, 2001.
[2] Jose Galaviz Casas. Elogio a la pereza. Vnculos Matematicos, Num. 8, 2001.
[3] Sun Corporation. The source for java technology. web page.
[6] Canek Pelaez and Elisa Viso. Practicas para el curso de Introduccion a Cien-
cias de la Computacion I. Vnculos Matematicos, por publicarse, 2002.
[8] Phil Sully. Modeling the world with objects. Prentice hall, 1993.