Sei sulla pagina 1di 394

Introducción a la Programación

con Orientación a Objetos

Camelia Muñoz Caro


Alfonso Niño Ramos
Aurora Vizcaíno Barceló
INTRODUCCIÓN A LA PROGRAMACIÓN
CON ORIENTACIÓN A OBJETOS
INTRODUCCIÓN A LA
PROGRAMACIÓN
CON ORIENTACIÓN A OBJETOS

Camelia Muñoz Caro


Alfonso Niño Ramos
Aurora Vizcaíno Barceló
Universidad de Castilla-La Mancha

Madrid • México • Santafé de Bogotá • Buenos Aires • Caracas • Lima • Montevideo • San Juan • San José
Santiago • São Paulo • White Plains
Datos de catalogación bibliográfica

MUÑOZ CARO, C.; NIÑO RAMOS, A.;


VIZCAÍNO BARCELÓ, A.
Introducción a la programación con orientación a objetos
PEARSON EDUCACIÓN, S.A., Madrid, 2002

ISBN: 84-205-3440-4
MATERIA: Informática 681.3

Formato: 195 3 250 mm Páginas: 408

Todos los derechos reservados.


Queda prohibida, salvo excepción prevista en la Ley, cualquier forma de reproducción, distribución,
comunicación pública y transformación de esta obra sin contar con autorización de los titulares
de propiedad intelectual. La infracción de los derechos mencionados puede ser constitutiva de delito
contra la propiedad intelectual (arts. 270 y sgts. Código Penal).

DERECHOS RESERVADOS
© 2002 respecto a la primera edición en español por:
PEARSON EDUCACIÓN, S.A.
Núñez de Balboa, 120
28006 Madrid

MUÑOZ CARO, C.; NIÑO RAMOS, A.; VIZCAÍNO BARCELÓ, A.


Introducción a la programación con orientación a objetos

ISBN: 84-205-3440-4
ISBN eBook: 978-84-8322-584-4

PRENTICE HALL es un sello editorial autorizado de PEARSON EDUCACIÓN, S.A.

Edición en español:
Equipo editorial:
Editor: David Fayerman Aragón
Técnico editorial: Ana Isabel García
Equipo de producción:
Director: José A. Clares
Técnico: Diego Marín
Diseño de cubierta: Equipo de diseño de Pearson Educación, S.A.

Composición: JOSUR TRATAMIENTOS DE TEXTOS, S.L.


Powered by Publidisa

IMPRESO EN ESPAÑA - PRINTED IN SPAIN

Este libro ha sido impreso con papel y tintas ecológicos


1

Sistemas basados
en computador

Sumario

1.1. Introducción 1.4. Arquitectura clásica o de von Neumann


1.2. Concepto de computación de un computador
1.2.1. Definición de computación 1.5. Redes de computadores
1.2.2. Dispositivos de computación 1.5.1. Generalidades
1.3. Concepto de Informática 1.5.2. Internet
1.3.1. Definición de informática 1.5.3. La World-Wide-Web (WWW)
1.3.2. Datos e información
1.3.3. Representación de la información
1.3.4. Sistemas informáticos
1.3.5. Consideraciones sobre el software
2 Introducción a la programación con orientación a objetos

1.1. INTRODUCCIÓN
Este tema pretende proporcionar una visión global de los sistemas basados en computador 1 y de la
informática como disciplina. Desde la perspectiva de un texto introductorio como es éste, presentare-
mos el concepto de computación, así como una pequeña semblanza cronológica, histórica, de las téc-
nicas de computación que han desembocado en el ordenador moderno. A partir de aquí presentaremos
el concepto de informática, como campo de conocimiento, y de sistema basado en computador. En el
cuarto apartado se considera la estructura y funcionamiento genérico de los computadores modernos.
Por su interés actual y su relación con el lenguaje que se utilizará para la implementación de los ejem-
plos (lenguaje Java) se presenta el tema de las redes de computadores y de Internet.

1.2. CONCEPTO DE COMPUTACIÓN


En este apartado vamos a definir el concepto de computación y a presentar el desarrollo de las técni-
cas y dispositivos de computación que han conducido al ordenador moderno.

1.2.1. DEFINICIÓN DE COMPUTACIÓN


Como definición clásica de computación tenemos la dada por el Merriam-Websters’s Collegiate Dic-
tionary en su décima edición que define computación como el acto y acción de computar, donde com-
putar equivale a determinar, especialmente por medios matemáticos. En el mundo anglosajón,
originalmente un “computer” era una persona que realizaba cálculos para resolver un problema. Sólo
hacia 1945 el nombre se empieza a aplicar a la maquinaria que realiza dicha tarea (Ceruzzi, 1999).
Los computadores (ordenadores) actuales son todavía capaces de computar (resolver problemas por
medios matemáticos), especialmente en el campo científico-técnico donde ésta es su misión principal.
Sin embargo, la tremenda repercusión del ordenador en la vida actual no proviene sólo de su capacidad
de cómputo, sino de su capacidad para almacenar y recuperar datos, para manejar redes de comunica-
ciones, para procesar texto, etc. Es decir, de su capacidad para procesar información. Ésta es la causa
de la gran importancia del computador en la vida actual. Para la mayoría de la gente el computador
representa una forma eficiente de gestionar información, sea en forma de texto, cartas enviadas por
correo electrónico, informes generados automáticamente o transacciones de todo tipo. Para la mayor
parte de la población la realización de cálculos es lo “menos importante” que un computador realiza.
Sin embargo, los computadores se desarrollaron con el principal objetivo de realizar cálculos, aun-
que la potencia final y los usos del ingenio habrían sido impensables para muchos de los que a lo lar-
go del tiempo participaron en la tarea. Resulta interesante presentar el desarrollo histórico de los
medios de computación y el desarrollo del computador moderno. Esta presentación implícitamente lle-
va aparejado el desarrollo del concepto de programación como técnica para describir al computador,
en términos inteligibles para él, las tareas que se desea que realice.

1.2.2. DISPOSITIVOS DE COMPUTACIÓN


Los dispositivos originales de computación eran dispositivos de ayuda para la realización de cálculos
aritméticos (computación en sentido etimológico), que poco a poco incrementaron sus posibilidades
hasta llegar al concepto moderno de computador. Veamos brevemente la evolución histórica.

1
Según el Diccionario de la Lengua de la Real Academia Española los términos computador, ordenador y computado-
ra se pueden utilizar indistintamente. A lo largo de este libro así se usarán.
Sistemas basados en computador 3

Sistemas de numeración

El punto de partida para cualquier técnica de computación es la preexistencia de un sistema de nume-


ración. Esto no es en absoluto trivial, hoy por hoy la educación elemental enseña a los niños a contar
usando un sistema de notación decimal, y la técnica parece algo consustancial con la existencia huma-
na. Sin embargo, el concepto de número como una abstracción de las entidades numeradas es un paso
fundamental en la evolución cultural del hombre. Que no siempre esto ha sido así se puede todavía
observar en algunos lenguajes tribales donde se usan distintos nombres para las cantidades depen-
diendo de la naturaleza de lo numerado. Desde este punto de vista no es lo mismo cuatro piedras que
cuatro naranjas y el paso de abstracción fundamental es la consideración de que en ambos casos tene-
mos cuatro elementos. Una vez que se dispone de un sistema de numeración el siguiente paso es su
uso para contar elementos.

Dispositivos de cómputo antiguos

Una vez establecido un sistema de numeración, la raza humana ideó dispositivos de ayuda para
la realización de tareas aritméticas. En los primeros tiempos de la historia humana, para las ta-
reas más sencillas no era necesaria una habilidad aritmética más allá de sumas y restas simples o
de multiplicaciones sencillas. Al aumentar la complejidad de la vida en común, se incrementa la
complejidad de los cálculos aritméticos necesarios para los tratos comerciales, los impuestos, la
creación de calendarios o las operaciones militares. Para agilizar la realización de estos “cómpu-
tos” la primera ayuda es la de los dedos, para simbolizar cantidades e incluso realizar opera-
ciones.
Los dedos pueden usarse de la forma más simple para indicar una cantidad mostrando el núme-
ro de dedos equivalentes. Sin embargo, también pueden usarse de forma simbólica para representar
cantidades arbitrarias con combinaciones distintas de dedos mostrados u ocultados. Una primera
necesidad en la antigüedad fue la de disponer de un medio de representar cantidades que fuera cono-
cido por todos los pueblos (al menos en el entorno euro-asiático-africano clásico). Esta técnica se
usaba fundamentalmente para el intercambio comercial entre pueblos cuyas lenguas podían ser des-
conocidas entre sí. En la antigüedad clásica existió este sistema que usaba los dedos (de las dos
manos) para representar simbólicamente cantidades hasta de 9999 elementos y que estaba extendi-
do por el norte de África, Oriente Medio y Europa. Herodoto y otros autores más modernos como
Cicerón o Marco Fabio Quintiliano lo mencionan. Con los dedos se pueden aplicar técnicas de
cálculo más complejas que la simple enumeración de elementos. Por ejemplo, en Europa existieron
hasta épocas relativamente recientes, técnicas de multiplicación que usaban las manos para realizar
los cálculos.
Sin embargo, el medio mecánico más antiguo de realización de cálculos parece ser el ábaco (en
sus diferentes versiones). El ábaco es, en esencia, una tabla de conteo que puede ser tan simple
como una serie de piedras colocadas sobre el suelo. Su estructura típica es la de un marco de made-
ra con una serie de alambres donde se ensartan varias cuentas, véase la Figura 1.1. Esta herra-
mienta, a pesar de su simplicidad, es una gran ayuda de computación si se usa adecuadamente.
Actualmente se asocia el ábaco con Oriente pero se usó en Europa desde la antigüedad clásica has-
ta hará unos 250 años. La potencia del ábaco reside en que no es una simple tabla de anotaciones;
además se pueden realizar operaciones aritméticas con él. De hecho, la capacidad de cálculo con
un ábaco es muy alta. Un ejemplo de su eficacia es el siguiente: en 1946 en un concurso de velo-
cidad y precisión de cálculo, Kiyoshi Matsuzake del Ministerio de Administración Postal Japonés
derrotó con un ábaco, en cuatro de cinco ocasiones, al soldado Thomas Nathan Wood del ejército
americano de ocupación que era el operador más experto de máquina mecánica de calcular de la
marina de los EE.UU.
4 Introducción a la programación con orientación a objetos

Figura 1.1. Ábaco

En la evolución hacia el computador moderno, merece mención especial el escocés John Napier,
barón de Merchiston, quien inventó los logaritmos en el siglo XVII 2. Como herramienta de cálculo los
logaritmos permiten transformar las multiplicaciones en sumas y las divisiones en restas. Los logarit-
mos presentan una tremenda utilidad práctica, pero el problema del barón era la creación de las tablas
de logaritmos necesarias para su uso. En la historia de los medios de computación el barón es impor-
tante por haber diseñado varios instrumentos de cómputo. Entre ellos el denominado rabdologia, popu-
larmente conocido como “huesos” de Napier, que él usaba para calcular sus tablas. Estos “huesos”
eran una serie de barras cuadrangulares que en esencia representaban la tabla de multiplicar y que per-
mitían realizar esta operación. El apelativo de “huesos” deriva del aspecto que presentaban y del mate-
rial del que muchas veces estaban hechos. Otros dispositivos de cálculo de Napier fueron el Prontuario
de Multiplicación (una versión más elaborada de los “huesos”) y el dispositivo de Aritmética Local,
un tablero de ajedrez modificado para ser usado como una especie de ábaco que trabajaba en sistema
binario. Napier publicó un libro describiendo el manejo de la rabdologia. El libro titulado Rabdologia
muestra también una de las primeras menciones del punto 3 decimal. Los huesos de Napier se difun-
dieron con rapidez por Europa.
Sin embargo, en la historia de la computación la invención de los logaritmos dio nacimiento al que
probablemente haya sido el dispositivo de cómputo más usado desde el siglo XVII hasta la última mitad
del siglo XX: la regla de cálculo. El origen de la regla de cálculo es el siguiente. Tras conocer la inven-
ción de los logaritmos por Napier, Henry Briggs, Profesor de Geometría del Gresham College en Lon-
dres, comenzó a trabajar en el tema, introduciendo los logaritmos en base 10 y creando nuevas tablas
de logaritmos para los números enteros. Edmund Gunter, Profesor de astronomía y matemáticas tam-
bién en el Gresham College, conoció por Briggs la existencia de los logaritmos. Gunter estaba intere-
sado en problemas de astronomía y navegación, lo que implicaba el uso de funciones trigonométricas.
Dado que las tablas de logaritmos de Briggs eran para números enteros, no presentaban mucha utili-
dad para Gunter, quien decidió abordar el cálculo de tablas de logaritmos para senos y tangentes. Gun-
ter había trabajado en la popularización y desarrollo del compás de sector, un instrumento de cálculo

2
El apellido Napier se escribía de varias formas en la época: Napier, Napeir, Napair, Nepier y algunas formas más, y de
él toman el nombre los logaritmos en base e, los logaritmos neperianos.
3
Entre los anglosajones los decimales se indican con punto, no con coma.
Sistemas basados en computador 5

Figura 1.2. Regla de cálculo

consistente en un par de brazos unidos en un extremo por un pivote en forma de compás y con una
serie de escalas calibradas sobre cada uno. Puesto que el logaritmo de un producto es la suma de los
logaritmos, Gunter pensó en un sistema parecido al sector que permitiera realizar mecánicamente un
producto. Gunter ideó una regla graduada en escala logarítmica con un compás. Para multiplicar dos
números x e y se abría el compás la cantidad x, midiendo sobre la regla logarítmica. A continuación
se colocaba uno de los brazos del compás apoyado en el punto de la regla correspondiente al valor y.
Sin cerrar el compás se colocaba el otro brazo en la dirección creciente de la escala. El proceso equi-
valía a sumar las dos distancias en la escala (suma de logaritmos), así que el valor que señalaba el bra-
zo final del compás sobre la escala logarítmica era el producto de los dos números. Para hacer un
cociente se sustraían las distancias en lugar de sumarse. El dispositivo simplificaba el cálculo de pro-
ductos y cocientes y también evitaba tener que gastar tiempo buscando en las tablas de logaritmos.
La modificación final del dispositivo se debió a William Oughtred quien hoy sería definido
como un matemático puro y que se puede considerar el inventor de la versión definitiva de la regla
de cálculo. En una visita que realizó en 1610 a Henry Briggs, conoció a Edmund Gunter quien le
mostró su instrumento de cálculo logarítmico. Oughtred se dio cuenta de que se podía eliminar la
necesidad del compás si se usaban dos reglas graduadas logarítmicamente que se deslizaran una con
respecto a la otra. Así, para multiplicar los dos números x e y bastaba con colocar el origen de la
segunda escala sobre el punto del valor x en la primera, localizar el valor y en la segunda y mirar
cuál era el valor que correspondía en la primera escala. Este sistema de reglas deslizantes en escala
logarítmica es la base de todas las reglas de cálculo posteriores 4. Las reglas de cálculo evoluciona-
ron a lo largo del tiempo hasta adquirir forma muy sofisticada, véase la Figura 1.2, representando
un instrumento analógico de cómputo de precisión. Hasta la introducción de las calculadoras
electrónicas de mano, la regla de cálculo era un instrumento que todo ingeniero o científico usaba
en su trabajo cotidiano.

Dispositivos mecánicos
Aparte de los instrumentos manuales indicados en el apartado anterior, los intentos auténticos de com-
putación automática comienzan con el desarrollo de los distintos modelos de calculadoras mecánicas.
Sin embargo, el desarrollo práctico de estas máquinas tuvo que esperar hasta el siglo XVII cuando la

4
Por esta razón, en inglés la regla de cálculo se denomina sliding rule (regla deslizante).
6 Introducción a la programación con orientación a objetos

ingeniería mecánica estuvo lo suficientemente desarrollada como para permitir la construcción de los
sistemas de engranajes y palancas en los que se basa su funcionamiento.
La primera calculadora mecánica (sumadora) con un dispositivo de acarreo para tener en cuenta
que se ha conseguido pasar a una posición decimal superior (el “me llevo uno” de la aritmética ele-
mental) se atribuye a Blaise Pascal. Sin embargo, la primera fue realizada por el matemático alemán
Wilhelm Schickard a principios del siglo XVII quien comunicó su invención a Kepler, con quien había
colaborado. Por la descripción y los diagramas que Schickard remitió a Kepler se sabe que la máqui-
na de Schickard automatizaba el trabajo con una serie de huesos de Napier, realizando mecánica-
mente las sumas implicadas en la obtención de multiplicaciones. La máquina representaba cada
posición decimal con una rueda donde estaban representados los diez dígitos 1-2-3-4-5-6-7-8-9-0. El
problema de la máquina era el del acarreo acumulado cuando se pasaba de 9 a 0 en un disco y había
que mover el disco de la siguiente posición decimal en una unidad. Esto se realizaba con una rueda
dentada (rueda de acarreo) con un diente que hacía avanzar a la rueda a la nueva posición decimal
cuando la de la posición anterior daba una vuelta completa. El problema se entiende si imaginamos
que tenemos el valor 9999999 y sumamos 1. Habrá un total de 7 discos que tienen que girar a la vez
a base de ruedas dentadas con un solo diente (una por posición decimal). Es fácil entender que la rue-
da de acarreo del primer dígito debe aguantar el esfuerzo necesario para poder girar todos los demás
dígitos. En la práctica el sistema no podía aguantar el esfuerzo y se rompía si el número de posicio-
nes decimales era grande. Schickard sólo construyó máquinas con un máximo de seis posiciones
decimales.
Posteriormente y de forma independiente Pascal desarrolló una serie de ingenios mecánicos simi-
lares. La primera máquina fue diseñada cuando Pascal contaba 19 años. Cuando intentó que los arte-
sanos locales construyeran las piezas necesarias, el resultado fue tan catastrófico que decidió él mismo
aprender mecánica, e incluso trabajó con un herrero para aprender a manejar el metal y construir las
piezas. Pascal construyó unas cincuenta máquinas a lo largo de su vida, en esencia todas máquinas
sumadoras. El problema de Pascal para construir máquinas capaces de multiplicar era nuevamente
acumular el acarreo sobre varias posiciones decimales. Pascal ideó un sistema de pesos que evitaba el
sistema de engranajes de Schickard. El problema era que la máquina sólo podía avanzar sus engrana-
jes en un sentido. En la práctica esto se traducía en que la máquina sólo podía aumentar acarreos, no
disminuir o dicho de otra forma, sólo sumaba.
Otro interesante diseño es el de la máquina de Leibniz. Habiendo oído hablar de la máquina suma-
dora de Pascal, Leibniz se interesa por el tema y comienza con el diseño de una máquina multiplica-
dora. El diseño original no era factible y Leibniz abandona el tema durante varios años. Finalmente,
acaba construyendo una máquina multiplicadora operativa gracias a la invención de un elegante siste-
ma de engranajes con dientes de anchura variable (tambor escalonado). Otras calculadoras mecánicas
fueron construidas por personajes como el inglés Samuel Morland o el francés René Grillet ambos en
el siglo XVII.
Comercialmente, la primera calculadora mecánica de utilidad fue el aritmómetro de Thomas de
Colmar fabricado en la década de 1820 en Francia y basado en el diseño de tambor escalonado de
Leibniz. Sin embargo, el gran paso en la producción comercial de calculadoras mecánicas se da con
las máquinas de Baldwin-Odhner. El problema con las calculadoras mecánicas previas era que el sis-
tema de tambor escalonado de Leibniz resultaba un dispositivo pesado y engorroso que implicaba que
las máquinas fueran grandes y masivas. A finales del siglo XIX, Frank. S. Baldwin en EE.UU. y W. T.
Odhner, un suizo que trabajaba en Rusia, idearon un nuevo diseño para las ruedas dentadas que repre-
sentaban los dígitos decimales de cada posición decimal. La idea era que el número de dientes en las
ruedas fuera variable, correspondiendo el número de dientes al número representado. Estos dientes
podían aparecer o desaparecer según se seleccionaba un dígito u otro con una palanca colocada sobre
la propia rueda. Estas ruedas dentadas variables se podían construir como discos finos y ligeros, lo que
permitía colocar varios de estos discos, representando cada uno un dígito, en unos pocos centímetros
de espacio. El resultado era una máquina mucho más compacta y ligera que las existentes hasta enton-
ces, véase la Figura 1.3. A principios del siglo XX estas máquinas se vendían por decenas de miles.
Sistemas basados en computador 7

Figura 1.3. La Minerva, una máquina de Baldwin-Odhler de fabricación española

Hasta la introducción de las calculadoras electrónicas las máquinas de Baldwin-Odhner se seguían


usando en oficinas y laboratorios.

Las máquinas de Babbage


Los instrumentos mecánicos mencionados en el aparatado anterior no son sino ayudas mecánicas de
computación. El primer gran paso hacia lo que es el concepto moderno de computador, lo dio Charles
Babbage (1791-1871) con sus trabajos sobre computación automática. El nivel tecnológico de su tiem-
po no era suficiente para poder llevar a cabo sus diseños, pero las ideas de Babbage eran muy avan-
zadas para la época. Tanto es así que Babbage se considera uno de los pioneros del desarrollo del
computador moderno, al mismo nivel de Konrad Zuse o Howard Aitken que trabajaron en el desarro-
llo de los primeros modelos de computador en las décadas de 1930-1940.
Charles Babbage era matemático y hombre de ciencia en el sentido más general que esta palabra
tenía en el siglo XIX. En aquella época, se hacía amplio uso de tablas matemáticas como las de loga-
ritmos, por ejemplo, para reducir el trabajo de cálculo. Dada su formación e intereses, Babbage hacía
uso intensivo de las tablas matemáticas y era consciente de la gran cantidad de errores que poseían. La
pasión por la precisión de Babbage le llevó a abordar la construcción de tablas matemáticas libres de
errores. Para ello incluso diseñó sistemas tipográficos para reducir la probabilidad de la aparición de
errores en las tablas. Sin embargo, el problema era siempre el mismo. Una persona calculaba los valo-
res y escribía un borrador de la tabla con la consiguiente posibilidad de error. Luego, el borrador era
traducido a mano en tipos de imprenta para imprimir las tablas, con la adicional posibilidad de error.
Babagge llegó a la conclusión de que la única forma de evitar los errores humanos era automatizar
8 Introducción a la programación con orientación a objetos

todo el proceso. Su idea era la de una máquina capaz de calcular e imprimir sin intervención humana
las tablas matemáticas deseadas. En la época, las tablas se calculaban aproximando las funciones a cal-
cular por formas polinómicas y manejando las formas polinómicas con el método de diferencias. Usan-
do el método de diferencias para representar un polinomio se evitaba tener que realizar operaciones de
multiplicación y división. Este método puede aún verse explicado en textos de cálculo numérico apli-
cado al problema de la interpolación de funciones (Demidovich y Maron, 1977; Kopchenova y Maron,
1987). Babbage imaginó su máquina de cálculo de tablas como una Difference Engine, Máquina de
Diferencias, que aplicara de forma automática el método de diferencias. Babbage construyó un
pequeño prototipo y solicitó ayuda oficial para la construcción del diseño completo. El problema era
que la tecnología mecánica no estaba suficientemente avanzada en aquel entonces para la construcción
de algunas partes de la máquina. El mismo Babbage colaboró en el desarrollo de nuevas herramientas
de fabricación mecánica que permitieran construir las piezas de la máquina. Este trabajo adicional y
las demoras oficiales en la provisión de fondos hicieron que el trabajo se parara numerosas veces y
que finalmente la máquina no acabara de construirse. Durante uno de estos períodos de inactividad
Babbage trabajaba en un rediseño de la máquina y se le ocurrió que el resultado de las computaciones
de la máquina pudiera volver a ser introducido como dato en la propia máquina. Babbage se dio cuen-
ta de que ese diseño circular dotaba a la máquina de una potencia de cómputo mucho mayor que la del
modelo inicial. Un diseño tal permitía el manejo de funciones que no tenían solución analítica. Bab-
bage denominó la nueva máquina Analytical Engine (Máquina Analítica) y al respecto de la misma
escribió en una carta en mayo de 1835 (Williams, 2000):

... durante seis meses he estado dedicado a los diseños de una nueva máquina de cálculo de
mucha mayor potencia que la primera. Yo mismo estoy asombrado de la potencia de que he
podido dotar a esta máquina; hace un año no hubiera creído que este resultado fuera posible.

La máquina analítica constaba de tres partes que Babbage denominó:

The store (El almacén)


The mill (La fábrica o taller)
The control barrell (El cilindro o tambor de control)

El almacén era una memoria mecánica, el taller una unidad aritmética y el cilindro de control una
unidad de control de procesos que contenía el equivalente mecánico de un juego de instrucciones bási-
cas de trabajo. En esencia, el diseño de Babbage respondía a la estructura moderna de un computador.
El trabajo de la máquina analítica se realizaba indicándole qué acciones elementales tenía que realizar
por medio de una serie de tarjetas perforadas, de forma similar a como entonces se introducían los
diseños en las tejedoras mecánicas de Jackard. La máquina, por lo tanto, respondía a un programa de
instrucciones externo que leía como entrada.
La máquina analítica no llegó a construirse principalmente porque Babbage no consiguió los fon-
dos necesarios. Desde la perspectiva actual, la máquina hubiera supuesto un avance de alcance inima-
ginable en la época 5.
La necesidad de desarrollar el conjunto de instrucciones que compusieran un programa a ser eje-
cutado por la máquina analítica da carta de nacimiento a la ciencia y el arte de la programación. En
este contexto tiene especial interés la colaboración entre Babbage y Ada Augusta, condesa de Love-

5
Como fabulación de lo que habría ocurrido en caso de construirse la máquina analítica se recomienda la lectura de la
novela The Difference Engine (Gibson y Sterling, 1996). Aquí se nos muestra un siglo XIX alternativo, donde Babbage ha
podido construir sus máquinas y un imperio británico que ha conjugado la revolución industrial con la revolución informáti-
ca controlando el mundo con sus computadoras mecánicas movidas a vapor.
Sistemas basados en computador 9

lace. Ada era hija del poeta Lord Byron y tenía una sólida formación matemática, algo muy raro en la
época para una mujer. En 1843 publicó un trabajo donde se describía la máquina analítica y la mane-
ra de programarla. En particular en el trabajo se presentaba el programa que permitía el cálculo de los
números de Bernoulli (Kim y Toole, 1999). En su trabajo, Ada mostraba la gran potencia y la flexibi-
lidad que un programa modificable de instrucciones permitía a la máquina. Por estas razones, la con-
desa de Ada Lovelace es considerada la primera teórica (y práctica) de la programación.

El computador moderno
A finales de la década de 1930 aparecieron distintos grupos de trabajo interesados en la construcción
de máquinas de calcular con algún tipo de sistema automático de control. Estos esfuerzos se aborda-
ron tanto desde el punto de vista mecánico como del electrónico.
Respecto a las máquinas mecánicas, destaca el trabajo en Alemania de Konred Zuse. Zuse, inge-
niero de formación, conocía el esfuerzo de cómputo necesario para los trabajos técnicos. Se dio cuen-
ta de que el problema fundamental, usando una regla de cálculo o una máquina sumadora mecánica,
era el almacenamiento de los resultados intermedios que se van produciendo. A tal efecto es necesa-
rio un sistema de memoria para mantener la información. En 1934 Zuse era consciente de que una cal-
culadora automatizada sólo requiere tres unidades funcionales: un control, una memoria y una sección
aritmética. Con este diseño básico construye la Z1 (la primera máquina de la serie Z). La Z1 usaba una
memoria mecánica codificada en binario y leía la secuencia de instrucciones a realizar de una serie de
tarjetas perforadas. Al mismo tiempo en Harvard, Howard Aitken construía otra secuencia de máqui-
nas automáticas, la serie de las Mark.
Todos estos esfuerzos se basaban aún en el uso de elementos mecánicos. La gran revolución sur-
gió con el advenimiento de las máquinas electrónicas. Con respecto a las máquinas electrónicas uno
de los primeros esfuerzos fue el diseño de la ABC (Atanasoff-Berry Computer). El ABC no llegó a ser
operativo, pero su diseño tuvo importancia en el desarrollo de modelos posteriores. En particular, el
ABC usaba el sistema binario, lo que simplificaba los circuitos electrónicos usados. La primera com-
putadora electrónica operativa fue la ENIAC (Electronic Numerical Integrator and Computer) que tra-
bajaba en sistema decimal.
Todas estas máquinas estaban programadas con algún tipo de instrucciones en tarjetas perforadas
o directamente como conexiones (cableado). El siguiente paso se gestó en el equipo de desarrollo del
ENIAC y se trata de la invención del concepto de programa almacenado. En este asunto tuvo cierta
participación John von Neumann (físico, químico y matemático) aunque no fue el inventor del con-
cepto. Sólo el hecho de que él escribiera el borrador del informe que se presentó a los patrocinadores
militares del proyecto ENIAC, y que recogía la idea de problema almacenado en memoria, fue la cau-
sa de que se asociara con él dicho concepto y hoy se hable de máquinas de von Neumann. Este nuevo
concepto formó parte del diseño de la descendiente del ENIAC, la EDVAC (Electronic Discrete Varia-
ble Arithmetic Computer). La EDVAC almacenaba el programa en memoria, con lo que las instruc-
ciones se leían a mucha mayor velocidad que haciéndolo una a una desde una fuente externa como las
tarjetas perforadas.

1.3. CONCEPTO DE INFORMÁTICA


Como hemos visto, el origen del ordenador o computador se debe a la necesidad de realizar cálculos
de forma automática. Sin embargo, el procesamiento numérico no es la única utilidad de un ordena-
dor. La posibilidad de realizar operaciones lógicas le dota de la capacidad de usarse para el procesa-
miento de información, entendida desde un punto de vista general. El cuerpo de conocimiento que se
encarga de todo lo relacionado con el desarrollo y uso de ordenadores para el tratamiento de informa-
ción (numérica o no) es la informática. Veamos una definición más precisa.
10 Introducción a la programación con orientación a objetos

1.3.1. DEFINICIÓN DE INFORMÁTICA


Informática del francés informatique es la designación castellana de Computer Science and Engineering
en inglés. Según la Real Academia Española se define como el conjunto de conocimientos científicos y
técnicas que hacen posible el tratamiento automático de la información por medio de ordenadores.
La siguiente pregunta es obvia: ¿qué se entiende por información?

1.3.2. DATOS E INFORMACIÓN


En primer lugar es necesario distinguir con precisión entre los conceptos de datos e información:

a) Datos
Como tales se entiende el conjunto de símbolos usados para representar un valor numérico, un hecho,
una idea o un objeto. Individualmente los datos tienen un significado puntual. Como ejemplo de dato
tenemos el número de la seguridad social de un empleado, un número de teléfono, la edad de una per-
sona, etc.

b) Información
Por tal se entiende un conjunto de datos procesados, organizados, es decir, significativos. La informa-
ción implica tanto un conjunto de datos como su interrelación. Dependiendo de esta última el mismo
conjunto de datos suministra diferente información. Por ejemplo, imaginemos los datos de los traba-
jadores de una empresa:

Nombre
Edad
Estudios
Salario

Por separado se trata de un conjunto de datos individuales. Sin embargo, si los organizamos por
edad y salario tenemos un informe sobre la distribución del sueldo en función de la edad. Por otro lado,
si organizamos por estudios y salario tendremos un informe diferente que nos indica la distribución
del salario en función de la formación de los empleados.
Como no vamos a tratar específicamente sistemas de gestión de información consideraremos datos
e información como sinónimos.

1.3.3. REPRESENTACIÓN DE LA INFORMACIÓN


Habiendo definido el concepto de información el problema es cómo representarla para poder mane-
jarla de forma automática. Es posible idear muchas maneras de hacerlo, pero la clasificación básica
nos lleva a la distinción entre técnicas analógicas y digitales. Veamos la diferencia.

a) Representación analógica
Cuando una magnitud física varía para representar la información tenemos una representación analó-
gica. Por ejemplo, el voltaje en función de las variaciones de presión producidas por la voz en un
micrófono.
Sistemas basados en computador 11

b) Representación digital
En este caso la información se divide en trozos y cada trozo se representa numéricamente. Lo que se
maneja al final es ese conjunto de números. La cantidad de trozos en que se divide lo que se quiere
representar está relacionada con la calidad de la representación. La Figura 1.4 ilustra este concepto
considerando una cierta magnitud, X, que varía con el tiempo, t.
En el caso de la Figura 1.4 (a) el espaciado entre los puntos tomados sobre el eje de abscisas no es
suficiente para representar el pico central. En el segundo caso, sin embargo, sí recogemos el pico y la
representación es más fiel a la realidad. Cuanto menor sea el intervalo entre puntos más fiable es la
representación, aunque mayor es el número de datos que necesitamos para representar el mismo inter-
valo (temporal en este ejemplo). Lo que al final almacenaríamos para representar la información repre-
sentada por la curva anterior sería el conjunto de valores de ordenada para cada punto tomado sobre
el eje de abscisas.
En los ordenadores modernos toda la información está almacenada digitalmente, desde los núme-
ros al texto pasando por el audio o el vídeo. Esto nos lleva a una cuestión: ¿cómo está representado el
texto en un ordenador?
En un ordenador el texto está representado por un código numérico. Cada carácter (letras mayúscu-
las y minúsculas, signos de puntuación, signos especiales como #, @, &, etc.) tiene asociado un valor
numérico. Estos valores numéricos son arbitrarios y se asigna un valor u otro dependiendo del código
usado. Un código típico y tradicional es el código ASCII (American Standard Code for Information Inter-
change) pero el conjunto de caracteres es muy limitado, sólo el conjunto básico necesitado en inglés, sin
caracteres acentuados, por ejemplo. Existen otros códigos, como el Unicode que puede codificar 216 posi-
bilidades (usa 16 bits), con lo que se pueden representar los caracteres de multitud de lenguajes sin tener
que estar mezclando códigos. Por ejemplo, en Unicode la frase “Hola, Pepe.” queda como:

H o 1 a , P e p e .

72 111 108 97 44 32 80 101 112 101 46

En el ejemplo anterior cada carácter muestra en la parte inferior el correspondiente código Unico-
de. Obsérvese que el blanco es un carácter con su código (el ordenador lo tiene que almacenar para
saber que está ahí) y que las mayúsculas tienen código distinto de las minúsculas. Al final lo que habría
en el ordenador sería la secuencia anterior de números.
Otro problema es cómo representar los números. Resulta conveniente, por su simplicidad, usar el
sistema de numeración binario donde sólo tenemos 2 dígitos: 0 y 1. Estos dos dígitos se pueden repre-
sentar fácilmente en los circuitos electrónicos, por ejemplo como conducción o no conducción o, en

Figura 1.4. Ilustración del efecto del incremento de muestras sobre una señal
12 Introducción a la programación con orientación a objetos

general, por cualquier método que pueda distinguir entre dos estados. La base dos se maneja con nota-
ción posicional igual que la decimal. Al construir el número se comienza con el 0, luego se pasa al 1
y al agotar la base se coloca un 0 o un 1 a la izquierda, véase la Tabla 1.1.

Tabla 1.1. Equivalencia decimal-binario

Decimal Binario
0 0
1 1
2 10
3 11

En informática un dígito binario se denomina bit (contracción de binary digit). Otra unidad común
es el conjunto de 8 bits, denominado byte:

1 byte < > 8 bits

Es interesante saber cuál es el mayor valor numérico que se puede representar con un cierto núme-
ro de bits. Puesto que con un bit se pueden representar dos posibilidades, con 2 bits tendremos cuatro
(2 3 2) y en general, con N bits tenemos 2N, véase la Tabla 1.2.

Tabla 1.2. Número de posibilidades en


función del número de bits

Bits Posibilidades
0 2051
1 2152
2 2254
3 2358
4 24516
5 25532
6 26564
7 275128
8 (1 byte) 285256
9 295512
10 21051024

Esta secuencia de valores es muy típica, aparece por ejemplo en los valores de la memoria de los
ordenadores (aunque referida a un múltiplo del byte, el kilobyte como veremos más adelante).

1.3.4. SISTEMAS INFORMÁTICOS


Llega ahora el momento de considerar dos conceptos nuevos de importancia, el de sistema y a partir
de él, el de sistema informático (o sistema basado en computador). Comencemos por la definición de
sistema.
Sistemas basados en computador 13

Un sistema es un conjunto de entidades que interrelacionan para un fin común.

El concepto de sistema es ubicuo en el campo informático y aparece en múltiples contextos. Así,


hablamos de sistemas físicos, de sistemas de información o de sistemas software.
Un sistema informático es un conjunto de ciertas entidades que tienen como objetivo un determi-
nado tipo de procesamiento de información. Estas entidades pueden ser:

— Físicas (Hardware 6 )
— Lógicas (Software 6 )
— Humanas (“Peopleware”)

La parte física del ordenador está constituida por los dispositivos que conforman el ordenador. La
palabra hardware es inglesa y literalmente significa material de ferretería. Con el advenimiento de la
informática se empieza a aplicar en el contexto que aquí se describe. Se suele coloquialmente decir
que todo lo que se puede tocar es hardware. En el siguiente apartado consideraremos en más detalle la
parte física de un ordenador.
La parte lógica está formada por los programas y toda la información asociada a ellos (informa-
ción de desarrollo y documentación del programa). En inglés se denomina software, haciendo un jue-
go de palabras con hardware, cambiando el hard (rígido o duro) por soft (blando). El software es la
componente lógica, intangible, de un sistema informático.
Siguiendo con los juegos de palabras la componente humana de un sistema informático se deno-
mina a veces peopleware. La componente humana de un sistema se refiere generalmente a los usua-
rios del mismo.
Puesto que la programación (una parte del proceso de desarrollo de software) va a ser el tema que
nos ocupe en este texto, vamos a considerar el software con algo más de detalle en el siguiente apar-
tado.

1.3.5. CONSIDERACIONES SOBRE EL SOFTWARE


Comúnmente se considera software como sinónimo de programa. Sin embargo, desde el punto de vis-
ta informático el concepto de software es más general, abarcando no sólo el código generado, sino
también toda la documentación asociada. Tradicionalmente, el software se clasifica en dos grandes
grupos, software de sistemas y software de aplicación.
El software de sistemas es software usado por otro software. El ejemplo clásico es el del sistema
operativo, que es el programa que permite comunicarse con, y usar de forma cómoda, el hardware. El
sistema operativo es la conexión con el hardware que usan el resto de programas que se ejecutan en
un ordenador. Por otro lado, el software de aplicación es el destinado a su utilización por un usuario y
no por otros programas. Una hoja de cálculo, un procesador de textos o un programa de juego son
ejemplos típicos.
La comunicación del usuario con el software se realiza hoy casi generalizadamente a través de inter-
faces gráficas de usuario. Lo primero es indicar qué interfaz representa en informática una entidad (hard-
ware o software) que actúa como intermediario o conexión entre otras dos, por ejemplo, entre un usuario
y un programa. En una interfaz gráfica de usuario las capacidades del programa están representadas por
medio de símbolos gráficos (iconos) a los que se puede acceder por medio de algún dispositivo apunta-

6
Tanto hardware como software son palabras que el uso ha adoptado en nuestro idioma y en este texto se utilizan tal
cual.
14 Introducción a la programación con orientación a objetos

dor (actualmente el ratón). La interfaz gráfica del sistema operativo Windows es un ejemplo típico. Des-
de el punto de vista del usuario la interfaz “es” el programa. Como una buena indicación de diseño tene-
mos que la interfaz debe estar separada de la parte funcional del programa, véase la Figura 1.5. Las
ventajas de esta organización son un mejor mantenimiento de las dos partes componentes del software
(son independientes, se pueden modificar por separado) y mayor reutilizabilidad de los elementos de la
interfaz y de la parte funcional (se pueden aprovechar con facilidad en nuevos desarrollos).

Figura 1.5. Diagrama mostrando la interrelación entre el usuario y un sistema software

1.4. ARQUITECTURA CLÁSICA O DE VON NEUMANN DE UN COMPUTADOR


La estructura de un computador típico responde a una organización concreta. Básicamente se trata de
la organización definida por Babbage con algunas características adicionales. En la actualidad esta
organización o arquitectura típica se denomina arquitectura clásica o de von Neumann, dado el papel
que éste jugó en el desarrollo del EDVAC, donde por primera vez se conjugaban estos factores. La
arquitectura de von Neumann esencialmente incorpora las siguientes características:

— Uso de la estructura funcional ya determinada por Babbage:

* Unidad de entrada
* Unidad de salida
* Unidad de control
* Unidad aritmética (hoy aritmético-lógica)
* Memoria

— Utilización del sistema binario (el ENIAC, por ejemplo, usaba sistema decimal).
— Incorporación del concepto de programa almacenado en la memoria. Así, la memoria no sólo
almacena los datos, sino también las instrucciones necesarias para el procesamiento de los mis-
mos.

El esquema funcional de un ordenador estándar (basado en la arquitectura clásica) se muestra en


la Figura 1.6.
Obsérvese que la unidad aritmético-lógica y la unidad de control conforman básicamente la uni-
dad central de proceso o CPU (Central Processing Unit). Esta unidad suele estar físicamente construi-
da sobre un chip, también denominado microprocesador. Se considera el “cerebro” del ordenador.
Obsérvese también que la CPU y la memoria central o principal definen la unidad central (no de pro-
ceso, sino el ordenador central propiamente dicho) y que los demás dispositivos no. Estos otros dis-
positivos se denominan periféricos (aunque físicamente pueden estar dentro de la misma carcasa que
el ordenador o computador central). Comentemos los distintos componentes:
Sistemas basados en computador 15

Figura 1.6. Estructura funcional clásica de un computador

a) Entrada o unidad de entrada


Representa el dispositivo a través del cual se introducen los datos y las instrucciones. Ambos serán
almacenados en la memoria (central o secundaria). Los dispositivos de entrada pueden ser variados:
un teclado, un escáner, un lector de códigos de barras, etc. Puede haber más de uno conectado y fun-
cionando en un sistema.

b) Salida o unidad de salida


Es el dispositivo que muestra el resultado de la ejecución del programa en el ordenador. También
podemos tener distintos tipos de dispositivos como monitores, impresoras o plotters.

c) Memoria
Sirve para almacenar los datos y las instrucciones del programa (recordemos que nuestro modelo
almacena el programa en memoria). Tenemos dos tipos de memoria, la primera es la denominada
memoria central. Ésta está formada por circuitería electrónica y de rápido acceso, pero relativamente
pequeña. La memoria central se encuentra organizada en “posiciones de memoria” (grupos de un
tamaño concreto de bits). Cada posición está identificada por una “dirección” de memoria que permi-
te acceder a ella (para leer o para escribir). La dirección puede entenderse como un número de orden,
véase la Figura 1.7. Un dato puede necesitar más de una posición de memoria para su codificación.
La memoria se mide en bytes (8 bits). Como ésta es una unidad muy pequeña se usan múltiplos,
véase la Tabla 1.3.
La memoria central es de tipo RAM (Random Access Memory) lo que indica que se puede acce-
der directamente a cualquier posición de memoria sin pasar por las anteriores. La RAM es de lectura-
escritura, es decir, se puede leer la información almacenada allí y se puede escribir en ella. Otra carac-
terística es que es volátil, entendiendo por ello que la información sólo se mantiene mientras está
conectada (al cortar la corriente se pierde). El tamaño de la memoria central es de algunos cientos de
KB (128 ó 256 en compatibles PC) a algunos o muchos GB en sistemas de altas prestaciones. En un
ordenador existen también memorias de tipo ROM (Read Only Memory) que son permanentes, sólo
16 Introducción a la programación con orientación a objetos

Figura 1.7. Representación esquemática de la organización de la memoria de un ordenador

Tabla 1.3. Medidas de memoria

Unidad Símbolo Valor (bytes)


kilobyte KB 210 (1024)
20
megabyte MB 2 (1024*1024)
gigabyte GB 230 (1024*1024*1024)
terabyte TB 240 (1024*1024*1024*1024)

permiten la lectura y se usan para almacenar, por ejemplo, el programa de arranque de un ordenador
o las operaciones básicas de entrada y salida.
El segundo tipo de memoria es la denominada masiva, auxiliar o secundaria, mucho más lenta de
acceso que la memoria principal, pero de mucha mayor capacidad y permanente (no se pierde la infor-
mación al cortar la corriente). Se trata, fundamentalmente, de los discos duros, los disquetes o los CD-
ROM (almacenamiento óptico). Algunos de estos dispositivos son regrabables y otros, como los CD-
ROM tradicionales, son de sólo lectura, no permitiendo la regrabación. Normalmente los programas y
los datos se graban desde algún dispositivo de entrada en la memoria secundaria y desde ahí se cargan
en la memoria principal para la ejecución. La capacidad típica, en la actualidad, de los discos duros es
de varias decenas de GB en los compatibles PC.
En un ordenador es posible encontrar también cierta cantidad de la denominada memoria caché
(oculta en francés). Una memoria caché es similar a la RAM pero mucho más rápida que ella y se usa
como un elemento intermedio entre la CPU y la memoria central, véase la Figura 1.8.
Cuando la CPU necesita leer datos o instrucciones de la RAM primero mira si ya se encuentran en
la caché. Si están allí los toma de ella. Al ser la caché mucho más rápida que la RAM el proceso se
realiza en mucho menos tiempo. Si los datos o la instrucción no están en la caché, la CPU los lee de
la RAM y se guarda una copia en la caché para poder tomarla de allí si se vuelven a necesitar. El resul-

Figura 1.8. Interrelación entre la CPU y las memorias caché y RAM


Sistemas basados en computador 17

tado de este proceso es una mejora en el rendimiento de la CPU. En un sistema moderno se dispone
de todos estos tipos de memoria.

d) Unidad aritmético-lógica o ALU (Arithmetic Logic Unit)


Es el conjunto de circuitos que permiten realizar las operaciones aritméticas y las operaciones lógicas
tales como las comparaciones o la aplicación del álgebra de Boole binaria. La ALU realiza su trabajo
usando la información contenida en unas posiciones de memoria internas a la CPU denominadas regis-
tros. Estos registros están especializados, existiendo registros de instrucciones (van almacenando las
instrucciones del programa) de uso general o el denominado contador de programa que almacena la
dirección de memoria en que se encuentra la siguiente instrucción del programa que se está ejecutando.

e) Unidad de control
Coordina los distintos pasos del procesamiento. En esencia recibe señales (de estado) de las dis-
tintas unidades determinando su estado de funcionamiento. Capta de la memoria central las instruc-
ciones del programa una a una y va colocando los datos en los registros correspondientes haciendo que
las distintas unidades implicadas realicen sus tareas. El trabajo de la unidad de control está sincroni-
zado por un reloj interno, que oscila con una frecuencia dada. La velocidad de trabajo de la CPU vie-
ne determinada por la frecuencia del reloj. La frecuencia se mide en ciclos por segundo o Hertzios (Hz)
y la unidad que se usa es un múltiplo, el MegaHertzio (1 MHz5106 Hz). Las frecuencias actuales de
los microprocesadores son del orden de 1000-2000 MHz (1-2 GHz). La inversa de la frecuencia es el
período y nos da el tiempo que tarda en realizarse un ciclo.
La información entre la CPU y la memoria se transfiere como grupos de un cierto número de bits
que se pasan a la vez. Esto define la palabra y, el número de bits transferidos de una vez es la longi-
tud de la palabra. Ejemplos de longitud de palabra son 32 ó 64 bits. Como para referirnos a una posi-
ción de memoria usamos una palabra en la CPU, la longitud de palabra determina el máximo número
de posiciones que se pueden representar (2N donde N es la longitud de palabra). Se puede tener menos
memoria central que la que corresponde a ese valor pero no se puede tener más.
La CPU funciona siguiendo lo que se denomina ciclo de recuperación-descodificación-ejecución, véa-
se la Figura 1.9. El ciclo funciona de la siguiente forma. De la memoria central se recupera la siguiente
instrucción del programa buscando en la dirección indicada por el contador del programa. El contador se
incrementa para saber en el siguiente ciclo donde está la siguiente instrucción. La instrucción actual se des-
codifica para saber qué operación hay que realizar y la unidad de control activa los circuitos necesarios
para realizar la instrucción, la cual puede cargar un dato en un registro o sumar dos valores, por ejemplo.

Figura 1.9. Ciclo de recuperación-descodificación-ejecución


18 Introducción a la programación con orientación a objetos

1.5. REDES DE COMPUTADORES


Por su importancia actual es útil presentar, aunque sea informalmente en un texto introductorio como
éste, el concepto de red de computadores.

1.5.1. GENERALIDADES
Una red consiste en dos o más ordenadores conectados entre sí de forma que puedan compartir infor-
mación o recursos. En particular el intercambio de información ha devenido en una parte fundamen-
tal de la informática. La comunicación por correo electrónico, el intercambio de información técnica
y científica o la compartición de la información de los clientes de una empresa en diferentes sucursa-
les de la misma son ejemplos de la utilidad de las redes de computadores. A pesar de todo, en una red
cada ordenador tiene su individualidad propia, poseyendo algún tipo de información de identificación,
una dirección de red. Dentro de una red el o los ordenadores que ofrecen algún tipo de servicio a los
demás se denominan servidores, por ejemplo un servidor de ficheros o de impresora.
Para conectar computadoras en red podemos usar distintas topologías, por ejemplo:

a) Conexión punto a punto, véase la Figura 1.10 (a)


Ésta sería la solución más sencilla, con todos los ordenadores directamente conectados a todos los
demás. Lógicamente, esta técnica sólo es factible para unos pocos ordenadores que estén físicamente
próximos, basta con imaginar los problemas de cableado, ya que el número de conexiones entre N
ordenadores es de N(N-1)/2. Una solución mucho más elegante es la de línea compartida.

b) Línea compartida, véase la Figura 1.10 (b)


En este caso, sólo hay una línea de conexión a la que se van conectando los ordenadores. La conexión
es muy sencilla, pero un software de gestión de red debe controlar cómo se envía la información de
los diferentes usuarios. Actualmente la técnica utilizada es la denominada packet-switched (conmuta-
ción de paquetes). En esta aproximación el software de gestión de la red divide la información en par-
tes (paquetes) que se van enviando por turnos a lo largo de la línea. La ventaja es que el sistema es el
que se ocupa de la división en paquetes y del ensamblaje posterior de los mismos para generar la infor-
mación original. El sistema de paquetes permite que múltiples comunicaciones entre ordenadores pue-
dan producirse concurrentemente sobre una misma línea compartida. Para el usuario este proceso es
transparente, el software se ocupa de todo el trabajo.
Las redes basadas en paquetes que abarcan grandes distancias (de país a país, por ejemplo) son
diferentes de que las que cubren una pequeña distancia, como una habitación o un edificio. Para dis-
tinguir unas de otras se dividen las redes de computadores en dos tipos, LANs y WANs.

Figura 1.10. Sistemas basados en computador


Sistemas basados en computador 19

Una LAN es una red de área local (Local Area Network) que cubre una pequeña distancia y está
formada por un número pequeño de ordenadores. Una WAN es una red de área ancha o amplia (Wide
Area Network) que conecta dos o más LANs sobre grandes distancias. Una LAN pertenece normal-
mente a una sola organización pero las WANs suelen conectar LANs de grupos diferentes, incluso de
países distintos. En las LAN conectadas en una WANs un ordenador de cada LAN maneja las comu-
nicaciones sobre la WAN.

1.5.2. INTERNET
Internet es una WAN que abarca todo el mundo. El término Internet proviene de internetworking indi-
cando que es una red de redes. Internet permite la comunicación entre sistemas hardware y software
heterogéneos usando una serie de estándares de comunicación. Internet es descendiente de la ARPA-
NET, un sistema de red desarrollado en un proyecto del ARPA (Advanced Research Projects Agency)
de los EE.UU. La historia del desarrollo de Internet puede encontrarse en el excelente libro de Hafner
y Lyon (Hafner y Lyon, 1998).
El software que gestiona la comunicaciones en Internet se denomina TCP/IP (Transmission Con-
trol Protocol/ Internet Protocol). Son dos entidades separadas, cada una conteniendo muchos progra-
mas. Estas dos entidades podrían definirse de la forma siguiente (Comer, 1995):

a) IP
Se trata de un protocolo y no de un programa específico. Sus misiones son varias. Define el formato
de todos los datos sobre la red, también realiza el proceso de routing (direccionamiento en la red) esco-
giendo el camino por el que circularán los datos, y finalmente establece una serie de reglas indicando
cómo los ordenadores deben procesar los paquetes de datos, cómo y cuándo deben generarse mensa-
jes de error y las condiciones bajo las cuales se deben descartar paquetes.

b) TCP
Se trata también de un protocolo de comunicación y no de una pieza de software. La misión concreta
del TCP es difícil de definir en pocas palabras. El protocolo especifica el formato de los datos y los
reconocimientos que dos computadores deben intercambiar para obtener una transmisión fiable, así
como los procedimientos que los computadores usan para asegurarse de que los datos llegan correcta-
mente a su destino.
En Internet cada ordenador conectado se identifica con lo que se denomina una dirección IP. La
dirección IP identifica tanto al ordenador como a la red a la que está conectada. Dicho en otras pala-
bras, la dirección IP no identifica una máquina sino una conexión a una red. Nada impide sustituir un
ordenador por otro y mantener la dirección IP original. Las direcciones IP están formadas por 32 bits
organizados en cuatro grupos de ocho. Cada uno de esos grupos de ocho se expresa en decimal y se
separan unos de otros por un punto, por ejemplo:

167.55.44.11

Es normal que la dirección IP tenga asociado un nombre que se suele denominar dirección de Inter-
net como,

pepe.uclm.es
20 Introducción a la programación con orientación a objetos

La primera parte es el nombre asignado a ese ordenador en concreto (pepe en el ejemplo), la segun-
da parte es el dominio e indica la organización a la que pertenece (uclm.es en el ejemplo). La última
sección de cada nombre de dominio indica, normalmente, el tipo de organización:

.com: negocio comercial


.org: organización sin finalidad de lucro
.edu: institución educativa

o bien el país:

.es: España
.uk: Reino Unido

Cuando se usa una dirección de Internet ésta se traduce a dirección IP por un software denomina-
do DNS (Domain Name Service). Cada organización conectada a Internet tiene un servidor de domi-
nio con una lista de todos los ordenadores de esa organización y sus direcciones IP. Cuando se pide
una IP, si el servidor DNS no la tiene, contacta a través de Internet con otro servidor que sí la tenga.

1.5.3. LA WORLD-WIDE-WEB (WWW) 7


El web es una forma de intercambiar información a través de Internet. En el web se usan los concep-
tos de hipertexto e hipermedia que podemos definir de la forma siguiente:

Hipertexto: Una forma de texto no secuencial en la cual se siguen unos caminos o enlaces a través
del conjunto completo de documentos. El concepto no es nuevo, se encontraba ya pergeñado en algu-
nos documentos del proyecto Manhattan (el proyecto secreto de los EE.UU. destinado al desarrollo de
la bomba atómica). La idea es poder saltar entre la información del documento en forma no secuen-
cial.
Hipermedia: Es una generalización del concepto de hipertexto donde no sólo se incluye texto en
la secuencia no lineal de información sino también gráficos, audio, vídeo o programas.

Al web se accede a través de un programa especial, un navegador (browser) que presenta la infor-
mación hipermedia, y con el que se va accediendo a los distintos enlaces, links, para obtener la infor-
mación deseada. Lógicamente, los documentos en el web se identifican con algún tipo de nombre, aquí
denominado URL (Uniform Resource Locator), por ejemplo:

www.inf-cr.uclm.es

Con una URL se accede a una página web donde aparece la información fundamentalmente como
en una página impresa. Estas páginas están escritas usando un lenguaje estándar formalizado llamado
HTML (HyperText Markup Language 8). En la actualidad se está trabajando con versiones más sofis-
ticadas del HTML como el XML (Extensible Markup Language) (Bosak y Bray, 1999).
Por último, merece la pena indicar que el lenguaje de programación Java (lenguaje que vamos a
usar para implementar todos los ejemplos de programación en este texto) ha sido diseñado con la capa-
cidad de interacción a través de Internet y, en particular, para poder generar aplicaciones que se eje-
cuten a través de una página web. La técnica para ello es simple. Al solicitar la ejecución del programa

7
World Wide Web significa literalmente red (o telaraña) de alcance mundial. Normalmente se conoce por las iniciales
www e incluso como w 3 o “w cubo”.
8
HTML puede traducirse como lenguaje de marcas para hipertexto.
Sistemas basados en computador 21

a través de la página web, pinchando en el correspondiente enlace con el ratón, una versión del pro-
grama se copia a la máquina del usuario donde se ejecuta de forma local. Los navegadores actuales
incorporan un intérprete de bytecode de Java 9 para poder ejecutar estos programas o Applets.

EJERCICIOS PROPUESTOS
Ejercicio 1.* ¿Cuál es la diferencia entre datos e información?

Ejercicio 2.* ¿Cómo se almacena la información en los ordenadores, con repre-


sentación analógica o digital?

Ejercicio 3.* ¿Qué diferencias existen entre el código ASCII y el UNICODE?

Ejercicio 4.* ¿Cuántos caracteres se podrían representar con 12 bits?

Ejercicio 5.* ¿Por qué unidades está formada la CPU?

Ejercicio 6.* ¿Cuántos bytes son un GB (gigabyte)?

Ejercicio 7.* ¿Qué características tiene la memoria RAM?

Ejercicio 8.* ¿Qué es la memoria caché?

Ejercicio 9.* ¿Qué tipología de conexión se suele usar actualmente para conec-
tar varios ordenadores en red?

Ejercicio 10.* ¿Cómo se denomina el software que gestiona las comunicaciones


en Internet?

REFERENCIAS
BOSAK, J. y BRAY, T.: “XML and the Second-Generation Web”, Scientific American, 79-83, May 1999.
CERUZZI, P. E.: A History of Modern Computing, The MIT Press, 1999.
COMER, D. E.: Internetworking with TCP/IP. Volume I, Third Edition, Prentice-Hall, 1995.
DEMIDOVICH, B. P. y MARON, I. A.: Cálculo Numérico Fundamental, Paraninfo, 1977.
GIBSON, W. y STERLING, B.: The Difference Engine, Orion paperback, 1996.
HAFNER, K. y LYON, M.: Where wizards stay up late. The origins of the internet, Touchstone, 1998.
KIM, E. E. y TOOLE, B. A.: “Ada and the First Computer”, Scientific American, 66-71, May 1999.
KOPCHENOVA, N. V. y MARON, I. A.: Computational Mathematics, MIR Publishers, Moscow, Fourth printing,
1987.
WILLIAMS, M. R.: A history of computing technology, Second edition, IEEE Computer Society Press, 2000.

9
El concepto de traductor e intérprete y de bytecode de Java se expondrá en el Capítulo 2.
2

Elementos de
programación y lenguajes

Sumario

2.1. Introducción 2.7. Algoritmos


2.2. Concepto de programa 2.8. Ingeniería del software
2.3. Lenguajes de programación 2.8.1. Concepto de Ingeniería del softwa-
2.4. Sintaxis y semántica re
2.5. Estructura de un programa 2.8.2. Ciclo de vida del software
2.6. Errores de un programa
24 Introducción a la programación con orientación a objetos

2.1. INTRODUCCIÓN

En este capítulo se introducen algunos conceptos fundamentales relativos a la programación y a los


lenguajes de programación. Además, se presentan los conceptos básicos necesarios para el desarrollo
de programas. Se introducen también los diferentes tipos de lenguajes de programación y sus diferen-
cias. En el apartado 2.4 se describen los conceptos fundamentales de sintaxis y semántica, que ayuda-
ran al lector a comprender la diferencia entre la forma de un programa y las tareas que realiza.
Seguidamente, se presentará la estructura general de un programa y los tipos de errores que se pueden
cometer al implementarlo. De esta forma se pretende dotar al lector desde el principio de una visión
de conjunto relativa al problema de la programación. Dada la importancia del concepto de algoritmo,
este capítulo le dedica una sección donde, además de definirlo, se describen las técnicas utilizadas para
su evaluación. No queremos concluir sin introducir el concepto de Ingeniería del Software, mostran-
do al lector que el desarrollo de software es una actividad profesional, racionalizada, que va más allá
de la simple generación de líneas de código en un lenguaje dado.

2.2. CONCEPTO DE PROGRAMA

La primera pregunta que podríamos plantear al introducir el problema de la programación, entendi-


da como el desarrollo de programas, sería ¿qué es un programa? Podemos definir un programa como
una serie de instrucciones que indican de forma precisa y exacta al computador qué tiene que hacer
(Kamin et al., 1998). El programa es el medio de comunicación con el computador. Por medio de él
conseguimos que la máquina utilice sus capacidades para resolver un problema que nos interesa. Este
punto de vista es importante. Un computador se puede entender como una máquina virtual, capaz de
realizar una serie de tareas genéricas pero no concretada hacia ninguna tarea específica. Es siempre
necesario un programa, que usando un lenguaje inteligible por la máquina, le indique qué tiene que
hacer. Para ello, como veremos más adelante, es necesario saber qué queremos indicar al computa-
dor y cómo hacerlo.
En cierto sentido, un programa modela algo (Arnow y Weis, 1998). Entendemos por modelo una
representación simplificada de un problema. El modelo considera las características relevantes del pro-
blema y las representa en el computador. La ciencia y el arte de la programación consisten en saber
construir un modelo de solución para un problema dado y en indicar una serie de instrucciones que
permitan describir dicho modelo al computador.
Como ejemplo de programa veamos un caso sencillo en Java (Programa 2.1).

Programa 2.1. Ejemplo de programa en Java

// Imprime un refrán
class Refran {
public static void main(String[] args) {
System.out.println(“Donde fueres haz lo que vieres”);
} // Fin método main
} // Fin class Refran

Cuando el programa se ejecute imprimirá la siguiente línea de texto:

Donde fueres haz lo que vieres

El Programa 2.1 está formado por varios elementos:


Elementos de programación y lenguajes 25

a) Un comentario (en la primera línea) que indica para qué sirve el programa.
b) Llaves, que se utilizan para definir bloques.
c) Definición de una clase, a partir de la segunda línea (en los Capítulos 5 y 7 se trata el concepto
de clase).
d) Una instrucción, en la cuarta línea. Esta sentencia indica lo que hay que hacer, en este caso
escribir una frase por pantalla.

Como podemos apreciar, es necesario indicar las instrucciones al computador usando un lenguaje
determinado (en el ejemplo anterior Java). Sin embargo, no existe un único lenguaje sino que hay
muchos que se han ido desarrollando a lo largo del tiempo y que pueden ser clasificados en varios tipos
estándar.

2.3. LENGUAJES DE PROGRAMACIÓN


La comunicación con el computador se realiza utilizando un lenguaje determinado, un lenguaje de pro-
gramación. Existen distintos tipos de lenguajes de programación y, dentro de cada tipo, diferentes len-
guajes. Estos tipos pueden definirse desde diferentes puntos de vista. Una clasificación típica agrupa
los lenguajes según su nivel de abstracción operativa. Esto implica que sea necesario detallar más o
menos las operaciones a realizar para desarrollar una tarea concreta. Cuanto menos haya que indicar
al ordenador, de mayor nivel se considera el lenguaje. La clasificación a la que estamos haciendo refe-
rencia es la siguiente:

a) Lenguaje máquina
b) Lenguaje ensamblador
c) Lenguajes de alto nivel
d) Lenguajes de cuarta generación

Vamos a comentar cada uno de los tipos de lenguajes de menor a mayor nivel de abstracción:

a) Lenguaje máquina
Es el lenguaje nativo de una CPU. Las instrucciones de este lenguaje se indican en binario. El código
se expresa como una serie de dígitos binarios y es muy difícil para los humanos leerlo y escribirlo,
aunque antiguamente se hacía. Hay un lenguaje máquina por cada tipo diferente de CPU. No podemos
por lo tanto hablar del lenguaje máquina, sino siempre de un lenguaje máquina determinado. Todo pro-
grama debe, en última instancia, ser traducido al lenguaje máquina del ordenador sobre el que se va a
ejecutar.

b) Lenguaje ensamblador
Corresponde a un mayor nivel de abstracción que los lenguajes máquina. Un ensamblador utiliza
símbolos mnemotécnicos, palabras cortas, para hacer referencia a las instrucciones o datos del len-
guaje máquina, en lugar de usar los dígitos binarios directamente. Para ejecutar un programa escri-
to en ensamblador es necesario convertir el programa a lenguaje máquina. Un lenguaje
ensamblador corresponde a un determinado lenguaje máquina, por lo tanto no hay un solo lengua-
je ensamblador.
Estos dos lenguajes se consideran de bajo nivel de abstracción operativa.
26 Introducción a la programación con orientación a objetos

c) Lenguajes de alto nivel (a veces llamados de tercera generación)


Se caracterizan porque son independientes de la máquina en la que se usan (generalmente, en la prác-
tica los fabricantes suelen proveer estos lenguajes con algunas capacidades específicas para máquinas
concretas). Estos lenguajes no hacen referencia al funcionamiento de la CPU, sino a tareas más orien-
tadas al usuario (sumar, restar o multiplicar dos números, por ejemplo). Usan instrucciones que se ase-
mejan al lenguaje ordinario. Cada una de las instrucciones en uno de estos lenguajes equivale
normalmente a varias a nivel de máquina. Como en el caso del ensamblador, es necesario convertir el
programa a lenguaje máquina. A tal efecto existen programas que realizan la traducción, por lo que el
programador sólo tiene que preocuparse del trabajo de escribir su programa en el lenguaje deseado. En
principio, un programa escrito en uno de estos lenguajes puede ejecutarse sobre cualquier ordenador.
Ejemplo de esos lenguajes son: Fortran, Cobol, Pascal, C o Java.

d) Lenguajes de cuarta generación


Son lenguajes que trabajan a mayor nivel de abstracción. Suelen incorporan capacidades para la gene-
ración de informes o interaccionar con bases de datos. Se denominan de cuarta generación, 4GL
(fourth generation languages).
Existe otra clasificación de los lenguajes de programación que se basa en el estilo de programa-
ción y que clasifica los lenguajes en,
a) Lenguajes imperativos o procedimentales
b) Lenguajes declarativos

a) Lenguajes imperativos o procedimentales


Son lenguajes basados en la asignación de valores. Se fundamentan en la utilización de variables para
almacenar valores y en la realización de operaciones con esos valores.

b) Lenguajes declarativos
Describen estructuras de datos y las relaciones entre ellas necesarias para una determinada tarea, indicando
también cuál es el objetivo de la tarea. El programador no indica el procedimiento (el algoritmo) para rea-
lizar la tarea. Hay dos tipos de lenguajes declarativos:
b.1. Lenguajes funcionales. Basados en la definición de funciones, como LISP.
b.2. Lenguajes de programación lógica. Basados en la definición de predicados (relaciones lógi-
cas entre dos o más elementos) como PROLOG.
Tanto si se usa un tipo de lenguaje u otro, al final el ordenador siempre lo traduce a lenguaje
máquina, pues éste es el único lenguaje que reconoce. El proceso de traducción puede realizarse de
diversas formas. Según el método que se use para llevar a cabo la traducción se habla de lenguajes
compilados o lenguajes interpretados.

a) Lenguajes compilados
Estos lenguajes realizan una traducción completa del programa a lenguaje máquina (compilación del
programa). Normalmente el proceso se realiza en dos etapas. En la primera, la compilación, el código
que hemos escrito, denominado código fuente, se traduce a lo que se denomina código objeto que aún
1
“Librería” es una mala traducción del término inglés library que significa biblioteca. Este término es muy común en
informática.
Elementos de programación y lenguajes 27

no es ejecutable. Para serlo, este código objeto debe enlazarse con los métodos, funciones o procedi-
mientos predefinidos en el lenguaje, y que se encuentran en librerías 1 externas. En el segundo paso
denominado de enlazado (“linkado” 2) se incorporan estas funciones, métodos o procedimientos y el
resultado es un programa “ejecutable”, es decir, un programa en lenguaje máquina que puede funcio-
nar, bajo una CPU determinada, veáse la Figura 2.1.

Figura 2.1. Procesos de compilación y enlazado (“linkado”)

b) Lenguajes interpretados
El código que se ha escrito, código fuente, se va leyendo poco a poco y va traduciéndose y ejecutándo-
se según se traduce. Aquí no hay una traducción completa, no generamos un programa directamente eje-
cutable. Por eso, tradicionalmente, los lenguajes interpretados son menos eficientes que los compilados.
En particular, el lenguaje Java aplica una aproximación intermedia entre estas dos. Existe una
“compilación” inicial donde el compilador de Java traduce el código fuente a bytecode, el cual es
una representación de programa a bajo nivel. El bytecode no es el lenguaje máquina de ninguna
CPU. El bytecode sería el código máquina de una hipotética CPU que hoy por hoy no existe física-
mente 3. En este contexto se dice que el bytecode se ejecuta sobre una máquina virtual. Tras la com-
pilación, el bytecode se interpreta. El intérprete de JAVA lo lee y lo ejecuta en una máquina
concreta. El bytecode es estándar y no depende de ninguna CPU. La idea es que pueda ejecutarse en
cualquier máquina. Interpretar el bytecode es más rápido que interpretar directamente el código
fuente, puesto que el bytecode está más próximo al lenguaje máquina que el fuente original. El byte-
code es transportable de máquina a máquina, aunque debe haber para cada tipo de procesador un
intérprete de bytecode para poder ejecutar los programas. De forma esquemática tendríamos el dia-
grama de la Figura 2.2. En él, se muestra un bucle en el proceso de interpretación, indicando que es
un proceso iterativo donde se lee una sección del bytecode y se ejecuta, repitiéndose el proceso has-
ta finalizar todo el bytecode.

2
Se trata de una traducción incorrecta del inglés linking que significa literalmente enlazar. Como el de librería, el tér-
mino está muy extendido en el campo informático.
3
En la actualidad se están desarrollando CPU’s que usan el bytecode como lenguaje nativo.
28 Introducción a la programación con orientación a objetos

Figura 2.2. Proceso de compilación e interpretación en Java

2.4. SINTAXIS Y SEMÁNTICA


Estos dos conceptos subyacen en todo lenguaje de programación correspondiendo al qué puede hacer
el lenguaje de programación y al cómo indicar que lo haga. Veamos una definición formal. Según el
Diccionario de Uso del Español de María Moliner:

Semántica: Es el estudio del significado de las unidades lingüísticas.


Sintaxis: Es la manera de enlazarse y ordenarse las palabras en la oración o las oraciones
en el período.

Desde el punto de vista de un lenguaje de programación conocer las reglas sintácticas del lengua-
je implica conocer cómo se usan las sentencias, declaraciones y los otros constructores del lenguaje.
La semántica de un lenguaje de programación representa el significado de los distintos constructores
sintácticos (Pratt y Zelkowitz, 1996). Las reglas de sintaxis de un lenguaje de programación dictan la
forma de un programa. Durante la compilación de un programa, se comprueban todas las reglas de sin-
taxis. La semántica dicta el significado de las sentencias del programa. La semántica define qué suce-
derá cuando una sentencia se ejecute. Dicho de otra forma, saber qué se puede decir en un lenguaje
hace referencia a la componente semántica. Por otro lado, saber cómo hay que decir en un lenguaje lo
que queremos se refiere a la componente sintáctica. En cierto sentido la sintaxis es el continente y la
semántica el contenido. Hay que tener muy en cuenta que un programa que sea sintácticamente correc-
to no tiene por qué serlo semánticamente. Un programa siempre hará lo que le digamos que haga y no
lo que queríamos decirle que hiciera.

2.5. ESTRUCTURA DE UN PROGRAMA


En este texto nos vamos a centrar en la programación desde el punto de vista de los lenguajes imperati-
vos. Desde este punto de vista, consideremos la estructura genérica de un programa. Esto nos propor-
cionará un esquema general que nos servirá para irnos introduciendo en la disciplina de la programación.
Lo primero que debemos indicar es que un programa siempre realiza una o varias tareas. Para
poder llevarlas a cabo se necesita uno o varios algoritmos. El concepto de algoritmo se trata en deta-
lle más adelante en este capítulo, de momento baste saber que un algoritmo puede definirse como:
Elementos de programación y lenguajes 29

Un conjunto finito, ordenado de reglas o instrucciones bien definidas, tal que siguiéndolas
paso a paso se obtiene la solución a un problema dado.

Tengamos claro que un programa implica usar uno o varios algoritmos. En cualquier caso, usan-
do un diagrama de bloques, la estructura genérica de un programa sería la que se muestra en la Figu-

Figura 2.3. Estructura genérica de un programa

ra 2.3.
Todo programa acepta información de entrada (los datos) y la procesa por medio de un/os algorit-
mo/s. El resultado constituye la información de salida que es la que vamos buscando. Por lo que res-
pecta a la parte de procesamiento, ésta debe considerarse en el contexto de los conceptos de eficacia y
eficiencia. Estos dos conceptos son importantes y deben ser claramente distinguidos por el programa-
dor. Eficacia se refiere a la consecución de los objetivos deseados, es decir, que el programa funcione.
Eficiencia se refiere a la consecución de los objetivos con un adecuado consumo de recursos (tiempo,
memoria central o de disco), es decir, que el programa funcione bien. En el terreno de la programación,
el objetivo no es sólo que el programa funcione, sino que funcione y además consuma pocos recursos.
Un programa no es bueno sólo por funcionar, eso es lo mínimo exigible. Se parte de que un programa
debe funcionar. Un programa es bueno o no en función del consumo de recursos que haga.
Por lo que respecta a un programa cualquiera, con sus componentes de entrada-procesamiento-
salida, podemos organizarlo como un solo bloque monolítico de código o modularizarlo (subdividir-
lo) en varios bloques más pequeños. Esta modularización se realizará, en nuestro contexto, desde un
punto de vista funcional, como veremos en el Capítulo 5. Dicho de otra forma, nos vamos a centrar
en las tareas o funciones que el programa debe desarrollar, véase la Figura 2.4.
En un programa monolítico sólo tendríamos un bloque, un solo programa principal (muchas veces

Figura 2.4. Tipos de programas


30 Introducción a la programación con orientación a objetos

denominado en inglés como main). Un programa modular se subdivide en función de las tareas a rea-
lizar. Cada uno de los bloques funcionales sería una unidad, una especie de programa principal que se
comunica con los bloques que necesite para tomar o enviar información. En la Figura 2.4 las flechas
indican el sentido de movimiento de la información. El programa principal es siempre el bloque que
comienza a ejecutarse al arrancar el programa. En un programa monolítico no habría ningún otro blo-
que de código, y en un programa modular existiría un modulo principal, a partir del cual se irían lla-

Figura 2.5. Delimitación del programa principal en Java

mando los otros bloques funcionales.


En particular en Java el programa o sección principal, es el que permite que el programa comien-
ce a funcionar y se define, delimitado por llaves, como un método denominado main dentro de una
clase 4 como en el ejemplo del programa refrán, véase la Figura 2.5.
En orientación a objetos la clase es la “unidad” elemental de organización y las tareas que deba
realizar se definen en ella por medio de los denominados métodos. De momento no entraremos en
detalles sobre clases y métodos.
En un programa tenemos una serie de instrucciones, denominadas sentencias. Éstas pueden eje-
cutarse de dos formas: secuencialmente (se van ejecutando una detrás de otra), o de manera no line-

Figura 2.6. Secuencia de ejecución de instrucciones o flujo lógico de un programa

4
El concepto de método se presentará en detalle en el Capítulo 5. Los conceptos interrelacionados de clase y objeto se
introducirán formalmente en el Capítulo 5 y se considerarán en el Capítulo 7.
Elementos de programación y lenguajes 31

al, véase la Figura 2.6. Cuando la ejecución es no lineal debe existir algún mecanismo (a fin de cuen-
tas una condición que se cumpla o no) que permita que se realice o no la bifurcación. Las bifurcacio-
nes no tienen por qué seguir el orden secuencial, es decir, puede ramificarse de arriba abajo o de abajo
arriba produciendo ciclos o bucles. Estos conceptos se expondrán con más detalle en los Capítulos 3
y 4.

2.6 ERRORES DE UN PROGRAMA


Al escribir un programa siempre se producen errores. Uno de los puntos clave en el proceso de desa-
rrollo de software es la depuración de los errores de un programa. Los errores de programación se pue-
den clasificar en distintos tipos y abordar su eliminación con un proceso sistemático de corrección.
Algunos errores los detecta el compilador, otros los debe encontrar el propio programador. La clasifi-
cación usada organiza los errores en tres tipos:

a) Errores de compilación. Este tipo de errores son detectados por el compilador. Son errores de
compilación los errores de sintaxis o el uso en el programa de tipos de datos incompatibles,
tal como pretender almacenar un valor real en una variable entera.
b) Errores en tiempo de ejecución. Aunque el programa compile bien puede dar error al ejecu-
tarlo, por ejemplo por intentar dividir por cero. En este caso el programa puede que estuviera
bien escrito, pero al adquirir la variable que realiza el papel de divisor el valor cero, y tratar
de realizar la división, es cuando se produce el error y ocasiona que el programa se pare. Los
programas tienen que ser “robustos”, es decir, que prevengan tantos errores de ejecución como
sea posible. En Java muchos errores de ejecución son “excepciones” que pueden ser “captu-
radas” y manejadas. Las excepciones se explicarán en el Capítulo 9.
c) Errores lógicos. Se producen cuando el programa compila bien y se ejecuta bien pero el resulta-
do es incorrecto. Esto ocurre porque el programa no está haciendo lo que debería hacer (le hemos
dicho en realidad que haga otra cosa). Los errores lógicos son los más difíciles de descubrir.

El proceso de localización y corrección de errores se denomina depuración (debugging 5). Es un


proceso que todo programador debe conocer y realizar en todos los programas. Conocer los tipos de
errores que un programa puede tener ayuda a detectar con mayor facilidad los errores reales de un pro-
grama concreto. Existen algunas estrategias para depurar un programa:

a) Eliminación de errores de compilación


Una buena estrategia es acceder al listado de errores y corregirlos en orden, desde el primero hacia el
último. Este orden es el más útil porque muchos de los errores iniciales determinan la existencia de los
siguientes.

b) Eliminación de los errores de ejecución


Para ello debemos localizar el punto del programa en el que se produce el error. Esto se hace siguien-
do la traza (flujo lógico del programa) hasta que éste falla. Así, siguiendo sentencia a sentencia el pro-

5
El término inglés bug puede traducirse por “bicho”. El porqué se denomina debugging “eliminación de bichos” a la
depuración de errores de un programa es cuanto menos curiosa. En los primeros tiempos de los computadores, cuando la escri-
tura de un programa consistía en cablear (soldar) una serie de cables en los ordenadores, un programa se empeñaba en fallar
sistemáticamente. Tras tratar de encontrar el “error” del programa se descubrió que un insecto estaba achicharrado entre un
par de conectores en la máquina, cortocircuitando el sistema. A partir de ese momento se empezó a utilizar el término debug-
ging para representar (con cierta sorna) el proceso de depuración de errores de un programa.
32 Introducción a la programación con orientación a objetos

grama localizaremos el punto, la sentencia, en el que se produce el fallo. A continuación, se debe ana-
lizar la sentencia a fin de identificar la causa del error. Es muy útil para esto consultar el valor de las
variables involucradas. Una vez identificado el problema el siguiente paso es su corrección, pasando
a continuación al siguiente error hasta que no haya ninguno más.

c) Eliminación de los errores de lógica


Es la tarea más difícil. Si el programa funciona pero da un resultado erróneo, lo mejor es tomar un
ejemplo conocido e ir contrastando los resultados intermedios del ejemplo con los que da el progra-
ma. En este último caso hay que ir, normalmente, consultando los resultados parciales del programa
en puntos concretos del mismo, estableciendo lo que se denomina breakpoints. Un breakpoint (punto
de ruptura) es simplemente un lugar del programa donde nos detenemos para consultar qué es lo que
el programa ha hecho hasta ese momento. Normalmente lo que se hace es consultar el contenido de
las variables que el programa maneja. Los entornos de desarrollo de software integrados modernos
incorporan la capacidad de fijar breakpoints y analizar sentencia a sentencia un programa. Cuando se
localice el primer resultado diferente entre el ejemplo conocido y el programa, probablemente se habrá
localizado la causa que produce el error. El siguiente paso es corregirlo.

2.7. ALGORITMOS
Anteriormente hemos indicado que un programa funciona aplicando un o una serie de algoritmos. Esto es
importante, para que un ordenador pueda llevar a cabo una tarea determinada (resolver un problema) debe
indicársele de forma precisa cómo realizarla. Ésta es la misión de los algoritmos. Lo primero es adquirir
una noción clara de qué es un algoritmo. Recordando la definición dada previamente, tenemos que:

Un algoritmo es un conjunto finito, ordenado de reglas o instrucciones bien definidas tal que
siguiéndolas paso a paso se obtiene la respuesta a un problema dado.

Esta definición nos indica que no todos los métodos de solución de un problema son susceptibles
de ser utilizados por un computador. Como el ordenador sólo interpreta instrucciones, el procedi-
miento debe:
a) Estar compuesto de acciones bien definidas.
b) Constar de una secuencia finita de operaciones.
c) Acabar en un tiempo finito.
Estos tres puntos indican que el algoritmo debe estar definido en términos que el ordenador pue-
da entender, es decir, en función de acciones que se puedan realizar con el ordenador. También, un
algoritmo para ser útil debe tener un número finito de operaciones. De nada serviría un algoritmo que
para resolver un problema necesitara realizar un número infinito (o tremendamente grande) de opera-
ciones. Por la misma razón, el algoritmo debe poder ejecutarse en un tiempo finito y no que la secuen-
cia de acciones implique una cantidad de tiempo que aunque no sea infinita sea muy larga. La idea
básica es que un algoritmo debe ser una “receta” práctica para resolver un problema.
Para resolver un mismo problema se pueden utilizar muchos algoritmos. ¿Nos basta con usar un
algoritmo cualquiera o hay algún criterio para escoger un algoritmo entre varios? Lógicamente, siem-
pre se debe intentar seleccionar el “mejor” algoritmo. El punto clave es cómo definir el “mejor” algo-
ritmo. El problema se soluciona a partir de los, previamente mencionados, conceptos de eficacia y
eficiencia. Todo algoritmo debe ser eficaz (debe resolver el problema), pero no todos son igualmente
Elementos de programación y lenguajes 33

eficientes (no usan los mismos recursos para resolver el problema). La eficiencia de un algoritmo está
siempre referida al problema concreto que resuelve el algoritmo. Se puede definir algún índice de efi-
ciencia que nos sirva para comparar distintos algoritmos para el mismo problema. ¿Cómo se determi-
na la “eficiencia” de un algoritmo? Éste es el problema del análisis de algoritmos.
Los algoritmos se pueden evaluar según diversos criterios. Frecuentemente estamos interesados en
la velocidad de crecimiento del tiempo o el espacio requeridos para resolver mayores y mayores casos
de un problema (tiempo de cómputo y espacio de almacenamiento). Para ello se asocia un entero con
el problema que se denomina tamaño del mismo. El tamaño es una medida de la cantidad de datos de
entrada en el algoritmo. Este tamaño puede ser el número de elementos a manejar. Por ejemplo, en la
multiplicación de matrices el tamaño podría ser la dimensión de la mayor matriz que se pueda tratar.
En un algoritmo de ordenación podría ser el mayor número de elementos que se puedan ordenar.
El tiempo empleado por un algoritmo como función del tamaño del problema se denomina com-
plejidad temporal del algoritmo. El comportamiento límite de la complejidad con el aumento del
tamaño se denomina complejidad temporal asintótica del algoritmo. Similares parámetros pueden deri-
varse con respecto al espacio (memoria): complejidad espacial y complejidad espacial asintótica.
La complejidad asintótica es la que determina en la práctica el tamaño, n, de los problemas que
puede tratar el algoritmo. El tiempo de trabajo del algoritmo se puede considerar como una función
del tamaño, tiempo5f(n). Dentro de esa función se considera como complejidad del algoritmo, u orden
del algoritmo, el término que crece más rápido con n de todos los que tenga la expresión. Sería el tér-
mino que determina el resultado en el límite de n‡. Si un algoritmo procesa una entrada de tamaño n
en un tiempo cn2, para una constante dada c, decimos que la complejidad temporal del algoritmo es
O(n 2) (orden n 2). La notación “O” es un tipo de notación asintótica y se usa mucho (Rawlins, 1992).
Por ejemplo, supongamos cinco algoritmos con distinta complejidad temporal, véase la Tabla 2.1.
Analicemos cuántos elementos se pueden procesar en 1 segundo y en 1 hora dependiendo de las com-

Tabla 2.1. Comportamiento de cinco algoritmos con distintas complejidades

Tamaño máximo manejable (n)


Algoritmo Complejidad
1 segundo 1 hora
1 n 1000 3.6 106
2 n log2 n 140 2.0 105
3 n2 32 1897
3
4 n 10 153
n
5 2 10 21

plejidades del algoritmo dadas en Tabla 2.1. Suponiendo que un elemento se procesa en 1 ms, el pri-
mer algoritmo podría procesar en un segundo un tamaño de n51000 (n*1ms51s). En los otros casos
tenemos los datos que se presentan en la Tabla 2.1. Observemos la tremenda diferencia entre el pri-
mer caso (complejidad lineal con n) y el último (complejidad exponencial con n). El número de ele-
mentos que se pueden procesar es muchísimo mayor en el primer caso. Para los algoritmos listados
en la Tabla 2.1, el primero es el más eficiente de todos.

2.8. INGENIERÍA DEL SOFTWARE


En la actualidad el desarrollo de software es una actividad de gran interés tanto económico como
social. Los sistemas software que se desarrollan son complejos y deben ser tan fiables como sea posi-
ble. El desarrollo de software es por tanto una actividad que debe realizarse de forma organizada y
34 Introducción a la programación con orientación a objetos

controlada. Esto que ahora parece evidente no lo ha sido siempre.

2.8.1. CONCEPTO DE INGENIERÍA DEL SOFTWARE


En la primera época de las computadoras, el hardware no era potente y por lo tanto el software no podía
ser altamente complejo. En estas condiciones los programas eran desarrollados por personal que aborda-
ba dicha labor sin utilizar técnicas de trabajo formalizadas. Dicho de otra manera, el desarrollo de soft-
ware tenía mucho de arte, dependiendo su calidad final de la experiencia y habilidad del programador.
En este contexto se dice que el desarrollo de software era artesanal. A mediados de los años sesenta del
siglo XX el hardware disponible comenzaba a ofrecer mayores posibilidades. Los sistemas software que
se comenzaban a desarrollar eran sistemas donde la complejidad era ya un factor a tener en cuenta. Al
abordar el desarrollo de estos sistemas grandes y complejos con las técnicas artesanales, el resultado era
que las planificaciones de tiempo y coste se quedaban muy cortas, rebasándose ampliamente las predic-
ciones originales. La conclusión estaba clara: las técnicas artesanales no servían para tratar el desarrollo
de sistemas complejos. El problema desde el punto de vista del desarrollo de software era muy impor-
tante, por lo que desató mucho interés y trabajo. La falta de control sobre el desarrollo de sistemas soft-
ware se denominó crisis del software, denominación que se sigue usando hoy en día (Gibbs, 1994).
El problema de la crisis del software era de tal magnitud que en la reunión del Comité Científico de
la OTAN realizada en 1968 se estudió el problema y se propuso una solución. La solución consistía en
tratar el software (y en particular su desarrollo) con las técnicas ingenieriles usadas por las industrias de
productos físicos. Este ideal (aún no alcanzado) se denominó ingeniería del software. La ingeniería del
software se puede definir como la aplicación de una aproximación sistemática, disciplinada y cuantifi-
cable al desarrollo, operación y mantenimiento del software (Pressman, 2002). La idea es aplicar al soft-
ware la filosofía ingenieril que se aplica al desarrollo de cualquier producto. La ingeniería del software
pretende el desarrollo de software de manera formal, cumpliendo unos estándares de calidad 6.
El punto de vista de la ingeniería del software trasciende a la mera programación. El software se
considera dentro de un ciclo de vida que va desde la concepción del producto, pasando por las etapas
de análisis del problema, diseño de una solución, implementación de la misma y mantenimiento del
producto, hasta su final retirada por obsolescencia.

2.8.2. CICLO DE VIDA DEL SOFTWARE


Un concepto fundamental en ingeniería del software es la consideración de un producto software en el

Figura 2.7. Esquema genérico del ciclo de vida de un producto software

6
Otro problema en el campo de la ingeniería del software es cómo definir el concepto de “calidad” en un producto soft-
ware. Sólo con una definición precisa y, a poder ser, en términos de magnitudes cuantificables, podríamos controlar el nivel
de calidad de un producto software.
Elementos de programación y lenguajes 35

contexto de un ciclo de vida que consta de una serie de etapas o fases. Desde un punto de vista gene-
ral, todo software pasa por tres etapas fundamentales: Desarrollo, Uso y Mantenimiento, véase la Figu-
ra 2.7. Consideremos estas tres etapas por separado.
a) Desarrollo. Inicialmente la idea para un programa es concebida por un equipo de desarrollo 7
de software o por un usuario con una necesidad particular. Este programa nuevo se construye
en la denominada etapa de desarrollo. En esta etapa tenemos varios pasos a realizar: análisis,
diseño, implementación y pruebas. Al final, generamos un programa operativo.
b) Uso. Una vez desarrollado el programa y después de considerar que está completo, que es
operativo, se pasa a los usuarios. La versión del programa que van a utilizar los usuarios se
denomina release o versión del programa.
c) Mantenimiento. Durante el uso, utilizamos el programa en su entorno normal de trabajo y
como consecuencia de la aparición de errores, del cambio de algún elemento de entorno (soft-
ware o hardware) o de la necesidad de mejorar el programa, éste se modifica. Esto es el man-
tenimiento. Casi siempre los usuarios descubren problemas en el programa. Además, hacen
sugerencias de mejoras en el programa o de introducción de nuevas características. Estos
defectos y nuevas ideas los recibe el equipo de desarrollo y el programa entra en fase de man-
tenimiento. Estas dos últimas etapas, uso y mantenimiento, se entremezclan y, de hecho, sólo
se habla de etapa de mantenimiento desde el punto de vista de la ingeniería del software.

La anterior clasificación es muy general. En particular, la etapa de desarrollo consta de una serie de
actividades o etapas bien definidas. Para cualquier herramienta software las etapas de su ciclo de vida
son las mismas y se recogen en la Figura 2.8. Vamos a considerar cada una de estas etapas por separa-
do.

a) Análisis. En la etapa de análisis se especifica el qué queremos. En este apartado se determinan los
requisitos que deberá cumplir el programa de cara al usuario final. También se imponen las con-
diciones que debe satisfacer el programa, así como las restricciones en el tiempo de desarrollo. La
persona o personas que encargan el desarrollo del software imponen los requisitos iniciales. Éstos
suelen ser incompletos o ambiguos. El equipo de desarrollo de software debe trabajar para refinar
esos requisitos hasta que todas las características clave estén bien prefijadas. En esta etapa se rea-
liza una determinación de requisitos y su posterior organización, para acabar obteniendo un mode-
lo lógico del sistema software a construir. Es fundamental entender y recoger todos los requisitos
de nuestra herramienta. Un error en esta etapa se transmite a las siguientes y cuanto más tarde se
corrija más esfuerzo implicará. Para estas tareas existen técnicas formales basadas en diagramas
de distintos tipos. Aunque no sea específicamente orientado a objetos, el texto de Yourdon sobre
técnicas de análisis y modelado es una referencia obligada (Yourdon, 1989).
b) Diseño. En esta etapa se determina cómo se consiguen realizar los requisitos recogidos y

Figura 2.8. Etapas del ciclo de vida del software

7
Cuando nos refiramos a un equipo de desarrollo no debe entenderse necesariamente como un grupo de personas, depen-
diendo de cada caso puede tratarse de una sola.
36 Introducción a la programación con orientación a objetos

organizados en la etapa anterior. Es importante obtener un buen diseño, ya que muchos de los
problemas en el software se pueden atribuir a un mal diseño inicial. Para ello se deben explo-
rar distintas alternativas. A menudo, el primer intento de diseño no es el mejor. En esta etapa,
una de las partes más importantes es la definición del algoritmo y de las estructuras de datos
a usar en el programa. En la aproximación orientada a objetos, el diseño incluye la determi-
nación de las clases, sus propiedades y métodos, los objetos y la relación entre ellos. El diseño
del programa se debe revisar varias veces. Después de que el diseño se ha desarrollado y refi-
nado se pasa a la fase de implementación.
c) Implementación o codificación. Una vez realizado el diseño hay que implementarlo, generan-
do el código fuente. Se trata de traducir el diseño a un lenguaje de programación determinado.
Muchos programadores se centran sólo en la implementación cuando realmente ésta es la eta-
pa menos creativa del proceso de desarrollo de software. Tanto es así que existen generadores
de código, programas capaces de escribir el código fuente en un lenguaje determinado a partir,
claro está, de una indicación de diseño. La implementación se centra en los detalles de la codi-
ficación, como el uso de un estilo adecuado o el desarrollo de la correspondiente documenta-
ción. Una vez concluida la implementación, el programa debe probarse de manera sistemática.
d) Pruebas. Lo primero de todo es tener claro que el objetivo de las pruebas es descubrir erro-
res, no demostrar que el programa funciona. Las pruebas deben diseñarse con la intención de
encontrar errores. De esta forma, una prueba con éxito es la que descubre un error. Para rea-
lizar las pruebas y localizar errores se irá viendo como responde el programa, o los módulos
o componentes del programa, a distintas entradas de información. Encontrando y fijando erro-
res se incrementa la precisión y fiabilidad del programa. Las pruebas deben diseñarse de tal
forma que cubran todas las posibilidades lógicas dentro del programa, para asegurarse de que
cumple el propósito deseado y que satisface todos los requisitos. Una indicación general es
que los casos de prueba deben acabar cubriendo todos los arcos del grafo de flujo de control
del programa 8. Esto se consigue evaluando como verdaderas y como falsas todas las condi-
ciones (explícitas o implícitas en bucles) que aparecen en el código.
e) Mantenimiento. Es la etapa que va desde la obtención de una herramienta operativa, al final
de la etapa de desarrollo, hasta la retirada del programa por obsolescencia. El mantenimien-
to de software es el proceso de modificar un programa para mejorarlo o corregir problemas.
Los cambios se hacen sobre una copia del programa, así el usuario puede utilizar la versión
original mientras el programa está siendo mantenido. Cuando es necesario realizar una modi-
ficación importante del programa, el resultado es una variante importante del mismo, una
nueva versión del programa. Cuando se considera que ya no es útil seguir realizando el man-
tenimiento de un programa sino desarrollar uno nuevo, el programa está obsoleto y se retira
de su uso activo. En este momento concluye el ciclo de vida (desarrollo, uso, mantenimien-
to y finalmente retirada). Su duración varía según cada programa. Sin embargo, sí podemos
indicar de forma general que la etapa de mantenimiento es la más larga, la que más esfuerzo
implica en el ciclo de vida. El esfuerzo de mantenimiento representa alrededor del 80% del
esfuerzo total para todo el ciclo de vida. En la Figura 2.9 vemos la distribución típica del
esfuerzo total asociado al ciclo de vida en esfuerzo de desarrollo y esfuerzo de manteni-
miento.

El equipo de mantenimiento no suele, ni tiene por qué, ser el mismo que el equipo de desarrollo
del programa. El éxito del mantenimiento depende de la habilidad de la/s persona/s encargada/s de él
para entender el programa, determinar cuál es el problema y corregirlo. Por tanto, todo lo que ayude
a realizar un mantenimiento más fácil es tremendamente útil. Esto indica que la generación de docu-
mentación adecuada en la etapa de desarrollo es de gran importancia, así como la aplicación de téc-

8
Esto se relaciona directamente con los estudios de complejidad ciclomática del grafo de flujo de control realizados por
McCabe (McCabe, 1976).
Elementos de programación y lenguajes 37

Figura 2.9. Proporción entre el esfuerzo de desarrollo y el de mantenimiento en el ciclo


de vida del software

nicas estandarizadas. La capacidad para leer y entender un programa depende de lo bien que esté ana-
lizado, diseñado y documentado, es decir, del esfuerzo implicado en la etapa de desarrollo. Cuando
los requisitos no están claros, o adecuadamente organizados, o no están documentados, o incluso
cuando el diseño es pobre porque “el programa se entiende” obtenemos una fuente de problemas.
Conseguiremos un software innecesariamente complejo y difícil de entender. Tendremos muchas
complicaciones para corregir errores o introducir modificaciones posteriormente. Cuanto más com-
plejo es un programa más fácil es introducir errores durante el desarrollo, y más difícil eliminarlos
cuando se encuentren. Cuanto más pronto se encuentren los errores, más fácil y menos costoso será
corregirlos.
Pretender crear un programa sin una planificación cuidadosa es como pretender construir una casa
sin diseñarla. La casa se caerá una y otra vez mientras vamos corrigiendo problemas, hasta que acer-
temos con el diseño. Incluso pequeños cambios en la etapa de desarrollo se reflejan en grandes cam-
bios en la etapa de mantenimiento. Es mejor emplear más tiempo y más cuidado en la etapa de
desarrollo, porque ello ahorra tiempo de mantenimiento y reduce el esfuerzo de todo el ciclo de vida.
El lector interesado en un estudio detallado del proceso de ingeniería del software puede consultar el
texto de Pressman (Pressman, 2002).

EJERCICIOS PROPUESTOS

Ejercicio 1.* Según el nivel de abstracción, cite cuatro tipos de lenguaje de pro-
gramación (desde el nivel más alto al más bajo).

Ejercicio 2.* ¿Qué tipo de lenguaje es más rápido, el compilado o el interpreta-


do? ¿Por qué?

Ejercicio 3.* ¿Qué tipos de errores se pueden dar en un programa?

Ejercicio 4.* ¿Qué tipo de error ha sucedido si el programa ha compilado bien


pero el resultado es incorrecto?

Ejercicio 5.* ¿Qué técnica de debugging utilizaría para detectar un error lógico?

Ejercicio 6.* ¿Cuáles son las etapas del ciclo de vida del software?
38 Introducción a la programación con orientación a objetos

Ejercicio 7.* ¿Cuál es la etapa del ciclo de vida que más esfuerzo implica?

Ejercicio 8.* ¿En qué etapa se determinan los requisitos y las restricciones?
Ejercicio 9.* ¿Qué algoritmo es más complejo, el A con una complejidad tempo-
ral n3 o el B con una complejidad temporal de n?

Ejercicio 10.* ¿Cuántos subprogramas hay en un programa monolítico?

Ejercicio 11.* ¿En qué etapa del ciclo de vida del software se escogen las estruc-
turas de datos más apropiadas para el problema a resolver?

REFERENCIAS
ARNOW, D. y WEISS, G.: Introduction to Programming using Java. An Object Oriented Approach, Addison-Wes-
ley, 1998.
GIBBS, W. W., “Software’s Chronic Crisis”, Scientific American, 72-81, September 1994.
KAMIN, S. N.; MICKNUNAS, M. D. y REINGOLD, E. M., An Introduction to Computers Science Using Java, WCB
McGraw-Hill, 1998.
MCCABE, T. J.: “A Complexity measure”, IEEE T. Software Eng., 308-320, SE-2 (4), December 1976.
PRATT, T. W. y ZELKOWITZ, M. V.: Programming Languages. Design and Implementation, 3td Edition, Prentice
Hall, 1996.
PRESSMAN, R. S.: Ingeniería del Software, McGraw-Hill, 5.ª Edición, 2002.
RAWLINS, G. J. E., Compared to what? An introduction to the analysis of algorithms, Computer Science Press,
1992.
YOURDON, E.: Modern Structured Analysis, Prentice-Hall, 1989.
3

Introducción a la programación

Sumario

3.1. Introducción 3.5. Operadores


3.2. Conceptos generales 3.5.1. Operadores aritméticos
3.3. Tipos de datos 3.5.2. Operadores de incremento
3.3.1. Tipos de datos primitivos y decremento
3.3.2. Variables y constantes 3.5.3. Operadores relacionales
3.3.3. Representación interna de datos 3.5.4. Operadores lógicos
3.3.4. Conversiones de tipo 3.5.5. Operadores de asignación
3.4. Instrucciones
3.4.1. Instrucciones de asignación
3.4.2. Instrucciones de entrada/salida
3.4.3. Instrucciones de ramificación
40 Introducción a la programación con orientación a objetos

3.1. INTRODUCCIÓN
Todo lenguaje contiene constructores específicos (palabras reservadas, elementos) que permiten reali-
zar las operaciones básicas en dicho lenguaje. En este capítulo se van a presentar los constructores
básicos de la programación imperativa, como son los tipos de datos, las variables y los operadores.
También presentaremos las instrucciones básicas y las usaremos para mostrar cómo se trabaja con un
lenguaje de programación. Consideraremos además, los operadores que permiten realizar operaciones
distintas de la asignación. En este capítulo expondremos todos los conceptos desde un punto de vista
genérico. El lector debe entender que los diferentes conceptos expuestos son generales y no particula-
res de un lenguaje determinado. Por tanto, el corazón del capítulo son consideraciones semánticas.
Lógicamente, los conceptos genéricos se llevarán a la práctica en un lenguaje determinado, en este
caso Java. Dicho de otra manera, una vez expuestas las consideraciones semánticas, las consideracio-
nes sintácticas se expondrán en dicho lenguaje. Al concluir el tema el lector estará en condiciones de
escribir programas en Java simples, pero completos y operativos.

3.2. CONCEPTOS GENERALES


En un lenguaje de programación se puede definir un programa como un conjunto de sentencias, y una
sentencia como una aserción matemática o lógica, o una frase o conjunto de frases informativas. Las
sentencias pueden ser de distintos tipos:

a) De especificación o declaración: No implican una operación matemática o lógica.


b) Ejecutables: Implican una operación matemática o lógica.
c) Comentario: Informativas, ignoradas por el computador.

Para construir un programa es necesario conocer cómo construir dichas sentencias en un lenguaje
determinado. A su vez, las sentencias se pueden considerar constituidas por tres elementos:

a) Datos.
b) Instrucciones.
c) Operadores.

A lo largo del tema vamos a ir considerando en detalle cada uno de estos elementos.

3.3. TIPOS DE DATOS


Cualquier programa, independientemente del lenguaje usado, se puede entender como un conjunto de
operaciones que se aplican a ciertos datos en una cierta secuencia (Pratt y Zelkowitz, 1996). La dife-
rencia básica entre los lenguajes se refiere a los tipos de datos permitidos, a los tipos de operaciones
disponibles y a los mecanismos provistos para el control de la secuencia en la que se aplican las ope-
raciones a los datos. Estos tres conceptos, datos, operaciones y control, forman el marco de discusión
y comparación de lenguajes. Un concepto de importancia capital en programación es el de tipo de dato.
Los datos que se manejan en un programa están organizados más o menos rígidamente en diferentes
categorías (los tipos). Sobre cada tipo se pueden realizar determinadas operaciones que no tienen por
qué ser equivalentes de un tipo a otro. El concepto de dato está muy relacionado con las operaciones
que se pueden realizar sobre él. En el ordenador, los diferentes tipos se representan de forma diferen-
te, y por lo tanto, para él son distintos. Un ejemplo típico de tipo de dato simple es el tipo entero
(número entero) o real (número real). Internamente se representan de forma diferente y dependiendo
del lenguaje puede ser más o menos fácil asignar un tipo a otro (convertir de uno a otro). Los lengua-
Introducción a la programación 41

jes que aplican una gestión estricta de tipos de denominan de tipos fuertes o de tipos estrictos. Java es
un lenguaje de tipos estrictos.
De forma más rigurosa, un tipo de dato queda definido como un conjunto de valores junto con un
conjunto de operaciones para crearlos y manipularlos. Cada valor almacenado en memoria se asocia
con algún tipo de dato en particular. Los tipos de datos simples, predefinidos, que encontramos en un
lenguaje se denominan tipos de datos primitivos. Las características de estos tipos de datos primitivos
se corresponden con características disponibles del hardware del computador. Dicho de otra forma, su
representación y manipulación se relaciona directamente con las características físicas del sistema (una
serie de bits con un significado concreto). Cuando las características atribuidas al dato se simulan por
software se habla de estructuras de datos (por ejemplo, un nuevo tipo de datos constituido por una
combinación de datos simples).

3.3.1. TIPOS DE DATOS PRIMITIVOS


Los tipos de datos primitivos son prácticamente los mismos en todos los lenguajes de programación. La
clasificación típica se recoge en la Figura 3.1. Esta clasificación básica se particulariza en cada lenguaje
de programación. En concreto en Java, la clasificación genérica da lugar a ocho tipos primitivos de datos:

a) 4 tipos de enteros.
b) 2 tipos de reales.
c) 1 tipo carácter.
d) 1 tipo lógico.

Figura 3.1. Organización típica de los tipos primitivos de datos


42 Introducción a la programación con orientación a objetos

Cada tipo tiene su nombre específico. Java distingue entre mayúsculas y minúsculas 1 así que es
necesario respetar con cuidado las palabras reservadas que identifican cada tipo primitivo, ocurriendo
lo mismo con los nombres de las variables 2. Por lo que respecta a los tipos, cada uno presenta un con-
junto de literales. Un literal representa uno de los posibles valores del tipo considerado, como 3 ó 5
para un tipo entero. Vamos a considerar los diferentes tipos primitivos en Java.

a) Tipos enteros
Existen cuatro tipos de datos primitivos para los enteros, que difieren por la cantidad de memoria que
requieren para ser almacenados. Cada tipo tiene un intervalo de valores que son los que se pueden
representar con él. Más adelante veremos cómo se realiza la representación de valores. El nombre del
tipo, la cantidad de bits usados para representarlo, y el valor máximo y mínimo que se puede repre-
sentar se recogen en la Tabla 3.1.

Tabla 3.1. Tipos de datos enteros

Nombre Memoria usada (bits) Valor mínimo Valor máximo


byte 8 2128 127
short 16 232768 32767
int 32 22147483648 2147483647
long 64 <29.22 1018 >9.22 1018

b) Tipos reales
Tenemos dos tipos de reales que sirven para representar (simular) números reales (números con parte
decimal). Estos tipos se denominan también de punto flotante, haciendo referencia al punto que usan
los anglosajones para indicar la parte decimal. Los tipos reales en Java y sus características se recogen
en la Tabla 3.2. Con float tenemos 7 dígitos significativos y con double 15 dígitos significativos.

Tabla 3.2. Tipos de datos reales

Nombre Memoria usada (bits) Valor mínimo Valor máximo


float 32 . 23.4 10 38
.3.4 10 38
double 64 . 21.7 10 308 .1.7 10 308

Estos dos tipos de reales son típicos en los lenguajes de programación. El primer tipo, float en
Java, se denomina a veces real de precisión simple y el segundo, double en Java, se denomina real
en doble precisión. Debemos conocer cuál es el intervalo de variación y el orden de magnitud de nues-
tros valores para usar un tipo adecuado. Por lo que respecta a los tipos reales, y dada la cantidad de
memoria en los sistemas actuales y el funcionamiento de los métodos matemáticos, en Java es reco-
mendable usar el tipo double.

1
Muchas características del lenguaje Java se entienden considerando que es un descendiente del lenguaje C. La distin-
ción entre mayúsculas y minúsculas es una de ellas.
2
Existen identificadores que son propios del lenguaje como class o main. El programador no puede usar estos iden-
tificadores como nombres de variables. Estos identificadores se denominan palabras reservadas, tienen significado y utilidad
específica y no pueden usarse para otra cosa.
Introducción a la programación 43

c) Tipo carácter
El tipo carácter almacena un símbolo alfanumérico (uno sólo) y se identifica como char. Un valor
char almacena un carácter simple del conjunto de caracteres Unicode. Un conjunto de caracteres es una
lista ordenada de caracteres. El conjunto de caracteres Unicode utiliza 16 bits para representar cada
carácter, pudiendo representar, por tanto, 216 (65536) caracteres diferentes. Unicode es un conjunto
internacional de caracteres y contiene los caracteres y símbolos provenientes de muchas lenguas del
mundo, como los caracteres latinos, árabes, chinos o cherokees, entre otros muchos (Unicode, 2002).
Otro conjunto de caracteres muy extendido es el ASCII. El ASCII original usaba 7 bits por carác-
ter y, por tanto, soportaba 27 (128) caracteres. Esto era suficiente para el conjunto de caracteres usa-
dos en inglés. Para reflejar otros alfabetos, el conjunto de caracteres ASCII se extendió para usar 8 bits
por carácter y el número de caracteres a representar se dobló a 256 (28). Sin embargo, se queda corto
para representar caracteres provenientes de los distintos idiomas que hay en el mundo por lo que el
equipo de desarrollo de Java escogió el conjunto de caracteres Unicode. Unicode está definido de for-
ma que el ASCII sea un subconjunto suyo.
En los conjuntos de caracteres encontramos:
Letras mayúsculas: A, B, C, ...
Letras minúsculas: a, b, c, ...
Signos de puntuación: (.) punto, (;) punto y coma, ...
Dígitos: 0, 1, 2, ...
Símbolos especiales: #, &, |, \, ...
Caracteres de control: retorno, tab, ...
En particular, los caracteres están en orden alfabético, con el número de código aumentado en el
propio orden alfabético. Así se puede luego ordenar alfabéticamente usando algoritmos apropiados.
El blanco también es un carácter y se usa memoria para representarlo, como para cualquier otro
carácter. Los caracteres de control tienen un significado especial para el ordenador, pero no para el
usuario. Ejemplo de estos caracteres es el que indica una tabulación en un texto o el que le indica al
sistema que debe saltar a una línea nueva. Dichos caracteres no se visualizan, por lo que a veces se
denominan caracteres no imprimibles o caracteres invisibles. En cualquier caso, tienen asignado un
código específico que los representa, y son tan válidos como cualquier otro carácter.
El tipo carácter almacena un solo carácter. Un literal de tipo carácter se denota con comillas sim-
ples como, ‘A’, ‘B’. Cuando se trata de varios caracteres literales formando una frase, una palabra, en
programación se habla de cadenas de caracteres. En algunos lenguajes existe un tipo primitivo cade-
na. En Java no es así, no existe el tipo primitivo cadena. Para representar las cadenas alfanuméricas
existe una clase 3 denominada String (cadena en inglés). Como veremos más adelante, una cadena
de caracteres se delimita con dobles comillas como en la siguiente palabra, “Ejemplo”. Es necesario
recordar este punto: con comillas simples se delimitan caracteres, con comillas dobles cadenas.

d) Tipo lógico
Este tipo primitivo se usa para representar los dos posibles valores lógicos: verdadero (true) o falso
(false) 4. El nombre del tipo en Java es boolean y los dos únicos posibles valores son true o fal-
se. Estrictamente hablando sólo se necesitaría un bit para almacenarlo (con los dos valores 0 y 1 se
pueden representar las dos posibilidades de true y false).

3
El concepto de clase es el elemento central en orientación a objetos. Una clase es mucho más general que un tipo pri-
mitivo. Como ya veremos más adelante, una clase puede contener uno o más datos de tipo primitivo y contener también pro-
cedimientos para manipular dichos datos.
4
Las palabras reservadas true y false (verdadero y falso en inglés) se usan generalmente en los lenguajes de progra-
mación para representar los dos posibles valores lógicos.
44 Introducción a la programación con orientación a objetos

3.3.2. VARIABLES Y CONSTANTES


Como se ha comentado anteriormente, en los programas se manejan datos. Es necesario, por lo tanto,
disponer de un mecanismo que permita el almacenamiento y la manipulación de los datos. Estas labo-
res se realizan en un programa por medio de las entidades denominadas variables y constantes. Ana-
licemos cada una de ellas.

a) Variables
Una variable es el nombre que asignamos para una posición (posiciones) de memoria usada para alma-
cenar un valor de cierto tipo de datos. Las variables deben declararse (definirse) antes de usarse. Cuan-
do se declara una variable estamos reservando una porción de memoria principal para almacenar
valores correspondientes al tipo de la variable. La declaración de las variables implica el dotarlas de
un nombre denominado identificador de la variable. El valor que almacena una variable se puede
modificar a lo largo del programa.
La sintaxis de la declaración de una variable en Java es:

tipo_de_dato nombre_de_la_variable;

(en Java el ; sirve para indicar el fin de sentencia)


Por ejemplo,

int total;

Aquí declaramos una variable denominada total que puede almacenar valores enteros de tipo int.
Se pueden declarar varias variables en una misma sentencia,

int total, cuenta, suma;

En este caso total, cuenta y suma son los nombres (identificadores) de tres variables de tipo ente-
ro.
Las variables pueden inicializarse (darles un valor inicial) en la propia declaración:

int total = 0, cuenta = 20;


float precioUnitario = 49.32;

Se dice que total vale 0, cuenta 20 y precioUnitario 49.32. Es decir, estas variables están
almacenando esos valores en su posición de memoria correspondiente. Las variables se pueden enten-
der como contenedores de valores. La variable puede existir a lo largo de todo un programa, pero se
puede modificar el valor que almacena.

b) Constantes
En programación una constante es una entidad similar a una variable, pero con la diferencia de que
tras su asignación el valor que contiene no se puede cambiar en el programa. Se puede considerar
como un tipo especial de variable donde después de asignarle un valor, dicho valor no se puede modi-
ficar. Si se intenta modificar el valor, se produce un error.
La sintaxis de la declaración de una constante en Java es muy similar a la declaración de una varia-
ble, pero en este caso hay que añadir al principio la palabra reservada “final”:
Introducción a la programación 45

final tipo_de_dato nombre_de_la_variable;


Un ejemplo sería:

final double e=2.718281828;

En el ejemplo se declara una constante de tipo real (en doble precisión, double) que almacena el
número e, la base de los logaritmos naturales.
Respecto a las variables, es importante conocer en un lenguaje cuál es la parte del programa don-
de están definidas. Esto es, si una variable una vez declarada se puede usar en cualquier parte del pro-
grama o si su alcance está limitado a alguna zona. De una forma u otra los lenguajes están organizados
en bloques. Dependiendo del lenguaje, estos bloques pueden estar claramente indicados con algún
símbolo o palabra reservada o pueden estar definidos implícitamente. En Java los bloques de código
se indican entre llaves ({ }). Es importante señalar que en Java una variable queda definida únicamente
dentro del bloque (sección entre una pareja de símbolos { }) en el que se ha declarado. Dicho de otra
forma, su alcance 5 es el bloque en el que se ha declarado. Si se intenta usar la variable fuera del blo-
que en el que se ha declarado, se producirá un error de variable no declarada (este error indica que en
la parte del programa donde se pretende usar, la variable es inexistente). La situación se ilustra en el
Programa 3.1 6.

Programa 3.1. Ilustración del alcance de las variables en Java

1class Alcance{
2 public static void main(String [] args) {
3 int numero=100;
4 if (numero<103){
5 int dentroAmbito=3;
6 System.out.println(“Dentro del bloque “+dentroAmbito);
7 } // Cierra el bloque del if
8 System.out.println(“Fuera del bloque solo existe numero “
10 +numero);
11 } // Cierra el bloque del main
12} // Cierra la clase

En el Programa 3.1 hemos incluido el número de línea como ilustración. En Java no se numeran
las líneas, así que si deseáramos probar el Programa 3.1 deberíamos eliminar los números de línea. En
el programa se define una clase denominada Alcance (línea 1) dentro de la cual aparece un bloque
de instrucciones llamado main (principal) en la línea 2. Este bloque es un ejemplo de un método en
Java. Un método es un bloque de instrucciones identificado por un nombre que se puede llamar des-
de otra parte de un programa y que se maneja como una unidad. De momento y hasta que se exponga
el tema de la definición de clases, los ejemplos constarán de una sola clase donde se define el método
main. El método main es el punto de arranque de todo programa en Java, es por él por donde empie-
za a ejecutarse el programa.
En el programa se declara una variable llamada numero (línea 3) cuyo alcance es todo el método
main y que se inicializa a 100. A continuación, aparece una sentencia if (si condicional) que pregunta
si numero es menor que 103, en cuyo caso se declara una variable llamada dentroAmbito (línea 5)

5
El concepto de alcance o ámbito (en inglés scope) de un elemento es general en programación y siempre hace referen-
cia a la zona donde un elemento en cuestión existe y puede usarse.
6
Debido a que la salida estándar por pantalla no soporta los acentos, éstos no se incluirán en las sentencias
System.out.println () y System.out.print ().
46 Introducción a la programación con orientación a objetos

con valor 3. Luego se imprime por la pantalla con la llamada a System.out.println () el valor de
la variable dentroAmbito, y en la línea 7 se cierra el bloque if. Dentro del bloque if existen las varia-
bles numero y dentroAmbito. En la línea 8, ya fuera del bloque if, se imprime la variable numero.
Aquí no se puede imprimir dentroAmbito pues no existe fuera del bloque if.
Un concepto relacionado con el de constante es el de literal. Un literal (o constante literal) es una
constante cuyo nombre (identificador) es la representación escrita de su valor. Así, el literal ‘A’ es la
representación escrita del carácter A (los caracteres literales se indican entre comillas). Los literales
pueden pertenecer a cualquier tipo de dato. Así, 2 es un literal que corresponde al entero dos.
Respecto a los identificadores en Java, éstos pueden construirse con letras, dígitos, el carácter de
subrayado (_) y el signo de dólar ($). Un identificador no puede comenzar con un dígito, pero puede
tener cualquier longitud. Recordemos que Java distingue entre mayúsculas y minúsculas, con lo cual
si se declara una variable llamada Datos sería distinta de otra llamada datos. Estas reglas para la
construcción de identificadores se aplican en Java a cualquier entidad, no sólo a variables.

3.3.3. REPRESENTACIÓN INTERNA DE DATOS


Como ya hemos indicado, toda la información, incluyendo los datos de los diferentes tipos, se repre-
senta en el ordenador usando código binario. Por ejemplo, ¿qué quiere decir la cadena de bits
00101100101? No es posible saberlo sólo con mirar la cadena. Para poder interpretar una cadena bina-
ria tenemos que poder descodificarla 7 de acuerdo a algunas reglas. Hay reglas para codificar y alma-
cenar cada tipo primitivo. Es interesante conocer cómo se realiza la codificación de los tipos primitivos
usando código binario. Vamos a considerar este punto a continuación, analizando cómo se codifica
cada uno de los tipos primitivos en Java.

a) Tipo entero
Los tipos enteros representan tanto valores negativos como positivos. Para representar el signo se usa
un bit en cada tipo denominado bit de signo. Si el bit de signo es 1, el número es negativo; si es 0, el
número es positivo. Así, para representar el valor absoluto del número se usa un bit menos de los dis-
ponibles. Para los distintos tipos enteros en Java la situación se ilustra en la Figura 3.2.
En todos ellos el primer bit es el usado para indicar el signo. Este bit, el primero, el colocado más
a la izquierda, es el bit de signo.
En Java (y en muchos otros lenguajes) los tipos enteros están almacenados en formato comple-

Figura 3.2. Representación de enteros con signo

7
En castellano el término correcto es descodificar. Sin embargo, se encuentra muy frecuentemente el término decodifi-
car que es una mala traducción del inglés.
Introducción a la programación 47

mento a dos con signo. El complemento de un número es en cierto sentido la imagen especular del
número original. Por ejemplo, en decimal son típicos el complemento a nueve y a diez. Como ilustra-
ción, consideremos en decimal la obtención del complemento a nueve de 63:

1º: Se pone un 9 por cada dígito de nuestro número y a esto se resta el número

299
263
236

2º: se suma 1

136
101
137

Por tanto, el complemento a 9 de 63 es 37.


La ventaja de los complementos es que es posible transformar las restas en sumas, esencialmente
sumando el minuendo con el complemento del sustraendo.
En binario se trabaja con el complemento a dos. En caso de querer convertir un numero binario a
su complemento a dos, las operaciones anteriores se transforman en una serie de operaciones senci-
llas. Para hacer el cambio basta con cambiar 1 por 0 y 0 por 1 en el número y sumar 1 al resultado. La
traducción a complemento a dos con signo se realiza de la siguiente manera:

— Un valor positivo es un número binario directamente.


— Un valor negativo se representa invirtiendo todos los bits del correspondiente valor positivo y
añadiéndole 1.
— Para “descodificar” un valor negativo, se invierten todos los bits y se suma 1.

Por ejemplo, evaluemos el complemento a dos de 101010 (binario):

1º: Se pone 1 por cada dígito y al resultado se le resta el número

2111111
2101010
2010101

2º: se suma 1

1010101
1000001
1010110 <....... complemento a dos

Usando la receta abreviada:

1º: Invertir 1 y 0: 010101.


2º: Sumar 1. Es el mismo caso que en el paso 2o anterior, obtenemos 010110

Para invertir el signo se hace lo siguiente:

El número 25 se representa en 8 bits (un byte) como


48 Introducción a la programación con orientación a objetos

00011001
Para representar 225, primero se invierten todos los bits
11100110
después se añade 1
11100111
Observe que el bit del signo se invierte, indicando que el número es negativo.

Usando esta técnica, las sumas y restas quedan reducidas a sumas y se reduce la complejidad de
los circuitos de la unidad aritmético-lógica (no hacen falta circuitos específicos para restar). El lector
interesado en una introducción al diseño de sistemas digitales puede consultar el capítulo cuarto de
(Prieto et al., 1995).

b) Reales
En Java, como ya vimos, y en muchos otros lenguajes hay dos tipos de datos reales (de punto flotan-
te) los simples y los dobles, llamados en Java float y double. La representación típica de un núme-
ro en punto flotante es la siguiente:

Un valor decimal (base 10) en punto flotante se define con la siguiente expresión:
signo * mantisa * 10 exponente

donde:
signo es 1 ó 21.
mantisa es un entero positivo que representa los dígitos significativos del número.
exponente es un entero que indica cómo se coloca el punto decimal con respecto a la mantisa.

Se usan los tres componentes para representar un número cualquiera. Veamos dos ejemplos:

1. Representación de punto flotante de 129,34 (129.34 usando el punto decimal)


11*12934110 22

Aquí, signo= 11, mantisa= 12934, y exponente= 22

2. Representación de punto flotante de 2843,977


21 * 843977 * 10 23

Los números en punto flotante se pueden representar en binario de la misma manera, excepto que
la mantisa y el exponente son números binarios y la base es 2 en vez de 10, es decir:

signo * mantisa * 2 exponente

En Java se usa el estándar IEEE 754 para representar números en punto flotante. En esta norma la
representación es binaria y se usa un bit para el signo. La norma IEEE 754 establece para precisión

Tabla 3.3. Convenio establecido por la norma IEEE 754 para preci-
sión simple o doble

Tipo signo mantisa exponente


simple (32 bits) 1 bit 23 bits 8 bits
doble (64 bits) 1 bit 52 bits 11 bits
Introducción a la programación 49

simple o doble el convenio mostrado en la Tabla 3.3.


Los valores en punto flotante se almacenan guardando cada uno de los tres componentes en el
espacio asignado.
En la norma IEEE 754 hay tres valores especiales que se pueden representar: infinito positivo
(positive infinity), infinito negativo (negative infinity) y no número (Not a Number). Este último apa-
rece como NaN.
Como hemos visto, los valores numéricos se representan usando un formato finito, un número
determinado de bits. Por ello, podemos decir que no tenemos números enteros o reales, sino una simu-
lación de números enteros o reales. Al usar un formato finito para esa simulación hay siempre un valor
máximo y uno mínimo que se pueden representar en un tipo numérico de datos. Intentar almacenar un
valor que caiga fuera de ese intervalo 8, en el mejor de los casos produce resultados incorrectos y en el
peor, un error. Las dos situaciones se denominan Overflow y Underflow:

— Overflow ocurre cuando un número se hace demasiado grande para entrar en su espacio asig-
nado.
— Underflow ocurre cuando un número se hace demasiado pequeño para entrar en su espacio
asignado.

Como ejemplo ilustraremos la situación de overflow en enteros en el apartado 3.4.2, tras introdu-
cir las sentencias de entrada/salida.

c) Caracteres
En Java, como vimos, los caracteres se representan en código Unicode (Unicode, 2002), con 16 bits.
Un carácter Unicode está representado como un entero de 16 bits sin signo. Como no se usa ningún
signo, los 16 bits contribuyen a codificar cada carácter. Por eso, se pueden representar 216 (65536)
caracteres, aunque en la actualidad sólo se usan aproximadamente la mitad. Por ejemplo, el carácter
‘z’ tiene el valor Unicode 122, el cual se representa con 16 bits como:

0000000001111010

Los caracteres Unicode se almacenan como un conjunto de 16 bits. La conversión directa de los
16 bits al sistema decimal da un número que corresponde al valor decimal del carácter Unicode repre-
sentado. Al almacenarse como números, Java nos permite realizar algún procesamiento aritmético
sobre los caracteres. Por ejemplo, como ‘A’ se almacena con el valor Unicode 65, la sentencia

char car = ‘A’ + 5;

almacenará el carácter ‘F’ en la variable car (valor Unicode de ‘F’, 70).


Otro ejemplo sería una letra en mayúsculas a la que se le suma 32 (diferencia entre ‘A’ y ‘a’). Se
conseguirá la letra equivalente en minúsculas.

d) Lógico
Es el tipo de dato usado para las condiciones (verdadero/falso). Para optimizar el tiempo de acceso a

8
Es muy habitual encontrar la palabra rango en lugar de intervalo en la literatura técnica. Esta equivalencia es absolu-
tamente incorrecta. El error proviene de una mala traducción del término inglés range que significa intervalo y no rango. En
castellano, rango refiere a la posición dentro de un conjunto u organización.
50 Introducción a la programación con orientación a objetos

memoria se usa más de un bit (sólo haría falta uno) para representarlo. Por ejemplo, en la Sun JVM
(Java Virtual Machine) todos los tipos “enteros” (el boolean se considera como tal) menores de 32 bits
se promueven a 32 bits cuando se colocan en la pila (stack) de datos durante la ejecución de un pro-
grama (van der Linden, 1999).

3.3.4. CONVERSIONES DE TIPO


En un programa es habitual cambiar de tipo de datos. Esto quiere decir que un determinado valor alma-
cenado en una variable de un cierto tipo lo queremos colocar en otra variable de tipo diferente. A tal
efecto, en todos los lenguajes hay algún, o algunos, mecanismos para realizar la conversión. Lo más
importante es no perder información en la transformación. Por ejemplo, supongamos que tenemos una
variable entera de tipo short (16 bits) que almacena el valor 1000 y queremos convertirla a tipo byte
(8 bits). Como 1000 no se puede representar con 8 bits la conversión nos dará un resultado que no
corresponde con el valor original.
No todas las conversiones entre los distintos tipos son posibles. Por ejemplo, en Java los valores
lógicos (booleanos) no se pueden convertir a ningún otro tipo de dato y viceversa. Incluso aunque una
conversión sea posible, debemos tener cuidado en no perder información en el proceso. Desde un pun-
to de vista general, las conversiones de tipo se clasifican en dos categorías:

— De ensanchamiento o promoción.
— De estrechamiento o contracción.

Las conversiones de ensanchamiento transforman de un tipo a otro con el mismo o mayor espacio
para almacenar información. En este caso no se pierde información, pero puede perderse precisión al
convertir de tipo entero a real, ya que algunos de los dígitos menos significativos pueden perderse.
Las conversiones de estrechamiento transforman de un tipo a otro con menos espacio para alma-
cenar información. En una conversión de contracción es probable perder información. Un ejemplo típi-
co es el de pasar de un real a un entero. Aquí, a no ser que la parte decimal sea cero se perderá en el
cambio.
Las conversiones de ensanchamiento o promoción, en Java, pueden realizarse de la forma mostra-

Tabla 3.4. Conversiones de ensanchamiento

Origen Destino
byte short, int, long, float, double
short int, long, float, double
char int, long, float, double
int long, float, double
float float, double
double ———
long double

da en la Tabla 3.4.
Por otro lado, las conversiones de estrechamiento o contracción son más peligrosas, porque cam-
bian de un espacio de almacenamiento determinado para el tipo original a un espacio menor en el tipo
destino. Se corre el riesgo de perder o alterar información. Las conversiones de contracción entre los
Introducción a la programación 51

Tabla 3.5. Conversiones de estrechamiento

Origen Destino
byte char
short byte, char
char byte, short
int byte, short, char
long byte, short, char, int
float byte, short, char, int, long
double byte, short, char, int, long, float

tipos en Java se muestran en la Tabla 3.5.


Debido al signo, la conversión de byte y short a char se considera de estrechamiento, aunque
un byte o un short ocupen menos o igual número de bits que el tipo char. Un byte o un short
usan un bit como signo y el char usa 16 bits sin signo. Por eso, un entero negativo (con signo) se con-
vierte en un carácter (sin signo) que no tiene relación con el entero que representaba. La conversión
pasa directamente los 8 bits o los 16 bits de entero, incluyendo el bit de signo, a carácter y no los 7 ó
15 bits que son los que realmente representan el número en valor absoluto.
Recordemos que los valores booleanos (lógicos) no pueden convertirse a otro tipo de datos.
Como hemos visto, hay conversiones por promoción o por estrechamiento pero, ¿cómo se realizan
las conversiones de tipo? Las conversiones de tipo se realizan por tres mecanismos diferentes:

a) Conversión por asignación.


b) Conversión por promoción aritmética.
c) Conversión con “moldes” (casting).

Consideremos cada una de ellas por separado:

a) Conversión por asignación


Se realiza cuando un valor de un tipo determinado se asigna a una variable de otro tipo. Se pro-
duce una promoción automática. Por ejemplo, si dinero es una variable de tipo float y euros es
una variable de tipo int (que almacena el valor 82), entonces en la sentencia
dinero = euros;

se convierte el valor 82 de euros a 82.0 (valor real, con decimal) cuando se almacena en dinero. El
valor en euros no se cambia, se mantiene el original. Lógicamente, a través de la asignación sólo se
permiten conversiones de ensanchamiento.

b) Promoción aritmética
Ocurre automáticamente cuando se realiza una operación aritmética como la suma o la división.
En este caso, los operadores aritméticos modifican los tipos de sus operandos para realizar correcta-
mente la operación. Así, si dividimos una variable real por una entera, el operador promueve la ente-
ra a real para realizar la operación. Por ejemplo, si resultado es una variable de tipo float, suma
es también una variable de tipo float, y contador es una variable de tipo int, la sentencia
52 Introducción a la programación con orientación a objetos

resultado = suma / contador;

convierte internamente el valor entero en contador a un float y después hace la división, produ-
ciendo un resultado en punto flotante. El valor en contador no se cambia. La promoción aritmética
es siempre de ensanchamiento.

c) Moldes (Casting 9)
En este caso, se utiliza un mecanismo específico para realizar la transformación. A tal efecto, los
lenguajes de programación proporcionan alguna instrucción que realiza la conversión. Éste es el méto-
do más seguro, pues la instrucción permite realizar conversiones de promoción o contracción mante-
niendo, en lo posible, la información original. El molde es la instrucción que produce la conversión de
tipo. En Java, el molde es un operador que se especifica como un nombre de tipo colocado entre parén-
tesis a la izquierda del dato a convertir.
La sintaxis para el uso de moldes es:
(Tipo) variable_a_convertir

donde tipo es el nombre del tipo al que se quiere convertir. En el siguiente ejemplo se hace una con-
versión de estrechamiento. Se trata de convertir el valor real almacenado en una variable denominada
dinero a un valor entero que se va a almacenar en una variable entera llamada euros. Lógicamente,
el valor decimal se trunca, perdiéndose la parte fraccionaria del valor en punto flotante.
int euros;
double dinero=30.2;
euros = (int) dinero;

El valor en dinero no cambia. La variable euros almacena ahora el valor entero 30 ya que la
conversión al tipo entero pierde la parte decimal. En Java, si una conversión es posible, puede hacer-
se a través de un molde.
Los moldes son útiles en muchas situaciones donde temporalmente necesitamos tratar un valor
como de otro tipo. Por ejemplo, si quiero dividir el valor entero total por el valor entero contador
y conseguir un resultado de tipo float que almacenaremos en la variable resultado, podría hacer:
resultado=(float) total/contador;

Primero el operador de molde devuelve la versión float del valor en total. Esta operación no
cambia el valor almacenado en total. Después contador se convierte a float vía promoción
aritmética. El operador división hace la división en punto flotante y produce el valor buscado. Si el
operador de molde no se usa, se hubiera hecho la división entera y truncado la respuesta antes de asig-
nar el valor a resultado. Obsérvese que se ha indicado que el molde se aplica en primer lugar y que
luego se realiza la división. Esto es así porque el molde tiene precedencia sobre la división. Este com-
portamiento es un ejemplo de precedencia de operadores, problema que consideraremos más adelan-
te.
Una conversión de tipo no altera el valor de las variables convertidas. Recordemos que una varia-
ble actúa como un contenedor para el valor almacenado. No se puede sacar el valor, sólo se puede
poner uno nuevo “machacando, borrando” el anterior, véase la Figura 3.3. Por ejemplo, consideremos
tres sentencias como las siguientes,

9
Cast en inglés se traduciría en el presente contexto como molde. La idea es que nosotros cambiamos de tipo,
“moldeando” el tipo antiguo para adaptarlo al nuevo.
Introducción a la programación 53

Figura 3.3. Asignación de valores a una variable

int total, contador;


float resultado;
resultado=(float) total/contador;

Tras realizar el cociente, total y contador siguen siendo enteras, su tipo no se altera. Lo que ocu-
rre es que se hace una copia del valor de total y se convierte a float. El valor original sigue siendo
entero.

3.4. INSTRUCCIONES
Como vimos, un programa resuelve un problema dado usando un o una serie de algoritmos determi-
nados. El programa se escribe en un lenguaje concreto y en él se van indicando las distintas acciones
a través de determinadas instrucciones. Dichas instrucciones son generales, aunque su sintaxis especí-
fica depende del lenguaje. Vamos a ver los distintos tipos de instrucciones y los particularizaremos en
lenguaje Java. Es decir, vamos a considerar las instrucciones básicas que soportan todos los lenguajes
desde el punto de vista semántico.

3.4.1. INSTRUCCIONES DE ASIGNACIÓN


La o las instrucciones de asignación sirven para dotar de valor a las variables. Lo que se hace es colo-
car un valor dado en la porción de memoria simbolizada por el identificador de la variable. En Java (y
en otros muchos lenguajes) la asignación se representa por el símbolo de igualdad “5” . Así, para asig-
nar el valor 5.0 a una variable real llamada total haríamos:
total=5.0;
54 Introducción a la programación con orientación a objetos

Es importante recordar que el signo “=” no representa una igualdad, sino la colocación del valor
determinado en la variable. Cuando no se desea usar un lenguaje determinado, la asignación se sim-
boliza con una flecha i. Así, en el ejemplo anterior tendríamos:
total i5.0

Este simbolismo representa más claramente lo que ocurre en realidad. El signo “5” puede causar
confusión si se considera como una igualdad. Por ejemplo, la sentencia:

total=total+1.0;

no tiene sentido si se considera como una igualdad. Para interpretarlo se debe entender que total
representa una porción de memoria. Así, la expresión anterior significa: Toma el valor almacenado en
la variable total, súmale 1 y el resultado guárdalo en la variable total. Lógicamente, el valor anti-
guo de total se pierde al “rellenarla” con el nuevo valor.
La sintaxis general de asignación en Java es:

Nombre_de_variable=expresión;

donde expresión puede ser un valor literal, como en los ejemplos anteriores, o bien representar una
o unas operaciones cuyo resultado es el que se almacena en la variable.
Dependiendo del lenguaje de programación así se permite la conversión implícita de un tipo de
datos a otro. Java es un lenguaje de tipos estrictos, lo que significa que no permite que se asigne un
valor a una variable que sea inconsistente con el tipo declarado para esa variable. Por ejemplo, no se
puede asignar un valor entero a una variable lógica y viceversa. La compatibilidad entre tipos se com-
prueba en tiempo de compilación.
Ahora que ya hemos presentado la instrucción de asignación ilustramos la estructura de un pro-
grama en Java con el Programa 3.2. Nuevamente, por claridad, se numeran las líneas, aunque esto no
se hace en Java.

Programa 3.2. Programa simple en Java

1 class Ejemplo {

2 // Ejemplo de la estructura de un programa en Java

3 public static void main(String [] args) {

4 // Declaración de variables

5 double total, suma;


6 total=10.0;
7 suma=5.0;

8 // Operación
9 total=total+suma;

10 } // Fin método main


11 } // Fin clase Ejemplo

El Programa 3.2 es un ejemplo muy sencillo, que aunque no tiene mucha utilidad (realiza una tarea
Introducción a la programación 55

pero no genera información) nos servirá para analizar la estructura genérica mínima de un programa
en Java. Antes de comenzar es conveniente saber que Java usa formato libre. Esto quiere decir que el
código puede empezar y terminar donde se desee. Lo normal es usar líneas de 80 caracteres, que es lo
que se puede visualizar bien en la anchura de una pantalla o en una hoja impresa. La línea de 80 carac-
teres tiene una razón de ser. Los 80 caracteres eran el número de caracteres que entraban en una tar-
jeta perforable. Por eso la línea de 80 caracteres se denomina imagen de tarjeta.
A nivel organizativo la entidad básica en un lenguaje orientado a objetos es la clase, en Java iden-
tificada con la palabra reservada class. La línea 1 del programa indica que estamos definiendo una
clase llamada Ejemplo. Puede observarse que la clase corresponde a un bloque de código, pues des-
pués del nombre de la clase se abre una llave que no se cierra hasta la línea 11. Una clase, como se
expondrá más adelante, se puede considerar como un tipo abstracto de datos, siendo los objetos “varia-
bles” de ese tipo de dato. Las clases se tratarán con detalle en los Capítulos 5 y 7, aquí se explican los
conceptos básicos para poder entender el ejemplo. Tradicionalmente, los lenguajes consideran inde-
pendientes los datos y las funciones o procesos que se realizan sobre ellos. En la programación orien-
tada a objetos esto no es así, los objetos (que se definen a través de una clase) contienen los datos y
también los procedimientos o funciones que se realizan sobre ellos (denominados métodos en Java).
Se dice que un objeto encapsula datos y procedimientos. En Java existen clases predefinidas que pro-
veen de métodos útiles. Podemos imaginarlo como una biblioteca de clases para distintas aplicacio-
nes. Estas bibliotecas están organizadas como “paquetes”, que son conjuntos de clases relacionadas,
que deben ser “importados” por nuestro programa para poder usar los métodos contenidos en sus cla-
ses. En Java, un conjunto importante de clases predefinidas son las que forman la API (Applications
Programming Interface). Estas clases están organizadas en paquetes y poseen métodos para la entrada
y salida de información.
De lo anterior se sigue que dentro de una clase se pueden definir métodos. Los métodos son frag-
mentos de código que realizan una tarea y que pueden devolver un resultado. Si un método no devuel-
ve ningún resultado, debe indicarse con la palabra reservada del lenguaje void. Que no devuelva un
resultado no quiere decir que no haga nada. Las instrucciones de entrada/salida estarían dentro del
método, pero no sería necesario devolver nada a quien haya llamado (invocado) el método en cues-
tión. En todo programa hay un método, main, que representa el programa principal. En el Progra-
ma 3.1 el método main comienza en la línea 3. El método main es el punto de partida del programa,
no se invoca desde ninguna parte del programa, sino que es el sistema operativo quien comienza a eje-
cutarlo. Como el método main no tiene que devolver ningún valor, lleva la indicación de void. Los
modificadores de public y static hacen referencia a ciertas facetas del comportamiento del méto-
do que no consideraremos en este momento. Este punto se tratará en detalle en el Capítulo 7. A la dere-
cha del nombre del método y entre paréntesis se pueden indicar los datos que se pasan al método para
trabajar con ellos. En el método main siempre hay que indicar que es posible pasar una lista de cade-
nas, lo que se indica como String []. Al lado se indica el nombre que se desea para la lista. En el
programa se usa el identificador args como abreviatura de argumentos. De momento, baste conside-
rar que la línea 3 del Programa 3.1 es la cabecera que usaremos siempre que escribamos el método
main. Al final de la línea 3 podemos observar que se abre el bloque correspondiente al método. El blo-
que se cierra en la línea 10.
Los bloques de código se suelen sangrar para delimitar visualmente su alcance. Ésta es una nor-
ma de estilo a la que el lector debe habituarse desde el principio, puesto que le ayudará a estructurar
sus programas, depurarlos y sobre todo ayudará a la legibilidad de los mismos (véase el Apén-
dice D).
Dentro ya del método main (al igual que en la línea 2 del programa) encontramos una línea de
comentario. Los comentarios son fundamentales en un programa, puesto que representan información
muy útil para entender su funcionamiento. Los comentarios deben incluirse en el código, pues con el
tiempo será necesario modificar o adaptar el programa. Estas labores de modificación o adaptación del
software se denominan mantenimiento del software, y como vimos en el Capítulo 2, tienen una gran
importancia en el ciclo de vida de un programa. Los comentarios nos indican la intención del autor del
56 Introducción a la programación con orientación a objetos

programa cuando lo escribió. Cuando se usa un programa durante muchos años, y se le van haciendo
modificaciones, es buena política documentarlo de manera apropiada, no sólo a través de los manua-
les técnicos y de usuario sino también a través de texto dentro del propio código. El computador igno-
ra los comentarios. Éstos no afectan a la ejecución de los programas. Los comentarios en un programa
se denominan documentación interna y se incluyen para explicar el propósito del programa o de los
pasos de procesamiento.

Figura 3.4. Representación simbólica de la entrada/salida en Java


Introducción a la programación 57

Básicamente existen dos formas de escribir un comentario en Java, una es usando dos barras “//”,
y otra usando una barra y un asterisco “/*” al principio del comentario y un asterisco y una barra, “*/”
al final del mismo.

// comentarios que van hasta fin de línea

/* comentarios que van hasta el símbolo de terminación, incluso a


través de saltos de línea */

La diferencia es que el primer tipo sólo sirve para escribir una línea. Cuando se necesita escribir
comentarios más largos, de varias líneas, se utiliza el segundo tipo que delimita perfectamente dónde
comienza y dónde termina el comentario considerado.
En la línea 5 del Programa 3.2, que estamos analizando, se puede observar una declaración de
variables. Cuando se declara una variable se le asigna un valor por defecto. Este valor depende del sis-
tema, y normalmente es cero o el valor nulo. La asignación de valores se realiza en las líneas 6 y 7 del
programa. La línea 9 modifica el valor de la variable total. La línea 10 cierra el bloque delimitado
por el método main. La llave de la línea 11 indica el fin de la clase.

3.4.2. INSTRUCCIONES DE ENTRADA/SALIDA


Los programas necesitan aceptar entradas de datos y producir salidas de datos. Sin embargo, la fuente
de la entrada o el destino de la salida puede variar. Frecuentemente se lee de ficheros y se escribe en
ficheros. Otras veces se acepta la entrada desde el teclado y la salida se realiza por el monitor. En cual-
quier caso, para poder leer o escribir, los lenguajes de programación proveen de algún mecanismo. Exis-
ten instrucciones para lectura/escritura que muchas veces forman parte del lenguaje. En Java (en el

Tabla 3.6. Corrientes de lectura y escritura estándar en Java

Corriente (Stream) Propósito Dispositivo (defecto)


System.in lectura teclado
System.out escritura monitor
System.err salida de errores monitor

lenguaje como tal) no hay instrucciones de lectura/escritura. Lo que hay son métodos para ello provis-
tos por las clases de la API de Java. Vamos en este apartado a explicar algunas nociones de la entrada
y salida en Java para poder empezar a hacer ejemplos donde se lean datos y se impriman resultados.
En Java todos los tipos de entradas y salidas se realizan por streams (corrientes o flujos) y se
habla de corrientes de entrada o de salida. Un stream es un flujo de datos, independientemente de
donde venga o vaya. Una definición más precisa es la siguiente: Un stream es una secuencia orde-
nada de datos de longitud indeterminada (Harold, 1999). Es una abstracción de fuentes y destinos
externos de datos que permiten leer y escribir de ellos independientemente del tipo exacto de fuente
o destino, véase la Figura 3.4. Este mecanismo de abstracción permite que la entrada y la salida se

10
El acrónimo “io” proviene del término inglés Input-Output (Entrada-Salida). No es raro verlo usado en castellano.
11
Como se comentó con anterioridad, para usar un paquete de clases hay que realizar una operación de importación. El
paquete java.lang contiene clases con los métodos para realizar las tareas más comunes. Por eso, se importa automáti-
camente en todos los programas, no hay que dar ninguna instrucción especial para ello. El resultado es transparente para el
usuario, para el cual los métodos del java.lang parecen formar parte del lenguaje.
58 Introducción a la programación con orientación a objetos

manejen formalmente de la misma manera, independientemente de dónde proceda la entrada o a dón-


de vaya la salida. Por ejemplo, la entrada podría ser desde un fichero, desde el teclado o desde una
conexión a Internet.
Originalmente, en Java 1.0 los streams eran entidades de 8 bits. Estas corrientes de entrada y sali-
da de 8 bits se denominan:

InputStream: Corrientes de entrada de 8 bits.


OutputStream: Corrientes de salida de 8 bits.

Sin embargo, esto no resultó útil para convertir entre códigos de caracteres externos y el Unicode
que usa Java internamente y que necesita 16 bits. Por eso, a partir de la versión 1.1 de Java se intro-
ducen las corrientes de 16 bits que son compatibles con Unicode. A tal efecto, se introducen dos enti-
dades denominadas:

Reader: Corrientes de entrada de caracteres Unicode (16 bits)


Writer: Corrientes de salida de caracteres Unicode (16 bits).

Java proporciona mecanismos para adaptar las entradas de 8 bits a las de 16 (a efectos de compa-
tibilidad con código desarrollado para streams de 8 bits). Estas corrientes de 16 bits también se deno-
minan corrientes de caracteres (por aquello de usar el código Unicode). La ventaja de usar corrientes
de 16 bits es que es más fácil escribir programas que no sean dependientes de una codificación especí-
fica de caracteres.
No hay sentencias de entrada y salida en el lenguaje Java. La entrada y salida se realiza usando
bibliotecas de clases predefinidas. La mayoría de las operaciones de entrada-salida están definidas
en el paquete java.io de la API de Java 10. Sin embargo, los métodos print y println que son
los más comunes para salida, son parte de la clase System del paquete java.lang. Este paquete
se importa automáticamente en todos los programas en Java. No es necesario importarlo explícita-
mente para usarlo 11. Las corrientes de lectura y escritura estándar se muestran en la Tabla 3.6 don-
de in, out y err son objetos de la clase System. Los streams System.in, System.out y
System.err son de 8 bits.
La salida estándar, a nivel básico, es muy sencilla. Basta con usar los métodos print() y
println() del stream System.out. La sintaxis completa es,

Tabla 3.7. Algunas secuencias de esca-

Secuencia de escape Significado


\t Tabulador
\n Línea nueva
\’ Comilla simple
\” Comilla doble
\\ Barra invertida

System.out.print();
System.out.println();

El primer método produce una salida sin salto a la línea siguiente y el segundo produce un salto a
la línea siguiente tras escribir la salida. La diferencia del uso de print y println se ilustra en el Pro-
grama 3.3.
Introducción a la programación 59

Programa 3.3. Ilustración de salida estándar con los métodos print() y println()

class Escritura {
public static void main(String[] args) {
int i, j;
i=1;
j=3;
System.out.print(“Sin salto de linea “);
System.out.print(“i: “+i);
System.out.print(“ j: “+j);
System.out.println(); //Salto de línea
System.out.println(“Con salto de linea “);
System.out.println(“i: “+i);
System.out.println(“j: “+j);
} // Fin método main
} //Fin clase Escritura

El resultado sería:

Sin salto de linea i: 1 j: 3


Con salto de linea
i: 1
j: 3

Como puede observarse, tras la impresión realizada con print() continuamos en la misma línea.
Después de usar println() se produce un salto a la línea siguiente. En ambos métodos observamos
que las cadenas alfanuméricas se imprimen sin más que colocarlas entre comillas dobles. Vemos tam-
bién que el contenido de una variable se imprime indicando el nombre de la variable precedido de un
signo 1. El signo 1 indica que el contenido de la variable se añade a la salida.
Los métodos print() y println() aceptan las denominadas secuencias de escape. Una secuen-
cia de escape es una serie de caracteres con un significado especial precedida por una barra invertida
(backslash): \. Las secuencias de escape indican algún propósito específico, como se muestra en la
Tabla 3.7.
La barra invertida implica que lo que viene detrás tiene algún significado para el sistema. Por
ejemplo, para el sistema la comilla simple indica principio o fin de carácter y la comilla doble princi-
pio o fin de cadena de caracteres. Esto quiere decir que si pretendemos imprimir una comilla simple o
doble no basta con escribirla en un print() o println(), pues el sistema no la entendería como un
carácter literal. Usando una secuencia de escape, sí que el sistema lo interpreta literalmente. La barra
invertida, \, hace que estos símbolos aparezcan tal cual, como se puede observar en el Programa 3.4.

Programa 3.4. Ilustración del uso de secuencias de escape

class Ejemplo {
public static void main(String [] args) {
System.out.println (“El dijo: \” Fuera de aqui \””);
} // Fin método main
} // Fin clase ejemplo

El resultado es:
El dijo: “Fuera de aquí”
60 Introducción a la programación con orientación a objetos

Usando la secuencia de escape \” hemos podido escribir las dobles comillas y evitar que el com-
pilador lo interprete como fin de cadena.
Una vez introducida la salida en Java, y tal y como se indicó en el apartado 3.3.3, veamos un pro-
blema de overflow en el Programa 3.5.

Programa 3.5. Ejemplo adicional de salida de datos ilustrando un overflow con enteros

class Overflow {
// Ejemplo de overflow
public static void main(String[] args) {

short numero = 32766; // <..... El tipo short solo puede


// representar hasta 32767

System.out.println(“numero: “ + numero); //<... 32766


numero=(short) (numero+1);
System.out.println(“numero: “ + numero); //<... 32767
numero=(short) (numero+1); // <.... Se suma 1

/* La operación anterior es
011111111 11111111+00000000 00000000=100000000 00000000
Pero el 1 está en el bit de signo y en complemento a dos el resultado
es
-32768 */

System.out.println(“numero: “ + numero); //<.... -32768


numero=(short)(numero+1);

System.out.println(“numero: “ + numero); //<.... -32767

} // Fin método main


} // Fin clase Overflow

El resultado sería:
numero: 32766
numero: 32767
numero: -32768
numero: -32767
Lo primero es indicar que el uso del molde (short) es para trabajar con tipo short, pues el lite-
ral 1 se interpreta como de tipo int. La razón del comportamiento observado en la salida, es que pre-
tendemos almacenar un valor mayor que el permitido en el tipo short. Como valor positivo, el tipo
short sólo permite representar hasta 32767. Esto corresponde al valor binario, 011111111 11111111.
Si sumamos uno más obtenemos en binario 10000000 00000000. Ahora bien, este valor tiene el pri-
mer bit (bit de signo) con valor 1 y el sistema lo interpreta como un número negativo. Como un valor
negativo se interpreta en complemento a dos, el valor binario indicado corresponde a 232768. Así lo
interpreta el sistema y va sumando uno a este valor. El problema aparece por pretender almacenar un

12
Big o little endian indica en qué orden están almacenados los bytes para los tipos de datos. Por ejemplo, un entero
representado con cuatro bytes (byte1 byte2 byte3 byte4) puede estar almacenado con los cuatro bytes en el orden natural (con
los bytes más significativos al principio: byte1 byte2 byte3 byte4). Ésta es la codificación big endian. Otra posibilidad es que
el entero se almacene con los bytes menos significativos al principio (en el orden: byte4 byte 3 byte2 byte1). Ésta es la con-
vención little endian. Java usa la convención big endian y los PC’s la little endian (véase van der Linden, 1999).
Introducción a la programación 61

valor positivo mayor que el que el tipo puede representar. Éste es un ejemplo típico de overflow de
enteros. Un overflow de reales usualmente lo que produce es un error del programa.
En Java, la salida hacia pantalla con println() o print() se realiza con buffer. Un buffer es un
almacenamiento intermedio. El uso de un buffer es útil, pues hace más eficiente el procesamiento.
Normalmente, el buffer se usa para “adaptar” dos entidades que trabajan a distinta velocidad. Así, la
mas rápida, por ejemplo, envía la información al buffer, que la puede almacenar, mientras la más len-
ta va procesando poco a poco dicha información. De esta forma la entidad rápida puede ocuparse de
otra tarea. En Java, la salida con buffer almacena la información en dicho buffer hasta que éste está
lleno, el programa se completa o se vacía de alguna forma (flush). El buffer puede ser explícitamente
vaciado usando el método flush: System.out.flush(). Este método fuerza a System.out a
mostrar todo lo que había guardado en el buffer. Se puede usar después del método println() o
print() si interesa que se muestre en ese mismo momento el contenido del buffer. En las versiones
actuales de Java la gestión del buffer es eficiente y para las tareas habituales, como las presentadas en
este texto, no se precisa interaccionar directamente con él.
La salida de información es relativamente sencilla, pero la entrada no lo es tanto debido a la nece-
sidad de convertir desde la corriente (stream) de 8 bits a la de 16. Esto no es forzoso, pero se reco-
mienda en las aplicaciones actuales, a fin de trabajar directamente con corrientes Unicode (16 bits).
Con corrientes de 8 bits, se puede usar la clase DataInputStream para la lectura. Esta clase per-
mite leer datos en binario como corrientes de 8 bits, poseyendo métodos para leer todos los tipos pri-
mitivos. Dichos métodos son:

readBoolean()
readByte()
readShort()
readInt()
readLong()
readFloat()
readDouble()
readChar()

Si usáramos la clase DataInputStream no habría problema respecto a la conversión de sistemas


de caracteres. Sin embargo, puede haber problemas relativos a la no estandarización de cómo se repre-
senta la información (convención big or little endian para el almacenamiento de datos 12). Para la lec-
tura de datos estándar, en este libro vamos a usar las corrientes de 16 bits.
Para trabajar con 16 bits debemos usar una clase de tipo Reader. Lo que debemos tener claro es que de
esta forma obtenemos una corriente de caracteres que primero habrá que leer y, en su caso, convertir a for-
mato numérico. En el Programa 3.6 vamos a ilustrar cómo realizar la lectura de una cadena de caracteres.

Programa 3.6. Lectura por teclado de una cadena de caracteres

import java.io.*;

class Lectura {
public static void main(String [] args)throws IOException {
BufferedReader leer =new BufferedReader
(new InputStreamReader(System.in));
String mensaje;
System.out.print(“Introduzca una cadena de caracteres: “);

13
Como veremos más adelante, para crear un objeto de una clase determinada se usa un método de la propia clase deno-
minado método constructor.
62 Introducción a la programación con orientación a objetos

mensaje=leer.readLine(); // Leyendo una línea del teclado


System.out.println(“Mensaje: “+mensaje);

} // Fin método main


} // Fin clase Lectura

En el Programa 3.6 usamos la clase BufferedReader que provee de un método para leer una
línea entera de caracteres conectada a la nueva corriente. La clase BufferedReader no está en el
paquete estándar java.lang sino en el java.io. Por tanto, lo primero que hacemos es importar con
la palabra reservada import todas las clases del paquete java.io, indicando java.io.*. Los méto-
dos en las clases del java.io pueden producir errores recuperables de entrada/salida. Que un error
sea recuperable indica que, si se produce, es posible controlarlo desde dentro del programa. Como
veremos con detalle en el Capítulo 9, Java usa el mecanismo de excepciones para representar y ges-
tionar estos errores recuperables. En nuestro ejemplo es posible generar la excepción (error recupera-
ble) general de entrada/salida, denominada IOException. Java exige que se indique siempre qué se
debe hacer si se llega a producir una excepción en un método. El equivalente a decir que no se quiere
controlar la excepción, es decir, que ésta se arroja (throws). Esto se indica en la cabecera del méto-
do donde puede aparecer la excepción. En nuestro caso, el método main() usa la clase Buffere-
dReader, así que para no preocuparnos del manejo de la IOException indicamos throws
IOException como parte final de la cabecera del main().
Con la clase BufferedReader lo que hacemos es crear un objeto llamado leer para realizar la
lectura deseada. Analicemos la creación del objeto leer:

BufferedReader leer = new BufferedReader


(new InputStreamReader (System.in) );

El método constructor 13 de BufferedReader recibe como argumento un objeto de clase


InputStreamReader creado con el constructor de esta clase, que a su vez recibe como argumento la
entrada estándar System.in. Estamos creando el objeto leer con las propiedades adecuadas de entra-
da-salida. La idea es conectar (adaptar) la entrada estándar (System.in), que es de 8 bits, con una
corriente de entrada de 16 bits, que es la InputStreamReader. A su vez, se conecta la corriente de
16 bits con otra corriente que provee de buffer de lectura, la BufferedReader. Con esta clase Buffe-
redReader se crea un objeto que llamamos leer. Es importante indicar que el nombre del objeto, leer
en este caso, es arbitrario, pudiendo escogerse cualquier identificador. El nuevo objeto puede usar el
método readLine() que lee una cadena de caracteres introducida por teclado. Desde un punto de vista
abstracto podemos entender el objeto leer como el equivalente al teclado dentro de nuestro programa.
De momento y hasta que se exponga la definición de clases y creación de objetos en el Capítulo 7, el lec-
tor puede considerar la creación del objeto leer como una indicación genérica, una receta, para lectura
desde el teclado.
Tras construir el objeto leer se declara una cadena de caracteres. No existe tipo primitivo para
cadenas de caracteres sino una clase, la clase String. Debido a que el manejo de cadenas es tan habi-
tual, la clase String permite construir objetos con la misma sintaxis que si de un tipo primitivo se tra-
tase, como vemos en la creación del objeto mensaje. A continuación, el programa imprime con
print una frase preguntando por la cadena a introducir. El siguiente paso es leer una cadena que intro-
duzcamos por teclado. Esto se consigue invocando el método readLine del objeto leer haciendo
leer.readLine(). El resultado de la lectura se almacena en mensaje, escribiéndose posteriormen-
te con un println(). El resultado sería:

Introduzca una cadena de caracteres: Hola


Mensaje: Hola
Introducción a la programación 63

Una vez vista la lectura de cadenas de caracteres el siguiente objetivo es la conversión de una cade-
na leída por teclado a formato numérico. Para ello usamos las clases contenedoras. De momento bas-
te saber que hay una clase contenedora asociada a cada tipo primitivo y que se denomina con el
nombre completo del tipo. Así, para enteros existe la clase contenedora Integer y no int.
Para realizar la lectura numérica con corrientes de 16 bits habrá que realizar dos tareas:

a) Leer la cadena que contiene los dígitos numéricos.


b) Convertir a formato numérico.

El punto a) se lleva a cabo con el método readLine() de la clase BufferedReader. Este


método lee una línea entera hasta el retorno de carro. El punto b) se lleva a cabo con el método
parseTipo_de_dato() que corresponda. Por ejemplo, parseInt() para la clase contenedora
Integer o parseDouble() para la clase contenedora Double. Ilustremos la técnica con un progra-
ma, véase el Programa 3.7.

Programa 3.7. Programa que lee y suma dos números enteros introducidos por teclado

import java.io.*;
class Sumita {
public static void main(String [] args) throws IOException {
int a, b, suma;

BufferedReader leer =new BufferedReader


(new InputStreamReader(System.in));

System.out.println(“Introduzca primer numero:”);

a=Integer.parseInt(leer.readLine()); /*Lectura y
conversión a
entero */

System.out.println(“Introduzca segundo numero:”);


b=Integer.parseInt(leer.readLine());

suma=a+b;

System.out.println(“Suma: “+suma);
} //Fin método
} //Fin clase

En el programa anterior se hacen dos lecturas con readLine(), una para cada valor entero. La
lectura con readLine() devuelve una cadena de caracteres. Así, si tecleamos como datos el entero
123 lo que el programa lee con readLine() no es el entero 123 sino la cadena “123”. La cadena se
convierte al valor numérico que representa con el Integer.parseInt().
En el Programa 3.8 se muestra una conversión a tipo Double. El programa convierte grados centí-
grados a Fahrenheit teniendo en cuenta que la relación entre ellos es, °F=9/5 °C 132.

Programa 3.8. Programa para convertir de grados centígrados a Fahrenheit

import java.io.*;
64 Introducción a la programación con orientación a objetos

class Fahrenheit {

// Programa para la conversión de grados centígrados a


// Fahrenheit. La relación es grados_F = grados_C*9/5+32

public static void main(String[] args) throws IOException {

// Declaración de variables
double grados_C, grados_F;
final double NUEVE_QUINTOS=9.0/5.0;
final double TREINTAYDOS=32.0;

BufferedReader leer =new BufferedReader


(new InputStreamReader (System.in));

// Entrada de datos
System.out.println (“Introduzca la temperatura en grados”
+” centigrados:”);
grados_C=Double.parseDouble(leer.readLine());

// Aplicación del algoritmo de conversión


grados_F=grados_C*NUEVE_QUINTOS+TREINTAYDOS;

// Salida de información
System.out.println (“La temperatura en Fahrenheit es: “
+grados_F +” F”);

} // Fin método main


} //Fin clase Fahrenheit

Para una entrada de 100 °C el resultado del programa sería 212 °F. Para una entrada de 0 °C el resul-
tado sería 32 °F.
El lector interesado en una visión más detallada de la entrada/salida en Java puede consultar
Eckel, 2002; Naugton y Schildt, 1997; Harold, 1999.

3.4.3. INSTRUCCIONES DE RAMIFICACIÓN


Hasta ahora sólo hemos visto la secuencia de operaciones, una detrás de otra, es decir, que la ejecu-
ción del programa procede de manera lineal. Los ejemplos de programas que hemos mostrado comen-
zaban ejecutándose en la primera línea del método main() e iban paso a paso hasta el final del mismo.
Sin embargo, en un lenguaje de programación es posible realizar ramificaciones del flujo de control.
Los lenguajes de programación proveen de dos formas típicas de realizar las bifurcaciones:

a) Por medio de condiciones.


b) Por medio de bucles.

Consideremos cada una de estas posibilidades.

a) Condiciones
Introducción a la programación 65

Las condiciones, también llamadas sentencias de selección o decisiones, evalúan una condición lógi-
ca y deciden qué fragmento de código se ejecutará en función del resultado. Para realizar una decisión
se usa en la mayoría de los lenguajes la sentencia if (si condicional inglés). En Java, la sintaxis com-
pleta de la sentencia if es:

if (condición) {
---- bloque de sentencias ----
}
else {
---- bloque de sentencias ----
}

Si la condición es verdadera, se realiza el primer bloque y si no lo es, se realiza el segundo. Los


dos bloques son excluyentes, se ejecuta uno u otro pero no los dos. La cláusula else (en inglés sino)
es opcional, se puede usar sólo el if como en el caso siguiente,

if (condición) {
---- bloque de sentencias ----
}

Igual que en el caso anterior, cuando la condición es cierta se realiza el bloque, si es falsa el flujo
de control salta el bloque y se ejecuta la sentencia que se encuentra después de la llave que cierra el
if. Como hemos visto, en un if se evalúa una condición. En dicha condición, la relación más senci-
lla que podemos establecer es la de comparación expresada con el operador = = (distinguir de la asig-
nación, =). La ilustración más simple es la determinación de la igualdad entre dos valores. Veamos un
ejemplo. Consideremos dos variables, valor1 y valor2. Podemos construir una condición con ellas
tal como:

valor1 == valor2 (valor1 igual a valor2)

Esta condición podría aparecer en un if como:

if (valor1 == valor2) {
---- sentencias para condición verdadera ——
}
else {
---- sentencias para condición falsa ——
}

Cuando el bloque de la clausula if o else sólo contiene una sentencia las llaves se pueden omi-
tir, aunque el código es más claro manteniéndolas. El Programa 3.9 muestra un ejemplo de la senten-
cia if-else.

Programa 3.9. Programa que muestra la instrucción condicional if-else

import java.io.*;
class Condicion {
// Ejemplo de sentencia condicional if-else
public static void main(String[] args)throws IOException{
int x;
BufferedReader leer =new BufferedReader
(new InputStreamReader (System.in));
66 Introducción a la programación con orientación a objetos

Programa 3.10. Programa que suma los enteros desde 1 hasta 4 usando un bucle while
controlado por contador (continuación)
// Lectura de datos
System.out.println(“Introduzca un entero”);
x=Integer.parseInt(leer.readLine());
System.out.println(“Entero introducido “+x);

// Aplicación de la condición
if (x < 0) {
System.out.println(“El numero es negativo”);
}
else {
System.out.println(“El numero es positivo”);
} // Fin de la cláusula else

} // Fin método main


} // Fin clase Condicion

b) Bucles
Otra forma de modificar el flujo de control por medio de una cierta ramificación es usando bucles. Los
bucles son útiles porque frecuentemente es necesario repetir una sentencia o un bloque de sentencias
varias veces en un programa. Esto se consigue con la sentencia de repetición, iteración o bucle. En los
lenguajes de programación suele haber varias.
Como ejemplo consideremos el bucle de tipo “while” (mientras) que repite un bloque de senten-
cias mientras se cumpla una cierta condición. En Java el bucle “while” se implementa como una sen-
tencia, la sentencia while. La sintaxis de la sentencia while en Java es:
while (condición) {
---- bloque de sentencias ----
}

El bloque de sentencias (puede ser una única sentencia y entonces no harían falta las llaves) se
repetiría mientras la condición es verdadera. Si la condición es falsa al principio, las sentencias den-
tro del bucle while no se ejecutan. En el momento en el que sea falsa la condición, el flujo de con-
trol salta a la sentencia colocada después del cuerpo del bucle while. El uso del bucle while se ilustra
en el Programa 3.10.

Programa 3.10. Programa que suma los enteros desde 1 hasta 4 usando un bucle while
controlado por contador

class Contador {
public static void main (String [] args) {
final int FIN=4;
int i=0;

// Ejemplo de bucle while controlado por contador

while (i<=FIN) {
System.out.println(“Valor del contador: “+i);
Introducción a la programación 67

Tabla 3.8. Operadores aritméticos en

Operación realizada Operador


Suma 1
Resta 2
Multiplicación *
División /
Resto %

i=i+1; // Incremento del contador


}
} //Fin método main
} // Fin clase Contador

La salida sería:

Valor del contador: 0


Valor del contador: 1
Valor del contador: 2
Valor del contador: 3
Valor del contador: 4

Fijémonos en que el bucle se repite cinco veces, pero que el contador (i) varía de cero a cuatro.
Éste es un ejemplo típico de bucle controlado por contador. Tanto la ramificación como los bucles se
tratarán en detalle en el siguiente capítulo.

3.5. OPERADORES

A menudo, las sentencias de programación involucran expresiones. Una expresión es una combi-
nación de operadores y operandos usados para realizar un cálculo. Un operador es una entidad que
realiza una operación. Un operando es una entidad que experimenta el efecto de un operador. El
valor calculado no tiene por qué ser numérico, aunque a menudo lo es. Los operandos usados en
las operaciones pueden ser literales, constantes, variables u otras fuentes de datos. Los operadores
son diversos en un lenguaje de programación y dependiendo del número de operandos sobre los
que actúan, los operadores pueden ser unarios, también llamados monarios, si actúan sobre uno, o
binarios si actúan sobre dos operandos. Por ejemplo, el operador 1 puede ser binario si representa
suma, como en una adición de dos enteros (212), o puede ser unario, si es el operador de signo
como en (12).
En los lenguajes de programación los operadores son genéricamente los mismos y admiten una
clasificación en función del tipo de operación que realizan. Vamos a considerar los distintos tipos de
operadores.

3.5.1. OPERADORES ARITMÉTICOS

14
En algunos lenguajes este operador se denomina módulo.
68 Introducción a la programación con orientación a objetos

Los operadores aritméticos son operadores binarios que aplican las operaciones aritméticas. La sinta-
xis es prácticamente homogénea en los distintos lenguajes de programación. En particular en Java, la
sintaxis es la recogida en la Tabla 3.8.
La suma, resta y multiplicación no merecen comentario especial. La división, por otro lado, mere-
ce más atención. Si ambos operandos, numerador y denominador, son enteros el resultado es entero y
se trunca la parte decimal. Este caso se denomina de cociente entero. Sin embargo, si alguno de los
operandos es real se promueve aritméticamente el otro operando y el resultado es real. El último ope-
rador es el operador resto. La operación resto 14 actúa sobre operandos enteros y devuelve un entero
que es el resto del cociente entre los operandos. El resultado toma el signo del numerador. Ilustremos
el comportamiento de los operadores de división y resto con algunos ejemplos.
a) Supongamos que cociente_real es de tipo double y N,M son enteros. Entonces si hace-
mos:
N=9;
M=4;
cociente_real=N/M;
En primer lugar se evalúa el cociente entero de N/M. El resultado sería 2. Luego el 2 (entero) se asig-
na a cociente_real y se promueve a tipo real. El resultado sería 2.0 almacenado en cociente_real.

b) Supongamos que cociente_real y A son de tipo double y que B es una variable int.
Entonces si hacemos:
A=9;
B=4;
cociente_real=A/B;
el resultado final sería 2.25, almacenado en cociente_real.

c) Supongamos que cociente_entero es entero y N,M son enteros. Entonces si hacemos:


N=9;
M=4;
cociente_entero=N%M;

obtendríamos el valor 1 (el resto del cociente de 9 entre 4). Observemos que el signo es positivo, como
el del numerador. El operador resto en muy útil para determinar si un valor es divisible por otro.

d) Supongamos que cociente_entero es entero y N,M son enteros. Entonces, haciendo:


N=29;
M=4;
cociente_entero=N%M;
obtendríamos el valor 21 (el resto del cociente con el signo del numerador).
e) Supongamos que cociente_entero es entero y N,M son enteros. Entonces si hacemos:
N=6;
M=7;
cociente_entero=N%M;
obtendríamos el valor 6. Éste es el resultado cuando el numerador es menor que el denominador.

En un lenguaje de programación, los operadores se aplican en un cierto orden, es decir, hay una
Introducción a la programación 69

precedencia. Es necesario conocer el orden de precedencia de los operadores y cómo se van aplican-
do (de derecha a izquierda o de izquierda a derecha). En Java los operadores se aplican de izquierda a
derecha. La precedencia de los operadores aritméticos es:

(* , / , %) > (1,2)

y dentro de cada grupo la precedencia es la misma. Ilustremos el problema de la precedencia de ope-


radores con algún ejemplo. Consideremos la expresión,

514/3

¿Cómo se interpretaría? Hay dos posibilidades,

a) (514)/3
b) 51(4/3)

Según las reglas de precedencia el cociente se realiza en primer lugar, así que estaríamos en el
caso b).
Veamos otro ejemplo. Sea la expresión,

Tabla 3.9. Operadores de incremento


y decremento en Java

Operación realizada Operador


Incremento en una unidad 11
Decremento en una unidad --

5112/5210%3

teniendo en cuenta la precedencia y la evaluación de izquierda a derecha, el orden en el que se apli-


carían los operadores sería:

/, %, 1, 2

Con esto, el resultado paso a paso sería,

a) 5 1 2 (cociente entero de 12/5)210%3.


b) 51221 (resto entero de 10 entre 3).
c) 721.
d) El resultado final sería: 6.

Es muy importante destacar que en un lenguaje de programación la precedencia se puede alterar


usando paréntesis. Las expresiones entre paréntesis se evalúan como un bloque. Los paréntesis se pue-
den anidar y siempre se evaluarán las expresiones desde el paréntesis más interno hacia el más exter-
no. Es recomendable usar paréntesis en expresiones complicadas, incluso aunque no sean
estrictamente necesarios, porque así queda más claro cómo se evaluarán las expresiones. Para que una
expresión sea sintácticamente correcta debe tener el mismo número de paréntesis izquierdos que dere-
chos y deben estar correctamente anidados. Ilustremos cómo es posible alterar la precedencia en el
70 Introducción a la programación con orientación a objetos

último ejemplo considerado anteriormente. Sea ahora la expresión,

((5112)/5)210%3
evaluemos el resultado paso a paso.

a) En primer lugar se evalúa (5112), por lo que la expresión quedaría como (17/5)210%3.
b) Ahora se evalúa el cociente entero (17/5) y la expresión resulta (3)210%3.
c) Se realiza ahora el resto de 10 entre 3, resultando 321.
d) El resultado final tras aplicar el operador resta es 2.

El operador de asignación (5) tiene menor precedencia que los operadores aritméticos, por lo que
la asignación se hace en último lugar. Así,

a 5 (312)/5

almacena 1 en la variable a. Es decir, almacena el resultado final de las operaciones.

3.5.2. OPERADORES DE INCREMENTO Y DECREMENTO


Estos operadores son unarios (monarios) que suman (incremento) o restan (decremento) una unidad a
un operando entero o real. La sintaxis se recoge en la Tabla 3.9.
Por ejemplo, las sentencias,

contador++;
valor--;

son equivalentes a:

Tabla 3.10. Efecto como sufijo y prefijo de los operadores de incremento y decremento al apli-
carlo sobre un valor inicial de contador=5

Sentencia Resultado en valor Resultado en contador


valor=++contador 6 6
valor=contador++ 5 6
valor=--contador 4 4
valor=contador-- 5 4

contador=contador+1;
valor=valor-1;

Estos operadores pueden usarse como prefijos o como sufijos. Cuando actúan sobre una sola
variable, como en el ejemplo anterior, el resultado es el mismo. Así;

11contador equivale a contador11


--valor equivale a valor--

Usando el operador de incremento o decremento sobre una variable, la variable se incrementa o


Introducción a la programación 71

decrementa siempre. Sin embargo, cuando estos operadores se usan en una expresión, el resultado es dife-
rente si aparecen como sufijos que como prefijos. Por ejemplo, sea el siguiente fragmento de código,

contador=5;
valor=contador++;
System.out.println(“valor: “+valor);
System.out.println(“contador: “+contador);

el resultado sería:

valor: 5
contador: 6

Es decir, en valor=contador11, primero se realiza la asignación y después se incrementa la


variable contador. Ésta es la forma de actuar del operador incremento o decremento como sufijo. El
incremento o decremento se realiza después de usar el valor de la variable en la expresión. Por el con-
trario, para el caso del prefijo tendríamos el siguiente comportamiento,

contador=5;
valor=++contador;
System.out.println(“valor: “+valor);
System.out.println(“contador: “+contador);

el resultado ahora sería:

valor: 6
contador: 6

Ahora, en la sentencia valor=++contador se incrementa primero contador y luego se usa su


valor. Actuando el operador como prefijo, la variable se incrementa en la expresión antes de usarse.
Como vemos, la variable contador siempre queda incrementada al final, pero el resultado que se alma-
cena en valor depende del orden del operador. Como resumen, veamos en la Tabla 3.10 el resultado
de la aplicación de los operadores de incremento o decremento como prefijos y sufijos en el caso de
contador=5 como valor inicial.
A continuación, se muestra otro ejemplo que refleja la diferencia de usar los operadores de incre-
mento o decremento como prefijo o sufijo. Si la variable suma contiene 30, entonces la sentencia

System.out.println (suma++ + “ “ + ++suma +


“ “ + suma + “ “ + suma--);

imprimirá:

Tabla 3.11. Operadores relacionales


en Java

Relación Sintaxis
Igual 55
Distinto !5
Mayor .
Menor ,
Mayor o igual .5
Menor o igual ,5
72 Introducción a la programación con orientación a objetos

30 32 32 32

y suma contendrá 31 después de que se complete la línea.


Es interesante indicar que como los caracteres internamente se manejan como enteros, los opera-
dores de incremento y decremento se pueden aplicar a caracteres. En este caso, el operador incremen-
to cambia el valor de la variable al siguiente carácter en el conjunto Unicode. Si se aplica el
decremento, se obtiene el carácter Unicode anterior. Las versiones sufijo y prefijo tienen el mismo
efecto que en valores aritméticos como podemos observar en el Programa 3.11.

Programa 3.11. Efecto de los operadores de incremento y decremento sobre el tipo char

class Operador{
public static void main(String [ ] args) {
char letra;
letra=’a’;
System.out.println(“letra es igual a: “+letra);
++letra;
System.out.println(“letra es igual a: “+letra);

Tabla 3.12 Operadores lógicos en Java

Significado Operador
“y” lógico &&
“o” lógico ||
“no” lógico !

System.out.println(“letra es igual a:”+ letra++ + letra--);


} //Fin del main
}//Fin de la clase

La salida del programa es:

letra es igual a: a
letra es igual a: b

Tabla 3.13. Tabla de verdad del “y”

X Y X&&Y
V V V
V F F
F V F
F F F

letra es igual a: bc

Si se añade la sentencia System.out.println (”letra es: “+letra); al final del progra-


Introducción a la programación 73

ma, la salida que se habría obtenido sería:

letra es igual a: a
letra es igual a: b
letra es igual a: bc
letra es: b

3.5.3. OPERADORES RELACIONALES


Como hemos visto al hablar de la condición (if), se puede evaluar una condición produciendo un
resultado lógico (true o false). Muy frecuentemente la condición expresa una relación entre dos
entidades, tal como su igualdad. Para expresar las diferentes relaciones existen una serie de operado-
res denominados relacionales. Dichos operadores, y su sintaxis en Java, se recogen en la Tabla 3.11.

Tabla 3.14. Tabla de verdad del “o” lógi-

X Y XY
V V V
V F V
F V V
F F F

Por ejemplo, dadas dos variables valor1 y valor2, serían expresiones válidas:

a) valor1!=valor2
b) valor1 >valor2
c) valor1<=valor2

Estos operadores son de importancia fundamental a la hora de construir expresiones lógicas como
las usadas en los if o en los bucles.

3.5.4. OPERADORES LÓGICOS


En relación con las operaciones lógicas hemos presentado en el apartado anterior los operadores rela-
cionales (igual, mayor, menor, diferente, mayor e igual y menor e igual). Existen algunos operadores
más, relacionados con las expresiones lógicas. Son los denominados operadores lógicos y permiten
aplicar el álgebra de Boole.

Tabla 3.15. Tabla de verdad del “no”

X !X
V F
F V

Hay tres operadores lógicos que son: el “y” lógico, el “o” lógico y el “no” lógico. La Tabla 3.12
muestra cómo se representan dichos operadores en Java.
El “y” y el “o” lógicos son operadores binarios y el “no” es unario. La acción de estos operado-
res se representa claramente usando las tablas de verdad. Una tabla de verdad o de operación propor-
74 Introducción a la programación con orientación a objetos

ciona el resultado de la acción del operador lógico en función de todos los posibles valores (verdade-
ro, V, o falso, F) de los operandos. Las tablas de verdad son una forma sencilla de exponer el efecto
de los operadores lógicos. Veamos las tablas de verdad de los tres operadores considerados.

a) En la Tabla 3.13 encontramos la tabla de verdad del “y” lógico. Como se puede ver, las dos
condiciones X e Y deben ser verdaderas para que la intersección lógica (el “y”) sea verdadera.
Es decir, se tiene que cumplir una condición “y” la otra para obtener un resultado verdadero.
Veamos un ejemplo de “y” lógico,

int i=1;
int j=2;
if (i==1 && j==2) {
System.out.println (“La condicion se cumple”);
}
else {
System.out.println (“La condicion no se cumple”);
}

El resultado sería,

La condicion se cumple

b) La tabla de verdad del “o” lógico se encuentra en la Tabla 3.14. En este caso, una de las dos
condiciones X e Y debe ser verdadera para que la unión lógica (el “o”) sea verdadero. Se tie-
ne que cumplir una condición “o” la otra para que el resultado sea verdadero.
Veamos un ejemplo de “o” lógico,

int i=1;
int j=2;

if (i==1 || j==3) {
System.out.println (“La condicion se cumple”);
}
else {
System.out.println (“La condicion no se cumple”);
}

El resultado también sería:

La condicion se cumple

c) Por último, encontramos la tabla de verdad del “no” lógico en la Tabla 3.15.
En este caso se consigue la negación lógica.
Debe tenerse en cuenta que las comparaciones no tienen en absoluto por qué involucrar valores
numéricos, aunque es tremendamente frecuente. Se puede trabajar directamente con valores lógicos,
como en el siguiente ejemplo ilustrativo del “no” lógico,
boolean etiqueta=true;

if (!etiqueta) {
System.out.println (“La condicion se cumple”);
}
else {
System.out.println (“La condicion no se cumple”);
Introducción a la programación 75

Tabla 3.16. Operadores de asignación en

Operador Ejemplo Equivalencia


15 a15b a5a1b
25 a25b a5a2b
*5 a*5b a5a*b
/5 a/5b a5a/b
%5 a%5b a5a%b

El resultado sería:

La condicion no se cumple

Los operadores lógicos tienen distinta precedencia. El operador lógico NO es el de más alta pre-
cedencia, el siguiente es el Y, y después el O es decir, la precedencia es:

! > && > ||

Un ejemplo típico de uso incorrecto de operadores lógicos es el siguiente:

if (a==3 || 4) {
---- bloque de acciones ----
}

En Java en particular, esto produciría un error de compilación, pues 4 es un literal entero y no se


puede convertir a tipo lógico para establecer la relación con el resultado de a 55 3 (que sí es un resul-
tado lógico). La forma correcta sería,

if (a==3 || a==4) {
---- bloque de acciones ----
}

Los operadores lógicos && y || aplican la evaluación “cortocircuitada” de operaciones. Esto quie-
re decir que si la primera condición ya ha determinado el resultado de toda la expresión, no se evalúa
la segunda. Por ejemplo,

if (x!=0 && x!=4) {


---- bloque de acciones ----
}

si x es igual a cero el && ya no puede cumplirse y no se evalúa la segunda condición.

3.5.5. OPERADORES DE ASIGNACIÓN

Existen varios operadores que combinan una operación básica con la asignación. La idea es simpli-
ficar la operación habitual de realizar una operación sobre una variable y almacenar el resultado en
76 Introducción a la programación con orientación a objetos

esa misma variable. En Java, los operadores de asignación más frecuentes se recogen en la

Tabla 3.16.
Introducción a la programación 77

Como puede observarse, la sintaxis de estos operadores es “Operación5”. Al usar estos operado-
res no estamos limitados a tener que usar una sola variable en el lado de la derecha de la igualdad.
Podemos usar expresiones. En este caso, la expresión en el lado derecho se evalúa primero y luego se
combina con la variable de la derecha. Dicho de otra forma, todos estos operadores evalúan comple-
tamente la expresión de la parte derecha en primer lugar, y luego usan el resultado como el operando
derecho de otra operación. Veamos un ejemplo:

a/=Valor/5.0+Total;

es equivalente a:

a=a/(Valor/5.0+Total);

Fijémonos en que los paréntesis en el segundo caso abarcan toda la expresión que estaba en el lado
de la derecha en el primer caso.

EJERCICIOS PROPUESTOS
Ejercicio 1.* ¿Cuál es el resultado del siguiente programa?

class Ejemplo {
public static void main(String [] args) {
int a=1, b=4, c=2, d=1;
int x=a+b/c+d;
System.out.print(“x “+ x);
}
}

Ejercicio 2.* Suponga que b es una variable lógica (boolean). ¿Cuál es el resul-
tado de las siguientes expresiones?

a) b==true
b) b=true

Ejercicio 3.* Suponiendo que las variables total y num son de tipo entero y que
inicialmente contienen los valores 2 y 3, respectivamente ¿Cuál es
el valor que adquieren total y num después de las siguientes sen-
tencias?

a) total=++num;
b) num=total++;
c) total=++num + num++;
Ejercicio 4.* Suponga que r1 y r2 son dos números reales. Escriba el código
necesario para determinar si son iguales suponiendo que la precisión
de la representación numérica es p.

Ejercicio 5.* Escriba un programa en Java que acepte por teclado el radio de una
circunferencia y evalúe su perímetro y su superficie. Nota: para el
número š utilice la constante PI de la clase Math (Math.PI).
78 Introducción a la programación con orientación a objetos

Ejercicio 6.* Escriba un programa que calcule la suma de los cuadrados de los
números enteros comprendidos entre 1 y N donde N es un entero
que se lee por teclado.

Ejercicio 7.* ¿Cuál es el resultado del siguiente programa?

class Alcance {
public static void main(String [] args) {
int i=3;
{
int j=4;
}
System.out.println(“j: “+j);
System.out.println(“i: “+i);
}
}

Ejercicio 8.* Para una disolución de un ácido débil, HA, cuya constante de aci-
dez es Ka, el pH viene dado por la expresión (aproximada):

pH . (1/2)(pKa2log[HA])

donde pKa es el menor logaritmo decimal de Ka, log representa el


logaritmo decimal y [HA] es la concentración molar (moles/litro) del
ácido.
Escriba un programa en Java que acepte la constante de acidez de
un ácido débil y luego pregunte por la concentración de la disolu-
ción, evaluando el pH. El programa debe solicitar un valor de con-
centración tras cada cálculo, hasta que el usuario indique que no
desea calcular el pH de ninguna nueva disolución.

Ejercicio 9.* Indique cuál es la salida del siguiente programa:

class Ejercicio {
public static void main(String[] args) {
char probador;
probador=’c’;
System.out.println(“probador:” + probador);
++probador;
System.out.println(“probador:”+probador);
System.out.println(“probador:”+ probador++ +
probador
+probador-- + probador);
}//del main
}// de la clase
Introducción a la programación 79

Ejercicio 10.* Escriba un programa que calcule la frecuencia, v (s-1), de oscilación


de un péndulo dada su masa m y longitud l. La expresión corres-
pondiente es
1
v5 } !ßg}
Ejercicio 10.* donde g es la aceleración normal de la gravedad en el campo gravi-
tatorio terrestre (9.8 m/s2). Aunque la frecuencia es independiente
de la masa, consideremos que si ésta es mayor de 1 kg, el hilo del
que cuelga la misma se romperá. El programa debe distinguir esta
situación y calcular la frecuencia sólo si m<1 kg.

Ejercicio 11.* Indique cuál es la salida del siguiente programa:

class Ejercicio {
public static void main(String[] args) {
int indice;
indice=20;
System.out.println(++indice + “ “+indice++ + “ “ +
indice);
}
}

REFERENCIAS
ECKEL, B.: Piensa en Java, Segunda Edición, Prentice Hall, 2002.
HAROLD, E. R.: Java I/O, First Edition, O’Reilly, 1999.
NAUGHTON, P. y SCHILDT, H.: Java Manual de Referencia, Osborne/McGraw-Hill, 1997.
PRATT, T. W. y ZELKOWITZ, M. V.: Programming Languages. Design and Implementation, Third edition, Prenti-
ce Hall, 1996.
PRIETO, A., LLORIS, A. y TORRES, J. C.: Introducción a la Informática, Segunda Edición, McGraw-Hill, 1995.
Unicode: www.unicode.org última visita realizada en junio de 2002.
VAN DER LINDEN P.: Just Java. 1.2, Fourth Edition, Sun Microsystems Press, 1999.
4

Programación estructurada

Sumario

4.1. Introducción 4.5. Técnicas de representación


4.2. El salto incondicional 4.5.1. Diagramas de flujo de control
4.3. El teorema de estructura 4.5.2. Pseudocódigo
4.4. Estructuras de control elementales 4.5.3. Diagramas de acción
4.4.1. Secuencia
4.4.2. Selección
4.4.3. Iteración
80 Introducción a la programación con orientación a objetos

4.1. INTRODUCCIÓN
Hasta la década de los años sesenta del siglo XX los computadores poseían unos recursos muy limita-
dos. Era responsabilidad del programador formular los algoritmos de forma que fueran utilizados de
la manera más eficiente posible en una máquina dada. Como indica Wirth (Wirth, 1974), la esencia de
la programación era la optimización de la eficiencia de máquinas particulares ejecutando algoritmos
particulares. Según crecía la potencia de los ordenadores y aumentaba la complejidad de los progra-
mas, el objetivo dejó de ser la necesidad de ahorrar bits y microsegundos de cómputo. En su lugar, el
problema devino en la gestión del desarrollo de programas grandes y complejos, es decir, la gestión
de la complejidad del software. Con las técnicas artesanales de la época estos sistemas eran difíciles
de diseñar, codificar y probar, y prácticamente imposibles de entender y mantener. Se requerían mejo-
res tecnologías y, como ya hemos visto, es en este contexto donde se acuña el concepto de Ingeniería
del Software. La idea clave que surgió en esta época es que ningún nivel de eficiencia es útil si el pro-
grama no es fiable de antemano.
Como parte de estos esfuerzos por disciplinar el desarrollo de software se realizaron estudios sobre
la estructura lógica del código. Ya en esta época, se reconocía que el salto incondicional (la sentencia
goto) muy usado en los lenguajes de aquel entonces, era una sentencia que producía más problemas
que los que solucionaba. Los estudios de Böhm y Jacopini sobre la estructura del código (Böhm y
Jacopini, 1966) demostraron que en condiciones normales era posible construir o reescribir cualquier
programa, consiguiendo el mismo objetivo, sin usar la sentencia goto. Los problemas engendrados
por la sentencia goto desde el punto de vista del desarrollo de software, fueron tratados por E. W.
Dijkstra en un famoso trabajo (Dijkstra, 1968), y es a él al que se atribuye el acuñamiento de la expre-
sión programación estructurada.
Por programación estructurada entendemos un estilo de codificación libre del salto incondicional,
que refuerza la inteligibilidad y la fiabilidad del código. A pesar de lo que se entiende normalmente,
la programación estructurada no es simplemente codificación sin goto, sus objetivos son más amplios
y variados (Martin y McClure, 1988):

— Mejorar la fiabilidad de los programas.


— Mejorar la legibilidad de los programas.
— Minimizar la complejidad de los programas.
— Simplificar el mantenimiento de los programas.
— Incrementar la productividad de los programadores.
— Proveer de una metodología de programación disciplinada.

Hoy por hoy la programación estructurada es un tema que todo programador debe conocer desde
los cursos más elementales de programación. La programación estructurada debe ser el estilo normal
de programación para todo profesional. En este capítulo vamos a presentarla formalmente. Para ello,
comencemos con un elemento que, aunque no forma parte de la programación estructurada, es conve-
niente conocer (aunque no se recomienda su uso). Se trata del salto o ramificación incondicional.

4.2. EL SALTO INCONDICIONAL


En la mayoría de los lenguajes existe una sentencia de salto incondicional que normalmente se deno-
mina sentencia goto y cuya sintaxis genérica es:
goto etiqueta

donde etiqueta es un identificador numérico o alfanumérico que corresponde a una sentencia en


concreto. El efecto del goto es producir un salto desde donde él se encuentra hasta la sentencia mar-
Programación estructurada 81

cada con la etiqueta. Este salto se produce inevitablemente ya que no hay ninguna condición en fun-
ción de la cual se pueda dar o no. Por esta razón, este salto forzoso se denomina salto incondicional,
por contraposición a otras sentencias donde también se produce una ramificación del flujo lógico pero
dependiendo de una condición.
En Java el goto como tal no existe, así que veamos un ejemplo de su uso en otro lenguaje, por
ejemplo, en el clásico Fortran 77. Consideremos el siguiente fragmento de código:

if (A.NE.1) goto 10
A=A+1
goto 20
10 A=A-1
20 Write (6, *) A

El operador .NE. es un operador relacional que significa NonEqual (distinto). En el ejemplo, si


A es distinto de 1 se produce un salto a la sentencia marcada con la etiqueta 10. Si no es así, se hace
A=A+1 y luego saltamos a la sentencia con la etiqueta 20 donde con el Write se imprime la variable A.
El salto a la sentencia con etiqueta 20 evita pasar por la sentencia marcada con la etiqueta 10. Como
podemos ver, el efecto del fragmento de código anterior es equivalente a un if-else. Es también fácil
entender que un programa escrito a base de saltos incondicionales tiene una lógica complicada. Tanto
es así que en la jerga de programación a los programas escritos de esta forma se los denomina spa-
guetti-like (de tipo espagueti).
A pesar de todo lo que se ha escrito en contra del goto todos los lenguajes incorporan esta sen-
tencia, o algún tipo de salto incondicional más o menos controlado. Java no soporta la instrucción
goto. Sin embargo, ésta es una palabra reservada, de forma que el compilador puede detectar cual-
quier uso erróneo de este identificador. Esto no quiere decir que Java no incorpore otras formas de rea-
lizar el salto incondicional. Existen dos sentencias que realizan un salto incondicional más o menos
controlado, las sentencias break y continue. Son sentencias para alterar el flujo de control produ-
ciendo un salto incondicional. Se podría decir que son gotos disfrazados.
La sentencia break se usa en los switch (sentencia de selección múltiple que más adelante vere-
mos). El break produce un salto al final del switch y el flujo de control salta a la sentencia que se
encuentra después de la llave que cierra el switch. También puede usarse en bucles. Su efecto es el
de finalizar la ejecución del bucle, ejecutándose la sentencia que hay después del mismo. En un bucle
produce un salto incondicional al final del mismo. De esa forma salimos del bucle y continuamos en
la sentencia siguiente. Como ejemplo consideremos el fragmento de código ilustrado en el Progra-
ma 4.1. Cuando dato sea igual a 12 el flujo de control salta fuera del bucle while y se ejecuta la sen-
tencia siguiente al bucle que es System.out.println (“Fuera del bucle”);.

Programa 4.1. Ilustración de salto incondicional

import java.io.*;
class Ejercicio {
public static void main(String [] args) throws IOException {
int num=1;
int dato;
BufferedReader leer = new BufferedReader
(new InputStreamReader(System.in));
while (num !=20) {
System.out.println(“\nIntroduzca un numero: “);
dato=Integer.parseInt(leer.readLine());
if (dato ==12) {
break;
}// Fin del if
82 Introducción a la programación con orientación a objetos

Programa 4.1. Ilustración de salto incondicional (continuación)


System.out.println(“\ndato: “ +dato);
num++;
} // Fin del bucle
System.out.println(“\nFuera del bucle”);
} //Fin del main
}//Fin de la clase

No es una buena costumbre de programación usar las sentencias break para acabar bucles. Siem-
pre se puede hacer lo mismo sin necesidad de usarlas. Para ello basta con incluir la condición que se
usaría para disparar el break en la condición de finalización del bucle, realizando un producto lógico
de condiciones (usando el “y” lógico). Por ejemplo, el caso anterior se podría reescribir usando como
condición de finalización del bucle que num sea distinto de 20 al mismo tiempo que dato es distinto
de 12, véase el Programa 4.2. Cuando dato sea igual a 12 la condición de control del while se vuel-
ve false y el bucle termina sin necesidad de usar break.

Programa 4.2. Ejemplo de reestructuración de código

import java.io.*;
class Ejercicio {
public static void main(String [] args) throws IOException {
int num=1;
int dato=0;

BufferedReader leer = new BufferedReader


(new InputStreamReader(System.in));

while (num !=20 && dato !=12) { // Producto lógico de condiciones


System.out.println(“\nIntroduzca un numero: “);
dato=Integer.parseInt(leer.readLine());
System.out.println(“\ndato: “ +dato);
num++;
} // Fin del bucle

System.out.println(“\nFuera del bucle”);

} //Fin del main


}//Fin de la clase

La sentencia break hace que el flujo del programa salte de un sitio a otro y, normalmente, no es
necesario usarlo. La excepción es la sentencia switch donde break es necesario porque, como vere-
mos, no hay otra forma de conseguir el funcionamiento correcto.
La sentencia continue también produce un salto incondicional. Su comportamiento se puede
ilustrar con un bucle. Un continue dentro de un bucle produce un salto desde el punto donde esté
hasta el final del bucle, pero sin salir de él. El bucle continúa realizando iteraciones, véase el Progra-
ma 4.3. En este caso, cuando dato=12, se salta al final del while, no ejecutándose las sentencias
System.out.println y num++. Sin embargo, el bucle no acaba, se realiza una nueva iteración y el
bucle continúa hasta que num valga 20.
Programación estructurada 83

Programa 4.3. Ilustración del funcionamiento de la sentencia continue

import java.io.*;
class Ejercicio {
public static void main(String [] args) throws IOException {
int num=1;
int dato=0;

BufferedReader leer = new BufferedReader


(new InputStreamReader(System.in));

while (num !=20) {


System.out.println(“\nIntroduzca un numero: “);
dato=Integer.parseInt(leer.readLine());
if (dato ==12) {
continue;
} // Fin del if
System.out.println(“\ndato: “ +dato);
num++;
} // Fin del while
System.out.println(“\nFuera del bucle”);

} //fin del main


}//fin de la clase

El Programa 4.3 se puede modificar de forma que el código haga lo mismo pero sin usar la sen-
tencia continue, véase el Programa 4.4. Como podemos observar, la sentencia continue no es
necesaria, se puede conseguir el mismo objetivo sin ella. Por la misma razón que la sentencia break,
la sentencia continue debe evitarse.

Programa 4.4. Reestructuración del programa 4.3

import java.io.*;
class Ejercicio {
public static void main(String [] args) throws IOException {
int num=1;
int dato=0;

BufferedReader leer = new BufferedReader


(new InputStreamReader(System.in));
while (num !=20) {
System.out.println(“\nIntroduzca un numero: “);
dato=Integer.parseInt(leer.readLine());
if (dato !=12) {
System.out.println(“\ndato: “ +dato);
num++;
} // Fin del if
} // Fin del while

System.out.println(“\nFuera del bucle”);

} //Fin del main


}//Fin de la clase
84 Introducción a la programación con orientación a objetos

break y continue admiten el uso de etiquetas alfanuméricas para producir un salto a la senten-
cia (o bloque) identificada con dicha etiqueta. La sintaxis es:

etiqueta: {
—- bloque de sentencias —-
}

La diferencia entre break y continue con respecto a las etiquetas es la siguiente. La sentencia
break con etiqueta se usa para saltar desde un bucle o switch (o en realidad desde cualquier sen-
tencia de bloque como el if) a la sentencia que se encuentra después del bloque que lleva en su cabe-
cera la etiqueta. Cuidado, la sentencia de bloque (lo normal es un bucle o switch) lleva la etiqueta al
principio pero se salta al final. Un ejemplo sería:

meses:while (m<=12) {
d=1;
while (d<=31) {
if (coste > presupuesto) break meses;
d++;
}
m++;
}

Si se pone sólo break, sin etiqueta, se terminaría de ejecutar el while interno, pero seguiría eje-
cutándose el while externo (el de m<=12). Con break etiqueta, se dejan de ejecutar el interno y
el externo. En nuestro caso, al ejecutarse break meses, se salta al final del bloque de sentencias eti-
quetado como meses. Resumiendo, para finalizar un bucle o bloque externo se debe etiquetar la sen-
tencia externa y usar el nombre de la etiqueta en la sentencia break colocada en el interior.
Igual que break, continue también puede portar etiqueta. La sentencia continue con etiqueta
permite saltar hasta el final del bloque que lleva la etiqueta. La diferencia con el break es que el sal-
to no implica la finalización de la estructura de control en la que estemos inmersos. Por ejemplo, con-
sideremos varios bucles anidados,

meses:while (m<=12) {
m++;
d=1;
while (d<=30) {
if (m==2 && d==29) continue meses;
System.out.println (m+” “+d);
d++;
}
}

Con continue sin etiqueta se saltaría a la siguiente iteración del bucle más interno (el de d<=30).
Por otro lado, con continue etiqueta se controla el bucle al que se salta, en este caso al externo.
En nuestro caso, el continue envía el control al final del bloque etiquetado con meses. En otras pala-
bras, envía el control al final de la presente iteración del bucle con m<=12 pero el bucle continúa en la
siguiente iteración. Un continue con etiqueta saldrá de todos los bucles internos hasta llegar al bucle
etiquetado. En este caso, se comprobará la condición y si es posible continuarán las iteraciones.
Es conveniente conocer el funcionamiento del salto incondicional para poder realizar labores de
mantenimiento en software ya existente. Sin embargo, como norma de programación se debe evitar el
uso de cualquier salto incondicional. Siempre es posible realizar el mismo trabajo sin necesidad de
recurrir a su uso.
Programación estructurada 85

4.3. EL TEOREMA DE ESTRUCTURA


En la introducción de este tema se ha hecho referencia al trabajo de Böhm y Jacopini (Böhm y Jaco-
pini, 1966) sobre la posibilidad de construir todos los programas que satisfagan ciertas condiciones sin
usar el salto incondicional. Esto constituye el teorema de estructura que podríamos enunciar en len-
guaje actual de la siguiente forma (Joyanes, 1996):

Teorema de estructura: Todo programa con un único punto de entrada y un único punto
de salida, cuyas sentencias se alcancen todas en algún momento y que no posea bucles infinitos
(programa propio) se puede construir con tres constructores elementales: secuencia,
selección y bucle.

El trabajo de Böhm y Jacopini demuestra que estos tres constructores forman un conjunto sufi-
ciente para construir cualquier algoritmo concebible. El salto incondicional no es un constructor nece-
sario. Si se usan sólo los tres constructores elementales, el código queda organizado de forma arriba-
abajo. Esto quiere decir que cuando se produzca una ramificación (no iterativa) del código, el flujo de
control se desplaza hacia abajo en el código. La lógica del diseño resultante es fácil de seguir y enten-
der. Esto simplifica, entre otras, las labores de pruebas y de mantenimiento. Las condiciones que defi-
nen un programa propio no son exigentes ni extrañas, se dan en prácticamente todos los programas o
secciones de código. Un buen programador debe aplicar un estilo de codificación estructurado como
algo absolutamente normal en sus programas. En otras palabras, la programación estructurada debe ser
el estilo natural de todo programador.
El hecho de poder construir cualquier programa propio de forma estructurada permite la reestruc-
turación de código. Es ésta una labor de mantenimiento 1 del software consistente en transformar códi-
go no estructurado en estructurado. En esencia, se trata de la modelización de la lógica del programa
usando el álgebra de Boole con las técnicas propuestas por Warnier (Warnier, 1974). Para más deta-
lles sobre las técnicas de reestructuración (véase Yourdon, 1975).
El hecho de poder reestructurar código no estructurado no quiere decir que esto pueda hacerse
usando el mismo número de constructores y sin variables adicionales. Muy frecuentemente es necesa-
ria la duplicación de parte del código, aunque éste es un precio pequeño a pagar por el aumento de la
manejabilidad del código. Veamos un ejemplo que ilustra la posibilidad de reestructurar un programa
no estructurado. Consideremos el caso mostrado en la Figura 4.1. Es un diagrama donde cada bloque
indica una serie de sentencias. Consideremos el bloque E. Este bloque recibe el flujo de control (las
flechas) por dos sitios distintos. Éste es el efecto típico del salto incondicional, alcanzamos el código
por dos puntos diferentes según se salte o no se salte con el goto. Es fácil imaginar los problemas que
se pueden producir cuando se llega al mismo código desde dos puntos diferentes del programa, pues
los valores de las variables dependerán del camino recorrido. Para reestructurar el código deberíamos
evitar la entrada en E por dos puntos. Esto se puede hacer si duplicamos el bloque E tal y como se
muestra en la Figura 4.2. Obsérvese que ahora todos los bloques presentan un solo punto de entrada.

4.4. ESTRUCTURAS DE CONTROL ELEMENTALES


Para abordar una programación estructurada en un lenguaje determinado se necesita conocer los cons-
tructores que permiten realizar las tres acciones elementales de secuencia, selección y bucle. Ya hemos

1
En realidad se trataría de labores de reingeniería del software. El lector interesado puede encontrar más información
sobre este tema en el texto de Pressman (Pressman, 2002).
86 Introducción a la programación con orientación a objetos

Figura 4.1. Ejemplo de programa no estructurado

Figura 4.2. Versión estructurada del programa de la Figura 4.1

visto algunos de ellos y el resto los veremos en este capítulo. Consideremos cada constructor por sepa-
rado.

4.4.1. SECUENCIA
La secuencia de acciones no tiene un constructor específico, viene determinada de forma natural por
el flujo de control del programa. La secuencia de acciones está implícita en el orden en el que apare-
cen las sentencias en el programa, por ejemplo:

int valor=0;
valor=valor+1;
System.out.println(valor);
Programación estructurada 87

Figura 4.3. Anidamiento y concatenación de estructuras

La secuencia de acciones indicada en las sentencias anteriores se corresponde con el orden en el


que encontramos dichas sentencias en el código.

4.4.2. SELECCIÓN
El mecanismo de selección básico es la sentencia if-else. En ésta se evalúa una condición lógica y
se elige qué sucederá en función del resultado. Estas sentencias se llaman sentencias de selección o
sentencias condicionales o simplemente decisiones. La sintaxis de la sentencia if-else en Java ya se
expuso en el capítulo anterior.
Se pueden encadenar sentencias if sin problemas. Cuando enlazamos varias de ellas (o en gene-
ral varias estructuras de control) tenemos dos formas de hacerlo:

— Por anidamiento: con una estructura dentro de otra.


— Por concatenación: con una estructura tras otra.

Las dos situaciones se ilustran gráficamente en la Figura 4.3.


Vamos a mostrar cada caso por medio de condiciones (sentencia if).

a) Concatenación (incluyendo la cláusula else):

Consideremos dos if controlados por una condición cada uno. La Figura 4.4 ilustra el caso de una
concatenación de nuestras dos estructuras condicionales.

Como puede observarse, tenemos las dos condiciones del ejemplo colocadas una a continuación
de la otra. Independientemente de si la primera condición es verdadera o falsa se llega siempre a la
segunda condición. Como ejemplo consideremos un programa con dos variables denominadas pre-
cio y cantidad. Si precio es mayor de 10 e se hace un descuento del 5%. Por otro lado si can-
tidad es mayor de 5 se añade un artículo más de regalo. El código correspondiente sería,
88 Introducción a la programación con orientación a objetos

Figura 4.4. Ejemplo de concatenación de sentencias if

if (precio > 10.0) {


precio=precio*0.9; // Descuento del 10%
}

if (cantidad >5) {
cantidad++; // Regalo de un artículo más
}

Obsérvese cómo en el ejemplo la concatenación de condiciones permite que comprobemos la


segunda independientemente del resultado de la primera.

b) Anidamiento incluyendo la cláusula else (las condiciones están unas dentro de otras)
En este caso vamos a considerar tres condiciones. La Figura 4.5 muestra un ejemplo de organiza-
ción anidada de estas condiciones.

En este caso, tenemos condiciones dentro de condiciones. Así, dentro de la parte de la sentencia
if con la condición1 se encuentra un nuevo if con la condición2. A su vez, dentro de la parte
del else del if con la condición1 se encuentra otro if con la condición3. Ahora los if no son
independientes. Por ejemplo, si condición1 es verdadera no se llega al if con la condición3 y si
condición1 es falsa no se llega al if con condición2. Las flechas usadas indican el alcance de
cada estructura. El hecho de que los bloques delimitados por las flechas queden unos dentro de otros
y que las líneas de alcance no se corten es una consecuencia de la programación estructurada. Como
ejemplo de estructura condicional anidada consideremos un ejemplo similar al anterior con las varia-
bles precio y cantidad. Como antes, si precio es mayor de 10 e se hace un descuento del 5%. Sin
embargo, consideremos ahora que sólo si precio es mayor de 20 e se añade el artículo de regalo a
cantidad. En este caso, el código sería:

if (precio >10.0) {
precio=precio*0.9; // Descuento del 10%
Programación estructurada 89

Figura 4.5. Ejemplo de anidamiento de sentencias if

if (precio >20.0) {
cantidad++; // Artículo de regalo
} // Fin if interno

} // Fin if externo

Obsérvese ahora cómo pasar o no por la segunda condición depende del resultado de la primera.
En los lenguajes modernos existe la posibilidad de selección múltiple. En Java, esto se consigue
con la sentencia switch. Se trata de una sentencia de selección que permite elegir una entre varias
posibilidades. Así, en función de un único valor podemos seguir uno entre varios caminos. La senten-
cia switch evalúa una expresión y compara el resultado con una serie de valores. La ejecución se
transfiere a la lista de sentencias asociada con el primer valor que coincide. El comportamiento es
equivalente a una serie de if anidados. La sintaxis de esta sentencia es:

switch (expresión) {
case literal 1:
— bloque 1—
break;
case literal 2:
— bloque 2 —
break;

...
default:
— bloque n —
}
90 Introducción a la programación con orientación a objetos

La expresión debe producir un valor de tipo int o char y el literal debe ser del mismo tipo que
el resultado de la expresión. No es necesario colocar llaves para delimitar los bloques de cada caso. La
sentencia break se coloca como última sentencia después de cada case y hace que la sentencia
switch se acabe y se ejecute la siguiente sentencia después de ella. Si no se usa break, se siguen
ejecutando las sentencias de los siguientes case hasta llegar al final del switch, o hasta que en algún
case haya un break. La necesidad de usar una sentencia break es una característica poco elegante
de Java. La cláusula default representa todos los casos no considerados en los case anteriores, es
decir, todos los valores de la expresión que no se correspondan con algún literal de los case, produ-
cirán un salto al default. La sentencia default no es necesaria a no ser que queramos que se eje-
cute algo cuando no se haya alcanzado ninguno de los casos. Si no se usa, se sale del switch y se
ejecuta la siguiente sentencia.
El Programa 4.5 muestra el uso del switch y además ilustra el uso de la lista de parámetros en la
línea de órdenes.

Programa 4.5. Ilustración del uso de la sentencia switch y de lectura por línea de órdenes

class Switch {
public static void main(String [] args) {
int numero;
numero=Integer.parseInt(args[0]);

switch (numero) {
case 1:
System.out.println(“El numero es un 1”);
break;
case 2:
System.out.println(“El numero es un 2”);
break;
default:
System.out.println(“El numero no era ni 1 ni 2”);

} // Fin switch

} // Fin método main


} // Fin clase

Antes de explicar la salida del programa consideremos el uso de la variable args. La cadena args
que hemos visto habitualmente en la cabecera del método main, es en realidad una lista de objetos de
clase cadena (String). Esta lista empieza a enumerarse por cero (0) y contiene las cadenas de carac-
teres que se coloquen a la derecha del nombre del programa cuando se ejecuta. La primera cadena es
la número cero (args[0]), la siguiente la uno (args[1]), etc. Por ejemplo, consideremos un progra-
ma con la clase Ejemplo, contenida en el fichero Ejemplo.java y compilada para dar un fichero
Ejemplo.class que contiene el bytecode. Si ahora interpreto el bytecode con el intérprete del JDK
podría hacer:

c:\datos> java Ejemplo 23.4 33.9 54.8

Los tres números separados con blanco al lado del nombre del fichero .class se almacenan
automáticamente como cadenas alfanuméricas en los elementos args[0], args [1] y args [2],
de forma equivalente a las asignaciones:

args [0]=”23.4”;
Programación estructurada 91

args [1]=”33.9”;
args [2]=”54.8”;

Obsérvese que lo que se lee son cadenas de caracteres y que si queremos su equivalente numérico
hay que convertirlos al formato numérico correspondiente usando la clase contenedora necesaria.
En el caso del ejemplo que estamos considerando (Programa 4.5), se lee un número entero de la
línea de órdenes y se mira su valor. Si es 1 ó 2, se indica y si no, se salta al caso defecto (default)
indicándose que no era ni un 1 ni un 2.
¿Qué ocurriría si se elimina el primer break y se introduce 1 por la línea de órdenes? Al llegar al
switch se produciría un salto al caso 1 y se escribiría:

El numero es un 1

Al no haber break se continuaría en el siguiente caso y se escribiría:

El numero es un 2

Al encontrar aquí un break se saltaría al final de la sentencia switch. Por lo tanto la salida obte-
nida sería:

El numero es un 1
El número es un 2

Si se suprimiera además el segundo break, la salida sería:

El numero es un 1
El numero es un 2
El numero no era ni 1 ni 2

Además de las sentencias if y switch en Java existe un operador condicional que es un opera-
dor “ternario”, pues usa tres operandos. Las sintaxis es:

condición ? expresión1 : expresión2;

El operador realiza un if-else abreviado. Si la condición es verdadera, se ejecuta la expre-


sión1 y si es falsa la expresión2. El ejemplo anterior es equivalente a:

if (condición){
expresión1;
}
else {
expresion2;
}

Como es un operador compacto se puede colocar en un System.out.println, por ejemplo, para


que se imprima un resultado u otro en función de la condición. En una asignación este operador se
podría usar de la forma siguiente:

mayor= (n1 <n2) ? n2 : n1;

si n2 es el mayor se salva en la variable mayor y si es n1 es este valor quien se almacena en mayor.


92 Introducción a la programación con orientación a objetos

4.4.3. ITERACIÓN
Otra forma de modificar el flujo de control por medio de una ramificación es usando bucles. Los
bucles son útiles porque frecuentemente es necesario repetir una sentencia, o un bloque de sentencias,
varias veces en un programa. En los lenguajes de programación suele haber varias sentencias de repe-
tición o bucle. Recordemos el tipo de bucle que podemos considerar como el más directo, el bucle
while, cuyo efecto, como ya vimos, es que se repita un bloque de sentencias mientras se cumpla una
cierta condición. Tal y como se indicó en el Capítulo 3, la sintaxis de la sentencia while en Java es:

while (condición) {
—— bloque de sentencias ——
}

Las llaves ({}), que indican el final y el principio del bucle while, no son necesarias cuando el
cuerpo del while está formado por sólo una sentencia. El bloque de sentencias se repetiría mientras
la condición sea verdadera. Si la condición es falsa desde el principio, las sentencias dentro del whi-
le no se ejecutan. Esto es importante, un bucle while se repite cero o más veces. En algún momen-
to el bloque de sentencias debe alterar la condición para que se vuelva falsa y entonces se acabe el
bucle. En el momento en el que sea falsa el proceso continúa con la sentencia colocada después del
cuerpo del bucle while.
Otro tipo de bucle es el que comprueba la condición al final del bloque de sentencias. En Java se
denomina sentencia do-while. La sintaxis de esta sentencia es:

do {
—— bloque de acciones ——
} while (condición);

El bucle do-while se ejecuta hasta que la condición es falsa. Un bucle do-while se ejecuta al
menos una vez, ya que la condición se evalúa al final. Este tipo de bucle es especialmente útil cuando
se procesa la selección de un menú, ya que siempre se desea que el bucle del menú se ejecute al menos
una vez. Para ilustrar su uso, veamos la utilización de un bucle do-while, para la obtención del fac-
torial de un entero mayor que cero.
Como sabemos, el factorial de un entero N . 0 viene dado como un producto de factores,
N
N! 5 1 ? 2 ? ... ? N 5 P i
i 5 1

Para evaluar el factorial, podemos implementar el productorio con un do-while de la forma mos-
trada en el Programa 4.6.

Programa 4.6. Cálculo de factorial con un bucle do-while

import java.io.*;
class Factorial {
public static void main(String [] args) throws IOException {
int i,n;
double factorial;
BufferedReader leer =new BufferedReader
(new InputStreamReader(System.in));

System.out.println(“Introduzca un numero para calcular”


+” el factorial:”);
n=Integer.parseInt(leer.readLine());
Programación estructurada 93

Programa 4.6. Cálculo de factorial con un bucle do-while (continuación)


System.out.println(“Numero introducido: “+n); // Eco de
// la entrada
i=0;
factorial=1.0;
do {
i=i+1;
factorial=factorial*i;
} while (i<n);
System.out.println(“Factorial: “+factorial);
} // Fin método main
} // Fin clase Factorial

En el programa anterior se lee el valor n por el teclado y se repite un do-while hasta que el con-
tador i alcanza el valor n. El ejemplo es simple y no distingue el caso n=0, pero sirve para indicar que
un sumatorio o un productorio se simulan con un bucle. Así, para un sumatorio como,
N

6i
i 5 1

el algoritmo, suponiendo que hemos declarado una variable suma como double, una i como int
(para usarla como contador) y una n que indicaría el límite superior del sumatorio, sería:

// Inicialización de variables
suma=0.0;
i=0;
while (i<n) {
i=i+1;
suma=suma+i;
}

En este caso, hemos ilustrado el ejemplo con un while. Fijémonos en que la variable que acumu-
la el valor se inicializa a uno para un productorio y a cero para un sumatorio. Éstos son ejemplos de
bucles controlados por contador. Dichos bucles son muy frecuentes y existe una forma abreviada de
implementarlos. Dicha forma se entiende como un nuevo tipo de bucle, el bucle for que no es sino
un while controlado por contador. El for se usa cuando se conoce el número de veces que se tiene
que repetir el bucle. En Java la sintaxis es:
for (inicialización; condición; incremento) {
-- bloque de sentencias --
}

En la inicialización lo que se inicializa es la variable contadora. La condición indica qué se tiene


que cumplir para que el bucle continúe (recordemos que un for es un tipo de bucle while), y qué
incremento recibe la variable contadora. El for se ejecuta mientras la condición es verdadera. Semán-
ticamente, un for es equivalente a un while con la estructura:
inicialización;
while (condición) {
-- bloque de sentencias --
incremento;
}
94 Introducción a la programación con orientación a objetos

Por ejemplo, implementemos el caso del sumatorio desde 1 a N con un for:

suma=0.0;
for (i=1;i<=n;i=i+1) {
suma=suma+i;
}

Fijémonos en que en un bucle for el incremento del contador se hace después de ejecutar el blo-
que de acciones. Por eso, si queremos que el bloque se ejecute incluyendo el valor límite de la varia-
ble que controla el final del bucle (en este caso N) en la condición hay que incluir un igual (en este
caso i<=n).
La variable contadora se puede declarar en el propio for. En este caso, debido a las reglas de
alcance de Java, dicha variable sólo existe dentro del bucle for. Cuando salgamos del mismo, la varia-
ble no existirá. También es habitual el uso de los operadores de incremento o decremento para variar
la variable de control del bucle. Con todo esto, la sintaxis habitual de un for sería, en el ejemplo del
sumatorio,

suma=0.0;
for (int i=1;i<=n;i++) {
suma=suma+i;
}

Si ahora pretendemos usar la variable i, en un método println, por ejemplo, el compilador


daría un error de variable no declarada. Al igual que un bucle while, la condición de la sentencia
for se comprueba antes de ejecutarse el cuerpo del bucle, por lo que el bucle for se ejecuta cero o
más veces.
Sintácticamente, no es necesario colocar los tres componentes (inicialización, condición e incre-
mento) de la cabecera del for. Es decir, las tres expresiones de la cabecera de un bucle for son
opcionales. Si no se incluye alguna de ellas se produce un comportamiento por defecto. Dicho com-
portamiento es el siguiente:

a) Si no se incluye la expresión de inicialización, la variable no se inicializa en ese punto y debe


inicializarse (antes) en otro punto del programa.
b) Si no se incluye la condición, se considera siempre verdadera, y por tanto estamos en un bucle
infinito.
c) Si no se incluye el incremento, no se incrementa la variable.

En cualquier caso, siempre deben colocarse en la cabecera del for los punto y coma (;) de cada
componente, aunque no se incluya alguno de ellos. Por ejemplo:

for ( ;i<100; ){
System.out.println(“i es:” +i);
i++;
}

También, en Java se puede incluir más de una sentencia en las secciones de inicialización y de
incremento de un for. A tal efecto se utiliza el operador coma (,). Por ejemplo:

for (i=0, j=0; i<100; i++, j+=2) {


—- bloque de sentencias —-
}
Programación estructurada 95

for (a=1, b=4; a<b ; a++, b--) {


-- bloque de sentencias --
}

El hecho de colocar dos variables (como i y j o a y b en los ejemplos anteriores) no implica que
tengamos bucles anidados. Esto se observa en las condiciones que controlan los bucles. En los ejem-
plos anteriores hay una sola condición para cada bucle.
Puede ocurrir que en un bucle la condición que lo controla nunca cambie a falsa y que por lo tan-
to, el bucle se cicle, entrando en un bucle infinito. Esto implica que si no se produce antes un error, el
bucle se ejecutaría sin parar hasta que se interrumpa el programa. Este error lógico, el bucle infinito,
es muy frecuente y debe ser evitado. Veamos un ejemplo de bucle infinito,

int contador = 1;
final int LIMITE=25;

while (contador <= LIMITE) {


System.out.println(contador);
contador = contador - 1;
}

El valor de contador empieza en 1 y se va decrementando, así que nunca llega a 25, con lo cual,
el bucle nunca terminaría de ejecutarse.

4.5. TÉCNICAS DE REPRESENTACIÓN


El teorema de estructura nos indica que cualquier programa puede construirse con tres estructuras ele-
mentales o privilegiadas. Para trabajar a mayor nivel de abstracción que el código, podemos usar téc-
nicas simbólicas que nos permitan representar algoritmos de forma independiente del lenguaje. En esta
sección se presentan tres técnicas típicas: los diagramas de flujo, el pseudocódigo y los diagramas de
acción. De éstas, las más extendidas son las dos primeras: los diagramas de flujo (de control) y el pseu-
docódigo.

4.5.1. DIAGRAMAS DE FLUJO DE CONTROL


Los diagramas de flujo de control (también denominados ordinogramas) son una herramienta gráfica
que sirve para representar las acciones generales en un programa, y entre ellas las tres estructuras ele-
mentales. Es una de las herramientas de representación basadas en diagramas más antiguas y, aunque
en la actualidad no se recomienda su uso, se siguen utilizando en muchas ocasiones. En los diagramas
de flujo de control se usan una serie de símbolos que representan el tipo de operación a realizar. Los
símbolos están unidos por flechas que representan el flujo de control. Los símbolos más comunes nor-
malizados por la ANSI (American National Standards Institute) son los mostrados en la Figura 4.6.
El bucle se representa normalmente jugando con el símbolo de condición. Los conectores se usan
para encadenar diagramas que estén en la misma o distinta página. Cada conector se identifica por un
número colocado en su correspondiente símbolo. El mismo número aparece en los conectores del final
del diagrama y principio del diagrama con el que conecta el anterior.
Veamos un ejemplo de utilización de los diagramas de flujo. Representemos un algoritmo para la
suma de los diez primeros números enteros distintos de cero. El diagrama resultante se ilustra en la
96 Introducción a la programación con orientación a objetos

Figura 4.6. Símbolos más comunes usados en los diagramas de flujo de control

Figura 4.7. Obsérvese que en la decisión se indica qué rama es la del sí y cuál es la del no. También
se ha usado la flecha i para representar la asignación en las variables Suma y Número. Éste es un sim-
bolismo muy común. Obsérvese también que el diagrama de flujo representa un bucle do-while, con
su condición al final del bloque de acciones.
En el ejemplo anterior se muestra cómo representar un bucle do-while. El diagrama de flujo de
control que representa al bucle while se recoge en la Figura 4.8.
En este caso la condición aparece al principio del bloque de sentencias a repetir. Observamos tam-
bién que en ambos ejemplos, el bucle se repite mientras la condición es verdadera (rama del sí).
Los diagramas de flujo de control no se consideran una herramienta estructurada y, en todo caso, su
uso se limita a programas pequeños. La principal desventaja es que son una herramienta demasiado
detallada para usarse como instrumento de diseño o incluso de documentación de programas. A pesar
de ello, y por la fuerza de la costumbre, los diagramas de flujo se emplean para ilustrar algoritmos o
fragmentos de algoritmos. Por ello, son una herramienta que debe ser conocida por todo programador.

4.5.2. PSEUDOCÓDIGO
El pseudocódigo o lenguaje estructurado se puede considerar como un lenguaje de especificación de
algoritmos. Su primer uso fue como una alternativa a los diagramas de flujo de control para promover
la metodología estructurada. Como originalmente estos desarrollos se hicieron en Norteamérica, el
lenguaje usado era inglés, y como inglés estructurado se puede ver denominado en la literatura anglo-
Programación estructurada 97

Figura 4.7. Diagrama de flujo de control que muestra la suma de los diez primeros enteros
distintos de cero

Bloque 1

No
Condición

Bloque 2

Figura 4.8. Diagrama de flujo de control del bucle while


98 Introducción a la programación con orientación a objetos

sajona 2. El pseudocódigo es una herramienta útil para representar y refinar algoritmos en una notación
que se corresponde de forma casi inmediata con los constructores de un lenguaje de programación. No
existe un estándar para el pseudocódigo, por lo que se pueden encontrar distintas variantes del mismo.
El pseudocódigo permite representar las tres estructuras principales de secuencia, selección y bucle.
Veamos una propuesta de pseudocódigo en castellano que podamos usar a lo largo de este texto.

a) Secuencia
No se representa con ningún formalismo especial. La secuencia se considera implícita en el orden occi-
dental de lectura; de arriba a abajo y de izquierda a derecha.

b) Selección
El if(condición)-else se representa como tal en inglés y como Si(condición)entonces-
Si_no en castellano.
El switch se representa como:

Según (variable) Hacer:


valor 1: -- bloque de sentencias 1 --
valor 2: -- bloque de sentencias 2 --
...
valor n: -- bloque de sentencias n --
Fin_según

c) Bucle
Los bucles while y do-while se representan como Mientras y Haz-Mientras. El for lo pode-
mos representar como:

Para (valor inicial) mientras (condición) incremento (valor del


incremento)

El operador de asignación en pseudocódigo se suele representar por una flecha que apunta hacia
la variable en la que se coloca el valor, tal y como se muestra a continuación,

Variable i 3.1416

Los bloques de sentencias se marcan a veces con start-end en inglés o Inicio-Fin en caste-
llano. A veces se usan los delimitadores del lenguaje que se acostumbre a manejar. El fin de los if o
de los bucles se marca con End_if, End_while, End_for o en castellano Fin_Si, Fin_Mientras,
Fin_Para.
El pseudocódigo se planteó en principio como una herramienta independiente del código. Es muy
frecuente que si un lenguaje de programación es el más usado en un entorno de trabajo, las palabras
clave usadas en el pseudocódigo sean las de ese lenguaje. Como ilustración, veamos un ejemplo de
utilización de pseudocódigo. Establezcamos el pseudocódigo del programa que suma los 10 primeros

2
Estrictamente hablando no es lo mismo pseudocódigo que lenguaje estructurado, (véase Martin y McClure, 1988).
Programación estructurada 99

números enteros distintos de cero,


Inicio
sumai0
numeroi1
Haz
sumaisuma+numero
numeroinumero+1
Mientras (numero 10)
Escribe suma
Fin

Obsérvese que el pseudocódigo generado se puede traducir con facilidad a un lenguaje de progra-
mación.

4.5.3. DIAGRAMAS DE ACCIÓN


Aunque de uso menos frecuente y más profesional, los diagramas de acción proporcionan una técnica
de diagramas capaz de representar de la misma manera la estructura de alto nivel de un programa que
la visión detallada de su lógica interna (Martin y McClure, 1988; Martin y McClure, 1989) . Los dia-
gramas de acción emplean los conceptos de la programación estructurada y pueden representar las
estructuras de control elementales. La simbología usada es la siguiente:

a) Secuencia
La secuencia de acciones, representada por un bloque de sentencias que se van ejecutando una detrás
de otra, se indica por un símbolo de apertura de corchete, véase la Figura 4.9.

{
---bloque de sentencias---
}

Figura 4.9. Representación de la secuencia de acciones en los diagramas de acción

b) Selección
La selección simple se representa como la secuencia, véase la Figura 4.10.

if (condici n) {
---bloque de sentencias---
}

Figura 4.10. Diagrama de acción para la selección simple


100 Introducción a la programación con orientación a objetos

La sentencia if-else es una variante del símbolo anterior, véase la Figura 4.11.

if (condici n) {
--- bloque de sentencias 1 ---
}
else {
--- bloque de sentencias 2 ---
}

Figura 4.11. Diagrama de acción para el if-else

Finalmente, la representación de la selección múltiple se encuentra en la Figura 4.12.

switch (valor) {
case 1:
--- bloque de sentencias 1 ---
break;
case 2:
--- bloque de sentencias 2 ---
break;
case 3:
--- bloque de sentencias 3 ---
}

Figura 4.12. Diagrama de acción para la selección múltiple

Como puede observarse, cada rama horizontal corresponde a un caso de la selección.

c) Bucle
Para representar cualquier tipo de bucle se usa un corchete, como en la secuencia, que abarca todo el
bucle pero que en la parte superior tiene una doble barra, véase la Figura 4.13.

while (condici n) {
--- bloque de sentencias ---
}

Figura 4.13. Diagrama de acción para bucles


Programación estructurada 101

A nivel de codificación los diagramas de acción son muy útiles pues se adaptan totalmente a un
estilo estructurado y delimitan muy bien los constructores que se estén utilizando y su alcance.
Existen multitud de técnicas adicionales basadas en diagramas para representar la estructura del
código a alto nivel de abstracción. El lector interesado puede consultar el texto de Martin y McClure
(Martin y McClure, 1988).

EJERCICIOS PROPUESTOS

Ejercicio 1.* Diseñe y codifique un programa estructurado en Java que calcule el


factorial de un número entero positivo cualquiera (incluido cero). El
programa debe solicitar un entero para calcular el factorial, identifi-
car el caso de un entero negativo y, en ese caso, solicitar un nue-
vo valor hasta que se introduzca un entero no negativo. Tras
calcular el factorial el programa debe preguntar si se desea introdu-
cir un nuevo entero. Si es así, el programa debe solicitar un valor
entero en las mismas condiciones que anteriormente. El proceso se
repetirá hasta que el usuario indique que no desea seguir calculan-
do factoriales.

Ejercicio 2.* Dada la siguiente sentencia switch transfórmela en una secuencia


equivalente de sentencias if.

switch (opcion) {
case 1:
System.out.println(“Uno”);
break;
case2:
System.out.println(“Dos”);
break;
case 3:
System.out.println(“Tres”);
break;
default:
System.out.println(“Otros”);
}

Ejercicio 3.* Usando bucles como únicas estructuras de control y una única sen-
tencia de impresión para el asterisco, escriba un programa en Java
que imprima la siguiente salida:

*****
***
*

Ejercicio 4.* El siguiente fragmento de programa pretende sumar los enteros de


1 a n (ambos inclusive) almacenando el resultado en la variable sum.
¿Es correcto el programa? Si no lo es, indique por qué y qué habría
que hacer para solucionarlo.

i=0;
sum=0;
102 Introducción a la programación con orientación a objetos

while (i<=n) {
i=i+1;
sum=sum+i;
}

Ejercicio 5.* Utilizando bucles como únicas estructuras de control escriba un pro-
grama que imprima lo siguiente:

*****
****
***
**
*

Se debe imprimir un único carácter cada vez.

Ejercicio 6.* Reestructure el siguiente fragmento de código para evitar el uso de


saltos incondicionales.

while (i < n) {
j=objetoX.valor(i);
if (j==-1) break;
i++;
}

Ejercicio 7.* Usando bucles como únicas estructuras de control escriba un pro-
grama que imprima lo siguiente:

*******
******
*****
****
***
**
*

Se debe imprimir un único carácter cada vez.

Ejercicio 8.* ¿Qué imprimiría el siguiente programa?

class EjemploSwitch {
public static void main(String [] args) {
for (int i=0; i<=7; i++)
switch (i) {
case 0:
case 1:
case 2:
System.out.println(i+” es menor que 3”);
break;
case 3:
case 4:
case 5:
System.out.println(i+” es menor que 6”);
break;
Programación estructurada 103

default:
System.out.println(i+” es 6 o mayor”);
}
}//fin main
}//fin clase

Ejercicio 9.* ¿Cuál es el resultado del siguiente programa?

class Ejercicio {
public static void main(String [] args) {
int s,x=0;
switch (x) {
case 0:
s=0;
default:
if (x<0)
s=-1;
else
s=1;
}
System.out.println(s);
}//fin main
}//fin clase

Ejercicio 10.* Reescriba el siguiente fragmento de código sin usar la sentencia con-
tinue pero manteniendo la funcionalidad del código.

meses: while (m<=11) {


m++;
d=1;
while (d<=30) {
if (m==2 && d==29) continue meses;
System.out.println(m+” “+d);
d++;

}
}

Ejercicio 11.* Una línea de autobuses cobra un mínimo de 20 e por persona y tra-
yecto. Si el trayecto es mayor de 200 km, el billete tiene un recar-
go de 3 céntimos por km. Sin embargo, para trayectos de más de
400 km el billete tiene un descuento del 15%. Por otro lado, para
grupos de 3 o más personas el billete tiene un descuento del 10%.
Con las consideraciones anteriores, escriba en Java un programa
estructurado que lea por teclado la distancia del viaje a realizar, así
como el número de personas que viajan juntas y que con ello cal-
cule el precio del billete individual.

Ejercicio 12.* Escriba un programa estructurado en Java que lea por teclado una
serie de números reales y que calcule su media, mostrando el resul-
tado en pantalla. Utilice un bucle for para solicitar los datos y para
104 Introducción a la programación con orientación a objetos

realizar el sumatorio de valores al calcular la media.

REFERENCIAS
BÖHM, C. y JACOPINI, G.: ACM, 366-371, 9(5), 1966.
DIJKSTRA, E. W.: “Go To Statement Considered Harmful”, Comm. ACM, 147-148, 11(3), 1968.
JOYANES AGUILAR, L.: Fundamentos de Programación, Segunda Edición, McGraw-Hill, 1996.
MARTIN, J. y MCCLURE, C.: Action Diagrams, Second Edition, Prentice-Hall, 1989.
MARTIN, J. y MCCLURE, C.: Structured Techniques. The Basis for CASE, Prentice-Hall, 1988.
PRESSMAN, R. S.: Ingeniería del Software, McGraw-Hill, Quinta Edición, 2002.
WARNIER, J. D.: Logical Construction of Programs, Van Nostrand Reinhold, 1974.
WIRTH, N.: “On the Composition of Well-Structured Programs”, Computing Surveys, 247-259, 6(4), 1974.
YOURDON, E.: Techniques of Program Structure and Design, Prentice-Hall, 1975.
5

Abstracción procedimental
y de datos

Sumario

5.1. Introducción 5.3. Tipos abstractos de datos. Clases y objetos


5.2. Programación Modular 5.3.1. Conceptos generales
5.2.1. Modularización funcional 5.3.2. Clase cadena (String)
5.2.2. Paso de parámetros 5.3.3. Clase matriz
5.2.3. Sobrecarga de métodos 5.3.4. Clases contenedoras
106 Introducción a la programación con orientación a objetos

5.1. INTRODUCCIÓN
Como ya hemos comentado en capítulos anteriores los avances tecnológicos de los años sesenta del siglo
XX hicieron posible abordar el desarrollo de programas cada vez más complejos. Con el objetivo de que
los programas fueran más fáciles de diseñar, codificar y probar surgió la programación estructurada, así
como otra técnica clásica relacionada con el tratamiento de la complejidad: la programación modular.
Esta última se centra en la descomposición de un problema complejo en subproblemas más pequeños
que se puedan resolver por separado. De esta forma sólo hay que tratar en cada momento con problemas
sencillos y manejables resueltos por medio de bloques de código independientes. Estos bloques se deno-
minan módulos y en este capítulo vamos a presentar, en primer lugar, el concepto de programación
modular. Una vez construido un módulo para resolver una tarea, se puede usar sin más que conocer la
función que realiza y la información que hay que suministrarle para trabajar. De esta forma se alcanza
una abstracción procedimental, de tal manera que los detalles internos (código) del módulo se vuelven
innecesarios para utilizarlo. Íntimamente relacionada con la abstracción procedimental está la abstrac-
ción de datos, que se considerará en la segunda parte del capítulo. Así, se presentarán los tipos abstrac-
tos de datos y, a través de ellos, se introducirán los conceptos de clase y objeto. Como ilustración se
expondrán tres tipos de clases predefinidas en Java: las cadenas, las matrices y las clases contenedoras.

5.2. PROGRAMACIÓN MODULAR


El comienzo de la programación modular se remonta a los primeros tiempos de la programación como
disciplina. De hecho, existía programación modular antes de existir la programación estructurada. La
programación modular aplica el principio de divide-y-vencerás para poder tratar con la complejidad.
Según este principio, cuando abordemos un problema complejo dividámoslo en subproblemas más
pequeños, hasta que estos subproblemas sean fáciles de tratar por separado. En una primera aproxi-
mación, la solución de los subproblemas nos proporciona la solución del problema completo 1.
Aplicado a un programa, este principio nos lleva a dividir el programa en “subprogramas” que rea-
lizan cada una de las tareas necesarias. La aproximación contraria, es decir, construir un único pro-
grama principal monolítico, no es en absoluto eficiente para un desarrollo medianamente complejo. En
la Figura 5.1 se ilustran gráficamente las dos aproximaciones.
Para poder llevar a la práctica la modularización es necesario un mecanismo que permita implan-
tarla en los lenguajes de programación. Este mecanismo existe en todos los lenguajes, y consiste en la
posibilidad de definir tareas específicas como módulos de código independientes del programa prin-
cipal. Lógicamente, estos módulos de código deben poder invocarse desde el principal para que empie-
cen a trabajar, y deben acabar devolviendo el control al principal cuando terminen de ejecutarse.
Vamos a considerar estos bloques de código independiente.

5.2.1. MODULARIZACIÓN FUNCIONAL


Los bloques de código independientes del programa principal que hemos definido tienen siempre
una connotación funcional. Esto quiere decir que estos bloques realizan alguna (en el caso ideal sólo
una) tarea específica. De hecho, cuando definimos estos subprogramas lo que hacemos es identifi-
car tareas específicas y escribir el subprograma para resolverlas.

1
Esta afirmación merece matización. Considerar que la solución de los subproblemas nos da la solución del problema
global es una simplificación reduccionista. Esto es equivalente a suponer que el todo es la suma de las partes. En muchos casos
esto es una muy buena aproximación. Sin embargo, siempre debemos tener en cuenta el efecto de la interacción entre las par-
tes, lo que supone un factor adicional a tener en cuenta.
Abstracción procedimental y de datos 107

Bloque de
sentencias

Programa
principal

Subprograma 1 Subprograma 2 Subprograma 3

a) Programa b) Programa modular


monolítico

Figura 5.1. Esquema estructural de un programa monolítico frente a un programa modular

Estos subprogramas generalizan la noción de un operador. Con los subprogramas se pueden defi-
nir las operaciones necesarias para un trabajo sobre operandos que no tienen por qué ser tipos primi-
tivos. Otra ventaja es que de esta manera se puede encapsular, aislar, un algoritmo, colocando como
una unidad todas las sentencias relevantes para una parte concreta del programa (Aho et al., 1987).
Una ventaja de la encapsulación es que de esta forma sabemos dónde acceder para realizar cambios en
los algoritmos. Siempre lo haremos en esas secciones encapsuladas del problema. Por lo tanto, los pro-
gramas son más fáciles de probar y mantener.
La creación de estos subprogramas implica la realización de una auténtica abstracción procedimental.
El subprograma realiza una tarea y una vez programado lo podemos usar tantas veces como haga falta,
sin más que conocer qué datos necesita para trabajar y qué resultado produce. En otras palabras, después
de crearlo nos podemos abstraer totalmente de su implementación, sólo necesitamos saber cómo se usa.
Otra ventaja de los subprogramas es que nos evitan repetir el mismo código múltiples veces. Ima-
ginemos un subprograma que ordena una serie de números. Si en un problema determinado necesita-
mos ordenar cinco veces, resuelto con un programa monolítico tenemos que repetir cinco veces el
mismo código. En un programa modular haríamos un único subprograma para ordenar y después lo
llamaríamos, invocaríamos, cinco veces sin necesidad de repetir el código.
Estos bloques de código o subprogramas reciben distintas denominaciones en los diferentes len-
guajes. En particular, en los lenguajes no orientados a objetos, la división tradicional distingue entre:
a) Subprogramas, subrutinas o procedimientos, si pueden devolver más de un valor.
b) Funciones, si devuelven un único valor a través de su identificador (como las funciones
matemáticas).
En los lenguajes orientados a objetos los dos conceptos se funden dando lugar a los procedimien-
tos (o métodos) que pueden aplicar los objetos. Dada la orientación a objetos de este texto, hablare-
mos aquí exclusivamente de métodos. Los métodos 2 siempre realizan, aíslan una tarea concreta. Una
definición genérica de método sería:

Un grupo de sentencias de programa identificadas con un nombre.

2
En orientación a objetos, de forma genérica se usa más el término procedimiento que método. Aquí, por claridad, usa-
remos método ya que es el término utilizado en Java.
108 Introducción a la programación con orientación a objetos

En orientación a objetos, un método se asocia con una clase particular. Cada método contiene el
código que se ejecutará cuando el método se invoque. Cuando se llama a un método, el flujo de con-
trol del programa se transfiere al método y se ejecutan una a una las sentencias del mismo. Cuando se
ha ejecutado el método, el control se devuelve a la localización desde donde se hizo la llamada y el
programa continúa en la siguiente sentencia, véase la Figura 5.2.
Como podemos ver, un método siempre debe ser invocado desde algún punto (otro método). Lógi-
camente debe haber un método especial que arranque cuando el programa comience su ejecución. En
Java éste es el método main (principal). El método main comienza a ejecutarse cuando se pone a fun-
cionar el programa y lógicamente no necesita ser invocado desde dentro del propio programa. Es el
sistema operativo, en última instancia, quien “invoca” al método main. Es importante resaltar que un
método puede invocar a su vez a otros métodos.
Un método acepta como entrada una serie de parámetros que le pasa el método que lo invoca y
puede devolver un valor a través de su identificador. Este valor puede ser de un tipo primitivo o un
objeto. Si no devuelve nada, se indica con la palabra reservada void. En Java, la sintaxis de un méto-
do es:

modificador tipo_a_devolver identificador_método(


tipo parámetro_1, ..., tipo parámetro_n){
---- bloque de sentencias ----
}

Los modificadores se considerarán con detalle en el Capítulo 7, cuando se exponga la creación de


clases. De momento baste con saber que para el método main y cualquier otro definido en su misma
clase debemos usar el modificador static, que implica que el método se puede usar aunque no haya-
mos creado un objeto de su clase (el método se usa directamente a través de la clase). En la lista de
parámetros hay que indicar el tipo de cada uno (tipo primitivo o clase, si el parámetro en cuestión es
un objeto). Para invocar un método, en el caso general debemos crear un objeto y luego usar el ope-
rador “.” (punto):

nombre_objeto.método(parámetros);

Esta forma de actuar es la que se utilizaba en capítulos previos cuando, por ejemplo, se invocaba
al método readLine() para leer un dato introducido por el teclado. Sin embargo, cuando usemos un
método dentro de la propia clase en la que está definido no hace falta poner el nombre de un objeto,
sólo invocar el método.

Sentencia 1; Sentencia 1;
Sentencia 2; Sentencia 2;
Ejecutar método; .
Sentencia 3; .
. .
. Sentencia n;
.
Sentencia m;
Método

Programa principal

Figura 5.2. Flujo de control en una llamada a un método


Abstracción procedimental y de datos 109

Los métodos pueden devolver un valor. Por esta razón, es necesario decir de qué tipo es el valor
que se devuelve al ejecutar un método (puede no devolver ninguno). Esto se indica en la cabecera del
método. Por ejemplo en el caso siguiente,

int factorial(int n){


---- bloque de sentencias ----
}

se indica que el método factorial va a devover un entero de tipo int. Si por el contrario el méto-
do no devolviera ningún valor, el tipo de retorno sería void y la cabecera del método sería:

void factorial (int n) {


---- bloque de sentencias ----
}

A su vez, dentro del método debe indicarse qué es lo que éste devuelve, como por ejemplo el
valor de una variable que almacena el resultado final de un algoritmo. Esto se consigue por medio
de la sentencia return. La sentencia return indica lo que se devuelve y puede tomar una de dos
formas,

return;
o
return expresión;

En el primer caso no se devuelve ningún valor, por lo que debe usarse la palabra reservada void
como tipo_a_devolver en la cabecera del método (tipo void). En el segundo, se devuelve el resul-
tado de la expresión. Siempre se debe especificar un tipo en la cabecera del método (void o el del
valor devuelto). La sentencia return hace que el control se devuelva inmediatamente al punto en el
que se invocó el método. Si no hay sentencia return, el proceso continúa hasta que se alcanza el final
del método y entonces se devuelve el control al módulo invocante. Si hay una sentencia return, el
proceso se acaba cuando se ejecuta la sentencia return. Lo normal es que sólo haya una sentencia
return al final del método, aunque en casos excepcionales podríamos encontrar varias.
Vamos a ver un ejemplo de uso de métodos con una variante del Ejercicio 5 propuesto del Capí-
tulo 3, que calculaba el perímetro y la superficie de un círculo, dado un radio. Vamos a modularizar
el problema definiendo dos métodos para estas dos tareas (véase el Programa 5.1).

Programa 5.1. Demostración del uso de métodos. Cálculo del área y del perímetro de un
círculo

class Circulo {
public static void main(String[] args) {
double radio, perimetro, superficie;

// Leyendo el radio
radio=Double.parseDouble(args[0]);
System.out.println(“El radio es: “ +radio+” unidades”);

// Determinando perímetro y superficie


perimetro=calcular_ perimetro(radio);
superficie= calcular_superficie(radio);

// Salida de resultados
System.out.println(“El perimetro es: “ +perimetro
110 Introducción a la programación con orientación a objetos

Programa 5.1. Demostración del uso de métodos. Cálculo del área y del perímetro de un
círculo (continuación)
+” unidades”);
System.out.println(“La superficie es: “ +superficie
+” unidades^2”);

} // Fin método main

public static double calcular_ perimetro(double dato_radio) {


double valor;
valor=2.0*Math.PI*dato_radio;
return valor;

} // Fin método calcular_ perimetro

public static double calcular_superficie(double dato_radio) {


return Math.PI*dato_radio*dato_radio;

} // Fin método calcular_superficie

} // Fin de la clase

En el método main hay dos llamadas a otros métodos, concretamente al método calcular_perime-
tro y calcular_superficie. Como ambos devuelven un valor, en el método main se han declarado
dos variables llamadas perimetro y superficie utilizadas para recoger el resultado que devuelve cada
método. Si no devolvieran nada, no haría falta asignar la salida del método a una variable, bastaría con
invocar al método directamente. Lógicamente, las variables que reciben la salida de un método deben
ser del mismo tipo que el tipo de dato que devuelve el método. En el ejemplo anterior, los dos méto-
dos invocados reciben un parámetro (radio en el método main) cuyo nombre en los métodos es
dato_radio. Los métodos realizan su labor y devuelven el resultado al método main. Es conveniente
recordar que al ser declarados ambos métodos como static no es necesario crear un objeto para invo-
carlos. Indiquemos por último que para el cálculo del área se necesita el valor de la constante š. En
Java dicha constante es pública y está definida en la clase predefinida Math. Por tanto, para usar dicho
valor no tenemos más que escribir Math.PI 3.
La estructura del programa en función de sus módulos se puede representar con los denominados
diagramas de estructura. Los diagramas de estructura son herramientas típicas del denominado diseño
estructurado y forman parte de una completa metodología de trabajo. Estos diagramas son una repre-
sentación en forma de árbol genealógico de la estructura del software, donde los diferentes módulos
se representan como cajas rectangulares. La relación de invocaciones de unos métodos a otros se repre-
senta en estos diagramas por medio de flechas. Para mayor información se remite al lector a textos más
especializados en ingeniería del software o diseño estructurado, véase (Mynatt, 1990; Martin y McClu-
re, 1988). Como ilustración, en la Figura 5.3 se muestra el programa anterior en forma de diagrama de
estructura. Sólo se recoge el simbolismo básico de estos diagramas, con los módulos representados por
bloques conectados definiendo una relación jerárquica. El sentido de las flechas representa el orden de
invocación de los módulos.
En general cada módulo de código debería realizar una sola y concreta tarea. Es decir, todos

3
De acuerdo a la filosofía de orientación a objetos, nunca debemos acceder directamente a los datos de un objeto. El
mismo efecto se puede obtener usando los denominados métodos de consulta o retorno que devuelven los valores de dichos
datos. Toda la interacción con un objeto debe realizarse a través de sus métodos y no directamente a través de sus datos. Sin
embargo, en el caso de constantes (al no poder ser modificadas) a veces se relaja esta recomendación.
Abstracción procedimental y de datos 111

Programa Principal

Calcular_perímetro Calcular_superficie

Figura 5.3. Diagrama de estructura del programa 5.1

los componentes de un módulo (sentencias) deben ir dirigidos a resolver un y solo un problema.


A esta propiedad se la denomina cohesión y se dice que los módulos deben ser cohesivos. El obje-
tivo en el diseño modular es conseguir módulos tan cohesivos como sea posible. Muchas veces
esto no es posible al cien por cien, pero siempre debe pretenderse desarrollar módulos de alta
cohesión.
Cuando se consideran varios módulos de software, y no un solo programa principal, surge el pro-
blema del alcance de las variables. Por tal se entiende la zona del programa donde una variable es acce-
sible. Desde este punto de vista, en un lenguaje de programación las variables pueden ser de dos tipos;
locales o globales. Consideremos cada una de ellas.

a) Variables locales
Las variables locales sólo existen en un ámbito determinado del programa, por ejemplo en un subpro-
grama o en un bloque de sentencias. En Java, ya hemos indicado que las variables sólo son accesibles
dentro del bloque de código en el que están definidas. Según esta regla, las variables declaradas en los
métodos son locales al método en el que se han declarado, no conociéndose fuera de él. Así, si dentro
de un método declaramos una variable con el mismo nombre que otra de otro método no hay ningún
problema pues las variables son distintas, correspondiendo cada una a áreas de memoria diferentes.

b) Variables globales
Por otro lado, las variables globales son las que son accesibles desde cualquier punto del programa y
se pueden usar desde cualquier módulo o subprograma. Esto conlleva la posibilidad de posibles efec-
tos colaterales 4 indeseados, pues la variable puede usarse en cualquier parte del programa (su alcance
engloba todo el programa) y su valor se puede alterar incontroladamente. Si posteriormente es nece-
sario usar la variable en otra parte del programa con su valor original, tendremos un error. El punto
donde se da el error es fácil de localizar, pero no lo es tanto el origen del mismo (la modificación ori-
ginal de la variable). Este tipo de efectos colaterales produce errores cuyo origen es difícil de trazar y

4
El concepto de efecto colateral (en inglés side effect) es ubicuo en programación y va asociado al tipo de problemas
engendrado por variables globales.
112 Introducción a la programación con orientación a objetos

localizar. Por esa razón, se debe evitar el uso de variables globales.


En Java no hay variables globales como tales. Algo parecido a una variable global se puede con-
seguir declarando variables dentro de una clase pero fuera de todos los métodos de la misma 5. La
situación se puede ilustrar con el Programa 5.2.

Programa 5.2. Demostración de variable “global” en Java

class Global {
static int global=1; // Variable global y static porque no se
// crea ningún objeto

public static void main(String[] args) {


metodo_1();
System.out.println(“En main global vale: “ +global);
}

public static void metodo_1() {


System.out.println(“En metodo_1 global vale: “ +global);
}
} // Fin clase

Por la misma razón que algunos métodos se declaran static, las variables globales a nivel de la
clase que contiene el método main deben ser static. En el ejemplo anterior, los dos métodos tienen
acceso a la variable global e imprimirían el mismo valor: 1.
Para finalizar, indiquemos que en programación orientada a objetos se recomienda que la clase que
lleva el método main tenga tan pocos métodos como sea posible, idealmente sólo el método main.
Lógicamente, en los ejemplos que vamos a considerar hasta empezar a crear más de una clase, en el
Capítulo 7, se va a relajar esta recomendación. Sin embargo, se retomará cuando se exponga la pro-
gramación orientada a objetos.

5.2.2. PASO DE PARÁMETROS


Un método puede aceptar información para usarla en su cuerpo de sentencias. Esta información se
suministra en forma de literales, variables u objetos pasados al método a través de su cabecera. Cada
uno de estos elementos de información se denomina parámetro. De manera más formal se puede defi-
nir un parámetro como un valor que se pasa al método cuando éste se invoca. La lista de parámetros
en la cabecera de un método especifica los tipos de los valores que se pasan y los nombres por los cua-
les el método se referirá a los parámetros en la definición del método. En la definición del método, los
nombres de los parámetros aceptados se denominan parámetros formales. En las invocaciones al méto-
do desde algún punto del programa, los valores que se pasan al método se denominan parámetros
actuales 6. Los parámetros actuales y formales no necesitan tener los mismos identificadores, ya que
se corresponden por tipo y posición, véase la Figura 5.4.
Cuando se invoca un método, los parámetros se pasan entre paréntesis. Si no se necesitan pará-
metros se usan paréntesis vacíos. Los parámetros formales son identificadores que actúan como varia-
bles locales al método y cuyo valor inicial se toma de los parámetros actuales. Los parámetros actuales

5
Estrictamente hablando esto no son variables globales, pues su alcance está limitado a la clase en la que están defini-
das, y un programa real en orientación a objetos consta de más de una clase.
6
La denominación de actual es una errónea traducción del inglés donde estos parámetros se denominan actual parame-
ters, literalmente parámetros reales, no actuales.
Abstracción procedimental y de datos 113

Figura 5.4. Correspondencia de parámetros actuales y formales

pueden ser literales, variables o expresiones que se evalúan, y cuyo resultado es el que se pasa al
correspondiente parámetro formal. Por ejemplo, en el Programa 5.1, “radio” es el parámetro actual
ya que es el parámetro que se usa al invocar a los métodos. El parámetro formal es “dato_radio”
que es el que aparece en la cabecera de cada método. En este caso el parámetro formal y el actual tie-
nen distinto nombre, pero no pasaría nada si los nombres fueran iguales. Es importante tener en cuen-
ta que los parámetros actuales y formales que se corresponden por posición deben ser del mismo tipo,
y que debe haber el mismo número de parámetros actuales que formales. Por ejemplo, en la Figura 5.4
tiene que haber tres parámetros formales y tres actuales, y el tipo de actual_1, debe ser el mismo de
formal_1, el de actual_2 igual al de formal_2, y el de actual_3 igual al de formal_3.
En relación con los parámetros, una cuestión clásica, que es necesario conocer en cualquier lengua-
je, es cómo se realiza el paso de los parámetros actuales a los formales. Éste es el problema del paso de
parámetros. Hay dos posibles formas, por valor o por referencia. Consideremos cada una por separado.

a) Paso por valor


En este caso, el método recibe una copia del valor que se le pasa. La variable original (el parámetro actual)
no cambia de valor, independientemente de que en el método se cambie el contenido del parámetro for-
mal. En Java los datos de tipo primitivo se pasan por valor, es decir, que se asigna al parámetro formal una
copia del valor contenido en el parámetro actual. Por ejemplo, si en la clase Circulo (Programa 5.1)
“radio” se inicializa en el método main con el valor 5, al invocar al método calcular_ perimetro el
parámetro “dato_radio” recibe el valor 5. Si también dentro del método calcular_ perimetro se
modificara el valor de “dato_radio” el valor de “radio” no cambiaría.

b) Paso por referencia


En el paso por referencia no se pasa una copia del valor, sino la identificación de la zona de memoria

Parámetro Zona de
actual memoria

Parámetro
formal

Figura 5.5. En un paso por referencia el parámetro formal y el actual devienen en sinónimos
de la misma zona de memoria donde se almacena el dato
114 Introducción a la programación con orientación a objetos

donde se almacena dicho valor, véase la Figura 5.5. Por esa razón, al trabajar dentro del método con
la entidad pasada por referencia estamos manipulando el mismo valor que se utiliza fuera. A efectos
prácticos, si hacemos una modificación de ese valor dentro del método, la modificación se mantiene
al salir del mismo. Obsérvese la diferencia con el paso por valor, donde el método maneja una copia
del valor original, por lo que las modificaciones realizadas dentro del método no afectan a la variable
externa (que de hecho es otra distinta). En Java, los objetos se pasan por referencia, es decir, que el
parámetro formal deviene en alias (sinónimo) del mismo objeto. Veremos un ejemplo cuando acabe-
mos la parte de matrices, puesto que éstas, al ser objetos, se pasan por referencia.

5.2.3. SOBRECARGA DE MÉTODOS


Cuando se compila un programa, cada invocación de un método se enlaza con el código (instruccio-
nes) que lo define. Algunos lenguajes, entre ellos Java, permiten usar el mismo identificador de méto-
do para múltiples métodos. Esta capacidad se denomina sobrecarga de métodos. La distinción entre los
distintos métodos se realiza a través de la lista de parámetros, usando distinto número de ellos o dis-
tintos tipos de datos. Un ejemplo de sobrecarga en Java lo encontramos en el método println que
acepta distintos tipos de parámetros para escribir (enteros, reales, cadenas, boolean...).
El nombre del método junto con el tipo, orden y número de sus parámetros es la “firma” del méto-
do. El tipo de retorno del método no es parte de la firma. Para que haya sobrecarga se debe usar el mis-
mo identificador para el método y el mismo tipo de retorno, pero distinta firma. Veamos un ejemplo.
Sean los siguientes métodos:

int metodo_1(int variable_1)


int metodo_1(double variable_1)
int metodo_1(int variable_1, double variable_2)
int metodo_1(double variable_2, int variable_1)

tenemos cuatro métodos con el mismo identificador (nombre) pero distinta firma, hay por lo tanto
sobrecarga. Sin embargo, en los dos métodos siguientes,

int metodo_1(int variable_1)


double metodo_1(int variable_1)

obtendríamos un error de compilación indicando que un método no puede ser redefinido con un tipo
de retorno diferente (int en el primer caso y double en el segundo).
El compilador usa la firma del método para enlazar la invocación del método con la definición
apropiada. Para usar distintos métodos con el mismo nombre la firma debe ser distinta. Esta técnica es
útil cuando se necesita hacer operaciones similares (la misma tarea) sobre diferentes tipos de datos. Un
ejemplo típico de sobrecarga de métodos que trataremos más adelante es el uso de varios métodos
constructores sobrecargados para inicializar de forma diferente un objeto cuando éste se crea 7.

5.3. TIPOS ABSTRACTOS DE DATOS. CLASES Y OBJETOS


En el apartado anterior hemos considerado la abstracción procedimental, mostrando que es una técni-
ca útil para el desarrollo de software. En este apartado se presenta la abstracción de datos como otra
técnica también de utilidad que es necesario conocer. En particular, se presenta la relación entre el con-

7
En algunos lenguajes (por ejemplo Fortran 90) existe también la sobrecarga de operadores. Así, es posible controlar el
efecto de un operador según actúe sobre un tipo de dato u otro. Java no permite al usuario la sobrecarga de operadores.
Abstracción procedimental y de datos 115

cepto de tipo abstracto de datos y las clases y objetos.

5.3.1. CONCEPTOS GENERALES


Un concepto fundamental en el campo de la programación es el de tipo abstracto de dato. Hasta ahora
hemos encontrado únicamente datos primitivos, tales como el tipo entero o el real. Una generalización del
concepto de dato primitivo es el de estructura de datos. Una estructura de datos es un conjunto de datos
primitivos agrupados juntos en una forma determinada que se pueden manejar como una unidad (Smith,
1987). A su vez podemos definir un concepto más general que es el de tipo abstracto de datos (TAD). Un
tipo abstracto de datos queda definido por una estructura de datos y la serie completa de operaciones que
se pueden realizar sobre un ejemplar de ese TAD. Una nomenclatura usada con respecto a estos concep-
tos (tipos primitivos, estructuras de datos o TAD) es la de ejemplar, para indicar un miembro del tipo (gru-
po) considerado 8. Por ejemplo, las variables de un tipo primitivo determinado serían ejemplares de ese
tipo. En orden creciente de abstracción, la relación entre los datos primitivos, las estructuras de datos y los
tipos abstractos de datos se podría representar gráficamente como muestra la Figura 5.6.
Cuando se trabaja con TAD se pretende que la manera en la que está definido quede oculta para el
usuario del mismo. El usuario crearía ejemplares del TAD y los usaría llamando a los distintos proce-
dimientos o tareas que ese TAD permite. El usuario no necesitaría conocer cuál es la implementación
del TAD (el usuario podría trabajar a mayor nivel de abstracción), sólo necesitaría conocer cómo se usa.
Este mecanismo asegura que no se pueden realizar operaciones incorrectas sobre los ejemplares del
TAD (siempre que esté correctamente definido). Un programa escrito en términos de TAD se puede
transportar a cualquier máquina, en tanto y en cuanto la nueva máquina tenga definido el mismo TAD.
Pues bien, el concepto de la programación orientada a objetos (POO) se fundamenta en el concepto
de TAD. Los objetos son los ejemplares de un determinado TAD. En POO el TAD se define por medio
de lo que se denomina clase. Por lo tanto, la clase se define como un conjunto de datos junto con las ope-

Tipo primitivo Tipo primitivo

Estructura de datos Operaciones

Tipo abstracto de datos

Figura 5.6. Representación gráfica de la relación entre los tipos primitivos, las estructuras y
los tipos abstractos de datos

8
En la literatura en castellano se encuentra frecuentemente el término “instancia” por ejemplar. Ésta es una mala tra-
ducción del término inglés instance que se puede traducir como ejemplar perteneciente a una clase o grupo.
116 Introducción a la programación con orientación a objetos

Ejemplares de la clase A

Clase A Objeto 1 (de la clase A)

Datos (Propiedades)

Objeto 2 (de la clase A)

Procedimientos (Métodos)
Objeto 3 (de la clase A)

Figura 5.7. Relación entre datos, procedimientos, clase y objetos

raciones que se pueden realizar sobre ellos. En la jerga del campo se dice que los objetos encapsulan
datos y procedimientos (métodos). Es importante recalcar que los objetos se definen a través de clases.
La clase es el modelo de los objetos de ese tipo, es como haber definido un nuevo tipo de datos. Una vez
que existe la clase se pueden declarar tantos objetos de esa clase como se necesiten, véase la Figura 5.7.
El programador debe construir las clases que necesite, indicando cuáles son los datos (propie-
dades) que le corresponden y los métodos (procedimientos) que quiere aplicar a esos datos. Lue-
go, en el programa se crearán ejemplares (objetos) de esa clase y se usarán llamando a los
procedimientos que se han incorporado a la clase. Veamos un ejemplo que ayude a concretar los
conceptos teóricos vistos hasta ahora. Definamos genéricamente las propiedades y los procedi-
mientos de una clase automóvil usada para definir las características de los automóviles distribui-
dos por un concesionario. Supongamos que el concesionario sólo necesita conocer la cilindrada,
potencia, modelo, tipo de motor, precio y disponibilidad en el almacén de cada tipo de coche. A su
vez, supongamos que lo que se necesita que el programa haga sea imprimir los datos del coche,
actualizar su precio e indicar si está disponible o no en ese momento. Podríamos organizar una cla-
se Automóvil de la forma siguiente.

Nombre clase:
Automóvil
Propiedades:
Cilindrada
Potencia
Modelo
Tipo_de_motor
Precio
Disponibilidad
Procedimientos:
Imprimir_datos
Actualizar_precio
Determinar_disponibilidad

Aunque en el Capítulo 7 el concepto de clase se tratará en detalle, a modo de ejemplo se presenta


a continuación una posible implementación de la clase Automovil.

class Automovil{

//Características
Abstracción procedimental y de datos 117

int cilindrada, potencia;


String modelo, tipo_de_motor;
double precio;
boolean disponibilidad;

//Métodos
public void imprimir_datos( ){
System.out.println(“El modelo del coche es:”+ modelo);
System.out.println(“El precio del coche es :”+ precio);
}
public void actualizar_ precio( double nuevo_ precio){
precio=nuevo_ precio;
}
public boolean determinar_disponibilidad( ){
return disponibilidad;
}
}//Fin de la clase

Las propiedades y los procedimientos que uno decide incorporar cuando crea una clase dependen
de lo que se persigue con el programa. En el ejemplo anterior el contenido de la clase corresponde a
la descripción realizada del problema del concesionario. Si el programa fuera un juego de carreras de
coches, la clase no sería igual. Por ejemplo, tendríamos métodos como acelerar o frenar.
Al igual que es necesario declarar una variable indicando su tipo para poder usarla, debemos crear
los objetos indicando la clase antes de usarlos. Para crear un objeto la sintaxis en Java es:

Nombre_clase Nombre_objeto= new Método_constructor(parámetros);

El método constructor es un método especial que tiene el mismo nombre que la clase. Por ejem-
plo, un constructor de la clase Automovil tendría la siguiente estructura:

Automovil(tipo parámetro_1,..., tipo parámetro_n){


—bloque de sentencias—
}

Otro constructor de la clase Automovil podría no aceptar parámetros, quedando de la siguiente


forma:

Automovil(){
—bloque de sentencias—
}

Para crear un objeto de la clase Automovil tendríamos que escribir el siguiente código:

Automovil coche1= new Automovil();

donde coche1 es el nombre del objeto que acabamos de crear. Hemos supuesto que el constructor de
la clase Automovil no tiene ningún parámetro. Veamos otro ejemplo. Suponiendo que existe una cla-
se llamada Pieza_de_ajedrez, creemos un objeto llamado caballo:

Pieza_ajedrez caballo=new Pieza_ajedrez();

Ahora existiría el objeto caballo y podríamos usarlo en el programa.


Para invocar un método se usa el operador “.” (punto). Por ejemplo, si queremos invocar al méto-
118 Introducción a la programación con orientación a objetos

do actualizar_ precio de la clase Automovil, usaríamos el objeto coche1 y haríamos:

coche1.actualizar_ precio(precio);

donde precio sería la variable que contendría el valor del nuevo precio.
Un concepto relacionado con el de objeto es el de herencia. Unos objetos se pueden construir a
partir de otros. De esta forma podemos aprovechar las propiedades y procedimientos de clases ya exis-
tentes añadiendo sólo las propiedades y procedimientos específicos de la nueva clase. Supongamos
que tenemos la clase Vehículo (coches, aviones, trenes) que contiene las características comunes a
todos los vehículos, por ejemplo: velocidad máxima. Podemos usar la clase Vehículo para crear o
derivar la clase Automóvil. Así, Automóvil contendría todas las características de un vehículo gené-
rico y nosotros podríamos añadir los detalles específicos que hacen único al automóvil frente a los
otros vehículos. Usando una clase para formar la base de la definición de otra clase, herencia, se pro-
mueve la reutilizabilidad. Ésta consiste en la utilización de bloques de software (los mismos bloques)
en distintos desarrollos, como si fueran piezas intercambiables. De esta manera se agiliza el proceso
de desarrollo de software al poder aprovechar trabajo ya realizado. Objetos, clases y herencia son tres
de los conceptos básicos de la programación orientada a objetos. Todos estos elementos se explicarán
en detalle en capítulos posteriores. El lector interesado puede encontrar información más detallada
sobre estructuras de datos y Java en (Weiss, 1998).
Aparte de definir clases nuevas, en un lenguaje orientado a objetos encontramos algunas clases
predefinidas. En este capítulo vamos a considerar algunas de las que Java posee, tales como la clase
cadena (String), la clase matriz o las clases contenedoras.

5.3.2. CLASE CADENA (String)

En programación es habitual que se necesite manejar cadenas completas de caracteres alfanuméricos.


Por eso, los distintos lenguajes de programación proporcionan algún tipo de dato para el manejo de
cadenas. En algunos lenguajes de programación las cadenas son tipos primitivos de datos. En otros,
las cadenas de caracteres no se representan por tipos primitivos. En Java, por ejemplo, se representan
como objetos de una clase, la clase String, definida en el paquete java.lang. Para crear una cade-
na podemos usar la sintaxis habitual de creación de un objeto, pasando como argumento al método
constructor la cadena deseada, por ejemplo:

String cadena=new String(“Esto es un ejemplo”);

Sin embargo, el uso de las cadenas es tan habitual que existe una forma abreviada de hacer lo mis-
mo que recuerda la declaración de un tipo primitivo:

String cadena=”Esto es un ejemplo”;

Una vez que un objeto String contiene un valor no se puede modificar (acortar, alargar o cam-
biar caracteres). Decimos que un objeto String es inmutable. Sin embargo, lo que sí se puede
hacer es usar métodos de la clase String que pueden devolver nuevas cadenas, resultado de modi-
ficar la cadena original. Para usar algunos de estos métodos es necesario saber que en Java un
carácter en una cadena se refiere por su posición o índice en la cadena. El índice del primer carác-
ter es el 0. Ejemplo:

Esto es un ejemplo <....... Cadena


Abstracción procedimental y de datos 119

Tabla 5.1. Algunos métodos de la clase String de Java

Método Significado
length() Devuelve la longitud de la cadena en caracteres
indexOf(‘carácter’) Devuelve la posición de la primera ocurrencia de ‘carácter’
lastIndexOf(‘carácter’) Devuelve la posición de la última ocurrencia de ‘carácter’
charAt(N1) Devuelve el carácter que está en la posición N1
subString(N1, N2) Devuelve la subcadena comprendida entre las posiciones N1 y (N2-1)
toUpperCase() Devuelve la cadena convertida a mayúsculas
toLowerCase() Devuelve la cadena convertida a minúsculas
equals(“cadena”) Compara dos cadenas y devuelve verdadero si son iguales
equalsIgnoreCase Como equals pero sin considerar mayúsculas y minúsculas
(“cadena”)
valueOf(N1) Convierte el número N1 a cadena

012345678901234567 <....... Numeración

Con esto podemos entender el funcionamiento de algunos métodos útiles de la clase String,
como los recogidos en la Tabla 5.1.
En el Programa 5.3 se expone el uso de algunos de los métodos.

Programa 5.3. Uso de algunos métodos de la clase String

class Cadenas {
public static void main(String[] args) {
String C1=”Cadena 1” ;
String C2=”Cadena 1 “; //Tiene un blanco al final
int N1, N2;

// Determinando la longitud de las cadenas

N1=C1.length();
N2=C2.length();

System.out.println(“Longitud cadena 1: “+N1);


System.out.println(“Longitud cadena 2: “+N2);

// Obteniendo el cuarto carácter

System.out.println(“Caracter en posicion 3 (cuarto): “


+C1.charAt(3));

// Comparación de cadenas

if (C1.equals(C2)) {
System.out.println(“Las dos cadenas son iguales”);
}
else {
System.out.println(“Las dos cadenas no son iguales”);
}

// Conversión de un número a cadena


120 Introducción a la programación con orientación a objetos

Programa 5.3. Uso de algunos métodos de la clase String (continuación)

System.out.println(“Conversion: “+C2.valueOf(420.37));

// Impresión de C2 viendo que no se ha alterado

System.out.println(“C2: “+C2);

// Conversión de un número a cadena usando una variable

N1=342;
System.out.println(“Conversion: “+C2.valueOf(N1));

}
}

El resultado del Programa 5.3 sería:

Longitud cadena 1: 8
Longitud cadena 2: 9
Caracter en posicion 3 (cuarto): e
Las dos cadenas no son iguales
Conversion: 420.37
C2: Cadena 1
Conversion: 342

Observemos que las dos cadenas no son iguales, pues la cadena C2 tiene un blanco al final. El blan-
co es un carácter como cualquier otro, con su correspondiente código Unicode y el sistema lo maneja
como tal.
Las cadenas se pueden unir, concatenar, usando el operador +. El resultado es una nueva cadena
unión de las concatenadas. Algunos operadores de asignación hacen funciones particulares depen-
diendo de los tipos de operandos, como en sus contrapartidas normales (sin el =). En particular, si los
operandos del operador += son cadenas, entonces también se hace una concatenación de cadenas.

5.3.3. CLASE MATRIZ 9


En programación es muy frecuente encontrar una situación en la que hay que manejar muchos ele-
mentos del mismo tipo. Los lenguajes de programación suelen proporcionar una forma de tratar a
todos los elementos como un conjunto, con un identificador para el conjunto que permite mane-
jarlo como un todo. A su vez, es posible tratar los elementos individuales distinguiéndolos por un
índice. La forma de conseguir esto es por medio de una estructura de datos denominada matriz 10.
En programación, una matriz representa una colección de elementos del mismo tipo organizados
de acuerdo a un criterio. Este punto es importante, en una matriz tenemos una organización que se

9
Dada la existencia en Java de una clase Vector, para evitar confusión denominaremos en este texto a las matrices y
vectores ordinarios como matrices o arrays.
10
El término inglés es array que significa disposición determinada. En español el término matriz se generalizó a partir
de los lenguajes, que como Fortran, usaban esta estructura de datos para representar matrices matemáticas. Es también habi-
tual el uso del término tabla. Aquí usaremos el término matriz. En español americano es muy frecuente encontrar el término
arreglo.
Abstracción procedimental y de datos 121

define por uno o varios índices. Dichos índices nos permiten hacer referencia a un elemento con-
creto. En función del número de índices tendremos una estructura mono o multidimensional. Por
ejemplo,

a0, a1, a2, .... <---- caso monodimensional

aij, i=0,N <---- caso bidimensional


j=0,M

aijk, i=0,N
j=0,M ---- caso tridimensional
k=0,L

Como ya hemos indicado, las matrices son, en los lenguajes de programación, estructuras que se
usan para agrupar y organizar datos. Son estructuras simples pero muy potentes. Cuando escribimos
un programa que maneja una gran cantidad de información, tal como una lista de 100 valores enteros,
no es conveniente declarar variables separadas para cada elemento de datos. Si lo hiciéramos, estaría-
mos obligados a manejar las 100 variables independientemente. Las matrices resuelven este problema
permitiendo declarar una entidad (una sola) que puede contener muchos valores. La gran ventaja es
que la matriz se maneja como una unidad.
Como hemos visto, una matriz es una lista organizada de valores. Cada valor se almacena en una
posición específica y numerada de la matriz. El número correspondiente a cada posición viene dado
por un índice. En programación, dependiendo del lenguaje, los índices de las matrices pueden empe-
zar desde dos orígenes:

a) En cero (0-origen).
b) En uno (1-origen).

En Java, en particular, los índices de las matrices empiezan en cero, igual que sucedía con la nume-
ración de los caracteres de una cadena alfanumérica. Esto implica que si una matriz es de tamaño N
los índices van desde 0 a N-1. Por ejemplo,

Elementos: 4 5 6 7 3 2 0
Índice : 0 1 2 3 4 5 6

En Java las matrices son objetos. Los elementos que almacenan pueden ser de cualquier tipo como
int, double, etc., e incluso objetos, pero todos los elementos tienen que ser del mismo tipo. La sin-
taxis para declaración de una matriz monodimensional en Java es:

tipo [] nombre_matriz = new tipo [dimensión_matriz];

donde dimensión_matriz puede ser un valor literal o una variable. Pongamos un ejemplo. La decla-
ración de una matriz de enteros llamada resultados con 20 posiciones se haría de la forma siguien-
te:

int [] resultados = new int [20];

Esta declaración también se puede dividir en dos pasos:

tipo[] nombre_matriz;
nombre_matriz =new tipo [dimensión_matriz];
122 Introducción a la programación con orientación a objetos

En la primera sentencia se declara el identificador de la matriz, pero no se le asigna aún su tamaño


(no se reserva memoria para ella). En la segunda sentencia se asigna el tamaño de la matriz.
En la declaración de matrices los corchetes se pueden colocar detrás del nombre de la matriz de la
forma siguiente:

tipo nombre_matriz [] = new tipo [dimensión_matriz];

Sin embargo, es más legible el primer formato por comparación con otras declaraciones. Por ejem-
plo, con la sentencia,
int n1,n2,n3;

declaramos tres variables de tipo entero. A su vez, con la sentencia,


int [] matriz1,matriz2,matriz3;

declaramos 3 objetos de clase (tipo) matriz de enteros. En los dos casos los tipos se aplican a todas las
variables de la declaración particular. Sin embargo, la segunda alternativa para las matrices, puede lle-
varnos a situaciones confusas. Por ejemplo, la declaración:
int matriz1[], matriz2, matriz3[];

declara matriz1 y matriz3 de tipo matriz y matriz2 de tipo entero, es decir, que en una misma
declaración declaramos entidades de distintos tipos. ¿Ha cometido el programador un error? ¿Por qué
lo ha hecho así? Esas dudas las evitamos usando la primera alternativa.
Una vez que existe la matriz podemos hacer referencia a sus elementos. Esto se hace usando el
índice entre corchetes. Por ejemplo, recordando que los índices de las matrices empiezan en cero, para
referirnos al cuarto elemento de la matriz monodimensional 11 resultados haríamos:
resultados [3]

El Programa 5.4 muestra un ejemplo de manejo de una matriz usando bucles para recorrerla (es la
técnica habitual).

Programa 5.4. Ejemplo del uso de una matriz

class Matrices_1 {
public static void main(String [] args) {
final int LIMITE=5;

int [] lista = new int [LIMITE];

// Inicializando la matriz

for (int i=0; i<LIMITE; i++) {


lista[i]=10*i;
}

// Escribiendo la matriz

11
Las matrices monodimensionales se suelen denominar vectores. Para evitar confusión, aquí no usaremos esta nomen-
clatura pues en Java existe una clase Vector, con sus propiedades específicas.
Abstracción procedimental y de datos 123

for (int i=0; i<LIMITE; i++) {


System.out.print(lista[i]+» “);
}

} // Fin método
} // Fin clase Matrices

Creamos una matriz que puede contener un máximo de 5 elementos, inicializamos esos cinco ele-
mentos con unos valores y luego los escribimos. Fijémonos en que la dimensión se establece con una
constante. Así, para cambiar la dimensión basta con cambiar la constante. Otro punto a comentar es
que puesto que en Java las matrices son 0-origen, en los bucles for el contador no debe pasar del valor
LIMITE-1. Por eso la condición en los for es i menor que LIMITE y no menor o igual. El ejemplo
ilustra un proceso muy habitual, el recorrido de una matriz. El resultado del Programa 5.4 es,
0 10 20 30 40

No debemos hacer nunca referencia a un elemento cuyo índice esté fuera del intervalo de la matriz.
Si dimensionamos una matriz a 10 elementos, sólo podremos referirnos a los elementos cuyos índices
estén comprendidos entre 0 y 9. Si nos referimos al elemento de la posición 10 estamos fuera de los
límites de la matriz y pueden pasar dos cosas:
a) Que el lenguaje que usemos no genere error con lo que el resultado es indefinido.
b) Que el lenguaje detecte el error, como es el caso de Java. El operador [] de Java hace un con-
trol de límites automático, produciendo un error si el índice que usamos está fuera del inter-
valo para el cual se ha declarado la matriz. En este caso, se lanza una excepción
(ArrayOutofBoundsException) que es posible capturar para actuar en consecuencia.
Si las dimensiones se especifican en tiempo de compilación, como en el caso anterior, se reserva la
memoria necesaria para almacenar toda la matriz, aunque en el programa no se usen todos sus elementos.
Por otro lado, en estas circunstancias tendremos que conocer el número máximo de elementos que pue-
den aparecer en condiciones normales y dimensionar la matriz a ese valor. Esta forma de dimensionar en
tiempo de compilación se denomina dimensionamiento estático de matrices. Sin embargo, en Java, al igual
que en muchos lenguajes modernos, la memoria necesaria para cada matriz se puede reservar dinámica-
mente. Esto quiere decir que se puede reservar memoria en tiempo de ejecución, no en tiempo de compi-
lación, por lo que es posible asignar una u otra dimensión en función de una variable leída. En este caso
tenemos el dimensionamiento dinámico de matrices. A pesar de esto, las matrices son de longitud fija. Una
vez creadas (asignada una cantidad de memoria) no se puede modificar su tamaño. En el Programa 5.5 se
presenta una variante del Programa 5.4 en la que la dimensión de la matriz se lee desde el teclado.

Programa 5.5. Ejemplo de manejo de una matriz donde su dimensión se lee desde teclado

import java.io.*;
class Matrices {
public static void main(String [] args) throws IOException {
int limite;
BufferedReader leer =new BufferedReader
(new InputStreamReader(System.in));
// Lectura del tamaño de la matriz

System.out.println(“Introduzca el tamagno de la matriz: “);


limite = Integer.parseInt(leer.readLine());

int [] lista = new int [limite];


// Inicializando y escribiendo la matriz
124 Introducción a la programación con orientación a objetos

for (int i=0; i<limite; i++) {


lista[i]=10*i;
System.out.print(lista[i]+” «);
}

} // Fin método
} // Fin clase

En el ejemplo se observa que cuando se recorre la matriz con el bucle for, la condición de finali-
zación es que la variable contadora, i, llegue hasta el valor limite-1. Esto es lógico, ya que la varia-
ble i representa el índice y si tenemos un número N de elementos, al empezar el índice en 0, el último
valor será N-1. En el ejemplo anterior hemos usado un solo bucle para inicializar la matriz y escribir
el resultado.
En Java podemos conocer en cualquier momento la dimensión de una matriz (el número de ele-
mentos que tiene) usando la constante pública length de la clase matriz. Para ello bastaría indicar
nombre_matriz.length. Así, por ejemplo, para conocer la longitud de la matriz lista del ejem-
plo anterior y salvarlo en una variable entera llamada dimension, haríamos

dimension = lista.length;

Por otro lado, en Java se puede usar una lista para crear e inicializar una matriz. En este caso la
sintaxis es:

tipo [] nombre_matriz = {Lista};

donde lista es una serie de valores separados por comas. Por ejemplo:

int [] valores = {22, 56, 1, 39, 88};

Así, creamos la matriz valores con 5 elementos. Es decir, en la inicialización por lista, la matriz
se dimensiona al número de elementos en la lista. Fijémonos que al crear la matriz usando una lista no
hace falta el operador new.
Una vez introducidas las matrices monodimensionales, podemos entender la cabecera del método
main. Por defecto, en el método main debemos especificar una matriz de clase cadena (String). Es
lo que encontramos habitualmente en la cabecera como: String [] args,

public static void main(String [] args) {


-- - bloque de sentencias - - -
}

Los elementos de la matriz args recogen las cadenas de caracteres que se introduzcan en la línea
de órdenes al invocar el programa. El elemento 0 recoge la primera, 1 la segunda, etc. El Programa 5.6
muestra un ejemplo.

Programa 5.6. Ejemplo de la matriz de argumentos de la línea de órdenes

class Matrices {
public static void main(String [] args) {
int N1, N2;

//Leyendo de la línea de órdenes


Abstracción procedimental y de datos 125

N1=Integer.parseInt(args[0]);
N2=Integer.parseInt(args[1]);

System.out.println(“El primer argumento es: “+N1);


System.out.println(“El segundo argumento es: “+N2);

} // Fin método
} // Fin clase

Si al ejecutar el Programa 5.6 hacemos:

C:> java Matrices 2 3

El resultado sería:

El primer argumento es: 2


El segundo argumento es: 3

Hasta ahora los ejemplos usados han ilustrado el uso de matrices monodimensionales. Sin embar-
go, es posible trabajar con más de una dimensión generando matrices multidimensionales. Todos los
lenguajes permiten de una u otra forma el uso de matrices multidimensionales. Técnicamente hablan-
do Java no las soporta. Sin embargo, de manera práctica sí, puesto que una matriz monodimensional
puede tener una matriz como elemento. Por ejemplo, una matriz cuyo tipo de elemento sea matriz de
enteros es, esencialmente, una matriz bidimensional de enteros. Es decir, en Java las matrices multi-
dimensionales son matrices de matrices.
Veamos la sintaxis de la definición de matrices multidimensionales en Java. Para representar cada
dimensión de la matriz se usan corchetes []. Por ejemplo:

int [][] dosD=new int [4][5];

declara dosD en la práctica como una matriz bidimensional de 4 por 5 elementos. Se puede usar una
lista de inicialización para crear la matriz, donde cada elemento sea a su vez una lista de inicialización
de una matriz. Como cada matriz es un objeto separado, las longitudes de cada fila podrían ser dife-
rentes, como veremos en el siguiente programa. Cada dimensión tiene como índice inicial cero. Ésta
es una forma de crear una matriz en la que una de las dimensiones no tenga siempre el mismo núme-
ro de elementos. Como con las matrices monodimensionales, hay que tener cuidado de no salirse de
los límites de cada dimensión. A tal efecto, es muy útil la constante length que contiene el tamaño
de cada matriz individual.

4 filas
0 1 Esta fila es una matriz de 1 elemento
1 2 3 Esta fila es una matriz de 2 elementos
2 4 5 6 Esta fila es una matriz de 3 elementos
3 7 8 9 10 Esta fila es una matriz de 4 elementos

Vamos a ver un ejemplo de matriz multidimensional inicializada por lista y donde se usa la cons-
tante length para controlar su recorrido. Empecemos por la inicialización de la matriz:
126 Introducción a la programación con orientación a objetos

int [][] tabla ={{1},{2,3},{4,5,6},{7,8,9,10}};

En este caso, tenemos una matriz de matrices. Hay una matriz principal, que siempre es la de las
filas, que tiene 4 filas, y a su vez cada una de las cuatro filas es una matriz de 1, 2, 3 ó 4 elementos,
respectivamente. En forma tabular, lo que tendríamos sería,
Veamos el uso de la matriz tabla en el Programa 5.7. Se trata de crear la matriz anterior, reco-
rrerla fila a fila, imprimirla y sumar los elementos de cada fila imprimiendo también el resultado.

Programa 5.7. Ejemplo de matriz bidimensional

class Matrices {
public static void main(String [] args) {
int [][] tabla ={{1},{2,3},{4,5,6},{7,8,9,10}};
int suma;

// Imprimiendo la matriz
// tabla.length es el número de filas
// tabla[i].length es la longitud de cada fila. Cada fila
// (elemento de la matriz) está compuesta por una matriz

for (int i=0;i<tabla.length;i++) { // Recorriendo


// filas
for (int j=0;j< tabla[i].length;j++) { // Recorriendo
// columnas
System.out.print(tabla[i][j] +” “);
}
System.out.println();
}

// Sumando filas e imprimiendo el resultado

for (int i=0; i<tabla.length;i++) {


suma=0;
for (int j=0; j< tabla[i].length; j++) {
suma+=tabla[i][j];
}
System.out.println(“Fila: “+(i+1)+” Suma: “+suma);
}

} // Fin método
} // Fin clase

Fijémonos en que la primera dimensión corresponde a las filas y la segunda a las columnas. La
constante length da el número de elementos, así que el índice máximo será length-1. Por eso, en
el bucle for el límite se indica con “menor que” length y no con menor e igual. Téngase en cuenta
que tenemos una matriz (filas) de matrices (columnas). Por eso, en el bucle controlado con la variable
j (la de las columnas) usamos tabla[i].length para saber cuántas columnas hay en cada fila. El
resultado sería 1 para i=0, 2 para i=1, 3 para i=2 y 4 para i=3. La salida del Programa 5.7 sería:
1
2 3
4 5 6
7 8 9 10
Abstracción procedimental y de datos 127

Fila: 1 Suma: 1
Fila: 2 Suma: 5
Fila: 3 Suma: 15
Fila: 4 Suma: 34

Para acabar el apartado de matrices y una vez vistas éstas, podemos ilustrar en la práctica el paso
de parámetros por valor y por referencia. Para ello, preparemos un ejemplo (Programa 5.8) con un
método que acepte unas variables y una matriz como parámetros, y que modifique los valores de estos
elementos. Observaremos la diferencia entre el paso por valor (para las variables de tipo primitivo) y
el paso por referencia (para la matriz que es un objeto).

Programa 5.8. Ejemplo de paso de parámetros

class Parametros {
public static void main(String [] args) {
int num1;
double num2;
double [] num3 =new double [1];
num1=1;
num2=4.5;
num3 [0]=5.1;
System.out.println(“Valores originales”);
System.out.println(num1+” “+ num2+” “+ num3[0]);
cambiar(num1,num2,num3);
System.out.println(“Valores tras los cambios”);
System.out.println(num1+” “+ num2+” “+ num3[0]);

} // Fin método main

public static void cambiar(int a, double b, double [] c) {


a=5;
b=7.2;
c[0]=9.1;
System.out.println(“Valores dentro del metodo”);
System.out.println(a+” “+ b+” “+ c[0]);
} // Fin método cambiar

} // Fin clase

La salida del Programa 5.8 sería:

Valores originales
1 4.5 5.1
Valores dentro del metodo
5 7.2 9.1
Valores tras los cambios
1 4.5 9.1

La matriz es un objeto y por lo tanto se pasa por referencia. Por esta razón, lo que se ha cambiado
dentro del método queda cambiado al salir de él. Las variables de tipo primitivo se pasan por valor y
lo que cambiamos dentro del método no afecta a su valor fuera (lo que se pasa al método es una copia

12
En inglés estas clases se denominas wrappers, es decir, envolventes. En castellano se usa el término clase envolvente
o contenedora.
128 Introducción a la programación con orientación a objetos

Tabla 5.2. Clases contenedoras en Java

Tipo primitivo Clase contenedora


byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
chart Character

del valor contenido, no una referencia a la zona de la memoria donde se almacena el valor).

5.3.4. CLASES CONTENEDORAS


Ya hemos comentado que en Java existen unas clases que corresponden a los tipos primitivos (de
hecho, en un lenguaje puramente orientado a objetos lo que existiría serían estas clases y no los tipos
primitivos). Hay una de estas clases para cada tipo primitivo de datos, se denominan clases contene-
doras 12 y contienen métodos relacionados con cada uno de los tipos. Las clases contenedoras se deno-
tan con el nombre completo del tipo considerado comenzando con mayúscula. La sintaxis es la
indicada en la Tabla 5.2.
Las clases contenedoras son útiles cuando se necesita un objeto en lugar de un tipo primitivo. Hay
clases que sólo tratan con objetos, por lo que si se desea almacenar un tipo primitivo en una de ellas
será necesario crear un objeto a partir de su clase contenedora. Los objetos creados a partir de clases
contenedoras se comportan como los objetos tradicionales. Por esa razón, todo el procesamiento sobre
dicho objeto se realiza a través de métodos. Un ejemplo de método útil contenido en estas clases es el
ya visto parseNombre_de_tipo para convertir una cadena (clase String) que contiene un número
a su valor entero o real. Todas estas clases están en el paquete java.lang.
Existen dos constantes que se usan en las clases contenedoras numéricas y que dan el mayor y
menor valor que se puede representar con ese tipo. Estas constantes son MAX_VALUE y
MIN_VALUE. El Programa 5.9 muestra el resultado de utilizar dichas constantes con la clase Float.

Programa 5.9. Ejemplo del uso de las constantes MAX_VALUE y MIN_VALUE de la clase Float

class Ejemplo {
public static void main(String [] args) {
System.out.println(Float.MAX_VALUE);
System.out.println(Float.MIN_VALUE);
}
}

La salida del Programa 5.9 sería:

3.4028235E38
1.4E-45

EJERCICIOS PROPUESTOS
Abstracción procedimental y de datos 129

Ejercicio 1.* Escriba un programa que simule el lanzamiento de un dado con ayu-
da del método random() de la clase Math. Defina un método que
simule el comportamiento del dado.

Ejercicio 2. Como variante del ejercicio anterior escriba un programa que gene-
re combinaciones de la lotería primitiva (6 números enteros elegidos
al azar en el intervalo de 1 a 49).

Ejercicio 3.* Escriba un programa que devuelva el máximo y el mínimo de tres


números introducidos como argumentos por la línea de órdenes.

Ejercicio 4.* Escriba un programa que posea dos métodos con el mismo nombre
para determinar el máximo de dos o de tres números introducidos
como argumentos en la línea de órdenes.

Ejercicio 5.* Escriba un programa que imprima el triángulo de Pascal con el


número de filas introducido como argumento por la línea de órde-
nes. Para ello, utilice la función combinatoria c (n, k) usando la defi-
nición:

n
1 2
c (n,k) 5 n!/(k! (n-k)! ) 5

con k # n

Ejercicio 6. Como complemento del ejercicio anterior escriba un algoritmo que


imprima por pantalla el triángulo de Pascal con su formato correcto.
Sugerencia: Tenga en cuenta en primer lugar el número de filas que
se van a escribir.

Ejercicio 7.* ¿Cuál es el resultado del siguiente programa?

class Ejercicio {
public static void main(String [] args) {
int [] a={0,1,2};
int [] b=a;
b[1]=3;
a=metodo1(a ,b);
for (int i=0; i<a.length;i++)
System.out.print(a[i]+” “+b[i]+” “);
}//fin main

public static int [] metodo1(int [] a, int [] b) {


int [] c={3,4,5};
for (int i=0; i< a.length; i++)
c[i]=a[i]+c[i];
return c;
}//fin metodo 1
}//fin clase

Ejercicio 8.* ¿Cuál es el resultado del siguiente programa?


130 Introducción a la programación con orientación a objetos

class Ejercicio {

public static void main(String [] args) {

int [] Valores1 ={9, 48, 5, 3, 29, 62};

int [] Valores2 ={45, 1, 33};

metodo1(Valores1 [4]);

metodo1(Valores2 [2]);

metodo2(Valores2);

Valores1=metodo3(Valores2);

System.out.print(Valores1[0]);

for (int i=1; i<Valores2.length;i++)

System.out.print(“ “+Valores1[i]);

}
Abstracción procedimental y de datos 131

public static void metodo1(int numero) {


numero = 0;
numero=numero+10;
}

public static void metodo2(int [] lista) {


lista[1]=lista[2];
}

public static int [] metodo3(int [] lista) {


for (int i=0; i<lista.length;i++)
lista[i]=lista[i]+i;
return lista;
}
}

Ejercicio 9.* La matriz transpuesta de una dada se define como aquella que inter-
cambia filas por columnas (el elemento (i, j) pasa a ser el (j, i)). Escri-
ba un método que transponga una matriz cuadrada.

Ejercicio 10.* Escriba un programa que calcule numéricamente el valor de la inte-


gral:

E 0
p

sen (u) d u

aplicando los siguientes métodos:

a) Regla del trapecio.


b) Montecarlo.

Úsese un método (módulo) para cada tipo de cálculo. Imprima el


resultado de ambos métodos.

Ejercicio 11. Se trata de una variante modular del Ejercicio 12 del Capítulo 4.
Escriba un programa que use dos módulos para calcular la media
aritmética y la suma de una serie de valores introducidos por tecla-
do. Sugerencia: Almacene los valores, según se vayan leyendo, en
una matriz y pásela como parámetro a los módulos.

Ejercicio 12.* Sea el siguiente algoritmo propuesto por Euclides en sus Elementos,
libro séptimo, para determinar el máximo común divisor de dos
enteros, n y m, tal que n<m:

a) Tómese el resto del cociente m/n.


b) Si el resto es cero, entonces n es el máximo común divisor.
c) Si el resto es distinto de cero se hace m=n y n=resto.
d) Se vuelve al punto a).

Escriba un programa que acepte dos números enteros leídos por


teclado y determine su máximo común divisor aplicando el algorit-
132 Introducción a la programación con orientación a objetos

mo de Euclides. Escriba un método que aplique el algoritmo.

REFERENCIAS
AHO, A. V., HOPCROFT, J. E. y ULLMAN, J. D.: Data Structures and Algorithms, Addison-Wesley, 1987.
MARTIN, J. y MCCLURE, C.: Structured Techniques. The Basis of CASE, Prentice-Hall, 1988.
MYNATT, B. T.: Software Engineering with Student Project Guidance, Prentice-Hall, 1990.
SMITH, H. E.: Data Structures. Form and Function, Harcourt Brace Jovanovich, Publishers, 1987.
WEISS, M. A.: Data Structures and Problem Solving Using Java, Addison-Wesley, 1998.
6

Recursividad

Sumario

6.1. Introducción 6.4. Recursividad frente a iteración


6.2. Principio de inducción 6.5. Aplicaciones
6.3. Recursividad elemental
6.3.1. Casos base e inductivo
6.3.2. Funcionamiento de la recursividad
6.3.3. Corrección de la recursividad
6.3.4. Recursividad indirecta
134 Introducción a la programación con orientación a objetos

6.1. INTRODUCCIÓN

La recursividad o recursión es una técnica muy potente en la que un método 1 está parcialmente defi-
nido en términos de sí mismo. Esto parece una contradicción si recordamos el aforismo de que el con-
cepto definido no debe formar parte de la definición, para evitar situaciones como la del siguiente
ejemplo. Adorno: aditamento para adornar algo. La idea de recursividad transmite cierta sensación
cíclica, que en términos prácticos se transformaría en una definición tautológica como a=a. Como
veremos, la definición recursiva no es tautológica ya que siempre debe haber diferencia entre el méto-
do que estamos definiendo y la versión del propio método que se usa en la definición. Esto evita el
razonamiento circular.
Cuando se estudia por primera vez, la recursividad puede parecer difícil de entender, esotérica.
Esto es así porque a diferencia de otras técnicas, la recursividad es un concepto no familiar en la
vida diaria que exige una nueva forma de pensar sobre los problemas. Dicho de otra manera, nues-
tra mente no funciona de forma recursiva. La recursividad es una técnica de resolución de proble-
mas complejos que se basa en reducir el problema en subproblemas, los cuales tienen la misma
estructura que el original pero son más fáciles de resolver. La recursividad es una poderosísima
herramienta de programación que permite muchas veces obtener algoritmos cortos y eficientes.
Existen muchos problemas cuya solución más eficiente es recursiva, aunque siempre se debe tener
cuidado en realizar apropiadamente dicha definición para evitar la lógica circular y caer en un ciclo
infinito de llamadas.
Como un ejemplo de definición recursiva tenemos la búsqueda de una palabra en un diccionario.
Abrimos el diccionario por la mitad y si la palabra está en esa hoja el problema está resuelto. Si la pala-
bra está antes, abrimos por la mitad de la sección anterior a la que tenemos y si la palabra está detrás,
abrimos por la mitad de la sección siguiente. Ahora lo que habría que hacer es exactamente lo mismo
que antes, es decir, si la palabra está en la hoja actual se acabó la búsqueda. Si la palabra no está en la
hoja pero alfabéticamente está antes, abrimos en la subsección anterior del diccionario. Si está des-
pués, abrimos en la subsección posterior. El proceso continuaría hasta encontrar la palabra. Desde un
punto de vista formal, la presentación de la recursividad debe comenzar en el principio matemático en
el que se basa: el principio de inducción.

6.2. PRINCIPIO DE INDUCCIÓN

El principio de inducción o inducción matemática es una técnica para probar un teorema (Apostol,
1979), normalmente referido a números enteros. El principio se aplica en dos etapas:

a) Comprobamos que si el teorema se cumple para N (hipótesis inductiva) se cumple también


para N 1 1.
b) Comprobamos que el teorema se cumple para el primer valor posible. Si esto es así, por el
apartado a) será también válido para el segundo valor considerado y al serlo para éste también
lo será para el tercero, etc., lo que completa la demostración.

Veamos un ejemplo. Sea el siguiente teorema:

1
Una vez más debe quedar claro que nos referimos a métodos por coherencia con el lenguaje que estamos usando para
ilustrar las técnicas: Java. En otros lenguajes hablaríamos de funciones, procedimientos o subrutinas.
Recursividad 135

Para todo entero N $ 1, la suma de los N primeros enteros dada por:


N

6 i 5 1 1 2 1 ... 1 N
i 5 1
N(N 1 1)
es igual a }}.
2
Prueba por inducción:

a) Hipótesis inductiva: Consideramos que el teorema es válido para N, es decir, que:

N
N(N 1 1)
6 i 5 }}2
i 5 1

veamos si se cumple para N 1 1. Para ello tenemos que

N 1 1 N

6 i 5 6 i 1 (N 1 1);
i 5 1 i 5 1

con esto

N
N(N 1 1)
6 i 1 (N 1 1) 5 }}2 1 (N 1 1)
i 5 1

reorganizando

N(N 1 1) N(N 1 1) 1 2(N 1 1)


}} 1 (N 1 1) 5 }}} ;
2 2

y sacando factor común (N 1 1)

N(N 1 1) 1 2(N 1 1) (N 1 1)(N 1 2)


}}} 5 }} ;
2 2

con esto,

N 1 1
(N 1 1)(N 1
6 i 5 }2
i 5 1

lo que completa la demostración (considérese N 1 1 5 M para ver la expresión más clara).


136 Introducción a la programación con orientación a objetos

b) Vamos a ver si el teorema se cumple para el primer entero considerado, es decir 1.

6 i 5 1;
i 5 1

con la expresión cerrada para N=1 tendríamos:

1(1 1 1)2
}} 5 }} 5 1
2 2

con lo que se cumple para 1. Por lo tanto por a) también se cumple para 2 y al cumplirse para 2 por a)
también se cumple para 3, etc. Esto completa la prueba del teorema.
Como acabamos de ver, una prueba por inducción trabaja en dos etapas. Existe una en la que se
demuestra que el teorema se cumple para el caso más pequeño, ésta se denomina caso base. Por otro
lado, demostramos que si el teorema se cumple para un caso dado, se puede extender para incluir el
caso siguiente, ésta es la componente inductiva.

6.3. RECURSIVIDAD ELEMENTAL

En este apartado vamos a presentar la racionalización formal de la recursividad basándonos en el prin-


cipio de inducción.

6.3.1. CASOS BASE E INDUCTIVO


La recursividad está relacionada con el principio de inducción. Existe un caso base, en el que no se
realiza ninguna llamada inductiva. Por otro lado, existe un caso o cláusula inductiva (también llama-
do caso recursivo o caso general), en el que se van realizando llamadas a versiones del propio méto-
do. A estas versiones se les van pasando parámetros más simples que los que se usan en la versión del
método actual y que van llevando hacia el caso base. Por tanto, en un método recursivo:

a) Hay que incluir al menos un caso base (puede haber más de uno) que se resuelva sin necesi-
dad de recursividad. El método debe comprobar si debe realizar una nueva llamada recursiva
o si ya se ha alcanzado el caso base.
b) Todas las llamadas recursivas deben llevar hacia el caso base.

Lógicamente el caso base supone el final de las llamadas recursivas. Es la existencia del caso base
y la realización de llamadas recursivas que llevan a él lo que evita que entremos en un ciclo infinito
de llamadas. El caso en el que no se alcanza el caso base se denomina recursividad infinita y es un
error de programación que debe ser evitado.
Para crear un método recursivo es necesaria una definición recursiva del problema. Como ejemplo
veamos el problema de la suma de los n primeros enteros con nŽ1. Denotemos la función suma de los
n enteros como s(n). Así, tendríamos:

Caso base: s(1)=1;


Caso inductivo: s(n) 5 s(n 2 1) 1 n;
Recursividad 137

Se puede observar que el problema está definido de forma recursiva, puesto que para conocer la
suma de n números se debe previamente conocer la suma de los n21 enteros anteriores. En el Progra-
ma 6.1 se muestra un ejemplo donde se implementa el resultado anterior usando un método recursivo.

Programa 6.1. Suma recursiva de los n primeros enteros

class Recursion {
public static void main(String [] args) {
int n, resultado;
n=Integer.parseInt(args[0]);
resultado= s(n);
System.out.println(“Suma de los primeros “+n+” enteros:”);
System.out.println(resultado);
} // Fin método main

public static int s(int n) {


int valor;
if (n==1) // Caso base
valor = 1;
else
valor = s(n-1)+n; // Caso inductivo
return valor;
} // Fin método s
} // Fin clase

En el Programa 6.1 se lee por la línea de órdenes el valor n y se invoca al método recursivo s. En
el método s hay un único caso base que corresponde a n=1. Cuando n es distinto de 1 alcanzamos el
caso inductivo y se vuelve a invocar al método s pero con el argumento n21. Fijémonos en que en un
método recursivo es necesaria una condición para distinguir el caso base del inductivo.
Este mismo ejemplo nos sirve para ilustrar la “robustez” de un algoritmo recursivo. Recordemos
que en nuestro contexto robusto significa capacidad de respuesta a fallos. En el ejemplo usado (Pro-
grama 6.1) vemos una posible fuente de problemas. ¿Qué ocurre si n # 0? En este caso no progresa-
mos hacia el caso base y el método fallaría. Para corregirlo deberíamos comprobar que el número es
mayor o igual que uno, pero esto crea un problema, como podemos observar si programamos de la
siguiente forma el método:

public static int s(int n) {


int valor;
if (n>0) {
if (n==1){ // Caso base
valor = 1;
}
else {
valor = s(n-1)+n; // Caso inductivo
}
}
else {
System.out.println(“El numero no puede ser menor”+
“ que 1”);
valor = -1;
}
return valor;
} // Fin método s
138 Introducción a la programación con orientación a objetos

En el ejemplo, el primer if se comprobaría en cada llamada al método usando el valor -1 como


valor centinela, para indicar que la entrada de datos es errónea. Sin embargo, si es el propio método
recursivo el que comprueba con una condición (un if) si n 0, la condición se va a probar en todas las
llamadas del método, aunque basta con probar dicha condición la primera vez que se invoca el méto-
do para saber si se cumple o no. Esta situación es muy normal con los métodos recursivos y la solu-
ción consiste en usar un driver 2 donde se comprueba la condición y desde donde luego se invoca al
método recursivo. Dependiendo del caso, el driver será un método nuevo o un fragmento de código
nuevo inserto en el módulo que haya realizado la llamada al método recursivo. En el ejemplo anterior,
si usamos un método nuevo como driver tendríamos el Programa 6.2.

Programa 6.2. Uso de un driver para el método recursivo que suma los N primeros números

class Recursion {
public static void main(String [] args) {
int n;
n=Integer.parseInt(args[0]);
suma(n);
} // Fin método main

// Método driver
public static void suma(int n) {
if (n>0) {
System.out.println(“Suma de los primeros “+n+” enteros:”);
System.out.println(s(n));
}
else
System.out.println(“El numero no puede ser <1”);
} // Fin método suma

public static int s(int n) {


int valor;
if (n==1) // Caso base
valor = 1;
else
valor = s(n-1)+n; // Caso inductivo
return valor;

} // Fin método s

} // Fin clase

Como vemos, ahora desde el método main se invoca al método suma que se encarga de todo el
proceso de comprobación y que es el que invoca al método recursivo s.
Como puede observarse en los ejemplos anteriores la sintaxis de un método recursivo es sencilla.
La pregunta, sin embargo, es ¿cómo se implementa la recursividad en el ordenador? En otras palabras,
¿cómo funciona la recursividad?

2
El concepto de driver es muy común en programación. Un driver es un fragmento de código cuya misión es dirigir o
conducir (to drive en inglés) el funcionamiento de otro fragmento de código. Los driver son normalmente módulos de códi-
go con entidad propia. En este caso, el driver sería un método al que invocaríamos y desde el que se invocaría el método recur-
sivo.
Recursividad 139

6.3.2. FUNCIONAMIENTO DE LA RECURSIVIDAD


Vamos a utilizar el problema del factorial para explicar el funcionamiento e implementación del pro-
ceso recursivo. Para calcular recursivamente el factorial de un número necesitamos indicar cuál es el
caso base y cuál es el caso inductivo. El caso base debe ser el caso más simple de calcular. El facto-
rial de 0 es 1, éste es el caso más simple que se nos puede dar, por lo que:

Caso base: fact (0) 5 1

El factorial de cualquier otro número mayor que 0 se puede definir recursivamente como el pro-
ducto del número por el factorial del número anterior. Éste sería el caso inductivo:

Caso inductivo: fact(n) 5 fact (n 2 1) ? n

En el Programa 6.3 se muestra dicho cálculo.

Programa 6.3. Cálculo recursivo del factorial de n.

class Factorial_recursivo {
public static void main(String [] args) {
int n;

System.out.println(“Calculo recursivo del factorial\n”);


n=Integer.parseInt(args[0]);

if (n >=0) {
System.out.println(“El factorial de “+n +” es:”);
System.out.println(factorial(n));
}
else
System.out.println(“No se puede evaluar el factorial”
+” si n<0”);
} // Fin del main

public static long factorial(int n) {


long nFact;
if (n==0){
nFact = 1;
}
else{
nFact = factorial(n-1)*n;
}
return nFact;

} // Fin método factorial


} // Fin de la clase

Se ha usado tipo de retorno long en factorial porque el valor del factorial aumenta muy rápi-
damente y un int se queda corto enseguida.
Para entender cómo funciona la recursividad es necesario tener bien claro que en memoria no va
a existir una sola versión del método recursivo. Cada vez que se invoque el método se crea una nue-
va versión del mismo. La estructura de todas las versiones es la misma, pero no así los datos que con-
140 Introducción a la programación con orientación a objetos

tiene cada una. Con esta idea básica, analicemos lo que ocurre en el programa. Supongamos que se
invoca el programa Factorial_recursivo con un argumento n=3. Como n en este caso es mayor
que cero se escribiría por pantalla la frase “El factorial de 3 es: “ y se llamaría al método
recursivo factorial tomando n el valor de 3. Dentro del método se comprueba que n no es igual a 0,
por lo que se realiza otra llamada al método factorial. En este caso el argumento de factorial va
a valer n-1, es decir, 2. Otra vez se realiza la comprobación n==0 y se vuelve a llamar al método fac-
torial con
n-1, que en esta ocasión equivale a 1. Se vuelve a realizar la misma operación, y se vuelve a invocar
al método factorial con el valor n-1 que ahora sería 0. Al hacer factorial(0) el método devuel-
ve 1, ya que entra en el caso base, terminando así el ciclo de llamadas recursivas y produciéndose una
vuelta atrás que va recogiendo los valores que van devolviendo todos los métodos que han sido invo-
cados. En el diagrama de la Figura 6.1 se ilustra cómo se van realizando las llamadas recursivas (línea
continua) y como, una vez terminadas éstas, se realiza la devolución de valores (línea de puntos).
El ejemplo de la Figura 6.1 muestra cómo las llamadas recursivas se van produciendo hasta alcan-
zar el caso base. En ese punto se acaban las llamadas y empiezan las devoluciones de valores hasta lle-
gar al método main, que fue quien hizo la primera invocación del método. Tenemos un movimiento
en dos sentidos. Primero hacia adelante hasta alcanzar el caso base. Segundo hacia atrás devolviendo
los resultados de cada llamada al método. Las llamadas realizadas implican una estructura de pila.
Organizándolo en forma tabular obtendríamos el resultado mostrado en la Tabla 6.1.

Main
n=3 devuelve 2 * 3 = 6
factorial (2) * 3
factorial
n=2 devuelve 1 * 2 = 2
factorial (1) * 2
factorial
n=1 devuelve 1 * 1 = 1
factorial (0) * 1
factorial
n=0 devuelve 1 = factorial (0)
factorial

Figura 6.1. Ilustración del funcionamiento interno de las llamadas recursivas

Tabla 6.1. Análisis de la pila de llama-

IDA VUELTA
Llamada n Valor
a
1. 3 6
2.a 2 2
3.a 1 1
4.a (Caso base) 0 1
Recursividad 141

En cada llamada se realiza una copia del método recursivo (hablando de manera correcta del espa-
cio de datos). Esto es importante, porque cada llamada implica una nueva copia de las variables del
método. Esto consume memoria y si tuviéramos un problema de recursividad infinita agotaríamos la
memoria disponible y obtendríamos un error.

6.3.3. CORRECCIÓN DE LA RECURSIVIDAD


Una consecuencia evidente de la exposición del apartado anterior es que no podemos determinar si un
algoritmo recursivo es correcto o no visualizando in mente el conjunto de llamadas recursivas reali-
zadas. Imaginemos, por ejemplo, la complejidad de la visualización de lo que ocurre en el método del
factorial para un valor n, arbitrario. Sin embargo, es necesario poder determinar si un algoritmo recur-
sivo es o no correcto, es decir, si funciona de forma adecuada. Podemos alcanzar este objetivo con ayu-
da del principio de inducción (Weiss, 1998) de la siguiente forma. Supongamos un algoritmo recursivo
genérico con un caso base y un caso inductivo,

Inicio
Recibir valor de la variable

Si (valor más pequeño)entonces


Caso base
Si_no
Invocar al método con un valor menor al inicial
Fin_Si

Proporcionar resultado
Fin

Caso base
Si entramos en el caso base, entonces no se hacen llamadas recursivas. El caso base suele ser el caso
más sencillo. Por ejemplo, en el caso del factorial es n=0. En el algoritmo, el caso base devolvería el
resultado correcto para el valor más pequeño. Este punto es importante, el caso base debe proporcio-
nar el resultado correcto para el caso más sencillo del problema que estamos considerando.

Hipótesis inductiva
Supongamos que el algoritmo recursivo funciona para todos los valores mayores que el del caso base
(en el caso del factorial n>0). Esto implica que el algoritmo recursivo funcionará correctamente para
cualquier valor (por ejemplo, en el factorial para un valor n). Como el valor suministrado implica la
llamada recursiva, se volverá a aplicar el algoritmo recursivo sobre un valor menor que el original (en
el caso del factorial se llama al algoritmo con un valor n-1). Sin embargo, como la parte inductiva
funciona para cualquier valor mayor que el caso base también funcionará para el nuevo valor. Si esto
es así, también funcionará para el siguiente valor, y el siguiente, etc. hasta llegar al caso base.
Lógicamente, en las condiciones anteriores el principio de inducción nos asegura que la solución
recursiva para un caso arbitrario, general, es correcta. No es necesario representar la pila de llamadas
que se vayan haciendo para asegurar la validez de un algoritmo recursivo. A pesar de todo, es impor-
tante recalcar que este resultado no nos asegura que el algoritmo recursivo que estemos usando sea el
que resuelve el problema. Por ejemplo, si quiero calcular un factorial y utilizo un algoritmo recursivo
142 Introducción a la programación con orientación a objetos

para sumar n números, no estoy resolviendo el problema que me interesa. Lo importante de la demos-
tración anterior es que asegura que para diseñar un algoritmo recursivo que resuelva un problema sólo
hay que hacer dos cosas. Primero, identificar el caso base de mi problema, y segundo, identificar cómo
expresar mi problema para un tamaño dado (en el ejemplo del factorial para un entero n) como función
del mismo problema para un tamaño menor (en el factorial n21). Con sólo hacer esto el principio de
inducción me asegura que el algoritmo es correcto y que resolverá mi problema en el caso general.

6.3.4. RECURSIVIDAD INDIRECTA


La recursividad directa se da cuando un método se llama a sí mismo, tal como el factorial que llama al
propio factorial. La recursividad indirecta ocurre cuando un método llama a otro método, que invoca a
otro, etc., terminando finalmente en que el método original se invoca otra vez. Por ejemplo, si el méto-
do m1 llama al método m2, el cual llama al método m3, el cual llama a m1 nuevamente, decimos que m1
es recursivo indirectamente. La “indirección” puede tener varios niveles de profundidad, por ejemplo
m1 invoca a m2, el cual invoca a m3, el cual invoca a m4, que invoca a m1. En la Figura 6.2 se presen-
ta un ejemplo de recursividad indirecta. Las invocaciones a los métodos se marcan con líneas continuas,
y las devoluciones con líneas discontinuas.

ida
método 1 método 2 Llamada recursiva
vuelta
ida
ida
método 1 método 2 Llamada recursiva
vuelta
ida vuelta
ida
método 1 método 2 Caso base
vuelta

Figura 6.2. Esquema de llamadas recursivas en un caso de recursividad indirecta.


Las líneas continuas representan las llamadas hasta llegar al caso base. Las líneas
discontinuas representan el camino de vuelta desde el caso base
Al igual que en la recursividad directa, se sigue el camino completo de invocaciones hasta el caso
base. En ese momento, se van “deshaciendo” a la inversa la serie de llamadas recursivas. La recursi-
vidad indirecta requiere que se ponga la misma atención a los casos base e inductivo que la directa.
Sin embargo, es más complicada de seguir (trazar) y de depurar. El Programa 6.4 muestra un ejemplo
de recursividad indirecta.

Programa 6.4. Programa ilustración de recursividad indirecta

class Indirecta {
public static void main(String [] args) {
int n=6;
metodo1(n);
}
public static void metodo1(int n) {
Recursividad 143

Programa 6.4. Programa ilustración de recursividad indirecta (continuación)

// El caso base es n=0


if (n == 0) {
System.out.println(“En metodo1 con N: “ +n);
}
else { // Caso inductivo
metodo2(n);
}
}
public static void metodo2(int n) {
System.out.println(“En metodo2 con N: “ +n);
metodo3(n-1);
}
public static void metodo3(int n) {
System.out.println(“En metodo3 con N: “ +n);
metodo1(n-1);
}
}

El resultado sería,
En metodo2 con N: 6
En metodo3 con N: 5
En metodo2 con N: 4
En metodo3 con N: 3
En metodo2 con N: 2
En metodo3 con N: 1
En metodo1 con N: 0

En el Programa 6.4 hay un método1 que recibe un valor entero y lo pasa a un metodo2 que lo
transmite a metodo3 quien vuelve a invocar a metodo1. El caso base se encuentra en metodo1 y
corresponde al valor n 5 0. Obsérvese que si n es par se alcanza el caso base. Sin embargo, si n es
impar se entraría en un caso de recursividad infinita, pues nos saltaríamos el caso n 5 0. Aunque éste
es un caso sencillo obsérvese que la existencia de la recursividad no es evidente en metodo1. Es nece-
sario conocer cuál es la secuencia completa de llamadas realizadas a los métodos para poder seguir y
depurar, por ejemplo, el problema de recursividad infinita anteriormente mencionado.

6.4. RECURSIVIDAD FRENTE A ITERACIÓN


En este apartado se presenta una comparación entre la iteración y la recursividad. Se indican las carac-
terísticas de ambas y las ventajas y desventajas de cada una de ellas.

Características comunes
a) Tanto la iteración como la recursividad implican repetición. La iteración usa explícitamente
una estructura de repetición mientras que la recursión logra la repetición mediante llamadas
sucesivas al propio método.
b) Tanto la iteración como la recursividad requieren una prueba de terminación. La iteración ter-
144 Introducción a la programación con orientación a objetos

mina cuando deja de cumplirse la condición para terminar el ciclo y la recursión termina cuan-
do se reconoce un caso base.
c) Tanto la iteración con repetición controlada por contador como la recursividad, se aproximan
gradualmente a la terminación. La iteración continúa modificando un contador hasta que éste
adquiere un valor que hace que deje de cumplirse la condición de continuación del ciclo. Por
otro lado, la recursividad sigue produciendo versiones más sencillas del problema original
hasta llegar al caso base.
d) Tanto la iteración como la recursividad pueden continuar indefinidamente. En la iteración
ocurre un ciclo infinito si la prueba para continuar el ciclo nunca deja de cumplirse. A su vez,
tenemos recursión infinita si cada llamada recursiva no simplifica el problema llevándonos
hacia el caso base o si, aún dirigiendonos al caso base, lo saltamos.

Diferencias
La recursividad presenta una desventaja frente a la iteración: la invocación repetida del método. Por
tanto, se incurre en el gasto extra de las llamadas necesarias. Esto puede ser costoso tanto en tiempo
de procesador como en gasto de memoria. Cada llamada recursiva hace que se cree otra copia del
método (en realidad sólo de las variables del método). Esto puede consumir una cantidad considera-
ble de memoria. La iteración normalmente ocurre en el mismo método, por lo que se omite el gasto
extra de las llamadas a método y de la asignación adicional de memoria. La diferencia en eficiencia
suele depender de cuánto crezca la pila de datos en el caso recursivo. Además, en principio, toda tarea
que pueda realizarse con recursividad puede también hacerse de otra manera. Siempre hay una solu-
ción iterativa para cualquier problema recursivo. Si esto es así, ¿por qué escoger la recursividad? Se
escoge la recursividad cuando el enfoque recursivo refleja de forma más natural el problema y produ-
ce un programa más fácil de entender y depurar. Otra razón para escoger una solución recursiva es
cuando una solución iterativa no es viable.
La equivalencia de recursividad e iteración se puede ilustrar con un ejemplo. Consideremos el caso
de la suma recursiva de 1 a n mostrada en el Programa 6.1. Se puede resolver el mismo problema ite-
rativamente de la siguiente forma:

suma =0;
for (int numero=1; numero<=n; numero++){
suma+=numero;
}

Esta solución es más clara que la recursiva. Este ejemplo se usó para exponer la recursión porque
es un caso muy sencillo, no porque se use la recursividad en condiciones normales para resolver este
tipo de problemas. Como hemos indicado, la recursividad tiene el coste de múltiples invocaciones a
un método, y en este caso presenta una solución más complicada que su equivalente iterativa. Como
ejemplo de cómo convertir una solución recursiva en una iterativa veamos el cálculo iterativo del fac-
torial ejemplificado en el Programa 6.5.

Programa 6.5. Ilustración de la evaluación iterativa del factorial

class Factorial_iterativo {
public static void main(String[] args) {
int n=0;
System.out.println( “Calculo iterativo del Factorial” ) ;
Recursividad 145

Programa 6.5. Ilustración de la evaluación iterativa del factorial (continuación)


n=Integer.parseInt(args[0]);
if (n>=0)
System.out.println( n + “ ! = “ +factorial(n) ) ;
else
System.out.println(“No se puede evaluar el factorial”
+” si n<0”);

} // método main

public static long factorial(int numero) {


long nFact = 1;
for (int i = numero ; i > 1 ; i-- ) {
nFact = nFact * i ;
}
return nFact ;

} // Fin método factorial

} // Fin clase Factorial_iterativo

Podemos decir que la recursividad es la mejor manera de resolver algunos problemas, pero para
otros es más complicada que la solución iterativa. Un programador debe evaluar cada situación para
determinar la aproximación adecuada dependiendo del problema a resolver. Todos los problemas se
pueden resolver de manera iterativa, pero en algunos casos la versión iterativa es mucho más compli-
cada. La recursividad en ciertas ocasiones permite crear soluciones relativamente cortas y elegantes.
Por último, es conveniente comentar que no todos los lenguajes de programación permiten la recursi-
vidad, como por ejemplo Cobol o Fortran 77.

6.5. APLICACIONES
La recursividad representa un papel muy importante en el diseño de distintos tipos de algoritmos como
son los de:

a) Divide y vencerás.
b) Programación dinámica.
c) Backtracking (vuelta atrás).

El tratamiento de estos tipos de algoritmos cae fuera del alcance de este texto. A tal efecto con-
sulténse las referencias (Weiss, 1998; Aho et al., 1987; Brassard y Bratley, 1997). Sin embargo, como
ilustración de la potencia de la recursividad vamos a ilustrar un ejemplo de algoritmo de backtracking
utilizado para la resolución de laberintos. Para ello, comencemos presentando brevemente en qué con-
sisten los algoritmos de backtracking.
A veces, nos enfrentamos con la tarea de encontrar la solución de un problema, pero no hay nin-
guna teoría aplicable que nos ayude a encontrar la solución óptima sin recurrir a una búsqueda exhaus-
tiva. Pues bien, es posible realizar una búsqueda exhaustiva usando la técnica denominada
backtracking (vuelta atrás). En el backtracking usamos la recursividad para probar sistemáticamente
todas las posibilidades. El backtracking se utiliza para crear programas que realicen juegos de estrate-
gia, desde las tres en raya hasta el ajedrez. Vamos a aplicar esta técnica a un problema clásico, la reso-
lución de un laberinto.
146 Introducción a la programación con orientación a objetos

Resolver un laberinto involucra un tratamiento de prueba y error. Así, debemos seleccionar una
dirección, después seguir ese camino y retornar al punto anterior si en algún momento no se puede
continuar y probar otras direcciones. Un laberinto puede resolverse si se realiza una exploración sis-
temática del mismo y a tal efecto puede utilizarse un backtracking. Como ilustración de la técnica y
del papel que representa la recursividad, abordemos la resolución genérica de un laberinto en dos
dimensiones.
En primer lugar establezcamos las reglas que definen el laberinto (esto corresponde a la etapa de
análisis). El laberinto consistirá en un rectángulo de N posiciones horizontales y M posiciones verti-
cales. El símbolo ‘*’ marcará una posición prohibida (una pared) y un blanco indicará una posición
accesible. Según se vayan probando caminos, cada posición probada se marcará con una ‘x’. El cami-
no de salida del laberinto debe indicarse con una serie de puntos, ‘.’. La posición de salida se marca
con una ‘s’. Los movimientos permitidos en el laberinto son los de la torre de ajedrez (arriba, abajo,
derecha e izquierda) pero limitados a una sola casilla cada vez. Se comienza la búsqueda en una casi-
lla libre identificada por sus coordenadas (índices), no se puede salir de los límites del laberinto, y la
búsqueda concluye cuando se encuentra la salida o se comprueba que no la hay.
Determinemos ahora cómo se resuelve el problema (etapa de diseño). Como no existe ninguna for-
ma de deducir cuál es la solución de un laberinto, realizaremos una exploración sistemática del mis-
mo. Así, si hay salida la encontraremos y si no la hay estaremos seguros de ello. A tal efecto,
diseñaremos un algoritmo recursivo que por un lado identifique si hemos acabado la exploración y por
otro realice la exploración en las cuatro direcciones de movimiento permitidas. De esta manera defi-
nimos un caso base (unos en realidad) y un caso inductivo. Para el caso inductivo basta con indicar la
estrategia para hacer un movimiento. Como vimos en el Apartado 6.3.3 el principio de inducción ase-
gura que la solución es correcta.

a) Casos base
Identifiquemos como tales todas aquellas situaciones que implican que se deja de aplicar en ese
momento la exploración sistemática (se corta una línea de llamadas recursivas). Así tendremos:

— Hemos alcanzado la salida.


— Intentamos movernos fuera de los límites del laberinto.
— Ya hemos pasado por esa posición.

b) Caso inductivo
Indiquemos cómo realizar la exploración sistemática. Lo que haremos será, desde la casilla en la que
estemos, movernos hacia arriba. Si no se puede, intentaremos ir hacia abajo. Si tampoco es posible,
iremos a la derecha y si esto tampoco es posible intentaremos ir a la izquierda. Para simplificar el algo-
ritmo recursivo colocaremos en un método aparte la comprobación de un movimiento válido. Como
tal entenderemos el que no se sale de los límites de la matriz y que alcanza una casilla vacía o la casi-
lla final.
El laberinto se definirá en el método main como una matriz NxM de caracteres. En el Progra-
ma 6.6 se presenta un ejemplo de backtracking para resolver laberintos genéricos.

Programa 6.6. Programa que aplica un backtracking para resolver un laberinto bidimensional

class Laberinto {
public static void main(String [] args) {
Recursividad 147

Programa 6.6. Programa que aplica un backtracking para resolver un laberinto bidimensional
(continuación)
int i_x=0, i_ y=3; // Punto de salida
char [][] labe= {{‘*’,’*’,’*’,’ ‘,’*’},
{‘ ‘,’ ‘,’ ‘,’ ‘,’*’},
{‘*’,’*’,’ ‘,’ ‘,’ ‘},
{‘*’,’*’,’ ‘,’ ‘,’*’},
{‘*’,’*’,’s’,’*’,’*’}};
// Imprimiendo laberinto inicial
imprime(labe);
// Se intenta resolver el laberinto
if (resolver(labe, i_x, i_ y)) {
System.out.println(“Laberinto resuelto”);
imprime(labe);
}
else {
System.out.println(“El laberinto no tiene solucion”);
imprime(labe);
}
} // Fin método main

public static void imprime(char [][] labe){


int N=labe.length;
int M=labe[0].length;

System.out.println();
for (int i=0; i<N; i++) {
for (int j=0; j<M; j++) {
System.out.print(labe[i][j]);
}
System.out.println();
}
System.out.println();
} // Fin método imprime

public static boolean valido(char [][] labe, int x, int y)


int N=labe.length;
int M=labe[0].length;
boolean ok=false;

if (x<N && x>=0 && y<M && y>=0 &&


(labe[x][y]==’ ‘ || labe[x][y] == ‘s’)) {
ok=true; // Movimiento válido
}
return ok;
} // Fin método válido

public static boolean resolver(char [][] labe, int x, int y) {


boolean fin=false;
if (valido(labe,x,y)) {
if (labe[x][y] == ‘s’) {
fin =true;
148 Introducción a la programación con orientación a objetos

Programa 6.6. Programa que aplica un backtracking para resolver un laberinto bidimensional
(continuación)
}
else {
labe[x][y]=’x’; // Se marca la casilla
fin=resolver(labe, x-1, y); // Arriba
if (!fin) {
fin=resolver(labe, x+1, y); // Abajo
}
if (!fin) {
fin=resolver(labe, x, y+1); // Derecha
}
if (!fin) {
fin=resolver(labe, x, y-1); // Izquierda
}
} // Fin del segundo if
if (fin) {
labe[x][y] = ‘.’;
}
} // Fin del primer if
return fin;
} // Fin método resuelve

La salida del Programa 6.6 es:

*****
*****
*****
*****
**s**

Laberinto resuelto

***.*
xxx.*
**x.*
**..*
**.**

Observemos que a partir de la llamada inicial comienza la exploración del laberinto en el orden
arriba, abajo, derecha e izquierda. Cada vez que se llega a una posición prohibida se corta ahí la línea
recursiva y se va hacia atrás (backtracking) en la pila de llamadas. Esta vuelta atrás se va realizando
hasta que se llega a una casilla desde la que hay un movimiento válido, momento en el que comienza
otra serie de llamadas recursivas. En el momento en que lleguemos a la casilla de salida acaba el pro-
ceso recursivo y se inicia la vuelta atrás escribiendo un punto en cada posición hasta que se deshace
la serie completa de llamadas y se llega a la casilla inicial. Es importante notar que si el laberinto tie-
ne varias salidas, se dará como solución la primera que se encuentre ya que no se aplica ningún crite-
rio que implique la optimización de la solución.
Recursividad 149

Un tipo de problema relacionado con el backtracking y donde la recursividad encuentra aplicación


es el asociado a los juegos de estrategia, donde se aplica una aproximación prospectiva (lookahead). En
los juegos de estrategia el problema es parecido al de resolver un laberinto. Aquí, tenemos movimien-
tos que nos pueden conducir a la victoria o a la derrota y cada movimiento supone una elección que nos
lleva por un camino u otro. Un programa que juegue un juego de estrategia podía pensarse como uno
que pudiera moverse sobre ese “laberinto” de posibilidades buscando la “salida”, en este caso un movi-
miento que corresponda a un camino ganador. Este matiz es importante, intentamos elegir un movi-
miento que, desde el punto actual del juego, esté en un camino que lleve a la victoria. Esto no quiere
decir que haciendo ese movimiento se gane seguro. Por ejemplo, si tengo que elegir entre dos posibles
movimientos uno que está en un camino ganador y otro que no está en ningún camino ganador, el pro-
grama elegiría el primero. Podemos ilustrar la aplicación de esta aproximación con un ejemplo típico,
el del juego del Nim. En este juego tenemos una serie de fichas colocadas en filas. Hay tantas filas como
se deseen, con una ficha en la primera, dos en la segunda, tres en la tercera, etc., véase la Figura 6.3.

Fila 1

Fila 2

Fila 3

Fila 4

Figura 6.3. Tablero del Nim con cuatro filas

Las reglas del juego son muy simples. Hay dos contrincantes que juegan por turnos. En cada
movimiento se pueden quitar tantas fichas de una sola fila como se quiera, y gana el jugador que deja
el tablero vacío. Por ejemplo, si nos quedan dos filas con tres fichas, una en la primera fila y dos en
la segunda y nos toca jugar tomaríamos una sola ficha de la fila con dos. Así, nuestro contrincante
sólo puede coger una ficha de alguna de las dos filas que quedan (sólo hay una ficha en cada una y
no se pueden coger fichas de más de una fila). El resultado es que queda una fila con una ficha que
cogeríamos nosotros, dejando el tablero vacío y ganando la partida.
Con la idea de la existencia de jugadas buenas y malas podemos diseñar un algoritmo recursivo
que juegue al Nim. El caso base sería que el tablero estuviera vacío y el caso inductivo se organiza con
dos tareas. Una de ellas determina un buen movimiento como uno que es malo para el contrario y la
otra indica si un movimiento concreto es malo porque no es un buen movimiento. Con esta definición
ya queda implícita la naturaleza recursiva de la solución. La estrategia más simple es explorar todas
las posibles jugadas, localizando la primera que sea buena. Si no se encuentra ninguna que sea buena,
se indicará de alguna forma, en nuestro caso con un valor centinela. Representaremos el tablero como
una matriz con tantos elementos como filas, almacenando en cada elemento el número de fichas en la
fila. El algoritmo recursivo (en pseudocódigo) para encontrar un buen movimiento sería:

Inicio
seguiriverdadero
Para ii0 mientras i<numero_filas y seguir incremento iii+1
150 Introducción a la programación con orientación a objetos

Para ji1 mientras j<tablero(i) y seguir incremento jij+1


tablero(i)itablero(i)-j
Si contrario_ pierde entonces
sigueifalse
filaii
fichasij
Fin_Si
tablero(i)itablero(i)+j
Fin_Para
Fin_Para

Si sigue entonces
filai-1
Fin_Si

Devolver fila y fichas


Fin

El resultado es el valor de la fila que debemos elegir y el número de fichas que debemos quitar de
ella. Cuando no hay ningún movimiento bueno se coloca en filas el valor –1 (no puede haber un índi-
ce negativo en la matriz) como valor centinela para identificar luego la situación.
El algoritmo anterior no está completo porque falta indicar cómo se determina si una posición es
mala. Podemos conseguir este objetivo de la siguiente forma:

Inicio
finifalso
Si tablero_vacío entonces
finiverdadero
Si_ no
Ver si es un buen movimiento y devolver fila
Si filai-1 entonces
finiverdadero
Fin_Si
Fin_Si

Devolver fin
Fin

En este algoritmo encontramos el caso base, que es aquel en el que el tablero está vacío. Una mira-
da cuidadosa a los dos algoritmos revela que las variables para almacenar la fila y el número de fichas
a retirar deben ser accesibles a ambos. De momento, podemos solucionar este problema usando dos
variables static “globales” en el sentido usado en el Programa 5.2 3.
Construyamos ahora un programa en Java que implemente estos algoritmos para jugar al Nim con-
tra el ordenador. Abordemos el diseño del mismo. Consideremos los tres puntos principales: diagra-
ma de estructura, estructuras de datos y algoritmos.

a) Diagrama de estructura
Los dos algoritmos para la determinación de la posición buena y mala se implementarán en dos méto-
dos (movimiento_chachi y chungo) y se incluirán por comodidad un método para pintar el table-
3
La forma de hacerlo sin este uso de variables “globales” es por medio de la definición de una nueva clase que repre-
sente el tablero y todas sus propiedades. Diferiremos este asunto hasta el siguiente capítulo dedicado a clases y objetos.
Recursividad 151

ro en la pantalla (pintaTablero) y otro para determinar si el tablero está vacío (fin). Con esto el
diagrama de estructura es el recogido en la Figura 6.4.

b) Estructuras de datos
La estructura de datos central es la usada para definir el tablero. Como antes hemos indicado usare-
mos una matriz de enteros con tantos elementos como filas tenga el tablero. Cada elemento almace-
nará un entero que indicará el número de fichas en esa fila.

c) Algoritmos
Los dos algoritmos principales son los que permiten determinar un movimiento bueno y uno malo y que
han sido descritos anteriormente. Cuando no se encuentre ningún movimiento bueno se hará una jugada
aleatoria. Así, se seleccionará la primera fila que tenga alguna ficha y de las fichas que tenga se selec-
cionará al azar un valor para descontar. Usando la misma técnica que para el ejercicio 1 propuesto en
Capítulo 5 relativo a la simulación del lanzamiento de un dado usando números aleatorios tendríamos,

Inicio
otraiverdadero
Para ii0 mientras i<tamaño_tablero y otra incremento iii+1
Si tablero(i) ½0
otraifalso

filaii
fichasiparte entera de (1+random*tablero(i))
Fin_Si
Fin_Para

tablero(fila)itablero(fila)-fichas
Fin

Principal

pinta Tablero movimiento_chachi

chungo

fin

Figura 6.4. Diagrama de estructura para el programa del juego del Nim
152 Introducción a la programación con orientación a objetos

En el pseudocódigo anterior se ha supuesto que random genera un número aleatorio entre cero y
uno, como en Java hace el método random() de la clase Math.
Con todo esto, una propuesta de implementación sería la mostrada en el Programa 6.7.

Programa 6.7. Implementación del juego del Nim

import java.io.*;
class Nim {
static int fila_m, fichas_m;
public static void main(String [] args) throws IOException {
int fila, n_fichas, tamagno;
boolean seguir=true;
int [] tablero;

BufferedReader leer =new BufferedReader


(new InputStreamReader(System.in));

// Definiendo el tablero
System.out.println(“Bienvenido al juego del Nim”);
System.out.println(“Introduzca el numero de filas “
+”que desea: “);

// Colocando las fichas en el tablero


tamagno=Integer.parseInt(leer.readLine());
tablero=new int[tamagno];
for (int i=0; i<tamagno; i++) {
tablero[i]=i+1;
}

System.out.println();
System.out.println(“Tablero inicial”);
pintaTablero(tablero);

// Empieza el juego
while (seguir) {
// Mueve el humano
System.out.println(“Usted mueve”);
System.out.println(“Fila: “);
fila=Integer.parseInt(leer.readLine());
System.out.println(“Fichas a eliminar: “);
n_fichas=Integer.parseInt(leer.readLine());

tablero[fila]-=n_fichas;
System.out.println();
System.out.println(“Tablero tras el movimiento”);
pintaTablero(tablero);

if (fin(tablero)) {
System.out.println(“Usted gano”);
seguir=false;
}
else {
// Mueve la máquina
System.out.println(“Mueve la maquina”);
movimiento_chachi(tablero); // Algoritmo recursivo
Recursividad 153

Programa 6.7. Implementación del juego del Nim (continuación)

if (fila_m == -1) {
// Si no hay posición ganadora se juega al azar
boolean otra=true;
for (int i=0; i<tablero.length && otra; i++) {
if (tablero[i]!=0) {
otra=false;
fila_m=i;
fichas_m= (int)(1+Math.random()*tablero[i]);
System.out.println(fila_m+” “+fichas_m);
}
}
}
tablero[fila_m]-=fichas_m; // Jugada de la máquina

pintaTablero(tablero);
if (fin(tablero)) {
System.out.println(“Usted perdio”);
seguir=false;
}
}
} // Fin del while
}//fin main

public static void pintaTablero(int [] tablero) {

/* Se pinta el tablero indicando la fila y el número


de fichas en cada fila. Cada ficha se representa con
un asterisco */

System.out.println();
for (int i=0;i<tablero.length;i++){
System.out.print(i+”:”);
for (int j=0; j<tablero[i];j++){
System.out.print(“*”);
} // Fin del for j
System.out.println(); // Saltando una línea
}
System.out.println();
}//fin pintaTablero

public static boolean fin(int [] tablero) {


// Aquí se determina si no quedan fichas

int n_fichas=0;
boolean finito=false;
for (int i=0; i<tablero.length; i++) {
n_fichas+=tablero[i];
}
if (n_fichas==0) {
finito=true;
}
return finito;
}//fin método fin
public static boolean chungo(int [] tablero) {
154 Introducción a la programación con orientación a objetos

Programa 6.7. Implementación del juego del Nim (continuación)

// Este método indica si una jugada es mala

boolean kaput=false;

if (fin(tablero)) {
kaput=true; // Caso base
}
else {
movimiento_chachi(tablero);
if (fila_m ==-1) {
kaput =true;
}
}
return kaput;

} // Fin método chungo

public static void movimiento_chachi(int [] tablero) {


// Este método escoge una jugada buena
boolean sigue=true;

for (int i=0; i<tablero.length && sigue; i++) {


for (int j=1; j<=tablero [i] && sigue; j++) {
tablero [i]-=j;
if (chungo(tablero)) { // Saliendo de los for
sigue=false;
fila_m=i;
fichas_m=j;
}
tablero [i]+=j;
} // Fin for del j
} // Fin for del i
if (sigue) {
fila_m=-1; // Valor centinela si no hay jugada ganadora
}
} // Fin método movimiento_chachi

} // Fin de la clase Nim

El programa comienza preguntando por el tamaño del tablero entendido como el número de filas
a colocar. El juego empieza con un movimiento del jugador humano y a partir de ahí se va alter-
nando con la jugada de la máquina. El juego del Nim obedece a una estructura matemática explica-
ble en términos del o lógico exclusivo (xor) y notación binaria. El lector interesado puede consultar
(Nim, 2002).
No queremos concluir este capítulo sin indicar que una excelente exposición de la recursividad y
sus aplicaciones puede encontrarse en (Roberts, 1986).
Recursividad 155

EJERCICIOS PROPUESTOS
Ejercicio 1.* ¿Qué valor devolverá el método restados si le pasamos el valor 5?
¿Y si le pasamos 6?
int restados(int n) {
int valor=0;
if (n==2) {
valor=0;
}
else {
valor= n+restados(n-2);
}
return valor;
}

Ejercicio 2.* ¿Cuál es la salida del siguiente programa?


class Ejercicio {
public static void main(String[]args) {
metodoA(3);
}

public static void metodoA(int n) {


if (n<1){
System.out.println(‘B’);
}
else {
metodoA(n-1);
System.out.println(‘R’);
}
}
}

Ejercicio 3.* La función de Ackermann A(m, n) se define para enteros no negati-


vos como:
n+1 si m=0,
A(m,n)= A(m-1,1) si m>0, n=0,
A(m-1,A(m,n-1)) si m>0, n>0.

Escriba un programa que contenga un método recursivo que calcu-


le esta función. ¿Qué devolverá el método si m51 y n51? En este
caso, ¿cuántas llamadas se realizarán al método desde el propio
método?

Ejercicio 4.* Sin utilizar ningún bucle, escriba un método que acepte como pará-
metros una matriz a de números reales y un entero n 0. El méto-
do debe devolver la suma de los valores de la matriz a comprendidos
entre el primer elemento y el elemento n.

Ejercicio 5.* Cuenta la leyenda que en el gran templo de Benarés existe una base
de bronce de la que sobresalen tres varillas de diamante. En el
momento de la creación, Dios colocó 64 discos de oro ensartados en
156 Introducción a la programación con orientación a objetos

la primera varilla, colocados de abajo arriba en orden de tamaño


decreciente; ésta es la torre de Brahma. Los sacerdotes están tratan-
do de pasar la pila de la primera varilla a la segunda, sometidos a las
leyes de Brahma que indican que sólo se puede mover un disco a la
vez, y que en ningún momento se puede colocar un disco más gran-
de sobre uno más pequeño. Se cuenta con la tercera varilla para colo-
car los discos temporalmente. Cuando todos los discos hayan sido
transferidos, la torre, los sacerdotes, el templo, y todo el mundo
desaparecerá con un estruendo (Enunciado original del hoy conocido
como problema de las torres de Hanoi).
Desarrolle un programa con un método recursivo que solucione el
problema de las torres de Hanoi para un número arbitrario de dis-
cos. El programa debe imprimir la secuencia precisa de transferen-
cia de los n discos de una varilla a otra.

Ejercicio 6.* Escriba un método recursivo que calcule la potencia n de un núme-


ro x.

Ejercicio 7.* ¿Cuál es el error del siguiente método que pretende evaluar un
sumatorio? ¿Cómo corregiría el error?

int suma(int numero){


if (numero ==0)
return 0;
else
numero+suma(numero-1);

}//Fin del método suma

Ejercicio 8.* Implemente un método recursivo que imprima los elementos de una
matriz monodimensional.

Ejercicio 9.* ¿Cuál es la salida del siguiente programa?

class Indirecta{
public static void main(String [ ] args) {
metodoA(3);
} // Fin del main

public static void metodoA(int n){


if (n==1)
return;
else{
System.out.println(“A antes”);
metodoB(n);
System.out.println(“A despues”);
}

} // Fin del metodoA

public static void metodoB(int n){


System.out.println(“B antes”);
metodoA(n-1);
Recursividad 157

System.out.println(“B despues”);
} // Fin del metodoB
}
Ejercicio 10.* Considere el algoritmo de Euclides para la determinación del máxi-
mo común divisor expuesto en el Ejercicio 12 del Capítulo 5. Escri-
ba un método recursivo que aplique el algoritmo de Euclides.

REFERENCIAS
AHO, A. V., HOPCROFT, H. E. y ULLMAN, J. D.: Data Structures and Algorithms, Addison-Wesley, 1987.
APOSTOL, T. M.: Calculus, vol. 1, Segunda Edición, Reverté, 1979.
BRASSARD, G. y BRATLEY, P.: Fundamentos de Algoritmia, Prentice Hall, 1997.
Nim: http://www.cut-the-knot.com/nim_theory.shtml última visita realizada en mayo de 2002.
ROBERTS, E. S.: Thinking Recursively, John Wiley & Sons, Inc, 1986.
WEISS, M. A.: Data Structures & Problem Solving using Java, Addison-Wesley, 1998.
7

Clases y Objetos

Sumario

7.1. Introducción 7.5. Desarrollo de software orientado a objetos


7.2. Concepto de objeto y clase 7.6. Definición de clases y creación de objetos
7.2.1. Objetos 7.7. Referencias
7.2.2. Clases 7.7.1. Conceptos generales
7.3. Concepto de encapsulación y abstracción 7.7.2. Estructuras dinámicas: listas enlaza-
7.3.1. Encapsulación das
7.3.2. Abstracción 7.8. Modificadores
7.4. Relaciones entre clases 7.8.1. Modificadores de visibilidad
7.4.1. Relación de generalización 7.8.2. Modificador static
7.4.2. Relación de asociación
7.4.3. Relación de dependencia
160 Introducción a la programación con orientación a objetos

7.1. INTRODUCCIÓN
En este tema abordamos de forma sistemática la programación orientada a objetos, partiendo de los
conceptos de clase y objeto. Todos los contenidos presentados hasta el momento y relativos a la pro-
gramación imperativa y estructurada tradicional continuarán siendo válidos. Esto se debe a que la pro-
gramación imperativa se puede considerar como un subconjunto de la programación orientada a
objetos (Arnow y Weiss, 2001). Como podrá comprobarse a lo largo de este capítulo, la programación
orientada a objetos implica más un cambio de filosofía en el planteamiento de la resolución de los pro-
blemas que la introducción de nuevos elementos de programación. En cualquier caso, la intención cen-
tral que nos ocupa en este momento es la fundamentación teórica de la programación orientada a
objetos. Lógicamente, tendremos que incluir consideraciones sintácticas en este capítulo. Sin embar-
go, aconsejamos que el lector se centre en comprender los conceptos, y la sintaxis la considere como
un medio para implementar dichos conceptos. Recordemos que el objetivo de este texto es presentar
el desarrollo de software aplicando el paradigma de orientación a objetos y no simplemente enseñar a
codificar en un lenguaje orientado a objetos. Esta aclaración es importante, debemos distinguir entre,

— Programación con un lenguaje orientado a objetos.


— Programación orientada a objetos.

La programación con un lenguaje orientado a objetos implica simplemente la utilización de clases


y objetos como entidades útiles, pero no el aprovechamiento de las características implícitas en el para-
digma de orientación a objetos, tales como la herencia y el polimorfismo. La programación, o más
correctamente el desarrollo de software orientado a objetos implica un cambio de mentalidad desde el
tradicional punto de vista funcional. Así, debemos considerar desde un punto de vista general, casi
como un diseño, la formulación de la resolución de nuestro problema en términos de objetos y sus rela-
ciones.
Por estas razones, los primeros apartados de este capítulo se centran en la exposición de los con-
ceptos semánticos básicos en orientación a objetos. Sólo tras esta exposición se aborda la correspon-
diente sintaxis en Java con los correspondientes ejemplos. Comencemos considerando los conceptos
centrales de objeto y clase.

7.2. CONCEPTO DE OBJETO Y CLASE


Los conceptos centrales del paradigma orientado a objetos son el de objeto y el de clase, por lo que es
importante entender el papel que desempeña cada uno. Ambos conceptos están estrechamente rela-
cionados, pues uno deriva de otro. Algunos textos prefieren empezar con la definición de clase para
después describir lo que es un objeto. Aquí, adoptaremos el punto de vista contrario, partiendo del
objeto como concepto particular y generalizando luego al de clase.

7.2.1. OBJETOS
Un programa escrito según el paradigma orientado a objetos consiste en una serie de objetos que inte-
raccionan entre sí, pero la pregunta es ¿qué es un objeto? Podemos exponer el concepto de objeto esta-
bleciendo una analogía con los objetos físicos del mundo real, como si estuviéramos pretendiendo
desarrollar un programa de simulación.
Imaginemos un objeto del mundo real tal como una pelota. La pelota tiene ciertas características
como color, peso, diámetro o posición. Estas características definen el estado de la pelota, y la varia-
ción de alguna de esas características altera el estado de la pelota. Aparte, la pelota puede realizar cier-
Clases y Objetos 161

tas “tareas” como rodar o botar. Estas “tareas” definen el comportamiento de la pelota. Con el estado
y el comportamiento podemos conocer qué le pasa a la pelota en un instante determinado. Desde este
punto de vista, cualquier objeto físico queda definido por su estado y su comportamiento. Si imagina-
mos que tenemos que simular una pelota en un programa, necesitaremos datos (variables) para indicar
su estado, y métodos que alteren esos datos para representar su comportamiento, véase la Figura 7.1.

Estado Variables

Comportamiento Métodos

Figura 7.1. Relación entre las características externas de un objeto


y su modelización interna, en software

Si queremos programar un juego de baloncesto, la pelota se podría representar como un objeto que
posee variables para almacenar su tamaño y posición, y métodos que la dibujan en la pantalla y cal-
culan cómo se mueve. Las variables y métodos definidos establecen el estado y comportamiento que
son relevantes para el juego. Esto es importante, qué datos y qué métodos vamos a usar depende del
programa que vayamos a escribir. De hecho, son los requisitos del programa los que indican los datos
y métodos necesarios.
Con el estado y el comportamiento de la pelota podemos manejar este objeto en nuestro juego. Por
lo tanto, desde el punto de vista del juego, la pelota no es sino un conjunto de características o pro-
piedades, por un lado, y de tareas o métodos (procedimientos), por otro.
Los programas pueden, o quizá sea más preciso decir suelen, tener muchos objetos del mismo tipo,
pero cada objeto en concreto es único. Para cada objeto, sus propiedades tendrán un valor y su com-
portamiento será uno dado. Dos objetos similares pueden tener distinto o el mismo valor de sus pro-
piedades y el mismo posible comportamiento. En el hipotético programa para simular un partido de
baloncesto, por ejemplo, habría varios jugadores. Cada jugador se representaría como un objeto dis-
tinto, cada uno con sus propiedades (nombre, posición en el campo en cada momento, etc.). Los obje-
tos (dos pelotas, por ejemplo) son diferentes aunque sean del mismo tipo (tienen las mismas
propiedades y el mismo comportamiento) pero cada uno tiene su identidad y su nombre.
Consideremos otro ejemplo. Imaginemos una herramienta software de gestión de una universidad.
En este caso, dentro del programa, los alumnos se representarían como objetos con una serie de pro-
piedades, por ejemplo: nombre, carrera cursada, asignaturas cursadas, edad o notas. Cada objeto alum-
no almacenará información acerca de un alumno particular, es decir, cada alumno estará representado
por un objeto. También podemos asociar ciertos comportamientos o métodos (los que se necesiten en
el programa) a cada objeto alumno, como imprimir el nombre o calcular la nota media a partir de las
notas. La estructura de un objeto alumno podría ser,

Alumno:
Estado
— Nombre
— Carrera
— Asignaturas
— Edad
etc.
Comportamiento:
— Imprimir_nombre
— Calcular_nota_media
162 Introducción a la programación con orientación a objetos

etc.
Este concepto genérico de objeto toma carta de naturaleza en el paradigma de la programación
orientada a objetos. Los objetos son las entidades en las que se basa un programa orientado a objetos.
Estos objetos interaccionan (y el programa funciona) enviándose mensajes unos a otros que indican
tareas a realizar (son solicitudes de servicios, llamadas, invocaciones, a métodos). Los objetos soft-
ware no siempre se corresponden con objetos físicos. Podemos manejar objetos abstractos como por
ejemplo mensaje_de_error con propiedades como codigo_error o gravedad_de_error y
métodos como describir_error.
Un error muy común entre los neófitos en la programación orientada a objetos es el de definir obje-
tos 1 que constan sólo de atributos o sólo de procedimientos. En el primer caso tenemos una simple
estructura de datos sin los procedimientos para manipularlos. En el segundo caso tenemos una serie de
procedimientos sin datos sobre los que actuar. Estas dos situaciones son inconsistentes con el para-
digma de orientación a objetos y deben ser evitadas.
Recordando el ejemplo de los distintos jugadores en el programa de baloncesto es fácil entender
que algunos objetos pertenecen al mismo tipo, aunque individualmente sean diferentes. Por ejemplo,
todos los jugadores tendrán la misma estructura (mismas variables y métodos) aunque el estado de
cada uno sea diferente. Los objetos que tienen las mismas propiedades (aunque sus valores puedan ser
distintos) y el mismo comportamiento se agrupan en categorías llamadas clases como vamos a ver en
el siguiente apartado.

7.2.2. CLASES
Como se indicaba en el apartado anterior los objetos con las mismas propiedades y comportamiento
tienen o son del mismo tipo. El tipo del objeto define sus propiedades y su comportamiento, de forma
análoga a cómo el tipo de dato de una variable primitiva determina la naturaleza de datos que puede
contener y el intervalo de los mismos. Los objetos del mismo tipo serían elementos del mismo con-
junto. Dicho conjunto se denomina clase. La relación entre la clase y los objetos es similar a la exis-
tente entre un tipo primitivo y las variables de ese tipo. Por ejemplo, sea en Java el tipo primitivo int.
Sólo hay un tipo, pero podemos crear tantas variables de ese tipo como necesitemos. De la misma for-
ma, considerando una clase concreta, esa clase es única, pero se pueden crear tantos objetos de ella
como se necesiten. Por ejemplo, si existiera una clase jugador podríamos crear tantos jugadores (obje-
tos) como hagan falta. Cuando se hace un programa orientado a objetos no programamos cada objeto
por separado, pues todos los objetos de la misma clase tendrían el mismo código. Lo que programa-
mos es la clase y luego se crean tantos objetos de ella como se necesiten. Dicho de otra forma, para
crear un objeto es necesario haber escrito previamente una definición de la clase del objeto. La clase
es el “plano”, el modelo, el patrón o plantilla para crear objetos. Es como el plano de una casa. El pla-
no es único, pero a partir de él podemos crear casas diferentes, por ejemplo, con revestimientos exte-
riores distintos y localizadas en ciudades distintas. Los atributos (datos) y procedimientos (métodos)
de una clase se denominan miembros de la misma. Desde un punto de vista formal, como ya comen-
tamos en el Apartado 5.3 del Capítulo 5, una clase se puede entender como la realización de un tipo
abstracto de datos (TAD).
Una vez que hemos definido una clase podemos crear objetos de la misma. Se dice que creamos
un ejemplar, instancia de la clase. Cada objeto creado es único porque cada uno tiene su propio espa-
cio de datos (estado) posiblemente con diferentes valores ocupando su propia zona de memoria.
Para el desarrollo de un programa orientado a objetos es útil disponer de alguna técnica gráfica

1
Para ser estrictos deberíamos decir clase y no objeto ya que donde se realiza la especificación de los atributos y los
procedimientos es en la definición de la clase.
Clases y Objetos 163

que permita representar las clases. En este texto usaremos la notación del lenguaje unificado de mode-
lado, UML 2. En UML la clase se representa como un rectángulo dividido en tres secciones. La pri-
mera se usa para indicar el nombre de la clase, la segunda para recoger los atributos (datos) y la tercera
para los procedimientos (métodos). El tipo (o clase) al que pertenecen los atributos, los parámetros de
los métodos o el tipo de retorno de los métodos se indica tras el identificador correspondiente con la
sintaxis:

atributo:tipo
o
método(parámetro:tipo):tipo de retorno

Los objetos se representan con un rectángulo, donde se indica en subrayado el nombre del objeto
(si nos interesa indicarlo) seguido de dos puntos y el nombre de la clase (si nos interesa indicarla). En
esta notación, la relación clase-objeto quedaría ilustrada tal y como aparece en la Figura 7.2.

Clase A
Objeto 1: Clase A
Estado
(propiedades,
variables)
Objeto 2: Clase A
Comportamiento
(procedimientos)

Figura 7.2. Relación entre clase y objetos de esa clase

Un elemento de una clase (instancia, objeto) se representa en la memoria del ordenador, y es lo


que realmente se manipula en el sistema. En cambio, para una clase no se reserva espacio de datos en
la memoria.
Es importante indicar llegados a este punto la relación entre clases y objetos con las estructuras de
datos típicas en los lenguajes no orientados a objetos 3. Las estructuras de datos se pueden definir como
agregados de tipos de datos primitivos. Dichos tipos no tienen por qué ser todos iguales. La utilización
de tipos de datos heterogéneos impide que se pueda usar un tipo matriz (array, tabla). Estos tipos de
datos se denominan normalmente registros o estructuras y sus componentes se denominan campos.
Veamos como ejemplo un hipotético registro para almacenar información sobre los estudiantes de un
centro:

Registro Estudiante:
Campo 1: Nombre (Tipo cadena)
Campo 2: Apellido 1 (Tipo cadena)
Campo 3: Apellido 2 (Tipo cadena)
Campo 4: Edad (Tipo entero)

2
El UML (Unified Modeling Language) es un lenguaje gráfico usado en el proceso de desarrollo de software orientado
a objetos y que representa un estándar actual (Booch et al., 2000; Rumbaugh et al., 2000). En este texto, la notación utiliza-
da para el modelado de sistemas orientados a objetos será la indicada en UML. El Apéndice C recoge un resumen de esta
notación para la creación de diagramas de clase.
3
Ésta es una pregunta típica de los estudiantes que conocen otros lenguajes como Fortran 90, Pascal o C.
164 Introducción a la programación con orientación a objetos

Campo 5: Sexo (Tipo carácter)


Una vez definido el registro como estructura de datos se puede usar como un tipo primitivo y
declarar variables o matrices de dicho tipo. El mismo resultado se puede conseguir en programación
orientada a objetos sin utilizar ningún tipo de registro o estructura. El equivalente a estos registros
se obtiene definiendo una clase donde las propiedades (variables) de la misma sean los campos
necesarios y donde los procedimientos sean los que se usen para manipular los datos (por ejemplo,
actualizar_nombre o escribir_edad).

7.3. ENCAPSULACIÓN Y ABSTRACCIÓN


En este apartado vamos a definir y analizar estos dos conceptos relacionados, que se asocian de forma
indisoluble con el de clase. Tanto es así, que la encapsulación se considera como una de las carac-
terísticas definitorias de la orientación a objetos.

7.3.1. ENCAPSULACIÓN
Un objeto (en realidad una clase) puede considerarse desde dos puntos de vista:

a) En tiempo de desarrollo
Aquí se trataría el problema de la definición o diseño de la clase. El trabajo consistiría en decidir
qué datos corresponden a la clase y qué métodos vamos a necesitar para manipular esos datos. Nece-
sitaríamos también diseñar los diferentes métodos. En resumen, necesitaríamos construir el interior
de la clase.

b) En tiempo de ejecución
Cuando la clase ya existe, se crean objetos de la misma y se usan invocando los métodos necesarios.
En realidad creamos objetos para usar los servicios que nos proporciona la clase a través de sus méto-
dos. No necesitamos considerar cómo trabaja el objeto, ni qué variables usa, ni el código que contie-
ne. Desde este punto de vista, el objeto se usa como una caja negra de la que sólo necesitamos saber
lo que hay que darle para que nos proporcione un servicio determinado. Es la filosofía cliente-servi-
dor. El objeto es un servidor que proporciona servicios a los clientes que lo solicitan. Se usa el térmi-
no encapsulación para describir el hecho de que los objetos se usan como cajas negras. Se dice que un
objeto encapsula datos y métodos (atributos y procedimientos). Estos métodos y datos están conteni-
dos dentro del objeto. La encapsulación es la idea básica tras la filosofía o modelo cliente-servidor. El
conjunto de procedimientos (métodos) que sirven para que los objetos de una clase proporcionen sus
servicios define la interfaz pública de esa clase.
Los métodos que definen los servicios que el objeto proporciona se denominan métodos de servi-
cio y pueden ser invocados por un cliente. Puede haber métodos adicionales en un objeto que no defi-
nen un servicio utilizable por un cliente, pero que ayudan a otros métodos con sus tareas. Son los
denominados métodos de soporte, véase la Figura 7.3.
Como ejemplo, imaginemos un método que ordena una serie de valores. Supongamos que el algo-
ritmo de ordenación es complejo y que se modulariza en tres módulos. Uno de estos módulos es el que
acepta la petición de ordenación y necesariamente tendrá que poder ser invocado desde fuera del obje-
to para que pueda empezar el proceso de ordenación. Este método forzosamente formará parte de la
Clases y Objetos 165

Clase A

Atributos (datos)

Procedimientos Interfaz
públicos (de servicio) pública

Procedimientos
privados (de soporte)

Figura 7.3. Representación gráfica de los distintos tipos de procedimientos (métodos)


existentes en una clase

interfaz pública de la clase. Sin embargo, internamente este método invoca a los otros dos para poder
aplicar el algoritmo de ordenación. Estos dos nuevos métodos no necesitan ser llamados desde el exte-
rior de la clase y no tienen que formar parte de la interfaz pública. Estos dos métodos serían privados
(de soporte) a la clase.
La encapsulación es un mecanismo de control. Los datos de un objeto sólo deben poder ser modi-
ficados por medio de un método de ese objeto. Un cliente nunca debe ser capaz de acceder al estado
(datos) de un objeto directamente, cambiándolos. Esto es importante:

El estado de un objeto sólo debe ser modificado por medio de los métodos del propio objeto.

Una clase no debe tener el equivalente a atributos (datos) públicos que puedan ser directamente
accesibles desde el exterior. La modificación de un atributo debe realizarse por medio de un método
y la consulta del valor de un atributo debe realizarse por medio de un método especialmente dedicado
para ello.
Llegados a este punto, hemos podido comprobar la utilidad del concepto de encapsulación. Sin
embargo, dicho concepto se apoya sobre el de abstracción. Consideremos este concepto en el siguien-
te apartado.

7.3.2. Abstracción
En la mente humana, la memoria a corto plazo sólo puede manejar grupos de aproximadamente 7 ele-
mentos (Miller, 1956). Sin embargo, cualquier construcción humana, excepto las más simples, mane-
ja más de 7 constituyentes. La forma de trabajar en estos casos consiste en agrupar elementos
relacionados y manejar estos grupos como una unidad. Así, sólo consideramos lo que la unidad hace
y no nos fijamos en los detalles internos. Este proceso se denomina abstracción y se usa continuamente
en la vida diaria, como al conducir un coche. Cuando se conduce un coche no necesitamos conocer los
detalles de cómo al cambiar de marcha o mover el volante se manejan las piezas internas del automó-
vil, para conseguir más potencia o cambiar de dirección. Nosotros nos abstraemos de esos detalles y
sólo necesitamos conocer cómo interaccionar con el coche a nivel de usuario. En términos de progra-
mación orientada a objetos diríamos que sólo necesitamos conocer su interfaz pública. La encapsula-
ción es una forma de abstracción. La encapsulación es un mecanismo para llevar a la práctica la
166 Introducción a la programación con orientación a objetos

abstracción.
El nivel de abstracción puede ser mayor o menor. A bajo nivel de abstracción, en una clase esta-
remos manipulando los datos y los métodos individualmente. A alto nivel de abstracción, la clase (en
realidad los objetos creados a partir de ella) se considera una unidad y sólo se usan sus servicios. El
nivel de abstracción a aplicar depende del problema considerado. Una buena abstracción oculta los
detalles en el momento correcto, para que así podamos enfocar la atención en la dirección adecuada.
Todos los conceptos de orientación a objetos están basados en la abstracción. Un ejemplo de uso de
abstracción que hemos encontrado en Java está en el uso del método println. Lo hemos utilizado
desde el principio sin necesidad de conocer qué tiene dentro. De hecho, no necesitamos conocer su
contenido para poder usarlo.

7.4. RELACIONES ENTRE CLASES


Una relación es una conexión entre elementos. A la hora de buscar una solución orientada a objetos
para un problema, primero se identifican los objetos y, por tanto, las clases, necesarios. Una vez iden-
tificado un conjunto de clases básico se debe reflexionar sobre si existe alguna relación entre dichas
clases. Por lo tanto, cuando hacemos un modelo orientado a objetos no sólo debemos conocer los obje-
tos (clases) que lo conforman, sino también sus relaciones. Las relaciones entre clases son muy impor-
tantes y deben identificarse con claridad cuando se está realizando un desarrollo orientado a objetos.
Las relaciones entre clases pueden deberse a la existencia de un estado (variables) y/o comporta-
miento común o, también, a que una clase necesite usar otra clase. Aunque algunas clases pueden exis-
tir aisladas, la mayoría no pueden y deben cooperar unas con otras. En el ejemplo de la casa, ésta está
formada por otras entidades como paredes, suelo o techos. A su vez, la casa contiene elementos “de
uso” como puertas o ventanas. Podemos ver que en este ejemplo existen diferentes entidades que con-
forman estructuralmente, o que se usan en, la casa. Entre clases también pueden existir diferentes tipos
de relaciones. El estudio formal de las relaciones entre clases indica que dichas relaciones pueden ser
de tres tipos (Booch et al., 2000; Rumbaugh et al., 2000):

a) “es-un” o generalizaciones.
b) “tiene-un” o “parte-de” o asociaciones.
c) “usa a” o “trabaja con” o dependencias.

Una relación se representa en UML como una línea, usándose diferentes tipos de línea para dife-
renciar los tipos de relaciones. En este texto usaremos la notación UML elemental para representar las
tres relaciones, véase el apéndice C. Consideremos uno a uno los tres tipos existentes.

7.4.1. RELACIÓN DE GENERALIZACIÓN


La relación (“es un”) se da entre un elemento general (clase padre o superclase) y un caso específico
de ese elemento (clase hija o subclase). Una relación de generalización se presenta cuando una clase
es un subtipo de otra clase (a veces esta relación se denomina a-kind-of, un tipo de). Por ejemplo, un
oso es un mamífero. La clase mamífero sería la clase padre o superclase, y la clase oso la clase hija o
subclase. Otros ejemplos de relación “es un” serían el caso de una rosa, que es una flor, o de un
empleado, que es una persona. Esta relación se conoce como relación de herencia y es importante
recalcar que es una relación entre clases. La clase hija hereda los atributos (datos) y procedimientos
(métodos) del padre, pudiendo añadir los suyos propios. La herencia es una característica importantí-
sima en programación orientada a objetos. De hecho es una de las características definitorias de una
Clases y Objetos 167

verdadera programación orientada a objetos y por este motivo le dedicaremos el capítulo siguiente.
En este contexto, un error típico es confundir lo que sería un objeto de una clase con una clase nue-
va que hereda de la anterior. Por ejemplo, si definiéramos una nueva clase llamada Ciudad_Real que
heredase de una clase Ciudad sería erróneo, porque Ciudad_Real es un ejemplar (objeto) de la cla-
se Ciudad. El error se evita si se tiene en cuenta que la relación objeto-clase implica que el objeto
(elemento de un conjunto) es un ejemplar individual de la clase (el conjunto). Por otro lado, la rela-
ción de herencia entre la clase padre A y la clase hija B implica que todos los elementos (objetos) de
B son un caso particular de la clase A. En otras palabras, la herencia es una relación entre dos con-
juntos y no entre un conjunto y sus elementos.
Las relaciones de herencia se representan en UML por flechas con la punta vacía apuntando a la
clase padre. La relación de herencia genera jerarquías entre las clases como en el ejemplo ilustrado en
la Figura 7.4 donde tenemos un conjunto de personas entre las que hay estudiantes y empleados de una
empresa que son vendedores o secretarios.

Persona

Empleado Estudiante

Vendedor Secretario

Figura 7.4. Ejemplo de jerarquía de clases generada por herencia

En el ejemplo de la Figura 7.4 tenemos relaciones de herencia, pues cada subclase “es un” tipo
especial de la clase padre.

7.4.2. RELACIÓN DE ASOCIACIÓN


Cuando una clase está estructuralmente compuesta de otras clases se dice que hay una relación de aso-
ciación. Esto se consigue usando algún objeto de una de las clases como atributo (dato) de la clase
compuesta. En otras palabras, una clase puede estar formada por objetos de otra u otras clases. Por
ejemplo una hipotética clase Coche podría tener como atributos objetos de la clase Puerta, Rue-
da, etc.
Una cuestión de interés es el número de ejemplares involucrados en la relación de asociación. Si
se relacionan dos elementos, la relación se denomina binaria. Si se conectan más de dos elementos,
por ejemplo, n, la asociación se denomina n-aria. En UML una asociación se representa por una línea
continua que conecta los elementos (en realidad las clases) relacionados. Es posible indicar cuántos
168 Introducción a la programación con orientación a objetos

objetos (ejemplares de una clase) están conectados en una relación de asociación. Esto define la mul-
tiplicidad. Para denotar la multiplicidad se usa la siguiente nomenclatura:
a) Si es un valor exacto se indica numéricamente, por ejemplo si es uno: 1.
b) Si es un intervalo de posibles valores se indica con el valor mínimo... valor máximo, por ejem-
plo, si es entre dos y cuatro: 2..4.
c) Si son varios, en número indefinido, se usa un asterisco: *.
d) Cualquier otro caso se construye con las tres reglas anteriores.

Por ejemplo, entre una empresa y sus dos únicos empleados tendríamos:

1 2
Empresa Empleado

Lo cual significa que una empresa debe tener exactamente 2 empleados, ni uno más ni uno menos,
y que un empleado sólo puede trabajar en una empresa.
Si los empleados pudieran ser entre 2 y 4 tendríamos:

1 2..4
Empresa Empleado

Si el número de empleados fuera indefinido tendríamos:

1 *
Empresa Empleado

Finalmente, si fueran como mínimo 2 pero sin límite superior tendríamos:

1 2..*
Empresa Empleado

Veamos un ejemplo más completo. Consideremos un coche que posee un motor y un chasis, así
como ruedas y un sistema de transmisión. A su vez, las ruedas constan de neumático y tapacubos. El
diagrama correspondiente con las relaciones estructurales de asociación sería el representado en la
Figura 7.5.
A veces, en una relación de asociación queremos indicar explícitamente que tenemos un todo com-
puesto por partes. En este caso, se habla de agregación y en el diagrama de clases se usa un rombo
Clases y Objetos 169

Coche
1 1
1 1

1 1 4 1

Motor Chasis Ruedas Transmisión

1 1
1 1

Neumático Tapacubos

vacío en la parte que corresponde al todo. Por ejemplo, una empresa compuesta por departamentos se
representaría como: Figura 7.5. Ejemplo de relación de asociación
170 Introducción a la programación con orientación a objetos

1 *
Empresa Departamento

La relación de generalización (herencia, “es un”), vista en el apartado anterior, es únicamente una
relación entre clases, relación que define una jerarquía de clases-subclases. Por otro lado, la relación
de asociación describe elementos que se deben mantener dentro de una clase. La relación de asocia-
ción se caracteriza porque una clase tiene como miembros objetos de otra clase.

7.4.3. RELACIÓN DE DEPENDENCIA


La relación de dependencia es una relación de utilización, donde un cambio en el estado de un
objeto (el independiente) afecta al estado de otro (el dependiente), pero no a la inversa. Esta rela-
ción aparece en la práctica cuando una clase se relaciona con otra a través de los mensajes que
le envía (métodos que invoca). Es decir, que se pasa un ejemplar de la clase independiente como
uno de los parámetros del método invocado (el de la clase dependiente). Por ejemplo si la clase
Monitor tiene un método llamado dibujar y se quiere dibujar un objeto de la clase Circulo, se
debería hacer mi_monitor.dibujar(mi_circulo) siendo mi_monitor y mi_circulo obje-
tos de las clases correspondientes.
En UML la relación de dependencia se representa por una flecha discontinua dirigida hacia el ele-
mento del cual se depende con la punta de la flecha apuntando hacia la clase independiente. Las depen-
dencias se usarán cuando se quiera indicar que un elemento usa o utiliza a otro. Un ejemplo sería el
elemento ducha como dependiente del elemento cañería:

Ducha Cañería

Si se modifica el comportamiento de las cañerías se afecta el comportamiento de la ducha. No es


una relación estructural, la ducha no está hecha de cañerías sino que las utiliza. Otro ejemplo podría
ser un elemento estudiante que utiliza uno o varios elementos asignatura, el diagrama sería:

Estudiante Asignatura

4
Una alta cohesión implica que todos los elementos del módulo, en este caso los objetos, están dirigidos hacia la mis-
ma misión. Todo lo que hay dentro del objeto es coherente consigo mismo, véase el texto de Lewis y Loftus (Lewis y Lof-
tus, 1998).
Clases y Objetos 171

Como comentario final podemos indicar que desde un punto de vista general y a nivel elemental,
a veces sólo se distingue entre dos relaciones: la de herencia y la de uso. Esto se observa, por ejem-
plo, en la herramienta de desarrollo BlueJ (BlueJ, 2002).

7.5. DESARROLLO DE SOFTWARE ORIENTADO A OBJETOS


La utilización de la aproximación orientada a objetos para el desarrollo de software presenta una serie
de ventajas (Pressman, 2002) que explican el peso que dicha aproximación va adquiriendo en la actua-
lidad. El punto central es que la abstracción de datos (atributos) está escondida detrás de una barrera
de abstracciones procedimentales (los procedimientos de servicio) que son los únicos que pueden
manipular dichos datos, véase la Figura 7.6.

Clase
Métodos

Métodos
Métodos

Datos

Métodos

Figura 7.6. Representación esquemática de la relación entre datos (atributos)


y métodos (procedimientos)

La única manera de acceder a los atributos (datos) es a través de los procedimientos de servicio.
De esta forma se implementa el ocultamiento de información y se reduce el impacto de efectos cola-
terales provenientes de cambios incontrolados sobre los datos. Como los procedimientos manejan un
conjunto limitado de datos que le son propios obtenemos una alta cohesión 4. Por otro lado, como la
comunicación sólo se realiza a través de los procedimientos de servicio, el acoplamiento con otros ele-
mentos del sistema está muy controlado. Como consecuencia de estos factores de diseño obtenemos
software de alta calidad, de acuerdo a los patrones de la ingeniería del software.
Cuando se programa siguiendo un paradigma orientado a objetos también se debe abordar una eta-
pa de análisis y otra de diseño antes de la de codificación. En la orientación a objetos las etapas de aná-
lisis y diseño se solapan aún más que en la tradicional aproximación funcional o procedimental.
Consideremos estas dos etapas del ciclo de vida del software desde el punto de vista de la orientación
a objetos (Larman, 1999).
Análisis: En el análisis orientado a objetos se pretende encontrar las clases, y las relaciones, rele-
vantes para la descripción del problema. Se realiza una investigación del problema, centrada en la
identificación y descripción de los conceptos (objetos) en el dominio del problema (de su definición).
En este contexto, se habla de un modelo conceptual que recoge (entre otras cosas) los conceptos (obje-
tos), sus relaciones, sus atributos (datos) y sus procedimientos (métodos). En esta etapa se identifican
los objetos que surgen como consecuencia de los requisitos.
Diseño: En el diseño orientado a objetos se parte del modelo de análisis para crear un nuevo mode-
lo que sirva como patrón para la creación del programa. Aquí, se definen los objetos lógicos que final-
mente serán implementados en un lenguaje de programación orientado a objetos. En esta etapa,
aparecen objetos que no son consecuencia de los requisitos (el dominio del problema) sino de la solu-
172 Introducción a la programación con orientación a objetos

ción propuesta (dominio de la solución). Por ejemplo, imaginemos un programa que gestiona los alum-
nos de una universidad. Con esta información podríamos plantear dos clases, Universidad y Alum-
no relacionadas estructuralmente (la Universidad está formada por Alumnos). Si pensamos en cómo
(diseño) implementar el programa podríamos decidir organizar los alumnos como una lista de personas
y definir una nueva clase Lista. Esta nueva clase no surge de los requisitos (dominio del problema)
sino de la forma en que vamos a solucionar el problema (dominio de la solución). La etapa de diseño
continuaría hasta disponer de un diagrama de clases para cada una de las clases identificadas, donde
especifiquemos sus atributos y procedimientos. A su vez, para cada procedimiento deberíamos haber
diseñado los algoritmos necesarios y representado el código, por ejemplo, a nivel de pseudocódigo.
Una regla sencilla para elegir las clases y los objetos es la asociación del software con entidades
físicas o componentes hardware que las clases controlen. Los objetos que representan entidades físi-
cas son más fáciles de entender, puesto que se puede establecer una analogía con el software. Estas
clases corresponderían al dominio del problema. Sin embargo, a medida que el software es más com-
plicado, hay mayor necesidad de clases lógicas que representen ideas intangibles, tal como una clase
que controle el acceso y uso de varios objetos. Una indicación general es la de fijarse en los sustanti-
vos que aparecen en la descripción del problema. Sin embargo, esta técnica no es rigurosa. Dada la
ambigüedad del lenguaje puede no hacerse referencia a una entidad que en nuestro problema es una
clase. Esta técnica no puede pretender usarse como una panacea sino como una simple orientación. El
proceso de determinación de clases implica proponer clases posibles y eliminar las que no sean real-
mente clases en el dominio de nuestro problema. Ante una posible clase candidata una pregunta a
hacer es qué datos y qué procedimientos necesita. Una clase debe trabajar con algunos datos (atribu-
tos) y tener unos procedimientos asociados para ello. En caso contrario, no es una clase. Para una dis-
cusión más detallada véase (Meyer, 1999).
Cuando se están identificando los objetos y las clases, algunos de sus detalles (atributos, comporta-
miento) son obvios. A menudo, al mismo tiempo que se identifica la clase, se obtiene una idea general de
los métodos que cada clase debe soportar. Por ejemplo, una clase que está relacionada con el control de
un robot debería tener un método asociado con el movimiento del robot. Estas suposiciones sobre la cla-
se deberían documentarse. Obsérvese que no todos los detalles sobre la clase se conocerán en este punto
del ciclo de desarrollo. Otra consideración es la reutilización de clases existentes. Un programador debe
tener en cuenta que existen clases ya escritas que pueden, y deben, usarse en los nuevos desarrollos.
Errores comunes en la selección de clases: De entrada, recordemos siempre que un objeto encap-
sula una serie de datos que se manipulan con sus métodos. Un objeto no contiene sólo datos ni sólo
métodos. Un error habitual es el de la rutina glorificada. En este caso tenemos una clase que contiene
sólo un método. Eso no es una clase, simplemente estamos considerando una rutina como muy impor-
tante y erróneamente la clasificamos como una clase. Este error se puede magnificar cuando se crean
clases que sólo tienen métodos. Es un error en el que estamos confundiendo un enfoque funcional con
un objeto (clase). La clase que hemos creado es en realidad un módulo funcional, por lo que no tiene
sentido dentro de la filosofía de objetos.
Con lo visto, está claro ahora que en orientación a objetos usamos el mismo tipo de herramientas
para análisis y diseño, por lo que las dos etapas no están tan claramente diferenciadas como en la pro-
gramación funcional tradicional. Estos conceptos son muy generales y se pueden aplicar con las herra-
mientas de modelado proporcionadas por UML, adaptándose a diferentes estrategias de desarrollo.
El lector interesado en el estudio detallado de la ingeniería del software orientada a objetos puede
consultar textos especializados como Pressman, 2002; Larman, 1999; Bruegge y Dutoit, 2002.

7.6. DEFINICIÓN DE CLASES Y CREACIÓN DE OBJETOS


Hasta este punto hemos presentado consideraciones conceptuales relativas a clases y objetos. Aborde-
mos ahora las consideraciones sintácticas, en el lenguaje que estamos utilizando, para la creación de
Clases y Objetos 173

las clases y los objetos.

a) Definición de clases
Una clase es, en cierto sentido, equivalente al “tipo” de una “variable” objeto. Por ejemplo, conside-
remos las dos siguientes sentencias en Java,

int total=10;
Cuenta cuenta_corriente = new Cuenta();

En la primera se declara una variable, total, de tipo entero y se le asigna el valor 10. La segun-
da sentencia crea una “variable” objeto, cuenta_corriente, de “tipo” (clase) Cuenta. Es evidente
que, como requisito para poder crear objetos de una clase, debemos definir antes la clase. En Java la
sintaxis general para la definición de una clase es:

class Nombre_clase {
declaraciones
constructores
métodos
}

El Nombre_clase es arbitrario y por convenio la primera letra del nombre de la clase se escribe
en mayúsculas. En declaraciones declaramos las variables que son accesibles a todos los métodos
de esa clase. Los constructores son uno o más métodos (si hay más de uno es un método sobre-
cargado) que tienen el mismo nombre de la clase y se usan para crear los objetos de la misma. Apar-
te de los métodos constructores se pueden escribir todos los métodos que se necesiten.
Una clase puede tener cualquier número de variables y métodos. Como ya indicamos, las variables
y métodos especificados para la clase se denominan miembros de la clase. Las variables se denomi-
nan variables de ejemplar o de “instancia” porque no existen hasta que se crea un ejemplar, es decir,
un objeto de la clase. Cuando un objeto se define tiene su propio espacio de almacenamiento para sus
variables y, por tanto, sus valores pueden ser diferentes a las de otro objeto de la misma clase. A su
vez, los métodos actúan sobre los datos del objeto que se esté usando en cada momento y no sobre los
de otro. Cuando se invoca un método se hace a través de un ejemplar particular de la clase. Recorde-
mos que las “variables” del objeto pueden ser a su vez objetos. Un objeto que contiene otros objetos
se denomina objeto agregado. Estas ideas genéricas quedarán más claras con un ejemplo. Considere-
mos una clase que representa una cuenta corriente.

Análisis
Identifiquemos los requisitos. Supongamos que la cuenta contiene una identificación de la misma (un
entero) y el saldo de la cuenta. Por otro lado, las operaciones sobre la cuenta se limitan al ingreso
(depósito de cantidades), retirada de fondos y consulta del saldo. Supongamos que a la hora de crear
una cuenta nueva se le puede indicar un saldo inicial.

Diseño
Con estos requisitos la estructura de la clase podría ser la recogida en la Figura 7.7.
174 Introducción a la programación con orientación a objetos

Cuenta
numero_cuenta:int
saldo:double

Cuenta (cuenta:int, inicial:double)


depositar (cantidad:double):void
retirar (cantidad:double):void
devuelveSaldo ( ):double

Figura 7.7. Diagrama de la clase Cuenta

El código correspondiente a la clase Cuenta en Java sería el siguiente:

class Cuenta {
int numero_cuenta;
double saldo;

public Cuenta(int cuenta, double inicial) {


numero_cuenta=cuenta;
saldo = inicial;
} // Fin constructor cuenta

public void depositar(double cantidad) {


saldo = saldo + cantidad;
} // Fin método deposito

public void retirar(double cantidad) {


saldo = saldo - cantidad;
} // Fin método retirada

public double saldo(){


return saldo;
} // Fin método saldo

} // Fin clase Cuenta

5
Pronombre demostrativo que significa éste, ésta, esto.
Clases y Objetos 175

En este ejemplo, declaramos dos variables de ejemplar, numero_cuenta y saldo. Es importan-


te darse cuenta de que las variables de ejemplar están definidas en el bloque de código más externo de
la clase. Por otro lado, los métodos también están definidos en ese mismo bloque. Esto implica que las
variables de ejemplar son visibles dentro de los métodos. Por ejemplo, el método depositar y el
método retirar usan la variable de ejemplar saldo y alteran su valor previo.
En la clase anterior también se define un método constructor. El método constructor es el que tiene
el mismo nombre que la clase. Es importante indicar que los métodos constructores no tienen tipo de
retorno (no tienen, ni siquiera void) ya que su misión es la creación de un objeto. El método construc-
tor se ejecuta siempre para crear un objeto de la clase considerada. Un constructor puede aceptar pará-
metros. El uso típico de los métodos constructores es la inicialización de las variables de ejemplar del
objeto creado. Por ejemplo, en la clase anterior, en el constructor se declaran dos parámetros formales
en la cabecera, cuenta e inicial y se asigna su contenido a numero_cuenta y saldo, respectiva-
mente. De esta forma al crear una cuenta nueva (un objeto) se le asignará un número y un saldo inicial,
como establecen los requisitos. Una clase puede tener varios constructores, que tendrán el mismo nom-
bre (sobrecarga), sabiéndose a cuál se llama por el tipo y el número de parámetros que se pasan. Si no
se definen constructores para una clase, el compilador crea un constructor por defecto que no recibe
argumentos y no hace nada (simplemente inicializar las variables de tipos de datos numéricos a 0, las
boolean a false y las referencias a null), pero asegura que cada clase siempre tenga un constructor.
El método saldo() es un ejemplo de método de consulta. En orientación a objetos ésta es la for-
ma de devolver la información contenida en un objeto, nunca se debe acceder al contenido del objeto
(sus datos) directamente. En el ejemplo hemos usado la convención de llamar al método de consulta
con el nombre de la variable de ejemplar cuyo valor devuelve.
Dentro de una clase es posible declarar parámetros de un método con el mismo identificador que
alguna de las variables de ejemplar. Dentro del correspondiente método esto da lugar a ambigüedad,
pues en él existiría una variable de ejemplar y una variable local con el mismo nombre. Para evitar esa
ambigüedad se usa la palabra reservada this 5 que siempre hace referencia a los miembros de la cla-
se. El uso de la cláusula this se muestra en el siguiente ejemplo. Sea la siguiente clase,

class Alumno{
//Atributos de ejemplar
int dni, edad;

//Constructor
Alumno(int dni, int edad){
this.dni = dni;
this.edad = edad;
}
}

En el método constructor Alumno, los nombres de los atributos coinciden con los nombres de los pará-
metros del constructor declarados en su cabecera (parámetros formales). Para indicar dentro del método si
nos estamos refiriendo al parámetro o la variable de ejemplar se utiliza this. Cuando hacemos referencia
al dni o a la edad precedido de this. estamos haciendo referencia a los atributos de ejemplar.

b) Creación de objetos
Una vez definida la clase podemos crear diferentes objetos de esa clase, tantos como necesitemos, véa-
se la Figura 7.8.
176 Introducción a la programación con orientación a objetos

cuenta 1
numero_cuenta:
123456
saldo:
250532
Clase Cuenta
numero_cuenta:int
saldo:double

cuenta 2
numero_cuenta:
456784
saldo:
1523879

Figura 7.8. Clase cuenta y dos objetos de esa clase

En la Figura 7.8. se observa que de la clase Cuenta se crean dos objetos diferentes llamados
cuenta_1 y cuenta_2. Cada uno de estos objetos tiene su propio valor de numero_cuenta y de
saldo. Los objetos se crearían de la siguiente forma:

Cuenta cuenta_1=new Cuenta(123456, 250532);


Cuenta cuenta_2=new Cuenta(456784, 1523879);

Vamos a analizar la sentencia de creación de un objeto de clase Cuenta como:

Cuenta cuenta_1 = new Cuenta(123456,250532);

Creamos, declaramos, un objeto cuenta_1 de clase Cuenta, inicializándolo con el operador new
y un constructor de la clase Cuenta. En esta sentencia en realidad tenemos dos operaciones: declara-
ción e inicialización. Estas dos operaciones se pueden separar,

declaración: Cuenta cuenta_1;


inicialización: cuenta_1 = new Cuenta(123456,250532);

En la primera línea se declara la variable cuenta_1 como una referencia a un objeto de la clase
Cuenta. Todavía no se ha creado el objeto y la referencia no refiere a nada. Este valor de “nada” se
identifica en Java con la palabra reservada null. Es posible usar null en un if para saber si una refe-
rencia ya refiere a un objeto o aún no. Veremos un ejemplo de esta técnica en el apartado sobre estruc-
turas dinámicas. No hay todavía un ejemplar o “instancia” de la clase (objeto) sino una referencia que
puede referir, apuntar, a un objeto. Las variables en Java pueden ser de tipo primitivo o referencias a
objetos. Una referencia a un objeto se puede entender como una variable que almacena la dirección en
memoria donde se encuentra el objeto. Hasta que se le asigna un objeto a la referencia, ésta no refie-
re a nada. En el ejemplo, en la segunda línea se crea un objeto de la clase Cuenta con el operador new
y se asigna a la referencia de la variable cuenta_1. En otras palabras, hacemos que la referencia refie-
ra, apunte, a algo.
Cuando un objeto ya existe, se pueden invocar sus métodos con el operador punto “.” como en el
Clases y Objetos 177

Referencia Objeto

cuenta_1 cuenta_1
Atributos (datos)
Procedimientos
(métodos)

Figura 7.9. Relación entre la referencia al objeto y el objeto en sí

caso del método println() en System.out.println(). El operador se pone a continuación del


nombre del objeto seguido por el método al que invocamos. No tiene sentido invocar directamente a
los datos o variables (ocultamiento de información).
Existiendo ya la clase Cuenta podríamos usarla en un programa, véase el Programa 7.1 donde
creamos una cuenta bancaria y se realizan varias operaciones con ella.

Programa 7.1. Programa que utiliza la clase Cuenta

class Banco {
public static void main(String [] args) {
double total_cuenta;

//Se crea la cuenta


Cuenta cuenta_1 = new Cuenta(123456, 250532);

// Se consulta el saldo
total_cuenta=cuenta_1.saldo();
System.out.println(“Total actual en la cuenta: “
+total_cuenta +” Euros”);

// Se hace un ingreso en la cuenta


cuenta_1.depositar(10000);

// Se consulta el saldo otra vez


total_cuenta=cuenta_1.saldo();
System.out.println(“Total actual en la cuenta: “
+total_cuenta +” Euros”);
}
}

La salida del Programa 7.1 sería:

Total actual en la cuenta: 250532.0 Euros


Total actual en la cuenta: 260532.0 Euros

7.7. REFERENCIAS
El concepto de referencia (relacionado con el de puntero en otros lenguajes) es muy importante. Es la
178 Introducción a la programación con orientación a objetos

base de la creación de estructuras dinámicas. Vamos a ilustrarlo partiendo del concepto de alias, sinó-
nimo, de un objeto.

7.7.1. CONCEPTOS GENERALES


El concepto de alias como “nombre” equivalente, sinónimo, es muy general en informática. Vamos a
considerarlo aplicado a referencias y objetos. En primer lugar, debemos saber que la asignación de
objetos no corresponde a una asignación de contenidos. En realidad, una referencia almacena la direc-
ción de memoria donde se encuentra el objeto, no el objeto en sí, véase la Figura 7.9.
Toda la interacción con el objeto transcurre a través de la referencia, lo que implica una relación
por variable y no por valor. Esto es así porque aunque manipulemos la referencia, por ejemplo, pasán-
dola a un método, el objeto referido siempre es el mismo (como ya vimos en el Capítulo 5). Esta “rela-
ción por variable” se nota en acciones tales como la asignación. Consideremos esta cuestión con más
detalle. Con variables de tipos primitivos de datos como en,

int numero_1=5;
int numero_2=12;

la asignación

numero_1=numero_2;

hace que numero_1 almacene lo mismo que numero_2, en este caso 12. Se trata de una asignación
del valor. Al final, la variables numero_1 y numero_2 contienen lo mismo. Es posible cambiar cual-
quiera de las variables sin que se modifique el contenido de la otra, ya que corresponden a zonas de
memoria distintas, independentemente de tener el mismo contenido, véase la Figura 7.10.

numero_1 numero_2
Antes de
la asignación
5 12

numero_1 numero_2
Después de
la asignación
12 12

Figura 7.10. Efecto de una asignación sobre un tipo primitivo de datos

En la Figura 7.10 los rectángulos simbolizan posiciones de memoria. Como podemos ver, hay una
relación directa entre el identificador y el contenido de la memoria. A efectos prácticos, el identifica-
dor “es” el contenido de la memoria.
Con objetos, una asignación no se refiere al contenido sino a la dirección de memoria donde se
almacena el contenido. Aquí tendríamos el identificador que corresponde a una posición de memo-
ria, la cual indica dónde está la zona de la memoria en la que se almacena el objeto, véase la Figu-
ra 7.11.
Clases y Objetos 179

Identificador: cuenta_1

Dirección de memoria

Objeto cuenta_1

Figura 7.11. Representación esquemática de la referencia a un objeto

La Figura 7.11 muestra que ahora existe un paso más. Comparando con el caso de una variable de
tipo primitivo mostrado en la Figura 7.10, observamos que ahora la referencia “es” la dirección de
memoria donde se encuentra el objeto. Para llegar al objeto primero examinaríamos el contenido de la
referencia para identificar la dirección de memoria, y después iríamos allí para manipular el objeto. En
el resto del capítulo simplificaremos el diagrama y simplemente representaremos el identificador refi-
riendo al objeto. Podemos decir que cuando creamos la referencia lo que hacemos es establecer la pri-
mera flecha del diagrama y cuando creamos el objeto enlazamos la referencia con el objeto “real” en
la memoria, véase la Figura 7.12.

Identificador: cuenta_1 Identificador: cuenta_1

Dirección de memoria Dirección de memoria

Objeto cuenta_1

Declaración Inicialización

Figura 7.12. Funcionamiento de la declaración de referencias y creación de objetos

A partir de la exposición anterior se explica el distinto funcionamiento de la asignación por refe-


rencia y por valor. En la asignación por referencia, si se cambia el contenido de un objeto usando una
referencia, se cambia el contenido del objeto al que se accede usando la otra, porque en realidad esta-
mos trabajando con el mismo objeto. Veamos un ejemplo paso a paso. Si hacemos,
180 Introducción a la programación con orientación a objetos

Cuenta cuenta_1= new Cuenta(1001,10000.0);


Cuenta cuenta_2= new Cuenta(1002,0.0);

inicialmente cuenta_1 y cuenta_2 referirán a dos objetos diferentes de tipo Cuenta, cada uno en
posiciones de memoria diferentes. Si hacemos la siguiente asignación,

cuenta_1=cuenta_2;

como trabajamos sobre referencias y no sobre datos primitivos, el resultado es diferente de la asigna-
ción de valores enteros. Aquí, lo que hacemos es que cuenta_1 adquiera el valor (dirección de memo-
ria) almacenado en cuenta_2. No hemos sustituido el contenido del objeto cuenta_1 con el objeto
cuenta_2, manteniendo dos zonas independientes de memoria. Lo que hemos hecho ha sido que la
referencia cuenta_1 no refiera o apunte al objeto cuenta_1, sino al cuenta_2, como muestra la
Figura 7.13.

cuenta_1 cuenta_2

Objeto cuenta_1 Objeto cuenta_2


Antes de
la asignación

cuenta_1 cuenta_2

Objeto cuenta_1 Objeto cuenta_2


Después de
la asignación

Figura 7.13. Efecto de la asignación de referencias

Las dos referencias originalmente referían a distintos objetos con espacio independiente para las
variables, cada objeto ocupa distinta posición de memoria. Después de la asignación, cuenta_1 y
cuenta_2 se refieren al mismo objeto, que es al que refería originalmente cuenta_2. Las dos refe-
rencias apuntan a la misma zona de memoria, la de cuenta_2, y el objeto cuenta_1 sigue ocupan-
do una zona de memoria, aunque no tiene ninguna referencia apuntando a él. Los nombres de los dos
objetos son ahora alias, sinónimos que refieren a la misma entidad. Los dos identificadores refieren a
lo mismo, así que los cambios realizados a través de uno o a través de otro lo son sobre el mismo obje-
to. En otras palabras, sólo hay un objeto con sus propios datos, pero se puede acceder a él de dos for-
mas. Cualquier cambio realizado a través de un alias se refleja en lo que ven todos los demás alias.
Clases y Objetos 181

Respecto a la referencia a objetos, ¿qué pasa cuando un objeto no tiene ya referencia que apunte a
él, como el objeto cuenta_1 en el ejemplo de la Figura 7.13? En este caso no hay forma de referir-
nos a dicho objeto. En principio el objeto se mantendría en memoria, ocupando espacio inútilmente.
Por esta razón, Java dispone de un mecanismo denominado recogida automática de basura que elimi-
na de la memoria los objetos que no se utilizan más. En los lenguajes orientados a objetos donde no
hay recogida automática de basura, es el usuario el que debe eliminar el objeto usando algún tipo de
método destructor.
Ahora podemos entender por qué los objetos se pasan por referencia. En realidad lo que se pasa es
la referencia al objeto y, estrictamente hablando, la referencia se pasa por valor. Lo que ocurre es que
ese “valor” de la referencia refiere a la posición de memoria donde se almacena el objeto, es decir, que
el nombre local (en el método) de la referencia es un alias del nombre externo. Por esa razón ambos
se refieren a la misma zona de memoria y los cambios que hagamos al objeto en el método se reflejan
al salir de él. Hay que tener en cuenta que cuando un método “devuelve” un objeto, en realidad devuel-
ve una referencia a ese objeto. La situación se ilustra en el ejemplo mostrado en el Programa 7.2 don-
de hay dos clases y un método de una de ellas que acepta un objeto de la otra.

Programa 7.2. Ilustración del manejo de referencias

class Clase1 {
private int valor=10;

public void modificar(Clase2 objt){ // Actúa sobre un objeto


objt.cambiar(valor); // de Clase2
}
} // Fin Clase1

class Clase2 {
private int indicador=0;

public void cambiar(int x) { // Modifica el valor de indicador


indicador=x;
}

public int indicador() {


return indicador; // Método de consulta
}
} // Fin Clase2

class Referencias {
public static void main(String [] args) {
int i;
Clase1 obj1 = new Clase1();

Clase2 obj2 = new Clase2();


i=obj2.indicador(); // Valor inicial
System.out.println(“i antes de aplicar el metodo modificar: “+i);

//Se pasa la referencia a obj2 como parámetro

6
En el campo de las estructuras de datos existe un tipo abstracto de datos (TAD) denominado lista. Este TAD es muy
flexible y puede considerarse como una estructura que puede crecer y decrecer según se necesite. Sus elementos pueden ser
accedidos, insertados o eliminados en cualquier posición de la lista. La lista de nuestro ejemplo no se corresponde al tipo abs-
tracto de datos lista, sino que es una simplificación didáctica del mismo. El lector interesado puede consultar algún texto espe-
cializado en estructuras de datos (Aho et al., 1987; Smith, 1987; Weiss, 1998).
182 Introducción a la programación con orientación a objetos

texto texto texto etc.

siguiente siguiente siguiente

Objeto 1 Objeto 2 Objeto 3

Figura 7.14. Enlazamiento de objetos a través de una referencia

obj1.modificar(obj2);
i=obj2.indicador(); // Modificación a través de la referencia
System.out.println(“i tras aplicar el metodo modificar: “+i);
}
}

El resultado es:

i antes de aplicar el metodo modificar: 0


i tras aplicar el metodo modificar: 10

En el método main del Programa 7.2 se construye un objeto de Clase1 y otro de Clase2. En pri-
mer lugar se usa el método indicador sobre obj2 para obtener el valor de la variable de ejemplar
indicador de obj2. En este caso el valor es 0. A continuación, al método modificar de Clase1
se le pasa la referencia al objeto obj2 de Clase2. Dentro del método modificar se declara un pará-
metro actual objt de Clase2, aplicándosele el método cambiar de la Clase2. Esto implica que la
variable indicador de objt se actualiza con el contenido de la variable valor de Clase1 que es
10. Al acabar el método modificar se vuelve al principal y se usa el método indicador sobre obj2
para obtener el valor de la variable indicador de obj2. Ahora el resultado es 10. Esto se debe a que
en el paso de parámetros al método modificar, el parámetro actual (obj2) y el formal (objt) devie-
nen en sinónimos del mismo objeto. Por eso las modificaciones que se realizan en objt se realizan en
el mismo objeto que el referido por obj2.

7.7.2. ESTRUCTURAS DINÁMICAS: LISTAS ENLAZADAS


El hecho de que tengamos referencias a objetos nos permite construir estructuras dinámicas. Es éste
un tema que no vamos a abordar en detalle en este libro, pero sí vamos a presentar un ejemplo senci-
llo de esta técnica.
Una matriz es una estructura estática en el sentido de que, una vez dimensionada, su tamaño no se
puede cambiar en tiempo de ejecución. En muchas ocasiones, sin embargo, se necesitan estructuras de
datos dinámicas, donde el tamaño de la misma pueda aumentar o disminuir, según se necesite, en tiem-
po de ejecución. Un ejemplo sería un programa que deba manipular una serie de artículos que vayan
llegando o consumiéndose continuamente. Otro ejemplo típico lo encontramos en la gestión de proce-
sos de un sistema operativo. El número de procesos aumenta o disminuye arbitrariamente según el tra-
bajo que se realice con el sistema. La estructura de datos unas veces tendrá que ir creciendo para
acomodar los nuevos elementos y cuando éstos se vayan eliminando habrá que ir reduciendo el tamaño
de la estructura. Estos problemas dinámicos se resuelven con el uso de referencias (o punteros en otros
lenguajes) para enlazar los objetos. La clave está en definir una clase que contenga una referencia (no
un objeto) a un objeto de la misma clase, como en el ejemplo siguiente,
Clases y Objetos 183

class Nodo {
String texto;
Nodo siguiente;
}

Aquí declaramos una referencia, siguiente, que puede referir a un objeto de clase Nodo. Fijé-
monos en que estamos sólo declarando la referencia y no creando un objeto. Ahora podríamos ir cre-
ando objetos de clase Nodo y encadenarlos, enlazarlos, haciendo que la referencia siguiente de uno
de ellos refiera al siguiente objeto creado, véase la Figura 7.14.
Hemos creado una lista enlazada 6 de objetos. El primer nodo en la lista se puede referenciar usan-
do una variable separada, el segundo, a partir del primero usando la referencia siguiente del primer
objeto de clase Nodo, etc. El último nodo de la lista tendrá una referencia siguiente que al no refe-
rir a nada contendrá el valor null, indicando el final de la lista.
Vamos a ver un ejemplo que ilustre de modo básico cómo se puede construir una lista enlazada.
Consideremos el siguiente ejemplo. Se trata de un programa que gestiona una serie de libros identifi-
cados por su título. El programa debe ser capaz de ir añadiendo libros a la serie mantenida y de poder
imprimir los títulos de toda la serie cuando se le indique. Con esta definición abordemos las etapas de
análisis y diseño.

Análisis
Identificamos una clase Lista que contendrá toda la información a manejar (en este caso solo el títu-
lo) y que realiza dos operaciones; añadir un libro a la lista e imprimir la lista.

Diseño
Implementaremos la lista como una estructura dinámica, como una lista enlazada. Así, en el dominio
de la solución identificamos una clase Nodo que representará a cada libro dentro de la lista y que
incluirá una referencia de la propia clase Nodo para el enlazamiento dinámico. Como operaciones, esta
clase debe poder conectar un objeto de la misma a otro, debe poder devolver el título para imprimirlo
y, para poder recorrer la lista, debe devolver el nodo siguiente a aquel en el que estamos. Por lo tanto,
necesitaríamos dos clases, una clase Lista y una clase Nodo que representaría cada elemento de la
lista. La relación entre ellas sería estructural, la lista está formada por un conjunto de nodos. El dia-
grama de clases correspondiente se muestra en la Figura 7.15. El programa principal creará un objeto
de clase Lista, añadirá algunos elementos y luego imprimirá la lista.

Lista Nodo
1 *
primero: Nodo titulo:String
siguiente:Nodo
Lista ( )
incluir (titulo:String):void Nodo ( )
imprimir ( ):void void poner (siguiente_nodo:Nodo)
coger ( ):Nodo
titulo ( ):String
184 Introducción a la programación con orientación a objetos

Programa 7.3. Ejemplo de lista enlazada (continuación)

Figura 7.15. Relación de asociación entre Lista y Nodo

Codificación
En el Programa 7.3 implementamos el diagrama de clases de la Figura 7.15 y un ejemplo de progra-
ma principal que usa dicha estructura de clases.

Programa 7.3. Ejemplo de lista enlazada

class Ejemplo_lista {
public static void main(String [] args) {

Lista lista_titulos =new Lista();


lista_titulos.incluir(“Don Quijote de la Mancha”);
lista_titulos.incluir(“Hamlet”);
lista_titulos.incluir(“El Principito”);
lista_titulos.imprimir();

} // Fin metodo main


} // Fin clase principal

class Nodo {
private String titulo;
private Nodo siguiente; /*Se refiere al siguiente elemento
de la lista */

public Nodo(String cadena) {


titulo=cadena;
siguiente=null;
} // Fin constructor

public void poner(Nodo siguiente_nodo) {


siguiente=siguiente_nodo;
}

public Nodo coger() {


return siguiente;
}

public String titulo() {


return titulo;
}
} // Fin clase Nodo

class Lista {
private Nodo primero;

public Lista() {
primero=null; // Almacena el primer elemento de la lista
Clases y Objetos 185

} // Fin constructor

public void incluir(String cadena) {

Nodo elemento = new Nodo(cadena);


if (primero==null) {
primero=elemento; /*la asignación implica el enlazar la
referencia con el objeto */
}
else {

Nodo aux;

// Se pone en el último para añadir al final


for (aux=primero;aux.coger()!= null;aux=aux.coger());

aux.poner(elemento);
}
} // Fin metodo incluir

public void imprimir() {

for( Nodo aux=primero; aux != null; aux=aux.coger()) {


System.out.println(aux.titulo());
}
}
} // Fin clase Lista

Es interesante destacar en el Programa 7.3 cómo la clase Lista usa una referencia de clase Nodo
llamada primero para almacenar el primer nodo de la lista. Téngase en cuenta que a diferencia de una
matriz, en nuestra lista no hay un índice que nos permita alcanzar directamente uno de los nodos. Lo
único que podemos hacer es salvar el primer nodo de la lista y empezar a movernos a partir de él. Tam-
bién merecen especial atención los métodos imprimir e incluir de la clase lista. Considerémoslos
por separado.

a) Método imprimir

public void imprimir() {

for( Nodo aux=primero; aux != null; aux=aux.coger()){


System.out.println(aux.titulo());
}
}

La condición de finalización del bucle for es que aux sea nulo. De esta forma vamos recorrien-
do la lista hasta llegar al último objeto referido que es el objeto nulo (null). Así, se imprimen todos
los títulos. Si en lugar de aux!=null pusiéramos aux.coger()!= null, como se muestra a conti-
nuación:

for( Nodo aux=primero; aux.coger() != null; aux = aux.coger()){


System.out.println(aux.titulo());
186 Introducción a la programación con orientación a objetos

el último título no se imprimiría porque al llegar al penúltimo nodo aux.coger() ya devuelve null.

b) Método incluir
public void incluir(String cadena) {

Nodo elemento = new Nodo(cadena);


if (primero==null) {
primero=elemento;
}
else {

Nodo aux=primero;
for (aux=primero;aux.coger()!= null;aux = aux.coger());

aux.poner(elemento);
}
} // Fin metodo incluir

Fijémonos que cuando primero==null se hace primero=elemento y no


primero.poner(elemento). Esto es así porque cuando primero==null no refiere a un objeto de
clase Nodo. De hecho no refiere a nada, así que no existe un campo siguiente en primero sobre el
que pueda actuar el método poner. Lo que hay que hacer es la asignación, primero=elemento.
Ahora la referencia primero sí apunta a un objeto de clase Nodo, en concreto al objeto elemento. A
partir de este momento lo que haremos para añadir un nodo nuevo será partir del primer nodo (identi-
ficado por primero) y movernos hasta el final de la lista con el bucle for,

for (aux=primero;aux.coger()!= null;aux = aux.coger());

Tabla 7.1. Modificadores de visibilidad en Java

Modificador Clases e Interfaces Métodos y variables


default (no Visible en su paquete Accesibles desde cualquier clase en el mismo
modificador) paquete que su clase, pero no sus subclases
que no estén en el mismo paquete

public Visible en cualquier lugar Accesibles desde cualquier lugar

protected No se aplica Accesibles desde cualquier clase en elmismo


paquete que su clase o desde cualquier
subclase aún en paquetes distintos

private No se aplica Accesible sólo desde la propia clase

7
En el Capítulo 8 trataremos el concepto de paquetes con detalle. De momento baste considerar un paquete como un
conjunto o biblioteca de clases identificadas con un nombre.
8
Las interfaces representan un mecanismo adicional de abstracción basado en herencia y como tal se tratarán en deta-
lle en el Capítulo 8. De momento baste con considerarlas como un tipo especial de clases.
Clases y Objetos 187

Obsérvese que el alcance del bucle es una sola sentencia. Dicho de otra forma, el bucle se usa para
recorrer la lista entera hasta que la referencia aux refiera al último nodo. Es en la siguiente sentencia,
ya fuera del bucle, donde asignamos el nodo nuevo.

7.8. MODIFICADORES
Comencemos definiendo qué es un modificador. Un modificador es una palabra reservada que espe-
cifica una característica particular de un elemento de un lenguaje de programación (Winder y Roberts,
2000). Por ejemplo, la palabra final, que es modificador usado para declarar constantes. En Java las
clases, los objetos y los miembros de las clases pueden estar afectados por modificadores. Veamos los
distintos tipos de modificadores.

7.8.1. MODIFICADORES DE VISIBILIDAD


Los modificadores de visibilidad se usan para especificar dónde se puede usar la entidad declarada.
Este tipo de modificadores especifica las características de acceso de los miembros (datos y métodos,
incluyendo constructores) de una clase. Estos modificadores se denominan de visibilidad porque con-
trolan en qué medida un miembro de la clase puede ser accedido y referido. Así, estos modificadores
nos permiten definir las características de encapsulación de un objeto. En Java (y semánticamente en
general en orientación a objetos) tenemos tres modificadores de visibilidad:
a) public
b) private
c) protected

Estos tres modificadores se pueden aplicar a las variables y a los métodos de una clase. Vamos
a considerar en este capítulo los dos primeros. El tercero lo detallaremos cuando hayamos visto
herencia y paquetes.
Cuando una variable o método va precedido del modificador public se puede invocar desde fue-
ra de la clase a la que pertenece. Cuando el modificador es private (privado) la variable o método
sólo se puede invocar desde dentro de la clase, no se puede invocar externamente pero puede ser usa-
do en cualquier lugar dentro de la definición de la clase. Lógicamente, los métodos constructores son
forzosamente de tipo public, pues hay que invocarlos desde fuera de la clase. Si no se indica una visi-
bilidad específica, se asigna el valor por defecto que implica que el miembro (dato, método) es acce-
sible sólo a las clases del mismo paquete 7.
Un cliente de una clase (un objeto) debe ser capaz de referirse a los métodos de servicio (la inter-
faz pública), pero no debe ser capaz de invocar directamente a los métodos internos (de soporte).
Por tanto, debe usarse el modificador de visibilidad public para los métodos de servicio, pero cual-
quier método que no defina un servicio debe declararse private (privado). Por ejemplo, el méto-
do main siempre es público porque es ejecutado directamente por el intérprete de Java desde el
exterior. Como ya hemos indicado, un objeto encapsula información y las variables sólo se deben
cambiar a través de un método del propio objeto. Por tanto, todas las variables deberían declararse
private. En resumen,

Métodos de servicio: public.


Métodos de soporte: private.
Atributos (datos): private.
188 Introducción a la programación con orientación a objetos

Programa 7.4. Ejemplo del uso de variables estáticas (continuación)

Como se indicará más adelante si queremos que los métodos o variables sean accesibles a través
de una relación de herencia, pero no a clases fuera de dicha relación, habría que declararlos como
protected (protegido) y no private. De momento usaremos el modificador private.
Los modificadores de visibilidad también se aplican a las clases. En este caso sólo se puede utili-
zar el modificador public. Con public la clase será accesible desde cualquier sitio, no estando res-
tringido dicho acceso a relaciones de herencia o de pertenencia al mismo paquete.
El efecto de los modificadores resulta más claro en forma tabular, véase la Tabla 7.1 8:

En notación UML los tres modificadores se indican en los miembros (datos, métodos) de una cla-
se precediendo su nombre con un + para los públicos, un # para los protegidos, y un - para los priva-
dos (Booch et al., 2000; Rumbaugh et al, 2000), véase la Figura 7.16.

Clase Ejemplo
# dato_protegido
- dato_privado
+ método_públido ( )
# método_protegido ( )
- método_privado ( )

Figura 7.16. Notación UML para denotar miembros públicos, protegidos y privados de una cla-
se

7.8.2. MODIFICADOR STATIC


Este modificador asocia una variable o método con la clase y no con objetos de la clase. Veamos el
efecto sobre variables y métodos.

a) Variables static
De momento hemos visto dos tipos de variables, las variables locales, que son las declaradas dentro
de un método y las variables de ejemplar, que están declaradas en la clase pero no dentro de un méto-
do. Estas variables se denominan de ejemplar porque se accede a ellas a través de un ejemplar parti-
cular (un objeto) de una clase. Cada objeto tiene espacio de memoria distinto para cada variable, es
decir, cada objeto puede almacenar un valor distinto en esa variable.
Hay otra clase de variables, las denominadas variables static (estáticas) o variables de clase. Se
construyen usando el modificador static. Éste hace que una variable sea “global” a todos los obje-
tos de la clase. Dicho de otra manera, sólo hay una copia en memoria de esa variable y se comparte
por todos los objetos de la clase. Cambiar el valor de una de estas variables en un objeto lo cambia en
todos los demás de la misma clase. Las variables static no se pueden declarar dentro de los méto-
dos, porque serían locales al método en cuestión. Las variables con atributo de static son accesibles
a través del nombre de la clase o del nombre de un objeto de dicha clase.
Las constantes, que llevan el modificador final, a menudo se declaran como estáticas. Así, el
valor de la constante será compartido por todos los objetos de la clase. Al igual que las variables está-
Clases y Objetos 189

Programa 7.5. Ejemplo del uso de una variable y un método estáticos (continuación)

ticas, las constantes estáticas no se pueden declarar dentro de un método.


Las variables estáticas son muy útiles para controlar el número de objetos que se van creando de
una clase concreta. El Programa 7.4 muestra un ejemplo que incrementa una variable estática cada vez
que se crea un objeto de la clase Alumno.

Programa 7.4. Ejemplo del uso de variables estáticas

class Alumno{
private String nombre;
private int matricula;
static long numero;
public Alumno(){
numero = numero+1; // Incremento de la variable estática
}
}//fin clase Alumno
class Principal{
public static void main(String args[]){
Alumno alumno1 = new Alumno();
System.out.print(Alumno.numero);
System.out.print(alumno1.numero);
Alumno alumno2 = new Alumno();
System.out.print(Alumno.numero);
}// fin main
}//fin clase Principal

La salida del Programa 7.4 sería:


112

Como el Programa 7.4 muestra, se puede acceder a la variable estática usando el nombre de la cla-
se (Alumno.numero) o el del objeto (alumno1.numero). La segunda forma es mejor evitarla para
no dar la falsa impresión de que numero es una variable de ejemplar.

b) Métodos static
Los métodos también pueden declararse de tipo static. Se habla entonces de métodos estáticos o de
clase. Los métodos estáticos están asociados a la clase y no a ejemplares de dicha clase. Un método
estático no se invoca sólo a través de un objeto (“instancia”) de una clase, sino también a través de la
clase misma. Por eso no hace falta tener un objeto de esa clase para poder invocarlo.
Los métodos estáticos no operan en el contexto de un objeto particular, por esta razón no pueden
usar variables de ejemplar, las cuales sólo existen en un ejemplar de una clase. El compilador daría un
error. Un método estático sólo puede usar variables que sean estáticas o locales al método, porque si
no, la variable no existiría hasta que no haya un objeto de la misma. Con las variables estáticas no hay
problema, porque éstas existen independientemente de objetos específicos.
El método main de una clase en Java debe declararse de tipo static para poder invocarlo sin que
exista un objeto de dicha clase. Otros ejemplos son los métodos de la clase Math en el paquete
java.lang como:

double pow(double a, double b) ‡ devuelve ab


double cos(double a) ‡ devuelve cos (a)
190 Introducción a la programación con orientación a objetos

double sqrt(doubla a) ‡ devuelve -a


double abs(int a) ‡ devuelve el valor absoluto de a en doble precisión

Como estos métodos son de tipo estático se pueden invocar sin crear un objeto de clase Math. En
el Programa 7.5 se ilustra el uso de un método y variables estáticas desde el método main.

Programa 7.5. Ejemplo del uso de una variable y un método estáticos

class SomosEstaticos{
static int estatica1=13;
static int estatica2=80;
static void estatico( ){
System.out.println(“variable1= “ + estatica1);

}
}

class Principal2{
public static void main(String args[]){
SomosEstaticos.estatico();
System.out.println(“variable2= “+ SomosEstaticos.estatica2);
System.out.print(“variable1= “+ SomosEstaticos.estatica1);

}
}

La salida del Programa 7.5 sería:

variable1=13
variable2=80
variable1=13

Obsérvese cómo el método main del Programa 7.5 invoca al método estático a través del nombre
de la clase y cómo se accede a las variables estáticas a través del nombre de la clase.

EJERCICIOS PROPUESTOS
Ejercicio 1.* El siguiente diagrama de clases, incompleto, representa la estructu-
ra básica de un programa para recopilar los encargos de productos
de una compañía.
Clases y Objetos 191

Encargo Cliente
encargoID
fecha 1 * nombre
valor dirección

pago ( )

*
Producto Cliente corporativo Cliente particular

ID cuenta tarjeta Nmr


coste

pago ( ) pago ( )

Escriba una versión en Java de la clase Producto. Incluya métodos


que permitan acceder a la información del producto.

Ejercicio 2.* Una empresa de desarrollo de software tiene una serie de emplea-
dos que forman parte de un equipo de desarrollo. Estos empleados
pueden ser contratados, cobrando por horas, o bien programadores
en plantilla con un sueldo fijo. En el equipo, uno/a de los programa-
dores/as en plantilla actúa como director/a cobrando un comple-
mento adicional. Cree un diagrama de clases que represente las
relaciones existentes en el sistema descrito.

Ejercicio 3.* Una universidad está formada por una serie de departamentos a los
que están asignados los distintos profesores. Cada curso impartido
está vinculado a un departamento y cada profesor puede impartir
uno o más cursos. Los profesores pueden ser titulares o asociados.
Los alumnos pertenecen a la universidad y asisten a uno o más cur-
sos. A su vez, los alumnos pueden ser de dos tipos: de curso com-
pleto o de curso de verano. Cree un diagrama de clases que muestre
las distintas relaciones existentes en este modelo de universidad.

Ejercicio 4.* Considere una recta en el plano cartesiano. Implemente una estruc-
tura de clases que permita obtener su pendiente y su ordenada en
el origen. Para ello se debe poder caracterizar la recta por medio de
dos puntos o por medio de un punto y de la pendiente. También
debe ser posible determinar si los dos puntos que se pasan son igua-
les o no, y cuál es el valor de y (ordenada) que corresponde a un
valor de x (abscisa) determinado.

Ejercicio 5.* Implemente una estructura de clases que represente una serie de
personas caracterizadas por el nombre (compuesto de nombre de
pila y dos apellidos) y el número del DNI. Debe ser posible imprimir
192 Introducción a la programación con orientación a objetos

los datos completos de una persona y devolver el nombre o el DNI


independientemente.

Ejercicio 6.* Modifique el ejemplo anterior para poder construir un árbol gene-
alógico donde se establezca dinámicamente un vínculo que indique
qué persona es el padre y cuál la madre de una persona dada.

Ejercicio 7.* ¿Cuál es el resultado del siguiente programa?

class Ejercicio {
public static void main(String [ ] args){
Clase1 obj1=new Clase1();
obj1.imprimir(3.2);
}
}
class Clase1 {
private double valor=9.8;
public void imprimir(double valor) {
System.out.println(valor);
}
}
Ejercicio 8.* Desarrolle un programa que sirva para evaluar el valor de un polino-
mio, cuyo grado y coeficientes se introducen por teclado, en un
valor de abscisa determinado.

REFERENCIAS
AHO, A. V., HOPCROFT, J. E. y ULLMAN, J. D.: Data Structures and Algorithms, Addison-Wesley, 1987.
ARNOW, D. y WEISS, G.: Introducción a la programación con Java, Addison-Wesley, 2001.
BlueJ: http://www.bluej.org, última visita realizada en junio 2002.
BOOCH, G., RUMBAUGH, J. y JACOBSON, I.: El Lenguaje Unificado de Modelado, Addison Wesley, Primera reim-
presión, 2000.
BRUEGGE, B. y DUTOIT, A. H.: Ingeniería del software orientado a objetos, Primera Edición, Prentice-Hall, 2002.
LARMAN, C.: UML y Patrones, Prentice Hall, Primera Edición, 1999.
LEWIS, J. y LOFTUS, W.: Java Software Solutions, Addison-Wesley, 1998.
MEYER, B.: Construcción de Software Orientado a Objetos, Segunda Edición, Prentice-Hall, 1999.
MILLER, G. A.: “The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing
Information”, Psychol. Rev., 63(2), 81-97, 1956.
PRESSMAN, R. S.: Ingeniería del Software, Quinta Edición, Mc Graw Hill, 2002.
RUMBAUGH, J., JACOBSON, I., y BOOCH G.: El Lenguaje Unificado de Modelado. Manual de Referencia, Addison-
Wesley, 2000.
SMITH, H. E.: Data Structures. Form and Function, Harcourt Brace Jovanovich, Publishers, 1987.
WEISS, M. A.: Data Structures & Problem Solving using Java, Addison-Wesley, 1998.
WINDER, R. y ROBERTS, G.: Developing Java Software, John Wiley & Sons, Ltd, 2000.
8

Herencia

Sumario

8.1. Introducción 8.3. Mecanismos adicionales de abstracción


8.2. Herencia basados en herencia
8.2.1. Concepto de herencia 8.3.1. Clases y métodos abstractos
8.2.2. Enmascaramiento de variables y 8.3.2. Interfaces
sobrescritura de métodos
8.2.3. Jerarquías de clases
8.2.4. Organización de jerarquías de clases
(paquetes)
8.2.5. Polimorfismo
194 Introducción a la programación con orientación a objetos

8.1. INTRODUCCIÓN
Las tres características fundamentales que definen la programación orientada a objetos son: encapsu-
lación, herencia y polimorfismo. En el capítulo anterior hemos visto la utilidad de la abstracción,
expresada a través de la encapsulación de información y procedimientos, e implementada por medio
de los conceptos de clase y objeto. Encapsulando aumentábamos la cohesión y disminuíamos el aco-
plamiento en los programas, lo que se traducía en una mayor reutilizabilidad y fiabilidad del softwa-
re, mayor facilidad de desarrollo, de pruebas y de mantenimiento (aumento de la calidad del software).
Otra característica básica de la orientación a objetos es la herencia. La herencia es un mecanismo
de abstracción consistente en la capacidad de derivar nuevas clases a partir de otras ya existentes. La
herencia se suele usar cuando la clase hija y la padre comparten código común. De esta manera no es
necesario repetir ese código común, sino que éste se transmite (se hereda) de la clase padre a la clase
hija. Así, podemos centrarnos sólo en las características propias de la clase hija que se está desarro-
llando. La herencia permite reutilizar código y también mantener la complejidad de las nuevas clases
dentro de límites manejables. Además, por medio de la herencia podemos construir jerarquías de clases
que podemos manejar cómodamente a través de los paquetes de clases, como veremos más adelante.
Otra característica clave de la orientación a objetos es el polimorfismo. El polimorfismo permite
que diferentes objetos puedan responder al mismo mensaje en diferentes formas (Arnow y Weiss,
1998). Usando polimorfismo podemos tratar de forma unificada diferentes clases relacionadas por
herencia. Como veremos, el polimorfismo involucra la denominada sobrescritura de métodos y la uti-
lización de referencias.
La característica de herencia dota de una gran potencia a la programación orientada a objetos. De
hecho, para ser estrictos, la programación usando objetos pero sin herencia se denomina programación
basada en objetos. La programación orientada a objetos implica el uso de herencia (Joyanes, 1998).

8.2. HERENCIA
La herencia es una técnica de desarrollo de software muy potente y, como hemos visto, una carac-
terística que define la programación orientada a objetos. La herencia relaciona los datos y métodos de
clases nuevas con los de clases ya existentes, de forma que la nueva clase se puede entender como una
extensión de la antigua. En cualquier caso, la nueva clase es un tipo particular de la clase original.
Abordemos el tratamiento de esta característica de la orientación a objetos.

8.2.1. CONCEPTO DE HERENCIA


Como ya hemos indicado, la herencia en programación orientada a objetos permite crear una clase
nueva a partir de otra ya existente. La nueva clase contendrá automáticamente algunos o todos los atri-
butos (variables) y procedimientos (métodos) de la clase original. El diseñador de software puede aña-
dir nuevas variables y métodos a la clase nueva, o bien modificar las variables y métodos heredados
para definir de manera apropiada la nueva clase. Las nuevas clases que se crean usando herencia se
construyen más rápidamente al aprovechar el código heredado, y son más fáciles de manejar al ser
menos complejas que si tuvieran que incluir todo el código. Por todo esto consumen menos esfuerzo
de desarrollo. En el corazón de la herencia subyace la idea de reutilizabilidad del software. Usando
componentes software ya existentes para crear otros nuevos, sacaremos partido de todo el esfuerzo que
se hizo en el diseño, implementación y pruebas del software existente.
Consideremos en más detalle la herencia de clases. La palabra clase proviene de la idea de clasi-
ficar grupos de objetos con características similares. Los esquemas de clasificación usan normalmen-
te niveles jerárquicos que se relacionan. Por ejemplo, todos los mamíferos comparten ciertas
características: sangre caliente, pelo, etc. Un subconjunto serían los caballos. Todos los caballos son
Herencia 195

mamíferos, pero además tienen características que los hacen diferentes de los otros mamíferos. En tér-
minos de software, tendríamos una clase, Mamíferos, que tendría ciertas variables y métodos que des-
cribirían el estado y comportamiento de los mamíferos. La clase Caballo se podría derivar de la clase
Mamífero, heredando automáticamente las variables y métodos que contiene la clase Mamífero.
Además, se pueden añadir nuevas variables y métodos a la clase Caballo, la clase derivada, que defi-
nan al Caballo. A su vez, la clase Mamífero sería un tipo particular de vertebrado que a su vez es un
tipo particular de animal. La Figura 8.1 muestra una jerarquía de clases que va desde lo más general
(animal) a lo más concreto (caballo).
El proceso de derivación de una clase nueva por herencia corresponde a la existencia de una rela-
ción particular entre las dos clases: la relación es-un que vimos en el capítulo anterior. En esta rela-
ción, la clase derivada es una versión más específica de la original. Esto debe estar bien claro, la
herencia implica que la clase nueva es un tipo particular de la clase original. Así, todos los ejemplares
de la clase nueva pertenecen al tipo de la clase antigua, pero no al revés. Por ejemplo, dada la relación
de herencia entre caballo y mamífero se puede decir que Babieca (ejemplar de la clase caballo) es un
mamífero. Esto no quiere decir que todos los mamíferos sean caballos, pero sí que todos los caballos
son mamíferos. Como notación, indiquemos que la clase original que se usa para derivar una clase
nueva se denomina clase padre, superclase, clase base o ascendiente. La clase derivada se llama clase
hija, subclase o descendiente.
Los lenguajes orientados a objetos proporcionan mecanismos para implementar la herencia. En
Java se usa la palabra reservada extends para indicar que una clase nueva está siendo derivada de (es
decir que extiende a) otra. La sintaxis básica es:

class clase_hija extends clase_ padre {


--- contenido de la clase ---
}

Animal

Invertebrado Vertebrado

Reptil Ave Mamífero

Caballo

Figura 8.1. Organización jerárquica (de parte) del reino animal. El diagrama utiliza la notación
UML para la relación de herencia
196 Introducción a la programación con orientación a objetos

Por ejemplo, si existe una clase Vehículo y queremos derivar una nueva clase Coche haríamos,

class Coche extends Vehiculo {


--- contenido de la clase ---
}

La sintaxis especificada representa la definición de la clase clase_hija. La clase hija automáti-


camente hereda los métodos y variables de la clase clase_ padre. Podemos imaginar que hemos
copiado el código de la clase padre y lo hemos pegado en la clase hija, pero sin necesidad de hacerlo
explícitamente. Las variables y métodos heredados pueden usarse en la clase clase_hija como si
hubieran sido declarados localmente en dicha clase. Además, las variables y métodos heredados retie-
nen sus características de visibilidad originales en la subclase. Es importante indicar que la herencia
actúa en una dirección: de la clase padre a la clase hija. Esto implica que las variables y los métodos
nuevos declarados en la clase hija no pueden usarse en la clase padre.
Desde un punto de vista general la herencia puede ser múltiple o simple. Existe herencia múltiple
cuando podemos heredar de varias clases y herencia simple cuando sólo se puede heredar de una cla-
se, tal y como ilustra la Figura 8.2.
Dependiendo del lenguaje es posible disponer de herencia múltiple o simple. En Java, sólo es posi-
ble la herencia simple.
Las variables y los métodos que se heredan vienen controlados por los modificadores de visibilidad.
Los miembros con visibilidad public se heredan y los que tienen visibilidad private no se heredan.
Si éstas fueran las únicas posibilidades, la herencia serviría de poco, pues todos los miembros (datos,
métodos) heredables tendrían que ser públicos. En estas condiciones, al tener variables públicas per-
deríamos las ventajas de la encapsulación. Para conseguir que el comportamiento para clases foráneas
sea privado pero para las clases derivadas sea público usamos el modificador protected. El modifi-
cador protected establece un nivel intermedio de protección entre un acceso public y uno priva-
te. Los miembros (métodos y variables) de una superclase o clase padre etiquetados como protected
son accesibles para las subclases (clases hijas) y las otras clases del mismo paquete 1. Se recomienda al
lector revisar la tabla de modificadores de visibilidad que se presentó en el Apartado 7.8 del capítulo
anterior.
Vamos a ver un ejemplo de herencia. Consideremos un programa que maneja publicaciones y que
tiene que trabajar con tesis doctorales. Puesto que las tesis (todas) son un tipo particular de publica-
ción tenemos una relación de herencia. Consideremos que las publicaciones, entre otros atributos,
están caracterizadas por el título, los autores y el año de publicación. A su vez, las tesis tendrán tam-

Clase Padre_1 Clase Padre_2 Clase Padre

Clase Hija Clase Hija

Figura 8.2. Relación entre clases en los casos de herencia múltiple y simple usando notación
UML

1
El concepto de paquete de clases se expone posteriormente en este capítulo en relación con la organización de rela-
ciones jerárquicas.
Herencia 197

bién estos atributos generales (año, autor, título). Sin embargo, algo que distingue una tesis de otro tipo
de publicación (como un libro o una revista) es que se presenta en un departamento universitario. Si
representamos la clase publicación y la clase tesis con los datos indicados y con métodos de retorno
para estos datos tendríamos el diagrama de clases de la Figura 8.3.
La relación de herencia entre la clase Publicacion y la clase Tesis mostrada en la Figura 8.3
se implementaría de la forma siguiente:

Publicación

titulo:String
autores:String
fecha_publicacion:int [ ]

titulo ( ):String
autores ( ):String
fecha ( ):int [ ]

Tesis

departamento:String

departamento ( ):String

Figura 8.3. Relación de herencia entre las clases Publicación y Tesis usando notación UML

class Publicacion {
protected String titulo;
protected String autores;
protected int[]fecha_edicion=new int [3];

public String titulo() {


return titulo;
}
public String autores() {
return autores;
}
public int [] fecha() {
return fecha_edicion;
}
} // Fin clase publicación

class Tesis extends Publicacion {


protected String departamento;

public Tesis(String titulo, String autores,


String departamento,int dia,
int mes, int agno) {
this.titulo=titulo;
this.autores=autores;
this.departamento=departamento;
198 Introducción a la programación con orientación a objetos

fecha_edicion [0]=dia;
fecha_edicion [1]=mes;
fecha_edicion [2]=agno;
}

public String departamento() {


return departamento;
}
}

Las variables fecha_edicion, titulo y autores se han declarado protected, por lo que se
puede heredar en la clase hija pero no se puede acceder directamente a ellas desde clases externas a la
relación de herencia. Un ejemplo que usa las dos clases implementadas anteriormente se presenta en
el Programa 8.1.

Programa 8.1. Programa que usa la relación de herencia entre las clases Publicacion y
Tesis

class Herencia {
public static void main(String [] args) {
Tesis una_tesis= new Tesis
(“Simulacion de sistemas biologicos”,
“Francisco Perez”,”Informatica”,1,3,2002);

System.out.println(“Titulo: “ +una_tesis.titulo());
System.out.println(“Autor/es: “ +una_tesis.autores());
System.out.println(“Departamento: “ +una_tesis.departamento());

int [] fecha = una_tesis.fecha();


System.out.println(“Fecha: “ +fecha[0]+”/”+fecha[1]
+”/”+fecha[2]);
}
}

El resultado del Programa 8.1 sería,

Titulo: Simulacion de sistemas biologicos


Autor/es: Francisco Perez
Departamento: Informatica
1/3/2002

Fijémonos en que desde el objeto una_tesis estamos llamando a métodos que se han heredado y
que se manejan variables que también se han heredado. Sin embargo, la variable departamento y el
método departamento() que se declaran en la clase hija no se podrían invocar desde objetos de la cla-
se padre.
Cuando hablamos de heredar métodos surge la cuestión de qué pasa con los constructores. No tie-
ne sentido que un método constructor se herede, puesto que su misión es crear ejemplares de una cla-
se dada. Si estamos en una clase hija querremos crear ejemplares de ella y no de la superclase. Los
constructores no se heredan ni aunque tengan visibilidad pública. Sin embargo, los constructores se
suelen usar para inicializar variables y puede interesarnos que las variables de la clase padre que se
heredan se inicialicen cuando creemos un objeto de la clase hija. De esta forma, si existen varias cla-
Herencia 199

ses hijas no tendríamos que estar repitiendo el código de inicialización de las variables heredades en
todas y cada una de ellas. Para ello, ¿habría alguna forma de inicializar estas variables como hace el
constructor de la clase padre pero sin crear un objeto de la clase padre? En Java es posible conseguir-
lo con la referencia super (de superclase) que obedece a la siguiente sintaxis:

super(lista_de_ parámetros;

donde, lista_de_ parámetros especifica los parámetros del constructor de la superclase. Si se uti-
liza super(), tiene que ser la primera sentencia ejecutada dentro del constructor de la subclase. Como
el constructor puede estar sobrecargado en la superclase, super() puede ser llamado utilizando cual-
quier forma definida en la superclase. El constructor ejecutado será aquel que tenga la misma firma.
En una jerarquía de herencia de varios niveles, super() siempre se refiere a la superclase inmediata-
mente superior a la clase que lo utiliza. Usando esta superreferencia lo que conseguimos es utilizar el
código del constructor de la clase padre. El comportamiento es el que obtendríamos si hubiéramos
copiado el código del constructor de la clase padre y lo hubiéramos pegado en el constructor de la cla-
se hija. Veamos una variación de la implementación anterior de la estructura jerárquica de clases
Publicación y Tesis donde se usa la referencia super().

class Publicacion {
String titulo;
String autores;
protected int[]fecha_edicion=new int [3];

// Método constructor
public Publicacion(String titulo, String autores,
int dia, int mes, int agno) {
this.titulo=titulo;
this.autores=autores;
fecha_edicion [0]=dia;
fecha_edicion [1]=mes;
fecha_edicion [2]=agno;
}
// Métodos de servicio
public String titulo() {
return titulo;
}
public String autores() {
return autores;
}
public int [] fecha() {
return fecha_edicion;
}
} // Fin clase publicación

class Tesis extends Publicacion {


protected String departamento;

public Tesis(String titulo, String autores,


String departamento,
int dia, int mes, int agno) {
super(titulo,autores,dia,mes,agno); // Uso de super
this.departamento=departamento;
}

public String departamento() {


return departamento;
200 Introducción a la programación con orientación a objetos

}
}

En la clase Tesis se ha definido un nuevo constructor usando super. En el método constructor


Tesis hemos inicializado las variables heredadas del padre usando la referencia super. No se ha
creado ningún objeto de la clase padre, pero sí hemos podido inicializar usando el código del cons-
tructor Publicacion. En realidad super ejecuta el constructor de la clase padre, pero sin crear un
ejemplar de dicha clase.
Otra cuestión a tener en cuenta es que los miembros (datos y métodos) declarados como privados
en la clase padre, y que por lo tanto no se heredan, existen y se usan normalmente si el hijo invoca a
un método heredado del padre que a su vez usa esa/s variable/s o métodos. Un ejemplo típico serían
los métodos privados de soporte que usa un método del padre que se hereda. Cuando el método here-
dado se invoque desde el hijo dichos métodos funcionarán normalmente. No es necesario declarar
como protected los métodos de soporte.

8.2.2. ENMASCARAMIENTO DE VARIABLES Y SOBRESCRITURA


DE MÉTODOS

Supongamos que en una clase hija declaramos una variable o definimos un método con el mismo nom-
bre o firma que una variable o método heredadas, ¿qué ocurre en este caso? Veámoslo, considerando
en primer lugar las variables y luego los métodos.

a) Variables
Cuando una variable se hereda y en la clase hija declaramos una nueva variable con el mismo identi-
ficador, la nueva variable es la que se usa. Se dice que hemos enmascarado la variable original. No es
que hayamos reasignado su contenido, sino que hemos creado una variable nueva. Lógicamente, cuan-
do desde la clase hija usemos el identificador de la variable se usa la variable definida en la clase hija,
con el valor que se le haya asignado. La variable de la clase padre sigue existiendo, y se podría acce-
der a ella pero indicando explícitamente que nos referimos a la clase padre con el prefijo super. En
el Programa 8.2 se ilustra un ejemplo.

Programa 8.2. Ilustración del enmascaramiento de variables

class Padre{
protected int dato=10;

} // Fin clase Padre

class Hija extends Padre {


private int dato;

public Hija(int dato) {


this.dato =dato;
}
Herencia 201

Programa 8.2. Ilustración del enmascaramiento de variables (continuación)

public void imprime_hija() {


System.out.println(“Valor de dato en Hija: “+dato);
}
public void imprime_ padre() {
System.out.println(“Valor de dato en Padre: “
+super.dato);
}
}
class Herencia {
public static void main(String [] args) {
Hija ejemplo = new Hija(5);
ejemplo.imprime_hija();
ejemplo.imprime_ padre();
}
}

El resultado del Programa 8.2 sería,

Valor de dato en Hija: 5


Valor de dato en Padre: 10

Como podemos ver en el Programa 8.2, dato en Padre almacena el valor 10. En Hija, dato
se enmascara recibiendo ahora el valor 5. Esto no implica que la antigua variable haya desapareci-
do, sino que en la clase Hija cuando usemos el identificador dato nos vamos a referir a la defini-
ción local. La otra variable dato no ha desaparecido y podemos acceder a ella por medio del prefijo
super, como hacemos en el método imprime_ padre(). Como se puede comprobar en el Progra-
ma 8.2, el enmascaramiento de variables suele producir código confuso, por lo que se recomienda
evitar su uso.

b) Métodos

Un comportamiento similar ocurre cuando definimos un método con la misma firma (nombre y pará-
metros) que otro que se hereda. En este caso, el método al que se accede a través de los objetos de la
clase hija es al definido en la clase hija. Se dice que el nuevo método sobrescribe el método heredado.
Es importante distinguir la sobrescritura de la sobrecarga:

— Sobrecarga: El mismo identificador pero distinta firma (diferentes parámetros).


— Sobrescritura: La misma firma que un método heredado.

Como vemos, la sobrescritura está asociada a la herencia. Si pretendemos sobrescribir un método


heredado pero usamos una firma distinta (por ejemplo, añadiendo un parámetro más) no tenemos
sobrescritura sino sobrecarga y los dos métodos sobrecargados son accesibles desde los ejemplares
(objetos) de la clase hija. La finalidad de la sobrescritura es que podamos usar sólo un identificador y
un único conjunto de parámetros para diferentes clases relacionadas por herencia. El código de cada
método sobrescrito es diferente en la clase hija que en la clase padre. Así, se consigue particularizar el
202 Introducción a la programación con orientación a objetos

comportamiento de la clase. El sistema sabe qué versión del método debe usar, porque conoce la cla-
se del objeto en el que se invoca el método. Este proceso es transparente para el usuario. Veamos un
ejemplo de sobrescritura de métodos en el Programa 8.3.

Programa 8.3. Ilustración de la sobrescritura de métodos

class ClaseX {
protected int n=25;

public void imprimir(){


System.out.println(“En ClaseX, n= “+n);
}
}

class ClaseY extends ClaseX {

protected int m=10;

public void imprimir(){


System.out.println(“En ClaseY, m= “+m);
}
}

class Herencia {
public static void main(String [] args) {
ClaseX x = new ClaseX();
ClaseY y = new ClaseY();
x.imprimir();
y.imprimir();
}
}

ClaseY se hereda de ClaseX con lo que el método imprimir en principio contendría el mismo
código que el de ClaseX y al heredar también la variable n, el resultado del Programa 8.3 parece que
debería ser:

En ClaseX, n= 25
En ClaseX, n= 25

Sin embargo, en ClaseY sobrescribimos el método imprimir, con lo que el código para los obje-
tos de esa clase será el escrito para dicha clase y el resultado real es:

En ClaseX, n= 25
En ClaseY, m= 10

La gran ventaja de la sobrescritura es que podemos especificar versiones personalizadas de un


método para las clases hijas. El nombre y el uso del método es el mismo para todas las clases, pero el
comportamiento está totalmente adaptado a cada necesidad. Así, no es necesario crear un nuevo méto-
do con nombre distinto al del padre para realizar el mismo tipo de tarea, como en el Programa 8.3 para

2
Una vez más usamos método por semejanza con la notación de Java.
Herencia 203

imprimir una información.


Aunque como hemos indicado el proceso es transparente al usuario, es interesante exponer cómo
puede el sistema saber qué versión del método sobrescrito tiene que activar en cada momento. En los
lenguajes donde no existe sobrescritura de métodos 2 cuando se realiza una llamada a un método no hay
ambigüedad posible. En el mismo momento de la compilación se puede establecer el enlace entre el
código del método invocado y el punto de invocación. En este caso se dice que tenemos enlazamiento
estático. El enlazamiento estático es el soportado por los lenguajes no orientados a objetos y esto sig-
nifica que el compilador genera una llamada a un nombre específico de método y el enlazador (linker)
resuelve la llamada a la dirección absoluta del código que se ha de ejecutar. Ahora, consideremos el pro-
blema cuando hay sobrescritura de métodos. En este caso, el compilador se asegura, por un lado, de que
el método existe y, por otro, realiza la verificación de tipos de los argumentos y del valor de retorno.
Sin embargo, el compilador no conoce cuál es el método a ejecutar. Por ejemplo, pensemos en una cla-
se padre donde se define un método para que lo herede una clase hija. Supongamos que ese método
(método A) usa una invocación a otro método distinto (método B) que se sobrescribe en la clase hija.
¿Qué pasa al crear un objeto de la clase hija? Que tendremos el código heredado del primer método
(método A) el cual usa una invocación al código sobrescrito del método B en el objeto. Sin embargo,
la clase padre se puede compilar independientemente de la clase hija, así que el método A no puede
saber en tiempo de compilación qué versión del método B se va a usar, véase la Figura 8.4.
En memoria sólo hay una copia de cada método. Si el enlazamiento es estático, ¿con qué Méto-
do_B enlazaríamos al Método_A? En función de lo que ocurra en la ejecución del programa podemos
necesitar una versión u otra, y esto no es posible determinarlo de antemano. La sobrescritura implica
determinar cuál es la versión a usar del método sobrescrito en tiempo de ejecución. Por lo tanto, el
enlazamiento entre el código del método a usar y el punto de llamada no puede ser estático. Lo que
hace el sistema (de forma totalmente transparente para el usuario) es incluir algo más de código para
que en tiempo de ejecución se pregunte a qué clase pertenece el método invocado y así poder activar
la versión correcta de dicho método. Esta forma de enlazamiento se denomina enlazamiento dinámi-
co (dynamic binding) (Bishop y Bishop, 2000; Bishop, 1999; Savitch, 1999). Como ilustración del
problema, imaginemos un programa que lee un entero y si éste es 1 crea un objeto de Clase_Padre
y si no crea un objeto de Clase_Hija. Tendríamos,
if (i==1){
Clase_Padre objeto = new Clase_Padre();

Método_B
(Clase padre)

Método_A ?

Método_B
(Clase hija)

Figura 8.4. Si el Método_B está sobrescrito es necesario determinar en tiempo de ejecución


qué versión se activará
204 Introducción a la programación con orientación a objetos

}
else {
Clase_Hija objeto = new Clase_Hija();
}
objeto.método_B();

Sólo se sabrá si el objeto debe ser de una clase u otra después de haber leído la variable i. Sin
embargo, a la hora de compilar el compilador no puede saber a qué clase pertenece el método_B que
se invoca en el código, pues esto depende de la historia del programa. Para obtener enlazamiento diná-
mico se mantiene para cada objeto una tabla con sus métodos y se decide en tiempo de ejecución la
versión a ejecutar de cualquier método sobrescrito, véase la Figura 8.5.
Por defecto, los métodos en Java usan enlazamiento dinámico lo que implica una disminución
(pequeña) de rendimiento, ya que se consume algún tiempo en determinar dinámicamente el método
a usar. Si queremos optimizar en velocidad un método que sabemos que no va a ser sobrescrito, lo
debemos declarar con modificador final para forzar enlazamiento estático.

8.2.3. JERARQUÍAS DE CLASES


Hasta ahora hemos visto derivaciones por herencia simple. Esto es, hemos usado una clase para deri-
var otra nueva. Sin embargo, con la herencia se puede ir más allá, estableciendo auténticas jerarquías
de clases. Para establecer una jerarquía basta con hacer que algún descendiente de una clase padre ten-
ga a su vez clases descendientes, véase la Figura 8.6.
Una clase hija puede ser a su vez ser padre de una o más clases, creando la jerarquía. No hay lími-
te en el número de hijos que una clase puede tener, o en el número de niveles que constituyen la jerar-
quía. Lógicamente, un buen diseño de la jerarquía de clases mantiene las propiedades y
procedimientos comunes tan alto en la jerarquía como sea posible.
El mecanismo de herencia es transitivo, esto es, los atributos o procedimientos pasan de la clase
padre a las clases hijas y de éstas a su vez pasan a sus descendientes (incluyendo en este caso los nue-
vos atributos y procedimientos declarados en la clase hija). Dicho de otra forma, el miembro (atribu-
to o procedimiento) heredado puede provenir del padre inmediato o de algún nivel anterior. En
cualquier caso, siempre debe existir en la jerarquía de clases una relación de tipo es-un en las clases

Clase padre
Método_B
(Clase padre)

Método_A

Método_B
(Clase hija)
Clase hija

Figura 8.5. El enlazamiento del Método_A con el Método_B lo realiza dinámicamente el siste-
ma en tiempo de ejecución
Herencia 205

Padre Primer nivel


jerárquico

Hijo 1 Hijo 2 Hijo 3 Segundo nivel


jerárquico

Nieto 1 Nieto 2 Nieto 3 Tercer nivel


jerárquico

Figura 8.6. Diagrama que muestra la organización jerárquica de clases

descendientes con respecto a sus antecesoras.


Respecto a la relación de herencia podemos establecer la denominada herencia del invariante
(Meyer, 1999). Una clase descendiente tiene algo que la distingue de la antecesora. Las características
(propiedades) que distinguen una clase descendiente de una ascendiente se denominan invariantes de
la clase. Si de verdad hay una relación de herencia, una subclase debe admitir como característica suya
la unión por un “y” lógico de lo que la distingue a ella y a todas sus antecesoras. Es decir, a una sub-
clase le corresponden todos los invariantes de su línea jerárquica unidos por un “y” lógico. Por ejem-
plo, consideremos la relación jerárquica mostrada en la Figura 8.7, donde los invariantes de clase son
evidentes a partir del nombre de las clases.

Figura_cerrada

Polígono

Triángulo

Triángulo_equilátero

Figura 8.7. Relación jerárquica entre clases para figuras geométricas


206 Introducción a la programación con orientación a objetos

La herencia del invariante se manifiesta en que un triángulo equilátero tiene tres lados iguales “y”
tres lados (Triángulo) “y” lados rectos (Polígono) “y” es cerrado (Figura_cerrada).
Tras todas las consideraciones anteriores abordemos un ejemplo donde usemos una jerarquía de
clases. Consideremos un programa para gestionar cuentas de un banco. En todas las cuentas pueden
retirarse fondos y hacerse depósitos. Tenemos unas cuentas (corrientes) que se usan para realizar pagos
y que no proporcionan ningún interés. Estas cuentas tienen una libreta de ahorro (cuenta) asociada para
cubrir descubiertos de la cuenta corriente. Por otro lado las libretas de ahorro proporcionan un interés
(el 4%). Además, existe otro tipo de libretas de ahorro (libreta 2000) que rinde un mayor interés (8%),

Cuenta_bancaria

1 1
Cuenta_bancaria Libreta_de_ahorro

Libreta_2000

Figura 8.8. Estructura jerárquica de clases para el ejemplo de las cuentas bancarias

Cuenta_bancaria Libreta_de_ahorro extends


Cuenta_bancaria
# numero:int
# saldo:double # interes:double

+ Cuenta_bancaria (numero int :,


saldo_inicial:double) + Libreta_de_ahorro (numero:int,
+ depositar (cantidad:double):void saldo_inicial:double,
+ retirar (cantidad:double):void interes:double)
+ saldo ( ):double + interes ( ):void

Libreta_2000 extends Cuenta_corriente extends


Libreta_de_ahorro Cuenta_bancaria
- penalizacion:double - cuenta_asociada:Libreta_de_ahorro

+ Libreta_2000 (numero:int, + Cuenta_corriente (numero:int,


saldo_inicial:double, saldo_inicial:double,
interes:double, libreta:Libreta_de_ahorro)
penalizacion:double) + retirar (cantidad:double):void
+ retirar (cantidad:double):void

Figura 8.9. Diagramas de clase para las clases del ejemplo de las cuentas bancarias
Herencia 207

pero que penalizan la retirada de fondos con un 2% de la cantidad retirada. Con esta descripción, el
correspondiente diagrama de clases resultado del diseño sería el recogido en la Figura 8.8.
Obsérvese que la relación entre Cuenta_corriente y Libreta_de_ahorro es estructural, lo que
implica que uno de los atributos de Cuenta_corriente va a ser un objeto de clase Libreta_de_aho-
rro. Continuando con el diseño detallado, los miembros de cada clase podrían ser los recogidos en la
Figura 8.9.
Los métodos y atributos que incorporemos en las clases dependen de los requisitos que nos exi-
jan, aunque es mejor planificar para el cambio. Por ejemplo, si nos indicaran que las cuentas se abren
con saldo inicial de cero y que después se hacen los ingresos, no tendríamos entonces que usar una
variable saldo_inicial en los constructores. Sin embargo, poniéndola cubrimos un futuro cambio
consistente en abrir una cuenta con un saldo inicial. Para el caso actual bastaría con introducir valor
cero para la variable saldo_inicial. Por la misma razón, se introducen los intereses y la penali-
zación a través del constructor.
El código correspondiente a las clases del diagrama de la Figura 8.9 sería,

class Cuenta_bancaria {
protected int numero;
protected double saldo;

public Cuenta_bancaria(int numero, double saldo_inicial) {


this.numero=numero;
saldo=saldo_inicial;
}

public void depositar(double cantidad) {


System.out.println();
System.out.println(“Deposito en la cuenta: “+numero);
System.out.println(“Cantidad depositada: “+cantidad
+” Euros”);
saldo+=cantidad;
System.out.println(“Saldo actual: “+saldo+ “ Euros”);
}

public void retirar(double cantidad) {


System.out.println();
System.out.println(“Retirada de fondos de la cuenta: “
+numero);
System.out.println(“Cantidad solicitada: “+cantidad
+” Euros”);
if (saldo < cantidad)
System.out.println(“No hay fondos suficientes”);
else {
saldo-=cantidad;
System.out.println(“Saldo actual: “+saldo+” Euros”);
}
}

public double saldo() {


return saldo;
}
} // Fin Cuenta_bancaria

class Libreta_de_ahorro extends Cuenta_bancaria {


208 Introducción a la programación con orientación a objetos

protected double interes;

public Libreta_de_ahorro(int numero, double saldo_inicial,


double interes) {
super(numero, saldo_inicial);
this.interes=interes;
}

public void intereses() {


System.out.println();
System.out.println(“Actualizando intereses en la cuenta: “
+numero);
saldo+=saldo*interes/100.0; // Esto es:
// saldo=saldo+saldo*interes/100.0
System.out.println(“Saldo tras la actualizacion: “+saldo
+” Euros”);
}
} // Fin Libreta_de_ahorro

class Libreta_2000 extends Libreta_de_ahorro {

private double penalizacion; // Penalizacion (%) por retirada

public Libreta_2000(int numero, double saldo_inicial,


double interes, double penalizacion) {
super(numero, saldo_inicial, interes);
this.penalizacion=penalizacion;
}

public void retirar(double cantidad) {


System.out.println();
System.out.println(“Retirando fondos con penalizacion del “
+penalizacion+”% de la cuenta: “+numero);
System.out.println(“Cantidad solicitada: “+cantidad
+” Euros”);
if (saldo < cantidad)
System.out.println(“No hay fondos suficientes”);
else {
saldo-=cantidad*(1.0-penalizacion/100.0);
System.out.println(“Saldo actual: “+saldo +” Euros”);
}
}
} // Fin Libreta_2000

class Cuenta_corriente extends Cuenta_bancaria {


private Libreta_de_ahorro cuenta_asociada;

public Cuenta_corriente(int numero, double saldo_inicial,


Libreta_de_ahorro libreta) {

super(numero, saldo_inicial);
cuenta_asociada=libreta;
}

public void retirar(double cantidad) {


System.out.println();
Herencia 209

System.out.println(“Retirada de fondos de la cuenta: “


+numero);
System.out.println(“Cantidad solicitada: “+cantidad
+” Euros”);

if (saldo < cantidad)


if (cuenta_asociada.saldo() < (cantidad-saldo) )
System.out.println(“No hay fondos suficientes”);
else {
System.out.println(“Retirando fondos adicionales”+
“ de la cuenta asociada”);
cuenta_asociada.retirar(cantidad-saldo);
saldo=0.0;
System.out.println(“Saldo actual cuenta corriente: “
+”0.0 Euros”);
}
else {
saldo-=cantidad;
System.out.println(“Saldo actual: “+saldo+” Euros”);
}
}
} // Fin Cuenta_corriente

Fijémonos en que en los constructores estamos usando la referencia super para llamar al cons-
tructor de la clase padre y no tener que repetir ese mismo código otra vez en las clases hijas. Obsér-
vese también cómo el método retirar se sobrescribe en varias clases.
Un posible ejemplo de programa que usara estas clases podría ser el mostrado en el Programa 8.4.

Programa 8.4. Ejemplo de uso de las clases del sistema de cuentas bancarias

class Herencia {
public static void main(String [] args) {

Libreta_de_ahorro libreta = new Libreta_de_ahorro(12345,


2000,4);
Libreta_2000 libreton = new Libreta_2000(23456,
2000,8,2);
Cuenta_corriente cuenta = new Cuenta_corriente(34567,
2000, libreta);
libreta.retirar(100);
cuenta.retirar(300);
libreton.retirar(200);
libreton.intereses();
}
}

El resultado sería,

Retirada de fondos de la cuenta: 12345


Cantidad solicitada: 100.0 Euros
Saldo actual: 1900.0 Euros

Retirada de fondos de la cuenta: 34567


Cantidad solicitada: 300.0 Euros
210 Introducción a la programación con orientación a objetos

Saldo actual: 1700.0 Euros

Retirando fondos con penalizacion del 2.0% de la cuenta: 23456


Cantidad solicitada: 200.0 Euros
Saldo actual: 1804.0 Euros

Actualizando intereses en la cuenta: 23456


Saldo tras la actualizacion: 1948.32 Euros

Respecto a las relaciones de herencia es interesante conocer que específicamente en Java, todas las
clases heredan de la clase Object. Esta clase constituye el nodo raíz de toda la jerarquía de clases de
Java. Esto es importante, todas las clases, predefinidas o creadas por el usuario, heredan implícita-
mente de la clase Object. Esto implica que las siguientes definiciones son equivalentes:

class nombre_clase {
}

class nombre_clase extends Object {


}

sea cual sea la clase considerada. El caso en el que una clase hereda de otra no es óbice para que tam-
bién implícitamente se herede de Object. Esta situación no se considera herencia múltiple.
La clase Object posee métodos interesantes que heredan todos sus descendientes, como el méto-
do toString(). Este método devuelve una cadena conteniendo una descripción del objeto que lo lla-
ma. Este método es particularmente útil, porque permite que un objeto lleve asociada una cadena.
Cualquier clase puede sobrescribir este método para proporcionar una representación String de una
clase particular. Si no se sobrescribe y se llama al método, se usará el método de la clase Object que
imprime el nombre de la clase seguido de un código hexadecimal. En el Programa 8.5 se muestra un
ejemplo de cómo en una clase Alumno se sobrescribe el método toString().

Programa 8.5. Uso del método toString()

class Alumno{
public String nombre;
public int matricula;

// Constructor
Alumno(String nombreAlumno, int matriculaAlumno){
nombre = nombreAlumno;
matricula = matriculaAlumno;
}

// Sobrescritura del método toString


public String toString( ) {
return “Los datos del alumno son:”+ nombre+” “ + matricula;
}

}
Herencia 211

class Facultad{
public static void main(String args[]){
Alumno alumno1 = new Alumno(“Juan”,123);
System.out.println(alumno1);
Alumno alumno2 = new Alumno(“Maria”,124);
System.out.print(alumno2);
}
}

La salida del Programa 8.5 sería,

Los datos del alumno son: Juan 123


Los datos del alumno son: Maria 124

Obsérvese en el Programa 8.5 que en los métodos System.out.println(alumno1) y


System.out.print(alumno1) se usan directamente los objetos, sin necesidad de invocar el método
toString(). Esto puede considerarse una sintaxis abreviada de la invocación a toString(), y se reali-
zaría como System.out.println(alumno1.toString()) y System.out.print(alumno2.toS-
tring()). Si no hubiésemos sobrescrito el método toString() y se hubiera utilizado el método tal y
como se hereda de la clase Object la salida del Programa 8.5 hubiese sido:

Alumno@111f71
Alumno@273d3c

Un uso típico del método toString() es utilizarlo para devolver simplemente el nombre de la
clase o algún código que la identifique. De esa manera invocando a toString() se puede saber a qué
clase pertenece un objeto determinado.

8.2.4. ORGANIZACIÓN DE JERARQUÍAS DE CLASES (PAQUETES)


Usando jerarquías de clases podemos agrupar clases relacionadas pero, ¿hay alguna forma de agrupar
varias jerarquías de clases? Por ejemplo, supongamos una colección de clases que permiten simular un
coche. Tenemos una jerarquía de clases para tipos de carrocerías, otra para ruedas, otra para motores, etc.
Es conveniente agrupar juntas esas jerarquías distintas bajo un único nombre que muestre que están con-
ceptualmente relacionadas. En Java tenemos un mecanismo para agrupar diferentes jerarquías de clases y
referirlas con un único identificador, se trata de los paquetes 3. Un paquete es una colección de clases agru-
padas juntas bajo un solo nombre. Cuando se agrupan varias jerarquías en un paquete es porque de algu-
na manera se considera que dichas jerarquías tienen algo en común. Las clases en un paquete no tienen
por qué estar relacionadas por herencia.
Para poder trabajar con paquetes tenemos que hacer dos cosas:

a) Crearlos, indicando qué clases forman parte del mismo y dónde se va a encontrar dicho paquete.
b) Usarlo, importando el paquete tal y como ya hemos visto en programas anteriores.

Para crear un paquete, el programador debe hacer dos cosas: primero identificar los ficheros y cla-
ses que pertenecerán al paquete. Segundo, colocar todas las versiones compiladas de las clases (los
ficheros .class) en un subdirectorio con el mismo nombre del paquete. Así, si tenemos un fichero
con varias clases, la sintaxis sería:

3
En realidad el mecanismo no controla si las clases están relacionadas o no. Podemos entender el mecanismo de paque-
tes como una técnica para agrupar clases. Será responsabilidad del diseñador/a el incluir en el paquete unas clases u otras.
212 Introducción a la programación con orientación a objetos

package nombre_ paquete;

class nombre_clase {
...
}
class nombre_clase {
...
}

La cláusula package debe estar colocada al principio del fichero. Todas las clases que haya en
el fichero donde aparezca la cláusula package se añaden al paquete. Las clases que son públicas
(modificador public) serán accesibles desde otras clases aunque no estén en el mismo paquete.
Las clases sin modificador (valor por defecto) sólo son accesibles a otras clases dentro del mismo
paquete.
Si queremos que todas las clases existentes en distintos ficheros formen parte del mismo paquete,
debemos colocar la sentencia “package nombre_ paquete;” como primera línea en todos los fiche-
ros. En Java todas las clases están contenidas en un paquete. Si no aparece la declaración de un paque-
te en un fichero dado, las clases de ese fichero pertenecerán al paquete por defecto que es el paquete
sin nombre. Sólo debe aparecer una cláusula package por fichero.
Veamos un ejemplo. Creemos un paquete con una clase para lectura cómoda. Se va a tratar de una
clase pública (para poder usarla desde cualquier parte) donde vamos a incluir una serie de métodos
estáticos (para no tener que crear objetos de la clase) que lean un entero (int) y una línea como cade-
na. En un mismo fichero pondríamos lo siguiente,

package LecturaComoda;
import java.io.*;

public class Lectora {

static BufferedReader leer = new BufferedReader


(new InputStreamReader(System.in));

public static int read_int() throws IOException {


return Integer.parseInt(leer.readLine());
} // Fin método read_int

public static String read_line() throws IOException {


return leer.readLine();
} // Fin método read_line

} // Fin clase Lectora

La clase Lectora ahora pertenece al paquete LecturaComoda. Después de compilar la clase, el


compilador creará el fichero Lectora.class. La clase Lectora se usaría igual que hemos hecho
con la clase predefinida Math. Es decir, haríamos Lectora.read_int() para leer un entero por
teclado. Los ficheros compilados se deben copiar en un subdirectorio 4 con el mismo nombre que el
paquete. En nuestro ejemplo deberíamos copiar Lectora.class al subdirectorio .../LecturaCo-
moda/.

4
El uso de la barra normal, /, o la barra invertida, \, para identificar los distintos subdirectorios depende del sistema ope-
rativo empleado. Aquí se usa la barra normal por ser la notación del sistema más extendido (al menos en número de usuarios).
Herencia 213

El emplazamiento del directorio donde se encuentran los paquetes se identifica normalmente en la


variable de entorno CLASSPATH. Es conveniente consultar la documentación del compilador y el sis-
tema operativo que estemos usando para más información sobre variables de entorno y subdirectorios.
Para más información sobre la variable CLASSPATH, véase Eckel, 2002.
Los nombres de los paquetes pueden contener un punto (.). Se usa normalmente para indicar algu-
na relación entre las partes de dos o más paquetes. Por ejemplo, java.lang y java.io son dos
paquetes diferentes pero están relacionados. Los ficheros .java y .class del paquete lang se alma-
cenan en un directorio llamado java/lang y las del paquete io en un subdirectorio llamado
java/io. Para localizar el subdirectorio, el punto se transforma en la barra normal, /, o la barra inver-
tida, \, en función del sistema operativo utilizado.
Hasta ahora hemos visto cómo se define un paquete, vamos a ver cómo se usa. Para usar una cla-
se de un paquete debemos incluir la sentencia:
import nombre_ paquete.nombre_clase;

o
import nombre_ paquete.*;

en el fichero donde aparezca la clase que va a usar alguna de las clases contenidas en el paquete. Pode-
mos usar tantos paquetes como queramos, pero las sentencias de importación deben colocarse inme-
diatamente después de la sentencia de declaración de paquete o si no existe, al principio del fichero.
De las dos formas de import mencionadas anteriormente, la primera importará sólo la clase indi-
cada. La segunda forma, la del .*, permitirá que todas las clases de ese paquete sean accesibles a todas
las clases del fichero actual. Como ya dijimos, por defecto siempre se importa automáticamente el
paquete predefinido java.lang.
Es posible usar las clases de un paquete sin importarlo, pero hay que indicar el nombre del paque-
te precediendo a la clase de la forma siguiente,

nombre_ paquete.nombre_clase

Veamos cómo usar la clase Lectora del paquete LecturaComoda sin importarlo en el Progra-
ma 8.6.

Programa 8.6. Ilustración del uso de las clases de un paquete sin importarlo

import java.io.*;
class Herencia {
public static void main(String[] args) throws IOException {
int valor = LecturaComoda.Lectora.read_int();
String linea = LecturaComoda.Lectora.read_line();

System.out.println(valor);
System.out.println(linea);

} // Fin método main


} // Fin clase

El uso normal, sin embargo, sería con la cláusula import, véase el Programa 8.7.

Programa 8.7. El programa del Ejemplo 8.6 importando el paquete con la cláusula import

import java.io.*;
214 Introducción a la programación con orientación a objetos

import LecturaComoda.*;
class Herencia {
public static void main(String[] args) throws IOException {
int valor = Lectora.read_int();
String linea = Lectora.read_line();

System.out.println(valor);
System.out.println(linea);

} // Fin método main


} // Fin clase

Hay un caso en el que se necesita la primera forma. Es cuando se usan dos paquetes en los que
existe una clase con el mismo nombre. En ese caso debemos identificar la clase de forma absoluta,
incluyendo el nombre del paquete al crear objetos de la misma, como en el Programa 8.6.

8.2.5. POLIMORFISMO
Polimorfismo indica en orientación a objetos que una misma identificación puede referir a distintas
entidades. Se trata de una de las características definitorias de la orientación a objetos. El polimorfis-
mo es la capacidad de una entidad determinada (entiéndase referencia) de conectarse a objetos de varias
clases relacionadas por herencia (Meyer, 1999). Esta capacidad tiene sentido y sirve para algo si esos
distintos objetos de diferentes clases tienen métodos sobrescritos, que son los que se van a invocar a
través de la referencia polimórfica. El polimorfismo implica la posibilidad de usar una sola referencia
para referirse a varios objetos relacionados jerárquicamente. Generalmente, en una clase padre se decla-
ra un método que es el que se va usar polimórficamente. Entonces, esa misma función (método) se rede-
fine en clases que son derivadas de la clase padre, es decir, el método se sobrescribe. Así, existirán
métodos en las clases descendientes con la misma firma que el de la clase padre. Si un objeto de la cla-
se padre se declara en un programa, la definición del método original que se encuentra en la clase padre
será la que se invoque cuando se llame al método. Sin embargo, si un objeto de una clase hija se asig-
na posteriormente a la referencia de la clase padre, entonces se invoca la definición de método para la
clase hija. Como podemos ver, hay una clara diferencia entre una simple sobrecarga y el polimorfismo.
Cuando tenemos un método polimórfico necesariamente se usa el enlazamiento dinámico y el sistema
comprueba la clase real del objeto antes de invocar la definición de método apropiada.
En términos más prácticos, el polimorfismo implica que una referencia puede referir a cualquier obje-
to de su clase o a un objeto de una clase descendiente de la primera. Por ejemplo, si tenemos una clase
Vacaciones que es heredada por una clase Navidades, y otra Verano, véase la Figura 8.10, la refe-

Vacaciones

Navidades Verano

Figura 8.10. Relación jerárquica entre las clases Vacaciones, Navidades y Verano
Herencia 215

rencia de un objeto Vacaciones se puede usar para referir a un objeto de clase Navidades o Verano
de la forma siguiente,

Vacaciones mis_vacaciones;
mis_vacaciones = new Navidades();

Suponiendo que exista un método sobrescrito llamado duracion() que nos dé el número de días
festivos, la referencia mis_vacaciones (que es de tipo clase Vacaciones) se podría usar para invo-
car el método duracion() en objetos de clase Navidades o Verano. En estos casos, sería la clase
real del objeto al que refiere mis_vacaciones la que determinaría qué versión del método se usa.
Esto es importante, es la clase del objeto referido la que indica qué método usar, no la clase con la que
se declara inicialmente la referencia.
Veamos un ejemplo que muestra la mecánica de uso del polimorfismo, véase el Programa 8.8.

Programa 8.8. Ilustración del uso de referencias polimórficas

class Padre{
void dondeEstoy() {
System.out.println(“Estoy en el metodo del Padre”);
}
}
class HijaPrimera extends Padre{
void dondeEstoy() {
System.out.println(“Estoy en el metodo de la HijaPrimera”);
}
}
class HijaSegunda extends Padre{
void dondeEstoy() {
System.out.println(“Estoy en el metodo de la HijaSegunda”);
}
}

class Herencia {
public static void main(String [] args) {
Padre padre =new Padre();
HijaPrimera hijaPrimera =new HijaPrimera();
HijaSegunda hijaSegunda =new HijaSegunda();
Padre polimorfico;

polimorfico=padre; // hace referencia a un objeto padre


polimorfico.dondeEstoy();

polimorfico=hijaPrimera; //hace referencia a un objeto


//hijaPrimera
polimorfico.dondeEstoy();

polimorfico=hijaSegunda; //hace referencia a un objeto


//hijasegunda
polimorfico.dondeEstoy();
}
}

La salida del Programa 8.8 es:


216 Introducción a la programación con orientación a objetos

Estoy en el metodo del Padre


Estoy en el metodo llamada de la HijaPrimera
Estoy en el metodo llamada de la HijaSegunda

En el Programa 8.8 las asignaciones polimorfico=padre, polimorfico=hijaPrimera y


polimorfico=hijaSegunda son asignaciones polimórficas. Una entidad tal como la variable
polimorfico que aparece en alguna asignación polimorfa es una entidad polimorfa.
Como muestra la salida del Programa 8.8, la versión del método ejecutada se determina en función
del tipo de objeto que está siendo referido en el momento de realizar la llamada. Si hubiese sido deter-
minado por la clase de la referencia polimorfico, se hubiesen realizado tres llamadas al método
dondeEstoy() de la clase Padre. Cuando vamos reasignando la referencia polimorfico, los obje-
tos liberados no se vuelven a usar, así que son candidatos a la recogida de basura. Una consideración
interesante es que al derivar en Java, en última instancia, todos las clases de la clase Object, cual-
quier objeto se puede asignar a una referencia Object.
Para entender la utilidad del polimorfismo imaginemos un método que acepta como parámetro una
referencia polimórfica. Dentro del método se usará la referencia de forma normal, invocando los méto-
dos sobrescritos sin mayor problema (de hecho ni siquiera necesitaríamos saber qué son métodos
sobrescritos). Fijémonos en que el método sólo acepta la referencia, pero él no realiza la asignación de
la referencia a un objeto de una clase o de otra. Por lo tanto, el método es el mismo, independiente-
mente de la clase a la que pertenezca el objeto referido por la referencia. Ésta es la potencia del poli-
morfismo. Yo podría cambiar entera la estructura jerárquica de las clases de las que uso la referencia
polimórfica, y el método considerado no habría que cambiarlo nunca. En el ejemplo de las vacaciones
podríamos tener un método en otra clase que imprimiera los días de vacaciones, invocando al método
duracion() anteriormente mencionado. Este método podría ser:

public void dias(Vacaciones mis_vacaciones) {


mis_vacaciones.duracion();
}

y el método sería el mismo independientemente de a qué clase de objeto refiera mis_vacaciones.


Otro ejemplo típico es el de una estructura de datos de referencias polimórficas. Por ejemplo, ima-
ginemos que necesitamos una lista, implementada como una matriz monodimensional de objetos de
clase Navidades y otra de objetos de clase Verano. Sin polimorfismo no hay más solución que usar
dos matrices. Sin embargo, con el polimorfismo podríamos usar una sola matriz de referencias de cla-
se Vacaciones. El polimorfismo nos permite ir vinculando cada elemento de esta matriz a objetos de
clase Navidades o Verano, según convenga. Es decir, sólo hace falta una estructura de datos.

8.3. MECANISMOS ADICIONALES DE ABSTRACCIÓN BASADOS EN HERENCIA


En este punto consideraremos dos mecanismos de abstracción a usar en conjunción con la herencia.
En primer lugar consideraremos las clases abstractas y su relación con las subclases. En segundo lugar
introduciremos las interfaces, que nos permiten definir la especificación de un conjunto de métodos.
Comencemos con las clases abstractas.

8.3.1. CLASES Y MÉTODOS ABSTRACTOS


En programación orientada a objetos no es necesario que todos los métodos de una clase estén imple-
mentados. Puede ser conveniente disponer de clases en las que todos los procedimientos estén recogidos
Herencia 217

pero no todos estén implementados. Estas clases son útiles como ayuda en los procesos de análisis y diseño
de la estructura de clases. De hecho, en este último caso puede ser aconsejable mantener estas clases como
indicación de diseño. Estas clases no totalmente implementadas se denominan clases diferidas o abstrac-
tas. Su uso, lógicamente, es a través de la herencia ya que no tiene sentido pretender crear objetos de una
clase no totalmente implementada. La utilidad de las clases abstractas es la de definir la especificación de
la abstracción representada por la clase. Los métodos no implementados, aunque especificados (es decir,
indicando su firma y tipo de retorno), se denominan métodos abstractos y lógicamente (aunque no nece-
sariamente) una clase abstracta debe contener al menos un método abstracto. Un método abstracto es un
método que no contiene ninguna implementación, sólo tipo de retorno y firma.
En la práctica, las clases abstractas se usan dentro de una jerarquía de clases en la cual la clase abs-
tracta define sólo parte de su implementación, difiriendo el resto a las clases hijas por medio de la
sobrescritura de los métodos abstractos.
Cualquier clase que contenga métodos abstractos debe ser declarada como abstracta, aunque se
puede declarar una clase abstracta sin tener ningún método abstracto. Sin embargo, una clase abstrac-
ta no tiene por qué contener métodos abstractos propios, podría derivar de una clase padre abstracta,
a partir de la cual hereda un método abstracto que no se implementa.
Para declarar una clase como abstracta en Java se usa el modificador abstract. El mismo modi-
ficador se aplica para identificar un método abstracto. La sintaxis para declarar una clase abstracta, por
lo tanto, es:
abstract class nombre_clase {
// código de la clase
}

Análogamente, para definir un método abstracto dentro de una clase abstracta la sintaxis sería:
abstract visibilidad tipo_retorno nombre_método(lista_ parámetros);

Obsérvese que en lugar de un grupo de sentencias siguiendo a la cabecera del método (implemen-
tación del método) hay un punto y coma (;). La falta de implementación indica que el método es abs-
tracto. En un método abstracto no se indica su implementación, pero sí qué parámetros acepta y qué
devuelve. Lógicamente un método abstracto no puede ser declarado como final (pues se debe poder
sobrescribir) ni static (pues no puede ser invocado ya que no tiene implementación). Tampoco se
pueden declarar constructores abstractos. Sería una operación sin sentido ya que un constructor siem-
pre se usa para crear un objeto (lo que no se puede hacer con una clase abstracta). En notación UML
indicamos una clase abstracta especificando su nombre en cursiva. Si esto es complicado de represen-
tar (en una pizarra, por ejemplo) se puede incluir un valor etiquetado (entre llaves, {}) al lado de la
clase para indicar que es abstracta (Booch et al., 1999), véase la Figura 8.11.
No hay restricciones sobre dónde se puede definir una clase abstracta en la jerarquía de clases. Por
ejemplo, una clase abstracta podría derivarse de una clase padre no abstracta. Sin embargo, puesto que
la utilidad de las clases abstractas es especificar una abstracción, lo lógico es que aparezcan en nive-
les altos (si no en el primero) de las jerarquías de clases.

Clase {abstracta}

Atributos

Procedimientos

Figura 8.11. Notación UML de una clase abstracta


218 Introducción a la programación con orientación a objetos

Las clases descendientes de una clase padre abstracta no tienen por qué sobrescribir todos los
métodos abstractos de sus padres. Sin embargo, si una clase hija no sobrescribe todos los métodos abs-
tractos de la clase padre debe ser declarada también como abstracta, pues a través de la herencia la cla-
se hija contiene los métodos abstractos. La utilidad práctica de las clases abstractas radica en el hecho
de no poderse crear objetos de dicha clase. Así, la clase abstracta especifica una abstracción. Las cla-
ses hijas heredan la estructura de métodos de las clases padre y para poder crear objetos de las clases
hijas éstas deben sobrescribir todos los métodos abstractos que han heredado. Al usar un método abs-
tracto en la clase padre, forzamos a que explícitamente todas las clases hijas que lo hereden tengan que
elegir como implementarlo. Si no se usan métodos abstractos, las clases hijas no están forzadas a hacer
la elección. Pueden usar el método heredado de su clase padre.
La elección de qué métodos deben ser abstractos es una decisión de diseño que afecta a la aplica-
ción completa. Tal elección debe ser hecha después de mucha reflexión y experimentación. Usando
cuidadosamente las clases abstractas, nos aseguramos que el personal de mantenimiento futuro de la
aplicación entienda la estructura de clases y su propósito.
Una clase abstracta puede contener datos y mezclar métodos abstractos con no abstractos. Todo
será heredado por sus subclases.
Las clases abstractas se pueden usar para declarar referencias a objetos, al igual que hemos usado
las clases no abstractas. De hecho una clase abstracta se puede usar de la misma forma que cualquier
otra clase, excepto que no se pueden crear objetos de ella. Esta forma de trabajar es habitual, se decla-
ra una referencia de la clase abstracta (la clase padre de una jerarquía) para referir a objetos de clases
hijas (no abstractas) por medio de referencias polimórficas. Veamos un ejemplo de esta técnica. Con-
sideremos un programa que trabaja con distintos tipos de automóviles: deportivos, turismos y familia-
res. La relación jerárquica sería la recogida en la Figura 8.12.

Automóvil
{abstracta}

Deportivo Turismo Familiar

Figura 8.12. Relación de herencia en el ejemplo de los automóviles

Es normal tener que construir versiones específicas de los métodos que se van heredando en una
estructura jerárquica. Una buena forma de evitar los posibles problemas, como el típico de no sobres-
cribir el método heredado con la versión apropiada para la clase hija, y usar por error el heredado del
padre, es utilizar una clase abstracta. En el ejemplo que estamos viendo, supongamos que queremos
incluir un método eslogan() en todos los automóviles (podemos imaginar que es un método nuevo
que se añade a los ya existentes) que imprime una frase publicitaria. Si no usáramos un método abs-
tracto eslogan() en la clase raíz, estaríamos obligados a escribir un método eslogan() inútil en la
clase Automovil (inútil porque no vamos a crear objetos de esa clase sino de sus hijas), para que se
pudiera heredar y sobrescribir en sus clase hijas. En el ejemplo vamos a usar una matriz de referen-
cias de clase Automovil que refieran polimórficamente a los objetos de clase Deportivo, Turis-
mo y Familiar y a usar una sola llamada a un método imprimir_eslogan() al que se le pase la
matriz. El código apropiado sería el mostrado en el Programa 8.9.
Herencia 219

Programa 8.9. Uso de clases y métodos abstractos (continuación)

Programa 8.9. Uso de clases y métodos abstractos

abstract class Automovil {


abstract public void eslogan();
}

class Deportivo extends Automovil {


public void eslogan() {
System.out.println(“Veloz como el rayo”);
}
}

class Turismo extends Automovil {


public void eslogan() {
System.out.println(“Para el uso diario”);
}
}

class Familiar extends Automovil {


public void eslogan() {
System.out.println(“Cabe hasta el gato”);
}
}

// Clase que contiene el método main

class Herencia {
public static void main(String [] args) {
Automovil [] auto=new Automovil [3];
auto [0]=new Deportivo();
auto [1]=new Turismo();
auto [2]=new Familiar();
imprime_eslogan(auto);
}

public static void imprime_eslogan(Automovil [] auto) {


for (int i=0; i<=2; i++)
auto[i].eslogan();

}
}

El resultado del programa sería:

Veloz como el rayo


Para el uso diario
Cabe hasta el gato

Obsérvese en el Programa 8.9 cómo se crea la matriz de objetos de clase Automovil y cómo des-
pués se hace que las referencias refieran polimórficamente a objetos de las clases hijas. Fijémonos
también que el método imprime_eslogan() recibe la matriz y que el método no cambiaría aunque
nosotros alteráramos la estructura de la jerarquía. Es decir, el programa principal sería el mismo inde-
220 Introducción a la programación con orientación a objetos

pendientemente de las modificaciones que hiciéramos en la estructura jerárquica, en tanto y en cuan-


to siga existiendo el método eslogan(). Este uso de una clase abstracta es una técnica común para
escribir clases reutilizables. Podemos reemplazar toda la jerarquía de clases que emergen de la clase
Automovil por una jerarquía completamente diferente, sin afectar para nada al método
imprime_eslogan(). El polimorfismo es muy útil también a la hora de definir estructuras de datos,
pues todos los elementos de la estructura se pueden declarar de un solo tipo usando referencias
polimórficas de la clase padre.

8.3.2 INTERFACES
Si en una clase abstracta algún método puede (y podríamos decir debe) constar sólo de su especifica-
ción, las interfaces sólo contienen especificaciones de métodos. Una interfaz es una colección de cons-
tantes y métodos abstractos. No son clases, pero pueden usarse en la definición de una clase. La
sintaxis general en Java es:

interface nombre_interfaz {
declaración de constantes
declaración de métodos abstractos
}

En la declaración de los métodos se debe incluir el tipo de retorno y la lista de parámetros.


Además, sólo está permitido el uso de los modificadores public y abstract. Sin embargo, no son
necesarios, porque por defecto los métodos de una interfaz son públicos y abstractos. Las constantes
en una interfaz son siempre public y final.
Las interfaces no se usan por sí mismas sino que se “implementan” (una especie de herencia) en
una clase. La clase en cuestión proporciona las implementaciones para cada uno de los métodos defi-
nidos en la declaración de la interfaz. En Java se usa la palabra clave implements para indicar que
una clase implementa una interfaz. La sintaxis es:

class nombre_clase implements nombre_interfaz {


implementación de los métodos de la interfaz
}

Si una clase incluye una o varias interfaces pero no implementa todos los métodos definidos por
la/s interface/s la clase debe declararse como abstract.
Con respecto a las constantes definidas en la interfaz, éstas se comportan como si estuvieran defi-
nidas en la clase que implementa dicha interfaz. Esto nos proporciona una forma de poder distribuir
constantes entre varias clases. Para ello basta con definir las constantes en una interfaz y luego hacer
que las diferentes clases implementen dicha interfaz.
Por lo que respecta al paso de parámetros y las interfaces, hay que decir que un parámetro formal
declarado de “tipo” interfaz puede aceptar como parámetro actual cualquier clase o subclase que
implemente dicha interfaz.
Tras lo visto, ¿cuáles son las diferencias entre interfaces y clases abstractas? Dos fundamental-
mente. Una, la simplicidad en la notación. En una interfaz todos los métodos son abstractos por lo que
se puede omitir la palabra clave abstract. A su vez, todas las constantes son public, static y
final, por lo que se pueden omitir las palabras clave. La otra razón, más importante, es que una cla-
se puede implementar más de una interfaz, mientras que una subclase sólo se puede derivar de una cla-
se. Por lo tanto, en cierto sentido, las interfaces en Java nos permiten cierta capacidad de herencia
múltiple. La sintaxis para la implementación de varias interfaces sería:
Herencia 221

class nombre_clase implements interfaz_1,..., interfaz_n {


--- cuerpo de la clase ---
}

Una vez vistas las diferencias, ¿cuáles son las similitudes entre interfaces y clases abstractas? Por
un lado, que tanto interfaces como clases abstractas definen métodos abstractos que serán sobrescritos
posteriormente en clases particulares. Por otro lado, que ambas se pueden usar como nombres de tipos
genéricos para referencias. Ambas se pueden usar como parámetros formales.
Para finalizar, comentemos que el uso de clases abstractas e interfaces es importante en el desa-
rrollo de una aplicación software desde el punto de vista de la encapsulación y ocultamiento de infor-
mación. Cuanto mayor es un proyecto, más importante resulta la capacidad de encapsulación y de
ocultamiento de información. Esta capacidad reduce el número de detalles que cada miembro especí-
fico del equipo de desarrollo necesita conocer y entender acerca del sistema software. Usando clases
abstractas e interfaces un programador puede identificar los enlaces entre las diferentes partes del sis-
tema sin necesidad de tener que proporcionar detalles de su implementación. Como las clases abs-
tractas e interfaces no especifican cómo se hace algo, sino qué se debe hacer, son un buen mecanismo
para identificar partes relativamente independientes del sistema software. Después de haber identifi-
cado un buen conjunto de clases abstractas e interfaces, varios programadores pueden trabajar en para-
lelo, lo que supone una optimización de los recursos de desarrollo.
Tras la exposición anterior veamos un ejemplo de uso de interfaces para especificar constantes.
Abordemos como ilustración el cálculo del volumen molar de una sustancia a una presión y tempera-
tura dadas usando la ecuación de estado de un gas ideal:

pV= n Na k T

donde p es la presión, V el volumen, n el número de moles, Na la constante de Avogadro, k la cons-


tante de Boltzmann y T la temperatura, véase el Programa 8.10.

Programa 8.10. Ilustración del uso de una interfaz

interface Constantes_fisicas {
double Na=6.023e23; // Constante de Avogadro
// en (partículas/mol)

double k=1.38066e-23; // Constante de Boltzmann en J/K


}

class Herencia implements Constantes_fisicas {


public static void main(String[] args) {
double p, T, v;
p = Double.parseDouble(args[0]); // p en pascales (SI)
// 1Pa= 1 Newton/(m*m)
// 1atm = 101325 Pa
T = Double.parseDouble(args[1]);
System.out.println(“Presion: “+p +” Pa”);
System.out.println(“T: “+T+” K”);

v=k*Na*T/p; // Volumen molar (metros cúbicos)


v=v*1000.0; // litros (1 metro cubico = 1000 litros)

System.out.println(“Volumen molar: “+v+” litros”);


}
222 Introducción a la programación con orientación a objetos

La salida del Programa 8.10 con datos de entrada de 1 atm. (101325 Pa) y 298.15 K (25 °C) sería,

Presion: 101325.0 Pa
T: 298.15 K
Volumen molar: 24.46908937495189 litros

En la interfaz hemos definido unas constantes que se usan luego en el programa. Lógicamente, si
hemos creado la interfaz es porque las constantes recogidas tienen interés para varias clases (cada una de
ellas implementará la interfaz). Otro caso típico de constantes en una interfaz es el de usar unas constan-
tes cuyo valor codifique algo. Por ejemplo, imaginemos un código numérico para los permisos típicos
sobre un fichero: lectura, escritura y ejecución. Podríamos definir tres constantes en una interfaz denomi-
nadas LECTURA, ESCRITURA Y EJECUCIÓN, cada una con el valor numérico que se use para indi-
car el permiso correspondiente. Un programa que lea el permiso de un fichero (el código numérico) y
necesite saber lo que significa ese código, sólo tendría que implementar la interfaz y comparar con las
constantes. Si se cambia el código numérico de los permisos habría que modificar la asignación de valo-
res a las constantes en la interfaz, pero no en todos los programas que la usen, lo que es una gran ventaja.
Para información más detallada sobre interfaces, el lector interesado puede consultar el texto de
Eckel (Eckel, 2002).

EJERCICIOS PROPUESTOS
Ejercicio 1.* ¿Cuál es el resultado del siguiente programa?

class Programa {
public static void main(String [ ] args){
Clase1 obj1=new Clase1();
obj1.imprimir(4);
Clase2 obj2=new Clase2();
obj2.imprimir(5);
}
}

class Clase1 {
int prop1=0,prop2=0;
public void imprimir(int i){
prop1=prop1+i;
prop2=prop2+i;
System.out.print(prop1+” “+prop2+” “);
}
}

class Clase2 extends Clase1 {


public void imprimir(int i){
prop1=prop1+i;
prop2=prop2+i;
System.out.print(prop1+” “+prop2);
}
}
Herencia 223

Ejercicio 2.* ¿Cuál es el resultado del siguiente programa?

class Uno {
int i=2;
public void frase() {
System.out.println(“Estoy en un objeto de clase
Uno”);
}
}

class Dos extends Uno {


public void frase() {
i=3;
System.out.println(“Estoy en un objeto de clase Dos
con i:”+i);
}
}
class Tres extends Dos {
public void frase() {
System.out.println(“Estoy en un objeto de clase Tres
con i:”+i);
}
}

class Driver {
public static void main(String[] args) {
Uno [] lista =new Uno [2];
lista [0]= new Dos();
lista [1]= new Tres();
for (int i=0; i<2; i++){
lista[i].frase();
}
}
}

Ejercicio 3.* ¿Cuál es el resultado del siguiente programa?

class Uno {
public void imprime(double x,int j) {
System.out.println(“Valor de las variables pasadas:
“+x +”,”+j);
}
}
class Dos extends Uno {
public void imprime(int j,double x) {
System.out.println(“Valor de la variable:
“+j+”,”+x);
}
}
class Ejercicio {
public static void main(String [] args) {
Dos objeto1 =new Dos();
double a=5.0;
int i=4;
objeto1.imprime(a,i);
224 Introducción a la programación con orientación a objetos

objeto1.imprime(i,a);
}
}

Ejercicio 4.* ¿Cuál es el resultado del siguiente programa?

class Uno {
public void imprime(double x) {
System.out.println(“Valor de la variable pasada:
“+x);
}
}

class Dos extends Uno {


public void imprime(int j) {
System.out.println(“Valor de la variable: “+j);
}
}

class Ejercicio {
public static void main(String [] args) {
Dos objeto1 =new Dos();
double a=5.0;
int i=4;
objeto1.imprime(a);
objeto1.imprime(i);
}
}

Ejercicio 5.* En una empresa hay dos tipos de empleados: los encargados, que
reciben un salario mensual, y los empleados a comisión que reciben
un salario mensual base más el 10% de las ventas que hayan reali-
zado. Escriba un programa que indique el nombre y apellido de cada
trabajador y su salario. (Use polimorfismo y una clase abstracta.)

Ejercicio 6.* Implemente una interfaz que contenga las constantes SALARIO y
COMISION que se utilizan en la clase TrabajadorComision del ejerci-
cio anterior. ¿Cómo quedaría el código de esta clase?

Ejercicio 7.* Implemente el código de una clase abstracta denominada ObjetoGra-


fico que represente un objeto que se puede dibujar. Esta clase debe
contener dos atributos que indiquen las coordenadas de la figura. La
clase debe tener también dos métodos, uno debe ser abstracto y otro
no. Los nombres de los métodos son: mueveObjeto, que desplaza las
coordenadas x e y de la figura diez posiciones y el método dibujar
que dibuja la figura. Implemente dos clases Rectangulo y Circunfe-
rencia que hereden de la clase ObjetoGrafico.

Ejercicio 8.* Implemente el código de una interfaz llamada Primera que conten-
ga dos métodos A y B. Defina otra interfaz llamada Segunda que
herede de la anterior y además contenga un método llamado C.
Escriba el código de otra clase llamada Objetos que use la segunda
interfaz. ¿Cuántos métodos debe implementar esta clase? Imple-
Herencia 225

mente dichos métodos de forma que cada método imprima una línea
indicando el nombre del método. Cree un programa que utilice los
métodos definidos.

REFERENCIAS
ARNOW, D. y WEISS, G.: Introduction to Programming Using Java. An Object-Oriented Approach, Addison-Wes-
ley, 1998.
BISHOP, J. y BISHOP, N.: Java Gently for Engineers & Scientists, Addison-Wesley, 2000.
BISHOP, J.: Java. Fundamentos de Programación, Addison-Wesley, 1999.
BOOCH, G., RUMBAUGH, J. y JACOBSON, I.: El Lenguaje Unificado de Modelado, Addison-Wesley, 1999.
ECKEL, B.: Piensa en Java, Segunda Edición, Prentice-Hall, 2002.
JOYANES AGUILAR, L.: Programación Orientada a Objetos, Segunda Edición, Osborne McGaw-Hill, 1998, y las
referencias allí incluidas.
MEYER, B.: Construcción de Software Orientado a Objetos, Segunda Edición, Prentice-Hall, 1999.
SAVITCH, W.: Java. An Introduction to Computer Science & Programming, Prentice-Hall, 1999.
9

Ficheros

Sumario

9.1. Introducción to
9.2. Definición y uso 9.4. Operaciones sobre los ficheros
9.3. Clasificación de ficheros 9.5. Ficheros en Java
9.3.1. Clasificación de acuerdo al tipo de 9.5.1. Manejo de excepciones
acceso 9.5.2. Ficheros de acceso secuencial
9.3.2. Clasificación de acuerdo al tipo de 9.5.3. Ficheros de acceso directo
organización 9.5.4. La clase File
9.3.3. Clasificación de acuerdo al forma- 9.5.5. Ficheros y objetos
226 Introducción a la programación con orientación a objetos

9.1. INTRODUCCIÓN
Hasta ahora hemos estado manejando cantidades pequeñas de información. Esta información se gestio-
naba en la memoria central. En los ejercicios que se han visto, la información de entrada era tan poca
que se podía introducir por teclado con comodidad. A su vez, el procesamiento era tan sencillo que no
se tardaba mucho en obtener los resultados, y la información de salida se podía representar en unas pocas
líneas en la pantalla. Sin embargo, muchas veces se necesita trabajar con grandes cantidades de infor-
mación que, además, debe guardarse sobre un soporte duradero. En ocasiones, la información de entra-
da tiene una estructura compleja y no se puede teclear fácilmente cada vez que se necesita. Otras veces
el procesamiento (sobre todo en los problemas de computación científica) puede ser muy complejo invo-
lucrando muchas horas de cálculo, por lo que no se puede estar ejecutando el programa cada vez que
necesitemos algún dato de la información de salida. Por su parte, la información de salida puede ser tan
compleja que no se puede visualizar en una (o en muchas) pantallas o debe poderse consultar una y otra
vez. Es también frecuente que sea necesario usar ficheros auxiliares en el procesamiento, al manejarse
cantidades de información que no caben en la memoria central. En todas estas condiciones, la memoria
central no nos sirve (es volátil y limitada) y debemos trabajar con dispositivos de almacenamiento secun-
dario. La herramienta para realizar estas tareas es el fichero 1, que se usa sobre memoria secundaria, aun-
que en ocasiones, en algunos sistemas, se pueda implementar en memoria central.

9.2. DEFINICIÓN Y USO


Desde un más bien alto nivel de abstracción un fichero es una colección de datos estructurados que se
manejan como un todo (Prieto et al., 1995; Ruiz et al., 1998). Los ficheros están organizados en uni-
dades elementales, todas con la misma estructura, que se denominan registros y que a su vez constan
de unidades denominadas campos, vease la Figura 9.1. Desde el presente punto de vista, un fichero es
un conjunto de registros que a su vez son un conjunto de campos.

Fichero

Registros

Campos

Figura 9.1. Organización de un fichero

1
El término archivo también está muy extendido y es equivalente. En este texto usaremos preferentemente la palabra
fichero.
Ficheros 227

Un registro suele tener varios campos que almacenan diferente información. Por ejemplo, los dife-
rentes datos de un empleado en una empresa formarían los campos de un hipotético registro. Dichos
campos podrían ser:

Número de identificación; Nombre; Dirección; Teléfono; Categoría; Salario

El Número de identificación podría ser un entero, el Nombre y Dirección cadenas, el Teléfono un


entero o una cadena, la Categoría podría estar codificada usando un carácter y el Salario podría ser un
real. Como se puede observar, cada elemento de información (campo) puede ser de tipo diferente. El
conjunto de los campos define el registro en sí, es decir, un registro es un conjunto de campos rela-
cionados entre sí. El registro contiene información relativa a una entidad particular, en el ejemplo ante-
rior un empleado. El registro se usa de manera unificada en un programa, y es la unidad para el uso de
un fichero. Un registro tal como lo hemos considerado es un registro lógico (abstracto). Cuando se
desea transferir información del almacenamiento secundario a la memoria central se puede transferir
un registro lógico, menos de un registro lógico o incluso más de un registro lógico. Esta unidad de
transferencia se denomina registro físico o bloque. El número de registros lógicos por registro físico
se denomina factor de bloqueo (o más correctamente factor de bloque).
Un conjunto de registros relacionados entre sí define un fichero. Por lo tanto, un fichero es un con-
junto de registros que tienen algo en común. En el ejemplo anterior, un fichero de empleados con-
tendría la información de los empleados de una empresa con un registro para cada empleado y una
serie de campos para los elementos de información de cada empleado.
Para facilitar la recuperación de registros específicos de un fichero, se escoge por lo menos un
campo de cada registro como clave de registro. Una clave de registro identifica un registro como per-
teneciente a una entidad en particular y como distinto de todos los demás registros del fichero. En el
ejemplo anterior el número de identificación del empleado se podría escoger como clave del registro.
Para buscar y recuperar un empleado (registro) determinado dentro de un fichero lo que haríamos sería
localizar el registro cuya clave es la que buscamos.

9.3. CLASIFICACIÓN DE FICHEROS


Los ficheros pueden clasificarse desde distintos puntos de vista. Sin embargo, como los ficheros se
implementan sobre memoria secundaria, el tipo de fichero está relacionado con el soporte físico sobre
el que se implementan. El soporte es el medio físico donde se almacenan, donde se graban, los datos.
Los tipos de soporte utilizados en la gestión de ficheros pueden ser (Joyanes, 1997):

a) Soportes secuenciales.
b) Soportes direccionables.

Los soportes secuenciales son aquellos en los que el acceso se realiza de forma secuencial, de for-
ma que para acceder a una posición determinada hay que pasar por todas las anteriores (o las poste-
riores si estamos yendo de atrás hacia adelante). Dicho de otra forma, la información (los registros)
está almacenada consecutivamente. Así, para acceder a un determinado registro, n, se necesita pasar
por los n–1 registros anteriores. La secuencia puede corresponder al orden físico de los registros en el
fichero (organización secuencial) o bien al orden de claves (ascendente o descendente) de los registros
(organización indexada). Un ejemplo típico de soporte secuencial es una cinta magnética.
Los soportes direccionables permiten al acceso directo a una posición dada. Estos soportes se
estructuran de modo que cada elemento de información registrado se pueda localizar directamente por
una dirección, no requiriéndose pasar por los registros precedentes. En estos soportes, los registros
deben poseer un campo clave que los diferencie del resto de los registros del fichero. A través del valor
del campo clave, el sistema determina la dirección en la que se almacena el registro sobre el soporte.
228 Introducción a la programación con orientación a objetos

Una dirección en un soporte direccionable podría ser el número de pista junto con el número de sec-
tor de un disco magnético, por ejemplo. Los soportes direccionables típicos son los distintos tipos de
discos, ópticos o magnéticos, usados en los ordenadores. Lógicamente un soporte direccionable se
puede usar, si se desea, secuencialmente.

9.3.1. CLASIFICACIÓN DE ACUERDO AL TIPO DE ACCESO


Retomando ahora los ficheros como entidad, la primera distinción que podemos hacer es relativa al
tipo de acceso. El tipo de acceso a un fichero se refiere a la forma en la que se puede acceder a un
registro concreto, es decir, la manera en la que se localiza un registro. Según las características del
soporte empleado y el modo en que se han organizado los registros, se consideran dos tipos de acce-
so a los registros de un fichero:

a) Acceso secuencial.
b) Acceso directo.

Esta clasificación es paralela a la del tipo de soporte, pero no hay una correspondencia uno a uno
entre ellas.
El acceso secuencial es aquel en el que se van recorriendo los registros de forma consecutiva. Los
registros se recorren en orden, y no se puede saltar de uno, a otro que esté más de una posición por
encima o por debajo. Se accede a los registros según el orden de almacenamiento, uno detrás de otro.
Siempre es posible avanzar desde el principio hacia el final del fichero y a veces, depende del siste-
ma, puede ser posible también avanzar desde una posición hacia atrás. Un fichero de acceso secuen-
cial puede implementarse sobre soporte secuencial o direccionable.
El acceso directo es aquel en el que se puede acceder a cualquier registro desde cualquier otro, es
decir, saltando. No es forzoso recorrer el fichero secuencialmente, no hay que consultar los registros
precedentes. Este tipo de acceso sólo es posible con soportes direccionales.

9.3.2. CLASIFICACIÓN DE ACUERDO AL TIPO DE ORGANIZACIÓN


La organización de un fichero define la forma en que los registros se disponen sobre el soporte de
almacenamiento o la forma en la que se estructuran los datos en un fichero. La forma de organización
se decide cuando se diseña el fichero y, lógicamente, desde un punto de vista físico sólo se podrá
implementar un tipo determinado de organización sobre un soporte físico que la admita. En general,
se consideran tres organizaciones:

a) Organización secuencial.
b) Organización directa o aleatoria.
c) Organización secuencial indexada.

En la organización secuencial se diseña el fichero como una sucesión de registros almacenados


consecutivamente sobre el soporte externo, de tal forma que para acceder a un registro n hay que pasar
por los n-1 anteriores. Esta organización implica un tipo de acceso secuencial. Los registros se graban
consecutivamente cuando se crea el fichero y se debe acceder consecutivamente cuando se leen dichos
registros. El orden físico en que fueron grabados (escritos) los registros es el orden de lectura de los
mismos. Existe un registro especial, que es el último, e indica el fin del fichero. Este registro lo inclu-
ye el sistema automáticamente (no lo tiene que incluir el programador) y se suele denominar EOF (End
Of File), o marca de fin de fichero. Todos los dispositivos de memoria auxiliar soportan la organiza-
ción secuencial.
Ficheros 229

En la organización directa el orden físico de los registros no se corresponde con el orden lógico.
El acceso a los registros se hace directamente a través de su posición (lugar relativo que ocupan), no
de su orden. Por lo tanto, en cada momento se puede acceder al registro que necesitemos conocien-
do su posición dentro del fichero e indicando que queremos ir a esa posición concreta. Este tipo de
organización se relaciona con el acceso de tipo directo. En esta forma de organización podemos leer
y escribir en cualquier orden. Para saber a qué registro queremos llegar necesitamos usar la informa-
ción de uno de los campos (campo clave 2 o índice). Esto quiere decir que el programa que use el
fichero debe determinar la posición del registro que nos interesa a partir del valor del campo clave.
La idea es que conociendo el valor de la clave (un apellido o un número de identificación, por ejem-
plo) se obtenga el número del registro usando alguna técnica. La más sencilla es que la clave sea el
número de orden que corresponda al registro (el registro 1, el 2, etc.) y saltar a ese registro. Otras
veces no es tan sencillo y hay que usar algún algoritmo que permita convertir la clave en un número
de registro. Se dice en este contexto que usamos algún tipo o técnica de direccionamiento. Para una
exposición de diferentes modos de direccionamiento en los ficheros de organización directa véase
Prieto et al., 1995. Para que un fichero sea de organización directa debe estar almacenado en un
soporte direccionable.
En la organización secuencial indexada la información está organizada con ayuda de dos ficheros 3.
Uno de ellos, fichero de datos o registros, contiene la información total ordenada de acuerdo a un cam-
po clave. Este fichero debe permitir el acceso directo. Para acceder a los registros de este fichero tra-
bajamos con la ayuda de otro fichero, el de índices, que es de organización secuencial. Para poder usar
el fichero de índices debemos considerar el fichero de registros organizado en zonas. En el fichero de
índices tenemos tantas entradas (registros en este fichero) como zonas en el fichero de registros. En
cada entrada del fichero índice se almacena el último valor del campo clave de cada zona del fichero
de registros y la dirección del primer registro de esa zona. En la Figura 9.2 se presenta un ejemplo don-
de tenemos una serie de registros de personas, actuando el nombre de pila como campo clave.

Bartolo |1 1| Amparo
Juan |4 2| Ana Zona 1
Pedro |7 3| Bartolo
Fichero de índices 4| Ester
5| Eva Zona 2
Acceso secuencial 6| Juan
7| Lola
8| Lourdes
Zona 3
9| Luis
10 | Pedro

Fichero de índices

Acceso directo

Figura 9.2. Ejemplo de organización secuencial indexada

2
La clave de un registro puede estar formada por uno o varios campos dependiendo, de si un solo campo identifica de
forma única el registro. Por simplicidad, en este texto hablaremos de campo clave refiriéndonos a un solo campo del registro.
3
Estrictamente hablando no necesitaríamos dos ficheros, bastaría con uno dividido en dos zonas. Lo normal es, sin
embargo, dos ficheros.
230 Introducción a la programación con orientación a objetos

Por ejemplo, para buscar un nombre se busca primero secuencialmente en el fichero de índices
hasta encontrar un nombre que sea “mayor” (alfabéticamente) que el que buscamos. Miramos en ese
registro el valor de la dirección del fichero de registros y “saltamos” allí. Una vez en esa posición,
que es la primera de un bloque, recorremos secuencialmente el fichero hasta encontrar el nombre bus-
cado o un nombre que sea “mayor” que él, en cuyo caso el nombre no está en la lista. Veamos un
ejemplo intentando localizar a Luis en el caso de la Figura 9.2. El primer paso sería buscar a Luis u
otro nombre mayor, alfabéticamente, en el fichero de índices. Después de leer Bartolo y Juan lle-
garíamos a Pedro que es mayor (alfabéticamente), y entonces se empezará a realizar una búsqueda
secuencial en el fichero de registros, comenzando en la posición que indicaba la entrada donde esta-
ba Pedro, en nuestro caso la posición 7. Después de leer Lola y Lourdes se llegaría al nombre bus-
cado.
Los soportes que se utilizan para esta organización son los que permiten el acceso directo, nor-
malmente los discos magnéticos. Los soportes de acceso secuencial no pueden utilizarse ya que no dis-
ponen de la posibilidad de direccionamiento directo necesaria para el fichero de datos.
Desde el punto de vista genérico podemos distinguir únicamente entre la organización secuencial
y la de acceso directo. Normalmente, por tanto, se habla de ficheros secuenciales o ficheros de acce-
so directo.

9.3.3. CLASIFICACIÓN DE ACUERDO AL FORMATO

También conviene comentar que podemos clasificar los ficheros en función del formato usado para
almacenar la información en ellos. Las dos posibilidades son,

a) Ficheros binarios.
b) Ficheros de texto.

Los ficheros binarios son aquellos cuyo contenido son secuencias de dígitos binarios. Los ficheros
binarios se diseñan para que se lean desde un programa, pero no pueden leerse directamente con un
editor de texto.
Los ficheros de texto son aquellos cuyos contenidos son una secuencia de caracteres y pueden, por
lo tanto, ser leídos con un editor de texto.
Estos dos tipos de ficheros presentan sus ventajas y desventajas respectivas, considerémoslas.

— Los ficheros de texto pueden ser transportados de un ordenador a otro. Sin embargo, los bina-
rios dependen del ordenador y sólo pueden ser leídos en el mismo tipo de ordenador que los
creó. En el caso de Java, los ficheros binarios son independientes de la plataforma usada, pero
esto es una excepción.
— La gran ventaja de los binarios es que se procesan de manera más eficiente, ya que el sistema
se ahorra la conversión del texto al formato binario, que es el que al final maneja la máquina.
— Una gran ventaja de los ficheros de texto es que pueden leerse con cualquier editor de texto,
siendo inteligibles para las personas. Los ficheros binarios sólo los puede leer y escribir un pro-
grama. Si intentamos editar un fichero binario, se obtiene un galimatías sin sentido.
— En un fichero binario los datos se almacenan igual que en memoria principal. Cada elemento
de datos se almacena como una secuencia de bytes.

Cada tipo de fichero presenta sus particularidades y es en función de ellas que debemos escoger
un tipo u otro. Por ejemplo, como fichero auxiliar en un programa de cálculo científico nos intere-
sará un fichero binario, pues el procesamiento es más eficiente y su contenido no se necesita editar,
Ficheros 231

sólo se necesita que lo lea el propio programa. Sin embargo, para imprimir resultados necesitamos
un fichero de texto.

9.4. OPERACIONES SOBRE LOS FICHEROS

Desde el punto de vista semántico las operaciones más comunes que se pueden realizar sobre un fiche-
ro pueden clasificarse y considerarse independientemente de cualquier lenguaje. Una posible clasifi-
cación de dichas operaciones es la siguiente:

a) Creación.
b) Apertura.
c) Consulta.
d) Modificación.
e) Inserción.
f) Borrado.
g) Eliminación.
h) Clausura (cerrar).
i) Movimiento (sobre el fichero).

Pensando en orientación a objetos, si tuviésemos que crear una clase que representara los ficheros
deberíamos añadir métodos que realizarán las operaciones anteriores. Consideremos cada una de ellas.
La creación es la primera operación sobre un fichero, para ello es necesario saber el tipo del fiche-
ro y qué organización necesitamos para el mismo. Todo fichero debe estar creado antes de empezar a
operar en él por primera vez.
La apertura es la primera operación sobre un fichero que ya existe. Esta operación consiste en la
conexión desde el programa al fichero para permitir su uso.
La consulta de un fichero es el acceso a sus registros para recuperar y utilizar su información.
La modificación consiste en la alteración (actualización) de la información de algún o algunos
registros del fichero.
La inserción es la inclusión de un nuevo registro en el fichero.
El borrado es la eliminación del fichero.
La eliminación es la supresión de uno o más registros del fichero considerado.
La clausura (cerrar ficheros) es la operación que corta el acceso (desconecta) el fichero. Aunque
el sistema lo haga automáticamente cuando el programa termina normalmente, conviene cerrar los
ficheros, principalmente por dos razones. Primera, si el programa acaba anormalmente, el sistema no
cerrará el fichero automáticamente. El fichero quedará abierto sin tener ningún programa conectado a
él y esto puede dañarlo o bloquearlo. Segundo, en algunos casos cuando se quiere leer de un fichero
después de haber escrito en él, es necesario cerrar el fichero y reabrirlo para poder empezar la lectura
desde el principio. En cualquier caso, es más que recomendable cerrar los ficheros tras haber acabado
de trabajar con ellos.
El movimiento se refiere a la posibilidad de cambiar de un registro a otro dentro del fichero. En
un fichero secuencial no hay ninguna instrucción de “salto” hasta un registro y habrá que moverse
sobre dicho fichero un registro tras otro hasta llegar al que nos interesa. Si nuestro sistema lo permi-
te puede ser posible ir hacia atrás (retroceder) para localizar registros anteriores a aquel en el que esta-
mos. Si nuestro sistema no permite este retroceso es necesario “rebobinar” totalmente el fichero y
empezar a avanzar otra vez buscando el registro. En este caso, si existe una instrucción para rebobinar
deberemos usarla y si no habrá que cerrar el fichero y volverlo a abrir. En los ficheros de acceso direc-
to el movimiento se realiza por medio de desplazamientos a los registros de interés usando el valor
deseado del campo índice.
232 Introducción a la programación con orientación a objetos

9.5. FICHEROS EN JAVA


En un lenguaje determinado necesitaremos saber cómo crear ficheros de los distintos tipos que hemos
mencionado. También es necesario conocer cómo se realizan cada una de las operaciones citadas ante-
riormente. Consideremos este problema desde el punto de vista del lenguaje Java.
Como ya comentamos anteriormente, en Java todos los tipos de entradas y salidas se realizan por
stream (corriente) y se habla de corrientes de entrada o de salida. Un stream es un flujo de datos.
No hay sentencias de entrada y salida en Java. La entrada y salida se realiza usando bibliotecas de cla-
ses predefinidas. La mayoría de las operaciones de entrada-salida están definidas en el paquete
java.io (io es un acrónimo de input output) de la API de Java. Por las razones anteriores, Java con-
sidera los ficheros simplemente como flujos secuenciales de bytes. Cada fichero termina con un mar-
cador de fichero o bien en un número de byte específico registrado en una estructura de datos
administrativa mantenida por el sistema. Cuando se abre un fichero se crea un objeto y se asocia un
flujo (stream) a dicho objeto. A partir de ese momento, ese objeto “es” el fichero para nuestro pro-
grama.
En el manejo de ficheros es especialmente útil capturar las excepciones que se pueden producir.
Como ya introdujimos en un capítulo anterior, una excepción se puede entender como un problema
recuperable. Esto es, nuestro programa puede detectar que se ha producido el problema y actuar en
consecuencia para resolverlo. Si en el lenguaje que manejamos existe un mecanismo de gestión de
excepciones, es necesario conocerlo. Por esta razón vamos a presentar el mecanismo de captura y ges-
tión de excepciones particularizando la sintaxis a Java.

9.5.1. MANEJO DE EXCEPCIONES


Un programa puede llegar a encontrar una situación problemática que implique la finalización del mis-
mo. Si la causa de este problema se puede identificar, puede ser posible evitar que el programa finali-
ce a causa del mismo. Este tipo de problemas recuperables son las excepciones. Existe una clara
diferencia entre excepción y error. En ambos casos tenemos una situación problemática, pero la excep-
ción se puede capturar y hacer que el programa siga funcionando y el error es irrecuperable y hace que
el programa termine. Para manipular las excepciones debemos disponer de algún mecanismo de ges-
tión de excepciones. Una excepción se puede producir en distintos casos, por ejemplo, cuando:

— El índice de una matriz (array) está fuera de los límites permitidos.


— Se intenta dividir entre cero.
— Se intenta leer de un fichero que no existe.

En algunos lenguajes (como Java) existe un mecanismo de gestión de excepciones que permite
“capturar” el problema y evitar la finalización irrecuperable del programa. En Java toda excepción que
pueda producirse debe manejarse de alguna manera. La gestión de excepciones funciona, genérica-
mente, de la forma siguiente. En primer lugar debe definirse el bloque de código en el que se puede
producir la o las excepciones. A continuación, hay que indicar qué se debe hacer cuando se presente
cada una de dichas excepciones. Para ello en Java disponemos de la sentencia try-catch. En la par-
te del try (intenta) se debe indicar el bloque de código que puede producir las excepciones. Como
habitualmente, el bloque se delimita entre llaves. Las llaves indican dónde comienza y acaba el blo-
que try. A continuación, se escriben bloques que indican qué hacer si una determinada excepción es
capturada. Estos bloques van precedidos por la palabra catch (coge, captura) seguida entre parénte-
sis del nombre de la excepción que se captura. Se pueden escribir tantos bloques catch como excep-
ciones posibles puedan darse. La sintaxis del manejo de excepciones es:

try {
Ficheros 233

-- bloque de sentencias para el caso normal --


}
catch(excepción_1 identificador_1) {
-- bloque de sentencias para el caso de producirse la excepción_1 --
}
...
catch(excepción_n identificador_n) {
-- bloque de sentencias para el caso de producirse la excepción_n --
}

finally { //opcional
-- bloque de sentencias que se ejecuta siempre --
}

La cláusula finally es opcional y se ejecuta siempre, tanto si se produce una excepción como si
no, incluso aunque no exista ninguna cláusula catch. El finally se ejecuta en cuanto el flujo de con-
trol abandona la sentencia try.
En Java las excepciones son clases. Por eso, como podemos observar en los catch en el ejemplo
genérico anterior, al lado del nombre de cada excepción se incluye un identificador. Este identificador
es el de una referencia de la clase correspondiente a la excepción. Este identificador es arbitrario y muy
habitualmente se usa una simple e (de excepción).
El manejo de excepciones puede modificar el flujo de control del programa. Cuando se lanza (se
produce) una excepción, el control del programa sale del bloque try y examina los bloques catch
buscando un gestor o manejador (bloque catch) adecuado para ese tipo de excepción. Si se encuen-
tra, se ejecuta el bloque catch correspondiente. Seguidamente el flujo de control salta hasta el bloque
finally, si existe, o hasta el código que haya a partir del último catch.
Además de las excepciones generadas por el intérprete de Java y predefinidas en el sistema se pue-
den crear nuevas excepciones. La sintaxis para crear una excepción es similar a la sintaxis para crear
una clase hija que hereda de una clase padre. En concreto, toda excepción nueva debe heredar de la
clase Exception que es la clase padre de todo el sistema jerárquico de excepciones de Java. La sin-
taxis para la creación de una excepción nueva sería:

public class NombreException extends Exception {


public NombreException( ){ //Constructor
-- código del método --
}
}

Es normal que en el cuerpo del constructor no se incluya código ya que el interés es únicamente
la existencia de la clase. La ventaja es que el sistema de captura y gestión de estas nuevas excepcio-
nes es el mismo que para las predefinidas. Este tipo de excepciones siempre se lanzan explícitamente,
es decir, debe ser el programador quien las lance usando la palabra reservada throw. La sintaxis para
lanzar una excepción es:

throw new nombreExcepcion();

Por ejemplo:

if (denominador == 0)
throw new DivideporCeroExcepcion();

Si en un método se pueden producir (lanzar) excepciones pero no se capturan se debe indicar en


la cabecera del método que no queremos considerar esas excepciones. Dicho de otra forma, que vamos
234 Introducción a la programación con orientación a objetos

a permitir que se produzca el correspondiente error. Para ello se usa la palabra reservada throws en
la cabecera del método tal y como hemos hecho en algunos ejemplos previos. Por ejemplo:
public double division(int numerador, int denominador) throws
DivideporCeroExcepcion {
if (denominador == 0)
throw new DivideporCeroExcepcion();
return (double)numerador/denominador;
}

En el ejemplo anterior, se puede producir la excepción DivideporCeroExcepcion. Como no


hay una cláusula catch para ella es necesario indicar el throws en la cabecera del método. Otro ejem-
plo típico sería throws IOException que hemos ido escribiendo en los programas de capítulos ante-
riores porque no se capturaba dicha excepción.
Los catch se van considerando en el orden de escritura, de forma que se deben colocar las
excepciones más específicas primero y las más generales después. Por ejemplo, consideremos la
relación de herencia existente en Java entre las clases IndexOutOfBoundsException,
ArrayIndexOutOfBoundsException, y ArrayIndexOutOfBoundsException, ilustrada en la
Figura 9.3.

IndexOutOfBoundsException

StringIndexOutOfboundsException ArrayIndexOutOfBoundsException

Figura 9.3. Relación de herencia entre las clases consideradas

Para manejar las excepciones IndexOutOfBoundsException y ArrayIndexOutOfBoundsEx-


ception un programa tendría que tener la forma indicada en el Programa 9.1.

Programa 9.1. Ejemplo de excepciones

class Excepcion {
public static void main(String[] args) {
try {
System.out.println(args[0]);
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“B”);
}
catch(IndexOutOfBoundsException e) {
System.out.println(“A”);
}
}
}

ArrayIndexOutOfBoundsException es una excepción que hereda de IndexOutOfBoundsEx-


ception y, por lo tanto, la primera excepción es más específica. El programa anterior funcionaría nor-
malmente, pero si ponemos IndexOutOfBoundsException como primera excepción obtendríamos un
Ficheros 235

error, porque el segundo catch ya se habría tenido en cuenta al probar la excepción de la clase padre. Este
comportamiento permite un pequeño truco. Si no se recuerda el nombre de la excepción que se desea cap-
turar o se pretende capturar todas las excepciones de forma inespecífica se puede usar Exception. Como
ésta es la clase padre de toda la jerarquía de excepciones, capturándola se capturan todas ellas. Para ello
tendríamos que incluir en la estructura try-catch correspondiente el siguiente código:

catch(Exception e) {
//bloque de código
}

En el Programa 9.2 se muestra un ejemplo de excepciones definidas por el usuario.

Programa 9.2. Ejemplo de excepciones definidas por el usuario

class Aexception extends Exception {


public Aexception() {
}
}

class Bexception extends Exception {


public Bexception() {
}
}

class EjemploExcepciones{
public static void main(String [] args){
int valor=Integer.parseInt(args[0]);
try {
if (valor==3) throw new Aexception();
if (valor==5) throw new Bexception();
System.out.println(“llega hasta aqui”);
} //Fin del bloque try

catch(Aexception a){
System.out.println(“Error tipo A”);
}
catch(Bexception b){
System.out.println(“Error tipo B”);
}
finally{
System.out.println(“Ahora me toca a mi”);
}
System.out.println(“Imprimeme”);
}
}

La salida para una entrada con valor 3 es:

Error tipo A
Ahora me toca a mi
Imprimeme
236 Introducción a la programación con orientación a objetos

Para una entrada 5 la salida sería:

Error tipo B
Ahora me toca a mi
Imprimeme

Para una entrada distinta de 3 y 5 la salida sería:

llega hasta aqui


Ahora me toca a mi
Imprimeme

Obsérvese que las dos excepciones definidas no tienen código. Esto no es ningún problema pues-
to que lo importante es que exista la clase para poder luego arrojar la correspondiente excepción con
throw.
Tras haber presentado el mecanismo de captura y gestión de excepciones, abordemos cómo traba-
jar en Java con los distintos tipos de ficheros.

9.5.2. FICHEROS DE ACCESO SECUENCIAL


Java no obliga a los ficheros a tener una estructura, por tanto, el concepto de registro no existe. Esto
implica que el programador debe estructurar los ficheros desde un punto de vista lógico para satisfa-
cer las necesidades de las aplicaciones. El programador debe imponer una estructura de registros al
fichero. Como ya hemos indicado, un registro se compone de varios campos que en Java serán varia-
bles.
Recordemos que en Java la entrada/salida se realiza por medio de “corrientes” (streams) de datos.
De dónde venga o a dónde vaya dicha corriente no afecta al hecho de tomar o poner datos en ella.
Recordemos también que en Java los Streams son corrientes de 8 bits y que para una adecuada inter-
nacionalización con Unicode se introdujeron los Readers y Writers que son corrientes de entrada o
salida de 16 bits.
Con estas ideas generales, comencemos con la exposición de los ficheros secuenciales que pueden
ser de tipo binario o de tipo texto.

Ficheros secuenciales binarios

En Java podemos trabajar de dos formas con los ficheros secuenciales binarios:

a) Directamente byte a byte.


b) Como datos pasados a byte.

Consideremos las dos posibilidades.

a) Trabajo byte a byte

En este caso, los ficheros para escritura o lectura se abren creando objetos de las clases (streams)
FileOutputStream y FileInputStream.
Ficheros 237

a.1) Escritura

Cuando se abre un fichero para escribir en él, es decir, para salida, se debe crear un objeto de clase
FileOutputStream. Además, se debe pasar un argumento al constructor del objeto indicando el
nombre del fichero. Los ficheros existentes que se abren para salida se borran, es decir, se desechan
todos los datos del fichero. Si el fichero especificado no existe, se crea un fichero con ese nombre.
Veamos la sintaxis:

FileOutputStream salida;
salida= new FileOutputStream(nombre);

donde nombre, de clase String, contendrá el nombre del fichero a abrir, incluyendo opcionalmente
el directorio en el que se encuentra. Se puede escribir directamente entre los paréntesis y entre comi-
llas el nombre del fichero como una cadena literal, sin necesidad de utilizar una variable que habría
que declarar e inicializar previamente. El constructor admite otra forma:

FileOutputStream(File Objeto_File);

Esta segunda forma indica que se puede usar en el constructor un objeto de clase File. Como
expondremos más adelante, esta clase permite trabajar con las propiedades de un fichero y cuando se
crea un objeto de dicha clase se puede vincular a un fichero determinado.
Una vez abierto el fichero podemos escribir con el método write(int i) que escribe un byte en
el fichero. Tras concluir el trabajo con el fichero debemos cerrarlo por medio del método close().
A partir de la versión 1.1 de Java se añadió un nuevo constructor para la clase FileOutputStre-
am que permite abrir un fichero en modo de adición, sin borrar la información existente. La sintaxis es:

FileOutputStream(String nombre_fichero, boolean añadir)

si añadir es true el fichero se abre en modo de adición. El puntero interno del fichero se coloca al
final del mismo, lo que implica que la escritura se realiza a partir de lo último escrito. Si el fichero no
existe se crea y si existe se añaden los nuevos datos al final del mismo.
Ilustremos el uso de la clase FileOutputStream con un ejemplo donde se abra un fichero
secuencial binario para escribir en él una serie de valores enteros. El nombre del fichero se introducirá
por línea de órdenes, véase el Programa 9.3.

Programa 9.3. Escritura de información en un fichero binario de acceso secuencial usando la


clase FileOutputStream

import java.io.*;
class Ficheros {
public static void main(String [] args) {
FileOutputStream salida; //Se crea la referencia

try {
// Creando el fichero
salida=new FileOutputStream(args[0]);

/* Si no ha habido error de lectura se escribe un fichero


de enteros */
for (int i=0;i<99;i++) {
salida.write(i);
}
238 Introducción a la programación con orientación a objetos

Programa 9.3. Escritura de información en un fichero binario de acceso secuencial usando la


clase FileOutputStream (continuación)
salida.close();

} // Fin try

catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“Introduzca como argumento el “
+”nombre del fichero destino “);
}
catch(IOException e) {
System.out.println(“Excepcion de entrada/salida: “
+e.toString());
}

} // Fin método main

} // Fin clase Ficheros

En el Programa 9.3 se abre un fichero representado por el objeto salida y se escriben en él los
valores de 0 a 98. En el ejemplo se capturan dos excepciones. La primera es ArrayIndexOutOf-
BoundsException que se puede producir si no hemos introducido el nombre del fichero por línea de
órdenes. La segunda es la excepción general de entrada/salida (IOException). Al crear un objeto
FileOutputStream e intentar abrir un fichero, el programa prueba si la operación de apertura tuvo
o no éxito. Si la operación falla, se genera una excepción de E/S (IOException) que debe ser captu-
rada por el programa. Algunas de la posibles razones de que se lance una IOException al abrir fiche-
ros son intentar abrir para lectura un fichero que no existe, intentar abrir para lectura un fichero sin
tener permiso o abrir un fichero para escritura cuando no hay espacio en disco. Si el fichero se abre
con éxito, el programa está listo para procesar datos. El método close() lanza también la IOExcep-
cion que hay que capturar. El método toString del objeto e invocado en la IOException impri-
me información sobre la naturaleza de la excepción producida. Por ejemplo, si el fichero donde
queremos escribir está ya abierto por otro programa se produciría la excepción y el método toS-
tring() indicaría que el fichero ya está abierto.

a.2) Lectura

Para abrir un fichero binario para lectura a nivel de byte se usa la clase FileInputStream. El nom-
bre del fichero a leer se puede pasar como argumento al constructor de FileInputStream. Al igual
que en la escritura hay dos constructores de lectura habituales, y la creación del objeto con ellos sería:

FileInputStream entrada;
entrada=new FileOutputStream(nombre);

o
FileInputStream entrada;
entrada=FileInputStream(objeto File);

donde nombre, de clase String, contendrá el nombre del fichero a abrir, incluyendo el directorio si se
desea. En la segunda forma pasamos al constructor un objeto de la clase File, que veremos más adelan-
te. Todos estos constructores pueden lanzar una FileNotFoundException. Esta excepción puede cap-
Ficheros 239

turarse para indicar al usuario que ese fichero no se ha encontrado. La clase FileNotFoundException
se deriva de la IOException por lo que si se captura ésta y no la FileNotFoundException también
la gestionaremos. De todas formas conviene capturar la FileNotFoundException, porque así sabre-
mos exactamente la razón de la excepción. Además, al crear un objeto de clase FileInputStream e
intentar abrir un fichero, el programa determina si la operación de apertura tuvo éxito. Si la operación
falla, se genera una excepción de entrada/salida (IOException) que debe ser capturada por el progra-
ma.
Para leer del fichero, disponemos del método read() que lee un único byte y lo devuelve como
un entero (int). Es interesante saber que la marca de fin de fichero en estos ficheros se identifica como
valor –1. Localizando cuándo hemos leído –1 sabremos cuándo hemos llegado al fin del fichero. Una
vez finalizado el trabajo con el fichero debemos cerrarlo con el método close().
Ilustremos el uso de la clase FileInputStream con un ejemplo donde se abra el fichero secuen-
cial binario escrito en el Programa 9.3. Leeremos el contenido del fichero byte a byte copiándolo en
otro fichero y mostrando los datos leídos por pantalla. Los nombres de los ficheros origen y destino se
introducirán por línea de órdenes, véase Programa 9.4.

Programa 9.4. Ejemplo de la clase FileInputStream

import java.io.*;
class Ficheros {
public static void main(String [] args) {
int i;
boolean sigue=true;
FileInputStream entrada; //Se crea la referencia
FileOutputStream salida; //Se crea la referencia

try {
// Creando los ficheros
entrada=new FileInputStream(args[0]);
salida=new FileOutputStream(args[1]);

/* Si no ha habido error de lectura se escribe un fichero


de enteros */
do {
i=entrada.read();
sigue=(i!=-1);
if (sigue) {
salida.write(i);
System.out.println(i); // Visualizando contenido
}
} while (sigue);

salida.close();

} // Fin try

catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“Introduzca como argumento los “
+”nombres de los ficheros “
+”origen y destino”);
}
catch(FileNotFoundException e) {
System.out.println(“Fichero origen inexistente”);
}
240 Introducción a la programación con orientación a objetos

Programa 9.4. Ejemplo de la clase FileInputStream (continuación)


catch(IOException e) {
System.out.println(“Excepcion de entrada/salida: “
+e.toString());
}

} // Fin metodo main

} // Fin clase Ficheros

Cuando no introducimos el nombre del fichero por la línea de órdenes se lanza la excepción
ArrayIndexOutOfBoundsException que capturamos para indicar cómo hay que ejecutar el progra-
ma. También capturamos la excepción FileNotFoundException, que se produce cuando no se
encuentra el fichero especificado. Por último capturamos la excepción general de entrada y salida
(IOException).
Cuando se encuentra el fin de fichero, el método read() devuelve –1, por lo que lo aprovecha-
mos este hecho para leer hasta fin de fichero. El bucle do-while está controlado por la condición de
leer un valor distinto de –1. Observemos que la lectura y la copia de bytes es independiente de lo que
codifiquen dichos bytes. Es decir, eliminando la sentencia System.out.println(i) tendríamos un
programa de copia literal de ficheros.

b) Datos pasados a bytes


Escribir o leer datos como bytes puros y duros es rápido pero burdo, por lo que se usan las clases
DataOutputStream y DataInputStream que permiten trabajar con tipos primitivos. En cualquier
caso, el fichero al final está en formato binario. De forma análoga al caso de los ficheros binarios de
bytes organicemos la exposición en escritura y lectura.

b.1) Escritura
En este caso se puede usar la clase DataOutputStream conectada a un objeto de clase FileOutputS-
tream mediante encadenamiento de objetos de flujo (como ya hicimos con el BufferedReader y el
InputStreamReader). La creación del objeto sería:

DataOutputStream salida;
salida=new DataOutputStream(new FileOutputStream(nombre));

donde nombre, de clase String, contiene el nombre del fichero. También es válida la segunda forma
del constructor de la clase FileOutputStream vista anteriormente y que aceptaba un objeto de cla-
se File. En el ejemplo anterior creamos un objeto de clase DataOutputStream llamado salida
asociado al fichero indicado por nombre. El argumento nombre se pasa al constructor FileOut-
putStream que abre el fichero. Esto establece una “línea de comunicación” con el fichero. Es impor-
tante recordar que si abrimos un fichero existente directamente para salida, el contenido del fichero se
desechará sin advertencia alguna. Si deseamos añadir información podemos abrir el fichero indicando
en el constructor de la clase FileOutputStream que la opción de añadir es true.
Para aumentar la eficiencia de la salida al fichero podemos realizar almacenamiento temporal de
la salida a través de un buffer. Para ello, podemos usar el FileOutputStream como parámetro del
constructor de la clase BufferedOutputStream:

DataOutputStream salida=new DataOutputStream


Ficheros 241

(new BufferedOutputStream
(new FileOutputStream(nombre)));

Para escribir datos en un fichero podemos usar los métodos de la clase DataOutputStream.
Algunos de estos métodos son los siguientes:

writeInt(variable_tipo_entero): escribe un entero de tipo int.

writeUTF(objeto_tipo_cadena): escribe una cadena en formato UTF (formato independiente de


la plataforma) que usa 8 bits para los caracteres alfabéticos normales.

writeDouble(variable_tipo_doble): escribe un dato de tipo double.

writeFloat(variable_tipo_float): escribe un dato de tipo float.

writeChar(variable_tipo_carácter): escribe un dato de tipo char.

De forma análoga tenemos métodos para el resto de tipos primitivos como: writeBoolean, wri-
teByte, writeLong, writeShort, etc.

Por ejemplo, para escribir un entero en un fichero llamado salida haríamos:

int i=10;
salida.writeInt(i);

Una vez que hemos terminado de escribir es conveniente cerrar el fichero con el método close().
El trabajo con DataOutputStream se ilustra en el Programa 9.5 donde se van solicitando por
teclado una serie de valores reales y se van salvando en un fichero cuyo nombre se introduce también
por teclado.

Programa 9.5. Ilustración del uso de la clase DataOutputStream

import java.io.*;
class Ficheros {
public static void main(String [] args) {
double valor;
boolean seguir=true;
DataOutputStream salida;
BufferedReader teclado;

try {
teclado=new BufferedReader(new InputStreamReader(
System.in));
System.out.println(“Introduzca nombre del fichero:”);

salida=new DataOutputStream(new BufferedOutputStream


(new FileOutputStream(teclado.readLine())));

while (seguir) {
System.out.println();
System.out.println(“Introduzca un valor real:”);
valor=Double.parseDouble(teclado.readLine());
242 Introducción a la programación con orientación a objetos

Programa 9.5. Ilustración del uso de la clase DataOutputStream (continuación)

salida.writeDouble(valor);
System.out.println();
System.out.println(“Desea continuar. Si:0 No:1”);
valor=Double.parseDouble(teclado.readLine());
if (valor==1) {
seguir=false;
}
} // Fin while
salida.close();
}
catch(IOException e) {
System.out.println(“Excepcion de entrada/salida: “+
e.toString());
}

} // Fin metodo main

} // Fin clase Ficheros

b.2) Lectura

Como ya hemos indicado para la escritura, leer datos como bytes directamente es rápido pero burdo,
por lo que se usa la clase DataInputStream. Esta clase nos permite leer datos de tipo primitivo gra-
bados en un fichero binario. Lógicamente, los datos deben leerse del fichero en el mismo formato en
el que se escribieron en él. Por tanto, para leer datos escritos con DataOutputStream debemos usar
un DataInputStream encadenado a un FileInputStream. La declaración del objeto sería:

DataInputStream entrada;
entrada=new DataInputStream(new FileInputStream(nombre));

donde nombre, de clase String, identifica el fichero. De la forma anterior creamos un objeto de cla-
se DataInputStream llamado entrada asociado al fichero identificado por nombre. Este paráme-
tro, nombre, se pasa al constructor de la clase FileInputStream. Esto establece una “línea de
comunicación” con el fichero. También es válido el constructor de la clase FileInputStream que
acepta un objeto de clase File previamente asociado al fichero.
Como en el caso de la escritura con la DataOutputStream, para aumentar la eficiencia de la sali-
da se puede usar un buffer. Para ello podemos usar el FileInputStream como argumento del cons-
tructor de la clase BufferedInputStream de la forma siguiente,

DataInputStream entrada;
entrada=new DataInputStream(new BufferedInputStream
(new FileInputStream(nombre)));

Si el programa se abre con éxito estamos en condiciones de leer con los métodos correspondien-
tes, algunos de los cuales son,

readChar(): lee y devuelve un carácter


Ficheros 243

readDouble(): lee y devuelve un valor de tipo double

readInt(): lee y devuelve un entero de tipo int


244 Introducción a la programación con orientación a objetos

Programa 9.6. Ilustración del uso de la clase DataInputStream (continuación)

readFloat(): lee y devuelve un valor de tipo float

readLine(): lee una línea devolviendo una cadena (clase String)

readUTF(): lee una cadena en formato UTF y devuelve la misma cadena como String

Análogamente, existen métodos para lectura del resto de tipos primitivos como readBoolean,
readByte, readShort, readLong, etc. Por ejemplo, para leer un dato de tipo int de un fichero
identificado por el objeto salida haríamos,

int i=salida.readInt();

Todos los métodos anteriores arrojan la IOException. Si ésta no se captura en un catch debe-
mos indicar en la cabecera del método donde se realice la lectura throws IOException. Por otro
lado, si se llega al fin del fichero durante la lectura se lanza una excepción de fin de fichero identifi-
cada como EndOfFileException. Esta excepción puede capturarse en una cláusula catch de la for-
ma habitual,

catch(EOFException e) {
//código si se llega al final del fichero
}

Capturando esta excepción podemos leer del fichero con un bucle controlado por una condición
que devenga false cuando se produzca la EOFException. Esto se puede conseguir colocando una
variable lógica que controle el bucle y que se ponga a false en el catch que captura la EOFExcep-
tion.
Como habitualmente, cuando acabemos de leer es conveniente cerrar el fichero con el método
close().
Como ilustración construyamos un programa que lea hasta el final la serie de valores reales intro-
ducidos anteriormente en un fichero en el Programa 9.5 y los muestra por la pantalla. El nombre del
fichero se introducirá por teclado, véase el Programa 9.6.

Programa 9.6. Ilustración del uso de la clase DataInputStream

import java.io.*;
class Ficheros {
public static void main(String [] args) {
double valor;
boolean seguir=true;
DataInputStream entrada=null;
BufferedReader teclado;
try {
try {
teclado=new BufferedReader(new InputStreamReader(
System.in));
System.out.println(“Introduzca nombre del fichero origen:”);
entrada=new DataInputStream(new BufferedInputStream
(new FileInputStream(teclado.readLine())));
}
catch(FileNotFoundException e) {
Ficheros 245

System.out.println(“Nombre de fichero incorrecto”);


seguir=false; // Para no entrar en el if de abajo
} // Fin del segundo try-catch
// Si no se ha producido la FileNotFoundException se sigue
if (seguir) {
while (seguir) {
try { // try interno al while
valor=entrada.readDouble();
System.out.println(valor);
}
catch(EOFException e) {
System.out.println(“Alcanzado el fin del fichero”);
seguir=false; // Diciendo al while que acabe
} // Fin del try-catch interno al while

} // Fin del while

entrada.close(); // Cerrando el fichero

} // Fin del if

} // Se cierra el try más externo

catch(IOException e) {
System.out.println(“Excepcion de entrada/salida”+
e.toString());

} // Este catch corresponde al try más externo

} // Fin método main


} // Fin clase Ficheros

El Programa 9.6 muestra que la sentencia try-catch se puede concatenar y anidar como, por
ejemplo, los bucles. Hay un total de tres sentencias try-catch. La primera abarca todo el programa
y sirve para capturar la IOExcepcion que se puede producir en los distintos métodos que se usan. El
segundo try-catch está anidado al primero y captura la FileNotFoundException que se produ-
ce si el nombre dado al fichero es erróneo. En caso de producirse esta excepción se salta el if siguien-
te, dentro del cual está la lectura del fichero. En caso de entrar en el if hay una tercera sentencia
try-catch colocada dentro del bucle while que lee el fichero. Obsérvese cómo al producirse la
EOFExcepcion por haber llegado al fin del fichero la variable seguir que controla el bucle se pone
a false. Finalmente, cerramos el fichero con el método close(). A continuación, encontramos la
cláusula catch de la IOExcepcion del try más externo.

Ficheros secuenciales de texto


Para trabajar con ficheros secuenciales de texto se pueden usar las clases BufferedReader y
PrintWriter para escritura y lectura, respectivamente. Veamos cómo.

a) Escritura
Para escribir en un fichero de texto se puede usar la clase PrintWriter, que contiene los cono-
cidos métodos println y print. Estos métodos se comportan igual que los métodos
246 Introducción a la programación con orientación a objetos

System.out.println() o System.out.print() para la pantalla. Para abrir un fichero para escri-


tura debemos crear un stream de la clase PrintWriter, conectándolo al fichero de texto con el
siguiente constructor,

PrintWriter Nombre_flujo_salida;
salida =new PrintWriter(new FileWriter(nombre));

donde nombre, de clase String, identifica el fichero a usar. También es válida la forma del constructor
de la clase FileWriter donde se acepta un objeto de clase File. Al igual que con FileOutputStre-
am, si queremos añadir información podemos usar el constructor con la estructura FileWriter (nom-
bre, añadir) con la opción añadir a valor true. Conviene recordar que el fichero debe cerrarse
usando el método close() cuando se ha terminado de escribir en él.
Como ejemplo, construyamos un programa que lea tres líneas por pantalla y las escriba en un
fichero cuyo nombre se da como argumento por la línea de órdenes, véase Programa 9.7.

Programa 9.7. Ilustración del uso de la clase PrintWriter

import java.io.*;
class Ficheros {
public static void main(String [] args){
PrintWriter salida=null; //Inicializando referencias
BufferedReader entrada=null;
String fichero=””;
String linea=null;

try {
fichero=args[0];
entrada= new BufferedReader
(new InputStreamReader(System.in));
salida= new PrintWriter
(new FileWriter(fichero));

System.out.println(“Introduzca tres lineas de texto:”);


for (int cont=1;cont<4; cont++) {
linea=entrada.readLine();
salida.println(cont+” “+linea);
}
entrada.close();
salida.close();
System.out.println(“Estas lineas se han escrito en “
+fichero);
}

catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“Debe introducir el nombre del “
+”fichero como argumento”);
}
catch(IOException e){
System.out.println(“No se abrio bien el fichero\n”
+e.toString());
}
} //Fin método main
Ficheros 247

Programa 9.8. Ilustración de lectura-escritura en ficheros secuenciales de texto (continuación)

} //Fin clase Ficheros

b) Lectura

Podemos usar la clase BufferedReader para leer un fichero de texto con el habitual método
readLine(). La clase BufferedReader se usa como ya sabemos, pero para leer de un fichero se le
pasa a su constructor un objeto de la clase FileReader. La sintaxis es la siguiente,

BufferedReader entrada;
entrada= new BufferedReader(new FileReader(nombre));

donde nombre, de clase String, contiene el nombre físico del fichero a leer. El constructor de
FileReader también acepta un objeto de tipo File que contenga el fichero a abrir.
Una vez abierto el fichero podemos usar el método readLine(). Este método funciona de la
misma forma que cuando hemos leído de teclado, es decir, devuelve una cadena. La clase
BufferedReader también contiene el método read() para leer un solo carácter. El método devuel-
ve el carácter leído como un tipo entero. Si lo queremos en forma de carácter debemos usar un mol-
de, como se indica a continuación:

BufferedReader entrada=new BufferedReader(


new FileReader(“fichero.txt”);
char car;
car=(char)(entrada.read());

Conviene recordar que cuando se ha acabado la lectura se debe cerrar el fichero con el método
close().
Es interesante saber que el método readLine() devuelve null cuando llega al final del fichero,
y que el método read() devuelve –1. El programa puede entonces comprobar el fin de fichero com-
probando si readLine() devuelve null o si read() lee un –1. Estos métodos no lanzan una
EOFException.
Como ejemplo vamos a construir un programa que lea líneas de caracteres de un fichero y las
imprime en otro fichero incluyendo el número de línea, véase el Programa 9.8.

Programa 9.8. Ilustración de lectura-escritura en ficheros secuenciales de texto

import java.io.*;
class Ficheros {
public static void main(String [] args) {
String fichero1=””,fichero2=””;
BufferedReader entrada=null;
PrintWriter salida=null;

try {
try {
fichero1=args[0];
fichero2=args[1];
entrada= new BufferedReader
248 Introducción a la programación con orientación a objetos

(new FileReader(fichero1));
salida= new PrintWriter
(new FileOutputStream(fichero2));
int cont=0;

String linea=entrada.readLine();
while(linea!=null) { // Leyendo hasta final del fichero
cont++;
salida.println(cont+” “+linea);
linea=entrada.readLine();
}

System.out.println(cont +” lineas escritas en “


+fichero2);

} // Fin del try interno

catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“Debe introducir como argumentos “
+”los ficheros de entrada y salida”);
}
catch(FileNotFoundException e) {
System.out.println(“Fichero “+fichero1
+” no encontrado”);
}
finally {
entrada.close();
salida.close();
}
} // Fin del try externo

catch(IOException e){
System.out.println(“No se abrio bien el fichero\n”+
e.toString());
}

} //Fin metodo main


} //Fin clase Ficheros

Una vez más, usamos dos try-catch anidados. El más externo controla la aparición de la
IOException, que lanza, por ejemplo, el método close(). El más interno controla las excep-
ciones ArrayIndexOutOfBoundsException que se produce si falta el nombre de algún fichero,
y la FileNotFoundException que se produce si el nombre del fichero es incorrecto. En este caso
hemos colocado los close() de los dos ficheros en la cláusula finally. El método toString()
del objeto e, invocado en la IOException, informa sobre la naturaleza de la excepción produci-
da.

9.5.3. FICHEROS DE ACCESO DIRECTO


Hasta ahora hemos visto cómo crear y leer ficheros de acceso secuencial. Estos ficheros no son los
apropiados para aplicaciones donde debemos acceder de inmediato a un determinado registro de infor-
mación. Como ejemplos de aplicaciones donde es necesario acceso directo podríamos citar sistemas
Ficheros 249

bancarios, sistemas de reservas, etc. Estos sistemas se incluyen dentro de los sistemas llamados de pro-
cesamiento de transacciones, que requieren acceso rápido a datos específicos. Este tipo de acceso de
forma rápida y directa a los registros de un fichero se puede realizar con los ficheros de acceso direc-
to o aleatorio. En un fichero de acceso aleatorio se pueden insertar datos sin necesidad de destruir los
demás datos del fichero. Los datos previamente almacenados también pueden actualizarse o eliminarse
sin tener que reescribir todo el fichero.
Como ya hemos indicado, Java no impone estructura alguna a los ficheros, así que la aplicación
que desee usar ficheros de acceso aleatorio deberá crear y mantener la estructura necesaria. Podemos
usar diversas técnicas para crear ficheros de acceso aleatorio. La más sencilla es la de exigir que todos
los registros tengan la misma longitud (longitud fija). El empleo de ficheros de longitud fija permite a
un programa calcular con facilidad, en función del tamaño del registro y de la clave del registro, la
posición exacta de cualquier registro relativa al principio del fichero.
Comencemos exponiendo las generalidades en Java de los ficheros de acceso directo o aleatorio.

Creación de un fichero de acceso directo


Cuando de un fichero de acceso directo se trata, utilizamos la clase RandomAccessFile (fichero de
acceso aleatorio). No tenemos clases distintas para lectura y escritura. Los objetos RandomAccessFi-
le (fichero de acceso aleatorio) tienen todas las capacidades de los objetos de clase DataInputS-
t r e a m
y DataOutputStream que hemos expuesto en el anteriormente. Cuando se asocia un flujo
RandomAccessFile a un fichero, los datos se leen o escriben a partir de la posición en que nos
encontremos en el fichero. Todos los datos se leen o escriben como tipos de datos primitivos. Por
ejemplo, al escribir un valor int, se envían 4 bytes al fichero. Al leer un valor double, se recuperan
8 bytes del fichero. El tamaño de los diversos tipos de datos está garantizado porque Java tiene
tamaños fijos para todos los tipos de datos primitivos, sea cual sea el sistema (ordenador, sistema ope-
rativo) de trabajo.
La clase RandomAccessFile tiene dos constructores:

RandomAccessFile(File objeto_fichero, String modo)

y
RandomAccessFile(String nombre, String modo)

donde nombre identifica el nombre del fichero y modo indica el modo de apertura del fichero. Este
modo puede ser sólo lectura (sólo se podrá leer del fichero) o lectura-escritura (se puede leer y escri-
bir en el fichero). El modo de sólo lectura se indica con “r” (read) mientras que el de lectura-escritu-
ra se indica con “rw” (read-write). Obsérvense las dobles comillas usadas para denotar el modo, ya
que se trata de una cadena, incluso en el caso de indicar modo de sólo lectura, “r”.
Especialmente para los ficheros de acceso directo, debemos entender claramente que en Java el
fichero es una secuencia lineal de bytes que concluye en algún tipo de marca de fin de fichero (EOF,
End Of File). Para conocer dónde estamos en el fichero existe un puntero interno de posición. La situa-
ción se ilustra en la Figura 9.4.

EOF

Puntero
250 Introducción a la programación con orientación a objetos

Programa 9.9. Ilustración del uso de la clase RandomAccessFile (continuación)

Figura 9.4. Estructura de un fichero de acceso directo en Java

Con este esquema in mente podemos comentar algunos de los métodos genéricos de la clase
RandomAccessFile:

void seek(long posición): Se utiliza para establecer la posición actual del puntero dentro del
fichero. La variable posición indica el número de bytes desde el principio del fichero. Después de lla-
mar a seek, la siguiente operación de lectura o escritura se realizará en la nueva posición dentro del
fichero.
long getFilePointer(): Devuelve la posición actual del puntero del fichero (en bytes) a partir del
principio del fichero.
int skipBytes(int desplazamiento): Mueve el puntero, desde la posición actual, el número
de bytes indicado por desplazamiento, hacia delante si el valor es positivo o hacia atrás si el valor
es negativo. El método devuelve el número de bytes que se ha saltado (podemos haber alcanzado el
fin de fichero y no saltar todos los indicados en desplazamiento).
long length(): devuelve la longitud del fichero en bytes.

Una vez usado el fichero, debemos cerrarlo como siempre con el método close().

Escritura en un fichero de acceso directo


Para crear y escribir en un fichero de este tipo debemos crear un objeto de clase RandomAccessFi-
le. Al constructor se le pasan dos argumentos, el primero de clase String o de clase File indican-
do el nombre del fichero, y el segundo con el modo de apertura del fichero. En este caso el modo será
lectura-escritura, “rw”:

RandomAccessFile salida;
salida=new RandomAccessFile(nombre,”rw”);

Al igual que en los ficheros de acceso secuencial se debe capturar la IOException.


Una vez abierto el fichero, o creado si no existe, escribiremos en él. Existen varios métodos aná-
logos a los de la clase DataOutputStream. Algunos de ellos son:

writeInt(entero): Escribe un entero de tipo int.

writeDouble(doble): Escribe un dato de tipo double.

writeBytes(cadena): Escribe una cadena como una secuencia de bytes.

writeUTF(String): Escribe una cadena usando el formato UTF (formato independiente de la


plataforma) que usa 8 bits para los caracteres normales. Este formato añade 2 bytes al principio, que
indican la cantidad de bytes que conforman la cadena. Por eso, si escribimos n caracteres en UTF, en
el fichero se escribirán n * 1 byte 1 2 bytes.

Como siempre usaremos el método close para cerrar el fichero.


Como ejemplo, construyamos un programa que añade a un fichero una serie de registros formados
por un número de orden (de tipo int) y valores reales introducidos por teclado, véase el Programa 9.9.
Ficheros 251

Programa 9.9. Ilustración del uso de la clase RandomAccessFile

import java.io.*;
class Ficheros {
public static void main(String [] args) {
double valor;
int contador=0;
boolean seguir=true;
RandomAccessFile salida;
BufferedReader teclado;
try {
teclado=new BufferedReader(new InputStreamReader(
System.in));
System.out.println(“Introduzca el nombre del fichero:”);

salida=new RandomAccessFile(teclado.readLine(), “rw”);

salida.seek(salida.length()); /* Moviéndose al final


del fichero */

while (seguir) {
contador++;
System.out.println();
System.out.println(“Introduzca un valor real:”);
valor=Double.parseDouble(teclado.readLine());

salida.writeInt(contador);
salida.writeDouble(valor);

System.out.println();
System.out.println(“Desea continuar. Si:0 No:1”);
valor=Double.parseDouble(teclado.readLine());
if (valor==1) {
seguir=false;
}
} // Fin while
salida.close();
}
catch(IOException e) {
System.out.println(“Excepcion de entrada/salida: “+
e.toString());
}

} // Fin método main

} // Fin clase Ficheros

Obsérvese cómo nos colocamos al final del fichero. Se usa el método seek al que se le pasa como
parámetro la longitud del fichero obtenida con el método length. El Programa 9.9 es similar al Pro-
grama 9.5 con la DataOutputStream. En el programa si el fichero no existe se crea.

Lectura en un fichero de acceso aleatorio


252 Introducción a la programación con orientación a objetos

En esta sección vamos a abrir un fichero de acceso aleatorio para lectura, con el modo “r”. El uso del
constructor es similar al caso de escritura:

RandomAccessFile entrada;
entrada=new RandomAccessFile(nombre, “r”);

donde, como habitualmente, nombre es un objeto de clase String o File.


También aquí tenemos métodos similares a los de la clase DataInputStream. Algunos de tales
métodos son:

readInt(): lee y devuelve un entero de tipo int.

readDouble(): lee y devuelve un dato de tipo double.

readUTF(): Lee una cadena en formato UTF. Para los caracteres normales se usa un byte por
carácter, usándose dos caracteres adicionales para saber el número de bytes de la cadena. En otras pala-
bras, se lee directamente la misma cadena que se escribió con UTF, con su misma longitud.

Disponemos de métodos adicionales para leer todos los tipos primitivos, como readFloat(),
readShort(), etc.
Es importante tener en cuenta que si sólo queremos leer de un fichero se recomienda abrir en modo
lectura “r”. Esto evitará una modificación no intencional del contenido del fichero.
Especialmente para lectura es importante saber cómo moverse en el fichero. Para trabajar a nivel de
registro lo más útil es poder saltar registros y no bytes. En Java esto se puede hacer conociendo el núme-
ro de bytes de cada registro. Supongamos que en un fichero de acceso aleatorio vamos a almacenar los
datos de los clientes de una organización, que constan de un número de orden (int) el nombre escrito
en formato UTF con 30 caracteres (30+2 bytes) y un saldo (double). El tamaño del registro, en bytes,
es:

int (4 bytes) 1 nombre (30 1 2 bytes) 1 double (8 bytes) 5 44 bytes

Podemos asignar a una variable el tamaño del registro:

l_registro=44;

Conociendo la longitud del registro (número de bytes por registro) puedo cambiar de unidades: de
registros a bytes o de bytes a registros.
Para realizar la conversión es importante decidir si los registros van a comenzar a numerarse en
cero o en uno. La Figura 9.5 muestra un fichero como una serie de nueve bytes, numerados de 0 a 8,
que acaba con la marca de fin de fichero (EOF), y donde se usan registros de un tamaño de tres bytes.
En ella se muestra también la numeración de registros en 0-origen y en 1-origen. Claramente, si el pri-
mer registro lo numero como uno, para ir al registro n (al principio de él) debería colocarme en el byte
dado por la siguiente expresión:
Ficheros 253

0 1 2
0-origen

0 1 2 3 4 5 6 7 8 EOF

1-origen

1 2 3

Figura 9.5. Relación registros-bytes en un fichero de clase RandomAccessFile

posicion=(n-1)*l_registro;

Por otro lado, si el primer registro lo numero como cero, para llegar al principio del registro n
debería saltar la siguiente cantidad de bytes:

posicion=n*l_registro;
Como ilustración, desarrollemos un programa que devuelva del fichero del Programa 9.9 un regis-
tro que le solicitemos. Para solicitar el registro usaremos su número de orden (1-origen). Las peticio-
nes se harán por teclado, véase el Programa 9.10.

Programa 9.10. Ejemplo de acceso directo a registros en un fichero

import java.io.*;
class Ficheros {
public static void main(String [] args) {
double valor;
int n, l_registro, entero;
boolean seguir=true;
RandomAccessFile entrada;
BufferedReader teclado;

l_registro=12; // 4 bytes (int)+ 8 bytes (double)

try {
teclado=new BufferedReader(new InputStreamReader(
System.in));
System.out.println(“Introduzca el nombre del fichero:”);

entrada=new RandomAccessFile(teclado.readLine(), “r”);

while (seguir) {
System.out.println();
System.out.println(“Introduzca un numero de registro:”);
n=Integer.parseInt(teclado.readLine());

entrada.seek((n-1)*l_registro);

entero=entrada.readInt();
valor=entrada.readDouble();
254 Introducción a la programación con orientación a objetos

System.out.println();
System.out.println(“Registro: “+entero+” “+valor);
System.out.println();
System.out.println(“Desea continuar. Si:0 No:1”);
valor=Double.parseDouble(teclado.readLine());
if (valor==1) {
seguir=false;
}
} // Fin while
entrada.close();
}
catch(IOException e) {
System.out.println(“Excepcion de entrada/salida”
+e.toString);
}

} // Fin método main

} // Fin clase Ficheros

En el Programa 9.9 se calcula la longitud del registro y se abre el fichero para lectura. Dentro del
bucle se pregunta por los registros que se desea visualizar hasta que el usuario decide terminar. Como
se usa 1-origen lo que se debe indicar al programa son números de orden para identificar los registros.

9.5.4. LA CLASE FILE


Esta clase no trabaja sobre flujos como la mayoría de las clases definidas en java.io. Trabaja direc-
tamente con los ficheros y el sistema de ficheros, es decir, los objetos de la clase File no abren real-
mente un fichero ni ofrecen funciones de procesamiento de ficheros, sólo se usan para describir las
propiedades de un fichero. Un objeto File se utiliza para obtener o modificar información asociada
con un fichero de disco, como los permisos, hora, fecha y subdirectorio, o para navegar por la jerar-
quía de subdirectorios. Esta clase es de gran utilidad para recuperar del disco información acerca de
un fichero o de un subdirectorio concreto. Una utilidad importante de esta clase es la verificación de
si un fichero existe. Ya comentamos que si abrimos directamente con FileOutputStream un fiche-
ro que ya existe, se borra toda la información. Con la clase File podríamos ver si el fichero existe y
si es así abrir el fichero en modo añadir, o por lo menos advertir al usuario que se borrará toda la infor-
mación contenida en el fichero.
Un objeto de clase File se inicializa con uno de los tres constructores siguientes:

File(String nombre)

donde nombre contendrá un nombre de fichero o directorio, incluido el camino hasta llegar a él. Por
defecto estaremos en el directorio de trabajo.

File(String directorio, String nombre)

donde directorio incluye la trayectoria hasta llegar al directorio o fichero indicado por nombre.

File(File directorio, String nombre)


Ficheros 255

igual al anterior pero en directorio se utiliza un objeto de la clase File, previamente creado, para
localizar el directorio o fichero indicado por nombre.

Un ejemplo de creación de un objeto de clase File asociado a un fichero sería:

File fichero=new File(“datos.dat”);

Algunos métodos de la clase File son:

exists(): Devuelve true si el nombre especificado como argumento en el constructor de la cla-


se File es un fichero o directorio que está en el camino especificado. Devuelve false en caso con-
trario.

getName(): Devuelve un String con el nombre del fichero o directorio.

length(): Devuelve un long que nos da la longitud del fichero en bytes.

lastModified(): Devuelve un long que es una representación, dependiente del sistema, de la


fecha y hora en que se modificó por última vez el fichero o directorio. El valor devuelto sólo sirve para
hacer comparaciones con otros valores devueltos por este método.
list(): Devuelve una matriz de objetos String que representa el contenido del directorio.

delete(): Borra el fichero o directorio. Devuelve true si se ha borrado, y false si no se puede


borrar.

Puedo entonces crear un fichero con la clase File y luego pasarlo como parámetro a los distintos
constructores de las clases vistas hasta ahora para manipulación de ficheros.

9.5.5. FICHEROS Y OBJETOS


Respecto al uso de ficheros y objetos existe una posibilidad no relacionada con lo visto hasta ahora en el
capítulo. Se trata de la posibilidad de salvar objetos como tales en un fichero. Es importante distinguir este
caso de lo considerado hasta el momento. La exposición realizada en el capítulo nos ha mostrado cómo
usar los ficheros como una estructura de datos externa. Es posible por lo tanto mantener más información
en el fichero que la que cabría en la memoria central. Lo que se plantea en este apartado es guardar en un
fichero un objeto existente en la memoria. No se trata, por lo tanto, de otra forma de trabajar con fiche-
ros, entendidos como estructuras de datos permanentes, sino de una manera de salvar en un fichero la
información contenida en un objeto en un momento concreto y poder recuperarla posteriormente.
En Java para poder escribir y leer un objeto de una clase en un fichero debe indicarse que la clase
implementa la interfaz Serializable de la forma siguiente,

class Ejemplo implements Serializable {

—- Código para la clase Ejemplo —-

A partir de este momento los objetos de la clase Ejemplo podrían guardarse y leerse de un fiche-
ro. Para guardar o leer un objeto en un fichero se debe crear una corriente de “objetos” con la clase
ObjectOutputStream para salida y ObjectInputStream para lectura, conectadas con las clases
FileOutputStream y FileInputStream, respectivamente. Los constructores se usan de la forma
256 Introducción a la programación con orientación a objetos

siguiente,

a) Creación de un stream de objetos para salida:

ObjectOutputStream salida;
salida=new ObjectOutputStream(FileOutputStream(nombre));

b) Creación de un stream de objetos para entrada:

ObjectInputStream entrada;
entrada=new ObjectInputStream(FileInputStream(nombre));

donde nombre, de tipo String, identifica el fichero a usar.


Tras crear las corrientes se usan los métodos,

writeObject(Objeto): Escribe el Objeto en la corriente de salida.

readObject(): Lee y devuelve el Objeto de la corriente de entrada.

En particular, readObject() devuelve un objeto de clase Object, así que luego se debe usar un
molde correspondiente a la clase del objeto en cuestión. Veamos un ejemplo. Supongamos que tene-
mos una clase Ejemplo indicada como Serializable. Para escribir un objeto, obj1, que hubiéra-
mos creado de la clase Ejemplo, en un fichero llamado datos.dat haríamos,

ObjectOutputStream salida;
salida=new ObjectOutputStream(FileOutputStream(“datos.dat”));
salida.writeObject(obj1);

Para leer el objeto del fichero y enlazarlo con una referencia llamada obj2 de clase Ejemplo haría-
mos,

ObjectInputStream entrada;
entrada=new ObjectInputStream(FileInputStream(“datos.dat”));
obj2=(Ejemplo) entrada.readObject();

Para información más detallada sobre la “serialización” de objetos en Java consúltense Arnold et
al., 2000; Lambert y Osborne, 1999. Se puede encontrar información específica sobre el paquete
java.io y sus clases en la documentación del Java 2 SDK de Sun (Java, 2002) y en el libro (Harold,
1999).

EJERCICIOS PROPUESTOS
Ejercicio 1.* En un fichero de acceso aleatorio se han escrito una serie de regis-
tros compuestos por un campo entero (int, 4 bytes) y uno real en
doble precisión (double, 8 bytes). Sobre el fichero se aplica el méto-
do seek de la forma siguiente: fichero.seek(24). Si el contenido
del fichero es:

8 20.5
1 15.8
3 40.9
Ficheros 257

6 2.5

donde cada línea representa un registro, ¿qué valores se leerían al


hacer fichero.readInt() seguido de fichero.readDouble()?

Ejercicio 2.* En un fichero de acceso aleatorio se han escrito una serie de regis-
tros compuestos por un campo entero (int, 4 bytes) y uno real en
doble precisión (double, 8 bytes). Si queremos leer a partir del cuar-
to registro escrito (inclusive), ¿cómo deberíamos aplicar el método
seek sobre el fichero?

Ejercicio 3.* Construya un programa que lea por teclado un cierto número de
clientes. Para cada cliente se debe introducir también por teclado un
número de cuenta, el nombre y el saldo. Estos datos se deben alma-
cenar en un fichero de acceso secuencial binario cuyo nombre se
dará como argumento por la línea de órdenes.

Ejercicio 4.* Suponga que tenemos dos excepciones del sistema, Exception y
NumberFormatException que hereda de Exception. Queremos que
un método capture la excepción NumberFormatException cuando se
produzca la situación específica a la que corresponde, y que en los
demás casos se capture la excepción Exception. ¿En qué orden
habría que colocar las excepciones en la sentencia try-catch?

Ejercicio 5.* Suponga que tenemos un fichero llamado datos donde se han escri-
to una serie de enteros (int) por medio de la clase DataOutputS-
tream. Escriba un programa que utilice el método readInt() de la
clase DataInputStream para leer todos los datos del fichero anterior
e imprimirlos por pantalla. Utilice la EOFException para controlar el
fin del fichero. Asegúrese de que en cualquier situación el fichero se
cierra adecuadamente.

Ejercicio 6.* A fin de ocultar los detalles de implementación, diseñe y programe


una clase para escritura en un fichero secuencial byte a byte.

Ejercicio 7.* A fin de ocultar los detalles de implementación, diseñe y programe


una clase para escritura de datos en un fichero secuencial.

Ejercicio 8.* A fin de ocultar los detalles de implementación, diseñe y programe


una clase para trabajar con registros en un fichero de acceso alea-
torio. Como ejemplo considere un registro de clientes formado por
un número de cliente, un nombre o identificación para el mismo y
un saldo.

REFERENCIAS
ARNOLD, K., GOSLING, J. y HOLMES, D.: El Lenguaje de Programación Java, Addison Wesley, 2000.
HAROLD, E. R.: Java I/O, First Edition, O’Reilly, 1999.
Java: http://java.sun.com última visita realizada en junio de 2002.
JOYANES, L.: Fundamentos de Programación, Segunda Edición, McGraw-Hill, 1997.
258 Introducción a la programación con orientación a objetos

LAMBERT, K. A. y OSBORNE, M.: Java A Framework for Programming and Problem Solving, PWS Publishing.
Brooks/Cole Publishing Company, 1999.
PRIETO, A., LLORIS, A. y TORRES, J. C.: Introducción a la Informática, McGraw-Hill, Segunda Edición, 1995.
RUIZ, I. L., ROMERO DEL CASTILLO, J. A. y GÓMEZ-NIETO, M. A.: Ficheros. Organizaciones Clásicas para el Alma-
cenamiento de la Información, Universidad de Córdoba, 1998.
10

Ordenación y Búsqueda

Sumario

10.1. Introducción 10.3. Búsqueda


10.2. Ordenación 10.3.1. Búsqueda lineal
10.2.1. Ordenación por intercambio 10.3.2. Búsqueda binaria
10.2.2. Ordenación por selección 10.3.3. Comparación de métodos
10.2.3. Ordenación por inserción
10.2.4. Comparación de métodos
258 Introducción a la programación con orientación a objetos

10.1. INTRODUCCIÓN

En este capítulo nos vamos a centrar en dos actividades comunes y totalmente extendidas en el
ámbito de la programación como son las de ordenación y búsqueda. La ordenación implica la dis-
tribución de una serie de elementos de acuerdo a una cierta regla o norma. Por ejemplo, ordenar
una serie de nombres aplicando un criterio alfabético. El interés de la ordenación radica en su fre-
cuente uso y su utilización por parte de otras técnicas como la búsqueda. Según estadísticas acep-
tadas, los ordenadores gastan más de un cuarto de su tiempo en labores de ordenación (Smith,
1987). Como hemos indicado, la ordenación suele ser un paso previo para acelerar búsquedas pos-
teriores. Justamente, la búsqueda es otra actividad, en principio sencilla, pero relacionada con la
ordenación. La búsqueda implica la determinación de la existencia de un cierto elemento en una
serie. Como podemos ver, en ambos casos se trabaja sobre un conjunto de elementos que se
encuentran recogidos en alguna estructura de datos que puede residir en memoria principal o
secundaria, normalmente dependiendo de su tamaño. En este capítulo y dado el carácter introduc-
torio del texto usaremos como estructuras de datos, estructuras lineales de elementos implementa-
das con matrices.

10.2. ORDENACIÓN

Específicamente en el campo de la computación se entiende por ordenación la distribución de una


serie de elementos en orden creciente o decreciente, de acuerdo a un cierto criterio. Puesto que la
ordenación es una actividad tan común, a lo largo del tiempo se han desarrollado muchos algorit-
mos con variada eficiencia. La clasificación taxonómica clásica es la debida a Knuth (Knuth,
1998).
Dependiendo normalmente de la cantidad de elementos a ordenar, se puede trabajar en memoria
principal, hablándose de ordenación interna, o en memoria secundaria, hablándose entonces de orde-
nación externa. En este capítulo vamos a considerar sólo la ordenación interna. Respecto a la ordena-
ción interna disponemos de distintas técnicas de variada complejidad. Así, las cinco categorías clásicas
de los métodos de ordenación son: inserción, intercambio, selección, fusión y distribución (Knuth,
1998). De acuerdo a Smith (Smith, 1987), una organización posible de estas cinco categorías para los
algoritmos de ordenación interna sería,

a) Métodos basados en comparación:

a.1. Ordenación por intercambio.


a.2. Ordenación por selección.
a.3. Ordenación por inserción.
a.4. Ordenación por fusión o mezcla (Merge sorting).

b) Métodos de distribución

En este texto nos vamos a centrar en los tres primeros casos de los métodos basados en compa-
ración. Se remite al lector interesado en una visión más completa de los diferentes métodos a textos
más especializados en el campo (Knuth, 1998; Smith, 1987; Wirth, 1986; Gonnet y Baeza-Yates,
1991).
La característica común de todos los métodos que vamos a estudiar es que se basan en la realiza-
ción de comparaciones sobre el conjunto de elementos a ordenar. La manera en que se realizan estas
comparaciones varía de algoritmo en algoritmo, siendo la aproximación más directa la del algoritmo
de ordenación por intercambio.
Ordenación y Búsqueda 259

10.2.1. ORDENACIÓN POR INTERCAMBIO


Este algoritmo de clasificación se conoce tradicionalmente como método de la burbuja. El algoritmo
se basa en el principio de comparar pares de elementos adyacentes e intercambiarlos entre sí hasta que
estén todos ordenados. Veamos un ejemplo de ordenación en orden creciente. Consideremos la
siguiente lista de valores:

50 15 56 14 35 1

que queremos ordenar en orden ascendente. Los pasos a seguir serían:

1. Comparar los dos primeros elementos, 50 y 15. Si están en orden se mantienen como están,
en caso contrario, se intercambian entre sí. En este caso se intercambiarían:

15 50 56 14 35 1

2. A continuación, se comparan los elementos 2.º y 3.º, 50 y 56 en este caso. De nuevo se inter-
cambian si es necesario. En este caso no lo sería.
3. El proceso continúa hasta que cada elemento de la lista ha sido comparado con sus elementos
adyacentes y se han realizado los intercambios necesarios. Al acabar de realizar esta opera-
ción sobre la serie tendríamos un valor colocado en su posición final. En este caso, el valor 56
quedaría en último lugar.
4. Ahora se repetiría el proceso sobre la lista pero sin incluir el último elemento pues ya está
ordenado. Dados n valores iniciales, si se efectúa (n 2 1) veces la operación sobre la lista de
valores se consigue ordenar la misma.

En el ejemplo, la secuencia de pasos (indicando en negrita las parejas comparadas) para la prime-
ra pasada sobre el conjunto de elementos serían:

50 15 56 14 35 1
15 50 56 14 35 1
15 50 56 14 35 1
15 50 14 56 35 1
15 50 14 35 56 1
15 50 14 35 1 56

Como podemos observar, el último número ya está en su sitio. Ahora repetiríamos con la sublista
sin ordenar (todos los elementos menos el último) y así hasta un total de cinco pasadas.
El nombre de método de la burbuja proviene del hecho de que el elemento que se ordena en cada
iteración va recorriendo la lista hasta su posición final, igual que una burbuja en un vaso va desde el
fondo hasta arriba. Dada una lista de n elementos, el pseudocódigo para el algoritmo, ordenando en
orden creciente y considerando 0-origen para la lista, sería el siguiente,

Inicio
Leer matriz lista con n valores
Para ii0 mientras i < (n-1) incremento iii+1
Para ji0 mientras j < (n-i-1) incremento jij+1
Si lista (j) >lista (j+1) entonces
intercambiar lista (j) con lista (j+1)
Fin_Si
Fin_Para
Fin
260 Introducción a la programación con orientación a objetos

Un método en Java que aplicase este algoritmo a una lista de enteros podría ser el siguiente,

// Ordenación por intercambio. Orden creciente

public void burbuja(int [] lista) {


int aux, longitud;
longitud=lista.length;
for (int i=0; i<longitud-1; i++) {
for (int j=0; j< (longitud-1-i); j++) {
if (lista [j]>lista[j+1]) {
aux=lista[j];
lista[j]=lista[j+1];
lista[j+1]=aux;
}
} // Fin for j
} // Fin for i
} // Fin método

Obsérvese que al pasar la lista por referencia (puesto que es un objeto de clase matriz) no es nece-
sario devolverla con un return.

10.2.2. ORDENACIÓN POR SELECCIÓN

La ordenación por selección toma su nombre del hecho de aplicar una serie de operaciones de selec-
ción para ordenar una lista. La idea básica es la de seleccionar elementos uno a uno y colocarlos en su
posición definitiva en la lista.
Supongamos que queremos ordenar una lista de valores numéricos en orden creciente, por ejem-
plo la misma lista usada en el método de la burbuja,

50 15 56 14 35 1

En la ordenación por selección actuaríamos de la siguiente forma:

1. Seleccionaríamos el elemento menor y lo colocaríamos en la primera posición, intercambian-


dolo con el valor existente allí. En el ejemplo seleccionaríamos el valor 1 y el resultado sería,

1. 1 15 56 14 35 50

1. El primer elemento ya está ordenado, ya está en su posición final.


2. Seleccionamos como nueva lista la sublista obtenida eliminando el primer elemento.
3. Volvemos al paso 1 para ordenar la sublista.

Si la lista contiene n elementos, tras un total de (n 2 1) pasadas la lista estaría ordenada. Como ilus-
tración consideremos los resultados en cada pasada para la lista del ejemplo,

50 15 56 14 35 1
1 15 56 14 35 50
1 14 56 15 35 50
1 14 15 56 35 50
1 14 15 35 56 50
1 14 15 35 50 56 i Resultado final
Ordenación y Búsqueda 261

Para una lista de n elementos, el pseudocódigo del algoritmo de selección, ordenando en orden cre-
ciente y con 0-origen para la lista, sería,

Inicio
Leer matriz lista con n valores
Para ji0 mientras j < n incremento jij+1
indice_minij
Para iij+1 mientras i < n incremento iii+1
seleccionar el índice del elemento menor
Fin_Para
intercambiar lista(j) con lista(indice_min)
Fin_Para
Fin

La implementación de la ordenación por selección usa dos bucles para ordenar la lista. El bucle
externo controla la posición de la matriz donde hay que colocar el valor menor. El bucle interno
encuentra el valor menor del resto de la lista, mirando en todas las posiciones mayores o iguales que
el índice especificado en el ciclo externo. Cuando se encuentra el valor menor, se intercambia con el
valor almacenado en indice_min. Este algoritmo encuentra el valor menor en la lista durante cada
iteración, por lo que ordena en orden ascendente. Se puede cambiar a orden descendente sólo con bus-
car el valor mayor en cada iteración.
Veamos un método en Java que aplica el algoritmo:

// Ordenación por selección. Orden creciente

public void seleccion(int [] lista) {


int aux, indice_min, n;

n=lista.length;

for (int j=0; j<n; j++) {


indice_min=j;
for (int i=j+1; i<n; i++) {
if (lista[i]<lista[indice_min]) {
indice_min=i;
}
}
aux=lista[j];
lista[j]=lista[indice_min];
lista[indice_min]=aux;
}
}//Fin método selección

Una vez más, el paso por referencia de la lista hace innecesaria la devolución de la matriz orde-
nada con un return.

10.2.3. ORDENACIÓN POR INSERCIÓN

En este método se selecciona un elemento y se coloca directamente en el sitio que le corresponde entre
todos los que ya se han ordenado. La técnica es la misma que cuando se ordena un palo de una bara-
ja de cartas.
262 Introducción a la programación con orientación a objetos

Vamos a ilustrar el método considerando una vez más una ordenación en orden creciente en la lis-
ta de valores,

50 15 56 14 35 1

1. Comenzamos con el segundo valor (el primero actúa como referencia) y lo colocamos en la
posición que le corresponda con respecto al primero. En nuestro ejemplo habría que poner el
valor 15 delante del 50.

1. 15 50 56 14 35 1

2. Tomamos el siguiente elemento y lo colocamos (insertamos) en la posición que le correspon-


da entre los anteriores.
3. Repetimos el paso 2 hasta que no queden más elementos.

Lógicamente, para n elementos necesitaríamos (n 2 1) inserciones para ordenar la lista. Para nues-
tra lista de ejemplo, el resultado de cada inserción (marcando el elemento insertado) sería,

15 50 56 14 35 1
15 50 56 14 35 1
14 15 50 56 35 1
14 15 35 50 56 1
1 14 15 35 50 56 i Resultado final

Cada elemento de la lista se va seleccionando consecutivamente. En cada selección se coloca el


elemento en la posición que le corresponda entre los anteriores (los ya ordenados). El efecto es el de
insertar cada elemento en su posición, moviendo hacia el final los elementos que quedan desde el
insertado hacia el último.
Para una lista de n elementos a ordenar en orden creciente, el pseudocódigo del algoritmo de inser-
ción, con 0-origen para la lista, sería,

Inicio
Leer matriz lista con n valores
Para ii1 mientras i < n incremento iii+1
valori lista(i)
posicionii
Mientras (posicion>0) y (lista(posicion-1) > valor)
lista(posicion)ilista(posicion-1)
posicion iposicion-1
Fin_Mientras
lista(posicion)ivalor
Fin_Para
Fin

De manera similar al método de selección esta ordenación usa dos bucles para ordenar una lista.
En la ordenación por inserción, sin embargo, el ciclo externo controla el índice en la matriz del ele-
mento a ser ordenado. El ciclo interno compara el valor actual a ser colocado (insertado) con los valo-
res anteriores (los cuales son una sublista ordenada de la lista entera). Si el valor actual es menor que
el valor en posición, entonces se corre el valor del índice posición a la derecha (una unidad
menos). Los desplazamientos continúan hasta que se localiza un valor menor que el que estamos colo-
cando o hasta que llegamos al principio de la lista. Cada iteración del ciclo externo añade un valor más
a la sublista ordenada de la lista, hasta que la lista entera queda ordenada. El algoritmo realiza la inser-
Ordenación y Búsqueda 263

ción de un valor copiando hacia la derecha los valores de la sublista ordenada. Por ejemplo, en nues-
tra lista cuando nos toca colocar el elemento con índice 3 (el valor 14) el bucle Mientras realizaría
la inserción de la forma siguiente,

15 50 56 14 35 1 i Punto de partida
15 50 56 56 35 1
15 50 50 56 35 1
15 15 50 56 35 1
14 15 50 56 35 1 i Resultado final

El efecto es que los valores de la sublista ordenada se mueven para hacer sitio al valor insertado.
Veamos un método en Java que aplica el algoritmo:

// Ordenación por Inserción. Orden creciente


public void insercion(int [] lista) {
int valor, posicion, n;

n=lista.length;
for (int i=1; i<n; i++) {
valor=lista[i];
posicion=i;
while (posicion>0 && lista[posicion-1] > valor) {
lista[posicion]=lista[posicion-1];
posicion--;
}
lista[posicion]=valor;
}
} // Fin método inserción

Una vez más, el paso por referencia de la matriz simplifica la devolución de los resultados.

10.2.4. COMPARACIÓN DE MÉTODOS

A la hora de seleccionar un método de ordenación u otro hay diferentes factores a tener en cuenta. Así,
la facilidad de inteligibilidad del algoritmo, su eficiencia o el consumo de recursos requeridos pueden
ser factores importantes. El criterio más usado es el de la eficiencia medida a través de la complejidad
de cada algoritmo. Desde este punto de vista debemos considerar el número de comparaciones reali-
zadas y el número de intercambios o recolocaciones que se hacen con los elementos de la lista. Ana-
licemos los dos factores,

a) Número de comparaciones
Para n elementos, el método de la burbuja realiza como máximo n 2 i comparaciones en la pasada i,
existiendo un total de n 2 1 pasadas. El número total de comparaciones es,

n21 n(n 2n(n


1) 2 1)
6(n 2 i) 5 (n 2 1)n 2 }}2 5 }}2
i51

y el algoritmo es de orden n 2.
264 Introducción a la programación con orientación a objetos

El algoritmo de selección y el de inserción también implican un máximo de n 2 i comparaciones


en la pasada i, con un total de n 2 1 pasadas. El resultado es el mismo que para el caso anterior, com-
plejidad de orden n 2.

b) Número de intercambios de variables


El método de la burbuja realiza n(n 2 1)/2 comparaciones donde cada comparación implica tres inter-
cambios de variables. La complejidad es entonces 3n(n 2 1)/2. Por otro lado, el algoritmo de selección
realiza n(n 2 1)/2 intercambios. Por contra, el algoritmo de inserción presenta la colocación del ele-
mento a ordenar fuera de uno de los bucles, con lo que la complejidad para esta tarea es de orden n.
Con los resultados anteriores los métodos de selección e inserción aparecen como más eficientes
que el de la burbuja, tradicionalmente considerado como el más ineficiente de los métodos de ordena-
ción. Por otro lado, el método de selección realiza sus O(n 2) comparaciones e intercambios siempre,
incluso si la lista ya está ordenada. Por esta razón si la lista está parcialmente ordenada se suele pre-
ferir el método de inserción. Para un análisis más detallado de la complejidad de los algoritmos de
ordenación véase Rawlins, 1992.
Para finalizar, indiquemos que hay varios algoritmos más eficientes que los examinados, obedecien-
do a complejidad de orden n log2(n). El lector interesado puede consultar Knuth, 1998; Smith, 1987.

10.3. BÚSQUEDA

La búsqueda es el proceso de determinar si un elemento particular, llamado a veces valor clave o valor
objetivo, está incluido (o no) en una lista de elementos, y si es así dónde. Al igual que para la ordena-
ción, existen distintos algoritmos de búsqueda, cada uno con su propia complejidad. Algunos algorit-
mos de búsqueda necesitan que la lista esté ya ordenada. Vamos a presentar en este texto dos técnicas
de búsqueda apropiadas para estructuras de datos lineales: la búsqueda lineal o secuencial (que no
necesita ordenación previa) y la búsqueda binaria que sí precisa ordenación.

10.3.1. BÚSQUEDA LINEAL

Éste es el algoritmo más sencillo, aunque no el más eficiente. Es la aproximación más directa, que
implica que comenzamos a recorrer secuencialmente nuestra lista hasta que localizamos el valor cla-
ve deseado o hasta que alcanzamos el final de la lista (lo que indica que la clave no está en la lista).
Este algoritmo no necesita de una ordenación previa. Los pasos a seguir son:

1. Comparar el valor clave buscado con el elemento actual de la lista. Si coinciden el proceso
concluye.
2. Si no hemos encontrado el valor clave pasamos al siguiente elemento de la lista y repetimos
el paso 1.

El pseudocódigo para el algoritmo de búsqueda secuencial para una lista de n valores implemen-
tada en una matriz con 0-origen, y usando un valor centinela de -1 para indicar que la clave no está en
la lista, sería,

Inicio
Leer lista y clave
posicioni 0
Ordenación y Búsqueda 265

Mientras (i< n) y (lista(posicion) clave)


posicioniposicion+1
Fin_Mientras

Si (posicion=n) entonces
posicióni -1
Fin_Si
Devuelve posicion
Fin

Como vemos, se recorre la matriz hasta que se encuentra el valor clave o hasta que se acaba la
matriz. Si hemos encontrado el elemento, se coloca su índice en la variable posicion y si no, se le
asigna 21.
Un método en Java que implementara el algoritmo sobre una lista representada por una matriz de
enteros sería el siguiente:

// Búsqueda secuencial o lineal

public int secuencial(int [] lista, int clave){


int n, posicion=0;
n=lista.length;

while (posicion<n && clave != lista[posicion]){


posicion++;
}
if(posicion==n) {
posicion=-1;
}
return posicion;

} //Fin método secuencial

Obsérvese que al método hay que pasarle la lista y la clave. Como la lista no tiene por qué estar
ordenada ni organizada de ninguna forma, el valor buscado (la clave) puede estar en cualquier posi-
ción. Por lo tanto, debemos examinar todos los elementos de la lista para determinar si la clave no está
en la lista. El método sólo encuentra el primer valor de la lista coincidente con el valor buscado. Si
queremos buscar todos los elementos de la lista coincidentes con el buscado, deberíamos recorrer la
lista entera y almacenar, por ejemplo, en una matriz auxiliar las posiciones donde se encuentran los
elementos encontrados.

10.3.2. BÚSQUEDA BINARIA

Si la lista a usar está previamente ordenada podemos hacer uso de este hecho para acelerar el proceso
de búsqueda. Ésta es la idea básica de la búsqueda binaria. En este caso se aplica una aproximación de
tipo divide y vencerás. Lo que se hace es localizar el elemento central de la lista y ver si corresponde
a la clave. Si es así hemos solucionado el problema pero, aunque no lo sea, el hecho de tener la lista
ordenada me permite, comparando si el valor de la clave es mayor o menor que el elemento central,
descartar toda una mitad de la lista. Es decir, he reducido el problema a buscar en la mitad de ele-
mentos. Los pasos del algoritmo serían,
266 Introducción a la programación con orientación a objetos

1. Seleccionar el elemento central (aproximadamente) de la lista y comparar con la clave. Si son


iguales o no hay más elementos acabamos el proceso.
2. Si la clave y el elemento central no coinciden, determinar si la clave es mayor o menor que
dicho elemento para descartar la mitad de la lista donde no puede encontrarse la clave
(dependerá de si la lista está ordenada en orden creciente o decreciente) volviendo al
paso 1.

En el algoritmo debemos considerar que puede que no haya exactamente un valor central que divi-
da en dos la lista. Si no lo hay (porque haya un número par de elementos) tomamos el primer elemento
de los dos centrales.
Veamos el método en la práctica. Por ejemplo, dada la siguiente lista

1231 1473 1545 1838 1892 1898 1983 2005 2446 2685 3200

busquemos el número 1983:

1. Examinamos el elemento central, en este caso como hay once elementos el central sería el sex-
to, 1898. Como 1983 es mayor que 1898, se desprecia la primera sublista y nos centramos en
la segunda:

1. 1983 2005 2446 2685 3200

2. Exploramos el elemento central, 2446. Como es mayor que 1983, eliminamos la segunda par-
te de la sublista y nos queda:

1. 1983 2005.

3. Al no haber término central, elegimos el primero de los dos que quedan, que en este caso
es el número buscado. Se han hecho 3 comparaciones. En la búsqueda lineal se hubieran
hecho 7.

Suponiendo que tenemos una lista de n elementos con 0-origen, ordenada en orden creciente, y que
se devuelve el valor 21 como valor centinela para indicar que la clave no está en la lista, el pseu-
docódigo para el algoritmo sería:

Inicio
posicion i 0
izquierda i 0
derecha i n-1
clave i Valor buscado

Mientras (izquierda <= derecha) y (clave lista(posicion))


posicion i Parte entera de(izquierda + derecha)/2
Si (clave > lista(posicion)) entonces
izquierda i posicion+1
Si_no
derecha i posicion-1
Fin_Si
Fin_Mientras

Si (lista(posicion) clave) entonces


Ordenación y Búsqueda 267

posicion= -1
Fin_Si

Devuelve posicion
Fin

Veamos un método en Java que implementa el algoritmo:

// Búsqueda binaria

public int binaria (int[] lista, int clave) {

int posicion=0, izquierda = 0, derecha = lista.length - 1;

while (izquierda <= derecha && clave != lista[posicion]) {


posicion = (izquierda + derecha)/2; // Cociente entero,

if (clave > lista[posicion]) {


izquierda=posicion+1; // clave esta en la segunda mitad
}
else {
derecha = posicion-1; // clave esta en la primera mitad
}
} // Fin del while

if (clave != lista[posicion]) {
posicion=-1;
}
return posicion;

} //Fin método binaria

La implementación de la búsqueda binaria usa los enteros derecha e izquierda para indicar los
límites de la región de la matriz que se está considerando. Inicialmente, son la primera y última posi-
ciones de la matriz. En cada iteración, al eliminarse la mitad de los datos a considerar, se actualizan los
valores de derecha e izquierda. El elemento del medio se consigue haciendo el cociente entero del
promedio de derecha e izquierda. Cuando el número de elementos a considerar es par, hay dos
posibilidades para la elección del elemento del medio. Esta implementación elige el de la izquierda, es
decir, el anterior, puesto que al hacer la división el operador trunca el resto. Esta decisión es arbitraria.
La búsqueda termina cuando se encuentra la clave buscada o cuando se termina de explorar la lista.

10.3.3. COMPARACIÓN DE MÉTODOS

Al igual que para los algoritmos de ordenación, resulta interesante el estudio comparativo de los méto-
dos de búsqueda presentados.
Respecto al comportamiento en el caso más desfavorable el análisis es sencillo. Para la búsqueda
lineal tenemos una comparación que se realiza siempre dentro de un bucle que se repite, en el peor
caso (cuando el elemento buscado no está en la lista o es el último), n veces para n elementos. El núme-
ro de comparaciones es por lo tanto n y la complejidad del algoritmo es de orden n, O(n).
En el caso de la búsqueda binaria con n elementos en la lista, en el peor de los casos, el elemento
no estará en la lista y habrá que dividir la misma hasta que sólo quede un elemento. En cada partición
reducimos el número de elementos a la mitad (aproximadamente). Así, partiendo de n elementos iría-
268 Introducción a la programación con orientación a objetos

mos obteniendo n/2, n/4, n/8, etc., elementos en cada partición. Si el número de particiones por la
mitad necesarias para obtener un solo elemento es de m, se cumple que:
n
15} m}
2
o bien,
m5log2n

Como en cada partición se hace una comparación, m es justamente el número de comparaciones.


Por lo tanto, la complejidad es de orden log2 n, lo que representa una gran mejora frente a la bús-
queda lineal 1. El único requisito de la búsqueda binaria es que necesitamos una ordenación previa
de la lista.
La mayor eficiencia de la búsqueda binaria frente a la secuencial se puede constatar, de manera
práctica, en la Tabla 10.1 que recoge el número de comparaciones necesarias cuando se busca un ele-
mento en una lista de n en el caso más desfavorable (el elemento no está en la lista).

Tabla 10.1. Comparación entre las búsquedas secuencial y binaria

Número de elementos Búsqueda Búsqueda


(n) secuencial binaria
10 10 3
100 100 7
1000 1000 10
1.000.000 1.000.000 20

Como podemos observar cuanto mayor es n mucho más útil es el uso de la búsqueda binaria.

EJERCICIOS PROPUESTOS
Ejercicio 1.* Implemente en Java una versión recursiva del algoritmo de ordena-
ción por selección.

Ejercicio 2.* Implemente en Java una versión recursiva del algoritmo de bús-
queda lineal.

Ejercicio 3.* Implemente en Java una versión recursiva del algoritmo de bús-
queda binaria.

Ejercicio 4. Escriba una variante de los métodos de ordenación por intercambio,


selección e inserción, presentados en el capítulo 10, donde la orde-
nación sea de forma decreciente.

1
Éste es un tratamiento simplificado del cálculo de la complejidad del algoritmo de búsqueda binaria. El lector intere-
sado en un tratamiento más detallado y riguroso, aunque conducente al mismo resultado, puede consultar Rawlins, 1992.
Ordenación y Búsqueda 269

REFERENCIAS
GONNET, G. H. y Baeza-Yates, R.: Handbook of Algorithms and Data Structures, Second Edition, Addison-Wes-
ley, 1991.
KNUTH, D. E.: The art of computer programming. Vol. 3: Sorting and Searching, Second edition, Addison-Wes-
ley, 1998.
RAWLINS, G. J. E.: Compared to what? An introduction to the analysis of algorithms, Computer Science Press, 1992.
SMITH, H. F.: Data Structures. Form and Function, Harcourt Brace Jovanovich, Publishers, 1987.
WIRTH, N.: Algoritmos+Estructuras de datos=Programas, Ediciones del Castillo, Madrid, 5.ª reimpresión, 1986.
A

Soluciones a los
ejercicios propuestos
272 Introducción a la programación con orientación a objetos

Este apéndice contiene las soluciones a los ejercicios propuestos en los capítulos del libro divididas en
diez secciones, una por capítulo. Siempre que el ejercicio consiste en el desarrollo de un programa se
ha dividido la solución en análisis, diseño e implementación. Téngase en cuenta que la solución a la
implementación no es única, nosotros hemos indicado una de las posibles.

CAPÍTULO 1. SISTEMAS BASADOS EN COMPUTADOR

Ejercicio 1
La información es un conjunto de datos con una determinada organización, son datos significativos.

Ejercicio 2
Digital.

Ejercicio 3
El código UNICODE permite representar muchos más caracteres (por ejemplo, los acentuados) ya que
usa 16 bits.

Ejercicio 4
212 = 4096

Ejercicio 5
Por la Unidad de Control y la Unidad Aritmético-Lógica.

Ejercicio 6
230

Ejercicio 7
— Es de lectura-escritura.
— Es volátil.

Ejercicio 8
Una memoria adicional pequeña pero muy rápida, donde se guarda la información más usada.

Ejercicio 9
Conexión de línea compartida.

Ejercicio 10
TCP/IP.
Soluciones a los ejercicios propuestos 273

CAPÍTULO 2. ELEMENTOS DE PROGRAMACIÓN Y LENGUAJES

Ejercicio 1
— Lenguajes de cuarta generación.
— Lenguajes de alto nivel.
— Lenguaje ensamblador.
— Lenguaje máquina.

Ejercicio 2
El lenguaje compilado es más rápido, ya que el interpretado va traduciéndose y ejecutándose senten-
cia a sentencia (o sección a sección).

Ejercicio 3
— Errores en tiempo de compilación.
— Errores en tiempo de ejecución.
— Errores lógicos.

Ejercicio 4
Error lógico.

Ejercicio 5
Lo más aconsejable es comparar los resultados del programa con los de algún ejemplo conocido, o ir
siguiendo manualmente el flujo de control del programa para detectar qué es lo que no se hace correc-
tamente. Mostrar resultados parciales y consultar los valores de las variables ayuda a delimitar el ráea
donde se encuentra el error.

Ejercicio 6
Análisis, Diseño, Codificación, Pruebas y Mantenimiento.

Ejercicio 7
El mantenimiento (70-80% del esfuerzo total del ciclo de vida).

Ejercicio 8
En la etapa de análisis.

Ejercicio 9
El A sería más complejo.

Ejercicio 10
En un programa monolítico sólo hay un programa principal.

Ejercicio 11
En diseño.
274 Introducción a la programación con orientación a objetos

CAPÍTULO 3. INTRODUCCIÓN A LA PROGRAMACIÓN

Ejercicio 1
Se trata de un ejemplo de precedencia de operadores. Sustituyendo los valores de las variables y eva-
luando por orden de precedencia tendríamos,

1 1 4/2 1 1
1 1 2 (cociente entero) 1 1
311

Con un resultado final de 4

Ejercicio 2
El caso a) es una comparación, el resultado es true o false.
El caso b) es una asignación, el resultado es que la variable b (que debe ser lógica) adquiere el
valor true.

Ejercicio 3
En el caso a) primero se aplica el operador y luego la asignación, por tanto: total=4, num=4.
En el caso b) primero se realiza la asignación y luego el incremento, por tanto: num=2, total=3.
En el caso c) el operador usado como prefijo incrementa el valor de la variable antes de que ésta
se use en la expresión. Con el operador como sufijo se usa el valor primero y se incrementa después.
Entonces, con ++num se incrementa num usándose ya incrementado, mientras que en num++ se usa el
valor de num y después se incrementa. Con ++num incrementamos primero num a 4 y luego se usa
en la expresión. Luego se suma el valor de num (4) obteniendo 8, que es lo que se almacena en total
y num se incrementa posteriormente con el ++. El resultado final es total=8 y num=5.

Ejercicio 4
Cuando la comparación es con números en punto flotante la igualdad de dos números no se debe
comparar directamente como numero1==numero2. Esto es porque sólo van a ser iguales si todos los
bits que los representan son iguales, y en cálculos con números reales hay siempre error de redondeo.
Lo que hay que comprobar es si son muy parecidos, usando un valor límite adecuado a cada proble-
ma. Si en nuestro caso, el límite viene dado por la precisión de la representación numérica, p, tendría-
mos:

if (Math.abs (r1-r2) < p) {


11System.out.println(“Se consideran iguales”);
}
else {
11System.out.println(“Se consideran distintos”);
}

Hemos usado el método valor absoluto (abs) de la clase Math que contiene métodos matemáti-
cos. La clase Math estáen el paquete java.lang y, por lo tanto, no hay que importarla explícita-
mente. Tomamos el valor absoluto para que no afecte el signo de la diferencia entre r1 y r2.
Soluciones a los ejercicios propuestos 275

Ejercicio 5
ANÁLISIS
En estos ejemplos sencillos el análisis estáimplícito en el enunciado. En este caso vamos a leer los
datos por el teclado, evaluando los parámetros requeridos y produciendo una salida por el monitor.

DISEÑO
La entrada y salida de información se realizará usando la clase BufferedReader y
System.out.println(), respectivamente. El perímetro, 1, y la superficie, s, se obtendrán por
medio de las relaciones:

l 5 2šr
s 5 š r2

donde r representa el radio. Para evaluar el cuadrado de r podemos hace r*r o usar el método poten-
cia de la clase Math que permite evaluar ab como Math.pow(a,b).

IMPLEMENTACIÓN
import java.io.*;
class Circunferencia {
11public static void main(String[] args) throws IOException {

// Declaraciones
1111double radio, perimetro, superficie;
1111BufferedReader leer =new BufferedReader
111111111111111111(new InputStreamReader(System.in));

// Lectura
1111System.out.println(“Introduzca el radio: “);
1111radio=Double.parseDouble(leer.readLine());

// Procesamiento
1111perimetro=2.*Math.PI*radio;
1111superficie=Math.PI*radio*radio; // Como alternativa se
111111111111111111111111111111111111// puede usar el método
111111111111111111111111111111111111// Math.pow(a, b)

// Impresión de resultados
1111System.out.println(“perimetro: “+perimetro+” unidades”);
1111System.out.println(“superficie: “+superficie
1111111111111111111111+” unidades^2”);
11}//Fin del main
}//Fin de la clase

Ejercicio 6
ANÁLISIS
Una vez más, el enunciado define el análisis. Las labores de lectura y escritura se realizarán por tecla-
do y por monitor, respectivamente. La tarea es única, evaluar un sumatorio de cuadrados.
DISEÑO
La entrada y salida de información se realizará con la clase BufferedReader y
System.out.println(), respectivamente. El sumatorio de los cuadrados se evaluarácon un bucle de
tipo while y los cuadrados se evaluarán como productos ( n 2 5 n · n).
276 Introducción a la programación con orientación a objetos

IMPLEMENTACIÓN
import java.io.*;
class Cuadrados {
11public static void main(String [] args) throws IOException {
1111int n, n_2, i;
1111BufferedReader leer =new BufferedReader
11111111111111111(new InputStreamReader(System.in));

1111// Lectura de datos


1111System.out.println(“Introduzca el numero n”);
1111n=Integer.parseInt(leer.readLine());
1111System.out.println(“Calculando la suma de cuadrados “
111111111111111111111111+”desde 1 hasta “+n);

111// Suma de los cuadrados de 1 a n usando un bucle while


1111i=1; // Variable usada como contador
1111n_2=0;
1111while (i<=n) {
111111n_2=n_2+i*i;
111111i++;111// Incremento del contador
1111}

111// Salida de resultados


1111System.out.println(“Suma de los cuadrados: “+n_2);

11}//Fin del main


}//Fin clase

Ejercicio 7
El programa da un error de compilación porque la variable j estádeclarada dentro de la sección inter-
na que va delimitada por las llaves ({}). Fuera de ese bloque la variable no estádeclarada, es decir, su
alcance es el bloque en cuestión. Por eso se produce un error de variable no declarada al intentar usar
j en el println fuera del bloque.

Ejercicio 8
ANÁLISIS
La entrada y salida se realiza por teclado y monitor y la funcionalidad del programa queda definida en
el enunciado.
DISEÑO
La entrada y salida de información se gestionarán usando las clases BufferedReader y
System.out.println(), respectivamente. El pH se calcularáde acuerdo a la expresión dada en el
enunciado. La petición continuada de concentraciones se implementarácon un bucle while, que se
repetiráhasta que el usuario indique que no desea más cálculos. En este caso, pondremos a false la
condición que controla el bucle.

IMPLEMENTACIÓN

import java.io.*;
class Pehache {
11public static void main(String [] args) throws IOException {
1111double Ka, pKa, c, pH;
Soluciones a los ejercicios propuestos 277

1111boolean sigue=true;1111111// Variable de control del bucle


1111int opcion=0;

1111BufferedReader leer =new BufferedReader


11111111111111111(new InputStreamReader(System.in));

111// Lectura de datos


1111System.out.println(“Introduzca la constante de acidez:”);
1111Ka=Double.parseDouble(leer.readLine());

1111/* logaritmo base 10 de x =0.43429448*logaritmo natural de x


1111111Se usa esta forma porque Java no incorpora el logaritmo
1111111base 10 */
1111pKa=-0.43429448* Math.log(Ka);

1111while (sigue) {
111111System.out.println();
111111System.out.println(“Concentracion (M):”);
111111c=Double.parseDouble(leer.readLine());
111111pH= (pKa-0.43429448* Math.log(c))/2.0;
111111System.out.println();
111111System.out.println(“pH:”+pH);
111111System.out.println();
111111System.out.println(“Desea usar otra concentracion?”);
111111System.out.println(“Teclee 1 para si, otra opcion para no”);
111111opcion=Integer.parseInt(leer.readLine());
111111if (opcion!=1) {
11111111sigue=false;
111111}
1111}
11}111// Fin método main
} // Fin clase Pehache

Ejercicio 9
probador:c
probador:d
probador:deed

La primera línea imprime el valor de probador que es “c”. La segunda línea vuelve a imprimir
el valor de probador que en este caso es “d”, porque se ha incrementado en uno su valor, con lo cual
probador contiene la siguiente letra del código Unicode. En la tercera línea, la variable probador
se incrementa, pero al utilizar el operador sufijo primero se imprime el valor que tenía la variable (“d”)
y luego se incrementa. A continuación, se vuelve a imprimir probador cuyo valor en ese momento
es “e” (debido al incremento). Posteriormente probador es decrementado usando el operador sufijo.
Por esta razón se imprime el valor de probador antes de realizar el decremento (se imprime “e”) y
después se realiza el decremento. Por eso, cuando finalmente se vuelve a imprimir probador en pan-
talla aparece una “d”. Resultando la salida “deed”.

Ejercicio 10
ANÁLISIS
Una vez más, el problema es sencillo y el enunciado nos sirve como documento de análisis indicando
claramente la funcionalidad necesaria. Baste indicar que como habitualmente la lectura y la escritura
se realizarán por teclado y pantalla, respectivamente.
278 Introducción a la programación con orientación a objetos

DISEÑO
La distinción del caso de masa mayor de 1 kg se realizarácon un if. El cálculo de la frecuencia se
realizarácon la expresión genérica, sólo si la masa es menor de 1 kg. La entrada y salida de informa-
ción se realizaráusando las clases BufferedReader y System.out.println(), respectivamente.

IMPLEMENTACIÓN
import java.io.*;
class Pendulo {
11public static void main(String [] args) throws IOException {
1111double l, m, frecuencia;
1111final double g=9.8 ; // (m/s^2) Sistema internacional

1111BufferedReader leer =new BufferedReader


1111111111111111(new InputStreamReader(System.in));

// Lectura inicial de datos


1111System.out.println(“Introduzca la masa del pendulo (kg):”);
1111m=Double.parseDouble(leer.readLine());

1111if (m < 1.0 ) {


111111System.out.println(“Introduzca la longitud del pendulo (m):”);
111111l=Double.parseDouble(leer.readLine());

111111frecuencia = Math.sqrt(g/l);
111111frecuencia = frecuencia /(2.0*Math.PI);

111111System.out.println(“Frecuencia (1/s): “+frecuencia);


1111}
1111else {
111111System.out.println(“La masa debe ser menor de 1 kg”);
1111}

11}111// Fin método main


} // Fin clase Péndulo

Ejercicio 11
21 21 22

En el primer caso se usa el operador de incremento como prefijo, por lo cual primero se incrementa
la variable indice y luego se imprime. En el segundo caso el operador es sufijo por lo cual primero
se imprime y luego se incrementa. Finalmente, la última ocurrencia de indice en el println impri-
me el valor actual que es una unidad más que el impreso anteriormente.

CAPÍTULO 4. PROGRAMACIÓN ESTRUCTURADA

Ejercicio 1
Este ejemplo aunque simple presenta una situación más realista que los vistos hasta ahora. El progra-
ma es más complejo que los desarrollados anteriormente y presenta una estructura representativa de
un programa real. Ya en este simple ejemplo podemos ahondar en las ventajas de una aproximación
Soluciones a los ejercicios propuestos 279

sistemática al desarrollo de software. Aunque de manera un tanto informal abordemos una etapa de
análisis y diseño antes de la codificación.

ANÁLISIS
En esta etapa debemos responder a la pregunta de qué debe hacer el programa. En la aproximación tra-
dicional aplicamos un punto de vista funcional centrándonos en las tareas (funciones) que debe desa-
rrollar el software. A partir del enunciado podemos evaluar una lista de requisitos:

— Calcular factorial.
— Distinguir entero negativo.
— Distinguir caso de N 5 0 (relacionado con el primer requisito).
— Preguntar si continuar o no (repitiendo si no se hace una selección válida).
— Indicar si se continúa calculando factoriales o no.

En este proceso informal de análisis no vamos más alláde la recolección de requisitos. En la apro-
ximación estructurada al análisis, los requisitos se transforman en un diagrama que recoge las tareas y
el flujo de datos entre ellas. Esta labor supone la creación de un modelo lógico del sistema.

DISEÑO
Una vez realizada la recolección de requisitos abordemos el diseño. Vamos a determinar cómo lleva-
mos a la práctica cada uno de los requisitos. Como herramienta de diseño y, en particular, como herra-
mienta de especificación de algoritmos, utilizaremos pseudocódigo. Abordando cada uno de los
requisitos tendríamos:
Calcular factorial
Inicio
11factorial i1
11Para i i 1 mientras i n incremento i i i+1
1111factorial ifactorial*i
11Fin_Para
Fin

Distinguir entero negativo


Inicio
11Haz
1111repite ifalso
1111leer n
1111Si (n<0) entonces
111111repite iverdadero
111111Imprimir que n no puede ser negativo
1111Fin_Si
11Mientras (repite)
Fin

Obsérvese que la variable repite se pone a falso dentro del bucle. Si se pusiera a falso justo antes de
entrar en el bucle, cuando hubiera una lectura de un n menor que 0 habríamos generado un bucle infinito.
Distinción caso n 5 0
Teniendo en cuenta que 0! 5 1 el algoritmo podría ser

Inicio
11factoriali1
280 Introducción a la programación con orientación a objetos

11Si (n!=0) entonces


1111Calcular factorial
11Fin_Si
Fin

Integrándolo con el cálculo de N! obtendríamos,

Inicio
11factoriali1
11Si (n!=0) entonces
1111Para i i1 mientras i n incremento iiI+1
111111factorial ifactorial*i
1111Fin_Para
11Fin_Si
Fin

Preguntar si continuar o no (hasta introducir una opción válida)


Inicio
11otra_opcion iverdadero
11Haz
1111Preguntar si desea continuar
1111Si (continuar ½ si y continuar ½ no) entonces
111111Decir que no es una opción válida
1111Si_no
111111otra_opcion ifalsa
1111Fin_Si
11Mientras (otra_opcion)
Fin

Obsérvese que otra_opcion se inicializa a verdadero fuera del bucle. En este caso no hay posi-
bilidad de generar un bucle infinito pues la asignación de valor dentro del “Si” es a falso y en ese caso
terminaría el bucle.
Indicar si se continúa calculando factoriales o no
Inicio
11numero iverdadero
11Mientras (numero)
1111Distinguir entero negativo
1111Distinción caso n=0 y cálculo del factorial
1111Preguntar si continuar o no
1111Si (opcion = no seguir) entonces
111111numero ifalso
1111Fin_Si
11Fin ientras
Fin

Con esta serie de algoritmos tenemos resuelto el problema.

IMPLEMENTACIÓN

import java.io.*;
class Factorion {
Soluciones a los ejercicios propuestos 281

11public static void main(String[] args) throws IOException {


1111int n, opcion;
1111boolean repite, pide_numero, otra_opcion;
1111double factorial;
1111BufferedReader leer =new BufferedReader
111111111111111111111111(new InputStreamReader(System.in));

1111pide_numero=true;
1111while (pide_numero) {
111111factorial=1.0;
111111do {
11111111repite=false;
11111111System.out.println(“Introduzca N para calcular N!:”);

11111111n=Integer.parseInt(leer.readLine());
11111111if (n <0 ) {
1111111111repite=true;
1111111111System.out.println(“ N no puede ser negativo”);
11111111}
111111} while (repite);

111111if (n!=0) { // Si n=0 se queda con valor de factorial=1


11111111for (int i=1; i<=n; i++){
1111111111factorial=factorial*i;
11111111}
111111}

111111System.out.println(“El factorial de “+n+” es: “+factorial);

111111otra_opcion=true;
111111do {
11111111System.out.println(“Desea calcular otro factorial?”);
11111111System.out.println(“Si: 0”);
11111111System.out.println(“No: 1”);
11111111opcion=Integer.parseInt(leer.readLine());
11111111if (opcion!=0 && opcion !=1) {
1111111111System.out.println(“Las opciones validas son s o n”);
11111111}
11111111else {
1111111111otra_opcion=false;
11111111}
111111} while (otra_opcion);

111111if (opcion==1) {
11111111pide_numero=false;
111111}
1111}111// Fin del while (pide_numero)
11}111// Fin método main
} // Fin clase Factorion

Ejercicio 2
if (opcion==1){
11System.out.println(“Uno”);
282 Introducción a la programación con orientación a objetos

}
else {
11if (opcion==2) {
1111System.out.println(“Dos”);
11}
11else {
1111if (opcion==3) {
111111System.out.println(“Tres”);
1111}
1111else {
111111System.out.println(“Otros”);
1111}
11}
}

Ejercicio 3
class Asterisco{
11public static void main(String[] args){
1111for (int i=0;i<3;i++){
111111for (int j=0;j<i;j++) {
11111111System.out.print(“ “);
111111}
111111for (int k=0;k<5-2*i;k++){
11111111System.out.print(“*”);
111111}
111111System.out.println();
1111}
11}
}

Ejercicio 4
No es correcto, se suma hasta n+1. Para arreglar el problema basta con intercambiar las dos últimas
sentencias.

Ejercicio 5
class Asterisco {
11public static void main(String [] args) {
1111for (int i=5; i>0; i—) {
111111for (int j=0; j<i;j++) {
11111111System.out.print(“*”);
111111}
111111System.out.println(“”);
1111}
11}
}

Ejercicio 6
j=0;
while (i<n && j != -1) {
Soluciones a los ejercicios propuestos 283

11j=objetoX.valor(i);
11i++;
}

Ejercicio 7
class Asterisco {
111public static void main(String[] args) {
11111for (int i=7; i>0; i—) {
1111111for (int k=0; k<7-i;k++) {
111111111System.out.print(“ “);
1111111}
1111111for (int j=0; j<i; j++) {
111111111System.out.print(“*”);
1111111}
1111111System.out.println(“”);
11111}
111}
}

Ejercicio 8
Es un ejemplo de uso de un switch con algunos casos sin break. Hay un bucle que recorre un índi-
ce de cero a 7 y, en función de dónde hay break y dónde no, el resultado es:

0 es menor que 3
1 es menor que 3
2 es menor que 3
3 es menor que 6
4 es menor que 6
5 es menor que 6
6 es 6 o mayor
7 es 6 o mayor

Ejercicio 9
En el programa tenemos un switch controlado por el valor de x (que es cero). El flujo de control iría
al case 0 pero al no haber break entraría luego en el default. Allí, asignaría el valor 1 a s y al
acabar el switch imprimiría este valor. El resultado sería: 1.

Ejercicio 10
while (m<=11) {
11m++;
11d=1;
11while (d<=30){
1111if (m==2 && d==29){
111111d=31;
1111}
1111else{
111111System.out.println(m+” “+d);
111111d++;
1111}//del else
11}//del while
}//del while
284 Introducción a la programación con orientación a objetos

Ejercicio 11
ANÁLISIS
Es útil organizar en forma de tabla las acciones en función de las condiciones (Tabla de decisión):

3 céntimos/km 10% 15 %
distancia >200 km Sí No No
3 personas No Sí No
distancia >400 km Sí No Sí

DISEÑO
La tabla muestra que el descuento del 15% lleva como prerrequisito que la distancia sea mayor de
400 km y, lógicamente, en ese caso también se cumple que la distancia es mayor de 200 km. Esto nos
indica un if anidado. Como vemos en la tabla, el descuento del 10% sólo depende de si hay o no tres
personas. Al ser independiente de las condiciones anteriores lo que tenemos es un if concatenado. En
pseudocódigo, el algoritmo correspondiente sería,

Inicio
11Leer km y personas

11Si (km > 200) entonces


111billete_billete +0.03*(km-200)
1111Si km >400 entonces
111111billetei0.85*billete
1111Fin_Si
11Fin_Si

11Si (personas > 2) entonces


1111billetei0.90*billete
11Fin_Si

11Escribir billete
Fin

IMPLEMENTACIÓN
import java.io.*;
class Viaje {
11public static void main(String [] args) throws IOException {
1111int personas=0;
1111double billete, km;
1111BufferedReader leer =new BufferedReader
11111111111111111(new InputStreamReader(System.in));

1111System.out.println(“Introduzca kilometraje: “);


1111km=Double.parseDouble(leer.readLine());
1111System.out.println(“Introduzca personas: “);
1111personas=Integer.parseInt(leer.readLine());
1111System.out.println();

1111billete = 20.0;1111// Precio base

1111if (km >200 ) {


111111billete = billete + (km-200)*0.03; // Incluyendo precio
11111111111111111111111111111111111111111// por km
Soluciones a los ejercicios propuestos 285

111111if (km > 400) {


11111111billete = billete*0.85;11// Descuento del 15 %
111111}11// Fin if interno (anidado al primero)
1111}11// Fin if externo

1111if (personas>2) {
111111billete=billete*0.90; // Descuento del 10%
1111} // Fin if (concatenado al anterior)

1111System.out.println(“Precio del billete: “+billete+ “ euros”);

11}11// Fin main


} // Fin clase

Ejercicio 12
ANÁLISIS
Para poder usar un bucle for debemos conocer cuántas veces va a repetirse el mismo. Una forma sen-
cilla de conseguirlo es leer el número de valores desde el teclado y realizar la lectura de cada valor
dentro del bucle. En este mismo bucle podemos ir haciendo el sumatorio de valores.

DISEÑO
El pseudocódigo para el proceso solicitado sería,

Inicio
11Leer numero de valores, n

11media i 0.0
11Para i i 0 mientras i < n incremento i i i+1
1111Leer valor i
1111media i media+valor
11Fin_Para

11mediaimedia/n

11Escribir media
Fin

IMPLEMENTACIÓN
Implementando el algoritmo en Java el resultado sería el siguiente,

import java.io.*;
class Media {
11public static void main(String [] args) throws IOException {
1111int n;
1111double valor, media;
1111BufferedReader leer =new BufferedReader
111111111111111111111111(new InputStreamReader(System.in));

1111System.out.println(“Introduzca el numero de valores:”);


1111n=Integer.parseInt(leer.readLine());

1111media=0.0;
1111for (int i=0; i<n; i++) {
286 Introducción a la programación con orientación a objetos

111111System.out.println(“Introduzca valor “+(i+1)+” :”);


111111valor=Double.parseDouble(leer.readLine());
111111media=media+valor;
1111}
1111media=media/n;

1111System.out.println(“Media: “+media);
11}
}

CAPÍTULO 5. ABSTRACCIÓN PROCEDIMENTAL Y DE DATOS

Ejercicio 1
ANÁLISIS
Se trata de una simulación. Nuestro programa debe realizar la simulación del lanzamiento del dado
generando uno de los valores enteros comprendidos entre 1 y 6.

DISEÑO
El método random() de la clase Math devuelve un double mayor o igual que 0 y menor que 1. Noso-
tros queremos que nos devuelva los valores 1, 2, 3, 4, 5, 6. Para ello debemos cambiar la escala del
resultado del método. ¿Cómo? Como queremos que el valor máximo sea 6 y el origen sea 1, multipli-
camos por 6 el resultado del número aleatorio. Obtendremos como máximo el valor 5.999999 y como
valor mínimo 0. Como queremos que el valor mínimo sea 1, sumamos 1 al resultado obtenido. Ten-
dremos entonces valor mínimo 1 y valor máximo 6.9999999. Como sólo queremos números enteros nos
quedamos con la parte entera, usando un molde. La simulación del dado se encapsularáen un método.

IMPLEMENTACIÓN
/*——————————————————————————————————————————————————————
111Programa que simula el lanzamiento de un dado usando
111el método random() Genera un numero double 0<=n<1
————————————————————————————————————————————————————————*/
import java.io.*;

class Aleatorio {
11public static void main(String [] args) throws IOException {
1111int n;111111111111//número de tiradas
1111int tirada;
1111BufferedReader leer = new BufferedReader
1111111111111111111111111(new InputStreamReader(System.in));

1111System.out.println(“Numero de tiradas:”);
1111n=Integer.parseInt(leer.readLine());

1111for (int i=0; i<n;i++) {


111111tirada = dado();
111111System.out.println(“Tirada “+i+” : “+tirada);
1111}
11} // Fin método main
11public static int dado() {
Soluciones a los ejercicios propuestos 287

11111return ((int)(1+6*Math.random()));
11}// Fin del método dado

} // Fin clase

Ejercicio 3
ANÁLISIS
Una vez más en este ejemplo tan sencillo los requisitos están claramente establecidos en el enuncia-
do. Así, la lectura seráa través de la línea de órdenes y la salida por pantalla. La funcionalidad está
clara: obtener un valor máximo y uno mínimo.

DISEÑO
a) Estructuras de datos
Lo más cómodo es usar una matriz monodimensional para almacenar y procesar los datos. Esto per-
mite usar un bucle para el procesamiento. La otra alternativa es usar una variable para cada valor, pero
eso hace que se complique mucho el código para obtener el mismo resultado.
b) Algoritmos
Para obtener el máximo y el mínimo vamos seleccionando los valores mayores o menores de la lista
que tengamos, usando un bucle. Veamos el pseudocódigo:

Determinación del máximo de n valores:

Inicio
11máximo ivalores (0)
11Para i i1 mientras i n incremento iii+1
1111Si (máximo < valores(i)) entonces
111111máximo ivalores (i)
1111Fin_Si
11Fin_Para
Fin

La determinación del mínimo es trivial visto el ejemplo anterior, basta con sustituir la compara-
ción máximo < valores(i) por mínimo > valores(i).
c) Diagrama de estructura
Podemos modularizar el programa definiendo dos métodos que determinen, uno el máximo (método max)
otro el mínimo (método min). Con esto el diagrama de estructura sería el recogido en la Figura A.5.1.

main (Principal)

max min

Figura A.5.1. Diagrama de estructura del programa para el cálculo del máximo y del mínimo
288 Introducción a la programación con orientación a objetos

IMPLEMENTACIÓN
class Maxmin {
11public static void main(String [] args) {
1111double maximo, minimo;
1111double [] valores =new double [3];

11// Asignación de los datos leídos y eco de la entrada

1111for (int i=0; i<=2; i++){//De manera general se puede


11111111111111111111111111111//sustituir 2 por valores.length

111111valores[i]=Double.parseDouble(args[i]);
111111System.out.println(“ valor [“+i+”]: “+valores[i]);
1111}

1111maximo=max(valores);
1111minimo=min(valores);
11// Salida de resultados
1111System.out.println(“valor maximo: “+maximo);
1111System.out.println(“valor minimo: “+minimo);
11} // Fin método main

11public static double max(double [] valores) {


1111double max;
1111max=valores[0];
1111for (int i=1; i<=2; i++){111//De manera general se puede
11111111111111111111111111111111// sustituir 2 por valores.length
111111if (max < valores [i]){
11111111max = valores [i];
111111}
1111}
1111return max;
11} // Fin método max

11public static double min(double [] valores) {


1111double minimo;
1111minimo=valores[0];
1111for (int i=1; i<=2; i++){
111111if (minimo > valores [i]){
11111111minimo = valores [i];
111111}
1111}
1111return minimo;
11} // Fin método min

} // Fin de la clase

Obsérvese que en el método max hemos usado el identificador max para denominar al método y a
una variable. No hay ningún problema en ello, el sistema sabe lo que es el método y lo que es la varia-
ble.

Ejercicio 4
ANÁLISIS
El enunciado es una vez más muy claro. Se trata de un ejemplo de sobrecarga de métodos. La lectura
Soluciones a los ejercicios propuestos 289

se harádirectamente por la línea de órdenes.

DISEÑO
a) Estructuras de datos
Para poder ser un ejemplo de sobrecarga de métodos debemos pasar los dos o tres números al método
correspondiente. Esto quiere decir que aquí no debemos usar una matriz monodimensional pues enton-
ces sólo haría falta un método.
b) Algoritmos
El algoritmo se ilustra con el caso más complejo, el de tres elementos. El pseudocódigo correspon-
diente sería,
Inicio
11máximoia
11Si (b>máximo) entonces
1111máximoib
11Fin_Si
11Si (c>máximo) entonces
1111máximoic
11Fin_Si
Fin
c) Diagrama de estructura
La Figura A.5.2 ilustra la estructura arquitectónica del programa solicitado.

main (Principal)

max max

Figura A.5.2. Diagrama de estructura del programa para el cálculo del máximo con dos o tres
números

IMPLEMENTACIÓN
class MaxSobrecarga {
11public static void main(String [] args) {
1111double a, b, c, maximo;
11// Asignación de los datos y eco de los mismos
1111a= Double.parseDouble(args[0]);
1111b= Double.parseDouble(args[1]);
1111System.out.println(“Primer valor : “+a);
1111System.out.println(“Segundo valor: “+b);
1111if (args.length==2) {
111111maximo = max(a, b);
1111}
290 Introducción a la programación con orientación a objetos

1111else {
111111c= Double.parseDouble(args [2]);
111111System.out.println(“Tercer valor : “+c); // Eco del
11111111111111111111111111111111111111111111111// tercer dato
111111maximo = max(a,b,c);
1111}

11// Salida de resultados


1111System.out.println(“El maximo es : “+maximo);

11} // Fin método main


11public static double max(double a, double b) {
1111double maximo=a;
1111if (b > maximo){
111111maximo=b;
1111}
1111return maximo;
11} // Fin método max versión de dos parámetros

11public static double max(double a, double b, double c) {


1111double maximo=a;
1111if (b > maximo){
111111maximo=b;
1111}
1111if (c > maximo){
111111maximo=c;
1111}
1111return maximo;
11}11// Fin método max versión con tres parámetros

} // Fin de la clase

Ejercicio 5
El triángulo de Pascal es una disposición triangular de enteros donde los elementos de cada fila son
simétricos, empiezan (y terminan) en uno y los restantesColumnas
elementos de la fila vienen dados por la suma
de los elementos contiguos5 colocados
4 en
3 la fila
2 superior.
1 Las
0 primeras
1 filas
2 del3 triángulo
4 son:5
Fila 0 1
Fila 1 1 1
Fila 2 1 2 1
Fila 3 1 3 3 1
Fila 4 1 4 6 4 1
Fila 5 1 5 10 10 5 1

ANÁLISIS
Estos coeficientes están relacionados con los coeficientes binómicos, es decir, con el número de com-
binaciones, formas diferentes de seleccionar, k elementos tomados de un conjunto de n elementos en
total. Se trata de determinar los subconjuntos, así que el orden de los elementos en el subconjunto no
cuenta ({A,B,C} es el mismo subconjunto que {B,C,A}). Un ejemplo con 3 elementos sería:

Subconjuntos de 0 elementos: 1
Soluciones a los ejercicios propuestos 291

Subconjuntos de 1 elementos: 3
Subconjuntos de 2 elementos: 3
Subconjuntos de 3 elementos: 1

La expresión para determinar este número de subconjuntos es la dada en el enunciado: c (n,k) =


=n! /(k! (n-k)! ). En el triángulo de Pascal las filas darían el número total de elementos (n) y las colum-
nas el número de elementos del subconjunto (k). En ambos casos empezaríamos a contar en cero.
Estos coeficientes c (n, k) se denominan binómicos porque son los que aparecen en la expresión
de las potencias del binomio:

(A 1 B)0 5 1
(A 1 B)1 5 1A1 1B
(A 1 B)2 5 1A21 2AB1 1B2
(A 1 B)3 5 1A3 1 3A2B1 3AB21 1B3
etc.

DISEÑO
Los coeficientes del triángulo se obtienen con la expresión dada en el enunciado. Para escribir el trián-
gulo en la entrada de datos se daría el número de filas del mismo, teniendo en cuenta que la primera
fila sería la de índice cero. Dado un valor n y empezando en 0, los valores de k van desde 0 hasta n.
a) Estructuras de datos
Como estructuras de datos no tenemos que usar ninguna en especial. Trabajaremos con variables.
b) Algoritmo
En pseudocódigo tendríamos,
Inicio
1Leer número de filas (filas)
1Para ni0 mientras n<filas incremento nin+1
111Para ki0 mientras k n incremento kik+1
11111númeroic(n,k)
11111Escribir número sin saltar línea
111Fin_Para
111Escribir línea en blanco
1Fin_Para
Fin

c) Diagrama de estructura
En este caso podemos definir un módulo para calcular cada coeficiente y otro para evaluar el factorial.
El diagrama correspondiente se muestra en la Figura A.5.3.
292 Introducción a la programación con orientación a objetos

Principal (main)

c(n,k)

Factorial

Figura A.5.3. Diagrama de estructura para el ejemplo del triángulo de Pascal

IMPLEMENTACIÓN
/*****************************************************
111Programa para la obtención del triángulo de Pascal
111El número de filas a generar se introduce como
111argumento en la línea de órdenes
1*****************************************************/
class Pascal {
11public static void main(String [] args ) {
1111int filas, numero;
1111filas=Integer.parseInt(args[0]);
1111for (int n=0; n<filas; n++){
111111for (int k=0; k<=n;k++){
11111111numero=c(n,k);
11111111System.out.print(numero+” “);
111111}
111111System.out.println();
1111}
11} // Fin método main
11public static int c(int n, int k) {
1111return fact(n)/(fact(k)*fact(n-k));
11}11// Fin método c(n,k)

11public static int fact(int n) {


1111int producto=1;
1111for (int i=1; i<=n;i++){
111111producto=producto*i;
1111}
1111return producto;
11}11// Fin método fact
} // Fin clase

Obsérvese que el resultado es un triángulo de Pascal escrito sin formato, es decir, que para las pri-
meras cinco filas tendríamos,
1
11
121
1331
Soluciones a los ejercicios propuestos 293

14641

Ejercicio 7
Al hacer b=a hacemos que b refiera a lo mismo que a. Es decir, se realiza una copia de la referencia
tal como se ilustra en la Figura A.5.4.

{0, 1, 2}
b

Figura A.5.4. Asignación de las referencias a y b

Al hacer b[1]=3 el elemento original que es 1 se sustituye por 3. Teniendo en cuenta que tanto a
como b refieren a lo mismo, se pasan al metodo1 y se hace la suma con c, salvando el resultado en
la propia matriz c. Lo que se devuelve es la referencia al resultado, por lo que al final del programa,
a y c se refieren a lo mismo, dejando a de referirse a la matriz b. En un diagrama tendríamos el resul-
tado ilustrado en la Figura A.5.5.

{0, 3, 2}

c
{3, 7, 7}

Figura A.5.5. Relación entre las matrices a, b y c

Con esto, el resultado final de la impresión sería,

3 0 7 3 7 2

Ejercicio 8
A metodo1 se le pasa un entero y, por tanto, el paso es por valor. Tras las manipulaciones en el método
la variable original queda inalterada. A metodo2 se le pasa una matriz y el paso, por tanto, es por refe-
rencia. Por ello, al acabar el método los cambios realizados se mantienen. Lo mismo ocurre en meto-
do3, con la diferencia de que ahora se devuelve la matriz lista (en realidad es la referencia al objeto
de clase matriz). Esta referencia devuelta se asigna en el principal a la referencia llamada Valores1 con
lo que ésta a partir de este momento refiere a la matriz llamada lista dentro de metodo3. El resultado
294 Introducción a la programación con orientación a objetos

es que la impresión de Valores2 produce la siguiente salida,

45 34 35

Ejercicio 9
ANÁLISIS
Vamos a considerar el caso de matrices cuadradas. Excluyendo la diagonal, la matriz queda dividida
en un triángulo superior y uno inferior. La transposición se realiza intercambiando los elementos simé-
tricos con respecto a la diagonal.

DISEÑO
Para construir la transpuesta sólo hay que considerar un triángulo de la matriz. Para cada elemento de
ese triángulo se intercambia su valor con el elemento de la matriz que tiene intercambiados los índi-
ces. El algoritmo en pseudocódigo sería,

Inicio
11Leer matriz a de tamaño n
11Para ii0 mientras i<n incremento iii+1
1111Para j i0 mientras j < i incremento jij+1
111111aux i a (i, j)
111111a(i, j) i a (j, i)
111111a(j, i) i aux
1111Fin_Para
11Fin_Para
Fin

IMPLEMENTACIÓN
Se incluye el método transponer dentro de un programa que ilustra el uso de dicho método.

class Transpon {
11public static void main(String [] args) {
1111int [][] a = {{0,1,2},{3,4,5},{6,7,8}};

1111for (int i=0; i<a.length; i++) {


111111for (int j=0; j<a[0].length; j++){
11111111System.out.print(a[i][j]+” “);
111111}
111111System.out.println();
1111}

1111transponer(a);

1111System.out.println();

1111for (int i=0; i<a.length; i++) {


111111for (int j=0; j<a[0].length; j++){
11111111System.out.print(a[i][j]+” “);
111111}
111111System.out.println();
Soluciones a los ejercicios propuestos 295

1111}
11}

11public static void transponer(int [][] a) {


1111int aux;
1111for (int i=0; i<a.length; i++)
111111for (int j=0; j<i; j++) {
11111111aux =a[i][j];
11111111a[i][j]=a[j][i];
11111111a[j][i]=aux;
111111}
11}
}

El método tiene tipo de retorno void (no devuelve nada) porque la matriz al pasarse por referen-
cia mantiene los cambios al finalizar el método de transposición. Si queremos conservar la matriz ori-
ginal habría que usar otra matriz b para transponer.

Ejercicio 10

a) Método del trapecio

ANÁLISIS
Sabemos que el ráea debajo de una curva es la integral entre dos puntos. Una forma de calcular numé-
ricamente la integral es mediante la regla del trapecio.
La regla del trapecio se ilustra de la forma siguiente. Sea una función y 5 f (x) entre los valores de
x 5 a y x 5 b. Si unimos los puntos inicial (a) y final (b) de la curva con una recta, obtenemos un tra-
pecio, véase la figura A.5.6.

y f (b)
Área = (b – a) á
áf (b) – f (a)/2
y = f (x) f (a)

Área = (b – a) áf (a)

a b x a b

Figura A.5.6. Construcción del trapecio Figura A.5.7. Cálculo del área de un trapecio
296 Introducción a la programación con orientación a objetos

La idea es aproximar la integral por el ráea del trapecio que se obtiene, como indica la Figura A.5.7.

El ráea total serála suma de las dos ráeas:

f(a) 1 f(b
Total 5 (b 2 a) á }
2

Si usáramos un solo trapecio el error sería enorme, por lo que utilizaremos muchos trapecios
pequeños. Cuanto más pequeños sean menos errores cometeremos y más se parecerála línea curva a
la recta del trapecio. En el límite coincidirán. Por otro lado, debemos tener en cuenta que cuanto mayor
sea el número de trapecios mayor serátambién el error de redondeo acumulado.
Veamos cómo se evalúa el ráea total si tengo n trapecios con la misma base,

h 5 (b 2 a)/n.

La Figura A.5.8 ilustra la situación.

a b x

Figura A.5.8. División del intervalo a-b en trapecios

La integral sería la suma de todas las ráeas de los trapecios:

f(a) 1 f (a 1 h)f(a 1 h) 1 f(a 1 2 á h)


3
Integral 5 h á } }1
2
}}}
2
1 ...

f(a 1 (n 2 2) á h) 1 f(a 1 (n 2 1) á h) f(a 1 (n 2 1) á h) 1 f(b)


1 }}}}
2
1 }}}
2 4
con lo cual al final tenemos,
Soluciones a los ejercicios propuestos 297

f(a) 1 f(b) n 2 1
Integral 5 h á 3 }}
2
1 6 f(a 1 i áh)
i 5 1
4
En nuestro caso, f 5 sen (u), a = 0 y b = š radianes.

DISEÑO
La expresión anterior se puede implementar en pseudocódigo. Para ello, supongamos que disponemos
de un método que nos devuelve el valor de la función que estemos usando.

Inicio
11Leer n, a y b
11hi(b-a)/n

11integrali (f(a) + f(b) )/2


11Para ii1 mientras i < n incremento iii+1
1111integral i integral + f(a+i*h)
11Fin_Para

11integrali integral*h
11Devolver integral
Fin

IMPLEMENTACIÓN
Con el diseño previo, podemos escribir un método en Java que aplique de forma genérica la regla del
trapecio. Lo único que necesitamos es un método adicional, funcion, que evalúe la función que que-
remos integrar. De esta forma cuando queramos integrar otra función basta con actualizar el método
funcion. El código sería,
// Método que calcula una integral por medio de la regla
// del trapecio

11public static double trapecio(int n, double a, double b) {


1111double integral, h;

1111h=(b-a)/n; // Incremento a usar

1111// Ahora se aplica la regla del trapecio


1111integral= (funcion(a)+funcion(b))/2;
1111for (int i=1; i<n; i++){
111111integral=integral+ funcion(a+i*h);
1111}
1111integral=integral*h;

1111return integral;
}1//Fin método trapecio

b) Montecarlo
ANÁLISIS
Vamos a ilustrar el método con un caso en una dimensión. La integral es el ráea debajo de la curva defini-
da por la función a integrar y el eje x, véase la Figura A.5.9. Si distribuimos aleatoriamente Nt puntos sobre
el rectángulo definido por los puntos a, b, f 0 y f1, la densidad de puntos serála misma en todas partes ya
298 Introducción a la programación con orientación a objetos

que la distribución es uniforme (a más puntos más ráea y a menos puntos menos ráea). Por esta razón, el
número de puntos debajo de la curva, Na, es proporcional al ráea de la misma y, por tanto, a la integral,

f1

y = f (x)

f0
a b x

Figura A.5.9. Diagrama para la explicación del método de Monte Carlo

Na ~ E a
b

f(x) dx

Análogamente, el número total de puntos N t es proporcional al ráea total,

Nt ~ (b 2 a) á( f1 2 f0)

y como la constante de proporcionalidad en ambos casos tiene que ser la misma, se cumple el cocien-
te:

Na
}} 5 E b
f(x) d x
}
Nt [(b 2a a) á( f 1 2f
Despejando,

E a
b
N
f(x) dx 5 [(b 2 a) á( f1 2 f0)] á } }a
Nt
DISEÑO
Como conocemos la función y de dónde a dónde queremos integrar nos basta con generar puntos ale-
atoriamente y ver cuáles están por encima o por debajo de la función. Lo que haremos serágenerar
puntos sobre el intervalo de x entre a y b y sobre el intervalo de y entre f0 a f1. Para el punto genera-
do aleatoriamente (x, y) calculamos el valor de la función, z 5 f(x) y vemos si z . y. Si es así lo con-
tamos como un punto de Nt (todos los puntos generados contribuyen a este resultado) pero no como
un punto de Na. Esto se hace para un cierto número de puntos y se calcula la integral. Si el número
total de puntos se da como dato Nt ya es conocido. Con todo esto podemos diseñar un algoritmo cuyo
pseudocódigo sería,

Inicio
11Leer Nt, a, b, f0 y f1
11Na i 0
Soluciones a los ejercicios propuestos 299

11hx i b-a
11hy i f1-f0
11Para i i 1 mientras (i Nt) incremento i i i+1
1111x i a+hx*random
1111y i f0+hy*random
1111z i f(x)
1111Si (y z) entonces
111111Na i Na +1
1111Fin_Si
11Fin_Para

11integral i Na*hx*hy / Nt

11Devolver integral
Fin

En el algoritmo anterior se usa un método random que devuelva un número aleatorio en el inter-
valo [0, 1).

IMPLEMENTACIÓN
Un método que implemente el algoritmo anterior podría ser el siguiente,

// Método que calcula una integral por medio del


// método de Montecarlo
11public static double montecarlo(int Nt, double a, double b,
1111111111111111111111111111double f0, double f1) {
1111int Na;
1111double integral, h_x, h_ y, x, y, z, fz;
1111// Inicializando variables
1111Na=0;
1111h_x=b-a;111111111111// Intervalo entre a y b
1111h_ y=f1-f0;1111111111// Intervalo entre f0 y f1
1111for (int i=1; i<=Nt; i++) {
111111x=a+h_x*Math.random();11// Selección al azar de un punto
111111y=f0+h_ y*Math.random(); //sobre los segmentos a-b y f0-f1
111111z=funcion(x);
111111if (y<=z) {
11111111Na=Na+1;
111111} // Fin if
1111}// Fin for
1111integral = Na*h_x*h_ y/Nt;
1111return integral;
11}11//Fin método Monte Carlo

Al método hay que pasarle los valores f0 y f1 que pueden ser el valor mínimo y el máximo de la
función. Con esta elección ningún punto generado aleatoriamente entre las valores de abscisa a y b
podrágenerar un valor de la función fuera del intervalo f0, f1. Para ser general el método usa otro
método llamado funcion que devuelve el valor de la función deseada.
El método de MonteCarlo es útil cuando se trabaja en muchas dimensiones con funciones muy
difíciles de manejar. El método con algunas variantes se usa, por ejemplo, para simular el comporta-
miento de fluidos. El problema es que la precisión del resultado aumenta como -Nt. Por eso, si quere-
300 Introducción a la programación con orientación a objetos

mos diez veces más precisión (un dígito más) hay que aumentar 100 veces el número de puntos.

PROGRAMA
Nuestro programa constaráde 3 métodos: main que llamaráa los otros dos métodos: montecarlo y
trapecio que a su vez invocará n al método auxiliar funcion que devuelve los valores de sen (x) obteni-
dos con el método sin() de la clase Math. El diagrama de estructura sería el recogido en la Figura A.5.10.

Principal

trapecios MonteCarlo

función

Figura A.5.10. Diagrama de estructura para el ejercicio de integración numérica


El código correspondiente sería,
/*——————————————————————————————————————————————————————
111Cálculo numérico de la integral de seno de un ángulo
111entre 0 y 180 grados (0 y Pi radianes)
————————————————————————————————————————————————————————*/

class Integral {
11public static void main(String [] args) {
1111int n;
1111double i_trapecio;
1111double i_Montecarlo;
1111n=Integer.parseInt(args[0]); // numero de puntos
1111i_trapecio=trapecio(n, 0, Math.PI);
1111i_Montecarlo=montecarlo(n, 0, Math.PI, 0, 1);
1111System.out.println(“La integral calculada por la regla del”
111111111111111111111111+” trapecio es: “+i_trapecio);
1111System.out.println(“La integral calculada por el metodo de”
111111111111111111111111+”Montecarlo es: “+i_Montecarlo);
} //Fin método main

// Método que calcula una integral por medio de la regla


// del trapecio

11public static double trapecio(int n, double a, double b) {


1111double integral, h;

1111h=(b-a)/n;1111111111// Incremento a usar

1111// Ahora se aplica la regla del trapecio


1111integral = (funcion(a)+funcion(b))/2;
1111for (int i=1; i<n; i++){
111111integral=integral+ funcion(a+i*h);
Soluciones a los ejercicios propuestos 301

1111}
1111integral=integral*h;

1111return integral;
11}111//Fin método trapecio

// Método que calcula una integral por medio del


// método de Montecarlo

11public static double montecarlo(int Nt, double a, double b,


1111111111111111111111111111double f0, double f1) {
1111int Na;
1111double integral, h_x, h_ y, x, y, z, fz;

1111// Inicializando variables

1111Na=0;
1111h_x=b-a;111111111111// Intervalo entre a y b
1111h_y=f1-f0;1111111111// Intervalo entre f0 y f1

1111for (int i=1; i<=Nt; i++) {


111111x= a+h_x*Math.random(); // Selección al azar de un punto
111111y=f0+h_ y*Math.random(); //sobre los segmentos a-b y f0-f1
111111z=funcion(x);
111111if (y<=z) {
11111111Na=Na+1;
111111} // Fin if
1111}// Fin for

1111integral = Na*h_x*h_ y/Nt;

1111return integral;
11}1111//Fin método Monte Carlo
// Método que implementa la función que se integra numéricamente
11public static double funcion(double teta) {
1111return Math.sin(teta);
11}
} //Fin de la clase Integral

El lector puede comprobar cómo el resultado obtenido va variando con el número de puntos que
se introducen y, cómo al aumentar el número de puntos, se va alcanzando al resultado analítico de la
integral que es 2.0.

Ejercicio 12
ANÁLISIS Y DISEÑO
En este ejemplo tan sencillo tanto el análisis como el diseño están prácticamente implícitos en el enun-
ciado. Como labor de diseño únicamente particularizaremos el algoritmo de Euclides en pseudo-
código,
Inicio
11Leer n, m
11resto i0
11sigue i verdadero
302 Introducción a la programación con orientación a objetos

11Si (m < n) entonces


1111aux i m
1111m i n
1111n i aux
11Fin_Si

11Mientras (sigue)
1111resto i resto de m/n
1111Si (resto ½ 0) entonces
111111m i n
111111n i resto
1111Si_no
111111resto i n
111111sigue i falso
1111Fin_Si
11Fin_Mientras

11Devolver resto
Fin

Obsérvese que en el peor de los casos el máximo común divisor es 1. Este algoritmo lo imple-
mentaremos en un método.
IMPLEMENTACIÓN
import java.io.*;
class Divisor {
11public static void main(String [] args) throws IOException {
1111int m, n, mcd;

1111BufferedReader leer =new BufferedReader


11111111111111111(new InputStreamReader(System.in));

1111System.out.println(“Introduzca primer numero:”);


1111m=Integer.parseInt(leer.readLine());

1111System.out.println(“Introduzca segundo numero:”);


1111n=Integer.parseInt(leer.readLine());
1111System.out.println();
1111System.out.println(euclides(n, m));
11}

// Algoritmo de Euclides para obtener el máximo común


// divisor de dos números enteros

11public static int euclides(int n, int m) {


1111int resto=0;
1111boolean sigue=true;

1111if (m<n) {1111// Ordenando los valores


111111int aux=m;
111111m=n;
111111n=aux;
1111}

1111while (sigue) {
Soluciones a los ejercicios propuestos 303

111111resto= m%n;11111// Determinando el resto


111111if (resto!=0) {
11111111m=n;
11111111n=resto;
111111}
111111else {
11111111resto=n;
1111111sigue=false;
111111}
1111}111// Fin while

1111return resto;

11}11// Fin método euclides

} // Fin clase

CAPÍTULO 6. RECURSIVIDAD

Ejercicio 1
a) Con n 5 5 no alcanza el caso base, recursividad infinita
b) Con n 5 6, devuelve 10

Ejercicio 2
Obsérvese que cuando n alcance el valor 0 tenemos el caso base, y que con n $ 1 estamos en la parte
inductiva. En ésta, primero se hace la llamada recursiva y luego aparece la impresión. Por ello, no se
escribe la ‘R’ hasta que se alcanza el caso base y se va devolviendo el control de las llamadas recur-
sivas. Representando la pila de llamadas tendríamos,

N53 R
™ e
N52 R
™ e
N51 R
™ e
N50‡ B

La salida, por tanto, sería:

B
R
R
R

Ejercicio 3
ANÁLISIS
El problema es claro, tenemos que implementar el cálculo de la función de Ackermann recursivamen-
te. No hay ningún requisito adicional.

DISEÑO
304 Introducción a la programación con orientación a objetos

De acuerdo a su definición, la función de Ackermann tiene un caso base que corresponde a m 5 0.


Teniendo en cuenta que la función estádefinida para enteros positivos, automáticamente sabemos que
la otra opción es la de m . 0, no haciendo falta contrastarla en una condición (if). Por la misma razón,
respecto a n basta con contrastar si n 5 0 puesto que si no, la única opción es que sea n . 0. Con estas
consideraciones, el pseudocódigo para el algoritmo recursivo sería,

Inicio
Leer m y n
valor i 0
Si (m=0) entonces
valor i n+1
Si_no
Si (n = 0) entonces
Llamar al método con (n-1) y 1
Si_no
Llamar al método con (m-1) y el resultado de
llamar al método con m y (n-1)
Fin_Si
Fin_Si
Fin

IMPLEMENTACIÓN
Un programa con un método que implementa el algoritmo anterior y lo aplica al caso m=1, n=1 podría
ser el siguiente,

class Ackermann {
public static void main(String [] args) {
int m=1, n=1;
System.out.println(“\nEl valor de la funcion para m= “+m
+” y n= “+n +” es: “+Acker(m,n));
}
public static int Acker(int m, int n) {
int valor=0;
if (m==0){
valor=n+1;
}
else {
if (n==0){
valor= Acker(m-1,1);
}
else {
valor=Acker(m-1,Acker(m,n-1));
}
}
return valor;
}
}

Con m 5 1, n 5 1 y siguiendo el código del algoritmo habría tres llamadas a Acker desde que se
recibe el valor inicial de m y n. Éstas serían, en primer lugar una llamada que incluye otra llamada,
Acker(0,Acker(1,0)), y en segundo lugar otra llamada con Acker(0,1)). El resultado final de la
función sería 3.

Ejercicio 4
Soluciones a los ejercicios propuestos 305

ANÁLISIS
Aparte de la funcionalidad del método no hay ningún requisito especial, por lo que abordaremos direc-
tamente el diseño.
DISEÑO
Debemos realizar un sumatorio desde el primer elemento hasta el número n. Teniendo en cuenta el 0-
origen de las matrices en Java los índices deberían correr desde 0 hasta (n 2 1). Si no se pueden usar
bucles, se utilizaráuna solución recursiva como la del siguiente algoritmo,
Inicio
valor i 0
Leer matriz a y valor n
Si ((n-1) = 0) entonces
valor i a (0)
Si_no
valor i a (n-1) + resultado del propio método con matriz a y (n-1)
Fin_Si
Devolver valor
Fin

IMPLEMENTACIÓN
El método correspondiente al pseudocódigo anterior sería el siguiente,
double suma(double [] a, int n) {
double valor=0;
if (n-1==0){
valor = a[0];
}
else {
valor = a[n-1]+suma(a, n-1);
}
}

Ejercicio 5
ANÁLISIS
El problema de las torres de Hanoi es un problema clásico en recursividad. La complejidad del mis-
mo puede observarse resolviéndolo para un caso sencillo, como el de tres discos. El lector puede rea-
lizar el ejercicio usando tres monedas de distintos tamaños. La secuencia de transferencias de discos
queda ilustrada en la Figura A.6.1 donde se transfieren tres discos de la varilla de la izquierda a la vari-
lla central (la derecha actúa como auxiliar). Obsérvese que incluso en este caso tan sencillo la solu-
ción no es inmediata. La complejidad del problema crece exponencialmente con el número de discos
a transferir.
306 Introducción a la programación con orientación a objetos

Figura A.6.1. Resolución del problema de las torres de Hanoi con tres discos

DISEÑO
Obtener un algoritmo que resuelva en el caso general el problema de las torres de Hanoi parece una
tarea Homérica. Sin embargo, vamos a ver cómo el teorema de inducción nos permite una solución
recursiva muy elegante. Para ello, fijémonos en que si queremos mover n discos (tres en el ejemplo)
de la primera a la segunda varilla lo que hacemos es montar los n 2 1 (2 en este caso) discos menores
en la varilla auxiliar (la tercera en este caso). El problema concluye al mover el disco sobrante (el más
grande) a la varilla final y mover los n 2 1 discos de la varilla auxiliar a la final. Si tuviéramos cuatro
discos tendríamos que montar primero los tres anteriores en la auxiliar, etc. Esta solución es clara-
mente recursiva. Para resolver el problema con n discos tenemos que resolverlo con n 2 1. Para resol-
verlo con n 2 1 discos necesitamos resolverlo con n 2 2 y así hasta que no quede ningún disco.
Fijémonos en que si dados n discos resolvemos antes para n 2 1 no incumplimos los requisitos del jue-
go, porque el disco que queda suelto es siempre mayor que los otros n 2 1. Por eso, cuando lo mova-
mos a su varilla final quedarásiempre debajo de cualquier otro disco que coloquemos posteriormente.
En cierto sentido es como decir que ese disco “ha desaparecido”. Cuando no queden discos estamos
en el caso base y el problema estáresuelto.
La solución general, por tanto, consiste en mover los n 2 1 discos superiores a la varilla auxiliar.
Luego mover el disco restante a la varilla final y ahora mover los n 2 1 discos desde la varilla auxiliar
a la final. Fijémonos que la última acción representa una versión simplificada del caso general.
Tras esta discusión organicemos el algoritmo. Etiquetemos los varillas como 1, 2 y 3. El objetivo
es pasar n discos de 1 a 2, con 3 como varilla auxiliar. Los pasos serían:

a) Pasar n 2 1 discos de 1 a 3 con 2 como varilla auxiliar


b) Poner el disco que queda en 1 (el más grande) en la varilla 2
c) Repetir moviendo los n 2 1 discos de la varilla 3 a la varilla 2 usando la varilla 1 como auxiliar

Un truco cómodo para saber cuál es la varilla auxiliar es el siguiente. Debemos conocer cuáles son
las dos varillas (origen y destino) que queremos usar. Si etiquetamos las tres varillas como 1, 2 y 3
tenemos que 1 1 2 1 3 5 6. Si llamanos i, j a las varillas a usar tendremos que 6 2 i 2 j nos da el núme-
ro de la tercera varilla (la auxiliar) independientemente del valor de i y j.

La formulación recursiva sería la siguiente,

a) Caso base n 5 0
b) Caso inductivo

Para mover n discos de la varilla i a la j movamos (de alguna forma que no viole las reglas del jue-
go) los (n 2 1) discos superiores a la varilla auxiliar (6 2 i 2 j). Después, llevemos el disco que queda
en la varilla i a la varilla j. Finalmente, movamos (de alguna forma que no viole las reglas del juego)
los (n 2 1) discos de la varilla auxiliar a la varilla j.
Obsérvese que sólo necesitamos especificar la estrategia de actuación y no el conjunto completo
de movimientos disco a disco. El pseudocódigo para el algoritmo recursivo sería,

Inicio
Leer n, i (origen), j(destino)
Si (n>0) entonces
Llamar al propio método con n-1, i, 6-i-j
Escribir de i a j
Llamar al propio método con n-1, 6-i-j, j
Soluciones a los ejercicios propuestos 307

Fin_Si
Fin

IMPLEMENTACIÓN
El algoritmo anterior se implementa con facilidad. El siguiente es un programa que soluciona el pro-
blema de las torres de Hanoi con un método recursivo,

/******************************************************************
Programa para resolver el problema de las torres de Hanoi
El programa lee por la línea de órdenes el número de discos y
el número (1, 2, 3) de las varillas inicial y final
*****************************************************************/

class Torres_de_Hanoi {
public static void main(String [] args) {
int n, i, j;
n=Integer.parseInt(args[0]);
i=Integer.parseInt(args[1]);
j=Integer.parseInt(args[2]);
hanoi(n, i, j);
} // Fin metodo main

public static void hanoi(int n,int i,int j) {


if (n>0) {
hanoi(n-1, i, 6-i-j); // Moviendo los n-1 discos superiores
// a la varilla auxiliar

System.out.println(i+”—->”+j); // Moviendo el ultimo disco


// a la varilla destino

hanoi(n-1,6-i-j,j); // Moviendo los n-1 discos a la varilla


// destino
}
} // Fin metodo recursivo hanoi

} // Fin clase

Con datos iniciales de n=3, i=1 y j=2 (el ejemplo de los tres discos) el resultado del programa es:

1—>2
1—>3
2—>3
1—>2
3—>1
3—>2
1—>2

Ejercicio 6
ANÁLISIS
La funcionalidad del código estáexplícitamente indicada en el enunciado. Se trata de evaluar x n recur-
sivamente. Veamos cómo.
308 Introducción a la programación con orientación a objetos

DISEÑO
La potencia n de un número x se puede definir recursivamente como:

potencia(x, n) 5 x ápotencia( x, n 2 1)

El caso base sería que n tomase valor 0. Como sabemos un número elevado a cero es 1.

Con esta información el pseudocódigo para el correspondiente algoritmo sería,

Inicio
Leer x y n
valor i1
Si (n>0) entonces
valor ivalor * resultado del propio método con x y (n-1)
Fin_Si
Devolver valor
Fin

IMPLEMENTACIÓN

class Potencia{
public static void main(String [ ] args) {
int x, n;
x=Integer.parseInt(args[0]);
n=Integer.parseInt(args[1]);
System.out.println(“La potencia “+n+ “ de “
+ x+” es: “+calcular_ potencia(x,n));
} // Fin del main

public static int calcular_ potencia(int x, int n){


int valor=1;
if (n>0) {
valor = x * calcular_ potencia(x, n-1);
}
return valor;
} // Fin del método
}//Fin clase

Como puede observarse el programa lee los valores de x y n por línea de órdenes.

Ejercicio 7
Al método suma le falta la palabra clave return en la parte que realiza la llamada recursiva. Sin
return el programa devolvería algún valor sólo en el caso de numero=0. El programa corregido
sería:

int suma(int numero){


if (numero ==0)
return 0;
else
return numero+suma(numero-1);

} //Fin del método suma


Soluciones a los ejercicios propuestos 309

Esta versión presenta una característica no recomendable de acuerdo a los principios de la progra-
mación estructurada. Se trata de la existencia de dos puntos de retorno desde el método (hay dos
return). Una solución más elegante y acorde además a las reglas de estilo del Apéndice D, sería la
siguiente,

int suma(int numero){


int valor;
if (numero ==0) {
valor = 0;
}
else {
valor = numero+suma(numero-1);
}
return valor;
} //Fin del método suma

Ejercicio 8
ANÁLISIS
Una vez más el problema es tan sencillo que no hay más que considerar una sola tarea.

DISEÑO
La solución consiste en recorrer la matriz cambiando el índice de uno en uno. Un algoritmo posible
sería el siguiente,

Inicio
Leer matriz y tamaño (n)
índice i 0
Si (n > 0) entonces
Llamar al propio método con matriz a y (n-1)
Escribir a (n-1)
Fin_Si
Fin

Obsérvese que la impresión estácolocada después de la llamada recursiva. Como el caso base
resulta ser n 5 0 los elementos empezarán a escribirse desde el índice 0 (que es cuando se empiezan a
devolver desde el caso base las llamadas recursivas) hasta el índice (n 2 1).

IMPLEMENTACIÓN
A continuación, se muestra para un caso particular, un programa que aplica, a través de un método, el
algoritmo anterior.

class ImprimeMatriz{
public static void main(String [ ] args) {
int a[]={1,2,3,4,5};
imprime(a, a.length);// Se pasa la matriz y su longitud

} //Fin del main


public static void imprime(int a[], int n){
if (n > 0) {
imprime(a, n-1);
310 Introducción a la programación con orientación a objetos

System.out.print(a[n-1]);
}
} //Fin del método imprime
}

La salida de este programa sería:

12345

Ejercicio 9
Obsérvese que el método A llama al método B que a su vez invoca al método A. Es un caso de recur-
sividad indirecta. Siguiendo la traza del programa observamos que el resultado es:

A antes
B antes
A antes
B antes
B despues
A despues
B despues
A despues

Ejercicio 10
Directamente podemos abordar el diseño. Teniendo en cuenta el algoritmo tal y como se describe en
el Ejercicio 12 del Capítulo 5 tenemos,

a) Caso Base
m
Cuando resto de } } = 0
n

b) Caso inductivo
Si resto ½ 0 se invoca el algoritmo con m 5 n y n 5 resto

El pseudocódigo del algoritmo sería,

Inicio
Leer n y m
valor i0
resto iparte entera de m/n

Si (resto = 0)
valor in
Si_no
valor iresultado del propio algoritmo con resto y n
Fin_Si

Devolver valor
Fin
Soluciones a los ejercicios propuestos 311

IMPLEMENTACIÓN
Una variante del programa del Ejercicio 12 del Capítulo 5 con un método que implementa la versión
recursiva del algoritmo es la siguiente,

import java.io.*;
class Divisor {
public static void main(String [] args) throws IOException {
int m, n, mcd;

BufferedReader leer =new BufferedReader


(new InputStreamReader(System.in));

System.out.println(“Introduzca primer numero:”);


m=Integer.parseInt(leer.readLine());

System.out.println(“Introduzca segundo numero:”);


n=Integer.parseInt(leer.readLine());
System.out.println();

if (m<n) { // Ordenando los valores


int aux=m;
m=n;
n=aux;
}
System.out.println(euclides(n, m));

}
312 Introducción a la programación con orientación a objetos

/* Algoritmo de Euclides recursivo para obtener el máximo común


divisor de dos números enteros */
public static int euclides(int n, int m) {
int resto, valor;
resto = m%n;

if (resto==0) {
valor=n;
}
else {
valor = euclides(resto, n);
}

return valor;

} // Fin método euclides


} // Fin clase

Obsérvese cómo la ordenación de n y m se ha colocado en el método main para evitar que se rea-
lice más de una vez.

CAPÍTULO 7. CLASES Y OBJETOS

Ejercicio 1
De acuerdo al diagrama de clases debemos incluir un atributo para la identificación del producto (ID)
que podemos considerar como un número de identificación e implementarlo como un entero. El atri-
buto coste que aparece en el diagrama se puede implementar con una variable real. Parece lógico
incluir un nuevo atributo, nombre, que describa la naturaleza del producto y que podríamos imple-
mentar con una cadena. Desde el punto de vista de los métodos, podemos diseñar un constructor que
inicialice los atributos anteriores. De acuerdo a los requisitos, también debemos incluir métodos de
consulta para los tres atributos. También parece lógico incluir un método para la actualización del cos-
te del producto. Los atributos los declararemos como privados y los métodos propuestos, que repre-
sentan la interfaz pública de la clase, como públicos. En resumen, en un diagrama de clase tendríamos
el resultado mostrado en la Figura A.7.1.

Producto

- ID:int
- coste:double
- nombre:String

+ Producto (nombre:String,
coste:double, ID:int)
+ nombre ( ):String
+ ID ( ):int
+ coste ( ):double
+ actualizar_coste (double coste):void

Figura A.7.1. Diagrama para la clase Producto


Soluciones a los ejercicios propuestos 313

IMPLEMENTACIÓN
El diagrama anterior se puede implementar como se indica a continuación,
class Producto {
private int ID;
private double coste;
private String nombre;
public Producto(String nombre, double coste, int ID) {
this.nombre=nombre;
this.coste=coste;
this.ID=ID;
}
public String nombre() {
return nombre;
}
public int ID() {
return ID;
}
public double coste() {
return coste;
}
public void actualizar_coste(double coste) {
this.coste=coste;
}
}

En el constructor se ha usado la palabra reservada this que identifica al propio objeto que esta-
mos manejando. El identificador this se refiere siempre al objeto con el que estamos trabajando, hace
referencia al ejemplar y no a la clase. Este modificador se usa para deshacer la ambigüedad entre un
miembro de la clase y otro elemento. Para entenderlo tengamos en cuenta que el nombre completo de
un miembro de la clase (dato o método) es:
this.miembro

Por ejemplo, cuando tenemos una variable local a un método con el mismo nombre que una varia-
ble de ejemplar podemos usar this para referirnos a la variable de ejemplar dentro del método.

Ejercicio 2
Realizando una traducción a UML de la descripción del sistema obtenemos el resultado mostrado en
la Figura A.7.2.

1 * 1 *
Empresa Equipo_desarrollo Programador

Programador_Contratado Programador_Fijo

Director
314 Introducción a la programación con orientación a objetos

Figura A.7.2. Diagrama de clases del sistema descrito


La parte más clara, con los requisitos indicados, es la de herencia entre los distintos tipos de
empleados. En el diagrama se ha considerado que la empresa estáorganizada como un conjunto
de equipos de desarrollo (podría ser que se tuviera que considerar como un conjunto de departa-
mentos) y que cada equipo de desarrollo estáformado por un conjunto de programadores.

Ejercicio 3
Una vez más, el planteamiento es traducir la descripción del sistema a UML. El resultado se ilus-
tra en la Figura A.7.3.

1 1..* 1
Universidad Departamento

1..*
1..* 1..*
Estudiante Curso Profesor

Curso verano Curso completo

Estudiante Estudiante
curso de Profesor Profesor
completo verano titular asociado

Figura A.7.3. Diagrama de clases para el modelo de Universidad

La relación entre Estudiante y Curso no es simple, pues un estudiante sigue varios cursos
y un curso consta de varios estudiantes. Aquí se ha considerado una relación de asociación.
Problemas similares surgen entre Profesor, Estudiante y Curso. En este caso se ha representa-
do como una relación de dependencia entre Curso y Profesor y de asociación entre Curso y
Estudiante. Este tipo de dudas deben resolverse en la etapa de análisis recabando más infor-
mación sobre el sistema.

Ejercicio 4
ANÁLISIS Y DISEÑO
Como habitualmente, en estos ejemplos sencillos el enunciado sirve como indicación de requisitos. En
el dominio del problema definido por los mismos sólo van a ser necesarias dos clases, una clase Línea
y una clase Punto. Entre ambas existe una relación de dependencia. El diagrama de clases sería el
recogido en la Figura A.7.4.
Abordemos cómo conseguir que el comportamiento de las clases sea el deseado. Veamos cómo
obtener la pendiente y la ordenada en el origen. La ecuación de la recta es: y 5 a 1 bx. Con dos pun-
Soluciones a los ejercicios propuestos 315

tos tendríamos:
Punto Linea

x:double b:double
y:double a:double

Punto (double,double) Linea (Punto,Punto)


x ( ):double Linea (Punto,double)
y ( ):double a ( ):double
igual (Punto):boolean b ( ):double
y (double):double

Figura A.7.4. Relación entre las clases Punto y Línea

y1 5 a 1 bx1 , y2 5 a 1 bx2

Restando las dos ecuaciones:


(y 2y )
b 51}} 2
( x12 x2)
y

a 5 y1 2 b áx1

CLASES Y RELACIONES
Tal y como estáplanteado tenemos una relación de dependencia. Podemos declarar una clase Línea
que usa una clase Punto. Para poder cumplir los requisitos podemos usar dos constructores (sobre-
carga) en la clase Línea.

IMPLEMENTACIÓN

class Punto {
private double x;
private double y;

public Punto(double x_i, double y_i) {


x=x_i;
y=y_i;
}

public double x() {


return x;
}
public double y( ) {
return y;
}

public boolean igual(Punto p) {


return (x==p.x() && y==p.y());
}
} // Fin clase punto
316 Introducción a la programación con orientación a objetos

class Linea {
private double a;
private double b;

public Linea(Punto p1, Punto p2) {


b= (p1.y()-p2.y())/(p1.x()-p2.x());
a= p1.y() - b*p1.x();
}

public Linea(Punto p, double b_i) {


b= b_i;
a= p.y() - b*p.x();
}

public double a() {


return a;
}
public double b() {
return b;
}

public double y(double x_i) {


return a+b*x_i;
}
} // Fin clase Linea

Fijémonos que la relación de uso se traduce en que los objetos de la clase dependiente (Línea)
aceptan como dato en algún método objetos de la clase independiente (Punto).
Como ejemplo de uso de la clase Línea tenemos el siguiente programa principal que define dos
puntos, determina la recta que pasa por ellos y calcula la y para una x dada.

class Ejercicio {
public static void main(String [] args) {
Punto p1 =new Punto(1.0, 1.0);
Punto p2 =new Punto(2.0, 3.0);

if (p1.igual(p2)) {
System.out.println(“Los dos puntos son iguales”);
}
else {
Linea recta = new Linea(p1, p2);
System.out.println(“Ordenada en el origen: “+recta.a());
System.out.println(“Pendiente: “+recta.b());
System.out.println(“y para x=5 : “+recta.y(5.0));
}// Fin del if-else
}// Fin del main
}// Fin de la clase Ejercicio

El resultado sería,

Ordenada en el origen: -1.0


Pendiente: 2.0
y para x=5 : 9.0
Soluciones a los ejercicios propuestos 317

Ejercicio 5
ANÁLISIS Y DISEÑO
En el dominio del problema podemos identificar dos clases relacionadas, una clase Nombre y una cla-
se Persona. La relación entre ellas sería de asociación. El correspondiente diagrama de clase, inclu-
yendo atributos y procedimientos, sería de la forma ilustrada en la Figura A.7.5.

Nombre Persona

nombre:String 1 1 nombre_persona:Nombre
apellido_1:String dni:int
apellido_2:String
Persona (nombre,int)
Nombre (String,String,String) imprime_datos ( ):void
nombre ( ):String nombre ( ):Nombre
apellido_1:String dni ( ):int
apellido_2:String
imprime_nombre ( ):void

Figura A.7.5. Relación entre las clases Nombre y Persona

Es costumbre poner a un método de consulta el mismo nombre que a la variable que se devuelve.
Otra opción es poner un devuelve_Nombre_de_la_variable(). En inglés se suele usar getNa-
me().

IMPLEMENTACIÓN
class Nombre {
private String nombre;
private String apellido_1;
private String apellido_2;
public Nombre(String nombre, String apellido_1,
String apellido_2) {
this.nombre=nombre;
this.apellido_1=apellido_1;
this.apellido_2=apellido_2;
}
public String nombre() {
return nombre;
}
public String apellido_1() {
return apellido_1;
}
public String apellido_2() {
return apellido_2;
}

public void imprime_nombre() {


System.out.print(nombre+” “);
System.out.print(apellido_1 +” “);
System.out.println(apellido_2);
318 Introducción a la programación con orientación a objetos

}
} // Fin clase Nombre

class Persona {
private Nombre nombre_ persona;
private int dni;

public Persona(Nombre nombre, int dni) {


nombre_ persona=nombre;
this.dni=dni;
}

public void imprime_datos( ) {


nombre_ persona.imprime_nombre();
System.out.println(“DNI: “+dni);
System.out.println();
}

public Nombre nombre() {


return nombre_ persona;
}

public int dni() {


return dni;
}

} // Fin clase Persona

Fijémonos en que el método nombre devuelve un objeto de clase Nombre. Esto se puede hacer sin
problemas.
Veamos ahora un ejemplo de programa principal que usa algunos de los métodos anteriormente
definidos y que sirve para exponer cómo crear matrices de objetos.

class Ejercicio {
public static void main(String [] args) {

/*Declaramos una matriz de 3 elementos de clase persona.


Es una matriz de objetos */
Persona [] individuos = new Persona [3];
Nombre aux;

aux=new Nombre(“Juan”, “Guijarro”, “Sobrino”);


individuos[0]= new Persona(aux, 67453219);

aux=new Nombre(“Marta”, “Salvador”, “Rodriguez”);


individuos[1]= new Persona(aux, 10567953);

individuos[2]= new Persona


(new Nombre(“Ana”, “Guijarro”, “Salvador”),
28765301);

System.out.println();
System.out.println(“Personas en la lista \n——————————”);
System.out.println();
for (int i=0; i<=2; i++){
individuos [i].imprime_datos();
}
Soluciones a los ejercicios propuestos 319

} // Fin del método main


} // Fin de la clase Ejemplo

En este ejemplo se ha usado el constructor de dos formas distintas, creando un objeto auxiliar, aux, o
bien creando el objeto directamente como parámetro actual (al igual que con la clase BufferedReader).
El resultado del programa sería,

Personas en la lista
——————————

Juan Guijarro Sobrino


DNI: 67453219

Marta Salvador Rodriguez


DNI: 10567953

Ana Guijarro Salvador


DNI: 28765301

Ejercicio 6

ANÁLISIS Y DISEÑO
Para cumplir los nuevos requisitos debemos incluir dos atributos nuevos en la clase Persona que sean
dos referencias (no dos objetos) de la propia clase Persona. Vamos a llamarlos madre y padre.

IMPLEMENTACIÓN
El nuevo código para la clase Persona sería:

class Persona {
private Nombre nombre_ persona;
private int dni;
private Persona madre=null;
private Persona padre=null;

public Persona(Nombre nombre, int dni) {


nombre_ persona=nombre;
this.dni=dni;
}

public void imprime_datos( ) {


nombre_ persona.imprime_nombre();
System.out.println(“DNI: “+dni);
System.out.println();
}

public Nombre nombre() {


return nombre_ persona;
}

public int dni() {


return dni;
320 Introducción a la programación con orientación a objetos

public void madre(Persona madre){


this.madre=madre;
}
public void padre(Persona padre){
this.padre=padre;
}
public Persona da_madre() {
return madre;
}
public Persona da_ padre() {
return padre;
}
} // Fin clase Persona

En los métodos madre y padre hemos usado el identificador this. Esto nos permite usar el
mismo nombre para el parámetro formal que para el atributo madre. Al indicar this.madre está
claro que nos referimos al atributo, si no lo pusiéramos tendríamos madre=madre lo que es total-
mente ambiguo.
La clase Nombre sería la misma que en el problema anterior.
Un posible programa principal que ilustra el uso de las nuevas capacidades es el siguiente:

class Ejemplo {
public static void main(String [] args) {
Persona [] individuos = new Persona [3];
Nombre aux;

aux=new Nombre(“Juan”, “Guijarro”, “Sobrino”);


individuos[0]= new Persona(aux, 67453219);

aux=new Nombre(“Marta”, “Salvador”, “Rodriguez”);


individuos[1]= new Persona(aux, 10567953);

individuos[2]= new Persona


(new Nombre(“Ana”, “Guijarro”, “Salvador”),
28765301);

individuos[2].madre(individuos[1]);
individuos[2].padre(individuos[0]);
System.out.println();
System.out.println(“Personas en la lista \n——————————”);
System.out.println();
for (int i=0; i<=2; i++){
individuos [i].imprime_datos();

if (individuos[i].da_madre() !=null) {
System.out.println(“La madre es:”);
individuos[i].da_madre().imprime_datos();
System.out.println(“El padre es:”);
individuos[i].da_ padre().imprime_datos();
}// Fin del if
Soluciones a los ejercicios propuestos 321

}// Fin del for

} // Fin del método main


} // Fin de la clase Ejercicio

Resultado:
Personas en la lista
——————————

Juan Guijarro Sobrino


DNI: 67453219

Marta Salvador Rodriguez


DNI: 10567953

Ana Guijarro Salvador


DNI: 28765301

La madre es:
Marta Salvador Rodriguez
DNI: 10567953

El padre es:
Juan Guijarro Sobrino
DNI: 67453219

Ejercicio 7
El resultado es 3.2.
Si se quisiera hacer referencia a la variable de clase para que el resultado de imprimir fuera 9.8
se tendría que usar la cláusula this.

Ejercicio 8
ANÁLISIS Y DISEÑO
Un polinomio queda definido como,

n
u 5 6 ai x i
i 5 0

donde los coeficientes ai son números reales. Nuestro programa contendráuna clase Polinomio que
represente el polinomio a manejar y permita realizar la operación deseada. La inicialización del poli-
nomio se realizaráa través del método constructor. El diagrama para la clase Polinomio podría
ser el mostrado en la Figura A.7.6
322 Introducción a la programación con orientación a objetos

Polinomio

- n:int
- coeficientes:double [ ]

+ Polinomio (coeficientes:double [ ])
+ y (x:double):double

Figura A.7.6. Diagrama UML de la clase Polinomio

IMPLEMENTACIÓN
El código para la clase Polinomio podría ser el siguiente,

class Polinomio{
private int n;
private double [] coeficientes;

public Polinomio(double [] coeficientes) {


this.coeficientes = coeficientes;
}

public double y(double x){


double valor, y=0.0;
int n=coeficientes.length;

for (int i=0; i < n; i++){


y=y+coeficientes[i]*Math.pow(x,i);
}
return y;
}
}//Fin clase Polinomio

El programa principal podría ser,

import java.io.*;
class Ejercicio {
public static void main(String [] args)throws IOException{
int n;
double [] coeficientes=null;
double x, y;
Polinomio poli;
boolean sigue=true;

BufferedReader leer =new BufferedReader


(new InputStreamReader(System.in));

System.out.println(“Introduzca numero de terminos:”);


n=Integer.parseInt(leer.readLine());
coeficientes = new double[n];

for (int i=0; i<n; i++){


System.out.println(“Coeficiente del termino “+i+” :”);
Soluciones a los ejercicios propuestos 323

coeficientes[i]=Double.parseDouble(leer.readLine());
}

poli=new Polinomio(coeficientes); // Creando objeto

System.out.println(“\nCalculando ordenadas”);
while (sigue){
System.out.println(“Introduzca abscisa: “);
x=Double.parseDouble(leer.readLine());
y=poli.y(x);
System.out.println(“y= “+y);
System.out.println(“\nDesea continuar? Si(1)/No(Otro valor)”);
sigue=(1==Integer.parseInt(leer.readLine()));
}
} // Fin método main
} // Fin clase Ejercicio

Obsérvese que la asignación del valor de la variable lógica sigue dentro del bucle while se reali-
za directamente como el resultado de la comparación de la lectura de la opción de continuación con el
valor 1.

CAPÍTULO 8. HERENCIA

Figura A.8.2. Diagramas de clase para el Ejercicio 5 del Capítulo 8


324 Introducción a la programación con orientación a objetos

Ejercicio 1
El resultado es,

4455

Fijémonos en que lo que se hereda es el valor inicial de prop1 y prop2, pero no su valor tras haber
sido modificado en el objeto obj1. En otras palabras, se heredan las definiciones de la clase pero no
las modificaciones en los ejemplares de esa clase.

Ejercicio 2
El resultado del programa es,

Estoy en un objeto de clase Dos con i:3


Estoy en un objeto de clase Tres con i:2

Obsérvese cómo en el método frase de la clase Dos la variable i es local, pues estádeclarada den-
tro del método frase. Por lo tanto i vale 3 aunque la variable i heredada de uno no se sobrescribe y
sigue existiendo implícitamente con el valor 2. En el caso de la clase Tres no se declara ninguna
variable i local. Por lo tanto, el identificador i corresponde a la variable de ejemplar heredada de la
clase Uno. Por esta razón i es igual a 2. La variable i de la clase Dos no se hereda, puesto que es una
variable local a un método.

Ejercicio 3
Valor de las variables pasadas: 5.0,4
Valor de la variable: 4,5.0

La clase Dos tendría el método imprime que heredaría de la clase Uno y ese método ya heredado
se sobrecarga en la clase Dos. La clase Dos tendría entonces 2 métodos que difieren en el orden de
los parámetros: imprime(j,x) e imprime(x,j).

Ejercicio 4
Valor de la variable pasada: 5.0
Valor de la variable: 4

La clase Dos tendría el método imprime que heredaría de la clase Uno y ese método ya here-
dado se sobrecarga en la clase Dos. La clase Dos tendría entonces 2 métodos imprime (j) e
imprime (x).

Ejercicio 5
ANÁLISIS Y DISEÑO
El problema se puede organizar con tres clases, Empleado, Encargado y Trabajador. De éstas, Emple-
ado sería la clase padre de una jerarquía de clases. El correspondiente diagrama se recoge en la Figura
A.8.1.
Soluciones a los ejercicios propuestos 325

Empleado

Trabajador
Encargado a comisión

Figura A.8.1. Diagrama de clases para la jerarquía de empleados

La clase Empleado seráuna clase abstracta que contenga como atributos el nombre y el apellido
de cada empleado. Como métodos tendráel constructor, dos métodos que devuelvan el valor de los
atributos y un método llamado calcularSalario que seráabstracto, puesto que el salario serádis-
tinto dependiendo de la categoría del empleado. Los miembros de cada clase se recogen en los dia-
gramas mostrados en la Figura A.8.2.
IMPLEMENTACIÓN

// Superclase
abstract class Empleado{
private String nombre;
private String apellido1;
private String apellido2;

// Constructor

public Empleado(String nombre, String apellido1, String apellido2){


this.nombre=nombre;
this.apellido1=apellido1;
this.apellido2=apellido2;
}

//Devuelve el nombre
public String nombre(){
return nombre;
}

//Devuelve el apellido1

public String apellido1(){


return apellido1;
}
//Devuelve el apellido2

public String apellido2(){


return apellido2;
}

//Método abstracto

abstract double calcularSalario();

}//Fin clase Empleado


326 Introducción a la programación con orientación a objetos

//Clase hija Encargado

class Encargado extends Empleado{


final double SALARIO=2000.0;

//Constructor

public Encargado(String nombre, String apellido1,


String apellido2){
super(nombre, apellido1, apellido2); //Utiliza el constructor del
// padre
}

//Método calcularSalario

public double calcularSalario(){


return SALARIO;
}

public String toString(){


return “Encargado: “+nombre()+ “ “+apellido1()+ “ “+apellido2();
}
}//Fin clase Encargado

// Clase hija TrabajadorComision

class TrabajadorComision extends Empleado{


private final double SALARIO=1000.0;
private final double COMISION=0.1;
private double vendido;

//Constructor

public TrabajadorComision(String nombre, String apellido1,


String apellido2, double vendido){
super(nombre, apellido1, apellido2); //Utiliza el constructor
//del padre
this.vendido=vendido;
}

// Método calcularSalario
public double calcularSalario(){
return SALARIO+COMISION*vendido;
}
public String toString(){
return “Trabajador a comision: “+nombre()+ “ “+apellido1()
+ “ “+apellido2();
}

}//Fin clase TrabajadorComision

// Clase principal
class Empresa{
public static void main(String [ ] args){
Empleado trabajador;
Soluciones a los ejercicios propuestos 327

Encargado encargado1;
TrabajadorComision trabajadorComision1;
encargado1 = new Encargado(“Maria”, “Garcia”,”Romo”);
trabajadorComision1 = new TrabajadorComision(“Raul”,”Paz”,
“Martin”,200);

trabajador = encargado1; // Referencia de superclase a objeto de


// clase Encargado
System.out.print(trabajador.toString());
System.out.println(“ gana “+trabajador.calcularSalario());

trabajador= trabajadorComision1; // Referencia de superclase a


//objeto de clase trabajador

System.out.print(trabajador.toString());
System.out.println(“ gana “+trabajador.calcularSalario());
}//Fin main
}//Fin clase Empresa

Obsérvese que en la clase padre Empleado se declaran los atributos privados. Esto implica que las
clases descendientes no los heredan. Sin embargo, su manejo implícito a través de los métodos here-
dados es perfectamente correcto, como se muestra en el ejercicio. De todas formas se recomienda
declarar como protected los atributos que van a ser heredados, así siempre seráposible usarlos
directamente en la clase descendiente.

Ejercicio 6
La definición de la interfaz es sencilla,

interface Valores{
double SALARIO=1000.0;
double COMISION=0.1;
}

Con esta interfaz la clase TrabajadorComisión podría implementarse de la forma siguiente,

class TrabajadorComision extends Empleado implements Valores{


private double vendido;

//Constructor
public TrabajadorComision(String nombre, String apellido1,
String apellido2, double vendido){
super(nombre, apellido1, apellido2); //Utiliza el constructor
// del padre
this.vendido=vendido;
}

//método calcularSalario
public double calcularSalario(){
return SALARIO+COMISION*vendido;
}

public String toString(){


328 Introducción a la programación con orientación a objetos

return “trabajador a comision:”+nombre()+ “ “+apellido1()+


“ “+apellido2();
}
}//Fin clase TrabajadorComision

Ejercicio 7
Con los requisitos especificados en el enunciado la implementación es directa,

//clase abstracta
abstract class ObjetoGrafico{
int x,y;
ObjetoGrafico(){
x = 0;
y = 0;
}
public void moverObjeto(){
x = x + 10;
y = y + 10;
}

abstract public void dibujar();

}//Fin clase ObjetoGrafico

El método moverObjeto se puede implementar aquí ya que el desplazamiento se hace siempre


igual, independientemente de la figura. El segundo método se ha definido como abstracto porque cam-
bia según el objeto a dibujar. El código para las clases que representan a las dos figuras indicadas en
los requisitos puede ser:

class Rectangulo extends ObjetoGrafico{


Rectangulo(){
super(); //utiliza el constructor de la clase padre
}

public void dibujar(){


//implementación del método
}
}//Fin clase Rectangulo

class Circunferencia extends ObjetoGrafico{


Circunferencia(){
super();
}
public void dibujar(){
//implementación del método
}
}//Fin clase Circunferencia

Estas dos clases heredan de la clase padre los atributos y los métodos. Para poder crear objetos de
clase Rectangulo o Circunferencia es necesario implementar el método abstracto dibujar que
han heredado. Dicho método tendráimplementaciones distintas según cada clase.
Soluciones a los ejercicios propuestos 329

Ejercicio 8
De acuerdo a los requisitos tendríamos las siguientes interfaces y clase,

//Primera interfaz
interface Primera{
void A();
void B();
}
//Segunda interfaz
interface Segunda extends Primera{
void C();
}

class Objetos implements Segunda {


public void A(){
System.out.println(“Método A”);
}
public void B(){
System.out.println(“Método B”);
}
public void C(){
System.out.println(“Método C”);
}
}//Fin clase Objetos

Un ejemplo de uso de la clase Objetos se recoge en el siguiente programa principal,

class Ejercicio {
public static void main(String [ ] args){
Objetos objeto1= new Objetos();
objeto1.A();
objeto1.B();
objeto1.C();
}
}//Fin clase Ejercicio

CAPÍTULO 9. FICHEROS

Ejercicio 1
Cada registro son 4 1 8 5 12 bytes. Por lo tanto, los 24 bytes que saltamos con seek(24) representan
24 4 12 5 2 registros. Dicho de otra forma, saltamos dos registros, así que con fichero.readInt()
seguido de fichero.readDouble() se leerían el valor entero 3 y el real 40.9.

Ejercicio 2
Cada registro son 4 1 8 5 12 bytes. Deberíamos saltar los tres primeros registros, es decir, los 3 á12 5
36 primeros bytes, haciendo nombre_objeto.seek(36).
330 Introducción a la programación con orientación a objetos

Ejercicio 3
import java.io.*;
class Ejercicio {
public static void main(String [] args) throws IOException{
int cuenta,clientes;
String nombre,fichero;
double saldo;
DataOutputStream salida=null; // Referencias
BufferedReader entrada;

try {
fichero=args[0];
entrada= new BufferedReader
(new InputStreamReader(System.in));
salida= new DataOutputStream
(new FileOutputStream(fichero));
System.out.println(“Introduzca numero de clientes”);

clientes=Integer.parseInt(entrada.readLine());

for (int i = 0; i < clientes; i++) {


System.out.println(); //Presentación
System.out.println(“Cliente “+(i+1));
System.out.print(“Cuenta:”);
cuenta=Integer.parseInt(entrada.readLine()); //Lee la cuenta
salida.writeInt(cuenta);
System.out.print(“nombre:”);
nombre=entrada.readLine();
salida.writeUTF(nombre);
System.out.print(“saldo:”);
saldo=Double.parseDouble(entrada.readLine());
salida.writeDouble(saldo);
}
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println(“Introduzca el nombre del “
+”fichero como argumento”);
}
catch(IOException e){
System.out.println(“No se abrio bien el fichero\n”+
e.toString());
}

salida.close();

} // Fin método main


} // Fin clase

Ejercicio 4
Teniendo en cuenta que debemos capturar primero las excepciones más específicas, el orden tendría
Soluciones a los ejercicios propuestos 331

que ser NumberFormatException, Exception.

Ejercicio 5
A fin de capturar la IOException en todos los casos y no repetir el catch que la captura vamos a
usar dos try-catch. El externo se encargaráde capturar específicamente la IOException y todas
las demás se capturarán en el try-catch interno. El código sería,

import java.io.*;
class Ejercicio {
public static void main(String [] args) {

/* Este bloque de código no corresponde al enunciado del problema.


Simplemente se utiliza para generar un fichero de datos del que
poder leer.*/

//Empieza el bloque de generación del fichero de datos


try {
DataOutputStream salida =new DataOutputStream
(new FileOutputStream(“datos”));
for (int j=0; j<10;j++){
salida.writeInt(j);
}
salida.close();
}
catch(IOException e) {
System.out.println(“Error en la escritura”);
}
//Acaba el bloque de generación del fichero de datos

//Empieza el código del método main del ejercicio propiamente


//dicho

//Inicializacion
int i=0;
DataInputStream entrada=null;

try {
// Abriendo el fichero y leyendo de él
try {
entrada=new DataInputStream
(new FileInputStream(“datos”));
do {
i=entrada.readInt();
System.out.println(i);
} while (true);
} //Fin try interno

// Excepción EOF que se usa para leer hasta fin de fichero


catch(EOFException e) {
System.out.println(“\nFin del fichero\n”);
}
332 Introducción a la programación con orientación a objetos

finally {
if (entrada!=null){
entrada.close();
}
}
} //Fin try externo

catch(IOException io) {
System.out.println(“Error en la E/S\n\n”+io.toString());
}

} // Fin método main


} // Fin clase Ejercicio

Ejercicio 6
ANÁLISIS Y DISEÑO
De acuerdo con los requisitos, la nueva clase usarála clase FileOutputStream. Aquí sólo necesita-
mos un método constructor que acepte el nombre del fichero, otro para escribir y vamos a incorporar
otro para cerrar el fichero. El diagrama de clase sería el recogido en la Figura A.9.1.

Secuencial
# salida:FileOutputStream
- Secuencial (nombre:String)
+ escribe (entero:int):void
+ cerrarFichero ( ):void

Figura A.9.1. Diagrama UML para la clase Secuencial

IMPLEMENTACIÓN
La clase quedaría como sigue,
class Secuencial{
protected FileOutputStream salida=null;

public Secuencial(String nombre){


try{
salida = new FileOutputStream(nombre);
}
catch(IOException e){
System.out.print(“Error al abrir el fichero “
+e.toString());
}
}

public void escribe(int entero){


try{
Soluciones a los ejercicios propuestos 333

salida.write(entero);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
}
}

public void cerrarFichero(){


try{
salida.close();
}
catch(IOException e){
System.out.print(“Error al cerrar el fichero “
+e.toString());
}
}
}// Fin clase Secuencial

Un ejemplo de su uso se muestra en el siguiente programa,

import java.io.*;
class Primero {
public static void main(String [] args) {
Secuencial fichero=new Secuencial(“nuevo”);
fichero.escribe(333);
fichero.cerrarFichero();
}
}

Ejercicio 7
ANÁLISIS Y DISEÑO
Es un caso similar al anterior pero aquí vamos a usar la clase DataOutputStream. Al existir varios
métodos de escritura vamos a aprovechar más las propiedades de encapsulación y sobrecarga de méto-
dos. Tendremos un método constructor que aceptaráel nombre del fichero, varios métodos de escri-
tura sobrecargados y un método para cerrar el fichero. El diagrama de clase es el recogido en la
Figura A.9.2.

SecuencialDatos
# salida:DataOutputStream
+ SecuencialDatos (nombre:String)
+ escribe (entero:int):void
+ escribe (doble:double):void
+ escribe (numero:float):void
+ escribe (cadena:String):void
+ escribe (caracter:char):void
+ cerrarFichero ( ):void
334 Introducción a la programación con orientación a objetos

Figura A.9.2. Diagrama UML para la clase SecuencialDatos

IMPLEMENTACIÓN
El código correspondiente es el siguiente,

class SecuencialDatos{
protected DataOutputStream salida=null;

public SecuencialDatos(String nombre){


try{
salida = new DataOutputStream
(new FileOutputStream(nombre));
}
catch(IOException e){
System.out.print(“Error al abrir el fichero “
+e.toString());
}
}

/*El método escribe se sobrecarga para leer valores de tipo int,


double, float, String y char */

public void escribe(int entero){

try{
salida.writeInt(entero);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
}
}

public void escribe(double doble){

try{
salida.writeDouble(doble);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
}
}

public void escribe(float numero){

try{
salida.writeFloat(numero);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
Soluciones a los ejercicios propuestos 335

}
}

public void escribe(String cadena){

try{
salida.writeUTF(cadena);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
}
}

public void escribe(char caracter){

try{
salida.writeChar(caracter);
}
catch(IOException e){
System.out.print(“Error al escribir en el fichero “
+e.toString());
}
}

public void cerrarFichero(){


try{
salida.close();
}
catch(IOException e){
System.out.print(“Error alcerrar el fichero “
+e.toString());
}
}
} // Fin de la clase SecuencialDatos

El uso de la clase se ilustra en el siguiente programa,

import java.io.*;
// Clase para usar ficheros secuenciales con datos
// Sólo escritura
class Primero{
public static void main(String [] args) {
SecuencialDatos fichero=new SecuencialDatos(“nuevo”);
fichero.escribe(333);
fichero.escribe(“mi casa”);
fichero.escribe(‘S’);
fichero.escribe(3.45);
fichero.cerrarFichero();
}
}
Obsérvese cómo la sobrecarga del método escribe simplifica el uso de la clase.
336 Introducción a la programación con orientación a objetos

Ejercicio 8
ANÁLISIS Y DISEÑO
Vamos a usar dos clases, una para el registro y otra para el fichero directo (aleatorio). La intención es
independizar totalmente el uso del fichero de la naturaleza del registro. La relación queda recogida en
la Figura A.9.3. La clase registro deberáadaptarse a cada caso. En este ejercicio se supone que cada
registro estáformado por tres campos que podrían corresponder a los datos de un cliente de un
almacén: un número de cuenta, un nombre y un saldo. Esta clase contiene un método que devuelve la
longitud del registro. La clase para el fichero de acceso directo trabaja con los registros y acepta la
posición en el fichero definida en términos de registros y no de bytes.

Fichero_directo Registro

Figura A.9.3. Diagrama de clases para el ejercicio del fichero de acceso directo
Los atributos de cada clase se muestran en las Figuras A.9.4. y A.9.5.

Registro
# cuenta:int
# nombre:String
# saldo:double
# l_registro:int

+ Registro (cuenta:int, nombre:String, saldo:double)


+ cuenta ( ):int
+ nombre ( ):String
+ saldo ( ):double
+ l_registro ( ):int
+ lee_registro (fichero:RandomAccessFile):Registro
+ pon_registro (fichero:RandomAccessFile,
regis:Registro):Registro

Figura A.9.4. Diagrama para la clase Registro

Figura A.9.4. Diagrama para la clase Registro


Soluciones a los ejercicios propuestos 337

Fichero_directo
# fichero:RandomAccessFile
# registro:Registro
# l_registro:int

+ Fichero_directo (nombre:String, modo:String)


+ pon_registro (regis:Registro):void
+ salta_registro (nregistro:int):void
+lee_registro ( ):Registro
+ cerrar_ fichero ( ):void

Figura A.9.5. Diagrama para la clase Fichero_directo

IMPLEMENTACIÓN
El código correspondiente a las dos clases anteriores es el siguiente,

import java.io.*;

/* Clase que representa un registro con tres campos*/

class Registro {
protected int cuenta;
protected String nombre;
protected double saldo;
protected int l_registro=44;

public Registro(int cuenta, String nombre, double saldo) {


this.cuenta=cuenta;
this.nombre=nombre;
this.saldo=saldo;
} // Fin método constructor

public int cuenta(){


return cuenta;
} // Fin método cuenta

public String nombre(){


return nombre;
} // Fin método nombre

public double saldo(){


return saldo;
} // Fin método saldo

public int l_registro(){


return l_registro;
} // Fin método l_registro

public Registro lee_registro(RandomAccessFile fichero) {


try {
cuenta=fichero.readInt();
338 Introducción a la programación con orientación a objetos

nombre=fichero.readUTF();
saldo=fichero.readDouble();
}
catch(IOException e) {
System.out.println(“Error leyendo registro”);
e.toString();
}
return this; // Se devuelve el presente objeto
// de clase registro
} // Fin método lee_registro(versión para fichero directo)

public void pon_registro(RandomAccessFile fichero,


Registro regis) {
try {
String cadena;
fichero.writeInt(regis.cuenta());
fichero.writeUTF(regis.nombre());
fichero.writeDouble(regis.saldo());
}
catch(IOException e) {
System.out.println(“Error escribiendo registro”);
e.toString();
}
} // Fin método pon_registro

} // Fin clase Registro

/* Clase que representa un fichero de acceso directo */

class Fichero_directo {
protected RandomAccessFile fichero;
protected int l_registro=0;
protected Registro registro=new Registro(0, “”, 0.0);

public Fichero_directo(String nombre, String modo){


try{
fichero=new RandomAccessFile(nombre, modo);
l_registro=registro.l_registro();
}
catch(IOException e) {
System.out.println(“Error de entrada/salida”);
e.toString();
}
} // Fin método constructor

public void pon_registro(Registro regis) {


registro.pon_registro(fichero, regis);
} // Fin método pon_registro

public void salta_registro(int nregistro) {


Soluciones a los ejercicios propuestos 339

try{
fichero.seek((nregistro-1)*l_registro);
}
catch(EOFException e) {
System.out.println(“Ese registro no existe”);
e.toString();
}
catch(IOException e) {
System.out.println(“Error de entrada salida”);
e.toString();
}

} // Fin método salta_registro

public Registro lee_registro() {


return registro.lee_registro(fichero);
} // Fin método lee_registro

public void cerrar_fichero() {


try{
fichero.close();
}
catch(IOException e) {
System.out.println(“Error de entrada salida”);
e.toString();
}

} // Fin método cerrar_fichero

} // Fin clase Fichero_directo

Como ejemplo de uso de la estructura de clases anterior tenemos el siguiente programa,

/***************************************************************
Programa que ilustra el uso de un programa de acceso directo
organizado en registros de longitud:
4 bytes(int) + 32 bytes (30 caracteres UTF+2
+8 bytes (double)=44
***************************************************************/

class FicheroAleatorio {
public static void main(String [] args) throws IOException {
int fin, longitud, n, cont=0;
double saldo;
char si_no;
char [] aux =new char[30];
boolean seguir=true;
String nombre_in;
Registro registro;

Fichero_directo fichero= new Fichero_directo(args[0], “rw”);


340 Introducción a la programación con orientación a objetos

BufferedReader leer= new BufferedReader(new InputStreamReader


(System.in));

while(seguir) {
cont++;
System.out.println(“Introduzca nombre del cliente”);
nombre_in=leer.readLine();

// Creando una cadena con 30 caracteres


fin=30;
longitud=nombre_in.length();
if (longitud < 30){
fin=nombre_in.length();
}
for (int i=0; i<fin; i++) {
aux[i]=nombre_in.charAt(i);
}
if (longitud < 30) {
for (int i=fin; i<30;i++)
aux[i]=’ ‘;
}

String nombre= new String(aux, 0, 30);

System.out.println(“Introduzca saldo del cliente”);


saldo=Double.parseDouble(leer.readLine());
registro=new Registro(cont, nombre, saldo);
System.out.println(registro.cuenta()+” “+registro.nombre()
+” “+registro.saldo());
fichero.pon_registro(registro);

System.out.println(“Desea introducir otro cliente(s/n)?”);


si_no=leer.readLine().charAt(0);

while (si_no != ‘s’ && si_no != ‘n’) {


System.out.println(“Debe introducir s (si) o n (no)”);
si_no=leer.readLine().charAt(0);
}
if (si_no == ‘n’){
seguir=false;
}
}

// Búsqueda de un registro

System.out.println(“Que cuenta desea consultar?”);


n=Integer.parseInt(leer.readLine());
fichero.salta_registro(n);
registro=fichero.lee_registro();
System.out.println(registro.cuenta()+” “+registro.nombre()
+” “+registro.saldo());

fichero.cerrar_fichero();
Soluciones a los ejercicios propuestos 341

}//Fin de main
}//Fin clase FicheroAleatorio

CAPÍTULO 10. ORDENACIÓN Y BÚSQUEDA

Ejercicio 1

ANÁLISIS Y DISEÑO
Es posible reformular la solución iterativa habitual del método de ordenación por selección para dar-
le forma recursiva. La idea es eliminar el primero de los bucles, el que va recorriendo los elementos.
Almacenando en la variable fin el número de elementos de la lista, el pseudocódigo para el método
sería el siguiente,

Inicio
Leer lista, inicio, fin
Si (inicio ½ fin) entonces
indice_min i inicio
Para i i inicio+1 mientras i< fin incremento i i i+1
Si (lista (i) < lista (indice_min)) entonces
indice_min i i
Fin_Si
Fin_Para

aux i lista (inicio)


lista (inicio) i lista (indice_min)
lista (indice_min) i aux
Llamar al propio método con lista, inicio+1, fin
Fin_Si
Fin

IMPLEMENTACIÓN
La traducción a Java del algoritmo anterior se muestra a continuación,
public static void seleccion(int [] lista, int inicio,
int fin) {
int aux, indice_min;
if (inicio != fin) {
indice_min=inicio;
for (int i=inicio+1; i<fin; i++) {
if (lista[i]<lista[indice_min]) {
indice_min=i;
}
}
aux=lista[inicio];
lista[inicio]=lista[indice_min];
lista[indice_min]=aux;
seleccion(lista, inicio+1, fin);
}
}

Ejercicio 2
342 Introducción a la programación con orientación a objetos

ANÁLISIS Y DISEÑO
La búsqueda lineal se puede formular de forma recursiva. Para ello, distingamos en primer lugar los
casos base e inductivo,
a) Casos base
– Hemos acabado de recorrer la lista sin encontrar el elemento clave.
– Hemos encontrado el elemento clave.
b) Caso inductivo
– Si no hemos encontrado el elemento clave en la posición actual buscamos si estáen la posi-
ción siguiente.
Si usamos 21 como valor centinela para indicar que el elemento clave buscado no estáen la lista,
y si la variable fin almacena el índice del último elemento, el pseudocódigo para el método sería,
Inicio
Leer lista, inicio, fin, clave
Si (lista (inicio)=clave) entonces
valor i inicio
Si_no
Si (inicio=fin) entonces
valor i -1
Si_no
valor i Resultado del propio método con lista, inicio+1,
fin, clave
Fin_Si
Fin_Si
Devolver valor
Fin

IMPLEMENTACIÓN
El código en Java quedaría de la forma siguiente,
public static int secuencial(int [] lista, int inicio,
int fin, int clave){
int valor;

if (lista[inicio]==clave) {
valor = inicio;
}
else {
if (fin==inicio) {
valor=-1;
}
else {
valor =secuencial(lista, inicio+1, fin, clave);
}
}

return valor;

} //Fin método búsqueda secuencial recursiva

Ejercicio 3
Soluciones a los ejercicios propuestos 343

ANÁLISIS Y DISEÑO
La búsqueda binaria admite una versión recursiva. Consideremos el problema. Como siempre, dis-
tingamos el caso base (casos bases aquí) del caso inductivo:

a) Casos base
– Hemos encontrado la clave en la lista.
– No quedan elementos en la lista.

b) Caso inductivo
– Se repite la búsqueda binaria sobre la mitad de la lista en la que puede estar la clave.

Con la información anterior, el pseudocódigo del algoritmo recursivo sería la siguiente,

Inicio
Leer lista, clave, derecha, izquierda
posicion i Parte entera de (derecha+izquierda)/2

Si izquierda > derecha


valor i -1
Si_no
Si clave = lista (posicion)
valor i posicion
Si_no
Si clave > lista(posicion)
valoribúsqueda binaria con lista, clave, derecha y posicion+1
Si_no
valoribúsqueda binaria con lista, clave, posicion+1 e
izquierda
Fin_Si
Fin_Si
Fin_Si
Devuelve valor
Fin

IMPLEMENTACIÓN
Un método en Java que implementa este algoritmo se presenta a continuación,

public static int binaria_rec(int[] lista, int clave,


int derecha, int izquierda) {

int valor;
int posicion = (izquierda + derecha) / 2;
if (izquierda > derecha) { // clave no encontrada
valor= -1;
}
else {
if (clave == lista[posicion]) { // clave encontrada
valor=posicion;
}
else {
if (clave > lista[posicion]) {
valor= binaria_rec(lista, clave, derecha, posicion+1);
}
else {
344 Introducción a la programación con orientación a objetos

valor= binaria_rec(lista, clave, posicion-1, izquierda);


}
}
}

return valor;
} // Fin método de búsqueda binaria_rec

Tenemos un caso base con dos posibilidades. Así, la recursión se para cuando se cruzan los lími-
tes izquierda y derecha, lo cual significa que hemos realizado una búsqueda exhaustiva sin encon-
trar el elemento, o cuando se ha encontrado el elemento buscado. Los criterios, como vemos, son los
mismos que para la búsqueda binaria simple.
Si no estamos en el caso base, el método ejecutaráuna de las dos llamadas recursivas, dependien-
do de la mitad de la lista en la que busquemos. El valor buscado se compara con el valor del medio y
los límites de la búsqueda se modifican con los parámetros que se pasan al método. De manera especí-
fica, indice+1 se usa como nuevo valor izquierda si el valor buscado es mayor que el valor del
medio. Si el valor buscado es menor se usa indice-1 como nuevo valor derecha.
B

Prácticas y casos
de estudio propuestos

Sumario

Prácticas propuestas Casos de estudio


Parte I. Programación estructurada y modular Parte I. Programación estructurada y modular
Parte II. Programación Orientada a Objetos Parte II. Programación Orientada a Objetos
344 Introducción a la programación con orientación a objetos

En este apéndice se presentan una serie de ejercicios de mayor entidad que los propuestos en los capí-
tulos. Estos ejercicios son susceptibles de ser utilizados como base para una serie de prácticas orien-
tadas a la aplicación de los conceptos presentados a lo largo de los capítulos del texto. Los ejercicios
están pensados para permitir el trabajo personal, por lo que sólo se dan unas sugerencias orientativas
sobre la solución más sencilla. También se incluye al final de este apartado una propuesta de casos de
estudio orientados a que el lector se familiarice con las actividades propias del desarrollo de un pro-
yecto software.
De acuerdo a la organización del libro, las prácticas y los casos de estudio propuestos se han divi-
dido en dos partes. La primera considera ejercicios relacionados con la programación estructurada y
modular mientras que la segunda se centra en la programación orientada a objetos.

PRÁCTICAS PROPUESTAS

PARTE I. PROGRAMACIÓN ESTRUCTURADA Y MODULAR


PRÁCTICA 1. Implemente un programa que ilustre el funcionamiento de los operadores de divi-
sión. Para ello el programa debe realizar el cociente de dos números enteros, de un entero con un
número real y por último calcular el resto de un cociente.
PRÁCTICA 2. Construya un programa que devuelva los distintos números primos existentes des-
de el 1 hasta un número entero introducido como parámetro por la línea de órdenes. Sugerencia: Use
un bucle para recorrer todos los números considerados y otro anidado con el anterior, que para un
número dado, vaya comprobando si el resto del cociente con los anteriores (en realidad basta con mirar
hasta la mitad de los anteriores) es o no cero.
PRÁCTICA 3: Desarrolle un programa que calcule las soluciones reales de una ecuación de segun-
do grado introduciendo los coeficientes reales o enteros por la línea de órdenes. Sugerencia: Use un
if para distinguir el caso de discriminante negativo.
PRÁCTICA 4. Desarrolle un programa que determine el precio de un billete de ida y vuelta en tren
o en autobús. Se sabe que: a) el precio es directamente proporcional a la distancia a recorrer, b) si el
número de días de la estancia es superior a 7 y la distancia superior a 800 kilómetros el billete tiene
una reducción del 30% si se viaja en tren y del 40% si se viaja en autobús, c) el precio del kilómetro
es de 0,06 euros.
PRÁCTICA 5. Utilizando las propiedades y características de la programación modular, diseñe un
programa que simule el funcionamiento de una sencilla calculadora con un conjunto básico de opera-
ciones. El programa consistirá básicamente en la presentación de un menú al usuario en el que se le
indicarán las operaciones de las que dispone. Éstas serán: suma, resta, multiplicación, división y poten-
cia de 2 operandos, que podrán ser números enteros o reales. Además, existirá una opción dentro del
menú para finalizar el programa. Si la opción que introduce el usuario no es la de finalización, el pro-
grama le solicitará los datos necesarios para realizar la operación elegida. El programa debe controlar
la introducción errónea de datos, mostrando un mensaje de error cuando esto se produzca y ofrecien-
do al usuario la posibilidad de que vuelva a introducir los datos correctamente.
PRÁCTICA 6. Construya un programa que obtenga el término n de la serie de Fibonacci. La serie
de Fibonacci es una secuencia de enteros, cada uno de los cuales es la suma de los dos anteriores. Los
dos primeros números de la secuencia son 0 y 1. La serie se define como:

Fibonacci(n) 5 Fibonacci(n21)+Fibonacci(n22) para todo n . 1


Fibonacci 5 n para n # 1
Prácticas y casos de estudio propuestos 345

Úsese un método iterativo para calcular dicho término. Sugerencia: Use un bucle que se recorra
hasta el valor del término (n) y dos variables que vayan almacenando los valores último y penúltimo
de la serie para evaluar el nuevo término de la misma.

PRÁCTICA 7. Construya un programa que calcule los valores de e(x), cos(x) y sen(x) a partir de las
series de Taylor (es una expansión alrededor del punto x=0) siguientes:

n
xi
ex 5 6 } }
i50 i !

n
x2i
cos(x) 5 6 (21)i } }
i50 (2 i)!

n
x2i 1 1
sen(x) 5 6 (21)i }}
i50 (2 i 1 1)!

El número de términos de la serie, n, será el suficiente para que la diferencia absoluta entre dos
valores sucesivos, para n-1 y n, sea menor de 1023. Use un método para cada caso, imprimiendo los
distintos resultados en el método principal. Sugerencia: Use un bucle para simular el sumatorio.

PRÁCTICA 8. Diseñe un programa iterativo que informe si una cadena es un palíndromo (una
cadena es un palíndromo si se lee igual de izquierda a derecha que de derecha a izquierda). Sugeren-
cia: Use el método charAt(int indice), el cual devuelve el carácter que se encuentra en la posi-
ción dada por indice, para ir comprobando si el carácter inicial y el final de la cadena son iguales.
Use un bucle para irse desplazando hacia el centro de la cadena.

PRÁCTICA 9. Construya un programa que obtenga la matriz suma de dos matrices, a (con dimen-
siones m y m) y b (con dimensiones p y q). Sugerencia: Use un método para leer las dos matrices, otro
para sumarlas y otro para mostrarlas. Tenga en cuenta que si c es la matriz suma, sus elementos se
obtienen como c(i, j) 5 a(i, j) 1 b(i, j). Use un bucle para cada dimensión.

PRÁCTICA 10. Reutilizando los métodos que implementó en el anterior ejercicio para leer y mos-
trar matrices implemente un programa que obtenga la matriz producto de dos matrices, a (con dimen-
siones m y n) y b (con dimensiones p y q). Sugerencia: Tenga en cuenta que si c es la matriz producto,
sus elementos se obtienen como,
n
c(i, j) 5 6 a(i, k) · b(k, j)
k
Use tres bucles, uno para realizar el sumatorio sobre k y otros dos para recorrer las dimensiones i, j.

PRÁCTICA 11. Diseñe un programa que indique si una palabra es un palíndromo usando un méto-
do recursivo. Sugerencia: Contraste si el carácter inicial y final de la cadena son iguales y, si lo son,
continúe la exploración eliminando de la cadena el primer y último carácter con ayuda del método
substring() de la clase String.

PRÁCTICA 12. Construya un programa recursivo que obtenga el término n de la serie de Fibo-
nacci. La serie de Fibonacci es una secuencia de enteros, cada uno de los cuales es la suma de los dos
anteriores. Los dos primeros números de la secuencia son 0 y 1. La serie se define como:
346 Introducción a la programación con orientación a objetos

Fibonacci(n) = Fibonacci(n21) 1 Fibonacci(n 2 2) para todo n . 1


Fibonacci 5 n para n # 1

Usando varios valores de n, compare la eficiencia del programa actual con la versión iterativa de
la Práctica 6.

PRÁCTICA 13. Desarrolle un programa que, por medio de un método recursivo, determine el
número de dígitos de un número entero positivo o negativo. Por ejemplo, al recibir el número 100
debería devolver 3 y si se le pasa el número 1 devolvería 1. Sugerencia: Vaya sumando uno mientras
el número se vaya dividiendo por diez y de un resultado mayor o igual a 10 en valor absoluto.

PARTE II. PROGRAMACIÓN ORIENTADA A OBJETOS

PRÁCTICA 14. Desarrolle un programa que permita representar una serie de alumnos. Para cada alum-
no se debe poder especificar su nombre, número de DNI y recoger la calificación obtenida en los dos
parciales que se hacen en el curso. El programa debe evaluar la nota media de cada estudiante y la nota
media promedio de todos los estudiantes del curso. Sugerencia: Implemente una clase Alumno que
contenga las características especificas de cada alumno. Además, codifique una clase Curso donde
una de sus variables de ejemplar sea una matriz de alumnos. En esta clase se debe calcular la nota
media del curso.

PRÁCTICA 15. Construya un programa que permita realizar algunas operaciones sobre los núme-
ros racionales. El programa aceptará dos números racionales definidos por su numerador y denomina-
dor y devolverá su suma, producto y cociente. El programa también debe devolver el cociente de los
dos números racionales leídos. Deberá darse formato a la salida, imprimiendo tanto los números racio-
nales de entrada como los de salida. Sugerencia: Al crear la clase Racional tenga en cuenta que los
métodos suma, producto y cociente sólo necesitan un parámetro, el otro número racional.

PRÁCTICA 16. Dada una matriz monodimensional de enteros, desarrolle un programa que sume
los elementos de la matriz comprendidos entre dos posiciones que se introducirán por teclado. El
programa debe capturar las excepciones que se puedan producir, tales como introducción de núme-
ros no enteros como índices, índices introducidos fuera del intervalo de la matriz, error en la entra-
da-salida y error que se puede producir si el primer índice introducido es mayor que el segundo.
Puesto que la última excepción no existe, deberá crearla para poder capturarla. Sugerencia: Use una
sentencia try-catch para capturar la excepción nueva que debe definir y las excepciones del siste-
ma NumberFormatException, ArrayIndexOutOfBoundsException e IOException. Cree
una clase para la nueva excepción.

PRÁCTICA 17. Desarrolle la estructura de clases necesaria para un sistema de cobro de peaje de
camiones en una autopista. Los camiones llegan hasta una cabina de peaje donde se determinan el
número de ejes y el tonelaje a partir de los datos técnicos del vehículo. Los camiones deben pagar
5 euros por eje más 10 euros por tonelada de peso total del camión. En la cabina de peaje se emite un
recibo y una pantalla muestra la cantidad total correspondiente a los recibos de peaje cobrados, así
como el número total de camiones que han pasado. Sugerencia: Defina una clase camión y una clase
cabina de peaje.

PRÁCTICA 18. Desarrolle un programa que permita hacer algunas operaciones sobre círculos y
rectángulos. Estas manipulaciones consisten en determinar la posición x e y de la figura en un sistema
de coordenadas cartesianas, mover la figura a otra posición x e y, así como redimensionar la figura.
Prácticas y casos de estudio propuestos 347

Además, se quiere calcular el área de cada una de las figuras y, en el caso del rectángulo, intercambiar
altura por anchura. También se pretende que cada figura responda al mensaje toString() devol-
viendo una descripción de sus atributos. Sugerencia: Cree una clase abstracta de donde hereden la cla-
se Círculo y Rectángulo los atributos y los métodos que tengan en común. Tenga en cuenta qué
métodos deben ser abstractos y cuáles no.

PRÁCTICA 19. Declare una clase VehiculoMotorizado que sirva como clase padre para vehí-
culos de tipo Motocicleta, Automóvil y Camión. Todos los vehículos poseen un fabricante, modelo,
año de fabricación y kilometraje. Los automóviles son de distintos estilos y las motocicletas se dedi-
can a usos determinados. A su vez, los camiones pueden tener uno o varios remolques y tienen un nivel
de seguridad, dependiendo de si sobrepasan o no el número máximo de pasajeros autorizados. Cree
también una interfaz llamada CapacidadLimite implementada por las clases Automóvil y Camión.
Esta interfaz debe incluir constantes que indiquen el límite de pasajeros admitidos en automóviles y
camiones. Los límites para automóviles deben incluir el límite de pasajeros para automóviles norma-
les y para furgonetas. Con esta estructura de clases escriba un programa principal que usando una refe-
rencia polimórfica construya un objeto de clase Automóvil, Motocicleta o Camión según
decisión del usuario. El programa deberá imprimir la información del vehículo considerado. Sugeren-
cia: Utilice el método toString().

PRÁCTICA 20. Construya un programa que registre sobre un fichero una serie de clientes de un
banco. Para cada cliente se debe especificar el número de cuenta (que será el número de orden del
cliente), el nombre (con un máximo de 30 caracteres), y el saldo actual. El programa debe permitir
consultar directamente los datos del cliente de una cuenta determinada y actualizar su saldo. El nom-
bre del fichero se debe introducir por línea de órdenes. Sugerencia: Cree una clase registro que repre-
sente los datos de los clientes y las operaciones sobre ellos. Cree además, una clase fichero que
contenga un método para insertar un registro en una determinada posición, otro para leer un registro y
otro para saltar a un determinado registro del fichero.

CASOS DE ESTUDIO

PARTE I. PROGRAMACIÓN ESTRUCTURADA Y MODULAR

Caso 1. Sistema informático para la gestión de reservas de billetes


de avión
Ante la inminente creación del aeropuerto de Ciudad Real, se desea desarrollar el sistema informático
de gestión de reservas de billetes para los vuelos de los aviones de la nueva compañía AeroLíneas
Manchegas. Los datos que hay que tener en cuenta para establecer la reserva de un asiento son los
siguientes:

a) En cada avión de la compañía habrá N asientos de 1.ª clase y M de 2.ª clase, siendo N , M.
b) En 1.ª clase, habrá 4 asientos por fila (2 de ventanilla y 2 de pasillo)
c) En 2.ª clase, habrá 6 asientos por fila (2 de ventanilla, 2 centrales y 2 de pasillo)

En ambas clases, las primeras filas correspondientes a los dos tercios del número total de asientos
de dichas clases, son para los NO FUMADORES y el tercio restante para los FUMADORES.
Cuando un cliente desee solicitar la reserva de un billete, tendrá la posibilidad de elegir clase, sec-
ción de fumador o no fumador y posición en la fila (ventana, central o pasillo). Las prioridades para
seleccionar un asiento son las siguientes: clase, fumador y posición. Es decir, si un cliente desea un
348 Introducción a la programación con orientación a objetos

billete en una clase, en NO FUMADOR y en ventanilla, pero ya no quedan billetes de esas carac-
terísticas, se le consultará si desea en pasillo, si lo hubiera; en caso contrario, se le ofrecería la posibi-
lidad de sentarse en FUMADOR y, si no, cambiar de clase. Una vez encontrado el asiento que el
cliente desea se le dará la posibilidad de anular o confirmar. En este último caso, el asiento quedará
reservado. El sistema indicará qué asiento ha sido reservado y ya no se le podrá asignar a otro cliente
en ese mismo vuelo. En caso de que no haya billetes con las características que desea el usuario, se le
indicará que tendrá que esperar al siguiente vuelo. Si desea anular, el asiento queda disponible para
otro posible cliente. Cuando el vuelo esté completo, el sistema debe indicarlo. Si un cliente desea
reservar más de un asiento, se intentará que estén juntos y, si no, lo más cercanos posible.
Los valores de N y M se introducirán desde la línea de órdenes al llamar al programa. Se debe con-
trolar la entrada de datos incorrecta, en cuyo caso el programa no finalizará su ejecución sino que ofre-
cerá la posibilidad de introducir nuevamente los datos.

Caso 2. Juego de las tres en raya


Implemente un juego entre dos jugadores sobre un tablero teniendo en cuenta los siguientes requisitos:
a) El juego consiste en colocar, por turnos, una serie de fichas sobre un tablero de 3 3 3. Cada
jugador dispone de 5 fichas de un mismo color. Las fichas pueden situarse en cualquier posi-
ción, y gana el jugador que logra poner 3 fichas alineadas (tres en raya), vertical, horizontal o
diagonalmente. Cada jugada consiste en situar una ficha en una posición libre; una vez situa-
da la ficha, ésta no puede moverse en el transcurso de la partida. La partida puede terminar
con la victoria de un jugador o en tablas, situación en la que ningún jugador logra alinear tres
fichas. El usuario, a partir de ahora el contrincante, jugará contra el ordenador.
b) Tanto el nombre del contrincante como la información de quién comienza la primera partida
debe suministrarse desde el teclado, no por línea de órdenes. En partidas sucesivas (jugadas
consecutivamente) el jugador inicial se irá alternando. El nombre del contrincante se mostrará
por pantalla al solicitar una jugada (fila y columna) y al informar del resultado de la partida
(hay un ganador o se producen tablas). El juego continúa, partida tras partida, hasta que el con-
trincante indique que desea dejar de jugar (se recomienda utilizar un menú de opciones que
solicite la pulsación de una tecla al finalizar cada partida). Se debe controlar la entrada de
datos incorrecta, en cuyo caso el programa no finalizará su ejecución sino que ofrecerá la posi-
bilidad de introducir nuevamente los datos por teclado. En particular, se deberá controlar que
las coordenadas introducidas para indicar la casilla a la que el contrincante desea mover la
ficha estén entre los límites aceptables.
c) El estado del tablero después de cada jugada debe “dibujarse” utilizando caracteres ASCII,
representando las fichas de cada jugador con caracteres diferentes. La jugada (fila y columna)
que realiza el ordenador se generará de manera aleatoria (lógicamente tiene que ser una posi-
ción vacía). A tal efecto se recomienda el método random()de la clase Math. Incluya un
método que devuelva un valor lógico indicando si la casilla central está libre en cuyo caso el
ordenador colocará la ficha allí. Es opcional la posibilidad de dotar al ordenador de una mejor
estrategia de juego para ganar la partida. Incluya un método que compruebe si el contrincan-
te va a hacer tres en raya, es decir, que el contrincante tenga dos fichas en la misma línea. El
método devolverá los valores i, j de la casilla libre de la línea donde el contrincante ya tiene
las dos fichas. El ordenador colocará la siguiente ficha en esa posición para evitar que el con-
trincante consiga tres en raya. El algoritmo deberá usar bucles for para comprobar si existen
dos fichas en la misma columna o fila, y comprobar aparte si hay dos fichas del contrincante
en diagonal. Para saber si después de un movimiento la partida termina porque se produce una
situación de “tres en raya”, debe utilizarse un método que tome como argumento el tablero
(habrá que decidir con qué estructura de datos se implementa el mismo) y devuelva verdade-
ro, si hay tres en raya, o falso, en caso contrario.
Prácticas y casos de estudio propuestos 349

d) Al finalizar el programa se debe mostrar una estadística que informe del número total de par-
tidas jugadas, del número de victorias del contrincante, del número de victorias del ordenador
y del número de partidas en tablas. Como conclusión debe mostrarse un mensaje que informe
del jugador que mejor juega.

PARTE II. PROGRAMACIÓN ORIENTADA A OBJETOS

Caso 3. Gestión de una biblioteca


Desarrolle un programa para gestionar una biblioteca. Básicamente el programa debe permitir catalo-
gar las nuevas incorporaciones y consultar las existentes. Las publicaciones catalogadas pueden ser
periódicas y no periódicas. De momento las publicaciones periódicas sólo incorporan revistas pero
está previsto en un futuro incorporar otras publicaciones. El sistema debe recoger esta estructura. Las
publicaciones no periódicas actualmente corresponden a libros y a trabajos de investigación, que pue-
den ser informes técnicos o tesis doctorales. Para cada uno de estos elementos se debe especificar
como mínimo en la catalogación:

a) Revistas: Título; Volumen; Número; Fecha de edición(día, mes y año); Materia


b) Libros: Autor/es; Título; Editorial; Edición; Año de publicación; ISBN; Materia
c) Informes técnicos: Autor/es; Título; Departamento donde se realiza el trabajo; Fecha (mes y
año) de publicación; Materia
d) Tesis doctorales: Autor (1 sólo); Director; Tutor; Título; Departamento de realización del tra-
bajo; Departamento de presentación; Materia; Calificación

En todos los casos debe quedar reflejado el número de ejemplares de un mismo elemento que se
catalogan. Bajo consulta, el sistema debe poder determinar independientemente: el número total de
ejemplares catalogados; el número de publicaciones periódicas y no periódicas; el número total de
libros o de trabajos de investigación; el número de informes técnicos; el número de tesis doctorales.
El sistema también debe poder indicar el número de publicaciones (sin especificar) que corresponden
a una materia dada.
Diseñe e implemente este sistema usando una aproximación orientada a objetos, haciendo uso apro-
piado de las características de encapsulación, herencia y polimorfismo. En particular, se debe usar una
única estructura de datos para representar todas las publicaciones. Sugerencia: Establezca una jerarquía
de clases por herencia e implemente el programa usando una matriz de referencias polimórficas. En una
segunda versión, implemente el programa usando ficheros. No utilice la clase Vector de Java.

Caso 4. Tienda de informática


Desarrolle un programa que permita gestionar una tienda, especializada en productos hardware, de
acuerdo a los siguientes requisitos:

a) La tienda vende sólo algunos componentes hardware, clasificados en dispositivos de almace-


namiento (discos duros, disqueteras, lectores de CDROM y grabadoras de CDROM) y com-
ponentes no montables (monitores, teclados, altavoces, ratones e impresoras).
b) Todos los elementos de la tienda poseen una marca, un modelo, una referencia para la tienda
y se compran a un distribuidor por un precio determinado.
c) El precio de venta al público vendrá determinado por un incremento porcentual, fijado por el
dueño y constante para todos los productos, sobre el precio de compra al distribuidor.
350 Introducción a la programación con orientación a objetos

d) Cada producto de la tienda tiene sus propias características (los discos duros su capacidad, los
monitores su tamaño, etc.). Nota: Añada a cada producto las características que considere
oportunas.
e) Debemos controlar las existencias de los productos y las ventas realizadas, siendo necesario
avisar cuando las existencias queden por debajo de un margen establecido para cada pro-
ducto.
f) Debemos controlar los datos personales de cada cliente, a los que se emitirá una factura cuan-
do realicen una compra.
g) Las facturas presentarán por pantalla los datos del producto comprado, los del cliente y el total
que se debe abonar.

El programa que se pide debe permitir, mediante el uso de menús:

a) Insertar y actualizar la información de los productos y de los clientes.


b) Almacenar en ficheros la información referente a los productos y a los clientes.
c) Permitir al propietario determinar el incremento del precio final de los productos, así como
establecer el mínimo de existencias al que hace referencia el apartado e) anterior.
d) Gestionar la venta de componentes, realizando todas las operaciones necesarias sobre la tien-
da y sobre los clientes (descontar los productos vendidos y emitir factura, simplemente visua-
lizándola por pantalla).
e) Listar un catálogo de productos con su descripción técnica y su precio final.
f) Listar los productos cuyas existencias sean iguales o inferiores al mínimo permitido.

Diseñe e implemente este sistema usando una aproximación orientada a objetos, haciendo un uso
apropiado de las características de encapsulación, herencia y polimorfismo no utilizando la clase
Vector de Java.
C

Resumen de la notación UML

Sumario

La vista estática Relaciones entre clases


Diagramas de clases A) Relación de generalización o herencia
Clases y objetos B) Relación de asociación
A) Clases C) Relación de dependencia
B) Objetos
352 Introducción a la programación con orientación a objetos

El lenguaje unificado de modelado o UML (Unified Modeling Language) es un sucesor de los dife-
rentes métodos de análisis y diseño orientados a objetos que aparecieron en la década de los años
ochenta y principios de los noventa del siglo XX. En palabras de sus autores (Rumbaugh et al.,
2000) UML es un lenguaje gráfico de modelado que sirve para la especificación, visualización,
construcción y documentación de los elementos de un sistema software. En resumen, UML es un
lenguaje de modelado de propósito general, pero no un método de modelado específico. Dicho de
otra forma, UML define las herramientas a usar pero no cómo usarlas para obtener un diseño. Hoy
por hoy UML ha devenido en un estándar para el modelado de sistemas orientados a objetos. UML
provee de gran cantidad de herramientas de modelado como los diagramas de casos de uso, de cla-
se, de interacción, de estado, de actividad o físicos. Lógicamente, la exposición de estos conceptos
cae fuera de un texto introductorio como éste. Este apartado recoge un resumen elemental de nota-
ción UML apropiada al nivel de conocimientos considerados en este texto. Esencialmente lo que se
presenta es el punto de vista estático a través de los diagramas de clase. El objetivo es dotar al lec-
tor de los conocimientos básicos que le permitan empezar a aprovechar la potencia de UML y fami-
liarizarle con su uso desde un primer momento. El lector interesado en un mayor nivel de detalle
puede consultar las referencias genéricas sobre UML (Rumbaugh et al., 2000; Booch et al., 2000)
o referencias más específicas sobre el uso del mismo (Fowler&Scott, 2000; Larman, 1999; Oeste-
reich, 1999).

LA VISTA ESTÁTICA
UML puede considerarse organizado en varias áreas, que a su vez se subdividen en las denomina-
das vistas, a las que corresponden los distintos diagramas (Rumbaugh et al., 2000). Dentro de las
áreas, una vista se puede definir como un subconjunto de UML que modela un aspecto concreto del
sistema bajo estudio. Una de las áreas consideradas es la estructural, donde se describen los ele-
mentos del sistema y sus relaciones. Una de las vistas dentro del área estructural es la vista estáti-
ca, que modela los conceptos tanto del dominio del problema como de la solución. Esta vista se
considera estática porque no muestra la evolución del sistema a lo largo del tiempo. En esencia se
trata de modelar las clases y sus relaciones, y la herramienta usada son los diagramas de clase. Estos
diagramas son los que se han introducido a lo largo del texto y son los que se consideran aquí con
un poco más de detalle.

DIAGRAMAS DE CLASES
Los diagramas de clase son una herramienta de gran importancia en cualquier metodología orientada
a objetos y UML no es una excepción. Un diagrama de clases describe las diferentes entidades (cla-
ses) del sistema, así como sus relaciones estáticas. Un diagrama de clases también muestra los com-
ponentes (miembros) de cada clase.

CLASES Y OBJETOS

A) CLASES
Las clases se representan como rectángulos y quedan identificadas por un nombre que las distingue
unas de otras. Es posible representar una clase sólo con su nombre o indicando el denominado nom-
bre de camino o ruta que corresponde al nombre de la clase precedido por el nombre del paquete en el
Resumen de la notación UML 353

Nombre_de_la_clase

Paquete::Nombre_de_la_clase

Figura C.1. Representación simple de clases

que se encuentra, tal y como se muestra en la Figura C.1.


Es posible representar los atributos y los procedimientos correspondientes a la clase. A tal efecto
se divide el rectángulo en tres secciones con el nombre de la clase en la primera, los atributos en la

Nombre_de_la_clase

Atributos

Procedimientos

Figura C.2. Representación de los atributos y procedimientos de una clase

segunda y los procedimientos en la tercera, ver Figura C.2.


En los atributos se puede especificar el tipo de cada uno e incluso un valor inicial usando la sin-
taxis, atributo: tipo=valor_inicial. En los procedimientos se puede especificar sólo el nombre o bien
su firma (nombre, parámetros aceptados y el tipo de los mismos) y también el tipo de retorno del pro-
cedimiento. En este caso, la sintaxis es similar a la de los atributos por ejemplo, actualizar (valor: dou-

Cliente

nombre:String
número:int
saldo:double=0.0

ActualizarSaldo (valor:double):void
daSaldo ( ):double
daNúmero ( ):int
daNombre ( ):String

Figura C.3. Caracterización de atributos y procedimientos


354 Introducción a la programación con orientación a objetos

ble): double. Como ejemplo, véase la Figura C.3.


Los miembros de una clase que se puedan considerar globales a la misma, tal como los miembros
estáticos (static) en Java, se representan subrayados en el diagrama de clase, tal y como ilustra la

Nombre clase

atributo estático

Figura C.4. Representación de miembros estáticos de clase

Figura C.4.
La visibilidad de los miembros de una clase se indica con los símbolos 1 , # y 2 para representar
visibilidad pública, protegida o privada. Estos conceptos se relacionan con los modificadores public,
protected y private de Java. Los símbolos anteriores se usan precediendo al identificador del

Nombre_de_la_clase

+ Atributo público
# Atributo protegido
- Atributo privado

+ Procedimiento público
# Procedimiento protegido
- Procedimiento privado

Figura C.5. Representación de la visibilidad de los miembros de una clase

miembro de la clase considerado, véase la Figura C.5.


Es interesante poder representar también las clases abstractas como parte del proceso de mode-
lado. UML lo permite y a tal efecto se usan diagramas de clase donde el nombre de la clase se indi-
ca en cursiva. En las situaciones donde el uso de la cursiva no es posible (como sobre una pizarra)
puede resultar útil indicar que la clase es abstracta por medio de un valor etiquetado. Estrictamen-
te hablando, un valor etiquetado corresponde a un elemento de información sobre una entidad y
consiste en un identificador que indica una propiedad de la entidad, un signo de igual y el valor que
dicha propiedad adquiere. El valor etiquetado se coloca entre llaves ({}). Para indicar una clase abs-
tracta podemos simplemente asociar el valor etiquetado {abstracta} al nombre de la clase, véase la
Figura C.6.

B) OBJETOS

Los objetos, entendidos como ejemplares de clase, se representan con un rectángulo, como las clases,
con la salvedad de que el nombre debe estar subrayado. El nombre de la clase a la que pertenece el
Resumen de la notación UML 355

Nombre_de_la_clase
{abstracta}
Atributos
Procedimientos

Figura C.6. Representación de una clase abstracta

PrimerCliente:Cliente

Figura C.7. Representación de un objeto llamado PrimerCliente perteneciente a a clase Cliente

objeto se puede indicar con la sintaxis, nombre objeto:nombre_de_la_clase, vease la Figura C.7.
El estado del objeto, entendido como el contenido real de los atributos en un momento dado, se
puede representar indicando explícitamente cuál es el valor de los atributos del objeto.

RELACIONES ENTRE CLASES


A la hora de construir un modelo de un sistema es importante no sólo identificar las clases presentes
o necesarias, sino también sus relaciones. En un modelo orientado a objetos podemos distinguir tres
tipos de relaciones, la de generalización o herencia, la de asociación y la de dependencia.

A) RELACIÓN DE GENERALIZACIÓN O HERENCIA


La generalización o herencia es una relación taxonómica entre clases. Una de las clases representa el
conjunto general y otra de ellas es un subconjunto específico de esa clase general. La primera de las
clases se denomina clase padre o antecesora y la segunda es la clase hija o descendiente. En UML esta
relación se simboliza por medio de una flecha, con la punta hueca, que apunta hacia la clase padre. Un

Clase Padre

Clase Hija1 Clase Hija2

Figura C.8. Notación para la relación de generalización o herencia


356 Introducción a la programación con orientación a objetos

ejemplo de la notación se muestra en la Figura C.8.

B) RELACIÓN DE ASOCIACIÓN
Esta relación se definiría como una relación estructural entre ejemplares de la misma o diferentes cla-

Clase A Clase B

Figura C.9. Representación básica de la relación de asociación

ses. La idea básica es que uno de los ejemplares contiene ejemplares (en realidad referencias a ejem-
plares) de la otra clase. La asociación se representa con una línea continua que conecta las clases a las
que pertenecen los objetos involucrados, como se muestra en la Figura C.9.
El número de ejemplares involucrados en la relación de asociación (multiplicidad) se indica con
el siguiente convenio. Un número específico de ejemplares se indica explícitamente, colocando dicho
valor numérico al lado de la clase correspondiente. Si el valor está indeterminado se indica con un
asterisco, *. Finalmente, si lo que tenemos es un intervalo de valores posibles esto se indica como

1 1..*
Clase A Clase B

Figura C.10. Multiplicidad en una relación de asociación. Un ejemplar de Clase A contiene


un mínimo de uno y un máximo indeterminado de ejemplares de Clase B

”valor mínimo..valor máximo”. Por ejemplo, si tenemos un mínimo de uno y un máximo de 4 la nota-
ción sería 1..4. Cualquier otra situación se construye como combinación de estas reglas básicas, véa-
se la Figura C.10.
Frecuentemente, una relación de asociación implica que desde un ejemplar de una clase se puede
acceder a ejemplares de la otra, pero no al revés. Un ejemplo sería que en la primera clase existieran
como atributos referencias a ejemplares de la segunda clase. Esta situación define la “navegación” o
“navegabilidad” de la asociación (en inglés navigability). Si se desea indicar explícitamente la nave-
gabilidad de la asociación, se incluye una cabeza de flecha en la línea continua que representa la aso-

Clase A Clase B

Figura C.11. Representación de la “navegabilidad” de una relación de asociación


Resumen de la notación UML 357

ciación y que va en dirección a la clase a cuyos ejemplares se puede acceder. Así, si desde los ejem-
plares de la Clase A se puede acceder a ejemplares de la Clase B el diagrama sería el mostrado en la
Figura C.11.
En la asociación normal las dos clases relacionadas están al mismo nivel conceptual, es decir, la
relación entre ellas existe por la naturaleza específica del sistema considerado. Una relación de aso-
ciación especial es la agregación. En la agregación existe una relación conceptual todo-parte entre las
clases. Una de las clases (la agregada) corresponde al todo y la otra a las partes. Cuando se da esta

Clase A Clase B

Figura C.12. Relación de agregación entre la Clase A y la Clase B

situación, se puede indicar específicamente incluyendo un rombo vacío sobre la línea continua de la
relación al lado de la clase agregada. Así, si cualquier ejemplar de la Clase A está formado por ejem-
plares de la Clase B el diagrama correspondiente sería el mostrado en la Figura C.12.
Existen casos en los que la relación de agregación es más estrecha, de tal forma que la existencia
de los ejemplares de las partes depende de la existencia del ejemplar del todo. Un ejemplo es el de un
ejemplar de clase factura que contenga varios elementos de clase Producto Facturado. Si desaparece

Clase A Clase B

Figura C.13. Relación de composición entre la Clase A y la Clase B

la factura desaparecen sus productos facturados, no pueden existir sin ella. Este tipo de asociación se
denomina composición y se representa igual que la agregación pero con un rombo negro, tal y como
se muestra en la Figura C.13.

C) RELACIÓN DE DEPENDENCIA

Clase A Clase B

Figura C.14. Relación de dependencia entre la Clase A y Clase B

La relación de dependencia es una relación de uso o utilización, donde un elemento de una clase
usa elementos de otra, de tal forma que la variación del estado de los elementos usados implica la
358 Introducción a la programación con orientación a objetos

variación del estado del elemento que los usa. Esta relación se indica con una flecha de trazo discon-
tinuo, donde la cabeza de la flecha apunta hacia la clase a la que pertenecen los ejemplares usados. Por
ejemplo, si la Clase A tiene una dependencia de uso de la Clase B el diagrama correspondiente sería
el mostrado en la Figura C.14.
Un ejemplo típico donde se aprecia la relación de dependencia lo tenemos cuando una clase posee
algún procedimiento (método en Java) que acepta como parámetro un ejemplar de otra clase.

REFERENCIAS
BOOCH, G., RUMBAUGH, J. y JACOBSON, I.: El Lenguaje Unificado de Modelado, Addison-Wesley, Primera reim-
presión, 2000.
FOWLER, M. y SCOTT, K.: UML Distilled, Addison-Wesley, Second Edition, 2000.
LARMAN. C.: UML y Patrones, Prentice-Hall, 1999.
OESTEREICH, B.: Developing software with UML, Addison-Wesley, 1999.
RUMBAUGH, J., JACOBSON, I. y BOOCH G.: El Lenguaje Unificado de Modelado. Manual de Referencia, Addison-
Wesley, 2000.
D

Guía de estilo en Java

Sumario

Formato de líneas Declaraciones


Ficheros Sentencias de control
Clases Documentación
Visibilidad de miembros de clase
Identificadores
360 Introducción a la programación con orientación a objetos

Este apéndice proporciona unas indicaciones sobre cómo llevar a cabo la codificación en Java. Sien-
do un lenguaje de formato totalmente libre, cualquier estilo de codificación es posible. El hecho de
abordar desarrollos software en equipo, o simplemente de intercambiar software entre programadores,
implica que el uso de un estilo uniforme, comprensible para todos, sea de gran ayuda. Por otro lado,
si tenemos en cuenta que un sistema software emplea la mayor parte de su existencia en labores de
mantenimiento y que dicho mantenimiento no se realiza normalmente por la misma o las mismas per-
sonas que desarrollaron el sistema, un estilo estandarizado es una ayuda invaluable. En resumen, un
estilo estandarizado actúa como un vehículo que permite la transmisión fiable de información, es en
este caso, del significado del código. Este estilo de codificación implica no sólo el formato de las sen-
tencias ejecutables del programa, sino también de los comentarios.
Respecto al lenguaje Java, la guía aquí propuesta está basada en las recomendaciones oficiales de
Sun (Sun, 2002). Guías similares pueden encontrarse en otros textos (Lewis y Loftus, 1998) o en Inter-
net (Javaranch, 2002).

FORMATO DE LÍNEAS
1. No usar más de 80 caracteres por línea (imagen de tarjeta). De esta forma se pueden visuali-
zar las líneas completas con un editor de texto o en una hoja impresa tamaño DIN A4.
2. Cuando la línea sea mayor de 80 caracteres, divídala en varias partes, cada una sobre una línea.
Salte de línea al final de una coma o al final de un operador. Si se trata de una expresión con
paréntesis salte, si es posible, a línea nueva después de finalizar el paréntesis. Por ejemplo,
casos válidos serían,

public void metodoHaceAlgo(int valor1, double valor2,


int valor3){

resultado = aux* (final-inicial+desplazamiento)


+ referencia;

3. Use líneas en blanco como elemento de separación entre bloques de código conceptualmente
diferentes.
4. Sangre adecuadamente cada nuevo bloque de sentencias. Entre dos y cuatro espacios en blan-
co son suficientes para cada nivel de sangrado. De esta forma se aprecia visualmente la dife-
rencia de nivel entre bloques de sentencias, sin rebasar, normalmente, los 80 caracteres por
línea. A la hora de sangrar, y para evitar problemas de compatibilidad entre editores de texto,
use espacios y no tabuladores. Por ejemplo,

public double calcularDescuento (double total) {


int aux=0; // Se sangra dos espacios
if (total>LIMITE) {
total = total * 0.9; // Se sangra otros dos espacios
}
return total;
}

FICHEROS
1. Incluya una sola clase o interfaz por fichero, o al menos una sola clase o interfaz pública.
2. Si hay comentarios globales para los contenidos del fichero colóquelos en primer lugar.
Guía de estilo en Java 361

3. Si la clase forma parte de un paquete, la sentencia package deber ser la primera del fichero.
4. Si se importan paquetes, la sentencia import debe aparecer después de la sentencia packa-
ge.
5. Coloque la declaración de las clases o interfaces a continuación de la sentencia package.

CLASES
1. Coloque en primer lugar los comentarios sobre la clase (vea el apartado comentarios).
2. Coloque los atributos (datos) a continuación. Coloque primero las variables estáticas y a con-
tinuación las variables de ejemplar en el orden: público (se recomienda no incluir variables
públicas), protegidas y privadas, es decir, public, protected y private en Java.
3. A continuación, se declaran los métodos, con los constructores en primer lugar. El resto de
métodos se colocará en orden de interrelación y no por visibilidad. Así, si un método público
usa dos métodos privados, se debería colocar primero el público seguido de los dos privados.
La idea es que al leer el código se siga con facilidad la funcionalidad de los métodos. Las reco-
mendaciones de los dos últimos puntos se resumirían de la forma siguiente,

class NombreClase {
variables estáticas
variables de ejemplar públicas (debe evitarse su uso)
variables de ejemplar protegidas
variables de ejemplar privadas
métodos constructores
resto de métodos
}

4. No deje espacio en blanco entre el identificador del método y los paréntesis, es decir, escriba,

public void nombreMétodo(int valor1, double valor2)

en lugar de,

public void nombreMétodo (int valor1, double valor2)

obsérvese el espacio en blanco delante de la apertura de paréntesis.

VISIBILIDAD DE MIEMBROS DE CLASE


1. Los atributos (datos) deben declararse privados (private) excepto los que se pretenda que
sean accesibles por herencia que deben ser protegidos (protected). Se debe evitar el uso de
datos públicos.
2. Los procedimientos (métodos) de la interfaz pública deben declararse public, los de sopor-
te privados (private).

IDENTIFICADORES
1. Escoja identificadores significativos y a ser posible breves. En cualquier caso prefiera la cla-
ridad a la brevedad. Por ejemplo, es preferible el identificador,
362 Introducción a la programación con orientación a objetos

integralDefinida

que el de,

intdef

2. Para clases e interfaces escoja como identificador un sustantivo. Use minúsculas excepto para
la letra inicial. Si el identificador consta de varias palabras colóquelas juntas, con la inicial de
cada una en mayúsculas. Por ejemplo, serían identificadores apropiados,

class Cliente
class ClientePreferencial

3. Para las variables y objetos utilice identificadores en minúsculas. Si el identificador consta de


varias palabras se colocan separadas por el carácter de subrayado o bien todas seguidas. Es
este último caso, la primera de ellas se escribe en minúsculas y la inicial de las demás en
mayúsculas. Por ejemplo,

double valorTotal=0.0;
int cantidadInicial=0;
String nombre_cliente=”Alfonso”;

4. Para las constantes, el identificador debe usarse en mayúsculas. Si el identificador consta de


varias palabras se separan con el carácter de subrayado. Ejemplo correctos serían,

final int MAXIMO=10;


final double PRECISION=0.001;
final double ANCHURA_TOTAL=29.5;

5. En el caso de los métodos, el identificador debe ser preferentemente un verbo y debe usarse
en minúsculas. Si el identificador contiene varias palabras, la inicial de todas las posteriores a
la primera va en mayúsculas. Ejemplos válidos serían,

public void incorporar(double cantidad){


- - - cuerpo del método - - -
}
public double obtenerTotal(){
- - - cuerpo del método - - -
}

6. Para el identificador de los paquetes se recomienda usar minúsculas.

DECLARACIONES

1. Declare las variables al principio del bloque en el que se vayan a utilizar. En los métodos,
declare las variables al principio del método. Intente inicializar todas las variables que se
declaren.
2. En relación con el punto anterior, si en los bucles no se precisa que alguna variable exista antes
o después del mismo, declárela dentro del bucle.
3. Declare las matrices con los corchetes al lado del nombre del tipo y no del identificador, es
decir, con la sintaxis,
Guía de estilo en Java 363

int [] lista;

y no como,

int lista [];

SENTENCIAS DE CONTROL
1. No use nunca saltos incondicionales. Más concretamente, no utilice la sentencia break excep-
to en la sentencia switch, donde es forzoso hacerlo. Nunca use la sentencia continue.
2. No use más de un return en cada método y coloque éste al final del método.
3. Coloque la apertura de bloque ({ ) al final de la línea inicial de la sentencia. El fin de bloque
( } ) colóquelo en línea aparte y alineado con el principio de la sentencia. Si la sentencia de
control es un if-else la cláusula else debe comenzar en la línea siguiente al fin del bloque
de la cláusula if. Por ejemplo, escriba,

if (i==j) {
valor=1;
}
else {
valor=2;
}

en lugar de,

if (i==j)
{
valor=1;
} else
{
valor=2;
}
4. Ponga cada nuevo bloque de sentencias entre llaves aunque conste de una sola sentencia. Por
ejemplo, escriba

if (i==j) {
valor=1;
}
else {
valor=2;
}
y no,

if (i==j)
valor=1;
else
valor=2;

5. Una recomendación muy frecuente es la siguiente. En los if anidados si un if interno corres-


ponde a una cláusula else externa se coloca el if a continuación y en la misma línea que el
else. El bloque de este nuevo if se cierra al nivel del else anterior. Por ejemplo,
364 Introducción a la programación con orientación a objetos

if (valor1==0) {
resultado=total;
} else if (valor1>10) {
resultado = total*0.9;
} else {
resultado = total*0.95;
}

Aunque ésta es una recomendación muy extendida que intenta que los if se interpreten en
cascada, aquí recomendamos el formato obtenido al aplicar las normas del Apartado 3 de esta
sección, que para el ejemplo anterior produciría el siguiente resultado,

if (valor1 == 0){
resultado=total;
}
else {
if (valor1 > 10) {
resultado = total*0.9;
}
else {
resultado = total*0.95;
}
}

Este formato permite identificar el alcance de las distintas estructuras if-else por el nivel de
sangrado de las sentencias.

DOCUMENTACIÓN
1. Documente el código. No se trata de sustituir la información de análisis y diseño, pero la docu-
mentación interna es un complemento que puede servir como guía del sistema en el peor de
los casos, cuando no hay otra documentación disponible.
2. Como cabecera de una clase incluya como información el autor o autores, la fecha de la últi-
ma modificación, el propósito general de la clase y una breve explicación de los datos y méto-
dos incorporados.
3. Como cabecera de los métodos cuya acción no sea evidente indique el autor o autores, la fecha
de la última modificación, el propósito del método, el significado de los parámetros formales,
el significado de la información devuelta con return y las excepciones que se capturen.
4. En el cuerpo de los métodos use comentarios para indicar el propósito de las tareas que no
sean evidentes, tales como algoritmos específicos. En cualquier caso aumente la legibilidad
del código usando identificadores significativos para variables o métodos.

REFERENCIAS
Javaranch: http://www.javaranch.com/style.jsp última visita realizada en junio de 2002.
LEWIS J. y LOFTUS W., Java Software Solutions, Addison-Wesley, 1998.
Sun: http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html última visita realizada en junio de 2002.
E
Interfaces gráficas de usuario

Sumario

Componentes AWT y Swing Programación dirigida por sucesos


366 Introducción a la programación con orientación a objetos

En la actualidad las interfaces gráficas de usuario han devenido en el método estándar que el usuario
utiliza para interaccionar con una herramienta software. Por esta razón, Java presenta una serie de cla-
ses agrupadas en paquetes que permiten la creación de interfaces gráficas de usuario (o IGU), de for-
ma independiente de la plataforma. Para la construcción de una IGU necesitamos aplicar tanto un
punto de vista estático como uno dinámico. El punto de vista estático corresponde a la selección y dis-
tribución de los elementos gráficos que comporta la interfaz, tales como botones o ventanas. El punto
de vista dinámico se refiere a cómo, a través de estos componentes, se puede llevar a cabo la ejecu-
ción del programa usando la denominada programación dirigida o conducida por sucesos 1. En este
apéndice se describen brevemente los dos grupos de herramientas gráficas que Java ofrece para la
construcción de interfaces y la programación conducida por sucesos. Para una visión más profunda y
detallada del desarrollo de interfaces gráficas de usuario bajo Java el lector interesado puede consul-
tar cualquier texto sobre el lenguaje (Eckel, 2002; Savitch, 2001; Froufe, 2000; Geary, 1999; Winder
y Roberts, 2000). Pasemos a continuación a presentar los componentes (punto de vista estático) y la
programación dirigida por sucesos.

COMPONENTES AWT Y SWING


En este apartado se describen brevemente los dos grupos de herramientas gráficas que Java ofrece para
la construcción de interfaces. Se trata de los componentes AWT (Abstract Window Toolkit), también
llamados componentes pesados, y los componentes ligeros, conocidos generalmente como compo-
nentes Swing. Vamos a considerarlos por separado.

COMPONENTES AWT
El AWT representa una biblioteca de clases con un amplio conjunto de clases y métodos que permiten
la creación y gestión de ventanas, administración de tipos de letra, texto de salida y gráficos, tanto en
applets como en otros entornos gráficos de interfaz de usuario, por ejemplo sistemas de ventanas. El
AWT es la biblioteca original implementada por Java para la creación de interfaces gráficas de usuario.
El paquete java.awt, que está organizado de manera jerárquica, contiene todas las clases del AWT.

Jerarquía de clases
La idea fundamental para construir una interfaz de usuario en Java es considerar que una ventana es
un conjunto de componentes anidados. Dicha anidación crea una jerarquía de componentes que va des-
de la ventana general de la pantalla hasta, por ejemplo, el botón más pequeño. Los componentes prin-
cipales que se pueden usar son:

a) Los contenedores (la clase container)


Son componentes AWT que pueden incluir a otros componentes. Los contenedores (como muestra la
Figura E.1) heredan de la clase abstracta Component que encapsula los atributos de los componentes
visuales. Todos los elementos de una interfaz de usuario son subclases de la clase Component, la cual
contiene más de cien métodos que se encargan tanto de la gestión de sucesos (eventos), como de la
entrada por teclado o por ratón, o de repintar una ventana.

1
En inglés, Event Driven Programming.
Interfaces gráficas de usuario 367

Object

Component

Container

Window Panel

Frame

Figura E.1. Jerarquía básica de clases del AWT

Un ejemplo de subclase de Container es la clase Panel, la cual no añade nuevos métodos sino
que implementa los que hereda de la clase padre, que es abstracta. Un objeto de la clase Panel es un
componente de pantalla, concretamente una ventana que no contiene ni barra de títulos, ni bordes, ni
barra de menús. Para añadir componentes sobre un objeto de la clase Panel se utiliza un método here-
dado de la clase Container llamado add(). En relación con las applets de Java (que se conside-
rarán en el apéndice F de este texto) comentaremos que la clase Panel es la superclase de la clase
Applet. Siempre que la salida por pantalla se dirige a una applet, se dibuja sobre un objeto de la cla-
se Panel.
Otra clase que hereda de Container es la clase Window, veáse la Figura E.1, que crea ventanas
que se sitúan directamente sobre el escritorio. Generalmente, no se crean objetos de esta clase sino de
la clase que hereda de ella, definida como clase Frame, y que representa lo que normalmente se
entiende por ventana. Una ventana de esta clase posee una barra de título y una barra de menú, además
de bordes y esquinas para cambiar de tamaño.

b) Componentes de interfaz de usuario


Estos componentes también heredan de la clase Component. Ejemplos de este tipo de componente
son los botones, las casillas de verificación, las etiquetas, las barras de desplazamiento o las listas.
Aunque no forma parte de la jerarquía de ventanas que se muestra en la Figura E.1 conviene seña-
lar que existe otro tipo de ventanas llamado Canvas. Una ventana Canvas representa una ventana
vacía sobre la que se puede dibujar.

COMPONENTES SWING
En la versión 1.2 de Java y posteriores, aparte de las clases AWT, se ha introducido otro conjunto de
368 Introducción a la programación con orientación a objetos

clases que proporcionan componentes más potentes y flexibles que los AWT originales. Éstos reciben
el nombre de componentes Swing. Los componentes Swing frente a los componentes AWT (llamados
componentes pesados) presentan la ventaja de ser independientes de la plataforma, por eso reciben el
nombre de componentes ligeros. Los componentes Swing son más numerosos que los AWT (por ejem-
plo, fichas, paneles con scroll, árboles), algunos de los cuales se describen a continuación.
La clase padre de casi todos los componentes Swing es la clase JComponent que es una especia-
lización de la clase abstracta java.awt.Component. Las clases relacionadas con Swing se encuen-
tran en el paquete javax.swing.
Los componentes Swing se pueden anidar unos dentro de otros, y casi todos permiten que se les
incluya una pequeña imagen gráfica o icono. Además, facilitan la creación de ayudas asociadas a los
componentes (ayudas contextuales), y se puede mejorar el aspecto de los componentes añadiendo un
borde visible. Por estas razones, permiten crear interfaces más atractivas y con mayor funcionalidad
que los componentes AWT.
A continuación, se describen algunos de los componentes Swing. Muchos de ellos reciben el mis-
mo nombre que su correspondiente componente AWT, pero con la diferencia de que los Swing
comienzan por una “J” mayúscula.

La clase JFrame
Hereda de la clase Frame. La clase JFrame se utiliza para crear la ventana principal de una aplica-
ción, veáse la Figura E.2. Esta ventana incluye los controles habituales de cambio de tamaño y cierre.
Las ventanas JFrame poseen un panel raíz (de la clase JRootPanel) que gestiona el interior de la
ventana. La forma estándar de proporcionar el comportamiento de la ventana es mediante un gestor u
oyente de sucesos.

La clase JPanel
JPanel es un contenedor básico que sirve para agrupar a otros componentes. Normalmente se utiliza
para dividir una zona de pantalla en secciones. A cada sección se le puede aplicar un diseño diferen-
te. Si se desea crear un panel con barra de desplazamiento se puede usar la clase JscrollPanel.

La clase JLabel
Esta clase implementa una etiqueta que puede contener una cadena de texto, un icono o ambas cosas,
véase la Figura E.2. Una etiqueta puede mostrarse en una ventana o en cualquier contenedor. El texto
que muestra no es editable por el usuario. El uso más frecuente de las etiquetas es dar un nombre a
otros componentes de la interfaz gráfica. Una novedad introducida en los Swing es que en una etiqueta
se pueden escribir varias líneas, con distinto formato y fuente. Los constructores de la clase JLabel
son:

JLabel(String s, Icon i, int linea). Se indica el texto inicial de la etiqueta, el icono y la


alineación horizontal del contenido en la etiqueta, respectivamente.
JLabel(String s). Inserta sólo el texto inicial.
JLabel(String s, int i). Inserta el texto inicial y la posición del mismo.
JLabel(Icono i). Inserta un icono en la etiqueta.
JLabel(). Crea una etiqueta sin texto.

Para insertar texto en la etiqueta se usa el método:


void setText(String s)

Para obtener el texto de una etiqueta se utiliza el método:


String getText()

La clase JButton
Esta clase añade un botón gráfico que el usuario puede utilizar (mediante el ratón o teclado) para inte-
Interfaces gráficas de usuario 369

JFrame

JTextField JButton JLabel


Figura E.2. Ejemplo de JFrame, J Button, JLabel y JTextField

raccionar con el sistema, véase la Figura E.2. En todos los botones se puede incorporar además de tex-
to un icono, e incluso se pueden asignar varios iconos a un mismo botón para indicar los distintos esta-
dos en los que puede estar un botón. Algunos de sus constructores son:

JButton(Icon i). Inserta un icono en el botón.


JButton(String s). Inserta un texto en el botón.
JButton(String s, Icon i). Inserta un texto y un icono en el botón.

La clase JRadioButton
Implementa los botones de opción que son una especialización de un botón con estado (los posibles esta-
dos son botón seleccionado o no seleccionado). La principal característica de este tipo de botones es que
son excluyentes. En un grupo de botones de opción sólo uno puede estar seleccionado. La representación
gráfica del botón seleccionado difiere del resto de botones. Como ejemplo véase la Figura E.3.
Con el objetivo de conseguir la exclusión mutua, los botones se agrupan en un objeto de la clase
ButtonGroup. Esta agrupación indica al usuario que los botones están relacionados. Normalmente se
incluyen en un contendedor tipo panel con algún tipo de borde. Algunos ejemplos de constructores de
la clase JRadioButton son:

JRadioButton(Icon i). Inserta un icono en el botón.


JRadioButton(Icon i, boolean estado). Igual que el anterior pero además indica si el botón

Figura E.3. Ejemplo de JRadioButton


370 Introducción a la programación con orientación a objetos

está seleccionado.
JRadioButton(String s). Inserta el texto asociado al botón.
JRadioButton(String s, Icon i, boolean estado). Inserta un texto asociado al botón,
un icono e indica si el botón estará seleccionado inicialmente.

La clase JCheckBox
Esta clase ofrece las características operativas de una casilla de verificación, donde las casillas tie-
nen dos estados, seleccionado o no seleccionado, que normalmente se denota por una cruz o marca de
selección. Las casillas de verificación se utilizan cuando se quiere que el usuario decida si elegir o no
una opción. Si hay varias casillas, todas pueden estar seleccionadas, es decir, no son excluyentes.
Como ilustración véase la Figura E.4.
Algunos de sus constructores se muestran a continuación:

JCheckBox(Icon i). Inserta un icono en la casilla de verificación.


JCheckBox(Icon i, boolean estado). Igual que el anterior pero además indica si la casilla de
verificación esta inicialmente seleccionada.
JCheckBox(String s). Inserta el texto asociado a la casilla de verificación.
JCheckBox(String s, Icon i). Similar al anterior pero indicando el icono que se quiere inser-
tar en la casilla.
JCheckBox(String s, Icon i, boolean estado). Se indica el texto, el icono y el estado de
la casilla.

La clase JComboBox
Dicha clase implementa una caja, llamada normalmente caja combo, que es una combinación de un
campo de texto y una lista desplegable. Una caja combo muestra una selección por defecto. Si el usua-
rio desea elegir otra opción, puede desplegar la lista o incluso teclear la selección en el campo de tex-
to. A continuación, se indican dos de sus constructores.

JComboBox(). Es el constructor por defecto.


JComboBox(Vector v). Este constructor necesita un objeto de la clase Vector que contenga
los elementos que deben aparecer en la lista del combo.

La clase JTable
En una tabla, los datos se distribuyen en filas y columnas. La clase JTable permite la implementa-
ción de tablas. Un constructor común de la clase JTable es:

Figura E.4. Ejemplo de JCheckBox


Interfaces gráficas de usuario 371

JTable(Object datos[][], Object colTitulos[])

donde datos es una matriz (array) bidimensional que contiene la información de la tabla. El objeto
colTitulos contiene los títulos de las columnas.

La clase JMenuBar
Implementa una barra de menús que habitualmente se añade a un panel raíz. Normalmente, después
de crear una barra de menú se le añaden los menús desplegables usando la clase JMenu. El construc-
tor de la clase JMenuBar no necesita parámetros. El método add(JMenu) permite añadir un menú
desplegable a una barra de menús.

La clase JTextComponent
Proporciona las funcionalidades de trabajo con texto, permitiendo seleccionar, copiar, cortar y pegar
el texto. Algunos de los métodos más usados de esta clase son:

getSelectedText(). Devuelve el texto seleccionado por el usuario.


cut(). Corta el texto seleccionado y lo coloca en el portapapeles del sistema.
copy(). Copia el texto seleccionado y lo coloca en el portapapeles del sistema.
paste(). Pega el texto contenido en el portapapeles en la posición seleccionada.
getText(). Devuelve el texto contenido en un componente.
setText(String s). Inserta texto en un componente.

La clase JTextField
Es una subclase de la clase JTextComponent, por lo que hereda sus métodos. La clase JTextField
permite mostrar y editar una línea de texto, veáse la Figura E.2. Generalmente, se utiliza para solici-
tar al usuario entradas breves. Su constructor es:

JTextField(String s, int ancho), donde se indica el contenido inicial y la anchura del com-
ponente en número de caracteres, respectivamente.

La clase JTextArea
Permite mostrar y editar varias líneas de texto sencillo en el que sólo se puede utilizar un tipo de letra.
Algunos de sus constructores son:

JTextArea(). Constructor por defecto.


JTextArea(String s). Inserta un texto inicial.
JTextArea(String s, int filas, int columnas). Inserta un texto inicial y además indica
el número de filas y columnas que va a tener el componente.

ADMINISTRADORES DE ORGANIZACIÓN
Los gestores o administradores de organización (también llamados de disposición o diseño) posicio-
nan de forma automática los componentes dentro de un contenedor. La apariencia de una ventana
depende del gestor que se esté utilizando. También es posible colocar manualmente los componentes
dentro de una ventana. Sin embargo, resulta mucho más cómodo usar un gestor que determine automá-
ticamente la forma y posición de cada componente. El gestor deseado se establece con el método,

void setLayout (LayoutManager layoutObjeto)

donde layoutObjeto es una referencia a un gestor de organización. Si no se invoca a este método se


372 Introducción a la programación con orientación a objetos

utiliza el gestor por defecto. Si se prefiere no usar un gestor porque se desea colocar los componentes
manualmente layaoutObjeto debe ser null. A continuación, se describen algunos de los gestores exis-
tentes:

FlowLayout es el gestor de organización por defecto. Organiza un componente uno detrás de otro (de
izquierda a derecha y de arriba hacia abajo).
BoderLayout distribuye los componentes en cinco zonas: norte, sur, este y oeste y deja una zona
central pare el resto de los componentes.
CardLayout tiene varias posibilidades de organización. Permite manejar fichas o tarjetas de tal forma
que sólo una esté visible cada vez y ocupe todo el área.

PROGRAMACIÓN DIRIGIDA POR SUCESOS


Hasta ahora hemos presentado los componentes de una IGU pero, ¿cómo funciona una IGU? La mane-
ra de funcionar es diferente de los programas que hemos presentado hasta ahora. En los programas vis-
tos, las sentencias se van ejecutando una tras otra en un orden determinado. De hecho, la filosofía
básica es que hay una sola entidad (el computador) que va ejecutando las instrucciones. En una IGU
no es éste el modelo aplicado sino el denominado de “programación dirigida o conducida por suce-
sos”. En este modelo se crean entidades (objetos) que representan los sucesos de interés (como hacer
un click con el ratón, o moverlo, o pulsar una tecla). De hecho, estos objetos se pueden considerar
como los encargados de disparar el suceso cuando éste se produzca para que el sistema lo reconozca.
¿Cómo sabe el sistema cuándo se ha producido un suceso? Porque construimos entidades (objetos) que
actúan como gestores (u oyentes) de sucesos (event listeners). Estos objetos entran en juego cuando se
produce el suceso al que están asociados. Como podemos ver, aquí la evolución temporal del progra-
ma depende de los sucesos que se produzcan, de forma que el mismo programa se comporta de mane-
ra diferente si la serie de sucesos que experimenta son diferentes.
Para gestionar un suceso se deben seguir los siguientes pasos: Primero definir una clase Liste-
ner (oyente, receptora) para una determinada clase de suceso. Dicha clase debe implementar la inter-
faz receptora que corresponda al suceso que queramos gestionar. El segundo paso sería sobrescribir
los métodos de la interfaz receptora encargados de capturar los sucesos que nos interesan. De esta for-
ma personalizamos el método a nuestras necesidades, ya que indicamos cómo debe comportarse el
programa ante la ocurrencia de un determinado suceso. El tercer paso sería definir en la clase princi-
pal un objeto de la clase receptora, y además registrarla para la notificación de la ocurrencia de suce-
sos generados por los componentes. Además, para que todo funcione correctamente se debe importar
el paquete java.awt.event que es el que contiene la mayoría de las clases e interfaces de sucesos.
A continuación, se muestra un ejemplo, Programa E.1, donde se han indicado los tres pasos ante-
riores. El programa muestra el ejemplo de cálculo de un factorial del Programa 4.6, ahora con una inter-
faz gráfica. En el programa se gestiona un suceso de un componente de clase TextField. Esta clase
dispara un suceso cuando el usuario escribe un número en el área de texto y después pulsa INTRO. Éste
es el suceso que vamos a gestionar. Para ello, creamos una clase nueva llamada MiactionListener,
que representa nuestro receptor de sucesos. El método heredado actionPerformed, de la clase recep-
tora MiactionListener, se ha sobrescrito para que muestre el factorial del número leído cuando se
pulse INTRO.

Programa E.1. Ejemplo de gestión de sucesos

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
Interfaces gráficas de usuario 373

Programa E.1. Ejemplo de gestión de sucesos (continuación)

public class Principal extends Applet{

int numero; //Declaramos la variable donde se va a almacenar


// el número
TextField entrada; //Declaramos el área de texto
Label etiqueta1; //Declaramos la etiqueta que muestra el resultado
double resultado; //Declaramos la variable resultado

//Se inicializan las etiquetas y se añaden a la ventana


public void init(){
etiqueta1=new Label(“Programa Factorial”);
entrada= new TextField(8);
add(entrada);
add(etiqueta1);
}

//Creación y registro un receptor de sucesos para el área de texto


public void start(){
entrada.addActionListener
(new MiactionListener(entrada)); //PASO 3
}

//Método que calcula el factorial


public double factorialIterativo (int n){

int i;
i=0;
double factorial;
factorial=1.0;
do {
i=i+1;
factorial=factorial*i;
} while (i<n);
return factorial;
}

//clase para recibir los sucesos que se produzcan sobre el TextField


// PASO 1

class MiactionListener implements java.awt.event.ActionListener{

TextField otrocampoTexto;
MiactionListener(TextField campoTexto){

otrocampoTexto=campoTexto;
}

public void actionPerformed


374 Introducción a la programación con orientación a objetos

(java.awt.event.ActionEvent evt){ //PASO 2


//Sobreescribo el método para que cuando se introduzca un número
//y se pulse intro se lea el número y se calcule el
//factorial del mismo
numero=Integer.parseInt(otrocampoTexto.getText());
resultado=factorialIterativo(numero);
etiqueta1.setText(“El resutado es “+resultado);
}
}//De la clase MiactionListener

}//De la clase principal

EJEMPLO DE UTILIZACIÓN DE SWINGS


A continuación, se presenta un ejemplo, Programa E.2, donde se utilizan varios de los componentes
anteriormente considerados. El programa implementa una calculadora con un campo de texto donde
introducir los datos y visualizar los resultados. Las distintas operaciones se representan sobre varios
botones. El código que se muestra ha sido generado con un entorno de desarrollo integrado, en inglés
Integrated Development Environment (IDE) el cual facilita mucho la elaboración de interfaces gráfi-
cas ya que basta con elegir el componente a usar entre una paleta de componentes que el IDE mues-
tra. Otra ventaja de usar un IDE es que genera parte del código automáticamente, por ejemplo declara
los controles o escribe parte de las clases y los métodos que gestionan los sucesos. Esta aplicación se
ha realizado con el programa JBuilder, sin embargo existen otros como el VisualCafe o el Visual J++.

Programa E.2. Ejemplo de calculadora simple implementada en una IGU

import java.awt.*;
import javax.swing.*;
//El siguiente paquete se usa para gestionar los sucesos
import java.awt.event.*;

public class Principal {


public static void main(String[] args) {
//Se crea un objeto calculadora, se le asigna un tamaño y
//se hace visible
Calculadora calc = new Calculadora();
calc.setSize(250,150);
calc.setVisible(true);
}
}

class Calculadora extends JFrame {


JPanel jPanel1 = new JPanel(); //Se crea un objeto panel
BorderLayout borderLayout1 = new BorderLayout(); //Se crea un gestor

//Se crea un campo de texto donde se introduciran


//los operandos y el resultado
JTextField txtEntrada = new JTextField();

//Se crea un botón para cada operación


Interfaces gráficas de usuario 375

Programa E.2. Ejemplo de calculadora simple implementada en una IGU (continuación)

JButton btSuma = new JButton();


JButton btResta = new JButton();
JButton btMultiplicacion = new JButton();
JButton btDivision = new JButton();
JButton btResultado = new JButton();
376 Introducción a la programación con orientación a objetos

Programa E.2. Ejemplo de calculadora simple implementada en una IGU (continuación)

//Se crea una etiqueta que servirá para indicar que la


//operación se realizó
JLabel lblResultado = new JLabel();

double operando1, operando2 = 0;


double resultado=0;
int operador=-1;

public Calculadora() {
try{
dibujaVentana();
}
catch (Exception e){
System.out.println(“Se ha producido la exception”+e.toString());
}
}

private void dibujaVentana() throws Exception {


//Asigna el gestor creado al panel
this.getContentPane().setLayout(borderLayout1);
this.setTitle(“Calculadora”);

//Se configura el campo de texto y se asigna un tamaño


txtEntrada.setPreferredSize(new Dimension(200, 19));
txtEntrada.setMinimumSize(new Dimension(200, 19));

//Se indica que el texto esté alineado a la derecha


txtEntrada.setHorizontalAlignment(JTextField.RIGHT);
txtEntrada.setText(“”);

//A continuación se configuran todos los botones


//asignándole una etiqueta, un tipo de letra y
//un receptor (gestor) de sucesos
btSuma.setText(“+”);
btSuma.setFont(new Font(“Dialog”, 1, 18));
btSuma.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
btSuma_actionPerformed(e);
}
});

btResta.setText(“-”);
btResta.setFont(new Font(“Dialog”, 1, 18));
btResta.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
btResta_actionPerformed(e);
}
});

btMultiplicacion.setText(“*”);
btMultiplicacion.setFont(new Font(“Dialog”, 1, 18));
btMultiplicacion.addActionListener
(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
Interfaces gráficas de usuario 377

Programa E.2. Ejemplo de calculadora simple implementada en una IGU (continuación)

btMultiplicacion_actionPerformed(e);
}
});

btDivision.setText(“/”);
btDivision.setFont(new Font(“Dialog”, 1, 18));
btDivision.addActionListener
(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
btDivision_actionPerformed(e);
}
});

btResultado.setText(“=”);
btResultado.setFont(new Font(“Dialog”, 1, 18));
btResultado.addActionListener
(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
btResultado_actionPerformed(e);
}
});

//Se configura una etiqueta


lblResultado.setMaximumSize(new Dimension(500, 30));
lblResultado.setPreferredSize(new Dimension(250, 15));
lblResultado.setMinimumSize(new Dimension(250, 15));
this.getContentPane().add(jPanel1, BorderLayout.CENTER);

//Se añaden los componentes creados al panel


jPanel1.add(txtEntrada, null);
jPanel1.add(btSuma, null);
jPanel1.add(btResta, null);
jPanel1.add(btMultiplicacion, null);
jPanel1.add(btDivision, null);
jPanel1.add(btResultado, null);
jPanel1.add(lblResultado, null);
}

//Los siguientes métodos definen las operaciones de la calculadora


static double sumar(double a1,double a2){
return a1+a2;
}//fin sumar

static double restar(double a1,double a2){


return a1-a2;
}//fin restar

static double multiplicar(double a1,double a2){


return a1*a2;
}//fin multiplicar

static double dividir(double a1,double a2){


if (a2!=0)
return a1/a2;
378 Introducción a la programación con orientación a objetos

Programa E.2. Ejemplo de calculadora simple implementada en una IGU (continuación)

else return 0;
}//fin dividir

//Los siguientes métodos gestionan los sucesos que se produzcan


void btSuma_actionPerformed(ActionEvent e) {
Double aux=new Double(txtEntrada.getText());
operando1=aux.doubleValue();
txtEntrada.setText(“”);
txtEntrada.requestFocus();
operador=1;
lblResultado.setText(“”);

Figura E.5. Interfaz de la calculadora

void btResta_actionPerformed(ActionEvent e) {
Double aux=new Double(txtEntrada.getText());
operando1=aux.doubleValue();
txtEntrada.setText(“”);
txtEntrada.requestFocus();
operador=2;
lblResultado.setText(“”);
}

void btMultiplicacion_actionPerformed(ActionEvent e) {
Double aux=new Double(txtEntrada.getText());
operando1=aux.doubleValue();
txtEntrada.setText(“”);
txtEntrada.requestFocus();
operador=3;
lblResultado.setText(“”);
}

void btDivision_actionPerformed(ActionEvent e) {
Double aux=new Double(txtEntrada.getText());
operando1=aux.doubleValue();
txtEntrada.setText(“”);
txtEntrada.requestFocus();
operador=4;
Interfaces gráficas de usuario 379

lblResultado.setText(“”);
}

void btResultado_actionPerformed(ActionEvent e) {
Double aux=new Double(txtEntrada.getText());
operando2=aux.doubleValue();
switch(operador){
case 1:
resultado=sumar(operando1,operando2);
break;
case 2:
resultado=restar(operando1,operando2);
break;
case 3:
resultado=multiplicar(operando1,operando2);
break;
case 4:
resultado=dividir(operando1,operando2);
break;
}
txtEntrada.setText(resultado+””);
operador=-1;
lblResultado.setText(“ Operación realizada”);
}
}

La Figura E.5 muestra el aspecto final de la interfaz.

REFERENCIAS
ECKEL, B., Piensa en Java, Segunda Edición, Prentice-Hall, 2002.
FROUFE, A., Java 2. Manual de usuario y tutorial. Segunda Edición.Ra-Ma, 2000.
GEARY, D. M., Java. Mastering the JFC. Third Edition. Volume II: Swing. Sun Microsystems. Press 1999.
SAVITCH W., Java An Introduction to Computer Science & Programming, Second Edition, Prentice-Hall, 2001.
WINDER, R. y ROBERTS, G., Developing Java Software, Second Edition, John Wiley & Sons Ltd, 2000.
F

Applets

Sumario

Diferencias entre las aplicaciones y las applets Funcionamiento de las applets


Creación de una applet
380 Introducción a la programación con orientación a objetos

Java permite crear dos tipos de programas: las aplicaciones y las “applets”. Todos los ejemplos que se
han presentado en este libro eran aplicaciones, caracterizadas porque se pueden ejecutar en una com-
putadora usando su sistema operativo. Sin embargo, el lenguaje Java es muy popular por sus applets.
Las applets son aplicaciones diseñadas para ser incluidas en un documento HTML, y que pueden ser
transmitidas por Internet y ejecutadas en un navegador Web compatible con Java 1. En este apéndice
se recogen las diferencias existentes entre las aplicaciones y las applets, se describe cómo construir
applets, la forma en que funcionan y cómo transformar una aplicación en una applet.

DIFERENCIAS ENTRE LAS APLICACIONES Y LAS APPLETS


La primera diferencia es que todas las aplicaciones necesitan un método main para comenzar su eje-
cución. Las applets, en cambio, no lo requieren, puesto que contienen otros métodos que la inicializan
y que se invocan automáticamente. En el último apartado de este apéndice describiremos algunos de
estos métodos y su funcionamiento. La segunda diferencia es que las applets no utilizan para la entra-
da y salida los flujos o streams, como ocurre con las aplicaciones. En su lugar usan la interfaz pro-
porcionada por los AWT (descritos en el apéndice E).
Las applets terminan su ejecución cuando lo hace el visor o la página web en la que se están ejecu-
tando, por esta razón no necesitan mecanismos de parada del programa (por ejemplo, un botón de cierre).

CREACIÓN DE UNA APPLET


Cada applet es una subclase de la clase Applet contenida en el paquete applet. Puesto que una
applet necesita emplear métodos de la clase Applet, hay que importar siempre el paquete applet.
Además, siempre se indica explícitamente que se hereda (extends) de Applet. Con todo esto, las
dos primeras líneas de una applet son:

import java.applet.*;
public class NombreClase extends Applet{
- - - Cuerpo de la clase - - -
}

Como muestra el código anterior la clase de una applet siempre debe ser pública para ser accesi-
ble desde cualquier parte.
El siguiente paso es definir los métodos de la clase. Para mostrar un ejemplo completo escribire-
mos una applet que muestre en una ventana (bien de un navegador o de un visor) la típica frase “Hola
Mundo”, véase el Programa F.1.

Programa F.1. Ejemplo de applet

import java.applet.*;
import java.awt.*;
public class HolaMundo extends Applet{
public void paint (Graphics g){
g.drawString (“Hola Mundo!!!”, 50,50);
}
}

1
Actualmente todos los navegadores incorporan un JVM (Java Virtual Machine) para poder interpretar bytecode y eje-
cutar las applets.
Applets 381

Como se ha indicado anteriormente, las applets utilizan los componentes AWT para la entrada y
salida, por eso se ha importado también el paquete java.awt que contiene la clase Graphics, cuyo
método drawString hemos utilizado para mostrar en la ventana una frase. Los números 50,50 indi-
can las coordenadas donde debe aparecer el texto, medidas desde el ángulo superior izquierdo de la
zona definida por la applet.
El método paint es el que utiliza Java cada vez que actualiza o repinta una applet y cuando la
applet se ejecuta por primera vez. El método paint acepta un parámetro de clase Graphics que des-
cribe el entorno gráfico en el que la applet se ejecuta. Éste se utiliza siempre que una applet tiene que
presentar su salida.
El siguiente paso es la ejecución del código. Lo primero que se debe hacer es compilarlo como si
fuera una aplicación clásica, creando por lo tanto el fichero HolaMundo.class. Después tendremos
que decidir si queremos probar el código en un navegador o en un visor.
Para poder transmitir la applet por Internet y ejecutarla en un navegador, es necesario escribir un
pequeño fichero HTML con una etiqueta <APPLET> tal y como se muestra a continuación:

<APPLET CODE=”HolaMundo.class” WIDTH=300 HEIGHT=300>


</APPLET>

Las sentencias WIDTH (anchura) y HEIGHT (altura) especifican las dimensiones del área de pan-
talla utilizada por la applet. Una vez creado este fichero, se puede ejecutar el navegador y después car-
gar este fichero (esto es, pinchar con el ratón en el enlace vinculado a este fichero o cargar la URL del
fichero), lo que permite la ejecución de la applet HolaMundo. La Figura F.1 muestra el resultado que
se obtiene al ejecutar HolaMundo desde el Microsoft Internet Explorer.
Para ejecutar la applet desde un visor como el appletviewer basta con escribir desde el símbolo del
sistema:

C:\appletviewer HolaMundo.html <Enter>

La Figura F.2 muestra el resultado.

Figura F.1. Resultado de la applet HolaMundo


382 Introducción a la programación con orientación a objetos

Figura F.2. Applet HolaMundo ejecutada desde un visor

FUNCIONAMIENTO DE LAS APPLETS


Una pregunta que puede plantearse es cómo arrancan y terminan las applets, puesto que no tie-
nen método main. La técnica es la siguiente. Las applets proporcionan cuatro métodos: init(),
destroy(), start() y stop(). Estos métodos gestionan los procesos de arranque, detención, reac-
tivación y parada definitiva de la applet. Dichos procesos los gestiona el sistema de ejecución de Java
que se esté usando.
El método init() es similar a un método constructor, ya que proporciona los valores iniciales
para una applet. Se llama cuando la applet se carga por primera vez en el navegador o visualizador.
El método start() se llama después de invocar al método init(). La diferencia entre ambos méto-
dos es que init() sólo se llama una vez durante la vida de la applet. En cambio, start() puede
llamarse varias veces, por ejemplo cada vez que el usuario regrese a la página después de haber visi-
tado otra u otras. Además, start() se utiliza cuando se requieren elementos dinámicos al estable-
cer inicialmente la applet (por ejemplo, en una animación). Después del arranque se invocan el resto
de métodos.
El método stop()se ejecuta cuando la applet deja de ser visible. Al regresar a la página se vuel-
ve a invocar el método start(). El método destroy() libera los recursos reservados para la applet,
por lo que se usa cuando el usuario decide cerrar la applet.
Estos cuatro métodos están definidos en la clase Applet pudiendo el usuario sobrescribirlos según
sus necesidades.
Teniendo en cuenta lo visto hasta ahora estamos en condiciones de convertir una aplicación en una
applet. Para ello, basta recordar tres cosas: en las applets la entrada y salida de datos se realiza a través
de la interfaz del AWT. Segundo, las applets no necesitan mecanismos de parada ya que terminan su
ejecución cuando se cierra la página web o la ventana del visor. Tercero, las applets no requieren un
método main, puesto que su funcionamiento es realizado por el paquete applet, el cual se encarga
entre otras cosas de crear la ventana y establecer su visibilidad.
Como ejemplo de conversión de una aplicación en una applet convertiremos el Programa 4.6 mos-
trado en el Capítulo 4 que calculaba de forma iterativa el factorial de un número que se introducía por
por teclado. El código de dicha aplicación era:
import java.io.*;
class Factorial {
public static void main (String [] args) throws IOException {
int i,n;
double factorial;
BufferedReader leer =new BufferedReader
(new InputStreamReader (System.in));
Applets 383

System.out.println (“Introduzca numero para calcular el ”+


“factorial:”);
n=Integer.parseInt (leer.readLine());
System.out.println (“Numero introducido: “+n); // Eco de
// la entrada

i=0;
factorial=1.0;
do {
i=i+1;
factorial=factorial*i;
} while (i<n);
System.out.println (“Factorial: “+factorial);

} // Fin método
} // Fin clase Factorial

Veamos paso a paso cómo pasar de la aplicación a la applet, para lo cual usaremos algunos con-
ceptos relativos a los componentes AWT.
En la aplicación se lee directamente el número del cual se desea calcular el factorial. Para leer un
texto en la applet debemos usar un componente AWT adecuado. En este caso usaremos un
TextField. Lo mismo ocurre con la salida por pantalla, hay que elegir otro componente, en este
ejemplo hemos utilizado una etiqueta para mostrar el resultado.
Cuando se utiliza un área de texto (TextField) se necesita un receptor de sucesos que detecte
cuándo se pulsa la tecla INTRO. En nuestra applet hemos creado la clase MiactionListener que
será la encargada de recibir y gestionar los sucesos relacionados con el TextField. En esta clase,
además de escribir el método constructor, se ha sobrescrito el método actionPerformed que es el
encargado de capturar el suceso de pulsación de la tecla INTRO. Cuando este suceso sea capturado,
se lee el número que se ha introducido en el TextField y se invoca al método
factorialIterativo, mostrando posteriormente el resultado obtenido.
Es importante resaltar que para que la applet funcione correctamente, el gestor de sucesos debe
ponerse “a escuchar” al comenzar a ejecutarse dicha applet. Por esta razón, dentro del método start
se ha creado el receptor de sucesos del componente TextField. El Programa F.2 muestra el código
obtenido

Programa F.2. Applet que calcula el factorial de un número

import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class Factorial extends Applet{

int numero; //declaramos la variable donde


//se va a almacenar el número
TextField entrada; //declaramos el área de texto
Label etiqueta1; //declaramos la etiqueta que muestra el resultado
double resultado; //declaramos la variable resultado

public void init(){


//Se inicializan las etiquetas y se añaden a la ventana
384 Introducción a la programación con orientación a objetos

Programa F.2. Applet que calcula el factorial de un número (continuación)

etiqueta1=new Label(“Programa Factorial”);


entrada= new TextField(8); // TextField con 8 caracteres
add(entrada);
add(etiqueta1);
}

public void start(){


//Se crea y registra un gestor de sucesos para el área de texto
entrada.addActionListener(new MiactionListener(entrada));
}

//Método que calcula el factorial


public double factorialIterativo (int n){

int i;
i=0;
double factorial;
factorial=1.0;
do {
i=i+1;
factorial=factorial*i;
} while (i<n);
return factorial;
}

//Clase para recibir los sucesos que se produzcan sobre el TextField


class MiactionListener implements java.awt.event.ActionListener{

TextField otrocampoTexto;

MiactionListener(TextField campoTexto){
otrocampoTexto=campoTexto;
}

// Se sobrescribe el método para que cuando se introduzca un número


// y se pulse intro se lea el numero y se calcule el
// factorial del mismo
public void actionPerformed (java.awt.event.ActionEvent evt){

numero=Integer.parseInt(otrocampoTexto.getText());
resultado=factorialIterativo(numero);
etiqueta1.setText(“El resultado es “+resultado);
}
}//Fin de la clase interna MiactionListener

}//Fin de la clase Factorial

La clase MiactionListener es una clase interna (está declarada dentro de otra clase). Para más
información sobre el uso y comportamiento de las clases internas véase Eckel, 2002; Arnold et al.,
2001. El lector interesado en profundizar en el tema del diseño de applets puede consultar Eckel, 2002;
Wu, 2001; Jaworski, 1999.
Applets 385

REFERENCIAS
JAWORSKI, J.: Java 1.2. Al Descubierto, Prentice-Hall, 1999.
ARNOLD, K., GOSLING, J. y HOLMES, D.: El Lenguaje de Programación Java, Tercera Edición, Addison-Wesley,
2001.
ECKEL B.: Piensa en Java, Segunda Edición, Prentice-Hall, 2002.
WU, C. T.: Introducción a la Programación Orientada a Objetos con Java, McGraw-Hill, 2001.

Potrebbero piacerti anche