Int Ciencias de La Comp PDF
Int Ciencias de La Comp PDF
Int Ciencias de La Comp PDF
INTRODUCCIÓN
A LAS CIENCIAS
DE LA COMPUTACIÓN
CON JAVA
ISBN: 978-970-32-4268-9
1. Introducción 1
1.1. Conceptos generales . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Historia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Sistemas numéricos . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4. La arquitectura de von Neumann . . . . . . . . . . . . . . . . . . . 13
1.5. Ejecución de programas . . . . . . . . . . . . . . . . . . . . . . . . 25
1.6. Caracterı́sticas de Java . . . . . . . . . . . . . . . . . . . . . . . . . 27
3. Clases y objetos 55
3.1. Tarjetas de responsabilidades . . . . . . . . . . . . . . . . . . . . . 55
3.2. Programación en Java . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.3. Expresiones en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6. Herencia 167
6.1. Extensión de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
6.2. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.3. Aspectos principales de la herencia . . . . . . . . . . . . . . . . . . 189
iv Índice general
a todo lo que tiene que ver con computación, nosotros lo entendemos más bien
como refiriéndose a aquellos aspectos de la computación que tienen que ver con la
administración de la información (sistemas de información, bases de datos, etc.).
Al igual que la programación, la informática la podemos considerar contenida
propiamente en la computación.
El término cibernética es un término forjado por los soviéticos en los años
cincuenta. Sus raı́ces vienen de combinar aspectos biológicos de los seres vivos
con ingenierı́a mecánica, como es el caso de los robots, la percepción remota, la
simulación de funciones del cuerpo, etc. A pesar de que se utiliza muchas veces en
un sentido más general, no lo haremos ası́ en estas notas.
Definición 1.1 Un algoritmo es un método de solución para un problema que cumple con:
1. Trabaja a partir de 0 o más datos (entrada).
2. Produce al menos un resultado (salida).
3. Está especificado mediante un número finito de pasos (finitud).
4. Cada paso es susceptible de ser realizado por una persona con papel y lápiz
(definición).
5. El seguir el algoritmo (la ejecución del algoritmo) lleva un tiempo finito
(terminación).
Estamos entonces preocupados en ciencias de la computación por resolver pro-
blemas; pero no cualquier problema, sino únicamente aquéllos para los que po-
damos proporcionar un método de solución que sea un algoritmo – más adelante
en la carrera demostrarán ustedes que hay más problemas que soluciones, ya no
digamos soluciones algorı́tmicas.
La segunda parte importante de nuestra disciplina es la implementación de
algoritmos. Con esto queremos decir el poder llevar a cabo un algoritmo dado (o
diseñado) de manera automática, en la medida de lo posible.
1.2 Historia 3
La computación es una disciplina mucho muy antigua. Tiene dos raı́ces fun-
damentales:
tiguo. También los mayas tenı́an un sı́mbolo asociado al cero. Este concepto es
muy importante para poder utilizar notación posicional. La notación posicional
es lo que usamos hoy en dı́a, y consiste en que cada sı́mbolo tiene dos valores
asociados: el peso y la posición. Por ejemplo, el número 327.15 se puede presentar
de la siguiente manera:
EJEMPLO 1.3.2
II. Los ceros antes del primer dı́gito distinto de cero, desde la izquierda, no apor-
tan nada al número.
III. Los ceros a la derecha del punto decimal y antes de un dı́gito distinto de cero
sı́ cuentan. No es lo mismo .75 que .00075.
IV. Los ceros a la derecha del último dı́gito distinto de cero después del punto
decimal no cuentan.
8 Introducción
VI. Cada dı́gito aporta su valor especı́fico multiplicado por el peso de la posición
que ocupa.
Sabemos todos trabajar en otras bases para la notación posicional. Por ejem-
plo, base 8 (mejor conocida como octal ) serı́a de la siguiente manera:
EJEMPLO 1.3.3
413708 4 1 3 7 |. 214310
BA7C16 11 10 7 12 |. 4774010
Como se puede ver, tratándose de números enteros, es fácil pasar de una base
cualquiera a base 10, simplemente mediante la fórmula
0̧
num10 di bi
i n
Trabajemos con los dos números en base 10 para corroborar que el algoritmo
trabajó 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 después 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 último es sencillo, pues el residuo es, forzosamente, un número entre 0 y b.
Cuando una de las bases es potencia de la otra, el pasar de una base a la otra es
todavı́a más sencillo. Por ejemplo, si queremos pasar de base 8 a base 2 (binario),
observamos que 8 23 . Esto nos indica que cada posición octal se convertirá a
exactamente a tres posiciones binarias. Lo único que tenemos que hacer es, cada
dı́gito octal, pasarlo a su representación 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 dı́gito base 16 cuatro dı́gitos base 2.
El proceso inverso, para pasar de base 2, por ejemplo, a base 16, como 16 24
deberemos tomar 4 dı́gitos binarios por cada dı́gito hexadecimal:
Las computadoras actuales son, en su inmensa mayorı́a, digitales, esto es, que
representan su información de manera discreta, con dı́gitos. Operan en base 2
12 Introducción
¿Cuáles son los distintos elementos que requerimos para poder implementar un
algoritmo en una computadora digital?
¿Cómo se representan en binario esos distintos elementos?
Procesador
central
Unidad de control
Unidad aritmética
Unidad lógica
Cache
comparar y decidir si un número es mayor que otro, etc. En realidad son instruc-
ciones que hacen muy pocas cosas y relativamente sencillas. Recuérdese que se
hace todo en sistema binario.
Lenguajes de programación
(4)
Programa
objeto
(binario)
(3)
Unidad
Datos para de control
el programa (5)
Programa a
ejecutar Resultados
Programa (2) MEMORIA (6)
ensamblador
(binario) (1)
16 Introducción
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
dı́as.
Estos lenguajes tenı́an un formato también más o menos estricto, en el sentido
de que las columnas de las tarjetas perforadas estaban perfectamente asignadas,
y cada elemento del lenguaje tenı́a una posición, como en lenguaje ensamblador.
El proceso por el que tiene que pasar un programa en alguno de estos lenguajes
de programación 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 máquina (por eso el calificativo de
“alto nivel”, alto nivel de información por enunciado), que es el único que puede
ser ejecutado por la computadora.
Hacia finales de los años 50 se diseñó un lenguaje, ALGOL – ALGorithmic
Oriented Language – que resultó ser el modelo de desarrollo de prácticamente
todos los lenguajes orientados a algoritmos de hoy en dı́a, como Pascal, C, C++,
Java y muchos más. No se pretende ser exhaustivo en la lista de lenguajes, bas-
te mencionar que también en los años 50 surgió el lenguaje LISP, orientado a
inteligencia artificial; en los años 60 surgió BASIC, orientado a hacer más fácil
el acercamiento a las computadoras de personas no forzosamente con anteceden-
tes cientı́ficos. En los años 60 surgió el primer lenguaje que se puede considerar
orientado a objetos, SIMULA, que era una extensión de ALGOL. En los aproxi-
madamente 50 años que tienen en uso las computadoras, se han diseñado y usado
más de 1,000 lenguajes de programación, por lo que pretender mencionar siquiera
a los más importantes es una tarea titánica, y no el objetivo de estas notas.
18 Introducción
Representación de la información
Carácteres
Números enteros
sin embargo, que el resultado no sea válido. Por ejemplo, si sumamos dos números
positivos y el resultado es mayor que la capacidad, tendremos acarreo sobre el
bit 15, dando aparentemente un número negativo. Sabemos que el resultado es
inválido 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 números 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
22 Introducción
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 máximo entero positivo
(una palabra con 0 en el bit 15 y 1’s en el resto de los bits) el resultado resulta
ser un número negativo, aquel que tiene 1’s en todas las posiciones de la palabra
– ver figura 1.7.
Números 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 notación es que es muy sencillo hacer
operaciones aritméticas, 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 números
que tengan muchos dı́gitos en la fracción, o muy pocos.
Como podemos ver del ejemplo anterior, nos ponemos de acuerdo en cuántos
dı́gitos van a estar a la izquierda del punto decimal, y todos los números
reales los representamos con ese número de enteros. A continuación, damos
una potencia de 10 por la que hay que multiplicar el número, para obtener
el número que deseamos.
Una abreviatura de esta notación serı́a escribiendo los dos números anterio-
res de la siguiente forma:
1,32456E6 1324560
1,32456E 6 ,00000132456
1,32456E3 1324,56
24 Introducción
Definición 1.4 Un intérprete 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 instrucción le indique que pare,
o bien tenga un error fatal en la ejecución.
A los intérpretes se les conoce también como máquinas virtuales, porque una
vez que están cargados en una máquina, se comportan como si fueran otra compu-
tadora, aquella cuyo lenguaje de máquina es el que se está traduciendo y ejecu-
tando.
Especificación
Análisis y Diseño
Implementación
Validación
Mantenimiento
Refinamiento y extensión
d) Alta cohesión, que se refiere al hecho de que todo lo que esté relacionado
(funciones, datos) se encuentren juntos, para que sean fáciles de localizar,
entender y modificar.
Especificación
Una buena especificación, sea formal o no, hace hincapié, antes que nada, en
cuál es el resultado que se desea obtener. Este resultado puede tomar muy distintas
formas. Digamos que el cómputo corresponde a un modelo (la instrumentación o
implementación 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 serı́a:
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
34 El proceso del software
valor de K2 (el valor que tiene y al empezar) y a y le damos el valor que tenı́a x.
Podemos representar este proceso de la siguiente manera:
Análisis y diseño
Podemos decir, sin temor a equivocarnos, que la etapa de diseño es la más
importante del proceso. Si ésta se lleva a cabo adecuadamente, las otras etapas
se simplifican notoriamente. La parte difı́cil en la elaboración de un programa
de computadora es el análisis del problema (definir exactamente qué se desea) y
el diseño de la solución (plantear cómo 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 análisis y diseño. Dado que la experiencia
se debe adquirir, es conveniente contar con una metodologı́a que nos permita ir
construyendo esa experiencia.
2.1 ¿Qué es la programación? 35
Ası́ como hay diversidad en los seres humanos, ası́ hay maneras distintas de
analizar y resolver un problema. En esa búsqueda por “automatizar” o matemati-
zar el proceso de razonamiento, se buscan métodos o metodologı́as que nos lleven
desde la especificación de un problema hasta su mantenimiento, de la mejor ma-
nera posible. El principio fundamental que se sigue para analizar y diseñar una
solución es el de divide y vencerás, reconociendo que si un problema resulta de-
masiado complejo para que lo ataquemos, debemos partirlo en varios problemas
de menor magnitud. Podemos reconocer dos vertientes importantes en cuanto a
las maneras de dividir:
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, tendrán
que trabajar con programas ya hechos y extenderlos, lo que tiene que ver con el
mantenimiento.
36 El proceso del software
Quedan agrupados en una misma clase aquellos objetos que presentan las
mismas caracterı́sticas y funcionan de la misma manera.
En el diseño 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 patrón de comportamiento: nos dicen qué y
cómo se vale hacer con los datos de la clase. Es como el guión 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 propósitos similares, por ejemplo), deberemos abstraer, encontrando carac-
terı́sticas 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)
Métodos (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 geométricos
Herencia (inheritance)
Polimorfismo (polymorphism)
Dado que tenemos la posibilidad de agrupar a las clases por “familias”, que-
remos la posibilidad de que, dependiendo de cuál de los herederos se trate, una
función determinada se lleve a cabo de manera “personal” a la clase. Por ejemplo,
si tuviéramos una familia, la función de arreglarse se deberı́a llevar a cabo de
distinta manera para la abuela que para la nieta. Pero la función se llama igual:
arreglarse. De la misma manera, en orientación a objetos, conforme definimos he-
rencia podemos modificar el comportamiento de un cierto método, para que tome
en consideración los atributos adicionales de la clase que hereda. A esto, el que
el mismo nombre de método 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 solución del problema y la
relación entre ellas; asignar a cada clase las responsabilidades correspondien-
tes en cuanto a información y proceso (atributos y métodos respectivamen-
te); identificar las interacciones necesarias entre los objetos de las distintas
clases (diseño).
3. Codificar el diseño en un lenguaje de programación, en este caso Java.
4. Compilar y depurar el programa.
5. Probarlo con distintos juegos de datos.
De lo que hemos visto, la parte más importante del proceso va a ser el análisis
y el diseño, ası́ que vamos a hablar de él con más 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 jerarquı́a
entre las clases que definimos. Esta jerarquı́a establece las relaciones de herencia
entre las clases. Dependiendo de la complejidad del diseño, 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 clasificación
y estructura. Las clases proporcionan mecanismos para estructurar la aplicación
de tal manera que sea reutilizable.
Si bien suena sencillo eso de “determina las clases en tu aplicación”, en la vida
real éste es un proceso no tan directo como pudiera parecer. Veamos con un poco
de más detalle estos subprocesos:
¿Qué es lo que el objeto tiene que saber de tal manera que pueda
realizar las tareas que tiene encomendadas?
¿Cuáles son los pasos, en la dirección del objetivo final del sistema, en
los que participa el objeto?
¿Con qué otros objetos tiene que colaborar para poder cumplir con sus
responsabilidades (a quién le puede delegar)?
¿Qué objetos en el sistema poseen información que necesita o sabe como
llevar a cabo alguna operación que requiere?
¿En qué consiste exactamente la colaboración entre objetos?
Es importante tener varios objetos que colaboran entre sı́. De otra manera,
el programa (o sistema) va a consistir de un objeto enorme que hace todo.
En este paso, aunque no lo hemos mencionado, tenemos que involucrarnos ya
con el cómo cumple cada objeto con su responsabilidad, aunque no todavı́a a
mucho nivel de detalle. Un aspecto muy importante es el determinar dónde
es que se inician las acciones. En el esquema de cliente/servidor del que
hemos estado hablando, en este punto se toman las decisiones de si el objeto
subcontrata parte de su proceso, si es subcontratado por otro objeto, etc.
Es importante recalcar que mientras en la vida real algunos de los objetos
tienen habilidad para iniciar por sı́ mismos su trabajo, en el mundo de la
programación orientada a objetos esto no es ası́: se requiere de un agente
que inicie la acción, que ponga en movimiento al sistema.
Es muy importante en esta etapa describir con mucha precisión cuáles son
las entradas (input) y salidas (output) de cada objeto y la manera que cada
objeto va a tener de reaccionar frente a una solicitud. En teorı́a, un objeto
siempre da una respuesta cuando se le solicita un servicio. Esta respuesta
puede ser
Como ya mencionamos antes, para diseñar cada uno de los métodos o funciones
propias de un sistema debemos recurrir a otro tipo de análisis que el orientado
a objetos. Esto se debe fundamentalmente a que dentro de un método debemos
llevar a cabo un algoritmo que nos lleve desde un estado inicial a otro final, pero
donde no existe colaboración o responsabilidades, sino simplemente una serie de
tareas a ejecutar en un cierto orden.
Tenemos cuatro maneras de organizar a los enunciados o lı́neas de un algoritmo:
Secuencial, donde la ejecución prosigue, en orden, con cada lı́nea, una después
de la otra y siguiendo la organización fı́sica. Por ejemplo:
1: pone 1 en x
2: suma 2 a x
3: copia x a y
2: Repite 10 veces:
Toda solución algorı́tmica que demos, sobre todo si seguimos un diseño es-
tructurado, deberá estar dado en términos de estas estructuras de control. El
problema central en diseño consiste en decidir cuáles de estas estructuras utili-
zar, cómo agrupar enunciados y como organizar, en general los enunciados del
programa.
Una parte importante de todo tipo de diseño es la notación que se utiliza
para expresar los resultados o modelos. Al describir las estructuras de control
utilizamos lo que se conoce como pseudocódigo, pues escribimos en un lenguaje
parecido al español las acciones a realizar. Esta notación, si bien es clara, resulta
fácil una vez que tenemos definidas ya nuestras estructuras de control, pero no
nos ayuda realmente a diseñar. Para diseñar utilizaremos lo que se conoce como
la metodologı́a de Warnier-Orr, cuya caracterı́stica principal es que es un diseño
controlado por los datos, i.e. que las estructuras de control están dadas por las
estructuras que guardan los datos. Además, el diseño parte siempre desde el estado
final del problema (qué es lo que queremos obtener) y va definiendo pequeños 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 notación que utiliza el método de Warnier-Orr, y dado
que es un método dirigido por los datos, veamos la notación para representar
48 El proceso del software
grupos de datos, que al igual que los enunciados, tienen las mismas 4 formas
de organizarse: secuencial, iterativa, condicional o recursiva . Por supuesto que
también debemos denotar la jerarquı́a de la información, donde este concepto se
refiere a la relación que guarda la información entre sı́. Representa a los datos
con una notación muy parecida a la de teorı́a de conjuntos, utilizando llaves para
denotar a los conjuntos de datos (o enunciados). Sin embargo, cada conjunto puede
ser “refinado” con una “ampliación” de su descripción, 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
método de Warnier-Orr representa estos conceptos se explica a continuación:
Jerarquı́a
Abre llaves. El “nombre” de la llave es el objeto de mayor jerarquı́a 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.
'
'
'
&descr 2
“nombre”
'
'
'
'
...
'
'
%descr 3
2
'
'. . .
pMientras te diganq
% descrn
À
Condicional: Por último, para denotar selección se utiliza el sı́mbolo del o
exclusivo , que aparece entre una pareja de opciones – ver figura 2.5.
Veamos cómo quedarı́an representados los pequeños procesos que dimos arriba
en términos de la notación de Warnier-Orr en las figuras 2.6 y 2.7, donde el sı́mbolo
“Д significa “obtén el valor de lo que está a la derecha y coloca ese valor en la
variable que está a la izquierda”.
50 El proceso del software
N ombre del
'
& '
& ...
P roblema '
'
P roceso
'
%. . .
'
' #
'
'
%.F inal
Entregar resultados
Atar cabos sueltos
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 página
anterior. En el primer momento, todo lo que sabemos es qué es lo que tenemos de
datos (n) y qué 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 último que vamos a hacer es proporcionar o escribir la
lista de factores primos del número n. Esto corresponde al final de nuestro proceso.
?
Sabemos, porque algo de matemáticas 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. También sabemos, porque corresponde a
nuestros datos, que al principio de nuestro proceso deberemos obtener n. Con esta
información, podemos producir el diagrama inicial que, como ya mencionamos, se
encuentra en la figura 2.10 en la página anterior.
'
' & #
'k es factor de n 'esP rimo true
Verifica si k es factor '
Agrega k a
& '
' À la lista
' '
'
pk 2, . . . , ?nq '
primo de n
' '
' !
'
' '
' esP rimo true
'
' '
' ! ∅
'
' %.F inal ∅
'
' À
'
'
'
' !
'
' !
k es factor de n ∅
'
%.F inal ∅
2.3 Diseño estructurado 53
$ !
$n Ð el número
'
' .P rincipio
$ a f actorizar !
'
'
'
'
'
'
'
'
'
'
.P rincipio
$ esP rimo Ð#true
'
' '
' '
' '
' i es factor esP rimo Ð f also
'
' '
' '
' '
' iÐn
'
' '
' '
'
V erif ica si
'
& À
de k
'
' '
' '
&
i divide a k
'
' 'k es factor pi ?2,
V erif ica si k '
'
'
' #
' ' ' . . . , kq '
& divide a '
' &
de n '
' ' i es factor ∅
% de k
'
' !
OF P
'
' p
n
k 2...
'
' '
' esP
À
rimo true Agrega k a la lista
'
' ? '
' '
'
' . . . 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
lı́mite. El valor concreto de cada manecilla es suficientemente simple como para
que se lleve en un entero, lo mismo que el lı́mite; excepto que este último debe ser
constante porque una vez que se fije ya no deberá cambiar. Las horas y los minutos
3.1 Tarjetas de responsabilidades 57
Manecilla
Datos: valor
lı́mite
Funciones: constructores: Poner el lı́mite
incrementa
pon valor
muestra
Reloj
Datos: horas Una manecilla con lı́mite 12
minutos Una manecilla con lı́mite 60
Funciones: constructores
incrementa
pon valor
muestra
1
Usaremos la palabra get en estos casos, en lugar del término en español da o dame, ya que
en Java existe la convención de que los métodos de acceso a los atributos de una clase sean de
la forma get seguidos del identificador del atributo empezado con mayúscula. Similarmente en
los métodos de modificación o actualización de los valores de un atributo la convención es usar
set seguido del nombre del atributo escrito con mayúscula.
58 Clases y objetos
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 página opuesta para la clase Manecilla.
En forma esquemática las tarjetas quedan como se muestran en las figuras 3.4 y 3.5
en la página opuesta.
Sintaxis:
x declaración de interfazy ::=
xaccesoy interface xidentificadory {
xencabezado de métodoy;
(xencabezado de métodoy;)*
}
Semántica:
Se declara una interfaz en un archivo. El nombre del archivo debe tener
como extensión .java y coincide con el nombre que se le dé a la interfaz.
Una interfaz, en general, no tiene declaraciones de atributos, sino única-
mente de métodos, de los cuáles únicamente se da el encabezado. Los en-
cabezados de los distintos métodos se separan entre sı́ por un ; (punto y
coma). El que únicamente contenga encabezados se debe a que una interfaz
no dice cómo se hacen las cosas, sino únicamente cuáles cosas sabe hacer.
Sintaxis:
xacceso y ::= public | private | protected | package | ∅
Semántica:
El acceso a una clase determina quién 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 Sólo la pueden ver las clases que extienden a ésta. No tiene
sentido para clases.
package Sólo la pueden ver clases que se encuentren declaradas en el mismo
subdirectorio (paquete). Es el valor por omisión.
Puede no haber regla de acceso, en cuyo caso el valor por omisión es package.
En el caso de las interfaces, el acceso sólo puede ser de paquete o público, ya
que el concepto de interfaz tiene que ver con anunciar servicios disponibles.
3.2 Programación en Java 65
Sintaxis:
xidentificador y ::= (xletra y | )(xletray | xdı́gitoy | )*
Semántica:
Los identificadores deben ser nemónicos, esto es, que su nombre ayude a la
memoria para recordar qué es lo que representan. No pueden tener blancos
insertados. Algunas reglas no obligatorias (aunque exigidas en este curso)
son:
Clases: Empiezan con mayúscula y consiste de una palabra descriptiva,
como Reloj, Manecilla.
Métodos: Empiezan con minúsculas y se componen de un verbo – da,
calcula, mueve, copia – seguido de uno o más sustantivos. Cada uno
de los sustantivos empieza con mayúscula.
Variables: Nombres sugerentes con minúsculas.
Constantes: Nombres sugerentes con mayúsculas.
Hay que notar que en Java los identificadores pueden tener tantos carácteres
como se desee. El lenguaje, además, distingue entre mayúsculas y minúscu-
las – no es lo mismo carta que Carta.
Una interfaz puede servir de contrato para más de una clase (que se llamen
distinto). Es la clase la que tiene que indicar si es que va a cumplir con algún
contrato, indicando que va a implementar a cierta interfaz.
El acceso a los métodos de una interfaz es siempre público o de paquete. Esto
se debe a que una interfaz anuncia los servicios que da, por lo que no tendrı́a
sentido que los anunciara sin que estuvieran disponibles.
Siempre es conveniente poder escribir comentarios en los programas, para que
nos recuerden en qué estábamos pensando al escribir el código. Tenemos tres tipos
de comentarios:
Empiezan con // y terminan al final de la lı́nea.
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 documentación del
programa.
66 Clases y objetos
Sintaxis:
xdeclaración de clasey ::= xaccesoy class xidentificadory
(∅ |p implements (xidentificadory)+)
(∅ | extends xidentificadory) {
xdeclaracionesy
(∅ | xmétodo mainy)
}
Semántica:
Se declara una clase en un archivo. El nombre del archivo debe tener como
extensión .java y, en general, coincide con el nombre que se le dé a la
clase. Una clase debe tener xdeclaracionesy y puede o no tener xmétodo
mainy. La clase puede o no adoptar una interfaz para implementar. Si lo
hace, lo indica mediante la frase implements e indicando a cuál o cuáles
interfaces implementa. Las xdeclaracionesy corresponden a los ingredientes o
variables y a los métodos que vamos a utilizar. Una xvariabley corresponde a
una localidad (cajita, celda) de memoria donde se va a almacenar un valor.
El xmétodo 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 método. Sin embargo, muchas veces para probar que la clase
funciona se le escribe un método main. En Java todo identificador tiene que
estar declarado para poder ser usado.
terminar con un punto. Después del punto se puede dar una explicación más am-
plia. A continuación deberá aparecer la descripción de los parámetros, cada uno
en al menos un renglón precedido por @param y el nombre del parámetro, con una
breve explicación del papel que juega en el método. Finalmente se procederá a
informar del valor que regresa el método, precedido de @returns y que consiste de
una breve explicación de qué es lo que calcula o modifica el método.
Métodos de acceso
Los métodos 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 método debe tener información 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.
Los tipos que se manejan en Java pueden ser primitivos o de clase. Un tipo
primitivo es aquél cuyas variables no son objetos y son atómicos, esto es, no se
subdividen en otros campos o atributos. En la tabla 3.3 en la siguiente página 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
carácteres. La clase se llama String. Las cadenas (String) son cualquier sucesión
de carácteres, menos el de fin de lı́nea, entre comillas. Los siguientes son objetos
tipo String:
"Estaesuna cadena 123"
""
70 Clases y objetos
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 estándar IEEE 754-1985
double 64 bits de acuerdo al estándar IEEE 754-1985
"a"+"b"+"c" "abc"
"Esta cadena es"+"muy bonita " "Esta cadena esmuy bonita "
Sintaxis:
xParámetrosy::= ∅ |
xparámetroy(, xparámetroy)*
xparámetroy ::= xtipoy xidentificadory
Semántica:
Los parámetros pueden estar ausentes o bien consistir de una lista de
parámetros separados entre sı́ por coma (,). Un parámetro marca lugar
y tipo para la información que se le dé al método. Lo que le interesa al
compilador es la lista de tipos (sin identificadores) para identificar a un
método dado, ya que se permite más de un método con el mismo nombre,
pero con distinta firma.
Por ejemplo, los métodos 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 método que regrese un valor, y tendrı́a entonces la sintaxis de
los métodos de acceso. Como lo que queremos del Reloj es que se muestre, no que
nos diga qué valor tiene, no tenemos ningún método de acceso para esta clase.
Código 3.3 Métodos 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
72 Clases y objetos
Métodos de implementación
Estos métodos son los que dan los servicios. Por ello, el método muestra cuya
firma aparece en el listado 3.4 es de este tipo. Es común que este tipo de métodos
regresen un valor que indiquen algún resultado de lo que hicieron, o bien que
simplemente avisen si pudieron o no hacer lo que se les pidió, regresando un
valor booleano. En el caso de que sea seguro que el método va a poder hacer lo
que se le pide, sin contratiempos ni cortapisas, se indica que no regresa ningún
valor, poniendo en lugar de xtipo yla palabra void. Por ejemplo, el encabezado del
método que muestra la Manecilla queda como se muestra en el listado 3.4. También
en el listado 3.5 mostramos el método de implementación muestra para la interfaz
ServiciosReloj.
Como pueden ver, ninguno de estos dos métodos regresa un valor, ya que
simplemente hace lo que tiene que hacer y ya. Tampoco tienen ningún parámetro,
ya que toda la información que requerirá es el estado del objeto, al que tienen
acceso por ser métodos de la clase.
Métodos de manipulación
Los métodos de manipulación son, como ya mencionamos, aquellos que cam-
bian el estado de un objeto. Generalmente tienen parámetros, pues requieren in-
formación de cómo modificar el estado del objeto. Los métodos que incrementan
y que asignan un valor son de este tipo, aunque el método que incrementa no re-
quiere de parámetro ya que el valor que va a usar es el 1. Muchas veces queremos
que también nos proporcionen alguna información respecto al cambio de estado,
3.2 Programación en Java 73
como pudiera ser un valor anterior o el mismo resultado; también podrı́amos que-
rer saber si el cambio de estado procedió sin problemas. En estos casos el método
tendrá valor de regreso, mientras que si no nos proporciona información será un
método de tipo void. Por ejemplo, el método que incrementa a la Manecilla nos
interesa saber si al incrementar llegó a su lı́mite. Por ello conviene que regrese un
valor de 0 si no llegó al lı́mite, y de 1 si es que llegó (dio toda una vuelta). La
firma de este método se muestra en los listados 3.6 y 3.7.
porque éste es el objetivo principal del programa. Veamos cómo queda lo que lle-
vamos del programa en el listado 3.8 (omitimos los comentarios de JavaDoc para
3.2 Programación en Java 75
ahorrar algo de espacio). Como declaramos que nuestras clases Reloj y Manecilla
implementan, respectivamente, a las interfaces ServiciosReloj y ServiciosManecilla,
estas clases tendrán que proporcionar las implementaciones para los métodos que
listamos en las interfaces correspondientes. El esqueleto construido hasta ahora
se puede ver en el listado 3.8. Como a la clase Manecilla únicamente la vamos a
utilizar desde la clase Reloj no le damos un archivo fuente independiente.
De las cinco variedades de métodos que listamos, nos falta revisar a los métodos
constructores y a los métodos auxiliares, que tienen sentido sólo en el contexto de
la definición de clases.
Métodos auxiliares
Estos métodos 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 parámetros:
depende de para qué se vayan a usar. Dado que el problema que estamos atacando
por el momento es relativamente simple, no se requieren métodos auxiliares para
las clases.
Métodos constructores
Una clase es un patrón (descripción, modelo, plano) para la construcción 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 ( xParámetrosy ) {
ximplementacióny
}
xParámetrosy ::=xparámetroy(, xparámetroy) | ∅
xparámetroy ::=xtipoy xidentificadory
Semántica:
Los constructores de una clase son métodos que consisten en un acceso –
que puede ser cualquiera de los dados anteriormente – seguido del nombre
de la clase y entre paréntesis los xParámetrosy del método. Un parámetro
corresponde a un dato que el método tiene que conocer (o va a modificar).
Cada parámetro deberá tener especificado su tipo. Los nombres dados a ca-
da parámetro pueden ser arbitrarios, aunque se recomienda, como siempre,
que sean nemónicos 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 máximo y v a l o r i n i c i a l ∗/
// xImplementación 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 omisión, sin parámetros, que puede ser in-
vocado, siempre y cuando no se haya declarado ningún constructor para la clase.
Esto es, si se declaró, por ejemplo, un constructor con un parámetro, el construc-
tor sin parámetros ya no está accesible. Por supuesto que el programador puede
declarar un constructor sin parámetros que sustituya al que proporciona Java por
omisión.
Atributos
Sintaxis:
xdeclaración de atributoy ::=xaccesoy xmodificadory xtipo y
xidentificador y(,xidentificadory)*;
xmodificador y ::=final | static | ∅
xtipo y ::=xtipo primitivoy | xidentificador de clase y
Semántica:
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. También 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 último que se da es el identificador. Se puede asociar una lista de
identificadores separados entre sı́ por una coma, con una combinación de
acceso, modificador y tipo, y todas las variables de la lista tendrán las mis-
mas caracterı́sticas. Al declararse un atributo, el sistema de la máquina 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 posición en el heap donde quedará el objeto que se asocie a esa
variable3 .
Las declaraciones de las lı́neas 5:, 7:, 13: y 15: son declaraciones de atributos del
tipo que precede al identificador. En la lı́nea 5: se están declarando dos atributos
de tipo Manecilla y acceso privado, mientras que en la lı́nea 13: se está declarando
un atributo de tipo entero y acceso privado. En la lı́nea 15: aparece el modificador
final, que indica que a este atributo, una vez asignado un valor por primera vez,
este valor ya no podrá ser modificado. Siguiendo las reglas de etiqueta de Java,
el identificador tiene únicamente mayúsculas. En el caso de los atributos de tipo
Manecilla, debemos tener claro que nada más estamos declarando un atributo, no
el objeto. Esto quiere decir que cuando se construya el objeto de tipo Manecilla,
la variable horas se referirá a este objeto, esto es, contendrá una referencia a un
objeto de tipo Manecilla. Como los objetos pueden tener muy distintos tamaños
serı́a difı́cil acomodarlos en el espacio de ejecución 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 dirá la dirección del mismo en el heap.
4
El valor de una referencia es una dirección del heap. En esa dirección se encuentra el objeto
construido.
80 Clases y objetos
El método main
El método main corresponde a la colaboración que queremos se dé entre clases.
En él se define la lógica de ejecución. No toda clase tiene un método main, ya que
no toda clase va a definir una ejecución. A veces pudiera ser nada más un recurso
(como es el caso de la clase Manecilla). El sistema operativo (la máquina virtual
de Java) reconoce al método main y si se “invoca” a una clase procede a ejecutar
ese método. El encabezado para este método se encuentra en el listado 3.13.
Sintaxis:
xReferencia a atributo o métodoy::=
(xreferencia de objeto o clasey.) (xid de atributoy |
xinvocación a métodoy)
Semántica:
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 qué objeto
forma parte. También podemos usarlo para identificar a alguna clase que
pertenezca a un paquete. En el caso de un identificador de método, éste
deberá presentarse con los argumentos correspondientes entre paréntesis. la
xreferencia de objetoy puede aparecer en una variable o como resultado de
una función 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 más 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 método de la misma
clase. Adicionalmente, los nombres que tengan acceso público o de paquete son
accesibles también desde fuera de la clase.
Sin embargo, hemos dicho que una clase es nada más 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
éste es el caso, ¿cómo hacemos desde fuera de la clase para saber de cuál objeto
estamos hablando? Muy fácil: 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
82 Clases y objetos
acceder a sus métodos públicos, como por ejemplo incrementa como se muestra en
el listado 3.14.
Es claro que para que se puedan invocar estos métodos desde la clase Reloj
deben tener acceso público o de paquete. También los objetos horas y minutos
tienen que ser conocidos dentro de la clase Reloj.
Sin embargo, cuando estamos escribiendo la implementación de algún método,
al referirnos, por ejemplo, al atributo valor no podemos saber de cuál objeto,
porque el método va a poder ser invocado desde cualquier objeto de esa clase.
Pero estamos asumiendo que se invoca, forzosamente, con algún objeto. Entonces,
para aclarar que es el atributo valor del objeto con el que se está invocando,
identificamos a este objeto con this. Cuando no aparece un identificador de objeto
para calificar a un atributo, dentro de los métodos de la clase se asume entonces
al objeto this. En el código que sigue las dos columnas son equivalentes para
referirnos a un atributo dentro de un método 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 todavı́a manejado nada de cómo hacen los métodos
lo que tienen que hacer. En general un método va a consistir de su encabezado
y una lista de enunciados entre llaves, como se puede ver en la figura 3.15 en la
página anterior.
Las declaraciones
Cuando estamos en la implementación de un método es posible que el método
requiera de objetos o datos primitivos auxiliares dentro del método. Estas varia-
bles auxiliares se tienen que declarar para poder ser usadas. El alcance de estas
variables es únicamente entre las llaves que corresponden al método. Ninguna va-
riable se puede llamar igual que alguno de los parámetros del método, ya que si
ası́ fuera, como los parámetros son locales se estarı́a repitiendo un identificador en
el mismo alcance. La sintaxis para una declaración se puede ver en la figura 3.16.
Figura 3.16 Declaración de variables locales
Sintaxis:
xdeclaración de variable localy ::= xtipoy xLista de identificadoresy;
Semántica:
La declaración de variables locales es muy similar a la de parámetros 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 sucesión 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 método está marcado para regresar un valor, en cuyo caso el tipo
del método es distinto de void, el método debe tener entre sus enunciados a return
xexpresióny. En el punto donde este enunciado aparezca, el método suspende su
funcionamiento y regresa el valor de la xexpresióny al punto donde apareció su
invocación. Cuando un método tiene tipo void, vamos a utilizar el enunciado re-
turn para salir del método justo en el punto donde aparezca este enunciado. Por
ejemplo, los métodos de acceso lo único que hacen es regresar el valor del atributo,
3.2 Programación en Java 87
El enunciado de asignación
Sintaxis:
xenunciado de asignacióny::=| xvariabley = xexpresióny
xexpresióny ::= xvariabley| xconstantey
| new xconstructory | ( xexpresióny )
| xoperador unarioy xexpresióny
| xexpresióny xoperador binario y xexpresióny
| xmétodo que regresa valor y
| xenunciado de asignacióny
Semántica:
Podemos hablar de que el xenunciado de asignacióny consiste de dos partes,
lo que se encuentra a la izquierda de la asignación (=) 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 expresión de la derecha se evalúa (se ejecuta) y el
valor que se obtiene se coloca en la variable de la izquierda. Si la expresión
no es del mismo tipo que la variable, se presenta un error de sintaxis. Toda
expresión tiene que regresar un valor.
88 Clases y objetos
Sintaxis:
xconstrucción de objetoy ::=new xinvocación método constructory
Semántica:
Para construir un objeto se utiliza el operador new y se escribe a conti-
nuación de él (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:
xinvocación de métodoy ::=xnombre del métodoy(xArgumentosy )
xArgumentosy ::=xargumentoy (,xargumentoy)* | ∅
xargumentoy ::=¡expresión¿
Semántica:
Los xArgumentosy tienen que coincidir en número, tipo y orden con los
xParámetrosy que aparecen en la declaración del método. La sintaxis indi-
ca que si la declaración no tiene parámetros, la invocación no debe tener
argumentos.
Si el método regresa algún valor, entonces la invocación podrá aparecer
en una expresión. Si su tipo es void tendrá que aparecer como enunciado
simple.
3.2 Programación en Java 89
El operador new nos regresa una dirección en el heap donde quedó 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 implementación de estos constructores se
pueden ver en el listado 3.19.
Una expresión en Java es cualquier enunciado que nos regresa un valor. Por
ejemplo, new Manecilla(limH) es una expresión, 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 numérico entonces tenemos una expre-
sión aritmética; si regresan falso o verdadero tenemos una expresión booleana; si
regresan una cadena de carácteres tenemos una expresión tipo String. También
podemos hacer que las expresiones se evalúen a un objeto de determinada clase.
Cuando escribimos una expresión aritmética tenemos, en general, dos dimen-
siones en las cuales movernos: una vertical y otra horizontal. Por ejemplo, en la
fórmula que da la solución de la ecuación 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 división tiene mayor prece-
dencia (se hace antes) que la suma, por lo que en ausencia de paréntesis se
evalúa como en el segundo ejemplo. Con los paréntesis estamos obligando a
que primero se evalúe la suma, para que pase a formar el segundo operando
de la división, como se muestra en el primer ejemplo.
4ac 4ac
3px 2y q 3 px 2 y q
Finalmente, son pocos los lenguajes de programación que tienen como opera-
dor la exponenciación, por lo que expresiones como b2 se tendrán que expresar en
términos de la multiplicación de b por sı́ misma, o bien usar algún método (como
el que usamos para raı́z cuadrada) que proporcione el lenguaje o alguna de sus
bibliotecas. La “famosa” fórmula para la solución de una ecuación de segundo
grado quedarı́a entonces
?2
x1 b b
2a
4ac x1 pb M ath.sqrtppb bq p4 a cqqq{p2 aq
Hay que recordar que estos métodos, por ser constructores, de hecho regresan
3.3 Expresiones en Java 95
Tenemos ya las clases terminadas. Ahora tendrı́amos que tener un usuario que
“comprara” uno de nuestros relojes. Hagamos una clase cuya única función sea
probar el Reloj. La llamaremos UsoReloj. Se encuentra en el listado 3.27.
/∗ M a n i p u l a c i ó 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
98 Clases y objetos
Uno de los ingredientes que más comúnmente vamos a usar en nuestros pro-
gramas son las expresiones. Por ello, dedicaremos este capı́tulo a ellas.
cadenas la sintaxis de Java es mucho más flexible para la creación de cadenas que
de objetos en general y nos permite cualquiera de los siguientes formatos:
II. En una asignación. 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 expresión, 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 mayúsculas una cadena, podrı́amos tener la siguiente sucesión
de enunciados:
Descripción:
Cada objeto del curso consiste del número del grupo (una cadena), la lista de
alumnos y el número de alumnos. La lista de alumnos consiste de alumnos,
donde para cada alumno tenemos su nombre completo, su número de cuenta,
la carrera en la que están 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 métodos que hacen la con-
sulta trabajan a partir de saber la posición relativa del registro que queremos. Sin
embargo, una forma común de interrogar a una base de datos es proporcionándole
información parcial, como pudiera ser alguno de los apellidos, por lo que conviene
agregar un método al que le proporcionamos esta información y nos deberá de-
cir la posición relativa del registro que contiene esa información. Este método lo
podemos ver en el Listado 4.2.
Sin embargo, pudiéramos buscar una porción del registro que se repite más de
una vez, y quisiéramos que al interrogar a la base de datos, ésta nos diera, uno tras
108 Manejo de cadenas y expresiones
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 pasarı́a
del primero. Le agregamos entonces un nuevo parámetro para que la búsqueda
sea a partir de un posición. El encabezado de este método se puede ver en el
Listado 4.3.
private String
lista =
" Aguilar Solı́s Aries Olaf" + "Mate" + " 975412191 " + " aguilarS " +
"CruzCruzGilNoé" + "Comp" + " 990363584 " + " cruzCruz " +
" Garcı́a Villafuerte Israel " + "Comp" + " 025986583 " + " garciaV " +
" Hubard Escalera Alfredo " + "Comp" + " 002762387 " + " hubardE " +
" Tapia Vázquez Rogelio " + "Actu" + " 026393668 " + " tapiaV " ;
Habı́amos comentado que queremos dos constructores, uno que trabaje a partir
de una lista que dé el usuario, y otro que inicie con una lista vacı́a y vaya agregando
4.2 Implementación de una base de datos 113
nombres conforma el usuario los va dando. El código para ambos casos se puede
ver en el listado 4.5.
Código 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 número 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 número de g r u p o
44: ∗ @param g r u p o Número 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-ésimo registro vamos
a elaborar un método, privado, que me dé el carácter en el que empieza el i-ésimo
registro, mediante la fórmula
posición pi 1q T AM REG.
Hay que tomar en cuenta acá 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 método queda
como se puede observar en el listado 4.7.
Los métodos que regresan un campo siguen todos el patrón dado en la figura 4.4
y su implementación se puede ver en el listado 4.8 en la página opuesta.
4.2 Implementación de una base de datos 115
una posición inicial a partir de donde buscar. Le damos al método daPosicion otra
firma que tome en cuenta este parámetro adicional. La programación se encuentra
también en los listados 4.9 en la página opuesta y 4.10.
El único método 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 único relevante es preguntar si el registro que
nos piden existe o no.
De las funciones más comunes a hacer con una lista de un curso es listar toda
la lista completa. Sabemos cuántos 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 podrı́a ser el que se ve en la figura 4.7.
Sintaxis:
xenunciado compuesto whiley::=
while ( xexpresión booleanay ) {
xenunciado simple o compuestoy
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
}
Semántica:
Lo primero que hace el programa es evaluar la xexpresión booleanay. Si ésta
se evalúa a verdadero, entonces se ejecutan los enunciados que están entre
las llaves, y regresa a evaluar la xexpresión booleanay. Si se evalúa a falso,
brinca todo el bloque y sigue con el enunciado que sigue al while.
1
Figura 4.9 Encontrar el lı́mite de 2n
, dado ε
$ $
'
' '
&ε = .001
'
' double f Actl Ð 1{2
'
'
Inicializar
'
% double f Ant Ð 0
'
&
Calcular lı́mite de #
' f Ant Ð f Actl
'
1 Calcular siguiente término
2n
'
' (mientras |f f | ε) f Actl Ð f Ant{2
'
'
ant actl
'
%Final !
Reporta f Actl
Como se puede ver, esta iteración es ideal cuando no tenemos claro el número
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 parámetro fuera mayor que 1 2, la iteración no se llevarı́a 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 números que nos den desde
la consola hasta que nos den un 1. El algoritmo se puede ver en la figura 4.10.
4.2 Implementación de una base de datos 121
Sintaxis:
x y
enunciado compuesto do. . . while ::=
do {
x enunciado simple o compuesto y
x enunciado simple o compuesto y
...
x enunciado simple o compuesto y
x
} while ( expresión booleana ); y
Semántica:
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-
x
les al bloque. Una vez ejecutado el bloque procede a evaluar la expresión
y
booleana . Si ’esta se evalúa a verdadero, la ejecución continúa en el primer
enunciado del bloque; si es falsa, sale de la iteración y sigue adelante con el
enunciado que sigue al do . . . while.
Es claro que lo que se puede hacer con un tipo de iteración 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 iteración. Esto
nos va a repetir el código, pero el resultado de la ejecución 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 condición, y como cuerpo de la condicional colocamos el
bloque original. De esta manera si la condición no se cumple al principio el bloque
no se ejecuta. Esto quiere decir que si un lenguaje de programación únicamente
cuenta con una de estas dos iteraciones, sigue teniendo todo lo necesario para
elaborar métodos pensados para la otra iteración.
Tenemos una tercera iteración conocida como for, que resulta ser el cañón
de las iteraciones, en el sentido de que en un solo enunciado inicializa, evalúa
una expresión booleana para saber si entra a ejecutar el enunciado compuesto e
incrementa al final de la ejecución del enunciado compuesto. Lo veremos cuando
sea propicio su uso. Por lo pronto volveremos a nuestro problema de manejar una
pequeña base de datos.
La lista del curso debemos mostrarla en algún 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 información del usuario. Veamos los principales métodos que vamos a
usar por el momento en la tabla 4.3 en la página opuesta.
4.2 Implementación de una base de datos 123
Para ajustar los tamaños de las cadenas, primero les agregamos al final un
montón de blancos, para luego truncarla en el tamaño que debe tener. La progra-
mación se encuentra en el listado 4.15 en la página opuesta.
4.2 Implementación de una base de datos 125
loooooooooooooooooooooooooooooooooooooooooooooomoooooooooooooooooooooooooooooooooooooooooooooon
Caso 1
loooooooooooooooooomoooooooooooooooooon looooooooooooooooooooooooomooooooooooooooooooooooooon
Caso 2
loooooooooooooooooooooooooooooooooooooooooooooomoooooooooooooooooooooooooooooooooooooooooooooon
Caso 3
126 Manejo de cadenas y expresiones
Conocemos de cual de estas tres situaciones se trata (no hay otra posibilidad)
dependiendo de la posición del registro:
Es el primero si i vale 1.
Es el último si i vale NUM REGS.
Está en medio si 1 i NUM REGS.
Para este tipo de enunciado es conveniente que introduzcamos el xenunciado
compuesto condicionaly cuya sintaxis y semántica se encuentra en la figura 4.15 en
la página opuesta. Con esto podemos ya pasar a programar el método que elimina
al i-ésimo registro de nuestra lista, en el listado 4.16 en la página opuesta.
El único método que nos falta, para terminar esta sección, 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 página 128.
4.2 Implementación de una base de datos 127
Sintaxis:
xenunciado compuesto condicionaly::=
if ( xexpresión booleanay ) {
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
} else {
xenunciado simple o compuestoy
...
xenunciado simple o compuestoy
}
Semántica:
Lo primero que hace el programa es evaluar la xexpresión booleanay. Si
ésta se evalúa a verdadero, entonces se ejecutan los enunciados que están
entre las primeras llaves y si se evalúa a falso se ejecutan los enunciados
que están a continuación del else. Pudiéramos en ambos casos tener un
único enunciado, en cuyo caso se pueden eliminar las llaves correspondien-
tes. También podemos tener que no aparezca una cláusula else, en cuyo
caso si la expresión booleana se evalúa a falso, simplemente se continúa la
ejecución con el enunciado que sigue al if.
Figura 4.16 Método que encuentra TODOS los que contienen a una subcadena.
$ $
'
' '
'
' '
'
'
' &donde Ð Primer registro que caza.
'
'
' ' cazaCon Ð ””(cadena vacı́a)
P rincipio
'
' '
'
'
' '
%
'
&
Arma cadena
' $
'
' '
'
'
' '
'cazaCon Ð cazaCon
'
'
Arma siguiente & armaRegistropdondeq
'
' (mientras hayas
'
'
' '
'
%donde Ð Siguiente registro que caza
'
encontrado)
'
%
Lo único importante en este caso es darnos cuenta que ya tenemos dos ver-
siones que nos dan la posición de una subcadena, ası́ que la programación es
prácticamente directa. La podemos ver en el listado 4.17 en la página opuesta.
4.3 Una clase Menu 129
Código 4.17 Método 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 á 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 algún medio de comunicación con la misma, como
pudiera ser un lenguaje de consulta (query language) o, simplemente, un menú que
nos dé acceso a las distintas operaciones que podemos realizar sobre la base de
datos. Construiremos un menú para tal efecto.
$ $
'
' '
' Muestra el menú
'
' '
' !
' '
Pide opción al usuario
'
' '
' À
opción == Termina Salir
'
' '
'
'
' '
' #
'
' '
'
Pide registro
' '
opción == Agrega
' ' À
' Agrega registro
'
' $
' '
' '
'
' ' ' Pregunta a quién
' '
' '
'
'
' '
' &Búscalo !
'
' ' ' À
opción == Quita Encontrado Quı́talo
' '
' '
' '
Menú & Maneja Menú & %Encontrado !Reporta No encontrado
'
para
'
'
(mientras desee
'
' À
Cursos
'
'
el usuario) '
' $
'
' '
' '
' Pregunta a quién
'
' '
' '
'
' ' &Búscalo !
'
' '
' ' À
opción == Busca Encontrado Repórtalo
'
' '
' '
' !
'
' '
' '
%
'
' '
' À
Encontrado Reporta No encontrado
'
' '
' !
'
' '
'
'
' '
'
opción
À
== Lista Lista todo el curso
'
' '
' #
'
' '
'
% %
opción == Todos los Pregunta subcadena
que cazan Arma la lista
El menú que requerimos corresponde a una clase que va a hacer uso de la clase
Curso que ya diseñamos. Por lo tanto, podemos empezar ya su codificación. El
hacerlo en una clase aparte nos permitirı́a, en un futuro, tal vez manejar nuestra
base de datos con un lenguaje de consulta o hasta, posiblemente, con un lenguaje
de programación.
4.3 Una clase Menu 131
Sintaxis:
switch( xexpresióny ) {
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;
}
Semántica:
Los valores valor1 , . . . deben ser del mismo tipo que la expresión, 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. Evalúa la expresión 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 opción para que únicamente se ejecuten los
enunciados relativos a esa opción. El switch tiene una cláusula de escape,
que puede o no aparecer. Si aparece y la expresión no toma ninguno de
los valores explı́citamente listados, se ejecuta el bloque correspondiente a
default. Si no hay cláusula de escape y el valor de la expresión no caza con
ninguno de los valores en los cases, entonces el programa abortará dando
un error de ejecución.
Sintaxis:
xenunciado breaky ::= break
Semántica:
Hace que el hilo de ejecución del programa no siga con el siguiente enun-
ciado, sino que salga del enunciado compuesto en el que está, 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 ( "NOestá" ) ;
break ;
case t r u e :
c o n s o l a . i m p r i m e l n ( "SIestá" ) ;
}
Como las únicas dos posibilidades para el selector del switch, una expresión
booleana, es falso o verdadero, no hay necesidad de poner una cláusula de escape,
ya que no podrá tomar ningún otro valor. Este ejemplo es realmente un enunciado
if disfrazado, ya que si la expresión se evalúa a verdadero (true) se ejecuta lo que
corresponde al caso etiquetado con true, mientras que si se evalúa 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 ( "SIestá" ) ;
else
c o n s o l a . i m p r i m e l n ( "SIestá" ) ;
Otro ejemplo más apropiado, ya que tiene más 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: unión libre
En una variable de tipo carácter tenemos una de estas opciones, y deberemos
4.3 Una clase Menu 133
Código 4.19 Encabezado de la clase bfseries Menu y el método 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 ( "Damela 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 ( "Dala 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 ( "Nohubo ningún 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 opción válida .\n" +
79: "Porfavor vuelve a elegir ." ) ;
80: return 0;
81: }
82: }
Para el menú construimos una cadena que se muestra separada por renglones,
como se observa en las lı́neas68 a 73, y le pedimos al usuario que elija un núme-
ro de opción, en la lı́nea 76. En esta lı́nea aparece algo nuevo, ya que estamos
interaccionando con el usuario. La manera de hacerlo es a través de una consola
(por eso aparece un objeto de esta clase como parámetro). Por lo pronto vamos
a leer cadenas, y tenemos la opción de pedirle o no que escriba una cadena como
136 Manejo de cadenas y expresiones
tı́tulo de la pequeña pantalla en la que solicita el dato. Las dos posible firmas del
método de lectura de cadenas de la clase consola son
String leeString ()
String leeString ( String )
4.3.1. Salir
En esta opción se emite un mensaje para el usuario, avisándole del final del
proceso, y se regresa un valor de -1 para que el programa principal ya no siga
mostrando el menú y termine.
Esta opción tiene que funcionar como una interface entre el método 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 (serı́a absurdo
pedirle al usuario que conociera cómo está implementada nuestra base de datos).
4.3 Una clase Menu 137
Por ello se procede a solicitarle al usuario cada uno de los campos involucrados.
Elegimos hacer un método distinto para cada campo, para poder indicarle al
usuario que tipo de cadena estamos esperando. La codificación de cada uno de
estos métodos se encuentran en el listado 4.20. Algunos de estos métodos los
volveremos a usar, ya que nos proporcionan, por parte del usuario, información.
Cada uno de estos métodos simplemente le dice al usuario qué es lo que espera y
recibe una cadena, que, idealmente, deberá ser lo que el usuario espera.
Código 4.20 Métodos 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 ( "Dameel numero de cuenta del"
121: + " estudiante ,de9 dı́gitos " ) ;
122: return cuenta ;
123: }
También realiza su tarea usando métodos 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 opción correcta y que debe volver a elegir.
Con esto damos por terminada nuestra primera aproximación a una base de
datos. En lo que sigue haremos más eficiente y flexible esta implementación, bus-
cando que se acerque más a lo que es una verdadera base de datos.
Datos estructurados
5
La implementación que dimos en el capı́tulo 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 colección de registros, donde cada registro
tiene uno o más campos. Tratemos de acercarnos un poco más a esta abstracción.
El tipo de colección que usaremos en esta ocasión es una lista. La definición
de una lista es la siguiente:
Por ejemplo, si una lista nada más tiene un elemento, ésta consiste del primer
elemento de una lista, seguido de una lista vacı́a.
Toda lista es una referencia a objetos de cierto tipo que contienen determinada
información 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 vacı́a tendrá 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 página, con la letra “r” representando
un campo para guardar allı́ una referencia, y el sı́mbolo representando que no
sigue nadie (una referencia nula).
Como la definición de la lista es recursiva, debemos siempre tener “anclado”
142 Datos estructurados
1
Del inglés, chain.
5.1 La clase para cada registro 143
Una vez que tenemos los constructores, tenemos que proveer, para cada campo
al que queramos que se tenga acceso, un método de consulta y uno de actuali-
zación. La programación de estos métodos se encuentra en el listado 5.3 en la
siguiente página.
144 Datos estructurados
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: }
5.1 La clase para cada registro 145
Podemos también poner métodos más 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 código 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 ( ) ;
146 Datos estructurados
Código 5.4 Métodos que arman y actualizan registro completo (Estudiante) 2/2
Finalmente, de esta clase únicamente nos faltan dos métodos, uno que regrese
todo el registro armado, listo para impresión, y uno que actualice todos los campos
del objeto. Podemos ver su implementación en el listado 5.5.
Código 5.5 Métodos que arman y actualizan registro completo (2) (Estudiante)1/2
Código 5.5 Métodos 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 ó 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 cn 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 estará vacı́a
en tanto no le agreguemos ningún objeto del tipo Estudiante. Por lo tanto, la única
diferencia entre las declaraciones de nuestra implementación anterior y ésta es el
tipo de la lista, quedando las primeras lı́neas de la clase como se puede apreciar
en el listado 5.6.
Al igual que con la clase Curso, tenemos dos constructores, uno que únicamente
coloca el número del grupo y otro inicializa la lista y coloca el número 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 numéricos en 0. Los constructores se encuentran en el listado 5.7.
$ $ !
'
' ' cuantos Ð 0
' '
'
inicia contador
'
' & $
'
' Inicio
' Colócate al '
&
'
' '
' actual Ð lista
'
' % la lista
inicio de
'
%
'
'
'
' $ $
'
& '
' '
&
Cuenta registros '
'
incrementa
en una lista '
' '
' contador '
%
cuantos
'
'
Cuenta el &
'
' registro actual
' $
'
'(mientras haya) ' pasa al '
&
' '
' actual Ð toma el siguiente
'
' '
%siguiente '
%
'
'
'
' !
%F inal Entrega el contador
Código 5.9 Recorrido de una lista para contar sus registros (ListaCurso)
43: /∗ ∗ C a l c u l a e l número 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 número 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: }
5.2 La lista de registros 151
Este método nos da el patrón 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 patrón general es como se muestra en la figura 5.3.
Junto con los métodos de acceso, debemos tener métodos que alteren o asig-
nen valores a los atributos. Sin embargo, tanto el atributo numRegs como lista
deberán ser modificados por las operaciones de la base de datos, y no directa-
mente. En cambio, podemos querer cambiarle el número de grupo a una lista. Lo
hacemos simplemente indicando cuál es el nuevo número de grupo. Podemos ver
este método 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 qué es lo que queremos decir. El diagrama de Warnier-Orr
para hacer esto está en la figura 5.5 y la programación del método se encuentra
en el listado 5.11.
$
Agrega registro
'
&Pon a nuevo a apuntar a lista
al principio '
%Pon a lista a apuntar a nuevo
Antes:
r lista
r Inf o r
nuevo
Después:
r lista
r Inf o r
nuevo
5.2 La lista de registros 153
r lista
r r
Info
nuevo
'
' '
% '
% al actual) '
%
registro
'
'
'
'
%
Pon al último registro a
apuntar al nuevo
Como se puede ver, seguimos el mismo patrón que recorre la lista, pero nuestra
5.2 La lista de registros 155
Para el método que escribe todos los registros que cazan con una cierta subca-
dena también vamos a usar este patrón, 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 programación en el
listado 5.14.
'
'
'
' !
%NO se encontró el registro Emite mensaje
lista
r Eliminación 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
160 Datos estructurados
información 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.
'
'
'
' !
'
' ¿Es el primero? ∅
'
'
Eliminar &
un
'
'
Colocarse en el segundo de la lista
estudiante
'
'
Anotar como anterior al primero
'
' $
' Revisar la lista ' &
'
'
'
'
(mientras haya Y
'
%
moverse al siguiente de la lista
'
' no lo encuentre)
'
' #
'
'
'
' ¿Lo encontré?
Pon al anterior a apuntar
'
' À al siguiente del actual
'
'
'
' $
'
' '
&
'
'
'
%
¿Lo encontré?
'
%
Avisa que no se pudo
5.2 La lista de registros 161
La programación del menú 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 programación
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 (lı́neas 95: en la página 164- 98: en la página 164 en el listado 5.17),
procedemos a invocar al método agrega de la clase ListaCurso. Este método tiene
como argumento un objeto, que es creado en el momento de invocar a agrega –
ver lı́nea 99: en la página 164 del listado 5.17. Este es el único método en el que
se crean objetos, y esto tiene sentido, ya que se requiere crear objetos sólo cuando
se desea agregar a un estudiante.
Para el caso de los métodos de la clase ListaCurso que regresan una referencia a
un objeto de tipo estudiante, es para lo que se declaró la variable donde – lı́nea 76:
en la página 164 del listado 5.17. En estos casos el objeto ya existe, y lo único que
hay que pasar o recibir son las variables que contienen la referencia.
Con esto damos por terminado este capı́tulo. Se dejan como ejercicios las
siguientes mejoras:
subclase hereda los atributos y métodos de la superclase. Que herede quiere decir
que, por el hecho de extender a la superclase, tiene al menos todos los atributos
y métodos 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 tendrı́an la misma información básica. Quitemos la clave de usuario, porque
esa información no la necesitan más que en el centro de cómputo, y entonces
nuestra clase EstudianteBasico quedarı́a definida como se muestra en el listado 6.1.
Omitimos los comentarios para tener un código más 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 tendrı́a, además de la
información de EstudianteBasico, los libros en préstamo. Un listado para la División
de Estudios Profesionales deberı́a tener una historia académica y el primer año
de inscripción a la carrera. Un listado para calificar a un grupo deberı́a tener un
cierto número de campos para las calificaciones. Por último, un listado para las
salas de cómputo deberı́a contar con la clave de acceso.
En todos los casos que listamos, la información original debe seguir estando
presente, ası́ como los métodos que tiene la superclase. Debemos indicar, entonces,
que la clase que se está definiendo extiende a la superclase, heredando por lo tanto
todos los métodos y atributos de la superclase. La sintaxis en Java se muestra a
continuación.
Sintaxis:
xsubclase que hereday ::= class xsubclasey extends xsuperclasey {
xdeclaraciones de campos y métodosy
}
Semántica:
La xsubclasey es una nueva declaración, que incluye (hereda) a todos los
campos de la superclase, junto con todos sus métodos. Las xdeclaraciones
de campos y métodosy se refiere a lo que se desea agregar a la definición de
la superclase en esta subclase.
6.2 Arreglos
donde estamos asumiendo que cada estudiante tiene lugar para, a lo más, 15
calificaciones (del 0 al 14). Se ve, asimismo, muy útil el poder manejar a cada una
de las calificaciones refiriéndonos a su subı́ndice, en lugar de declarar calif0, calif1,
etc., imitando el manejo que damos a los vectores en matemáticas.
172 Herencia
Es importante notar que lo que tenemos es una colección de datos del mismo
tipo que se distinguen uno de otro por el lugar que ocupan. A estas colecciones les
llamamos en programación arreglos. La declaración de arreglos tiene la siguiente
sintaxis:
Sintaxis:
xdeclaración de arregloy ::= xtipoy[ ] xidentificadory;
Semántica:
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:
xdeclaración de arreglos con inicializacióny ::=
xtipoy[ ] xidentificadory = { xlistay };
Semántica:
Para inicializar el arreglo se dan, entre llaves, valores del tipo del arreglo,
separados entre sı́ por coma. El arreglo tendrá tantos elementos como apa-
rezcan en la lista, y cada elemento estará creado de acuerdo a la lista. En
el caso de un arreglo de objetos, la lista deberá contener objetos que ya
existen, o la creación 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 continuación cómo 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 dirección en el heap. Las localidades
están identificadas con rectángulos. El número que se encuentra ya sea inmedia-
tamente a la izquierda o encima del rectángulo corresponde a la dirección en el
heap.
174 Herencia
MEMORIA HEAP
MEM HEAP
6.2 Arreglos 175
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:
xdeclaración de un arreglo con tamaño dadoy ::=
xtipoy xidentificadory = new xtipoy[xexpresión enteray];
Semántica:
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 hubiéramos hecho sin la inicialización serı́an:
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
6.2 Arreglos 177
MEMORIA HEAP
MEMORIA HEAP
27844 27850
cadenas @ 27844 @null @null
r0s r1s
MEMORIA HEAP
Sintaxis:
xselección de un elemento de un arregloy ::=
xid. de arregloy [ xexpresión enteray ]
Semántica:
El operador r s es el de mayor precedencia de entre los operadores de Java.
Eso indica que evaluará la expresión dentro de ella antes que cualquier otra
operación (en la ausencia de paréntesis). Una vez obtenido el entero corres-
pondiente a la xexpresión enteray – que pudiera ser una constante entera
– procederá a elegir al elemento con ese ı́ndice en el arreglo. El resultado
de esta operación es del tipo de los elementos del arreglo. De no existir el
elemento al que corresponde el ı́ndice calculado, el programa abortará con
el mensaje ArrayIndexOutOfBoundsException. Al primer elemento del arre-
glo le corresponde siempre el ı́ndice 0 (cero) y al último elemento el ı́ndice
n 1, donde el arreglo se creó con n elementos.
Es importante apreciar que el tamaño de un arreglo no forma parte del tipo.
Lo que forma parte del tipo es el número de dimensiones (vector, matriz, cubo,
. . . ) y el tipo de sus elementos.
enteros @ null
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 lı́nea 3 del
código no es el mismo que el que se crea en la lı́nea 7: no se encuentran en la
misma dirección del heap, y no contienen lo mismo. Insistimos: no es que haya
cambiado el tamaño del arreglo, sino que se creó un arreglo nuevo.
Sintaxis:
xenunciado de iteración enumerativay ::=
for ( xenunciado de inicializacióny ;
xexpresión booleanay ;
xlista de enunciadosy )
xenunciado simple o compuestoy
Semántica:
En la ejecución va a suceder lo siguiente:
1. Se ejecuta el xenunciado de inicializacióny.
2. Se evalúa la xexpresión booleanay.
a) Si es verdadera, se continúa en el paso 3.
b) Si es falsa, se sale de la iteración.
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 vacı́a, aunque el ; sı́ tiene que
aparecer. En el caso de la primera y tercera parte, el que esté vacı́o indica
que no se hace nada ni al inicio del enunciado ni al final de cada iteración.
En el caso de una xexpresión 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 iteración quedarı́a 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 pequeña diferencia entre las dos versiones. En el caso del for, la varia-
ble i es local a él, mientras que en while tuvimos que declararla fuera. En ambos
casos, sin embargo, esto se hace una única vez, que es el paso de inicialización.
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:
6.2 Arreglos 181
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 declaración. Si alguna de las variables está declarada antes y
fuera del for aparecerá el mensaje de que se está repitiendo la declaración, aunque
esto no es correcto. Lo que sı́ es válido es tener varios for’s, cada uno con una
variable local i.
Por supuesto que el xenunciado simple o compuestoy puede contener o consistir
de, a su vez, algún otro enunciado for o while o lo que queramos. La ejecución va
a seguir el patrón dado arriba, terminando la ejecución 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 parámetro. El método podrı́a estar codificado como se muestra en el
código 6.3.
EJEMPLO 6.2.8
Supongamos ahora que queremos tener dos variables para controlar la itera-
ción, donde la primera nos va a decir en cuál iteración va (se incrementa de 1 en
182 Herencia
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 descripción de la sintaxis.
Por ejemplo, queremos producir un triángulo 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 renglón por renglón, y en cada renglón reco-
rrer tantas columnas como renglones llevamos en ese momento. El código para
conseguir escribir esto se encuentra en el listado 6.5.
Es obvio que podemos necesitar arreglos de más de una dimensión, como por
ejemplo matrices. Para Java los arreglos de dos dimensiones son arreglos de arre-
184 Herencia
glos, y los de tres dimensiones son arreglos de arreglos de arreglos, y ası́ su-
cesivamente. La declaración se hace poniendo tantas parejas de corchetes como
dimensiones queramos en un arreglo, como se puede ver a continuación.
int [ ] [ ] matriz ; // 2 d i m e n s i o n e s : m a t r i z
EstudianteBasico [ ] [ ] [ ] f a c u l t a d ; // 3 d i m e n s i o n e s : cubo
Por la manera en que maneja Java los arreglos, si quiero inicializar un arreglo,
por ejemplo, de dos dimensiones, tendré que darle entre llaves las inicializaciones
para cada uno de los renglones:
int [ ] [ ] matriz = {{2 ,2 ,3 ,3} ,{3 ,4 ,5} ,{8}};
Reconocemos inmediatamente tres sublistas encerradas entre llaves, lo que
indica que el arreglo matriz tiene tres renglones. Cada uno de los renglones tiene
tamaño distinto, y esto se vale, pues siendo un arreglo de arreglos, cada uno de los
arreglos en la última dimensión puede tener el número de elementos que se desee.
Un esquema de cómo quedarı́an en memoria se puede ver en la figura 6.10.
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 ] [ ] ;
Código 6.6 Arreglos como parámetros y valor de regreso de una función 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 dos 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 ó 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 os a r r e g l o s de una d i m e n s i ó n
19: ∗ @param Dos a r r e g l o s de una d i m e n s i ó 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 os 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 ;
188 Herencia
Código 6.6 Arreglos como parámetros y valor de regreso de una función 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 métodos ∗/
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 ó n a l a suma de dos m a t r i c e s ∗/
41: miSuma = p r u e b i t a . suma ( uno , d o s ) ;
42: /∗ I m p r e s i ó 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 través de las superclases, sin que sepamos concretamente de cuál
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 método getRegistro(). Podrı́amos tener el código 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 lı́neas 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 más; 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 ( ) ;
192 Herencia
se da cuenta de que lo que tiene ahı́ es una referencia a algo del tipo EstudianteCur-
so, por lo que utilizará la implementación dada en esa subclase para dar respuesta
a esta solicitud. Decimos que gobierna el tipo de la referencia, no el tipo de la
declaración. La decisión de a cuál de las implementaciones de getRegistro() debe
ser invocada se toma en ejecución, ya que depende de la secuencia de ejecución
el tipo de la referencia guardada en la localidad del arreglo. A esta capacidad de
los lenguajes orientados a objetos de resolver dinámicamente el significado de un
nombre de método 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 sección 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 deberá tener un programa que identifique de qué 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:
xexpresión de objetoy instanceof xidentificador de clasey
y regresa el valor booleano verdadero si, en efecto, el objeto dado en la expresión
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 tendrı́amos un código 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 jerarquı́a de clases para las figuras geométricas,
que se podrı́a ver como en la figura 6.12.
Figura geométrica
Sintaxis:
xencabezado de clase abstractay ::= abstract class xidentificadory
Semántica:
Le estamos indicando al compilador dos cosas:
No pueden crearse objetos de esta clase.
Contiene al menos un método cuya implementación no está definida.
No hay la obligación de que todos los métodos en una clase abstracta sean
abstractos. Dependerá del diseño y de las posibilidades que tenga la superclase
para definir algunos métodos para aquellas clases que hereden. Aún cuando la
clase no tenga métodos 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 más
una superclase. Pero muchas veces necesitamos que hereden de más de una clase.
Las interfaces corresponden a un tipo, como las clases, que definen exclusivamente
comportamiento. Lo único que pueden tener las interfaces son constantes estáticas
y métodos 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 implementación de los
métodos declarados en la interfaz. La sintaxis para la declaración de una interfaz
es:
Sintaxis:
xencabezado de interfazy ::= interface xidentificadory
...
Semántica:
Se declara un tipo que corresponde a constantes y encabezados de métodos.
Como todo lo que se declare dentro de una interfaz es público, pues corresponde
siempre a lo que puede hacer un objeto, no se usa el calificativo public en los
enunciados dentro de la declaración de la interfaz. Como forzosamente todos los
métodos son abstractos, ya que no tienen implementación, tampoco se pone este
calificativo frente a cada método. Y como sólo se permiten constantes estáticas, los
calificativos de static y final se omiten en las declaraciones de constantes dentro
de una interfaz. Por lo tanto, las constantes y métodos en una interfaz serán
declarados nada más con el tipo de la constante o el tipo de valor que regrese el
método, los identificadores y, en el caso de las constantes el valor; en el caso de
los métodos, los parámetros.
Pensemos en el comportamiento de una lista, como las que hemos estado vien-
6.6 Interfaces 197
do. Sabemos que no importa de qué sea la lista, tiene que tener un método que
agregue, uno que busque, etc. Dependiendo de los objetos en la lista y de la imple-
mentación particular, la implementación de cada uno de los métodos puede variar.
Tenemos acá un caso perfecto para una interfaz. En el listado 6.13 podemos ver
la declaración 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
198 Herencia
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
paréntesis, 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 método busca nos regresa un objeto de tipo Object (la referencia), pero
sabemos, por la implementación 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 implementación de los
métodos en las interfaces, aún cuando una misma firma aparezca en más de una
interfaz (o inclusive en la superclase) la implementación que se va a elegir es la
que aparezca en la clase, por lo que no se presentan conflictos.
Las interfaces también pueden extender a una o más interfaces de la misma
manera que subclases extienden a superclases. La sintaxis es la misma que con la
extensión de clases:
Sintaxis:
interface xidentif1 y extends xidentif2 y, . . . , xidentifny
Semántica:
De la misma manera que con las clases, la subinterfaz hereda todos
los métodos de las superinterfaces.
Clase
atributos
método
método
bloque
bloque
método
bloque
método
bloque
bloque
1
Sin embargo, si existe alguna declaración de una variable con el mismo nombre fuera del
bloque y que la precede, el compilador dará error de sintaxis por identificador ya declarado.
2
No pueden tener el mismo identificador que una declaración previa y fuera del bloque.
7.1 El stack y el heap 201
Para los métodos estáticos, como es el caso del método main de las clases esto
funciona un poco distinto, ya que este tipo de métodos no tiene acceso más que a
los atributos o métodos estáticos de la misma clase, y a los métodos o atributos
públicos o de paquete de las clases a las que se tenga acceso. Olvidándonos un poco
de los métodos estáticos (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 método, sin
embargo, no podemos ver lo que está declarado dentro de otro método o bloques.
Pero, ¿qué pasa cuando el nombre de una variable de clase se repite dentro de
un bloque como parámetro 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 más cerca”, siempre y cuando tenga
acceso a ella, la pueda ver.
El rango de una variable es estático, pues está definido por la estructura del
programa: de ver el listado podemos decir cuál es el rango de una variable dada, y
definirlo como público (global, pero atado a un objeto) o local (declarado dentro
de un método 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 ejecución del programa, esta estructura presenta un cierto “anida-
miento”, distinto del anidamiento sintáctico o lexicográfico, donde desde dentro
de un método se puede llamar a cualquiera de los que está en rango, o sea, cual-
quiera de los métodos declarados en la clase o que sean públicos de otras clases.
Se lleva a cabo una anidamiento dinámico durante ejecución, donde se establece
una cadena de llamadas tal que el último método que se llamó 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 página 203 tenemos los bloques en el orden
en que son invocados, mostrando el anidamiento en las llamadas. El nombre del
método junto con el valor de sus parámetros se encuentra sobre la lı́nea que lo
demarca. En la esquina superior derecha de cada bloque se encuentra el nivel de
anidamiento dinámico que tiene cada método. El valor de las variables, tanto de los
atributos como de las variables locales, dependerá del anidamiento lexicográfico.
Por ejemplo, dentro del método B en el que se le asigna valor a una variable
entera a, ésta 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 lı́nea 29: al método B, los valores con
202 Administración de la memoria durante ejecución
A(10) [2]
B(10,3) [3]
C( ) [4]
B(3,2) [2]
C( ) [3]
C( ) [2]
$
'
' $
Construcción de objeto
'
' '. . .
'
' ' $
' '
& '
&. . .
'
'
'
& '
' '
%. . .
Llamada a A(10) Llamada a B(10,3) Llamada a C()
Secuencia de '
%. . .
Ejecución '
' $
'
' '
&. . .
'
'
'
'
Llamada a B(3,2)
'
%
Llamada a C()
'
' ! ...
%Llamada a C() . . .
Lo que me indican los dos diagramas anteriores, es que la ejecución del pro-
grama debe proseguir de la siguiente manera:
204 Administración de la memoria durante ejecución
Para poder hacer esto, la ejecución del programa se lleva a cabo en la memoria
de la máquina, organizada ésta como un stack, que es una estructura de datos con
las siguientes caracterı́sticas:
a) Respecto a su estructura:
La estructura es lineal, esto es, podemos pensarla con sus elementos “for-
mados” uno detrás del otro.
Es una estructura homogénea, donde todos sus elementos son del mismo
tipo.
Es una estructura dinámica, esto es, crece y se achica durante ejecución.
Tiene asociado un tope, que corresponde al último elemento que se colocó en
el stack.
b) Respecto a su uso:
Un stack empieza siempre vacı́o, sin elementos.
Conforme progresa la ejecución, 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 también 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 instrucción
'
' '
'
' '
Obtén los operandos
'
' '
'Incrementa el contador$del programa
'
' '
' '
'
& Ejecuta instrucciones & ' À
Suma
Ejecuta '
'
programa '
'
(Hasta que encuentres
'
' '
&Resta
À
'
'
la de “parar”) '
'Ejecuta la instrucción '
'
' ' '
' '
' '
'
.
'
' ' '
% '
% '
%.
.
se “desmonta” del stack al mismo. Para “montar” un método al stack hay que
construir lo que se conoce como su registro de activación, que es una tabla en
la que hay lugar para los parámetros y las variables locales del método, de tal
manera que durante la ejecución se encuentren siempre en la parte superior del
stack.
Al invocar un método (para transferirse a ejecutarlo), el sistema debe realizar
los siguientes pasos:
1. Dejar un lugar en el stack para que el método coloque ahı́ el valor que va a
regresar, si es que regresa valor.
2. Hacer la marca en el stack, copiando ahı́ el contenido del contador del pro-
grama.
3. Buscar en el stack, en el registro de activación global, la dirección de código
donde se encuentra definida ese método. Copiar esa dirección al contador
del programa (es donde va a continuar la ejecución cuando se termine de
montar al método en el stack).
4. Construir el registro de activación del método, dejando un lugar para cada
parámetro, en el orden en que están declarados, y un lugar para cada variable
local (o estructura de datos pública o privada, si se trata de una clase).
5. Evaluar los argumentos, para entregarle al método una lista de valores e
irlos colocando en el registro de activación.
6. Copiar al stack, en el orden en que aparece, el registro de activación (el
último es el que queda en el tope del stack).
7. Copiar los valores de los argumentos a los parámetros.
8. Conforme se va ejecutando el método, se van colocando en el stack las va-
riables y objetos que se van declarando.
1. Localizar la marca del stack más cercana al tope, la última que se colocó.
2. Colocar en el contador del programa la dirección de regreso que se encuentra
en esa marca.
3. Si el enunciado que causa la terminación 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, incluyéndola.
Se quita algo del stack simplemente “bajando” el apuntador al tope del
stack a que apunte al último registro que se quitó (recordar que lo último
que se colocó es lo primero que se quita). No hay necesidad de borrar la
información pues la ejecución solo va a tomar en cuenta aquella información
que se encuentre antes del tope del stack.
5. Continúa la ejecución en el lugar del código 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 ejecución 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 máquina) 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-
trucción de máquina. Es suficiente para nuestros propósitos manejar el programa
a nivel de enunciado. En los esquemas del stack que presentamos a continuación,
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 instrucción a ejecutarse.
El tope del stack apunta al primer lugar vacı́o 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
métodos de la clase3 , quedando el stack como se observa en la figura 7.6.
El sistema operativo sabe que el primer método a ejecutarse es main, por lo
que inicia la ejecución con él. 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
dirección de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m #5:
*10
xString r sy args #heap Contador del
d.r. Sistema Programa
Operativo
main
xinty i *10
dirección de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m #5:
*10
xString r sy args #heap Contador del
d.r. Sistema Programa
Operativo
main
En la lı́nea #8: tenemos una llamada al método B(i,a), por lo que nuevamente
marcamos el stack, copiamos la dirección del PC a la marca, armamos el registro
de activación para B y lo montamos en el stack, colocamos la dirección donde
empieza B a ejecutarse en el PC y proseguimos la ejecución 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 lı́nea de código #16: hay una llamada desde B al método C, por
lo que nuevamente se marca el stack, se actualiza el contador del programa y se
monta en el stack el registro de activación de C(). El resultado de estas acciones
se pueden ver en la figura 7.13 en la página 214.
Se ejecuta el método C() y al llegar al final del mismo el sistema desmonta
el registro de activación de C() del stack, coloca en el contador del programa
la dirección que se encuentra en la marca y elimina la marca puesta por esta
invocación. El stack se ve como en la figura 7.14 en la página opuesta.
Se termina la ejecución de B(10,3) en la lı́nea #18:, por lo que se desmonta el
registro de activación de b(10,3) del stack, se copia la dirección 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.
7.1 El stack y el heap 213
xinty a *13
xinty j *3
xinty i *10
dirección de
regreso: #9:
B(10,3)
xinty i *10
dirección de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xString r 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
214 Administración de la memoria durante ejecución
xinty k *6
dirección de
regreso: #17:
C()
xinty a *13
xinty j *3
xinty i *10
dirección de
regreso: #9:
B(10,3)
xinty i *10
dirección de
regreso: #29:
A(10)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xString r 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
7.1 El stack y el heap 215
xinty a *13
xinty j *3
xinty i *10
dirección de
regreso: #9:
B(10,3)
xinty i *10
dirección de
regreso: #29:
A(10)
xCualquieray objeto # heap
xinty n *5 #17:
xinty m *10
Contador
xString[ ]y args # heap del
dirección de Programa
regreso: #29:
main
xinty i *10
dirección 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
216 Administración de la memoria durante ejecución
Se llega al final del método A(10), por lo que se desmonta el registro de ac-
tivación de A(10), se copia la dirección 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 ejecución del programa a la lı́nea 29: se encuentra con otra invoca-
ción a B(3,2), que son los campos de la clase. Se coloca la marca en el stack con
dirección de regreso 30:, se actualiza el PC para que marque el inicio del método
B y se monta al stack el registro de activación de B(3,2). Los resultados de estas
acciones se muestran en la figura 7.17.
7.1 El stack y el heap 217
xinty a *5
xinty j *2
xinty i *3
dirección de
regreso: #29:
B(3,2)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xString r 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
dirección de
regreso: #17:
C()
xinty a *5
xinty j *2
xinty i *3
dirección de
regreso: #29:
B(3,2)
xCualquieray objeto #heap
xinty n *5
xinty m *10
xString r 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
dirección 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 lı́nea 30: nuevamente se hace una llamada al método C(), por lo que se
marca el stack y se monta su registro de activación. El resultado se puede ver en
la figura 7.21 en la página opuesta.
xinty k *6
dirección de
regreso: #31:
C()
xCualquieray objeto #heap
xinty n *5
xinty m *10
xString r 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 lı́nea 31: es la que termina main, se descarga del stack el registro de
activación de este método, se copia al PC la dirección de regreso de la marca y se
quita la marca. En ese momento termina la ejecución del programa, por lo que se
libera el stack y el PC.
En todo momento durante la ejecución, el sistema puede utilizar lo que se
encuentre en el bloque global, más aquello que se encuentre por encima de la última
marca en el stack y hasta inmediatamente antes de la celda antes de la última
marca que se puso. De esta manera, Cada método “crea” su propio ambiente de
ejecución.
Resumiendo, el stack se utiliza para la administración de la memoria en eje-
cución. Cada vez que se invoca una rutina o método, se construye el registro de
activación de la misma y se coloca en el stack. Cada vez que se sale de un método,
se quita del stack el registro de activación de la rutina que está en el tope y la
ejecución continúa en la dirección de regreso desde la que se invocó a esa instancia
222 Administración de la memoria durante ejecución
del método.
Durante la ejecución de un programa, el sistema trabaja con dos variables, el
tope del stack, que indica cuál es la siguiente celda en la que se va a colocar in-
formación, y el contador del programa, que indica cuál es la siguiente instrucción
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 dirección del
stack) y el contador del programa apunta a una dirección de memoria del progra-
ma donde se encuentra almacenado el código 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 continuación el nombre
del atributo. Debo insistir en que el bloque o registro de activación en el que se
encuentra la variable debe ser visible desde el punto de ejecución y únicamente
se aplica a variables que hayan sido ocultadas por una reutilización del nombre.
Si la variable se encuentra en un registro de activación inaccesible, entonces el
compilador emitirá 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 lı́neas del programa para poder hacer referencia a ellas en
7.2 Recursividad 225
la ejecución, que empieza en la lı́nea 13:. En las lı́neas 13: y 15: tenemos las
declaraciones e inicializaciones de variables locales a main, y en las lı́neas 16: y 17:
está el único enunciado realmente de ejecución 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
dirección 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
dirección de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
dirección 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
dirección de
regreso #7:
factorial(2)
xlongy valor de regreso
xinty n *3
dirección de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
dirección 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
dirección de
regreso #7:
factorial(1)
xlongy valor de regreso
xinty n *2
dirección de
regreso #7:
factorial(2)
xlongy valor de regreso
xinty n *3
dirección de
regreso #7:
factorial(3)
xlongy valor de regreso
xinty n *4
dirección 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 sólo tenemos dos fichas las sabemos
mover “a pie”. Para el caso de que tenga más de dos fichas (n ¡ 2), suponemos
que pudimos mover las n 1 fichas que están en el tope del poste al poste
auxiliar, siguiendo las reglas del juego, después 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 cálculo de factorial con recursividad, se entra al método
234 Administración de la memoria durante ejecución
decrementando la n en 1 hasta que tengamos que mover una sola ficha; en cuanto
la movemos, pasamos 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 código (de
manera esquemática) 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 número 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 */
236 Administración de la memoria durante ejecución
Comprobemos que este algoritmo trabaja viendo una visualización 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 colocó. Las
reglas exigen que cada vez que se mueve una ficha, ésta sea la que se encuentra
hasta arriba.
/* 2 */
/* 3 */
/* 6 */
/* 5 */
/* 11 */ /* 9 */
238 Administración de la memoria durante ejecución
/* 12 */
/* 14 */ /* 15 */
Como se puede ver del ejercicio con las torres de Hanoi, 4 fichas provocan
15 movimientos. Podrı́amos 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 más fácil expresarlo ası́, que ocupa aproxi-
madamente 10 lı́neas, 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 solución
recursiva es elegante y mucho más clara que la iterativa se encuentra el recorrido
de árboles, las búsquedas binarias y algunos ordenamientos como el de mezcla y
el de Quick.
Con esto damos por terminado una descripción somera sobre cómo se com-
porta la memoria durante la ejecución de un programa, en particular el stack de
ejecución. Esta descripción no pretende ser exhaustiva, sino únicamente propor-
cionar una idea de cómo identifica la ejecución los puntos de entrada, de regreso
y parámetros a una función.
Ordenamientos
usando estructuras 8
de datos
Habiendo ya visto arreglos, se nos ocurre que puede resultar más fácil 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 cuál es el tamaño máximo de un grupo y
reservar ese número de localidades en un arreglo. La superclase para el registro
con la información del estudiante queda casi exactamente igual al que utilizamos
como EstudianteBasico, excepto que como ahora la relación de quién sigue a quién
va a estar dada por la posición en el arreglo, no necesitamos ya la referencia al
siguiente estudiante. Todos los métodos quedan exactamente igual, excepto que
todo lo relacionado con el campo siguiente ya no aparece – ver listado 8.1 en la
siguiente página.
240 Ordenamientos usando estructuras de datos
Como se puede observar en el listado 8.1 en la página 240, todo quedó prácti-
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 posición que ocupa en el arreglo, pos y los métodos
correspondientes. Este campo se verá actualizado cuando se entregue la referencia
del registro sin especificar su posición.
La clase ası́ definida puede ser usada, por ejemplo, para cuando queramos
una lista de estudiantes que tengan esta información básica incluida. Un posible
ejemplo se muestra en el listado 8.2. Esta jerarquı́a 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 número 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 básicas 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 posición 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 estén más allá 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 posición que se desea mover. Si no se hace en este orden se
tendrá como resultado el valor del primer registro que se desea mover copiado a
todos los registros a su derecha. El método para recorrer hacia la derecha se en-
cuentra en el listado 8.5 en la siguiente página. El método 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 responderá falso, y nos responderá 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 último, para no acabar
con una repetición de lo mismo.
250 Ordenamientos usando estructuras de datos
tando los que se quitan, con los registros activos ocupando posiciones consecutivas
$
'
&1 Si s1 s2
s1.compareTo(String s2)
'
%1
0 Si s1 == s2
Si s1 ¡ s2
en las lı́neas 149: – 152: del listado 8.8 en la página 252. Nos colocamos al principio
del vector, poniendo el ı́ndice que vamos a usar para recorrerlo en 0 – lı́nea 149:. A
continuación, mientras no se nos acaben los registros del arreglo y estemos viendo
registros lexicográficamente menores al que buscamos – condicionales en lı́neas
150: y 151: – incrementamos el ı́ndice, esto es, pasamos al siguiente.
Podemos salir de la iteración 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
llegó al número 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 método verificamos que todavı́a 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 él para hacer lugar. Esto
se hace en la lı́nea 158:, donde de paso preguntamos si lo pudimos hacer – segu-
ramente sı́ porque ya habı́amos verificado que hubiera lugar. Una vez recorridos
los elementos del arreglo, colocamos el nuevo elemento en el lugar que se desocu-
pó gracias al corrimiento, y avisamos que todo estuvo bien – lı́neas 161: y 162: –
no sin antes incrementar el contador de registros.
lo siguiente: localiza al registro que contenga al nombre completo, que llega como
parámetro – lı́neas 174: a 178: del listado 8.9 en la página opuesta.
Una vez que encontró el registro en el que está ese nombre, se procede a
“desaparecerlo”, recorriendo a los registros que están después que él un lugar a
la izquierda, encimándose en el registro que se está quitando; esto se hace con la
llamada a regresa(actual,1) en la lı́nea 181: del listado 8.9 en la página opuesta. Al
terminar de recorrer a los registros hacia la izquierda, se decrementa el contador
de registros numRegs. La ubicación de este decremento en el método regresa – lı́nea
85: del listado 8.5 en la página 250 – se justifica bajo la óptica de que numRegs
indica la posición del primer registro vacı́o; otra opción hubiera sido poner el
decremento en quitaEst, que estarı́a justificado bajo la óptica de que numRegs
cuenta el número de estudiantes en la base de datos. Como numRegs juega ambos
papeles la respuesta a dónde poner el decremento no es única. El diagrama de
Warnier correspondiente se encuentra en la Figura 8.1.
Código 8.10 Búsqueda de una subcadena en algún 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 com 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 á 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 cómo están implementadas las estructuras de datos
o los métodos de la clase VectorLista, sino simplemente saber usarlos y saber que
le tiene que pasar como parámetro y qué espera como resultado. Excepto por
los métodos que agregan y quitan estudiantes, que los volvimos booleanos para
que informen si pudieron o no, todos los demás métodos mantienen la firma que
tenı́an en la implementación con listas ligadas. Vale la pena decir que podrı́amos
modificar los métodos de las listas ligadas a que también contestaran si pudieron
o no, excepto que en el caso de las listas ligadas siempre podrı́an.
Nos falta revisar nada más dos métodos: 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
método nuevamente se aplica la transformación de que colocarse al principio de la
lista implica poner al ı́ndice que se va a usar para recorrerla en 0 – lı́nea 207: en el
listado 8.11. Nuevamente nos movemos por los registros incrementando el ı́ndice
en 1, y verificamos al salir de la iteración si encontramos lo que buscábamos o no.
En el caso del método que lista a los que cazan con cierto criterio – listado 8.12
en la siguiente página – 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 listó o
no a alguno, se cuentan los que se van listando – lı́nea 223: en el listado 8.12 en la
siguiente página. Si no se encontró ningún registro que satisficiera las condiciones
dadas, se da un mensaje de error manifestándolo.
258 Ordenamientos usando estructuras de datos
Código 8.12 Listando los que cumplan con algún criterio (CursoEnVector)
214: /∗ ∗
215: ∗ Imprime 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 ó 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 á l campo s e d e s e a com 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 ó n i n g ú 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 ( "Nose encontró ningún 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 código que ya tenemos para InfoEstudiante. Todo
lo que tenemos que hacer es agregarle los campos que InfoEstudiante no tiene y
los métodos de acceso y manipulación para esos campos. La programación de la
8.2 Mantenimiento del orden con listas ligadas 259
Código 8.13 Definición 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 Menú
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 á 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 cnt a , S t r i n g c l v e ,
27: String crrera ) {
28: super ( nmbre , cnt 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 á 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: }
260 Ordenamientos usando estructuras de datos
Hay que notar que lo que programamos como de acceso privado cuando no
tomábamos en consideración la herencia, ahora se convierte en acceso protegido,
para poder extender estas clases.
Algunos de los métodos que enunciamos en esta clase son, simplemente, méto-
dos nuevos para los campos nuevos. Tal es el caso de los que tienen que ver con
clave y siguiente. El método getCampo se redefine en esta clase, ya que ahora tiene
que considerar más posibilidades. También el método getRegistro es una redefini-
ción, aunque usa a la definición de la superclase para que haga lo que correspondı́a
a la superclase.
Los constructores también son interesantes. Cada uno de los constructores,
tanto el que tiene parámetros como el que no, llaman al correspondiente cons-
tructor de la superclase, para que inicialice los campos que tiene en común con la
superclase.
La palabra super se está utilizando de dos maneras distintas. Una de ellas,
en el constructor, estamos llamando al constructor de la superclase usando una
notación con argumentos. En cambio, en el método getRegistro se usa igual que
cualquier otro objeto, con la notación punto. En este segundo caso nos referimos
al “super-objeto” de this, a aquél definido por la superclase.
lista
nuevo Alberto ∅
nuevo Ricardo ∅
'
' '
'
principio
'
%
primer elemento
'
' ' de À la lista de la lista
'
' '
' $
'
' '
' '
' anterior Ð Primero de
'
& '
' '
'
Agrega registro '
' '
' Ð
la lista
'
' & '
'
actual Segundo de
lista H
en orden
'
' ' '
' $ la lista
' '
' Le toca al ' & Recorre ' ' anterior Ð actual
'
' ' '
' ' la lista '
'
' '
'
principio
'
' '
&
'
' '
'
de la lista ' (mientras
' ' '
' la llave ' 'actual Ð siguiente
'
' '
' '
' sea menor ' '
' '
' '
' '
%
'
' '
' ' o igual)
% % %Insértalo entre anterior y actual
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 podrı́a 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 ) ;
8.3 *Ordenamiento usando árboles 265
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 nodoÀque no tiene hijos
Un árbol n-ario es
'
%Un nodo que tiene como hijos a n árboles
A esta definición le corresponde el esquema en la figura 8.5.
raı́z
Info
......
Árbol Árbol
Árbol
En estos momentos revisaremos únicamente a los árboles binarios, por ser éstos
un mecanismo ideal para organizar a un conjunto de datos de manera ordenada.
Un árbol binario, entonces, es un árbol donde el máximo número de hijos para
cada nodo es dos. Si lo utilizamos para organizar cadenas, podemos pensar que
dado un árbol, cada nodo contiene una cierta cadena. Todas las cadenas que se
encuentran en el subárbol izquierdo son menores a la cadena que se encuentra en
la raı́z. Todas las cadenas que se encuentran en el subárbol derecho, son mayores
8.3 *Ordenamiento usando árboles 267
“H”
∅ “A” “P”
Cuando todos los nodos, excepto por las hojas del último nivel, tienen exac-
tamente el mismo número de hijos, decimos que el árbol está completo. Si la pro-
fundidad del subárbol izquierdo es la misma que la del subárbol derecho, decimos
que el árbol está equilibrado 2 . El árbol del esquema anterior no está 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 subárbol
izquierdo y derecho, y los métodos de acceso y manipulación de estos campos. La
programación se puede ver en el listado 8.15 en la siguiente página.
Como se ve de la declaración del registro ArbolEstudiante, tenemos una es-
tructura recursiva, donde un registro de estudiante es la información, con dos
referencias a registros de estudiantes.
2
En inglés, balanced
268 Ordenamientos usando estructuras de datos
∅ “B”
∅ “D”
∅ “E”
∅ “F”
∅ “G”
∅ “H”
∅ “M”
∅ “N”
∅ “P”
∅ “R”
∅ “T”
∅ “Y” ∅
8.3.3. Inserción
raı́z
“Manuel”
∅ “Anita” ∅ ∅ “Octavio” ∅
Como podemos ver en este esquema, para listar el contenido de los nodos en
orden tenemos que recorrer el árbol de la siguiente manera:
los árboles son estructuras recursivas, el hijo izquierdo es, a su vez, un árbol (lo
mismo el hijo derecho). Por ello, si pensamos en el caso más general, en que el
hijo izquierdo pueda ser un árbol tan complicado como sea y el hijo derecho lo
mismo, nuestro algoritmo para recorrer un árbol binario arbitrario quedarı́a como
se muestra en el esquema de la figura 8.10.
'
' $
' &
'
' %H
%Hay hijo derecho
En nuestro caso, el proceso del nodo raı́z de ese subárbol en particular consiste
en escribirlo. El método público que muestra toda la lista queda con la firma como
la tenı́a en las otras dos versiones que hicimos, y programamos un método privado
que se encargue ya propiamente del recorrido recursivo, cuya firma será
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 )
Quisiéramos guardar el contenido del árbol en disco, para la próxima vez em-
pezar a partir de lo que ya tenemos. Si lo guardamos en orden, cuando lo volvamos
a cargar va a producir un árbol degenerado, pues ya vimos que lo peor que nos
puede pasar cuando estamos cargando un árbol binario es que los datos vengan en
orden (ya sea ascendente o descendente). Por ello, tal vez serı́a más conveniente
recorrer el árbol de alguna otra manera. Se nos ocurre que si lo recorremos en
preorden, vamos a producir una distribución adecuada para cuando volvamos a
cargar el directorio. Esta distribución 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 simétrico y se muestra en la figura 8.11 en la siguiente
página.
276 Ordenamientos usando estructuras de datos
8.3.6. Búsquedas
$ !
'
' À
subárbol vacı́o regresa nulo
'
'
'
' !
'
' À
cadena en raı́z regresa la raı́z
'
' #
'
&Hay árbol izquierdo donde Ð Busca cadena en
Busca cadena
en subárbol '
'
subárbol izquierdo
'
' !
'
' donde nulo
'
' À Regresa donde
'
' #
'
%Hay subárbol derecho Regresa búsqueda en
subárbol derecho
'
' $
'
' &
'Hay hijo izquierdo H
'
' %
' $
' ' subcadena en !
'
' '
' Reporta el registro
& & Àraı́z
'
Visita subárbol Procesa la raı́z
' '
' #
'
' '
% raı́z
subcadena en
'
' # H
'
'
'
Visita subárbol
'
'
Hay hijo derecho
À derecho
'
' $
'
' &
'
'
%Hay hijo derecho
%H
Código 8.21 Listado de registros que contienen a una subcadena (ArbolOrden) 1/2
227: /∗ ∗ Imprime 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 ó 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 á l campo s e d e s e a com 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 ( "Nohay registros enlabasede"
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 ( "Nohay registros quecacen." ) ;
243: }
8.3 *Ordenamiento usando árboles 281
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 eliminación de una hoja, pasemos a ver la parte más complicada,
que es la eliminación de un nodo intermedio. Veamos, por ejemplo, el árbol de la
figura 8.6 en la página 267 y supongamos que deseamos eliminar el nodo etiquetado
con “E”. ¿Cómo reacomodamos el árbol 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 subárbol derecho. ¿Por qué? Si colocamos al nodo menor del subárbol
derecho en lugar del que deseamos eliminar, se sigue cumpliendo que todos los que
estén en el subárbol izquierdo son menores que él, mientras que todos los que estén
en el subárbol derecho son mayores o iguales que él:
Para localizar el elemento menor del subárbol derecho simplemente bajamos a
la raı́z del subárbol 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 subárbol se encuentra en la figura 8.15 en la página opuesta.
La programación de este método se muestra en el listado 8.23.
8.3 *Ordenamiento usando árboles 283
$ $ !
'
' '
' À
El nodo es hoja Elimina a la raı́z
'
' '
'
'
' '
' #
'
' '
'
El nodo tiene sólo Pon al subárbol derecho
'
' '
' À
subárbol derecho en la raı́z
'
' '
& #
'
'
'
' '
nodo == raı́z El nodo tiene sólo
'
Pon al subárbol izquierdo
'
' '
' À
subárbol izquierdo en la raı́z
'
' ' $
' '
' '
'
' '
' &Localiza menor en subárbol derecho
' '
El nodo tiene
'
' % ambos hijos '
%
Intercambia a menor y nodo
'
' $ !
nodo
' '
' À
El nodo es hoja Anula el apuntador del padre
'
' '
'
'
' '
' #
'
' '
'
El nodo tiene sólo Sube al subárbol derecho
'
' '
' À
subárbol derecho al lugar del nodo
'
' & #
'
' '
nodo == raı́z El nodo tiene sólo
'
' '
'
Sube al subárbol izquierdo
'
' '
' À
subárbol izquierdo al lugar del nodo
'
' '
' $
'
' '
' '
&Localiza menor en subárbol derecho
'
' '
'
El nodo tiene
'
% % ambos hijos %Elimina a quien quedó en menor
Intercambia a menor y nodo
8.3 *Ordenamiento usando árboles 285
Por último, el mecanismo para modificar algún registro no puede ser, sim-
plemente, modificar la información, pues pudiera ser que la llave cambiara y el
registro quedara fuera de orden. La estrategia que vamos a utilizar para modificar
la información de un registro es primero borrarlo y luego reinsertarlo, para evitar
que se modifique la llave y se desacomode el árbol.
Todos hemos padecido en algún momento errores de ejecución. Java tiene me-
canismos muy poderosos para detectar errores de ejecución de todo tipo, a los que
llama excepciones. Por ejemplo, si se trata de usar una referencia nula, Java ter-
minará (abortará) el programa con un mensaje de error. Generalmente el mensaje
tiene la forma:
En el primer renglón del mensaje Java nos dice el tipo de excepción que causó que
el programa “abortara”, que en este caso es NullPointerException, y a partir del
segundo renglón aparece la “historia” de la ejecución del programa, esto es, los
registros de activación montados en el stack de ejecución 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 tamaño declarado (ArrayIndexOutOfBoundsException) o una división
entre 0 (ArithmeticException).
290 Manejo de errores en ejecución
La clase Exception es una clase muy sencilla que tiene, realmente, muy po-
cos métodos. De hecho, únicamente 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 métodos más que se pueden invocar desde
cualquier excepción, ya sea ésta de Java o del programador. Veamos primero los
dos constructores de Exception.
public Exception() Es el constructor por omisión. La única información que pro-
porciona es el nombre de la excepción.
public Exception(String msg) Construye el objeto pasándole una cadena, que pro-
porciona información adicional a la del nombre de la clase.
3
en adelante JVM.
9.3 Cómo detectar y cachar una excepción 295
Constructores:
public Throwable() Es el constructor por omisión.
public Throwable(String msg) Da la oportunidad de construir el objeto con infor-
mación que se transmite en la cadena.
Método 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 excepcióny
} catch ( xtipo de excepcióny xidentify ) {
xEnunciados que reaccionan frente a la excepcióny
}
Semántica:
Se pueden agrupar tantos enunciados como se desee en la cláusula 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
excepción que califique con ser del tipo especificado en las cláusulas catch.
de cada clase de excepción que se pudiera presentar en el cuerpo del try. El tipo
de excepción puede ser Exception, en cuyo caso cualquier tipo de excepción va
a ser cachada y manejada dentro de ese bloque. En general, una excepción 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
excepción 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 únicamente dentro del bloque.
Se ejecuta el bloque enunciado por enunciado.
En el momento en que se presenta una excepción, la ejecución del bloque se
interrumpe y la ejecución prosigue buscando a un manejador de la excepción.
La ejecución verifica los manejadores, uno por uno y en orden, hasta que
encuentre el primero que pueda manejar la excepción. Si ninguno de los
manejadores puede manejar la excepción que se presentó, 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 excepción, la ejecución continúa en el enunciado
que sigue al último manejador.
El programa que se encuentra en el listado 9.4 cacha las excepciones posibles
en el bloque del try mediante una cláusula catch para la superclase Exception. Una
vez dentro del manejador, averigua cuál fue realmente la excepción que se disparó,
la reporta y sigue adelante con la ejecución del programa.
javac DivPorCeroUso.java
DivPorCeroUso.java:7: unreported exception DivPorCeroException;
must be caught or declared to be thrown
throw new DivPorCeroException("¡Se pide raı́z de número
negativo!");
^
1 error
Una vez corregido esto, la llamada al método sqrt tiene que aparecer dentro
de un bloque try que se encargue de detectar la excepción que lanza el método.
Estos cambios se pueden ver en los listados 9.8 en la siguiente página y 9.9 en la
siguiente página.
302 Manejo de errores en ejecución
en este caso es main, la excepción no se propaga hacia afuera de main, por lo que
el método no tiene que avisar que pudiera lanzar una excepción.
Como ya mencionamos antes, las excepciones se pueden lanzar en cualquier
momento: sin simplemente un enunciado más. Por supuesto que un uso racional
de ellas nos indica que las deberemos asociar a situaciones no comunes o crı́ticas,
pero esto último tiene que ver con la semántica de las excepciones, no con la
sintaxis.
Tal vez el ejemplo del listado 9.9 en la página opuesta no muestre lo útil que
pueden ser las excepciones, porque redefinen de alguna manera una excepción
que la JVM lanzarı́a de todos modos. Pero supongamos que estamos tratando de
armar una agenda telefónica, donde cada individuo puede aparecer únicamente
una vez, aunque tenga más de un teléfono. Nuestros métodos de entrada, al tratar
de meter un nombre, detecta que ese nombre con la dirección ya está registrado.
En términos generales, esto no constituye un error para la JVM, pero si para el
contexto de nuestra aplicación. Una manera elegante de manejarlo es a través de
excepciones, como se muestra en los listados 9.10 a 9.12 en la siguiente página.
El listado 9.12 en la página opuesta hace uso del hecho de que cuando en un
bloque se presenta una excepción, la ejecución salta a buscar el manejador de la
excepción y deja de ejecutar todo lo que esté entre el punto donde se lanzó la
excepción y el manejador seleccionado. Como tanto agrega como elimina lanzan
excepciones, su invocación tiene que estar dentro de un bloque try – que va de la
lı́nea 24: a la lı́nea 35:. Si es que estos métodos lanzan la excepción, ya sea en la
lı́nea 26: o 29:, ya no se ejecuta la lı́nea 27: en el primer caso y la lı́nea 30: en el
segundo. Por lo tanto se está dando un control adecuado del flujo del programa,
utilizando para ello excepciones.
Otra caracterı́stica que tiene este segmento de aplicación es que como el bloque
try está dentro de una iteración, y si es que se hubiere lanzado una excepción en
alguno de los métodos invocados, una vez que se llegó al final del bloque try y
habiéndose o no ejecutado alguno de los manejadores de excepciones asociados al
bloque try, la ejecución regresa a verificar la condición del ciclo, logrando de hecho
que el programa no termine por causa de las excepciones. Esta forma de hacer las
cosas es muy común. Supongamos que le pedimos al usuario que teclee un número
entero y se equivoca. Lo más 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 omisión, los que no tienen ningún
parámetro.
El constructor por omisión siempre nos va a informar del tipo de excepción que
fue lanzado (la clase a la que pertenece), por lo que el constructor de Exception
que tiene como parámetro una cadena no siempre resulta muy útil. Sin embargo,
podemos definir una clase tan compleja como queramos, con los parámetros que
queramos en los constructores. Únicamente hay que recordar que si definimos
alguno de los constructores con parámetros, automáticamente perdemos acceso
al constructor por omisión. 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 más méto-
dos o información que la que nos provee la clase Exception o su superclase Th-
rowable. Por ejemplo, la clase RegNoEncontradoException que diseñamos para la
base de datos pudiera proporcionar más información al usuario que simplemente el
mensaje de que no encontró al registro solicitado; podrı́a proporcionar los registros
inmediato anterior e inmediato posterior al usuario. En ese caso, deberı́a poder
armar estos dos registros. Para ello, podrı́amos agregar a la clase dos campos, uno
para cada registro, y dos métodos, 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 página opuesta podemos ver un bosquejo de cómo se lograrı́a esto.
excepción, y que son llenados por el constructor, para proveer más información al
usuario de la situación presente en el momento de la excepción. Haciéndolo de esta
manera, en el momento de lanzar la excepción se puede invocar un constructor
que recoja toda la información posible del contexto en el que es lanzada, para
reportar después en el manejador.
Únicamente 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 información desde el
punto donde se lanza la excepción al punto donde se maneja, lo mejor es pasarla
en la excepción misma. Esto último se consigue redefiniendo y extendiendo a la
clase Exception.
Además de la información que logremos guardar en la excepción, tenemos
también los métodos de Throwable, como el que muestra el estado de los registros
de activación en el stack, o el que llena este stack en el momento inmediato anterior
a lanzar la excepción. Todos estos métodos se pueden usar en las excepciones
creadas por el programador.
Veamos en los listados 9.15 y 9.16 otro ejemplo de declaración y uso de ex-
cepciones creadas por el programador. En este ejemplo se agrega un constructor
y un atributo que permiten a la aplicación recoger información 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 última pasada al código,
de tal manera de garantizar que todo quede en un estado estable. No siempre es
necesario, ya que Java cuenta con recolección automática de basura y destructores
de objetos también automáticos. 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 excepción o no. Veamos un ejemplo con unos interruptores
eléctricos en el listado 9.19.
III. Calcular algún resultado alternativo en lugar del que el método se supone que
debı́a haber calculado.
IV. Hacer lo que se pueda en el contexto actual y relanzar la excepción para que
sea manejada en un contexto superior.
VIII. Hacer una aplicación (o biblioteca) más segura (se refleja a corto plazo en la
depuración y a largo plazo en la robustez de la aplicación).
Con esto damos por terminado este tema, aunque lo usaremos extensivamente
en los capı́tulos 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 aplicación de la máquina virtual (que termina) la información
que generamos no vive más allá. No tenemos manera de almacenar lo que cons-
truimos en una sesión para que, en la siguiente sesión, empecemos a partir de
donde nos quedamos. Prácticamente en cualquier aplicación que programemos y
usemos vamos a requerir de mecanismos que proporcionen persistencia a nuestra
información.
En los lenguajes de programación, 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 aplicación 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. También hemos usado en algunos ejemplos del capı́tulo
anterior dos archivos (objetos) que están 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, también en consola, pero de errores. Por ejemplo,
cuando un programa que aborta reporta dónde se lanzó la excepción, el reporte
322 Entrada y salida
lo hace a System.err.
La razón por la que usamos nuestra propia clase hasta el momento es que en
Java prácticamente 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 operación que tenga que ver con archivos, esta operación 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 métodos de
estas clases ya no haya que vigilar las excepciones.
El diseñar los métodos de entrada y salida para que lancen excepciones en
caso de error es no sólo conveniente, sino necesario, pues es en la interacción con
un usuario cuando la aplicación puede verse en una situación no prevista, como
datos erróneos, 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 carácteres (ya sea de 8 o
16 bits). En el caso de los flujos de entrada, éstos proporcionan carácteres, uno
detrás 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 carácteres para que sean proporcionados al
mundo exterior, también de manera secuencial.
En las figuras 10.1 y 10.2 en la página opuesta vemos los algoritmos generales
para lectura y escritura, no nada más para Java, sino que para cualquier lenguaje
de programación.
Dispositivo D
A
T Leer
O S Aplicación
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 jerarquı́as.
DataOutput
OutputStream ObjectOutput
ObjectOutputStream
ByteArrayOutputStream
PipedOutputStream
FileOutputStream
FilterOutputStream
PrintStream
BufferedOutputStream
DataOutputStream
BufferedWriter
CharArrayWriter
FilterWriter
Writer PrintWriter
PipedWriter
StringWriter
OutputStreamWriter FileWriter
330 Entrada y salida
StringReader
CharArrayReader
PipedReader
Reader
BufferedReader LineNumberReader
FilterReader PushbackReader
InputStreamReader FileReader
Filtro
Origen Destino
También estas jerarquı́as corren paralelas a las que trabajan con bytes, por lo
que no daremos una nueva explicación de cada una de ellas. Se aplica la misma
10.4 Entrada y salida de carácteres 331
explicación, excepto que donde dice “byte” hay que sustituir por “carácter”. Úni-
camente explicaremos aquellas clases que no tienen contra parte en bytes.
Vale la pena hacer la aclaración 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 carácter (16 bits). Por lo demás 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 jerarquı́as (las de bytes y las
de carácteres) definen prácticamente los mismos métodos para bytes y carácteres,
pero para fomentar la portabilidad de las aplicaciones se ha optado por soportar
de mejor manera las clases relativas a carácteres.
Sin embargo, como ya mencionamos, la entrada y salida estándar de Java es
a través de clases que pertenecen a la jerarquı́a 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 menú y de esta manera
ir abriendo las cajas negras que nos proporcionaba la clase Consola para este fin.
Por ser objetos estáticos de la clase se pueden usar sin construirlos. Todo
programa en ejecución cuenta con ellos, por lo que los puede usar, simplemente
refiriéndose a ellos a través 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 jerarquı́a de
clases que mostramos en las figuras 10.5 en la página 325 y 10.6 en la página 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 dejándole al usuario la tarea de pegar los
bytes para interpretarlos. Más adelante revisaremos con cuidado todos los méto-
dos de esta clase. Por el momento únicamente revisaremos los métodos que leen
byte por byte, y que son:
Como podemos ver de los métodos de la clase InputStream, son muy primitivos
y difı́ciles de usar. Por ello, como primer paso en la inclusión de entrada y salida
completa en nuestra aplicación, para entrada utilizaremos una subclase de Reader,
BufferedReader, más actual y mejor soportada.
Esta es una clase abstracta que deja sin implementar uno de sus métodos. El
constructor y los métodos se listan a continuación:
Antes:
import i c c 1 . i n t e r f a z . C o n s o l a ;
Después:
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 todavı́a, IOException está en este paquete
y pudiéramos 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" ) ;
}
Después:
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 método únicamente maneja el de salida,
por lo cambiamos su parámetros a que sea un flujo de salida del mismo tipo
que son out y err.
338 Entrada y salida
Antes:
S t r i n g nombre = c o n s . l e e S t r i n g ( "Dameelnombredel"
+ " estudiante empezando por"
+ " apellido paterno:" ) ;
Después:
S t r i n g nombre ;
System . o u t . p r i n t ( "Dameelnombredelestudiante ,"
+ " empezando por apellido paterno:" ) ;
// Acá 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 ( "Eligeunaopción-->" ) ;
Después:
System . o u t . p r i n t l n ( menu ) ;
System . o u t . p r i n t ( "Eligeunaopción-->" ) ;
// Acá v i e n e l o de l a l e c t u r a de l a o p c i ó 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"
+ "Hastapronto ..." ) ;
Después:
case 0 : // S a l i r
System . o u t . p r i n t l n ( "Espero haberte servido.z n"
+ "Hastapronto ..." ) ;
10.5 El manejo del menú de la aplicación 339
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 ) ;
Después:
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 ( "Damela subcadena a"+
"buscar:" ) ;
do {
s c u a l = c o n s . l e e S t r i n g ( "Ahoradimedecuálcampo:"
+ "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 ( "Opciónnoválida" ) ;
} while ( c u a l < 1 ) ;
Después:
try {
System . o u t . p r i n t ( "Damela 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 ( "Ahoradimedecuálcampo:"
+ "1: Nombre2: Cuenta3: Carrera 4: Clave" ) ;
// L e e r o p c i ó 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 ( "Opciónnoválida" ) ;
} while ( c u a l < 1 ) ;
340 Entrada y salida
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 ( "Erroraldarlosdatos"
+ "parabuscar" ) ;
} // 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 ) ;
Después:
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 parámetro 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 ( "Nodisteunaopciónválida .\n" +
"Porfavorvuelveaelegir." ) ;
return 0;
Después:
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 ( "Nodisteunaopciónválida .\n"
+ "Porfavorvuelveaelegir." ) ;
Antes:
import i c c 1 . i n t e r f a z . C o n s o l a ;
Después:
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 aR eg i s t ro ( ) ) ;
...
c o n s . i m p r i m e l n ( "Nohay registros enlabasededatos" ) ;
Después:
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 ( "Nohay registros enlabasededatos" ) ;
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 aR eg i s t ro ( ) ) ;
...
c o n s . i m p r i m e l n ( "Nose encontró ningún registro "
+ "quecazara" ) ;
Despué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 ) {
...
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 ( "Nose encontró ningún registro "
+ "quecazara" ) ;
342 Entrada y salida
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 ) ;
Después:
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
344 Entrada y salida
Constructor:
protected FilterInputStream(InputStream in)
Construye el flujo con in como flujo subyacente.
Métodos:
Hereda los métodos 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 información.
No existe una clase paralela a ésta en la jerarquı́a de Reader. Sin embargo, al re-
visar la documentación vemos que nuestro método favorito, readLine(), está anun-
ciado como descontinuado (deprecated ), lo que quiere decir que no lo mantienen al
dı́a y lo usarı́amos arriesgando problemas. Pero también en la documentación vie-
ne la sugerencia de usar BufferedReader, subclase de Reader, por lo que volteamos
hacia ella para resolver nuestros problemas. Presentamos nada más lo agregado
en esta clase, ya que los métodos 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 métodos que sobreescribe (overrides) ya que tienen los mismos
parámetros y el mismo resultado, ası́ que los pensamos como heredados aunque
estén redefinidos en esta clase.
Como se puede ver, tampoco esta clase es muy versátil, pero como lo único que
10.5 El manejo del menú de la aplicación 347
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 ( "Dameelnombredelestudiante ,"
+ " empezando por apellidopaterno:" ) ;
nombre=c o n s . l e e S t r i n g ( ) ;
r e t u r n nombre ;
}
Después:
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 ( "Dameelnombredelestudiante ,"
+ " empezando por apellidopaterno:" ) ;
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 ( "Erroralleernombre" ) ;
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 ( "Eligeunaopción-->" ) ;
348 Entrada y salida
Después:
System . o u t . p r i n t ( "Eligeunaopción-->" ) ;
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 ( "Erroralleeropción" ) ;
throw new I O E x c e p t i o n ( "Favorde repetir elección " ) ;
} // 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 = pideClave ( 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 ) ) ;
Después:
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 = pideClave ( 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 ( "Erroralleerdatosdel"
+ " estudiante .\ nNosepudo 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 ) ;
10.5 El manejo del menú de la aplicación 349
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 + "\nHasido eliminado " ) ;
}
e l s e r e p o r t a N o ( nombre ) ;
Después:
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 + "\nHasido 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 ( "Nose proporcionaron bien"
+ "losdatos .\ nNosepudo 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 ( "Damela subcadena a"
+ "buscar:" ) ;
do {
s c u a l = c o n s . l e e S t r i n g ( "Ahoradimedecuál"
+ "campo:1: Nombre2: 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 ( "Opciónnoválida" ) ;
} while ( c u a l < 1 ) ;
350 Entrada y salida
Antes: (continúa. . . )
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 ) ;
Después:
case BUSCA : // Busca s u b c a d e n a
try {
System . o u t . p r i n t ( "Damela subcadena a"+
"buscar:" ) ;
subcad = cons . r e a d L i n e ( ) ;
do {
System . o u t . p r i n t ( "Ahoradimedecuál"
+ "campo:1: Nombre2: Cuenta"
+ "3: Carrera4: 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 ( "Opciónnoválida" ) ;
} 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 ( "Erroraldarlos"
+ "datosparabuscar" ) ;
} // 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 ( "Dala 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 ( "Ahoradimedecuálcampo:"
+ "1: Nombre2: Cuenta3: Carrera4: Clave-->" ) ;
10.5 El manejo del menú de la aplicación 351
Antes: (continúa. . . )
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 ( "Opciónnoválida" ) ;
} while ( c u a l < 1 ) ;
miCurso . losQueCazanCon ( System . out , c u a l , s u b c a d ) ;
Después:
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 ( "Dala 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 ( "Ahoradimedecuálcampo:"
+ "1: Nombre2: Cuenta3: 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 ( "Opciónnoválida" ) ;
} 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 ( "Erroraldarlosdatos"
+ "paralistar" ) ;
} // 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 ( ) ;
Después:
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 ) ) ;
352 Entrada y salida
Antes:
Después:
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 ( "Opciónmal 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 únicamente hemos trabajado con la consola o bien con redirec-
cionamiento de la consola, pero no hemos entrado a la motivación principal de
este capı́tulo 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
ejecución.
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 ocasión queremos que el flujo
subyacente sea un archivo en disco y no un flujo estándar. 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 jerarquı́a únicamente hemos
revisado la clase Reader, ası́ que procedemos a ver las otras clases de la jerarquı́a
que vamos a requerir.
Realmente el único método con el que hay que tener cuidado es el que escribe
un carácter (entero), porque el resto de los métodos se construyen simplemente
invocando a éste.
Las clases que heredan directamente de Reader y Writer son, respectivamente,
10.7 Persistencia de la base de datos 355
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 únicamente se definen los constructores,
ya que se heredan precisa y exactamente los métodos implementados en InputS-
treamReader.
Para los flujos de salida que escriben a disco tenemos una situación similar a
la de archivos de entrada, pues lo único que se define para la subclase FileWri-
ter son los constructores. Para el resto de los métodos y campos se heredan las
implementaciones dadas por OutputStreamWriter. Veamos la definición.
358 Entrada y salida
Hay que recordar que la herencia permite que donde quiera que aparezca una
clase como parámetro, los argumentos pueden ser objetos de cualquiera de sus sub-
clases. Con esto en mente pasamos a implementar las opciones en el menú de leer
de un archivo en disco o escribir a un archivo en disco para guardar la información
generada en una sesión dada.
10.7 Persistencia de la base de datos 359
nicarnos con el usuario. Queremos que el mensaje sea preciso respecto a qué vamos
a hacer con el archivo, pero como usamos el mismo método simplemente le pasa-
mos de cual caso se trata caso para que pueda armar el mensaje correspondiente
(lı́neas 59: a 64:). Después entramos a un bloque try. . . catch en el que vamos a
leer del usuario el nombre del archivo. El método, como lo indica su encabezado,
exporta la excepción que pudiera lanzarse al leer el nombre del archivo. Estamos
listos ya para programar el algoritmo de la figura 10.10 en la página opuesta. El
código lo podemos ver en el listado 10.2.
En las lı́neas 202: y 203: solicitamos el nombre del archivo a usar y procedemos
a abrir el archivo. En este punto la única excepción que pudo haber sido lanzada
es en la interacción con el usuario, ya que la apertura de un archivo en disco
difı́cilmente va a lanzar una excepción.
El algoritmo para leer registros de una archivo en disco es la imagen del proceso
para guardar. Éste se puede ver en la figura 10.11.
Para identificar el archivo del que vamos a leer usamos el mismo método,
excepto que con un mensaje apropiado. Al abrir el archivo automáticamente nos
encontraremos frente al primer registro. A partir de ahı́, suponemos que el archivo
está correcto y que hay cuatro cadenas sucesivas para cada registro que vamos a
leer. El código que corresponde a esta opción se encuentra en el listado 10.3 en la
página opuesta.
10.7 Persistencia de la base de datos 363
$ $Identificar archivo
'
' '
'
'
' &Abrir el archivo
'
'
Inicio
'
'
' '
%Colocarse aldelprincipio
Leer registros & archivo
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
'
Código 10.3 Opción 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 ( "Nopude 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 ( "Nohubo carrera " ) ;
258: } // end o f i f ( c a r r e r a == n u l l )
364 Entrada y salida
Código 10.3 Opción 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 ( "Nohubo 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 ( "Nohuboclave" ) ;
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 ( "Nopude 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 lı́neas 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 él.
En las lı́neas 307: a 313: se usa el método append en lugar del método 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 tamaño de
cada campo. Podemos suponer que esto es ası́, agregando a la clase InfoEstudiante
un arreglo con esta información:
s h o r t [ ] tamanhos = { 4 , 4 0 , 9 , 4 , 2 0 } ;
quedando en tamanhos[0] el número de campos, en tamanhos[1] el tamaño del primer
campo y ası́ sucesivamente. Agregamos a la clase un método
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 tamaño del campo solicitado.
Podemos pensar en un archivo que es heterogéneo, en el sentido de que lo que
llamamos el encabezado del mismo no tiene la forma que el resto de los elementos;
éstos 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 sección: todos los
archivos en disco se componen de bytes; la manera de agrupar los bytes para
obtener información que tenga sentido depende del software que se use para verlo,
de las máscaras que le apliquemos al archivo. Una vez que terminemos de armar
nuestro archivo con el formato que acabamos de ver, podrán observar el archivo
con alguno de los visores de su sistema operativo y verán que también los primeros
2pn 1q bytes podrı́an tratar de interpretarlos como carácteres ASCII, no como
variables de Java; por supuesto que si hacen esto la mayorı́a de estos carácteres
no se podrán ver en pantalla (por ejemplo, el 0 binario) o aparecerán carácteres
que no guardan ninguna relación con lo que ustedes esperarı́an ver.
10.8 Escritura y lectura de campos que no son cadenas 369
XXX Y Y Y Descripción
Boolean boolean Escribe una booleana como un valor de
1 byte.
Byte int Escribe un byte que corresponde a la par-
te baja del entero
Bytes String Escribe la cadena como una sucesión de
bytes.
Char int Escribe un carácter (los 2 bytes más ba-
jos del entero), el byte alto primero.
Chars String Escribe la cadena como una sucesión de
carácteres (2 bytes por carácter).
Double double Convierte el double a un long usando
el método doubleToLongBits de la clase
Double y luego escribe el valor obtenido
como una sucesión de 8 bytes, el byte alto
primero.
Float float Convierte el valor float a un valor entero
(int) usando el método floatToIntBits de
la clase Float para luego escribirlo como
u7n entero de 4 bytes, el byte alto prime-
ro.
Int int Escribe un entero en 4 bytes, byte alto
primero.
Long long Escribe el entero largo en 8 bytes, byte
alto primero.
Short int Escribe el entero corto en 2 bytes (los
dos bytes más bajos del entero) byte alto
primero.
UTF String Escribe una cadena en el flujo usando
codificación UTF-8 modificada de manera
que es independiente de la computadora.
Como se puede ver, este flujo sirve para escribir en disco imágenes (copias)
del contenido de variables en memoria, siguiendo el patrón de bits dado
para su codificación binaria. Por esta última caracterización, a los archivos
creados con este tipo de flujos se les conoce como archivos binarios, esto
es, que los bytes deben interpretarse como si fueran variables en memoria.
10.8 Escritura y lectura de campos que no son cadenas 371
......
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 método que
maneja el menú. La razón de esto es que estas opciones se pueden elegir en cual-
quier momento y más de una vez, en cada ocasión con flujos fı́sicos distintos, por lo
que hacerlos globales a la clase o, peor aún, al uso de la clase, amarrarı́a a utilizar
únicamente el flujo determinado antes de empezar, cuando existe la posibilidad
de que no se elija esta opción o que, como ya mencionamos, se desee hacer varias
copias de l;a información. En las lı́neas 15:, 16: 156: a 170: simplemente agrega-
mos dos opciones al menú, y los mecanismos para manejarlas – posponemos por el
momento el desarrollo de la opción correspondiente dentro del switch –. La decla-
372 Entrada y salida
ración de los flujos la hacemos en las lı́neas 141: y 142:. Tanto en este caso como en
el los flujos BufferedReader y BufferedWriter podrı́amos 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 prácticamente paralelas, excepto
que cuando una escribe la otra lee. Pasemos a revisar primero la opción de escri-
tura, que serı́a el orden en que programarı́amos para probar nuestra aplicación.
10.8 Escritura y lectura de campos que no son cadenas 375
'
' '
%Pasar al siguiente de la lista
Escribir clave ajustada con blancos
'
'
'
'
%Cerrar el archivo
En las lı́neas 443: a 450: se verifica que la lista en memoria no esté vacı́a. De
ser ası́ se procede a obtener el descriptor de los campos (el encabezado del archivo
binario) en un arreglo de enteros pequeños (short); si la lista está vacı́a se sale del
menú lanzando una excepción, que es atrapada desde el método principal (main)
de la aplicación.
Se procede a escribir el encabezado del archivo binario en las lı́neas 452: a 455:
como lo indica el diagrama del algoritmo, utilizando para ello el método writeShort
de la clase DataOutputStream – ver documentación de la clase en las páginas 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 número de
bytes que indica el encabezado del archivo – en las lı́neas 459: y 460: se ajusta el
campo a su tamaño agregando blancos y en la lı́nea 463: se escribe utilizando el
método 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.
'
'
excepción
'
' $Leer número de campos en tamanhos[0]
' ''
Leer de
'
'
&Leer descriptor & Leer tamaño $
'
&
' '' de campo 'Leer de disco tamanhos[i]
archivo
'
de campos
% tamanhos[0] %
binario
'
'
en binario
'
' $Leer a nombre tamanhos[1] bytes
'
' ''
'
' ''Leer a cuenta tamanhos[2] bytes
'
' &Leer a carrera tamanhos[3] bytes
'
'
Procesar registro
''Leer a clave tamanhos[4] bytes
'
' mientras haya
''Agregar a la lista el registro
'
' % construido con estos datos
'
'
'
'
'
%Cerrar el archivo
Comparemos ahora el algoritmo con el código del listado 10.6. La parte que
corresponde a abrir el archivo y localizarlo se encuentra en las lı́neas 380: a 389:.
En esta opción sı́ procesamos por separado la excepción que nos pueda dar la
localización del archivo binario del que el usuario solicita leer porque es posible que
no exista dicho archivo. Por eso, en lugar de la tradicional excepción IOException,
acá tratamos de atrapar una excepción que nos indica que no se encontró el
archivo. Igual que en el caso anterior, sin embargo, salimos con una excepción del
método, pues tenemos que regresar a solicitar otra opción.
De manera similar a como lo hicimos para escribir, construimos un flujo de la
378 Entrada y salida
acá es 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 opción al
menú para poder acceder al i-ésimo registro, siempre y cuando se haga al principio
del proceso. Esto pudiéramos usarlo para descartar un cierto número de registros
y leer únicamente a partir de cierto punto.
6
Para poder hacer esto en archivos secuenciales tenemos que cerrar y volver a abrir el archivo
para que se vuelva a colocar al principio del mismo y poder saltar hacia el final del archivo. Otra
opción es que el archivo tenga implementado los métodos mark y reset.
382 Entrada y salida
En Java tenemos una clase que nos da esta facilidad y que es la clase Ran-
domAccessFile. Hay que tener presente que aunque esta clase maneja archivos en
disco no hereda de ningún 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
está 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 máquina virtual lanzará una excepción EOFException. Si se intenta
realizar alguna operación después de que el archivo fue cerrado, se lanzará una
IOException. Se tiene un método que se encarga de mover al apuntador, lo que
consigue que la siguen te lectura/escritura se lleve a cabo a partir de la posición a
la que se movió el apuntador. Estas posiciones son absolutas en términos de bytes.
Este tipo de archivos se pueden usar para lectura, escritura o lectura/escritura,
dependiendo de qué 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 también son implementadas por DataOutputStream y DataInputS-
tream, por lo que tendremos prácticamente los mismos métodos que ya conocemos
de estas dos últimas clases, todos en una única clase. Revisaremos únicamente
aquellos métodos que no conocemos todavı́a, dando por sentado que contamos
con los métodos para leer y escribir de DataInputStream y DataOutputStream.
Con esto ya tenemos las herramientas necesarias para acceder al disco con
acceso directo.
10.8 Escritura y lectura de campos que no son cadenas 385
Figura 10.15 Algoritmo para agregar registros desde archivo de acceso directo
$
'
' Pedir nombre de archivo
'
' Abrir archivo para#lectura
'
' tamanhos[0] Ð Primer short
'
'
Leer número de
'
'
campos del archivo
'
' $ $
'
' '
& '
&Leer siguiente
'
'
Calcular tamaño Sumar tamanhos[i]
'
' de registro '
% i=1,. . . , tamanhos[0] ' %Sumarlo
short
'
'
'
' $ $Leer cadena de
'
' '
' Obtener del usuario el '
'
'
' '
' & consola
'
' '
' número del registro
'
' ' % Procesar dı́gito
& '
solicitado
Agregar registros
'
'
mientras haya
desde archivo de
'
' '
' $
acceso directo '
' '
' &pos Ð
'
'
' '
'
Calcular posición en
'
%
tamR numR
'
' '
'
bytes
|encabezado|
'
' &
'
'
Procesar registro
(mientras se den) '
Colocar el apuntador en esa posición
'
' '
' $Leer nombre
'
' '
' '
'Leer cuenta
' ' &
'
' '
'
Leer registro desde
'
' ' disco '
%Leer carrera
'
' '
' Leer clave
'
' '
'
'
' '
' $
' ' '
&Construir registro
'
' '
'
Agregar a lista
% % registro leı́do '
%Agregar a lista
nuevo
386 Entrada y salida
Código 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 ) {
10.8 Escritura y lectura de campos que no son cadenas 387
Código 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 tamaño del encabezado (numBytes), el tamaño del
registro (tamR) y el número de registros lógicos en el archivo (fileSize) procedemos
a iterar, pidiéndole al usuario el número de registro, tantos como desee, del registro
que desea leer y agregar a la lista en memoria. Esto se lleva a cabo en las lı́neas
642: a 667: en el listado 10.10 en la siguiente página.
388 Entrada y salida
Elegimos leer del usuario una cadena, para evitar errores de lectura en caso
de que el usuario no proporcione un entero – lı́nea 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
cnxn cn1 xn1 . . . c0
Podemos pensar en un número en base b como un polinomio donde x b y
tenemos la restricción de que 0 ¤ ci b. En este caso para saber el valor en base
10 evaluamos el polinomio. La manera fácil y costosa de hacerlo es calculando
cada una de las potencias de b para proceder después a multiplicar ci por bi.
ņ
P p xq c i xi
i 0
Pero como acabamos de mencionar, esta es una manera costosa y poco elegante
10.8 Escritura y lectura de campos que no son cadenas 389
lo que nos permite evaluar el polinomio sin calcular previamente las potencias de
b. Por ejemplo, el número 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 dı́gitos 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 posición que ocupa, sino simplemente que
es el que está más a la izquierda. Dependiendo de cuántos dı́gitos se encuentren a
su derecha va a ser el número de veces que multipliquemos por 10, y por lo tanto
la potencia de 10 que le corresponde. Este algoritmo se encuentra codificado en
las lı́neas 655: a 658:.
En las lı́neas 651: a 654: verificamos que el usuario no esté proporcionando un
número negativo (que empieza con ’-’). Si es ası́, damos por terminada la sucesión
de enteros para elegir registros.
Quisiéramos insistir en que no importa si el flujo es secuencial o de acceso
directo, una lectura se hace siempre a partir de la posición en la que se encuentra
el flujo. Si se acaba de abrir esta posición 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 método skip se puede avanzar sin usar los bytes saltados,
pero siempre hacia adelante. En cambio, en los archivos de acceso directo se cuenta
390 Entrada y salida
con el comando seek que es capaz de ubicar la siguiente lectura o escritura a partir
de la posición dada como argumento, colocando el apuntador de archivo en esa
posición.
En el listado 10.11 se encuentra el código que corresponde a la ubicación del
apuntador del archivo, frente al primer byte del registro solicitado por el usuario
en la iteración actual.
Código 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 = numR∗tamR + 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 ( "Huboerroren 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
10.8 Escritura y lectura de campos que no son cadenas 391
'
' '
'
en esa posición
$Escribir nombre
'
' ' '
'
' '
' &Escribir cuenta
'
' '
'
Escribir campos
'
'
% '
%
en el disco '
%Escribir carrera
Escribir clave
10.8 Escritura y lectura de campos que no son cadenas 393
La lista que acabamos de dar dista mucho de ser exhaustiva, pero contiene la
suficiente información 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 definición de la clase ObjectOutputStream.
8
Reescribimos estas dos clases agregando al nombre Serial para mantener intacto el trabajo
que realizamos con anterioridad.
10.9 Lectura y escritura de objetos 403
......
En las lı́neas 9:, 13:, 30:, 50: y 56: se encuentran las consecuencias de haber
cambiado el nombre a ambas clases de las que hablamos. En la lı́nea 10: se en-
cuentra la declaración de que esta clase implementa a la interfaz Serializable, lo
que la hace susceptible de ser escrita o leı́da de un flujo de objetos.
Dado que la base de datos está compuesta por objetos de la clase Estudiante
y eso no lo queremos modificar, tenemos que dar métodos que conviertan de
EstudianteSerial a Estudiante y viceversa en la clase EstudianteSerial. Son métodos
sencillos que únicamente copian los campos. El código se puede ver en el listado
10.15.
......
219: case LEEROBJETOS :
220: m e n s a j e += "dedóndevasaleer objetos " ;
221: break ;
222: case GUARDAROBJETOS :
223: m e n s a j e += "endónde vasa escribir objetos " ;
224: break ;
......
406 Entrada y salida
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 prácticamente idéntico al de leer registros de un archivo directo, excepto
que por el hecho de leer objetos la máquina virtual se encarga de interpretarlos.
$ #
'
' Abrir el archivo
'
'
Inicio
'
Colocarse al principio de la lista
'
' $Convertir al estudiante actual
& '
Escritura de Objetos
' '
& en estudiante serial
'
'
Tomar registro
' (mientras haya) '
'
'
' %Escribirlo al flujo de objetos
'
%Cerrar el archivo Tomar como actual al siguiente
10.9 Lectura y escritura de objetos 407
10.10 Colofón
La clase PingPong podrı́a no tener método main. Lo ponemos para hacer una
demostración de esta clase. En la lı́nea 22: nuevamente invocamos a los métodos
414 Hilos de ejecución
currentThread y getName para que nos indiquen el nombre del hilo donde aparece
esa solicitud. A continuación creamos dos objetos anónimos y les solicitamos que
cada uno de ellos inicie su hilo de ejecución con el método start().
La ejecución del método main de esta clase produce la salida que se observa
en la figura 11.1 en la página anterior.
Como se puede observar en la figura 11.1 en la página anterior, mientras que
el nombre del proceso que está ejecutándose inmediatamente antes de lanzar los
hilos de ejecución se llama main, que es el que tiene, los hilos de ejecución lanzados
tienen nombres asignados por el sistema. Podı́amos 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 ésta sea el nombre del hilo de ejecución. 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 página opuesta.
ejecución.
Debe quedar claro que en el caso de hilos de ejecución, cuando uno termina su
ejecución no es que “regrese” al lugar desde el que se le lanzó, sino que simplemente
se acaba. El hilo de ejecución, 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 omisión. Construye un hilo de ejecución anónimo, al
que el sistema le asignará nombre. Cuando una clase que extiende a Thread
no invoca a ningún constructor de la superclase, éste es el constructor que
se ejecuta.
public Thread(String name)
Éste construye un hilo de ejecución y la asigna el nombre dado como argu-
mento.
418 Hilos de ejecución
cronizados o no, sobre el mismo objeto procederá sin problemas, ya que el hilo
de ejecución posee el candado respectivo. El candado se libera cuando el hilo de
ejecución termina de ejecutar el primer método sincronizado que invocó, que es el
que le proporcionó el candado. Si esto no se hiciera ası́, cualquier método recursivo
o de herencia, por ejemplo, bloquearı́a la ejecución llegando a una situación en la
que el hilo de ejecución no puede continuar porque el candado está comprometi-
do (aunque sea consigo mismo), y no puede liberar el candado porque no puede
continuar.
El candado se libera automáticamente en cualquiera de estos tres casos:
Sintaxis:
xenunciado de sincronizacióny::= synchronized( xexpry) {
xenunciadosy
}
Semántica:
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 más de un objeto, se recurrirá a enunciados de estos
anidados. El candado quedará liberado cuando se alcance el final de los
enunciados, o si se lanza alguna excepción dentro del grupo de enunciados.
La forma general para que un proceso espere a que una condición 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 ó 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 ejecución, la condición puede cambiar de estado
aún cuando este hilo de ejecución no esté haciendo nada. Sin embargo, es
importante que estos enunciados estén sincronizados. Si estos enunciados no
están sincronizados, pudiera suceder que el estado de la condición se hiciera
verdadero, pero entre que este código sigue adelante, algún otro proceso hace
que vuelva a cambiar el estado de la condición. Esto harı́a que este proceso
trabajara sin que la condición sea verdadera.
La definición de wait pide que la suspensión del hilo de ejecución y la libera-
ción del candado sea una operación atómica, esto es, que nada pueda suceder
ni ejecutarse entre estas dos operaciones. Si no fuera ası́ podrı́a 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 notificación se
podrı́a perder.
La condición que se está probando debe estar siempre en una iteración, para
no correr el riesgo de que entre que se prueba la condición una única vez y
se decide qué hacer, la condición podrı́a cambiar otra vez.
Por otro lado, la notificación de que se cambió el estado para una condición
generalmente toma la siguiente forma:
synchronized v o i f changeCondition ( ) {
// . . . Cambia a l g ú 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 ó n
notifyAll (); // o b i e n n o t i f y ( )
}
Si se usa notifyAll() todos los procesos que están en estado de espera – que eje-
cutaron un wait() – se van a despertar, mientras que notify() únicamente despierta
a un hilo de ejecución. Si los procesos están esperando distintas condiciones se
debe usar notifyAll(), ya que si se usa notify() se corre el riesgo de que despierte un
proceso que está esperando otra condición. En cambio, si todos los procesos están
esperando la misma condición y sólo uno de ellos puede reiniciar su ejecución, no
es importante cuál de ellos se despierta.
426 Hilos de ejecución
Como sabemos a estas alturas, en los sistemas Unix las impresoras están conec-
tadas a los usuarios a través de un servidor de impresión. Cada vez que un usuario
solicita la impresión de algún trabajo, lo que en realidad sucede es que se arma la
solicitud del usuario y se forma en una cola de impresión, 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 impresión se tiene que hacer
en al menos dos hilos de ejecución: 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 implementación de una cola genérica 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 implementación listas ligadas, ya que siendo genérica no hay una idea de
cuantos elementos podrán introducirse a la cola. Cuando se introduce algún ele-
mento a la cola en el método add, se notifica que la cola ya no está vacı́a. Cuando
se intenta sacar a un elemento de la cola, si la cola está vacı́a, el método take entra
a una espera, que rompe hasta que es notificado de que la cola ya no está vacı́a.
Con todas las clases que usa el servidor de impresión ya definidas, en el lista-
do 11.10 mostramos el servidor de impresión propiamente dicho.
Código 11.10 Servidor de impresión que corre en un hilo propio de ejecución
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 quedófueradelı́nea" ) ;
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: }
11.5 Comunicación entre hilos de ejecución 429
Una ejecución breve de este programa produce se puede ver en la figura 11.4
en la siguiente página.
El hilo de control en el que aparece el wait se duerme hasta que sucede una
de cuatro cosas:
430 Hilos de ejecución
Equivalente a wait(0).
Métodos de notificación:
public f i n a l void n o t i f y A l l ( )
Notifica a todos los procesos que están en estado de espera que alguna con-
dición cambió. Despertarán los procesos que puedan readquirir el candado
que están esperando.
public f i n a l void n o t i f y ( )
Notifica a lo más a un hilo esperando notificación, pero pudiera suceder que
el hilo notificado no estuviera esperando por la condición que cambió de
estado. Esta forma de notificación sólo se debe usar cuando hay seguridad
de quién está esperando notificación y por qué.
......
tema que del tipo de proceso. En general es aceptable asumir que el sistema le
dará preferencia a los procesos con más alta prioridad y que estén en estado de
poder ser ejecutados, pero no hay ninguna garantı́a al respecto. La única manera
de modificar estas polı́ticas de desalojo es mediante la comunicación de procesos.
La prioridad de un hilo de ejecución es, en principio, la misma que la del
proceso que lo creó. Esta prioridad se puede cambiar con el método public final
void setPriority(int newPriority) que asigna una nueva prioridad al hilo de ejecución.
Esta prioridad es un valor entero tal que cumple
MIN PRIORITY ¤ newPriority ¤ MAX PRIORITY
La prioridad de un hilo que se está 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 más raras,
como la alimentación de datos. Por ejemplo, cuando un usuario oprime el botón
de cancelar para un proceso, quiere que éste termine lo antes posible. Sin embargo,
si se están 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
más alta posible, puede suceder que evite que cualquier otro proceso se ejecute,
una situación que se conoce como hambruna (starvation).
Podemos ver dos ejecuciones con lı́neas de comandos distintas, una que provoca
el desalojo y la otra no. Sin embargo, en el sistema en que probamos este pequeño
programa, el resultado es el mismo. Deberı́amos ejecutarlo en varios sistemas para
ver si el resultado cambia de alguna manera. Veamos los resultados en la figura 11.5
en la página opuesta.
11.7 Abrazo mortal (deadlock ) 435
Siempre que se tienen dos hilos de ejecución y dos objetos con candados se
puede llegar a una situación conocida como abrazo mortal, en la cual cada proceso
posee el candado de uno de los objetos y está esperando adquirir el candado del
otro proceso. Si el objeto X tiene un método sincronizado que invoca a un método
sincronizado del objeto Y, quien a su vez tiene a un método sincronizado invocando
a un método sincronizado de X, cada proceso se encontrará 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 compañero.
Código 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: }
436 Hilos de ejecución
En las corridas que hicimos en nuestra máquina siempre pudo Lupe terminar
su intercambio antes de que Juana intentara el suyo. Sin embargo, pudiera suceder
11.7 Abrazo mortal (deadlock ) 437
que en otro sistema, o si la JVM tuviera que atender a más hilos de ejecución, ese
programita bloqueara el sistema cayendo en abrazo mortal.
Introduciendo un enunciado wait dentro del método reaApapacha conseguimos
que el proceso caiga en un abrazo mortal. Se lanza el hilo de ejecución con lupe,
pero como tiene que esperar cuando llega a reApapacha(), esto permite que también
el hilo de ejecución 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 ejecución de reApapacha, lo ninguno de los dos puede hacer – cada
uno está esperando a que termine el otro. La aplicación modificada se muestra
en el listado 11.15 y en la figura 11.7 en la siguiente página se muestra cómo se
queda pasmado el programa, sin avanzar ni terminar, hasta que se teclea un ˆ C.
Como se puede ver en las lı́neas 22, 23 y 26, lo único que se le agregó a esta clase
es que una vez dentro del método reApapacha espere 3 milisegundos, liberando el
candado. Este es un tiempo suficiente para que el otro hilo de ejecución se apodere
del candado. Acá es cuando se da el abrazo mortal, porque el primer hilo no puede
terminar ya que está 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 costó trabajo lograr el abrazo mortal, en un entorno donde se están
ejecutando múltiples aplicaciones a la vez no podrı́amos predecir que el segundo
hilo de ejecución no se apoderara del candado del objeto antes de que el primer
hilo lograra llegar a ejecutar el método reApapacha, por lo que tendrı́amos que
hacer algo al respecto.
Es responsabilidad del cliente evitar que haya abrazos mortales, asegurándose
del orden en que se ejecutan los distintos métodos y procesos. Esto lo conseguirá el
programador usando enunciados wait, yield, notify, notifyAll y sincronizando alre-
dedor de uno o más objetos, para conseguir que un proceso espere siempre a otro.
11.8 Cómo se termina la ejecución de un proceso 439
vivo: Un método está vivo desde el momento que es iniciado con start() y hasta
que termina su ejecución.
ejecutable: Na hay ningún obstáculo para que sea ejecutado, pero el procesador
no lo ha atendido.
Un método deja de estar vivo, como ya dijimos, por cualquiera de las causas
que siguen:
Es posible que se detecte que algo mal está sucediendo en cierto proceso y se
desee terminar su ejecución. Sin embargo, destroy deberı́a ser un último recurso, ya
que si bien se consigue que el proceso termine, pudiera suceder que no se liberen
los candados que posee el proceso en cuestión y se queden en el sistema otros
procesos bloqueados para siempre esperando candados que ya nunca van a poder
ser liberados. Es tan drástico y peligroso este método para el sistema en general
que muchas máquinas virtuales han decidido no implementarlo y simplemente
lanzan una excepción NoSuchMethodError, que termina la ejecución del hilo. Esto
sucede si modificamos el método apapacha como se muestra en el listado 11.16 en
la siguiente página.
440 Hilos de ejecución
// En e l h i l o 1
thread2 . i n t e r r u p t ()
desde un método distinto que el que se desea interrumpir. En el método por in-
terrumpir deberemos tener una iteración que en cada vuelta esté 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 cálculos complejos, que pudieran hacerse simultánea-
mente. Sin embargo, muchas veces no se sabe cuál de los dos cálculos se va a
tardar más, por lo que el proceso principal tendrá que esperar a que termine, en
su caso, el proceso que lanzó antes de poder utilizar los resultados del mismo.
Java tiene el método join() de la clase Thread que espera a que el proceso con
el que se invoca el método termine antes de que se proceda a ejecutar la siguiente
lı́nea del programa. Veamos en los listados 11.19 y 11.20 en la página opuesta un
ejemplo en el quesupuestamente se desean hacer dos cálculos que pueden llevarse
a cabo de manera simultánea. El método principal (main) crea un proceso paralelo
para que se efectúe uno de los cálculos mientras él ejecuta el otro. Una vez que
termina de ejecutar su propio cálculo, “se sienta” a esperar hasta que el proceso
que lanzó termine.
vez hecha la tarea que se podı́a realizar de manera simultánea, se espera para
garantizar que el coproceso terminó. 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 esté ejecutándose no quiere decir que el proceso no esté vivo.
Un proceso está vivo si está en espera, ya sea de alguna notificación, de que
transcurra algún tiempo, o de que la JVM continúe ejecutándolo. Por lo tanto, aún
cuando un proceso sea interrumpido por alguna de las condiciones que acabamos
de mencionar, el proceso seguirá vivo mientras no termine su ejecución.
Nos surge una pregunta natural respecto a la relación que existe entre los
procesos lanzados desde una aplicación, y el proceso que corresponde a la aplica-
ción misma: ¿qué pasa si el proceso principal termina antes de que terminen los
procesos que fueron lanzados por él?
La respuesta es más simple de lo que se piensa: el proceso principal no termina
hasta que hayan terminado todos los procesos que fueron lanzados por él. Bajo
terminar nos referimos a dejar de estar vivo, no forzosamente a estar ejecutando
algo. Esta situación, pensándolo bien, es lo natural. El proceso principal es aquel
cuyo método main se invocó. El resto de los procesos fueron invocados utilizando
start(). Esa es la única diferencia que hay entre ellos. Al lanzar procesos se genera
una estructura como de cacto, donde cada proceso tiene su stack de ejecución, que
es una continuación del stack de ejecución del proceso que lo lanzó. Por lo tanto,
11.9 Terminación de la aplicación 447
no puede terminar un proceso hasta en tanto todos los procesos que dependen de
él (que montaron su stack de ejecución 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 lanzó. En el caso de los demonios,
cuando termina el proceso que los lanzó, en ese momento y abruptamente se
interrumpen todos los demonios lanzados por ese proceso. Es como si aplicáramos
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 ejecución “normales” y los demonios. El proceso lanzador
no puede terminar hasta en tanto los hilos de ejecución iniciados por él no ter-
minen, mientras que los demonios se suspenden abruptamente cuando el proceso
que los lanzó termina.
Un proceso se puede hacer demonio simplemente aplicándole el método setDae-
mon(true) al hilo de ejecución antes de que éste inicie su ejecución. 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 está. La teorı́a es que al
terminar de ejecutarse el método main de esta clase, los demonios suspenderán
también su funcionamiento. Y en efecto ası́ es. La ejecución la podemos ver en la
figura 11.10 en la página opuesta.
Mostramos dos ejecuciones de la misma aplicación. En la primera ejecución,
se alcanzó a lanzar 5 demonios antes de que el proceso principal terminara. En la
segunda ejecución, únicamente se alcanzaron a lanzar 2 demonios. El número de
demonios que se logre lanzar dependerá de las polı́ticas de atención 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 ejecución de todos los demonios
que se hayan iniciado desde él, o desde procesos o demonios iniciados por él.
Si cambiamos la lı́nea 8: de esta aplicación para que Daemon sea un proceso
común y corriente con la lı́nea
8: setDaemon ( f a l s e ) ;
entre las lı́neas 37: y 38: de la clase DaemonSpawn, la ejecución de Daemon tiene
que terminar antes de que la aplicación pueda hacerlo, por lo que se alcanzan a
lanzar todos los demonios. Como no hay ninguna manera de que termine el proceso
Daemon, también el proceso principal nunca termina, hasta que tecleamos ctrl-C.
Si en las lı́neas 18: y 19: de la clase Daemon quitamos el ciclo y únicamente 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 página.
Dependiendo de la programación particular que dé la JVM, podrı́a suceder
que alguno de los hilos de ejecución causara una excepción, o que al suspenderse
alguno de los demonios abruptamente lanzara una excepción. Esto resulta en
que ejecuciones de la misma aplicación puedan dar distintos resultados cuando
estamos trabajando con hilos de ejecución, sobre todo si no se ejerce ninguna
sincronización, como es el caso del ejemplo con el que estamos trabajando.
Java proporciona dos métodos a utilizar cuando hay problemas con la ejecución
de coprocesos. Éstos son:
450 Hilos de ejecución
Entre los temas que ya no veremos en este capı́tulo por considerarlos más
apropiados para cursos posteriores, podemos mencionar:
Esperamos haber proporcionado una visión del potencial que tienen los hilos
de ejecución en Java. Una vez que estén realizando programación en serio, y en el
ambiente actual de procesos en red, tendrán que usarlos.
Bibliografı́a
[1] Ken Arnold and James Gosling. The Java Programming Language Third Edi-
tion. Addison-Wesley, 2001.
[2] José Galaviz Casas. Elogio a la pereza. Vı́nculos Matemáticos, Núm. 8, 2001.
[3] Sun Corporation. The source for java technology. web page.
[6] Canek Peláez and Elisa Viso. Prácticas para el curso de Introducción a Cien-
cias de la Computación I. Vı́nculos Matemáticos, por publicarse, 2002.
[8] Phil Sully. Modeling the world with objects. Prentice hall, 1993.
Esta obra terminó de imprimirse en
abril de 2007 en los talleres de
Publidisa Mexicana S. A. de C. V.
Calzada de Chabacano 69, planta alta.
México 06850, D. F.