Estructuras de Datos en Java: Guía Completa
Estructuras de Datos en Java: Guía Completa
en Java
4.a edición
Mark Allen Weiss
PEARSON
ALWAYS LeArNING
Estructuras de datos
en Java
Cuarta edición
Estructuras de datos
en Java
Cuarta edición
Mark Allen Weiss
Florida International University
Traducción
Vuelapluma
PEARSON
Datos de catalogación bibliográfica
Estructuras de dates en Java. Cuarta edición
Mark Allen Weiss
PEARSON EDUCACION, S. A. 2013
ISBN: 978-84-155-223-9
Matcria: Informática, 004
Formnto: I95 x 250 mm
Piginns: 1.000
Cualquier fonns de reproducción, distnbución, comunicacion
pública 0 transformación de esta obra solo puede ser realizada
con la autorizsción
de sus titulares, salvo excepcion prevista por la ley, Dirljases
ICEDRO (Centro Espaiol de Derechos Reprogrificos)si necesita
biocopiaro
csanear algún fingunto de ceta obra (wwu [Link]; 91 702
i9 70/93 27204 47).
Tolos los derchos reservados.
c 2013. FEARSON EDUCACIÓN SA.
Ribera del Loirm, 29
28(H2 Madrid (Espuña)
wwwpcarson cs
Aimtharized trxslavion frowr the English langinge edrion, enditied
DATA STRUCTURES cee PROBLEM SOLVING USING JAVA, Fourti
Ediion
by MARK ALLEN WEISS, published by Pearson Edurcarion, Inc,
publishing as Prentice Haili„ Copyriglu o 2012.
AI riglur reserwed, Na PXTrt ot this boak mey be reprochiced ar
frunamifed ir am form or bny any cans, electronic or mechanical,
including
pharocopying recording ar by ny information sferage refriewl
system, wirhcut perunssion from Pearson Education, Inc
SPANISH lungags edition mbbshed by PEARSON EDUCACIONS. A..
Copwieht o 2013.
ISBN: 978-84-1555-223-9
Depdsilo Legal: M-2037-2013
Equipo de edición
Elitor: Miguel Martin-Romo
Equipo de diseña
Dscindomm Scnior: Elcnn Jammillo
Técnicode diseño: Pablo Hoccs de la Guardia
Eqquipo de producción
Directora: Mnrtn Illescas
Chordinadora: Tni Cardoso
Discio de cutierta: Copibook,SL.
Composicián: Vuclapluma
Impreso por:
IMPRESO EN ESPAÑA-PRINTED INSPAIN
Nota sobre enlacesn paginas web njenas: este libro incluye
enlacesa sitios webs cuyn Festión, mantenimiento yconttol son
resposabildad
inica y exelusiva de tercers ajnosa PEARSON EDUCACIÓN, s [Link]
enlaces w otras nlernciass I sitios web se ingluyen con fialiad
strictamcate infomativa " se proporcionan cn cl esindo en que se
encucntrmn en el momento de publcación sin gurntias, expnets o
implicitns,
sobre la infonmación quc se proporcionc cn cllas. Los cnlaces no
implican cl aval de PEARSON EDUCACIÓN, s. A.a talcs sitios,
páginas web.
furcionalidades y suS respectivos contenidos o cuulquicr
asociación con SuS udministradores. En consccuencin, PEARSON
EDUCACIÓN, s.
A., no asume responsabilidad alguna por los dahos que se poedan
derivar de hipotéticas infracciones de los derechos de propiedad
inteleetual
yo industrial que puedan contener dichos sitios web ni por las
pendidas delitos o los dañosy parjuicios derivados, directa o
indirectamente, del
1sO de lales sntios web y de su intormación. Al accedera talcs
enlaces extenos de los sitios ueb, el usuario estará bajo In
protección k datosy
roliticas de privacidad 0 pricticas y otros conknidos de tules sitios
woby nO de PEARSON EDUCACIÓN .S. A.
Este libro ha sido impreso COn papcly tintas ccológicos
A David y David
PREFACIO
Este libro está diseñado para una secuencia de dos semestres
académicos dentro de una carrera en
Ciencias de la Computación, comenzando por lo que típicamente
se conoce como Estructuras de
datos y continuando con estructuras de datos avanzadas y análisis
de algoritmos. Es apropiado para
las secuencias tanto de dos cursos como de tres cursos relativos
a temas introductorios, tal como
se esbozan en el informe final del proyecto 2001 sobre Cunicula en
Ciencias de la Computación
(CC2001), un proyecto conjunto de ia ACM y el IEEE.
El contenido del curso sobre Estructuras de datos ha estado
evolucionando durante algún tiempo.
Aunque existe un cierto consenso general en lo que respecta a la
cobertura de temas, sigue habiendo
considerables desacuerdos acerca de los detalles. Uno de los
temas generalmente aceptados son los
principios de desarrollo software, y especialmente los conceptos
de encapsulación y ocultamiento
de información. Algorítmicamente, todos los cursos sobre
Estructuras de datos tienden a incluir una
introducción al análisis del tiempo de ejecución, la recursión, los
algoritmos básicos de ordenación
y las estructuras de datos elementales. Muchas universidades
ofrecen un Curso avanzado en el que se
cubren temas sobre estructuras de datos, algoritmos y análisis de
tiempo de ejecución a un nivel más
alto. El material de este texto está diseñado para utilizarlo en
ambos tipos de cursos, eliminando así
la necesidad de adquirir un segundo libro de texto.
Aunque los debates más apasionados en el campo de las
Estructuras de datos se refieren al
tema de la elección de un lenguaje de programación, es necesario
también realizar algunas otras
elecciones fundamentales:
Mi objetivo al escribir este texto era proporcionar una introducción
práctica a las estructuras de
datos y algoritmos, desde el punto de vista de pensamiento
abstracto y de las técnicas de resolución
de problemas. He tratado de cubrir todos los detalles importantes
concemientes a las estruc-
turas de datos, sus análisis y sus implementaciones Java, al
mismo tiempo que evitaba las estructuras
de datos que son interesantes desde el punto de vista teórico,
pero que no se utilizan ampliamente.
Es imposible cubrir en un solo curso todas las distintas
estructuras de datos descritas en este texto,
incluyendo sus usos y el análisis. Por ello, he diseñado el libro de
texto para proporcionar a los
profesores un cierto grado de flexibilidad en cuanto a la cobertura
de temas. El profesor tendrá
que decidir el equilibrio apropiado entre teoría y práctica, y luego
seleccionar los temas que mejor
encajen con el curso. Como se explica posteriormente en este
Prefacio he organizado el texto con el
fin de minimizar las dependencias entre los distintos capítulos.
viii
Prefacio
Resumen de los cambios en la cuarta edición
Un enfoque original
Mi premisa básica es que las herramientas de desarrollo software
en todos los lenguajes incluyen
librerías de gran tamaño, junto con estructuras de datos que
forman parte de esas librerías.
Mi previsión es que terminará habiendo un desplazamiento en el
foco de interés de los cursos
sobre estructuras de datos, en el que se pasará de la
implementación al uso. En este libro, he
decidido adoptar un enfoque original, separando las estructuras de
datos en su especificación y su
subsiguiente implementación, y aprovechando una librería de
estructuras de datos ya existente, la
API de Colecciones de Java.
Analizamos un subconjunto de la API de Colecciones, que es
adecuado para la mayoría de
las aplicaciones, en un único capítulo (Capítulo 6) de la Parte Dos.
La Parte Dos también cubre
las técnicas de análisis básico, la recursión y la ordenación. La
Parte Tres contiene diversas
aplicaciones que utilizan las estructuras de datos de la API de
Colecciones. La implementación
de la API de Colecciones no se muestra hasta la Parte Cuatro, una
vez que ya se han utilizado las
estructuras de datos. Puesto que la API de Colecciones fonman
parte de Java, los estudiantes pueden
disenar proyectos de gran envergadura casi desde el principio,
utilizando componentes software ya
existentes.
A pesar del papel fundamental que la API de Colecciones
desempeña en este texto, este no es
un libro sobre la API de Colecciones, ni tampoco es un tomo
dedicado específicamente a estudiar
la implementación de la API de Colecciones, sigue siendo un libro
que pone el énfasis en las
estructuras de datos y en la técnicas básicas de la resolución de
problemas. Por supuesto, las técnicas
generales empleadas en el diseño de estructuras de datos son
aplicables a la implementación de la
API de Colecciones, por lo que varios capítulos de la Parte Cuatro
incluyen implementaciones de
la API de Colecciones. Sin embargo, los profesores pueden
seleccionar las implementaciones más
simples de la Parte Cuatro donde no se analice el protocolo de la
API de Colecciones. El Capítulo 6,
ix
Prefacio
que presenta la API de Colecciones, resulta esencial para
comprender el código de la Parte Tres. En
el libro, he intentado utilizar únicamente las partes básicas de ia
API de Colecciones.
Muchos profesores preferirán un enfoque más tradicional en el que
se defina, implemente
y luego se utilice cada estructura de datos. Puesto que no existe
ninguna dependencia entre los
materiales de las Partes Tres y Cuatro, puede impartirse
fácilmente un curso tradicional utilizando
este libro.
Prerrequisitos
Los estudiantes que utilicen este libro deberian tener
conocimientos de un lenguaje de programación
orientado a objetos o de un lenguaje de programación
procedimental. Se supone un cierto conoci-
miento de las caracteristicas básicas, incluyendo los tipos de
datos primitivos, los operadores, las
estructuras de control, las funciones (métodos) y la entrada y
salida (aunque no necesariamente de
matrices y clases).
Los estudiantes que hayan seguido un primer curso en el que se
empleara CHt o Java pueden
encontrarse con que la lectura de los cuatro primeros cupítulos es
bastante "ligera" en algunos
lugares. Sin embargo, otras partes son ciertamente savanzadas"
con detalles Java que pueden no
haber sido cubiertos en cursos introductorios.
Los estudiantes que hayan seguido un primer curso en algún otro
lenguaje deberían comenzar
por el Capítulo 1 e ir progresando lentamente. Si un estudiante
quiere utilizar también algún libro de
referencia sobre Java, en el Capítulo 1 se proporcionan algunas
recomendaciones.
El conocimiento de las matemáticas discretas resulta útil, aunque
no es un prerrequisito absoluto.
Se presentan diversas demostraciones matemáticas, pero las más
complejas van precedidas de
una breve revisión matemática. Los Capitulos 7 y 19 a 24 requieren
un cierto grado de sofisti-
cación matemática. El profesor puede elegir făcilmente saltarse
los aspectos matemáticos de las
demostraciones, presentando únicamente los resultados. Todas las
demostraciones del texto están
claramente marcadas y separadas del cuerpo principal del libro.
Java
Este libro de texto presenta el material utilizando el lenguaje de
programación Java. Java es un
lenguaje que a menudo se suele examinar en comparación con
C+T. Java ofirece muchas ventajas,
y
los programadores ven Java como un lenguaje más seguro, más
portable y más făcil de utilizar que
CHi.
EI uso de Java requiere que se tomen ciertas decisiones a la hora
de escribir un libro de texto.
Algunas de las decisiones tomadas son las siguientes:
x
Prefacio
Organización del texto
En este texto, se presenta el lenguaje Java y la programación
orientada a objetos (en particular la
abstracción) en la Parte Uno. Se explican los tipos primitivos, los
tipos de referencia y algunas de las
clases y excepciones predefinidas antes de pasar al diseno de
clases y a la herencia.
En la Parte Dos se explican los paradigmas o mayúscula y
algorítmico, incluyendo la recursión
y la alcatorización, Se dedica un capítulo completo al tema de la
ordenación y un capítulo separado
contiene una descripción de las estructuras de datos básicas. Se
utiliza la API de Colecciones para
presentar las interfaces y los tiempos de ejecución de las
estructuras de datos. En este punto del
texto, el profesor puede adoptar varios enfoques para presentar el
material restante, incluyendo los
dos siguientes:
xi
Prefacio
proyectos de programación. Más adelante proporcionaré más
detalles sobre la utilización de
este enfoque.
La Parte Cinco describe estructuras de datos avanzadas tales
como los árboles splay, los
monticulos de emparejamiento y la estructura de datos para
conjuntos disjuntos, que se pueden
cubrir, si el tiempo lo permite o, más probablemente, en un curso
posterior.
Organización del texto capítulo a capítulo
La Parte Uno consta de cuatro capítulos que describen los
aspectos básicos de Java utilizados a lo
largo del libro. El Capítulo 1 describe los tipos primitivos e ilustra
cómo escribir programas básicos
en Java. El Capítulo 2 habla de los tipos de referencia e ilustra el
concepto general de puntero, aun
cuando Java no tiene punteros, para que los estudiantes puedan
aprender acerca de este importante
tema sobre Estructuras de datos. Se ilustran varios de los tipos de
referencia básicos (cadenas de
caracteres, matrices, archivos y objetos Scanner) y se analiza
también el uso de excepciones. El
Capítulo 3 continúa esta explicación describiendo cómo se
implementa una clase. El Capítulo 4
ilustra el uso de la herencia en el diseño de jerarquías (incluyendo
clases de excepciones y de E/S)
y componentes genéricos. En la Parte Uno se puede encontrar el
material relativo a los patrones de
diseño, incluyendo el patrón envoltorio, el adaptador y el
decorador.
La Parte Dos se centra en los algoritmos y bloques componentes
básicos. En el Capítulo 5 se
proporciona una explicación completa del tema de la complejidad
temporal y de la notación o
mayúscula. También se explica y analiza la búsqueda binaria. EI
Capítulo 6 es crucial, porque cubre
la API de Colecciones y argumenta intuitivamente cuál debería ser
el tiempo de ejecución de las
operaciones soportadas para cada estructura de datos. (La
implementación de estas estructuras de
datos, tanto en el estilo de la API de Colecciones como en versión
simplificada no se proporciona
hasta la Parte Cuatro). Este capítulo también introduce el patrón
iterador, las clases anidadas,
locales y anónimas. Las clases internas se dejan para la Parte
Cuatro, donde se explican como
técnica de implementación. El Capítulo 7 describe la recursión,
introduciendo primero el concepto
de demostración por inducción. También explica las técnicas de
divide y vencerás, programación
dinámica y retroceso. Una sección describe varios algoritmos
numéricos recursivos que se emplean
para implementar el criptosistema RSA. Para muchos estudiantes,
el material de la segunda mitad
del Capítulo 7 resulta más adecuado para un curso posterior. El
Capítulo 8 describe, codifica
y analiza varios algoritmos de ordenación básicos, incluyendo la
ordenación por inserción, la
ordenación Shellsort, la ordenación por mezcla y la ordenación
rápida, así como la ordenación
indirecta. También demuestra la cota inferior clásica para la
ordenación y explica los problemas
de selección relacionados. Finalmente, el Capítulo 9 es un corto
capítulo en el que se explican los
números aleatorios, incluyendo su generación y uSo en algoritmos
aleatorizados.
La Parte Tres proporciona varios casos de estudio, y cada capítulo
está organizado alrededor de
un tema general. EI Capítulo 10 ilustra varias técnicas importantes
examinando el tema de los juegos.
El Capítulo 11 explica el uso de pilas en lenguajes de
computadora, examinando un algoritmo
para comprobar el equilibrado de simbolos y el algoritmo clásico
de análisis de precedencia de
operadores, Para ambos algoritmos se proporcionan
implementaciones completas con su código. El
Capítulo 12 analiza las utilidades básicas de compresión de
archivos y de generación de referencias
cruzadas y proporciona una implementación completa de ambas.
El Capítulo 13 examina de
xii
Prefacio
manera amplia el tema de la simulación examinando un problema
que puede contemplarse como
una simulación y luego echando un vistazo a las simulaciones más
clásicas dirigidas por sucesos.
Finalmente, el Capítulo 14 ilustra cómo se utilizan las estructuras
de datos para implementar de
manera eficiente varios algoritmos del camino más corto para
grafos.
La Parte Cuatro presenta las implementaciones de las estructuras
de datos. El Capítulo 15 explica
las clases intemas como una técnica de implementación e ilustra
su uso en la implementación de
Arraylist. En los restantes capítulos de la Parte Cuatro, se
proporcionan implementaciones que
utilizan protocolos simples (variaciones de insert, find, remove). En
algunos casos, se presentan
implementaciones de la API de Colecciones que tienden a utilizar
sintaxis Java más complicada
(además de ser ellas mismas complejas, debido a su conjunto tan
amplio de operaciones requeridas).
En esta parte se usan algunos conceptos matemáticos,
especialmente en los Capítulos 19 a 21, y el
profesor puede decidir si quiere saltarse dichos temas. El Capítulo
16 proporciona implementaciones
tanto para pilas como para colas. En primer lugar, se implementan
estas estructuras de datos
utilizando una matriz ampliable y luego se implementan empleando
listas enlazadas. Al final del
capítulo se explican las versiones de la API de Colecciones. Las
listas enlazadas de carácter general
se describen en el Capítulo 17 . Las listas simplemente enlazadas
se ilustran con un protocolo simple,
y al final del capítulo se proporciona la versión más compleja de la
API de Colecciones que utiliza
listas doblemente enlazadas. EI Capítulo 18 describe los árboles e
ilustra los esquemas de reconido
básicos. El Capítulo 19 es un capítulo detallado que proporciona
varias implementaciones de
árboles de búsqueda binaria. Inicialmente se muestra el árbol de
búsqueda binaria básico y luego
se desarrolla un árbol de búsqueda binaria que soporta
estadisticas de orden, Los árboles AVL
se explican pero no se implementan, aunque sí que se
implementan los árboles rojo-negro y los
árboles AA que son más prácticos. Después, se implementan las
estructuras Treeset y TreeMap
de la API de Colecciones. Por último, se examina el árbol-B. El
Capítulo 20 explica las tablas
hash e implementa el esquema de sondeo cuadrático como parte
de HashSet y Ha shMap, después
de examinar una altermativa más simple. El Capítulo 21 describe
el monticulo binario y examina la
ordenación heapsort y la ordenación extema,
La Parte Cinco contiene material adecuado para su uso en un
curso más avanzado o como
referencia general. Los algoritmos son accesibles incluso en un
nivel de primer año. Sin embargo,
en aras de la exhaustividad, se han incluido análisis matemáticos
sofisticados que caen, casi con
total seguridad, fuera del alcance de un estudiante de primer año.
El Capítulo 22 describe el árbol
splay, que es un árbol de búsqueda binaria que parece
comportarse de manera extremadamente
adecuada en la práctica, y que puede competir con el montículo
binario en algunas aplicaciones
que requieren colas con prioridad. El Capítulo 23 describe las
colas con prioridad que soportan
operaciones de mezcla y proporciona una implementación del
monticulo de emparejamiento. Por
último, el Capítulo 24 examina la estructura de datos clásica para
conjuntos disjuntos.
Los apéndices contienen material de referencia adicional de Java.
El Apéndice A enumera los
operadores y su precedencia. El Apéndice B proporciona
infonmación sobre Swing y el Apéndice c
describe los operadores bit a bit utilizados en el Capítulo 12.
Dependencias de los capítulos
Generalmente, la mayoria de los capítulos son independientes
entre sí. Sin embargo, he aquí las
dependencias más notables.
xiii
Prefacio
Entidades separadas
Los restantes capítulos tienen pocas o ninguna dependencia:
xiv
Prefacio
Matemáticas
He tratado de proporcionar el necesario rigor matemático para los
cursos sobre Estructuras de datos
que enfatizan la teoría y para los cursos posteriores que requieren
un mayor grado de análisis. Sin
embargo, este material destaca del texto principal en forma de
teoremas separados y, en algunos
casos, secciones o subsecciones separadas. Por tanto, los
profesores que impartan cursos en los que
no haya tanto énfasis teórico pueden saltarse este material.
En todos los casos, la demostración de un teorema no es
necesaria para comprender el significado
del teorema. Esta es otra ilustración de la separación de una
interfaz (el enunciado del teorema) de su
implementación (la demostración). Parte del material
inherentemente matemático, como la Sección
7.4 (aplicaciones numéricas de la recursión), puede ser obviado sin
que eso afecte a la compresión
del resto del capítulo.
Organización del curso
Una cuestión crucial a la hora de impartir el curso es decidir cómo
utilizar el material de las Partes
Dos a Cuatro. El contenido de la Parte Uno debe ser cubierto en
profundidad y el estudiante debería
escribir uno O dos programas que ilustren el diseño, la
implementación y prueba de clases y de
clases genéricas y quizá también el diseño orientado a objetos
utilizando la herencia. El Capítulo 5
explica la notación o mayúscula. Puede proporcionarse al
estudiante un ejercicio en el que escriba
un corto programa y compare el tiempo de ejecución con su
correspondiente análisis, con el fin de
comprobar su compresión del tema.
Cuando se emplea la técnica de separar el uso de las clases de su
implementación, el concepto
clave del Capíulo 6 es que las diferentes estructuras de datos
soportan diferentes esquemas de
acceso y con una eficiencia distinta. Se puede emplear cualquier
caso de estudio (excepto el ejemplo
del juego de las tres en raya, que utiliza recursión) para explicar
las aplicaciones de las estructuras
de datos. De esta forma, el estudiante puede ver la estructura de
datos y saber cómo se utiliza,
aunque no cómo se implementa de manera eficiente, Esto es una
auténtica separación entre la
implementación y el USO de las clases. Ver las cosas de esta
forma penmitirá mejorar enormemente
la capacidad de los estudiantes para pensar de forma abstracta.
Los estudiantes también pueden
proporcionar implementaciones simples de algunos de los
componentes de la API de Colecciones
(se proporcionan algunas sugerencias en los ejercicios del
Capítulo 6) y ver la diferencia existente
entre las implementaciones eficientes de estructuras de datos en
la API de Colecciones existente
y las implementaciones ineficientes de esas mismas estructuras
de datos que ellos desarrollarán.
También se puede pedir a los estudiantes que amplien el caso de
estudio, pero de nuevo, no hace
falta que conozcan nada acerca de los detalles de las estructuras
de datos.
y
La implementación eficiente de las estructuras de datos se puede
explicar posteriormente,
el tema de la recursión puede introducirse cuando el profesor crea
que es apropiado, siempre y
cuando lo haga antes de tocar el tema de los árboles de búsqueda
binaria. Los detalles relativos
a
los algoritmos de ordenación se pueden explicar en cualquier
momento posterior a la recursión.
En este punto, el curso puede continuar empleando los mismos
casos de estudio y experimentando
con modificaciones en las implementaciones de las estructuras de
datos. Por ejemplo, el estudiante
puede experimentar con diversos tipos de árboles de búsqueda
binaria equilibrados.
Xv
Prefacio
Los profesores que opten por un enfoque más tradicional pueden
simplemente analizar un caso
de estudio en la Parte Tres después de explicar una
implementación de una estructura de datos en la
Parte Cuatro. De nuevo, los capítulos de este libro están diseñados
para ser lo más independientes
posible entre sí.
Ejercicios
Se presentan ejercicios de varios tipos; en concreto, he
proporcionado cuatro variedades.
CU
Los ejercicios básicos En resumen plantean una pregunta simple o
requieren simulaciones
a mano de un algoritmo descrito en el texto. La sección En teoria
plantea cuestiones que
requieren un análisis matemático o que piden soluciones
interesantes, desde el punto vista teórico a
los problemas. La sección En la prăctica contiene cuestiones
simples de programación, incluyendo
cuestiones acerca de la sintaxis o acerca de lineas
particularmente complejas de código. Finalmente,
la sección Proyectos de programación contiene ideas para la
asignación de trabajos de mayor
envergadura.
Caracteristicas pedagógicas
o
3
Suplementos
Hay disponibles diversos materiales complementarios para este
texto. Los siguientes recursos están
disponibles en http:/, IwWW. aw .com/cssupport para todos los
lectores de este libro de texto:
= Archivos de código fiente del libro. (La sección Internet al final
de cada capítulo
enumera los nombres de archivo correspondientes al código del
capítulo.)
o
Además, los siguientes suplementos están disponibles para los
profesores. Para acceder a ellos, visite
http:/ /www -pears onhi ghered. com/ cs y busque en nuestro
catálogo el título Data Structures and
Problem Solving Using Java. Una vez en la página del catálogo del
libro, seleccione el enlace a
Instructor Resources (recursos del profesor).
xvi
Prefacio
Agradecimientos
Son muchas las personas que me han ayudado en la preparación
de este libro. A muchas de ellas
ya las he dado las gracias en la edición anterior y en ia versión
relacionada sobre CHT. Otras,
demasiado numerosas para mencionarlas a todas, han enviado
mensajes de comeo electrónico y me
han señalado errores e incoherencias en las explicaciones, las
cuales he tratado de corregir en esta
edición.
Para esta edición me gustariía dar las gracias a mi editor Michael
Hirsch, a la ayudante editorial
Stephanie Sellinger, a la supervisora senior de producción Marilyn
Lloyd y a la jefa de proyecto
Rebecca Lazure y a su equipo en Laserwords. Gracias también a
Allison Michael y Erin Davis de
marketing y a Elena Sidorova y Suzanne Heiser de Night & Day
Design por la maravillosa cubierta
del libro.
Parte del material de este texto está adaptado de mi libro Efficient
C Programming: A Practical
Approach (Prentice Hall, 1995) y está utilizado con permiso del
editor. He incluido referencias al
final de los capítulos alli donde ha sido apropiado.
Mi página web, http:/ /www .cs. fiu. edu/~weiss, contendrá código
fuente actualizado, una
lista de erratas y un enlace para recibir informes sobre errores.
M. A. W.
Miami, Florida
CONTENIDO
Introducción a Java
parte uno
20
„.i iiiii. Au HiiKi HAiiiiAi T VHKTKAN ii
Conceptos clave,
..-..-..................HHHiAiHA .22
Errores comunes
23
Internet Etir
xviii
Contenido
23
Ejerciclos. thaiss
25
carita
Roferencias.
59
... ... .' .. ........-. ..-.....
Concaptos clave.
61
mne
Errores comunes
61
Internet inteeeertease
62
Ejerciclos "iH iiH HTiHAHHTitHiiAEtHHmtteitmHtmmtemnmmm
67
Referencias
xix
Contenido
XX
Contenido
xxi
Contenido
. 166
Resumen santhnss
nn
167
as
Conceptos clave
Errores comunes
169
pssns.
170
n
Internet test ..-.. ..-..
....iT...E..THTHAMKNNIN
171
Ejercicios.
Referencias
181
parte dos
Algoritmos y bloques
fundamentales
xxii
Contenido
xxiv
Contenido
B.6.7
Matrices de pequeño tamaño.. .367
368
8.6.8
Rutina de ordenación rápida en Java
..mm.....H.m..HHhm
8.7
370
Selección
rápida....... .....--......- -.-.---..---
372
8.8
Una cota inferior para la ordenación.
373
Resumen
eora..n*n.-
374
OHRRRIRWNNINAHAHHAHAWAH RiNTEnlE
Conceptos clave.
375
eo
Errores comunes
375
Internet aNnn
...............H...H.........[Link]
uin
-... 375
Ejerciclos.
Raferencias
.
nan
380
parte tres
Aplicaciones
XXV
Contenido
427
Internet netinn
427
...............H......H.......-.........H.H. Hmn
Ejercicios:
429
,....A.H.............HHH.-...H..i..iHHiHHHHi
Refarencias
VHHItNtNintA HAtA RAiHEAtHEN
xxVi
Contenido
13.2 Simulación dirigida por sucesos *. .- .-... .. -. .. ......-* ..501
13.2.1
Ideas básicas.
503
13.2.2
Ejemplo: una simulación de un servicio de atención telefónica
504
512
......*..-..............N
Resumen satreteenes
512
Concaptos clave.
..,..Hi.H......[Link].i.....[Link].H..HH..[Link]
..--...--.-[Link]
513
Errores comunes
.*. ...*.*-..
513
Internet uetee
ata 513
bssanneis
Ejercicios..
parte cuatro Implementaciones
xxvii
Contenido
15.4 StringBul1der ".[Link] litittir mitiHinimninl
""iikimmtiittmttitmtim 571
572
15.5 Implementación de Arraylist con un Iterador.
579
Resumen
579
..[Link] KHIITTTI
Concaptos clave.
Errores comunes
579
..TTTTHHHHEITTNINIMN
579
Internat ...i..AA i.E. R. . krii .....[Link]...[Link]...umn
Ejercicios.
5B0
HTNHTAEHHHEHHHT..
[Link]
ETNRERTH Tm.
xxviii
Contenido
xxix
Contenido
19.6.3
Implementación Java - .. .. . .- ... .. .. -.- .723
19.7 Implementación de las clases TreeSet y TreeMap de la API de
Colecciones...... 726
19.8 Árboles-B
747
752
.HHAHHmHn
Rasumen tttasss
Conceptos clavo.
753
. . .-. .......-...
Errores comunes
754
it
755
Internet eneeeiertautataiu.."
755
Ejercicios.
WREHAHENRNGRERINHANATHRNINEHNKHNEHEMAEHAHRNTHAG
HENRKHAHTHTANENHATITNIAN
ate e e. - P4-. . -.. 759
[Link].
Referencias
XXX
Contenido
21.2.1
Inserción........... ........HHTttttt ..801
21.2.2
La operación de leteMin.
.[Link].H.m -.805
21.3 La operación bu1l dHeap: construcción de un monticulo en
tiempo
lineal............. 807
812
21.4 Operaciones avanzadas: decreasekey y merge -..........
21.5 Ordenación interna: heapsort.....
813
816
21.6 Ordenación externa """"THitmituititiltil
816
21.6.1
.. .. -. ........-....-
Por qué necesitamos nuevos algoritmos
816
21.6.2
Modelo para la ordenación externa..
817
21.6.3
........................i.E..HE.E
E algoritmo simple.
.................H.......i....i .818
21.6.4
Mezcla multivia
819
21.6.5
...............................N
Mezcla polifásica.
820
21.6.6
......
Selección de sustitutos
822
Fesumen srpeteret iiiHHHAHHAm
822
.[Link] T [Link]
Conceptos clave.
823
Aannd
Errores comunes
B23
internet aaerttis
824
Ejercicios.
B27
.-...
Rofgrencias
parte cinco Estructuras de datos
avanzadas
xxxi
Contenido
Referencias ntas ..-mleati. 855
Capitulo 24
B79
La clase conjunto disjunto
879
24.1 Relaciones de equivalencia
880
- ... .. ......--..
24.2 Equivalencia dinámica y aplicaciones
24.2.1
......[Link] .881
Aplicación: generación de laberintos
24 2.2
.....H.H..n .883
Aplicación: árboles minimos de recubrimiento...
24.2.3
..... -.......,..........
Aplicación: el problema del ancestro comůn más próximo
886
889
""HHmHHHHiHiHiHHn
24.3 EI algoritmo rápido de búsqueda
890
24.4 EI algoritmo rápido de unión -.-.. .
892
24 .4.1
..... ...-..............
Agoritmos inteligentes de unión.
894
24.4.2
..,........,,......,..........
Drroti
Compresión de caminos
895
""iii HiHT [Link] T'Hi 'HiiT Hiii lil'HHHm
24.5 Implementación Java.
24.6 Caso peor para la unión por rango con compresión de
caminos.
-- -.- ..-.- .- -.-.... -...-.*-4 **-. 898
24.6.1
Análisis del algoritmo union/find..
899
.-. .--......
905
Resumen --.
905
Concaptos clave.
IHNNNNNHNTNANHHIHRNINIANAENTHINANNNTNT
Errores comunes
906
PaNA.
906
..uAHEWUAN NIN
Internet. ... ..... ...........-.
906
Ejorcicios.
MHLRNINRRHAHNTREEHNEENIHENARNENNANTEINHREHIUILNIN
TIHNRANTAHTTHNHNTHINHTHNNHRM
908
menteals
Roferencias
xxxii
Contenido
Apándice A
911
Operadores
parte
Introducción a Java
uno
Capitulo 1 Estructura primitiva
del lenguaje Java
Capítulo 2 Tipos de referencia
Capítulo 3 Objetos y clases
Capítulo 4 Herencia
1
Capítulo
Estructura primitiva
del lenguaje Java
El enfoque fundanental de este libro son las técnicas de resolución
de problemas que permiten
la construcción de programas sofisticados y. con un tiempo de
ejecución eficiente. Casi todo el
material que se expone es aplicable a cualquier lenguaje de
programación, Algunas personas
podrían argumentar, en ese sentido, que una descrípción amplia de
estas técnicas, en términos de
pseudocódigo, bastaría para ilustrar los conceptos. Sin embargo,
estamos convencidos de que es de
enorme importancia trabajar con código real.
Desde luego, lo que no falta son textos sobre lenguajes de
programación. Este texto utiliza
Java, que goza de una gran popularidad tanto académica como
comercial. En los primeros cuatro
capítulos, expondremos las características de Java que se
utilizarán ampliamente a lo largo del libro.
Las características no utilizadas y los detalles más técnicos no se
tratarán. Las personas que estén
buscando información más profunda sobre Java podrán
encontrarla en cualquiera de los múltiples
libros de Java disponibles.
Comenzaremos analizando la parte del lenguaje que más se
asemeja a un lenguaje de
programación de los años 1970, como por ejemplo Pascal o C. Esto
incluye los tipos primitivos, las
operaciones básicas, las estructuras condicionales e iterativas y
el equivalente Java de las funciones.
En este capítulo, veremos:
1.1
El entorno general
¿Cómo se introducen, compilan y ejecutan los programa de
aplicación en Java? La respuesta
depende, por supuesto, de la plataforma concreta en la que esté
albergado el compilador de Java.
4
Capitulo 1 Estructura primitiva del lenguaje Java
E.l código fuente Java reside en archivos cuyos nombres terminan
con
jevac compila los afchivos
-Ja va ygenera architros
el sufijo java. EI compilador local, javac, compila el programa y
genera
cass que conticnen códligo
archivos .class que contienen código de bytes. El código de bytes
Java
die bytes. java Invoca al
representa el lenguaje intermedio portable que luego será
interpretado
interprete de Java, el cual
tambien es conocido como
ejecutando el intérprete de Java, java. El intérprete también se
conoce con el
Máquina virtual
nombre de Máquina virtual.
Para los programas Java, la entrada puede provenir de varios
puntos
diferentes:
Los argumentos de la linea de comandos son especialmente
importantes para especificar las
opciones del programa; hablaremos de ellos en la Šección 2.4.5.
Java proporciona mecanismos
que permiten leer y escribir archivos, lo que se aborda brevemente
en la Sección 2.6.3 y en más
detalle en la Sección 4.5.3, como ejemplo del patrón decorador.
Muchos sisternas operativos
proporcionan una alternativa que se conoce con el nombre de
redirección de archivos, en la que el
sistema operativo se encarga de leer la entrada desde (o de enviar
la salida a) un archivo, de forma
totalmente transparente para el programa que se está ejecutando.
En Unix (y también desde una
ventana MS/DOS), por ejemplo el comando
Programa java < archivoentrada > archivosalida
s encarga de organizar las cosas automáticamente para que las
lecturas del terminal se redirijan
de modo que provengan de archi voen trada y para que las
escrituras en el terminal se redirijan
y
vayan a archi vosalida.
1.2
El primer programa
Comenzaremos examinando el sencillo programa Java mostrado
en la Figura 1.1. Este programa
imprime una frase corta en el terminal. Óbserve que los números
de linea mostrados a la izquierda
del código no forman parte del programa. Se suministran
simplemente para facilitar las referencias,
Guarde el programa en el archivo fuente FirstProgram. java y
después compilelo y ejecútelo.
Tenga en cuenta que el nombre del archivo fuente debe
corresponderse con el nombre de la clase
(mostrado en la linea 4), respetando el uso de mayúsculas y
minúsculas. Si está empleando el JDK,
los comandos serán:'
javac Firstprogr am .java
ja va FirstProgram
1 Si está empleando el JDK de Sun, javac y java se utilizan
directamente. En Caso contrario, en un entorno de desrrollo
interactivo
IDE) típico, como Netbeanso Eclipse, estcei comandos son
ejecutados en nuestd nombre entre bastidores.
5
1.2 EI primer programa
-
1 I/ Primer programa
2 /l MW. 5/1/10
3
public class FirstProgram
4
5
l
6
public static void ma in( String L J args
)
7
B
System- out -println( "Is there anybody out there?" ):
9
10 |
Figura 1.1 Un primer programa sencillo.
1.2.1
Comentarios
Java dispone de tres formas de comentarios, La primera de ellas,
heredada de C, comienza con la
secuencia de simbolos /* y termina con *l. He aquí un ejemplo:
/* Esto es un comentario
de dos lineas * /
Los comentarios no pueden anidarse.
La segunda forma, heredada de C++, comienza con la secuencia de
Los comevtarias factitan
símbolos lly no emplea ninguna secuencia de simbolos de
terminación. En
a las personas ta lectura
del código. Java dispone
lugar de ello, el comentario se extiende simplemente hasta el final
de la linea,
de tres formalos de
como se muestra en las lineas 1 y 2 de la Figura 1.1.
comentario.
La tercera forma comienza con los simbolos /** en lugar de /*. Esta
forma
se puede utilizar para proporcionar información a la utilidad
javadoc la cual
generará la documentación del programa a partir de los
comentarios. Esta forma se explica en la
Sección 3.3.
La finalidad de los comentarios es facilitar la lectura del código a
los seres humanos, entre los que
se incluyen otras programadores que necesiten modificar o utilizar
nuestro código, aunque también
nos facilitarán la compresión del programa a nasotros mismos. Un
programa bien comentado es un
signo claro de que su autor es un buen programador,
1.2.2
ma in
Un programa Java está compuesto por una colección de clases
que interactúan, las cuales contienen
una serie de métodos. El equivalente Java de las funciones y
procedimientos es el método estático,
que describiremos en la Sección 1.6. Al ejecutar cualquier
programa, se
invoca el método estático especial ma in. La Ăínea 6 de la Figura
1.1 muestra
Cusando se ejecuta un
que el método estático ma in puede invocarse con argumentos de
la linea de
programa, se inmvoca el
comandos. Los tipos de parámetros de main yel tipo de retorno
void que se
mttodo especłal ma 1 n.
muestran en esa linea son obligatorios.
6
Capitulo 1 Estructura primitiva del lenguaje Java
1.2.2
Salida a través de terminal
El programa de la Figura 1.1 está compuesto por una única
instrucción,
printlns se uliliza
mostrada en la línea 8. printin es el mecanismo de salida principal
para enviar la salda del
en Java. Aquí, se coloca una cadena de caracteres constante en el
flujo
programa.
estándar de salida System. out aplicando un método println.
Hablaremos
con más detalle acerca de la entrada y la salida en la Sección 2.6.
Por
ahora, nos limitaremos a mencionar que se utiliza esa misma
sintaxis para llevar a cabo la salida,
independientemente de la entidad que se quiera emplear: no
importa que esa entidad sea un entero,
un número en coma flotante, una cadena de caracteres o de
cualquier otro tipo.
1.3
Tipos primitivos
Java define ocho tipas primitivos También proporciona al
programador uina gran flexibilidad
a
la hora de definir nuevos tipas de objetos, denominados clases. Sin
ermbargo, existen importantes
diferencias en Java entre los tipos primitivos y los tipos definidos
por el usuario. En esta sección
vamos a examinar los tipos primitivos y las operaciones básicas
que pueden definirse con ellos.
1.3. 1
Los tipos primitivos
Java dispone de los ocho tipos primitivos mostrados en la Figura
1.2. El tipo
Los tlpos primilnos en Java
más común es el correspondiente a los números enteros, el cual
se especifica
son los del lipo entero, de
coma fotante, boolicanos y
mediante la palabra clave int. A diferencia de muchos otros
lenguajes de
de carácter,
programación, el rango de los enteros no depende de la máquina.
En lugar
de ello, coincide en todas las implementaciones Java,
independientemente
de la arquitectura de computadora subyacente. Java también
permite utilizar
El estándar Unicodo
entidades de tipo byte, short y 1ong, que son conocidas como
tipos enteros.
Contiene mas die 30.000
y
caraceres codfcados
Los números en coma flotante se representan mediante los tipos
float
distintos, lo que cubire
double. double tiene más digitos significativos, por lo que se
recomienda su
los idiomas escrilos más
Uso antes que el de float. EÌ tipo char se utiliza para representar
caracteres
importantes.
individuales. Un char ocupa 16 bits para representar ei estándar
Unicode.
Este estándar contiene más de 30.000 caracteres codificados
distintos, lo que
aubre todos los idiomas escritos más importantes. El subconjunto
de menor peso de Unicode es
idéntico a ASCII. El último de los tipos primitivos es boolean, que
puede tomar los valores true o
false.
1.3.2
Constantes
Las constantes enteras pueden representarse en notación
decimal, octal
o
hexadecimal. La notación octal se indica mediante el prefijo 0; la
notación
Las constantes enteras
hexadecimal se indica mediante el prefijo Ox o 0x. `Todas las
siguientes
pueden representarse en
nolación decimat, oetalo
serían formas equivalentes de representar el entero 37: 37, 045.
Ox25. En este
hexadecinnł.
texto no vamos a utilizar los enteros octales; sin embargo,
tenemos que ser
conscientes de su existencia, con el fin de utilizar Os como prefijo
solamente
7
1.3 Tipos primitivos
Figura 1.2 Los ocho bipos prlmitlvos de Java.
cuando pretendamos hacerlo conscientemente así. Utilizaremos
hexade-
Lina constante de cadena
cimales en un único lugar (Sección 12.1), y allf hablaremos con
algo más de
está compuesta por unta
secuoncia de caracteres
detalle acerca de los mismos.
oncerrados entre dobics
Una constante de caracteres se encierra entre una pareja de
comillas
oumilas.
simples, como en *a' . Internamente, esta secuencia de caracteres
se interpreta
como un número de pequeña magnitud. Las rutinas de salida
interpretarán
posteriormente ese número de pequeña magnitud como el
carácter corres-
Las sacuencias de escape
se utitzan para representar
pondiente. Una constante de cadena consta de una secuencia de
caracteres
ciestas cunstantes de
encerrada entre comillas dobles, como en "Hola". Existen algunas
secuencias
carácter.
especiales, conocidas con el nombre de secuencias de escape,
que tienen usos
específicos (por ejemplo, ¿cómo representaríarmos una comilla
simple?). En
este texto utilizaremos *In', *\\',' *\ ' ' y *\"' , que representan,
respectivamente, el carácter de
nueva línea, el carácter de barra inclinada a la izquierda, la comilla
simple y la comilla doble.
1.3.3
Declaración e inicialización de tipos primitivos
Cualquier variable, incluyendo las de tipo primitivo, se declara
propor-
Las variables se ncenbran
cionando su nombre, su tipo y, opcionalmente, su valor inicial. El
nombre
medianie un idlentificador.
deber ser un identificador: Los identificadores pueden estar
compuestos
por cualquier combinación de letras, digitos y caracteres de guión
bajo; sin
embargo, no pueden empezar por un digito. Las palabras
reservadas, como Int, no están permitidas.
Aunque es legal hacerlo, es recomendable no reutilizar los
nombres de identificador que ya se estén
empleando de forma visible (por ejemplo, no utilice ma in como
nombre de una entidad).
Java diferencia entre mayúsculas y minúsculas, lo que quiere decir
que Age y age son identi-
ficadores distintos. En este texto se utiliza el siguiente convenio
para denominar a las variables:
todas las variables comienzan con una letra minúscula y cada
palabra
lava diferencia entre
nueva empieza con una letra mayúscula. Un ejemplo sería el
identificador
mayusculas Y minuscules.
mint mumWage.
8
Capitulo 1 Estructura primitiva del lenguaje Java
He aquí algunos ejemplos de declaraciones:
int num3 :
// Inicialización predetermi nada
double minimumwa ge - 4 .50 :
// Inicialización estándar
int x = 0 . num1 = O:
// Se declaran dos entidades
int num2 = num1 :
Las variables deben declararse cerca del lugar en el que se las
utilice por primera vez.. Como
veremos, la colocación de una declaración determina su ámbito y
su significado.
1.3.4
Entrada y salida a través de terminal
La E/S formateada básica a través de terminal se realiza mediante
nextline y println. El flujo
estándar de entrada es System. in y el flujo estándar de salida es
System. out.
El mecanismo básico para la É/S formateada utiliza el tipo String,
del que hablaremos en la
Sección 2.3. Para la salida, + permite combinar dos objetos String.
Si el segundo argumento no es
de tipo String, se crea para él un objeto String temporal, sies de
tipo primitivo. Estas conversiones
a String también pueden definirse para los objetos (Sección 3.4.3).
Para la entrada, asociamos un
objeto Scanner con System. in, lo que nos perrmitirá leer un objeto
String o un tipo primitivo. En
la Sección 2.6 se proporciona una explicación más detallada de la
E/S, incluyendo el tratamiento de
los archivos formateados.
1. .4
Operadores básicos
En esta sección se describen algunos de los operadores
disponibles en Java. Estos operadores se
emplean para formar expresiones. Una constante o una entidad
son, por sí mismas, una expresión,
como también lo son las combinaciones de constantes y variables
mediante operadores. Una
expresión seguida por un punto y coma es una instrucción simple.
En la Sección 1.5, examinaremos
otros tipos de instrucciones, en las que presentaremos operadores
adicionales.
1.4.1
Operadores de asignación
En la Figura 1.3 se muestra un programa Java simple que ilustra
unos cuantos operadores. El
operador de asignación básico es el signo igual. Por ejemplo, en la
linea 16, se asigna a la variable
a el valor de la variable c (que en ese punto es 6). Las
modificaciones posteriores en el valor de
c
no afectan a a. Los operadores de asignación se pueden
encadenar, como por ejemplo en z-y=x=0.
Otro operador de asignación es to, cuyo uSo se ilustra en la línea
18 del listado de la Figura 1.3.
El operador +- suma el valor situado en el lado derecho (del propio
operador) a la variable indicada
en el lado izquierdo. Por tanto, en la Figura 1.3, c se incrementa,
pasando del valor de 6 que tenía
antes de la linea 18 a un valor de 14.
Jarva proporciona dhversos
Java proporciona varios otros operadores de asignación, como por
opevracoros de asinacibn,
ejemplo --. *-y / -, que modifican la variable indicada en el lado
izquierdo
incluyendon " *-, ",
del operador mediante las operaciones de resta, multiplicación y
división,
yl-.
respectivamente.
9
1.4 Operadores básicos
-
public class OperatorTest
1
2
3
// Progr ama que ilustra 1os operadores básicos
4
// La salida es 1a siguiente:
5
Il 12 8 6
6
Il 6 8 6
7
I/ 6 8 14
B
I/ 22 8 14
9
I/ 24 10 33
10
11
public static void ma In( String C 1 args
)
12
int a = 12 . b = B. c = 6 :
13
14
**
"
"*
*#b+
15
#c
):
System -out -println( a +
16
a - c:
-*
*.
*.
*.
+
+
17
+
b
):
c
System. .out ·println( a +
18
c t= b:
**
*.
*.
+
my
19
#
+
):
b
C
System .out .println( a +
20
a = b # c:
**
**
C
b
+
21
.
+
):
System .out -println( a +
22
a++:
23
+b;
24.
c = a++ * ++b;
25
System. out .println( a + "*. *. # b + *. + c ):
26
27. )
Figura 1.3 Prograna que llustra el uso delos operadores.
1.4.2
Operadores aritméticos binarios
La linea 20 de la Figura 1.3 ilustra uno de los operadores
aritméticos binarios que son típicos de
todos los lenguajes de programación: el operador suma (+). El
operador +- hace que los valores de
b y cse sumen; b y c por su parte, no sufrirán ninguna
modificación. El valor resultante se asigna
a a. Otros operadores aritméticos típicamente utilizados en Java
son -, *, /y %, que se emplean,
respectivamente, para la resta, la multiplicación, la división y la
operación de cálculo del resto. La
división entera solo devuelve la parte entera y descarta el resto.
Como suele ser habitual, la suma y la resta tienen la misma
precedencia, y dicha precedencia
es inferior a la del grupo compuesto por los operadores de
multiplicación,
llava propcrciona diversos
división y módulo; por tanto, 1+2*3 daría como resultado 7. Todos
estos
oporadores arilmotoos
operadores se asocian de izquierda a derecha (por lo que 3-2-2 da
como
binarios, Indiuyendo 1.2.
resultado -1). Todos los operadores tienen precedencia y
asociatividad. En el
*,/ y %.
Apéndice A se proporciona una tabla completa de operadores.
10
Capitulo 1 Estructura primitiva del lenguaje Java
1.4.3
Operadores unarios
Además de los operadores aritméticos binarios, que requieren das
operandos,
Estan definklos varios
Java proporciona operadores unarios, que solo requieren un único
operando.
opevradonts unanias,
incfuyendo -
El más familiar de estos operadores es el menos unario, que
proporciona
omo resultado el negado de su operando. Así, -x devuelve el
negado de X.
Java proporciona también ei operador de autoincremento para
sumar
1 a una variable (dicho operador se denota mediante +t) y el
operador de
Eloperador de
ailoncrementoy el de
autodecremento para restar 1 de una variable (dicho operador se
denota
aulodecremanto suman 1y
mediante --). El uso más benigno de esta funcionalidad se muestra
en
reslan 1. respetivamente.
las lineas 22 y 23 de la Figura 1.3. En ambas lineas, el operador de
Los oporadores paara
esto son ** y --. Hay dos
autoincremento + suma 1 al valor de la variable. En Java, sin
embargo, un
iormas de inorementas
operador aplicado a una expresión nos da una expresión que tiene
un valor.
ydecrementar; prelie y
Aunque se garantiza que la variable será incrementada antes de la
ejecución
posifija.
de la siguiente instrucción, nos surge inmediatamente la pregunta:
¿cuál es el
valor de la expresión de autoincremento si se utiliza dentro de una
expresión
de mayor tamaño?
En este caso, es crucial la colocación del operador **. La
semántica de #tx es que el valor de la
expresión será el nuevo valor de X. Esto se denomina incremento
prefjo. Por contraste, x++ significa
que el valor de la expresión es el valor original de x. Esto se
denomina incremento postfijo. Esta
característica se muestra en la línea 24 de ia Figura 1.3, donde a y
b se incrementan en 1, y c se
calcula sumando el valor original de a al valor incrementado de b.
1.4.4
Conversiones de tipo
El operador de conversión de tipo se utiliza para generar una
entidad temporal de un tipo nuevo.
Considere, por ejemplo
double quotient:
int x = 6 :
int y = 10:
quotient = x / y: Hl iProbabl emente incorrectol
La primera operación es la división, y como X ey son ambas
enteras, el resultado será una división
entera, y obtendremos 0. El entero O será después convertido
implícitamente a un valor double para
poderlo asignar a quotient. Pero lo que queríamos era asignar a
quo tient el valor 0.6. La solución
es generar una variable temporal para x 0 y, de modo que la
división se lleve a cabo aplicando las
reglas para double. Esto se haría de la siguiente forma:
quotient en (
dauble ) x y;
Observe que ni x ni y cambian. Se crea una entidad temporal sin
nombre
yse utiliza ei valor de esa entidad temporal para la división. El
operador de
El operador dla convors kin
conversión de tipo tiene una mayor precedencia que la división,
por lo que
de tipo se utiliza para
genefaf una entitiad
primero se hace la conversión de tipo de xy luego se lleva a cabo
ia división
emporal de un lipo nuevo
(en lugar de realizarse la conversión después de hacer la división
de dos
valores int).
11
1.5 Instrucciones condicionales
1. 5
Instrucciones condicionales
En esta sección se examinan las instrucciones que afectan al flujo
de control: las instrucciones
condicionales y los bucles. Como consecuencia, presentaremos
nuevos operadores.
1.5. 1
Operadores relacionales y de igualdad
La prueba básica que podemos realizar con los tipos primitivos es
la compara-
En Java, lors operadovres dla
ción. Esta se lleva a cabo utilizando los operadores de igualdad y
desigualdad,
iguakiad son --yl-..
así como los operadores relacionales (menor que, mayor que, etc.).
Ein Java, los cperadores de igualdadson -y !=. Por ejemplo,
expr Izquierda=-exprDerecha
se evalúa como true si exprIzquierda y exprDerecha son iguales;
en caso contrario, dará como
resultado false. De forma similar,
exprIzquierda! -exprDerecha
se evalúa como truesi exprIzquierda y exprDerecha no son iguales;
y false en caso contrario,
Los operadores relacionales son <, <-. >y >-. Estos tienen sus
significados naturales para los
tipos predefinidos. Los operadores relacionales tienen una
precedencia mayor que los operadores
de igualdad y ambos tienen una precedencia inferior a los
operadores aritméticos, pero mayor
precedencia que los operadores de asignación, de modo que
frecuentemente es innecesario utilizar
parêntesis. Todos estos operadores se asocian de izquierda a
derecha, pero este hecho es irrelevante:
en la expresión a<b<6, por ejemplo, el primer < genera un boo lean
y el
Los operaciores refacionales
sgundo es ilegal porque <no está definido para los valores de tipo
boolean.
son <62y2=.
En la siguiente sección se describe la forma correcta de realizar
esta
comprobación.
1.5.2
Operadores lógicos
Java proporciona operadores lógicos que se utilizan para simular
los
kava propordona
conceptos de AND, OR y NOT propios del álgebra booleana, En
ocasiones,
opovradores logicos que sc
utäzan pora simula kos
estos operadores se designan con el nombre de conjunción,
disyunción y
conceptos de AND ORy
ngación, respectivamente, y sus operadores correspondientes son
&&, Il y 1.
HOT propios del álgebra
booleana. Lod operadores
La comprobación de la sección anterior se podría implementar
correctamente
coxrespondientes son S&
como a<b && b<6. La precedencia de la conjunción y la disyunción
es lo
lyl
suficientemente baja como para que no sean necesarios los
paréntesis. E.l
operador && tiene una mayor precedencia que I!. mientras que ! se
agrupa
con otras operadores unarios Čy tiene, por tanto, la mayor
precedencia de los
tres). Los operandos y los resultados de los operadores lógicos
son de tipo boolean. La Figura 1.4
muestra el resultado de aplicar los operadores lógicos a todas las
posibles entradas.
Una regla importante es que &&y || son operaciones de evaluación
cortocircuitables, El concepto
de evaluación cortocircuitable quiere decir que si el resultado
puede determinarse examinando la
primera expresión, entonces no se evalúa la segunda expresión.
Por ejemplo, en
12
Capitulo 1 Estructura primitiva del lenguaje Java
Figura 1.4 Resultado de las operadores lógicos.
El concepto de evabacin
x !- 0 && 1/x != 3
corfocircurable quiere
dece que si ed resuttado de
si xes 0, entonces la primera mitad será false. Automáticamente,
el resultado
un operador lópico puede
delerminarse examinando
de la operación AND deberá ser false, por lo que ni siquiera se
llega
a
la primera oxpresion,
evaluar la segunda mitad. Esto es bueno porque la división por
cero nos daría
entonces no se evalla la
un comportamiento erróneo. La técnica de evaluación
cortocircuitable nos
segunda expresión.
permite no tener que preocuparnos acerca de esa división por
cero concreta.2
1.5.3 La instrucción if
La instrucción if es la principal de las formas para llevar a cabo la
toma de
La instructión 1f es la
decisiones en los programas. Su forma básica es:
principal de las lormas
para llevar a cabola toma
de decisiones en los
if( expresión )
propamas.
instrucción
siguiente instrucci ión
Si expres i ón se evalúa como true, entonces se ejecuta instrucc i
ón; en caso contrario, no se
ejecuta. Cuando se completa la instrucción if (sin que se haya
producido un error no tratado), el
control pasa a la siguiente instrucción.
Opcionalmente, podemos utilizar una instrucción if-else de la
forma siguiente:
if( expresión )
instrucción1
else
instrucción2
siguiente instrucción
En este caso, si expresión se evalúa como true, entonces se
ejecuta ins trucci ón1; en caso
contrario, se ejecuta instrucción2. En cualquiera de los dos casas,
el control pasa a continuación a
la siguiente instrucción, como en
1
Hay casos (extremadamente) raros en los que cs preferible no
reallza el cortocirculto. En tales casoS, los operadores & y |con
argu:
mentos bool ean garantizan que ambos argumentos se evalúen,
inclusa si el resuliado de la operación pxede determinarse a partir
del
primer argumento.
13
1.5 Instrucciones condicionales
System. .out -print( "1/x 1s *- ):
if( x !- 0 )
System .out -print( 1 / x ):
else
System .out -print( "Undefined" ):
System .out ·println( ):
Recuerde que cada una de las cláusulas if y else contiene al
menas una instrucción,
independientemente de cómo utilicemos la sangría del texto. He
aquí dos errores típicos:
if( x a 0 ): H/ : es una instrucción nula Cy hay que tenerla en
cuenta)
System .out -printin( "x is zero *. );
else
System .out -print( "x is );
System .out -printin( x ): / Dos instrucc iones
El primer error es la inclusión del carácter : al final del primer if.
Este
Uin punto y coxm a alslado
sea, por si mismo, una
punto y coma cuenta, por sí mismo, como instrucción nula, en
consecuencia,
instrucción OLlE
este fragmento no se compilará correctamente (el else ya no estå
asociado
con un if). Una vez corregido este ertor, tenemos otro error lógico:
la
última linea no forma parte del else, aun cuando el sangrado del
texto lo
lin bloque esuna
sugiera, Para corregir este problema tenemos que utilizar un
bloque, en el
socuoncia de Instruocionos
que tendremos una secuencia de instrucciones encerrada entre
una pareja de
encerrada entre 1laves.
llaves:
if( x -- 0 )
System .out -printin( "x 1s zero" ):
else
{
System .out -print( "x is " ):
System .out -println( x ):
l
La înstrucción if puede ser a su vez incluida dentro de otra
cláusula if o else, al igual que
otras instrucciones de control de las que hablaremos más
adelante en esta sección. En el caso de
instrucciones if-else anidadas, cada else se corresponderá con el
1f abierto más interno. Puede
ser necesario añadir llaves si ese no es el significado que
pretendemos.
1.5.4
La instrucción while
Java proporciona tres formas básicas de bucle: la Instrucción
while, la
La Instrucoon while
instrucción fory la instrucción do. La sintaxis para la instrucción
while es
es una de lastres formas
básicas de Implementacion
while( expresión
)
de budes
instrucción
siguiente instrucción
14
Capitulo 1 Estructura primitiva del lenguaje Java
Observe que, coma en la instrucción if, no hay ningún punto y
coma en la sintaxis. Si se incluye
uno, se interpretará como instrucción nula.
Mientras que expresión es true se ejecuta instrucción; despuês,
vuelve a evaluarse
expresión. Si expres ión es inicialmente false, instrucción mse
ejecutará nunca. Generalmente,
instrucci ón hace algo que puede modificar potencialmente el
valor de expresión; en caso
contrario, podría ser infinito. Cuando el bucle while termina
(normalmente), el control se devuelve
a la siguiente instrucción.
La instrucción for
1.5.5
La instrucción whiles es suficiente para expresar todo tipo de
repeticiones. Aun así, Java proporciona
otras dos formas de implementación de bucles: la instrucción fory
la instrucción do. La instrucción
for se utiliza principalmente para las iteraciones. Su sintaxis es
for( inicialización; comprobación : actualización
)
Instrucción
sigufente instruccián
Aquí, inicialización, compr obación y actualizaci ón son todas
La instructáon for es uns
estructura de bude que se
expresiones y son todas ellas opcionales. Si na se proporciona
compr `obación,
utilza principalmente pea
toma como valor predeterminado true. No se incluye ningún punto
y coma
Reradones simples.
después del paréntesis de cierre.
La instrucción for se ejecuta realizando primero la inicialización.
Después, mientras que compr obac1 6n es true, se llevan a cabo
las dos acciones siguientes: se
ejecuta instrucción y luego se realiza la actualización, Si se
omiten la inicializac i ón y la
actual izaci ón, entonces la instrucción for se comporta
exactamente como una instrucción while.
La ventaja de una instrucción for es la claridad, en el sentido de
que para las variables que sirven
como contador (de iteraciones), la instrucción for hace que sea
mucho más fácil ver cuál es el rango
de ese contador. Por ejemplo, el siguiente fragmento imprime los
primeros 100 enteros positivos:
)
for( int 1 = 1. i c= 100: itt
System .out -printin( 1 ):
Este fragmento ilustra la técnica común de declarar un contador
en la parte de inicialización del
bucle. EI ámbito del contador solo abarcará el interior del bucle.
Tanto inicialización como actual Izaci ión pueden utilizar una coma
para incluir múltiples
expresiones. El siguiente fragmento ilustra esta variante:
)
forc 1 - 0, sum - 0; 1 <- n: i++. sum #- n
System .out -println( i + *\t" + sum ):
Los bucles se anidan de la misma forma que las instrucciones 1f,
Por ejemplo, podemos
encontrar todas las parejas de números pequeños cuya suma sea
igual a Su producto (como por
ejemplo 2 y 2, cuya suma y producto son ambos iguales a 4):
)
for( int i = 1: i C 10; i+t
for( int j = 1; j <= 10: jtt
15
1.5 Instrucciones condicionales
if( i + j= 1 * j )
System out -println( 1i + *- " + j )
Como veremos, sin embargo, al anidar bucles podemos llegar a
crear fácilmente programas
cuyos tiempos de ejecución crezcan rápidamente.
Java 5 añade un bucle for "mejorado". Hablaremos de esta mejora
en la Sección 2.4 y en el
Capítulo 6.
La instrucción do
1.5.6
La instrucción while ejecuta de forma repetida una comprobación.
Si la
La Iinstruccioón does una
estruciura de bude que
comprobación es true, entonces ejecuta una instrucción
especificada. Sin
arantiza quie el budle se
embargo, si la comprobación inicial da como resultado false, la
instrucción
ejecuta al menos una vez.
especificada no llega nunca a ejecutarse. No obstante, en algunos
casos,
deseáremos garantizar que la instrucción especificada se ejecute
al menos una
vez. Esto se hace utilizando la instrucción do. La instrucción do es
idéntica
a la instrucción while, excepto porque la comprobación se realiza
después de haber ejecutado la
instrucción especificada, La sintaxis es:
do
instrucción
while( expresión ):
siguiente instrucción
Observe que la instrucción do incluye un punto y coma. Un uso
tipico de la instrucción do es el que
se muestra en el siguiente fragmento de pseudocódigo:
do
l
Pedir datos a1 usuario:
Leer e1 valor:
while( el valor no sea correcto):
La instrucción do es, con mucho, la menos frecuentemente
utilizada de las tres estructuras de
bucle. Sin embargo, cuando tenemos que hacer algo al menos una
vez y por alguna razón resulta
inapropiado emplear un bucle for, entonces la instrucción do es el
método preferido.
1.5.7
break y continue
Las instrucciones for y while permiten terminar el bucle antes del
principio de una instrucción
repetida. La instrucción do permite terminar el bucle después de
la ejecución de una instruc-
ción repetida. Ocasionalmente, lo que querriamos es terminar la
ejecución en mitad de una
instrucción (compuesta) repetida. La instrucción break, que está
compuesta por la palabra
clave break seguida por un punto y coma, puede emplearse para
conseguir precisamente esto.
Normalmente, la instrucción break irá precedida de una
instrucción if, como en
16
Capitulo 1 Estructura primitiva del lenguaje Java
while( a. )
l
)
ifl algo
break:
La instrucción break solo existe en el bucle más interno (tambiên
se
utiliza en conjunción con la instrucción switch, que se describe en
la
La instrucoin break hace
siguiente sección). Si es necesario salir de varios bucles, la
instrucción break
que se salga del bucle O
instrucdon switch más
no funcionará, y lo más probable es que terminemos obteniendo un
código
internos. La instruccion
con un diseño bastante pobre. Aun así, Java proporciona una
instrucción
break etiquetada permite
sals de un bude anidado.
break etiquetada. En la instrucción break etiquetada, se etiqueta
un bucle
y
luego puede aplicarse una instrucción break al bucle,
independientemente de
cuántos otros bucles haya anidados. He aquí un ejemplo:
externo:
whilel al )
t
while( . )
ifc desastre )
break externo: Hl Ir a 1a instrucción siguiente a externo
l
/l. ET control pa sa aquf despues de salir del bucle externo
Ocasionalmente, lo que queremos es terminar la iteración actual
de un
La instrucdin continue
pasa a la siguionte ileración
bucle e ir directamente a la siguiente iteración. Esto puede
conseguirse
del buciae mas Interno,
utilizando una instrucción cont inue. Al igual que la instrucción
break, la
instrucción continue incluye un punto y coma y se aplica
únicamente al
bucle más interno. El siguiente fragmento imprime los primeros
100 enteros, con excepción de
aquellos que sean divisibles por 10:
for( int i = 1: i C 100: 1+
l
If( 1 x 10 --- 0 )
cont inue:
System .out -printin( i D:
}
Por supuesto, en este ejemplo, existen alternativas que podrian
utilizarse en lugar de la instrucción
cont Inue. Sin embargo, se emplea continue de forma bastante
común para evitar incluir patrones
if-else complicados dentro de los bucles.
17
1.6 Métodos
1.5.8
La instrucción swi tch
La instrucción switch se utiliza para elegir entre varios valores
pequeños de
La instrucción swItch
tipo entero (o carácter). Está compuesta por una expresión y un
bloque. El
se ulitza pera sedleccionar
entre varios valores
bloque contiene una secuencia de instrucciones y una colección
de etiquetas,
pequenosi de tipo enero (o
que representan los posibles valores de la expresión. Todas las
etiquetas
carscier).
deben ser constantes de tiempo de compilación diferentes. Si está
presente,
una etiqueta predeterminada opcional permite hacer referencia a
todas las
etiquetas no representadas. Si no hay ningún caso aplicable a la
expresión de la instrucción switch,
la Instrucción switch termina; en caso contrario, el control pasa a
la etiqueta apropiada y se ejecutan
todas las instrucciones que se encuentren a partir de ahí. Puede
utilizarse una instrucción break para
forzar la terminación anticipada de la instrucción switch y. de
hecho, casi siempre se emplea una
instrucción break para separar casos que Sean lógicamente
distintos. En la Figura 1.5 se muestra un
ejemplo típico de esta estructura,
El operador condicional
1.5.9
El operador condicional ? : se utiliza como abreviatura para
instrucciones
El cperncior condicionel 2:
if-else simples. Su formato general es:
se utiza como abreviatura
para instrucciones
comprobaci onExpr ? exprSi : exprNo
ffr-else soncillas.
En primer lugar se evalúa comprobac 1ónExpr, seguida por exprsi o
exprNo, generando así el resultado de la expresión completa,
exprsi se
evalúa si comprobaci lónExpr es true; en caso contrario, se evalúa
exprNo. La precedencia del
operador condicional está justo por encima de la de los
operadores de asignación. Por esta razón,
podemos evitar el uso de parêntesis al asignar el resultado del
operador condicional a una variable.
Como ejemplo, podemos asignar a minval el mínimo de x e y de la
forma siguiente:
mi nVa1 - X C= y ? x : y:
1. 6
Métodos
Lo que se conoce como función o procedimiento en otros
lenguajes, en Java,
Lin mátodo es simier a una
e denomina método. En el Capítulo 3 explicaremos en detalle los
métodos.
funcion en otros !enguajes.
La cabooura col mótodo
En esta sección se presentan algunos de los conceptos básicas
para la escritura
está compuesta por cl
de funciones, tales como ma in, de una forma no orientada a
objetos (como la
nombre, el tpo de retorno
y una ista de parámetros.
que podríamos encontrar en un lenguaje como C), con el fin de
poder escribir
La doclaracián del método
algunos programas sencillos.
induye el cuerpo det mismo.
Una cabecera de método consta de un nombre, una lista de
parámetros
(posiblemente vacía) y un tipo de retorno. El código concreto para
imple-
mentar el método, en ocasiones denominado aerpo del método, es
formalmente un bloque. Una
declaración de métodoconsta de una cabecera y un cuerpo. En la
Figura 1.6 se muestran un ejemplo
de declaración de método y una rutina ma in que lo utiliza.
18
Capitulo 1 Estructura primitiva del lenguaje Java
switch( algunCaracter
)
1
2{
3
case *(*:
4
case L' :
5
ca se "{':
6
I/ Código para procesar 1os simbolos de apertura
7
break:
8
9
case *)':
10
case 1' :
11
case * 1' :
12
I/ Código para procesar 1os símbolos de cierre
13
break:
14.
15
case *Ins
16
// Código para manejar e1 carácter de nueva lfnea
17
break:
18
19
default:
20
// Codigo para manejar otros casos
21
break;
22 1
Figura 1.5 Estructura de una Instrucción switch,
public class Mintest
1
2
3
public static void main( String C 1 args
)
4
{
5
int a = 3 :
6
int b - 7:
7
8
System aut -println( min( a, b) ):
g
10
11
Il Declaración del método
12
public static int minc int x. int y
)
13
return x < y 2 x : y:
14
15
16 }
Figura 1,6 Bustración de la declaración de un metodoy de una
llamada a ese método
19
1.6 Métodos
Anteponiendo a cada método las palabras public static, podemos
Lin método public
statlc esel equivalenle
imitar las funciones globales de estilo c. Aunque declarar un
método como
de una funcidn giobial
static resulta ser una técnica útil en algunas instancias, no se
debe abusar
estioC".
de ella, ya que, en general, no nos Interesará utilizar Java para
escribir código
"estilo c". En la Sección 3.6 hablaremos de la utilización más
típica de
static.
El nombre del método es un identificador. La lista de parámetros
está
En el paso pevr valor, los
compuesta por cero 0 más parámetros formales, cada uno de ellos
con un
argumentos reales se
coplan en los parámetros
tipo especificado. Cuando se invoca un método, los argumentos
reales
lormales, Las variabics se
se envían a los parámetros formales usando la asignación normal.
Esto
pasan medante paS0 pox
valor.
significa que los tipos primitivos se pasan utilizando únicamente
paso de
parámetros de tipo paso por valor: Los argumentos reales no
pueden ser
modificados por ia función. Como sucede en la mayoría de los
lenguajes de
programación modernos, las declaraciones de métodos pueden
disponerse en
La instrucdion return
cualquier orden.
se ulžiza para devolver un
valor al laname.
La instrucción return se utiliza para devolver un valor al llamante.
Si
el tipo de retorno es vo1d, entonces no se devuelve ningún valor y
no debe
utilizarse return: dentro del método.
1.6. 1
Nombres de métodos sobrecargados
Supongamos que necesitamos escribir una rutina que devuelva el
máximo de tres valores de tipo
int. Una cabecera de método razonable sería
}
int ma x( int a . int b , int c
En algunos lenguajes, esto puede ser inaceptable si max ya está
declarada. Por ejemplo, podríamos
escribir también
int max( int a. int b
Java permite la sobrecarga de los nombres de métodos. Esto
significa
La sobrecarga die un
que puede haber varios métodos con el mismo nombre y que todos
ellos
nombre de mélodo quiere
decir qua puede haber
pueden declararse con el mismo ámbito de clase, siempre y
cuando sus
varios métodos con el
signaturas (es decir, los tipos de su lista de parámetros) sean
distintas.
mesimo nomtee, siempre
Cuando se hace una llamada a max, el compilador puede deducir
cuál
ycuando los tipos de su
sta de parametros sean
de los significados posibles hay que aplicar, basándose en el tipo
de los
distinios
argumentos utilizados. Puede haber dos signaturas que tengan el
mismo
número de parámetros, siempre y cuando al menos uno de los
tipos de esos
parámetros sea distinto.
Observe que el tipo de retorno no se incluye en la signatura. Esto
quiere decir que es ilegal
tener dos métodos con el mismo ámbito de clase que solo se
diferencien por el tipo de retorno. Los
métodos con diferentes ámbitos de clase pueden tener los mismos
nombres, las mismas signaturas
e
incluso los mismos tipos de retorno; esto se explica en el Capítulo
3.
20
Capitulo 1 Estructura primitiva del lenguaje Java
Clases de almacenamiento
1.6.2
las entidades que se declaran dentro del cuerpo de un método son
variables locales y solo se puede
acceder a ellas por su nombre dentro del cuerpo del método. Estas
entidades se crean al ejecutarse
el cuerpo del método y desaparecen cuanlo el cuerpo del método
termina.
Una variable declarada fuera del cuerpo de un método será global
para
esa clase. Es similar a las variables globales en otros lenguajes, si
se utiliza
Las variables static
la palabra static (que es probable que sea necesaria, para poder
hacer que
final son constantes.
la entidad sea accesible por parte de los métodos estáticos). Si se
utilizan
tanto static como final, serán constantes simbólicas globales.
Como por
ejemplo,
static final double PI = 3.141592653589 7932 ;
Observe el uso del convenio común para denominar las constantes
simbólicas, que se escriben
completamente en mayúsculas. Si el nombre del identificador está
formado por varias palabras, se
las separa mediante un carácter de guión bajo, como en MAX, INT
_VALUE.
Si se ornite la palabra static, entonces ia variable (o constante)
tiene un significado distinto, del
que hablaremos en la Sección 3.6.5.
Resumen
En este capítulo hemos visto las características primitivas de
Java, tales como los tipos primitivos,
los operadores, las instrucciones condicionales y de bucle, y los
métodos, características todas ellas
que se encuentran en prácticamente todos los lenguajes.
Cualquier programa no trivial requerirá el uso de tipos no
primitivos, denominados tipos de
rferencia, de las que hablaremos en el siguiente capítulo.
o
Conceptos clave
bloque Una secuencia de instrucciones encerrada entre llaves.
(13)
break, instrucción Una instrucción que permite salir de la
instrucción de bloque o switch
más interna. (15)
break etiruetada, instrucción Una instrucción break utilizada para
salir de bucles
anidados. (16)
cahecera de método Está compuesta por el nombre, el tipo de
retorno y la lista de
parâmetros. (17)
constantede cadena Una constante compuesta por una secuencia
de caracteres encerrados
entre dobles comillas. (7)
cont inue, instrucción Una instrucción que hace que se salte a la
siguiente iteración del
bucle más interno. (15)
21
Conceptos clave
obdigo de bytes Código intermedio portable generado por el
compilador Java. (4)
comentarios Hacen que el código sea más fácil de leer por parte
de las personas, pero no
tienen ningún significado semántico. Java proporciona tres formas
de comentarios. (5)
constantes enteras octales y hexadeciales Constantes enteras
que pueden representarse
en notación decimal, octal a hexadecimal. La notación octal se
indica mediante un o
prefijo; la notación hexadecimal se indica mediante el prefijo Ox o
OX. (6)
declaración de mitodo Esta compuesta por la cabecera y el cuerpo
del método. (18)
do, instrucción Una estructura de bucle que garantiza que el bucle
Se ejecute al menos una
vez. (15)
entrada stindar El terminal, a menos que se redirija. También hay
flujos para la salida
estándar y la salida estándar de error. (4)
evaluación cortocircuitable El proceso por el cual, si el resultado
de un operador lógico
puede determinarse examinando la primera expresión, entonces la
segunda expresión
no se evalúa. (11)
for, instruccióa Una estructura de bucle utilizada principalmente
para iteraciones
simples. (14)
ientificador Se emplea para denominar una variable o método. (7)
if, instrucción La instrucción fundamental para la implementación
de la toma de
decisiones, (12)
instrucción nula Una instrucción que está compuesta por solo un
punto y coma. (13)
java El intérprete Java, que procesa código de bytes. (4)
javac El compilador Java; genera código de bytes. (4)
main El método especial que se invoca al ejecutarse el programa.
(5)
Mlaquina virtual El intérprete del código de bytes. (4)
metodo El equivalente Java de una función. (18)
aperadar condicioaal (2:) Un operador que se utiliza en una
expresión como abreviatura
para instrucciones if-else sencillas. (18)
aperador decanversión detipo Un operador utilizado para generar
una variable temporal
sin nombre de un nuevo tipo. (10)
operadares aritméticos binarios Se utilizan para efectuar las
operaciones aritméticas
básicas. Java proporciona varios de estos operadores, incluyendo
+. -, *,/y' %. (9)
operadares de asignación En Java, se utilizan para modificar el
valor de una variable.
Estos operadores incluyen =, += =. *=yl-. (8)
y
aperadares de autoincremento (++) y autoderremento (--)
Operadores que suman
y
restan 1, respectivamente. Existen doas formas de incremento y
decrenento: prefija
postfija. (10)
operadres de iguakdad En Java, => y l= se emplean para comparar
dos valores;
devuelven true o false (según sea apropiado). (11)
aperadores lógicos &&, Ily !, utilizados para simular los conceptos
de AND, OR y NOT
propios del álgebra booleana. (11)
22
Capitulo 1 Estructura primitiva del lenguaje Java
aperadares relacionales En Java, <4-,>y5- se utilizan para decidir
cuál de dos
valores es menor o mayor. Devuelven true o false. (11)
operadares unarios Requieren un operando. Hay definidos varios
operadores unarios,
incluyendo el menos unario (-)y los operadores de autoincremento
y autodecremento
(+ + y --). (10)
paso por valor El mecanismo de paso de parámetros en Java
mediante el cual se copia el
argumento real en el parámetro formal. (19)
return, instrucción Una instrucción utilizada para devolver
información al llamante. (19)
seruencia de escapeSe utiliza para representar ciertas constantes
de carácter, (7)
signaturaLa combinación del nombre del método y de los tipos de
la lista de parámetros.
El tipo de retorno no forma parte de la signatura, (19)
sobrecarga de nombres de métodos L.a acción de permitir que
haya varios métodos con
el mismo nombre, siempre y cuando los tipos de su lista de
parámetros difieran. (19)
static final, entidad Una constante global. (20)
static, mitodo Ocasionalmente utilizado para simular funciones
estilo C; hablaremos
más en detalle de este tipo de método en la Sección 3.6. (18)
switch, instrucción Una instrucción utilizada para seleccionar
entre valores enteros
pequeños. (17)
tipos enteros byte, char, short, inty 1ong. (6)
tipos primitivos En Java, son las enteros, los de coma flotante, los
booleanos y los de
carácter. (6)
Unicode Conjunto internacional de caracteres que contiene más
de 30.000 caracteres
distintos, que cubren los lenguajes escritos más importantes. (6)
while, instrucción La forma más básica de bucle. (13)
Errores comunes
23
Ejercicios
Internet
A continuación se indican las archivos disponibles para este
capítulo. Todo está auto-
contenido y nada de ello se utiliza posteriormente en el texto.
FirstProgramjava
El primer programa, como se muestra en la Figura 1.1.
Operata Testjava
llustración de varios operadores, como se muestran en la Figura
1.3.
MinTest java
llustración de los métodos, como se muestra en la Figura 1.6.
Ejercicios
EN RESUMEN
24
Capitulo 1 Estructura primitiva del lenguaje Java
EN TEORÍA
1.11 Para los siguientes fragmentos de código, proporcione un
ejemplo en el que el bucle
for de la izquierda no sea equivalente al bucle while de la derecha:
init:
forc inic: comprob: actualiza )
whilec comprob
)
l
l
instrucciones
instrucc iones
actualizacion:
1.12 Para el siguiente programa, ¿cuáles son las posibles salidas?
public class WhatIsx
)
public static void f( int X
l /* cuerpo desconocido */ }
public static void main( String l 1 args )
l
int x = 0:
f( x ):
[Link] .printinc x ):
113 Supongamas que b tiene el valor 7y que c tiene el valor 12.
¿Cuál será el valor de a,
by c después de cada linea del siguiente fragmento de programa:
1.14 ¿Cuál es el resultado de true |l false && true?
EN LA PRÁCTICA
25
Referencias
L18 Escriba un instrucción while que sea equivalente al siguiente
fragmento for, ¿Para
qué podría resultar esto útil?
for( : }
instrucción
PROYECTOS DE PROGRAMACION
A-1 W=2 N-3 R=4 E=5
MARK
9147
+
L=6 K=7 I=8 M=9 S=0
ALLEN
+16653
--.- ---
25800
WEISS
Referencias
Parte del material estilo C de este capítulo se ha tomado de [5].
Puede encontrar la
especificación completa del lenguaje Java en [2]. Entre los libros
de introducción a Java
podemos citar [1], i3]y [4].
2
Capítulo
Tipos de referencia
En el Capítulo 1 se han examinado los tipos primitivos de Java
Todos los tipos distintas de los ocho
tipos primitivos son los tipos de referencia, incluyendo entidades
de importancia como las cadenas,
las matrices y los flujos de archivos. En este capítulo, vamos a ver
2. 1
¿Qué es una referencia?
En el Capítulo 1 se han descrito los ocho tipos primitivos, junto
con algunas de las operaciones
que estos tipos pueden realizar. Todos los demás tipos en Java son
tipos de referencia, inclu-
yendo las cadenas, las matrices y los flujos de archivos.
¿Entonces, qué es una referencia? Una
variable de referencia (que a menudo se abrevia designándola
simplemente como referencia)
en Java es una variable que almacena de alguna manera la
dirección de memoria en la que un
objeto reside.
Como ejemplo, en la Figura 2.1 se muestran dos objetos de tipo
Point. Sucede, por azar, que
estos objetos están almacenados en las posiciones de memoria
1000 y 1024, respectivamente.
Para estos dos objetos, hay tres referencias: pointl, point2 y
point3. Tanto pointl como
point3 hacen referencia al objeto almacenado en la posición de
memoria 1000; point2 hace
referencia al objeto almacenado en la posición de memoria 1024.
Tanto point1 como point3
almacenan el valor 1000, mientras que po int2 almacena el valor
1024. Observe que las
posiciones concretas, como por ejemplo 1000 y 1024, son
asignadas por el sistema de tiempo de
ejecución de manera completamente discrecional (cuando
encuentra memoria disponible). Por
tanto, estos valores no son útiles externamente como números. Sin
embargo, el hecho de que
pointl y po int3 almacenen valores idénticos s que resulta útil:
quiere decir que están haciendo
referencia al mismo objeto.
Una referencia se almacenará siempre en la dirección de memoria
en la que resida el objeto,
a
menos que no esté haciendo actualmente referencia a ningún
objeto. En ese caso, almacenará la
rferencia nula, nu11. Java no permite referencias a variables
primitivas.
28
Capitulo 2 Tipos de referencia
( O, o )
1000
(0, 0 )
(5, 12)
1024
(en 1000)
point2 = 1024
3200
(5, 12 )
3600
pointl = 1000
(en 1024)
5124
point3 = 1000
Figura 2.1 Ilustración delas relerenclas El objelo Point almacenado
en la posición de memoria 1000 está siendo referenclado tanto pat
pofntl
COmO por point3. El objeto Point aimacenado enla posición de
memoria 1024 está slendo referenciado por point2. Las posiciones
de memoria
en las que están aimacenadas las wariables son artitrarlas.
Hay dos categorias amplias de operaciones que se pueden aplicar
a las variables de referencia,
Una de ellas nos permite examinar o manipular el valor de
referencia. Por ejemplo, si cambiamos el
valor almacenado de po intl (que es 1000), podemos conseguir que
haga referencia a otro objeto.
También podemos comparar pe Int1 y point3 y determinar si
estamos haciendo referencia al
mismo objeto. La otra categoria de operaciones se aplica al objeto
que está siendo referenciado;
quizá podamos examinar o cambiar el estado interno de uno de los
objetos Point. Por ejemplo,
podriamos examinar algunas de las coordenadas xe yde los
objetos Point.
Antes de describir lo que podemos hacer con las referencias,
veamos qué es lo que no está
permitido. Considere la expresión pointl*point2. Puesto que los
valores almacenados de pointl
y point2 son 1000 y 1024, respectivamente, su producto sería
1024000. Sin embargo, este es un
cálculo que no tiene ningún sentido y que no tendría ninguna
aplicación. Las variables de referencia
almacenan direcciones, y no existe ningún significado lógico que
pueda asociarse con el hecho de
multiplicar dos direcciones.
De forma similar, pointi++ no tiene significado en Java; sugiere que
po intl -1000- deberia
Incrementarse a 1001, pero en ese caso no estaría haciendo
referencia a un objeto Point válido.
Muchos lenguajes (por ejemplo, C++) definen el puntero, que se
comporta como una variable
de referencia. Sin embargo, los punteros en C+r son mucho más
peligrosos, porque se permiten
operaciones aritméticas con las direcciones almacenadas. Por
tanto, en Č++, pointl++ sí que tiene
significado. Puesto que C++ permite punteros a tipos primitivos, es
preciso tener cuidado a la hora
de distinguir entre las operaciones aritméticas con las direcciones
y las operaciones aritméticas con
los objetos a los que se hace referencia. Esto se hace des-
referenciando explicitamente el puntero,
En la práctica, los punteros no seguros de Ct+ tienden a causar
numerosos errores de programación.
Algunas operaciones se realizan sobre las propias referencias,
mientras que otras operaciones
se llevan a cabo sobre los objetos que están siendo referenciados.
En Java, los únicos operadores
permitidos para los tipos de referencia (con solo una excepción en
el caso de String) son la
asignación mediante -y la comparación de igualdad mediante --
o !-,
1a Figura 2.2 ilustra el operador de asignación para variables de
referencia. Al asignar a point3
el valor almacenado de point2, obligamos a po int3 a hacer
referencia al mismo objeto que estaba
siendo referenciado por point2. Ahora, poInt2--point3 será true,
porque tanto point2 como
po int3 almacenan 1Ò24 y hacen referencia por tanto al mismo
objeto. po intl!-point2 es tambiên
true porque pointl y point2 hacen referencia a objetos distintos.
29
2.2 Conceptos básicos sobre objetos y referencias
1000
( 0, 0 )
( 0, 0 )
pointl
(5, 12)
1024
(en 1000)
3200
point2 = 1024
(5, 12 )
point2
3600
pointl = 1000
(en 1024)
5124
point3 = 1024
point3
Figura 2.2EI resultado de pe Int3"po int2: point3 hace ahora
referencia al misma objelo que point2.
La otra categoría de operaciones trata con el objeto que está
siendo referenciado. Solo hay tres
acciones básicas que se pueden llevar a cabo:
La siguiente sección ilustra con más detalle las operaciones
comunes con las referencias.
2.2
Conceptos básicos sobre
objetos y referencias
En Java, un objeto es una instancia de cualquiera de los tipos no
primitívos,
En Java, uin obieloes una
instancia de cualquiera de
Los objetos se tratan de forma distinta a los tipos primitivos. Los
tipos
los tipos noi primithios
primitivos, como ya hemos visto, se manejan mediante su valor, lo
que quiere
decir que los valores asumidos por las variables primitivas se
almacenan
en esas variables y se copian de una variable primitiva a otra
durante las
asignaciones. Como se muestra en la Sección 2.i, las variables de
referencia almacenan referencias
a objetos. El propio objeto está almacenado en algún otro lugar de
la memoria, y la variable de
referencia almacena la dirección en memoria del objeto. Por tanto,
una variable de referencia
representa simplemente un nombre para designar a esa parte de la
memoria. Esto quiere decir que
las variables primitivas y las variables de referencia se comportan
de forma distinta. Esta sección
examina dichas diferencias con más detalle e ilustra las
operaciones permitidas para las variables de
referencia.
2.2.1
EI operador punto (.)
El operador punto (.) se utiliza para seleccionar un método con el
fin de aplicarlo a un objeto. Por
ejemplo, suponga que tenemos un objeto de tipo Circle que define
un método area para calcular
30
Capitulo 2 Tipos de referencia
el área de un círculo. Si theCircle hace referencia a Circle,
entonces podemos calcular el área del
Circle referenciado y guardarla en una variable de tipo double) de
la forma siguiente:
double theArea = theCircle. area ( ):
Es perfectamente posible que theCircle almacene la referencia
nuT1. En este caso, aplicar el
operador punto generaria una excepción NullPointerExcept ion
durante la ejecución del programa.
Generalmente, esto provocará una terminación anormal del
programa,
El operador punto también puede utilizarse para acceder a los
componentes individuales de un
objeto, siempre y cuando se hayan tomado medidas para permitir
que los componentes internos sean
visibles. En el Capítulo 3 se explica cómo conseguir esto. También
se explica en el Capítulo 3 por
qué es preferible, generalmente, no permitir el acceso directo a los
componentes individuales.
2.2.2
Declaración de objetos
Ya hemos la sintaxis para declarar variables primitivas. En el caso
de los objetos, hay una diferencia
importante. Cuando declaramos una variable de referencia,
estamos proporcionando simplemente
un nombre que puede utilizarse para hacer referencia a un objeto
almacenado en la memoria. Sin
embargo, la declaración no proporciona por si misma ningún
objeto. Por ejemplo, supongamos que
hay un objeto de tipo Button que queremos añadir a un Pane1 P
existente utilizando el método add
(todo esto se proporciona en la librería Java). Considere las
instrucciones:
Button b:
Todo parece ser correcto con estas instrucciones hasta que
recordamos que b es el nombre de
algún objeto Button, pero que no se ha creado todavía ningún
Button. Como resultado, después
b
de la declaración de b, el valor almacenado por la variable de
referencia
es nuT1, lo que quiere decir que b no está todavía haciendo
referencia a un
Cuando se declara un tipo
da relerencia, no s8 crea
objeto Button válido. En consecuencia, la segunda línea es ilegal,
porque
ningun cbjcio. En cse
estamos intentando modificar un objeto que aun no existe. En este
escenario,
momeno, la refcrencla es a
el compilador detectaría probablemente ei error y nos indicaría
que "b no está
nu11. Para crear el obijeto,
utilce new.
inicializado" . En otros casos, el compilador no se dará cuenta, y
algún error
de tiempo de ejecución provocará la aparición del críptico
mensaje de error
NuTlPoin terException.
La palata clave newse
la forma (la única forma común) de crear un objeto es utilizar la
palabra
utiliza para construtun
clave new, new se emplea para construir un objeto. Una forma de
hacer esto
obeto
sería la siguiente:
Button b:
b = new Button( );
b. setlabel( "No" ):
// Etiquetar el botőn b con "No"
p. add( b ):
// y afadirlo a1 Pane1 p
Observe que los paréntesis son necesarios después del nombre
del objeto.
31
2.2 Conceptos básicos sobre objetos y referencias
También se pueden combinar la declaración y la construcción del
objeto,
Los parêntesis son
como en
obigalorios cuando s8
UlEza new.
Button b = new Buttont ):
b. setlabel( "No" ) :
P. add( b );
Muchos objetos pueden también construirse con valores iniciales.
La construcción puode
cspecifkcar un estado iricial
Por ejemplo, sucede que Button puede construirse con una String
que
del objeto,
especifique la etiqueta del botón:
Button b = new Button( " No " ):
D. add( b ): H Afadirlo a1 Pane1 p
2.2.3
Recolección de basura
Puesto que todos los objetos deben ser construidos, cabría
esperar que sea
lava utiliza un mecanismo
necesario destruirlos explicitamente una vez que ya no son
necesarios. En
de recolacclon de basura.
Con la recoleccion de
Java, cuando un objeto construido ya no está siendo referenciado
por ninguna
basura, la memoria rso
variable de objeto, la memoria que consume es reclamada
automáticamente,
ricrenciada s0 reclama
aulomálicamente.
con lo que pasa a estar disponible para ser utilizada de nuevo.
Esta técnica se
conoce con el nombre de recolección de basura.
El sistema de tiempo de ejecución (es decir, la Máquina Virtual
Java)
garantiza que un objeto no sea nunca reclamado mientras sea
posible acceder a él mediante una
referencia o una cadena de referencias. Una vez que el objeto deje
de ser alcanzable mediante una
cadena de referencias, puede ser reclamado a discreción del
sistema de tiempo de ejecución, en caso
de que haya poca memoria. Si la memoria no escasea, es
perfectamente posible que la máquina
virtual no intente reclamar esos objetos.
EI significado de =
2.2.4
Suponga que tenemos dos variables primitivas Ths y rhs, donde Ihs
quiere
1ns yrhs quicren deck
decir lado izquierdo y rhs quiere decir lado derecho. Entonces la
instrucción
lado izquierdp y lado
derecho, respetlivamente.
de asignación
Ihs = rhs;
tiene un significado muy sencillo. El valor almacenado en rhs se
almacena en la variable primitiva
Ths. Los subsiguientes cambios que se realicen en Ihs o rhs no
afectarán a la otra variable.
Para los objetos, el significado de =es el mismo: se copian los
valores almacenados. Si Ihs
y
rhs son referencias (de tipos compatibles), entonces después de la
instrucción de asignación, Ihs
hará referencia al mismo objeto que rhs, Aquí, lo que se está
copiando es
Para los otjetos, - -es ung
una dirección. El objeto al que antes hacia referencia Ths ya no
estará siendo
asignacion de referendia,
referenciado por 1hs. Si Ihs era la única referencia a dicho objeto,
entonces
en kugar de una copla de
ese objeto no estará siendo ya referenciado por nadie y podrá ser
sometido al
objclos.
mecanismo de recolección de basura. Observe que los objetos no
se copian.
32
Capitulo 2 Tipos de referencia
He aquí algunos ejemplos. En primer lugar, suponga que queremos
dos objetos Button. Suponga
también que intentamos obtenerlas creando primero el objeto
noButton. Después intentamos crear
yesButton modificando noButton de la forma siguiente:
Button noButton = new Button( "No " ):
Button yesButton = noButton:
yesButton. setlabet( "Yes- ):
p. add( noButton :
p. add( yesButton ):
Esto no funciona porque solo se ha construido un objeto Button.
Por tanto, la segunda instrucción
simplemente indica que yesButton es ahora otro nombre para el
objeto Button que hemos
construido en la linea 1. Ese Button construido es conocido ahora
mediante dos nombres. En la
linea 3, se cambia la etiqueta del objeto Button construido a Yes,
pero esto quiere decir que ese
único objeto Button que es conocido por dos nombres distintos,
estará ahora etiquetado con Yes.
Las dos últimas lineas añaden ese objeto Button al Pane1 P dos
veces.
El hecho de que yesButton nunca hiciera referencia a su propio
objeto no tiene ninguna
importancia en este ejemplo. El problema es la asignación.
Considere el siguiente fragmento de
programa.
Button noBut ton a new Button( " No " );
Button yesButton - new Button( ):
yesButton - noButton:
yesbutton. setlabel( "Yes" ):
p. add( noButton ):
P. addl yesButton ):
Las consecuencias son las mismas. Aquí, se han construido dos
objetos Button. Al final de la
secuencia, el primer objeto está siendo referenciado tanto por
noButton como por yesButton,
mientras que ai segundo objeto no le referencia nadie.
A primera vista, el hecho de que los objetas no puedan copiarse
parece una grave limitación.
Pero en la práctica no lo es, aunque sí es cierto que hace falta
acostumbrarse a esta característica del
lenguaje. Älgunos objetos necesitan ser copiados. Para ellos, debe
utilizarse el método c1 one, si es
que hay uno disponible. Sin embargo, no vamos a utilizar clone en
este texto.
Paso de parámetros
2.2.5
Gracias al paso por valor, los argumentos reales se envian dentro
de los parámetros formales,
utilizando la asignación normal. Si el parámetro es un tipo de
referencia, entonces sabemos que
la asignación normal quiere decir que el parámetro formal hace
ahora
El paso por valor indica que
referencia al mismo objeto que el argumento real. Cualquier
método aplicado
para los tipos de refcrencia,
al parámetro formal se estará también aplicando al argumento
real. En otros
el parámetro formal hace
lenguajes, esto se conoce con el nombre de paso de parámetros
por referencia,
referencia al mismo objelo
que el arguneniorcal
Sin embargo, utilizar esta terminologia para Java sería algo
confuso, porque
parece implicar que el paso de parámetros es distinto, cuando en
realidad el
33
2.2 Conceptos básicos sobre objetos y referencias
(a)
yesButton
Yes
-
b
(b)
yesButton
=
No
=
b
(c)
yesButton
No
b -- null
(d)
yesButton
No
Figura 2.3 El resultado del pasopor valor. (a) b es una copla de
yesButton; (b} despues de basetlabel ("Ho" ):los canbios en el
estado del
objelo referenciadopa b se verân reflejados en el objeto
referenciado por yesButton, porque se tralta del mlsmo objeto; (c)
después de b=nu11:el
cambio en el valcr de b no afecla al valor de yesButton; (d)
después de que el métoda vuelva, b cae lfuera de ámbito.
paso de parámetros no ha cambiado: más bien son los parámetros
los que han cambiado, de tipos de
no referencia a tipos de referencia.
Como ejemplo, suponga que pasamos yesButton como parámetro a
la rutina clearButton que
se define de la forma siguiente:
public static void clearButton( Button b )
b, setlabel( "No" ):
b = nulT;
Entonces, como muestra la Figura 2.3, b hace referencia al mismo
objeto que yesButton, y. los
cambios realizados en el estado de este objeto por los métodos
invocados a través de b serán visibles
cuando se vuelva de ejecutar c1 earButton. Los cambios en el
valor de b (es decir, los cambios que
provoquen que b haga referencia a otros objetos) no tendrán
ningún efecto sobre yesButton.
El significado de ==
2.2.6
Para los tipos primitivos, = es true si los valores almacenados son
idénticos.
Para los tpos de relerenda,
Para los tipos de referencia su significado es diferente, pero
completamente
-es true solo silas
dos reicrencias hacen
coherente con las explicaciones anteriores.
referenda al misno objero.
Dos tipos de referencia son iguales según F si hacen referencia al
mismo objeto almacenado (o si ambos son nu11). Considere, por
ejemplo, lo
siguiente:
Button a - new Button( "Yes" ):
Button b = new Button( "Yes" ) :
Button c = b:
34
Capitulo 2 Tipos de referencia
Aquí, tenemos dos objetos, E.l primero se conoce con el nombre de
a y el segundo se conoce con dos
nombres: b y c. b--c es true. Sin embargo, aun cuando a y b están
haciendo referencia a objetos
que parecen tener el mismo valor, a-b es false, ya que hacen
referencia a objetos diferentes. Se
aplican reglas similares a !-,
En ocasiones, es importante conocer si son idénticos los estados
de los
objetos a los que se está haciendo referencia. Todos los objetos
pueden
EI mólodo equals
compararse utilizando equals, pero para muchos objetos
(incluyendo
puede utlizarse pars ver
s dos refcrencias hacen
Button) equals devuelve false a menos que las dos referencias
estén
referencia a otjetos que
haciendo referencia al mismo objeto (en otras palabras, para
algunos objetos,
tengan estdos idénticos,
equals es exactamente lo mismo que la comprobación -). Veremos
un
ejemplo de dónde resulta equa1 s útil cuando hablemos del tipo
String en la
Sección 2.3.
2.2.7
No hay sobrecarga de operadores para los objetos
Salvo por la única excepción descrita en la siguiente sección, no
pueden definirse nuevas operadores
como +, - , *y f para manejar objetos. Por tanto, no hay disponible
ningún operador < para ningún
objeto. En su lugar, deberá definirse para ese tipo de comparación
un método nominado, como por
ejemplo lessThan,
2.3
Cadenas
Las cadenas en Java se manejan con el tipo de referencia String.
El lenguaje
EI tlpo Stringse
hace parecer que el tipo String es un tipo primitivo, porque
proporciona los
composta CONIT un tlpo de
referencia,
aperadores +y +- para la concatenación. Sin embargo, este es el
único tipo de
referencia para el que se permite una sobrecarga de operadores.
Por lo demás,
String se comporta como cualquier otro tipo de referencia.
2.3.1
Conceptos básicos sobre manipulación de cadenas
Hay dos reglas fundamentales acerca de los objetos String. En
primer lugar,
Las cadenas do caradercs
con la excepción de las operadores de concatenación, se
comportan como un
son inmutables; es deck,
un obijelo String no sera
cbjeto. En segundo lugar, un objeto String es inmutable. Esto
quiere decir
modificado.
que, una vez que se ha construido un objeto String, su contenido
no puede
modificarse.
Puesto que un objeto String es inmutable, se puede usar sin
problemas el operador "'. Por tanto,
podemos declarar un objeto String de la forma siguiente:
String empty - "-
String message = "He1lo" :
String repeat = message :
Después de estas declaraciones habrá dos abjetos String. El
primero será la cadena vacía, que
está referenciada por empty. El segundo será el objeto String
"He1lo", que estará referenciado
35
2.3 Cadenas
tanto por message como por repeat. Para la mayoria de los
objetos, estar referenciado tanto por
message como por repeat podría dar problemas. Sin embargo,
como las cadenas de caracteres son
inmutables, la compartición de objetos String es segura, además
de eficiente. La única forma de
cambiar el valor de la cadena a la que repeat hace referencia es
construir un nuevo objeto String
y obligar a repeat a hacer referencia a él. Esto no tiene ningún
efecto sobre el objeto String
referenciado por message.
2.3.2
Concatenación de cadenas
Java no permite la sobrecarga de operadores para los tipos de
referencia, Sin embargo, en el caso de
la concatenación de cadenas existe una excepción en ei lenguaje.
El operador *+, cuando al menos uno de sus operandos es un
objeto String, realiza la
concatenación. El resultado es una referencia a un objeto String
de nueva construcción. Por
ejemplo,
"this" + n that2 H/ Genera "this that"
La concalenacion de
cadenas se realza con *
// Genera "abc5"
"abc " + 5
(y +-).
/l Genera "5abc"
5 + "abc"
"a " + "b + "c"
Il Genera "abc"
Las cadenas de caracteres de un solo carácter no deberian
sustituirse por constantes de carácter;
en el Ejercicio 2.8 le pedimos que demuestre por qué. Observe que
el operador + es asociativo a la
izquierda y por tanto
"a " + 1 + 2
/ l Genera "a12"
1 + 2 1 "a"
/ / Genera "3a "
1 + ( 2 * "a"
)
/l Genera "12a "
Asimismo, también se proporciona el operador += para String. El
efecto de strizexp es igual
que el de str=strtexp. Específicamente, esto quiere decir que str
hará referencia al objeto String
de nueva construcción generado por strtexp.
2.3.3
Comparación de cadenas
Puesto que el operador básico de asignación funciona para objetos
String,
Ullice equalsy
resulta tentador suponer que también funcionan las operadores
relacionales y
compareto para
comparar cadenas.
de igualdad. Sin embargo, esto no es así.
De acuerdo con la prohibición de sobrecarga de operadores, los
operadores
relacionales (<, >. <-y >-) no están definidos para el tipo String.
Además,
=y !- tienen el significado típico para las variables de referencia.
Para dos objetos String Ths y
rhs, por ejemplo, Insrhs será true solo si Ths y rhs hacen
referencia al mismo objeto String.
Por tanto, si hacen referencia a objetos distintos que tienen un
contenido idéntico, Ihs--rhs será
false. Para el caso de !-se aplica una lógica similar,
Para comparar la igualdad de dos objetos String, utilizamos el
método equals. lhs .equals
(rhs) será true si Ihs y rhs hacen referencia a objetos String que
almacenan valores idénticos,
36
Capitulo 2 Tipos de referencia
Se puede llevar a cabo una comprobación más general mediante el
método compareTo.
1hs. compareTo(rhs) compara dos objetos String, 1hs y rhs.
Devuelve un número negativo,
cero, o positivo, dependiendo de si Ths es lexicográficamente
menor, igual o mayor que rhs,
respectivamente.
2.3.4
Otros métodos String
La longitud de un objeto String (una cadena de caracteres vacía
tiene longitud cero) se puede
obtener mediante el método length. Puesto que length es un
método, es necesario utilizar
paréntesis.
Hay definídos dos métodos para acceder a los caracteres
individuales de
Ulllice length, charat
un objeto String. E.l método charAt obtiene un único carácter
especificando
ysubstring para
una posición (la primera posición es la posición 0), E.l método
substring
calcular la longiud de
una cedena, extraet
devuelve una referencia a un objeto String de nueva construcción.
La
un solo carácler y
llamada a este método se realiza especificando el punto de inicio y
la primera
exiraer una subcadona,
posición no incluida.
respeciivamente,
He aqui un ejemplo de estos tres métodos:
String greeting = "he1lo" :
int len = greeting. length( ): I1 len es 5
char ch = greeting. .charAt( 1 ): 7/ ch es Del
String sud = greeting. substring( 2 . 4 ): I/ sub es "11 "
2.3.5
Conversión de otros tipos a cadenas de caracteres
La concatenación de cadenas proporciona una forma simple de
convertir
tostring comierte
Ipos primtiyos (yobjelos) a
cualquier valor primitivo a un objeto String. Por ejemplo, "*+45 .3
devuelve
objetos String.
el objeto String de nueva construcción "45 .3". También existen
métodos
para hacer esto directamente.
El método toSstring se puede utilizar para convertir cualquier tipo
primitivo en un objeto String. Por ejemplo, Integer. tostring( 45)
devuelve una referencia
al objeto String de nueva construcción "45" . Todos los tipos de
referencia proporcionan
también una implementación de toString de calidad variable. De
hecho, cuando el operador
+ solo tiene un argumento de tipo String, el argumento que no es
una cadena de caracteres se
convierte en un objeto String aplicándole automáticamente un
método toString apropiado.
Para los tipos enteros, una forma alternativa de Integer. tostring
permite la especificación de
una base de numeración, Así,
System .out printin( "55 in base 2: m + Integer .tostring( 55, 2 ) );
imprime la representación binaria de 55.
El valor int representado por el objeto String puede obtenerse
invocando el método Integer.
parseInt. Este método genera una excepción si el objeto String no
representa un valor int. Las
excepciones se explican en la Sección 2.5. Se pueden aplicar
conceptos similares a los valores
double. He aquí algunos ejemplos:
37
2.4 Matrices
int x = Integer . parseInt( " 75 " ):
double y = Double. parseDoub lel "3.14" ):
2.4
Matrices
Un agregadoes una colección de entidades almacenadas en una
unidad. Una
Uina matrizalmacena una
colecdon de entidades de
matriz es el mecanismo básico para almacenar una colección de
entidades
tipo identico.
de tipo idéntico, En Java, la matriz no es un tipo primitivo. En lugar
de ello,
se comporta de forma bastante similar a un objeto. Por tanto,
muchas de las
reglas aplicables a los objetos también se aplican a las matrices.
Se puede acceder a cada entidad de la matriz mediante el
operador de
El operador de indexadon
de maitriz [ J proporciona
indexación de matriz C1. Decimos que el operador C1 indexa la
matriz, lo
acceso a cualquier objeto
que quiere decir que especifica a que objeto hay que acceder. A
diferencia
de la matriz,
de lo que sucede en c y C++, la comprobación 'de los límites se
realiza
autonáticamente,
En Java, las matrices siempre se indexan comenzando por cero.
Por tanto,
Las malrices se Indexan
una matriz a de tres elementos almacenará al0], a[1] y a[2]. El
número de
comenzando por CefO.
EI número de elementos
elementos que se puede almacenar en una matriz. a puede
obtenerse siempre
almacenados en la matriz
mediante a . length. Observe que no se usan paréntesis. Un bucle
típico para
s0 olitiene modkanie el
una matriz utilizaría
campo 1ength, No se
utilzan parêntesis
for( int i = 0: i < a .Tength: itt
Declaración, asignación y métodos
2,4.1
Una matriz es un objeto, por lo que cuando se formula la
declaración de la matriz
int L 1 errayl:
todavía no habrá ninguna memoria asignada para almacenar la
matriz. array1
Para asignar memorta a
una matriz utllce new.
es simplemente un nombre (referencia) para una matriz y en este
punto su
valor es nu11. Por ejemplo, para disponer de 100 valores int,
utilizaríamos
new:
array1 = new int C 100 1:
Ahora, array1 hará referencia a una matriz de 100 valores int.
Existen otras dos formas de declarar matrices. Por ejemplo, en
algunos contextos
int L 1 array2 = new int L 100 1:
resultaría aceptable. Asimismo, pueden utilizarse listas de
inicializadores, como en C o C+t, para
especificar los valores iniciales. En el siguiente ejemplo, se
construye una matriz de cuatro enteros y
luego se hace referencia a ella mediante array3.
int L J array3 = 3. 4 . 10. 6 I:
38
Capitulo 2 Tipos de referencia
Los corchetes pueden ir antes o después del nombre de la matriz
Colocarlos antes hace que sea
más fácil ver que ese nombre es un tipo de matriz, de modo que
ese es el estilo que utilizaremos
aquí, Para declarar una matriz de tipos de referencia (en vez de
tipos primitivos) se utiliza la
misma sintaxis. Observe, sin embargo, que cuando creamos una
matriz de tipos de referencia, cada
referencia inicialmente almacena un valor nu11. Asimismo, cada
una de esas referencias deberá ser
configurada para hacer referencia a un objeto construido. Por
ejemplo, una matriz de cinco botones
se construiría de la forma siguiente:
Button L J arrayofButtons:
arrayOfBut tons = new Button E 5 1:
for( int 1 0: 1 < arrayof Buttons. length: i+ )
arrayOfButtonsl 1 ] = new Button( ):
La Figura 2.4 ilustra el uso de las matrices en Java, El programa de
la Figura 2.4 elige
repetidamente números comprendidas entre 1 y 100, ambos
inclusive. La salida es el número de
veces que se ha seleccionado cada número. La directiva import de
la línea 1 se explicará en la
Sección 3.8.1.
La línea 14 declara un matriz de enteros que cuenta las veces que
Asegurese Siempre de
aparece cada número. Puesto que las matrices se indexan
comenzando con
dedarar el tamano correclo
cero, el +1 es crucial si queremos acceder al elemento situado en
la posición
de las matrices. Los
etores de una unídad en
DIFF _NUMBERS. Sin él, tendriamos una matriz cuyo rango
indexable iría de
el damensionaminto son
0 a 99, por lo que cualquier acceso al indice 100 estaría fuera de
límites,
bastane comunas.
El bucle de las lineas 15 y 16 inicializa con el valor cero las
entrada de la
matriz; esto es, en realidad, innecesario, ya que de manera
predeterminada
los elementos de las matrices se inicializan con cero para los
tipos primitivos y con null para las
referencias.
El resto del programa es relativamente simple. Utiliza el objeto
Random definido en la
librería java. util (de aquí la directiva import de la linea 1). El
rmétodo nextInt proporciona
repetidamente un número (hasta cierto punto) aleatorio en el
rango que va desde cero hasta uno
menos que el parámetro pasado a nextInt; por tanto, sumando 1,
obtendremos un número dentro
del rango deseado. Los resultados se imprimen en las lineas 25 y
26.
Puesto que una matriz es un tipo de referencia, =no copia
matrices. En lugar de ello, si Thsy rhs
son matrices, el efecto de
int L 1 Ins - new int c 100 1:
int L 1 rhs = new int E 100 1:
Ihs = rhs:
es que el objeto matriz que estaba siendo referenciado por rhs
estará ahora referenciado también
por Ihs. Por tanto, moditicar rhs[o] tambiên hace que se modifique
1hsr0]. (Para hacer que Ths
sea una copia independiente de rhs, podria utilizarse el método
clone, pero a menudo no hace falta
realmente realizar copias completas.)
Por último, puede utilizarse una matriz como un parámetro de un
método. Las reglas se deducen,
desde el punto de vista lógico, del hecho de que un nombre de
matriz es una referencia. Suponga que
39
2.4 Matrices
1
import java. util .Random:
2
3
public class RandomNumbers
4
l
5
/l Generar números aleatorios (entre 1-100)
6
Il Imprimir el número de apariciones de cada número
7
8
public static final int DIFF NUMBERS = 100:
9
public static final int TOTAL NUMBERS = 1000000:
10
11
public static void main( String L 1 args }
12
13
Il Crear 1a matriz. inicfalizarla con välores 0
14
int E numbers - new int L DIFF NUMBERS + 1 1:
15
forl int i = 0 : 1 < numbers. Tength: i+ )
16
numbersl i ] = 0:
17
Random r = new Randomt ):
18
19
20
Il Generar 1os números
)
21
forc int i * 0: 1 < TOTAL _NUMBERS: i++
22
numbersl [Link] DIFF _NUMBERS ) + 1 ]++:
23
24
/l Imprimir e1 resumen
)
25
fort int i = 1: i K= DIFF _NUMBERS : i++
26
System .out -printing 1 + ": " * numbersl i J ):
27
28
Figura 2,4 fustracián simple del usa de matrices.
tenemos un método me thodCa11 que acepta una matriz de Int
como parámetro. Las vistas llamante/
llamado serían
/l 11 amada a método
ne thodca1l( actualArray );
Il declaración de método
vaid methadcall( int r ] formalArray
)
De acuerdo con los convenios de paso de parámetros para los
tipos de
EI contenido de una matriz
referencia en Java, forma TArray hace referencia al mismo objeto
matriz que
se pasa por referonca.
actualArray. Por tanto, forma 1Array[i] accede a actua 1Array[i].
Esto
quiere decir que si el método modifica cualquier elemento de la
matriz, esas
modificaciones serán observables después de haberse completado
la ejecución del método. Observe
también que una instrucción como
40
Capitulo 2 Tipos de referencia
formalArray = new int L 20 1:
no tiene ningún efecto sobre actua TArray. Finalmente, puesto que
los nombres de matriz son
simplemente referencias pueden ser devueltos por un método.
2.4.2
Expansión dinámica de matrices
Suponga que queremos leer una secuencia de números y
almacenarlos en
La expansión dinámica
d matrices nos permite
una matriz para su procesamiento. La propiedad fundamental de
una matriz
Construl matrices de
requiere que declaremos un tamaño, para que el compilador pueda
asignar
amano arbltrario hacerlas
la cantidad correcta de memoria. Asimismo, debemos hacer esta
declaración
má$ grandes en caso
necesario.
antes de acceder por primera vez a la matriz. Sino tenemos ni idea
de cuántos
elementos podemos esperar, entonces es dificil elegir un tamaño
razonable
para la matriz.. Esta sección muestra cómo expandir matrices si el
tamaño
inicial es demasiado pequeño. Esta técnica se denomina
expansión dinámica de matrices y nos
permite construir matrices de tamaño arbitrario y hacerlas más
grandes o más pequeñas durante la
ejecución del programa,
El método de construcción para las matrices que hemos visto
hasta ahora es:
int L 1 arri - new intl 10 1:
Suponga que decidimos después de las declaraciones, que en
realidad necesitamos 12 valores Int en
lugar de 1Ò. En este caso, podermos usar la siguiente técnica, que
se ilustra en la Figura 2.5.
int t 1 original = arr:
arr = new int L 12 1;:
forc int 1 -- 0: i < 10: 1t+ )
arrl i 1 -- originall i 1:
// 4 . Desreferenc iar 1a matriz original
or iginal - null;:
Unos momentos de reflexión le permitirán convencerse de que se
trata
Expanda slempre la matriz
de una operación bastante cara en términos de procesamiento.
Esto se debe
hasta un larnano que
sen multiplo del anterlors
a que tenemos que copiar todos los elementos de original a arr, Si
esta
Duplicar el tamaro de la
expansión de la matriz fuera en respuesta, por ejemplo, a
operaciones de
matriz suele ser una buena
lectura de entrada, sería bastante ineficiente volver a realizar la
expansión
elección.
cada vez que leemos unos cuantos elementos. Por tanto, cuando
se imple-
menta la expansión de matrices, lo que hacemos siempre es
multiplicar su
tamaño por una cierta constante. Por ejemplo, podríamos
expandirla para que fuera el doble de
grande. De esta forma, cuando expandamos la matriz de
Nelementos a 2 Nelementos, el coste de
las Ncopias se distribuye entre los siguientes N elementos que
podrán insertarse en la matriz sin
la necesidad de una nueva expansión,
Para concretar aun más las cosas, las Figuras 2.6 y 2.7 muestran
un programa que lee un número
ilimitado de cadenas de la entrada estándar y almacena el
resultado en una matriz dinámicamente
expansible. Se utiliza una linea vacía para indicar el final de la
entrada, (Los minimos detalles
de ES utilizados aqui no son importantes para este ejemplo y se
explican en la Sección 2.6.) La
rutina resize realiza la expansión (o la contracción) de la matriz,
devolviendo una referencia a la
41
2.4 Matrices
arr| =
-
(a)
arrl -
[
original|
(b)
arr| =
-
original|
(c)
-
arr|
origina1
(d)
Figure 2.5 E xpansión de una maltriz intermamente: (a) enel
momenlo Inictal, arr representa 10 enteros; (b) despues del paso 1.
original
representa los mismos 10 enteros: (c) despues de los pasos 2y3,
arr representa 12 enteros, de los cuales los 10 primcros han sido
coplados de
original;y (d) despues del paso 4,los 10 enleros estân disporibles
paraser reclamados.
nueva matriz. De forma similar, el método getStrings devuelve (una
referencia a) la matriz donde
residirá.
Al principio de getStrings, se asigna a items Read el valor 0 y
comenzamos con una matriz
inicíal de cinco elermentos, Leemos repetidamente nuevos
elementos en la línea 23. Si la matriz
está llena, lo cual es indicado por la prueba de la línea 26,
entonces la matriz se expande invocando
a resize. Las líneas 42 a 48 realizan la expansión de la matriz
utilizando la estrategia exacta que
hemos esbozado anteriormente. En la línea 28, se asigna a la
matriz el elemento de entrada real
y
se incrementa el número de elementos leídos. Si se produce un
error en la entrada, simplemente
detenemos el procesamiento. Finalmente, en la línea 36
contraemos la matriz para ajustarla al
número de elementos leídos antes de volver del método.
2.4.3
Arraylist
La técnica utilizada en la Sección 2.4.2 es tan común que la
librería Java
EI tipo Arraylist
contiene un tipo Arraylist con una funcionalidad integrada para
imple-
se utiliza para expandr
matrices.
mentarla. La idea básica es que un Arraylist mantiene no solo un
tamano,
sino tamblén una capacidad; la capacidad es la cantidad de
memoria que ha
reservado. La capacidad de Arraylist es realmente un detalle
interno, no
EI mélodo add incementa
algo por lo que debamos preocuparnos.
cl taralo en unoy anade
Ei método add incrementa el tamaño en una unidad y añade a la
matriz
un NUCvO elemento a
la matrtz en la posicion
un nuevo elemento en la posición apropiada. Esta es una
operación trivial
apropiada, expandiendo
si la capacidad máxima no se ha alcanzado. Si se ha hecho, la
capacidad
la capacdad en casp
se expande automáticamente utilizando la estrategia descrita en
la Sección
necesarfo.
2.4.2. Los objetos Arraylist se inicializan con un tamaño igual a 0.
42
Capitulo 2 Tipos de referencia
1
import java. util .Scanner:
2
3
public class ReadStrings
4
l
5
/l Leer un número ilimitado de objetos String: devol ver un String L
J
6
I/ Los minimos detalles de E/S utilizados aquí no son importantes
7
/p para este ejemplo y se explican en la Sección 2 .6 ..
8
public static String L ] getStrings(
)
9
10
Scanner in = new Scanner( System. in ):
I1
String E ] array = new String[ 5 1:
12
Int itemsRead -- 0:
13
14
System .out ·printin( "Enter strings. one per line: m ):
15
System. .out ·println( "Terminate with empty line: " ):
16
17
while( in. ha SNextline( ) )
[
18
19
String oneline = in. nextline( ):
20
ift onel ine= equals( " " ) )
21
break:
22
if( itemsread = array. length )
23
array = resize( array. array. length * 2 ):
24
arrayl itemsRead++ 1 = oneline:
25
)
26
27
return resize( array. 1temsRead :
28 1
Figura 2.6 Codigo para leer un numero Ilimitado de objetos Stringe
eimprimirlos (parte 1) .
Puesto que la indexación mediante está reservada solo para
matrices primitivas, como era
el caso en buena medida para objetos String, necesitamos utilizar
un método para acceder a los
elementos de un objeto Arraylist. E.l método get devuelve el objeto
en el indice concreto y el
método set puede utilizarse para modificar el valor de una
referencia en un indice determinado;
get se comporta por tanto como el método charAt. Describiremos
los detalles de implementación
de Arraylist en diversos puntos a lo largo del texto, y llegaremos
incluso a escribir nuestra propia
versión.
El código de la Figura 2.8 muestra cómo se utiliza add en
getStrings; claramente, es mucho
más simple que la función getStrings de la Sección 2.4.2. Como se
muestra en la linea 19, el
Arraylist especifica el tipo de objetas que almacena. Solo puede
añadirse el tipo especificado al
Arraylist; otros tipos provocarán un error de tiempo de
compilación. Es importante mencionar, sin
embargo, que a un Arraylist solo pueden añadirse objetos (a los
que se accede mediante variables
43
2.4 Matrices
29
I Red Imensi onar una matriz Stringl 1: devol ver una nueva matriz
30
public static String C 1 resize( String L 1 array.
31
int newSize )
32
33
String c ] original - array;
34
int numToCopy -- Math -min( original length . newSize ):
35
36
array = new StringL newSize 1:
37
for( int i = 0: 1 < numToCopy: i+t
)
B8
arrayl i J = original[ i 1:
39
return array:
40
41
42
)
public static vaid main( String C 3 args
43
44
String C J array = getStrings( ) :
45
)
forl int i = 0: i < array -Tength: i4+
46
System .out. println( arrayl i ] ):
47.
48 l
Figura 2.7 Codigo para leer un numero ilimitado de objetos Stringe
Imprimirlos (parte 2)
de referencia). Los ocho tipos primitivos no pueden añadirse. Sin
embargo, existe una solución fácil
para este problema, de la que hablaremos en la Sección 4.6.2.
La especificación del tipo es una caracteristica añadida en Java 5
que se conoce con el nombre
de genericos. Antes de Java 5, Arraylist no especificaba el tipo de
los objetos y podia añadirse
cualquier tipo al Arraylist. De cara a mantener la compatibilidad
descendente, sigue permitiéndose
no especificar el tipo de los objetos en la declaración de Arraylist,
pero si se usa Arraylist de
esta manera se obtendrá una advertencia de compilación, porque
estamos impidiendo al compilador
detectar las desadaptaciones de tipo y forzando a que esos
errores sean detectados mucho más tarde
por la Máquina Virtual, en el momento de ejecutarse realmente el
programa. En las Secciones 4.6
y
4.8 se describen tanto el estilo antiguo como el nuevo.
2.4.4
Matrices multidimensionales
En ocasiones, necesitamos acceder a las matrices utilizando más
de un
Uina matriz mutidimensianal
esuna matrtz a la que se
índice. Un ejemplo común serían las matrices bidimensionales.
Una matriz
accede emploando más de
multidimensionales una matriz a la que se accede mediante más
de un indice.
un indice.
Se construye especificando el tamaño de sus indices y luego se
accede a cada
elemento colocando cada uno de los îndices en su propio par de
corchetes,
Por ejemplo, la declaración
int L IC 1 x - new iIntt 2 IE 3 1:
44
Capitulo 2 Tipos de referencia
1
import java. util .Scanner:
2
import java. util .Arraylist:
3
4
public class ReadstringsWithArraylist
5
6
public static void main( String C J args
)
7
8
Arrayl ist<strt Ing> array = getStrings( ):
9
fort int i - 0 : 1 < [Link]( ): ilt
)
10
System. .out -printin( array .get( 1 ) ):
11
12
13
I Leer un número ilimitado de String: deval ver un Arraylist
14
Il Los minimos detalles de E/S utilizados aquf no son importantes
15
I/ para este ejemplo y se explican en 1a Sección 2 .6 .
16
public static Arraylist<string> getStrings(
)
17
18
Scanner in = new Scanner( System. in ):
19
Arrayl ist<string)> array = new Arraylist<string>( ):
20
21
System. .out -println( "Enter any number of strings. one per line: " ):
22
System .out -printinc "Terminate with empty line: w ):
23
24
while( in. ha SNextlinet ) )
[
25
26
String oneline = in. nextline( ):
27
)
if( oneline. equa 1s( "- )
28
break:
29
30
array. add( oneline ):
)
31
32
33
System .out ·printin( "Done reading" ):
34
return array:
35
36 )
Figura 2.8 Codigo para leer un numero limitado da objetos String
ulll!zando un Arraylist.
define la matriz bidimensional x, en la que el primer índice (que
representa el número de filas) va de
O a 1y el segundo indice (el número de columnas) va de 0 de 2 (lo
que nos da un total de seis valores
enteros). Se reservan seis posiciones de memoria para esas
valores enteros.
45
2.4 Matrices
En el ejemplo anterior, la matriz bidimensional es en realidad una
matriz de matrices. Como
tal, el número de filas es X. length, lo que da un valor igual a 2. El
número de columnas es x[O].
length o x[1]. length, siendo ambos valores iguales a 3.
La Figura 2.9 ilustra cómo imprimir el contenido de una matriz
bidimensional. El código funciona
no solo para matrices bidimensionales rectangulares, sino también
para matrices bidimensionales
irregulares, en las que el número de columnas varía de una fila a
otra. Esto se maneja fácilmente
utilizando m[i]. Tength en la linea 11 para representar el número de
columnas de la fila 1. También
se tiene en cuenta la posibilidad de que una nla pueda ser nu11 (lo
que es distinto de tener longitud
O), para lo cual se emplea la prueba de la linea 7. La rutina Imna in
ilustra la declaración de matrices
bidimensionales para el caso en el que los valores iniciales son
conocidos. Se trata simplemente
de una extensión del caso unidimensional expuesto en la Sección
2.4.1. La matriz a es una matriz
rectangular simple, la matriz b tiene una fila nu11 y la matriz C es
irregular.
1
public class MatrixDemo
2
I
3
)
public static void printMatrix( int L IL m
4
5
)
for( int 1 = 0 : 1 < m . 1 ength: i+t
6
[
7
if( mL i 1 a null )
B
System .out .println( "(null)" ):
9
else
l
10
11
)
forc int j = 0; j k mLi]. Tength: j++
12
):
System. .out ·print( mL i JL j 1 +
13
System .out -printin( ):
14.
15
)
16
)
17
18
public static void main( String C J args )
19
int E IL J a =l l 1. 2 ]. 3. 4 ). l 5. 6 ) 1:
20
int c L 1 b - l ( 1. 2 1. nu11. l 5. 61 I:
21
int C L ] c = I I 1. 2 h. 3 . 4. 5 I. 6) 1:
22
23
System. .out .println( "a: " ): printMatrixc a ):
24
25
System -out -println( "b: " ); printMatrix( b );
System. .out .println( "c: " ): pr intMatrix( c ) :
26
27
28 }
Figura 2.9 mpreslón de una matriz bidi mensional.
46
Capitulo 2 Tipos de referencia
Argumentos de la línea de comandos
2.4.5
Hay disponibles argumentos de la línea de comandos para
examinar el
Los argumeos de ta
iiea de comandosestán
parámetro empleado con ma 1n. La matriz de cadenas de
caracteres representa
doponitles examinado eci
los argumentos adicionales de línea de comandos. Por ejemplo,
cuando se
parâmetro de main.
invoca el programa con
java Echo this that
args[0] hace referencia al objeto String "this" y args[1] hace
referencia al objeto String
"that", Por tanto, el programa de la Figura 2.10 simula el comando
echo estándar,
Bucle for avanzado
2.4.6
Java 5 añade una nueva sintaxis que permite acceder a cada
elemento de una matriz o Arraylist sin
necesidad de utilizar Indices de matriz Su sintaxis es
for( tipo var : colección )
instrucción
Dentro de instrucción, var representa el elemento actual de la
iteración, Por ejemplo, para
imprimir los elementos de arr, que tiene tipo String[], podemos
escribir:
for( String val : arr
)
System, .out -println( val ):
El mismo código funcionaria sin cambios si arr tuviera tipo Arrayl
ist<String>, lo cual es una
ventaja, porque sin el bucle for avanzado, sería necesario
reescribir el código del bucle cuando
cambiáramos el tipo y pasáramos de utilizar una matriz a emplear
un Arraylist.
public class Echo
1
2
// Enumerar 1os argumentos de 1a linea de coma ndos
3
4
public static void main( String L J args )
5
6
for Int i - 0: 1 < args. length
1 : i++ )
7
System. .out -print( argsl i 1 + . ):
B
if( args length !- 0 )
9
System -out -println( argsl args. length - 1 J ):
10
else
11
System .out -println( " No arguments to echo" );
12
13 I
Figura 2.10 El comando echo.
47
2.5 Tratamiento de excepciones
El bucle for avanzado tiene algunas limitaciones. En primer lugar,
en muchas aplicaciones
necesitamos disponer del indice, especialmente si estamos
haciendo cambios a los valores de la
matriz (o Arraylist). En segundo lugar, el bucle for avanzado solo es
útil si estamos accediendo
a todos las elementos en orden secuencial. Si hay que excluir un
elemento, es necesario emplear el
bucle for estándar. Como ejemplos de bucles que no pueden
reescribirse facilmente utilizando el
bucle avanzado tendriamos
ford int i = 0: i < arrl. Tength: 1++ )
arri[ 1 1 m 0:
for( int i = 0 : 1 < args. length - 1: i++
)
System .out -printin( argst i 1 + " ):
Además de permitir la interacción a través de matrices y de
objetos Arraylist, el bucle for
avanzado puede utilizarse con otros tipos de colecciones,
Hablaremos de esa aplicación de esta
nueva sintaxis en el Capítulo 6.
2.5
Tratamiento de excepciones
Las excepciones son objetos que almacenan información y que se
Las excepootes se ulfizan
transmiten fuera de la secuencia normal de retorno. Se propagan
hacia
para manejar stuacknes
excepcionales como por
atrás a través de la secuencia de invocaciones hasta que alguna
rutina
cjemplo los crrores,
captura la excepción, En ese momento, puede extraerse la
información
almacenada en el objeto con el fin de realizar el tratamiento de
errores.
Dicha información incluirá siempre detalles acerca de dónde se
creó la excepción. La otra
parte de información importante es el tipo del objeto excepción.
Por ejemplo, cuando se
propaga una excepción ArrayIndex0utBoundsExcept ion está claro
que el problema básico es
un indice incorrecto. Las excepciones se utilizan para señalizar
sucesos excepcionales, como
por ejemplo los errores.
2.5. 1
Procesamiento de excepciones
El código de la Figura 2.11 ilustra el uso de las excepciones. El
código que pudiera provocar la
propagación de una excepción se encierra en un bloque try. El
bloque try se extiende desde la
linea 13 a la 17. Inmediatamente después del bloque try se
encuentran las rutinas de tratamiento
de excepciones. A esta parte del código solo se salta si se genera
una
Un bloque try endierra un
excepción; en el punto donde se genera la excepción, el bloque try
del que
codig0 que podria generar
proviene la misma se considera terminado. Cada bloque catch
(este código
una excepcion,
solo tiene uno) se va comprobando por orden, hasta que se
encuentra
una rutina de tratamiento adecuada. parseInt genera una
excepción
NumberForma tExcepti ion si onel ine no es convertible a un valor
int.
Ln bloque catch procesa
El código del bloque catch -en este caso la linea 18- se ejecuta en
caso
una excopoion.
de que se produzca una excepción apropiada. Después, el bloque
catch y
48
Capitulo 2 Tipos de referencia
1
import java. util .Scanner:
2
3
public class DivideByTwo
4
l
5
public static void main( String C J args
)
6
7
Scanner in = new Scanner( System. in ):
8
int x;
9
10
System .out -printin( "Enter an integer: n ):
I1
try
12
[
13
String oneline - in. nextl inec ):
14
x - Integer. parseint( oneline ):
15
System, .out ·println( "Half of x is n + (x / 2) ):
)
16
)
17
catch( NumberForma tException e
18
l
System -out .printin( e ): )
19
20 1
Figura 2.11 Programa simple para llustrar las excepciones,
la secuencia try/ catch se consideran terminados.' Se imprime un
mensaje significativo a partir
del objeto de excepción e. Alternativamente, podríamos incluir
instrucciones adicionales de
procesamiento y mensajes de error más detallados.
La cláusula finally
2.5.2
Con algunos objetos creados dentro de un bloque try es necesario
realizar
La cksusula finally
sicmpre se ejecuta antes
ciertas tareas de limpieza. Por ejemplo, puede que sea necesario
cerrar los
de completar un bioque,
archivos abiertos dentro del bloque try antes de salir de dicho
bloque. Un
independentemente de ts
problema que tiene esto es que, Si se genera un objeto excepción
durante la
excepciones.
ejecución del bloque try,. las tareas de limpieza podrian llegar a
omitirse,
porque la excepción provocaría una salida inmediata del bloque
try. Aunque
podemos colocar esas instrucciones de limpieza inmediatamente
después de la última cláusula
catch, esto solo funcionará si la excepción es atrapada por una de
las cláusulas catch. Y es posible
que resulte dificil garantizar que esto sea así.
1.
Observe que tanto try oomo catch requicren un bloque y no solo
una única Instrucción. Por tanto, las llaves no son opcionales. Para
ahorrar espacio, a memxdo escribimos las cláusulas catch simples
en una sola linea, junto con SuIS llaves, sangradas dos espacios
adicionales, en lugar de emplear tres lineas. Pasteriormente en el
texto utilizaremos este estilo para los métodos de una sola linxea.
49
2.5 Tratamiento de excepciones
-
En esta situación, lo que se hace es utilizar una cláusula finally,
que puede incluirse después
del último bloque catch (o del bloque try si no hay bloques catch).
La cláusula finally está
compuesta por la palabra clave finally seguida del bloque finally.
Existen tres casos básicos.
2.5.3
Excepciones comunes
En Java existen varios tipos de excepciones estándar. Las
excepciones
La excepaones de tiempo
estándar de tiempo de ejecución incluyen sucesos tales como la
división
de ejecuconno ticnon que
Ex tratadas.
entera por cero y el acceso ilegal a una matriz. Puesto que estos
sucesos
pueden ocurrir prácticamente en cualquier parte, sería demasiado
engorroso
exigir que se escribieran rutinas de tratamiento de excepciones
para ellos. Si
se proporciona un bloque catch, estas excepciones se comportan
como cualquier otra excepción. Si
no se proporciona un bloque catch para excepción estándar y se
genera una de estas excepciones,
entonces la excepción se propagará de la forma usual,
posiblemente más allá de ma in. En este caso,
provocará una terminación anormal del programa con un mensaje
de error. En la Figura 2.12 se
muestran algunas de las excepciones estándar de tiempo de
ejecución más comunes. En términos
generales, se trata de errores de programación y no se debería
intentar capturarlos. Una violación
otable de este principio es NumberFo rmatExcept i on, aunque
NulIPo1 nterExcepti on resulta más
típica.
Figura 2.12 Excepciones estândar de tlempo de ejecución
comunes.
50
Capitulo 2 Tipos de referencia
la mayor parte de las excepciones pertenecen a la categoría de
excepciones
Las excepoones
cornpobadas deben ser
comprobadas estándar. Si se invoca un método que pueda generar
directa
o
Faladas o incluidas dentro
indirectamente una excepción comprobada estándar, entonces el
programador
de una dausula throws.
debe proporcionar un bloque catch para ella, o indicar
explicitamente
que hay que propagar la excepción, incluyendo una cláusula
throws en la
declaración del método, Observe que esa excepción debe terminar
procesándose en último término,
porque constituye un estilo de programación terrible que main
tenga una cláusula throws. En la
Figura 2.13 se muestran algunas de las excepciones comprobadas
estándar.
Los errores son problemas de la máquina virtual. El error más
común es
OutOfMemoryError. Otros errores comunes son InternalError y el
infame
Los erTores son excepdicnes
UnknownError, en el que la máquina virtual ha decidido que tiene
problemas
nrecuperables
y que no sabe por qué, pero no quiere continuar. En términos
generales, un
Error es irrecuperable y no debe ser capturado.
2.,5.4
Las cláusulas throw y throws
El programador puede generar una excepción mediante el uso de
la
La ciâusula throw 50
cláusula throw. Por ejemplo, podemos crear y luego generar un
objeto
utöza para generar una
exnepcón.
Arithmet icExcepti ion mediante
throw new Arithmet IcException( "Divide by zero" ):
Puesto que la intención es señalizar al llamante que existe un
problema, nunca debe generarse
una excepción solo para tratarla unas pocas lineas después dentro
del mismo ámbito. En otras
palabras, no incluye una cláusula throw dentro de un bloque try
para luego tratarla inmediatamente
en el bloque catch correspondiente. En lugar de ello, déjela sin
tratar y pase la excepción al
llamante. De otro modo, estaria utilizando las excepciones como
una instrucción barata de salto, lo
cual no constituye un buen estilo de programación y no es,
ciertamente, para lo que están pensadas
las excepciones que lo que hacen es señalizar un suceso
excepcional.
Java permite a los programadores crear sus propios tipos de
excepción. En el Capitulo 4 se
proporcionan los detalles sobre cómo crear y generar excepciones
definidas por el usuario.
Como hemos mencionado anteriormente, las excepciones
comprobadas estándar deben ser
capturadas o propagadas explicitamente hacia la rutina llamante;
como último recurso, deberían ser
Figura 2.13 Excepciones comprobadas estándar comunes.
51
2.6 Entrada y salida
1
import java. 1o .IOException:
2
3
public class ThrowDemo
4
l
5
public static void processfile( String tofile )
6
throws IOException
7
{
8
/l La impl ementaci ón omitida propaga hacia el 1amante
9
// todas las excepciones IOExcept ion generadas
10
|
11
12
public static void main( String C 1 args )
13
14
forl String fileName : args )
15
(
16
try
17
processfile( fileName ): I
l
18
catch( IDException e
)
19
l
System. err -printin( e ): }
20
21
22
Figura 2.14 lustración de la ciáusula throws.
tratadas en ma in. Para propagar la excepción hacia el llamante, el
método que no quiera capturar la
excepción, deberá indicar mediante una cláusula throws, qué
excepciones puede propagar.
La cláusula throws se incluye al final de la cabecera del método.
La Figura
La clâu sula throws
2.14 ilustra un método que propaga cualquier excepción de tipo
IOExcept ion
indica las excepciones
que se encuentre; estas excepciones deberán terminar siendo
capturadas en
propagadas.
ma in (puesto que no vamos a incluir una cláusula throws en ma i
n).
2.6
Entrada y salida
La entrada y salida (E/S) en Java se lleva a cabo utilizando el
paquete java. 10. Los tipos del
paquete de EJS utilizan todas como prefijo java- 1o, incluyendo,
como ya hemos visto, java .
io .I0Exception. La directiva import permite no tener que utilizar
los nombres completos. Por
ejemplo, con
import java. io. .IOExcept ion:
se puede utilizar I0Exception como abreviatura de java.
io .I0Except ion al principio del
código. (Muchos tipos comunes, como String y Math, no requieren
directivas import, ya quie son
automáticamente visibles a través de sus abreviaturas, gracias a
que están incluidos en java. 1ang.)
52
Capitulo 2 Tipos de referencia
La librería Java es muy sofisticada y tiene una gran variedad de
opciones. Aquí vamos a
examinar únicamente los usos más básicos, concentrándonos
exclusivamente en la ES formateada.
En la Sección 4.5.3, hablaremos del diseno de la librería.
2.6.1
Operaciones básicas de flujos
Al igual que muchos lenguajes, Java utiliza para la E/S la noción de
flujos. Para realizar la E/S
Iacia o desde el terminal, un archivo o a través de Internet, el
programador crea un flujo de datos
asociado. Una vez hecho eso, todos los comandos de EIS se
dirigen hacia ese flujo de datos. El
programador define un flujo para cada destino de ES (por ejemplo.
cada archivo que requiere
entrada y salida).
Existen tres flujos predefinidos para la E/S de terminal: System. in,
la entrada estándar; System.
out, la salida estándar y System. err, la salida estándar de error.
Como ya hemos mencionado, los métodos print y println se utilizan
Los flujos predefinidos son
para la salida formateada. Cualquier tipo puede convertirse a un
objeto
System-In, System.
String adecuado para su impresión invocando a su método
toString;
out [Link].
en muchos casos, esto se hace automáticamente. A diferencia de
Cy C+#
que tienen una cantidad enorme de opciones de formato, la salida
en Java
se realiza casi exclusivamente mediante concatenación de objetos
String, sin ningún formateo
predefinido.
2.6.2
El tipo Scanner
El método más simple de leer una entrada con formato consiste en
utilizar un Scanner. Un Scanner
permite al usuario leer líneas de una en una mediante nextline;
leer objetos String de uno en
uno utilizando next 0 leer tipos primitivos de uno en uno utilizando
métodos como nextInt
y nex tDouble. Antes de intentar realizar una lectura, es habitual
verificar que la lectura puede
llevarse a cabo correctamente, utilizando métodos como
hasNextline, hasNext, hasNextInt
y
ha sNextDouble, que proporcionan resultados de tipo boolean;
debido a ello, normalmente hay
menos necesidad de preocuparse por el tratarmiento de
excepciones. Cuando se emplea un Scanner
es costumbre proporcionar ia directiva de importación
import java. util .Scanner:
Para utilizar un Scanner que lea de la entrada estándar, primero
tenemos que construir un objeto
Scanner a partir de System. in. Esto se ilustró ya anteriormente en
la Figura '2.11, en la línea 7. En
la Figura 2.11, vemos que se emplea nextline para leer un objeto
String y que luego el objeto
String se convierte en int. Teniendo en cuenta io que hemos
hablado sobre Scanner en el párrafo
anterior, sabemos que existen otras opciones,
Una opción alternativa, que quizá sea la más limpia, sería la
siguiente sustitución, que evita las
excepciones por completo y utiliza nextInt y ha sNextInt:
System. out -printin( "Enter an integer: m ):
)
ifc in .hasNextInt( )
l
53
2.6 Entrada y salida
x = in nextInt( ):
System .out -println( "Half of x is . + (x/ 2) ):
else
System -out .printin( "Integer was not entered. " }
l
Utilizar las diversas combinaciones de next y hasNext de Scanner
suele funcionar correc-
tamente, aunque pueden presentarse algunas limitaciones, Por
ejemplo, suponga que queremos
leer dos enteros e imprimir el máximo.
La Figura 2.15 muestra una idea que resulta algo engorrasa si
queremos llevar a cabo una
apropiada comprobación de errores sin utilizar excepciones, Cada
llamada a nextInt va precedida
por una ]lamada a hasNextInt, alcanzándose al final una linea
donde se imprime un mensaje de
error, a menos que haya dos valores int disponibles en el flujo de
entrada de datos estándar.
La Figura 2.16 muestra una alternativa que no utiliza llamadas a
hasNextInt. En su lugar, las
llamadas a nextInt generarán una excepción NoSuchEl
ementException si el valor int no está
disponible, y esto hace que el código parezca más limpio. El uso de
la excepción es quizá una
1
import java. util -Scanner:
2
3 class MaxTesta
4
l
5
public static void main( String C 1 args
)
6.
7
Scanner 1n - new Scanner( System. in ):
B
int X . y:
9
10
System .out -println( "Enter 2 ints: " ):
11
ifc in .hasNextInt( )
12
)
13
[
14
x - in .nextint( )
15
)
ifl in .hasNextInt( )
l
16
17
y = in .nextInt( ):
18
System .out -printìn( "Max: " + Math. ma x( X. y ) ;
19
return:
20
)
21
22
23
System -err -println( "Error: need two ints" ):
24
25 }
Figura 2.15 Leer dos enteros e lmprimir el maximo utllizando
Scanner Y sin recurrir alas excepcianes.
54
Capitulo 2 Tipos de referencia
class MaxTestb
1
2
{
3
public static void main( String C J args
)
4
Scanner in = new Scanner( System. in );
5
6
System .out ·printlnc "Enter 2 ints: " ):
7
B
9
try
10
l
int x = in -nextInt( ):
11
int y a* [Link] ( ):
12
13
14
System .out ·pri ntlnt "Max: " + Math. maxl x. y ) ):
15
)
16
catchc NoSuchi lement Exception e
)
17
System serr .println( "Error: need two ints" ):
,
l
18
19 l
Figura 2.16 Leer dos enteros e imprimir cl maximo uillzando
Scannery jexcepciones.
1
import java. util .Scanner:
2.
3
public class MaxTestc
4
l
5
public static void maint String C 1 args
)
6
7
Scanner in - new Scanner( System. in ):
8
9
System .out ·printin( "Enter 2 ints on one line: " ):
10
try
11
[
12
String oneline = in. nextline( );
13
Scanner str -- new Scanner( oneline ):
14
int x = str. nextInt( ):
15
16
int y " str. nextInt( ):
17
18
System. .out -printin( "Max: " + Math. ma x( x . y ) ):
19
)
20
catch( NoSuchElement Exception e
)
21
(
System .err -printin( "Error: need two ints" );
1
22
23 1
Figura 2.17 Leer dos enteros de la mlsma Ilnea elmprimir el
mâximo, ulizando dos objetos Scanner.
55
2.6 Entrada y salida
decisión razonable, porque en realidad se consideraría inusual que
el usuario no introdujera dos
enteros para este programa.
Sin embargo, ambas opciones están limitadas porque en muchos
casos podriamos insistir
en que los dos enteros se introdujeran en la misma lfnea de texto.
Podríamos incluso insistir
en que no hubiera ningún otro dato en dicha línea, En la Figura 2.17
se muestra una opción
distinta. Podemos construir un Scanner proporcionando un objeto
String. Así que podemos
crear primero un Scanner a partir de System. in (linea 7) para leer
una única linea (iinea 12)
y luego crear un segundo Scanner a partir de la única linea (linea
13) con el fin de extraer
los dos enteros (lineas 15 y 16). Si algo va mal, el programa
captura y trata una exce pción
NoSuchel ementException.
El uso del segundo Scanner en la Figura 2.17 puede funcionar bien
y resulta cómodo; sin
embargo, si es importante asegurarse de que no haya más de dos
enteros por linea, necesitaríamos
código adicional. En particular, tendriamos que añadir una llamada
a str .hasNext() y. si esa
llamada devolviera un valor verdadero, sabríamos que hay un
problema, Esto se ilustra en la
Figura 2.18. Existen otras opciones, como por ejemplo el método
split de String, tal y como se
describe en los ejercicios.
class MaxTestD
1
2
3
)
public static vofd mainc String E J args
4
5
Scanner in = new Scanner( System. in
6
7
System .out ·println( "Enter 2 ints on one line: " ):
8
try
9
(
10
String oneline = in. nextline( );
11
Scanner str - new Scanner( oneline ):
12.
13
int x - str. nextint( ):
14
int y - str. nextint( ):
15
16
ifl !str .hasNext( ) )
17
System .out -printin( "Max: " + Math. max( X. y ) }:
18
else
19
System .err -printin( "Error: extraneous data on the 1ne." ):
}
20
21
catch( NoSuche 1ementException e )
22
System .err -printin( Error: need two ints" ):
23
24 }
Figura 2.18 Leer exactamene dos enteros dela misma lined e
imprimit el máximo utillzando dos atijelos Scanner.
56
Capitulo 2 Tipos de referencia
Archivos secuenciales
2.6.3
Una de las reglas básicas de Java es que lo que funciona para la
E/S
a
través de terminal también funciona para los archivos, Para tratar
con un
FileReader se usiliza
para la entrada desde
archivo, no construimos ningún objeto Buf feredReader a partir de
un
archivo.
InputStreamReader. En lugar de ello, lo construimos a partir de un
objeto
Fi leReader, que a su vez puede ser construido proporcionando un
nombre
de archivo,
En la Figura 2.19 se muestra un ejemplo que ilustra estas ideas
básicas. En ella, tenemos un
programa que muestra el contenido de los archivos de texto
especificados como argumentos de la
linea de comandos. La rutina ma i n simplemente recorre los
argumentos de la linea de comandos,
pasando cada uno de ellos a listFile. Ën listFile, construimos el
objeto FileReader en la linea
22 y luego lo usamos para construir un objeto Scanner, fileIn.
Llegados a ese punto, la lectura es
iéntica al procedimiento que ya hemos visto.
4
public class Listfiles
5
6
7
public static void main( String C J args
)
8
g
2
ifl args .length = 0
10
System .out- ·printin( "No files specified" ):
11
fort String fileName : args
)
12
listfile( fi TeName ):
13
14
15
)
public static vofd listfile( String fileName
16
17
Scanner fileIn = nu11;
18
19
System .out -printin( "FILE: " + fileName ):
20
try
21
[
22
fileIn - new Scanner( new Filereader( fileName ) );
23
whilel fileln. hasNextline( ) )
24
l
25
String oneline =- fileln .nextline( ):
Continüa
Figura 2.19 Programa para mostrat el conlenido de un archivo.
57
2.6 Entrada y salida
26
System .out .printin( oneline ):
}
27
)
28
29
catch( I0Exception e )
30
}
l System -out .printin( e ):
31
finaTly
32
(
33
// Cerrar el flujo de datos
34
ifc fileIn !- null )
35
fileIn .close( ):
36
37
38 l
Figura 2.19 {Continusción).
Después de terminar con el archívo, debemos cerrarlo; en caso
contrario, podríamos terminar
quedándonos sin flujos de datos. Observe que esto no puede
hacerse al final del bloque try,
ya que una excepción podría provocar una salida prematura del
bloque. Por tanto, cerramos
el archivo en un bloque finally, que se garantiza que se ejecutará
independientemente de si
hay excepciones, y de si estas se tratan o no. El código para
gestionar la instrucción close es
complejo porque
La salida formateada a archivo es similar a la entrada desde
archivo.
Fileriter se uliliza
Filewriter, Printhriter y println sustituyen a FileReader, Scanner y
para la salkla z a archivo.
nextline, respectivamente. La Figura 2.20 ilustra un programa que
escribe
a doble espacio los archivos especificados en la línea de
comandos (los
archivos resultantes se colocan en un archivo con una
extensión .ds).
Esta descripción de la EJ/S en Java, aunque es suficiente para
realizar una E/S básica formateada,
oculta un interesante diseño orientado a objetos del que
hablaremos con más detalle en la Sección
4.5,3.
58
Capitulo 2 Tipos de referencia
1 I/ Escribir a doble espacio 1os archivos especificados en 1a
11nea de comandos
2
3
import java. io .Filereader:
4
Import java. [Link]:
5
Import java.1o. .Printhriter:
6
import [Link] IOException:
7
import java. [Link];
B
public class Doublespace
9
10
public static void maIn( String L 1 args
)
11
12
13
)
forl String fileName : args
14
doubleSpace( fileName ):
)
15
16
)
public static void doubleSpacel String fileName
17
[
18
19
Printhriter fileout = null;
20
Scanner fileln -- nuT1:
21
try
22
I
23
fileIn = new Scanner( new FileReader( fileName ) ):
24
fileDut - new Printwriter( new Filewriter( fileName + " .ds " ) ):
25
26
27
while( fileln. hasNextline( )
)
28
{
String oneline - fileln -nextl ine( ):
29
30
filedut. printin( oneline + "\n" ):
|
31
}
32
33
catch( I0Exception e )
34
l e-printStackTrace( ):
}
35
finally
l
36
if( file0ut I nu11 )
37.
38
fi [Link]( ):
if( fileIn != nu11 )
39
40
fileIn .close( ):
41
42
43 l
Figura 2.20 Pragrama para escribir achivosa doble espacio.
59
Conceptos clave
Resumen
En este capítulo hemos examinado los tipos de referencia. Una
referencia es una variable que
almacena la dirección de memoria en la que un objeto reside o la
referencia especial nu11. Solo
puede hacerse referencia a objetos y cada objeto puede ser
referenciado por varias variables
de referencia. Cuando se comparan dos referencias mediante -, el
resultado es true si ambas
referencias se refieren al mismo objeto. De forma similar, - hace
que una variable de referencia
referencie a otro objeto. Solo hay disponibles unas cuantas
operaciones adicionales. la más
significativa es el operador punto, que permite seleccionar un
método de un objeto o acceder a suS
datos internos,
Puesto que solo hay ocho tipos primitivos, casi todas las cosas
significativas en Java son objetos
yse accede a ellas mediante referencias. Esto incluye los objetos
de tipo String, las matrices, los
objetos de excepción, los flujos de datos de archivo y las
analizadores sintácticos de cadenas.
String es un tipo de referencia especial, porque pueden utilizarse
los operadores +y +o para la
concatenación. Por lo demás, un objeto String es como cualquier
otra referencia; hace falta equals
para comprobar si el contenido de dos objetos String son
idénticos. Una matrizes una colección
de valores con tipo idéntico. La matriz se indexa empezando por 0
y se garantiza la realización de
comprobaciones del rango del indice. Las matrices se pueden
expandir dinámicamente utilizando
new para asignar una cantidad mayor de memoria y luego
copiando los elementos individuales, Este
proceso lo realizan automáticamente los objetos Arraylist.
Las excepciones se emplean para señalar sucesos excepcionales.
Una excepción se señaliza
mediante la cláusula throw; la excepción se propaga hasta ser
tratada por un bloque catch asociado
con un bloque try. Salvo por las excepciones de tiempo de
ejecución y los errores, cada método
debe senalizar las excepciones que pueda propagar utilizando una
lista throws.
La entrada se gestiona mediante objetos Scanner y FileReader, En
el siguiente capítulo
veremos cómo diseñar nuevos tipos definiendo una clase.
a
Conceptos clave
agregado Una colección de objetos almacenados en una unidad,
(37)
argumento de linea de coandos Se accede a él mediante un
parámetro de ma in. (46)
Arraylist Almacena una colección de objetos en formato de matriz,
permitiendo
facilmente expandir la colección mediante el método add. (41)
bloque catch Utilizado para procesar una excepción. (47)
bucle for avanzado Añadido en Java 5, permite la iteración a través
de una colección de
elementos. 0
concatenación de cadenas Se lleva a cabo con los operadores +y
to. (35)
construcción Para los objetos, se realiza mediante la palabra clave
new, (31)
equals Se utiliza para comprobar si los valores almacenados en
dos objetos son iguales.
(34)
entrada y salida (E/S) Se lleva a cabo utilizando el paquete java. 1o.
(51)
60
Capitulo 2 Tipos de referencia
Error Una excepción no recuperable. (50)
excepción Utilizada para tratar sucesos excepcionales, como por
ejemplo los errores. (47)
excepción comprobada Debe ser atrapada o debe permitirse
explícitamente que se
propague utilizando una cláusula throws. (50)
excepción de tienpo de ejecución No es necesario tratarla. Como
ejemplas podríamos
citar ArithmeticExcepti on y Nu11PointerException. (49)
expansión dinámica de matrices Nos permite hacer más grandes
las matrices en caso
necesario. (40)
FileReader Usado para entrada desde archivo, (56)
Filewr 'iter Usado para salida a archivo, (57)
finally, ckáusula Siempre se ejecuta antes de salir de una
secuencia try/catch. (48)
inntable Objeto cuyo estado no puede cambiar. Específicamente,
los objetos String
son inmutables, (34)
java . 1o Paquete utilizado para la E/S no trivial. (51)
length, campo Utilizado para determinar el tamaño de una matriz.
(37)
length, método Utilizado para determinar la longitud de una
cadena. (36)
Ths y rhs Hacen referencia al lado izquierdo y al lado derecho,
respectivamente. (31)
atriz Almacena una colección de objetos de tipo idéntico. (37)
matriz multidimesional Una matriz a la que se accede mediante
más de un índice, (43)
new Utilizado para construir un objeto. (30)
nu11, referencia El valor de una referencia a objeto que no hace
referencia a ningún
objeto. (30)
NuT1Po interException Se genera cada vez que se intenta aplicar
un método a una
referencia nu11. (30)
ohjeto Una entidad no primitiva, (29)
aperador de indexación de matriz C1 Proporciona acceso a
cualquier elemento de la
matriz.. (37)
aperadar panto (.) Permite acceder a cada miembro del objeto. (29)
paso por referencia En muchos lenguajes de programación
significa que el parámetro
formal es una referencia al argumento real. Este es el efecto
natural que se consigue en
Java al utilizar el paso por valor con tipos de referencia. (32)
reoolecciún de basura Reclamación automática de la memoria no
referenciada, (31)
Scanner Se utiliza para llevar a cabo la entrada linea a linea.
También se emplea para
extraer lineas, cadenas y tipos primitivos a partir de una única
fuente de caracteres,
como por ejemplo un flujo de datos de entrada o un objeto String.
Se encuentra en el
paquete java .util. (52)
String Un objeto especial utilizado para almacenar una colección
de caracteres. (34)
System. in. System. outy System. err Los flujos de datos de E/S
predefinidos. (52)
tipo dereferencia Cualquier tipo que no sea un tipo primitivo. (30)
61
Internet
throw, cháusula Utilizada para generar una excepción. (50)
throws, chusula Indica que un método puede propagar una
excepción. (51)
tostring, método Convierte un objeto o un tipo primitivo a un objeto
String. (36)
try, bloque Encierra código que podría generar una excepción. (47)
Errores comunes
Internet
A continuación se indican los archivos disponibles para este
capítulo. Todo está auto-
contenido y nada de ello se utiliza posteriormente en el texto.
Contiene el código del ejemplo de la Figura 2,4.
RandomNumhers java
ReadStrings java
Contiene el código de los ejemplos de las Figuras
2.6 y 2.7.
Contiene el código del ejemplo de la Figura 2.8.
RendStringsWithArrayList,java
Contiene el código del ejemplo de la Figura 2.9.
MatrixDemo java
Echo java
Contiene el código del ejemplo de la Figura 2.10.
llustra el bucle for avanzado.
ForEachDemo java
Contiene el código del ejemplo de la Figura 2.11.
DivideByl wo java
Contiene el código de los ejemplos de las Figuras
Maxlestjava
2.15-2.18.
62
Capitulo 2 Tipos de referencia
Contiene el código del ejemplo de la Figura 2.19.
ListFiles java
Contiene el código del ejemplo de la Figura 2.20.
DoubleSpace java
Ejercicios
EN RESUMEN
EN TEORÍA
y
);
System. out .println( x +
+
y
);
System. .out -println( x +
"
t
EN LA PRÁCTICA
public static votd resize( int L J arr
)
int L old = arr:
arr = new intl old .Tength * 2 + 1 1:
for( int 1 = 0: 1 < old. Tength: i4t
)
arrt i 1 = oTdl i 1:
63
Ejercicios
public static void foot )
f
try
l
return 0:
)
finally
(
return 1:
I
1
public static votd barc )
l
try
l
throw new NullPointerExcept ion( ):
)
finally
throw new ArithmeticException ( ) :
Figura 2.21 Complicaciones causadas por el bloque finally.
64
Capitulo 2 Tipos de referencia
65
Ejercicios
2.23 Tanto Scanner como split se pueden configurar para utilizar
delimitadores que
sean distintos de los espacios en blanco normales. Por ejemplo, en
un archivo
delimitado por comas, ei único delimitador es la coma. Para split,
utilice "c .]"
como parámetro y para Scanner utilice la instrucción
)
scan .useDelimi terc "[ .1"
Con esta información modifique el código de la Sección 2.6.2 para
que funcione
con una linea de entrada delimitada por comas.
224 Una alternativa a utilizar Scanner consiste en utilizar el
método split para un
objeto String. Específicamente, en la instrucción siguiente
String L 1 arr = str. .split( "I1s" ):
si str es "this is a test". entonces arr será una matriz de longitud
cuatro que
almacenará las cadenas de caracteres "this", "1s", "a" y "test".
Modifique el
código de la Sección 2.6.2 para utilizar split en lugar de un objeto
Scanner.
225 Implemente el método startswith que devuelve un Arraylist
que contiene todas
las cadenas de caracteres de arr que comienzan con el carácter
ch.
public Arraylist<String> startswith( String L ] arr. char ch )
228 Implemente un método split que devuelva una matriz de
objetos String que
contenga las unidades sintácticas del objeto String. Utilice un
Scanner. La
signatura del método split es
)
public static String C J split( String str
2.27 Utilizando el método tolowerCase de String que crea un nuevo
objeto String,
que es el equivalente en minúsculas de un objeto String existente
(es decir, str.
toLowerCase() devuelve el equivalente en minúsculas de str,
dejando str sin
modificar), implemente los métodos getLowerCase y ma
keLowerCase siguientes.
getl owerCase devuelve una nueva colección de objetos String,
mientras que
ma keLowerCase modifica la colección existente,
public static String L 1 getlowerCa se ( String L 3 arr
)
public static void makeLowerCasel String L ] arr
public static Arraylist<String> getLowerCase( Arraylist<String> arr
)
)
public static void makeLowerCaset Arraylist<string> arr
PROYECTOS DE PROGRAMACIÓN
228 Escriba un programa que muestre el número de caracteres,
palabras y. lineas de los
archivos que se le suministren como argumentos de la linea de
comandos,
229 En Java, la división por cero de coma flotante es legal y no
provoca una excepción
(en lugar de ello, proporciona una representación del infinito, del
infinito negativo
o
un símbolo especial que indica que el valor no es un número).
a. Verifique la descripción anterior realizando algunas divisiones
en coma flotante.
66
Capitulo 2 Tipos de referencia
ApeTlido :Nombre: Examen1 :Examen2 : Examen3
Los exámenes se ponderan de la manera siguiente: 20% para el
primer examen,
25% para el segundo examen y 55% para el tercer examen.
Teniendo esto en cuenta
hay que asignar una nota final al alumno: A si el total es al menas
90, B si es al
menos 80, c si es al menos 70, D si es al menos 60 y F en cualquier
otro caso.
Siempre se asigna la nota más alta basada en el total de puntos
obtenidos, por lo que
un 75 equivaldría a una C.
El programa debería presentar en el terminal una lista de
estudiantes con la letra
correspondiente a su nota de la forma siguiente:
Apellido Nombre LetraNota
También debe guardar la salida en un archivo, cuyo nombre será
proporcionado por
el usuario. Las lineas del archivo deben tener el formato
Apellido Nombre Examen1 Examen2 Examen3 Totalpuntos
LetraNota
67
Referencias
Después de guardar en el archivo los datos, deberá proporcionar
como salida la
distribución de notas. Si la entrada es
Doe: John 1 :100 :100 : 100
Pantz: Smartee: 80 :90:80
Entonces la salida a través de terminal será
Doe John A
Pantz Smartee B
Y el archivo de salida contendrá
Doe John 100 100 100 100 A
Pantz Smartee 80 90 80 83
B
A1
B1
c0
D0
F0
2.38 Implemente un programa de copia de archivos de texto.
Incluya una comprobación
para garantizar que los archivos de origen y de destino sean
diferentes.
Referenclas
Puede encontrar más información en las referencias especificadas
al final del Capítulo
1.
3
Capítulo
Objetos y clases
En este capítula comienza la exposición acerca de la
programación orientada a objetos. Un
componente fundamental de la programación orientada a objetas
es la especificación, la imple-
mentación y el uso de objetos. En el Capítulo 2, hemos visto varios
ejemplos de objetos, incluyendo
cadenas de caracteres y archivos, que forman parte de la librería
Java obligatoria. También hemos
visto que estos objetos tienen un estado interno que puede
manipularse aplicando el operador punta
para seleccionar un método. En Java, el estado y la funcionalidad
de un objeto queda determinado
mediante la definición de una clase. Por tanto, un objeto es una
instancia de una clase.
Ein este capitulo, vamos a ver
3.1
¿Qué es la programación
orientada a objetos?
La programación orientada a objetos emergió como el paradigma
dominante a mediados de la
década de 1990. En esta sección vamos a exponer algunas de las
cosas que Java proporciona de
cara al soporte de la orientación a objetos y vamas a mencionar
algunos de los principios de la
programación orientada a objetos.
En el núcleo mismo de la programación orientada a objetas se
encuentra
Los otyjefos son enliades
el objeto. Un objeto es un tipo de datos que tiene una estructura y.
un
que tienen tunt estructura
estado, Cada objeto define operaciones que permiten acceder a
ese estado o
yun estado. Cada otjeto
definc opcradioncs que
manipularlo. Como ya hemos visto, en Java los objetos son
distintos de los
permiten acceder a dicho
tipos primitivos, pero esto es una caracteristica concreta de Java,
más que
estado o marăputarlo,
formar parte del paradigma de. orientación a objetos. Además de
realizar
operaciones generales podemos hacer lo siguiente:
70
Capitulo 3 Objetos y clases
Asimismo, consideramos el objeto como una unidad atómica que el
Ln objeto es una unkiad
alimica sus parles no
usuario no debería diseccionar. A la mayoría de nosotros ni
siquiera se nos
y
pueden ser diseccionadas
ocurriría enredar con los bits que forman un número en coma
flotante,
por los usuarlos gencrales
consideraríamos completamente ridículo tratar de incrementar
algún objeto
dei objeto.
de coma flotante modificando nosotros mismo su representación
interna.
El principio de atomicidad se conoce con el nombre de ocultación
de la
información El usuario no dispone de acceso directo a las distintas
partes
La ocuilacion de
del objeto ni a sus implementaciones; solo se puede acceder a
esas partes del
informadtn hace que scan
objeto indirectamente mediante métodos suministrados junto con
el propio
inaccesibles los detales de
implementacon, incluyendo
objeto. Podemos considerar cada objeto como si viniera
acompañado de
los componentes de un
una advertencia: "No abrir. No contiene ninguna parte que el
usuario pueda
oljsto.
modificar" . En la vida real, la mayoría de las personas que tratan
de arreglar
cosas que vienen con esa advertencia, terminan haciendo más
daño, en lugar
de arreglar lo que no funciona, A este respecto, la programación se
asemeja
La encapsuación es la
al mundo real. La agrupación de una serie de datos y de las
operaciones que
agrupacion de dalos yde
se aplican a ellos con el fin de formar un agregado, al mismo
tiempo que se
las operaciones que se
apican a clos, para formar
ocultan los detalles de implementación de ese agregado, se
conoce con el
un agregado, al mismo
rombre de encapsulación.
tiempo que se oculta la
Un objetivo importante de la programación orientada a objetos
consiste
implemenladon de ese
aregado.
en fomentar la reutilización de código. Al igual que los ingenieros
utilizan
componentes una y. otra vez. en sus diseños, los programadores
deberian
ser capaces de reutilizar objetos en lugar de reimplementarlos
varias veces.
Cuando disponemos de una implementación del objeto exacto que
necesitamos emplear, la
reutilización es bastante sencilla. ÉI desafio está en utilizar un
objeto existente cuando el objeto que
necesitamos no se corresponde exactamente con él, sino que
simplemente es bastante similar.
Los lenguajes orientados a objetos proporcionan varios
mecanismos para apoyar este objetivo.
Uno de esos mecanismos es el USo de código genérico. Si la
implementación es idéntica, salvo por
el tipo básico del objeto, no hay necesidad de reescribir
completamente el código: en lugar de ello,
lo que hacemos es escribir el código genéricamente para que
pueda funcionar con cualquier tipo
de objeto, Por ejemplo, la lógica utilizada para ordenar una matriz
de objetos es independiente del
tipo de los objetos que se estén ordenando, por lo que en este
caso podriamos utilizar un algoritmo
genérico.
El mecanismo de herencia permite ampliar la funcionalidad de un
objeto. En otras palabras,
podemos crear nuevos tipos con propiedades restringidas (o
ampliadas) con respecto al tipo
criginal, La herencia nos permite avanzar enormemente en nuestro
camino hacia la reutilización
de código.
Otro principio importante de la orientación a objetos es el
polimorfismo. Un tipo de referencia
polimórfico puede hacer referencia a varios tipos de objetos
distintos. Cuando se aplican métodos
al tipo polimórfico, se selecciona automáticamente la operación
que resulte apropiada para el objeto
actualmente referenciado. En Java, esto se implementa como
parte del mecanismo de herencia. El
polimorfismo nos permite implementar clases que compartan una
lógica común. Como se explica
en el Capítulo 4, este principio está ilustrado en ias propias
librerias Java. El uso de la herencia para
crear estas jerarquías distingue a la programación orientada a
objetos de la programación basada en
objetos, que es más simple.
71
3.2Un ejemplo simple
En Java, los algoritmos genéricos se implementan como parte del
mecanismo de herencia. En
el Capîtulo 4 se explica la herencia y el polimorfismo. En este
capítulo, vamos a describir cómo el
lenguaje Java utiliza las clases para conseguir la encapsulación y
la ocultación de información.
Un objeto en Java es una instancia de una clase. Una clase es
similar a
una estructura en c o a un registro en Pascal/Ada, salvo por dos
mejoras
Uina chse en Java estă
importantes, En primer lugar, los miembros pueden ser tanto
funciones
compuesta por campas
que almacenán dalosy de
como datos, a los que se conoce con los nombres de métodos y
campos,
mólodos que se aplcan a
respectivamente. En segundo lugar, la visibilidad de estos
miembros puede
as instanclas de esa clase,
estar restringida. Puesto que los métodos que manipulan el estado
del objeto
son miembros de la clase, se accede a ellos mediante el operador
punto, al
igual que pasa con los campos. En terminología de orientación a
objetos, cuanddo hacemos una
llamada a un método lo que hacemos es pasar un mensaje al
objeto. Los tipos que hemos explicado
en el Capítulo 2, como por ejemplo Stri ing, Arraylist, Scanner y
FfleReader. son todos ellos
clases implementadas en la librería Java.
3.2
Un ejemplo simple
Recuerde que a la hora de diseñar una clase es importante ser
capaz de ocultar
La funcionaidiad se
los detalles internos a ojos del usuario de esa clase. Esto se lleva
a cabo de
suministra mediante
miembros adidonales;
dos maneras, En primer lugar, la clase puede definir funcionalidad
mediante
ostos m tlodos manipulan el
miembros de la clase, denominados métodos Algunos de esos
mêtodos
estadoi del objeła.
describen cómo se crea e inicializa una instancia de la estructura,
cómo se
realizan las comprobaciones de igualdad y cómo se lleva a cabo la
salida,
Otros métodos serían especificos de cada estructura concreta. La
idea es que los campos de datos
internos que representan el estado de un objeto no deberían poder
ser manipulados directamente
por el usuario de la clase, sino que deberían manipularse
solamente a través de los métodos, Esta
idea puede ser reforzada ocultando los miembros a ojos del
usuario. Para hacer esto, podemos
especificar que esos miembros se almacenen en una sección
privada. El compilador impondrá la
regla de que los miembros contenidos en la sección privada sean
inaccesibles para los métodos que
no pertenezcan a esa clase de objetos. Hablando en términos
generales, todos los datos miembro
deberían ser privados.
La Figura 3.1 ilustra la declaración de clase para un objeto
IntCel1.' La
Los miembros públicos son
declaración está compuesta de dos partes: publica y privada. Los
miembros
usables para las rulinas que
nD portenecen a ta clse,
públicos representan la parte que es visible para el usuario del
objeto.
us miemtros privados no
Puesto que esperamos poder ocultar los datos, en la sección
pública solo
In son.
se deberian incluir, generalmente, los métodos y constantes. En
nuestro
ejemplo, disponemos de métodos que leen y escriben en el objeto
IntCe11.
La sección privada contiene los datos; estos son invisibles para el
usuario del objeto. Para acceder
al miembro storedvalue deben utilizarse las rutinas públicamente
visibles read y write; ma in no
puede acceder directamente a ese miembro. En la Figura 3.2 se
muestra otra forma de ver esto.
Las clases públicas deben almacenarse en archivos que tengan el
mismo nombre, Por tanto, IntCell debe estar en un archivo Ilamado
[Link]. Hablarenos del significado de publ icen la linea 5
autando expliquemos lo que son los paquekes.
72
Capitulo 3 Objetos y clases
1 I/ clase IntCeT1
--) Devuelve el valor a lmacenado
2 /l int read( )
3 // void write( int x ) --) se a lmacena x
4
5
public class IntCe11
6
7
/l Metodos públicos
8
public int read( ) l return storedvalue:
)
g
)
public void writel int x. storedVa lue = x:
10
11
H/ Representación privada interna de los datos
12
private int stor edVal lue:
13
Flgura 3.1 Una declaracion Completa de unaciase IntCe1l.
read
write
StoredValue
Figura 3.2 Mlembros de IntCelltread ywrite son accesibies, perd
storedvalue esta ocuilo.
La Figura 3.3 muestra cómo se utilizan los objetos IntCe11. Puesto
que
Los miermtros que se
dedaran como private
read y writeson miembros de la clase IntCe11, se accede a ellos
utilizando
no son visibles para las
el operador punto, Al miembro storedvalue también se podría
acceder
rutinas que no pertenecen
utilizando el operador punto, pero como es de tipo private, el
acceso de la
a la clase.
lînea 14 sería ilegal si no estuviera desactivado como comentario.
// Ejemplo de uso de la clase IntCe7l
1
2
3
public class TestIntCell
4
I
public static vofd main( String L 1 args )
5
6
IntCell m = new Intcell( );
7
8
m. write( 5 ):
9
10
System .out -println( "CeTl contents: * + m .read( ) ):
11
12
/l La siguiente 1inea serfa ilega1 si no estuviera desactiva por un
13
/l comentario, porque storedvalue es un mi embro privado
14.
Il m -storedvalue = :
15
16 l
Figura 3.3 Una sencilla derutina de comprobación para mostra
como se accede a los objetos IntCell.
73
3.3 javadoc
He aquí un resumen de la terminología, L.a clase define miembros
que
Lin campoes un miembro
que almacena datos; un
pueden ser campos (datos) o métodos (funciones). Los métodos
pueden
mólodoes un mäcmbro que
actuar sobre los campos y pueden invocar a otros métodos. El
modificador de
realiza una accion.
visibilidad public significa que el miembro es accesible para todo
el mundo,
mediante el operador punto. El modificador de visibilidad private
significa
que a ese miembro solo pueden acceder otros miembros de esta
clase. Si no se especifica ningún
modificador de visibilidad, lo que tendremos es un acceso con
visibilidad de paquete, del que
hablaremos en la Sección 3.8.4. También existe un cuarto
modificador, conocido como protected,
que veremos en el Capítulo 4.
3.3
javadoc
Al diseñar una clase, la especificación de la clase representa el
diseño de la
La especiicación de la
clase describe lo que puede
clase y nos dice lo que podemos hacer con un objeto. La
implementación
hacerse con un objeto. La
representa los detalles internos de cómo se lleva eso a cabo. En lo
que
impiementacion representa
concierne al usuario de la clase, estos detalles internos no son
importantes.
los delalles inlernos acerca
de cónO s0 salisacen tas
En muchos casos, la implementación representa información
confidencial
ospcclicadones
que el diseñador de la clase podría no querer compartir. Sin
embargo, la
especificación sí que debe ser compartida, de lo contrario, la
clase no podría
utilizarse.
En muchos lenguajes, el compartir la especificación al mismo
tiempo que se oculta la implemen-
tación se consigue colocando la especificación y la
implementación en archivos fuente separados.
Por ejemplo, C++ dispone de la interfaz. de clase que se almacena
en un archivo .hy de una impler
mentación de la clase, que se almacena en un archivo .cpp. En el
archivo .h, la interfaz de la clase
vuelve a enumerar los métodos (proporcionando cabeceras de
método) implementados por la clase.
Java adopta un enfoque diferente. Es fácil darse cuenta de que una
lista de métodos de una clase, con suS signaturas y tipos de
retorno, puede
El programa kavadoc
genera aulomaticamente
documentarse automáticamente a partir de la implementación.
Java utiliza la
documentzción para as
siguiente idea: el programa javadoc, que se suministra con todos
los sistemas
cases
Java, puede ejecutarse para generar automáticamente
documentación para las
clases. La salida de javadoc es un conjunto de archivos HTML que
pueden
visualizarse o imprimirse utilizando un explorador web.
El archivo de implementación Java también puede añadir
comentarios javadoc que comiencen
oon el símbolo de inicio de comentario /**. Esos comentarios se
añaden automáticamente, de
manera uniforme y coherente a la documentación producida por
javadoc:.
También hay varias marcadores especiales que pueden incluirse
en los comentarios javadoc. Entre
ellos están eau thor, eparam, @return y Gthrows. La Figura 3.4
ilustra el uso de la funcionalidad de
comentarios javadocpara la clase IntCe11. En la línea 3, se utiliza
el marcador
@author. Este marcador debe preceder a. la definición de la clase.
La línea
Entre los rmarcadores
10 ilustra el uSO del marcador @return y la linea 19 el del marcador
@param.
jvadoc s0 encuentran
euuthor, ®param,
Estos marcadores deben aparecer antes de la declaración de un
método. El
®returny athrows, Se
primer simbolo situado a continuación del marcador eparam es el
nombre del
uti zan en los comentarios
parámetro. El marcador @throws no se muestra, pero utiliza la
misma sintaxis
javadoc.
que ©param.
74
Capitulo 3 Objetos y clases
1 /**
2 * Una clase para simular una celda de memoria entera
3 * Cauthor Mark A. Weiss
4
*/
5
public class Intcell
6
7
l
B
/**
9
* Obtener el valor al macenado.
10
* @return el valor al ma cenado.
I1
*/
12
public int read( )
13
14
return storedvalue:
15
16
17
/**
18
* ATma cenar un valor.
19
* @par am x es el número que hay quie almacenar .
20
*/
21
)
public void write( int x
22
storedvalue - x:
23
24
25
26
private int stor edvalue :
27 }
Figura 3.4 La declaraciónde IntCell con comentarios javadoc
En la Figura 3.5 se muestra parte de la salida resultante de la
ejecución de javadoc. Ejecute
javadocsuministrando un nombre (incluyendo la extensión -java) de
archivo fuente.
La salida de javadoc son puros comentarios, salvo por las
cabeceras de métodos. El compilador
no comprueba que esos comentarios estén implementados. Sin
embargo, es imposible sobrevalorar
la importancia de una adecuada documentación de las clases.,
javadoc facilita la tarea de generar una
documentación bien formateada.
3.4
Métodos básicos
Algunos métodos son comunes a todas las clases. En esta sección
vamos a hablar de los
mutadores, accesores y tres métodos especiales: los
constructores, toString. y equals. También nos
ocuparemos de ma in.
75
3.4 Métodos básicos
HET X
OIntcells Mozitla Firefon
Ele Edt Mici iterre Fcolats ahol Tools iob
~
Package Class Tres Deprecated Index Help
PREV CLASS HEXT cuAss
ERAMES tO FRAME: ALEleras
SUMNARY HESTED|FIELO| GONSTE INFTHOILL
DETAIL FIELDI CONSTR MFTHOD
Class IntCell
Java, lang-ongect
L Intcell
public claas Intcell
axtends java. lang Cbjact
4 class for simulating an integer memory celi
Constructor Summary
Intcell ()
Methodls imherited frorm lass java .lang. Object
clonen equals, finalize, getclass, hasbcode, notiry,
waitn wait
netifyÄll, tostring, Maitn
Constructor Detail
Figura 3.5 La salida de javadloc correspondlente ala Figura 3.4
(salida porclal).
76
Capitulo 3 Objetos y clases
3.4. 1
Constructores
Como hemos mencionado anteriormente, una propiedad básica de
los objetos
Un Construciordice como
sedectarae einicaltza un
es que pueden definirse, añadiendo posiblemente una
inicialización, En Java,
otjeto
el método que controla cómo se crea e inicializa un objeto es el
constructor.
Gracias al mecanismo de sobrecarga, una clase puede definir
múltiples
constructores.
Si no se proporciona ningún constructor, como en el caso de la
clase
El construcior predeler-
minado consiste en uná
IntCell de la Figura 3.1, se genera un constructor predeterminado
que inicia-
apicadon niesnbro a mion-
liza cada dato miembro utilizando los valores predeterminados
normales.
bro de una iniclallzacion
predelerminada,
Esto quiere decir que los campos primitivos se inicializan con cero
y que los
campos de referencia se inicializan con la referencia nu11. (Estos
valores
predeterminados pueden sustituirse mediante la inicialización de
campos
en linea, que se ejecuta antes de ejecutar los cuerpos de los
constructores.) Por tanto, en el caso de
IntCell, el componente storedvalue es 0.
Para escribir un constructor, tenemos que proporcionar un mětodo
que tenga el mismo nombre
que la clase y ningún tipo de retorno (es fundamental que se omita
el tipo de retorno; un error
frecuente es incluir vo1d como tipo de retorno, lo que da como
resultado que se declare un método
que no es un constructor). En la Figura 3.6 hay dos constructores:
uno comienza en la linea 7 y el
otro en la linea 15. Utilizando estos constructores podemos
construir objetos Date de cualquiera de
las formas siguientes:
Date dl = new Date( ):
Date d2 = new Date( 4 . 15. 2010 ):
Observe qque una vez que se ha escrito un constructor, ya no se
genera un constructor
predeterminado con cero parámetros. Si deseamos uno, tenemos
que escribirlo explicitamente. Por
tanto, el constructor de la linea 7 es obligatorio de cara a permitir
la construcción del objeto que d1
hace referencia.
Mutadores y accesores
3.4.2
Generalmente, los campos de una clase se declaran como de tipo
private.
Un mélodo que examina
Por tanto, las rutinas que no pertenezcan a esa clase no pueden
acceder
paro que no modifica el
estado de un otyeto se
directamente a ellos. En ocasiones, sin embargo, nos gustaría
poder examinar
denomina accesar Un
el valor de un campo. Es posible incluso que deseemos
modificarlo.
mtodo quO modifica el
Una alternativa para hacer esto consiste en declarar los campos
como
estado es un mutardtor.
public. Sin embargo, esta elección no suele ser conveniente,
porque viola el
principio de ocultación de la información. En lugar de ello,
podemos propor-
cionar métodos para examinar y modificar cada campo. Un método
que examina pero no modifica el
estado de un objeto se denomina accesor. Un método que modifica
el estado se denomina mutador
(porque hace que mute el estado del objeto).
Algunos casos especiales de accesores y mutadores examinan
únicamente un solo campo.
Estos accesores suelen tener nombres que comienzan con get,
como ge tMonth, mientras que los
correspondientes mutadores suelen tener nombres que comienzan
con set, como setMonth.
77
3.4 Métodos básicos
I/ Clase Date mînima que ilustra algunas caracteristicas Java
1
2
/l No hay comprobaciones de errores ni comen tarios javadoc
3
4
public class Date
5
G
fl Constructor con cero par ámetros
7
public Date(
)
a
9
month = 1:
10
day - 1;
11
year 2010:
12
13
14.
Hl Constructor con tres parámetros
15
public Datec int theMonth, int theDay. int theyear )
16
17
month = theMonth:
18
day - theday:
19
year = theYear:
20
21
22
Nl Devuelve true si 1os valores son iguales
23
public boolean equals( Object rhs
)
24
25
ifc ! t rhs instanceof Date ) )
26
return false:
27
Date rhDate = c
Date ) rhs:
28
return rhDate. month == month 88 rhDate. day =a day a&
29
rhDate -year - year:
30
31
32
// Conversión a String
33
public String tostring(
)
34
return month + "/" + day + " /" + year:
35
36
37
38
/l Campos
39
private int month:
40
private int day:
41
private int year:
42. l
Flgura 3.6 Una clase Date minima que ilustralos constructores Y
los métodos equalsy tostring.
78
Capitulo 3 Objetos y clases
La ventaja de utilizar un mutador es que este puede garantizar que
los cambios efectuados en el
estado del objeto sean coherentes. Por tanto, un mutador que
modifique el campo day de un objeto
Date puede asegurarse de que solo se usen fechas correctas.
Salida de información y el método toString
3.4.3
Normalmente, querremos poder mostrar el estado de un objeto
utilizando
Pucde proporcionarse un
print. Esto se leva a cabo escribiendo el método de clase toString.
Este
método toString. Estc
melodo dewucive un otijeto
método devuelve un objeto String adecuado para la salida. Por
ejemplo, la
String basado en el
Figura 3.6 muestra una implementación básica del método toString
para la
estado del objeto.
clase Date.
3.4.4
equals
El método equals se utiliza para comprobar si dos objetos
representan el
Puede proporcionarse
mismo valor. La signatura es siempre
un mélodo equals
paia comprobes si dos
referencias se refieren al
public boolean equalst Object rhs
mismo valor.
Observe que el parámetro es del tipo de referencia Object en lugar
de ser del
tipo de la clase (en el Capítulo 4 se explica la razón de esto).
Normalmente, el
método equaTs de la clase NombreClase se implementa para
devolver true
solo si rhs es una instancia de Nombrec1 ase, y si después de la
conversión
El paramctro do equals
a Nombreci ase, todos los campos primitivos son iguales (via .--) y
todos los
es dc tipo Object.
campos de referencia son iguales (aplicando miembro a miembro
equa1s).
in la Figura 3.6 se muestra un ejemplo de cómo se implementa
equals
para la clase Date. El operador instanceof se explica en la Sección
3.6.3.
3.4.5
ma in
Cuando se ejecuta el comando java para iniciar el intérprete se
invoca el método ma in del archivo
de clase referenciado por el comando java. Por tanto, cada clase
puede disponer de su propio
método ma in, sin ningún problena. Esto hace que resulte fácil
probar la funcionalidad básica de
las clases individuales. Sin embargo, aunque puede probarse la
funcionalidad, el incluir ma 1n en la
clase proporciona a ma in más visibilidad de la que en general
querriamos permitir. De ese modo,
las llamadas a ma in desde métodos no públicos de la misma clase
se compilarán correctamente, aun
cuando serían ilegales en un entorno más general.
3.5
Ejemplo: utilización de java - ma th. BigInteger
En la Sección 3.3 hemos descrito cómo generar documentación a
partir de una clase y en la Sección
3.4 se han descrito algunos componentes típicos de una clase,
como los constructores, accesores,
mutadores y. en particular, los métodos equals y toString. En esta
sección vamos a mostrar las
partes de la documentación que más habitualmente usan los
programadores.
79
3.5 Ejemplo: utilización de java .math- .Biginteger
La Figura 3.7 muestra una sección de la documentación en línea
para la clase de librería java.
ma th .BigInteger. Falta la sección que proporciona una
panorámica de la clase en algo que se
asemeja al inglés (compare con la Figura 3.5 para ver el preámbulo
que falta). Ese preámbulo nos
dice, entre otras cosas, que los objetos BigInteger son inmutables,
como los objetos String: una
vez que creamos un objeto BigInteger, su valor no puede variar.
JJaN
PEgintege (lava Vlatliorm SEG) tiozlla fLrerek
Ells Eit lien Hishury Edeks Petoal Eoon teD
-
Figura 3.7 Javadoc simplificadopara Java .tma th. Biginteger.
80
Capitulo 3 Objetos y clases
A continuación hay una lista de campos, que en nuestro caso son
las constantes ZERO y. ONE. Si
a continuación hacemos clic en los hipervínculos ZERO o ONE,
obtendremos una descripción más
completa que nos dice que se trata de entidades de tipo public
static final.
La siguiente sección enumera los constructores disponibles. En
este caso hay seis, pero solo
se muestra uno en nuestro listado resumido, y ese constructor
exige un objeto String. De nuevo,
si hacemos clic sobre el nombre del constructor, el hipervinculo
nos lleva a una descripción más
completa que se muestra en la Figura 3.8. Entre otras cosas, venos
que si el objeto String contiene
espacios en blanco extra, el constructor fallara y generará una
excepción. Estos son los tipos de
detalles que siempre merece la pena conocer.
A continuación vemos una serie de métodos (de nuevo, este es un
listado abreviado). Dos
métodos importantes son equals y toString; puesto que se
enumeran especificamente aquí,
podemos estar seguros de que los objetos BigInteger pueden
compararse con total seguridad
utilizando equaTs e imprimirse obteniendo una representación
razonable. También hay un
método compareto y si hacemos clic en el hipervínculo, vemos que
el comportamiento general
de compareTo es idéntico al método compareTo de la clase String.
Esto no es por casualidad,
como veremos en el Capítulo 4. Observe también que examinando
las signaturas y las breves
descripciones podemos ver que métodos como add y multiply
devuelven objetos Bi gInteger
de nueva creación, dejando los originales sin modificar. Esto es por
supuesto obligatorio, ya que
BigInteger es una clase inmutable.
Más adelante en el capítulo utilizaremos la clase BigIn teger como
componente para implementar
nuestra propia clase BigRat ional, una clase con la que
representaremos los números racionales.
3. 6
Constructores adicionales
Tres palabras clave adicionales son this, instanceof y static. La
palabra clave thts tiene
varios usos en Java; veremas dos de ellos en esta sección. La
palabra clave ins tanceof también
tiene varios usos generales; aquí se emplea para garantizar que la
conversión de tipos se realizará
correctamente. Asimismo, static también tiene varios usos. Ya
hernos visto los métodos estáticos.
En esta sección veremos qué es un campo estáticoy un
inicializador estático.
BigInteger
public BigInteger (String val)
Translates the decimal String representatlon of a Biginteger into a
Biginteger. The String
representalion consists af an optional mints sign followed by a
sequence of one ce mote decimal
digits, The character to digit mapping is provided by
Character.d1git. The String may not
ontain any extraneous charactes (whitespace, for example).
Parameters
val - decimal String representation of BigInteger.
Throrvs:
MuuberFormmatExcept ion val Is not a valid representation of
aBiginteger,
See also:
Character. digit(char. int )
Figura 3.8 Detalles diel constructor BigInteger.
81
3.6 Constructores adlcionales
3.6. 1
La referencia this
El primer uso de this es como referencia al objeto actual. Piense
en la
this esuna refcrencta
referencia this como si fuera un dispositivo de orientación que nos
dijera,
al objcto actual. Pucdt
en cualquier momento, dónde nos encontramos. Un uso importante
de
utllzarse para enriar
el objeto actual, como
la referencia this es a la hora de gestionar el caso especial de las
auto-
uną unidad, a algun otro
asignaciones. Un ejemplo de este tipo de uso sería un programa
que copiara
método.
un archivo en otro. Un algoritmo normal comenzaría truncando el
archivo
de destino, para dejarlo con longitud cero. Si no efectuamos
ninguna
omprobación para cerciorarnos de que los archivos de origen y de
destino
La ullzaclon de alls es
un caso espedal que se
sean diferentes, entonces el archivo de origen podría llegar a ser
truncado, lo
presenta cuando cl mismo
que no suele ser lo que deseamos. A la hora de manejar dos
objetos, en uno
objeto aparece cumpiendo
de ellos escribimos y en el otro leemos, primero debemos
comprobar este
muis de uinla lunción.
caso especial, que se conoce con el nombre de utilización de alias
Para el segundo ejemplo, suponga que tenemos una clase Account
con un
método finalTransfer. Este método transfiere todo el dinero de una
cuenta a otra, En principio, se
trata de una rutina bastante fácil de escribir:
// Transferir toda el dinero de rhs a 1a cuenta actual
)
public vofd fina . 1Transfer( Account rhs
dollars + rhs .dollars:
rhs .dollars = 0:
Sin embargo, considere el resultado:
Account accountl:
Account account2:
..
account2 = account1:
accountl .final ITransfer( account2 ):
Puesto que estamos transfiriendo dinero de una cuenta a sí
misma, no deberia haber ningún cambio
en el saldo. Sin embargo, la última instrucción de finalTransfer
garantiza que la cuenta quedará
vacía. Una forma de evitar esto, consiste en utilizar una
comprobación de alias:
/l Transferir toda el dinero de rhs a Ia cuenta actual
public void fina ITransfer( Account rhs )
ifl this ~-" rhs ) Il Comprobación de alias
return:
dollars + rhs .dollars:
rhs. dollars - 0:
82
Capitulo 3 Objetos y clases
La abreviatura this para constructores
3.6.2
Muchas clases tienen varios constructores que se comportan de
forma
this puede ulilzarse para
hacer una Hanadaz I otro
similar. Podemos utilizar this dentro de un constructor para invocar
a uno
constructor de Ia misma
de los otros constructores de la clase. Una alternativa al
constructor de cero
case
parámetros de Date de la Figura 3.6 sería
public Datec )
l
this( 1, 1. 2010 ): I Invocar el constructor de 3 par ámetros
}
También son posibles otros USos más complicados, pero la
llamada a this puede ser la primera
instrucción del constructor; después de ella se pueden incluir
otras instrucciones.
El operador instanceof
3.6.3
El operador instanceof realiza una comprobación en tiempo de
ejecución,
El operacor instanceof
se LfiEza para comprobar
El resultado de
si una expresión es una
instancia de aiguna dase
exp instanceof NombreClase
delcrminadia.
será true si exp es una înstancia de Nombreci ase y false en caso
contrario.
Si exp es nu11, el resultado será siempre false. El operador ins
tanceof
suele utilizarse, típicamente, antes de realizar una conversión de
tipos y será true si la conversión
de tipos puede realizarse correctamente,
3.6.4
Miembros de instancia y miembros estáticos
Los campos y métodos declarados con la palabra clave static son
miembros
Los miembros de
instancia 5on campos o
estáticos Si se los declara sin la palabra clave static, decimos que
son
métodos declarados sin el
miembros de instancia. En la siguiente subsección se explica la
diferencia
modificador static.
entre miembros de instancia y miembros estáticos,
Campos y métodos estáticos
3.6.5
Un método estático es un método que no necesita un objeto que lo
controle,
Uin método eslatico es un
método que no necesta un
y que por tanto se suele llamar proporcionanxlo un nombre de
clase en lugar
otjelo que lo controle.
del nombre del objeto controlador. El método estático más común
es ma in.
Puede encontrar otras métodos estâticos en las clases Integer y
Math. Como
ejemplos tendriamos los métodos Integer. parseInt, Math .sin y
Math .max.
EI accesa a un método estático utiliza las mismas reglas de
visibilidad que los campos estáticos,
Estos métodos se asemejan a las funciones globales que podemos
encontrar en los lenguajes no
orientados a objetos.
Los campos estáticos se utilizan cuando tenemos una variable que
tiene que ser compartida por
todos los miembros de una cierta clase. Normalmente, se tratará
de una constante simbólica, pero
83
3.6 Constructores adlcionales
tampoco es obligatorio que sea así. Cuando una variable de clase
se declara
Los campos estálicas son
esencialmente vartabis
como static, solo se creará una instancia de esa variable. No
forma parte de
globale s cuyO âmbllo es a
ninguna instancia de la clase. En lugar de ello, se comporta como
una única
de una dase.
variable global, pero cuyo ámbito es el de la clase. En otras
palabras, en la
declaración
public class Sample
l
private int x:
private static int y:
cada objeto Samp le almacena su propia x, pero solo existe una y
compartida.
Un uSo común de un campo estático es como constante. Por
ejemplo, la clase Integer define el
campo MAX LVALUE como
public static fina1 int MAX_VALUE - 2147483647 :
Si esta constante no fuera un campa estático, entonces cada
instancia de Integer tendría un campo
de datos denominado MAX _VALUE, desperdiciando así espacio y
tiempo de inicialización. En lugar
de ello, solo hay una única variable denominada MAX VALUE,
Cualquiera de los métodos de Integer
puede acceder a esa variable utilizando el identificador MAX
_VALUE. También se puede acceder a
ella a través de un objeto obj de tipo Integer utilizando obj . MAX_
_VALUE, como con cualquier otro
campo. Observe que esto se permite únicamente porque MAX
_VALUE es pública. Por ültimo, tambiến
se puede acceder a MAX _VALUE utilizando el nombre de la clase
como en Integer. .MAX VALUE (lo
que de nuevo está permitido debido a que es un campo público).
Esto no se permitiría para un
campo no estático. Esta última forma es la preferible, porque
comunica al lector que el campo es, de
hecho, de tipo estático. Otro ejemplo de campo estático seria la
constante Math- .PI.
Incluso sin el cualificador final, los campos estáticos siguen
siendo útiles. La Figura 3.9 ilustra
un ejemplo típico. Aquí lo que queremos es construir objetos
Ticket, dando a cada ticket un número
de serie distintívo. Para poder hacer esto, tenemos que disponer
de alguna forma de llevar la cuenta
de todos los números de serie utilizados previamente; esto es
evidentemente un dato compartido y
que no forma parte de ningún objeto Ticket concreto.
Cada objeto Ticket tendrá su miembro de Instancia serial Number;
esto
un campo estaticoes
es un dato de instancia, porque cada instancia de Ticket tiene su
propio
compartido por todas las
Instancias (posblemente
campo serial Number. Todos los objetos Ticket compartirán la
variable
Oo) de la dase.
ticketCount, que indica el número de objetos Ticket que se han
creado.
Esta variable forma parte de la clase, en lugar de ser específica de
un objeto,
por lo que se la declara como de tipo static. Solo hay un ti
cketCount,
independientemente de que haya un objeto Ticket, 10 objetos
Ticket o incluso ningún objeto
Ticket. Este último punto -el de que los datos estáticos existen
incluso antes de crear ninguna
instancia de la clase- es importante, porque significa que los datos
estáticos no pueden inicializarse
en los constructores. Una forma de realizar la inicialización es en
línea, en el momento de declarar el
campo. En la Sección 3.6.6 se describe un método de inicialización
más complejo.
En la Figura 3.9, ahora podemos ver que la construcción de los
objetos Ticket se realiza
utilizando ti cketCount como número de serie e incrementando
ticketCount. También propor-
cionamos un método estático, getTicketCount, que devuelve el
número de tickets, Puesto que
84
Capitulo 3 Objetos y clases
class Ticket
1
{
2
public Ticket( )
3
4
5
System .out ·printin( "Calling constructor" ):
6
serial Number = +tticketCount;
7
)
8
public int get Serial( )
9
10
11
return seria lNumber:
12
13
14
public String toString(
)
15
return "Ticket #" + getserial( ):
16
17
1
18
)
19
public static int getTicketCount(
20
21
return ticketCount:
22
23
24.
private int serial Number:
25
private static int ticketCount a 0:
26 l
27
28 class TestTicket
29 {
30
public static void mainc String L 1 args
)
31
32
Ticket tl:
33
Ticket t2:
34
35
System .out- ·println( "Ticket count is *. +
36
Ticket -getTicketCount( ) ):
t1 - new Ticket( ):
37
t2 - new Ticket( ):
38
39
40
System .out -println( "Ticket count is *. +
41
Ticket .getTicketCount( ) ):
42
13
System .out -printin( tl. getSerial( ) ):
44
System .out ·println( t2. getser ial ) ):
45
46
Figura 3.9 Laciase Ticket: un ejemplo decampos y rmétodos
estáticos,
85
3.6 Constructores adlcionales
es estático, se puede invocar sin proporcionar una referencia a
objeto, como se muestra en las
lineas 36 y 41. La ]lamada de la linea 41 podría haberse realizado
utilizando tl1 o t2, aunque
muchos afirman que invocar un método estático utilizando una
referencia a objeto es un estilo
de programación inadecuado, por lo que nosotros no utilizaremos
esa técnica en este texto. Sin
embargo, es significativo que la llamada de la línea 36 no podría,
claramente, realizarse a través
de una referencia a objeto, porque en ese punto no existen todavía
objetos Ticket válidas. Esta
es la razón por la que es importante que getTi cketCount se
declare como método estático: si se
declarara como un método de instancia, solo podría invocarse a
través de una referencia a objeto.
Cuando se declara un método como estático, no existe referencía
this implicita. Por ello,
no puede acceder a datos de instancia 0 invocar métodos de
instancia,
sin proporcionar una referencia a objeto. En otras palabras, desde
dentro
Un método staticno
iene referencia th1s
de getTicketCount, el acceso no cualificado a serial Number
implicaría
mplicta, Y puede inwvocarse
this .seria INumber, pero como no existe thts, el compilador
generará un
sin una relerencs a objoto.
mensaje de error. Por tanto, un método de clase estático solo
podrá acceder
a un campo no estático, que forma parte de cada instancia de la
clase, si se
proporciona un objeto controlador.
3.6.6
Inicializador es estáticos
Los campos estáticos se inicializan en el momento de cargar la
clase. Ocasionalmente, necesitaremos
un mecanismo de inicialización complejo. Por ejemplo, suponga
que necesitamos una matriz estática
que almacene la raiz cuadrada de los 100 primeros números
enteros, Lo mejor sería hacer que esos
valores se calcularan automáticamente. Una posibilidad consiste
en proporcionar un método estático
y obligar al programador a invocarlo antes de utilizar la matriz.
Otra alternativa es el inicializador estático. En la Figura 3.10 se
muestra
Un inicializedor eslallcoes
un bioquo de código que
un ejemplo. All, el inicializador estático abarca de las líneas 5 a 9.
El uSo
se utilbza para iniclaiitzar
más simple del inicializador estático coloca el código de
inicialización para
campos estaticos
los campos estáticos en un bloque precedido por la palabra clave
static. El
inicializador estático debe seguir a la declaración dei miembro
estático.
public class Squares
1
2
3
private static double L J squareroots - new doublel 100 1:
4
5
static
6
7
fort int 1 = 0: i < squareRoots .Tength: i++ )
B
squareRootsl i 1 - Math .sqrt( C double ) i ):
9
/ Resto de la clase
10
11 |
Figura 3.10 Un ejemplo die inidalizador estatico.
86
Capitulo 3 Objetos y clases
3.7
Ejemplo: implementación de
una clase BigRati ona1
En esta sección, vamos a escribir una clase que ilustra muchos de
los conceptos que hemos descrito
en el capítulo, incluyendo:
La clase que vamos a escribir representará números racionales.
Un número racional alrmacena un
numerador y un denominador y utilizaremos objetos BigInteger
para representar el numerador y el
denominador, Por ello, nuestra clase se llamará de forma bastante
lógica Bi igRat 1onal.
La Figura 3.11 muestra la clase B1 gRational. El código en linea
está completamente comentado.
Aquí, hermos omitido los comentarios, para que el código pueda
caber en las páginas de texto,
Las lineas 5 y 6 son constantes BigRati onal .ZERO y BigRational.
ONE. También vemos que
la representación de los datos se hace mediante dos objetos
BigInteger, num y den, y nuestro
código se implementará de una forma que garantice que el
denominador nunca sea negativo.
Proporcionamos cuatro constructores, y dos de ellos se
implementan utilizando la palabra clave
ths. Los otros dos constructores tienen implementaciones más
complicadas, mostradas en la Figura
3.12. Aquí podemos ver que el constructor de dos parámetros Bi
gRati onal inicializa el numerador
Yy el denominador de la forma especificada, pero luego debe
garantizar que el denominador no sea
negativo (lo que lo hace invocando el método privado fixSt gns) y
luego eliminando los factores
comunes (invocando el método privado reduce), También
proporcionamos una comprobación para
garantizar que se no acepte 0/0 como un BigRat iona1, y esto se
hace en check00 que generará una
excepción si se intenta construir ese tipo de objeto como BigRati
onal. Los detalles de check00,
fixSigns y reduce son menos importantes que el hecho de que su
uso en un constructor y en otros
métodos permita al diseñador de la clase garantizar que los
objetos siempre se configuren en estados
válidos.
La clase BigRational también incluye métodos para devolver
valores absolutos y un negativo.
Se trata de métodos simples, que se muestran en las lineas 24 a
27 de la Figura 3.11. Observe que
estos métodos devuelven nuevos objetos BigRational, dejando el
original intacto.
y
add, subtract, multiply y divide se muestran en las lineas 29 a 36
de la Figura 3.11
están implementados en la Figura 3.13. Los aspectos matemáticos
son menos interesantes que el
concepto fundamental de que, debido a que cada una de las cuatro
rutinas termina creando un nuevo
BigRat ional, y el constructor de BigRa tional tiene llamadas a
check00, fixsigns y reduce, las
respuestas resultantes están siempre en la forma simplificada
correcta, y cualquier intento de dividir
cero entre cero será capturado por check00.
87
3.7 Implementación de una clase Bi gRat f ional
1
import java. ma th .BigInteger:
2
3
public class BigRationa1
4
{
5
public static final BigRational ZERO -- new BigRational( ):
6
public static final BigRational ONE = new BigRationaT( "1" );
7
8
public BigRational( )
9
this( BigInteger .ZERO ):
10
public BigRational( Biginteger n
)
11
[
this( n, BigInteger .ONE ): 1
12
)
public BigRational( BigInteger n , BigInteger d
t
13
/* Img lementac ión en 1a Figura 3.12 */ )
14
)
public BigRational( String str
[
15
/* Imp lementac ión en 1a Figura 3.12 */ )
16
17
private void check00( )
18
/* Implementación en 1a Figura 3.12 */ l
19
private void fixsigns( )
20
l
/* Implementación en 1a Figura 3.12 */ 1
21
private vofd reduce ( )
22
[ /* Implementación en Ta Figura 3.12 */ }
23
24
public BigRational abs( )
25
l
return new BigRational( num .abs( ). den ):
[
26
public Bigkational negate(
)
(
27
return new BigRationa1( n um negate( ). den );
28
29
public BigRational add( BigRational other )
(
30
/* Imp - lementación en 1a Figura 3.13 */
}
31
public BigRational subtract( BigRational other
)
l
32
/* Implementación en 1a Figura 3.13 */ }
33
public BigRational multiply( BigRationa1 other
)
34
( /* Implementación en 1a Figura 3.13 * f )
35
public BigRational dividel BigRational other )
l
36
/* Imp lementaci ión en 1a Figura 3.13 */ }
37
38
public boolean equals( Object other )
39
(
/* Imp lementaci ión en la Figura 3.14 */
}
40
public String tostring( )
41
/* Imp 1ementación en 1a Figura 3.14 */
(
)
42
43
private BigInteger num; H solo esto puede ser negativo
44.
private BigInteger den: Hl nunca negativo
45 )
Figura 3.11 Laclase BigRati ona 1, conuna implementactón parcial.
88
Capitulo 3 Objetos y clases
1
public BigRational( BigInteger n, BigInteger d
)
2
num = n: den = d:
3
checkaot ): fixSigns( ): reduce ( ):
4
5
6
public BigRational( String str
)
7
8
)
if( str. length( ) - 0
9
throw new Illega 1Argument Exception( "Zero length string" ):
10
11
/l Buscar */'
12
int slashl ndex = str indexOf( *,' );
13
if( slashIndex - -1
14
)
{
15
16
num -" new BigInteger( str .trim( ) ):
17
den - BigInteger .ONE : // No hay denominador. ..
usar 1
18
}
19
else
20
[
num = new BigInteger( str .substring( 0. slashIndex ).trim( ) ):
21
den = new BigInteger ( str .substring( slashIndex + 1 ).trimt ) ):
22
23
checkoot ): fixSigns( ): reduce( ):
24
25
26
27
private vofd check00( )
28
if( num. equa 1s( BigInteger . ZERO ) && den. equals( BigInteger.
ZERO
29
)
)
30
throw new ArithmeticException( "ZERO DIVIDE BY ZERO" ):
)
31
32
33
)
private void fixSigns(
34
if( den. compareTo( Biginteger -ZERO ) < 0
)
35
I
36
37
num -" num. negate( :
38
den - den. negate( 2:
39
}
1
40
41
42
private void reducel
)
43
44
BigInteger gcd - num gcdl den ):
45
nun = num. divide( gcd ):
46
den = den. divide( gcd ):
)
47
Figura 3.12 Los constructores de BigRatloral ymetodos check00,
fixSignsy reduce.
89
3.7 Implementación de una clase Bi gRat ional
Finalmente, equalsy tostring están implementados en la Figura
3.14. La signatura de equals,
como hemos explicado anteriormente, requiere un parámetro de
tipo Object. Después de una
comprobación instanceof estándar y una conversión de tipo
podemos comparar los numeradores
y
y denominadores. Observe que utilizamos equals (no -) para
comparar los numeradores
denominadores, y observe también que, como los objetos Bi gRat
ional están siempre en forma
simplificada, la prueba es relativamente simple, Para toString, que
devuelve una representación
String del objeto Bi gRat ional, la implementación podría ser una
sola linea, pero hemos añadido
oódigo para manejar los valores infinito y =infinito, así como para
no proporcionar como salida el
denominador en caso de que tenga el valor 1.
1
)
public BigRational add( BigRational other
2
3
BigInteger newNumerator =
4
num. multiply( other. den ).add(
5
other. num. multiply( den ) ):
6
BigInteger newDenomi nator = den .mul tiply( other: den ):
7
B
return new BigRational( newNumerator. nen ıDenominator ):
9
10
11
public Bigkational subtract( BigRational other )
12
13
return add( other. negate( ) ):
14
15
16
public BigRational multiply( BigRationa1 other
)
17
18
BigInteger newNumer = num .multiply( other - num ):
19
BigInteger newDenom - den -multiply( other den ):
20
21
return new BigRational( newNumer . newDenom )
22.
23
24
public BigRational divide( BigRational other )
25
26
BigInteger newNumer = num -multiply( other. den ):
27
BigInteger newDenom = den -multiply( other . num ):
28
29
return new BigRationa]( newNumer , newDenom ):
30
Figura 3.13 Metodos add. substract, multiplyydivide de
B1gRattonal.
90
Capitulo 3 Objetos y clases
1
public boolean equals( Object other )
2
3
)
if( I ( other Instanceof BigRational )
4
return false:
5
6
BigRat ional rhs = (BigRationa1) other:
7
8
return num .equals( rhs. num ) && den .equals( rhs. den ):
9
10
11
public String toString(
)
12
13
if( den. equa 1s( BigInteger .ZERO ) )
14
ifl num. compareTo( Biginteger .ZERO } < 0 )
15
return *-infinity":
16
else
17
return "infinity";
18
19
)
if( den. equa 1s( BigInteger. ONE )
20
return num tostring( ):
21
else
22
return num + * " + den:
23
Figura 3.14 Metodos equalsy toString de B1 gRatfonal.
Observe que la clase Bi gRational no tiene métodos mutadores:
rutinas como add simplemente
devuelven un nuevo BigRationa1 que representa una suma. Por
tanto, Bi gRational es un tipo
inmutable.
3.8
Paquetes
Los paquetes se utilizan para organizar clases similares, Cada
paquete está
Un paquete se utilza pera
compuesto por un conjunto de clases. Dos clases contenidas en el
mismo
organizar una colcccion de
dases
paquete tienen restricciones de visibilidad ligeramente menores
entre ellas
mismas que las que tendrían si se encontraran en paquetes
distintos.
Java proporciona varios paquetes predefinidos, incluyendo java. io,
java .lang y java. util. El paquete java .Tang incluye, entre otras, las
clases Integer, Math,
String y System. Algunas de las clases del paquete java- .util son
are Da te, Random y Scanner. El
paquete java- .1o se utiliza para la EIS e incluye las diversas
clases de flujos de datos vistos en la
Sección 2.6.
La clase C del paquete pse especifica como p. C. Por ejemplo,
podemos construir un objeto Date
con la fecha y hora actuales como estado inicial utilizando
91
3.8 Paquetes
java .util. Date today = new java .util. Date( ):
Observe que al incluir un nombre de paquete, evitamos conflictos
con otras
Por cormenio, ios nombres
clases de nombre idéntico contenidas en otros paquetes (como
por ejemplo
de las chses empieza por
mayuscula y las nombres
nuestra propia clase Date). Observe también el convenio de
denominación
de paquetes no.
típico: los nombres de las clases empiezan por mayúscula y los
nombres de
paquete no.
3.8.1
La directiva import
Utilizar un nombre de paquete y de clase completo puede ser
engorroso. Para
La directiva Import se
utitzn pard proporcionar
evitarlo, utilice la directiva import. Hay dos formas de la directiva
import
una abrreriatura para
que permiten al programador especificar una clase sin utilizar
como prefijo el
un nombre de clase
nombre del paquete.
compietamenic cualficado.
import nombrep aquete Nombrec1 ase;
import nombr eP aquete .*:
En la primera forma, NombreClase puede utilizarse como
abreviatura en lugar del nombre de clase
ompletamente cualificado. En la segunda forma, todas las clases
de un paquete pueden abreviarse
mediante el nombre de clase correspondiente. Por ejemplo, con las
directivas import
import java= util .Date:
import java. io.* :
podemos usar
Date today = new Datel ):
FileReader thefile = new FileReader( name ):
La utilización de la directiva import nos ahorra esfuerzo de tecleo.
Eluso desculdado Oe la
Puesto que el mayor ahorro se consigue aplicando la segunda
forma, podrá
dircctiva Import puede
Introdudr corfiictos de
comprobar que esa forma se emplea a menudo en muchos
programas, Las
denominacion.
directivas import tienen dos desventajas. En primer lugar, la
abreviatura
hace que resulte dificil, leyendo el código, determinar qué clase es
la que
está siendo utilizada cuando existen múltiples directivas import.
Además,
la segunda forma puede permitir que se usen abreviaturas para
clases que no deseábamos
e
introducir conflictos de denominación que tendremos que resolver
empleando nombres de clase
completamente cualificados.
Suponga que utilizamos
import javas .ut il.*: H/ Paquete de librerfa
import weiss .util. *; Il Paquete definido por el usuario
con la Intención de importar la clase java. ·util .Random y un
paquete que hemos escrito nosotros
mismos. Entonces, si tenemos nuestra propia clase Random en
weiss .util, la directiva import
generará un conflicto con weiss .util. Random y tendremos que
cualificar completamente el
nombre de la clase. Además, si estamos utilizando una clase
perteneciente a uno de estos paquetes,
92
Capitulo 3 Objetos y clases
al leer el código no se podrá determinar si esa clase proviene del
paquete de librería o de nuestro
propio paquete. Podriamos haber evitado estos problemas si
hubiéramos utilizado la forma
import java- .util .Random:
y por esta razón solo vamos a utilizar esa primera forma en este
texto, evitando las directivas import
mn caracteres "comodín".
Las directivas import deben aparecer antes del comienzo de la
declaración
de clase. Hemos visto un ejemplo de esto en la Figura 2.19.
Asimismo, todo
Java. Teng.* se importa
de manera aulomática.
el paquete java .Tang se importa de manera automática, Esta es la
razón por
la que podemos utilizar abreviaturas como Math. max, Integer -
parseInt,
System. .out, etc.
En la versiones de Java anteriores a Java 5, los miembros
estáticos como Math. max e Integer.
MAX VALUE no podian abreviarse para utilizar simplemente ma x y
MAX VALUE. Los programadores
que hacían un uSO intensivo de la librería matemática habían
estado esperando mucho tiempo
a
que se generalizara la directiva de importación y se permitiría
utilizar métodos como sin, cos,
tan en lugar de los nombres más largos Mat th .sin, Math .coS,
Math. tan. En Java 5, se ha añadido
esta característica al lenguaje a través de la directiva de
importación estática, La directiva de
importación estática permite acceder a las miembros estáticos
(métodos y campos) y proporcionar
explícitamente el nombre de la clase. La directiva de importación
estática tiene dos formas: la de
importación de un único miembro y la de importación comodín.
Así,
import static java. lang .Math. *:
import static java .lang. Integer .MAX VALUE ;
permite al programador escribir max en lugar de Math .ma x, PI en
lugar de [Link] y MAX_VALUE en
lugar de Integer. MAX _VALUE.
La instrucción package
3.8.2
Para indicar que una clase forma parte de un paquete, debemos
hacer dos
La instrucdion package
cosas. En primer lugar, debemos incluir la instrucción package en
la primera
Indica quC una clase es
parte de un paquele. Debe
línea, antes de la definición de la clase. En segundo lugar,
debemos colocar el
preceder ala definición de
código en un subdirectorio apropiado.
la dase.
En este texto vamos a utilizar los dos paquetes utilizados en la
Figura
3.15. Otros programas, incluyendo los programas de prueba y los
programas
de aplicación de la Parte Tres del libro, son clases autónomas que
no forman
parte de un paquete.
Figura 3,15 Paquetes delinidos en este texto.
93
3.8 Paquetes
package weiss. math:
1
2
3
Import java. ma th -BigInteger:
4
5
public class BigRational
6
7
/* La clase completa se muestra en el código en lînea*/
B
l
Figura 3.16 Inclusión de la clase B1gRational enel paquele weiss
math.
En la Figura 3.16 se muestra un ejemplo de cómo se utiliza la
instrucción package, en este caso
para incluir la clase Bi gRational en un nuevo paquete weiss .ma
th.
3.8.3
La variable de entorno CLASSPATH
Los paquetes se buscan en las ubicaciones designadas en la
variable
La variable CLASSPATH
CLASSPATH. ¿Qué significa esto? He aquí algunas posibles
configuraciones
especifka los archiosy
diărectorios qque hay que
de CLASSPATH, primero para un sistema Windows y en segundo
lugar para un
eplorar pera encontrar las
sistema Unix:
cases
SET CLASSPATH= :C: \bookcode\
setenv CLASSPATH :SHOME/ bookcade/
En ambos casos, la variable CLASSPATH enumera directorios (o
archivos jar") que contienen los
archivos de clase del paquete. Por ejemplo, si la variable
CLASSPATH está corrompida, no se podrá
ejecutar ni siquiera el programa más trivial, porque no podrá
encontrarse el directorio actual.
Una clase en un paquete P debe estar en un directorio p que pueda
ser
encontrado buscando en la lista de CLASSPATH; cada en el
nombre del
Una clase en un paquete
paquete representa un subdirectorio, A partir de Java 1.2, el
directorio actual
p dube estar en un
directorlo p que pueda ser
(directorio .) se explora siempre si CLASSPATH no está
configurada, así que
encontirado buscondo en la
si estamos trabajando desde un único directorio principal, basta
simplemente
sta de CLASSPATH.
con crear subdirectorias en él y no configurar CLASSPATH. Lo más
probable,
sin embargo, es que queramos crear un subdirectorio Java
separado y luego
crear subdirectorios de paquete dentro de él. Entonces lo que
hariamos sería ampliar la variable
CLASSPATH para incluir :y el subdirectorio Java. Esto es lo que
haciamos en la anterior declaración
Unix al anadir $HOME/bool kcode/ a la variable CLASSPATH. Dentro
del directorio bookcode,
crearemos un subdirectorio llamado welss, y dentro de él otros
subdirectorios denominados ma th,
util y nonstandard. En el subdirectorio math, incluiremos el código
para la clase BigRational.
Una aplicación, escrita en cualquier directorio, podrá entonces
utilizar la clase Bi gRational bien
con su nombre completo
Un archivo jatr es básicamente un archivo cornprimido (como un
archivo zip). con archivos ulicionales que contienen Informeción
especifica de Java. L.a herramienta jars suministrada con el JDK,
puede utilizarse para crear y expandir archivas jar.
94
Capitulo 3 Objetos y clases
weiss. ma th -BigRational:
osimplemente utilizando BigRationa1, si se proporciona una
directiva import apropiada.
Mover una clase de un paquete a otro puede ser tedioso, porque
puede requerir revisar
una secuencia de directivas import. Muchas herramientas de
desarrollo realizan esta tarea
automáticamente como una de las opciones del proceso de
refactorización.
3.8.4
Reglas de visibilidad de paquete
Los paquetes tienen varias reglas importante de visibilidad. En
primer
Los campos sin
modificadores de visiblidad
lugar, si no se especifica ningún modificador de visibilidad para un
campo,
tienen visibfidad de
entonces el campo tendrá visibilidad de paquete. Esto quiere decir
que será
paqueNe, lo que quiore Úecr
visible únicamente para las restantes clases del mismo paquete.
Esto significa
que sclo son vlsibles para
otras dases que están en el
una mayor visibilidad que con private (que es invisible incluso para
otras
mismo paqucte.
clases del mismo paquete), pero una menor visibilidad que con
public (que
es visible también para las clases que no son de ese paquete).
En segundo lugar, fuera del paquete solo se pueden utilizar las
clases püblicas de ese paquete.
Esa es la razón por la que a menudo hemos utilizado el calificador
public antes de class. Las
clases no pueden declararse como private. El acceso con
visibilidad de paquete también se
extiende a las clases. Si una clase no se declara public, entonces
solo podrá
Las clases no pubicas
ser accedida por otras clases del mismo paquete; será entonces
una clase
solo son Msibles para otras
con visibilidad de paquete. En la Parte Cuatro, veremos que las
clases con
dasos que se encucntren
visibilidad de paquete pueden utilizarse sin violar el principio de
ocultación
en el mismo paqueic
de la información. Por tanto, existen algunos casos en los que las
clases con
visibilidad de paquete pueden resultar muy útiles.
Todas las clases que no forman parte de un paquete pero que son
alcanzables a través de la
variable CLASSPATH se consideran parte del mismo paquete
predeterminado. Como resultado,
la visibilidad de paquete se aplica entre todas ellas. Esta es la
razón por la que la visibilidad no
se ve afectada si se omite el modificador public en las clases que
no pertenezcan a un paquete.
Sin embargo, este es un uso poco conveniente del acceso a
miembros con visibilidad de paquete.
Solamente lo empleamos para colocar varias clases en un mismo
archivo, porque eso hace que el
examen y la impresión de los ejemplos sean más sencillos. Puesto
que una clase public debe estar
en un archivo con el mismo nombre, solo puede haber una clase
pública por cada archivo.
3. 9
Un patrón de diseño: compuesto (par)
Aunque el diseño y la programación de software representan a
menudo un enorme desafio,
muchos ingenieros de software experimentados argumentan que
la ingeniería del software solo
trata, en realidad, con un conjunto relativamente pequeno de
problemas básicos, Quizá esto sea
una exageración, pero es cierto que muchos problemas básicos se
presentan una y otra vez en
los proyectos software. Los ingenieros software que están
familiarizados con estos problemas
a
Esto se aplica a las clases de alto nivel que hemas vIstO hasta
ahora. Posteriormente, veremos las clases anidadase Internas, que
si que
pueden declararse como private.
95
Resumen
y. en particular, con los esfuerzos que otros programadores han
hecho a la hora de resolver esos
problemas, tienen la ventaja de no tener que "reinventar la rueda" .
La idea de los patrones de diseño consiste en documentar un
problema
In patrán de diseñg
y su solución de modo que otros puedan aprovecharse de la
experiencia
desaibe un prrcblema que
sc presenta uns y otra
colectiva de toda la comunidad del ingeniería del software.
Escribir un patrón
wez en la ingenlerla del
se parece bastante a escribir una receta de un libro de cocina; se
han descrito
soltwuare, yluego descrihe
muchos patrones comunes y, en lugar de invertir energía en
reinventar la
la solucidn de una forma b
sulicientermente genérica
rueda, pueden utilizarse esos patrones para escribir mejores
programas.
cumo pera poder apicarla
Por tanto, un patrón de diseno describe un problema que se
presenta una y
en uns ampla variedad de
otra vez en la ingeniería del software y luego describe la solución
de una
cocxtos
manera lo suficientemente genérica como para poder aplicarla en
una amplia
variedad de contextos.
A lo largo del libro analizaremos diversos problemas que surgen a
menudo dentro de un diseño,
junto con una solución típica utilizada para resolver dichos
problemas. Vamos a comenzar con el
siguiente problema simple.
En la mayoría de los lenguajes, una función solo puede devolver un
único
Un patrón de diseño comin
objeto. ¿Quế podemos hacer si necesitamos devolver dos o más
cosas? La
consiste en devolver dors
objelos Como un Pev
forma más fácil de conseguir esto consiste en combinar los
objetos en un
único objeto, utilizando una matriz o una clase. La Situación más
común en la
que hace falta devolver múltiples objetos es el caso de dos
objetos. Por tanto,
un patrón de diseño común consiste en devolver los dos objetos
como un par. Este es el patrón
compuesto.
Ädemás de la situación que acabamos de describir, los pares son
útiles para implementar mapas
y diccionarios. En estas dos abstracciones, lo que hacemos es
mantener parejas clave-valor: los
pares se añaden al mapa o diccionario y luego buscamos una
clave, obteniendo como resultado su
valor. Una forma común de implementar un mapa consiste en
implementar
Los pares son uțies para
un conjunto. En un conjunto, tenernos una colección de elementos
y lo
implcmontar parejas
que hacemos es buscar correspondencias. Si esos elementos son
parejas y
ckeve- valor cn mapos y
el criterio de búsqueda de correspondencias se basa
exclusivanente en la
diccionarios.
componente de clave de la pareja, entonces es sencillo escribir
una clase que
construya un mapa basándose en un conjunto. Veremos esta idea
con más
detalle en el Capítulo 19.
Resumen
En este capítulo hemos descrito las estructuras sintácticas de
Java para clases y paquetes. La clase es
el mecanismo Java utilizado para crear nuevos tipos de referencia;
el paquete se utiliza para agrupar
clases relacionadas. Para cada clase, podemos
96
Capitulo 3 Objetos y clases
La clase consta de dos partes: la especificación y, la
implementación. La especificación
le dice al usuario de la clase lo que esa clase hace; la
implementación se encarga de hacerlo.
La implementación suele contener código confidencial y en
algunos casos solo se distribuye
como un archivo -class. Sin embargo, ja especificación es de
conocimiento público. En Java,
puede generarse a partir de la implementación que enumere los
métodos de la clase utilizando
javadoc.
La ocultación de la información puede conseguirse utilizando la
palabra clave private. La
inicialización de los objetos está controlada por los constructores,
y los componentes de cada objeto
pueden examinarse o modificarse mediante los métodos
accesores y mutadores, respectivamente.
La Figura 3.17 ilustra muchos de estos conceptos, tal como se
aplican a una versión simplificada
de Arraylist. Esta clase, StringArraylist, soporta add, gety size. En
el código en linea podrá
encontrar una versión más completa que incluye set, remove y
clear.
Las características expuestas en este capítulo implementan los
aspectos fundamentales de
la programación basada en objetos. En el siguiente capítulo
hablaremos de la herencia, que es
fundamental para la programación orientada a objetos.
1
/At
2.
* StringArraylist implementa una ma triz ampliable de objetos
String.
3
* Las inserciones siempre se hacen a1 final.
4
*/
5
public class StringArraylist
6
{
7
/**
8
9
10
*/
11
publ ic int sizet )
12
13
return thesize:
14
15
16
/**
17
18
19
20
*/
21
public String gett int idx )
22
23
if( idx < 0 I| idx > sizet ) )
24
throw new ArrayIndex0utOfBoundsExcept ion( ):
25
return theItemsl idx 1:
26
1
Continxia
Figura 3.17 Stringarraylist simplificadocon add, gety sfze.
97
Conceptos clave
27
/**
28
29
* Afade un elemento al final de esta colección.
*
30
@param x cualquier objeto.
*
31
@return true (como con java. util .Arraylist).
*/
32
33
public boolean add( String x )
34
ifc theltems .length -= sIze( )
)
35
r
36
37
String L 1 old - theItes:
theltems -" new Stringl theItems .length * 2 1 1 1:
38
)
fort int i ie 0: i < sizel ): 1++
39
40
theItemsl 1 J - oldl i 1:
)
41
42
43
theItems[ thesize++ J = x:
44
return true;
45
46
private static final int INIT_CAPACITY = 10:
47
48
private int thesize = :
49
private String L 1 theltems = new String[ INIT_CAPACITY 1;
50
51 |
Figura 3.17 (Continación).
Conceptos clave
aCOeSO con visibilidad de paquete Los miembros que no tienen
modificadores de
visibilidad solo son accesibles para los métodos de las clases
pertenecientes al mismo
paquete. (94)
aroesor Un método que examina un objeto pero no cambia su
estado,. (76)
alias Un caso especial que se produce cuando el mismo objeto
aparece desempeñando más
de un papel. (81)
campo Un miembro de una clase que almacena datos. (73)
canmpo estitico Un campo compartido por todas las instancias de
una clase. (83)
dase Está compuesta por campos y métodos que se aplican a
instancias de la clase. (71)
dase con visibilidad de paquete Una clase que no es pública y solo
es accesible desde
otras clases contenidas en el mismo paquete. (94)
98
Capitulo 3 Objetos y clases
CLASSPATH, variable Especifica los directorios y archivos en los
que hay que buscar para
encontrar las clases. (93)
constructor Establece cómo se declara e inicializa un objeto, El
constructor predeter-
minado consiste en una inicialización predeterminada miembro a
miembro, en la que
los campos primitivos se inicializan a cero y los campos de
referencia se inicializan con
nu11 . (76)
encapsulación La agrupación de datos y las operaciones que se
aplican a los mismos con
e fin de formar un agregado, al mismo tiempo que se oculta la
implementación del
agregado. (70)
equa1 Is, método Puede implementarse para ver si dos objetos
representan el mismo valor,
El parámetro formal es siempre de tipo Objetc. (78)
especificación de clase Describe la funcionalidad, pero no la
implementación. (73)
implementación Representa las interioridades relativas a cómo se
cumplen las
especificaciones. Ên lo que respecta al usuario de la clase, la
implementación no es
importante, (73)
import, directiva Utilizada para proporcionar una abreviatura para
un nombre de clase
oompletamente cualificado. Java s anade el mecanismo de
importación estática, que
permite utilizar una abreviatura para un miembro estático, (91)
inicializador estático Un bloque de código utilizado para inicializar
campos estáticos,
(85)
instanceof, operador Comprueba si una expresión es una instancia
de una clase. (82)
jaradoc Genera automáticamente documentación para las clases.
(73)
lamada a this en un constructor Utilizada para hacer una llamada a
otro constructor de
la misma clase, (82)
nkcador javadoc Como ejemplos tendríamos Cauthor, @param,
@return y ®throws. Se
utilizan dentro de los comentarios javadoc, (73)
mitodo Una función suministrada como miembro que, si no es
estática, opera sobre una
instancia de la clase. (71)
mitodo estático Un método que no tiene referencia this Implícita y
que por tanto puede
invocarse sin una referencia a un objeto controlador, (82)
miembros de instancin Miembros declarados sin el modificador
estático. (82)
nutador Un método que modifica el estado del objeto. (76)
objeto Una entidad que tiene una estructura y un estado y define
operaciones que pueden
acceder a dicho estado o manipularlo. Una instancia de una clase.
(70)
ocultación de información Hace que sean inaccesibles los detalles
de implementación,
incluyendo los componentes de un objeto. (70)
package, instrucció Indica que una clase es un miembro de un
paquete. Debe preceder a
la definición de la clase. (92)
paquete Se utiliza para organizar una colección de clases. (90)
par El patrón compuesto por dos objetos. (95)
99
Errores comunes
-
patrón compuesto El patrón en el que almacenamos dos o más
objetos en una entidad.
(95)
patrón de disefo Describe un problema que se presenta una y otra
vez en la ingeniería del
software. y luego describe la solución de una forma lo
suficientemente genérica como
para poder aplicarla en una amplia variedad de contextos. (95)
privado Un miembro que no es visible para los métodos que no son
de esa clase. (72)
progranaciún basada en objetos Utiliza los mecanismos de
encapsulación y de oculta-
ción de la información de los objetos, pero no emplea la herencia.
(70)
programación orientada a objetos Se distingue de la programación
basada en objetos
porque emplea la herencia para formar jerarquías de clases. (69)
público Un miembro que es visible para los métodos que no son de
esa clase. (71)
th1s, referenia Una referencia al objeto actual. Puede utilizarse
para enviar el objeto
actual como una unidad a algún otro método. (81)
toString, método Devuelve un objeto String basado en el estado del
objeto. (78)
unidad atónica En referencia a un objeto, sus partes n pueden ser
diseccionadas por la
generalidad de los usuarios del objeto. (70)
Errores comunes
100
Capituio 3 Objetos y clases
Ejercicios
EN RESUMEN
101
Ejercicios
1
class Person
2[
3
public static final int NO_SSN -1:
4
5
pr ivate int SSN - 0 :
6
String name = null:
7)
B
9
class TestPerson
10
private Person p = new Person ( ):
11
12
public static void ma inc String L 1 args )
13
14
Person q = new Person( ):
15
16
/ ilegal
System .out .printiní P ):
17
Il lega1
18
System .out .printin( q ):
19
I?
20
System .out .printin( q-NO_SSN ):
Il ?
21
System .out -println( q-SSN ):
/l?
22
System .out .printin( q-name ):
I?
System .out .printin( Person. NO_SSN ):
23
// ?
System .out .println( Person. 5SN ):
24
25
26 1
Figura 3.18 Codigo para el Ejercicio 3.11.
EN TEORÍA
314 Suponga que compilamos los códigos de la Figura 3.3
(TestIntCe11) y. de la
Figura 3.4 (IntCe11). A continuación, modificamos la clase IntCe1l
de la Figura
3.4 añadiendo un constructor de un parámetro (eliminando por
tanto el cons-
tructor predeterminado de cero parámetros). Por supuesto, si
volvemos a compilar
102
Capituio 3 Objetos y clases
Test IntCell, se producirá un error de compilación. Pero si
TestIntCell no se
compila de nuevo y recompilamos solo IntCe11 no habrá ningún
error. ¿Qué
sucederá cuando se ejecute entonces Test IntCe11.
EN LA PRÁCTICA
Implemente la clase BinaryArray, incluyéndola en un paquete de
su elección.
319 El paquete java- .math contiene una clase B1 gDec ima1,
utilizada para representar
un número decimal de precisión arbítraria. Lea la documentación
de BigDecimal y
responda a las siguientes preguntas:
320 Escriba un programa que lea un archivo de datos que
contenga númeras racionales,
uno por línea, que almacene los números en un Arraylist, que
elimine los dupli-
cados y que luego muestre la suma, la media aritmética y la media
armónica de los
restantes números racionales distintos.
103
Ejercicios
328 Las directivas de importación comodín son peligrosas debido
a que pueden
introducir ambigüedades y otras sorpresas. Recuerde que tanto
java .[Link]
como java .util. List son clases. Partiendo del código de la Figura
3.19:
import java. util .*:
1
2
import java. awt. * ;
3
;
4 class List It Desactive esta clase con un comentario
l
public String toString( ) l return "My List!!": )
8
9 class Wildca rdIsBad
10 (
)
public static void main( String L 1 args
11
12
System .out printin( new List( ) ):
13
14
15
Figura 3.19 Esle codigoparael Ejercicio 326 Ilustra por que las
Importaciones comodin no son convenientes.
104
Capituio 3 Objetos y clases
327 Para la clase BigRational, añada un constructor adicional que
acepte dos objetos
BigInteger como parámetros y asegúrese de generar las
excepciones apropiadas.
PROYECTOS DE PROGRAMACIÓN
328 Suponga que deseamos imprimir una matriz bidimensional en
la que todos los
números están comprendidos entre o y 999. La forma normal de
imprimir cada
número podría hacer que la matriz. no estuviera alineada. Por
ejemplo,
54 4 12 366 512
756 192 18 27 4
14 18 99 300 18
Examine la documentación del método format de la clase String y
escriba una
rutina que imprima la matriz bidimensional con un formato más
elegante, como por
ejemplo
054 004 012 366 512
756 192 018 027 004
014 018 099 300 018
105
Ejercicios
m
add, substract y multiply devuelven un nuevo polinomio que es
igual
a la suma, diferencia o producto, respectivamente, de este
polinomio y de
otro polinomio, rhs. Ninguno de estos métodos modifican ninguno
de los
polinomios originales,
=
equalsy toString siguen la especificación estândar para estas
funciones. Para
toString haga que la representación en forma de cadena de
caracteres tenga el
mejor formato posible.
El polinomio está representado por dos campos. Uno, degree,
representa el
grado del polinomio. Por tanto, A + 2x + 1 es de grado 2, 3x + 5 es
de grado
1 y 4 es de grado 0. Cero es automáticamente de grado 0. El
segundo campo,
coeff, representa los coeficientes (coeffli] representa el
coeficiente de x).
332 Modifique la clase del ejercicio anterior para almacenar los
coeficientes como
objetos BigRat iona1.
333 Un objeto Play1 ngCard representa una carta utilizada en
juegos como el póker
y el black jack, y almacena el palo (corazones, diamantes„ tréboles
o picas)
y el valor (2 a 1Ó, sota, reina, rey o as). Un objeto Deck representa
un mazo
completo de 52 cartas PlayingCard. Un objeto Multipledeck
representa uno
o más mazos (objetos Deck) de cartas (el número exacto se
especifica en el
constructor). Implemente las tres clases Play ingCard, Deck y Mul
tipl eDeck,
proporcionando una funcionalidad razonable para PlayingCard, y
para el caso de
Deck y MultipleDeck, proporcionando minimamente la capacidad
de barajar, de
repartir una carta y de comprobar si todavía quedan cartas.
334 Implemente una clase Date simple. Debería poder representar
cualquier fecha
desde el 1 de enero de 1800 hasta el 31 de diciembre de 2500;
restar dos fechas;
incrementar una fecha en un cierto número de dias y comparar dos
fechas utilizando
tanto equals como compareTo. Un objeto Date se representa
internamente como el
número de dias transcurridos desde un cierto momento inicial, que
aquí es el inicio
del año 1800. Esto hace que todos los métodos sean triviales,
salvo los constructores
y toString.
Ia regla para los años bisiestos es que un ano es bisiesto si es
divisible por 4 y
no lo es por 100„ a menos que también sea divisible por 400. Por
tanto, 1800, 1900
y 2100 no son años bisiestos, pero 2000 sí lo es. El constructor
debe comprobar la
validez de la fecha, como también debe hacerlo toString. El objeto
Date podría
llegar a tener un valor incorrecto si un operador de incremento o
de sustracción le
hiciera salirse fuera del rango permitido.
Una vez que haya decidido las especificaciones, puede realizar
una implemen-
tación. La parte dificil es convertir entre las representaciones
interna y externa de
una fecha. À continuación se presenta un posible algoritmo.
Configure dos matrices que sean campos estáticas. La primera
matriz,
daysTillFirstofMonth, contendrá el número de dias hasta el primero
de cada
mes en un año no bisiesto. Por tanto, contendrá 0, 31, 59, 90, etc.
La segunda
matriz, daysT111Janl, contendrá el número de dias hasta el primer
día de cada
año, comenzando con firstyear. Por tanto, contendrá 0, 365, 730,
1095, 1460,
106
Capituio 3 Objetos y clases
1826, y asf sucesivamente, porque 1800 no es un año bisiesto, pero
1804 sí lo es.
Debería hacer que su programa inicializara esta matriz una sola
vez utilizando
un inicializador estático. Después, puede emplear la matriz para
convertir de la
representación interna a la representación externa,
335 Un número complejo almacena una parte real y una parte
imaginaria. Proporcione
una implementación de una clase BigCompl ex, en la que la
representación de los
datos se analice mediante dos objetos Bi gDec ima 1 que
representen las partes real
e
imaginaria.
referencias
Puede encontrar más información sobre las clases en las
referencias especificadas al final
del Capitulo 1. La referencia clásica sobre patrones de diseño es
[1]. Este libro describe 23
patrones estándar, algunas de los cuales comentaremos
posteriormente.
1. E Gamma, R. Helm, R. Johnson y J. Vlissides, Elements of
Reusable Object-Oriented
Software, Addison-Wesley, Reading. MA, 1995.
4
Capítulo
Herencia
Como hemos mencionado en el Capítulo 3, un objetivo importante
de la programación orientada a
objetos es la reutilización de código. Al igual que los ingenieros
utilizan una y otra vez determinados
componentes en sus diseños, los programadores deberían ser
capaces de reutilizar los objetos,
en lugar de tener que implementarlos repetidamente. En un
lenguaje de programación orientado
a objetos, el mecanismo fundamental de reutilización del código
es la herencia La herencia nos
permite ampliar la funcionalidad de un objeto. En otras palabras,
podemos crear nuevos tipos con
propiedades restringidas (o ampliadas) con respecto al tipo
original, formando en la práctica una
jerarquía de clases.
Sin embargo, la herencia es algo más que una simple reutilización
de código. Utilizando la
herencia correctamente, el programador puede mantener y
actualizar el código más fácilmente,
tareas ambas que resultan esenciales en las aplicaciones
comerciales de gran envergadura, Com-
prender el uso de la herencia es esencial para poder escribir
programas Java de una cierta entidad,
y los mecanismos de herencia también son empleados por Java
para implementar clases y métodos
genéricos.
En este capítulo, veremos
4. 1
¿Qué es la herencia?
La herencia es el principio fundamental de orientación a objetos
empleado para reutilizar código
entre clases relacionadas. Los mecanismos de herencia modelan
la relación ES-UN. En una relación
ES-UNdecimos que la clase derivada es una (variación de la) clase
base. Por ejemplo, un Círculo
ES-UNA FormaGeometrica y un Automóvil ES-UN Vehículo. Sin
embargo, una Elipse NO-ES-UN
Círculo. Las relaciones de herencia forman jerarquias. Por ejemplo,
podemos ampliar Automóvil
a
108
Capituio 4 Herencia
otras clases, ya que un AutomovilDelmportacion ES-UN Automóvil
ly paga
En una reldion ES-UN,
dedmos que una clase
impuestos especiales) y un AutomovilNacional ES-UNAutomóvil (y
no paga
darivada eS una (variadión
impuestos especiales), etc.
de la) clase tase.
Otra tipo de relación es la relación TIENE-UN (a ESTÁ-
COMPUESTO-
POR). Este tipo de relación no posee las propiedades que son
naturales dentro
de una jerarquía de herencia. Ün ejemplo de relación TIENE-UN es
que un
En una relacion TIENE-UN,
automóvil TIENE-UN volante. Las relaciones de tipo TIENE-UN no
deben
dedmos que uną clase
ser modeladas mediante la herencia. En lugar de ello, deberían
emplear la
derivada tiene una
únstancia de ka) clase base.
têcnica de composición, en la que las componentes se tratan
simplemente
Para modełar as relaclones
como campos privados de datos.
de tipo TIENE UNse utiliza
Como veremos en los próximos capítulos, el propio lenguaje Java
hace
la técnica de compasicion.
un amplio uso de la herencia a la hora de implementar sus librerias
de clases.
4.1.1
Creación de nuevas clases
Nuestras explicaciones acerca de la herencia se centrarán en un
determinado ejemplo. En la Figura
4.1 se muestra una clase típica. La clase Person se emplea para
almacenar información acerca de
una persona; en nuestro caso, tenemos datos privados que
incluyen el nombre, la edad, la dirección
yel número de teléfono, junto con algunos métodos půblicos que
pueden acceder a esta información
y posiblemente modificarla. Cabe imaginar que, en la práctica,
esta clase sería significativamente
más compleja, almacenando quizá unos 30 campos de datos, junto
con unos 100 métodos,
Ahora suponga que queremos tener una clase Student para
representar estudiantes o una clase
Empl oyee para representar empleados, 0 ambas. Imagine que un
objeto Student es similar a un
objeto Person, con la adición de solo unos cuantos métodos y
miembros de datos. En nuestro
sencillo ejemplo, imagine que la diferencia es que un objeto
Student añade un campo gpa para
almacenar la nota media y un método accesor getGPA, De forma
similar, imagine que el objeto
EmpToyee tiene los mismos componentes que Person, pero que
además dispone de un campo
salary para representar el salario y de métodos para manipular ese
salario.
Una opción a la hora de diseñar estas clases sería la técnica
clásica de copiar y pegar: copiamos
la clase Person, cambiamos el nombre de la clase y de los
constructores y luego añadimos los
nuevos miembros. Esta estrategia se ilustra en la Figura 4.2.
El recurrir a la técnica de copiar y pegar es una opción de diseño
bastante poco conveniente, que
presenta numerosas desventajas. En primer lugar, tenemos el
problema de que si copiamos código
erróneo, terminaremos teniendo más errores en el código. Esto
hace que sea muy dificil corregir los
errores de programación detectados, especialmente cuando se
detectan de forma tardía,
En segundo lugar, tenemos el problema, relacionado con el
anterior, del mantenimiento y. el
versionado. Suponga que decidimos en una versión del programa
que sería mejor almacenar los
ombres con el formato apellido, nombre de pila, en lugar de
emplear un único campo. o quizá, que
sería mejor almacenar las direcciones utilizando una clase
especial Address. Para poder mantener la
y
coherencia, deberiamos realizar estos cambios en todas las
clases. Utilizando la técnica de copiar
pegar, estos cambios de diseño tienen que efectuarse en
numerosos sitios distintos.
Un tercer problema, quizá más sutil que los anteriores, es el que
hecho de que empleando la
técnica de copiar y pegar, Person, Student y Emp Toyee son tres
entidades separadas sin ninguna
relación entre SÍ, a pesar de sus similitudes. Así que, por ejemplo,
si tenemos una rutina que acepta
4.1 ¿Qué es la herencia?
109
1
class Person
2
3
)
public Person( String n. int ag. String ad. String p
4
name = n: age = ag: address = ad: phone = p:
}
(
5
6
public String toString( )
7
..
return getName [ ) # ". *" # getAge( ) + ..
[
B
|
+ getPhoneNumber( ):
9
10
public String getName( )
11
return name:
}
12
13
public int getagel )
14
l
return age :
15
16
public String getAddress(
17
return address:
(
18
19
public String getPhoneNumber ( )
20
1
(
return phone:
21
22
public void se tAddress( String newAddress )
23
l
address newAddress:
)
24
25
public vofd setPhoneNumber( String newPhone
)
26
phone - newPhane:
}
[
27
28
private String name;
29
private int age:
30
private String address:
31
private String phone:
32 }
Figura 4.1 Laciase Person almacena d nombre, la edad, la
direccióny o nürmero de Ieléfono.
un objeto Person como parámetro, no podriamos enviar a esa
rutina un objeto Student. Por tanto,
tendríamos que copiar y pegar todas esas rutinas para que
funcionaran para esos nuevos tipos.
El mecanismo de herencia resuelve estos tres problemas.
Utilizando la herencia, diríamos que un
objeto Student ES-UN objeto Person. A continuación,
especificaríamos los cambios que un objeto
Student tiene en relación con un objeto Person. Solo se permiten
tres tipos de cambios:
Student puede añadir nuevos campos, por ejemplo gpa.
Student puede añadir nuevos métodos, por ejemplo, getGPA.
Student puede sustituir métodos existentes, por ejemplo, toString.
110
Capitulo 4 Herencia
class Student
1
2
3
public Student( String n. int ag. String ad. String P .
4
double g )
5
name = n; age = ag: address = ad; phone = P: gpa = g: 1
6
7
public String tostring( )
B
-.
return get Name ( ) + . . + getAge( ) + ..
{
g
+ getPhoneNumber ( ) + * * + getGPA( ): }
10
11
public String getName ( )
12
C
return name: }
13
14
public int getAge( )
15
return age:
(
)
16
17
)
public String getaddress(
18
l
return address:
19
20
public String getPhoneNumber( )
21
return phone : )
(
22
23
)
public void setAddress( String newAddress
24.
address = newAddress:
}
25
26
public void setPhonel Iumbert String newp hone
)
27
phone = newp hone :
l
l
28
29
publ ic double getGpal
)
30
return gpa:
{
}
31
32
private String name :
33
private int age:
34
private String address:
35
private String phone:
36
private double gpa
37 )
Figura 4,2La clase Student almacena el nombre, la edad, la
dreccion, el numero de telelonoy la nola medla
mediante la lécnica de coplar y r pegar.
Hay dos cambios que están específicamente prohibidas porque
violarían el concepto de una
relación ES-UN:
Student no puede eliminar campos,
Student no puede eliminar métodos.
4.1 ¿Qué es la herencia?
111
Por último, la nueva clase debe especificar suS propios
constructores; es bastante probable que
esto implique cierta sintaxis de la que hablaremos en la Sección
4.1.6.
La Figura 4.3 muestra la clase Student, mientras que en la Figura
4.4 se muestra la estructura
de datos para las clases Person y Student. En esta última figura se
ilustra que la huella de memoria
de cualquier objeto Student incluye todos los campas que estarían
contenidos en un objeto
Person. Sin embargo, puesto que esos campos están declarados
como privados por Person, no son
accesibles desde los métodos de la clase Student. Esa es la razón
por la que el constnuctor resulta
problemático en este punto. No podemos tocar los campos de
datos en ningún método de Student, y
la única manera que tenemos de manipular los campos privados
heredados es utilizando los métodos
públicos de Person. Por supuesto, podríamos hacer que los
campos heredados fueran públicos, pero
eso resultaría, en general, una decisión de diseño errónea.
Permitiría a los implementadores de las
clases Student y Employee acceder directamente a los campos
heredados. Pero, sI hiciéramos eso
1 class Student extends Person
2
3
public Student( String n , int ag. String ad. String P.
4
)
double g
5.
l
6
/* i 0J0! Hace falta cierta sintaxis: consulte 1a Sección 4 .1 .6 */
7
gpa - g;
B
9
public String tostringí
)
".
return gethame( ) + .* + getAge( ) + ..
10
l
11
+ getPhoneNumber ( ) + *" + getGPa( ):
}
12
public double getGPa( )
13
14
return gpa :
(
}
15
private double gpa :
16
17 I
Figura 4.3 Herencla utliizada paracrear la clase Student.
name
Clase Person
address
phone
age
name
Clase Student
address
phone
gpa
Figura 4.4 Estructura de memorla ulizando herencla. El sombreado
más claro Indica campos que son privados y a los que solo se
puede
acceder medlanle melodos de la clase. El sombreado mas oscutO
enla clase Student Indica campos que no son accesibles
desde la propla iclase Student, pero sin embargo estân presentes,
112
Capituio 4 Herencia
posteriormente se realizaran modificaciones en la clase Person,
como por ejemplo un cambio en
y
la representación de los datos de nombre y dirección en Person,
nas veriamos obligados a repasar
todas las dependencias, lo que volvería a plantearnos los
problemas relacionados con la técnica de
copiar y pegar,
Como podemos ver, salvo para los constructores, el código es
relativamente simple. Hemos añadido un campo de datos, hemos
añadido
La herencianos permite
derivar dases a partir
un nuevo método y henos sustituido un método existente.
Internamente,
de una dase base sin
disponemos de memoria para todos los campos heredados, y.
también dispo-
perturbar la im plementacion
nemos de implementaciones de todos los métodos originales que
no han
de la clase base.
sido sustituidos. La cantidad de nuevo código que tenemos que
escribir para
Student sería aproximadamente la misma, independientemente de
lo grande
o pequeña que fuera la clase Person y tenemos la ventaja de la
reutilización directa del códigoy de
un fácil mantenimiento. Observe también que hemos hecho
nuestra implementación sin perturbar la
implementación de la clase existente.
Resumamos la sintaxis de lo que hemos visto hasta ahora. Una
clase derivada hereda todas las
y
propiedades de una clase base. Después podemos añadir
miembros de datos, sustituir métodos
añadir métodos nuevos. Cada clase derivada es una clase
completamente
La causula extends se
nueva. En la Figura 4.5 se muestra la disposición típica en el caso
de la
utilza para dociarar qua
herencia, utilizando la clase extends. Una cláusula extends
declara que
uina dlase deriva de otra
una clase se deriva de otra clase. Decimos que una clase derivada
extiende
dase.
(amplia) una clase hase. He aquí una breve descripción de una
clase derivada:
Una clase dertreda hereda
todos los miemibros de
dalos de la dase baso
ypuede anad más
miembros de datos.
public class Der ivada extends Base
1
2{
3
I Cua Tquier miembro no enumerado se hereda sin modificaciones.
4
// salvo por el constructor .
5.
6
Hl miembros públ icos
7
I/ Constructor '(es) si el predetermi nado no es aceptable
8
/l Métodos de Base cuyas definiciones deban cambiar en Derivada
9
Il Métodos públicos adicionales
10
11
Il miembros privados
12
Il Campos de datos adiciona les (general mente privados)
13
Il Métodos privados adicionales
14 }
Figura 4.5 Estructura general enel caso deherencia.
4.1 ¿Qué es la herencia?
113
La clase derivata hereda
Indos los mélodos da
la ciase base Pueda
aceplarios orodcfinilos,
Tamblen puede definir
nueros mélodos.
4.1.2
Compatibilidad de tipos
La reutilización directa del código descrita en el párrafo anterior
representa
Ceda clase derhadaes
una ventaja significativa. Sin embargo, la ventaja más significativa
es la
una clase completamente
nueva que de todos
reutilización indirecta del código. Esta ventaja procede del hecho
de que un
modos, presenta una cierta
objeto Student ES-UN objeto Person y un objeto Emp1 oyee ES-UN
objeto
compalibäidad con la dase
Person.
de la que se derfva.
Puesto que un objeto Student ES-UN objeto Person, puede
accederse a
un objeto Student utilizándose una referencia a Person. El
siguiente código
sería, por tanto, legal:
Student s = new Student ( "Joe" . 26 . "1 Main St" .
"202 -555 -1212" . 4 .0 ):
Person pP = s:
System .out printin( "Age is " + P .getage[ ) ):
Es legal porque el tipo estático (es decir, el tipo en tiempo de
compilación) de P es Person. Por
tanto, P puede hacer referencia a cualquier objeto del que
podamos decir que ES-UN objeto Person,
y cualquier métado que invoquemos a través de la referencia p
tendrá siempre sentido, ya que una
vez que se ha definido un método para Person, este no puede ser
eliminado por una clase derivada.
Puede que el lector se esté preguntando por qué esto representa
una ventaja. La razón es que esto
se aplica no solo a las asignaciones, sino también al paso de
parámetros. Un método cuyo parámetro
formal sea de tipo Person puede recibir cualquier cosa de la que
podamos decir que ES-UN objeto
Person, incluyendo Student y Empl oyee.
Considere por tanto el siguiente código escrito en aralquier clase:
public static boolean is0lder( Person p1. Person p2
)
return pl. ge tAge( ) > p2: getAge ( ):
Considere las siguientes declaraciones, en las que omitimos los
argumentos de los constructores
para ahorrar espacio:
Person P = new Persont ):
Student s = new Student( );
Empl oyee e = new Employeec ... ):
114
Capituio 4 Herencia
Podemos utilizar esa única rutina 1s0lder para todas las llamadas
siguientes: 1s01der(p.p),
1s01 der(s.s). is01 der(e.e), is01 der(p, e), is0lder(p. s), is0lder (s.p).
is0lder( s,e),
is0lder( e.p), 1s01 der(e.s).
Con ello henos conseguido que una rutina que no pertenece a esa
clase funcione en nueve casos
distintos. De hecho, no hay ningún limite al nivel de reutilización
que puede conseguirse de esta
forma. En cuanto utilicemos la herencia para añadir una cuarta
clase a la jerarquía, tendremos
4
por 4, es decir 16 métodos diferentes, sin cambiar 1s0lder en
absoluto. EỈ nivel de reutilización
sería todavia más significativo si un método admitiera como
parámetros tres referencias a Person.
E imagine el increible nivel de reutilización de código que se puede
alcanzar si un método utiliza
como parámetro una matriz de referencias a Person.
Es por eso que, para muchas personas, la compatibilidad de tipos
entre las clases derivadas y sus
clases base es el aspecto más importante de la herencia, porque
conduce a una masiva reutilización
indirecta del código. Y, como ilustra el caso de is0lder, también
hace que sea muy fácil añadir
nuevos tipos que funcionen automáticamente con los métodos
existentes.
4.1.3
Despacho dinámico y polimorfismo
Obviamente, existe el problema de la sustitución de métodos: si no
concuerdan el tipo de referencia
Y la clase del objeto al que se está haciendo referencia (en el
ejemplo anterior, serian Person y
Student, respectivamente), y ambos tienen diferentes
implementaciones, ¿qué implementación hay
que utilizar?
Como ejemplo, considere el siguiente fragmento de código:
Student s = new Student( "Joe" . 26 . "1 Main St" .
"202 -555 -1212" . 4 .O );
Empl oyee e = new Employee( "Boss" . 42 . *4 Main St. " .
"203 -555 -1212" , 100000. 0 ):
Person p = null:
If( getTodaysDay( ).equals( "Tuesday" ) )
P s:
else
p = e:
System .out ·printin( "Person is *. + p. toString( ) ):
Aquí, el tipo estático de P es Person. Al ejecutar el prograrma, el
tipo dinámico (es decir, el tipo
del objeto al que se está haciendo referencia en realidad) será
Student o Empl oyee. Es imposible
deducir el tipo dinámico hasta que se ejecuta el programa,
Naturalmente, sin
embargo, lo que querríamos es que se empleara el tipo dinámico, y
eso es lo
Una varlabie poimdrlia
pucde hacer refcrencia
que sucede en Java, Cuando se ejecute este fragmento de código,
el método
a obictos de vnrios lipos
toString utilizado será el que resulte apropiado para el tipo
dinámico de la
distintos. Cuando se aplcan
referencia al objeto controlador.
operaciones a la variable
požmorfica, se selecciona
Existe un importante principio de orientación a objetos que se
conoce con
auiomátic amente la
el nombre de polimorfismo. Úna variable de referencia polimórfica
puede
operacion apropiada para cl
hacer referencia a objetos de varios tipos diferentes. Cuando se
aplican
cbjato referendado
operaciones a la referencia, se selecciona automáticamente la
operación que
4.1 ¿Qué es la herencia?
115
es apropiada para el objeto realmente referenciado, Todos los
tipos de referencia son polimórficos
en Java. Este mecanismo se conoce también con el nombre de
despacho dinamico o acoplamiento
tardio (o en ocasiones acoplamiento dinámico).
Una clase derivada es compatible en cuanto a tipo con su clase
base, lo que quiere decir que
una variable de referencia del tipo de la clase base puede hacer
referencia a un objeto de la clase
derivada, pero no a la inversa. Las clases hermanas (es decir,
clases derivadas de una clase común)
no son compatibles en cuanto a tipo.
4.1.4
Jerarquías de herencia
Como hemos mencionado anteriormente, el uSo de la herencia
suele producir
SIXES-UN Y. cnionoes
una jerarquía de clases. La Figura 4.6 ilustra una posible jerarquía
para
Xes una sutclaso do Y
Person. Observe que Faculty deriva de Person indirectamente, en
lugar de
e Yesuna superclase de
X. Estas relaciones son
directamente -jasi que los profesores representados por la clase
Faculty,
tansiivas.
también son personas! Este hecho es transparente para el usuario
de las clases,
porque las relaciones de tipo ES-UN son transitivas, En otras
palabras, si X
ES-UN Ye YES-UN Z, entonces X ES-UN Z La jerarquía Person
ilustra los problemas típicos de
diseño a la hora de consolidar los aspectos comunes en una serie
de clases base y luego especializar
estas en las clases derivadas. En esta jerarquía, decimos que la
clase derivada es una subclasede la
clase base y que la clase base es una superclase de la clase
derivada. Estas relaciones son transitivas,
y además el operador instanceof funciona con las subclases. Así,
si obj es de tipo Undergrad (y
distinto de nu1ı), entonces obj instanceof Person es true,
Reglas de visibilidad
4.1.5
Sabemos que cualquier miembro que se declare con visibilidad
privada solo es accesible para los
métodos de esa clase. Asi, como ya hemos visto, los miembros
privados de la clase base no son
accesibles por parte de la clase derivada.
Person
Student
Employee
.
Staff
Undergrad
Facully
Graduate
Figura 4.6 La jerarqula de Person.
116
Capituio 4 Herencia
Ocasianalmente, querriamos que la clase derivada tuviera acceso
a los miembros de la clase
base. Para esto, existen dos opciones fundamentales. La primera
consiste en utilizar acceso con
visibilidad pública o de paquete (si las clases base y derivada se
encuentran en el mismo paquete),
según resulte apropiado. Sin embargo, esto permitiría que también
accedieran otras clases adenás
de las clases derivadas.
Si queremos restringir el acceso de forma que solo puedan
acceder las
clases derivadas, podermos hacer que los miembros sean
protegidos. Un
Un miembro profegido de
la clase sera visitle para la
miembro protegido de una clase es visible por parte de los
métodos de una
dase dervada ytambłen
clase derivada y también por parte de los métodos de las clases
contenidas en
pafa las clases contenidas
en el mismo paquele.
el mismo paquete, pero no es visible para nadie más.' Declarar los
miembros
y
de datos como protected o public viola el espíritu de
encapsulación
ocultamiento de la información, y solo se suele hacer,
generalmente, con el fin
de simplificar la programación, Normalmente, una mejora
alternativa consiste en escribir métodos
accesores y mutadores. Sin embargo, si una declaración de
visibilidad protegida nos permite evitar
el tener que emplear un código enrevesado, entonces no deja de
ser razonable hacerlo. En este
texto, se emplean miembros de datos protegidos precisamente por
esa razón. En el texto utilizamos
también métodos protegidos. Esto permite a una clase derivada
heredar un método interno, sin
hacer que este sea accesible desde fuera de la jerarquía de
clases. Observe que en aquellos códigos
de práctica en los que todas las clases se encuentran en el
paquete sin nombre predeterminado, los
miembros protegidas son visibles.
El constructor y super
4.1.6
Cada clase derivada debe definir sus propios constructores. Si no
se escribe ningún constructor,
se genera entonces un único constructor predeterminado con cero
parámetros. Este constructor
invocará el constructor de cero parámetros de la clase base para
la parte heredada y luego aplicará
la inicialización predeterminada para todos los campos de datos
adicionales (lo que quiere decir
asignar un valor ó a los tipos primitivos y nuTl a los tipos de
referencia).
ES bastante común construir un objeto de la clase derivada
construyendo primero la parte
heredada. De hecho, eso es lo que se hace de manera
predeterminada, e incluso aunque se
proporcione un constructor explicito para la clase derivada, Esto
es bastante natural, porque el punto
1
la regla de la visibilidad protegida es bastante compleja. Un
miembro protegido de la clase B será visible para todos las
métodos
de todas las clases que se encuentren en el mismo paquete que B.
Tambien sern visible para los métodos de cualquler clase D que
s* encuentre en un paquete distinto de B, siempre y cuando D
amplfe B, pero solo si se accede a través de una referencia que
sea
compatible en cuanto ai tipo con D (Incluyendo lin thts Implicito o
explicito). Especificanente, ese miembro NO SERÁ VISIBLE ea
la clase D si se accede a el a través de una referencia de tipo B. El
sigulenie ejemplo ilustra esta regla.
1
class Dero extends [Link] tertaputSt rca
2
I1 FilterInputStrean tiene un canpo de datos protegido 11arado in
3
public vo1d foor )
4
5
fava. [Link] b = thts: f1 1egal
6
[Link] sprintint In ):
IT Tegal
7
[Link] .pr Intin( [Link] E
/ / legal
B
Systenout -pr intiní befn 1:
It 1legal
9
i0
4.1 ¿Qué es la herencia?
117
1
class Student extends Person
2
l
3
public Student ( String n. int ag. String ad. String P.
4
)
double g
5
[
super( n. ag. ad . P ): gpa " g: )
6
7
Il Omitidos tostring y getage
B
9
private double gpa ;
10 |
Figura 4.7 Un constructor para la nueva clase Student; uiliza super,
de vista de la encapsulación nos dice que la parte heredada es
una entidad
Si no se escribe ningün
en sí misma y el constructor de la clase base nos dice cómo
inicializar esa
corstrucior, entonces se
gunera un único constructot
entidad diferenciada,
predeterminado de cEf0
Los constructores de la clase base pueden ser invocados
explicitamente
parametros que invoca
utilizando el método super. Así, el constructor predeterminado
para una
el constructce de cero
parametros de la dase base
clase derivada es, en realidad,
para la parte heredada, y
luego aplica la nidalzadon
public Derived( )
predoterninada para
l
los campos de datus
adicionales.
supert ):
}
El método super puede invocarse con parámetros que se
correspondan
super se usa para llamar
oon los de un constructor de la clase base. Como ejemplo, la
Figura 4.1 ilustra
al construcire de la clase
la implementación del constructor Student.
base.
El método super solo puede emplearse como primera línea de un
constructor. Si no se incluye, se genera una llamada automática a
super sin
ningún parámetro.
4.1.7
Clases y métodos final
Como hemos descrito anteriormente, la clase derivada acepta o
sustituye los
Uin mótodo final es
métodos de la clase base. En muchos casos, está claro que un
método concreto
Inarlante en toda la
prarqula die herencia yno
de la clase base debe ser invariante en toda la jerarquia„, lo que
quiere decir
puede ser suslituido,
que una clase derivada no debería sustituirlo, En este caso,
podemos declarar
que el método es fina1 y que no puede ser sustituido,
El declarar los métodos invariantes como final no solo es una
buena práctica de programación,
sino que tambiên permite obtener código más eficiente. Es una
buena práctica 'de programa-
ción porque, además de declarar nuestras intenciones al lector del
programa y de la documentación,
evitamos la sustitución accidental de un método que no deberia
ser sustituido.
Para ver por qué la utilización de final permite obtener un código
más eficiente, suponga que la
clase base Base declara un métoda final fy suponga que Derived
amplía Ba se. Considere la rutina:
118
Capitulo 4 Herencia
void doIt( Base obj
)
l
[Link] ):
}
Puesto que f es un método final, no importa si obj hace referencia
a
El acoptamicnio estático
podiria ulilizarse cuando
un objeto de tipo Base o Deri I ved; la definición de f es invariante,
así que
cl metodo Sca imariante
sabemos positivamente qué es lo que hace f. Como resultado,
puede tomarse
en toda la jerarqula de
una decisión en tiempo de compilación para resolver la llamada al
método,
herencia.
en lugar de tomar la decisión en tiempo de ejecución, Esto se
conoce con
el nombre de acoplamiento estático. Puesto que el acoplamiento
se realiza
durante la compilación, en lugar de en tiempo de ejecución, el
programa debería ejecutarse más
rápidamente. Ex que esto sea perceptible dependerá de cuántas
veces evitemos tomar la decisión en
tiempo de ejecución mientras se ejecuta el programa,
Un corolario de esta observación es que si f es un método trivial,
como por ejemplo un accesor a un único campo y se declara como
final,
Los métodos estáticos
el compilador podría sustituir la llamada a f por su definición en
línea,
no ticnen ningün objeto
oontrolador ypor tanto
Por tanto, la llamada al método seria sustituida por una única linea
que
se resuciven en tiempo
accediera a un campo de datos, ahorrando así tiempo. Si f no se
declara
de compiledon, mediante
acuplamienio eslático.
final, entonces es imposible hacer esto, ya que obj podría estar
haciendo
f
referencia a un objeto de una clase derivada, para el que la
definición de
podría ser diferente.? Los métodos estáticos no son métodos
finales, pero no
tienen ningún elemento controlador y por tanto se resuelven en
tiempo de compilación, utilizando el
mecanismo de acoplamiento estático,
Similar al concepto de método final es el concepto de clase final.
Una
clase final no puede ser ampliada mediante herencia. Como
resultado, todos
Una clase finsl no puode
ampdarse medlante
sus métodos son, automáticamente métodos finales. Por ejemplo,
la clase
herencia Une iclase hoja es
String es una clase final. Observe que el hecho de que una clase
tenga solo
una dase final
métodos finales no implica necesariamente que ella misma sea
una clase
final. Las clases finales también se conocen con el nombre de
clases hoja,
porque en la jerarquía de herencia, que se asemeja a un árbol, las
clases finales se encuentran en los
extremos finales, como si fueran las hojas del árbol.
En la clase Person, los métodos triviales accesores y mutadores
(los que comienzan con get
Yy set) son buenos candidatos para ser definidos como métodos
finales, y así hemos elegido
declararlos en el código que los lectores tienen asu disposición a
través de la web.
3
En Ics dos pirrafos anterioces decimas que el acoplamiento
estătico y las optimizaciones en linea "podrian" realizarse porque,
aunque
d tomae esas cecisiones en tiempo de compilación parece tener
sentido, la Sección B.4,3.3 de la especificación del lenguaje deja
claro
que las optimizaciones en linea para los métodns finales de
caracter trivial pueden realizarse, pero que esta optimización debe
Ser hecha
por la maquina virtual en tiempo de ejecución, en luga de por el
compilador en tiempo de compilación, Eso garantiza que las clases
dependientes no queden desincronizadas Oono resultado de ila
optimización.
4.1 ¿Qué es la herencia?
119
4.1.8
Sustitución de un método
Los métodas de la clase base se sustituyen en la clase derivada
simplemente
EI mêtodo de la dase
derivada dete tener el
proporcionando en la clase derivada un método que tenga la
misma signatura.
misno tpo de retormo
El método de la clase derivada debe tener el mismo tipo de retorno
y no puede
ysignalura, yno puede
añadir excepciones a la lista throws.^ La clase derivada no puede
reducir la
anadr excepdonos ak
Esta throws.
visibilidad, ya que eso violaría el espíritu de una relación ES-UN.
Por tanto,
no se puede sustituir un método publico por un método con
visibilidad de
paquete.
En ocasiones, el método de la clase derivada desea invocar el
método
La susiltuaidn parcial
impäca invocar un mélodo
de la clase base. Normalmente, esto se conoce con el nombre de
sustitución
de la dase base utillzando
parcial. Es decir, queremos hacer lo que hace la clase base y un
poco más,
super.
en lugar de hacer algo completamente distinto. Las llamadas al
método de la
clase base pueden realizarse utilizando super. He aqui un ejemplo:
publ ic class Workaholic extends Worker
public void dowork( )
super. dolork ( ): Hl Trabaja como un Worker
drinkCoffee( ): Il Se toma un descanso
super. doWork( ): Hl Trabaja como un Worker un poco más
|
Un ejemplo más típico es la sustitución de los mêtodos estándar,
como por ejemplo toString.
La Figura 4.8 ilustra este uso en las clases Student y EmpToyee.
4.1.9
Un nuevo análisis de la compatibilidad de tipos
La Figura 4.9 ilustra el uso típico del polimorfismo con matrices.
En la linea 17. creamos una matriz
de cuatro referencias a Person, cada una de las cuales se
inicializará con nu11. Los valores de estas
referencias pueden establecerse en las líneas 19 a 24, y sabemos
que todas las asignaciones son
legales, debido a la capacidad que tiene una referencia a un tipo
base para hacer referencia a objetos
de un tipo derivado.
La rutina printAll simplemente recorre la matriz e invoca el método
tostring, utilizando
el mecanismo de despacho dinámico. La comprobación de la linea
7 es importante porque, como
hemos visto, algunas de las referencias de la matriz podrían ser
nu11.
120
Capituio 4 Herencia
class Student extends Person
1
l
2
3
public Student( String n, int ag, String ad. String P.
4
double g
)
5
(
}
super( n, ag, ad. P ): gpa - g:
6
7
public String tostring( )
return super .toString( ) + getGPA( ):
l
B
9
10
public double getGPac )
l
return gpa :
11
}
12
private double gpa :
13
14 }
15
16 class Empl cyee extends Person
17
public Emp loyeel String n , int ag. String ad.
18
String P. double s )
19
[ super( n. ag. ad . P ): salary -- s: 1
20
21
public String toString( )
22
return super .tostring( ) + . $" + getSalary( ): }
23
(
24
public double getSalary(
)
25
return salary:
[
26
)
27
public vo1d raise( double percentRaise )
28
salary *-- C 1 + percent :Raise ):
I
29
30
private double salary:
31
32 |
Figura 4.8 Las clases Studenty Enployee compietas urlizando
ambas formas de super.
En el ejemplo, suponga que antes de completar la impresión
queremas conceder un aumento a
PL31, que sabemas que es un empleado, Puesto que PL31 es de
tipo Emp7 oyee, podría parecer que
[Link]( 0. 04 ):
sería legal. Pero no lo es. El problema es que el tipo estático de
p[3] es Person y raise no está
definida para Person. En tiempo de compilación, solo pueden
aparecer a la derecha del operador
punto los miembros (visibles) del tipo estático de la referencia.
Podemos cambiar el tipo estático utilizando una cláusula de
modificación del tipo:
((Employee) pl3]). raise( 0 .04 ):
4.1 ¿Qué es la herencia?
121
class PersonDemo
1
2
l
3
public static void printall( Person L ] arr
)
4
5
fort Int 1 - 0: 1 < arr .Tength: it
)
6
l
7
ift arrl i ] != null }
8
l
9
System .out .print( *I* + i + "] * ):
10
System .out .printin( arrl 1 1. toString( ) ):
I1
)
12
)
13
14
)
15
public static void main( String L J args
16
Person L 1 P - new Personl 4 1:
17
18
19
PLO] = new Person( "joe" , 25. "New York" ,
"212-555-1212" );
20
p[1 ] - new Student( "ji11" , 27 . "Chicaga" ,
21
"312-555 1212" . 4.0 ):
22
PL3] = new Employee( "bob" . 29. "Boston" .
23
24
"617-555 -1212" , 100000. .0 2:
25
printallc p ):
26
27
28 1
Figura 4.9 Una llustración del polimorfismo con matrices.
a
La línea anterior hace que el tipo estático de la referencia situada
Lina espedalzaciono
la izquierda del operador punto sea Emp Toyee. Si esto es
imposible (por
downcast es un cembio de
ejemplo, porque pL3] se encuentre en una jerarquía de herencia
comple-
tipo por ei que se desciene
tamente distinta), el compilador se quejará. Ši es posible que el
cambio de
dentro de la erarquia de
herencka. Los camblos de
tipo tenga sentido, el programa se compilará, por lo que ei código
anterior
tipo son siempee verificados
proporcionará correctamente a pL3] un tanto por ciento de
aumento. Esta
en tiempo de ejecucica por
estructura sintáctica, en la que modificamos el tipo específico de
una
la Máquina Vrlual
expresión, cambiando una clase base por otra situada por debajo
de ella
dentro de la jerarquía de herencia, se conoce con el nombre de
downcast o
especialización.
¿Qué pasaría si PL31 no fuera de tipo Emp oyee? Por ejemplo, ¿quế
pasaría si empleáramos la
linea siguiente?
122
Capituio 4 Herencia
(( Employee) pl1]). raise( 0. 04 ): // p[1] es un objeto Student
En ese caso, el programa se compilaría, pero la Máquina Virtual
generaría una excepción
ClassCastExcepti on, que es una excepción de tiempo de
ejecución que indica un error de
programación. Los cambios de tipo siempre se comprueban
exhaustivamente en tiempo de
ejecución, para cerciorarse que el programador (o un pirata
informático malicioso) no esté tratando
de pervertir el sólido sistema de tipos de Java. La forma segura de
hacer este tipo de llamadas
consiste en utilizar primero instanceof:
if( pI3] instanceof Emp Toyee )
(( Employee ) PE3]).raise( 0. 04 ):
4.1.10 Compatibilidad de tipos matriciales
Una de las dificultades en el diseño de lenguajes es como
gestionar la herencia en el caso de tipos
agregados. En nuestro caso, sabenos que Empl oyee ES-ÙN Person.
¿Pero quiere esto decir que
Empl oyeel] ES-UN Persont1? En otras palabras, si escribimos una
rutina para aceptar Person[]
como parámetro, ¿podemos pasarle un objeto de tipo Emp1 oyeel]
como
argumento?
Las matrices de subclases
y
son compatibles en quanto
A primera vista, parece que se trata de una cuestión muy sencilla
a tipo con las matrices de
que Empl oyeel] debería ser compatible en cuanto a tipo con
Personl].
superdase. Esto se conoce
con el nomibre de mainices
Śin embargo, el problema es más sutil de lo que parece. Suponga
que
covarlantes
además de Emp1 oyee, Student ES-UN Person. Suponga que Emp
loyeel] es
compatible en cuanto tipo con Personl]. Entonces, considere esta
secuencia
de asignaciones:
Person[] arr = new Employeel 5 1: 11 se compila: 1as matrices son
compa tibles
arrl 0 1 = new Student( ..* ):
// se compila: Student ES-UN Person
Ambas asignaciones se compilan, a pesar de lo cual arr[o] estará
haciendo
Sise Inserta un lipo
realmente referencia a un objeto de tipo Employee y Student NO-
ES-UN
incompatitle enla
matriz la 1aguina Virtual
Empl oyee. Por tanto, tenemos una confusión de tipos. El sistema
de tiempo
gnerara una excepdion
de ejecución no puede generar una excepción ClassCastExcept
ion, ya que
ArrayStore-
no hay ninguna conversión de tipo explícita.
Exception.
La forma más fácil de evitar este problema consiste en especificar
que
las matrices no son compatibles en cuanto a tipo. Sin embargo, en
Java, las
matrices soncompatibles en cuanto a tipo. Esto se conoce con el
nombre de tipo matricial covariante.
Cada matriz sabe el tipo de objeto que tiene permitido almacenar.
Si se inserta un tipo incompatible
dentro de la matriz, la Máquina Virtual generará una excepción
ArrayStoreExcepti on.
Tipos de retorno covariantes
4.1.11
Antes de Java 5, al sustituir un método, el método de la subclase
tenfa obligatoriamente que tener el
mismo tipo de retorno que el método de la superclase. En Java 5
se ha relajado esta regla, y el tipo
de retorno del método de la subclase solo necesita ser compatible
en cuanto a tipo (es decir, puede
4.2 Diseho de jerarquias
123
ser una subclase de) con el tipo de retorno del método de la
superclase. Esto
En Java 5, ei tipo de
se conoce con el nombre de tipo de retorno covariante. Por
ejemplo, suponga
relono de un metodo de la
subdase solo necesila ser
que la clase Person tiene un método ma keCopy
compalibie en cuanto a tipo
fes deck, puede ser uną
public Person ma keCopy( ):
subdase de) con el lipo de
relorno diel melodo de ta
que devuelve una copia del objeto Person. Antes de Java 5, si la
clase
superclase. Esto se conooS
con el nomtxe de lipo de
Empl oyee sustituia este método, el tipo de retorno tenía que ser
Person. En
retorno covorlanie.
Java 5, el método tiene que sustituirse de la forma siguiente:
public Employee ma keCopy[ ):
4.2
Diseño de jerarquías
Suponga que tenemos una clase Circley que para cualquier Circle
c no nulo, C. area( ) devuelve
el área del objeto Circle C. Suponga también que tenemos una
clase Rectangley que para cualquier
Rectangle rno nulo, [Link]( ) devuelve el área del objeto Rec tangle
r. Posiblemente tendríamos
otras clases como ETlipse, Triang1e y Square, todas con métodos
que permitan calcular el área de
la forma geométrica correspondiente. Suponga que tenemos una
matriz que contiene referencias a
estos objetas y que queremos calcular el área total de todos los
objetos, Puesto que todas las clases
disponen de un método area, el polimorfismo constituye una
opción atractiva, permitiéndonos
escribir un código como el siguiente:
)
public static double totaTArea( WhatType l 1 arr
I
double total = 0 . O :
forc int 1 m. 0 : f < arr .length: 1+ }
)
if( arrl 1 1 !- nu11
total + arrl i [Link]( ):
return total:
Para que este código funcione, tenemosque decidir
ladeclaraciónde tipo paral KhatType. Nictrcle,
ni Rectangl le, etc., funcionarian, ya que no existe una relación de
tipo ES-UN. Por tanto, necesitamos
definir un tipo, como por ejemplo Shape, que represente una forma
geométrica cualquiera, de modo
Circle ES-UNShape, Rectangle ES-UNShape, etc, En la Figura 4.
1Őse ilustra una posible jerarquía,
Además, para que arrli]. area( ) tenga sentido, area debe ser un
método disponible para Shape.
Esto sugiere una clase para Shape, como se muestra en la Figura
4.11. Una vez que disponemos
de la clase Shape, podemos proporcionar otras, como se muestra
en la Figura 4.12. Estas clases
también incluyen un método peri meter para calcular el perímetro.
El código de la Figura 4.12, con clases que amplian la clase Shape
simple de la Figura 4.11,
que devuelve -1 para area, puede utilizarse ahora
polimórficamente, como se muestra en la
Figura 4.13.
Una gran ventaja de este diseño es que podemos añadir una nueva
clase a la jerarquia sin
perturbar las implementaciones. Por ejemplo, suponga que
queremos añadir triángulos a nuestro
diseño. Lo único que necesitamos hacer es que Triangle amplie
Shape, sustituya el método area
124
Capitulo 4 Herencia
Shape
Circle
Rectangle
Figura 4.10 La jerarqula de formas geometricas usada en un
ejemplo de herencia.
public class Shape
1
2
l
3
public double area( )
4.
5
return -1;
6
7
|
Figura 4.11 Una posible clase Shape.
apropiadamente, y con ello se podrán incluir objetos Triangle en
cualquier objeto Shapel].
Óbserve que esto implica lo siguiente:
La cidstencla do muchoss
operadores fnstanceof
es un sintoma de un mal
diseno orientado a objeios.
lo que hace dificil que el código existente deje de funcionar
durante el proceso de adición de nuevo
código. Observe también que no hay ninguna prueba de tipo ins
tanceof, lo que es típico de un
buen código polimórfico.
4.2.1
Clases y métodos abstractos
Aunque el código del ejemplo anterior funciona, podenos realizar
mejoras en la clase Shape que
hemos escrito en la Figura 4.11. Observe que la propia clase
Shape, y el método area en particular,
son simples parâmetros de sustitución: no se pretende que el
método area de Shape sea nunca
invocado de forma directa. Está allí simplemente para que el
compilador y el sistema de tiempo de
ejecución puedan colaborar para utilizar el despacho dinámico e
invocar un método area apropiado.
De hecho, examinando ma in, vemos que tampoco se pretende que
se creen nunca objetos Shape. Esa
clase existe simplemente como superclase común para las otras.5
5
Declarar un constructor Shape privado NO RESUELVE el segundo
problema: el constructor es necesario para las subclases
125
4.2 Diseño de jerarquias
public class Circle extends Shape
1
2
l
public Circlel double rad
)
3
4
[ radius = rad: )
5
6
public double area ( )
7
[
return [Link] * radius * radius: )
8
g
public double peri meter(
)
10
return 2 * [Link] * radius : }
[
11
12
public String toString( )
13
return *Circle: * + radius: 1
l
14
15
private double radius;
16 }
17
18 public class Rectangle extends Shape
19
20
public Rectang le( double len. double wid
)
21
Tength = Ten: width = wid;
[
22
23
public double area ( )
24
return 1ength * width:
(
}
25
26
public double peri meterc )
}
l
27
return 2 * ( length + width ):
28
29
public String toString( )
30
l
return "Rectangle: . + length + ma *. + width:
I
31
32
public double getlength( )
33
return length:
l
}
34
35
public double getWidth( )
36
return width: I
l
37
38
private double length;
39
private double width:
40 )
Figura 4.12 Clases Circley Rectangle.
126
Capituio 4 Herencia
class ShapeDemo
1
2
l
3
public static double totalArea( Shape l ] arr
)
4
5
double total - 0:
6
7
fort Shape s : arr )
B
ifc s != nul 1 )
9
total += [Link]( ):
10
I1
return total:
12
13
14
public static void printallc Shape L 1 arr )
15
16
fort Shape s : arr )
17
System. out. printin( s ):
18
)
19
20
public static vofd mainc String C J args
)
21
Shape L ] a - new Circlec 2.0 ) . new Rectanglec 1.0. 3.0 ). null 1:
22
23
Systen out ·println( "Total area = . + tota 1Area( a ) ):
24
printallc a ):
25
26
27 I
Figura 4.13 Un programa de ejermplo que utiliza la jcrarquia de
formas geamétricas.
El programador ha intentado dejar claro que invocar el método de
cálculo del área de Shape es
un error devolviendo el valor -1, que es obviamente un área
imposible. Pero este es un valor que
podría ser 1gnorado. Además, se trata de un valor que será
devuelto si no se sustituye el método
del cálculo del área al ampliar Shape con una clase heredada. Esta
no sustitución del método
podría producirse debido a un error tipográfico: imagine que se
escribe una función Area en lugar
de area, haciendo dificil localizar el error en tiempo de ejecución,
Una solución mejor para area consiste en generar una excepción
de
tiempo de ejecución (una adecuada sería Unsuppor tedoperat
1onException)
Los mélodos yclases
en lä clase Shape. Esto es preferible a devolver -1 porque la
excepción no
absiracios representan
será ignorada.
elementos que habra que
sustitur,
Sin embargo, incluso utilizando esa solución, el problema solo se
resuelve en tiempo de ejecución. Sería mejor disponer de una
sintaxis que
indicara explícitamente que area es un método pensado para ser
sustituido
127
4.2 Diseho de jerarquias
yque no necesita ninguna implementación en absoluto, y que
además Shape es una clase pensada
para ser ampliada y no pueden construirse objetos de la misma,
aun cuando se declaren en ella
constructores y disponga de un constructor predeterminado si no
se declara ninguno. Si esta
sintaxis estuviera disponible, entonces el compilador podría, en
tiempo de compilación, declarar
como ilegal cualquier intento de construir una instancia de Shape.
También podría declarar como
ilegal cualquier clase, cormo Triangle, en la que se intentara
construir una instancia sin haber
sustituido el método area. Esto describe exactamente lo que son
los métodos abstractos y las
dases abstractas.
Un método abstracto es un método que declara funcionalidad que
todos
Un mélodo sbstraco no
los objetos de las clases derivadas deben terminar
implementando. En otras
tiene ninguna definición
palabras, dice lo que esos objetos pueden hacer. Sin embargo, no
proporciona
signifkcaliva yse define
Sacnpre, por tanto, en la
ninguna implementación predeterminada. En lugar de ello, cada
objeto debe
dlase dortreda.
proporcionar su propia implementación.
Una clase que tenga al menos un método abstracto se denomina
clase
abstracta. Java exige que todas las clases abstractas se declaren
explicitamente
Una clase con al mCnOS
como tales. Cuando una clase derivada se olvida de sustituir un
método
un metodo abstracto debe
abstracto con una implementación, el métado continúa siendo
abstracto en
ser definida Com clase
la clase derivada, Como resultado, si una clase que no se
pretendia que fuera
abstracta.
abstracta se olvida de sustituir un método abstracto, el compilador
detectará
la incoherencia e informará del error.
En la Figura 4.14 se muestra un ejemplo de cómo podemos hacer
que Shape sea abstracta. No
hace falta efectuar ningún cambio en el código de las `Figuras 4.12
y 4.13. Observe que una clase
abstracta puede tener métodos que no sean abstractos, como es
el caso de semi peri meter.
Una clase abstracta también puede declarar tanto campos
estáticos como de instancia. Al igual
que en las clases no abstractas, estos campos serán tipicamente
privados, y los campos de instancia
se inicializarían mediante constructores. Aunque no pueden
crearse instancias de clases abstractas,
esos constructores serán invocados cuando las clases derivadas
usen super. En un ejemplo más
amplio, la clase Shape podría incluir las coordenadas de los
vértices de los objetos, que serían
configuradas mediante constructores, y podría proporcionar la
implementación de métodos como
positi onof, que fueran independientes del tipo concreto del objeto;
positi onof sería un método
final.
Como hemos mencionado anteriormente, la existencia de al menos
un método abstracto hace
que la clase base sea abstracta e impide crear instancias de la
misma. Por tanto, no se puede crear
public abstract class Shape
1
2
l
3.
public abstract double areal ):
4
public abstract double perimeter( );
5
6
public double semi per imeter( )
7
(
return per imeter( ) / 2:
8
}
Figura 4.14 Unaclase Shape abstracta. El código delas Figuras
4.12y 4.13 nosulre modilicsciones.
128
Capituio 4 Herencia
un objeto de tipo Shape; solo pueden crearse los objetos
derivados. Sin embargo, como es habitual,
una variable de tipo Shape puede hacer referencia a cualquier
objeto derivado concreto, como un
Circle o un Rectangle. Asi
Shape a . b:
a - new Circle( 3.0 ): H Legal
b - new Shape( );
// Iegal
Antes de continuar, resumamos los cuatro tipos de métodos de
una clase:
Diseño pensando en el futuro
4.2.2
Considere la siguiente implementación para la clase Square:
publ ic class Square extends Rectangle
l
public Square( double side
)
)
l
super( side, side ):
Puesto que obviamente un cuadrado es un rectángulo cuya
longitud y anchura son iguales, parece
razonable hacer que Square amplíe Rect tang le,. para así evitar
tener que reescribir métodos como
area y perimeter. Aunque es cierto que, debido a la no sustitución
de toString, los objetos de
tipo Square siempre se mostrarán como objetos de tipo Rectangle
de longitud y anchura idénticas,
podemos corregir esa situación proporcionando un método
toString para Square. De ese modo,
la clase Square puede hacerse realmente escueta y podemos
reutilizar el código de Rectang le.
¿Pero es un diseño razonable? Para responder a esta pregunta,
debemos considerar de nuevo la regla
fundamental de la herencia.
La cláusula extends es apropiada solo si es cierto que Square ES-
UN Rectangle. Desde
una perspectiva de programación, esto no quiere decir
simplemente que un cuadrado deba ser
geométricamente un tipo de rectángulo; más bien, lo que significa
es que cualquier operación que
129
4.3 Herencia múltiple
podamos realizar con un objeto de tipo Rectangle pueda ser
realizada también con un objeto de
tipo Square. Y lo más importante es que esta no es una decisión
estática, lo que quiere decir que no
debemos simplemente mirar al conjunto actual de operaciones
soportadas por Rectangle. En lugar
de ello, debemos preguntarnos si es razonable asumir que en el
futuro puedan añadirse operaciones
a la clase Rectangle que no tendrían sentido para un objeto de tipo
Square. En ese caso, entonces
el argumento de que un Square ES-UN Rectang le se debilita
considerablemente. Por ejemplo,
suponga que la clase Rec tang le tuviera un método stretch para
modificar la forma del rectángulo
y cuya especificación indicara que stretch aumenta la dimensión
más larga del objeto Rectang 1e,
dejando intacta la dimensión más pequeña. Claramente, la
operación no puede estar disponible para
un objeto Square, porque si lo hiciéramos el objeto dejaría de ser
un cuadrado.
Si sabemos que la clase Rectangle tiene un método stretch,
entonces probablemente no sea
una buena decisión de diseño hacer que Square amplie Rectangle.
Si Square ya amplia Rectangle
y posteriormente deseamos añadir un método stretch a Rectangle,
hay dos formas básicas de
hacerlo.
La opción 1 sería que Square sustituya stretch con una
implementación que genere una
excepción:
public void stretch( double factor )
throw new Unsuppor tedoperat ionException( ):
Con este tipo de diseno, al menos los cuadrados nunca dejarán de
ser cuadrados.
la opción 2 sería rediseñar toda la jerarquía para que Square deje
de ser una ampliación de
Rectangle. Esta forma de actuar se conoce con el nombre de
refactorización. Dependiendo de lo
complicada que sea la jerarquia completa, podría tratarse de una
tarea increiblemente compleja.
Sin embargo, algunas herramientas de desarrollo permiten
automatizar buena parte del proceso.
El mejor plan, especialmente para una jerarquía de gran tamaño,
consiste en pensar en ese tipo de
problemas durante el diseño y preguntarse cuál será el aspecto
más razonable de la jerarquía en el
futuro. Aunque, por supuesto, esto es fácil de decir, pero bastante
dificil de llevar a cabo.
Una filosofía similar se aplica a la hora de definir qué excepciones
deben enumerarse en la lista
de excepciones generadas por un método. Debido a la relación ES-
UN, cuando se sustituye un
método no pueden anadirse nuevas excepciones comprobadas a la
lista de excepciones generadas.
La implementación sustituta puede reducir la lista original de
excepciones comprobadas, pero nunca
ampliarla, Por ello, a la hora de determinar la lista de excepciones
generadas por un método, el
diseñador no solo debe pensar en las excepciones que puedan ser
generadas en ia implementación
actual del método, sino también en las excepciones que podrían
llegar a ser generadas por ese
método en el futuro (en caso de que cambie la implementación) y
en las excepciones que podrían ser
generadas por las implementaciones sustitutas proporcionadas en
las subclases futuras,
4.3
Herencia múltiple
Todos los ejemplos de herencia que hemos visto hasta ahora
hacian derivar
La herencia maltinle se usa
una clase de otra única clase base. En la herencia múltiple, una
clase puede
para dortras una case de
varias clases base. Javs no
derivar de más de una clase base. Por ejemplo, podemos tener una
clase
permite la hcrencia mxittiplo.
Student y una clase Emp 1oyee. Podríamos pensar en otra nueva
clase
StudentEmp loyee que derivara de ambas clases.
130
Capituio 4 Herencia
Aunque la herencia múltiple parece atractiva, y algunas lenguajes
(incluyendo C++) la soportan,
su gestión está llena de sutilezas que hacen que el diseño resulte
dificil. Por ejemplo, las dos clases
base pueden contener dos métodos que tengan la misma signatura
pero diferentes implementaciones.
Alternativamente, podrían tener dos campos con un nombre
idéntico; ¿cuál de los das habría que
utilizar?
Por ejemplo, suponga que en el caso de la clase StudentEmp loyee
anterior, Person es una clase
con el campo de datos name y el método toString. Suponga
también que Student amplía Person y
sustituye toString para añadir el año de graduación del estudiante.
Además, suponga que Employee
amplia Person pero no sustituye toString; en lugar de ello, declara
que es final.
Cuando hay implicadas muchas clases, los problemas son aun
mayores. Sin embargo, parece que
los problemas típicos con la herencia múltiple pueden atribuirse a
la existencia de implementaciones
conflictivas o de campos de datos conflictivos. `Como resultado,
Java no permite la herencia múltiple
de implementaciones.
Sin embargo, permitir la herencia múltiple con propósitos de
compatibilidad de tipos puede
resultar muy útil, siempre y cuando podamos garantizar que no
haya conflictos de implemen-
tación.
Volviendo a nuestro ejemplo de Shape, suponga que nuestra
jerarquia contiene muchas formas
geométricas como Circle, Square, ETlipse, Rec tang le, Triang le.
Suponga que para algunas de
estas formas geométricas, pero no para todas, tenemos un método
stretch, tal como se describe en
la Sección 4.2.2, que alarga la dimensión más grande, dejando las
restantes sin modificar. Podemos
pensar razonablemente que el método stretch está implementado
para ETTipse, Rectangle y
Triangle, pero no para Circle o Square. Imagine ahora que
quisiéramos un método para aplicar
stretch a todas las formas geométricas contenidas en una matriz:
)
public static votd stretchall( WhatType l 1 arr, factor
forc WhatType s : arr
[Link] factar ):
|
La idea es que stretchA1l funcionaría para la matrices de ETTipse,
las matrices de Rectangle,
las matrices de Triangle, o incluso para una matriz que contuviera
objetos ETlipse, Rectangle y
Triangle,
Para que este código funcione, tenemos que decidir cuál será la
declaración de tipo para
WhatType. Una posibilidad es que WhatType sea de tipo Shape,
siempre y cuando Shape tenga un
método abstracto stretch. De nuevo podríamos sustituir stretch en
cada tipo de Shape, haciendo
que Circle y Square generaran excepciones Unsupportedoperati on
Except ion. Pero, como he-
mos explicado en la Sección 4.2.2, esta solución parece violar la
noción de una relación ES-UN, y
adlemás no se puede generalizar bien a casos más complicados.
Otra idea sería tratar de definir una clase abstracta Stretchables
de la forma siguiente:
131
4.3 Herencia múltiple
abstract class Stretchable
l
public abstract vofd stretcht double factor ):
l
Podríamos utilizar Stretchable como el tipo que nos falta en el
método stretchA11. Si lo
hiciéramos así, tratariamos de que Rectangle, ETlipse y Triangle
ampliaran Stretchable
y
proporcionaran el método stretch:
/l No funciona
public class Rectangle extends Shape. Stretchable
[
)
public void stretch( double factor
.
public votd area ( )
.
La jerarquía que tendriamos llegados a este punto se muestra en
la Figura 4.15.
En principio, esto sería correcto, pero entonces tendríamos
herencia múltíple, que ya hemos
dicho anteriormente que es ilegal, debido al problema de la posible
herencia de implementaciones
conflictivas. Con lo que hemos dicho hasta ahora, solo la clase
Shape dispone de una implementación;
Stretchable es puramente abstracta, así que podría argumentarse
que el compilador debería
ser benévolo en este caso. Pero es posible que después de
compilarlo todo, Stretchable fuera
modificada para proporcionar una implementación, en cuyo caso
tendríamos un problema. Lo que
nos gustaría es disponer de algo más que de una promesa vacía de
contenido; necesitamos algún tipo
de sintaxis que obligue a Stretchable a carecer ahora y en un
futuro de implementación, Si esto
fuera posible, entonces el compilador podria permitir heredar de
dos clases, mediante una jerarquía
parecida a la mostrada en la Figura 4.15.
Esa sintaxis que buscamos es precisamente la de la interfaz..
Shape
Stretchable
.
Circle
Ellipse
Triangle
Square
Rectangle
Figura 4.15 Herencia de multples clases. Estonofunciona amenos
qe Shape oStretchable
sedisenen especilficamente como carentes de Implementación,
132
Capituio 4 Herencia
4. 4
La interfaz
La interfaz en Java es la clase más abstracta posible. Está
compuesta
La Imerfazes una
únicamente de métodas públicos abstractos y campos finales
estáticos
dase abstracta que no
contiene ningun delalle de
públicos.
implementadón.
Decimos que una clase implementa la interfaz si proporciona
definiciones
para todos los métodos abstractos de la interfaz. Una clase que
implemente la
interfaz se comporta como si hubiera ampliado una clase
abstracta especificada por la interfaz.
En principio, la diferencia principal entre una interfaz y una clase
abstracta es que, aunque
ambas proporcionan una especificación de lo que las subclases
deben hacer, a la interfaz no se
le permite proporcionar ningún detalle de implementación, ni en
forma de campos de datos ni de
métados implementados, EÌ efecto práctico de esto es que la
utilización de interfaces múltiples
o sufre las mismos problemas potenciales que la herencia
múltiple, ponque no pueden aparecer
implementaciones conflictivas. Así, aunque una clase solo puede
ampliar a otra clase, sin embargo
sÍ que puede implementar más de una interfaz.
4.4 . 1
Especificación de una interfaz
Sintácticamente, no hay nada más sencillo que especificar una
interfaz. La interfaz parece una
declaración de clase, salvo porque utiliza la palabra clave interf
ace. Está compuesta de un listado
de los métodos que hay que implementar. Un ejemplo sería la
interfaz. Stretchab le mostrada en la
Figura 4.16.
La interfaz Stretchable especifica el método que toda subclase
debe implementar: Stretch.
Observe que no tenemos que especificar que estos métodos sean
de tipo publicy abstract. Puesto
que estos modificadores son obligatorios para los métodos de una
interfaz, se pueden omitir y
usualmente se omiten.
4.4.2
Implementación de una interfaz
Una clase implementa una interfaz de la manera siguiente:
4
*/
5
public interface Stretchable
6
7
void stretch( double factor ):
8
|
Figura 4.16 La interfaz Stretchable
133
4.4La interfaz
En la Figura 4.17 se muestra un ejemplo. Aquí, completamos la
clase Rectangle, que hemos
utilizado en la Sección 4.2.
La linea 1 muestra que a la hora de implementar una interfaz
utilizamos
La cláusuta ip lements
la palabra clave imp lements en lugar de extends. Podemos
proporcionar
se uliiza para decarar que
cualquier método que deseemos, pero estamos obligados a
proporcionar
uina caso Implemenia una
iIntertaz, La clase debe
al menos los que se enumeran en la interfaz. La interfaz se
implementa
Implementar todos Ios
en la lineas 5 a 14. Observe que debenos implementar el método
exacto
metodos de la intesfaz o
coniinuara sienido abstrada.
especificado en la interfaz.
Una clase que implemente una interfaz puede ser ampliada por
herencia,
siempre y cuando no sea final. La clase derivada implementa
automáticamente
la interfaz..
Como podemos ver a partir de nuestro ejemplo, una clase que
implemente una interfaz puede
continuar ampliando mediante herencia alguna otra clase. La
cláusula extends debe preceder a la
cláusula imp lements.
4.4.3
Interfaces múltiples
Como hernos mencionado anteriormente, una clase puede
implementar múltiples interfaces. La
sintaxis para hacerlo es sencilla. Una clase implementa můltiples
interfaces de la manera siguiente:
La interfaz es la clase más abstracta posible y representa una
solución elegante para el problema
de la herencia múltiple.
public class Rectangle extends Shape imp lements Stretchable
1
2
{
3
/* El resto de la clase no cambia con respecto a 1a Figura 4.12 */
4.
5
)
public vo1d stretch( double factor
6
7
ifc factor C= 0 )
8
throw new Illega TAr gumentException( ):
9
10
ifc length > width )
11
length *= factor:
else
12
13
width An factor:
14
15
Figura 4.17 Laciase Rectangle (abrevladia), que Implemenla la
imerfaz Stretchable.
134
Capituio 4 Herencia
4.4.4
Las interfaces son clases abstractas
Puesto que una interfaz. es una clase abstracta, se aplican todas
las reglas de la herencia. Especí-
ficamente,
interfaces).
4. 5
Herencia fundamental en Java
Dos lugares importantes en los que se emplea la herencia en Java
son la clase Object y la jerarquía
de excepciones.
4.5.1
La clase Object
Java especifica que si una clase no amplía a otra clase, entonces
amplía implicitamente a la clase
Object (definida en java .1ang). Como resultado, toda clase es una
subclase directa o indirecta de
Dbject.
La clase Object contiene varios métodos, y puesto que no es
abstracta, todos ellos contienen
implementaciones, El método más comúnmente utilizado es
toString, del que ya hemos hablado.
4.5 Herencia fundamenta! en Java
135
Si no se escribe toString para una clase, se proporciona
automáticamente una implementación que
concatena el nombre de la clase, un @y el "código hash" de la
clase.
Otros métodos importantes son equals y hashCode, del que
hablaremos más detalladamente en
el Capítulo 6, así como un conjunto de métodos algo complicados
con los que los programadores
avanzados de Java necesitan familiarizarse.
4.5.2
La jerarquía de excepciones
Como se describe en la Sección 2.5, existen varios tipos de
excepciones. La raíz. de la jerarquía,
una parte de la cual se muestra en la Figura 4.18, es Throwable,
que define un conjunto de métodos
pr intStackTrace, proporciona una implementación de tostring, una
pareja de constructores y
poco más. La jerarquía se divide en Error, Runt imeException y
excepciones comprobadas. Una
excepción comprobada es cualquier objeto Exception que no sea
una Runt ImeExcept ion. Por
regla general, cada nueva clase amplia otra clase de excepción,
proporcionando solo un par de
constructores. Se pueden proporcionar más, pero ninguna de las
excepciones estándar se preocupa
Throwable
Error
Exception
[Link]
QutOtMemoryError
nternalError
RuntimeException
UnknownError
[Link] .FileNotFoundException
Nul!PointerException
AraylndexOUIOIEoundSExcepllon
ArithmeticException
UnsupporedOperallnExceplion
NoSuchMethodE xception
Invalid ArgumenIException
gavaullNoSuchElenen!Excepllon
[Link] xccplion
[Link]
ClassCastException
Figura 4.18 La jerarqula de excepciones (llsta parcial).
136
Capituio 4 Herencia
package [Link]:
1
2
3
public class NoSuche lemen tException extends Runt
imeException
4
l
5
/**
6
*
Construye una excepción NoSuchel ementException
7
*
sin ningún mensaje detallado.
8
*/
9
public NoSuche 1ementException (
10
I1
12
13
/*
14
*
Construye una excepción NoSuchEl ementException
15
*
con un mensaje detallado.
16
*
@param msg el mensaje deta l1ado.
17
*/
18
)
public NoSuche lementException( String msg
19
20
super( msg ):
21
22 l
Figura 4.19 MaSuchElementExceptfon, Implementada enwelss .util.
de hacerlo. En weiss. util, implementamos tres de las excepciones
estándar de java .util. Una
de esas implementaciones, que ilustra que las nuevas clases de
excepción suelen proporcionar poco
más que constructores se muestra en la Figura 4.19.
4.5.3
E/S: el patrón decorador
La E/S en Java parece bastante compleja de utilizar, pero funciona
muy bien para llevar a cabo la
E/S con diferentes orígenes, como el terminal, archivos o sockets
Internet, Puesto que está diseñada
para ser ampliable, existen un montón de clases (más de 50 en
total). Resulta complicado de utilizar
para tareas triviales; por ejemplo, leer un número desde el
terminal requiere un trabajo sustancial.
La entrada se hace utilizando clases de flujos de datos. Puesto
que Java fue diseñado para
programación Internet, la mayor de la E/S se centra alrededor de la
lectura y escritura orientada
a
bytes.
La EIS orientada a bytes se lleva a cabo con clases de flujos de
datos que amplían InputStream
o Outputstream. InputStream y Outputstream son clases
abstractas y no interfaces, por lo que
no existe el concepto de flujo de datos abierto tanto para entrada
como para salida. Estas clases
declaran sendos métodos read y write abstractos para E/S de un
único byte, respectivamente,
así como un pequeño conjunto de métodos concretas como el
método close y métodas de E/IS
4.5 Herencia fundamenta! en Java
137
en bloque (que pueden implementarse en términos de llamadas a
operaciones de E/S de un único
byte). Como ejemplos de estas clases se incluyen FileInputStream
y File0utputStream, así
como las clases ocultas SocketInputStream y
SocketOutputstream. (Los flujos de datos de tipo
socket son generados por métodos que devuelven un objeto cuyo
tipo estático es InputStream o
OutputStream.)
La E/S orientada a caracteres se lleva a cabo mediante clases que
amplían
Lasdases
las clases abstractas Reader y Writer. Esta contienen también
métodos read
InputStreamReader y
OutputStrea=writer
ywrite. No existen tantas clases Reader y Writer como clases
InputStream
sun dases puente que
y Outputstream.
permien al programador
Sin embargo, esto no es problema gracias a las clases
InputStreamReader
cruzar de a jererqula
Streama las jerarqulas
y OutputStrea imWriter. Estas clases de denominan clase puente
porque
Readerywriter.
enlazan la jerarquía Stream con las jerarquias Reader y Writer. Un
Input-
StreamReader se construye con cualquier InputStream y crea un
objeto
que ES-UN Reader. Por ejemplo, podemos crear un Reader para
archivos
utilizando
InputStream fis = new FileInputStre am ( "foo. txt" ):
Reader fin = new InputStreamReader ( fis ):
Resulta que existe una clase FileReader de utilidad que ya se
encarga de hacer esto. La Figura
4.20 proporciona una posible implementación.
A partir de un Reader, podemos realizar una E/S limitada; el
método read devuelve un carácter.
Si queremos en su lugar una linea, necesitamos una clase
denominada BufferedReader. Al igual
que otros objetos Reader, un Bufferedreader se construye a partir
de cualquier otro Reader, pero
proporciona tanto un buffer de entrada como un método readline.
Por tanto, continuando con el
ejemplo anterior,
BufferedReader bin - new Bufferedre ader( fin ):
Envolver un InputStreamdentro de un InputStreamReader y reste
dentro de un Bufferedreader
funciona para cualquier InputStream, incluyendo System. in o
sockets. La Figura 4.21, que se ase-
meja a la Figura 2.i7, ilustra el uso de este patrón para leer dos
números desde la entrada estándar.
La idea de envolver una clase con otra es un ejemplo de un patrón
de diseño Java comúnmente
utilizado y con el que nos volverernos a encontrar en la Sección
4.6.2.
Similar a BufferedReader es Pri nthriter, que nos permite realizar
operaciones println.
La jerarquía OutputStream incluye varios envoltorios„ como por
ejemplo Data0utputStream,
ObjectOutputStream y GZIPOutputStream.
class FileReader extends Inputstrea mReader
1
2
3
public FileReader( String name ) throws FileNotFoundExcept ion
4
L
super( new FileInputStream( name ) ):
5
I
Figura 4.20La clase de utilldad FIleReader.
138
Capitulo 4 Herencia
1
import java. 1o .Input StreamRea der:
2
import java. io. .Bufferedre ader:
3
import java. io. IOException:
4
import java. [Link]:
5
import java. util -NoSuchel ementException:
6
7
class MaxTest
8
l
9
public static void main( String L 1 args
)
10
BufferedReader in - new BufferedReader ( new
11
InputStreamReader ( System. in ) ):
12
13
14
System .out ·printlnc "Enter 2 ints on one line: *. ):
15
try
16
l
17
String oneline = in. readline( ):
18
ff( onel ine -- null )
19
return:
20
Scanner str = new Scanner( oneline ):
21
int x - str. nextInt( ):
22
int y z str. nextIntt ):
23
24
25
System .out- println( "Max: a + Math. max( x. y ) ):
)
26
)
27
catch( IOException e
28
System -err .println( "Unexpected I/0 error" ):
)
l
29
catch( NoSuchETementException e
)
30
System -err .printlnc "Error: need two ints" ): )
l
31
32 1
Figura 4.21 Unprograma que llustra como envalver flujos dentro de
clases lectoras,
DataOutputStream nos permite escribir primitivas en forma binaria
(en lugar de en un formato
de texto legible); por ejemplo, una llamada a writelnt escribe los 4
bytes que representan un
entero de 32 bits. El escribir los datos de esa forma evita las
conversiones a formato de texto, lo
que permite ahorrar tiempo y (en ocasiones) espacio.
ObjectOutputStream nos permite escribir en
un flujo de datos un objeto completo, incluyendo todos sus
componentes, los componentes de sus
componentes, etc. El objeto y todos suS componentes deben
implementar la interfaz Serial1zable.
4.5 Herencia fundamenta! en Java
139
Na hay ningún método en la interfaz:; lo único que se debe
declarar es que una clase es serializable."
GZIPOutputStream envuelve Outputstream y comprime los datos
de salida antes de enviarlos
a
OutputStream. Además, hay una clase Buff ered0utputStream. En
el lado de InputStream existen
unas clases envoltorios similares. Por ejemplo, suponga que
tenemos una matriz de abjetos Person
serializables. Podemos escribir los objetos como una unidad,
comprimidos de la forma siguiente:
Person L 1 p - getPersons( ): H/ rellenar 1a matriz
FileOutput St ream fout = new Filedut putStream( "people. .gzip" ):
BufferedOutputStream bout = new BufferedOutputStream( fout ):
GZIPOutputStream gout = new GZIPOut putStream( bout ):
ObjectOutputStream oout = new ObjectOutputStream( gout ):
oout .write0bject( P ):
oout .closel ):
Más adelante, podemos volver a leerlo todo:
File InputStrean fin = new FileInputStream( "people -gzip" ):
BufferedInputStream bin - new BufferedInputStream( fin ):
GZIPInputStream gin - new GZIPInput Stream( bin ):
Object InputStream o1n - new Objectl nputStream( gin ):
Person C 1 P = (Personl 1) oin. read0bject( );:
oin. close( ):
El códiga proporcionado en línea amplía este ejemplo, haciendo
que cada objeto Person
almacene un nombre, una fecha de nacimiento y los dos objetos
Person que representan a los padres.
La idea de anidar envoltorios con el fin de añadir funcionalidad se
conoce con el nombre de patrón decorador: Al hacer esto,
disponemos de
La kiea de empicar
numerosas clases de pequeño tamaño que se combinan para
proporcionar
emohorios anidados con el
fin de anadir funcionalidad
una potente interfaz. Sin este patrón, cada origen de E/S tendría
que
se conoce con el nomibre
disponer de su propia funcionalidad para comprensión,
serialización, EIS
de pairdin decorador.
orientada a carácter, E/S orientada a byte, etc. Utilizando el patrón,
cada
origen es solo responsable de una E/S básica mínima, y las
caracteristicas
adlicionales son añadidas por las clases decoradoras,
B Larazón de esto es que la serialización os de manera
predeterminada, insegura. Cuandio se escribe un objeto en un
objectOutputStrean,
dl formato es blen conocido, por lo que sus miembros privados
pueden ser leidos pox un usuarlo maliciosn. De forma similar,
cuando
se vuelve a leer un objeto asf escrito, las datos cel flujo de datas
de entrada no comprueban para verificar su corección, por lo que
es posible leer un objeto corrupto, Existen técnicas avarzxlas que
pueden emplearse para garantizar la seguridad de la integridad en
CaO de emplear la serlalización, pero esa técnica queda fuera- del
alcance de este texto, Los disenadores de la librería de
serializncion
pensaron que la serialización To deberfa ser el mexcanismo
predeterminedo, porque un LisO correcto de la misma requlere
conocer estes
problenas, y esa es la razón de que pusleran un pequeño obstáculo
en el camino.
140
Capituio 4 Herencia
4. 6
Implementación de componentes genéricos
mediante la herencia
Recuerde que un objetivo importante de la programación orientada
a objetos es
La programacidn genérica
nos permile implementar
facilitar la reutilización del código. Un mecanismo importante que
apoya este
logica dependiente del tipo
objetivo es el mecanismo de los genéricos: si la implementación
es idéntica,
de objolo.
salvo por lo que se refiere al tipo básico del objeto, podemos
utilizar una
implementación genérica para describir la funcionalidad básica.
Por ejemplo,
podemos escribir un método para ordenar una matriz. de
elementos; la lógica es independiente
del tipo de los objetos que se estén ordenando, por lo que podría
utilizarse un método genérico.
A diferencia de muchos de los lenguajes más recientes (como C++,
que
En Jarva, el caracter
utilizan plantillas para implementar la programación genérica),
antes de la
generico se obtiene
versión i.5, Java no soportaba directamente las implementaciones
genéricas,
utit zanco la herenda.
En lugar de ello, la programación genérica se implementaba
utilizando
los conceptos básicos de la herencia. Esta sección describe cómo
pueden
implementarse métodos y clases genéricos en Java utilizando los
principios básicos de herencia.
El soporte directo para métodos y clases genéricos fue anunciado
por Sun en junio de 2001 como
futura adición al lenguaje. Finalmente, a finales de 2004, se lanzó
la versión Java 5 y se proporcionó
un soporte para métodos y clases genéricas. Sin embargo, la
utilización de clases genéricas re-
quiere comprender las estructuras sintácticas de programación
genérica anteriores a Java 5. Como
resultado, comprender cómo se emplea la herencia para
implementar programas genéricos resulta
esencial, e incluso en la versión Java 5.
4.6. 1
Utilización de Object para la programación genérica
La idea básica en Java es que podemos implementar una clase
genérica utilizando una superclase
apropiada, como Object.
Considere la clase IntCe1l mostrada en la Figura 3.2. Recuerde
que IntCell soporta los
métodos read y write. En principio, podemas convertir esto en una
clase Memoryce1l genérica que
almacene cualquier tipo de Object, sustituyendo las instancias de
int por Object. En la Figura 4.22
se muestra la clase MemoryCe11 resultante.
Hay dos detalles que debemos considerar al utilizar esta
estrategia. El primero se ilustra en la
Figura 4.23, que muestra un método ma in que escribe un "37" en
un objeto MemoryCe11 y luego
lee del objeto MemoryCe11. Para acceder a un método especifico
del objeto, debemos hacer una
especialización al tipo cortecto. (Por supuesto, en este ejemplo no
necesitamos la especialización,
puesto que simplemente estamos invocando el método tostring en
la linea 9, y esto puede hacerse
para cualquier objeto.)
Un segundo detalle de importancia es que no se pueden utilizar los
tipos primitivos. Solo los
tipos de referencia son compatibles con Object. En breve
expondremos una solución bastante
común a este problema.
MemoryCe1l es un ejemplo relativamente sencillo. La Figura 4.24
constituye un ejemplo de más
entidad y que es típico de la reutilización de código genérico; en
ella se muestra una clase genérica
Arraylist simplificada tal y como se escribiría antes de Java 5,
pudiendo el lector interesado
encontrar algunos métodos adicionales en el código que se
suministra en linea.
141
4.6 Implementación de componentes genéricos mediante la
herencia
1 I/ Clase MemoryCe11
2 I/ Object read( )
--> Devuelve el valor alacenado
3 // void writet Object x ) --) Se almacena x
4
5
public class Memor 'yCe1l
6
l
7
If Métodos públicos
8
}
public Object read( ). l return stor edvalue:
9
public void writel Object x I storedvalue - x: I
10
11
/l Representación privada interna de los datos
12
private Object storedvalue:
13 I
Figura 4.22 Unaclase HemoryCell generica (pre-Java 5).
1
public class TestMemoryCe11
2
3
)
public static void main( String C 1 args
4
5
MemoryCe1l m = new MemoryCe11( ):
6
7
TI writec "37 " ):
B
String val = (String) m .read( ):
g
System .out ·printlnc "Contents are: + val ):
10
I1 1
Figura 4.23 Utllizacidn de la clase Meaorycell genesica (pre- Java
5).
4.6.2
Envoltorios para tipos primitivos
Cuando implementamos algoritmos, a menudo nos encontramos
con un problema de tipos en el
lenguaje: disponemos de un objeto de un tipo, pero la sintaxis del
lenguaje requiere un objeto de un
tipo distinto.
Una clse envoltarfo
Esta técnica ilustra el concepto básico de clase envoltorio. Una
aplicación
almacena uina entidad (la
dase ernuelta) yanade
tipica consiste en almacenar un tipo primitivo y añadir
operaciones que el
cperaciones que el tipo
tipo primitivo no soporta o no soporta correctamente. Un segundo
ejemplo
originai no soportaba
lo hemos visto en el sistema de EIS, en el que un envoltorio
almacena una
cxrectamenie. Una dase
sdaplstora s0 utilza
referencia a un objeto y reenvía solicitudes hacia ese objeto,
embelleciendo
cuando la interfaz de
el resultado de alguna manera (por ejemplo, añadiendo un buffer o
un
una dase no coindidie
mecanismo de compresión). Un concepto similar es el de clase
adaptadora
exadamente con la que
necesitamos.
(de hecho, los têrminos envoltorio y adaptador se utilizan a
menudo de
manera intercambiable). Una clase adaptadora se utiliza
típicamente cuando
142
Capitulo 4 Herencia
1
/**
* Simp leArraylist i mp7 ementa una matriz a mpl iable de Object.
2
* Las inserciones siempre se hacen al final,
3
*/
4
public class SimpleArraylist
5
6
/* k
7
8
9
*/
10
public int sizel )
11
12
13
return theSize:
14
15
/**
16
*
17
Devuelve el e lemento en la posición idx.
*
®param idx e1 indice que hay que buscar.
18
*
19
@throws Array Index0utOfBounds Exception s1 e1 indice es
Incorrecto.
*/
20
)
21
public Object get( int idx
22
I. idx 2o size( ) )
if( idx < a
23
24
throw new ArrayIndexOutOfBoundsExcept ion( ) :
return the Itens[ idx ]:
25
26
27
/**
28
29
30
31
*/
32
)
33
public boolean add( Object x
34
)
if( theItems. length = sizel )
35
l
36
37
Object L J o1d - the Items:
38
theItems - new Objectl the Items. length * 2 * 1 1;:
)
39.
fort int 1 = 0: 1 < size( ): itt
40
theltems[ 1 = old[ 1 1:
)
41
42
43
theltemsl thesizert 1 = x:
44
return true:
45
46
private static final int INIT CAPACITY - 10:
47
48
private int thesize = 0:
49.
private Object L 1 theItems - new Objectl INIT_ CAPACITY J:
50
51 )
Figura 4.24 Lin Arraylist simpllficado con add, getysize (pre-Java
5).
143
4.6 Implementaclón de componentes genéricos mediante la
herencia
la interfaz. de una clase no es exactamente la que necesitamos, y
proporciona un efecto de envoltorio
al mismo tiempo que modifica la interfaz.
En Java, ya hemos visto que, aunque todo tipo de referencia es
compatible con Object, los ocho
tipos primitivas no lo son. Čoma resultado, Java proporciona una
clase envoltorio para cada uno
de los ocho tipos primitivos. Por ejemplo, el envoltoria para un tipo
int es Integer. Cada objeto
envoltorio es inmutable (lo que quiere decir que su estado no
puede variar nunca), almacena un
valor primitivo que se configura en el momento de construir el
objeto y proporciona un método para
consultar ese valor, Las clases envoltorio también contienen
diversos métodos estáticas de utilidad.
Como ejemplo, la Figura 4.25 muestra cómo podemos utilizar el
Arraylist de Java 5 para
almacenar enteros. Observe especialmente que no podemos
utilizar Arraylist<int>,
4.6.3
Autoboxing/unboxing
El código de la Figura 4.25 resulta incómodo de escribir, porque la
utilización de la clase envoltorio
requiere crear un objeto Integer antes de la llanada a add, y luego
extraer el valor int del Integer,
utilizando el método intValue. Antes de Java 1,4, esto era
obligatorio, porque si se pasaba un int
en algún lugar donde hacía falta un objeto Integer, el compilador
generaba un mensaje de error,
como también lo generaba sise asignaba el resultado de un objeto
Integer a un int. Este código de
la Figura 4.25 refleja con precisión la distinción entre tipos
primitivos y tipos de referencia, pero no
consigue expresar de manera limpia la intención del programador
de almacenar valores int dentro
de la colección,
Java 5 rectifica esta situación. Si se pasa un int en un lugar donde
hace falta un Integer, el
compilador inserta entre bastidores una llamada al constructor
Integer. Esta funcionalidad se
conoce con el nombre de autoboxing o auto-envolvimiento. Si se
pasa un Integer en un lugar
donde hace falta un int, el compilador inserta una llamada entre
bastidores al método intvalue.
Esto se conoce con el nombre de auto-unboxingo auto-
desenvolvimiento. Para los otros siete pares
de primitiva/envoltorio se manifiesta un comportamiento similar.
La Figura 4.26 ilustra el uso del
1
import java. util .Arraylist:
2
3
public class BoxingDemo
4
5
public static void main( String L 1 args
)
6
7
Arrayl ist<Integer> arr = new Arraylist<Integer>( ):
8
9
arr. add( new Integer ( 46 ) ):
10.
Integer wrapperval = [Link]( 0 );
11
int val - wrapperVal . intValue( ):
System. out -println( "Posttion 0: t. * val ):
12
13
14 }
Figura 4.25 Una llustracion de lactase enroltorio Integer utllizando
el Arraylist genárico de Java 5.
144
Capituio 4 Herencia
1
import java. util .Arraylist:
2
3
public class BoxingDemo
4
5
)
public static void ma in( String C 1 args
6
7
Arraylist<Integer> arr = new Arraylist<Integer>( ):
g
g
arr. add( 46 ):
10
int val = arr. get( 0 ):
System .out -println( "Position 0: * + val ):
11
12
13 |
Figura 4.26 Aufoboxing y unbaxing.
auto-envolvimiento y el desenvolvimiento. Observe que las
entidades referenciadas en el Arraylist
siguen siendo objetos Integer; no se puede utilizar int en lugar de
Integer en las instantaciones
de Arraylist.
Adaptadores: modificación de una interfaz
4.6.4
El patron adaptador se utiliza para modificar la interfaz de una
clase existente, con el fin de
adaptarla a otra especificación, En ocasiones se utiliza para
proporcionar una interfaz más simple,
bien con un número menor de métodos o bien con métodos más
fáciles de
E! patrón adaplador se
uilizar. Otras veces, se emplea simplemente para cambiar los
nombres
util za para modificar la
de algunos métodos. En cualquier caso, la técnica de
implementación es
interfaz de una clase
existente, con elfin
similar.
de adaptarla a otra
Ya hemos visto un ejemplo de adaptador: las clases puente
InputStream-
especificacón.
Reader y Outputstre amWr 'iter que convierten flujos de datos
orientados a
byte en flujos de datos orientados a carácter.
Como ejemplo adicional, nuestra clase Memo ryCe1l de la Sección
4.6.1 utiliza read y write.
¿Pero qué sucedería si quisiéramos que la interfaz empleara get y
put en lugar de esos métodas?
Hay dos alternativas razonables. Una consiste en cortar y pegar
una clase completamente nueva,
La otra es usar la técnica de composición, en la que diseñamos
una nueva clase que envuelve el
comportamiento de una clase existente.
En la Figura 4.27 utilizamos esta técnica para implementar la
nueva clase, StorageCe11.
Sus métodos están implementados mediante llamadas a la clase
MemoryCell envuelta. Es
tentador utilizar la herencia en lugar de la composición, pero la
herencia suplementa la inter-
faz (es decir, añade métodos adicionales, pero dejando los
originales). Si ese es el comporta-
miento apropiado que buscamos, entonces por supuesto que la
herencia puede ser preferible a
la composición.
145
4.6 Implementaclón de componentes genéricos mediante la
herencia
1 H/ A class for simulating a memory ce1l.
2
public class StorageCe11
{
3
4
public Object get( )
5
[
return [Link]( ): }
6
7
public void putl Object x )
8
[Link]( x ):
9
10
private MemoryCe1l m = new MemoryCe11( ):
11 |
Figura 4.27 Una iclase adapiadora que modilica la Interfaz de
Hemorycell para ulilizar gety put.
4.6.5
Utilización de tipos de interfaz para la
programación genérica
Utilizar Object corno un tipo genérico funciona solo si las
operaciones que se están realizando
pueden expresarse empleando solo los métados disponibles en la
clase Object.
Considere, por ejemplo, el problema de localizar el elemento
máximo de una matriz de
elementos. El código básico es independiente del tipo, pero
requiere la capacidad de comparar
cualesquiera dos objetos y decidir cuál de ellos es más grande y
cuál es más pequeño. Por ejemplo,
he aqui el código básico para encontrar el BigInteger máximo
dentro de una matriz:
)
public static BigInteger findMax( BigInteger L 1 arr
l
int maxIndex = 0:
forc int 1 - 1; f < arr .length: 1+ )
)
ifc arrli]. .compa reTo( arrl maxIndex 1 s 0
maxIndex - i:
return arrl maxl ndex 1:
Encontrar el elemento máximo en una matriz de String, donde el
máximo se entiende desde el
punto de vista lexicográfico (es decir, el último elemento
alfabéticamente) utiliza el mismo código
básico.
}
public static String findMax( String t 1 arr
l
int maxIndex - 0:
forl int i - 1: i < arr. 1ength: 1+ )
146
Capituio 4 Herencia
ifc arrlil. .compa reTo( arrl maxIndex ] < 0
maxIndex = 1 :
return arrl maxIndex 1:
)
Si deseamos que findMax funcione para ambos tipos o incluso
para otros tipos que también
dispongan de un método compareTo, entonces deberíamos poder
hacerlo, siempre y cuando
seamos capaces de identificar un tipo que sirva como unificador.
En este sentido, resulta que el
lenguaje Java define la interfaz Comparable, que contiene un
método compareTo. Muchas clases
de Äibrería implementan esta interfaz., y también nasotros
podemos implementarla en nuestras
propias clases. La Figura 4.28 muestra la jerarquía básica. Las
versiones anteriores de Java
requerían que las parámetros de compareTo se enumeraran como
de tipo Object; las versiones
más recientes (desde Java 5) hacen que Comparable sea una
interfaz. genérica, que veremos en la
Sección 4.7 .
Con esta interfaz, podemos simplemente escribir la rutina findMax
de modo que acepte una
matriz de objetos Compa rable. El estilo más antiguo, anterior a los
genéricos, para findMax se
muestra en la Figura 4.29, junto con un programa de prueba.
Es importante mencionar unas cuantas desventajas, En primer
lugar, solo pueden pasarse como
elementos de la matriz Compa rable los objetos que implementen
la interfaz. Comparable. Los
objetos que dispongan de un método compareTo pero no declaren
que implementan Comparable no
son Comparable, y no satisfacen el requisito de que se trate de
una relación del tipo ES-UN.
En segundo lugar, si una matriz Comparable tuviera dos objetos
que son incompatibles (por
ejemplo, un objeto Date y otra BigInteger). el método compareTo
generaría una ClassCast-
Exception. Este es el comportamiento esperado (de hecho, es el
comportamiento requerido.
En tercer lugar, como antes, las primitivas no se pueden pasar
como objetos Comparable, pero
los envoltorios SI que funcionan porque implementan la interlaz
Comparable.
En cuarto lugar, no se exige que la interfaz sea una interfaz de
librería estándar,
Por último, esta solución no siempre funciona, porque podría ser
imposible declarar que una clase
implementa una interfaz. necesaria. Por ejemplo, ia clase podría
ser una clase de libreria, mientras
que la interfaz. es una interfaz definida por el usuario, Ysi la clase
es final, ni siquiera podemos crear
una nueva clase. La Sección 4.8 ofrece otra solución para este
problema, que es el objeto función.
El objeto función utiliza también interfaces, y es quizá uno de los
conceptos fundamentales que nos
podemos encontrar dentro de la librería Java.
Comparable
4
Date
String
BigInteger
Figura 4.28 Tres clases que Implemenlan la interfaz Comparable,
147
4.7 Implementación de componentes genéricos con los
componentes genéricos de Java 5
import java. ma th. .BigInteger:
1
2
class FindMaxDemo
3
4
{
5
/* *
6
*
Devuelve el elemento máximo en a.
7
*
Precond ición: a .length > 0
8
*/
)
9
public static Comparable findMax( Comparable C J a
10
int maxIndex - 0 :
11
12
13
for( int i = 1: i < [Link]: i++ )
14
)
if( aL i ].compareTo( al maxIndex 1 )> O
15
maxIndex - 1;
16
17
return al maxIndex 1:
18
19
20
/**
21
* Probar findMax con objetos BigInteger y String.
22
*/
23
public static void maint String C args
)
24
25
BigInteger L ] bil = f new BigInteger( "8764 " ) .
26
new BigInteger( "29345" )
27
new BigInteger( "1818" ) I:
28
String c ] stl = t "Joe" , "Bob" , "B111" , "Zeke" }
29
30
31
System .out. printin( findMax( bil ) ):
32
System .out printin( findMax( stl ) ):
33
34 )
Figura 4.29 Una rulina findMax genérica, con un prograrma de
demostracion que utlliza formas geamétricas
ycadenas dle caracteres (pre-Java 5).
4.7
Implementación de componentes genéricos
con los componentes genéricos de Java 5
Ya hemos visto que Java 5 soporta las clases genéricas y que
estas clases son fáciles de utilizar, Sin
embargo, la escritura de clases genéricas requiere algo más de
trabajo. En esta sección, vamos a
ilustrar los fundamentos de la escritura de clases y métodos
genéricos. No pretendemos cubrir todas
las estructura relevantes del lenguaje, que son bastante complejas
y en ocasiones enganosas, En su
lugar, mostraremos la sintaxis y las estructuras más comunes que
se utilizan a lo largo de este libro.
148
Capitulo 4 Herencia
4.7 . 1
Interfaces y clases genéricas simples
La Figura 4.30 muestra una versión genérica de la clase
MemoryCell que antes hemos presentado
en la Figura 4.22. Aqui, hemos cambiado el nombre a Generi
cMemoryCe11 porque ninguna de las
clases se encuentra en un paquete y por tanto los nombres no
pueden coincidir.
Cuando se especifica una clase genérica, la declaración de la
clase incluye
Cuanoo se especica
uno 0 más parámetros de tipo encerrados entre corchetes
angulares <>
una dase generica, la
después del nombre de la clase. La linea 1 muestra que Generi
cMemoryCe11
decaración do la caso
induye uno o más
admite un parámetro de tipo. En este caso, no hay restricciones
explicitas
parametros de lipo,
sobre el parámetro de tipo, por lo que el usuario puede crear tipos
como
encertados entre corchetes
Generi cMemoryCe11<String> y Generi cMemoryCe11<Integer>
pero no
angulares 3, después del
nomibre de la dase.
Generi cMemoryCell<int>. Dentro de la declaración de la clase
Generic-
MemoryCe1l, podemos declarar campos del tipo genérico y
métodos que
utilicen el tipo genérico como parámetro o tipo de retorno.
Las inlerfaces lámbién
Las interfaces también pueden declararse como genéricas. Por
ejemplo,
pucden dederarse COmO
antes de Java 5, la interfaz Comparable no era genérica y. su
método
genericas,
campareTo tomaba un Object como parámetro. Como resultado,
cualquier
variable de referencia pasada al método compareTo se podía
compilar, incluso
si la variable no era de un tipo adecuado, y solo se informaba del
error en tiempo de ejecución
mediante una excepción Classcas tExcepti on. En Java 5, la clase
Comparable es genérica, como
se muestra en la Figura 4.31. Por ejemplo, la clase String ahora
implementa Compa rable<String>
y tiene un método compa reta que admite un String como
parámetro. Haciendo la clase genérica,
muchos de los errores que antes solo se señalizaban en tiempo de
ejecución han pasado a convertirse
en errores de tiempo de compilación.
4.7.2
Comodines con límites
En la Figura 4.13 vimos un método estático que calculaba el área
total en una matriz de objetos
Shape. Suponga que queremos reescribir el método de manera que
funcione un parámetro que sea
Arraylist<Shape>. Debido al bucle for avanzado, el código deberá
ser idéntico y el resultado se
muestra en la Figura 4.32. Si pasamos un Arraylist<Shape>, el
código funciona. Sin embargo,
¿qué ocurre si pasamos un Arraylist<Squa re>? L.a respuesta
depende de si un Arraylist<Squa re>
ES-UN Arraylist<Shape). Recuerde de la Sección 4.1.10 que el
término técnico para esto es si
tenemos covarianza o no.
public class Gener 1cMemor yCe1 I<AnyType>
1
2
l
a
public AnyType readt )
4
[
)
return storedvalue:
5
)
public void writet AnyType x
6
t
)
storedValue un x:
7
a
private AnyType storedvalue;:
9
)
Figura 4.30 impiementación genorica dela clase Memorycell.
4.7 Implementación de componentes genéricos con los
componentes genéricos de Java 5
149
package java .lang:
1
2
3
public Inter face Comparable<AnyType>
4
{
5
public int compa reTo( AnyType other ):
6}
Figura 4.31 Interfaz Comparable. Versidn Java 5„ que es generica.
En Java, como hemos mencionado en la Sección 4.1.10, las
matrices son covariantes. Por tanto,
Square[] ES-UN Shape[1. Por un lado, la coherencia sugeriría que si
las matrices son covariantes,
entonces las colecciones también deberían serlo. Por otro lado,
como vimos en la Sección 4.1.10, la
covarianza de matrices conduce a la obtención de código que se
compila pero que luego genera una
excepción de tiempo de ejecución (un ArrayStoreExcept i on).
Puesto que la
única razón de tener genêricos es generar errores de compilación
en lugar de
Las colecciones genericas
no son covarlantes.
excepciones de tiempo de ejecución cuando haya
desadaptaciones de tipos,
las colecciones genéricas no son covariantes. Como resultado, no
podemos
pasar un Arraylist<Square> como parámetro al método de la Figura
4.32.
La conclusión es que los genéricos ly las colecciones genéricas)
no son covariantes (lo que tiene
sntido), mientras que las matrices sÎ lo son. Sin sintaxis adicional,
los usuarios tenderían a evitar las
colecciones, porque la falta de covarianza hace que el código sea
menos flexible.
Java 5 solventa este problema mediante comodines. Los
comodines se utilizan para expresar
subclases (o superclases) de tipos de parámetros. La Figura 4.33
ilustra el uso de comodines con
un limite para escribir un método total Area que admite como
parámetro un
Arraylist<t>, donde T ES-UN Shape. Así, tanto Arraylist<Shape>
como
Los comodines se utiizan
para expeesar subdases
Arraylist<Square> serían parámetros aceptables. Los comodines
también
[p superclases) de lipos de
pueden utilizarse sin un límite (en cuyo caso se da por supuesto
que lo que se
parámolros
pretende es incluir extends Object) o con super en lugar de
extends (para
referirse a una superclase en lugar de a una subclase); hay
también algunos
otros usos sintácticos de los que no vamos a hablar aquí,
1
public static double tota 1Areat Arraylist<Shape> arr )
2
3
double total = 0:
4
5
for( Shape 5 : arr )
6
if( s != null )
7
total += [Link]( ):
8
9
return total:
10 l
Fgura 4.32 Método tota 1Area que no funclona si sele pasd un
Arrayl 1st<Square>.
150
Capituio 4 Herencia
public static double totalArea( Arraylist<? extends Shape> arr )
1
2
l
double total m 0:
3
4
5
fort Shape s : arr )
6
ifl s != nul1 )
7
total + [Link]( ):
8
9
return total:
10
Figura 4.33 Mélodo totalArea revisadocon comodinesy Ique
funciona si se le pasa un Arraylist<Square>.
4.7.3
Métodos estáticos genéricos
En un cierto sentido, el método totalArea de la Figura 4.33 es
El melodo gencrico se
genérico, ya que funciona para distintos tipos. Pero no hay una
lista
asomeja bastanto a la dast
gnerica, en ei sentido de
específica de parámetros de tipo, como se hacía en la declaración
de la clase
que a lista de parámetros
Generi cMemoryce11. En ocasiones, el tipo específico es
importante, quizá
de tipo utilza la misma
porque es aplicable una de las siguientes razones:
sintaxis La lisla de lipos
en un metodo gencrico
precede eltipo de retorno.
En caso de aplicarse alguna de estas razones, entonces hay que
declarar un método genérico
explícito con parámetros de tipo.
Por ejemplo, la Figura 4,34 ilustra un método estático genérico que
realiza una búsqueda
secuencial del valor X en la matriz arr. Utilizando un método
genérico en lugar de uno no genérico
que emplee Object como tipo de parámetro, podemos obtener
errores de tiempo de compilación si
tratamos de encontrar un objeto Apple en una matriz de objetos
Shape.
3
for( AnyType val : arr
)
4
5
if( [Link] Is( val ) )
6
return true:
7
B
returin false:
g
}
Figura 4.34 Mélod0 estálico genérico para buscar en una matriz.
151
4.7 Implementación de componentes genéricos con los
componentes genéricos de Java 5
El método genérico se parece bastante a la clase genérica, en el
sentido de que la lista de
parámetros de tipo utiliza la misma sintaxis. Los parâmetros de
tipo en un método genérico preceden
al tipo de retorno.
4.7.4
Límites de tipo
Suponga que queremos escribir una rutina findMax. Considere el
código de
EI limile de lipo se
la Figura 4.35. Este código no puede funcionar, porque el
compilador no
especifica dentro de los
corchetes angulares o
puede comprobar que la llamada a compareTo en la linea 6 es
válida; solo se
garantiza que compareTo exista si Any Type es Comparable.
Podemos resolver
este problema utilizando un límite de tipo. El límite de tipo se
especifica
dentro de los corchetes angulares <>,y especifica las propiedades
que los tipos de parámetro deben
tener. Un intento simplista sería reescribir la signatura como
public static KAnyType extends Comparable>
Esto es simplista porque, como ya sabemos, la interfaz
Comparable ahora es genérica. Aunque
este código se compilaría, una mejor solución sería
public static KAny Type extends Comparable<anyType>>
Sin embargo, este intento no es satisfactorio. Para ver dónde está
el problema, suponga que
Shape implementa Comparabl e<Shape>. Suponga que Square
amplia Shape. Entonces, todo
lo que sabernos es que Square implementa Comparab le<Shape>.
Por tanto, un Square ES-UN
Comparable<Shape>, pero NO-ES-ÜN Comparab le<Square).
Como resultado, lo que necesitamos decir es que AnyType ES-UN
Comparable<t>, donde T es
una superclase de AnyType, Puesto que no necesitamos conocer el
tipo exacto T, podemos emplear
un comodín, La signatura resultante es:
public static <Any Type extends Comparable<? super AnyType>>
La Figura 4.36 muestra la implernentación de findMax. El
compilador aceptará matrices
únicamente de tipos T que implementen la interfaz Comparab
le<s>, donde T ES-UNS. Ciertamente,
1
)
public static <AnyType> AnyType findMax( AnyType L1 a
2
l
3
int maxIndex = 0 :
4
5
for( int 1 = 1; f < a .Tength: itt )
6
if( aL i 1. campareTo( al maxIndex 1 ) > 0
)
7
maxIndex - 1:
B
9
return al maxIndex J:
10 }
Figura 4.35 Mélodo estálico generico para encontrar el elemento
mas grander de una matrizy queno funciona adecuadanente.
152
Capituio 4 Herencia
public static <AnyType extends Comparable<? super AnyType>>
1
2
)
AnyType findMax( AnyType L 1 a
[
3
4
int maxIndex - 0:
5
6
forc int i = 1: i < a .length: i+t
)
7
)
ifl aL i J .compa reTol a L maxIndex 1 } > O
8
maxIndex = i:
9
return al maxIndex 1:
10
11 |
Fgura 4.36 Método estitco genérico pata encontrar el mayor
elemento dentro de una matriz. llustra un llmile Impvasto al
parametro de tipo.
la declaración de limites parece un poco liosa. Afortunadamente,
no nos vamos a topar con otra casa
más complicada que esta estructura sintáctica,
Borrado de tipos
4.7.5
Los tipos genéricos, en su mayor parte, son estructuras del
lenguaje Java,
Las clase genericas
pero no existen como tales en la Máquina Virtual. Las clases
genêricas son
son convertidas por el
convertidas por el compilador en clases no genéricas mediante un
proceso
compllador endases n0
genericas mediante un
conocido con el nombre de borrado de tipos La versión
simplificada de lo
proceso conocido con el
que sucede es que el compilador genera una clase en bruto con el
mismo
noimibre da borado de
fipos
nombre que la clase genérica y en la que los parámetros de tipo se
han
eliminado. Las variables de tipo se sustituyen por sus límites y
cuando se
realicen llamadas a métodos genéricos que tienen un tipo de
retorno borrado,
se insertan automáticamente con versiones de tipos. Ši se utiliza
una clase
Los genéricos no hacen
genérica sin un parámetro de tipo, se emplea directamente la
clase en bruto.
el codigo mas rápido. Lo
Una consecuencia importante del mecanismo de borrado de tipos
es que
quc si hacen cs que sea
el código generado no es muy diferente del que los programadores
habian
mas seguro, en cuanto al
ratamiento de tipos , en
estado escribiendo durante años antes de la aparición de os
genéricos, y de
tiempo de compilacion.,
hecho, no es más rápido. La ventaja más significativa es que el
programador
no tiene que insertar él mismo conversiones de tipos en el código,
y que el
compilador podrá realizar una significativa verificación de los
tipos.
4.7.6
Restricciones a los genéricos
Hay numerosas restricciones que afectan a los tipos genéricos.
Cada una de las restricciones que
aquí se numeran es de carácter obligatorio, a causa del
mecanismo de borrado de tipos.
Los tipos primithos no
Tipos primitivos
se pueden usar como
paramctro de tlpo.
Los tipos primitivos no se pueden usar como parámetro de tipo.
Por tanto,
Arraylist'int> es ilegal. Ès necesario utilizar clases envoltorio.
4.7 Implementación de componentes genéricos con los
componentes genéricos de Java 5
153
Pruebas instanceof
Las pruebas instanceof y las conversiones de tipas solo funcionan
con el
Las puebas
tipo en bruto, Asísi
instanceof yles
cormversiones de Lpos Solo
Arraylist< Integer> listl = new Arraylist<Integer>( ):
funcionan con el tpo en
bruto.
listl. add( 4 ):
Object list = listl:
Arraylist<string> list2 = (Arrayliststring>) list:
String s = 1ist2 -get( 0 ):
fuera legal, entonces en tiempo de ejecución la conversión de tipo
se realizaría correctamente, puesto
que todos los tipas son Arraylist. Eventualmente, se produciría un
error de tiempo de ejecución en
la última línea, porque la llamada a get trataría de devolver un
objeto String pero no podría.
Contextos estáticos
Los metouos ycampos
En una clase genérica, los métodos y, campos estáticos no pueden
hacer
estáticos no pueden hacer
referenda a las varlables
referencia a las variables del tipo de la clase, puesto que después
de un
de tipo de la da se. Los
borrado de tipos, no existe ninguna variable de tipo. Además,
puesto que solo
campos estáticos s0n
hay realmente una clase en bruto, los campos estáticos son
compartidos entre
compa tidos pcr lodas las
Instantaciones gentrices de
todas las instantaciones genéricas de la clase.
la dase.
Instanciación de tipos genéricos
Es ilegal crear una instancia de un tipo genérico. SiT es una
variable de tipo,
la instrucción
Esilegal crear una Instancia
de un tipo gEnerko,
T obj = new T( );: Il ET 1ado derecho es 1legal .
es ilegal. T es sustituida por suS límites, que podrían ser Object (o
incluso
una clase abstracta), por lo que la llamada a new no tiene ningún
sentido.
Objetos matriz genéricos
Es ilegal crear una matriz de tipo genérico. Si T es una variable de
tipo, la
Es ilegai crear una matriz
instrucción
de un tipo genérica.
T L 1 arr new TL 10 1; I/ El 1ado derecho es i legal .
es ilegal. T sería sustituida por sus limites, que probablemente
Serían Object, y entonces la
conversión de tipo (generada por el mecanismo de borrado de
tipos) a TL] fallaría, porque Objectl]
NO-ES-UN TL]. La Figura 4.37 muestra una versión genérica de la
clase Simpl eArraylist que
hemos visto anteriormente en la Figura 4.24 . La única parte
complicada es el código de la linea 38.
Puesto que no podemos crear matrices de objetos genéricos,
debemos crear una matriz de Object
y luego utilizar una conversión de tipo. Esta conversión de tipo
generará una advertencia del
compilador acerca de una conversión de tipo no comprobada. Es
imposible implementar las clases
de colección genéricas con matrices sin obtener esta advertencia
de compilación. Si los clientes
quieren que su código se compile sin advertencias, deben utilizar
únicamente tipos de colección
genéricos, no tipos de matriz genéricos.
154
Capitulo 4 Herencia
1 /**
2.
* Gener icSimp leArraylist ampliable de Object.
3
* Las inserciones siempre se hacen al final.
4
*/
5
public class Gener icSimpl eArraylist<AnyType>
6
7
/**
8
9
10
*/
11
public int size( )
12
13
return theSize:
14.
15
/*
16
17
* Devue 1ve el elemento en 1a posición idx.
*
18
@param idx el índice que hay que buscar .
19
*
@throws Array IndexOutOfBoundsException st el indice es
incorrecto.
20
*/
21
public AnyType get( int idx )
22
23
ifl idx < 0 Il idx >- size( ) )
24.
throw new Array Index0utOfBound IsException( ):
25
return theItemsl idx 1:
26
27.
/A*
28
29
* Afade uin elemento a1 final de esta colección.
30
*
@param x cualquier objeto
31
* @return true.
32
*/
33
public boolean add( AnyType x
)
34
35
)
if( thel tems .1 ength -- size( )
l
36
37
AnyType C J old = theItems :
38
theItems = ( AnyType I])new 0bject[size( )*2 + 1J:
39
)
forc int i 0: i < size( ): 1++
40
theItemsl i 1 = oldL 1 J:
41
42
Continda
Figura 4.37 Clase Stmpl cArraylist ulllzando genéricos-
155
4.8 El functor (objetos función)
43
theltemsl thesizett 1 = x:
44
return true:
45
46
47
private static final int INIT_CAPACITY -- 10:
48
49
private int theSize:
50
private AnyType C 1 theItems:
51 )
Figura 4.37 (Cantinuación.
Matrices de tlpos parametrizados
La instantación de matrices de tipo parametrizado es ilegal.
Considere el
La inslantación de matrices
de tipas pafametrizados
siguiente código:
es llegal,
Arraylist<stri ing> t J arrl = new Arraylist<String> [ 10 1:
Object L 1 arr2 = arrl;
arr2L 0 J - new Arraylist<Double> ( ):
Normalmente, esperaríamos que la asignación de la linea 3, que
tiene el tipo incorrecto, generara
una excepción ArrayStoreException. Sin embargo, después del
borrado de tipos, el tipo de la
matriz es Arraylistl].y el objeto añadido a la matriz es Arraylist, por
lo que no se produce la
excepción ArrayStoreException. Por tanto, este código no tiene
ninguna conversión de tipos, a
pesar de lo cual terminará por generar una excepción C1
assCastExcept I on, que es exactamente la
situación que se supone que los genéricos tratan de evitar.
4.8
El functor (objetos función)
En las Secciones 4.6 y 4.7 , hemos visto cómo pueden utilizarse
las interfaces para escribir algo-
ritmos genéricos. Como ejemplo, puede emplearse el método de la
Figura 4.36 para encontrar el
elemento máximo en una matriz.
Sin embargo, el método findMax tiene una importante limitación.
Nos referimos a que solo
funciona para objetos que implementen la interfaz Compa rable y
sean capaces de proporcionar un
método compareTo como base para todas las decisiones de
comparación. Hay muchas situaciones en
la que esto no es factible. Por ejemplo, considere la clase
Simplerec tangle de la Figura 4.38.
La clase Simpl eRectangle no tiene una función compareTo, y no
puede por tanto implementar
la interfaz Comparable. La razón principal es que, como hay
muchas alternativas plausibles, resulta
dificil decidir cuál sería un buen significado para compa ireTo.
Podríamos basar la comparación en el
área de los rectángulos, en su perimetro, en su longitud, en su
anchura, etc. Una vez que escribamos
compareTo, nos vemos limitados a utilizar el criterio elegido. iY
qué sucede si queremas que
findMax funcione con varias alternativas de comparación?
La solución al problema consiste en pasar la función de
comparación como segundo parámetro
de findMax, y hacer que findMax utilice la función de comparación,
en lugar de asumir la existencia
156
Capituio 4 Herencia
1 l Una clase de rectángulos simples.
2
publIc class Simpl eRectangle
{
3
4
)
public Simpl eRectangle( int Ten . int wid
5
(
length - Ten: width - wid; 1
6
7
public int getlengtht }
B
return length:
(
}
9
10
public int getWidth( )
11
return width: )
(
12
13
public String tostringí )
14
return "Rectangle * + getlength( ) + * by .
15
+ getwidth( ):
}
16
17
private int length:
18
private int width:
19 1
Figura 4.38 La clase SimpleRectangle, que no implementa la
Interfaz Comparable.
de compareTo. De ese modo, findMax tendrá ahora dos
parámetros: una matriz de objetos Object
de un tipo arbitrario (que no tiene por qué tener definido
compareTo) y una función de compa-
ración.
El principal problema que nos queda por resolver es cómo pasar la
función de comparación.
Algunos lenguajes permiten que los parámetros sean funciones.
Sin embargo, esta solución
a
menudo presenta problemas de eficiencia y no está disponible en
todos los lenguajes orientados a
objetos. java no permite pasar funciones como parámetros; solo
podemos pasar valores primitivos y
referencias, Por tanto, parece que no tenemos forma de pasar una
función,
Sin embargo, recuerde que un objeto está compuesto por datos y
funciones.
La palabra funclores otro
Así que podemos integrar la función en un objeto y pasar una
referencia a
nombre para designar a los
este. De hecho, esta idea funciona en todos los lenguajes
orientados a objetos,
objcios funcion
El objeto se conoce como objeta funcióny en ocasiones también se
denomina
finctor.
El objeto función a menudo no contiene ningún dato. La clase
contiene simplemente un único
método, con un nombre determinado, que está especificado por el
algoritmo genérico (en este caso,
findMax). Después, al algoritmo se le pasa una instancia de la
clase, que a su
vez invoca el único método existente en el objeto función.
Podemos diseñar
La csse del obieło funcion
conbenei un Imetodo espe-
diferentes funciones de comparación simplemente declarando
nuevas clases.
cilicado para el alyontmo
Cada nueva clase contendrá una implementación diferente de ese
único
gnérico. Al akgorärmo se
método previamente acordado.
epasa una instancia de
la dase.
En Java, para implementar este tipo de estructura utilizariamos la
herencia,
y especificamente empleariamos las interfaces. La interfaz se
utiliza para
157
4.8 El functor (objetos función)
1
package [Link]:
2
3
/**
4
* Interfaz de1 objeto función Camparator .
5
*/
6
public interface Comparator <AnyType>
7
g
/* *
9
I0
11
12
13
14
15
*/
16
int compare( AnyType Ths. AnyType rhs ):
17 |
Figura 4,39 La interaz Comparator originaimente definida en java .
ut1l yreescrita para el paquele weiss .ut11.
declarar la signatura de la función previamente acordada, Por
ejemplo, la Figura 4.39 muestra la
interfaz Comparator, que forma parte del paquete estándar
java .util. Recuerde que para ilustrar
cómo está implementada la librería Java, vamos a reimplementar
una parte de java. util como
weiss. util. Äntes de Java 5, esta clase no era genérica.
La interfaz dice que cualquier clase (no abstracta) que afirme ser
un Comparator debe
proporcionar una implementación del método compare; por tanto,
cualquier objeto que sea una
instancia de una de esas clases dispondrá de un método compare
al que poder invocar.
Utilizando esta interfaz, podemos ahora pasar un Comparator
como segundo parámetro
a findMax. Si este Comparator es cmp. podemos hacer con total
seguridad la llamada cmp .
compare(o1.02) con el fin de comparar cualesquiera dos objetos
de la forma necesaria. Se emplea
un comodin en el parámetro Comparator para indicar que el
Comparator sabe cómo comparar
objetos que sean del mismo tipo que los de la matriz o supertipos
de ellos. Es responsabilidad del
llamante de findMax pasar como argumento real una instancia
apropiadamente implementada de
Comparator.
En la Figura 4.40 se muestra un ejemplo. findMax acepta ahora dos
parámetros. El segundo
parámetro es el objeto función. Como se muestra en la línea 11,
findMax espera que el objeto
función implemente un método denominado compare, y en efecto
debe hacerlo así, ya que
implementa la interfaz Comparator.
Una vez. escrita la interfaz findMax, se puede invocar desde ma
1n. Para ello, necesitamos pasar
a findMax una matriz de objetos Simpl eRec tangle y un objeto
función que implemente la interfaz.
Comparator. Implementamos OrderRectByWi dth, una nueva clase
que contiene el método compare
requerido. El método compare devuelve un entero que indica si el
primer rectángulo es menor, igual
o mayor que el segundo rectángulo, según sus anchuras. ma i n
simplemente pasa una instancia de
158
Capituio 4 Herencia
public class Utils
1
2
/l findMax genér ico con un objeto función.
3
4
/l Precondición: a .length 5 0 .
5
public static <AnyType> AnyType
6
fi ndMax( AnyType l ] a, Comparatore? super AnyType> cmp
7
B
int maxIndex = 0 :
9
10
forl int 1 = 1: 1 < a .length: 1++ )
11
)
if( cmp. compare( a L 1 1. al maxIndex 1 ) 0
12
maxIndex - 1;
13
14
return al maxIndex J:
15
16 )
Figura 4.40El algoritmo fIndhax generica. utilizando un objelo
función.
OrderRectBywidth a findMax. Tanto ma i n como Order Rectbywi
dth se muestran en la Figura
4.41. Observe que el objeto Order RectByWidth no tiene miembros
de datos. Esto suele pasar con
todos los objetos función.
La técnica del objeto función es una ilustración de un patrón que
aparece siempre una y otra vez,
no solo en Java, sino en cualquier lenguaje que disponga de
objetos. Ën Java, este patrón se emplea
casi de manera continua y representa quizá el uSo más extendido
de las interfaces.
4.8.1
Clases anidadas
Hablando en términos generales, cuando escribimas una clase,
esperamos que sea útil en muchos
contextos, no solo para ia aplicación concreta en la que estamos
trabajando,
Una característica un tanto molesta del patrón de objeto función,
especialmente en Java, es el
hecho de que, debido a que se utiliza tan a menudo, obliga a crear
numerosas clases de pequeño
tamaño, cada una de las cuales contiene un método y que se
utiliza quizá una única vez dentro de un
programa, teniendo una aplicabilidad bastante limitada fuera de la
aplicación actual.
Esto resulta molesto por al menos dos razones distintas. En primer
lugar, puede que tengamos
docenas de clases de objetos función. Si son públicos estarán
necesariamente dispersas en
archivos separados. Si tienen visibilidad de paquete„ puede que se
encuentren todas en el mismo
archivo, pero aun asi tendremos que andar recorriendo arriba y
abajo el archivo para encontrar sus
definiciones, que es probable que se encuentren muy alejadas del
pequeño conjunto de lugares de
1
al truco para implementar coupare medlante la operación de resta
funciona pars valores Int, slempre ycuando ambas tengan el misno
signo. En Caso contrario, existe una posibilidad de desboramiento.
Este tnico simplificador es también la razón par la que utilizamos
SimpleRectangle, en lugar de Rectangle (que almacenaba las
anchuras en forma ce valores double).
159
4.8 El functor (objetos función)
class OrderRectBywidth impl ements Comparator<s impl
leRectangle>
1
2.
l
3
public int compa re( Simpl eRectang le r1. Simp leRectangle r2 )
4
(
)
return( r1 getwi dth() - r2- getWidth() ):
5
)
6
7
public class CompareTest
8
{
9
)
public static void mainc String C J args
10
11
Simpl leRectangle t rects m new Simpl eRectanglel 4 :
12
rectsl 0 - new S imp leRectangle( 1. 10 ):
13
rectsl 1 1 = new Simp leRectangle( 20. 1 ):
14.
rects[ 2 J = new Simp leRectangle( 4 . 6 ):
15
rectsl 3 1 - new Simp leRectangle( 5. 5 );
16
17
System .out -printIn( "MAX WIDTH: *- +
18
Ut 1ls. findMax( rects. new OrderRectByWidth( ) ) ):
19
Z0 l
Figura 4.41 Ln ejemplo de objeto funcíón.
todo el programa en los que se las instancia como objetos función,
Sería preferible que cada clase
de objeto función pudiera declararse lo más próxima a su
instantación. En segundo lugar, una vez
que se utiliza un nombre, no se puede volver a emplear dentro del
paquete sin que se produzca la
posibilidad de una colisión de nombres. Aunque los paquetes
resuelven algunos problemas relativos
al espacio de nombres, no los resuelven todos, especialmente
cuando se usa dos veces el mismo
nombre de clase dentro del paquete predeterminado.
Con una clase anidada, podemos resolver algunos de estos
problemas. Una
Uina case antlada es
clase anidada es una declaración de clase situada dentro de otra
declaración
una declaración de clase
de clase -la clase exterma- utilizando la palabra clave static. Una
clase
stuada dentro óc olra
anidada se considera un miembro de la clase externa. Como
resultado, puede
decaracion de clase -la
dlase exterma- utilzando la
ser pública, prívada, con visibilidad de paquete o protegida, y
dependiendo
palabra dave static.
de la visibilidad puede o no ser accesible por parte de los métodos
que no
estén incluidos en la clase externa. Normalmente, la clase anidada
será
privada y por tanto inaccesible desde fuera de la clase externa,
Asimismo,
puesto que una clase anidada es un miembro de la clase externa,
sus métodos
Una clase enidada C5 una
parte de la dase externa y
pueden acceder a los miembras privados estâticos de la clase
externa, y
puede dedararse con un
también pueden acceder a los miembros privados de instancia
cuando se las
especificador de vsibisdad.
proporciona una referencia a un objeto externo.
Todos los miembros de la
case extema son vésibles
ia Figura 4.42 ilustra el uso de una clase anidada en conjunción
con el
para los metodos de la
patrón del objeto función. La palabra clave static delante de la
declaración
dase anidada
dle la clase anidada de Order RectBylidth es esencial; sin ella,
tendríamos una
160
Capituio 4 Herencia
1
import java. util .Comparator:
2
3
class CompareTestInnerl
l
4
5
private static class Order Rectbywi dth implements Compar ator<s
mpleRectangle>
6
7
public int compare( Simpl eRectangle rl, SimpleRectangle r2 )
8
return r1. getWidth( ) r2. getWidth( ): 1
l
9
l
10
11
public static void main( String L 1 args }
12
13
SimpleRectangle c J rects " new Simplerectanglel 4 1:
14
rectsl 0 J - new Simp leRectangle( 1. 10 ):
15
rectsl 1 1 = new Simp leRectangle( 20. 1 ):
16
rectsl 2 1 = new Simp leRectangle( 4 , 6 ):
17
rectsl 3 1 = new Simp TeRectangle( 5 . 5 ):
18
19
System .out. printin( "MAX WIDTH: .* +
20
[Link] ndMax( rects. new OrderRectByWidth( ) ) );
21
22 l
Figura 4.42 Uttlzacidn da una clase anidada para ocultar la
declaracion de la clase OrderRectBywidth.
clase interna, que se comporta de forma diferente y que es un tipo
de clase del que hablaremos
posteriormente en el libro (en el Capítulo 15).
Ocasionalmente, una clase anidada es pública. En la Figura 4.42, si
OrderRectBywidth se
declarara pública, la clase Compa reTestInner1. Orderrectbyw Idth
podría ser utilizada desde fuera
de la clase CompareTest Inner1.
Clases locales
4.8.2
Además de permitir declarar clases dentro de otras clases, Java
también permite declarar clases
dentro de métodos. Estas clases se denominan clases locales.
Esto se ilustra en la Figura 4.43.
Observe que cuando se declara una clase dentro de un método, no
se
puede declarar private o static, Sin embargo, la clase solo es
visible
larva tambión pormite
docarar clases dentro de
dentro del método en el que ha sido declarada. Esto facilita
escribir la clase
métodos Dichas ciases
justo antes de su primera ly quizás única) utilización, y evitar así
la polución
se conocencon el nomire
de los espacios de nombres.
de clases localesy rio
pueden decrarse con un
Una ventaja de declarar una clase dentro de un método es que los
métodos
modificadce de visblldad,
de esa clase (en este caso, compare) tienen acceso a las variables
locales
ni con el modificatfor
static.
de la función que hayan sido declaradas antes que la clase. Esto
puede ser
importante en algunas aplicaciones. Existe una regla técnica: para
poder
161
4.8 El functor (objetos función)
class CompareTestInner2
1
2
3
public static void main( String C J args
)
4
5
Simple Rectangle L 1 rects - new Simpl eRectanglel 4 J:
6
rectsl 0 3 = new Simp leRectangle( 1. 10 ):
7
rectsl 1 3 = new Simp leRectangle( 20, 1 ):
8
rectsl 2 3 = new Simp leRectangle( 4 . 6 ):
9
rectsl 3 J = new Simp leRectangle( 5 . 5 ):
10
I1
class OrderRectByWidth impl ements Compa rator<simpl
eRectangle>
l
12
13
public int campare ( Simpl eRec tangle r1. Simp leRectangle r2
)
14
return r1. getwidth( ) - r2. getwidth( ):
{
)
15
16
System .out .printin( *MAX WIDTH: .. +
17
18
Utils. fIndMax( rects, new Order RectByWidth( ) ) ):
19
20 l
Figura 4.43 Ullización de una clase local para ocultar aun mas la
declaración delaclase OrderRectBywidth.
acceder a las variables locales, las variables deben ser declaradas
como final. No vamos a utilizar
estos tipos de clases en el texto.
4.8.3
Clases anónimas
Uno tendería a sospechar que al colocar una clase en la linea
inmediatamente
Lina clise andnína es una
dase que no liero nomtre.
anterior a la línea de código en la que se la usa, hemos declarado
la clase en
el punto más próximo posible al punto de utilización. Sin embargo,
en Java,
podemos hacerlo todavía mejor.
La Figura 4.44 ilustra el concepto de clase interna anónima. Una
clase anónima es una clase
que no tiene ningún nombre. La sintaxis es que en lugar de escribir
new Inner() y de proporcionar
l implementación de Inner como clase nominada, escribimos new
Interface(), y luego propor-
cionamos la implementación de la interfaz (todo, desde el
corchete de apertura al de cierne)
inmediatamente despuês de la expresión new. En lugar de
implementar una interfaz anónimamente,
lambién es posible ampliar anónimamente mediante herencia una
clase, proporcionando solo los
métodos sustituidos.
La sintaxis parece aterradora, pero al cabo de poco tiempo uno
llega a
Las dases anonimas
acostumbrarse. Complica el lenguaje significativamente, porque la
clase
Introducen significalivas
anónima es una clase. Un ejemplo de estas complicaciones es que
puesto que
complicaciones en el
lenguaje.
el nombre de un constructor es el nombre de una clase, ¿cómo se
define un
constructor para una clase anónima? La respuesta es que no se
puede definir.
162
Capitulo 4 Herencia
class CompareTestInner3
1
2
public static vo id main( String L 1 args
)
3
4
5
SImplel Rectangle L ] rects -" new Simple eRectanglel 4 1:
6
rectsl 0 1 = new SimpleRectangle( 1. 10 ):
7
rectsl 1 J = new Simp leRectangle( 20 . 1 ):
B
rectsl 2 J - new Simp leRectangle( 4 ,. 6 ):
9
rectst 3 J = new Simp leRectangle( 5. 5 ):
10
11
System out println( "MAX WIDTH: ...
12
Ut ils. fi ndMa x( rects. new Compara tor<simpl eRectangle>(
)
13
{
14
public int compare( Simpl eRect tang 1e rl. Simp leRectangle r2
)
15
return rl. gethidth( ) . r2. getWidth( ); }
{
16
}
17
) ):
18
19 1
Figura 4.44 Utllizacion de una clase anónima para implementar el
objetc furcion.
La clase anónima es en la práctica muy útil, y su uSO suele
considerarse
parte del patrón de objeto función, en conjunción con el
tratamiento de
Lasdases ancoiimas suclen
utäzarse para implemertar
sucesos en las interfaces de usuario. En el tratamiento de
sucesos, el progra-
objelos funcion
mador está obligado a especificar una función„ lo que sucede cada
vez se
produce un cierto suceso.
4.8.4
Clases anidadas y genéricos
Cuando se declara una clase anidada dentro de una clase
genérica, la clase anidada no puede hacer
referencia a los tipos de parámetro de la clase externa genérica.
Sin embargo, la propia clase anidada
puede ser genérica y puede reutilizar los nombres de tipos de
parámetro de la clase genérica externa.
Como ejemplos de sintaxis tendríamos los siguientes:
class Outer<AnyType>
l
public static class Inner<AnyType>
}
public static class Other Inner
H/ Aquf no puede utilizarse AnyType
163
4.9 Detalles sobre el mecanismo de despacho dinámico
Outer. Inner<string> il = new Outer. Inner<string)( ):
Outer. OtherInner i2 = new Outer .Other 'Inner( ):
Observe que en las declaraciones de ile 12. Outer no tiene tipos de
parámetro.
4. 9
Detalles sobre el mecanismo de
despacho dinámico
Un mito muy común es el de que todos los métodos y todos los
parámetros
El mvecanismo de despacho
se acoplan en tiempo de ejecución. Esto no es cierto, En primer
lugar, hay
dinámico no es importanle
para los melodos estaticos,
algunos casos en los que el despacho dinámico nunca se utiliza o
ni siquiera
iinales o privados,
tiene sentido:
En otros escenarios, el mecanismo de despacho dinámico se
utiliza de una manera significativa,
¿Pero qué significa exactamente eso del despacho dinámico?
El despacho dinámicosignifica que se utilizará en cada momento el
método
En JJava, lors perámetros
de un melodo siempre s0
que sea apropiado para el objeto con el que se esté operando. Sin
embargo,
dediuoen estatiamente, en
no quiere decir que siempre se vaya a seleccionar la
correspondencia más
tiempo de compdacirn.
perfecta para todos los parámetros, Específicamente, en Java, los
parámetros
de un método siempre se deducen estáticamente, en tiempo de
compilación.
Para ver un ejemplo concreto, considere el código de la Figura
4.45. En el método whi chFoo, se
hace una llamada a foo. ¿Pero a cuál foo se llama? Cabría esperar
que la respuesta dependiera de
los tipos en tiempo de ejecución de argly arg2.
Puesto que las correspondencias de parámetros siempre se
realizan en tiempo de compilación, no
importa a quế tipo esté refiriéndose realmente arg2. EI método foo
que se seleccionará será
)
public void foo( Base x
La sobrecarga estitica
significa que os parametros
La única cuestión es si se empleará la versión de Base o de
Derived. Esa
de un meodo slempre se
es la decisión que se toma en tiempo de ejecución, una vez que se
conoce el
deducen estáticamente, en
tlempo de compladión.
objeto al que arg1 hace referencia.
La metodología precisa utilizada es que el compilador deduce en
tiempo
de compilación la mejor signatura, basándose en los tipos
estáticos de los
El despacho dinimico
parámetros y de las métodos disponibles para el tipo estático de la
referencia
significa que, una VEL
controladora. En ese punto, se fija la signatura del método. Este
paso se
que se ha determinado a
signatura de uIn método
denomina sobrecarga estática. Ex único problema que resta es
determinar
de inslanda, la clase del
de cuál clase hay que utilizar la versión de ese método. Esto se
lleva a cabo
molodo puede delerminarse
en tiempo de ejocucion
haciendo que la Máquina Virtual deduzca el tipo de ese objeto en
tiempo
basândose en cl tipo
de ejecución. Una vez conocido el tipo en tiempo de ejecución, la
Máquina
dinámico del objelo que
Virtual recorre hacia arriba la jerarquía de herencia, buscando la
última
realiza la inocación,
versión sustituida del método; este será el primer método de la
signatura
164
Capitulo 4 Herencia
class Ba se
1
2l
public void fool Base x
)
3
4
l
System .out .printin( "Base. .Base" ):
1
5
6
public void fool Der i ved x )
7
(
System .out -println( "Base. .Der ived" ): 1
8}
9
10 class Dert ved extends Base
11
public void fool Base x
)
12
System -out -println( "Deri ved. Base" ):
1
13
14
public votd fool Derived x )
15
)
{
System -out -printin( "Derived. Derived" ):
16
17 l
18
19 class Staticparams Demo
20 {
public static void whichFool Base arg1. Base arg2 )
21
22
/7 Se garantiza que 1lama remos a foo( Base )
23
f E1 único problema es qué versión de clase utilizar
24
Il de fool Base ): para decidir. se utiliza el tipo
25
/l dinámico de arg1.
26
arg1. .foo ( arg2 ):
27
28
29
30
public static void main( String CI args )
31
Base b = new Basel ):
32
Derived d = new Derived( ) :
33
34
whichFoo ( b, b ):
35
whichfool b. d ):
36
whichFoa( d. b ):
37
whichfoo( d , d ):
38
39
40 }
Figura 4.45 Rustración del acoplamlenlo estatico de parametros_
165
4.9 Detalles sobre el mecanismo de despacho dinámico
apropiada que la Máquina Virtual encuentre a medida que vaya
subiendo hacia Object.s El segundo
paso se denomina despacho dinámico.
La sobrecarga estática puede conducir a errores sutiles cuando un
método que se suponía
que tenía que ser sustituido ha sido, en su lugar, sobrecargado. La
Figura 4.46 ilustra un error de
programación común que se produce a la hora de implementar el
método equals.
final class SomeClass
1
2
3
public SomeClass( int i )
4
id - 1:
5
6
public boolean sameva1( Object other )
7
return other instanceof Someclass Lå equals( other ):
)
a
g
/**
*
iEsta es una ma la impl ementación!
10
*
other tiene el tipo incorrecto, por 1o que esto
11
*
no sustituye el método equals de Object.
12
*/
13
public boolean equals( SomeClass other )
14
{
return other != nu11 && id = other .id: 1
15
16
private int id:
17
18 1
19
20 class BadEqual sDemo
21
public static void mainc String C 1 args
)
22
23
SomeClass objl - new SomeClasst 4 ):
24
SomeClass obj2 - new SomeClasst 4 ):
25
26
System .out .println( obj1. equals( obj2 ) ): It true
27
System .out ·printlní objl. sameval( obj2 ) ). H/ false
28
29
30 }
Figura 4.46 llustracion de cómo se puede sobrecargar equals en
luga de sustituirlo. Aqui, la Ilamada a saneval derrvelve false.
B SI no encuentra tal metodo, quizá porque solo se recompiló parte
del programa, entonces la Máquina Virtual genera una excepción
HoSuchMethodExcept 1on,
166
Capituio 4 Herencia
El método equals se define en la clase Object y está pensado para
devolver true si dos
objetos tienen estados idénticos. Toma un Object como parámetro
y el Object proporciona una
implementación predeterminada que devuelve true si los dos
objetos son el mismo. En otras
palabras, en la clase Object, la implementación de equals es
aproximadamente
public boolean equalst Object other )
return this = other:
I
Al sustituir equals, el parámetro debe ser de tipo Object; en caso
contrario, lo que estaremos
haciendo es una sobrecarga del método. En la Figura 4.46, equals
no es sustituido, en lugar de ello
se sobrecarga (de forma no intencionada). Como resultado, la
llamada a sameva1 devolverá false,
lo que parece sorprendente, ya que la llamada a equals devuelve
true y sameval llama a equaTs.
El problema es que la llamada en sameva1 es this .equals(other).
El tipo estático de this
es Somec Tass. En Somec1 ass, hay dos versiones de equals: el
equals allí indicado que toma un
objeto Somec lass como parámetro y el equals heredado que
admite un Object. EI tipo estático
del parámetro (other) es Object, por lo que la correspondencia más
perfecta es el método equals
que admite un objeto de tipo Object. En tiempo de ejecución, la
máquina virtual busca ese método
equals y encuentra el de la clase Object. Ý puesto que this y other
son objetos distintos, el
método equals de la clase Object devuelve false.
Por tanto, equals debe escribirse para admitir un Object como
parámetro, y normalmente hará
falta una especialización del tipo, después de verificar que el tipo
es apropiado. Una forma de hacer
esto es utilizar una prueba instanceof, pero eso solo resulta seguro
para las clases finales. Sustituir
equals es, en realidad, bastante complicado en presencia de
herencia, y hablaremos de ello en la
Sección 6.7.
Resumen
La herencia es una potente caracteristica que constituye una
parte esencial de la programación
orientada a objetos y de Java. Nas permite abstraer funcionalidad
en clases base abstractas y hacer
que las clases derivadas implementen y amplien dicha
funcionalidad. En la clase base pueden
especificarse varios tipos de métodos, como se ilustra en la Figura
4.47.
La clase más abstracta, en la que no se permite ninguna
implementación, es la interfaz. La
interfaz. enumera los métodos que tienen que ser implementados
con una clase derivada. La clase
derivada debe implementar todos esos métodos (o ser ella misma
abstracta) y especificar, mediante
la cláusula impl ements, que está implementando una interfaz..
Una clase puede implementar múl-
tiples interfaces, proporcionando así una alternativa más simple al
mecanismo de herencia múltiple.
Por último, la herencia nos permite escribir más fácilmente clases
y métodos genéricas, que
funcionen para un amplio rango de tipos genéricos. Esto implicará
normalmente utilizar una
cantidad significativa de conversiones de tipos. Java 5 anade
clases y métodos genéricos que ocultan
esas conversiones de tipos. Las interfaces también se utilizan
ampliamente para componentes
genéricos, así corno para implementar el patrón de objeto función,
Este capítulo concluye la primera parte del texto, en la que hemos
proporcionado una panorámica
de Java y de la programación orientada a objetos. Ahora
continuáremos examinando los algoritmos
ylos componentes fundamentales del proceso de resolución de
problemas.
167
Conceptos clave
Figura 4.47 Cuatro upos de metodos de ciase.
a
Conceptos clave
acophuniento estático La decisión de cuál es la clase cuya versión
de un método hay que
utilizar se toma en tiempo de compilación. Solo se usa para
métodos estáticos, finales
o privados, (118)
borrado de tipos El proceso mediante el cual se reescriben las
clases genéricas en forma
de clases no genéricas. (152)
boxing Creación de una instancia en una clase envoltorio para
almacenar un tipo primitivo.
En Java 5, esto se hace automáticamente. (143)
dase abstracta Una clase que no puede construirse pero sirve para
especificar la
funcionalidad de las clases derivadas, (127)
dase adaptora Una clase que se suele utilizar cuando la interfaz de
otra clase no coincide
exactamente con lo que se necesita. La clase adaptadora
proporciona un efecto
envoltorio, al mismo tiempo que modifica la interfaz. (141)
case anidada Una clase dentro de otra clase, declarada con el
modificador static. (158)
dase anónina Una clase que no tiene nombre y que es útil para
implementar cortos
objetos función. (161)
clase base La clase en la que está basada la herencia, (112)
dase derivada Una clase completamente nueva que tiene, de todos
modos, una cierta
ompatibilidad con la clase de la que se deriva. (112)
dase en bruto Una clase en la que se han eliminado los parámetros
de tipo genérico. (152)
dase final Una clase que no puede ser ampliada mediante
herencia. (118)
dase hoja Una clase final. (118)
daselocal Una clase dentro de un método, declarada sin
modificador de visibilidad. (160)
168
Capitulo 4 Herencia
cases genericas Añadidas en Java 5, permiten a las clases
especificar parámetros de tipo
yevitan una significativa cantidad de conversiones de tipo. (148)
cinsula extends Una cláusula utilizada para declarar que una
nueva clase es una subclase
de otra clase. (112)
cansula impl ements Una cláusula utilizada para declarar que una
clase implementa los
métodos de una interfaz. (133)
composición Mecanismo preferido de herencia cuando no se
cumple una relación de tipo
ÉS-UN. En lugar de ello, decimos que un objeto de la clase B está
compuesto de un
objeto de la clase A y de otros objetos). (108)
despacho dinámico Una decisión tomada en tiempo de ejecución
para aplicar el método
correspondiente al objeto realmente referenciado, (163)
envoltorio Una clase utilizada para almacenar otro tipo y añadir
operaciones que el tipo
primitivo no soporta, o que no soporta correctamente. (141)
fiunctor Un objeto función. (156)
herencia El proceso del que podemos derivar una clase a partir de
una clase base, sin
perturbar la implementación de la clase base. También permite el
diseño de una
jerarquía de clases como Throwable e InputStream. (112)
herencia últiple El proceso de derivar una clase a partir de varias
clases base. La
herencia múltiple no está permitida en Java. Sin embargo, SI que
está permitida una
alternativa, las interfaces múltiples. (129)
interfaxs Un tipo especial de clase abstracta que no contiene
detalles de implementación.
(132)
lmites de tipo Especifican propiedades que los parámetros de tipo
deben satisfacer, (151)
lamada al constructor super Una llamada al constructor de la
clase base. (117)
matrices covariantes En Java, las matrices son covariantes, lo que
quiere decir que
Derived L1 es compatible en cuanto a tipo con Ba se[]. (122)
método abstracto Un método que no tiene ninguna definición
significativa y se define,
por tanto, en la clase derivada. (127)
mitodo final Un método que no puede Ser sustituido y es invariante
en toda la jerarquía de
herencia. Para los métodos finales se utiliza el acoplamiento
estático. (117)
miembro de clase protegido Accesible por parte de las clases
derivadas y de las clases
contenidas en el mismo paquete. (116)
objeto función Un objeto pasado a una función genérica con la
intención de que su único
método sea utilizado por la función genérica. (156)
objeto super Un objeto utilizado en la sustitución parcial para
aplicar un método de la
clase base. (119)
parámetros de tipos Los parámetros encerrados entre corchetes
angulares i> en una
declaración de un método o una clase genéricos. (148)
patrón decorador El patrón que implica la combinación de varios
envoltorios con el fin
de añadir funcionalidad. (139)
169
Errores comunes
polimoa fisuno La capacidaxd de una variable de referencia para
hacer referencia a objetos
de varios tipos distintos. Cuando se aplican operaciones a la
variable, se selec-
ciona automáticamente la operación que resulte apropiada para el
objeto realmente
referenciado, (114)
progranaciún genérica Utilizada para implementar lógica
independiente del tipo. (140)
relación ES-UN Una relación en la que la clase derivada es una
(variación de la) clase
base. (108. 115)
relación TIENE-UN Una relación en la que la clase derivada tiene
una (instancia de la)
clase base, (108)
relaciones subclase/superckase Si XES-UN Y, entonces Xes una
subclase de Ye Yes una
superclase de X. Estas relaciones son transitivas, (115)
sobrecarga estática El primer paso a la hora de deducir el método
que será utilizado. En
este paso se utilizan los tipos estáticos de los parámetros para
deducir la signatura del
método que será invocado. La sobrecarga estática siempre se
utiliza. (163)
sustitución parcial E.l acto de ampliar un método de una clase
base para realizar tareas
adicionales, pero no enteramente distintas, (119)
tipo deretorno covariante Sustitución de tipo de retorno por un
subtipo. Esto se permite
a partir de Java 5. (123)
Errores comunes
170
Capitulo 4 Herencia
Internet
A continuación se indican los archivos disponibles para este
capítulo. Parte del código se
ha presentado por etapas; para esas clases, solo se proporciona
una versión final.
PersonDemo java
La jerarquía Person V el programa de prueba.
Shape java
La clase abstracta Shape.
Circlejava
La clase Circle.
Rectangle java
La clase Rectangle.
Un programa de prueba para el ejemplo de
ShapeDemo java
Shape.
Stretchablejava
La interfaz Stretchable.
StretchDemo java
El programa de prueba para el ejemplo de
Stretchable.
[Link]
La clase de excepción de la Figura
4.19. Forma parte de weiss .util.
ConcurrentModificatimExccption java y
EmptyStackException java también están
disponibles en línea.
DecoratorDemo java
Una ilustración del patrón decorador,
incluyendo el mecanismo de buffer, compresión
y serialización.
MemryCelljava
La clase MemoryCe11 de la Figura 4.22.
TestMemoryCelljava
El programa de prueba para la clase de la celda
de memoria mostrada en la Figura 4.23.
SimpleArrayl ist java
La clase Arraylist genérica simplificada de la
Figura 4.24 , con algunos métodos adicionales.
Se proporciona un programa de prueba en
ReadStringsWithŠinleArrayList,java
PrinmitiveWrapperDeno java
Ilustra el uso de la clase Integer, como se
muestra en la Figura 4.25.
171
Ejercicios
BoxingDemo java
llustra los mecanismos de autoboxingy
unboxing, como se muestra en la Figura 4.26.
StorageCelidemo, java
El adaptador StorageCe11 como se muestra
en la Figura 4,27, junto con un programa de
prueba.
FindMaxDemo java
El algoritmo genérico findMax de la Figura
4.29.
GemnericMemoryCeljava
Ilustra la clase GenericMemoryCel1 de la Figura
4.30 actualizada para utilizar los genéricos de
Java 5.
TestGenericMemryCelljava prueba la clase.
GencricSimple4rrayList, java
La clase Arraylist genérica simplificada de la
Figura 4.37. con algunos métodos adicionales.
Se proporciona un programa de prueba en
ReadStringsWithGenericSinpleArrayList.
java
GeanericF'indMaxDenn,java
llustra el método genérico findMax de la Figura
4.36.
[Link]
Contiene la clase Simp leRectang1 e de la
Figura 4.38.
Comparator,java
La interfaz Comparator de la Figura 4.39.
CompareTest java
Ilustra el objeto función, sin clases anidadas,
como se muestra en la Figura 4.41.
[Link]
Ilustra el objeto función con una clase anidada,
como se muestra en la Figura 4.42.
CompareTestinner2 java
Ilustra el objeto función con una clase local,
como se muestra en la Figura 4.43.
CompareTestiner3 java
Ilustra el objeto función con una clase anónima,
como se muestra en la Figura 4.44.
StaticParamsDenmo java
El ejemplo de sobrecarga estática y despacho
dinámico mostrado en la Figura 4.45.
BadEqualsDemo,java
llustra las consecuencias de sobrecargar equals
en lugar de sustituirlo, como se muestra en la
Figura 4.46.
Elercicios
EN RESUMEN
Explique el polimorfismo. Explique el despacho dinámico. ¿Cuándo
no se utiliza el
41
despacho dinámico?
172
Capituio 4 Herencia
42
43
public class Base
1
2
3
public int bPublic:
4.
protected int bProtect:
5
private int bPrivate:
6
Ip Omitidos los métodos poblicos
7
l
8
9
public class Der ived extends Base
10
11
public Int dPublic:
12
private int dPrivate:
13
// Omitidos los métodos públicos
14
15
16
public class Tester
17
18
public static void ma in( String L 1 args )
19
20
Base b new Baset ):
21
Derived d = new Derived( ):
22
23
.
System -out -pri ntint [Link] *- * * [Link] *
24
**
+ b. bPrivate + a *. + [Link] +
25
+ [Link] ):
26
27
Figura 4.48 Un programa para probar la Msiblidad,
173
Ejercicios
EN TEORIA
417 Este ejercicio explora cómo realiza Java el despacho dinámico
y también el motivo
de que los métodos finales triviales no pueden ser integrados en
linea en tiempo de
compilación. Coloque cada una de las clases de la Figura 4,49 en
su propio archivo.
174
Capitulo 4 Herencia
public class Class1
1
2
l
public static int x 5;
3
4
5
)
public final String getx(
l
return ** *. + x + 12:
6
|
7
B
public class Class2
g
10 1
public static void main( String L 1 args
11
}
12
Classl abj - new Class1( ):
13
14
System .out ·println( obj -getx( ) ):
15
16 )
Figura 4.49 Las clases parael Ejercicio 4.17.
418 En cada uno de los siguientes fragmentas de código, localice
los errores existentes y
las conversiones de tipo innecesarias.
175
Ejercicios
EN LA PRÁCTICA
176
Capituio 4 Herencia
Por ejemplo, en el siguiente fragmento de código:
double E ] Input = [ 1.0 . -3.0, 5.0 1:
double r ] outputl -- new double C 3 1;
double L 1 output2 - new double C 3 ]:
double L ] output3 = new double C 4 1:
transform( input. outputl, new Computesquare( ) ):
transform( input. output2, new ComputeAbsoluteVa lue( ) ):
transformí input, output3, new ComputeSquare ( ) ):
177
Ejercicios
El resultado pretendido es que output1 contenga 1.0, 9.0, 25.0,
output2 contenga
1.0, 3.0, 5.0, y la tercera ]lamada a trans form genenre una
excepción I1lega1-
Argument Except ion porque las matrices tienen tamaños
diferentes. Implemente los
siguientes componentes:
439 El método contains admite una matriz de enteras y devuelve
true si existe algún
elemento de la matriz que satisface una condición especificada,
Por ejemplo, en el
siguiente fragmento de código:
int L ] Input - 100. 37 . 49 1:
booTean resultl = cantains( input , new Prime( ) ):
boolean result2 - contains( input. new Perfect Squa rel ) );
boolean result3 - contains( input, new Negative( ) :
El resultado pretendido es que resultl sea true porque 37 es un
número primo,
result2 sea true porque tanto 100 como 49 son cuadrados
perfectos y result3
sea false porque no hay números negativos en la matriz.
Implemente los siguientes
componentes:
442 Aunque los objetos función que hemos examinado no
almacenan ningún dato, esto
no es ninguna exigencia. Reutilice la interfaz del Ejercicio 4.4 1
(a).
178
Capituio 4 Herencia
PROYECTOS DE PROGRAMACIÓN
Un libro de una biblioteca es un libro que también dispone de una
fecha de entrega
yel nombre del actual depositario del libro, que puede ser un
objeto String que
representa a una persona que ha tomado prestado el libro o nu11
si el libro no
está actualmente prestado. Tanto la fecha de entrega como el
depositario del libro
pueden cambiar a lo largo del tiempo.
Una biblioteca contiene libros de la misma y soporta las siguientes
operaciones:
450 Añada el concepto de posición a la jerarquía Shape incluyendo
las coordenadas
como miembros de datos. Después añada un método di stance.
179
Ejercicios
451 Considere las cinco clases siguientes: Bank, Account,
NonInterestChecking-
Account, Interes tCheck ingAccount y Plati numCheckingAccount,
así como la
interfaz denominada InterestBear ingAccount que interactúan de
la manera
siguiente:
Para esta cuestión, haga lo siguiente. No tiene por qué
proporcionar ninguna
funcionalidad más allá de las especificaciones mencionadas:
180
Capituio 4 Herencia
Imprima el pedido, incluyendo el precio total.
181
Referencias
Referenclas
Estos libros describen los principios generales del desarrollo
software orientado a objetos:
parte
lgoritmos y bloques
dos
'undamentales
Capítulo 5 Análisis de algoritmos
Capítulo 6 La API de Colecciones
Capítulo 7 Recursión
Capítulo 8 Algoritmos de ordenación
Capítulo 9 Aleatorización
5
Capítulo
Análisis de algoritmos
Ein la primera parte hemos examinado cómo nos puede ayudar la
programación orientada a objetos
durante el diseño y la implementación de sistemas de gran
tamaño, No nos henos fijado en las
cuestiones de rendimiento. Generalmente, si utilizamos una
computadora es porque necesitamos
procesar una gran cantidad de datos. Y cuando ejecutamos un
programa con una gran cantidad
de datos, tenemos que estar seguros de que el programa termine
su tarea en un tiempo razonable.
Aunque el tiempo total de ejecución depende en alguna medida del
lenguaje de programación
que empleemos y en menor medida de la metodología utilizada
(cormo por ejemplo programación
procedimental en lugar de orientada a objetos), a menudo esos
factores son constantes del diseño
qque no se pueden modificar. Aún así, de lo que más depende el
tiempo de ejecución es de la elección
de los algorítmos.
Un algoritmo es un conjunto claramente especificado de
instrucciones que la computadora
sguirá para resolver un problema, Una vez. que se proporciona un
algoritmo para un problema y se
verifica que es correcto, el siguiente paso consiste en determinar
la cantidad de recursos, como por
ejemplo tiempo y espacio de memoria, que el algoritmo requerirá,
Este paso se denomina análisis de
algoritmos. Ún algoritmo que requiera varios cientos de gigabytes
de memoria principal no será útil
para la mayoría de las máquinas actuales, incluso aunque sea
completamente correcto.
Ein este capítulo, vamos a abordar las siguientes cuestiones:
5. 1
¿Qué es el análisis de algoritmos?
La cantidad de tiempo que cualquier algoritmo tarda en ejecutarse
depende
Lina malyor cantidad
casi siempre de la cantidad de entrada que deba procesar, Cabe
esperar, por
de dalos implica que el
ejemplo, que ordenar 10.000 elementos requiera más tiempo que
ordenar 10
programa neoasila más
elementos. El tiempo de ejecución de un algoritmo es, por tanto,
una función
ticmpo de ejccuclon,
del tamaño de la entrada. El valor exacto de la función dependerá
de muchos
186
Capituio 5 Análisis de algoritmos
factores, como por ejemplo de la velocidad de la máquina utilizada,
de la calidad del compilador y.
en algunos casos, de la calidad del programa. Para un determinado
programa en una determinada
computadora, podemos dibujar en una gráfica la función que
expresa el tiempo de ejecución. La
Figura 5.1 ilustra una de esas gráficas para cuatro programas
distintos. Las curvas representan cuatro
funciones comúnmente encontradas en el análisis de algoritmos:
lineal, ON log "M, cuadrática
y
cúbica., El tamaño de la entrada Nvarla de 1 a 100 elementos y los
tiempos de ejecución varían entre
Oy 10 microsegundos. Un rápido vistazo a la Figura 5.1, y a su
compañera, la Figura 5.2, sugiere
que las curvas lineal, O(Nlog M). cuadrática y cúbica representan
distintos tiempos de ejecución, en
orden de preferencia decreciente.
Un ejemplo sería el problema de descargar un archivo de Internet.
Delas funclones que
Suponga que hay un retardo inicial de 2 segundos (para establecer
una
comürmente nos Podemos
conexión), después de lo cual la descarga se produce una
velocidad de 160
encontrar en el analisis
de algortmos, ls ineales
KIseg. Entonces, si el archivo tiene N kilobytes, el tiempo para la
descarga
reprosentan los algorítnos
está dado por la fórmula T(M) = N160 + 2. Esta es una función
lineal.
más eficientes.
Descargar un archivo de 8.000K requiere aproximadamente 52
segundos,
mientras que descargar un archivo dos veces mayor (16.000K)
requiere unos
102 segundos, lo que aproximadamente es el doble de tiempo. Esta
propiedad en la que el tiempo
es, en esencia, directamente proporcional al tamaño de la entrada,
es la caracteristica fundamental
de un algoritmo lineal, que es el tipo de algoritmo más eficiente.
Por contraste, como muestran estas
dos primeras gráficas, algunos de los algoritmos no lineales
requieren tiempos de ejecución muy
grandes. Por ejemplo, el algoritmo lineal es mucho más eficiente
que el algoritmo cúbico.
En este capítulo, vamos a considerar varias cuestiones
importantes:
I
10
8
6
4
g
I
2
o
70
10
20
50
80
90
40
30
60
100
Tamaño de la entrada (N)
Figura 5.1 Tlempos da ejecución para entradas de pequeno
tamano.
5.1 ¿Qué es el análisis de algoritmos?
187
o,8
o,6
O,4
g
02
0
O
1000 2000 3000 4000 5000 6000 700O 5000 9000
10000
Tamaño de la entrada (N)
Figura 5.2 Tiempos de ejecuclón para entradas de tamano
moderado.
Una función cúbicaes una función cuyo término dominante es una
cierta constante multiplicada
por N*. Por ejemplo, 1ONs + N? + 40 N + 80 sería una función
cúbica. De forma similar, una
función cuadrática tiene un término dominante que es igual a una
constante multiplicada por N.
mientras que una función lineal tiene un término dominante que es
igual a una constante multipli-
cada por N. La expresión O(NI log M representa una función cuyo
término dominante es Nveces
el logaritmo de N. El logaritmo es una función que crece
lentamente; por ejemplo, el logaritmo de
1.000.000 (en la típica base 2) es solo 20. El logaritmo crece más
lentamente que una raiz cuadrada
O cúbica (o cualquier otra raíz). Hablaremos del logaritmo con más
profundidad en la Sección 5.5.
Si tenemos dos funciones, cualquiera de ellas puede ser más
pequeña que la otra en un punto
determinado, así que afirmar que, por ejemplo F(M) < G(M no tiene
sentido. En lugar de ello, lo
que hacemos es medir la tasa de crecimiento de las funciones.
Esto está
justificado por tres razones distintas, En primer lugar, para las
funciones
La lasa de crecimienlo
de una funcion Beng su
cúbicas, como la que se muestra en la Figura 5.2, cuando Nes
1.000 el valor
máxma importancia cuando
de la función cúbica está casi completamente determinado por el
término
Nes suflcentemente
cbico. En la función 1ON3 + NZ + 40 N+ 80, para N = 1.000, el valor
de
gande.
la función es [Link], del cual [Link] se debe al
término
ION. Si utilizáramos únicamente el término cúbico para estimar el
valor de
la función completa, el error que se produciría sería de
aproximadamente del 0,01 por ciento. Para
un valor de Nsuficientemente grande, el valor de una función está
principalmente determinado por
su término dominante (el significado de suficientemente grande
varia según la función).
La segunda razón por la que medimas la tasa de crecimiento de
las funciones es que el valor
exacto de la constante que multiplica al término dominante no
tienen ningún significado si tratamos
188
Capituio 5 Análisis de algoritmos
de comparar unas máquinas con otras (aunque los valores
relativos de esa constante para funciones
que crezcan de manera idéntica sí que pueden ser significativos).
Por ejemplo, la calidad del
compilador podria tener una gran influencia sobre el valor de esa
constante, La tercera razón es que
los valores pequeños de Nno suelen ser importantes, Para N= 20,
la Figura 5.1 muestra que todos
los algoritmos terminan en los 5 uS. La diferencia entre el
algoritmo mejor y el peor es inferior al
tiempo que tardamos en parpadear.
Utilizamos la notación o mayúscula o notación de Landau para
capturar
el término más dominante de una función y para representar la
tasa de
La notadion Omayuscula
crecimiento. Por ejemplo, el tiempo de ejecución de un algoritmo
cuadrático
se utEza para captuirar el
término más dominante de
se especifica como 'ON) (que se lee "de orden ene cuadrado "). La
una funcion.
notación o mayúscula también nos permite establecer un orden
relativo entre
funciones, comparando los términos dominantes. Veremos la
notación o
mayúscula de manera más formal en la Sección 5.4.
Para valores pequeños de N (por ejemplo, inferiores a 40), la Figura
5.1 muestra que una curva
puede ser inicialmente mejor que otra y que sin embargo eso no se
cumple para valores de N más
grandes. Por ejemplo, la curva cuadrática es inicialmente mejor
que la curva O(N log M), pero
a medida que Nse hace suficientemente grande, el algoritmo
cuadrático pierde su ventaja. Para
pequeñas cantidades de entrada, hacer comparaciones entre
funciones resulta dificil, porque las
constantes multiplicativas tienen un valor muy significativo. La
función N+ 2.500 es mayor que Nz
cuando Nes menor que 50. Pero a medida que crece N, la función
lineal siempre termina por tener
un valor inferior que la función cuadrática, Además, lo que es aún
más importante, para pequeños
tamaños de entrada los tiempos de ejecución suelen ser
irrelevantes, así que no necesitamos
preocuparnas por ellos. Por ejemplo, la Figura 5.1 muestra que
cuando Nes inferior a 25, los cuatro
algoritmos se ejecutan en menos de 10 HS. En consecuencia,
cuando los tamaños de entrada son muy
pequeños, una buena regla práctica consiste en utilizar el
algoritmo que sea más simple.
La Figura 5.2 demuestra claramente las diferencias entre las
distintas curvas para tamanos
de entrada grandes. Un algoritmo lineal resuelve el problema de
tamaño 10.000 en una pequeña
fracción de segundo. El algoritmo O(Nlog N) utiliza un tiempo
aproximadamente 10 veces mayor,
Observe que las diferencias de tiempo reales dependerán de las
constantes implicadas, y pueden
por tanto ser mayores o menores de ilas indicadas. Dependiendo
de estas constantes, un algoritmo
O(N log M) podría ser más rápido que un algoritmo lineal para
tamaños de entrada relativamente
grandes, Sin embargo, para algoritmos de igual complejidad, los
algoritmos lineales tienden a tener
un mejor rendimiento que los algoritmos O(Nlog Nj.
Sin embargo, esta relación no es cierta para los algoritmos
cuadráticos y
Los algcritmos cuadraticos
cúbicos. Los algoritmos cuadráticos no suelen ser nunca prácticos
cuando
noresuian practicos
el tamaño de entrada supera unos pocos miles, mientras que los
algoritmos
PENra tamafios de entrada
superiores a unos pocos
cúbicos dejan de ser prácticos para tamaños de entrada de solo
unos pocos
mios.
cntenares. Por ejemplo, no es práctico emplear un algoritmo de
ordenación
sencillo para un millón elementas, porque los algoritmos de
ordenación más
simples (como la ordenación por selección o el algoritmo de la
burbuja) son algoritmos cuadráticos.
Los algoritmos de ordenación presentados en el Capítulo 8 se
ejecutan en un tiempo subcuadrático:
es decir, mejor que OXNZ), lo cual hace que sea práctico ordenar
matrices de gran tamano.
La caracteristica más llamativa de estas curvas es que los
algoritmos cuadráticos y cúbicos no
son competitivos en comparación con los otros para entradas
razonablemente grandes. Podemos
189
5.2 Ejemplos de tiempos de ejecución de diversos algoritmos
Figura 5.3 Funciones ordenadas de menor 3 mayor tasa de
crecimiento.
codificar el algoritmo cuadrático utilizando un lenguaje máquina
altamente eficiente y no moles-
tarnos apenas en la codificación del algoritmo lineal, y a pesar de
todo el algoritmo cuadrático saldrá
perdiendo. Ni siquiera los más inteligentes trucos de programación
pueden
Los algoritmos cubicos
hacer que se ejecute rápidamente un algoritmo no eficiente. Por
tanto, antes
no resulan práclicos para
de perder el tiempo tratando de optimizar el código, lo primero que
hay que
tamafios ds entrada de s0lo
hacer es optimizar el algoritmo. `La Figura 5,3 muestra las
funciones que
urws pocoS centenares,
describen comúnmente los tiempos de ejecución de los algoritmos
por orden
de menor a mayor tasa de crecimiento.
5.2
Ejemplos de tiempos de ejecución
de diversos algoritmos
En esta sección vamos a examinar tres problemas. También
esbozaremos algunas posibles
soluciones y. determinaremos el tiempo de ejecución que
exhibirán los algoritmos, sin proporcionar
programas detallados. El objetivo de esta sección es proporcionar
al lector una cierta intuición
acerca del análisis de algoritmos. En la Sección 5.3 daremos más
detalles sobre el proceso y en la
Sección 5.4 abordarermos formalmente un problema de análisis de
algoritmos.
Vamos a examinar los siguientes problemas en esta sección:
Elemento mínimo de una matriz
Dada una matriz de N elementos, determinar el menor de ellos.
Puntos más próximos en el plano
Dados N puntos en un plano (es decir, en un sistema de
coordenadas x-y). encontrar la pareja
de puntos más próximos.
Puntos colineales en el plano
Dados N puntos en un plano (es decir, en un sistema de
coordenadas x-y), determinar si
cualesquiera tres puntos forman una línea recta.
El problema del elemento mínimo es fundamental en informática.
Se puede resolver de la forma
siguiente:
190
Capituio 5 Análisis de algoritmos
El tiempo de ejecución de este algoritmo será o( N) o lineal,
porque lo que hacemos es repetir
una cantidad fija de trabajo para cada elemento de la matriz. Un
algoritmo lineal es lo mejor que
podemos esperar. Si en este caso se consigue es porque tenemos
que examinar cada elemento de la
matriz, lo cual es un proceso que requiere un tiempo jineal.
El problema de los puntos más próximos es un problema
fundamental en gráficos, que se puede
resolver de la forma siguiente:
Sin embargo, este cálculo es muy costoso, porque hay N(N --- 1)/2
pares de puntos.' Por tanto,
hay aproximadamente N? pares de puntos. Examinar todos estos
pares y hallar la distancia minima
entre ellos requiere un tiempo cuadrático. Existe un algoritmo
mejor que se ejecuta en un tiempo
O(N log N y funciona por el procedimiento de evitar tener que
calcular todas las distancias.
Tambiên existe un algoritmo que requiere un tiempo O(N). Estos
dos algoritmos utilizan una serie
de sutiles observaciones para proporcionar resultados más
rápidamente, y caen fuera del alcance de
este texto.
El problema de los puntos colineales es importante para muchos
algoritmos gráficos. La razón
es que la existencia de puntos colineales introduce un caso
degenerado que requiere un tratamiento
especial. El problema se puede resolver directamente enumerando
todos los grupos de tres puntos.
Esta solución es aún más cara, desde el punto de vista
computacional, que la del problema de los
puntos más próximos, porque el número de grupos de tres puntos
distintos es N (N- 1) (N- 2)/6
(utilizando un razonamiento similar al empleado en el problema de
los puntos más próximos). Este
resultado nos dice que la solución directa se conseguiría en un
algoritmo cúbico. También existe una
estrategia más inteligente (que también queda fuera del alcance
de este texto) que permite resolver
el problema en un tiempo cuadrático (y actualmente se está
investigando activamente de manera
continua para conseguir mejoras adicionales).
En la Sección 5.3 examinaremos un problema que ilustra las
diferencias entre los algoritmos
lineales, cuadráticos y cúbicos. También mostraremos cómo se
compara el rendimiento de estos
algoritmos con una predicción matemática. Finalmente, después
de explicar las ideas básicas,
examinaremos de manera más formal la notación o mayúscula.
Cada lino de los N puntos puede emparejarse con N- 1 pantos, lo
que nos da un total de N (N - 1) parejas. Sin embargo, este
emparejamiento hace que se cuenten das veces las parejas A By B
A, asl quIe es preciso dividir entre 2.
5.3 EI problema de la suma máxima de una subsecuencla
contigua
191
5 .3
El problema de la suma máxima
de una subsecuencia contigua
En esta sección vamos a considerar el siguiente problema:
Problema de la suma máxima de una subsecuencia contigua
Dada una serie de enteros (posiblemente negativos) A,, A,, .**, A
encontrar el valor máximo
de E.A, e identificar la secuencia correspondiente. La suma
máxima de una secuencia
contigua es cero si todos los enteros son negativos.
Por ejemplo, si la entrada es {-2, 11, -4 13 -5, 2}, entonces la
respuesta es 20, que representa
la subsecuencia contigua que abarca los elementos 2 a 4
(mostrados en negrita). Como segundo
ejemplo, para la entrada{ i,-3, 4 -2 -1, 6}, la respuesta es 7 para la
subsecuencia que abarca los
últimos cuatro elementos.
En Java, las matrices comienzan con cero, por lo que un programa
Java
Los delalles dle
representaría la entrada como una secuencia Ad a Av Ésto no es
más que un
progranación toman en
detalle de programación y no forma parte del diseño de un
algoritmo.
consideración despues del
diseng dcl aigoritmo.
Antes de analizar los algoritmos existentes para resolver este
problema,
necesitamos comentar algo acerca del caso degenerado en el que
todos los
enteros de entrada sean negativos. El enunciado del problema nos
da una
Considiere siempre las
suma máxima de subsecuencia contigua igual a 0 para este caso,
Podriamos
soluciones vecias.
preguntarnos por quế se hace esto, en lugar de limitarse a devolver
el mayor
de los enteros negativos (es decir, el que tenga un módulo más
pequeño)
de la entrada. La razón es que la subsecuencia vacía, compuesta
de cero enteros, también es una
subsecuencia y su suma es claramente 0. Puesto que la
subsecuencia vacía es contigua, siempre
habrá una subsecuencia contigua cuya suma sea 0. Este resultado
es análogo al caso del conjunto
vacio, que siempre se considera un subconjunto de cualquier
conjunto. Tenga en cuenta que las
soluciones vacías siempre son una posibilidad y que en muchas
circunstancias no se trata en
absoluto de un caso especial.
El problema de la suma máxima de una subsecuencia contigua es
Hay un mantón de
interesante, fundamentalmente, porque existen muchos algoritmos
distintos
algoritmos enormemente
dilerentes (en tórminos
disponibles para resolverlo -y el rendimiento de estos algoritmos
varía
de cidiencta) que
enormemente. En esta sección vamos a analizar tres de estos
algoritmos.
pucden utltzarse para
El primero es un algoritmo obvio de búsqueda exhaustiva, que
resulta
resoêver ei probłema de
la suma maxima de ursa
muy ineficiente. El segundo, es una mejora del primero, que se
consigue
subsecuenda contigua.
realizando una simple ohservación. El tercero es un algoritmo muy
eficiente,
aunque no tan obvio. Demostraremos que su tiempo de ejecución
es lineal.
En el Capítulo 7 veremas un cuarto algoritmo, que tiene un tiempo
de ejecución O(N log N).
Dicho algoritmo no es tan eficiente como el algoritmo lineal, pero
es mucho más eficiente qque los
otros dos. También es un ejemplo típico de los algoritmos con
tiempos de ejecución O(N log N).
Las gráficas mostradas en las Figuras 5.1 y 5.2 son
representativas de estos cuatro algoritmos.
5.3. 1
El algoritmo obvio O(Ns)
El algoritmo más simple es una búsqueda exhaustiva directa, o un
algoritmo de fuerza bruta,
como se muestra en la Figura 5.4. Las lineas 9 y 10 controlan un
par de bucles que iteran a lo
192
Capituio 5 Análisis de algoritmos
1
/* *
2
*
Algoritmo cúbico de suma máxima de subsecuencia contigua.
3
*
seqStart y seqEnd representan 1a mejor secuencia actual.
4
*/
5
public static int ma xSubsequenceS um ( int [ 1 a )
6
7
int ma xS um = 0 :
8
9
forl int i = 0: i C a .length: itt )
10
fort int j i: j < [Link]: j++
5
[
11
int thisSum = 0 :
12
13
14
forc int k = i: k c= j: k++
)
15
thisSum += al k J:
16
17
if( thisSum > ma xSun )
{
18
19
ma xS um = this5um:
20
seqStart = i:
21
seqEnd - j:
22
|
23
24
25
return maxSum:
26
Figura 5.4 Un algoritmo cubico de suma máxima de subsetuencia
contigua.
largo de todas las posibles subsecuencias, Para cada posible
subsecuencia,
Un algoritmo de fucrza
el valor de su suma se calcula en las lineas 12 a 15. Si esa suma es
la mejor
truta es, gencralmente, el
de las encontradas hasta el momento, se actualiza el valor de
maxSun, que
método menos eficiente,
se termina devolviendo en la linea 25. También se actualizan dos
valores
Pefo el más simple de
Coxdiliczr.
int, seqStart y seqEnd (que son campos de clase estáticos y que
indican el
principio y el final de la subsecuencia) cada vez que se encuentra
una nueva
mejor secuencia,
El algoritmo directo de búsqueda exhaustiva tiene como mérito su
extremada simplicidad;
cuanto menos complejo sea un algoritmo, más probable será que
se programe correctamente. Sin
embargo, los algoritmos de búsqueda exhaustiva no suelen ser los
más eficientes posible. En el resto
de esta sección vamos a ver que el tiempo de ejecución de este
algoritmo es cúbico. Contaremos
el número de veces (como función del tamaño de la entrada) que
se evalúan las expresiones de la
Figura 5.4. Lo único que necesitamos es un resultado en notación
O mayúscula, por lo que una vez
que hayamos encontrado un término dominante, podemos ignorar
los términos de menor orden y las
constantes multiplicativas.
El tiempo de ejecución del algoritmo está dominado
completamente por el bucle for más interno
de las lineas 14 y 15. Hay cuatro expresiones que se ejecutan
repetidamente:
193
5.3 EI problema de la suma máxima de una subsecuencla contigua
El número de veces que se ejecuta la expresión 3 hace que sea el
Se utiliza un andlsis
término dominante en las cuatro expresiones. Observe que cada
inicialización
matemático para conlar
el numero de veces
va acompañada por al menas una comprobación. Estamos
ignorando las
que se ejeculan ciertas
constantes, por lo que podemos despreciar el coste de las
inicializaciones; las
instruccioncs.
inicializaciones no pueden ser el caste dorminante de un
algoritmo. Puesto que
la comprobación representada por la expresión 2 solo da un
resultado falso
una vez por cada bucle, el número de comprobaciones con
resultado falso realizado por la expresión
2 es exactamente igual al número de inicializaciones. En
consecuencia, no es dominante. El número
de comprobaciones con resultado verdadero en la expresión 2, el
núnero de incrementos realizados
por la expresión 3 y el número de ajustes de la expresión 4 son
idénticos, Por tanto, el número de
incrementos (es decir, el número de veces que se ejecuta la linea
15) es una medida dominante del
trabajo realizado en el bucle más interno,
EĂ número de veces que se ejecuta la linea 15 es exactamente
igual al número de tripletas
ordenadas (i, j. k que satisfacen 1 s is K5j5 N.? La razón es que el
índice i recorre toda la
matriz, mientras que jva de ial final de la matriz y kva de iaj. Una
estimación rápida y aproximada
es que el número de tripletas es algo inferior a Nx Nx N, o Ns,
porque i,jy k pueden asumir cada
una de ellas uno de Nvalores posibles. La restricción adicional is
ks jreduce este número. Un
cálculo preciso es algo dificil de obtener y lo realizamos en el
Teorema 5,1.
La parte más importante del Teorema 5.1 no es la demostración,
sino el resultado. Hay dos
formas de evaluar el número de tripletas. Una consiste en evaluar
la suma E z; E:,1: Podriamos
evaluar esta suma de dentro hacia fuera (véase el Ejercicio 5.11).
Pero, en lugar de ello, vamos a
emplear una alternativa.
: En java, los indices van de Dan -- 1. Hemas utilizado el
equivalente algorítmico I a Npara simplificar el análisis.