Vous êtes sur la page 1sur 81

Apuntes de Programación Avanzada

Facultad de Ingeniería
UDEP
Mgtr. Ernesto Guevara A.

9 de marzo de 2007
Índice general
1. Algorítmica 1
1.1. Complejidad algorítmica . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Notación O grande . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1. Propiedades de la notación O grande. . . . . . . . . . . . 2
1.2.2. Órdenes de Complejidad . . . . . . . . . . . . . . . . . . . 3
1.3. Métodos de resolución de problemas . . . . . . . . . . . . . . . . 4
1.3.1. Divide y vencerás: Divide & Conquer . . . . . . . . . . . . 4
1.3.2. Algoritmos voraces: Greedy . . . . . . . . . . . . . . . . . 5
1.3.3. Esquema de vuelta atrás: Backtracking . . . . . . . . . . 6
1.3.4. Ramicación y poda: Branch & Bound . . . . . . . . . . . 7

2. Programación orientada a objetos 9


2.1. Tipos de datos abstractos . . . . . . . . . . . . . . . . . . . . . . 9
2.1.1. Conceptos . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.2. Encapsulamiento de estructura de datos . . . . . . . . . . 10
2.1.3. Notación . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.4. TDA y orientación a objetos . . . . . . . . . . . . . . . . 12
2.2. Fundamentos de la orientació a objetos . . . . . . . . . . . . . . . 12
2.2.1. Representación de un objeto . . . . . . . . . . . . . . . . 12
2.2.2. Elementos de los objetos . . . . . . . . . . . . . . . . . . . 13
2.2.3. Otras deniciones . . . . . . . . . . . . . . . . . . . . . . 13
2.2.4. Denición de un atributo . . . . . . . . . . . . . . . . . . 14
2.2.5. Denición de un método . . . . . . . . . . . . . . . . . . . 14
2.2.6. Aspecto de los objetos . . . . . . . . . . . . . . . . . . . . 14
2.2.7. Interfaz . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.8. Denición de clase . . . . . . . . . . . . . . . . . . . . . . 15
2.2.9. Clases vs. Objetos . . . . . . . . . . . . . . . . . . . . . . 15
2.3. Programación orientada a objetos en Java . . . . . . . . . . . . . 15
2.3.1. Las clases . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.2. Los métodos . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.2.1. Constructores . . . . . . . . . . . . . . . . . . . 16
2.3.2.2. Variables y métodos estáticos . . . . . . . . . . . 18
2.3.3. La encapsulación . . . . . . . . . . . . . . . . . . . . . . . 20
2.3.4. Separación de la interfaz . . . . . . . . . . . . . . . . . . . 21
2.3.5. Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3.6. Atributos y métodos en herencia . . . . . . . . . . . . . . 22
2.3.7. Redenición de métodos . . . . . . . . . . . . . . . . . . . 23
2.3.8. Polimorsmo . . . . . . . . . . . . . . . . . . . . . . . . . 24

1
ÍNDICE GENERAL 2

2.3.9. Redenición y sobrecarga . . . . . . . . . . . . . . . . . . 25


2.3.10. Modicador nal . . . . . . . . . . . . . . . . . . . . . . . 25
2.3.11. Herencia y constructores . . . . . . . . . . . . . . . . . . . 26
2.3.12. Métodos abstractos . . . . . . . . . . . . . . . . . . . . . . 26
2.4. Interfaz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.5. Paquetes y clases públicas . . . . . . . . . . . . . . . . . . . . . . 28
2.5.1. Paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.5.2. Clases públicas . . . . . . . . . . . . . . . . . . . . . . . . 29

3. Estructuras de datos 30
3.1. Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.2. Archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2.1. Clase File . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2.2. Flujos de bytes (streams) . . . . . . . . . . . . . . . . . . 34
3.2.3. Flujos de accesos a archivos . . . . . . . . . . . . . . . . . 35
3.2.4. Uso de Streams de Archivos Secuenciales . . . . . . . . . 35
3.2.5. Trabajar con Archivos de Acceso Aleatorio . . . . . . . . 37
3.3. Serialización de Objetos . . . . . . . . . . . . . . . . . . . . . . . 39
3.3.1. Proporcionar Serialización de Objetos . . . . . . . . . . . 40
3.4. Estructuras dinámicas . . . . . . . . . . . . . . . . . . . . . . . . 43
3.4.1. Genéricos . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.4.2. Librería de colecciones . . . . . . . . . . . . . . . . . . . . 44
3.4.2.1. Interfaces Collection e Iterator . . . . . . . . . . 45
3.4.2.2. Clases de la librería . . . . . . . . . . . . . . . . 46
3.4.3. Listas enlazadas . . . . . . . . . . . . . . . . . . . . . . . 46
3.4.3.1. Uso de listas con la librería . . . . . . . . . . . . 50
3.4.4. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.4.4.1. Uso de la librería . . . . . . . . . . . . . . . . . . 52
3.4.5. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.4.5.1. Interfaz . . . . . . . . . . . . . . . . . . . . . . . 53
3.4.5.2. Colas de Prioridad . . . . . . . . . . . . . . . . 53
3.4.6. Tablas Hash . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.4.7. Uso de la la colección para tablas hash . . . . . . . . . . . 55

4. Aplicaciones con orientación a objetos 57


4.1. Interfaces grácas de usuario . . . . . . . . . . . . . . . . . . . . 57
4.1.1. Componentes grácos . . . . . . . . . . . . . . . . . . . . 57
4.1.2. Fundamentos de programación . . . . . . . . . . . . . . . 58
4.2. Diseño de aplicaciones . . . . . . . . . . . . . . . . . . . . . . . . 63
4.2.1. Etapas de desarrollo . . . . . . . . . . . . . . . . . . . . . 64

A. Lenguaje de programación Java 65


A.1. Gramática del lenguaje . . . . . . . . . . . . . . . . . . . . . . . 65
A.1.1. TIPOS DE DATOS . . . . . . . . . . . . . . . . . . . . . 65
A.1.1.1. ENTERO: . . . . . . . . . . . . . . . . . . . . . 65
A.1.1.2. REAL . . . . . . . . . . . . . . . . . . . . . . . . 66
A.1.1.3. CARÁCTER . . . . . . . . . . . . . . . . . . . . 66
A.1.1.4. BOOLEANO . . . . . . . . . . . . . . . . . . . . 66
A.1.1.5. OPERADORES ARITMÉTICOS BINARIOS . 67
ÍNDICE GENERAL 3

A.1.1.6. OPERADORES DE ASIGNACIÓN ARITMÉ-


TICOS . . . . . . . . . . . . . . . . . . . . . . . 67
A.1.1.7. OPERADORES RELACIONALES . . . . . . . 68
A.1.1.8. OPERADORES LÓGICOS . . . . . . . . . . . . 69
A.1.2. CADENAS DE CARACTERES: . . . . . . . . . . . . . . 69
A.2. ESTRUCTURAS DE CONTROL . . . . . . . . . . . . . . . . . . 71
A.2.0.1. If . . . . . . . . . . . . . . . . . . . . . . . . . . 72
A.2.0.2. Switch . . . . . . . . . . . . . . . . . . . . . . . 72
A.2.0.3. While . . . . . . . . . . . . . . . . . . . . . . . . 73
A.2.0.4. Do while . . . . . . . . . . . . . . . . . . . . . . 74
A.2.0.5. For . . . . . . . . . . . . . . . . . . . . . . . . . 75
A.3. Entradas y salidas . . . . . . . . . . . . . . . . . . . . . . . . . . 75
A.3.1. Lectura de teclado . . . . . . . . . . . . . . . . . . . . . . 75
Resumen
Los presentes apuntes son el contenido del curso de Programación Avanzada
(PAV). Están elaborados con el objetivo de dar al alumno el contenido teórico
mínimo que debe dominar.
Como es lógico, los apuntes no son un remplazo de la explicación dada en
clase. Es siempre necesaria la asistencia a clases para poder la comprensión del
curso.
El curso de Programación básica (PB) da al alumno el fundamento de la
programación estructurada. Los presentes apuntes, en cambio, están enfocados
a la enseñanza de la programación orientada a objetos, que es la evolución
del modelo estructurado. Es muy importante que el alumno tenga claros los
fundamentos de la programación estructurada para la comprensión del presente
curso.
Es también fundamental que el estudio del curso sea complementado por la
práctica en computador por parte del alumno. Se necesita desarrollar la habili-
dad de la programación.
Capítulo 1

Algorítmica
1.1. Complejidad algorítmica
La eciencia de un determinado algoritmo depende de la máquina en la
cual se ejecuta el programa, y de otros factores externos al propio diseño. Pa-
ra comparar dos algoritmos sin tener en cuenta estos factores externos se usa
la complejidad. Esta es una media informativa del tiempo de ejecución de un
algoritmo, y depende de varios factores:

1. Los datos de entrada del programa. Dentro de ellos, lo más importante es


la cantidad, su disposición, etc.

2. La calidad del código generado por el compilador utilizado para crear el


programa.

3. La naturaleza y rapidez de las instrucciones empleados por la máquina y


la propia máquina.

4. La propia complejidad del algoritmo base del programa.

El hecho de que el tiempo de ejecución dependa de la entrada, indica que el


tiempo de ejecución del programa debe denirse como una función de la entrada.
En general la longitud de la entrada es una medida apropiada de tamaño, y se
supondrá que tal es la medida utilizada a menos que se especique lo contrario.
Se acostumbra, pues, a denominar T(n) al tiempo de ejecución de un algo-
ritmo en función de n datos de entrada. Por ejemplo algunos programas pueden
2
tener un tiempo de ejecución. T(n)=Cn , donde C es una constante que engloba
las características de la máquina y otros factores.
Las unidades de T(n) se dejan sin especicar, pero se puede considerar a
T(n) como el número de instrucciones ejecutadas en un computador idealizado,
y es lo que entendemos por complejidad.
Así, un trozo sencillo de programa como

S1;
for (int i= 0; i < N; i++) S2;

requiere

1
CAPÍTULO 1. ALGORÍTMICA 2

T(N)= t1 + t2*N

siendo t1 el tiempo que lleve ejecutar la serie "S1" de sentencias, y t2 el que


lleve la serie "S2".
Prácticamente todos los programas reales incluyen alguna sentencia condi-
cional, haciendo que las sentencias efectivamente ejecutadas dependan de los
datos concretos que se le presenten. Esto hace que mas que un valor T(N) de-
bamos hablar de un rango de valores

Tmin(N) <= T(N) <= Tmax(N)

los extremos son habitualmente conocidos como "caso peor" y "caso mejor".
Entre ambos se hallara algún "caso promedio" o más frecuente.
Para muchos programas, el tiempo de ejecución es en realidad una función
de la entrada especica, y no sólo del tamaño de ella. En cualquier caso se dene
T(n) como el tiempo de ejecución del peor caso, es decir, el máximo valor del
tiempo de ejecución para las entradas de tamaño n.

Un algoritmo es de orden polinomial si T(n) crece mas despacio, a medida


que aumenta n, que un polinomio de grado n. Se pueden ejecutar en un
computador.

En el caso contrario se llama exponencial, y estos no son ejecutables en


un computador.

1.2. Notación O grande


Para hacer referencia a la velocidad de crecimiento de los valores de una
función se usara la notación conocida como "O grande". Decimos que un algo-
ritmo tiene un orden O(n) si existen un n0 y un c, siendo c>0, tal que para todo
n>=n0 , T(n)<=c-f(n).
c estará determinado por:

Calidad del código obtenido por el compilador.

Características de la propia máquina.

Ejemplo:

T(n) = (n+1)2
n2 +2n+1 <= c-n2
n >= n0
n0 = 1
2 2
Orden n es O(n )

1.2.1. Propiedades de la notación O grande.


Si multiplicamos el orden de una función por una constante, el orden del
algoritmo sigue siendo el mismo.

O(c.f(n)) = O(f(n))
CAPÍTULO 1. ALGORÍTMICA 3

La suma del orden de dos funciones es igual al orden de la mayor.

O(f(n)+g(n)) = O(máx(f(n),g(n)).

Si multiplicamos el orden de dos funciones el resultado es la multiplicación de


los ordenes.

O(f(n))*O(g(n)) = O(f(n)*g(n))

Orden de crecimiento de funciones conocidas.


O(1) < O(log(n)) < O(n) < O(log(n)n) < O(n^k) < O(k^n)

Reglas de calculo de complejidad de T(n).

El tiempo de ejecución de cada sentencia simple, por lo común puede


tomarse como O(1).

El tiempo de ejecución de una secuencia de proposiciones se determina por


la regla de la suma. Es el máximo tiempo de ejecución de una proposición
de la sentencia.

Para las sentencias de bifurcación (IF,CASE) el orden resultante será el


de la bifurcación con mayor orden.

Para los bucles es el orden del cuerpo del bucle sumado tantas veces como
se ejecute el bucle.

El orden de una llamada a un subprograma no recursivo es el orden del


subprograma.

1.2.2. Órdenes de Complejidad


La familia O(f(n)) dene un Orden de Complejidad. Se elige como represen-
tante de este Orden de Complejidad a la función f(n) más sencilla perteneciente
a esta familia.
Las funciones de complejidad algorítmica más habituales en las cuales el úni-
co factor del que dependen es el tamaño de la muestra de entrada n, ordenadas
de mayor a menor eciencia son:
Orden Signicado

O(1) Orden constante


O(log n) Orden logarítmico
O(n) Orden lineal
O(n log n) Orden cuasi-lineal
2
O(n ) Orden cuadrático
3
O(n ) Orden cúbico
a
O(n ) Orden polinómico
n
O(2 ) Orden exponencial
O(n!) Orden factorial
Se identica una Jerarquía de Ordenes de Complejidad que coincide con
el orden de la tabla mostrada; jerarquía en el sentido de que cada orden de
complejidad inferior tiene a las superiores como subconjuntos.
CAPÍTULO 1. ALGORÍTMICA 4

O(1): Complejidad constante. Cuando las instrucciones se ejecutan una


vez.

O(log n): Complejidad logarítmica. Esta suele aparecer en determinados


algoritmos con iteración o recursión no estructural, ejemplo la búsqueda
binaria.

O(n): Complejidad lineal. Es una complejidad buena y también muy usual.


Aparece en la evaluación de bucles simples siempre que la complejidad de
las instrucciones interiores sea constante.

O(n log n): Complejidad cuasi-lineal. Se encuentra en algoritmos de tipo


divide y vencerás como por ejemplo en el método de ordenación quicksort y
se considera una buena complejidad. Si n se duplica, el tiempo de ejecución
es ligeramente mayor del doble.

2
O(n ): Complejidad cuadrática. Aparece en bucles o ciclos doblemente
anidados. Si n se duplica, el tiempo de ejecución aumenta cuatro veces.

3
O(n ): Complejidad cúbica. Suele darse en bucles con triple anidación. Si
n se duplica, el tiempo de ejecución se multiplica por ocho. Para un valor
grande de n empieza a crecer dramáticamente.

a
O(n ): Complejidad polinómica (a >3). Si a crece, la complejidad del
programa es bastante mala.

n
O(2 ): Complejidad exponencial. No suelen ser muy útiles en la práctica
por el elevadísimo tiempo de ejecución. Se dan en subprogramas recursivos
que contengan dos o más llamadas internas. N

1.3. Métodos de resolución de problemas


Existen diversas técnicas algorítmicas para estudiar la resolución de proble-
mas. Algunos de estos problemas ya se han estudiado mucho y se ha encontrado
que aplicando ciertos principios de funcionamiento de algoritmos se pueden ob-
tener programas que resuelven los problemas de una mejor manera.
Se describen a continuación algunos de esos principios de resolución de pro-
blemas.

1.3.1. Divide y vencerás: Divide & Conquer


El esquema de Divide y Vencerás es una aplicación directa de las técnicas
de diseño recursivo de algoritmos.
Hay dos rasgos fundamentales que caracterizan los problemas que son reso-
lubles aplicando el esquema de Divide y Vencerás. El primero de ellos es que
es necesario que el problema admita una formulación recursiva. Hay que poder
resolver el problema inicial a base de combinar los resultados obtenidos en la
resolución de un número reducido de subproblemas. Estos subproblemas son del
mismo tipo que el problema inicial pero han de trabajar con datos de tamaño
estrictamente menor. Y el segundo rasgo es que el tamaño de los datos que
manipulan los subproblemas ha de ser lo más parecido posible y debe decrecer
en progresión geométrica. Si n denota el tamaño del problema inicial entonces
CAPÍTULO 1. ALGORÍTMICA 5

n/c, siendo c>0 una constante natural, denota el tamaño de los datos que recibe
cada uno de los subproblemas en que se descompone.
Estas dos condiciones están caracterizando un algoritmo, generalmente re-
cursivo múltiple, en el que se realizan operaciones para fragmentar el tamaño
de los datos y para combinar los resultados de los diferentes subproblemas re-
sueltos.
Además, es conveniente que el problema satisfaga una serie de condiciones
adicionales para que sea rentable, en términos de eciencia, aplicar esta estra-
tegia de resolución. Algunas de ellas se enumeran a continuación :

1. En la formulación recursiva nunca se resuelve el mismo subproblema más


de una vez.

2. Las operaciones de fragmentación del problema inicial en subproblemas


y las de combinación de los resultados de esos subproblemas han de ser
ecientes, es decir, han de costar poco.

3. El tamaño de los subproblemas ha de ser lo más parecido posible.

4. Hay que evitar generar nuevas llamadas cuando el tamaño de los datos
que recibe el subproblema es sucientemente pequeño.

Como ejemplos muy conocidos de este tipo de algoritmos están los métodos de
ordenamiento Quicksort y Mergesort.

1.3.2. Algoritmos voraces: Greedy


El conjunto de condiciones que deben satisfacer los problemas que se pue-
den resolver aplicando este esquema es un tanto variado y, a continuación, se
enumeran algunas de ellas :

1. El problema a resolver ha de ser de optimización y debe existir una función


que es la que hay que minimizar o maximizar. Es la denominada función
objetivo.

2. Existe un conjunto de valores candidatos para cada una de las variables


de la función objetivo. Ese conjunto de valores posibles para una variable
es su dominio.

3. Existe un conjunto de restricciones lineales que imponen condiciones a los


valores que pueden tomar las variables de la función objetivo. El conjunto
puede estar vacío o contener una única restricción.

4. Existe una función, que en nuestra notación denominaremos función solu-


ción, que permite averiguar si un conjunto dado de asociaciones variable-
valor es solución al problema. La asociación de un valor a una variable se
denomina decisión.

5. Y ya por último, existe una función que indica si el conjunto de decisiones


tomadas hasta el momento viola o no las restricciones. Esta función recibe
el nombre de factible y al conjunto de decisiones factibles tomadas hasta
el momento se le suele denominar solución en curso.
CAPÍTULO 1. ALGORÍTMICA 6

Esta es una caracterización muy genérica y en temas posteriores se comprobará


que algunas de sus características son compartidas por otros esquemas como
Vuelta Atrás, Programación Dinámica, etc. ¾Qué es lo que diferencia al método
Voraz de estos otros esquemas? : La diferencia fundamental estriba en el proceso
de selección de las decisiones que forman parte de la solución. En un algoritmo
Voraz este proceso es secuencial y a cada paso se determina, utilizando una
función adicional que denominamos de selección, el valor de una de las variables
de la función objetivo. A continuación el algoritmo Voraz se encuentra con un
problema idéntico, pero estrictamente menor, al que tenía en el paso anterior y
vuelve a aplicar la misma función de selección para obtener la siguiente decisión.
Esta es, por tanto, una técnica descendente.
En cada iteración del algoritmo la función de selección aplica un criterio jo
para escoger el valor candidato. Este criterio es tal que hace que la decisión
sea localmente óptima, es decir, la decisión seleccionada logra que la función
objetivo alcance el mejor valor posible ( ningún otro valor de los disponibles
para esa variable lograría que la función objetivo tuviera un valor mejor ). Pero
nunca se vuelve a reconsiderar ninguna de las decisiones tomadas. Una vez que
a una variable se le ha asignado un valor localmente óptimo, se comprueba si esa
decisión junto con la solución en curso es un conjunto factible. Si es factible se
añade la decisión a la solución en curso y se comprueba si ésta es o no solución
para el problema. Ahora bien, si no es factible simplemente la última decisión
tomada no se añade a la solución en curso y se continua el proceso aplicando
la función de selección al conjunto de variables que todavía no tienen un valor
asignado.
Ejemplo de esto son los algoritmos de vuelto y de la mochila.

1.3.3. Esquema de vuelta atrás: Backtracking


La caracterización de los problemas que son resolubles aplicando este es-
quema no es muy distinta de la que ya se ha visto antes y se utiliza la misma
terminología como, por ejemplo, decisión, restricción, solución, solución en cur-
so, etc. Se recuerdan algunas de las características de estos problemas :

1. se trata generalmente de problemas de optimización, con o sin restriccio-


nes.

2. la solución es expresable en forma de secuencia de decisiones.

3. existe una función denominada factible que permite averiguar si una se-
cuencia de decisiones, la solución en curso actual, viola o no las restriccio-
nes.

4. existe una función, denominada solución, que permite determinar si una


secuencia de decisiones factible es solución al problema planteado.

No parece que exista una gran diferencia entre los problemas que se pueden
resolver utilizando un esquema Voraz y los que se pueden resolver aplicando
este nuevo esquema de Vuelta Atrás. Y es que en realidad no existe. La técnica
de resolución que emplean cada uno de ellos es la gran diferencia.
Básicamente, Vuelta Atrás es un esquema que genera TODAS las secuencias
posibles de decisiones. Esta tarea también la lleva a cabo el algoritmo conocido
CAPÍTULO 1. ALGORÍTMICA 7

como de fuerza bruta pero lo hace de una forma menos eciente. Vuelta Atrás,
de forma sistemática y organizada, genera y recorre un espacio que contiene
todas las posibles secuencias de decisiones. Este espacio se denomina el espacio
de búsqueda del problema. Una de las primeras implicaciones de esta forma de
resolver el problema es que, si el problema tiene solución, Vuelta Atrás seguro
que la encuentra.

1.3.4. Ramicación y poda: Branch & Bound


El esquema de Ramicación y Poda, Branch & Bound en la literatura ingle-
sa, es, básicamente, una mejora considerable del esquema de Vuelta Atrás. Se
considera que es el algoritmo de búsqueda más eciente en espacios de búsqueda
que sean árboles.
Los típicos problemas que se suelen resolver con Ramicación y Poda son los
de optimización con restricciones en los que la solución es expresable en forma de
secuencia de decisiones, aunque cualquier problema resoluble con Vuelta Atrás
también se puede resolver con Ramicación y Poda, pero a lo mejor no merece
la pena.
En Ramicación y Poda los nodos del espacio de búsqueda se pueden eti-
quetar de tres formas distintas. Así tenemos que uno nodo está vivo, muerto o
en expansión. Un nodo vivo es un nodo factible y prometedor del que no se han
generado todos sus hijos. Un nodo muerto es un nodo del que no van a generarse
más hijos por alguna de las tres razones siguientes : ya se han generado todos
sus hijos o no es factible o no es prometedor. Por último, en cualquier instante
del algoritmo pueden existir muchos nodos vivos y muchos nodos muertos pero
sólo existe un nodo en expansión que es aquel del que se están generando sus
hijos en ese instante.
Tanto Vuelta Atrás como Ramicación y Poda son algoritmos de búsqueda,
pero mientras Vuelta Atrás es una búsqueda ciega, Ramicación y Poda es una
búsqueda informada. La principal diferencia entre una búsqueda o recorrido
ciego, bien sea en profundidad o en anchura, y una búsqueda informada es el
orden en que se recorren los nodos del espacio de búsqueda. Fijado un nodo
x del espacio de búsqueda, en un recorrido ciego se sabe perfectamente cual
es el siguiente nodo a visitar ( el primer hijo de x sin visitar, en un recorrido
en profundidad y el siguiente hermano de x, en un recorrido en anchura ). Sin
embargo, en un recorrido informado el siguiente nodo es el más prometedor de
entre todos los nodos vivos; es el que se va a convertir en el próximo nodo en
expansión.
En Vuelta Atrás tan pronto como se genera un nuevo hijo del nodo en curso,
este hijo se convierte en el nuevo nodo en curso o nodo en expansión, según
la terminología de Ramicación y Poda, y los únicos nodos vivos son los que
se encuentran en el camino que va desde la raíz al actual nodo en expansión.
En Ramicación y Poda se generan todos los hijos del nodo en expansión antes
de que cualquier otro nodo vivo pase a ser el nuevo nodo en expansión por lo
que es preciso conservar en algún lugar muchos más nodos vivos que pertenecen
a distintos caminos. Ramicación y Poda utiliza una lista para almacenar y
manipular los nodos vivos.
Dependiendo de la estrategia para decidir cual es el siguiente nodo vivo que
se va a expandir de la lista, se producen distintos órdenes de recorrido del espacio
de búsqueda :
CAPÍTULO 1. ALGORÍTMICA 8

1. estrategia FIFO ( la lista es una cola ) : da lugar a un recorrido del espacio


de búsqueda por niveles o en anchura.

2. estrategia LIFO ( la lista es una pila ) : produce un recorrido en profun-


didad aunque en un sentido distinto al que se da en Vuelta Atrás.

3. estrategia LEAST COST ( la lista es una cola de prioridad ) : este es el


recorrido que produce Ramicación y Poda para un problema de minimi-
zación y usando función de estimación. El valor de lo que un nodo vivo
promete, es decir, el coste de lo que ya se ha hecho junto con la estimación
de lo que costará como mínimo alcanzar una solución, es la clave de orde-
nación en la cola de prioridad. Para abreviar diremos que la clave de un
nodo x es coste(x) que es igual al coste_real(x)+coste_estimado(x). De
este modo se expande aquél nodo que promete la solución de coste más
bajo de entre todos los nodos vivos. Es de esperar que el nodo con la clave
más pequeña conduzca a soluciones con un bajo coste real.
Capítulo 2

Programación orientada a
objetos
2.1. Tipos de datos abstractos
2.1.1. Conceptos
Frente a un problema que va a ser resuelto con un programa se debe empezar
con una etapa de modelado del problema.
El modelado se puede realizar de muchas maneras. Cuando se desea hacer
uso de la tecnología de orientación a objetos se debe modelar usando tipos
abstractos de datos.
El modelo dene una perspectiva abstracta del problema. Esto implica que
el modelo se enfoca solamente en aspectos relacionados con el problema y que
trata de denir propiedades del problema. Estas propiedades incluyen

los datos que son afectados

las operaciones que son identicadas por el problema.

El modelo es una especicación exacta, pero totalmente independiente de la


tecnología que se utilizará luego en el programa.
La estructura de los datos puede ser accesada solamente por medio de ope-
raciones denidas. Este conjunto de operaciones es llamada interface y es expor-
tada por la entidad. Una entidad con las propiedades recién descritas se conoce
como un tipo de datos abstracto (TDA).
La gura siguiente muestra un TDA que consiste en una estructura de datos
abstracta y operaciones. Solamente las operaciones son visibles desde afuera y
denen la interface.

9
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 10

Un tipo de datos abstracto (TDA).


Tratemos de poner las características de un TDA en un modo más formal :
Un tipo de datos abstracto (TDA) se caracteriza por las siguientes propie-
dades :

1. Exporta un tipo.

2. Exporta un conjunto de operaciones. Este conjunto es llamado interface.

3. Las operaciones de la interface son el único y exclusivo mecanismo de


acceso a la estructura de datos del TDA.

4. Axiomas y precondiciones denen el ámbito de aplicación del TDA.

Con la primera propiedad es posible crear más de una instancia de un TDA.

2.1.2. Encapsulamiento de estructura de datos


El principio de esconder la estructura de los datos usada y solamente proveer
una bien denida interface se conoce como encapsulamiento. ¾Por qué es tan
importante encapsular la estructura de los datos ?
Para contestar a esta pregunta se puede considerar el siguiente ejemplo ma-
temático donde se quiere denir un TDA para números complejos. Para esto, es
suciente saber que los números complejos constan de dos partes: la parte real
y la parte imaginaria. Ambas partes están representadas por números reales.
Los números complejos denen varias operaciones: suma, resta, multiplicación
o división por nombrar algunas. Los axiomas y precondiciones son válidos tal
y como están denidos por la denición matemática de los números complejos.
Por ejemplo, existe un elemento neutral para la adición.
Para representar un número complejo es necesario denir la estructura de
datos que va a ser usada por su TDA. Uno puede pensar en al menos dos
posibilidades para hacer ésto :

Ambas partes son almacenadas en un arreglo doble, donde el primer valor


indica la parte real y el segundo valor la parte imaginaria del número
complejo. Si x detenta la parte real mientras que y la parte imaginaria, se
podría pensar en accesarlos vía subíndices de arreglos : x=c[0] y y=c[1].

Ambas partes son almacenadas en un registro doble. Si el nombre del


elemento de la parte real es r y el de la parte imaginaria es i, x y y pueden
ser obtenidos con: x=c.r y y=c.i.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 11

El Punto 3 de la denición del TDA dice que para cada acceso a la estructura de
los datos debe haber una operación denida. Los ejemplos de acceso de arriba
parecen contradecir este requisito. ¾Es esto realmente cierto?
Hay que ver otra vez las dos posibilidades para representar números com-
plejos. Considerar únicamente la parte real. En la primera versión, x es igual a
c[0]. En la segunda versión, x es igual a c.r. En ambos casos x es igual a "algo".
Es este "algo" el que diere en la estructura de datos actual que se está usando.
Pero en ambos casos, la operación ejecutada "igual a" tiene el mismo signicado
para declarar que x es igual a la parte real del número complejo c: ambos casos
logran la misma semántica.
Si se piensa en operaciones más complejas, el impacto de desacoplar estruc-
tura de datos y operaciones se hace aún más evidente. Por ejemplo, la suma
de dos números complejos requiere ejecutar una suma para cada parte. Por
consecuencia, se debe accesar el valor de cada parte, el cuál es diferente para
cada versión. Al proveer una operación "suma" tu puedes encapsular estos de-
talles aparte de su uso actual. En el contexto de una aplicación simplemente se
"suma dos números complejos" sin importar cómo se logra en la práctica esta
funcionalidad.
Una vez que se ha creado un TDA para números complejos, llamado Com-
plex, se puede usar de la misma manera que se usan los tipos de datos conocidos
tales como los enteros (integers).
Resumiendo: La separación de las estructuras de los datos y las operaciones
por una parte y la restricción de solamente accesar la estructura de los datos
vía una bien denida interface por la otra, permite escoger estructuras de datos
apropiadas para el ambiente de la aplicación.

2.1.3. Notación
Debido a que los TDAs proveen una perspectiva abstracta para describir
propiedades de conjuntos de entidades, su uso es independiente de un lenguaje
de programación en particular. Toda descripción de un TDA consiste en dos
partes :

Datos: Esta parte describe la estructura de los datos usada en el TDA de una
manera informal.

Operaciones: Esta parte describe las operaciones válidas para este TDA, por
lo tanto, describe su interface. Se usa la operación especial constructor
para describir las acciones que se van a ejecutar una vez que una entidad
de este TDA es creada y destructor para describir las acciones que se van a
efectuar cuando una entidad es destruída. Son dados para cada operación,
los argumentos provistos así como precondiciones y postcondiciones.

Se presenta como ejemplo la descripción del TDA Integer. Sea k una expresión
integer:
TDA Integer es

Datos Una secuencia de dígitos que opcionalmente presentan como prejo un


signo más o un signo menos. Nos referimos a este número entero con signo
como N.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 12

Operaciones
constructor Crea un nuevo integer.
add(k) Crea un nuevo integer, suma de N y k. Por consecuencia, la postcon-
dición de esta operación es add = N+k.
sub(k) Similar a add, esta operación crea un nuevo integer de la diferencia de
ambos valores integer. Por lo tanto la postcondición para esta operación
es sub = N-k.
set(k) Pone a N lo que vale k. La postcondición para esta operación es N = k.
...
end
La descripción de arriba es una especicación para el TDA Integer. Nótese
por favor, que se usan palabras para nombres de operaciones tales como "add".
Se podría haber usado el signo "+", que es más intuitivo, pero esto podría llevar
a alguna confusión El nombre de la operación es solamente sintaxis ahí donde
la semántica se describe por las pre- y postcondiciones asociadas. Sin embargo,
siempre constituye una buena idea el combinar ambos para hacer que la lectura
de las especicaciones del TDA sea más fácil.
Los lenguajes de programación reales son libres de escoger una implementa-
ción arbitraria para un TDA. Por ejemplo, podrían implementar la operación
add con el operador injo "+" que condujera a una lectura más intuitiva para
la suma de enteros.

2.1.4. TDA y orientación a objetos


Los TDAs permiten la creación de instancias con propiedades bien denidas
y comportamiento bien denido. En orientación a objetos, se hace referencia a
los TDAs como clases. Por lo tanto, una clase dene las propiedades de objetos
instancia en un ambiente orientado a objetos.
Los TDAs denen la funcionalidad al poner especial énfasis en los datos
involucrados, su estructura, operaciones, así como en axiomas y precondicio-
nes. Consecuentemente, la programación orientada a objetos es "programación
con TDAs" : al combinar la funcionalidad de distintos TDAs para resolver un
problema. Por lo tanto, instancias (objetos) de TDAs (clases) son creados diná-
micamente, usados y destruidos.

2.2. Fundamentos de la orientació a objetos


2.2.1. Representación de un objeto
Un objeto puede ser la representación de:

Una cosa tangible y/o visible (Una persona)

Algo que puede comprenderse intelectualmente (Un proceso)

Una entidad de software (Una colección de datos)


CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 13

2.2.2. Elementos de los objetos


Un objeto tiene tres elementos siempre presentes:

Identidad: un identicador unívoco


Estado: un conjunto de propiedades (atributos)
Comportamiento: un conjunto de operaciones (métodos)
En principio los métodos funcionan haciendo uso de la información que se en-
cuentra almacenada en los atributos.

Como un ejemplo práctico se puede pensar en un objeto unReloj, que posee


el estado y operaciones propias de un reloj real:

2.2.3. Otras deniciones


Diversos autores han dado múltiples deniciones al concepto de objeto. Bá-
sicamente expresan lo mismo desde diversos puntos de vista:

Un objeto se caracteriza por un número de operaciones y un estado


que recuerda el efecto de estas operaciones. Ivar Jacobson

Un objeto tiene un estado, comportamiento e identidad; la estruc-


tura y comportamiento de objetos similares se denen en sus clases
comunes. Grady Booch

Un objeto es una entidad que tiene estado (cuya representación está


oculta) y un conjunto denido de operaciones que operan sobre ese
estado. Ian Sommerville.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 14

Un objeto es una entidad con unos límites bien denidos que encap-
sulan estado y comportamiento. El estado se representa por atributos
y relaciones, el comportamiento es representado por operaciones y
métodos. Object Management Group

2.2.4. Denición de un atributo


Es una característica fundamental de cada objeto de una clase
Una clase puede denir un cierto número de atributos.
Todos los atributos tienen algún valor. Este valor puede ser una cantidad,
una relación con otro objeto, etc.

2.2.5. Denición de un método


Es una acción que se realiza sobre un objeto para consultar o modicar su
estado.
Tipos de operaciones.

Modicador (setter): altera el estado de un objeto


Selector (getter): accede al estado de un objeto sin alterarlo.
Iterador: permite accede a todas las partes de un objeto
Constructor: crea un objeto e inicializa su estado.
Destructor: limpia el estado de un objeto y lo destruye.
Propósito general: la lógica del programa

2.2.6. Aspecto de los objetos

2.2.7. Interfaz
Es el aspecto externo del objeto. La parte visible y accesible para el resto de
objetos.
También se le dene como el protocolo de comunicación de un objeto.
Puede estar formado por uno o varios métodos. No todos los métodos de un
objeto tienen que formar parte del interfaz.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 15

2.2.8. Denición de clase


Una clase es la representación de la estructura y comportamiento de un
objeto.
Es un patrón para la denición de atributos y métodos para un tipo parti-
cular de objetos.
Todos los objetos de una clase dada son idénticos en estructura y compor-
tamiento pero son únicos (aunque tengan los mismos valores en sus atributos).
Instancia es el término utilizado para referirse a un objeto que pertenece a
una clase concreta.

2.2.9. Clases vs. Objetos


Clase:

Un patrón para la denición del estado y el comportamiento de un tipo


particular de objetos.

Todos los objetos de una clase dada son idénticos en estructura y en com-
portamiento, pero tienen identidad única.

Objeto (instancia):

Pertenece a una clase en particular.

Los objetos son creados y destruidos en tiempo de ejecución. Residen en


el espacio de memoria.

2.3. Programación orientada a objetos en Java


2.3.1. Las clases
Una clase es un descripción estática de un conjunto de objetos. Una clase es
una parte de un código fuente. Un objeto es referenciado mediante la descripción
de una clase. Un objeto es dinámico y puede tener una innidad de objetos
descritos por una misma clase. Un objeto es un conjunto de datos presente en
memoria.
Así, si denimos una clase, ésta podrá instanciarse tantas veces como se
quiera. Pero todos los objetos creados tendrán la misma descripción y el mis-
mo comportamiento. Es decir, la descripción de un objeto viene dada por sus
atributos. Por ejemplo, se dene la clase ventana como:

class Ventana {
string titulo;
int coordx;
int coordy;
int altura;
int anchura;
}
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 16

El nombre de la clase es ventana y titulo, coordx, coordy, altura y anchura son


los atributos de esta clase. Cada objeto que se cree con esta clase compartirá
esta descripción, pero cada uno tendrá sus propio atributos (su titulo, sus coor-
denadas x e y, su altura y su anchura). Obsérvese que los atributos de un objeto
pueden ser tipos simples (enteros, booleanos....) o bien objetos como aquí la
cadena de caracteres del nombre de la ventana.
¾ Cómo crear un objeto ? Mediante la palabra clave new, la cual reserva el
espacio de memoria necesario para el objeto, el cual, el espacio, se calcula a partir
de la descripción de la clase, y se llama al constructor del objeto. Sólo reseñar,
por ahora, que el constructor es un método cuyo nombre es idéntico al de la clase
y que efectúa las operaciones que el programador le indica inmediatamente tras
la creación del objeto.
Por ejemplo, se dene los objetos ven1 y ven2 de la clase ventana y se llama
al constructor:

Ventana ven1 = new Ventana ("Word",20,20,100,100);


Ventana ven2 = new Ventana ("Excel",22,05,19,64);

Ambos objetos se encuentran en memoria. Aquí, los parámetros pasados al cons-


tructor sirven para dar un valor a los atributos. La sintaxis de este constructor
será la siguiente:

Ventana (string nombre, int x, int y, int b, int h, int l) {


titulo = nombre;
coordx = x;
coordy = y;
altura= h;
anchura = l;
}

Más adelante se detallarán más cosas sobre el constructor.

2.3.2. Los métodos


Por lo tanto, las clases describen los atributos de los objetos, y proporcionan
también los métodos. Un método es una función que se ejecuta sobre un objeto.
Salvo un excepción que ya se verá, no se puede ejecutar un método sin precisar
el objeto sobre el que se aplica. Los atributos del objeto son implícitamente
parámetros del método. Al ejecutarse un método lo hace como si estuviese dentro
del objeto.

2.3.2.1. Constructores
Para cada clase, pueden denirse uno o más métodos particulares: son los
constructores.
El constructor se llama en el momento de la creación de un objeto. La uti-
lización de new( ) implica la creación física del objeto y la llamada a uno de
sus constructores. Si hubiera más de un constructor, éstos se diferencian por los
parámetros que se pasan en el new( ). Véase el ejemplo siguiente:
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 17

import java.io.*;
// una clase que sirve de main
public class Democonstructor {
public static void main (String arg [] ) {
Ejemplo e;
e = new Ejemplo (2);
e = new Ejemplo ("2");
};
}
// una clase que tiene dos constructores diferentes
class Ejemplo {
public Ejemplo (int param) {
System.out.println ("ha llamado al constructor con un entero");
}
public Ejemplo (String param) {
System.out.println ("ha llamado al constructor con una string");
};
};

Los constructores no tienen tipo de retorno. Si por error denimos un cons-


tructor que tenga un tipo de retorno, el compilador lo toma como un método
normal. En tal caso se tendrá la impresión de que el constructor no es llamado
en el momento de la creación del objeto. En realidad, lo que sucede es que se
llamará a un constructor predeterminado, al no haberse denido ninguno.
En Java, no se liberan explícitamente los objetos creados, es el sistema quien
los recupera cuando hace falta. Estos atributos del objeto son liberados por el
núcleo de Java.
Es posible inicializar los atributos de un objeto indicando los valores que se
les darán en la creación de éste. Estos valores se adjudican tras la creación física
del objeto, pero antes de la llamada al constructor.
Véase el siguiente ejemplo:

class Valor {
static public void main ( string [ ] arg ) {
new Reserva (1000);
};
}
class Reserva {
int capacidad = 2; // Valor predeterminado

public Reserva (int laCapacidad) {


System.out.println (capacidad);
capacidad = laCapacidad;
System.out.println (capacidad);
};
}

Esto dará como resultado la visualización del valor que el núcleo le ha dado
al atributo capacidad en la inicialización (2) y posteriormente el valor que le
asigna el constructor (1000).
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 18

Si no se inicializan los atributos explícitamente, toman un valor predetermi-


nado, cero.
Como se dijo antes, si no se dene un constructor, el núcleo dene uno
predeterminado.

class Pobre {
int atributo;
void CualquierMetodo();
}

Hacer un new Pobre( ) sería correcto. Pero éste deja de existir si se dene uno,
y por lo tanto no se le puede llamar.

2.3.2.2. Variables y métodos estáticos


Como ya se ha dicho, cada objeto posee sus propios atributos y es posible
que todos los objetos de una misma clase tengan atributos en común: son los
atributos de la clase, introducidos por la palabra clave static. Estos atributos son
legibles y modicables por todos los objetos de una misma clase. La modicación
de un atributo static es tenida en cuenta inmediatamente por los demás objetos,
porque lo comparten.
Ejemplo:

class Demostatic {
static public void main ( String[ ] args ){
participante p1=new participante ( );
participante p2=new participante ( );
p1.modifica ( );
System.out.println ("p1: " + p1.participado + " " +
p1.noparticipado);
System.out.println ("p2: " + p2.participado + " " +
p2.noparticipado);
};
}
class Participante {
static int participado=2;
int noparticipado=2;
void modifica ( ) {
participado=3;
noparticipado=4;
};
}

Esto dará como resultado:

p1: 3 4
p2: 3 2
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 19

La llamada a modica ( ) ha modicado el atributo participado. Este resultado


se extiende a todos los objetos de la misma clase, mientras que sólo ha modicado
el atributo noparticipado del objeto actual.
Si un método trabaja únicamente sobre atributos estáticos, puede declararse
también static. Esto signica que no modica atributos de instancias, sino sola-
mente atributos de clase, bien directamente (en su cuerpo) o indirectamente (en
sus llamadas a otros métodos). Así, si volvemos a nuestra clase participante:

class participante {
static int participado;
int noparticipado; ...

el método:

void modifica ( ) {
participado + +;
}

puede ir prejado por static, mientras que:

void modifica2 ( ) {
noparticipado + +;
}

así como

void modifica3 ( ) {
modifica2 ( );
};

no pueden aspirar a ese título, porque ambos modican un atributo no static,


directa o indirectamente.
El interés de prejar los métodos con static es doble:

un método static puede ser llamado por otro método static, algo que no
se sale de lo normal

un método static puede llamarse sin referencia a un objeto.

Recuérdese que para llamar a un método, hay que referirlo a un objeto:


unobjeto.metodo ( ); y que metodo ( ); escrito sin objeto delante signica
en realidad que designa el objeto actual en Java.
Un método static puede ser llamado poniendo delante el nombre de la clase.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 20

2.3.3. La encapsulación
Se trata de proteger los datos en una clase. La encapsulación se basa en
la noción de servicios prestados. Una clase proporciona un cierto número de
servicios y los usuarios de esta clase no tienen que conocer la forma como se
prestan estos servicios.
Hay que distinguir en la descripción de la clase dos partes:

la parte pública, accesible por las otras clases;

la parte privada, accesible únicamente por los métodos de la clase.

Es recomendable poner los atributos de una clase en la parte privada.


En el ejemplo de la clase ventana, los métodos que modican el tamaño de
la ventana no deben manipular directamente los atributos del objeto. Si estos
fuesen públicos:

class ventana {

public int x1, y1; // coordenadas arriba izquierda


public int h, anchura; // altura, anchura
}

Entonces se pueden modicar directamente estos atributos:

class pantalla {
void desplaza10endiagonal (ventana unaV) {
unaV.x1 += 10;
unaV.y1 + = 10;
}
}

Esto resulta muy práctico pero muy imprudente, si más adelante se decide añadir
los atributos siguientes a la clase ventana:

int x2,y2; // coordenadas de la esquina inferior derecha

por ejemplo, para no tener que recalcularla cada vez. El método Desplaza10EnDiagonal
debe modicarse para tener en cuenta estos nuevos atributos:

class Pantalla {
void Desplaza10EnDiagonal (Ventana unaV) {
unaV.x1 += 10;
unaV.y1 + = 10;
unaV.x2 + = 10;
unaV.y2 + = 10;
}
}
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 21

Es demasiado complicado. Una modicación de la clase ventana que no ofrece


servicios suplementarios entraña entonces una modicación de las otras clases
que utilizan ventana. El mantenimiento de este programa se complica. En rea-
lidad, se han cometido dos errores sucesivos en el ejemplo anterior:

un error de encapsulación: los atributos x1 y x2 no debieran ser manipu-


lables directamente;

un error de diseño de método: el método Desplaza10EnDiagonal no debiera


situarse en la clase pantalla, sino en la clase ventana:

void Desplaza10EnDiagonal (ventana unaV) {


x1 + = 10;
y1 + = 10;
x2 + = 10;
y2 + = 10;
}

Se constata además que la escritura resulta más simple. Especialmente, al escri-


birlo así, se acerca el método a los datos que manipula. Finalmente, añadir los
atributos x2 e y2 modica entonces un solo método de clase ventana, en lugar
de modicar numerosos métodos de los usuarios de ventana.
Java proporciona varios niveles de encapsulado. La granularidad de la pro-
tección es el método o el atributo. Así, tal atributo podrá ser protegido, mientras
que tal otro no lo será.

2.3.4. Separación de la interfaz


¾Cuándo debe utilizarse qué? O en otras palabras: ¾cuáles son los diferen-
tes casos de utilización de los mecanismos de protección? Se puede distinguir
principalmente dos casos:

el atributo o el método pertenece a la interfaz de la clase: debe ser public;

el atributo o la clase pertenece al cuerpo de la clase: debe ser protegido.


Esta protección es diferente según los casos; en general, la protección más
fuerte es aconsejable porque es fácil desproteger un atributo, y es mucho
más difícil hacerlo inaccesible si ya se utiliza.

2.3.5. Herencia
La herencia es una de las nociones importantes del diseño y de la progra-
mación orientada a objetos. Es uno de los factores que permiten la reutilización
del código.
La herencia es una relación entre clases denida por la palabra clave extends.
Si se dice que una clase Hija, hereda de una clase Madre, quiere decir que éste
asimila los atributos y métodos del Madre y que un objeto de la clase Hija es
también de la clase Madre.
Esto último, no es recíproco, es decir, un objeto de la clase Madre no lo es
de la clase Hija.
La herencia, desde este punto de vista supone:
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 22

compartir código, el cual permite una economía de compilación, de disco


duro....

en mantenimiento, cualquier modicación en la clase Madre repercute au-


tomáticamente sobre la clase Hija.

2.3.6. Atributos y métodos en herencia


La clase que hereda, llamada también subclase, retoma los atributos y los
métodos de la superclase. Pero puede:

añadirles nuevos atributos y métodos.

redenir los métodos.

class Madre{
int entero;
void metodo () { ;};
}
class Hija extends Madre { // extends por heredar
void metododeHija () {
// . . .
};
}

La herencia que recibe Hija es igual a:

class Madre {
int entero;
void metodo () {;};
}
class Hija {
int entero;
void metodo () {;};
}

La herencia, además, permite capturar nuevas formas de abstracción, es decir,


gracias a ésta, se puede expresar formalmente ideas que provienen del nivel de
diseño. Esto permite:

a los otros diseñadores establecer inmediatamente relaciones de parentesco


entre clases;

al compilador tenerlo en cuenta.


CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 23

2.3.7. Redenición de métodos


Cuando se hace heredar una clase de otra, se pueden redenir ciertos métodos
con la intención de modicarlos o mejorarlos. El método lleva el mismo nombre
y la misma sintaxis, pero sólo se aplica a los objetos de la subclase o a sus
descendientes.
Véase el siguiente ejemplo:

import java.io.*;
public class redefinicion {
public static void main (String arg [ ]) {
Madre m new Madre ();
m.habla ();
Hija h = new Hija ();
h.habla ();
nieta n = new Nieta ();
n.habla ();
};
}
class Madre {
void habla () {

System.out.println ("soy de la clase Madre");


};
};
class Hija extends Madre {
void habla () {

System.out.println ("soy de la clase Hija");


};
};
class Nieta extends Hija {
void nuevometodo () {
System.out.println ("no es llamado"),
};
};

Esto dará como resultado:

soy de la clase Madre


soy de la clase Hija
soy de la clase Hija

Los objetos de la clase Madre utilizan el método de la clase Madre, los de la


clase Hija, el método redenido en la clase Hija y los de la clase Nieta utilizan
el método de la clase Hija, porque este método no ha sido redenido en la clase
Nieta.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 24

2.3.8. Polimorsmo
Como se ha dicho, un objeto de la clase Hija es también un objeto de la
clase Madre. Se puede aprovechar esta dualidad gracias a lo que se denomina
polimorsmo.
El polimorsmo es un aprovechamiento de la herencia imposible de realizar
en un lenguaje que no sea orientado a objetos. Consiste en llamar a un método
según el tipo estático de una variable, basándose en el núcleo para llamar a la
versión correcta del método.
Conservando la denición de las clases anteriores, Madre, Hija y Nieta, y
modicando la llamada a los métodos:

public static void main ( string arg [ ]) {


Madre m;
m = new Madre () ;
m.habla ();
m = new Hija ();
m.habla ();
m = new Nieta ();
m.habla ();
};

Aquí se utiliza una sola variable, m, a la que se asignan sucesivamente objetos


de las clases Madre, Hija y Nieta, mientras que m es siempre una variable del
tipo Madre. Esto es correcto, porque como se ha dicho, un Hija es también un
Madre, es decir, la herencia de la que se habló. Recuérdese que no es recíproco.
Si se hiciera un

Hija f = new Madre ( );

sería rechazado por el compilador.


Si se ejecutase el ejemplo anterior, se obtendría el siguiente resultado:

soy de la clase Madre


soy de la clase Hija
soy de la clase Nieta

Aunque la variable, m, sea siempre de tipo Madre, los objetos designados con
esta variable son sucesivamente del tipo Madre, Hija y Nieta, y se ejecutan los
métodos de sus clases.
Se puede pensar que ésto es una compilación innecesaria, pero no es así, se
trata de una funcionalidad potente de los lenguajes orientados a objetos, la cual
permite suprimir una buena parte de los case y otros switch.
Hasta ahora se ha hablado de ventana Madre y ventana Hija para designar
una ventana que contiene a otra. Esto no signica que la ventana Hija hereda de
la ventana Madre. Una ventana no puede heredar de otra porque son objetos.
Son las clases quienes heredan unas de otras.
En el ejemplo visto, la ventana Hija puede ser de la misma clase que la
ventana Madre, o bien de otra clase distinta. Por el contrario, esa ventana Hija
será sin duda un atributo de la ventana Madre.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 25

2.3.9. Redenición y sobrecarga


Se ha visto que una clase que hereda de otra podía redenir ciertos métodos.
También se vio que se podía dar el mismo nombre a dos métodos diferentes, a
poco que tengan parámetros diferentes.
La sobrecarga permite distinguir dos métodos de la misma clase que pueden
ser llamados uno u otro sobre el mismo objeto, pero que poseen parámetros
distintos. La redenición distingue dos métodos de dos clases de las cuales una
es ancestro de la otra y que tiene ambos los mismos parámetros.

class Madre {
void metodo ( ) { };
void metodo (int param) {
// el método está sobrecargado
}
}
class Hija extends Madre{
void metodo ( ) {
// redefinición de metodo () de Madre
}
}

2.3.10. Modicador nal


Esta posibilidad de redenición es peligrosa. Si diseñamos una clase algo so-
sticada que asegura servicios potentes, todo esto puede desmoronarse si cual-
quiera puede heredar de ella y redenir cualquier método.
Java ha previsto un mecanismo para impedir la redenición de métodos. Es
el empleo de la palabra clave nal, que indica al compilador que está prohibido
redenir un método.
El programa siguiente:

class Madre {
final void metodo () { ;} ;
}
class Hija extends Madre {
void metodo () { };
}
es rechazado por el compilador.
Los atributos también pueden ser nal, lo que permite de hecho denir cons-
tantes. Si un atributo es nal:

debe ser inicializado en su declaración y

no puede ser modicado ni por un método de su subclase, ni por un método


de la propia clase.

Si un atributo es nal static, es constante y accesible simplemente dando el


nombre de la clase. Se comporta, pues, como una verdadera constante.
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 26

2.3.11. Herencia y constructores


Si se tiene una clase Madre y otra Hija, cuando se construye un Hija, se
llama al constructor de Hija. Se sabe, que un Hija es también e implícitamente
un Madre. Es necesario, pues, que el constructor de Madre sea también invocado.
Esta invocación es implícita o explícita según la presencia o no de constructores
explícitos.
Puede ser implícita si el constructor de Madre no toma parámetros. En este
caso, sea el constructor de Hija implícito o no, la llamada al constructor de
Madre es automática.
En el caso de que el constructor de Madre esperase parámetros, hay que
pasárselos. Es necesario denir un constructor de Hija y, en este constructor,
utilizar la palabra clave super para indicar la llamada al constructor de Madre.
Esta palabra clave se sitúa en la primera línea del constructor de Hija.
Ejemplo:

import java.io.*;
class herecons {
public static void main (string arg [ ] ) {
new Hija (2);
}
};
class Madre {
int m;
public Madre ( int mm ) {
System.out.println ('madre');
};
}
class Hija extends Madre {
int f;
public Hija (int ff) {
super (ff);
System.out.println("Hija");
};
}

Esto visualizará sucesivamente Madre y después Hija. Si por casualidad se quiere


cambiar a otro sitio el constructor de Hija, el compilador indicará su oposición
a éste.

2.3.12. Métodos abstractos


Un método abstracto es un método del que sólo se da la declaración, pero
sin describir su implementación.

void metnormal ( int numero){


// cuerpo del método
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 27

}
abstract void metabstracto { } ; // sin cuerpo
void metvacio ( ) { ; }

// método no abstracto: hay un cuerpo, aunque no


// haga nada.
void otrometodo ( ) {
// cuerpo de otrometodo ( ) ;
}

Una clase de la que uno de los métodos sea abstracto es a su vez llamada
abstracta. Debe declararse como tal en el programa:

abstract class abstracta {


abstract void metabstracto ( ) ;
}

Si se dene una clase como abstracta cuando ninguno de sus métodos lo es, el
compilador no lo considera como un error. Pero la clase es entonces efectivamente
considerada como abstracta.
Si se olvida de añadir abstract delante de la declaración de una clase que
posee métodos abstractos, el compilador considera ésto como un error.
En denitiva, ¾ para qué sirven las clases abstractas ?, para denir conceptos
incompletos que deben ser completados en las subclases de la clase abstracta.
Así,

abstract class vehiculo {


int numpasajeros;
int peso;
abstract void arrancar ( );
abstract void correr ( );
void transpasajeros (int num) {
numpasajeros = num;
arrancar ( );
correr ( );
}
}

Se tienen los conocimientos comunes a sus subclases autobus y coche. En ambos


casos hay pasajeros a embarcar antes de arrancar ( ) y de correr ( ). Por el
contrario, arrancar ( ) y correr ( ) son diferentes en ambas subclases.
Si se hiciera una referencia a una clase abstracta, daría un error porque esta
clase no está completamente denida.
Si las subclases autobus y coche no dieran un cuerpo a los métodos abs-
tractos, éstas deben declararse también como abstractas y no se pueden utilizar
directamente.
Si no se quiere hacer usando clases abstractas, hay dos formas posibles para
ello. La primera de éstas sería dar a vehiculo un cuerpo para cada uno de sus
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 28

métodos, incluyendo los que no deben llamarse nunca porque deben ser rede-
nidos en las subclases. Debe tenerse en cuenta que ha de protegerse las clases de
una inesperada llamada. a sus métodos. La otra solución sería no denir vehicu-
lo y copiar y pegar entre las clases autobus y coche. Esto sería muy malo para
el mantenimiento del programa. Además, no se podría utilizar el polimorsmo,
que es posible con las clases abstractas. Una variable que es estáticamente de un
tipo abstracto puede pasarse como parámetro o ser el atributo de una clase. El
compilador sabe que en la ejecución es en realidad un objeto de una subclase que
será el parámetro o atributo, pero que este objeto respetará las especicaciones
de la clase abstracta.

2.4. Interfaz
Una interfaz es una clase:

en la que todos los métodos son abstractos;

en la que todos los atributos son nal.

No es necesario añadir las palabras clave abstract y nal en la declaración de


una interface, porque la palabra clave interface al principio de la declaración de
la clase basta:

interface vehiculo{
/* final */ int numruedas = 2;
/* abstract */ void metodo ();
}

Una interfaz es en realidad una especicación formal de la clase. Se dene lo


que las subclases deben ofrecer como servicios, dejándoles la libertad de imple-
mentación.
Cuando se hereda de una interface, se implementa, lo que se traduce por la
sintaxis siguiente:

class Hija implements Madreinterface {

Observe que una interfaz puede heredar (extends) en otra interfaz.


Una clase puede tener la implementación de más de una interfaz.

2.5. Paquetes y clases públicas


2.5.1. Paquetes
Los paquetes se hacen corresponder con un conjunto de clases que trabajan
conjuntamente sobre el mismo ámbito. Es una facilidad ofrecida por Java para:

agrupar sintácticamente clases que van juntas conceptualmente y

denir un alto nivel de protección para los atributos y los métodos.


CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS 29

Las clases que pertenecen, por ejemplo, al paquete java.awt (que son las grá-
cas):

están situadas en fuentes cuya primera línea de código es package java.awt;

están situadas en un directorio java/awt de uno de los directorios denidos


por la variable de entorno CLASSPATH.

Como se ve, es fácil crear paquetes. Estos, los paquetes, permiten utilizar los
modos de protección protected y friendly.

2.5.2. Clases públicas


Cada archivo Java incluye una sola, y solo una, clase public. Esta clase debe
llevar el mismo nombre que el archivo, exceptuando el sujo. Esta es la única
clase utilizable en las otras fuentes.
Las clases no public sólo son utilizables en el interior de la fuente en la que
son denidas.
Sólo hay una clase public por archivo y la fuente de una clase está obligato-
riamente en un solo archivo.
No hay clases private o private protected. La palabra clave public, colocada
delante del nombre de una clase, no tiene consecuencias sobre la protección de
los atributos de esta clase. Esto no signica que los atributos sean public de
modo predeterminado, sino que la clase es visible desde el exterior del archivo.
En la mayoría de los casos se escribe sólo una clase por archivo.
Capítulo 3

Estructuras de datos
Las estructuras de datos siempre se han entendido en la programación como
aquellos elementos que permiten la manipulación de una colección de datos. Su
clasicación típica ha sido la siguiente:

Estructuras estáticas: Aquellas que son creadas con un tamaño jo y que no
pueden cambiar de tamaño durante la ejecución del programa. Por ejemplo
un array.

Estructuras dinámicas: Aquellas que no tienen un tamaño jo y que pueden


variar durante la ejecución del programa. Pro ejemplo un archivo, listas
enlazadas, pilas, colas, tablas hashing, árboles y grafos.

Las estructuras estáticas son más fáciles de usar, pero evidentemente son más
limitadas. Siempre existe el peligro de declarar el tamaño por exceso o por
defecto en el programa.
El uso de estructuras de datos estáticas ya ha sido visto en la primera parte
del curso y a partir de ahora se verán únicamente las estructuras dinámicas.
Bajo el enfoque de programación orientada a objetos existe una forma orde-
nada de programar, que es la creación de dos clases distintas:

Una clase administradora de la estructura de datos, que se encarga de las


operaciones básicas de todo conjunto de datos: inserción, borrado, bús-
queda, etc.

Una clase nodo, que contiene los datos que se quieren manipular. La clase
administradora hará la manipulación de múltiples instancias de la clase
nodo.

La única excepción a esta regla es la de la programación de archivos, en la cual


pueden existir múltiples enfoques.

3.1. Arrays
En Java los arrays son objetos, por lo tanto deben ser creados usando el
operador new, indicando el tamaño del array en el momento de su creación .
Un ejemplo de declaración de un array:

30
CAPÍTULO 3. ESTRUCTURAS DE DATOS 31

int buffer[ ] = new int [10];

Así se declara que la variable buer es un array de enteros y tendrá espacio para
10 enteros.
Ejemplo:

char alfabeto[ ] = new char [28];

Declara un array de caracteres llamado alfabeto que contendrá 28 caracteres.


Algunos tipos de datos que en otros lenguajes se tratan como tipos de datos
básicos, en Java se consideran como objetos y se le pueden pasar mensajes. Por
ejemplo para obtener el número de elementos de un vector se envía el mensaje
length al array.
Por ejemplo la expresión:

vector.length( );

está compuesta por un objeto receptor (vector) y un mensaje (length( )). El


resultado de este mensaje es un entero que representa el número de elementos
del array.
Un manejo adecuado de arrays dentro de la orientación a objetos es hacer
uso de una clase que sea la administradora del array. Esta clase contiene las
operaciones elementales que toda colección de datos debe tener: insertar, borrar,
buscar, etc.
El siguiente ejemplo muestra el uso de una clase administradora de arrays.
Ejemplos de arrays con manipulación de objetos

import java.io.*;
class Person
{
private String lastName;
private String firstName;
private int age;
//-----------------------------------------------------------
public Person(String last, String first, int a)
{ // constructor
lastName = last;
firstName = first;
age = a;
}
//-----------------------------------------------------------
public void displayPerson()
{
System.out.print(" Apellido : " + lastName);
System.out.print(", Nombre : " + firstName);
System.out.println(", Edad: " + age);
}
//-----------------------------------------------------------
public String getLast() // obtener apellido
{ return lastName; }
CAPÍTULO 3. ESTRUCTURAS DE DATOS 32

}
class ClassDataArray
{
private Person[] a; // referencia
private int nElems; // número de elementos
//-----------------------------------------------------------
public ClassDataArray(int max) // constructor
{
a = new Person[max]; // crear el array
nElems = 0; // sin elementos
}
//-----------------------------------------------------------
public Person find(String searchName)
{ // buscar un valor
int j;
for(j=0; j<nElems; j++)
if( a[j].getLast().equals(searchName) )

break;
if(j == nElems)
return null;
else
return a[j];
}
//-----------------------------------------------------------
// insertar objeto Person
public void insert(String last, String first, int age)
{
a[nElems] = new Person(last, first, age);
nElems++; // incrementar
}
//-----------------------------------------------------------
public boolean delete(String searchName)
{ // eliminar Person
int j;
for(j=0; j<nElems; j++)
if( a[j].getLast().equals(searchName) )

break;
if(j==nElems)
return false;
else
{
for(int k=j; k<nElems; k++)
a[k] = a[k+1];
CAPÍTULO 3. ESTRUCTURAS DE DATOS 33

nElems--;
return true;
}
}
//-----------------------------------------------------------
public void displayA() // imprimir contenido
{
for(int j=0; j<nElems; j++)
a[j].displayPerson();
}
//-----------------------------------------------------------
}
class ClassDataApp
{
public static void main(String[] args)
{
int maxSize = 100;
ClassDataArray arr; // referencia
arr = new ClassDataArray(maxSize); // crear el array
// insertar 10 items
arr.insert("Evans", "Patty", 24);
arr.insert("Smith", "Lorraine", 37);
arr.insert("Yee", "Tom", 43);
arr.insert("Adams", "Henry", 63);
arr.insert("Hashimoto", "Sato", 21);
arr.insert("Stimson", "Henry", 29);
arr.insert("Velasquez", "Jose", 72);
arr.insert("Lamarque", "Henry", 54);
arr.insert("Vang", "Minh", 22);
arr.insert("Creswell", "Lucinda", 18);
arr.displayA(); // ver los items
String searchKey = "Stimson"; // buscar uno
Person found;
found=arr.find(searchKey);
if(found != null)
{
System.out.print("Encontrado ");
found.displayPerson();
}
else

System.out.println("No se puede encontrar " + searchKey);


System.out.println("Eliminando Smith, Yee, y Creswell");
arr.delete("Smith"); // borrar 3 items
arr.delete("Yee");
arr.delete("Creswell");
arr.displayA(); // imprimir items
}
CAPÍTULO 3. ESTRUCTURAS DE DATOS 34

3.2. Archivos
El manejo de archivos en Java implica el uso de dos grupos de clases. Están
las clases que permiten hacer manipulaciones de archivos a nivel de sistema
operativo y las que permiten hacer las operaciones sobre ellos a modo de ujo
de bytes.

3.2.1. Clase File


La clase File esta se utiliza para encapsular la interacción de los programas
con el sistema de archivos. Mediante la clase File no se limita a leer el contenido
del archivo, como ocurría con la clase FileInputStream, sino que se puede obtener
información adicional, como el tamaño del archivo, su tipo, su fecha de creación,
los permisos de acceso que se tienen con él, etc.
Además, la clase File es la única forma que se tiene de trabajar con directorios
(crearlos, ver los archivos que contienen, cambiar el nombre o borrar los archivos,
etc.).
La forma más sencilla de crear un objeto File es:

File elArchivo = new File("c:\\cursojava\\elarchivo.txt");

Es muy importante darse cuenta de la diferencia entre un objeto de tipo File y el


archivo o directorio al que se reere. Por ejemplo, el archivo c:\cursojava\elarchivo.txt
que aparece en el fragmento de código anterior no tiene porque existir. Para sa-
ber si un objeto File se reere a un archivo existente podemos usar el método
exists(), ejemplo:

File elArchivo = new File("c:\\cursojava\\elarchivo.txt");


if (elArchivo.exists()) {
// El archivo existe
}
else {
// El archivo no existe
}

3.2.2. Flujos de bytes (streams)


Un ujo es un sistema de comunicación implementado en el paquete java.io
cuyo n es guardar y recuperar la información en cada uno de los diversos
dispositivos de almacenamiento.
Se puede imaginar un ujo como un tubo donde se puede leer o escribir
bytes. No importa lo que pueda haber en el otro extremo del tubo: Puede ser un
teclado, un monitor, un archivo, un proceso, una conexión TCP/IP o un objeto
de Java.
CAPÍTULO 3. ESTRUCTURAS DE DATOS 35

Todos los ujos que aparecen en Java englobados en el paquete java.io, per-
tenecen a dos clases abstractas comunes: java.io.InputStream para los ujos de
Entrada (aquellos de los que se puede leer) y java.io.OutputStream para os ujos
de salida (aquellos en los que se puede escribir).
Estos ujos, pueden tener orígenes diversos (un archivo, un socket TCP,
etc.), pero una vez que se tiene una referencia a ellos, se puede trabajar siempre
de la misma forma: leyendo datos mediante los métodos de la familia read() o
escribiendo datos con los métodos write().
Hay formas de manipular ujos como:

Flujos de bytes (tipos básicos de datos)

Flujos de caracteres (solamente caracteres)

Hay ujos de entrada y salida:

Un ujo de entrada envía datos de una fuente a un programa

Un ujo de salida envía datos de un un programa hacia un destino

3.2.3. Flujos de accesos a archivos


Ya sea para leer o para escribir en un archivo, estos se manipulan con ujos
de acceso a archivos. En Java existe la clase FileInputStream , con los métodos
necesarios para abrir e interactuar con un canal de comunicación hacia un ar-
chivo de entrada para nuestra aplicación, y la clase FileOutputStream para el
caso de un archivo de salida.
La clases FileInputStream y FileOutputStream reciben en uno de sus cons-
tructores como parámetro el nombre del archivo a leer o escribir. Hay otras dos
variantes: una que recibe un objeto de tipo File y otra que recibe un objeto de
tipo FileDescriptor.

3.2.4. Uso de Streams de Archivos Secuenciales


Los streams de archivos son quizás los más fáciles de entender. Simple-
mente se usa el stream de archivos -FileReader, FileWriter, FileInputStream,
y FileOutputStream- cada uno de lectura o escritura sobre un archivo del siste-
ma de archivos nativo.
Se puede crear un stream de archivo desde un nombre de archivo en el
formato de un string, desde un objeto File, o desde un objeto FileDescriptor.
El siguiente programa Copy usa FileReader y FileWriter para copiar el con-
tenido de un archivo llamado farrago.txt en otro archivo llamado outagain.txt.

import java.io.*;
public class Copy {
public static void main(String[] args) throws IOException {

File inputFile = new File("farrago.txt");


File outputFile = new File("outagain.txt");
FileReader in = new FileReader(inputFile);
FileWriter out = new FileWriter(outputFile);
int c;
while ((c = in.read()) != 1)
CAPÍTULO 3. ESTRUCTURAS DE DATOS 36

out.write(c);
in.close();
out.close();
}
}

Este programa es muy sencillo.

1. Abre FileReader sobre farrago.txt y abre FileWriter sobre outagain.txt.

2. El programa lee caracteres desde el reader mientras haya más entradas en


el archivo de entrada. El método read() lee un byte y lo devuelve en forma
de entero. Cuando no encuentra nada más que leer revuelve el valor de -1.

3. Cuando la entrada se acaba, el programa cierra tanto el reader como el


writer.

Observar el código que usa el programa Copy para crear un FileReader.

File inputFile = new File("farrago.txt");


FileReader in = new FileReader(inputFile);

Este código crea un objeto File que representa el archivo nombrado en el sistema
de archivos nativo. File es una clase de utilidad proporcionada por java.io. Este
programa usa este objeto sólo para construir un FileReader sobre farrago.txt.
Sin embargo, se podría usar inputFile para obtener información sobre farra-
go.txt, como su path completo.
Después de haber ejecutado el programa, se debe encontrar una copia exacta
de farrago.txt en un archivo llamado outagain.txt en el mismo directorio.
Las clases FileReader y FileWriter son las más sencillas pero no son las
únicas para manipular archivos secuenciales.
Para el caso más general, cuando se desea trabajar con otros tipo de datos
que no sean caracter, se usan las clases DataInputStream y DataOutputStream.
Estas clases tienen métodos distintos para hacer cada lectura y escritura sobre
los tipos básicos de datos.
Ejemplo de manipulaciones de lectura:

double[] precios={1350, 400, 890, 6200, 8730};


int[] unidades={5, 7, 12, 8, 30};
String[] descripciones={"paquetes de papel", "lápices",
"bolígrafos", "carteras", "mesas"};
DataOutputStream salida=new DataOutputStream(new FileOutputStream("pedido.txt"));
for (int i=0; i<precios.length; i ++) {
salida.writeChars(descripciones[i]);
salida.writeChar('\n');
salida.writeInt(unidades[i]);
salida.writeChar('\t');
salida.writeDouble(precios[i]);
}
salida.close();
CAPÍTULO 3. ESTRUCTURAS DE DATOS 37

Ejemplo de manipulaciones de lectura:

double precio;
int unidad;
String descripcion;
double total=0.0;
DataInputStream entrada=new DataInputStream(new FileInputStream("pedido.txt"));
try {

while ((descripcion=entrada.readLine())!=null) {

unidad=entrada.readInt();
entrada.readChar(); //lee el carácter tabulador
precio=entrada.readDouble();
System.out.println("has pedido "+unidad+" "+
descripcion+" a "+precio+" soles.");
total=total+unidad*precio;
}
}catch (EOFException e) {}
System.out.println("por un TOTAL de: "+total+" soles");
entrada.close();

3.2.5. Trabajar con Archivos de Acceso Aleatorio


Hasta ahora los streams de entrada y salida que se han visto han sido
streams de acceso secuencial -streams cuyo contenido debe ser leído o escrito
secuencialmente-. A pesar de su increíble utilidad, los archivos de acceso secuen-
cial son una consecuencia de un medio secuencial como una cinta magnética.
Los archivos de acceso aleatorio, por otro lado, permiten acceso no secuencial,
o aleatorio, a los contenidos de un archivo.
Al contrario que las clases de streams de entrada y salida del paquete java.io,
RandomAccessFile se usa tanto para leer como para escribir. Se crea un objeto
RandomAccessFile con diferentes argumentos dependiendo si se intenta leer o
escribir.
La clase RandomAccessFile implementa los interfaces DataInput y DataOut-
put y por lo tanto puede usarse para leer y escribir. RandomAccessFile es similar
a FileInputStream y FileOutputStream en que se especica un archivo del siste-
ma de archivos nativo para abrirlo cuando es creado. Es posible hacer esto con
un nombre de archivo o un objeto File. Cuando se crea un RandomAccessFile,
se debe indicar si sólo se quiere leer o también escribir en el archivo. (hay que
poder leer un archivo para poder escribirlo). La siguiente línea de código cea un
RandomAccessFile que lee el archivo llamado farrago.txt:

new RandomAccessFile("farrago.txt", "r");

Y esta abre el mismo archivo tanto para lectura como para escritura:

new RandomAccessFile("farrago.txt", "rw");


CAPÍTULO 3. ESTRUCTURAS DE DATOS 38

Después de haber abierto el archivo, se pueden usar los métodos comunes re-
adXXX o writeXXX para realizar I/O en el archivo. Los métodos para lectura
son:

byte readByte()

int readUnsignedByte()

short readShort()

int readUnsignedShort()

char readChar()

int readInt()

long readLong()

oat readFloat()

double readDouble()

String readLine()

Los métodos para escritura son:

void writeBoolean( boolean b );

void writeByte( int i );

void writeShort( int i );

void writeChar( int i );

void writeInt( int i );

void writeFloat( oat f );

void writeDouble( double d );

void writeBytes( String s );

void writeChars( string s );

RandomAccessFile soporta la noción de puntero de archivo. Este puntero indica


la posición actual en el archivo, cuando el archivo se crea por primera ver, el
puntero de archivo es 0, indicando el principio del archivo. Las llamadas a los
métodos readXXX y writeXXX ajustan la posición del puntero de archivo según
el número de bytes leídos o escritos.
También se tienen muchos métodos para moverse dentro de un archivo:

long getFilePointer();

Devuelve la posición actual del puntero del chero

void seek( long pos );


CAPÍTULO 3. ESTRUCTURAS DE DATOS 39

Coloca el puntero del chero en una posición determinada. La posición se da


como un desplazamiento en bytes desde el comienzo del chero. La posición 0
marca el comienzo de ese chero.

long length();
Devuelve la longitud del chero. La posición length() marca el nal de ese -
chero.
Ejemplo que añade una cadena de caracteres a un archivo existente:

import java.io.*;
// Cada vez que ejecutemos el programa, se incorporara una nueva
// linea al fichero de log que se crea la primera vez que se ejecuta
//
class Log {

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

RandomAccessFile miRAFile;
String s = "Informacion a incorporar\n";
// Abrimos el fichero de acceso aleatorio
miRAFile = new RandomAccessFile( "C:\\tmp\\java.log","rw" );
// Nos vamos al final del fichero
miRAFile.seek( miRAFile.length() );

// Incorporamos la cadena al fichero


miRAFile.writeBytes( s );
// Cerramos el fichero
miRAFile.close();
}
}

3.3. Serialización de Objetos


El paquete java.io tiene otros dos streams de bytes ObjectInputStream y
ObjectOutputStream que funcionan como los otros streams de entrada y salida.
Sin embargo, son especiales porque pueden leer y escribir objetos.
La clave para escribir objetos es representar su estado de una forma seriali-
zada suciente para reconstruir el objeto cuando es leído.
Por eso, leer y escribir objetos es un proceso llamado serialización de objetos.
Escribir objetos a un stream es un proceso sencillo. Por ejemplo, aquí se
obtiene la hora actual en milisegundos construyendo un objeto Date y luego se
serializa ese objeto.

FileOutputStream out = new FileOutputStream("theTime");


ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();
CAPÍTULO 3. ESTRUCTURAS DE DATOS 40

ObjectOutputStream es un stream de proceso, por eso debe construirse sobre


otro stream. Este código construye un ObjectOutputStream sobre un FileOut-
putStream, para serializar el objeto a un archivo llamado theTime.
Luego, el string Today y un objeto Date se escriben en el stream con el
método writeObject de ObjectOutputStream.
Si un objeto se reere a otro objeto, entonces todos los objetos que son
alcanzables desde el primero deben ser escritos al mismo tiempo para poder
mantener la relación entre ellos. Así, el método writeObject serializa el objeto
especicado, sigue sus referencias a otros objetos recursivamente, y también los
escribe todos.
El stream ObjectOutputStream implementa el interface DataOutput que
dene muchos métodos para escribir tipos de datos primitivos, como writeInt,
writeFloat, o writeUTF. Se pueden usar estos métodos para escribir tipos de
datos primitivos a un ObjectOutputStream.
El método writeObject lanza una NotSerializableException si el objeto dado
no es serializable. Un objeto es serializable sólo si clase implementa el interface
Serializable.
Una vez que hemos escrito objetos y tipos de datos primitivos en un stream,
se quiere leerlos de nuevo y reconstruir los objetos. Esto también es sencillo.
Aquí está el código que lee el String y el objeto Date que se escribieron en el
archivo llamado theTime del último ejemplo.

FileInputStream in = new FileInputStream("theTime");


ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();

Cómo ObjectOutputStream, ObjectInputStream debe construirse sobre otro


stream. En este ejemplo, los objetos fueros archivados en un chero, por eso
el código construye un ObjectInputStream sobre un FileInputStream. Luego,
el código usa el método readObject de ObjectInputStream para leer el String
y el objeto Date desde el chero. Los objetos deben ser leídos desde el stream
en el mismo orden en que se escribieron. Observa que el valor de retorno de
readObject es un objeto que es forzado y asignado a un tipo especíco.
El método readObject des-serializa el siguiente objeto en el stream y revisa
sus referencias a otros objetos recursivamente para des-serializar todos los ob-
jetos que son alcanzables desde él. De esta forma, mantiene la relación entre los
objetos.
El stream ObjectInputStream implementa el interface DataInput que dene
métodos para leer tipos de datos primitivos. Los métodos de DataInput son
paralelos a los denidos en DataOutput para escribir tipos de datos primitivos.
Entre ellos se incluyen readInt, readFloat, y readUTF. Se usan estos métodos
para leer tipos de datos primitivos desde un ObjectInputStream.

3.3.1. Proporcionar Serialización de Objetos


Un objeto es serializable sólo si su clase implementa el interface Serializable.
Así, si queremos serializar un ejemplar de una de nuestras clases, la clase debe
implementar este interface.
CAPÍTULO 3. ESTRUCTURAS DE DATOS 41

Las buenas noticias es que Serializable es un interface vacío. Es decir, no


contiene ninguna declaración de método; su propósito es simplemente identicar
las clases cuyos objetos son serializables.
Crear ejemplares de una clase serializable es fácil. Sólo hay que añadir la
cláusula implements Serializable a la declaración de la clase:

public class MySerializableClass implements Serializable {


...
}
No hay que escribir ningún método. La serialización de un ejemplar de esta clase
la maneja el método defaultWriteObject de ObjectOutputStream.
Es posible personalizar la serialización de las clases proporcionando dos mé-
todos para ella: writeObject y readObject.
El método writeObject controla la información que se graba. Normalmente
se usa para añadir información adicional al stream. El método readObject lee la
información escrita por el correspondiente método writeObject o puede usarse
para actualizar el estado del objeto después de haber sido restaurado.
El método writeObject debe declarase exactamente como se muestra en el
siguiente ejemplo. Lo primero que debe hacer es llamar al método defaultWri-
teObject para realizar la serialización por defecto. Cualquier ajuste puede rea-
lizarse después.

private void writeObject(ObjectOutputStream s)


throws IOException {
s.defaultWriteObject();
// colocar el código personalizado
}
El método readObject debe leer todo lo escrito por writeObject en el mismo
orden en que se escribió. El método readObject también puede realizar cálculos o
actualizar el estado del objeto de alguna forma. Aquí está el método readObject
que corresponde al método writeObject anterior:

private void readObject(ObjectInputStream s)


throws IOException {
s.defaultReadObject();
// código personalizado
...
// código para actualizar el objeto, si es necesario
}
El método readObject debe declarase exactamente como se ha mostrado.
Los métodos writeObject y readObject son responsables de serializar só-
lo las clases inmediatas. Cualquier serialización requerida por la superclase se
maneja automáticamente. Sin embargo, una clase que necesita coordinarse ex-
plícitamente con su superclase para serializarse puede hacerlo implementando
el interface Externalizable.
Se coloca a continuación un ejemplo de código con serialización de objetos

public class cliente implements Serializable {


CAPÍTULO 3. ESTRUCTURAS DE DATOS 42

private String nombre = null, domicilio = null;


private int telefono, fax;
public cliente(String nombre, String domicilio, int telefono,
int fax) {
this.nombre = nombre;
this.domicilio = domicilio;
this.telefono = telefono;
this.fax = fax;
}
public String getNombre() {
return nombre;
}
public String getDirec() {
return domicilio;
}
public int getTel() {
return telefono;
}
public int getFax() {
return fax;
}
public String toString() {

return "Nombre: " + nombre + ", " + domicilio + ", Tel:"


+ telefono + ", Fax: " + fax;
}
}

Guardando un objeto en un archivo

try {

ObjectOutputStream salidaDatos = new ObjectOutputStream(new


FileOutputStream("clientes.dat"));
salidaDatos.writeObject(miCliente);
salidaDatos.close();
} catch(IOException ex) {

System.out.println("Error al guardar datos " + ex.toString());


}

Recuperando un objeto de un archivo

try {
try {
CAPÍTULO 3. ESTRUCTURAS DE DATOS 43

ObjectInputStream entradaDatos = new ObjectInputStream(new


FileInputStream("clientes.dat"));
miCliente = (cliente)(entradaDatos.readObject());
entradaDatos.close();
System.out.println("Datos existentes recuperados.");
} catch (ClassNotFoundException ex) {

System.out.println("Error al recuperar datos " + ex.toString());


}
} catch(IOException ex) {

System.out.println("Error de entrada de datos " + ex.toString());


}

3.4. Estructuras dinámicas


Las estructuras de datos dinámicas se caracterizan porque pueden cambiar
de tamaño durante la ejecución del programa. Son capaces de crecer o disminuir
dependiendo de la cantidad de elementos que deban de almacenar.
En el caso de los arrays siempre se crean de un tamaño jo y no pueden variar.
En las estructuras dinámicas se debe de hacer uso de operaciones de manejo
dinámico de memoria para poder crear nuevos elementos de la estructura.
Dentro de las estructuras dinámicas más usadas se encuentran:

listas enlazadas

pilas

colas

tablas hash

árboles

Muchos lenguajes de programación incluyen librerías para el manejo de estas


estructuras de datos. En Java existen librerías de código para su trabajo.
Desde la versión 1.5 de Java las librerías de uso de estructuras dinámicas
son implementadas con el uso de Genéricos.

3.4.1. Genéricos
Programación genérica signica escribir código que puede ser reusado por
objetos de tipos distintos.
Una clase genérica es una clase creada con uno o más tipos de variables. Se
coloca el código de una clase Pareja:

public class Pareja<T>


{
private T first;
CAPÍTULO 3. ESTRUCTURAS DE DATOS 44

private T second;
public Pareja() {
first = null;
second= null;
}
public Pareja(T first, T second)
{
this.first= first;
this.second = second;
}
public T getFirst() {return first;}
public T getSecond() {return second;}
public void setFirst(T nuevo) { first = nuevo;}
public void setSecond(T nuevo) { second = nuevo;}
}

En la declaración de la clase Pareja se introduce el tipo variable T después del


nombre de la clase. Una clase genérica puede tener más de un tipo de variable.
Por ejemplo:

public class Pareja<T,U> {... }

Se puede luego crear un objeto de la clase genérica que tenga la especicación


del tipo de dato exacto que se va a manejar:

Pareja<String> mm = new Pareja<String>();


mm.setFirst(Hola);

Desde el momento en el que se declara el objeto mm como un objeto de la clase


Pareja<String>, se debe de trabajar como si en la declaración de la clase Pareja
el tipo String estuviera en lugar del tipo genérico T.
De la misma manera se puede crear otro objeto con la misma clase genérica
usando otro tipo de dato en su declaración:

Pareja<int> ab = new Pareja<int>();


ab.setSecond(56);

Antes del uso de la programación con genéricos se lograba algo parecido usando
parámetros de tipo Object. Por polimorsmo cualquier objeto siempre se podía
comportar como un objeto de tipo Object. La desventaja de este enfoque es que
no era restrictivo, se podían mezclar objetos distintos tipos en las clases creadas.
Para los ejemplos expuestos el objeto mm solamente puede aceptar parámetros
tipo String para sus métodos y producirá un error si se quieren usar otros tipos
de datos.

3.4.2. Librería de colecciones


El lenguaje Java tiene unas librería con clases para el manejo de distintas
estructuras dinámicas. Esta librería tiene algunas características
CAPÍTULO 3. ESTRUCTURAS DE DATOS 45

Las clases se basan en el uso de genéricos. Se debe especicar el tipo


de datos que la colección va a almacenar. Una vez creada la colección
solamente manipulará ese tipo de datos y no otros

Existe una separación entre las interfaces y las implementaciones. De hecho


existen varias implementaciones de las mismas interfaces, donde cada una
aporta ciertas ventajas dependiendo de las necesidades del problema a
resolver.

Las interfaces poseen los métodos ordinarios para el manejo de las colecciones
de datos: agregar, eliminar, buscar, contar, etc. Además algunos brindan opera-
ciones más avanzadas, para trabajar con varias colecciones a la vez: intersección,
diferencia, unión, etc.

3.4.2.1. Interfaces Collection e Iterator


Las interfaces Collection e Iterator son las básicas de la librería de colec-
ciones. La primera se reere al funcionamiento de los objetos encargados del
almacenamiento de una colección de objetos. La segunda se reere a las funcio-
nes que tienen los objetos de tipo iterador.
El iterador es un objeto especial, que funciona a modo de variable auxiliar
para poder recorrer los objetos que se encuentran almacenados en la colección.
La interfaz Collection es genérica, aceptando un tipo de datos variable. Sus
métodos son los siguientes:
java.util.Collection<E>

Iterator<E> iterator() devuelve un iterador que puede ser usado para visi-
tar los objetos de la colección

int size() devuelve la cantidad de elementos que se encuentran almacenados


boolean isEmpty() devuelve verdadero si la colección no tiene elementos
boolean contains(Object obj) devuelve verdadero si la colección tiene un
objeto obj

boolean containsAll(Collection<?> other) devuelve verdadero si la colec-


ción contiene todos los elementos de other

boolean add(Object element) agrega element a la colección. Devuelve ver-


dadero si la colección ha cambiado.

boolean addAll(Collection<? extends E> other) agrega todos los elemen-


tos de other. Devuelve verdadero si la colección ha cambiado.

boolean remove(Object obj) Elimina obj de la colección. Devuelve verda-


dero si obj fue encontrado y eliminado.

boolean removeAll(Collection<?> other) Elimina de la colección los ele-


mentos que se encuentren en other. Devuelve verdadero si la colección ha
cambiado.

void clear() elimina todos los elementos de la colección


CAPÍTULO 3. ESTRUCTURAS DE DATOS 46

boolean retainAll(Collection<?> other) Elimina todos los elementos de


la colección que no se encuentren en other. Devuelve verdadero si la colec-
ción ha cambiado.

Object[] toArray() Devuelve un array con todos los elementos de la colección

Los siguientes son los elementos del Iterator.


java.util.Iterator<E>

boolean hasNext() Devuelve verdadero si hay otro elemento posible a visitar


en la colección.

E next() Devuelve el siguiente elemento a visitar


void remove() Elimina el último objeto visitado. Este método debe seguir la
visita a un elemento

3.4.2.2. Clases de la librería


La librería de manejo de colecciones de Java tiene implementada varias cla-
ses concretas. Cada una de ellas tiene un principio de funcionamiento distinto,
que trata de solucionar ciertos problemas comunes relacionados al manejo de
colecciones.
Las clases de la librería son:
Clase Descripción

ArrayList Secuencia indexada que crece y disminuye


atutomáticamente
LinkedList Secuencia ordenada que permite inserciones y
eliminaciones ecientes en toda posición
HashSet Colección desordenada que no acepta duplicados
TreeSet Conjunto ordenado
EnumSet Conjunto de tipos enumerados
LinkedHashSet Conjunto que recuerda el orden en que los elementos
fueron insertados
PriorityQueue Colección que permite eliminación eciente del
elemento más pequeño
HashMap Estructura que almacena asociaciones de llave/valor
TreeMap Mapa en el que las llaves están ordenadas
EnumMap Mapa donde las llaves son del tipo enumerado
LinkedHashMap Mapa que recuerda el orden en el que los elementos
fueron insertados
WeakHashMap Mapa donde los elementos pueden ser eliminados por
el garbage collector si no están siendo usados
IdentityHashMap

3.4.3. Listas enlazadas


Una lista enlazada es una serie de nodos, conectados entre sí a través de una
referencia, en donde se almacena la información de los elementos de la lista. Por
lo tanto, los nodos de una lista enlazada se componen de dos partes principales:
CAPÍTULO 3. ESTRUCTURAS DE DATOS 47

class NodoLista
{
Object elemento;
NodoLista siguiente;
}

La referencia contenida en el nodo de una lista se denomina siguiente, pues indica


en dónde se encuentra el siguiente elemento de la lista. El último elemento de
la lista no tiene nodo siguiente, por lo que se dice que la referencia siguiente del
último elemento es null (nula).
La siguiente gura muestra un ejemplo de una lista enlazada cuyos elementos
son strings:

La referencia lista indica la posición del primer elemento de la lista y permite


acceder a todos los elementos de ésta: basta con seguir las referencias al nodo
siguiente para recorrer la lista.

NodoLista aux=lista;

aux=aux.siguiente;

Siguiendo con el ejemplo anterior, para insertar un nuevo nodo justo delante del
nodo referenciado por aux se deben modicar las referencias siguiente del nodo
aux y del nodo a insertar.
CAPÍTULO 3. ESTRUCTURAS DE DATOS 48

NodoLista nuevo=new NodoLista(...);


//"nuevo" es la referencia del nodo a insertar en la lista
nuevo.siguiente=aux.siguiente;
aux.siguiente=nuevo;
//Nótese que no es lo mismo realizar los cambios de referencia
//en un orden distinto al presentado, puesto que en ese caso
//se "pierde" la lista desde el nodo siguiente a aux
El procedimiento presentado a continuación es un ejemplo de cómo se programa
el recorrido de una lista enlazada. Se supondrá que los objetos almacenados en
cada nodo son strings:

void recorrido(NodoLista lista)


{
NodoLista aux=lista;
while (aux!=null)
{
System.out.println(aux.elemento);
aux=aux.siguiente;
}
}
Para invertir el orden de la lista, es decir, que el último elemento de la lista ahora
sea el primero, que el penúltimo elemento de la lista ahora sea el segundo, etc...,
modicando sólo las referencias y no el contenido de los nodos, es necesario
realizar una sola pasada por la lista, y en cada nodo visitado se modica la
referencia siguiente para que apunte al nodo anterior. Es necesario mantener
referencias auxiliares para acordarse en donde se encuentra el nodo anterior y
el resto de la lista que aún no ha sido modicada:

void invertir(NodoLista lista)


{
NodoLista siguiente=lista;
NodoLista anterior=null;
while(lista!=null)
{
CAPÍTULO 3. ESTRUCTURAS DE DATOS 49

siguiente=lista.siguiente;
lista.siguiente=anterior;
anterior=lista;
lista=siguiente;
}
}

La implementación vista de los nodos también se conoce como lista de enlace


simple, dado que sólo contiene una referencia al nodo siguiente y por lo tanto
sólo puede recorrerse en un solo sentido. En una lista de doble enlace se agrega
una segunda referencia al nodo previo, lo que permite recorrer la lista en ambos
sentidos, y en general se implementa con una referencia al primer elemento y
otra referencia al último elemento.

Una lista circular es aquella en donde la referencia siguiente del último nodo
en vez de ser null apunta al primer nodo de la lista. El concepto se aplica tanto
a listas de enlace simple como doblemente enlazadas.

En muchas aplicaciones que utilizan listas enlazadas es útil contar con un


nodo cabecera, también conocido como dummy o header, que es un nodo "falso",
ya que no contiene información relevante, y su referencia siguiente apunta al
primer elemento de la lista. Al utilizar un nodo cabecera siempre es posible
denir un nodo previo a cualquier nodo de la lista, deniendo que el previo al
primer elemento es la cabecera.

Si se utiliza un nodo cabecera en una lista de doble enlace ya no es necesario


contar con las referencias primero y último, puesto que el nodo cabecera tiene
ambas referencias: su referencia siguiente es el primer elemento de la lista, y su
referencia anterior es el último elemento de la lista. De esta forma la lista de
doble enlace queda circular de una manera natural.
CAPÍTULO 3. ESTRUCTURAS DE DATOS 50

3.4.3.1. Uso de listas con la librería


La clase LinkedList proporciona una lista doblemente enlazada ya progra-
mada. Debe ser usada junto con la clase Iterator.
Para la librería no es necesario crear la clase nodo. Simplemente se inserta un
objeto que es el dato que el nodo almacena dentro. El siguient ejemplo muestra
el uso de la clase para la manipulación de una lista enlazada que manipula
Strings:

List<String> staff = new LinkedList<String>();


staff.add(Bruce Wayne);
staff.add(Dick Grayson);
staff.add(Barbara Gordon);
Iterator iter = staff.iterator();
String primero = iter.next();
String segundo = iter.next();
iter.remove(); //elimina el último visitado = Dick Grayson

El método add() de LinkedList inserta elemento al nal de la lista. En caso


de que se desee insertar elementos en una posición intermedia se debe usar el
método add() de la clase ListIterator. Se ve en el siguiente ejemplo:

List<String> staff = new LinkedList<String>();


staff.add(Bruce Wayne);
staff.add(Dick Grayson);
staff.add(Barbara Gordon);
ListIterator iter = staff.ListIterator();
iter.next(); //salta el primer elemento
iter.add(Jason Todd); //inserta el nuevo entre el primero y el segundo

El objeto de tipo ListIterator siempre hace la inserción en la posición previa al


elemento al cual está apuntando. La clase ListIterator hereda de Iterator. Tiene
más métodos especialmente pensados en las operaciones propias de una lista
enlazada doble.
Se explican todas estas operaciones con los siguientes ejemplos:
Uso del set() para remplazar valores:
CAPÍTULO 3. ESTRUCTURAS DE DATOS 51

ListIterator<String> iter = lista.listIterator();


String valorViejo = iter.next(); //leer el primer valor
iter.set(nuevoValor); //colocar un nuevo valor

Uso del prevoius() para navegar por la lista

ListIterator<String> iter = lista.listIterator();


String primero = iter.next(); //leer el primer valor
String segundo = iter.next(); //leer el segundo valor
String Otroprimero = iter.previous(); //leer el primer valor nuevamente

Se coloca a continuación una pequeña aplicación mostrando el uso de la lista


enlazada:

import java.util.*;
public class LinkedListTest
{
public static void main(String[] args)
{
List<String> a = new LinkedList<String>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
List<String> b = new LinkedList<String>();
b.add("Bob");
b.add("Doug");
b.add("Frances");
b.add("Gloria");
// mezclar las palabras de b en a
ListIterator<String> aIter = a.listIterator();
Iterator<String> bIter = b.iterator();
while (bIter.hasNext())
{
if (aIter.hasNext()) aIter.next();
aIter.add(bIter.next());
}
System.out.println(a);
// eliminar cada segunda palabra de b
bIter = b.iterator();
while (bIter.hasNext())
{
bIter.next(); // saltar un elemento
if (bIter.hasNext())
{

bIter.next(); // saltar al siguiente elemento


bIter.remove(); // eliminar ese elemento
}
CAPÍTULO 3. ESTRUCTURAS DE DATOS 52

}
System.out.println(b);
// eliminar de a todas las palabras de b
a.removeAll(b);
System.out.println(a);
}
}

3.4.4. Pilas
Una pila es una estructura de datos en la cual el acceso está limitado al
elemento más recientemente insertado y solamente puede crecer y decrecer por
uno de sus extremos.
Las pilas se denominan también estructuras LIFO (Last-In-First-Out), por-
que su característica principal es que el último elemento en llegar es el primero
en salir.
En todo momento, el único elemento visible de la estructura es el último que
se colocó.
Se dene el tope de la pila como el punto donde se encuentra dicho elemento.
En una pila, las tres operaciones naturales de insertar, eliminar y obtener el
dato, se renombran por push, pop y peek.

3.4.4.1. Uso de la librería


En la librería de colecciones existe la clase Stack, que tiene el siguiente
comportamiento:
java.util.Stack<E>

boolean empty() Prueba si la pila está vacía.


E peek() Devuelve el valor del objeto que va a salir de la pila sin eliminarlo
E pop() Devuelve el valor del objeto que va a salir de la pila y lo elimina de
la pila

E push(E item) Inserta un elemento en la pila


int search(Object o) Retorna la posición del elemento dentro de la pila
Se coloca a continucación una pequeña aplicación de ejemplo haciendo uso de
la clase de la librería.

import java.util.Stack;
public class StackExample {
public static void main(String args[]) {
Stack<String> s = new Stack<String>();
s.push("primero");
s.push("segundo");
s.push("tercero");
System.out.println("Siguiente a salir: " + s.peek());
s.push("cuarto");
System.out.println(s.pop());
CAPÍTULO 3. ESTRUCTURAS DE DATOS 53

s.push(".");
int count = s.search("primero");
while (count != 1 && count > 1) {
s.pop();
count--;
}
System.out.println(s.pop());
System.out.println(s.empty());
}
}

3.4.5. Colas
Una cola es una estructura lineal, en la cual los elementos sólo pueden ser
adicionados por uno de sus extremos y eliminados o consultados por el otro.
Este tipo de estructuras lineales se conocen como estructuras FIFO ( First-
In-First-Out), o sea, el primero en llegar es el primero es salir.
También hay que tener presente, que el único elemento visible en una cola es
el primero y mientras éste no haya salido (eliminado), no es posible tener acceso
al siguiente.

3.4.5.1. Interfaz
No existe en la librería de Java una clase que implmente directamente una
cola. Lo que existe es una interfaz con los métodos propios de una cola. La
interfaz presente lo siguiente:
java.util.Queue<E>

E element() Devuelve pero no elimina la cabeza de la cola


boolean oer(E o) Inserta el elemento o al nal de la cola si es posible
E peek() Devuelve pero no elimina la cabeza de la cola, devolviendo null si la
cola está vacía

E poll() Devuelve y elimina la cabeza de la cola, o da null si la cola está vacía


E remove() Devuelve y elimina la cabeza de la cola
Una manera de tener una cola es usar la clase LinkedList<E>, la cual es una
implementación de la interfaz Queue<E>. Esta clase tiene muchos otros mé-
todos, pero se puede usar limitándose a ver los propios de la interfaz y de esa
manera se puede limitar a comportarse como una cola.

3.4.5.2. Colas de Prioridad


Una cola de prioridad es una cola a cuyos elementos se les ha asignado una
prioridad, de forma que el orden en que los elementos son procesados sigue las
siguientes reglas:

El elemento con mayor prioridad es procesado primero.


CAPÍTULO 3. ESTRUCTURAS DE DATOS 54

Dos elementos con la misma prioridad son procesados según el orden en


que fueron introducidos en la cola.

En la librería de Java se encuentra la clase PriorityQueue<E>, la cual admi-


nistra los elementos generando un orden de prioridad entre ellos. Los elementos
que se agregan a esta clase deben poder ser comparables entre sí para que la
cola los pueda ordenar.

import java.util.*;
public class PriorityQueueTest
{
public static void main(String[] args)
{
PriorityQueue<GregorianCalendar> pq =
new PriorityQueue<GregorianCalendar>();
pq.add(new GregorianCalendar(1906, Calendar.DECEMBER, 9));
pq.add(new GregorianCalendar(1815, Calendar.DECEMBER, 10));
pq.add(new GregorianCalendar(1903, Calendar.DECEMBER, 3));
pq.add(new GregorianCalendar(1910, Calendar.JUNE, 22));
System.out.println("Iterando los elementos...");
for (GregorianCalendar date : pq)
System.out.println(date.get(Calendar.YEAR));
System.out.println("Eliminando elementos...");
while (!pq.isEmpty())

System.out.println(pq.remove().get(Calendar.YEAR));
}
}

3.4.6. Tablas Hash


Las técnicas de Hashing se llaman también de transformaciones de llaves o
de acceso directo.
El problema que el Hashing resuelve es el siguiente: si se tiene un conjunto
de elementos caracterizados por una llave (sobre la cual se dene una relación
de ordenación) ¾cómo se debe organizar el conjunto para que el acceso a un
elemento con cierta llave se realice con el menor esfuerzo posible?. El problema
radica fundamentalmente en encontrar un mapeo apropiado H de llaves (K) en
las direcciones (A):
H: K ->A
La organización de datos usada en esta técnica es la estructura de arrays. H
es, por tanto, un mapeo que transforma las llaves en índices de arreglo, y por
eso se emplea la designación transformación de llaves (cálculo de dirección) que
suele usarse para nombrar esta técnica.
Una tabla de hash consiste en un array en el cual los datos son accedidos por
un valor especial denominado una llave. La idea fundamental en una tabla de
hash es establecer un mapeo entre el conjunto de todos los posibles valores de
las llaves y las posiciones en el array usando una función hashing. Esta función
CAPÍTULO 3. ESTRUCTURAS DE DATOS 55

acepta una llave y devuelve su código hash o valor hash. Las llaves varían en su
tipo de datos pero los códigos hash son siempre enteros.
Se dice que una tabla hash está directamente direccionada cuando su función
hashing pueda garantizar que no hay dos llaves que puedan generar el mismo
código hash. Esto es lo ideal pero es difícil de lograr en la práctica.
Normalmente el número de entradas en una tabla hash es mucho menor al
universo de los posible valores de llaves. Consecuentemente muchas funciones
hashing mapean algunas llaves a la misma posición en la tabla. Cuando dos
llaves mapean a la misma posición se denomina una colisión. Una buena función
hashing minimiza las colisiones y está lista para poder resolverlas.

3.4.7. Uso de la la colección para tablas hash


Las tablas hash son implementadas con la clase HashMap, la cual implementa
la interfaz Map. Se soloca a continuación un ejemplo sencillo de su sintaxis:

Map<String, Empleado> personal = new HashMap<String, Empleado>();


Empleado alguien = new Empleado(Clark Kent);
personal.put(987-98-9955, alguien);
...
String codigo = 987989955;
emp = personal.get(codigo);

Con los métodos put() y get() de la clase HashMap se pueden añadir y eliminar
elementos a la colección basados en los valores de sus llaves.
Se coloca a continuación un programa de ejemplo demostrando el uso posible
del HashMap.

import java.util.*;
public class MapTest
{
public static void main(String[] args)
{
Map<String, Empleado> staff = new HashMap<String, Empleado>();
staff.put("144255464", new Empleado("Amy Lee"));
staff.put("567242546", new Empleado("Harry Hacker"));
staff.put("157627935", new Empleado("Gary Cooper"));
staff.put("456625527", new Empleado("Francesca Cruz"));
// imprimir todos
System.out.println(staff);
// eliminar uno
staff.remove("567242546");
// remplazar un elemento
staff.put("456625527", new Empleado("Francesca Miller"));
// mirar un valor
System.out.println(staff.get("157627935"));
// iterar por todos los elementos
for (Map.Entry<String, Empleado> entry : staff.entrySet())
{
CAPÍTULO 3. ESTRUCTURAS DE DATOS 56

String key = entry.getKey();


Employee value = entry.getValue();
System.out.println("key=" + key + ", value=" + value);
}
}
}
class Empleado
{
public Empleado(String n)
{
name = n;
salary = 0;
}
public String toString()
{

return "[nombre=" + name + ", sueldo=" + salary + "]";


}
private String name;
private double salary;
}
Capítulo 4

Aplicaciones con orientación a


objetos
4.1. Interfaces grácas de usuario
Las interfaces grácas de usuario (GUI) son soportadas en Java usando una
serie de librerías. El uso de GUI implica básicamente tener las siguientes fun-
cionalidades:

Creación y control de ventanas

Manipulación de controles grácos: cajas de texto, botones de comando,


listas deplegables, etc.

Control de apariencia: tipos de letras, tamaños, colores, etc.

Control de los eventos: las respuestas de la aplicación frente a las acciones


del usuario.

En su versión 1.0 solamente existía el Abstract Windows Toolkit (AWT). Desde


la versión 1.2 la librería para GUI ha sido el Swing, que tiene mayores capaci-
dades.

4.1.1. Componentes grácos


Los fundamentos de los componentes grácos en Java se explican a conti-
nuación:

Cada elemento gráco de GUI es un componente

Cada componente es una instancia de una clase

Un componente se crea como cualquier otro objeto Java

Algunos componentes pueden contener a otros componentes (son contene-


dores)

Cada contenedor de alto nivel tiene un JrootPane que es la raíz de la


jerarquía de contenedores.

57
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 58

Todo componente GUI debe formar parte de la jerarquía de contenedores.

Cada componente GUI sólo puede aparecer una vez.

Un contenedor de alto nivel puede opcionalmente tener una barra de me-


nús.

4.1.2. Fundamentos de programación


Para hacer programación con componentes grácos hay que seguir los si-
guientes pasos:

Importar paquetes javax.swing.XXX

Disponer un contenedor:

• JFrame

• Dialog

• JApplet

Agregar componentes al contenedor

Mostrar el contenedor

Los GUIs deben ser creados en el hilo de atención a eventos

Ejemplo de una ventana mínima (ventana vacía de 300 por 200 pixels):

import javax.swing.*;
public class SimpleFrameTest
{
public static void main(String[] args)
{
SimpleFrame frame = new SimpleFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 59

frame.setVisible(true);
}
}
class SimpleFrame extends JFrame
{
public SimpleFrame()
{
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}

La ventana creada es la siguiente:

A continuación se coloca el código de una ventana que contiene un control en


su interior, de tipo JPanel.

import javax.swing.*;
import java.awt.*;
public class NotHelloWorld
{
public static void main(String[] args)
{
NotHelloWorldFrame frame = new NotHelloWorldFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class NotHelloWorldFrame extends JFrame
{
public NotHelloWorldFrame()
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 60

{
setTitle("NotHelloWorld");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// añadir el panel al frame
NotHelloWorldPanel panel = new NotHelloWorldPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y);
}
public static final int MESSAGE_X = 75;
public static final int MESSAGE_Y = 100;
}

Cada componente puede generar eventos:

Acciones del usuario sobre el componente

Temporizaciones

Cambio de estado, etc.

En cada componente se pueden registrar escuchadores de eventos. Cuando el


componente genere un evento, invoca a todos sus manejadores de eventos
Cada evento es un objeto que hereda de la clase AWTEvent
Ejemplos de eventos:

ActionEvent

MouseEvent

KeyEvent

WindowEvent

FocusEvent

Otros...

Cada escuchador es un objeto que implementa la interfaz correspondiente al


tipo de evento a escuchar:

ActionListener

WindowListener

MouseListener
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 61

KeyListener

FocusListener

Otros...

Se coloca un ejemplo con uso de eventos:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ButtonTest
{
public static void main(String[] args)
{
ButtonFrame frame = new ButtonFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un frame con un button panel
*/
class ButtonFrame extends JFrame
{
public ButtonFrame()
{
setTitle("ButtonTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// añadir el panel al frame
ButtonPanel panel = new ButtonPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panel con 3 botones
*/
class ButtonPanel extends JPanel
{
public ButtonPanel()
{
// crear botones
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
// añadir los botones al panel
add(yellowButton);
add(blueButton);
add(redButton);
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 62

// crear las acciones de los botones


ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
// asociar las acciones a los botones
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
}
/**
cambiar el color del panel
*/
private class ColorAction implements ActionListener
{
public ColorAction(Color c)
{
backgroundColor = c;
}
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
private Color backgroundColor;
}
}

La ventana que se crea con el código anterior es la siguiente:

De manera ordinaria su utiliza algún tipo de IDE con soporte para GUI
cuando se trabaja en el desarrollo de aplicaciones con interfaz gráca. El IDE
puede tener algún tipo de soporte visual para la creación de las ventanas, de
modo que el programador dibuja la ventana con los controles que necesita y
el IDE crea el código fuente necesario. El programador debe crear siempre el
código para los eventos asociados a los controles.
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 63

4.2. Diseño de aplicaciones


Para el desarrollo de aplicaciones en Java se sugiere trabajar con algún tipo
de IDE que brinde las funcionalidades propias de un proyecto de desarrollo.
Actualmente se pueden encontrar IDEs de licencia sin costo como el Eclipse y
el Netbeans, los cuales tienen características bastante profesionales.
En estas herramientas se trabaja bajo el conepto de proyectos. Un proyecto
es una colección de recursos de distintos tipos:

código fuente

archivos de conguración

librerías compiladas

archivos para pruebas

otros

En el IDE usualmente se encuentran plantillas de código para distintos tipos de


componentes. Además brindan otras funcionalidades, tales como: depuración,
control de versiones, refactorización, etc.
CAPÍTULO 4. APLICACIONES CON ORIENTACIÓN A OBJETOS 64

Cuando la aplicación ha sido terminada el IDE genera los archivos para el


despliegue. Estos son los que serán entregados al usuario nal. Lo único que
debe necesitar el usuario nal es tener instalada una máquina virtual de Java
(Java Runtime Environment).
En un proyecto mediano o grande se suelen crear elementos dentro del pro-
yecto que no necesariamente estarán en el producto nal. Pueden ser código
fuente y otros elementos que son necesarios para el proceso de desarrollo de la
aplicación, pero que no serán útiles luego que la aplicación se haya terminado.
Por ejemplo el código de prueba para ver el funcionamiento correcto de unas
clases, el cual no tiene razón de ser una vez que las pruebas se hayan terminado.

4.2.1. Etapas de desarrollo


Aunque existen distintas maneras de plantear un proyecto de desarrollo de
una aplicación, los pasos elementales suelen ser siempre los mismos:

diseño de la aplicación

programación

pruebas

entrega

Para el primer paso se diseñan los componentes que la aplicación va a tener.


Esto es básicamente el diseño de clases y de GUI. El diseño implica dar las
especicaciones de las clases sin programar su comportamiento.
De manera ordinaria para el diseño se pueden seguir los siguientes pasos

encontrar las clases de lógica de negocio

agregar las clases de manejo de colecciones y de control de la aplicación

agregar las clases de GUI (si existen)

En todo el proyecto solamente puede existir una clase que tenga un método
main(), la cual será la que inicie la aplicación. Usualmente este main() es el que
invoca a la ventana principal donde se inicia la interacción con el usuario.
En la programación se van implementando los elementos que fueron especi-
cados en el diseño. En algunos casos se debe de ir probando el funcionamiento
del código mientras se va desarrollando. Estas pruebas llevan a la depuración,
etapa en la cual se quitan los errores de programación encontrados.
Usualmente en la depuración se trabaja con un IDE que permita la ejecución
del código línea por línea, así como examinar el contenido de memoria para
cualquier variable. Usando esas funcionalidades se puede ver si el programa
tiene el comportamiento esperado mientras se hacen las pruebas.
Para la entrega se deben crear los productos de despliegue. En Java se puede
crear un archivo .jar en el cual se encuentren todas las clases de la aplicación
compiladas y unidas en un único archivo. Esto facilita el despliegue asegurando
que no faltarán archivos de la aplicación cuando se entrega.
Lo único necesario para que la aplicación creada sea ejecutada en el compu-
tador del usuario debe ser que se encuentre instalada la máquina virtual de Java
(JRE). Todas las librerías usadas se deben incluir en el producto de despliegue.
Apéndice A

Lenguaje de programación
Java
A continuación se expone la sintaxis elemental del lenguaje de programación
Java. Es básicamente la mención a sus elementos propios de la programación
estructurada. El alumno ya debe de tener familiaridad con esta sintaxis, propio
de lo que ha aprendido en el curso de Programación Básica.

A.1. Gramática del lenguaje


A.1.1. TIPOS DE DATOS
A.1.1.1. ENTERO:
Existen cuatro tipos de datos numéricos enteros: byte, short , int y long; que
permiten almacenar enteros de longitud máxima 8bits, 16 bits, 32 bits y 64 bits
respectivamente. La longitud de los tipos de datos en Java es ja para asegurar
la portabilidad en distintas implementaciones del lenguaje.
byte >8 bits
short >16 bits
int >32 bits
long >64 bits
La asignación de un tipo a una variable se realiza escribiendo el nombre de
la variable después del tipo, en el siguiente ejemplo:

int x, y; //se declaran dos variables enteras de 32 bits.


long x, y; // se declaran dos variables enteras de 64 bits.

En la declaración también se puede asignar un valor a la variable mediante el


símbolo igual. Ejemplo:

byte var1, var2 = 7; /* se declaran dos variables de 8 bits y


se asigna el valor 7 a la segunda de ellas, dejando la primera
sin inicializar. */

65
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 66

Los valores enteros se pueden denotar como se muestra en el ejemplo anterior


o escribiéndolos en octal o hexadecimal. Para especicar un entero en octal se
precede el entero con un cero y si desea en hexadecimal se precede con 0x.

int a = 017, b = 0xF, c = 0xBB;

así se declaran tres variables enteras de 32 bits. En a se almacena el 17 en octal


(15 en decimal), en b se almacena F en hexadecimal (15 en decimal) y en c se
almacena BB en hexadecimal (187 en decimal).

A.1.1.2. REAL
Hay dos tipos de número real en Java.

oat: representa números en coma otante de 32 bits.


double: representa número en coma otante de 64 bits.
Ejemplo:

float x = 23.8, y, z; // se declaran tres reales y se asigna


el valor 23.8 a la variable x.

Los números en coma otante se denotan como: parte-entera.parte-fraccionaria


o también utilizando la notación cientíca donde el carácter e o E indica que el
número se multiplica por 10 elevado al número que sigue a la e o E.
Ejemplo:

float total = 1.56e2, parcial = 2.34E4;

total y parcial son dos variables en coma otante; total tiene el valor 156 y
parcial 23400.

A.1.1.3. CARÁCTER
Este tipo de dato utiliza un carácter Unicode de 16 bits y se establece me-
diante un valor entre comillas simples. Ejemplo:

char letra, caracter = e ;

A.1.1.4. BOOLEANO
Este tipo de variable puede tomar el valor verdadero o falso.
Ejemplo:

boolean flag = true;

la variable ag toma el valor verdadero.


OPERADORES
Todos los tipos de datos elementales en Java están asociados a operadores
que se utilizan para construir expresiones.
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 67

A.1.1.5. OPERADORES ARITMÉTICOS BINARIOS


Pueden utilizarse con reales y con los diversos tipos de enteros. En la tabla
siguiente se muestran los diferentes operadores que existen.
operador símbolo expresión signicado

multiplicación * a * b a por b
división / a / b a dividido entre b
suma + a + b b sumado a a
resta - a - b b restado a a
resto de la división % a% b resto de dividir a entre b
entera entre b
El operador % solamente se puede aplicar a datos enteros.

A.1.1.6. OPERADORES DE ASIGNACIÓN ARITMÉTICOS


Estos operadores realizan operaciones aritméticas tales como la suma y des-
pués asignan el resultado a una de las variables usadas en la expresión que
contiene el operador.
Ejemplos:

int a = 3, b = 9;
a += b;

El resultado es la suma de a y b ( 12 ), y se guarda el valor en a. Equivale: a =


a + b

int a = 4, b = 11;
b %= int a;

El resultado es el resto de la división entera de b entre a ( 3 ) y se guarda en b.


En la tabla siguiente se muestran los diferentes operadores que existen.
símbolo operador expresión signicado

= asignación a = b pone el valor de b en a


+= suma y asigna a += b pone el valor de a+b en a
-= resta y asigna a -= b pone el valor de a-b en a
*= multiplica y asigna a *= b pone el valor de a*b en a
/= divide y asigna a /= b pone el valor de a/b en a
%= resto de la división a% b pone el valor de a %b en b
entera y asigna
Otros dos operadores son el de incremento (++) y el de decremento (). Se
emplean en dos formas: antes o después de la variable a tratar.
Ejemplo:

cont ++;

incrementa el valor de la variable cont y lo reemplaza en cont. Devuelve el valor


antiguo de cont. Por ejemplo si inicialmente cont tenía el valor 8, el resultado
de esta expresión sería modicar el valor a 9 y retornar el valor 8.

++ cont;
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 68

suponiendo que el valor de cont es 12, esta expresión hace que se incremente el
valor en 1, el 13 se almacena en cont y se retorna dicho valor.
Los resultados que se obtienen con estos operadores pueden utilizarse en
otros contextos donde esos valores puedan emplearse, como en asignaciones.
Ejemplo:

int a = 0 , b;
b = a ++;

la variable b toma el valor 0 y la variable a se incrementa en 1.


símbolo operador expresión signicado

pos-incremento ++ x++ incrementa x en 1, retorna


el valor anterior
pre-incremento ++ ++x incrementa x en 1, retorna
el valor nuevo
pos-decremento - - x- - decrementa x en 1, retorna
el valor anterior
pre-decremento - - - -x decrementa x en 1, retorna
el valor nuevo

A.1.1.7. OPERADORES RELACIONALES


Estos operadores se pueden aplicar a números enteros y a números en punto
otante. Las operaciones que realizan devuelven un valor booleano.
símbolo operador expresión signicado

= = igual a x = = y verdadero si x es igual a y,


falso en otro caso
> mayor que x >y verdadero si x es mayor que
y, falso en otro caso
< menor que x <y verdadero si x es menor que
y, falso en otro caso
>= mayor o igual que x >= y verdadero si x es mayor o
igual que y, falso en otro ca-
so
<= menor o igual que x <= y verdadero si x es menor o
igual que y, falso en otro ca-
so
Ejemplo:

int a = 30, b = 26;


boolean var1, var2;
var1= (a < 30); // var1 toma el valor falso porque el valor
de a es 30.
var2 =(b >=26); // var2 toma el valor verdadero porque el
valor de b es igual a 26.
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 69

A.1.1.8. OPERADORES LÓGICOS


símbolo operador expresión signicado

&& and lógico a && b devuelve verdadero si a y


b son verdaderos, devuelve
falso en cualquier otro caso.
| | or lógico a | | b devuelve falso si a y b son
falsos, verdadero en cual-
quier otro caso
! negación lógica !a devuelve falso si a es verda-
dero, devuelve verdadero si
a es falso
Este tipo de operadores se pueden utilizar en expresiones que incluyen ope-
radores relacionales y variables de tipo numérico.
Ejemplo:

int a. b, c = 44;
boolean var1, var2;
a = 22; b = 33;
var1 = (a < 10) && (b == 33); // var1 toma el valor falso.
var2 = !var1 && !(b < 22 && a< 23); // var2 toma el valor verdadero.

A.1.2. CADENAS DE CARACTERES:


Las cadenas de caracteres se pueden denir de la misma manera que los tipos
de datos básicos, es decir, como se dene un entero o un carácter.
Ejemplo:

String nombre, dirección;

Se están declarando dos variables de tipo String.


Los String se escriben encerrados entre comillas.
Ejemplo:

String dirección, nombre = "David";

Al igual que los vectores, las cadenas de caracteres son consideradas como ob-
jetos en Java y tienen métodos asociados.
Ejemplo:

nombre.length( );

Determina la longitud del String nombre.


A todos los tipos de datos básicos en Java se les da valores mediante senten-
cias de asignación. Por ejemplo en este código

int a, b, c;
a = 10;
b = 37;
c = 91;
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 70

se declaran tres variables a, b y c y se utiliza la asignación para darle los valores.


Normalmente en Java, si se desea asignar un valor a un objeto hay que
mandarle un mensaje. Sin embargo, se hace una excepción con los vectores y
las cadenas de caracteres. Ejemplo:

String identificador;
identificador = "frodo";

La inicialización se realizó como si se tratara de un tipo de datos básico.


De igual forma se procede con los vectores. Ejemplo:

int posiciones[ ]= new int[5];


posiciones = (1, 2, 3, 4, 5);

En Java hay dos tipos de secuencias de caracteres: String y StringBuer. Los


objetos del tipo String son constantes y no se pueden cambiar, mientras que los
del tipo StringBuer son modicables.
Hay varias formas de crear Strings y StringBuer. Se pueden declarar de
forma fácil con la declaración normal que se ha visto hasta ahora. Ejemplo:

String nombre, dirección;


StringBuffer apellido;

Y se inicializan mediante asignaciones. Ejemplo:

nombre = "María";

Esto se debe a que, aunque en Java, las cadenas de caracteres sean consideradas
como objetos existen facilidades para este tipo de datos que son no orientadas
a objetos. Si las secuencias de caracteres fueran objetos puros, la asignación del
ejemplo anterior no estaría permitida. La única forma adecuada para comuni-
carse con un objeto real es a través de un mensaje. Ejemplo:

usuario.setStringValue ("a1065");

Las operaciones que se pueden efectuar con un String son las siguientes:

Crear una cadena.

Determinar la longitud de una cadena.

Extraer un carácter particular.

Buscar un carácter o una subcadena en el interior de la cadena.

Comparar dos cadenas.

Pasar los caracteres de minúsculas a mayúsculas y viceversa.

Extraer los blancos.

Reemplazar caracteres.

Añadir otra cadena a la serie de la cadena actual.


APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 71

Ejemplo:

String letras = "MAYÚSCULAS";


System.out.println(letras.toLowerCase( ));
Este ejemplo crea una segunda cadena a partir de la primera, que no sufre
cambios. Esta segunda cadena se utiliza para la llamada al método println( ),
y después desaparece.
Los métodos más utilizados se muestran en la siguiente tabla:
Método Signicado Retorna

length( ) determina la longitud de una cade- un entero


na y lo retorna
charAt(ch) busca el carácter situado en la po- un carácter
sición ch dentro del string
indexOf (str) busca el string str en el objeto des- un entero
tino y retorna la posición que ocu-
pa. La búsqueda comienza por el
principio del string destino
lastIndexOf(str) busca el string str en el objeto des- un entero
tino y retorna la posición que ocu-
pa. La búsqueda comienza por el
nal del string destino
substring(int1,int2) retorna un substring del string des- un string
tino que contiene los caracteres
desde la posición int1 hasta la int2
del string destino
Las operaciones que se pueden efectuar con un StringBuer son las siguien-
tes:

Añadir caracteres en el interior o al nal de la cadena (no se produce la


creación de una nueva cadena, sino que se modica la cadena actual).

Añadir directamente a una cadena objetos de tipo int, boolean, etc., sin
tener que convertirlos previamente en cadenas de caracteres.

Los StringBuer se diferencian de los Strings en que los objetos del primer tipo
pueden modicarse y se tratan como objetos puros. Ejemplo:

StringBuffer cadena = new StringBuffer ("hola");


Los objetos StringBuer también reconocen los cinco métodos mencionados an-
teriormente para los Strings, pero al ser objetos que pueden variarse, también
responden a métodos que alteran su estado. Los tres métodos más populares
son: append( ), insert( ), y setCharAt( ).

A.2. ESTRUCTURAS DE CONTROL


Las acciones en Java terminan con el separador punto y coma. Una secuencia
se escribiría así:
línea1; línea2; línea3;
Los bloques en Java van encerrados entre llaves.
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 72

A.2.0.1. If
La estructura if permite que secciones de código sean ejecutadas en función
de si cierta condición es verdadera o falsa. La sintaxis es:

if (condición)
acciones-cond-verdadera( );
else
acciones-cond-falsa( );

Si la condición es verdadera se ejecuta el código de acciones-cond-verd( ) y si es


falsa acciones-cond-falsa( ).
Se puede ejecutar una sola instrucción como en el ejemplo anterior, o bien,
un conjunto de ellas, que tienen que ir englobadas entre llaves. Ejemplo:

if (contador < =5)


contador ++;
else {
texto="el contador es mayor que 5";
x++;
y++;
}

En caso de que contador tenga un valor menor o igual que 5 se incrementa


el contador, si es mayor se realizan tres acciones: asignar a texto la ristra de
caracteres delimitada por "", e incrementar las variables x e y.
La condición del if puede estar compuesta por varias condiciones. Ejemplo:

if (cont < 10 && continuar)


cont++;

La acción de incrementar cont se hará si se cumple que cont sea menor que 10
y que la variable booleana continuar sea verdadera.

A.2.0.2. Switch
Permite establecer una condición múltiple, de forma que después de evaluar
un argumento se lleve a cabo la acción correspondiente a ese caso. El formato
es el siguiente.

switch(argumento) {
case uno: acción1;
case dos: acción2;
...
case n: acción n;
}

Si en las acciones correspondientes a cada caso no se incluye la instrucción


break, se continúa con la ejecución del siguiente caso y así sucesivamente hasta
encontrar dicha instrucción o el nal del switch.
Ejemplo:
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 73

switch(variable_entera) {
case (1): accion1( );
case(2): accion2( ); break;
case(3): accion3( );
default: accion_por_defecto( );
}

En este ejemplo si el valor examinado por el switch vale 1 (variable_entera =


1) se ejecutan accion1( ) y accion2( ). Si el valor examinado es distinto de 1, 2,
y 3 se ejecuta la accion_por_defecto().

A.2.0.3. While
La sintaxis es:

while (expresión)
acción;

En acción puede ir una sola acción o un grupo de ellas encerradas entre llaves.
La semántica es :
Mientras la expresión sea verdadera se ejecutan las acciones.
Ejemplo:

int i = 0, total = 0 ;
while (i < 5) {
total+= valores[i];
i++;
}

Primero se inicializan las variables i y total. Entra en el bucle donde se suma el


elemento iésimo del vector valores en total y se incrementa i en uno. El resultado
es la suma de los cinco elementos del vector y el almacenamiento de ese valor
en total.
Si se utiliza el break en una estructura repetitiva, al encontrar el intérprete
de Java el break, transere el control a la acción que sigue al bucle en el que se
encuentra encerrado el break.
Ejemplo:

acción;
while (expresión) {
acción1;
acción2;
acción3;
if (condición) break;
acción4;
...
acciónn;
}
otra_acción;
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 74

Si cuando se esté ejecutando el bucle, condición es verdadera, entonces el in-


térprete de Java cede el control a la acción siguiente al bucle, en este caso a
otra_acción.
Otro ejemplo más concreto se muestra a continuación:

boolean encontrado= false;


String palabra = "hola";
int cont=0; índice;
while(cont > n) {
if (tabla[cont].equals(palabra)) {
encontrado=true;
índice= cont;
break;
}
cont++;
}

En este fragmento de código se realiza la búsqueda de la palabra "hola" en el


vector de strings llamado tabla. El código asume que el vector tiene n elementos
que son strings. Si encuentra la palabra en la tabla la variable encontrado toma
el valor verdadero y en índice se guarda la posición que ocupa palabra dentro
de la tabla.

A.2.0.4. Do while
Es similar al while, se repite un conjunto de acciones mientras cierta condi-
ción sea verdadera. La única diferencia es que, en el do-while se tiene la seguridad
de pasar al menos una vez por el bucle.
La sintaxis es:

do{
acciones;
} while (expresión);

La semántica es:
ejecutar las acciones mientras la expresión de la tercera línea sea verdadera.
Ejemplo:

int i = 1;
do
i++;
while ( !nombres [i.equals (último_nombre));

Este código busca la string de la variable último_nombre en el vector de strings


nombres. Para ello en cada iteración se incrementa la variable i hasta que se
encuentra el nombre. Hay que tener en cuenta que en este ejemplo el código
asume que el nombre está contenido en el array; si no fuera así, el bucle conti-
nuaría sobrepasando los límites del vector y el intérprete de Java daría un error
en tiempo de ejecución.
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 75

A.2.0.5. For
La sintaxis es:

for (expresión1; expresión2; expresión3)


acciones;
La sintaxis es:
Las acciones se ejecutan continuamente. Existe una variable de control que
viene dada en expresión1. En cada iteración, una vez ejecutadas las acciones,
expresión3 dene el nuevo valor de la variable de control. El bucle termina
cuando la condición expresada en expresión2 se haga falsa.
Ejemplo:

for (int i = 0; i < 10; i++)


vector [i]=0;
Aquí se inicializan los elementos de un vector a 0. La variable de control ( i )
comienza con valor 0 y en cada iteración se incrementa su valor en 1. Cuando i
toma el valor 10 el proceso ha concluido.
La sentencia for puede ser más compleja. Ejemplo:

for ( int i = a + b; i <= max; i += 2)


acciones;
Las acciones se ejecutarán repetidas veces, la variable de control i se inicializa
con el valor de a+b, y se incrementa en 2 en cada iteración que se realiza.
Finaliza cuando i sea mayor que el contenido de la variable max.
La sentencia for existe en un varios formatos, todos ellos equivalentes al
especicado hasta ahora. Son:

for (int i = 0; i < 10; i++)


vector[i] = 0;
int i = 0;
for (; i > 10; i++)
vector[i] = 0;
for (int i = 0; i < 10; ){
vector[i] = 0;
i++;
};

A.3. Entradas y salidas


A partir de la versión 1.5 de Java se ha introducido una manera más sencilla
de manejar las entradas y salidas de datos. Para esto se hace uso de nuevas
clases incluídas en las librerías.

A.3.1. Lectura de teclado


Se hace uso de la clase java.util.Scanner
El siguiente ejemplo muestra una clase que lee valores enteros de teclado
hasta que se introduce un valor 9.
APÉNDICE A. LENGUAJE DE PROGRAMACIÓN JAVA 76

import java.util.Scanner;
public class ESEnteros {
public static void main (String[]args){
Scanner s = new Scanner(System.in);
int entero = 0;
do{
entero = s.nextInt();
}while(entero != 9 );
}
}

En el siguiente ejemplo se procesan líneas de texto, separando las cadenas y


mostrándolas en mayúsculas. El programa naliza al introducir la cadena exit.

import java.util.Scanner;
public class ESCadenas{
public static void main (String[] args){
Scanner s = new Scanner (System.in) ;
String cadena =  ;
do{
cadena = s.next() ; // devuelve un token
System.out.println(cadena.toUpperCase());
}while (!cadena.equals(exit));
s.close();
}
}

La clase Scanner dispone de un gran número de métodos de lectura, dependien-


do del tipo de datos a leer, como nextLine(), nextInt(), nextFloat(), etc. que
devuelven objetos de tipo String o bien realizan la conversión a tipo primitivo
directamente.

Vous aimerez peut-être aussi