Capítulos 12 a 19 unidos deitel 9 edición (cómo programar en java )

360
Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML 12 Las acciones hablan más alto que las palabras, pero no tan a menudo. —Mark Twain Siempre hay que diseñar algo, considerando que está incluido en un contexto más amplio. —Eliel Saarinen Oh, la vida es un glorioso ciclo de la canción. —Dorothy Parker El diseño de los hermanos Wright... les permitió sobrevivir lo suficiente como para aprender a volar. —Michael Potts Objetivos En este capítulo aprenderá a: Trabajar con una metodología simple de diseño orientado a objetos. Conocer lo que es un documento de requerimientos. Identificar las clases y los atributos de éstas a partir de un documento de requerimientos. Identificar los estados, actividades y operaciones de los objetos, a partir de un documento de requerimientos. Determinar las colaboraciones entre los objetos en un sistema. Trabajar con los diagramas de caso de uso, de clases, de estados, de actividades, de comunicaciones y de secuencia de UML para modelar en forma gráfica un sistema orientado a objetos.

Transcript of Capítulos 12 a 19 unidos deitel 9 edición (cómo programar en java )

Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML 12

Las acciones hablan más alto que las palabras, pero no tan a menudo.—Mark Twain

Siempre hay que diseñar algo, considerando que está incluido en un contexto más amplio.—Eliel Saarinen

Oh, la vida es un glorioso ciclo de la canción.—Dorothy Parker

El diseño de los hermanos Wright... les permitió sobrevivir lo suficiente como para aprender a volar.—Michael Potts

O b j e t i v o sEn este capítulo aprenderá a:

■ Trabajar con una metodología simple de diseño orientado a objetos.

■ Conocer lo que es un documento de requerimientos.

■ Identificar las clases y los atributos de éstas a partir de un documento de requerimientos.

■ Identificar los estados, actividades y operaciones de los objetos, a partir de un documento de requerimientos.

■ Determinar las colaboraciones entre los objetos en un sistema.

■ Trabajar con los diagramas de caso de uso, de clases, de estados, de actividades, de comunicaciones y de secuencia de UML para modelar en forma gráfica un sistema orientado a objetos.

470 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

12.1 Introducción al caso de estudio

12.2 Análisis del documento de requerimientos

12.3 Identifi cación de las clases en un documento de requerimientos

12.4 Identifi cación de los atributos de las clases

12.5 Cómo identifi car los estados y actividades de los objetos

12.6 Identifi cación de las operaciones de las clases

12.7 Indicar la colaboración entre los objetos

12.8 Conclusión

Respuestas a los ejercicios de autoevaluación

12.1 Introducción al caso de estudioAhora empezaremos la parte opcional de nuestro caso de estudio de diseño e implementación orientados a objetos. En este capítulo y en el 13, diseñará e implementará un sistema de software de cajero automático (ATM) orientado a objetos. Este caso de estudio le brindará una experiencia de diseño e implementación concisa, cuidadosamente pautada y completa. En las secciones 12.2 a 12.7 y 13.2 a 13.3, llevaremos a cabo los diversos pasos de un proceso de diseño orientado a objetos (DOO) utilizando UML, mientras relacio-namos estos pasos con los conceptos orientados a objetos que se describieron en los capítulos 2 a 10. En este capítulo trabajará con seis tipos populares de diagramas de UML para representar el diseño en forma gráfica, y en el siguiente (capítulo 13) optimizará el diseño con la herencia, y después realizará una imple-mentación completa del ATM, en una aplicación Java de 673 líneas (sección 13.4).

Éste no es un ejercicio sino una experiencia de aprendizaje de extremo a extremo que concluye con un análisis detallado del código en Java que implementamos con base en nuestro diseño. Este caso de estudio le ayudará a acostumbrarse a los tipos de problemas sustanciales que se encuentran en la industria.

Estos capítulos se pueden estudiar como una unidad continua después de que haya completado la introducción a la programación orientada a objetos en los capítulos 8 a 11. O también puede ir viendo las secciones, una a la vez, después de los capítulos 2 a 8 y 10. Cada sección del caso de estudio comienza con una observación que le indica el capítulo después del cual se puede cubrir.

12.2 Análisis del documento de requerimientos[Nota: esta sección se puede estudiar después del capítulo 2]. Empezaremos nuestro proceso de diseño con la presentación de un documento de requerimientos, el cual especifica el propósito del sistema ATM y qué debe hacer. A lo largo del caso de estudio, nos re-feriremos a este documento de requerimientos.

Documento de requerimientosUn banco local pretende instalar una nueva máquina de cajero automático (ATM), para permitir a los usuarios (es decir, los clientes del banco) realizar transacciones financieras básicas (figura 12.1). Cada usuario sólo puede tener una cuenta en el banco. Los usuarios del ATM deben poder ver el saldo de su cuenta, retirar efectivo (es decir, sacar dinero de una cuenta) y depositar fondos (es decir, meter dinero en una cuenta). La interfaz de usuario del cajero automático contiene los siguientes componentes:

• una pantalla que muestra mensajes al usuario

• un teclado que recibe datos numéricos de entrada del usuario

• un dispensador de efectivo que dispensa efectivo al usuario, y

• una ranura de depósito que recibe sobres para depósitos del usuario.

12.2 Análisis del documento de requerimientos 471

El dispensador de efectivo comienza cada día cargado con 500 billetes de $20. [Nota: debido al alcance limitado de este caso de estudio, ciertos elementos del ATM que se describen aquí no imitan exactamen-te a los de un ATM real. Por ejemplo, comúnmente un ATM contiene un dispositivo que lee el número de cuenta del usuario de una tarjeta para ATM, mientras que el ATM de nuestro caso de estudio pide al usuario que escriba su número de cuenta. Un ATM real también imprime por lo general un recibo al final de una sesión, pero toda la salida de este ATM aparece en la pantalla].

Fig. 12.1 � Interfaz de usuario del cajero automático.

Bienvenido!

Escriba su numero de cuenta: 12345

Escriba su NIP: 54321

Inserte aquí el sobre de depósitoInserte aquí el sobre de depósitoInserte aquí el sobre de depósito

Tome aquí el efectivoTome aquí el efectivoTome aquí el efectivo

Teclado

Pantalla

Ranura dedepósito

Dispensadorde efectivo

Problema deseguridad:el NIP no semuestra comotexto simpleen un ATM real

El banco desea que usted desarrolle software para realizar las transacciones financieras que inicien los clientes a través del ATM. Más tarde, el banco integrará el software con el hardware del ATM. El software debe encapsular la funcionalidad de los dispositivos de hardware (por ejemplo: dispensador de efectivo, ranura para depósito) dentro de los componentes de software, pero no necesita estar in-volucrado en la manera en que estos dispositivos ejecutan su tarea. El hardware del ATM no se ha desa-rrollado aún, por lo que en vez de que usted escriba un software para ejecutarse en el ATM, deberá desarrollar una primera versión del software para que se ejecute en una computadora personal. Esta versión debe utilizar el monitor de la computadora para simular la pantalla del ATM y el teclado de la computadora para simular el teclado numérico del ATM.

Una sesión con el ATM consiste en la autenticación de un usuario (es decir, proporcionar la identidad del usuario) con base en un número de cuenta y un número de identificación personal (NIP), seguida de la creación y la ejecución de transacciones financieras. Para autenticar un usuario y realizar transacciones, el ATM debe interactuar con la base de datos de información sobre las cuen-tas del banco [es decir, una colección organizada de datos almacenados en una computadora; en el capítulo 28 (en inglés) estudiaremos el acceso a bases de datos]. Para cada cuenta de banco, la base de datos almacena un número de cuenta, un NIP y un saldo que indica la cantidad de dinero en la cuenta. [Nota: asumiremos que el banco planea construir sólo un ATM, por lo que no necesitamos preocuparnos de que varios ATM puedan acceder a esta base de datos al mismo tiempo. Lo que es más, vamos a suponer que el banco no realizará modificaciones en la información que hay en la base de datos mientras un usuario accede al ATM. Además, cualquier sistema comercial como un

472 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

ATM se topa con cuestiones complejas y desafiantes de seguridad, las cuales van más allá del alcance de un curso de programación de primer o segundo semestre. No obstante, para simplificar nuestro ejemplo supondremos que el banco confía en el ATM para que acceda a la información en la base de datos y la manipule sin necesidad de medidas de seguridad considerables].

Al acercarse al ATM por primera vez (suponiendo que nadie lo está utilizando), el usuario deberá experimentar la siguiente secuencia de eventos (vea la figura 12.1):

1. La pantalla muestra el mensaje Bienvenido! y pide al usuario que introduzca un número de cuenta.

2. El usuario introduce un número de cuenta de cinco dígitos, mediante el teclado.

3. En la pantalla aparece un mensaje, en el que se pide al usuario que introduzca su NIP (número de identificación personal) asociado con el número de cuenta especificado.

4. El usuario introduce un NIP de cinco dígitos mediante el teclado numérico.1

5. Si el usuario introduce un número de cuenta válido y el NIP correcto para esa cuenta, la pan-talla muestra el menú principal (figura 12.2. Si el usuario introduce un número de cuenta inválido o un NIP incorrecto, la pantalla muestra un mensaje apropiado y después el ATM regresa al paso 1 para reiniciar el proceso de autenticación.

Fig. 12.2 � Menú principal del ATM.

Menu principal 1 - Ver mi saldo 2 - Retirar efectivo 3 - Depositar fondos 4 - SalirEscriba una opcion:

Inserte aquí el sobre de depósitoInserte aquí el sobre de depósitoInserte aquí el sobre de depósito

Tome aquí el efectivoTome aquí el efectivoTome aquí el efectivo

1 En este ATM simple, de línea de comandos y basado en texto, a medida que escribe el NIP, éste aparece en la pantalla. Sin duda ésta es una fuga de seguridad; no es conveniente que alguien esté detrás de usted en un ATM, viendo que aparece su NIP en la pantalla. En el capítulo 14 introduciremos el componente de GUI JPasswordField, el cual muestra asteriscos a medida que el usuario escribe; este componente es más apropiado para ingresar números de NIP y contraseñas. El ejercicio 14.18 le pedirá que cree una versión basada en GUI del ATM, y que use un objeto JPasswordField para obtener el NIP del usuario.

Una vez que el ATM autentica al usuario, el menú principal (figura 12.2) debe contener una opción numerada para cada uno de los tres tipos de transacciones: solicitud de saldo (opción 1), retiro (opción 2)

12.2 Análisis del documento de requerimientos 473

y depósito (opción 3). El menú principal también debe contener una opción para que el usuario pueda salir del sistema (opción 4). Después el usuario elegirá si desea realizar una transacción (oprimiendo 1, 2 o 3) o salir del sistema (oprimiendo 4).

Si el usuario oprime 1 para solicitar su saldo, la pantalla mostrará el saldo de esa cuenta bancaria. Para ello, el ATM deberá obtener el saldo de la base de datos del banco. Los siguientes pasos describen las acciones que ocurren cuando el usuario elige la opción 2 para hacer un retiro:

1. La pantalla muestra un menú (vea la figura 12.3) que contiene montos de retiro estándar: $20 (opción 1), $40 (opción 2), $60 (opción 3), $100 (opción 4) y $200 (opción 5). El menú también contiene una opción que permite al usuario cancelar la transacción (opción 6).

Fig. 12.3 � Menú de retiro del ATM.

Inserte aquí el sobre de depósitoInserte aquí el sobre de depósitoInserte aquí el sobre de depósito

Tome aquí el efectivoTome aquí el efectivoTome aquí el efectivo

Menu de retiro 1 - $20 4 - $100 2 - $40 5 - $200 3 - $60 6 - Cancelar transaccionElija un monto de retiro:

2. El usuario introduce la selección del menú mediante el teclado numérico.

3. Si el monto a retirar elegido es mayor que el saldo de la cuenta del usuario, la pantalla mues-tra un mensaje indicando esta situación y pide al usuario que seleccione un monto más pe-queño. Entonces el ATM regresa al paso 1. Si el monto a retirar elegido es menor o igual que el saldo de la cuenta del usuario (es decir, un monto de retiro aceptable), el ATM procede al paso 4. Si el usuario opta por cancelar la transacción (opción 6), el ATM muestra el menú principal y espera la entrada del usuario.

4. Si el dispensador contiene suficiente efectivo para satisfacer la solicitud, el ATM procede al paso 5. En caso contrario, la pantalla muestra un mensaje indicando el problema y pide al usua-rio que seleccione un monto de retiro más pequeño. Después el ATM regresa al paso 1.

5. El ATM carga el monto de retiro al saldo de la cuenta del usuario en la base de datos del banco (es decir, resta el monto de retiro al saldo de la cuenta del usuario).

6. El dispensador de efectivo entrega el monto deseado de dinero al usuario.

474 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

7. La pantalla muestra un mensaje para recordar al usuario que tome el dinero.

Los siguientes pasos describen las acciones que ocurren cuando el usuario elige la opción 3 (par-tiendo del menú principal de la figura 12.2) para hacer un depósito:

1. La pantalla muestra un mensaje que pide al usuario que introduzca un monto de depósito o que escriba 0 (cero) para cancelar la transacción.

2. El usuario introduce un monto de depósito o 0 mediante el teclado numérico. [Nota: el tecla-do no contiene un punto decimal o signo de dólares, por lo que el usuario no puede escribir una cantidad real en dólares (por ejemplo, $27.25), sino que debe escribir un monto de depó-sito en forma de número de centavos (por ejemplo, 2725). Después, el ATM divide este número entre 100 para obtener un número que represente un monto en dólares (por ejemplo, 2725 ÷ 100 = 27.25)].

3. Si el usuario especifica un monto a depositar, el ATM procede al paso 4. Si elije cancelar la tran-sacción (escribiendo 0), el ATM muestra el menú principal y espera la entrada del usuario.

4. La pantalla muestra un mensaje indicando al usuario que introduzca un sobre de depósito en la ranura para depósitos.

5. Si la ranura de depósitos recibe un sobre dentro de un plazo de tiempo no mayor a dos minutos, el ATM abona el monto del depósito al saldo de la cuenta del usuario en la base de datos del banco (es decir, suma el monto del depósito al saldo de la cuenta del usuario). [Nota: este dine-ro no está disponible de inmediato para retirarse. El banco debe primero verificar físicamente el monto de efectivo en el sobre de depósito, y cualquier cheque que éste contenga debe vali-darse (es decir, el dinero debe transferirse de la cuenta del emisor del cheque a la cuenta del beneficiario). Cuando ocurra uno de estos eventos, el banco actualizará de manera apropiada el saldo del usuario que está almacenado en su base de datos. Esto ocurre de manera indepen-diente al sistema ATM.] Si la ranura de depósito no recibe un sobre dentro de un plazo de tiempo no mayor a dos minutos, la pantalla muestra un mensaje indicando que el sistema canceló la transacción debido a la inactividad. Después el ATM muestra el menú principal y espera la entrada del usuario.

Una vez que el sistema ejecuta una transacción en forma exitosa, debe volver a mostrar el menú principal para que el usuario pueda realizar transacciones adicionales. Si el usuario elije salir del sistema, la pantalla debe mostrar un mensaje de agradecimiento y después el mensaje de bienvenida para el si-guiente usuario.

Análisis del sistema de ATMEn la declaración anterior se presentó un ejemplo simplificado de un documento de requerimientos. Por lo general, dicho documento es el resultado de un proceso detallado de recopilación de requeri-mientos, el cual podría incluir entrevistas con usuarios potenciales del sistema y especialistas en campos relacionados con el mismo. Por ejemplo, un analista de sistemas que se contrate para preparar un docu-mento de requerimientos para software bancario (por ejemplo, el sistema ATM que describimos aquí) podría entrevistar expertos financieros para obtener una mejor comprensión de qué es lo que debe hacer el software. El analista utilizaría la información recopilada para compilar una lista de requerimientos del sistema, para guiar a los diseñadores de sistemas en el proceso de diseño del mismo.

El proceso de recopilación de requerimientos es una tarea clave de la primera etapa del ciclo de vida del software. El ciclo de vida del software especifica las etapas a través de las cuales el software evoluciona desde el momento en que fue concebido hasta que deja de utilizarse. Por lo general, estas etapas incluyen: análisis, diseño, implementación, prueba y depuración, despliegue, mantenimiento y retiro. Existen varios modelos de ciclo de vida del software, cada uno con sus propias preferencias y especificaciones con respecto a cuándo y qué tan a menudo deben llevar a cabo los ingenieros de software las diversas etapas. Los modelos de cascada realizan cada etapa una vez en sucesión, mientras

12.2 Análisis del documento de requerimientos 475

que los modelos iterativos pueden repetir una o más etapas varias veces a lo largo del ciclo de vida de un producto.

La etapa de análisis se enfoca en definir el problema a resolver. Al diseñar cualquier sistema, uno debe resolver el problema de la manera correcta, pero de igual manera uno debe resolver el pro-blema correcto. Los analistas de sistemas recolectan los requerimientos que indican el problema es-pecífico a resolver. Nuestro documento de requerimientos describe nuestro sistema ATM con el suficiente detalle como para que usted no necesite pasar por una etapa de análisis exhaustiva; ya lo hicimos por usted.

Para capturar lo que debe hacer un sistema propuesto, los desarrolladores emplean a menudo una técnica conocida como modelado de caso-uso. Este proceso identifica los casos de uso del sistema, cada uno de los cuales representa una capacidad distinta que el sistema provee a sus clientes. Por ejemplo, es común que los ATM tengan varios casos de uso, como “Ver saldo de cuenta”, “Retirar efectivo”, “Depositar fondos”, “Transferir fondos entre cuentas” y “Comprar estampas postales”. El sistema ATM simplificado que construiremos en este caso de estudio requiere sólo los tres primeros casos de uso.

Cada uno de los casos de uso describe un escenario común en el cual el usuario utiliza el sistema. Usted ya leyó las descripciones de los casos de uso del sistema ATM en el documento de requerimien-tos; las listas de pasos requeridos para realizar cada tipo de transacción (como solicitud de saldo, retiro y depósito) describen en realidad los tres casos de uso de nuestro ATM: “Ver saldo de cuenta”, “Retirar efectivo” y “Depositar fondos”, respectivamente.

Diagramas de caso-usoAhora presentaremos el primero de varios diagramas de UML en el caso de estudio. Crearemos un diagrama de caso-uso para modelar las interacciones entre los clientes de un sistema (en este caso de estudio, los clientes del banco) y sus casos de uso. El objetivo es mostrar los tipos de interacciones que tienen los usuarios con un sistema sin proveer los detalles; éstos se mostrarán en otros diagramas de UML (los cuales presentaremos a lo largo del caso de estudio). A menudo, los diagramas de caso-uso se acompañan de texto informal que describe los casos de uso con más detalle; como el texto que apa-rece en el documento de requerimientos. Los diagramas de caso-uso se producen durante la etapa de análisis del ciclo de vida del software. En sistemas más grandes, los diagramas de caso-uso son herra-mientas indispensables que ayudan a los diseñadores de sistemas a enfocarse en satisfacer las necesi-dades de los usuarios.

La figura 12.4 muestra el diagrama de caso-uso para nuestro sistema ATM. La figura humana representa a un actor, el cual define los roles que desempeña una entidad externa (como una perso-na u otro sistema) cuando interactúa con el sistema. Para nuestro cajero automático, el actor es un

Fig. 12.4 � Diagrama de caso-uso para el sistema ATM, desde la perspectiva del usuario.

Depositar fondos

Retirar efectivo

Ver saldo de cuenta

Usuario

476 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Usuario que puede ver el saldo de una cuenta, retirar efectivo y depositar fondos del ATM. El Usua-rio no es una persona real, sino que constituye los roles que puede desempeñar una persona real (al desempeñar el papel de un Usuario) mientras interactúa con el ATM. Un diagrama de caso-uso puede incluir varios actores. Por ejemplo, el diagrama de caso-uso para un sistema ATM de un banco real podría incluir también un actor llamado Administrador, que rellene el dispensador de efectivo a diario.

Nuestro documento de requerimientos provee los actores: “los usuarios del ATM deben poder ver el saldo de su cuenta, retirar efectivo y depositar fondos”. Por lo tanto, el actor en cada uno de estos tres casos de uso es el usuario que interactúa con el ATM. Una entidad externa (una persona real) desem-peña el papel del usuario para realizar transacciones financieras. La figura 12.4 muestra un actor, cuyo nombre (Usuario) aparece debajo del actor en el diagrama. UML modela cada caso de uso como un óvalo conectado a un actor con una línea sólida.

Los ingenieros de software (más específicamente, los diseñadores de sistemas) deben analizar el documento de requerimientos o un conjunto de casos de uso, y diseñar el sistema antes de que los pro-gramadores lo implementen en un lenguaje de programación específico. Durante la etapa de análisis, los diseñadores de sistemas se enfocan en comprender el documento de requerimientos para producir una especificación de alto nivel que describa qué es lo que el sistema debe hacer. El resultado de la etapa de diseño (una especificación de diseño) debe detallar con claridad cómo debe construirse el sistema para satisfacer estos requerimientos. En las siguientes secciones llevaremos a cabo los pasos de un proceso simple de diseño orientado a objetos (DOO) con el sistema ATM, para producir una especificación de diseño que contenga una colección de diagramas de UML y texto de apoyo.

UML está diseñado para utilizarse con cualquier proceso de DOO. Existen muchos de esos pro-cesos, de los cuales el más conocido es Rational Unified Process™ (RUP), desarrollado por Rational Software Corporation, que ahora forma parte de IBM. RUP es un proceso robusto para diseñar aplicaciones a nivel industrial. Para este caso de estudio, presentaremos nuestro propio proceso de diseño simplificado, desarrollado para estudiantes de cursos de programación de primer y segundo semestre.

Diseño del sistema ATMAhora comenzaremos la etapa de diseño de nuestro sistema ATM. Un sistema es un conjunto de componentes que interactúan para resolver un problema. Por ejemplo, para realizar sus tareas desig-nadas, nuestro sistema ATM tiene una interfaz de usuario (figura 12.1), contiene software para ejecutar transacciones financieras e interactúa con una base de datos de información de cuentas bancarias. La estructura del sistema describe los objetos del sistema y sus interrelaciones. El com-portamiento del sistema describe la manera en que cambia el sistema a medida que sus objetos interactúan entre sí.

Todo sistema tiene tanto estructura como comportamiento; los diseñadores deben especificar am-bos. Existen diversos tipos de estructuras y comportamientos de un sistema. Por ejemplo, las interac-ciones entre los objetos en el sistema son distintas a las interacciones entre el usuario y el sistema, pero aun así ambas constituyen una porción del comportamiento del sistema.

El estándar UML 2 especifica 13 tipos de diagramas para documentar los modelos de un sistema. Cada tipo de diagrama modela una característica distinta de la estructura o del comportamiento de un sistema; seis diagramas se relacionan con la estructura del sistema; los siete restantes se relacionan con su comportamiento. Aquí listaremos sólo los seis tipos de diagramas que utilizaremos en nuestro caso de estudio; uno de ellos modela la estructura del sistema, mientras que los otros cinco modelan el com-portamiento. En el apéndice P (en inglés en el sitio web), veremos las generalidades sobre los siete tipos restantes de diagramas de UML.

1. Los diagramas de caso-uso, como el de la figura 12.4, modelan las interacciones entre un sistema y sus entidades externas (actores) en términos de casos de uso (capacidades del sis-tema, como “Ver saldo de cuenta”, “Retirar efectivo” y “Depositar fondos”).

Ejercicios de autoevaluación de la sección 12.2 477

2. Los diagramas de clases, que estudiará en la sección 12.3, modelan las clases o “bloques de construcción” que se utilizan en un sistema. Cada sustantivo u “objeto” que se describe en el documento de requerimientos es candidato para ser una clase en el sistema (por ejemplo, Cuenta, Teclado). Los diagramas de clases nos ayudan a especificar las relaciones estructurales entre las partes del sistema. Por ejemplo, el diagrama de clases del sistema ATM especificará que el ATM está compuesto físicamente de una pantalla, un teclado, un dispensador de efec-tivo y una ranura para depósitos.

3. Los diagramas de máquina de estado, que estudiará en la sección 12.5, modelan las formas en que un objeto cambia de estado. El estado de un objeto se indica mediante los valores de todos los atributos del objeto, en un momento dado. Cuando un objeto cambia de estado, pue-de comportarse de manera distinta en el sistema. Por ejemplo, después de validar el NIP de un usuario, el ATM cambia del estado “usuario no autenticado” al estado “usuario autenticado”, punto en el cual el ATM permite al usuario realizar transacciones financieras (por ejemplo, ver el saldo de su cuenta, retirar efectivo, depositar fondos).

4. Los diagramas de actividad, que también estudiará en la sección 12.5, modelan la actividad de un objeto: su flujo de trabajo (secuencia de eventos) durante la ejecución del programa. Un diagrama de actividad modela las acciones que realiza el objeto y especifica el orden en el cual desempeña estas acciones. Por ejemplo, un diagrama de actividad muestra que el ATM debe obtener el saldo de la cuenta del usuario (de la base de datos de información de las cuen-tas del banco) antes de que la pantalla pueda mostrar el saldo al usuario.

5. Los diagramas de comunicación (llamados diagramas de colaboración en versiones an-teriores de UML) modelan las interacciones entre los objetos en un sistema, con un énfasis acerca de qué interacciones ocurren. En la sección 12.7 aprenderá que estos diagramas mues-tran cuáles objetos deben interactuar para realizar una transacción en el ATM. Por ejemplo, el ATM debe comunicarse con la base de datos de información de las cuentas del banco para ob-tener el saldo de una cuenta.

6. Los diagramas de secuencia modelan también las interacciones entre los objetos en un sistema, pero a diferencia de los diagramas de comunicación, enfatizan cuándo ocurren las interaccio-nes. En la sección 12.7 aprenderá que estos diagramas ayudan a mostrar el orden en el que ocurren las interacciones al ejecutar una transacción financiera. Por ejemplo, la pantalla pide al usuario que escriba un monto de retiro antes de dispensar el efectivo.

En la sección 12.3 seguiremos diseñando nuestro sistema ATM; ahí identificaremos las clases del documento de requerimientos. Para lograr esto, extraeremos sustantivos clave y frases nominales del do-cumento de requerimientos. Mediante el uso de estas clases, desarrollaremos nuestro primer borrador del diagrama de clases que modelará la estructura de nuestro sistema ATM.

Recurso WebHemos creado un extenso Centro de recursos de UML, el cual contiene muchos vínculos a información adicional, incluyendo introducciones, tutoriales, blogs, libros, certificación, conferencias, herramien-tas para desarrolladores, documentación, libros electrónicos, secciones FAQ, foros, grupos, UML en Java, podcasts, seguridad, herramientas, descargas, cursos de capacitación, videos y otros más. Para navegar por nuestro Centro de recursos de UML, visite www.deitel.com/UML/.

Ejercicios de autoevaluación de la sección 12.212.1 Suponga que habilitamos a un usuario de nuestro sistema ATM para transferir dinero entre dos cuentas bancarias. Modifique el diagrama de caso-uso de la figura 12.4 para reflejar este cambio.

478 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

12.2 Los modelan las interacciones entre los objetos en un sistema, con énfasis acerca de cuándo ocurren estas interacciones.

a) Diagramas de clasesb) Diagramas de secuenciac) Diagramas de comunicaciónd) Diagramas de actividad

12.3 ¿Cuál de las siguientes opciones lista las etapas de un típico ciclo de vida de software, en orden secuencial?a) diseño, análisis, implementación, pruebab) diseño, análisis, prueba, implementaciónc) análisis, diseño, prueba, implementaciónd) análisis, diseño, implementación, prueba

12.3 Identificación de las clases en un documento de requerimientos

[Nota: esta sección se puede estudiar después del capítulo 3]. Ahora empezaremos a diseñar el sistema ATM. En esta sección identificaremos las clases necesarias para crear el sistema ATM, analizando los sustantivos y las frases nominales que aparecen en el documento de requerimientos. Presentaremos los diagramas de clases de UML para modelar las relaciones entre estas clases. Este primer paso es importante para definir la estructura de nuestro sistema.

Identificación de las clases en un sistemaPara comenzar nuestro proceso de DOO, identificaremos las clases requeridas para crear el sistema ATM. Más adelante describiremos estas clases mediante el uso de los diagramas de clases de UML y las imple-mentaremos en Java. Primero debemos revisar el documento de requerimientos de la sección 12.2, para identificar los sustantivos y frases nominales clave que nos ayuden a identificar las clases que conformarán el sistema ATM. Tal vez decidamos que algunos de estos sustantivos y frases nominales sean atributos de otras clases en el sistema. Tal vez también concluyamos que algunos de los sustantivos no corresponden a ciertas partes del sistema y, por ende, no deben modelarse. A medida que avancemos por el proceso de diseño podemos ir descubriendo clases adicionales.

La figura 12.5 lista los sustantivos y frases nominales que se encontraron en el documento de reque-rimientos. Los listaremos de izquierda a derecha, en el orden en el que los encontramos por primera vez. Sólo listaremos la forma singular de cada sustantivo o frase nominal.

Fig. 12.5 � Sustantivos y frases nominales en el documento de requerimientos del ATM.

Sustantivos y frases nominales en el documento de requerimientos del ATM

banco dinero / fondos número de cuenta ATM

pantalla NIP usuario teclado numérico

base de datos del banco cliente dispensador de efectivo solicitud de saldo

transacción billete de $20 / efectivo retiro cuenta

ranura de depósito depósito saldo sobre de depósito

Crearemos clases sólo para los sustantivos y frases nominales que tengan importancia en el sistema ATM. No modelamos “banco” como una clase, ya que el banco no es una parte del sistema ATM; el banco sólo quiere que nosotros construyamos el ATM. “Cliente” y “usuario” también representan enti-

12.3 Identifi cación de las clases en un documento de requerimientos 479

dades fuera del sistema; son importantes debido a que interactúan con nuestro sistema ATM, pero no necesitamos modelarlos como clases en el software del ATM. Recuerde que modelamos un usuario del ATM (es decir, un cliente del banco) como el actor en el diagrama de casos de uso de la figura 12.4.

No necesitamos modelar “billete de $20” ni “sobre de depósito” como clases. Éstos son objetos físicos en el mundo real, pero no forman parte de lo que se va a automatizar. Podemos representar en forma adecuada la presencia de billetes en el sistema, mediante el uso de un atributo de la clase que modela el dispensador de efectivo (en la sección 12.4 asignaremos atributos a las clases del sis-tema ATM). Por ejemplo, el dispensador de efectivo mantiene un conteo del número de billetes que contiene. El documento de requerimientos no dice nada acerca de lo que debe hacer el sistema con los sobres de depósito después de recibirlos. Podemos suponer que con sólo admitir la recepción de un sobre (una operación que realiza la clase que modela la ranura de depósito) es suficiente para re-presentar la presencia de un sobre en el sistema. En la sección 12.6 asignaremos operaciones a las clases del sistema ATM.

En nuestro sistema ATM simplificado, lo más apropiado sería representar varios montos de “dinero”, incluyendo el “saldo” de una cuenta, como atributos de clases. De igual forma, los sustantivos “número de cuenta” y “NIP” representan piezas importantes de información en el sistema ATM. Son atributos importantes de una cuenta bancaria. Sin embargo, no exhiben comportamientos. Por ende, podemos modelarlos de la manera más apropiada como atributos de una clase de cuenta.

Aunque, con frecuencia, el documento de requerimientos describe una “transacción” en un senti-do general, no modelaremos la amplia noción de una transacción financiera en este momento. En vez de ello, modelaremos los tres tipos de transacciones (es decir, “solicitud de saldo”, “retiro” y “depósito”) como clases individuales. Estas clases poseen los atributos específicos necesarios para ejecutar las transacciones que representan. Por ejemplo, para un retiro se necesita conocer el monto de dinero que el usuario desea retirar. Sin embargo, una solicitud de saldo no requiere datos adicionales aparte del número de cuenta. Lo que es más, las tres clases de transacciones exhiben comportamientos únicos. Para un retiro se requiere entregar efectivo al usuario, mientras que para un depósito se requiere recibir un sobre de depósito del usuario. En la sección 13.3 “factorizaremos” las características comunes de todas las transacciones en una clase de “transacción” general, mediante el uso del concepto orientado a objetos de la herencia.

Determinaremos las clases para nuestro sistema con base en los sustantivos y frases nominales res-tantes de la figura 12.5. Cada una de ellas se refiere a uno o varios de los siguientes elementos:

• ATM

• pantalla

• teclado numérico

• dispensador de efectivo

• ranura de depósito

• cuenta

• base de datos del banco

• solicitud de saldo

• retiro

• depósito

Es probable que los elementos de esta lista sean clases que necesitaremos implementar en nuestro sistema.

Ahora podemos modelar las clases en nuestro sistema, con base en la lista que hemos creado. En el proceso de diseño escribimos los nombres de las clases con la primera letra en mayúscula (una con-vención de UML), como lo haremos cuando escribamos el código de Java para implementar nuestro diseño. Si el nombre de una clase contiene más de una palabra, juntaremos todas las palabras y

480 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

escribiremos la primera letra de cada una de ellas en mayúscula (por ejemplo, NombreConVarias-Palabras). Utilizando esta convención, crearemos las clases ATM, Pantalla, Teclado, Dispensador-Efectivo, RanuraDeposito, Cuenta, BaseDatosBanco, SolicitudSaldo, Retiro y Deposito. Cons-truiremos nuestro sistema mediante el uso de todas estas clases como bloques de construcción. Sin embargo, antes de empezar a construir el sistema, debemos comprender mejor la forma en que las clases se relacionan entre sí.

Modelado de las clasesUML nos permite modelar, a través de los diagramas de clases, las clases en el sistema ATM y sus inte-rrelaciones. La figura 12.6 representa a la clase ATM. En UML, cada clase se modela como un rectángu-lo con tres compartimientos. El compartimiento superior contiene el nombre de la clase, centrado hori-zontalmente y en negrita. El compartimiento intermedio contiene los atributos de la clase (en las secciones 12.4 y 12.5 hablaremos sobre los atributos). El compartimiento inferior contiene las operacio-nes de la clase (que veremos en la sección 12.6). En la figura 12.6, los compartimientos intermedio e in-ferior están vacíos, ya que no hemos determinado los atributos y operaciones de esta clase todavía.

Fig. 12.6 � Representación de una clase en UML mediante un diagrama de clases.

ATM

Fig. 12.7 � Diagrama de clases que muestra una asociación entre clases.

Ejecuta1

transaccionActual

0..1RetiroATM

Los diagramas de clases también muestran las relaciones entre las clases del sistema. La figura 12.7 muestra cómo nuestras clases ATM y Retiro se relacionan una con la otra. Por el momento modelaremos sólo este subconjunto de las clases del ATM, por cuestión de simpleza. Más adelante en esta sección, presentaremos un diagrama de clases más completo. Observe que los rectángulos que representan a las clases en este diagrama no están subdivididos en compartimientos. UML permite suprimir los atribu-tos y las operaciones de una clase de esta forma, cuando sea apropiado, para crear diagramas más legibles. Un diagrama de este tipo se denomina diagrama con elementos omitidos (elided diagram): su infor-mación, como el contenido de los compartimientos segundo y tercero, no se modela. En las secciones 12.4 y 12.6 colocaremos información en estos compartimientos.

En la figura 12.7, la línea sólida que conecta a las dos clases representa una asociación: una rela-ción entre clases. Los números cerca de cada extremo de la línea son valores de multiplicidad; éstos indi-can cuántos objetos de cada clase participan en la asociación. En este caso, al seguir la línea de un extremo al otro se revela que, en un momento dado, un objeto ATM participa en una asociación con cero o con un objeto Retiro; cero si el usuario actual no está realizando una transacción o si ha solicitado un tipo distinto de transacción, y uno si el usuario ha solicitado un retiro. UML puede modelar muchos tipos de multiplicidad. La figura 12.8 lista y explica los tipos de multiplicidad.

12.3 Identifi cación de las clases en un documento de requerimientos 481

Una asociación puede tener nombre. Por ejemplo, la palabra Ejecuta por encima de la línea que conecta a las clases ATM y Retiro en la figura 12.7 indica el nombre de esa asociación. Esta parte del dia-grama se lee así: “un objeto de la clase ATM ejecuta cero o un objeto de la clase Retiro”. Los nombres de las asociaciones son direccionales, como lo indica la punta de flecha rellena; por lo tanto, sería inapro-piado, por ejemplo, leer la anterior asociación de derecha a izquierda como “cero o un objeto de la clase Retiro ejecuta un objeto de la clase ATM”.

La palabra transaccionActual en el extremo de Retiro de la línea de asociación en la figura 12.7 es un nombre de rol, el cual identifica el rol que desempeña el objeto Retiro en su relación con el ATM. Un nombre de rol agrega significado a una asociación entre clases, ya que identifica el rol que desempeña una clase dentro del contexto de una asociación. Una clase puede desempeñar varios roles en el mismo sistema. Por ejemplo, en un sistema de personal de una universidad, una persona puede desempeñar el rol de “profesor” con respecto a los estudiantes. La misma persona puede desempeñar el rol de “colega” cuando participa en una asociación con otro profesor, y de “entrenador” cuando entrena a los atletas estudiantes. En la figura 12.7, el nombre de rol transaccionActual indica que el objeto Retiro, que participa en la asociación Ejecuta con un objeto de la clase ATM, representa a la transacción que está procesando el ATM en ese momento. En otros contextos, un objeto Retiro puede desempeñar otros roles (por ejemplo, la “transacción anterior”). Observe que no especificamos un nombre de rol para el extremo del ATM de la asociación Ejecuta. A menudo, los nombres de los roles se omiten en los diagra-mas de clases, cuando el significado de una asociación está claro sin ellos.

Además de indicar relaciones simples, las asociaciones pueden especificar relaciones más comple-jas, como cuando los objetos de una clase están compuestos de objetos de otras clases. Considere un cajero automático real. ¿Qué “piezas” reúne un fabricante para construir un ATM funcional? Nuestro documento de requerimientos nos indica que el ATM está compuesto de una pantalla, un teclado, un dispensador de efectivo y una ranura de depósito.

En la figura 12.9, los diamantes sólidos que se adjuntan a las líneas de asociación de la clase ATM indican que esta clase tiene una relación de composición con las clases Pantalla, Teclado, Dispen-sadorEfectivo y RanuraDeposito. La composición implica una relación todo/parte. La clase que tiene el símbolo de composición (el diamante sólido) en su extremo de la línea de asociación es el todo (en este caso, ATM), y las clases en el otro extremo de las líneas de asociación son las partes; en este caso, las clases Pantalla, Teclado, DispensadorEfectivo y RanuraDeposito. Las composiciones en la figura 12.9 indican que un objeto de la clase ATM está formado por un objeto de la clase Pantalla, un objeto de

Símbolo Signifi cado

0 Ninguno

1 Uno

m Un valor entero

0..1 Cero o uno

m, n m o n

m..n Cuando menos m, pero no más que n

* Cualquier entero no negativo (cero o más)

0..* Cero o más (idéntico a *)

1..* Uno o más

Fig. 12.8 � Tipos de multiplicidad.

482 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

la clase DispensadorEfectivo, un objeto de la clase Teclado y un objeto de la clase RanuraDeposito. El ATM tiene una pantalla, un teclado, un dispensador de efectivo y una ranura de depósito (como vimos en el capítulo 9, la relación es un define la herencia. En la sección 13.3 veremos que hay una buena opor-tunidad de usar la herencia en el diseño del sistema ATM).

Fig. 12.9 � Diagrama de clases que muestra las relaciones de composición.

1 1 1 1

1

1

1

1

Pantalla

ATM

Teclado

RanuraDeposito DispensadorEfectivo

De acuerdo con la especificación de UML (www.uml.org/technology/documents/formal/uml.htm), las relaciones de composición tienen las siguientes propiedades:

1. Sólo una clase en la relación puede representar el todo (es decir, el diamante puede colocarse sólo en un extremo de la línea de asociación). Por ejemplo, la pantalla es parte del ATM o el ATM es parte de la pantalla, pero la pantalla y el ATM no pueden representar ambos el todo dentro de la relación.

2. Las partes en la relación de composición existen sólo mientras exista el todo, y el todo es respon-sable de la creación y destrucción de sus partes. Por ejemplo, el acto de construir un ATM inclu-ye la manufactura de sus partes. Lo que es más, si el ATM se destruye, también se destruyen su pantalla, teclado, dispensador de efectivo y ranura de depósito.

3. Una parte puede pertenecer sólo a un todo a la vez, aunque esa parte puede quitarse y unirse a otro todo, el cual entonces asumirá la responsabilidad de esa parte.

Los diamantes sólidos en nuestros diagramas de clases indican las relaciones de composición que cumplen con estas propiedades. Si una relación “es un” no satisface uno o más de estos criterios, UML especifica que se deben adjuntar diamantes sin relleno a los extremos de las líneas de asociación para indicar una agregación: una forma más débil de la composición. Por ejemplo, una computadora per-sonal y un monitor de computadora participan en una relación de agregación: la computadora “tiene un” monitor, pero las dos partes pueden existir en forma independiente, y el mismo monitor pue-de conectarse a varias computadoras a la vez, con lo cual se violan las propiedades segunda y tercera de la composición.

La figura 12.10 muestra un diagrama de clases para el sistema ATM. Este diagrama modela la mayoría de las clases que identificamos antes en esta sección, así como las asociaciones entre ellas que podemos inferir del documento de requerimientos. Las clases SolicitudSaldo y Deposito participan en asociaciones similares a las de la clase Retiro, por lo que preferimos omitirlas en este diagrama por cuestión de simpleza. En la sección 13.3 expandiremos nuestro diagrama de clases para incluir todas las clases en el sistema ATM.

12.3 Identifi cación de las clases en un documento de requerimientos 483

La figura 12.10 presenta un modelo gráfico de la estructura del sistema ATM. Este diagrama de clases incluye a las clases BaseDatosBanco y Cuenta, junto con varias asociaciones que no presenta-mos en las figuras 12.7 o 12.9. El diagrama de clases muestra que la clase ATM tiene una relación de uno a uno con la clase BaseDatosBanco: un objeto ATM autentica a los usuarios con base en un objeto BaseDatosBanco. En la figura 12.10 también modelamos el hecho de que la base de datos del banco contiene información sobre muchas cuentas; un objeto de la clase BaseDatosBanco participa en una relación de composición con cero o más objetos de la clase Cuenta. El valor de multiplicidad 0..* en el extremo de la clase Cuenta, de la asociación entre las clases BaseDatosBanco y Cuenta, indica que cero o más objetos de la clase Cuenta participan en la asociación. La clase BaseDatosBanco tiene una relación de uno a varios con la clase Cuenta; BaseDatosBanco puede contener muchos objetos Cuenta. De manera similar, la clase Cuenta tiene una relación de varios a uno con la clase BaseDatos-Banco; puede haber muchos objetos Cuenta en BaseDatosBanco. Si recuerda la figura 12.8, el valor de multiplicidad * es idéntico a 0..*. Incluimos 0..* en nuestros diagramas de clases por cuestión de claridad.

La figura 12.10 también indica que, en cualquier momento dado, puede existir 0 o 1 objeto Retiro. Si el usuario va a realizar un retiro, “un objeto de la clase Retiro accede a/modifica un saldo de cuenta a través de un objeto de la clase BaseDatosBanco”. Podríamos haber creado una asocia-ción directamente entre la clase Retiro y la clase Cuenta. No obstante, el documento de requeri-mientos indica que el “ATM debe interactuar con la base de datos de información de las cuentas del banco” para realizar transacciones. Una cuenta de banco contiene información delicada, por lo que los ingenieros de sistemas deben considerar siempre la seguridad de los datos personales al diseñar un sistema. Por ello, sólo BaseDatosBanco puede acceder a una cuenta y manipularla en forma directa.

Fig. 12.10 � Diagrama de clases para el modelo del sistema ATM.

Accede a/modifica el saldode una cuenta a través de

Ejecuta

1

1

1

1 1

1

1

1

1 1 1 1

1

0..*

0..10..1

0..1 0..10..1

1Contiene

Autentica al usuario en base a

Teclado

Retiro

RanuraDeposito

ATM

DispensadorEfectivo

Pantalla

Cuenta

BaseDatosBanco

1

1

484 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Todas las demás partes del sistema deben interactuar con la base de datos para recuperar o actualizar la información de las cuentas (por ejemplo, el saldo de una cuenta).

El diagrama de clases de la figura 12.10 también modela las asociaciones entre la clase Retiro y las clases Pantalla, DispensadorEfectivo y Teclado. Una transacción de retiro implica pedir al usuario que seleccione el monto a retirar; también implica recibir entrada numérica. Estas acciones requieren el uso de la pantalla y del teclado, respectivamente. Además, para entregar efectivo al usuario se requiere acceso al dispensador de efectivo.

Aunque no se muestran en la figura 12.10, las clases SolicitudSaldo y Deposito participan en varias asociaciones con las otras clases del sistema ATM. Al igual que la clase Retiro, cada una de estas clases se asocia con las clases ATM y BaseDatosBanco. Un objeto de la clase SolicitudSaldo también se asocia con un objeto de la clase Pantalla para mostrar al usuario el saldo de una cuenta. La clase Deposito se asocia con las clases Pantalla, Teclado y RanuraDeposito. Al igual que los retiros, las transacciones de depósito requieren el uso de la pantalla y el teclado para mostrar mensajes y recibir datos de entrada, respectivamente. Para recibir sobres de depósito, un objeto de la clase Deposito acce-de a la ranura de depósitos.

Ya hemos identificado las clases iniciales en nuestro sistema ATM (aunque tal vez descubramos otras, a medida que avancemos con el diseño y la implementación). En la sección 12.4 determinaremos los atributos para cada una de estas clases, y en la sección 12.5 utilizaremos estos atributos para exami-nar la forma en que cambia el sistema con el tiempo.

Ejercicios de autoevaluación de la sección 12.312.4 Suponga que tenemos una clase llamada Auto, la cual representa a un auto. Piense en algunas de las distintas piezas que podría reunir un fabricante para producir un auto completo. Cree un diagrama de clases (similar a la figura 12.9) que modele algunas de las relaciones de composición de la clase Auto.

12.5 Suponga que tenemos una clase llamada Archivo, la cual representa un documento electrónico en una compu-tadora independiente, sin conexión de red, representada por la clase Computadora. ¿Qué tipo de asociación existe entre la clase Computadora y la clase Archivo?

a) La clase Computadora tiene una relación de uno a uno con la clase Archivo. b) La clase Computadora tiene una relación de varios a uno con la clase Archivo.c) La clase Computadora tiene una relación de uno a varios con la clase Archivo.d) La clase Computadora tiene una relación de varios a varios con la clase Archivo.

12.6 Indique si la siguiente aseveración es verdadera o falsa. Si es falsa, explique por qué: un diagrama de clases de UML, en el que no se modelan los compartimientos segundo y tercero, se denomina diagrama con elementos omitidos (elided diagram).

12.7 Modifique el diagrama de clases de la figura 12.10 para incluir la clase Deposito, en vez de la clase Retiro.

12.4 Identificación de los atributos de las clases[Nota: esta sección se puede estudiar después del capítulo 4]. Las clases tienen atributos (datos) y operaciones (comportamientos). Los atributos de las clases se imple-mentan como campos, y las operaciones de las clases se implementan como métodos. En esta sección determinaremos muchos de los atributos necesarios en el sistema ATM. En la sección 12.5 examinaremos cómo esos atributos representan el estado de un objeto. En la sección 12.6 determinaremos las operacio-nes de las clases.

Identificación de los atributosConsidere los atributos de algunos objetos reales: los atributos de una persona incluyen su altura, peso y si es zurdo, diestro o ambidiestro. Los atributos de un radio incluyen la estación, el volumen y si está

12.4 Identifi cación de los atributos de las clases 485

en AM o FM. Los atributos de un auto incluyen las lecturas de su velocímetro y odómetro, la cantidad de gasolina en su tanque y la velocidad de marcha en la que se encuentra. Los atributos de una compu-tadora personal incluyen su fabricante (por ejemplo, Dell, Sun, Apple o IBM), el tipo de pantalla (por ejemplo, LCD o CRT), el tamaño de su memoria principal y el de su disco duro.

Podemos identificar muchos atributos de las clases en nuestro sistema, analizando las palabras y frases descriptivas en el documento de requerimientos. Para cada palabra o frase que descubramos desempeña un rol importante en el sistema ATM, creamos un atributo y lo asignamos a una o más de las clases identificadas en la sección 12.3. También creamos atributos para representar los datos adi-cionales que pueda necesitar una clase, ya que dichas necesidades se van aclarando a lo largo del pro-ceso de diseño.

La figura 12.11 lista las palabras o frases del documento de requerimientos que describen a cada una de las clases. Para formar esta lista, leemos el documento de requerimientos e identificamos cual-quier palabra o frase que haga referencia a las características de las clases en el sistema. Por ejemplo, el documento de requerimientos describe los pasos que se llevan a cabo para obtener un “monto de retiro”, por lo que listamos “monto” enseguida de la clase Retiro.

Clase Palabras y frases descriptivas

ATM el usuario es autenticadoSolicitudSaldo número de cuenta

Retiro número de cuentamonto

Deposito número de cuentamonto

BaseDatosBanco [no hay palabras o frases descriptivas]

Cuenta número de cuentaNIPsaldo

Pantalla [no hay palabras o frases descriptivas]

Teclado [no hay palabras o frases descriptivas]

DispensadorEfectivo empieza cada día cargado con 500 billetes de $20

RanuraDeposito [no hay palabras o frases descriptivas]

La figura 12.11 nos conduce a crear un atributo de la clase ATM. Esta clase mantiene información acerca del estado del ATM. La frase “el usuario es autenticado” describe un estado del ATM (en la sec-ción 12.5 introduciremos los estados), por lo que incluimos usuarioAutenticado como un atributo Boolean (es decir, un atributo que tiene un valor de true o false) en la clase ATM. El atributo tipo Boolean en UML es equivalente al tipo boolean en Java. Este atributo indica si el ATM autenticó con éxito al usuario actual o no; usuarioAutenticado debe ser true para que el sistema permita al usuario realizar transacciones y acceder a la información de la cuenta. Este atributo nos ayuda a cerciorarnos de la seguridad de los datos en el sistema.

Las clases SolicitudSaldo, Retiro y Deposito comparten un atributo. Cada transacción requiere un “número de cuenta” que corresponde a la cuenta del usuario que realiza la transacción. Asignamos

Fig. 12.11 � Palabras y frases descriptivas del documento de requerimientos del ATM.

486 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

el atributo entero numeroCuenta a cada clase de transacción para identificar la cuenta a la que se aplica un objeto de la clase.

Las palabras y frases descriptivas en el documento de requerimientos también sugieren ciertas diferencias en los atributos requeridos por cada clase de transacción. El documento de requerimientos indica que para retirar efectivo o depositar fondos, los usuarios deben introducir un “monto” especí-fico de dinero para retirar o depositar, respectivamente. Por ende, asignamos a las clases Retiro y Deposito un atributo llamado monto para almacenar el valor suministrado por el usuario. Los montos de dinero relacionados con un retiro y un depósito son características que definen estas transaccio-nes, que el sistema requiere para que se lleven a cabo. Sin embargo, la clase SolicitudSaldo no nece-sita datos adicionales para realizar su tarea; sólo requiere un número de cuenta para indicar la cuenta cuyo saldo hay que obtener.

La clase Cuenta tiene varios atributos. El documento de requerimientos establece que cada cuen-ta de banco tiene un “número de cuenta” y un “NIP”, que el sistema utiliza para identificar las cuentas y autentificar a los usuarios. A la clase Cuenta le asignamos dos atributos enteros: numeroCuenta y nip. El documento de requerimientos también especifica que una cuenta debe mantener un “saldo” del monto de dinero que hay en la cuenta, y que el dinero que el usuario deposita no estará disponible para su retiro sino hasta que el banco verifique la cantidad de efectivo en el sobre de depósito y cualquier cheque que contenga. Sin embargo, una cuenta debe registrar de todas formas el monto de dinero que deposita un usuario. Por lo tanto, decidimos que una cuenta debe representar un saldo utilizando dos atributos: saldoDisponible y saldoTotal. El atributo saldoDisponible rastrea el monto de dinero que un usuario puede retirar de la cuenta. El atributo saldoTotal se refiere al monto total de di-nero que el usuario tiene “en depósito” (es decir, el monto de dinero disponible, más el monto de depósitos en efectivo o la cantidad de cheques esperando a ser verificados). Por ejemplo, suponga que un usuario del ATM deposita $50.00 en efectivo, en una cuenta vacía. El atributo saldoTotal se incrementaría a $50.00 para registrar el depósito, pero el saldoDisponible permanecería en $0. [Nota: estamos suponiendo que el banco actualiza el atributo saldoDisponible de una Cuenta poco después de que se realiza la transacción del ATM, en respuesta a la confirmación de que se encontró un monto equivalente a $50 en efectivo o cheques en el sobre de depósito. Asumimos que esta ac-tualización se realiza a través de una transacción que realiza el empleado del banco mediante el uso de un sistema bancario distinto al del ATM. Por ende, no hablaremos sobre esta transacción en nues-tro ejemplo práctico].

La clase DispensadorEfectivo tiene un atributo. El documento de requerimientos establece que el dispensador de efectivo “empieza cada día cargado con 500 billetes de $20”. El dispensador de efec-tivo debe llevar el registro del número de billetes que contiene para determinar si hay suficiente efectivo disponible para satisfacer la demanda de los retiros. Asignamos a la clase DispensadorEfectivo el atri-buto entero conteo, el cual se establece al principio en 500.

Para los verdaderos problemas en la industria, no existe garantía alguna de que el documento de re-querimientos será lo suficientemente robusto y preciso como para que el diseñador de sistemas orientados a objetos determine todos los atributos, o inclusive todas las clases. La necesidad de clases, atributos y comportamientos adicionales puede irse aclarando a medida que avance el proceso de diseño. A medida que progresemos a través de este caso de estudio, nosotros también seguiremos agregando, modificando y eliminando información acerca de las clases en nuestro sistema.

Modelado de los atributosEl diagrama de clases de la figura 12.12 enlista algunos de los atributos para las clases en nuestro siste-ma; las palabras y frases descriptivas en la figura 12.11 nos llevan a identificar estos atributos. Por cuestión de simpleza, la figura 12.12 no muestra las asociaciones entre las clases; en la figura 12.10 mostramos estas asociaciones. Ésta es una práctica común de los diseñadores de sistemas, a la hora de desarrollar los diseños. En la sección 12.3 vimos que en UML, los atributos de una clase se colocan

12.4 Identifi cación de los atributos de las clases 487

en el compartimiento intermedio del rectángulo de la clase. Listamos el nombre de cada atributo y su tipo, separados por un signo de dos puntos (:), seguido en algunos casos de un signo de igual (=) y de un valor inicial.

Considere el atributo usuarioAutenticado de la clase ATM:

usuarioAutenticado : Boolean = false

La declaración de este atributo contiene tres piezas de información acerca del atributo. El nombre del atributo es usuarioAutenticado. El tipo del atributo es Boolean. En Java, un atributo puede repre-sentarse mediante un tipo primitivo, como boolean, int o double, o por un tipo de referencia como una clase. Hemos optado por modelar sólo los atributos de tipo primitivo en la figura 12.12; en breve hablaremos sobre el razonamiento detrás de esta decisión. Los tipos de los atributos en la figura 12.12 están en notación de UML. Asociaremos los tipos Boolean, Integer y Double en el diagrama de UML con los tipos primitivos boolean, int y double en Java, respectivamente.

Fig. 12.12 � Clases con atributos.

ATM

usuarioAutenticado : Boolean = false

SolicitudSaldo

numeroCuenta : Integer

DispensadorEfectivo

conteo : Integer = 500

RanuraDeposito

Pantalla

Teclado

Retiro

numeroCuenta : Integermonto : Double

BaseDatosBanco

Deposito

numeroCuenta : Integermonto : Double

Cuenta

numeroCuenta : Integernip : IntegersaldoDisponible : DoublesaldoTotal : Double

También podemos indicar un valor inicial para un atributo. El atributo usuarioAutenticado en la clase ATM tiene un valor inicial de false. Esto indica que al principio el sistema no considera que el usuario esté autenticado. Si no se especifica un valor inicial para un atributo, sólo se muestran su nombre y tipo (separados por dos puntos). Por ejemplo, el atributo numeroCuenta de la clase SolicitudSaldo es un entero. Aquí no mostramos un valor inicial, ya que el valor de este atributo es un número que to-

488 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

davía no conocemos. Este número se determinará en tiempo de ejecución, con base en el número de cuenta introducido por el usuario actual del ATM.

La figura 12.12 no incluye atributos para las clases Pantalla, Teclado y RanuraDeposito. Éstos son componentes importantes de nuestro sistema, para los cuales nuestro proceso de diseño aún no ha revelado ningún atributo. No obstante, tal vez descubramos algunos en las fases restantes de diseño, o cuando implementemos estas clases en Java. Esto es perfectamente normal.

Observación de ingeniería de software 12.1En las primeras fases del proceso de diseño, a menudo las clases carecen de atributos (y ope-raciones). Sin embargo, esas clases no deben eliminarse, ya que los atributos (y las operaciones) pueden hacerse evidentes en las fases posteriores de diseño e implementación.

La figura 12.12 tampoco incluye atributos para la clase BaseDatosBanco. En Java, los atribu-tos pueden representarse mediante los tipos primitivos o los tipos por referencia. Hemos optado por incluir sólo los atributos de tipo primitivo en el diagrama de clases de la figura 12.12 (y en los diagra-mas de clases similares a lo largo del caso de estudio). Un atributo de tipo por referencia se modela con más claridad como una asociación entre la clase que contiene la referencia y la clase del objeto al que apunta la referencia. Por ejemplo, el diagrama de clases de la figura 12.10 indica que la clase BaseDatosBanco participa en una relación de composición con cero o más objetos Cuenta. De esta composición podemos determinar que, cuando implementemos el sistema ATM en Java, tendremos que crear un atributo de la clase BaseDatosBanco para almacenar referencias a cero o más objetos Cuenta. De manera similar, podemos determinar los atributos de tipo por referencia de la clase ATM que correspondan a sus relaciones de composición con las clases Pantalla, Teclado, Dispensa-dorEfectivo y RanuraDeposito. Estos atributos basados en composiciones serían redundantes si los modeláramos en la figura 12.12, ya que las composiciones modeladas en la figura 12.10 transmi-ten de antemano el hecho de que la base de datos contiene información acerca de cero o más cuentas, y que un ATM está compuesto por una pantalla, un teclado, un dispensador de efectivo y una ranura para depósitos. Por lo general, los desarrolladores de software modelan estas relaciones de todo/parte como asociaciones de composición, en vez de modelarlas como atributos requeridos para implemen-tar las relaciones.

El diagrama de clases de la figura 12.12 proporciona una base sólida para la estructura de nues-tro modelo, pero no está completo. En la sección 12.5 identificaremos los estados y las actividades de los objetos en el modelo, y en la sección 12.6 identificaremos las operaciones que realizan los objetos. A medida que presentemos más acerca de UML y del diseño orientado a objetos, continuaremos refor-zando la estructura de nuestro modelo.

Ejercicios de autoevaluación de la sección 12.412.8 Por lo general, identificamos los atributos de las clases en nuestro sistema mediante el análisis de en el documento de requerimientos.

a) los sustantivos y las frases nominalesb) las palabras y frases descriptivasc) los verbos y las frases verbalesd) todo lo anterior.

12.9 ¿Cuál de los siguientes no es un atributo de un aeroplano?a) longitudb) envergadurac) volard) número de asientos

12.5 Cómo identifi car los estados y actividades de los objetos 489

12.10 Describa el significado de la siguiente declaración de un atributo de la clase DispensadorEfectivo en el diagrama de clases de la figura 12.12:

conteo : Integer = 500

12.5 Cómo identificar los estados y actividades de los objetos[Nota: esta sección se puede estudiar después del capítulo 5]. En la sección 12.4 identificamos muchos de los atributos de las clases necesarios para implementar el sistema ATM, y los agregamos al diagrama de clases de la figura 12.12. En esta sección le mostraremos la forma en que estos atributos representan el estado de un objeto. Identificaremos algunos estados clave que pueden ocupar nuestros objetos y hablaremos acerca de cómo cambian los objetos de estado, en respuesta a los diversos eventos que ocurren en el sistema. También hablaremos sobre el flujo de trabajo, o activi-dades, que realizan los objetos en el sistema ATM, y presentaremos las actividades de los objetos de transacción SolicitudSaldo y Retiro.

Diagramas de máquina de estadoCada objeto en un sistema pasa a través de una serie de estados. El estado actual de un objeto se in-dica mediante los valores de los atributos del objeto en cualquier momento dado. Los diagramas de máquina de estado (que se conocen comúnmente como diagramas de estado) modelan varios estados de un objeto y muestran bajo qué circunstancias el objeto cambia de estado. A diferencia de los diagramas de clases que presentamos en las secciones anteriores del ejemplo práctico, que se en-focaban principalmente en la estructura del sistema, los diagramas de estado modelan parte del comportamiento del sistema.

La figura 12.13 es un diagrama de estado simple que modela algunos de los estados de un objeto de la clase ATM. UML representa a cada estado en un diagrama de estado como un rectángulo redon-deado con el nombre del estado dentro de éste. Un círculo relleno con una punta de flecha ( ) de-signa el estado inicial. Recuerde que en el diagrama de clases de la figura 12.12 modelamos esta in-formación de estado como el atributo Boolean de nombre usuarioAutenticado. Este atributo se inicializa en false, o en el estado “Usuario no autenticado”, de acuerdo con el diagrama de estado.

Fig. 12.13 � Diagrama de estado para el objeto ATM.

Usuario no autenticado Usuario autenticado

la base de datos del banco autentica al usuario

el usuario sale del sistema

Las flechas ( ) indican las transiciones entre los estados. Un objeto puede pasar de un estado a otro, en respuesta a los diversos eventos que ocurren en el sistema. El nombre o la descripción del evento que ocasiona una transición se escribe cerca de la línea que corresponde a esa transición. Por ejemplo, el objeto ATM cambia del estado “Usuario no autenticado” al estado “Usuario autenticado”, una vez que la base de datos autentica al usuario. En el documento de requerimientos vimos que para autenticar a un usuario, la base de datos compara el número de cuenta y el NIP introducidos por el usuario con los de la cuenta correspondiente en la base de datos. Si el usuario ha introducido un nú-mero de cuenta válido y el NIP correcto, el objeto ATM pasa al estado “Usuario autenticado” y cambia su atributo usuarioAutenticado al valor true. Cuando el usuario sale del sistema al seleccionar la opción “salir” del menú principal, el objeto ATM regresa al estado “Usuario no autenticado”.

490 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Observación de ingeniería de software 12.2Por lo general, los diseñadores de software no crean diagramas de estado que muestren todos los posibles estados y transiciones de estados para todos los atributos; simplemente hay de-masiados. Lo común es que los diagramas de estado muestren sólo los estados y las transiciones de estado importantes.

Diagramas de actividadAl igual que un diagrama de estado, un diagrama de actividad modela los aspectos del comportamiento de un sistema. A diferencia de un diagrama de estado, un diagrama de actividad modela el flujo de trabajo (secuencia de objetos) de un objeto durante la ejecución de un programa. Un diagrama de ac-tividad modela las acciones a realizar y en qué orden las realizará el objeto. El diagrama de actividad de la figura 12.14 modela las acciones involucradas en la ejecución de una transacción de solicitud de saldo. Asumimos que ya se ha inicializado un objeto SolicitudSaldo y que ya se le ha asignado un número de cuenta válido (el del usuario actual), por lo que el objeto sabe qué saldo extraer de la base de datos. El diagrama incluye las acciones que ocurren después de que el usuario selecciona la opción de solicitud de saldo del menú principal y antes de que el ATM devuelva al usuario al menú principal; un objeto SolicitudSaldo no realiza ni inicia estas acciones, por lo que no las modelamos aquí. El diagrama empieza extrayendo de la base de datos el saldo de la cuenta. Después, SolicitudSaldo muestra el saldo en la pantalla. Esta acción completa la ejecución de la transacción. Recuerde que he-mos optado por representar el saldo de una cuenta como los atributos saldoDisponible y saldoTotal de la clase Cuenta, por lo que las acciones que se modelan en la figura 12.14 hacen referencia a la ob-tención y visualización de ambos atributos del saldo.

Fig. 12.14 � Diagrama de actividad para un objeto SolicitudSaldo.

obtener saldo de cuenta de la base de datos

mostrar saldo en la pantalla

UML representa una acción en un diagrama de actividad como un estado de acción, el cual se modela mediante un rectángulo en el que sus lados izquierdo y derecho se sustituyen por arcos ha-cia fuera. Cada estado de acción contiene una expresión de acción; por ejemplo, “obtener de la base de datos el saldo de la cuenta”; eso especifica una acción a realizar. Una flecha ( ) conecta dos es-tados de acción, con lo cual indica el orden en el que ocurren las acciones representadas por los estados de acción. El círculo relleno (en la parte superior de la figura 12.14) representa el estado inicial de la actividad: el inicio del flujo de trabajo antes de que el objeto realice las acciones modeladas. En este caso, la transacción primero ejecuta la expresión de acción “obtener de la base de datos el saldo de la cuenta”. Después, la transacción muestra ambos saldos en la pantalla. El círculo relleno encerrado en un círculo sin relleno (en la parte inferior de la figura 12.14) representa el estado final: el fin del flujo de trabajo una vez que el objeto realiza las acciones modeladas. Utilizamos diagramas de actividad

12.5 Cómo identifi car los estados y actividades de los objetos 491

de UML para ilustrar el flujo de control para las instrucciones de control que presentamos en los capí-tulos 4 y 5.

La figura 12.15 muestra un diagrama de actividad para una transacción de retiro. Asumimos que ya se ha asignado un número de cuenta válido a un objeto Retiro. No modelaremos al usuario selec-

Fig. 12.15 � Diagrama de actividad para una transacción de retiro.

[el usuario canceló la transacción]

[el usuario seleccionó una cantidad]

[monto > saldo disponible]

[monto <= saldo disponible]

[hay suficiente efectivo disponible]

[no hay suficiente efectivo disponible]

muestra el menú de montos de retiro y la opción para cancelar

introduce la selección del menú

interactuar con la base de datos para cargar el monto a la cuenta del usuario

dispensar efectivo

instruir al usuario para que tome el efectivo

establecer atributo del monto

mostrar mensaje de error apropiado

evaluar si hay suficiente efectivo en el dispensador de efectivo

obtener de la base de datos el saldo disponible de la cuenta del usuario

492 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

cionando la opción de retiro del menú principal ni al ATM devolviendo al usuario al menú principal, ya que estas acciones no las realiza un objeto Retiro. La transacción primero muestra un menú de montos estándar de retiro (que se muestra en la figura 12.3) y una opción para cancelar la transac-ción. Después la transacción recibe una selección del menú de parte del usuario. Ahora el flujo de actividad llega a una decisión (una bifurcación indicada por el pequeño símbolo de rombo). Este pun-to determina la siguiente acción con base en la condición de guardia asociada (entre corchetes, en-seguida de la transición), que indica que la transición ocurre si se cumple esta condición de guardia. Si el usuario cancela la transacción al elegir la opción “cancelar” del menú, el flujo de actividad salta inmediatamente al siguiente estado. Observe la fusión (indicada mediante el pequeño símbolo de rombo), en donde el flujo de actividad de cancelación se combina con el flujo de actividad principal, antes de llegar al estado final de la actividad. Si el usuario selecciona un monto de retiro del menú, Retiro establece monto (un atributo modelado originalmente en la figura 12.12) al valor elegido por el usuario.

Después de establecer el monto de retiro, la transacción obtiene el saldo disponible de la cuenta del usuario (es decir, el atributo saldoDisponible del objeto Cuenta del usuario) de la base de datos. Después el flujo de actividad llega a otra decisión. Si el monto de retiro solicitado excede al saldo disponible del usuario, el sistema muestra un mensaje de error apropiado, en el cual informa al usua-rio sobre el problema y después regresa al principio del diagrama de actividad, y pide al usuario que introduzca un nuevo monto. Si el monto de retiro solicitado es menor o igual al saldo disponible del usuario, la transacción continúa. A continuación, la transacción evalúa si el dispensador de efec-tivo tiene suficiente efectivo para satisfacer la solicitud de retiro. Si éste no es el caso, la transacción muestra un mensaje de error apropiado, después regresa al principio del diagrama de actividad y pide al usuario que seleccione un nuevo monto. Si hay suficiente efectivo disponible, la transacción interactúa con la base de datos para cargar el monto retirado de la cuenta del usuario (es decir, restar el monto tanto del atributo saldoDisponible como del atributo saldoTotal del objeto Cuenta del usuario). Después la transacción entrega el monto deseado de efectivo e instruye al usuario para que lo tome. Por último, el flujo de actividad se fusiona con el flujo de actividad de cancelación antes de llegar al estado final.

Hemos llevado a cabo los primeros pasos para modelar el comportamiento del sistema ATM y he-mos mostrado cómo participan los atributos de un objeto para realizar las actividades del mismo. En la sección 12.6 investigaremos los comportamientos para todas las clases, de manera que obtengamos una interpretación más precisa del comportamiento del sistema, al completar los terceros compartimien-tos de las clases en nuestro diagrama de clases.

Ejercicios de autoevaluación de la sección 12.512.11 Indique si el siguiente enunciado es verdadero o falso y, si es falso, explique por qué: los diagramas de estado mo-delan los aspectos estructurales de un sistema.

12.12 Un diagrama de actividad modela las(los) que realiza un objeto y el orden en el que las(los) realiza.a) accionesb) atributosc) estadosd) transiciones de estado

12.13 Con base en el documento de requerimientos, cree un diagrama de actividad para una transacción de depósito.

12.6 Identifi cación de las operaciones de las clases 493

12.6 Identificación de las operaciones de las clases[Nota: esta sección se puede estudiar después del capítulo 6]. En esta sección determinaremos algunas de las operaciones (o comportamientos) de las clases que son necesarias para implementar el sistema ATM. Una operación es un servicio que proporcionan los obje-tos de una clase a los clientes (usuarios) de esa clase. Considere las operaciones de algunos objetos reales. Las operaciones de un radio incluyen el sintonizar su estación y ajustar su volumen (que por lo general lo hace una persona que ajusta los controles del radio). Las operaciones de un auto incluyen acelerar (operación invocada por el conductor cuando oprime el pedal del acelerador), desacelerar (operación invocada por el conductor cuando oprime el pedal del freno o cuando suelta el pedal del acelerador), dar vuelta y cambiar velocidades. Los objetos de software también pueden ofrecer operaciones; por ejem-plo, un objeto de gráficos de software podría ofrecer operaciones para dibujar un círculo, dibujar una línea, dibujar un cuadrado, etcétera. Un objeto de software de hoja de cálculo podría ofrecer operaciones como imprimir la hoja de cálculo, totalizar los elementos en una fila o columna, y graficar la informa-ción de la hoja de cálculo como un gráfico de barras o de pastel.

Podemos derivar muchas de las operaciones de cada clase mediante un análisis de los verbos y las frases verbales clave en el documento de requerimientos. Después relacionamos cada una de ellas con las clases específicas en nuestro sistema (figura 12.16). Las frases verbales en la figura 12.16 nos ayudan a determinar las operaciones de cada clase.

Clase Verbos y frases verbales

ATM ejecuta transacciones fi nancierasSolicitudSaldo [ninguna en el documento de requerimientos]

Retiro [ninguna en el documento de requerimientos]

Deposito [ninguna en el documento de requerimientos]

BaseDatosBanco autentica a un usuario, obtiene el saldo de una cuenta, abona un monto de depósito a una cuenta, carga un monto de retiro a una cuenta

Cuenta obtiene el saldo de una cuenta, abona un monto de depósito a una cuenta, carga un monto de retiro a una cuenta

Pantalla muestra un mensaje al usuario

Teclado recibe entrada numérica del usuario

DispensadorEfectivo dispensa efectivo, indica si contiene sufi ciente efectivo para satisfacer una solicitud de retiro

RanuraDeposito recibe un sobre de depósito

Fig. 12.16 � Verbos y frases verbales para cada clase en el sistema ATM.

Modelar las operacionesPara identificar las operaciones, analizamos las frases verbales que se listan para cada clase en la figura 12.16. La frase “ejecuta transacciones financieras” asociada con la clase ATM implica que esta clase ins-truye a las transacciones a que se ejecuten. Por lo tanto, cada una de las clases SolicitudSaldo, Retiro y Deposito necesitan una operación para proporcionar este servicio al ATM. Colocamos esta opera-ción (que hemos nombrado ejecutar) en el tercer compartimiento de las tres clases de transacciones en el diagrama de clases actualizado de la figura 12.17. Durante una sesión con el ATM, el objeto ATM invocará estas operaciones de transacciones, según sea necesario.

494 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Para representar las operaciones (es decir, los métodos), UML lista el nombre de la operación, se-guido de una lista separada por comas de parámetros entre paréntesis, un signo de dos puntos y el tipo de valor de retorno:

nombreOperación( parámetro1, parámetro2, …, parámetroN ) : tipo de valor de retorno

Cada parámetro en la lista separada por comas consiste en un nombre de parámetro, seguido de un signo de dos puntos y del tipo del parámetro:

nombreParámetro : tipoParámetro

Por el momento, no listamos los parámetros de nuestras operaciones; en breve identificaremos y modelaremos los parámetros de algunas de las operaciones. Para algunas de estas operaciones no cono-cemos todavía los tipos de valores de retorno, por lo que también las omitiremos del diagrama. Estas omisiones son perfectamente normales en este punto. A medida que avancemos en nuestro proceso de diseño e implementación, agregaremos el resto de los tipos de valores de retorno.

Autenticar a un usuarioLa figura 12.16 lista la frase “autentica a un usuario” enseguida de la clase BaseDatosBanco; la base de datos es el objeto que contiene la información necesaria de la cuenta para determinar si el número de

Fig. 12.17 � Las clases en el sistema ATM, con atributos y operaciones.

ATM

usuarioAutenticado : Boolean = false

SolicitudSaldo

numeroCuenta : Integer

DispensadorEfectivo

cuenta : Integer = 500

RanuraDeposito

Pantalla

Teclado

Retiro

numeroCuenta : Integermonto : Double

BaseDatosBanco

Deposito

numeroCuenta : Integermonto : Double

autenticarUsuario() : BooleanobtenerSaldoDisponible() : DoubleobtenerSaldoTotal() : Doubleabonar()cargar()

Cuenta

numeroCuenta : Integernip : IntegersaldoDisponible : DoublesaldoTotal : Double

validarNIP() : BooleanobtenerSaldoDisponible() : DoubleobtenerSaldoTotal() : Doubleabonar()cargar()

ejecutar()

ejecutar()mostrarMensaje()

dispensarEfectivo()haySuficienteEfectivoDisponible() : Boolean

obtenerEntrada() : Integerejecutar()

seRecibioSobre() : Boolean

12.6 Identifi cación de las operaciones de las clases 495

cuenta y el NIP introducidos por un usuario concuerdan con los de una cuenta en el banco. Por lo tanto, la clase BaseDatosBanco necesita una operación que proporcione un servicio de autenticación al ATM. Colocamos la operación autenticarUsuario en el tercer compartimiento de la clase Base-DatosBanco (figura 12.17). No obstante, un objeto de la clase Cuenta y no de la clase BaseDatosBanco es el que almacena el número de cuenta y el NIP a los que se debe acceder para autenticar a un usuario, por lo que la clase Cuenta debe proporcionar un servicio para validar un NIP obtenido como entrada del usuario, y compararlo con un NIP almacenado en un objeto Cuenta. Por ende, agregamos una ope-ración validarNIP a la clase Cuenta. Especificamos un tipo de valor de retorno Boolean para las opera-ciones autenticarUsuario y validarNIP. Cada operación devuelve un valor que indica que la operación tuvo éxito al realizar su tarea (es decir, un valor de retorno true) o que no tuvo éxito (es decir, un valor de retorno false).

Otras operaciones de BaseDatosBanco y CuentaLa figura 12.16 lista varias frases verbales adicionales para la clase BaseDatosBanco: “extrae el saldo de una cuenta”, “abona un monto de depósito a una cuenta” y “carga un monto de retiro a una cuenta”. Al igual que “autentica a un usuario”, estas frases restantes se refieren a los servicios que debe proporcionar la base de datos al ATM, ya que la base de datos almacena todos los datos de las cuentas que se utilizan para au-tenticar a un usuario y realizar transacciones con el ATM. No obstante, los objetos de la clase Cuenta son los que en realidad realizan las operaciones a las que se refieren estas frases. Por ello, asignamos una ope-ración tanto a la clase BaseDatosBanco como a la clase Cuenta, que corresponda con cada una de estas frases. En la sección 12.3 vimos que, como una cuenta de banco contiene información delicada, no per-mitimos que el ATM acceda a las cuentas en forma directa. La base de datos actúa como un intermediario entre el ATM y los datos de la cuenta, evitando el acceso no autorizado. Como veremos en la sección 12.7, la clase ATM invoca las operaciones de la clase BaseDatosBanco, cada una de las cuales a su vez invoca a la operación con el mismo nombre en la clase Cuenta.

Obtener los saldosLa frase “obtiene el saldo de una cuenta” sugiere que las clases BaseDatosBanco y Cuenta necesitan una operación obtenerSaldo. Sin embargo, recuerde que creamos dos atributos en la clase Cuenta para re-presentar un saldo: saldoDisponible y saldoTotal. Una solicitud de saldo requiere el acceso a estos dos atributos del saldo, de manera que pueda mostrarlos al usuario, pero un retiro sólo requiere veri-ficar el valor de saldoDisponible. Para permitir que los objetos en el sistema obtengan cada atributo de saldo en forma individual, agregamos las operaciones obtenerSaldoDisponible y obtenerSaldo-Total al tercer compartimiento de las clases BaseDatosBanco y Cuenta (figura 12.17). Especificamos un tipo de valor de retorno Double para estas operaciones, debido a que los atributos de los saldos que van a obtener son de tipo Double.

Abonar y cargar a una CuentaLas frases “abona un monto de depósito a una cuenta” y “carga un monto de retiro a una cuenta” in-dican que las clases BaseDatosBanco y Cuenta deben realizar operaciones para actualizar una cuenta durante un depósito y un retiro, respectivamente. Por lo tanto, asignamos las operaciones abonar y cargar a las clases BaseDatosBanco y Cuenta. Tal vez recuerde que cuando se abona a una cuenta (como en un depósito) se suma un monto sólo al atributo saldoTotal. Por otro lado, cuando se carga a una cuenta (como en un retiro) se resta el monto de ambos atributos del saldo. Ocultamos estos detalles de implementación dentro de la clase Cuenta. Éste es un buen ejemplo de encapsulamiento y ocultamiento de información.

496 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Confirmaciones de depósitos realizados por otro sistema bancario Si éste fuera un sistema ATM real, las clases BaseDatosBanco y Cuenta también proporcionarían un conjunto de operaciones para permitir que otro sistema bancario actualizara el saldo de la cuenta de un usuario después de confirmar o rechazar todo, o parte de, un depósito. Por ejemplo, la operación confirmarMontoDeposito sumaría un monto al atributo saldoDisponible, y haría que los fondos de-positados estuvieran disponibles para retirarlos. La operación rechazarMontoDeposito restaría un monto al atributo saldoTotal para indicar que un monto especificado, que se había depositado recien-temente a través del ATM y se había sumado al saldoTotal, no se encontró en el sobre de depósito. El banco invocaría esta operación después de determinar que el usuario no incluyó el monto correcto de efectivo o que algún cheque no fue validado (es decir, que “rebotó”). Aunque al agregar estas operaciones nuestro sistema estaría más completo, no las incluiremos en nuestros diagramas de clases ni en nuestra implementación, ya que se encuentran más allá del alcance de este caso de estudio.

Mostrar mensajesLa clase Pantalla “muestra un mensaje al usuario” en diversos momentos durante una sesión con el ATM. Toda la salida visual se produce a través de la pantalla del ATM. El documento de requerimien-tos describe muchos tipos de mensajes (por ejemplo, un mensaje de bienvenida, un mensaje de error, un mensaje de agradecimiento) que la pantalla muestra al usuario. El documento de requerimientos también indica que la pantalla muestra indicadores y menús al usuario. No obstante, un indicador es en realidad sólo un mensaje que describe lo que el usuario debe introducir a continuación, y un menú es en esencia un tipo de indicador que consiste en una serie de mensajes (es decir, las opciones del menú) que se muestran en forma consecutiva. Por lo tanto, en vez de asignar a la clase Pantalla una operación individual para mostrar cada tipo de mensaje, indicador y menú, basta con crear una opera-ción que pueda mostrar cualquier mensaje especificado por un parámetro. Colocamos esta operación (mostrarMensaje) en el tercer compartimiento de la clase Pantalla en nuestro diagrama de clases (figura 12.17). No nos preocupa el parámetro de esta operación en estos momentos; lo modelaremos más adelante en esta sección.

Entrada desde el tecladoDe la frase “recibe entrada numérica del usuario” listada por la clase Teclado en la figura 12.16, po-demos concluir que la clase Teclado debe realizar una operación obtenerEntrada. A diferencia del teclado de una computadora, el teclado del ATM sólo contiene los números del 0 al 9, por lo cual es-pecificamos que esta operación devuelve un valor entero. Si recuerda, en el documento de requerimien-tos vimos que en distintas situaciones, tal vez se requiera que el usuario introduzca un tipo distinto de número (por ejemplo, un número de cuenta, un NIP, el número de una opción del menú, un monto de depósito como número de centavos). La clase Teclado sólo obtiene un valor numérico para un cliente de la clase; no determina si el valor cumple con algún criterio específico. Cualquier clase que utilice esta operación debe verificar que el usuario haya introducido un número apropiado según el caso, y después debe responder de manera acorde (por ejemplo, mostrar un mensaje de error a través de la clase Pantalla). [Nota: cuando implementemos el sistema, simularemos el teclado del ATM con el teclado de una computadora y, por cuestión de simpleza, asumiremos que el usuario no escribirá datos de entrada que no sean números, usando las teclas en el teclado de la computadora que no apa-rezcan en el teclado del ATM].

Dispensar efectivoLa figura 12.16 lista la frase “dispensa efectivo” para la clase DispensadorEfectivo. Por lo tanto, crea-mos la operación dispensarEfectivo y la listamos bajo la clase DispensadorEfectivo en la figura 12.17. La clase DispensadorEfectivo también “indica si contiene suficiente efectivo para satisfacer una soli-

12.6 Identifi cación de las operaciones de las clases 497

citud de retiro”. Para esto incluimos a haySuficienteEfectivoDisponible, una operación que devuelve un valor de tipo Boolean de UML, en la clase DispensadorEfectivo.

La figura 12.16 también lista la frase “recibe un sobre de depósito” para la clase RanuraDeposito. La ranura de depósito debe indicar si recibió un sobre, por lo que colocamos una operación seRecibio-Sobre, la cual devuelve un valor Boolean, en el tercer compartimiento de la clase RanuraDeposito. [Nota: es muy probable que una ranura de depósito de hardware real envíe una señal al ATM para indicarle que se recibió un sobre. No obstante, simularemos este comportamiento con una operación en la clase RanuraDeposito, que la clase ATM pueda invocar para averiguar si la ranura de depósito re-cibió un sobre].

La clase ATMNo listamos ninguna operación para la clase ATM en este momento. Todavía no sabemos de algún servi-cio que proporcione la clase ATM a otras clases en el sistema. No obstante, cuando implementemos el sis-tema en código de Java, tal vez emerjan las operaciones de esta clase junto con las operaciones adicionales de las demás clases en el sistema.

Identificar y modelar los parámetros de operación para la clase BaseDatosBancoHasta ahora no nos hemos preocupado por los parámetros de nuestras operaciones; sólo hemos tratado de obtener una comprensión básica de las operaciones de cada clase. Ahora daremos un vistazo más de cerca a varios parámetros de operación. Para identificar los parámetros de una operación, analizamos qué datos requiere la operación para realizar su tarea asignada.

Considere la operación autenticarUsuario de la clase BaseDatosBanco. Para autenticar a un usuario, esta operación debe conocer el número de cuenta y el NIP que suministra el usuario. Por lo tanto, especificamos que la operación autenticarUsuario debe recibir los parámetros enteros numero-CuentaUsuario y nipUsuario, que la operación debe comparar con el número de cuenta y el NIP de un objeto Cuenta en la base de datos. Colocaremos después de estos nombres de parámetros la palabra “Usuario” para evitar confusión entre los nombres de los parámetros de la operación y los nombres de los atributos que pertenecen a la clase Cuenta. Listamos estos parámetros en el diagrama de clases de la figura 12.18, el cual modela sólo a la clase BaseDatosBanco. [Nota: es perfectamente normal modelar sólo una clase. En este caso vamos a analizar los parámetros de esta clase específica, por lo que omitimos las demás clases. Más adelante en los diagramas de clase de este caso de estudio, en donde los pará-metros dejarán de ser el centro de nuestra atención, los omitiremos para ahorrar espacio. No obstante, recuerde que las operaciones que se listan en estos diagramas siguen teniendo parámetros].

Fig. 12.18 � La clase BaseDatosBanco con parámetros de operación.

BaseDatosBanco

autenticarUsuario(nombreCuentaUsuario : Integer, nipUsuario : Integer) : BooleanobtenerSaldoDisponible(numeroCuentaUsuario : Integer) : DoubleobtenerSaldoTotal(numeroCuentaUsuario : Integer) : Doubleabonar(numeroCuentaUsuario : Integer, monto : Double)cargar(numeroCuentaUsuario : Integer, monto : Double)

Recuerde que para modelar a cada parámetro en una lista de parámetros separados por comas, UML lista el nombre del parámetro, seguido de un signo de dos puntos y el tipo del parámetro (en nota-

498 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

ción de UML). Así, la figura 12.18 especifica que la operación autenticarUsuario recibe dos paráme-tros: numeroCuentaUsuario y nipUsuario, ambos de tipo Integer. Cuando implementemos el sistema en Java, representaremos estos parámetros con valores int.

Las operaciones obtenerSaldoDisponible, obtenerSaldoTotal, abonar y cargar de la clase BaseDatosBanco también requieren un parámetro nombreCuentaUsuario para identificar la cuenta a la cual la base de datos debe aplicar las operaciones, por lo que incluimos estos parámetros en el dia-grama de clases de la figura 12.18. Además, las operaciones abonar y cargar requieren un parámetro Double llamado monto, para especificar el monto de dinero que se abonará o cargará, respectivamente.

Identificar y modelar los parámetros de operación para la clase CuentaEn la figura 12.19 se modelan los parámetros de las operaciones de la clase Cuenta. La operación va-lidarNIP sólo requiere un parámetro nipUsuario, el cual contiene el NIP especificado por el usuario, que se comparará con el NIP asociado a la cuenta. Al igual que sus contrapartes en la clase BaseDatos-Banco, las operaciones abonar y cargar en la clase Cuenta requieren un parámetro Double llamado monto, el cual indica la cantidad de dinero involucrada en la operación. Las operaciones obtener-SaldoDisponible y obtenerSaldoTotal en la clase Cuenta no requieren datos adicionales para realizar sus tareas. Las operaciones de la clase Cuenta no requieren un parámetro de número de cuenta para diferenciar una cuenta de otra, ya que cada una de estas operaciones se puede invocar sólo en un obje-to Cuenta específico.

Fig. 12.19 � La clase Cuenta con parámetros de operación.

Cuenta

numeroCuenta : Integernip : IntegersaldoDisponible : DoublesaldoTotal : Double

validarNIP (nipUsuario : Integer) : BooleanobtenerSaldoDisponible() : DoubleobtenerSaldoTotal() : Doubleabonar(monto : Double)cargar(monto : Double)

Fig. 12.20 � La clase Pantalla con parámetros de operación.

Pantalla

mostrarMensaje( mensaje : String )

Identificar y modelar los parámetros de operación para la clase PantallaLa figura 12.20 modela la clase Pantalla con un parámetro especificado para la operación mostrarMen-saje. Esta operación requiere sólo un parámetro String llamado mensaje, el cual indica el texto que debe mostrarse en pantalla. Recuerde que los tipos de los parámetros que se enlistan en nuestros diagramas de clases están en notación de UML, por lo que el tipo String que se enlista en la figura 12.20 se refiere al tipo de UML. Cuando implementemos el sistema en Java, utilizaremos de hecho la clase String de Java para representar este parámetro.

12.7 Indicar la colaboración entre los objetos 499

Identificar y modelar los parámetros de operación para la clase DispensadorEfectivoLa figura 12.21 especifica que la operación dispensarEfectivo de la clase DispensadorEfectivo recibe un parámetro Double llamado monto para indicar el monto de efectivo (en dólares) que se dispensará al usuario. La operación haySuficienteEfectivoDisponible también recibe un parámetro Double llamado monto para indicar el monto de efectivo en cuestión.

Fig. 12.21 � La clase DispensadorEfectivo con parámetros de operación.

DispensadorEfectivo

dispensarEfectivo( monto : Double )haySuficienteEfectivoDisponible( monto : Double ) : Boolean

cuenta : Integer = 500

Identificar y modelar los parámetros de operación para otras clasesNo hablamos sobre los parámetros para la operación ejecutar de las clases SolicitudSaldo, Retiro y Depósito, de la operación obtenerEntrada de la clase Teclado y la operación seRecibioSobre de la clase RanuraDeposito. En este punto de nuestro proceso de diseño, no podemos determinar si estas operaciones requieren datos adicionales para realizar sus tareas, por lo que dejaremos sus listas de pará-metros vacías. A medida que avancemos por el caso de estudio, tal vez decidamos agregar parámetros a estas operaciones.

En esta sección hemos determinado muchas de las operaciones que realizan las clases en el sistema ATM. Identificamos los parámetros y los tipos de valores de retorno de algunas operaciones. A medida que continuemos con nuestro proceso de diseño, el número de operaciones que pertenezcan a cada clase puede variar; podríamos descubrir que se necesitan nuevas operaciones o que ciertas operacio-nes actuales no son necesarias. También podríamos determinar que algunas de las operaciones de nues-tras clases necesitan parámetros adicionales y tipos de valores de retorno distintos, o que algunos pará-metros son innecesarios o requieren distintos tipos.

Ejercicios de autoevaluación de la sección 12.612.14 ¿Cuál de las siguientes opciones no es un comportamiento?

a) leer datos de un archivob) imprimir los resultadosc) imprimir textod) obtener la entrada del usuario

12.15 Si quisiera agregar al sistema ATM una operación que devuelva el atributo monto de la clase Retiro, ¿cómo y en dónde especificaría esta operación en el diagrama de clases de la figura 12.17?

12.16 Describa el significado del siguiente listado de operaciones, el cual podría aparecer en un diagrama de clases para el diseño orientado a objetos de una calculadora:

sumar( x : Integer, y : Integer ) : Integer

12.7 Indicar la colaboración entre los objetos [Nota: esta sección se puede estudiar después del capítulo 7]. En esta sección nos concentraremos en las colaboraciones (interacciones) entre los objetos. Cuando dos objetos se comunican entre sí para realizar una tarea, se dice que colaboran (para ello, un objeto invoca a

500 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

las operaciones del otro). Una colaboración consiste en que un objeto de una clase envía un mensaje a un objeto de otra clase. En Java, los mensajes se envían mediante llamadas a métodos.

En la sección 12.6 determinamos muchas de las operaciones de las clases en nuestro sistema. En esta sección, nos concentraremos en los mensajes que invocan a esas operaciones. Para identificar las co-laboraciones en el sistema, regresaremos al documento de requerimientos de la sección 12.2. Recuerde que este documento especifica el rango de actividades que ocurren durante una sesión con el ATM (por ejemplo, autenticar a un usuario, realizar transacciones). Los pasos utilizados para describir cómo debe realizar el sistema cada una de estas tareas son nuestra primera indicación de las colaboraciones en nuestro sistema. A medida que avancemos por esta sección y por el capítulo 13, es probable que des-cubramos colaboraciones adicionales.

Identificar las colaboraciones en un sistemaPara identificar las colaboraciones en el sistema, leeremos con cuidado las secciones del documento de requerimientos que especifican lo que debe hacer el ATM para autenticar un usuario, y para realizar cada tipo de transacción. Para cada acción o paso descrito en el documento de requerimientos, decidimos qué objetos en nuestro sistema deben interactuar para lograr el resultado deseado. Identificamos un objeto como el emisor y otro como el receptor. Después seleccionamos una de las operaciones del objeto recep-tor (identificadas en la sección 12.6) que el objeto emisor debe invocar para producir el comportamiento apropiado. Por ejemplo, el ATM muestra un mensaje de bienvenida cuando está inactivo. Sabemos que un objeto de la clase Pantalla muestra un mensaje al usuario a través de su operación mostrarMensaje. Por ende, decidimos que el sistema puede mostrar un mensaje de bienvenida si empleamos una colabo-ración entre el ATM y la Pantalla, en donde el ATM envía un mensaje mostrarMensaje a la Pantalla mediante la invocación de la operación mostrarMensaje de la clase Pantalla. [Nota: para evitar repetir la frase “un objeto de la clase…”, nos referiremos a cada objeto sólo utilizando su nombre de clase, prece-dido por un artículo (como “un”, “una”, “el” o “la”); por ejemplo, “el ATM” hace referencia a un objeto de la clase ATM].

La figura 12.22 lista las colaboraciones que pueden derivarse del documento de requerimientos. Para cada objeto emisor, listamos las colaboraciones en el orden en el que ocurren primero durante una sesión con el ATM (es decir, el orden en el que se describen en el documento de requerimientos). Listamos cada colaboración en la que se involucre un emisor único, un mensaje y un receptor sólo una vez, aun cuando la colaboración puede ocurrir varias veces durante una sesión con el ATM. Por ejem-plo, la primera fila en la figura 12.22 indica que el objeto ATM colabora con el objeto Pantalla cada vez que el ATM necesita mostrar un mensaje al usuario.

Consideraremos las colaboraciones en la figura 12.22. Antes de permitir que un usuario realice transacciones, el ATM debe pedirle que introduzca un número de cuenta y que después introduzca un NIP. Para realizar cada una de estas tareas envía un mensaje a la Pantalla a través de mostrarMensaje. Ambas acciones se refieren a la misma colaboración entre el ATM y la Pantalla, que ya se listan en la figura 12.22. El ATM obtiene la entrada en respuesta a un indicador, mediante el envío de un mensa-je obtenerEntrada del Teclado. A continuación, el ATM debe determinar si el número de cuenta es-pecificado por el usuario y el NIP coinciden con los de una cuenta en la base de datos. Para ello envía un mensaje autenticarUsuario a la BaseDatosBanco. Recuerde que BaseDatosBanco no puede autenticar a un usuario en forma directa; sólo la Cuenta del usuario (es decir, la Cuenta que contiene el número de cuenta especificado por el usuario) puede acceder al NIP registrado del usuario para autenticarlo. Por lo tanto, la figura 12.22 lista una colaboración en la que BaseDatosBanco envía un mensaje validarNIP a una Cuenta.

Una vez autenticado el usuario, el ATM muestra el menú principal enviando una serie de men-sajes mostrarMensaje a la Pantalla y obtiene la entrada que contiene una selección de menú; para ello envía un mensaje obtenerEntrada al Teclado. Ya hemos tomado en cuenta estas colaboracio-

12.7 Indicar la colaboración entre los objetos 501

nes, por lo que no agregamos nada a la figura 12.22. Una vez que el usuario selecciona un tipo de transacción a realizar, el ATM ejecuta la transacción enviando un mensaje ejecutar a un objeto de la clase de transacción apropiada (es decir, un objeto SolicitudSaldo, Retiro o Deposito). Por ejem-plo, si el usuario elije realizar una solicitud de saldo, el ATM envía un mensaje ejecutar a un objeto SolicitudSaldo.

Un objeto de la clase… envía el mensaje… a un objeto de la clase…

ATM mostrarMensaje

obtenerEntrada

autenticarUsuario

ejecutar

ejecutar

ejecutar

Pantalla

Teclado

BaseDatosBanco

SolicitudSaldo

Retiro

Deposito

SolicitudSaldo obtenerSaldoDisponible

obtenerSaldoTotal

mostrarMensaje

BaseDatosBanco

BaseDatosBanco

Pantalla

Retiro mostrarMensaje

obtenerEntrada

obtenerSaldoDisponible

haySuficienteEfectivoDisponible

cargar

dispensarEfectivo

Pantalla

Teclado

BaseDatosBanco

DispensadorEfectivo

BaseDatosBanco

DispensadorEfectivo

Deposito mostrarMensaje

obtenerEntrada

seRecibioSobreDeposito

abonar

Pantalla

Teclado

RanuraDeposito

BaseDatosBanco

BaseDatosBanco validarNIP

obtenerSaldoDisponible

obtenerSaldoTotal

cargar

abonar

Cuenta

Cuenta

Cuenta

Cuenta

Cuenta

Fig. 12.22 � Colaboraciones en el sistema ATM.

Un análisis más a fondo del documento de requerimientos revela las colaboraciones involucradas en la ejecución de cada tipo de transacción. Un objeto SolicitudSaldo extrae la cantidad de dinero disponible en la cuenta del usuario, al enviar un mensaje obtenerSaldoDisponible al objeto Base-DatosBanco, el cual responde enviando un mensaje obtenerSaldoDisponible a la Cuenta del usuario. De manera similar, el objeto SolicitudSaldo extrae la cantidad de dinero depositado al enviar un mensaje obtenerSaldoTotal al objeto BaseDatosBanco, el cual envía el mismo mensaje a la Cuenta del usuario. Para mostrar en pantalla ambas cantidades del saldo del usuario al mismo tiempo, el obje-to SolicitudSaldo envía a la Pantalla un mensaje mostrarMensaje.

Un objeto Retiro envía a la Pantalla una serie de mensajes mostrarMensaje para mostrar un menú de montos estándar de retiro (es decir, $20, $40, $60, $100, $200). El objeto Retiro envía al Teclado un mensaje obtenerEntrada para obtener la selección del menú elegida por el usuario. A continuación, el objeto Retiro determina si el monto de retiro solicitado es menor o igual al saldo

502 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

de la cuenta del usuario. Para obtener el monto de dinero disponible en la cuenta del usuario, el ob-jeto Retiro envía un mensaje obtenerSaldoDisponible al objeto BaseDatosBanco. Después el objeto Retiro evalúa si el dispensador contiene suficiente efectivo, enviando al DispensadorEfectivo un mensaje haySuficienteEfectivoDisponible. Un objeto Retiro envía un mensaje cargar al objeto BaseDatosBanco para reducir el saldo de la cuenta del usuario. El objeto BaseDatosBanco envía a su vez el mismo mensaje al objeto Cuenta apropiado, que reduce tanto el saldoTotal como el saldo-Disponible. Para dispensar la cantidad solicitada de efectivo, el objeto Retiro envía un mensaje dispensarEfectivo al objeto DispensadorEfectivo. Por último, el objeto Retiro envía a la Pantalla un mensaje mostrarMensaje, instruyendo al usuario para que tome el efectivo.

Para responder a un mensaje ejecutar, un objeto Deposito primero envía a la Pantalla un men-saje mostrarMensaje para pedir al usuario que introduzca un monto a depositar. El objeto Deposito envía al Teclado un mensaje obtenerEntrada para obtener la entrada del usuario. Después, el objeto Deposito envía a la Pantalla un mensaje mostrarMensaje para pedir al usuario que inserte un sobre de depósito. Para determinar si la ranura de depósito recibió un sobre de depósito entrante, el objeto Deposito envía al objeto RanuraDeposito un mensaje seRecibioSobreDeposito. El objeto Deposito actualiza la cuenta del usuario enviando un mensaje abonar al objeto BaseDatosBanco, el cual a su vez envía un mensaje abonar al objeto Cuenta del usuario. Recuerde que al abonar a una Cuenta se incre-menta el saldoTotal, pero no el saldoDisponible.

Diagramas de interacciónAhora que identificamos un conjunto de posibles colaboraciones entre los objetos en nuestro sistema ATM, modelaremos en forma gráfica estas interacciones. UML cuenta con varios tipos de diagramas de interacción, que para modelar el comportamiento de un sistema modelan la forma en que los ob-jetos interactúan entre sí. El diagrama de comunicación enfatiza cuáles objetos participan en las colaboraciones. Al igual que el diagrama de comunicación, el diagrama de secuencia muestra las co-laboraciones entre los objetos, pero enfatiza cuándo se deben enviar los mensajes entre los objetos a través del tiempo.

Diagramas de comunicaciónLa figura 12.23 muestra un diagrama de comunicación que modela la forma en que el ATM ejecuta una SolicitudSaldo. Los objetos se modelan en UML como rectángulos que contienen nombres de la for-ma nombreObjeto : NombreClase. En este ejemplo, que involucra sólo a un objeto de cada tipo, des-cartamos el nombre del objeto y listamos sólo un signo de dos puntos (:) seguido del nombre de la clase. [Nota: se recomienda especificar el nombre de cada objeto en un diagrama de comunicación cuan-do se modelan varios objetos del mismo tipo.] Los objetos que se comunican se conectan con líneas sólidas y los mensajes se pasan entre los objetos a lo largo de estas líneas, en la dirección mostrada por las flechas. El nombre del mensaje, que aparece enseguida de la flecha, es el nombre de una operación (es decir, un método en Java) que pertenece al objeto receptor; considere el nombre como un “servicio” que el objeto receptor proporciona a los objetos emisores (sus clientes).

Fig. 12.23 � Diagrama de comunicación del ATM, ejecutando una solicitud de saldo.

: ATM : SolicitudSaldo

ejecutar()

La flecha rellena en la figura 7.26 representa un mensaje (o llamada síncrona) en UML y una lla-mada a un método en Java. Esta flecha indica que el flujo de control va desde el objeto emisor (el ATM)

12.7 Indicar la colaboración entre los objetos 503

hasta el objeto receptor (una SolicitudSaldo). Como ésta es una llamada síncrona, el objeto emisor no puede enviar otro mensaje, ni hacer cualquier otra cosa, hasta que el objeto receptor procese el mensaje y devuelva el control al objeto emisor. El emisor sólo espera. En la figura 12.23, el objeto ATM llama al método ejecutar de un objeto SolicitudSaldo y no puede enviar otro mensaje sino hasta que eje-cutar termine y devuelva el control al objeto ATM. [Nota: si ésta fuera una llamada asíncrona, represen-tada por una flecha ( ), el objeto emisor no tendría que esperar a que el objeto receptor devolviera el control; continuaría enviando mensajes adicionales inmediatamente después de la llamada asíncrona. Dichas llamadas se implementan en Java mediante el uso de una técnica conocida como subprocesa-miento múltiple, que veremos en el capítulo 26 (en el sitio web)].

Secuencia de mensajes en un diagrama de comunicaciónLa figura 12.24 muestra un diagrama de comunicación que modela las interacciones entre los objetos en el sistema, cuando se ejecuta un objeto de la clase SolicitudSaldo. Asumimos que el atributo nume-roCuenta del objeto contiene el número de cuenta del usuario actual. Las colaboraciones en la figura 12.24 empiezan después de que el objeto ATM envía un mensaje ejecutar a un objeto SolicitudSaldo (es decir, la interacción modelada en la figura 12.23). El número a la izquierda del nombre de un men-saje indica el orden en el que éste se pasa. La secuencia de mensajes en un diagrama de comunicación progresa en orden numérico, de menor a mayor. En este diagrama, la numeración comienza con el mensaje 1 y termina con el mensaje 3. El objeto SolicitudSaldo envía primero un mensaje obtener-SaldoDisponible al objeto BaseDatosBanco (mensaje 1), después envía un mensaje obtenerSaldo-Total al objeto BaseDatosBanco (mensaje 2). Dentro de los paréntesis que van después del nombre de un mensaje, podemos especificar una lista separada por comas de los nombres de los parámetros que se envían con el mensaje (es decir, los argumentos en una llamada a un método en Java); el objeto SolicitudSaldo pasa el atributo numeroCuenta con sus mensajes al objeto BaseDatosBanco para in-dicar de cuál objeto Cuenta se extraerá la información del saldo. En la figura 12.18 vimos que las opera-ciones obtenerSaldoDisponible y obtenerSaldoTotal de la clase BaseDatosBanco requieren cada una de ellas un parámetro para identificar una cuenta. El objeto SolicitudSaldo muestra a continua-ción el saldoDisponible y el saldoTotal al usuario; para ello pasa un mensaje mostrarMensaje a la Pantalla (mensaje 3) que incluye un parámetro, el cual indica el mensaje a mostrar.

Fig. 12.24 � Diagrama de comunicación para ejecutar una solicitud de saldo.

: SolicitudSaldo

: Pantalla

: BaseDatosBanco : Cuenta

3: mostrarMensaje(mensaje)

1: obtenerSaldoDisponible(numeroCuenta)2: obtenerSaldoTotal(numeroCuenta)

1.1: obtenerSaldoDisponible()2.1: obtenerSaldoTotal()

504 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

La figura 12.24 modela dos mensajes adicionales que se pasan del objeto BaseDatosBanco a un ob-jeto Cuenta (mensaje 1.1 y mensaje 2.1). Para proveer al ATM los dos saldos de la Cuenta del usuario (según lo solicitado por los mensajes 1 y 2), el objeto BaseDatosBanco debe pasar un mensaje obtenerSaldo-Disponible y un mensaje obtenerSaldoTotal a la Cuenta del usuario. Dichos mensajes que se pasan dentro del manejo de otro mensaje se llaman mensajes anidados. UML recomienda utilizar un esque-ma de numeración decimal para indicar mensajes anidados. Por ejemplo, el mensaje 1.1 es el primer men-saje anidado en el mensaje 1; el objeto BaseDatosBanco pasa un mensaje obtenerSaldoDisponible durante el procesamiento de BaseDatosBanco de un mensaje con el mismo nombre. [Nota: si el objeto BaseDatosBanco necesita pasar un segundo mensaje anidado mientras procesa el mensaje 1, el segundo mensaje se numera como 1.2.] Un mensaje puede pasarse sólo cuando se han pasado ya todos los men-sajes anidados del mensaje anterior. Por ejemplo, el objeto SolicitudSaldo pasa el mensaje 3 sólo hasta que se han pasado los mensajes 2 y 2.1, en ese orden.

El esquema de numeración anidado que se utiliza en los diagramas de comunicación ayuda a acla-rar con precisión cuándo y en qué contexto se pasa cada mensaje. Por ejemplo, si numeramos los cinco mensajes de la figura 12.24 usando un esquema de numeración plano (es decir, 1, 2, 3, 4, 5), podría ser posible que alguien que viera el diagrama no pudiera determinar que el objeto BaseDatosBanco pasa el mensaje obtenerSaldoDisponible (mensaje 1.1) a una Cuenta durante el procesamiento del mensaje 1 por parte del objeto BaseDatosBanco, en vez de hacerlo después de completar el procesamiento del men-saje 1. Los números decimales anidados hacen ver que el segundo mensaje obtenerSaldoDisponible (mensaje 1.1) se pasa a una Cuenta dentro del manejo del primer mensaje obtenerSaldoDisponi-ble (mensaje 1) por parte del objeto BaseDatosBanco.

Diagramas de secuenciaLos diagramas de comunicación enfatizan los participantes en las colaboraciones, pero modelan su sincronización de una forma bastante extraña. Un diagrama de secuencia ayuda a modelar la sincro-nización de las colaboraciones con más claridad. La figura 12.25 muestra un diagrama de secuencia que modela la secuencia de las interacciones que ocurren cuando se ejecuta un Retiro. La línea pun-teada que se extiende hacia abajo desde el rectángulo de un objeto es la línea de vida de ese objeto, la cual representa la evolución en el tiempo. Las acciones ocurren a lo largo de la línea de vida de un ob-jeto, en orden cronológico de arriba hacia abajo; una acción cerca de la parte superior ocurre antes que una cerca de la parte inferior.

El paso de mensajes en los diagramas de secuencia es similar al paso de mensajes en los diagra-mas de comunicación. Una flecha con punta rellena, que se extiende desde el objeto emisor hasta el objeto receptor, representa un mensaje entre dos objetos. La punta de flecha apunta a una activa-ción en la línea de vida del objeto receptor. Una activación, que se muestra como un rectángulo ver-tical delgado, indica que se está ejecutando un objeto. Cuando un objeto devuelve el control, un mensaje de retorno (representado como una línea punteada con una punta de flecha ( ) se extiende desde la activación del objeto que devuelve el control hasta la activación del objeto que envió origi-nalmente el mensaje. Para eliminar el desorden, omitimos las flechas de los mensajes de retorno; UML permite esta práctica para que los diagramas sean más legibles. Al igual que los diagramas de comu-nicación, los de secuencia pueden indicar parámetros de mensaje entre los paréntesis que van después del nombre de un mensaje.

La secuencia de mensajes de la figura 12.25 empieza cuando un objeto Retiro pide al usuario que seleccione un monto de retiro; para ello envía a la Pantalla un mensaje mostrarMensaje. Después el objeto Retiro envía al Teclado un mensaje obtenerEntrada, el cual obtiene los datos de entrada del usuario. En el diagrama de actividad de la figura 12.15 ya hemos modelado la lógica de control involucrada en un objeto Retiro, por lo que no mostraremos esta lógica en el diagrama de secuencia de la figura 12.25. En vez de ello modelaremos el escenario para el mejor caso, en el cual el saldo de la cuenta del usuario es mayor o igual al monto de retiro seleccionado, y el dispensador de efectivo con-tiene un monto de efectivo suficiente como para satisfacer la solicitud. Es posible modelar la lógica

12.7 Indicar la colaboración entre los objetos 505

de control en un diagrama de secuencia mediante marcos de UML (que no cubriremos en este caso de estudio). Para una rápida descripción general de los marcos de UML, visite www.agilemodeling.com/style/frame.htm.

Después de obtener un monto de retiro, el objeto Retiro envía un mensaje obtenerSaldoDis-ponible al objeto BaseDatosBanco, el cual a su vez envía un mensaje obtenerSaldoDisponible a la Cuenta del usuario. Suponiendo que la cuenta del usuario tiene suficiente dinero disponible para per-mitir la transacción, el objeto Retiro envía al objeto DispensadorEfectivo un mensaje haySuficien-teEfectivoDisponible. Suponiendo que hay suficiente efectivo disponible, el objeto Retiro reduce el saldo de la cuenta del usuario (tanto el saldoTotal como el saldoDisponible) enviando un men-saje cargar a la BaseDatosBanco. La BaseDatosBanco responde enviando un mensaje cargar a la Cuen-ta del usuario. Por último, el objeto Retiro envía al DispensadorEfectivo un mensaje dispensar-

Fig. 12.25 � Diagrama de secuencia que modela la ejecución de un Retiro.

obtenerSaldoDisponible()obtenerSaldoDisponible(numeroCuenta)

dispensarEfectivo(monto)

: DispensadorEfectivo: BaseDatosBanco: Pantalla

: Cuenta: Teclado: Retiro

cargar(monto)

haySuficienteEfectivoDisponible(monto)

cargar(numeroCuenta, monto)

mostrarMensaje(mensaje)

obtenerEntrada()

mostrarMensaje(mensaje)

506 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Efectivo y a la Pantalla un mensaje mostrarMensaje, indicando al usuario que quite el efectivo de la máquina.

Hemos identificado las colaboraciones entre los objetos en el sistema ATM, y modelamos algunas de estas colaboraciones usando los diagramas de interacción de UML: los diagramas de comunicación y los diagramas de secuencia. En la sección 13.2 mejoraremos la estructura de nuestro modelo para completar un diseño orientado a objetos preliminar, y después empezaremos a implementar el sistema ATM en Java.

Ejercicios de autoevaluación de la sección 12.712.17 Un(a) consiste en que un objeto de una clase envía un mensaje a un objeto de otra clase.

a) asociación c) agregaciónb) colaboración d) composición

12.18 ¿Cuál forma de diagrama de interacción es la que enfatiza qué colaboraciones se llevan a cabo? ¿Cuál forma en-fatiza cuándo ocurren las interacciones?

12.19 Cree un diagrama de secuencia para modelar las interacciones entre los objetos del sistema ATM que ocurren al ejecutar un Deposito con éxito, y explique la secuencia de los mensajes modelados por el diagrama.

12.8 Conclusión En este capítulo aprendió a trabajar a partir de un documento de requerimientos detallado para desa-rrollar un diseño orientado a objetos. Trabajó con seis tipos populares de diagramas de UML para mo-delar en forma gráfica un sistema de software de cajero automático orientado a objetos. En el capítulo 13 optimizaremos el diseño mediante el uso de la herencia, y luego implementaremos por completo el di-seño en una aplicación de Java de 673 líneas.

Fig. 12.26 � Diagrama de caso-uso para una versión modificada de nuestro sistema ATM, que también permite a los usuarios transferir dinero entre varias cuentas.

Transferir fondosentre cuentas

Depositar fondos

Retirar efectivo

Ver saldo de cuenta

Usuario

Respuestas a los ejercicios de autoevaluación12.1 La figura 12.26 contiene un diagrama de caso-uso para una versión modificada de nuestro sistema ATM, que también permite a los usuarios transferir dinero entre cuentas.

12.2 b.

Respuestas a los ejercicios de autoevaluación 507

12.3 d.

12.4 [Nota: las respuestas de los estudiantes pueden variar.] La figura 12.27 presenta un diagrama de clases que muestra algunas de las relaciones de composición de una clase Auto.

12.5 c. [Nota: en una red de computadoras, esta relación podría ser de varios a varios].

12.6 Verdadera.

12.7 La figura 12.28 presenta un diagrama de clases para el ATM, en el cual se incluye la clase Deposito en vez de la clase Retiro (como en la figura 12.10). La clase Deposito no accede a la clase DispensadorEfectivo, pero accede a la clase RanuraDeposito.

Fig. 12.27 � Diagrama de clases que muestra algunas relaciones de composición de una clase Auto.

Auto

Rueda

Parabrisas

CinturonSeguridadVolante11 5

2

1

1

4

1

Fig. 12.28 � Diagrama de clases para el modelo del sistema ATM, incluyendo la clase Deposito.

Accede a/modifica el saldode una cuenta a través de

Ejecuta

1

1

1

1

1

1

1

1

1 1 1 1

1

0..*

0..10..1

0..1 0..10..1

1Contiene

Autentica el usuario en base a

Teclado

Deposito

RanuraDeposito

ATM

DispensadorEfectivo

Pantalla

Cuenta

BaseDatosBanco

12.8 b.

12.9 c. Volar es una operación o comportamiento de un aeroplano, no un atributo.

12.10 Esta declaración indica que el atributo conteo es de tipo Integer, con un valor inicial de 500. Este atributo lleva la cuenta del número de billetes disponibles en el DispensadorEfectivo, en cualquier momento dado.

12.11 Falso. Los diagramas de estado modelan parte del comportamiento del sistema.

12.12 a.

12.13 La figura 12.29 modela las acciones que ocurren una vez que el usuario selecciona la opción de depósito del menú principal, y antes de que el ATM regrese al usuario al menú principal. Recuerde que una parte del proceso de recibir un monto de depósito de parte del usuario implica convertir un número entero de centavos a una cantidad en dólares. Recuerde también que para acreditar un monto de depósito a una cuenta sólo hay que incrementar el atributo saldoTotal del objeto Cuenta del usuario. El banco actualiza el atributo saldoDisponible del objeto Cuenta del usuario sólo des-pués de confirmar el monto de efectivo en el sobre de depósito y después de verificar los cheques que haya incluido; esto ocurre en forma independiente del sistema ATM.

508 Capítulo 12 Caso de estudio del ATM, Parte 1: Diseño orientado a objetos con UML

Fig. 12.29 � Diagrama de actividad para una transacción de depósito.

[el usuario canceló la transacción]

[el usuario escribió un monto]

[se recibió el sobre de depósito]

[no se recibió el sobrede depósito]

pedir al usuario que escriba un monto a depositar o que cancele

recibir la entrada del usuario

tratar de recibir el sobre de depósito

interactuar con la base de datos para abonar el monto a la cuenta del usuario

mostrar mensaje

establecer el atributo monto

instruir al usuario para que inserte el sobre de depósito

Respuestas a los ejercicios de autoevaluación 509

12.14 c.

12.15 Para especificar una operación que obtenga el atributo monto de la clase Retiro, se debe colocar el siguiente lis-tado de operaciones en el (tercer) compartimiento de operaciones de la clase Retiro:

obtenerMonto( ) : Double

12.16 Este listado de operaciones indica una operación llamada sumar, la cual recibe los enteros x y y como parámetros y devuelve un valor entero.

12.17 c.

12.18 Los diagramas de comunicación enfatizan qué colaboraciones se llevan a cabo. Los diagramas de secuencia enfa-tizan cuándo ocurren las colaboraciones.

12.19 La figura 12.30 presenta un diagrama de secuencia que modela las interacciones entre objetos en el sistema ATM, las cuales ocurren cuando un Deposito se ejecuta con éxito. Primero, un Deposito envía un mensaje mostrarMensaje a la Pantalla, para pedir al usuario que introduzca un monto de depósito. A continuación, el Deposito envía un men-saje obtenerEntrada al Teclado para recibir la entrada del usuario. Después, el Deposito pide al usuario que inserte un sobre de depósito; para ello envía un mensaje mostrarMensaje a la Pantalla. Luego, el Deposito envía un mensaje seRecibioSobreDeposito al objeto RanuraDeposito para confirmar que el ATM haya recibido el sobre de depósito. Por último, el objeto Deposito incrementa el atributo saldoTotal (pero no el atributo saldoDisponible) de la Cuenta del usuario, enviando al objeto BaseDatosBanco un mensaje abonar. El objeto BaseDatosBanco responde enviando el mismo mensaje a la Cuenta del usuario.

Fig. 12.30 � Diagrama de secuencia que modela la ejecución de un Deposito.

: Cuenta: RanuraDeposito: Pantalla

: BaseDatosBanco: Teclado: Deposito

seRecibioSobreDeposito()

abonar(numeroCuenta, monto)

obtenerEntrada()

mostrarMensaje(mensaje)

mostrarMensaje(mensaje)

abonar(monto)

Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos13

No se puede trabajar en lo abstracto.—I. M. Pei

Generalizar significa pensar.—Georg Wilhelm Friedrich Hegel

Todos somos un regalo, ésa es nuestra herencia.—Ethel Waters

Déjenme caminar por los campos de papel tocando con mi varita mágica los tallos secos y las mariposas atrofiadas…—Denise Levertov

O b j e t i v o sEn este capítulo aprenderá a:

■ Incorporar la herencia en el diseño del ATM.

■ Incorporar el polimorfismo en el diseño del ATM.

■ Implementar por completo en Java el diseño orientado a objetos, basado en UML, del software del ATM.

■ Estudiar un recorrido de código detallado del sistema de software del ATM que explica las cuestiones de implementación.

13.2 Inicio de la programación de las clases del sistema ATM 511

13.1 Introducción

13.2 Inicio de la programación de las clases del sistema ATM

13.3 Incorporación de la herencia y el polimorfi smo en el sistema ATM

13.4 Implementación del caso de estudio del ATM13.4.1 La clase ATM13.4.2 La clase Pantalla13.4.3 La clase Teclado

13.4.4 La clase DispensadorEfectivo13.4.5 La clase RanuraDeposito13.4.6 La clase Cuenta13.4.7 La clase BaseDatosBanco13.4.8 La clase Transaccion13.4.9 La clase SolicitudSaldo

13.4.10 La clase Retiro13.4.11 La clase Deposito13.4.12 La clase CasoEstudioATM

13.5 Conclusión

Respuestas a los ejercicios de autoevaluación

13.1 IntroducciónEn el capítulo 12 desarrollamos un diseño orientado a objetos para nuestro sistema ATM. Ahora imple-mentaremos nuestro diseño orientado a objetos en Java. En la sección 13.2 le mostraremos cómo convertir los diagramas de clases en código de Java. En la sección 13.3 optimizaremos el diseño mediante la herencia y el polimorfismo. Después le presentaremos una implementación completa en código de Java del software del ATM en la sección 13.4. El código contiene muchos comentarios cuidadosamente elaborados, y el análisis de la implementación es detallado y preciso. Al estudiar esta aplicación, usted tendrá la oportu-nidad de ver una aplicación más substancial, del tipo que probablemente encontrará en la industria.

13.2 Inicio de la programación de las clases del sistema ATM[Nota: esta sección se puede enseñar después del capítulo 8].

VisibilidadAhora aplicaremos modificadores de acceso a los miembros de nuestras clases. Ya presentamos en un ca-pítulo anterior los modificadores de acceso public y private. Los modificadores de acceso determinan la visibilidad, o accesibilidad, de los atributos y métodos de un objeto para otros objetos. Antes de em-pezar a implementar nuestro diseño, debemos considerar cuáles atributos y métodos de nuestras clases deben ser public y cuáles deben ser private.

Ya hemos observado que, por lo general los atributos deben ser private, y que los métodos invocados por los clientes de una clase dada deben ser public. Los métodos que se llaman sólo por otros métodos de la clase como “métodos utilitarios” deben ser private. UML emplea marcadores de visibilidad para modelar la visibilidad de los atributos y las operaciones. La visibilidad pública se indica mediante la co-locación de un signo más (+) antes de una operación o atributo, mientras que un signo menos (–) indica una visibilidad privada. La figura 13.1 muestra nuestro diagrama de clases actualizado, en el cual se in-cluyen los marcadores de visibilidad. [Nota: no incluimos parámetros de operación en la figura 13.1; esto es perfectamente normal. Agregar los marcadores de visibilidad no afecta a los parámetros que ya están modelados en los diagramas de clases de las figuras 12.17 a 12.21].

NavegabilidadAntes de empezar a implementar nuestro diseño en Java, presentaremos una notación adicional de UML. El diagrama de clases de la figura 13.2 refina aún más las relaciones entre las clases del sistema ATM, al agregar flechas de navegabilidad a las líneas de asociación. Las flechas de navegabilidad (repre-sentadas como flechas con puntas delgadas ( ) en el diagrama de clases) indican en qué dirección

512 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

puede recorrerse una asociación. Al implementar un sistema diseñado mediante el uso de UML, los programadores utilizan flechas de navegabilidad para ayudar a determinar cuáles objetos necesitan re-ferencias a otros objetos. Por ejemplo, la flecha de navegabilidad que apunta de la clase ATM a la clase BaseDatosBanco indica que podemos navegar de una a la otra, con lo cual se permite a la clase ATM invocar a las operaciones de BaseDatosBanco. No obstante, como la figura 13.2 no contiene una flecha de navegabilidad que apunte de la clase BaseDatosBanco a la clase ATM, la clase BaseDatosBanco no puede acceder a las operaciones de la clase ATM. Las asociaciones en un diagrama de clases que tienen fle-chas de navegabilidad en ambos extremos, o que no tienen ninguna flecha, indican una navegabilidad bidireccional: la navegación puede proceder en cualquier dirección a lo largo de la asociación.

Al igual que el diagrama de clases de la figura 12.10, el de la figura 13.2 omite las clases Solicitud-Saldo y Deposito para simplificarlo. La navegabilidad de las asociaciones en las que participan estas dos clases se asemeja mucho a la navegabilidad de las asociaciones de la clase Retiro. En la sección 12.3 vimos que SolicitudSaldo tiene una asociación con la clase Pantalla. Podemos navegar de la clase Solici-tudSaldo a la clase Pantalla a lo largo de esta asociación, pero no podemos navegar de la clase Pantalla a la clase SolicitudSaldo. Por ende, si modeláramos la clase SolicitudSaldo en la figura 13.2, colo-caríamos una flecha de navegabilidad en el extremo de la clase Pantalla de esta asociación. Recuerde

Fig. 13.1 � Diagrama de clases con marcadores de visibilidad.

ATM

– usuarioAutenticado : Boolean = false

SolicitudSaldo

– numeroCuenta : Integer

DispensadorEfectivo

– cuenta : Integer = 500

RanuraDeposito

Pantalla

Teclado

Retiro

– numeroCuenta : Integer– monto : Double

BaseDatosBanco

Deposito

– numeroCuenta : Integer– monto : Double

+ autenticarUsuario() : Boolean+ obtenerSaldoDisponible() : Double+ obtenerSaldoTotal() : Double+ abonar()+ cargar()

Cuenta

– numeroCuenta : Integer– nip : Integer– saldoDisponible : Double– saldoTotal : Double

+ validarNIP : Boolean+ obtenerSaldoDisponible() : Double+ obtenerSaldoTotal() : Double+ abonar()+ cargar()

+ ejecutar()

+ ejecutar()+ mostrarMensaje()

+ dispensarEfectivo()+ haySuficienteEfectivoDisponible() : Boolean

+ obtenerEntrada() : Integer+ ejecutar()

+ seRecibioSobreDeposito : Boolean

13.2 Inicio de la programación de las clases del sistema ATM 513

también que la clase Deposito se asocia con las clases Pantalla, Teclado y RanuraDeposito. Podemos navegar de la clase Deposito a cada una de estas clases, pero no al revés. Por lo tanto, podríamos colo-car flechas de navegabilidad en los extremos de las clases Pantalla, Teclado y RanuraDeposito de estas asociaciones. [Nota: modelaremos estas clases y asociaciones adicionales en nuestro diagrama de clases final en la sección 13.3, una vez que hayamos simplificado la estructura de nuestro sistema, al incor-porar el concepto orientado a objetos de la herencia].

Implementación del sistema ATM a partir de su diseño de UMLAhora estamos listos para empezar a implementar el sistema ATM. Primero convertiremos las clases de los diagramas de las figuras 13.1 y 13.2 en código de Java. Este código representará el “esqueleto” del sistema. En la sección 13.3 modificaremos el código para incorporar el concepto orientado a objetos de la herencia. En la sección 13.4 presentaremos el código de Java completo y funcional para nuestro modelo.

Como ejemplo, empezaremos a desarrollar el código a partir de nuestro diseño de la clase Retiro en la figura 13.1. Utilizaremos esta figura para determinar los atributos y operaciones de la clase. Usare-mos el modelo de UML en la figura 13.2 para determinar las asociaciones entre las clases. Seguiremos estos cuatro lineamientos para cada clase:

1. Use el nombre que se localiza en el primer compartimiento para declarar la clase como public, con un constructor sin parámetros vacío. Incluimos este constructor tan sólo como un recep-táculo para recordarnos que la mayoría de las clases necesitarán en definitiva constructores. En la sección 13.4, en donde completamos una versión funcional de esta clase, agregaremos todos los argumentos y el código necesarios al cuerpo del constructor. Por ejemplo, la clase Retiro

Fig. 13.2 � Diagrama de clases con flechas de navegabilidad.

Accede a/modifica un saldode cuenta a través de

Ejecuta

1

1

1

1 1

1

1

1

1

1 1 1 1

1

0..*

0..110..1

0..1 0..10..1

1Contiene

Autentica al usuario contra

Teclado

Retiro

RanuraDeposito

ATM

DispensadorEfectivo

Pantalla

Cuenta

BaseDatosBanco

514 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

produce el código de la figura 13.3. Si encontramos que las variables de instancia de la clase sólo requieren la inicialización predeterminada, eliminaremos el constructor sin parámetros vacío, ya que es innecesario.

2. Use los atributos que se localizan en el segundo compartimiento para declarar las variables de instancia. Por ejemplo, los atributos private numeroCuenta y monto de la clase Retiro producen el código de la figura 13.4. [Nota: el constructor de la versión funcional completa de esta clase asignará valores a estos atributos].

3. Use las asociaciones descritas en el diagrama de clases para declarar las referencias a otros ob-jetos. Por ejemplo, de acuerdo con la figura 13.2, Retiro puede acceder a un objeto de la clase Pantalla, a un objeto de la clase Teclado, a un objeto de la clase DispensadorEfectivo y a un objeto de la clase BaseDatosBanco. Esto produce el código de la figura 13.5. [Nota: el cons-tructor de la versión funcional completa de esta clase inicializará estas variables de instancia con referencias a objetos reales].

4. Use las operaciones que se localizan en el tercer compartimiento de la figura 13.1 para declarar las armazones de los métodos. Si todavía no hemos especificado un tipo de valor de retorno para una operación, declaramos el método con el tipo de retorno void. Consulte los diagramas de clases de las figuras 12.17 a 12.21 para declarar cualquier parámetro necesario. Por ejemplo, al agregar la operación public ejecutar en la clase Retiro, que tiene una lista de parámetros vacía, se produce el código de la figura 13.6. [Nota: codificaremos los cuerpos de los méto-dos cuando implementemos el sistema ATM completo en la sección 13.4].

Esto concluye nuestra discusión sobre los fundamentos de la generación de clases a partir de diagramas de UML.

Fig. 13.3 � Código de Java para la clase Retiro, con base en las figuras 13.1 y 13.2.

1 // La clase Retiro representa una transacción de retiro del ATM

2 public class Retiro

3 {

4 // constructor sin argumentos

5 public Retiro()

6 {

7 } // fin del constructor de Retiro sin argumentos

8 } // fin de la clase Retiro

Fig. 13.4 � Código de Java para la clase Retiro, con base en las figuras 13.1 y 13.2.

1 // La clase Retiro representa una transacción de retiro del ATM

2 public class Retiro

3 {

4 // atributos

5 private int numeroCuenta; // cuenta de la que se van a retirar los fondos

6 private double monto; // monto que se va a retirar de la cuenta

7

8 // constructor sin argumentos

9 public Retiro()

10 {

11 } // fin del constructor de Retiro sin argumentos

12 } // fin de la clase Retiro

Ejercicios de autoevaluación de la sección 13.2 515

Ejercicios de autoevaluación de la sección 13.213.1 Indique si el siguiente enunciado es verdadero o falso, y si es falso, explique por qué: si un atributo de una clase se marca con un signo menos (–) en un diagrama de clases, el atributo no es directamente accesible fuera de la clase.

Fig. 13.5 � Código de Java para la clase Retiro, con base en las figuras 13.1 y 13.2.

1 // La clase Retiro representa una transacción de retiro del ATM

2 public class Retiro

3 {

4 // atributos

5 private int numeroCuenta; // cuenta de la que se retirarán los fondos

6 private double monto; // monto a retirar

7

8 // referencias a los objetos asociados

9 private Pantalla pantalla; // pantalla del ATM

10 private Teclado teclado; // teclado del ATM

11 private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM

12 private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas

13

14 // constructor sin argumentos

15 public Retiro()

16 {

17 } // fin del constructor de Retiro sin argumentos

18 } // fin de la clase Retiro

Fig. 13.6 � Código de Java para la clase Retiro, con base en las figuras 13.1 y 13.2.

1 // La clase Retiro representa una transacción de retiro del ATM

2 public class Retiro

3 {

4 // atributos

5 private int numeroCuenta; // cuenta de la que se van a retirar los fondos

6 private double monto; // monto a retirar

7

8 // referencias a los objetos asociados

9 private Pantalla pantalla; // pantalla del ATM

10 private Teclado teclado; // teclado del ATM

11 private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM12 private BaseDatosBanco baseDatosBanco; // base de datos de información de las

cuentas

13

14 // constructor sin argumentos

15 public Retiro()

16 {

17 } // fin del constructor de Retiro sin argumentos

18

19 // operaciones

20 public void ejecutar()

21 {

22 } // fin del método ejecutar

23 } // fin de la clase Retiro

516 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

13.2 En la figura 13.2, la asociación entre los objetos ATM y Pantalla indica:a) que podemos navegar de la Pantalla al ATM. b) que podemos navegar del ATM a la Pantalla.c) (a) y (b); la asociación es bidireccional.d) Ninguna de las anteriores.

13.3 Escriba código de Java para empezar a implementar el diseño para la clase Teclado.

13.3 Incorporación de la herencia y el polimorfismo en el sistema ATM

[Nota: esta sección se puede enseñar después del capítulo 10]. Ahora regresaremos a nuestro diseño del sistema ATM para ver cómo podría beneficiarse de la he-rencia. Para aplicar la herencia, primero buscamos características comunes entre las clases del sistema. Creamos una jerarquía de herencia para modelar las clases similares (pero no idénticas) de una forma más elegante y eficiente. Después modificamos nuestro diagrama de clases para incorporar las nuevas relaciones de herencia. Por último, demostramos cómo traducir nuestro diseño actualizado en có-digo de Java.

En la sección 12.3 nos topamos con el problema de representar una transacción financiera en el sistema. En vez de crear una clase para representar a todos los tipos de transacciones, optamos por crear tres clases distintas de transacciones (SolicitudSaldo, Retiro y Deposito) para representar las trans-acciones que puede realizar el sistema ATM. La figura 13.7 muestra los atributos y operaciones de las clases SolicitudSaldo, Retiro y Deposito. Estas clases tienen un atributo (numeroCuenta) y una ope-ración (ejecutar) en común. Cada clase requiere que el atributo numeroCuenta especifique la cuenta a la que se aplica la transacción. Cada clase contiene la operación ejecutar, que el ATM invoca para realizar la transacción. Es evidente que SolicitudSaldo, Retiro y Deposito representan tipos de trans-acciones. La figura 13.7 revela las características comunes entre las clases de transacciones, por lo que el uso de la herencia para factorizar las características comunes parece apropiado para diseñar estas cla-ses. Colocamos la funcionalidad común en una superclase, Transaccion, que las clases SolicitudSaldo, Retiro y Deposito extienden.

Fig. 13.7 � Atributos y operaciones de las clases SolicitudSaldo, Retiro y Deposito.

SolicitudSaldo

- numeroCuenta : Integer

Retiro

- numeroCuenta : Integer- monto : Double

Deposito

- numeroCuenta : Integer- monto : Double

+ ejecutar()

+ ejecutar() + ejecutar()

GeneralizaciónUML especifica una relación conocida como generalización para modelar la herencia. La figura 13.8 es el diagrama de clases que modela la generalización de la superclase Transaccion y las subclases

13.3 Incorporación de la herencia y el polimorfi smo en el sistema ATM 517

SolicitudSaldo, Retiro y Deposito. Las flechas con puntas triangulares huecas indican que las cla-ses SolicitudSaldo, Retiro y Deposito extienden a la clase Transaccion. Se dice que la clase Trans-accion es una generalización de las clases SolicitudSaldo, Retiro y Deposito. Se dice que las clases SolicitudSaldo, Retiro y Deposito son especializaciones de la clase Transaccion.

Fig. 13.8 � Diagrama de clases que modela la generalización de la superclase Transaccion y las subclases SolicitudSaldo, Retiro y Deposito. Los nombres de las clases abstractas (por ejemplo, Transaccion) y los nombres de los métodos (por ejemplo, ejecutar en la clase Transaccion) aparecen en cursiva.

Transacción

– numeroCuenta: Integer

+ obtenerNumeroCuenta()+ ejecutar()

SolicitudSaldo

+ ejecutar()

Retiro

+ ejecutar()

– monto : Double

Deposito

+ ejecutar()

– monto : Double

Las clases SolicitudSaldo, Retiro y Deposito comparten el atributo entero numeroCuenta, por lo que factorizamos este atributo común y lo colocamos en la superclase Transaccion. Ya no listamos a numeroCuenta en el segundo compartimiento de cada subclase, puesto que las tres subclases heredan este atributo de Transaccion. Sin embargo, recuerde que las subclases no pueden acceder de manera directa a los atributos private de una superclase. Por lo tanto, incluimos el método public obtenerNumero-Cuenta en la clase Transaccion. Cada subclase heredará este método, con lo cual podrá acceder a su numeroCuenta según sea necesario para ejecutar una transacción.

De acuerdo con la figura 13.7, las clases SolicitudSaldo, Retiro y Deposito también compar-ten la operación ejecutar, por lo que colocamos el método public ejecutar en la superclase Trans-accion. Sin embargo, no tiene sentido implementar a ejecutar en la clase Transaccion, ya que la funcionalidad que proporciona este método depende del tipo de la transacción actual. Por lo tanto, de-claramos el método ejecutar como abstract en la superclase Transaccion. Cualquier clase que con-tenga cuando menos un método abstracto también debe declararse como abstract. Esto obliga a que cualquier clase de Transaccion que deba ser una clase concreta (es decir, SolicitudSaldo, Retiro y Deposito) a implementar el método ejecutar. UML requiere que coloquemos los nombres de clase abstractos (y los métodos abstractos) en cursivas, por lo cual Transaccion y su método ejecutar apare-cen en cursivas en la figura 13.8. Observe que el método ejecutar no está en cursivas en las subclases SolicitudSaldo, Retiro y Deposito. Cada subclase sobrescribe el método ejecutar de la superclase Transaccion con una implementación concreta que realiza los pasos apropiados para completar ese tipo de transacción. La figura 13.8 incluye la operación ejecutar en el tercer compartimiento de las clases SolicitudSaldo, Retiro y Deposito, ya que cada clase tiene una implementación concreta dis-tinta del método sobrescrito.

Procesamiento de objetos Transaccion mediante el polimorfismoEl polimorfismo proporciona al ATM una manera elegante de ejecutar todas las transacciones “en general”. Por ejemplo, suponga que un usuario elige realizar una solicitud de saldo. El ATM establece una refe-rencia Transaccion a un nuevo objeto de la clase SolicitudSaldo. Cuando el ATM utiliza su referencia

518 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Transaccion para invocar el método ejecutar, se hace una llamada a la versión de ejecutar de SolicitudSaldo.

Este enfoque polimórfico también facilita la extensibilidad del sistema. Si deseamos crear un nuevo tipo de transacción (por ejemplo, una transferencia de fondos o el pago de un recibo), sólo tenemos que crear una subclase de Transaccion adicional que sobrescriba el método ejecutar con una versión apropiada para ejecutar el nuevo tipo de transacción. Sólo tendríamos que realizar pequeñas modifica-ciones al código del sistema, para permitir que los usuarios seleccionen el nuevo tipo de transacción del menú principal y para que la clase ATM cree instancias y ejecute objetos de la nueva subclase. La clase ATM podría ejecutar transacciones del nuevo tipo utilizando el código actual, ya que éste ejecuta todas las transacciones de manera polimórfica, usando una referencia Transaccion general.

Cabe recordar que una clase abstracta como Transaccion es una para la cual el programador nun-ca tendrá la intención de crear instancias de objetos. Una clase abstracta sólo declara los atributos y comportamientos comunes de sus subclases en una jerarquía de herencia. La clase Transaccion de-fine el concepto de lo que significa ser una transacción que tiene un número de cuenta y puede ejecu-tarse. Tal vez usted se pregunte por qué nos tomamos la molestia de incluir el método abstract ejecutar en la clase Transaccion, si carece de una implementación concreta. En concepto, incluimos este método porque corresponde al comportamiento que define a todas las transacciones: ejecutarse. Técnicamente, debemos incluir el método ejecutar en la superclase Transaccion, de manera que la clase ATM (o cualquier otra clase) pueda invocar mediante el polimorfismo a la versión sobrescrita de este método en cada subclase, a través de una referencia Transaccion. Además, desde la perspec-tiva de la ingeniería de software, al incluir un método abstracto en una superclase, el que implementa las subclases se ve obligado a sobrescribir ese método con implementaciones concretas en las subcla-ses, o de lo contrario, las subclases también serán abstractas, lo cual impedirá que se creen instancias de objetos de esas subclases.

Atributo adicional de las clases Retiro y DepositoLas subclases SolicitudSaldo, Retiro y Deposito heredan el atributo numeroCuenta de la super-clase Transaccion, pero las clases Retiro y Deposito contienen el atributo adicional monto que las diferencia de la clase SolicitudSaldo. Las clases Retiro y Deposito requieren este atributo adicional para almacenar el monto de dinero que el usuario desea retirar o depositar. La clase SolicitudSaldo no necesita dicho atributo, puesto que sólo requiere un número de cuenta para ejecutarse. Aun cuando dos de las tres subclases de Transaccion comparten el atributo monto, no lo colocamos en la super-clase Transaccion; en la superclase sólo colocamos las características comunes para todas las subcla-ses, ya que de otra forma las subclases podrían heredar atributos (y métodos) que no necesitan y no deben tener.

Diagrama de clases en el que se incorpora la jerarquía de TransaccionLa figura 13.9 presenta un diagrama de clases actualizado de nuestro modelo, en el cual se incorpora la herencia y se introduce la clase Transaccion. Modelamos una asociación entre la clase ATM y la clase Transaccion para mostrar que la clase ATM, en cualquier momento dado, está ejecutando una transac-ción o no lo está (es decir, existen cero o un objetos de tipo Transaccion en el sistema, en un momento dado). Como un Retiro es un tipo de Transaccion, ya no dibujamos una línea de asociación direc-tamente entre la clase ATM y la clase Retiro. La subclase Retiro hereda la asociación de la superclase Transaccion con la clase ATM. Las subclases SolicitudSaldo y Deposito también heredan esta asocia-ción, por lo que ya no existen las asociaciones entre la clase ATM y las clases SolicitudSaldo y Deposito, que se habían omitido anteriormente.

13.3 Incorporación de la herencia y el polimorfi smo en el sistema ATM 519

También agregamos una asociación entre la clase Transaccion y la clase BaseDatosBanco (figura 13.9). Todos los objetos Transaccion requieren una referencia a BaseDatosBanco, de manera que pue-dan acceder a (y modificar) la información de las cuentas. Debido a que cada subclase de Transaccion hereda esta referencia, ya no tenemos que modelar la asociación entre la clase Retiro y BaseDatosBanco. De manera similar, ya no existen las asociaciones entre BaseDatosBanco y las clases SolicitudSaldo y Deposito, que omitimos anteriormente.

Mostramos una asociación entre la clase Transaccion y la clase Pantalla. Todos los objetos Transaccion muestran los resultados al usuario a través de la Pantalla. Por ende, ya no incluimos la asociación que modelamos antes entre Retiro y Pantalla, aunque Retiro aún participa en las aso-ciaciones con DispensadorEfectivo y Teclado. Nuestro diagrama de clases que incorpora la herencia también modela a Deposito y SolicitudSaldo. Mostramos las asociaciones entre Deposito y tanto RanuraDeposito como Teclado. La clase SolicitudSaldo no participa en asociaciones más que las heredadas de la clase Transaccion; un objeto SolicitudSaldo sólo necesita interactuar con la Base-DatosBanco y con la Pantalla.

La figura 13.1 muestra los atributos y las operaciones con marcadores de visibilidad. Ahora pre-sentamos un diagrama de clases modificado que incorpora la herencia en la figura 13.10. Este diagrama abreviado no muestra las relaciones de herencia, sino los atributos y los métodos después de haber empleado la herencia en nuestro sistema. Para ahorrar espacio, como hicimos en la figura 12.12, no in-

Fig. 13.9 � Diagrama de clases del sistema ATM (en el que se incorpora la herencia). El nombre de la clase abstracta Transaccion aparece en cursivas.

Accede a/modifica el saldode una cuenta a través de

Ejecuta

1

1

1

1 1

1

1

1

1

1 1 1 1

11

1

0..*

0..110..1

0..1 0..1

0..1 0..10..1

1Contiene

Autentica al usuario contra

Teclado

Transacción

SolicitudSaldo

RetiroRanuraDeposito

ATM

DispensadorEfectivo

Pantalla

Deposito

Cuenta

BaseDatosBanco

520 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

cluimos los atributos mostrados por las asociaciones en la figura 13.9; sin embargo, los incluimos en la implementación en Java que aparece en la sección 13.4. También omitimos todos los parámetros de las operaciones, como hicimos en la figura 13.1; al incorporar la herencia no se afectan los parámetros que ya estaban modelados en las figuras 12.17 a 12.21.

Observación de ingeniería de software 13.1Un diagrama de clases completo muestra todas las asociaciones entre clases, junto con todos los atributos y operaciones para cada clase. Cuando el número de atributos, mé-todos y asociaciones de las clases es substancial (como en las figuras 13.9 y 13.10), una buena práctica que promueve la legibilidad es dividir esta información entre dos diagramas de clases: uno que se enfoque en las asociaciones y el otro en los atributos y métodos.

Fig. 13.10 � Diagrama de clases con atributos y operaciones (incorporando la herencia). El nombre de la clase abstracta Transaccion y el nombre del método abstracto ejecutar en la clase Transaccion aparecen en cursiva.

ATM

– usuarioAutenticado : Boolean = false

SolicitudSaldo

DispensadorEfectivo

– cuenta : Integer = 500

RanuraDeposito

Pantalla

TecladoRetiro

– monto : Double

BaseDatosBanco

Deposito

– monto : Double

+ autenticarUsuario() : Boolean+ obtenerSaldoDisponible() : Double+ obtenerSaldoTotal() : Double+ abonar()+ cargar()

Cuenta

– numeroCuenta : Integer– nip : Integer– saldoDisponible : Double– saldoTotal : Double

+ validarNIP() : Boolean+ obtenerSaldoDisponible() : Double+ obtenerSaldoTotal() : Double+ abonar()+ cargar()

+ ejecutar()

Transacción

– numeroCuenta : Integer

+ obtenerNumeroCuenta()+ ejecutar()

+ ejecutar()

+ mostrarMensaje()

+ dispensarEfectivo()+ haySuficienteEfectivoDisponible() : Boolean

+ obtenerEntrada() : Integer

+ ejecutar()

+ seRecibioSobre() : Boolean

13.3 Incorporación de la herencia y el polimorfi smo en el sistema ATM 521

Implementación del diseño del sistema ATM (en el que se incorpora la herencia)En la sección 13.2 empezamos a implementar el diseño del sistema ATM en código de Java. Ahora modificaremos nuestra implementación para incorporar la herencia, usando la clase Retiro como ejemplo.

1. Si la clase A es una generalización de la clase B, entonces la clase B extiende a la clase A en la de-claración de la clase. Por ejemplo, la superclase abstracta Transaccion es una generalización de la clase Retiro. La figura 13.11 muestra la declaración de la clase Retiro.

2. Si la clase A es una clase abstracta y la clase B es una subclase de la clase A, entonces la clase B debe implementar los métodos abstractos de la clase A, si la clase B va a ser una clase concreta. Por ejemplo, la clase Transaccion contiene el método abstracto ejecutar, por lo que la clase Retiro debe implementar este método si queremos crear una instancia de un objeto Retiro. La figura 13.12 es el código en Java para la clase Retiro de las figuras 13.9 y 13.10. La clase Retiro hereda el campo numeroCuenta de la superclase Transaccion, por lo que Retiro no necesita declarar este campo. La clase Retiro también hereda referencias a las clases Pantalla y BaseDatosBanco de su superclase Transaccion, por lo que no incluimos estas referencias en nuestro código. La figura 13.10 especifica el atributo monto y la operación ejecutar para la clase Retiro. La línea 6 de la figura 13.12 declara un campo para el atributo monto. Las líneas 16 a 19 declaran la estructura de un método para la operación ejecutar. Recuerde que la subclase Retiro debe proporcionar una implementación concreta del método abs-tract ejecutar de la superclase Transaccion. Las referencias teclado y dispensador-Efectivo (líneas 7 y 8) son campos derivados de las asociaciones de Retiro en la figura 13.9. El constructor en la versión funcional completa de esta clase inicializará estas referencias con objetos reales.

Fig. 13.11 � Código de Java para la estructura de la clase Retiro.

1 // La clase Retiro representa una transacción de retiro en el ATM

2 public class Retiro extends Transaccion

3 {

4 } // fin de la clase Retiro

Fig. 13.12 � Código de Java para la clase Retiro, basada en las figuras 13.9 y 13.10 (parte 1 de 2).

1 // Retiro.java

2 // Se generó usando los diagramas de clases en las figuras 13.9 y 13.10

3 public class Retiro extends Transaccion

4 {

5 // atributos

6 private double monto; // monto a retirar

7 private Teclado teclado; // referencia al teclado

8 private DispensadorEfectivo dispensadorEfectivo; // referencia al dispensador de efectivo

9

10 // constructor sin argumentos

11 public Retiro()

12 {

13 } // fin del constructor de Retiro sin argumentos

14

522 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Observación de ingeniería de software 13.2Varias herramientas de modelado de UML convierten los diseños basados en UML en código de Java, y pueden agilizar el proceso de implementación en forma considerable. Para obtener más información sobre estas herramientas, visite nuestro Centro de recursos de UML en www.deitel.com/UML/.

¡Felicidades por haber completado la porción correspondiente al diseño del caso de estudio! En la sección 13.4 implementamos el sistema ATM, en código en Java. Le recomendamos leer con cuidado el código y su descripción. El código contiene muchos comentarios y sigue con precisión el diseño, con el cual usted ya está familiarizado. La descripción que lo acompaña está escrita cuida-dosamente, para guiar su comprensión acerca de la implementación con base en el diseño de UML. Dominar este código es un maravilloso logro culminante, después de estudiar las secciones 12.2 a 12.7 y 13.2 a 13.3.

Ejercicios de autoevaluación de la sección 13.3

13.4 UML utiliza una flecha con una para indicar una relación de generalización.a) punta con relleno sólidob) punta triangular sin rellenoc) punta hueca en forma de diamanted) punta lineal

13.5 Indique si el siguiente enunciado es verdadero o falso y, si es falso, explique por qué: UML requiere que subraye-mos los nombres de las clases abstractas y los nombres de los métodos abstractos.

13.6 Escriba código en Java para empezar a implementar el diseño para la clase Transaccion que se especifica en las figuras 13.9 y 13.10. Asegúrese de incluir los atributos tipo referencias private, con base en las asociaciones de la clase Transaccion. Asegúrese también de incluir los métodos establecer public que proporcionan acceso a cualquiera de estos atributos private que requieren las subclases para realizar sus tareas.

13.4 Implementación del caso de estudio del ATMEsta sección contiene la implementación funcional completa de 673 líneas del sistema ATM. Consi-deramos las clases en el orden en el que las identificamos en la sección 12.3: ATM, Pantalla, Teclado, DispensadorEfectivo, RanuraDeposito, Cuenta, BaseDatosBanco, Transaccion, SolicitudSaldo, Retiro y Deposito.

Aplicamos los lineamientos que vimos en las secciones 13.2 y 13.3 para codificar estas clases, con base en la manera en que las modelamos en los diagramas de clases UML de las figuras 13.9 y 13.10. Para desarrollar los cuerpos de los métodos de las clases, nos referimos a los diagramas de actividad pre-sentados en la sección 12.5 y los diagramas de comunicaciones y secuencia presentados en la sección 12.7. Nuestro diseño del ATM no especifica toda la lógica de programación, por lo que tal vez no especifi-que todos los atributos y operaciones requeridos para completar la implementación del ATM. Ésta es

Fig. 13.12 � Código de Java para la clase Retiro, basada en las figuras 13.9 y 13.10 (parte 2 de 2).

15 // método que sobrescribe a ejecutar

16 @Override

17 public void ejecutar()

18 {

19 } // fin del método ejecutar

20 } // fin de la clase Retiro

13.4 Implementación del caso de estudio del ATM 523

una parte normal del proceso de diseño orientado a objetos. A medida que implementamos el sistema, completamos la lógica del programa, agregando atributos y comportamientos según sea necesario para construir el sistema ATM especificado por el documento de requerimientos de la sección 12.2.

Concluimos el análisis presentando una aplicación de Java (CasoEstudioATM) que inicia el ATM y pone en uso las demás clases del sistema. Recuerde que estamos desarrollando la primera versión del sistema ATM que se ejecuta en una computadora personal, y utiliza el teclado y el monitor para lograr la mayor semejanza posible con el teclado y la pantalla de un ATM. Además, sólo simulamos las accio-nes del dispensador de efectivo y la ranura de depósito del ATM. Sin embargo, tratamos de implementar el sistema de manera tal que las versiones reales de hardware de esos dispositivos pudieran integrarse sin necesidad de cambios considerables en el código.

13.4.1 La clase ATM La clase ATM (figura 13.13) representa al ATM como un todo. Las líneas 6 a 12 implementan los atribu-tos de la clase. Determinamos todos estos atributos (excepto uno) de los diagramas de clase de las figuras 13.9 y 13.10. En la figura 13.10 implementamos el atributo Boolean usuarioAutenticado de UML como un atributo boolean en Java (línea 6). En la línea 7 se declara un atributo que no se incluye en nuestro diseño UML: el atributo int numeroCuentaActual, que lleva el registro del número de cuenta del usuario autenticado actual. Pronto veremos cómo es que la clase utiliza este atributo. En las líneas 8 a 12 se declaran atributos de tipo de referencia, correspondientes a las asociaciones de la clase ATM mo-deladas en el diagrama de clases de la figura 13.9. Estos atributos permiten al ATM acceder a sus partes (es decir, su Pantalla, Teclado, DispensadorEfectivo y RanuraDeposito) e interactuar con la base de datos de información de cuentas bancarias (es decir, un objeto BaseDatosBanco).

Fig. 13.13 � La clase ATM representa al ATM (parte 1 de 4).

1 // ATM.java

2 // Representa a un cajero automático

3

4 public class ATM

5 {

6 private boolean usuarioAutenticado; // indica si el usuario es autenticado

7 private int numeroCuentaActual; // número de cuenta actual del usuario

8 private Pantalla pantalla; // pantalla del ATM

9 private Teclado teclado; // teclado del ATM

10 private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM

11 private RanuraDeposito ranuraDeposito; // ranura de depósito del ATM12 private BaseDatosBanco baseDatosBanco; // base de datos de información de las

cuentas

13

14 // constantes correspondientes a las opciones del menú principal

15 private static final int SOLICITUD_SALDO = 1;

16 private static final int RETIRO = 2;

17 private static final int DEPOSITO = 3;

18 private static final int SALIR = 4;

19

20 // el constructor sin argumentos de ATM inicializa las variables de instancia

21 public ATM()

22 {

23 usuarioAutenticado = false; // al principio, el usuario no está autenticado

24 numeroCuentaActual = 0; // al principio, no hay número de cuenta

25 pantalla = new Pantalla(); // crea la pantalla

524 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Fig. 13.13 � La clase ATM representa al ATM (parte 2 de 4).

26 teclado = new Teclado(); // crea el teclado27 dispensadorEfectivo = new DispensadorEfectivo(); // crea el dispensador de

efectivo28 ranuraDeposito = new RanuraDeposito(); // crea la ranura de depósito29 baseDatosBanco = new BaseDatosBanco(); // crea la base de datos de información

de cuentas30 } // fin del constructor sin argumentos de ATM31 32 // inicia el ATM33 public void run()34 {35 // da la bienvenida al usuario y lo autentica; realiza transacciones36 while ( true )37 {38 // itera mientras el usuario no haya sido autenticado39 while ( !usuarioAutenticado )40 {41 pantalla.mostrarLineaMensaje( “\nBienvenido!” );42 autenticarUsuario(); // autentica el usuario43 } // fin de while44 45 realizarTransacciones(); // ahora el usuario está autenticado46 usuarioAutenticado = false; // restablece antes de la siguiente sesión con

el ATM

47 numeroCuentaActual = 0; // restablece antes de la siguiente sesión con el ATM

48 pantalla.mostrarLineaMensaje( “\nGracias! Adios!” );49 } // fin de while50 } // fin del método run51 52 // trata de autenticar al usuario en la base de datos53 private void autenticarUsuario()54 {55 pantalla.mostrarMensaje( “\nEscriba su numero de cuenta: ” );56 int numeroCuenta = teclado.obtenerEntrada(); // recibe como entrada el número

de cuenta57 pantalla.mostrarMensaje( “\nEscriba su NIP: ” ); // pide el NIP58 int nip = teclado.obtenerEntrada(); // recibe como entrada el NIP59 60 // establece usuarioAutenticado con el valor booleano devuelto por la base de

datos61 usuarioAutenticado =62 baseDatosBanco.autenticarUsuario( numeroCuenta, nip );63 64 // verifica si la autenticación tuvo éxito65 if ( usuarioAutenticado )66 {67 numeroCuentaActual = numeroCuenta; // guarda el # de cuenta del usuario68 } // fin de if69 else70 pantalla.mostrarLineaMensaje(71 “Numero de cuenta o NIP invalido. Intente de nuevo.” );72 } // fin del método autenticarUsuario73 74 // muestra el menú principal y realiza transacciones75 private void realizarTransacciones()76 {77 // variable local para almacenar la transacción que se procesa actualmente78 Transaccion transaccionActual = null;

13.4 Implementación del caso de estudio del ATM 525

Fig. 13.13 � La clase ATM representa al ATM (parte 3 de 4).

79 80 boolean usuarioSalio = false; // el usuario no ha elegido salir81 82 // itera mientras que el usuario no haya elegido la opción para salir del

sistema83 while ( !usuarioSalio )84 {85 // muestra el menú principal y obtiene la selección del usuario86 int seleccionMenuPrincipal = mostrarMenuPrincipal();87 88 // decide cómo proceder, con base en la opción del menú seleccionada por el

usuario89 switch ( seleccionMenuPrincipal )90 {91 // el usuario eligió realizar uno de tres tipos de transacciones92 case SOLICITUD_SALDO:93 case RETIRO:94 case DEPOSITO:95 96 // inicializa como nuevo objeto del tipo elegido97 transaccionActual =98 crearTransaccion( seleccionMenuPrincipal );99 100 transaccionActual.ejecutar(); // ejecuta la transacción101 break;102 case SALIR: // el usuario eligió terminar la sesión103 pantalla.mostrarLineaMensaje( "\nCerrando el sistema..." );104 usuarioSalio = true; // esta sesión con el ATM debe terminar105 break;106 default: // el usuario no introdujo un entero de 1 a 4107 pantalla.mostrarLineaMensaje(108 "\nNo introdujo una seleccion valida. Intente de nuevo." );109 break;110 } // fin de switch111 } // fin de while112 } // fin del método realizarTransacciones113 114 // muestra el menú principal y devuelve una selección de entrada115 private int mostrarMenuPrincipal()116 {117 pantalla.mostrarLineaMensaje( “\nMenu principal:” );118 pantalla.mostrarLineaMensaje( “1 - Ver mi saldo” );119 pantalla.mostrarLineaMensaje( “2 - Retirar efectivo” );120 pantalla.mostrarLineaMensaje( “3 - Depositar fondos” );121 pantalla.mostrarLineaMensaje( “4 - Salir\n” );122 pantalla.mostrarMensaje( “Escriba una opcion: ” );123 return teclado.obtenerEntrada(); // devuelve la opcion seleccionada por el

usuario124 } // fin del método mostrarMenuPrincipal125 126 // devuelve un objeto de la subclase especificada de Transaccion127 private Transaccion crearTransaccion( int tipo )128 {129 Transaccion temp = null; // variable temporal Transaccion130

526 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Las líneas 15 a 18 declaran constantes enteras que corresponden a las cuatro opciones en el menú principal del ATM (es decir, solicitud de saldo, retiro, depósito y salir). Las líneas 21 a 30 declaran el constructor, el cual inicializa los atributos de la clase. Cuando se crea un objeto ATM por primera vez, no se autentica ningún usuario, por lo que la línea 23 inicializa usuarioAutenticado a false. De igual forma, la línea 24 inicializa numeroCuentaActual a 0 debido a que todavía no hay un usuario actual. Las líneas 25 a 28 crean instancias de nuevos objetos para representar las partes del ATM. Recuerde que la clase ATM tiene relaciones de composición con las clases Pantalla, Teclado, Dispen-sadorEfectivo y RanuraDeposito, por lo que la clase ATM es responsable de su creación. La línea 29 crea un nuevo objeto BaseDatosBanco. [Nota: si éste fuera un sistema ATM real, la clase ATM recibiría una referencia a un objeto base de datos existente creado por el banco. Sin embargo, en esta imple-mentación sólo estamos simulando la base de datos del banco, por lo que ATM crea el objeto BaseDatos-Banco con el que interactúa].

El método run de ATM El diagrama de clases de la figura 13.10 no lista ninguna operación para la clase ATM. Ahora vamos a im-plementar una operación (es decir, un método public) en la clase ATM que permite a un cliente externo de la clase (en este caso, la clase CasoEstudioATM) indicar al ATM que se ejecute. El método run de ATM (líneas 33 a 50) usa un ciclo infinito (líneas 36 a 49) para dar la bienvenida repetidas veces a un usuario, tratar de autenticarlo y, si la autenticación tiene éxito, permite al usuario realizar transacciones. Una vez que un usuario autenticado realiza las transacciones deseadas y selecciona la opción para salir, el ATM se reinicia a sí mismo, muestra un mensaje de despedida al usuario y reinicia el proceso. Aquí usamos un ciclo infinito para simular el hecho de que un ATM parece ejecutarse en forma continua hasta que el banco lo apaga (una acción que está más allá del control del usuario). Un usuario del ATM tiene la opción de salir del sistema, pero no la habilidad de apagar el ATM por completo.

Autenticación de un usuarioEn el ciclo infinito del método run, las líneas 39 a 43 provocan que el ATM de la bienvenida al usuario y trate de autenticarlo repetidas veces, siempre y cuando éste no haya sido autenticado antes (es decir,

Fig. 13.13 � La clase ATM representa al ATM (parte 4 de 4).

131 // determina qué tipo de Transaccion crear

132 switch ( tipo )

133 {

134 case SOLICITUD_SALDO: // crea una nueva transacción SolicitudSaldo

135 temp = new SolicitudSaldo(

136 numeroCuentaActual, pantalla, baseDatosBanco );

137 break;

138 case RETIRO: // crea una nueva transacción Retiro

139 temp = new Retiro( numeroCuentaActual, pantalla,

140 baseDatosBanco, teclado, dispensadorEfectivo );

141 break;

142 case DEPOSITO: // crea una nueva transacción Deposito

143 temp = new Deposito( numeroCuentaActual, pantalla,

144 baseDatosBanco, teclado, ranuraDeposito );

145 break;

146 } // fin de switch

147

148 return temp; // devuelve el objeto recién creado

149 } // fin del método crearTransaccion

150 } // fin de la clase ATM

13.4 Implementación del caso de estudio del ATM 527

que !usarioAutenticado sea true). La línea 41 invoca al método mostrarLineaMensaje de la pantalla del ATM para mostrar un mensaje de bienvenida. Al igual que el método mostrarMensaje de Pantalla diseñado en el caso de estudio, el método mostrarLineaMensaje (declarado en las líneas 13 a 16 de la figura 13.14) muestra un mensaje al usuario, sólo que este método también produce una nueva línea después del mensaje. Agregamos este método durante la implementación para dar a los clientes de la clase Pantalla un mayor control sobre la disposición de los mensajes visualizados. La línea 42 invoca el método utilitario private autenticarUsuario de la clase ATM (declarado en las líneas 53 a 72) para tratar de autenticar al usuario.

Nos referimos al documento de requerimientos para determinar los pasos necesarios para auten-ticar al usuario, antes de permitir que ocurran transacciones. La línea 55 del método autenticar-Usuario invoca al método mostrarMensaje de la pantalla, para pedir al usuario que introduzca un número de cuenta. La línea 56 invoca el método obtenerEntrada del teclado para obtener la entrada del usuario, y después almacena el valor entero introducido por el usuario en una variable local lla-mada numero-Cuenta. A continuación, el método autenticarUsuario pide al usuario que introduzca un NIP (línea 57), y almacena el NIP introducido por el usuario en la variable local nip (línea 58). En seguida, las líneas 61 y 62 tratan de autenticar al usuario pasando el numeroCuenta y nip intro-ducidos por el usuario al método autenticarUsuario de la baseDatosBanco. La clase ATM establece su atributo usuarioAutenticado al valor booleano devuelto por este método; usuarioAutenticado se vuelve true si la autenticación tiene éxito (es decir, si numeroCuenta y nip coinciden con los de una Cuenta existente en baseDatosBanco), y permanece como false en caso contrario. Si usuario-Autenticado es true, la línea 67 guarda el número de cuenta introducido por el usuario (es decir, numeroCuenta) en el atributo numeroCuentaActual del ATM. Los otros métodos de ATM usan esta variable cada vez que una sesión del ATM requiere acceso al número de cuenta del usuario. Si usuarioAutenticado es false, las líneas 70 y 71 usan el método mostrarLineaMensaje de pantalla para indicar que se introdujo un número de cuenta inválido y/o NIP, por lo que el usuario debe inten-tar de nuevo. Establecemos numeroCuentaActual sólo después de autenticar el número de cuenta del usuario y el NIP asociado; si la base de datos no puede autenticar al usuario, numeroCuentaActual permanece como 0.

Después de que el método run intenta autenticar al usuario (línea 42), si usuarioAutenticado si-gue siendo false, el ciclo while en las líneas 39 a 43 se ejecuta de nuevo. Si ahora usuarioAutenticado es true, el ciclo termina y el control continúa en la línea 45, que llama al método utilitario realizar-Transacciones de la clase ATM.

Realizar transaccionesEl método realizarTransacciones (líneas 75 a 112) lleva a cabo una sesión con el ATM para un usua-rio autenticado. La línea 78 declara una variable local Transaccion a la que asignaremos un objeto SolicitudSaldo, Retiro o Deposito, el cual representa la transacción del ATM que el usuario seleccio-nó. Aquí usaremos una variable Transaccion para que nos permita sacar provecho del polimorfismo. Además, nombramos a esta variable con base en el nombre de rol incluido en el diagrama de clases de la figura 12.7: transaccionActual. La línea 80 declara otra variable boolean llamada usuarioSalio, la cual lleva el registro que indica si el usuario ha elegido salir o no. Esta variable controla un ciclo while (líneas 83 a 111), el cual permite al usuario ejecutar un número ilimitado de transacciones antes de que elija salir del sistema. Dentro de este ciclo, la línea 86 muestra el menú principal y obtiene la selección del menú del usuario al llamar a un método utilitario de ATM, llamado mostrarMenuPrincipal (decla-rado en las líneas 115 a 124). Este método muestra el menú principal invocando a los métodos de la pantalla del ATM, y devuelve una selección del menú que obtiene del usuario, a través del teclado del ATM. La línea 86 almacena la selección del usuario devuelta por mostrarMenuPrincipal en la va-riable local seleccionMenuPrincipal.

Después de obtener una selección del menú principal, el método realizarTransacciones usa una instrucción switch (líneas 89 a 110) para responder a esa selección en forma apropiada. Si selec-cionMenuPrincipal es igual a cualquiera de las tres constantes enteras que representan los tipos de

528 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

transacciones (es decir, si el usuario elige realizar una transacción), las líneas 97 y 98 llaman al método utilitario crearTransaccion (declarado en las líneas 127 a 149 para regresar un objeto recién instan-ciado del tipo que corresponde a la transacción seleccionada. A la variable transaccionActual se le asigna la referencia devuelta por crearTransaccion, y después la línea 100 invoca al método ejecutar de esta transacción para ejecutarla. En breve hablaremos sobre el método ejecutar de Transacción y sobre las tres subclases de Transaccion. Asignamos a la variable transaccionActual de Transaccion un objeto de una de las tres subclases de Transaccion, de modo que podamos ejecutar las transaccio-nes mediante el polimorfismo. Por ejemplo, si el usuario opta por realizar una solicitud de saldo, selec-cionMenuPrincipal es igual a SOLICITUD_SALDO, lo cual conduce a que crearTransaccion devuelva un objeto SolicitudSaldo. Por ende, transaccionActual se refiere a una SolicitudSaldo, y la in-vocación de transaccionActual.ejecutar() produce como resultado la invocación a la versión de ejecutar que corresponde a SolicitudSaldo.

Creación de una transacciónEl método crearTransaccion (líneas 127 a 149) usa una instrucción switch (líneas 132 a 146) para instanciar un nuevo objeto de la subclase Transaccion del tipo indicado por el parámetro tipo. Recuer-de que el método realizarTransacciones pasa la seleccionMenuPrincipal a este método sólo cuando seleccionMenuPrincipal contiene un valor que corresponde a uno de los tres tipos de transacción. Por lo tanto, tipo es SOLICITUD_SALDO, RETIRO o DEPOSITO. Cada case en la instrucción switch crea una instancia de un nuevo objeto llamando al constructor de la subclase apropiada de Transaccion. Cada constructor tiene una lista de parámetros únicos, con base en los datos específicos requeridos para inicia-lizar el objeto de la subclase. Un objeto SolicitudSaldo sólo requiere el número de cuenta del usuario actual y referencias tanto a la pantalla como a la baseDatosBanco del ATM. Además de estos parámetros, un objeto Retiro requiere referencias al teclado y dispensadorEfectivo del ATM, y un objeto Deposito requiere referencias al teclado y la ranuraDeposito del ATM. En las secciones 13.4.8 a 13.4.11 analizare-mos las clases de transacciones con más detalle.

Salir del menú principal y procesar selecciones inválidasDespués de ejecutar una transacción (línea 100 en realizarTransacciones), usuarioSalio sigue siendo false y se repiten las líneas 83 a 111, en donde el usuario regresa al menú principal. No obs-tante, si un usuario selecciona la opción del menú principal para salir en vez de realizar una transacción, la línea 104 establece usuarioSalio a true, lo cual provoca que la condición del ciclo while (!usuario-Salio) se vuelva false. Este while es la instrucción final del método realizarTransacciones, por lo que el control regresa al método run que hizo la llamada. Si el usuario introduce una selección de menú inválida (es decir, que no sea un entero del 1 al 4), las líneas 107 y 108 muestran un mensaje de error apropiado, usuarioSalio sigue siendo false y el usuario regresa al menú principal para in-tentar de nuevo.

Esperar al siguiente usuario del ATMCuando realizarTransacciones devuelve el control al método run, el usuario ha elegido salir del sistema, por lo que las líneas 46 y 47 reinician los atributos usuarioAutenticado y numeroCuentaActual del ATM como preparación para el siguiente usuario del ATM. La línea 48 muestra un mensaje de des-pedida antes de que el ATM inicie de nuevo y dé la bienvenida al nuevo usuario.

13.4.2 La clase PantallaLa clase Pantalla (figura 13.14) representa la pantalla del ATM y encapsula todos los aspectos rela-cionados con el proceso de mostrar la salida al usuario. La clase Pantalla simula la pantalla de un ATM real mediante un monitor de computadora y muestra los mensajes de texto mediante los mé-

13.4 Implementación del caso de estudio del ATM 529

todos estándar de salida a la consola System.out.print, System.out.println y System.out.printf. En este caso de estudio diseñamos la clase Pantalla de modo que tenga una operación: mostrarMensaje. Para una mayor flexibilidad al mostrar mensajes en la Pantalla, ahora declararemos tres métodos: mostrarMensaje, mostrarLineaMensaje y mostrarMontoDolares.

Fig. 13.14 � La clase Pantalla representa la pantalla del ATM.

1 // Pantalla.java

2 // Representa a la pantalla del ATM

3

4 public class Pantalla

5 {

6 // muestra un mensaje sin un retorno de carro

7 public void mostrarMensaje( String mensaje )

8 {

9 System.out.print( mensaje );

10 } // fin del método mostrarMensaje

11

12 // muestra un mensaje con un retorno de carro

13 public void mostrarLineaMensaje( String mensaje )

14 {

15 System.out.println( mensaje );

16 } // fin del método mostrarLineaMensaje

17

18 // muestra un monto en dólares

19 public void mostrarMontoDolares( double monto )

20 {

21 System.out.printf( “$%,.2f”, monto );

22 } // fin del método mostrarMontoDolares

23 } // fin de la clase Pantalla

El método mostrarMensaje (líneas 7 a 10) recibe un argumento String y lo imprime en la consola. El cursor permanece en la misma línea, lo que hace a este método apropiado para mostrar indicadores al usuario. El método mostrarLineaMensaje (líneas 13 a 16) hace lo mismo mediante System.out.println, que imprime una nueva línea para mover el cursor a la siguiente línea. Por último, el método mostrarMontoDolares (líneas 19 a 22) imprime un monto en dólares con un formato apropiado (por ejemplo, $1,234.56). La línea 21 utiliza a System.out.printf para imprimir un valor double al que se le aplica un formato con comas para mejorar la legibilidad, junto con dos lugares decimales.

13.4.3 La clase Teclado La clase Teclado (figura 13.15) representa el teclado del ATM y es responsable de recibir toda la entrada del usuario. Recuerde que estamos simulando este hardware, por lo que usaremos el teclado de la compu-tadora para simular el teclado del ATM. Usamos la clase Scanner para obtener la entrada de consola del usuario. Un teclado de computadora contiene muchas teclas que no se encuentran en el teclado del ATM. No obstante, vamos a suponer que el usuario sólo presionará las teclas en el teclado de computadora que aparezcan también en el teclado del ATM: las teclas enumeradas del 0 al 9, y la tecla Intro.

La línea 3 de la clase Teclado importa la clase Scanner para usarla en la clase Teclado. La línea 7 declara la variable Scanner entrada como una variable de instancia. La línea 12 en el constructor crea un nuevo objeto Scanner que lee la entrada del flujo de entrada estándar (System.in) y asigna la referen-cia del objeto a la variable entrada. El método obtenerEntrada (líneas 16 a 19) invoca al método nextInt de Scanner (línea 18) para devolver el siguiente entero introducido por el usuario. [Nota: el método

530 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

nextInt puede lanzar una excepción InputMismatchException si el usuario introduce una entrada que no sea número entero. Puesto que el teclado del ATM real permite introducir sólo enteros, vamos a suponer que no ocurrirá una excepción y no intentaremos corregir este problema. Para obtener más in-formación sobre cómo atrapar excepciones, vea el capítulo 11, Manejo de excepciones: un análisis más profundo]. Recuerde que nextInt contiene toda la entrada utilizada por el ATM. El método obtener-Entrada de Teclado sólo devuelve el entero introducido por el usuario. Si un cliente de la clase Teclado requiere entrada que cumpla con ciertos criterios (por decir, un número que corresponda a una opción válida del menú), el cliente deberá realizar la comprobación de errores.

13.4.4 La clase DispensadorEfectivo La clase DispensadorEfectivo (figura 13.16) representa el dispensador de efectivo del ATM. La lí-nea 7 declara la constante CUENTA_INICIAL, la cual indica la cuenta inicial de billetes en el dispensador de efectivo cuando el ATM inicia su operación (es decir, 500). La línea 8 implementa el atributo cuenta (modelado en la figura 13.10), que lleva la cuenta del número de billetes restantes en el DispensadorEfectivo en cualquier momento. El constructor (líneas 11 a 14) establece cuenta en la cuenta inicial. DispensadorEfectivo tiene 2 métodos public: dispensarEfectivo (líneas 17 a 21) y haySuficienteEfectivoDisponible (líneas 24 a 32). La clase confía en que un cliente (es decir, Retiro) llamará a dispensarEfectivo sólo después de establecer que hay suficiente efectivo dispo-nible mediante una llamada a haySuficienteEfectivoDisponible. Por ende, dispensarEfectivo tan sólo simula el proceso de dispensar la cantidad solicitada sin verificar si en realidad hay suficien-te efectivo disponible.

Fig. 13.15 � La clase Teclado representa al teclado del ATM.

Fig. 13.16 � La clase DispensadorEfectivo representa al dispensador de efectivo del ATM (parte 1 de 2).

1 // Teclado.java

2 // Representa el teclado del ATM 3 import java.util.Scanner; // el programa usa a Scanner para obtener la entrada del

usuario

4

5 public class Teclado

6 {

7 private Scanner entrada; // lee datos de la línea de comandos

8

9 // el constructor sin argumentos inicializa el objeto Scanner

10 public Teclado()

11 {

12 entrada = new Scanner( System.in );

13 } // fin del constructor sin argumentos de Teclado

14

15 // devuelve un valor entero introducido por el usuario

16 public int obtenerEntrada()

17 {

18 return entrada.nextInt(); // suponemos que el usuario introduce un entero

19 } // fin del método obtenerEntrada

20 } // fin de la clase Teclado

1 // DispensadorEfectivo.java

2 // Representa al dispensador de efectivo del ATM

3

13.4 Implementación del caso de estudio del ATM 531

El método haySuficienteEfectivoDisponible (líneas 24 a 32) tiene un parámetro llamado monto, el cual especifica el monto de efectivo en cuestión. La línea 26 calcula el número de billetes de $20 que se requieren para dispensar el monto solicitado. El ATM permite al usuario elegir sólo montos de retiro que sean múltiplos de $20, por lo que dividimos monto entre 20 para obtener el núme-ro de billetesRequeridos. Las líneas 28 a 31 devuelven true si la cuenta del DispensadorEfectivo es mayor o igual a billetesRequeridos (es decir, que haya suficientes billetes disponibles), y false en caso contrario (que no haya suficientes billetes). Por ejemplo, si un usuario desea retirar $80 (que billetesRequeridos sea 4) y sólo quedan tres billetes (cuenta es 3), el método devuelve false.

El método dispensarEfectivo (líneas 17 a 21) simula el proceso de dispensar el efectivo. Si nuestro sistema se conectara al hardware real de un dispensador de efectivo, este método interactuaría con el dispositivo para dispensar físicamente el efectivo. Nuestra versión del método tan sólo reduce la cuenta de billetes restantes con base en el número requerido para dispensar el monto especificado (línea 20). Es responsabilidad del cliente de la clase (es decir, Retiro) informar al usuario que se dispensó el efec-tivo; la clase DispensadorEfectivo no puede interactuar de manera directa con Pantalla.

13.4.5 La clase RanuraDepositoLa clase RanuraDeposito (figura 13.17) representa a la ranura de depósito del ATM. Al igual que la clase DispensadorEfectivo, la clase RanuraDeposito tan sólo simula la funcionalidad del hardware real de

Fig. 13.16 � La clase DispensadorEfectivo representa al dispensador de efectivo del ATM (parte 2 de 2).

4 public class DispensadorEfectivo

5 {

6 // el número inicial predeterminado de billetes en el dispensador de efectivo

7 private final static int CUENTA_INICIAL = 500;

8 private int cuenta; // número restante de billetes de $20

9 10 // el constructor sin argumentos de DispensadorEfectivo inicializa cuenta con el

valor predeterminado

11 public DispensadorEfectivo()

12 {

13 cuenta = CUENTA_INICIAL; // establece el atributo cuenta al valor predeterminado

14 } // fin del constructor de DispensadorEfectivo

15

16 // simula la acción de dispensar el monto especificado de efectivo

17 public void dispensarEfectivo( int monto )

18 {

19 int billetesRequeridos = monto / 20; // número de billetes de $20 requeridos

20 cuenta -= billetesRequeridos; // actualiza la cuenta de billetes

21 } // fin del método dispensarEfectivo

22

23 // indica si el dispensador de efectivo puede dispensar el monto deseado

24 public boolean haySuficienteEfectivoDisponible( int monto )

25 {

26 int billetesRequeridos = monto / 20; // número de billetes de $20 requeridos

27

28 if ( cuenta >= billetesRequeridos )

29 return true; // hay suficientes billetes disponibles

30 else

31 return false; // no hay suficientes billetes disponibles

32 } // fin del método haySuficienteEfectivoDisponible

33 } // fin de la clase DispensadorEfectivo

532 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

una ranura de depósito. RanuraDeposito no tiene atributos y sólo cuenta con un método: seRecibio-Sobre (líneas 8 a 11), el cual indica si se recibió un sobre de depósito.

En el documento de requerimientos vimos que el ATM permite al usuario hasta dos minutos para insertar un sobre. La versión actual del método seRecibioSobre sólo devuelve true de inmediato (línea 10), ya que ésta es sólo una simulación de software, por lo que asumimos que el usuario insertó un so-bre dentro del límite de tiempo requerido. Si se conectara el hardware de una ranura de depósito real a nuestro sistema, podría implementarse el método seRecibioSobre para esperar un máximo de dos minutos a recibir una señal del hardware de la ranura de depósito, indicando que en definitiva el usuario insertó un sobre de depósito. Si seRecibioSobre recibiera dicha señal en un tiempo máximo de dos minutos, el método devolvería true. Si transcurrieran los dos minutos y el método no recibiera ninguna señal, entonces devolvería false.

13.4.6 La clase Cuenta La clase Cuenta (figura 13.18) representa a una cuenta bancaria. Cada Cuenta tiene cuatro atributos (modelados en la figura 13.10): numeroCuenta, nip, saldoDisponible y saldoTotal. Las líneas 6 a 9 implementan estos atributos como campos private. La variable saldoDisponible representa el monto de fondos disponibles para retirar. La variable saldoTotal representa el monto de fondos disponibles, junto con el monto de los fondos depositados pendientes de confirmación o liberación.

Fig. 13.18 � La clase Cuenta representa a una cuenta bancaria (parte 1 de 2).

Fig. 13.17 � La clase RanuraDeposito representa a la ranura de depósito del ATM.

1 // RanuraDeposito.java

2 // Representa a la ranura de depósito del ATM

3

4 public class RanuraDeposito

5 {

6 // indica si se recibió el sobre (siempre devuelve true, ya que ésta

7 // es sólo una simulación de software de una ranura de depósito real)

8 public boolean seRecibioSobre()

9 {

10 return true; // se recibió el sobre

11 } // fin del método seRecibioSobre

12 } // fin de la clase RanuraDeposito

1 // Cuenta.java

2 // Representa a una cuenta bancaria

3

4 public class Cuenta

5 {

6 private int numeroCuenta; // número de cuenta

7 private int nip; // NIP para autenticación

8 private double saldoDisponible; // fondos disponibles para retirar

9 private double saldoTotal; // fondos disponibles + depósitos pendientes

13.4 Implementación del caso de estudio del ATM 533

Fig. 13.18 � La clase Cuenta representa a una cuenta bancaria (parte 2 de 2).

10 11 // el constructor de Cuenta inicializa los atributos

12 public Cuenta( int elNumeroDeCuenta, int elNIP,

13 double elSaldoDisponible, double elSaldoTotal )

14 {

15 numeroCuenta = elNumeroDeCuenta;

16 nip = elNIP;

17 saldoDisponible = elSaldoDisponible;

18 saldoTotal = elSaldoTotal;

19 } // fin del constructor de Cuenta

20

21 // determina si un NIP especificado por el usuario coincide con el NIP en la Cuenta

22 public boolean validarNIP( int nipUsuario )

23 {

24 if ( nipUsuario == nip )

25 return true;

26 else

27 return false;

28 } // fin del método validarNIP

29

30 // devuelve el saldo disponible

31 public double obtenerSaldoDisponible()

32 {

33 return saldoDisponible;

34 } // fin de obtenerSaldoDisponible

35

36 // devuelve el saldo total

37 public double obtenerSaldoTotal()

38 {

39 return saldoTotal;

40 } // fin del método obtenerSaldoTotal

41

42 // abona un monto a la cuenta

43 public void abonar( double monto )

44 {

45 saldoTotal += monto; // lo suma al saldo total

46 } // fin del método abonar

47

48 // carga un monto a la cuenta

49 public void cargar( double monto )

50 {

51 saldoDisponible -= monto; // lo resta del saldo disponible

52 saldoTotal -= monto; // lo resta del saldo total

53 } // fin del método cargar

54

55 // devuelve el número de cuenta

56 public int obtenerNumeroCuenta()

57 {

58 return numeroCuenta;

59 } // fin del método obtenerNumeroCuenta60 } // fin de la clase Cuenta

534 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

La clase Cuenta tiene un constructor (líneas 12 a 19) que recibe como argumentos un número de cuenta, el NIP establecido para la cuenta, el saldo disponible inicial y el saldo total inicial de la cuenta. Las líneas 15 a 18 asignan estos valores a los atributos de la clase (es decir, los campos).

El método validarNIP (líneas 22 a 28) determina si un NIP especificado por el usuario (es decir, el parámetro nipUsuario) coincide con el NIP asociado con la cuenta (es decir, el atributo nip). Recuer-de que modelamos el parámetro nipUsuario de este método en la figura 12.19. Si los dos NIP coinciden, el método devuelve true (línea 25); en caso contrario devuelve false (línea 27).

Los métodos obtenerSaldoDisponible (líneas 31 a 34) y obtenerSaldoTotal (líneas 37 a 40) devuelven los valores de los atributos double saldoDisponible y saldoTotal, respectivamente.

El método abonar (líneas 43 a 46) agrega un monto de dinero (el parámetro monto) a una Cuenta como parte de una transacción de depósito. Este método agrega el monto sólo al atributo saldoTotal (línea 45). El dinero abonado a una cuenta durante un depósito no se vuelve disponible de inmediato, por lo que sólo modificamos el saldo total. Supondremos que el banco actualiza después el saldo dis-ponible de manera apropiada. Nuestra implementación de la clase Cuenta sólo incluye los métodos requeridos para realizar transacciones con el ATM. Por lo tanto, omitiremos los métodos que invocaría cualquier otro sistema bancario para sumar al atributo saldoDisponible (confirmar un depósito) o restar del atributo saldoTotal (rechazar un depósito).

El método cargar (líneas 49 a 53) resta un monto de dinero (el parámetro monto) de una Cuenta, como parte de una transacción de retiro. Este método resta el monto tanto del atributo saldoDisponible (línea 51) como del atributo saldoTotal (línea 52), debido a que un retiro afecta ambas unidades del saldo de una cuenta.

El método obtenerNumeroCuenta (líneas 56 a 59) proporciona acceso al numeroCuenta de una Cuenta. Incluimos este método en nuestra implementación de modo que un cliente de la clase (por ejemplo, BaseDatosBanco) pueda identificar a una Cuenta específica. Por ejemplo, BaseDatosBanco contiene muchos objetos Cuenta, y puede invocar este método en cada uno de sus objetos Cuenta para localizar el que tenga cierto número de cuenta específico.

13.4.7 La clase BaseDatosBanco La clase BaseDatosBanco (figura 13.19) modela la base de datos del banco con la que el ATM in-teractúa para acceder a la información de la cuenta de un usuario y modificarla. En el capítulo 28 estu-diaremos el acceso a las bases de datos. Por ahora modelaremos la base de datos como un arreglo. Un ejercicio en el capítulo 28 le pedirá que vuelva a implementar esta parte del ATM, usando una base de datos real.

Fig. 13.19 � La clase BaseDatosBanco representa a la base de datos de información sobre las cuentas del banco (parte 1 de 3).

1 // BaseDatosBanco.java

2 // Representa a la base de datos de información de cuentas bancarias

3

4 public class BaseDatosBanco

5 {

6 private Cuenta cuentas[]; // arreglo de objetos Cuenta

7

8 // el constructor sin argumentos de BaseDatosBanco inicializa a cuentas

9 public BaseDatosBanco()

10 {

13.4 Implementación del caso de estudio del ATM 535

Fig. 13.19 � La clase BaseDatosBanco representa a la base de datos de información sobre las cuentas del banco (parte 2 de 3).

11 cuentas = new Cuenta[ 2 ]; // sólo 2 cuentas para probar12 cuentas[ 0 ] = new Cuenta( 12345, 54321, 1000.0, 1200.0 );13 cuentas[ 1 ] = new Cuenta( 98765, 56789, 200.0, 200.0 );14 } // fin del constructor sin argumentos de BaseDatosBanco15 16 // obtiene el objeto Cuenta que contiene el número de cuenta especificado17 private Cuenta obtenerCuenta( int numeroCuenta )18 {19 // itera a través de cuentas, buscando el número de cuenta que coincida20 for ( Cuenta cuentaActual : cuentas )21 {22 // devuelve la cuenta actual si encuentra una coincidencia23 if ( cuentaActual.obtenerNumeroCuenta() == numeroCuenta )24 return cuentaActual;25 } // fin de for26 27 return null; // si no se encontró una cuenta que coincida, devuelve null28 } // fin del método obtenerCuenta29 30 // determina si el número de cuenta y el NIP especificados por el usuario coinciden31 // con los de una cuenta en la base de datos32 public boolean autenticarUsuario( int numeroCuentaUsuario, int nipUsuario )33 {34 // trata de obtener la cuenta con el número de cuenta35 Cuenta cuentaUsuario = obtenerCuenta( numeroCuentaUsuario );36 37 // si la cuenta existe, devuelve el resultado del método validarNIP de Cuenta38 if ( cuentaUsuario != null )39 return cuentaUsuario.validarNIP( nipUsuario );40 else41 return false; // no se encontró el número de cuenta, por lo que devuelve false42 } // fin del método autenticarUsuario43 44 // devuelve el saldo disponible de la Cuenta con el número de cuenta especificado45 public double obtenerSaldoDisponible( int numeroCuentaUsuario )46 {47 return obtenerCuenta( numeroCuentaUsuario ).obtenerSaldoDisponible();48 } // fin del método obtenerSaldoDisponible49 50 // devuelve el saldo total de la Cuenta con el número de cuenta especificado51 public double obtenerSaldoTotal( int numeroCuentaUsuario )52 {53 return obtenerCuenta( numeroCuentaUsuario ).obtenerSaldoTotal();54 } // fin del método obtenerSaldoTotal55 56 // abona un monto a la Cuenta a través del número de cuenta especificado57 public void abonar( int numeroCuentaUsuario, double monto )58 {59 obtenerCuenta( numeroCuentaUsuario ).abonar( monto );60 } // fin del método abonar61

536 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Determinamos un atributo de tipo referencia para la clase BaseDatosBanco con base en su rela-ción de composición con la clase Cuenta. En la figura 13.9 vimos que una BaseDatosBanco está compuesta de cero o más objetos de la clase Cuenta. La línea 6 implementa el atributo cuentas (un arreglo de objetos Cuenta) para implementar esta relación de composición. La clase BaseDatos-Banco tiene un constructor sin argumentos (líneas 9 a 14) que inicializa cuentas para que contenga un conjunto de nuevos objetos Cuenta. A fin de probar el sistema, declaramos cuentas de modo que contenga sólo dos elementos en el arreglo (línea 11), que instanciamos como nuevos objetos Cuenta con datos de prueba (líneas 12 y 13). El constructor de Cuenta tiene cuatro parámetros: el número de cuenta, el NIP asignado a la cuenta, el saldo disponible inicial y el saldo total inicial. Recuerde que la clase BaseDatosBanco sirve como intermediario entre la clase ATM y los mismos obje-tos Cuenta que contienen la información sobre la cuenta de un usuario. Por ende, los métodos de la clase BaseDatosBanco no hacen más que invocar a los métodos correspondientes del objeto Cuenta que pertenece al usuario actual del ATM.

Incluimos el método private utilitario obtenerCuenta (líneas 17 a 28) para permitir que la BaseDatosBanco obtenga una referencia a una Cuenta específica dentro del arreglo cuentas. Para locali-zar la Cuenta del usuario, la BaseDatosBanco compara el valor devuelto por el método obtenerNumero-Cuenta para cada elemento de cuentas con un número de cuenta específico, hasta encontrar una coincidencia. Las líneas 20 a 25 recorren el arreglo cuentas. Si el número de cuenta de cuentaActual es igual al valor del parámetro numeroCuenta, el método devuelve de inmediato la cuentaActual. Si ninguna cuenta tiene el número de cuenta dado, entonces la línea 27 devuelve null.

El método autenticarUsuario (líneas 32 a 42) aprueba o desaprueba la identidad de un usuario del ATM. Este método recibe un número de cuenta y un NIP especificados por el usuario como argu-mentos, e indica si coinciden con el número de cuenta y NIP de una Cuenta en la base de datos. La línea 35 llama al método obtenerCuenta, el cual devuelve una Cuenta con numeroCuentaUsuario como su número de cuenta, o null para indicar que el numeroCuentaUsuario es inválido. Si obte-nerCuenta devuelve un objeto Cuenta, la línea 39 regresa el valor boolean devuelto por el método validarNIP de ese objeto. El método autenticarUsuario de BaseDatosBanco no realiza la compara-ción de NIP por sí solo, sino que envía nipUsuario al método validarNIP del objeto Cuenta para que lo haga. El valor devuelto por el método validarNIP de Cuenta indica si el NIP especificado por el usuario coincide con el NIP de la Cuenta del usuario, por lo que el método autenticarUsuario simplemente devuelve este valor al cliente de la clase (es decir, el ATM).

BaseDatosBanco confía en que el ATM invoque al método autenticarUsuario y reciba un valor de retorno true antes de permitir que el usuario realice transacciones. BaseDatosBanco también confía que cada objeto Transaccion creado por el ATM contendrá el número de cuenta válido del usuario actual autenticado, y que éste será el número de cuenta que se pase al resto de los métodos de BaseDatosBanco como el argumento numeroCuentaUsuario. Por lo tanto, los métodos obtener-SaldoDisponible (líneas 45 a 48), obtenerSaldoTotal (líneas 51 a 54), abonar (líneas 57 a 60) y cargar (líneas 63 a 66) tan sólo obtienen el objeto Cuenta del usuario con el método utilitario obte-

Fig. 13.19 � La clase BaseDatosBanco representa a la base de datos de información sobre las cuentas del banco (parte 3 de 3).

62 // carga un monto a la Cuenta con el número de cuenta especificado

63 public void cargar( int numeroCuentaUsuario, double monto )

64 {

65 obtenerCuenta( numeroCuentaUsuario ).cargar( monto );

66 } // fin del método cargar

67 } // fin de la clase BaseDatosBanco

13.4 Implementación del caso de estudio del ATM 537

nerCuenta, y después invocan al método de Cuenta apropiado con base en ese objeto. Sabemos que las llamadas a obtenerCuenta desde estos métodos nunca devolverán null, puesto que numero-CuentaUsuario se debe referir a una Cuenta existente. Los métodos obtenerSaldoDisponible y obtenerSaldoTotal devuelven los valores que regresan los correspondientes métodos de Cuenta. Además, abonar y cargar simplemente redirigen el parámetro monto a los métodos de Cuenta que invocan.

13.4.8 La clase TransaccionLa clase Transaccion (figura 13.20) es una superclase abstracta que representa la noción de una trans-acción con el ATM. Contiene las características comunes de las subclases SolicitudSaldo, Retiro y Deposito. Esta clase se expande a partir del código “estructural” que se desarrolló por primera vez en la sección 13.3. La línea 4 declara esta clase como abstract. Las líneas 6 a 8 declaran los atribu-tos private de las clases. En el diagrama de clases de la figura 13.10 vimos que la clase Transaccion contiene un atributo llamado numeroCuenta (línea 6), el cual indica la cuenta involucrada en la Transaccion. Luego derivamos los atributos de Pantalla (línea 7) y de BaseDatosBanco (línea8) de la clase Transaccion asociada y que se modela en la figura 13.9. Todas las transacciones requieren acceso a la pantalla del ATM y a la base de datos del banco.

Fig. 13.20 � La superclase abstracta Transaccion representa una transacción con el ATM (parte 1 de 2).

1 // Transaccion.java

2 // La superclase abstracta Transaccion representa una transacción con el ATM

3

4 public abstract class Transaccion

5 {

6 private int numeroCuenta; // indica la cuenta implicada

7 private Pantalla pantalla; // pantalla del ATM

8 private BaseDatosBanco baseDatosBanco; // base de datos de información de cuentas

9

10 // el constructor de Transaccion es invocado por las subclases mediante super()

11 public Transaccion( int numeroCuentaUsuario, Pantalla pantallaATM,

12 BaseDatosBanco baseDatosBancoATM )

13 {

14 numeroCuenta = numeroCuentaUsuario;

15 pantalla = pantallaATM;

16 baseDatosBanco = baseDatosBancoATM;

17 } // fin del constructor de Transaccion

18

19 // devuelve el número de cuenta

20 public int obtenerNumeroCuenta()

21 {

22 return numeroCuenta;

23 } // fin del método obtenerNumeroCuenta

24

25 // devuelve una referencia a la pantalla

26 public Pantalla obtenerPantalla()

27 {

28 return pantalla;

29 } // fin del método obtenerPantalla

30

538 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

La clase Transaccion tiene un constructor (líneas 11 a 17) que recibe como argumentos el número de cuenta del usuario actual y referencias tanto a la pantalla del ATM como a la base de datos del banco. Puesto que Transaccion es una clase abstracta, este constructor se llama sólo a través de los constructores de las subclases de Transaccion.

La clase tiene tres métodos obtener públicos: obtenerNumeroCuenta (líneas 20 a 23), obtener-Pantalla (líneas 26 a 29) y obtenerBaseDatosBanco (líneas 32 a 35). Estos métodos son heredados por las subclases de Transaccion y se utilizan para obtener acceso a los atributos private de esta clase.

La clase Transaccion también declara el método abstract ejecutar (línea 38). No tiene caso proveer la implementación de este método, ya que una transacción genérica no se puede ejecutar. Por ende, declaramos este método como abstract y forzamos a cada subclase de Transaccion a pro-veer una implementación concreta que ejecute este tipo específico de transacción.

13.4.9 La clase SolicitudSaldo La clase SolicitudSaldo (figura 13.21) extiende a Transaccion y representa una transacción de solici-tud de saldo con el ATM. SolicitudSaldo no tiene atributos propios, pero hereda los atributos numero-Cuenta, pantalla y baseDatosBanco de Transaccion, a los cuales se puede acceder por medio de los métodos public obtener de Transaccion. El constructor de SolicitudSaldo recibe los argumentos correspondientes a estos atributos, y lo único que hace es reenviarlos al constructor de Transaccion mediante el uso de super (línea 10).

31 // devuelve una referencia a la base de datos del banco

32 public BaseDatosBanco obtenerBaseDatosBanco()

33 {

34 return baseDatosBanco;

35 } // fin del método obtenerBaseDatosBanco

36

37 // realiza la transacción (cada subclase sobrescribe este método)

38 abstract public void ejecutar();

39 } // fin de la clase Transaccion

Fig. 13.20 � La superclase abstracta Transaccion representa una transacción con el ATM (parte 2 de 2).

Fig. 13.21 � La clase SolicitudSaldo representa a una transacción de solicitud de saldo en el ATM (parte 1 de 2).

1 // SolicitudSaldo.java

2 // Representa una transacción de solicitud de saldo en el ATM

3

4 public class SolicitudSaldo extends Transaccion

5 {

6 // constructor de SolicitudSaldo

7 public SolicitudSaldo( int numeroCuentaUsuario, Pantalla pantallaATM,

8 BaseDatosBanco baseDatosBanco )

9 {

10 super( numeroCuentaUsuario, pantallaATM, baseDatosBanco );

11 } // fin del constructor de SolicitudSaldo

12

13 // realiza la transacción

14 @Override

15 public void ejecutar()

16 {

13.4 Implementación del caso de estudio del ATM 539

La clase SolicitudSaldo sobrescribe el método abstracto ejecutar de Transacción para pro-veer una implementación discreta (líneas 14 a 36) que realiza los pasos involucrados en una solicitud de saldo. Las líneas 18 a 19 obtienen referencias a la base de datos del banco y la pantalla del ATM, al invocar a los métodos heredados de la superclase Transaccion. Las líneas 22 y 23 obtienen el saldo disponible de la cuenta implicada, mediante una invocación al método obtenerSaldoDisponible de baseDatosBanco. La línea 23 usa el método heredado obtenerNumeroCuenta para obtener el nú-mero de cuenta del usuario actual, que después pasa a obtenerSaldoDisponible. Las líneas 26 y 27 obtienen el saldo total de la cuenta del usuario actual. Las líneas 30 a 35 muestran la información del saldo en la pantalla del ATM. Recuerde que mostrarMontoDolares recibe un argumento double y lo imprime en la pantalla, con formato de monto en dólares. Por ejemplo, si el saldoDisponible de un usuario es 1000.5, la línea 32 imprime $1,000.50. La línea 35 inserta una línea en blanco de salida para separar la información del saldo de la salida subsiguiente (es decir, el menú principal repetido por la clase ATM después de ejecutar la SolicitudSaldo).

13.4.10 La clase Retiro La clase Retiro (figura 13.22) extiende a Transaccion y representa una transacción de retiro del ATM. Esta clase se expande a partir del código “estructural” para la misma, desarrollado en la figura 13.12. En el diagrama de clases de la figura 13.10 vimos que la clase Retiro tiene un atributo, monto, que la línea 6 implementa como campo int. La figura 13.9 modela las asociaciones entre la clase Retiro y las clases Teclado y DispensadorEfectivo, para las que las líneas 7 y 8 implementan los atributos de tipo referencia teclado y dispensadorEfectivo, respectivamente. La línea 11 de-clara una constante que corresponde a la opción de cancelación en el menú. Pronto veremos cómo es que la clase utiliza esta constante.

Fig. 13.21 � La clase SolicitudSaldo representa a una transacción de solicitud de saldo en el ATM (parte 2 de 2).

17 // obtiene referencias a la base de datos del banco y la pantalla

18 BaseDatosBanco baseDatosBanco = obtenerBaseDatosBanco();

19 Pantalla pantalla = obtenerPantalla();

20

21 // obtiene el saldo disponible para la cuenta implicada

22 double saldoDisponible =

23 baseDatosBanco.obtenerSaldoDisponible( obtenerNumeroCuenta() );

24

25 // obtiene el saldo total para la cuenta implicada

26 double saldoTotal =

27 baseDatosBanco.obtenerSaldoTotal( obtenerNumeroCuenta() );

28

29 // muestra la información del saldo en la pantalla

30 pantalla.mostrarLineaMensaje( “\nInformacion de saldo:” );

31 pantalla.mostrarMensaje( “ - Saldo disponible: ” );

32 pantalla.mostrarMontoDolares( saldoDisponible );

33 pantalla.mostrarMensaje( “\n - Saldo total: ” );

34 pantalla.mostrarMontoDolares( saldoTotal );

35 pantalla.mostrarLineaMensaje( “” );

36 } // fin del método ejecutar

37 } // fin de la clase SolicitudSaldo

540 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Fig. 13.22 � La clase Retiro representa una transacción de retiro en el ATM (parte 1 de 3).

1 // Retiro.java 2 // Representa una transacción de retiro en el ATM 3 4 public class Retiro extends Transaccion 5 { 6 private int monto; // monto a retirar 7 private Teclado teclado; // referencia al teclado 8 private DispensadorEfectivo dispensadorEfectivo; // referencia al dispensador de

efectivo

9 10 // constante que corresponde a la opción del menú a cancelar11 private final static int CANCELO = 6;12 13 // constructor de Retiro14 public Retiro( int numeroCuentaUsuario, Pantalla pantallaATM,15 BaseDatosBanco baseDatosBancoATM, Teclado tecladoATM,16 DispensadorEfectivo dispensadorEfectivoATM )17 {18 // inicializa las variables de la superclase19 super( numeroCuentaUsuario, pantallaATM, baseDatosBancoATM );20 21 // inicializa las referencias al teclado y al dispensador de efectivo22 teclado = tecladoATM;23 dispensadorEfectivo = dispensadorEfectivoATM;24 } // fin del constructor de Retiro25 26 // realiza la transacción27 @Override28 public void ejecutar()29 {30 boolean efectivoDispensado = false; // no se ha dispensado aún el efectivo31 double saldoDisponible; // monto disponible para retirar32 33 // obtiene referencias a la base de datos del banco y la pantalla34 BaseDatosBanco baseDatosBanco = obtenerBaseDatosBanco();35 Pantalla pantalla = obtenerPantalla();36 37 // itera hasta que se dispense el efectivo o que cancele el usuario38 do39 {40 // obtiene un monto de retiro elegido por el usuario41 monto = mostrarMenuDeMontos();42 43 // comprueba si el usuario eligió un monto de retiro o si canceló44 if ( monto != CANCELO )45 {46 // obtiene el saldo disponible de la cuenta implicada47 saldoDisponible =48 baseDatosBanco.obtenerSaldoDisponible( obtenerNumeroCuenta() );49 50 // comprueba si el usuario tiene suficiente dinero en la cuenta51 if ( monto <= saldoDisponible )52 {

13.4 Implementación del caso de estudio del ATM 541

Fig. 13.22 � La clase Retiro representa una transacción de retiro en el ATM (parte 2 de 3).

53 // comprueba si el dispensador de efectivo tiene suficiente dinero54 if ( dispensadorEfectivo.haySuficienteEfectivoDisponible( monto ) )55 {56 // actualiza la cuenta implicada para reflejar el saldo57 baseDatosBanco.cargar( obtenerNumeroCuenta(), monto );58 59 dispensadorEfectivo.dispensarEfectivo( monto ); // dispensar efectivo60 efectivoDispensado = true; // se dispensó el efectivo61 62 // instruye al usuario que tome efectivo63 pantalla.mostrarLineaMensaje( “\nSe dispenso su” +64 “ efectivo. Tomelo ahora.” );65 } // fin de if66 else // el dispensador no tiene suficiente efectivo67 pantalla.mostrarLineaMensaje(68 “\nNo hay suficiente efectivo disponible en el ATM.” +69 “\n\nSeleccione un monto menor.” );70 } // fin de if71 else // no hay suficiente dinero disponible en la cuenta del usuario72 {73 pantalla.mostrarLineaMensaje(74 “\nNo hay suficientes fondos en su cuenta.” +75 “\n\nSeleccione un monto menor.” );76 } // fin de else77 } // fin de if78 else // el usuario eligió la opción cancelar del menú79 {80 pantalla.mostrarLineaMensaje( “\nCancelando transaccion...” );81 return; // regresa al menú principal porque el usuario canceló82 } // fin de else83 } while ( !efectivoDispensado );84 85 } // fin del método ejecutar86 87 // muestra un menú de montos de retiro y la opción para cancelar;88 // devuelve el monto elegido o 0 si el usuario elije cancelar89 private int mostrarMenuDeMontos()90 {91 int opcionUsuario = 0; // variable local para almacenar el valor de retorno92 93 Pantalla pantalla = obtenerPantalla(); // obtiene referencia a la pantalla94 95 // arreglo de montos que corresponde a los números del menú96 int montos[] = { 0, 20, 40, 60, 100, 200 };97 98 // itera mientras no se haya elegido una opción válida99 while ( opcionUsuario == 0 )100 {101 // muestra el menú102 pantalla.mostrarLineaMensaje( “\nMenu de retiro:” );103 pantalla.mostrarLineaMensaje( “1 - $20” );104 pantalla.mostrarLineaMensaje( “2 - $40” );105 pantalla.mostrarLineaMensaje( “3 - $60” );

542 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

El constructor de la clase Retiro (líneas 14 a 24) tiene cinco parámetros. Usa super para pasar los parámetros numeroCuentaUsuario, pantallaATM y baseDatosBancoATM al constructor de la superclase Transaccion para establecer los atributos que Retiro hereda de Transaccion. El constructor también recibe las referencias tecladoATM y dispensadorEfectivoATM como parámetros, y las asigna a los atribu-tos de tipo referencia teclado y dispensadorEfectivo.

La clase Retiro sobrescribe el método ejecutar de Transaccion con una implementación concreta (líneas 27 a 85) que realiza los pasos de un retiro. La línea 30 declara e inicializa una variable boolean local llamada efectivoDispensado, la cual indica si se dispensó efectivo (es decir, si la transacción se completó con éxito), y en un principio es false. La línea 31 declara la variable double local llamada saldoDisponible, la cual almacenará el saldo disponible del usuario durante una transacción de retiro. Las líneas 34 y 35 obtienen referencias a la base de datos del banco y a la pantalla del ATM, invocando los métodos heredados de la superclase Transaccion.

Las líneas 38 a 83 contienen un ciclo do…while que ejecuta su cuerpo hasta dispensar efectivo (es decir, hasta que efectivoDispensado se vuelva true) o hasta que el usuario elija cancelar (en cuyo caso, el ciclo termina). Usamos este ciclo para regresar en forma continua al usuario al inicio de la transacción en caso de que ocurra un error (por ejemplo, si el monto de retiro solicitado es mayor que el saldo dispo-nible del usuario, o mayor que la cantidad de efectivo en el dispensador). La línea 41 muestra un menú de montos de retiro y obtiene una selección del usuario mediante una llamada al método private utilitario mostarMenuDeMontos (declarado en las líneas 89 a 133). Este método muestra el menú de montos y de-

106 pantalla.mostrarLineaMensaje( “4 - $100” );

107 pantalla.mostrarLineaMensaje( “5 - $200” );

108 pantalla.mostrarLineaMensaje( “6 - Cancelar transaccion” );

109 pantalla.mostrarMensaje( “\nSeleccione un monto a retirar: ” );

110 111 int entrada = teclado.obtenerEntrada(); // obtiene la entrada del usuario

mediante el teclado

112

113 // determina cómo proceder con base en el valor de la entrada

114 switch ( entrada )

115 {

116 case 1: // si el usuario eligió un monto de retiro

117 case 2: // (es decir, si eligió la opción 1, 2, 3, 4 o 5), devolver

118 case 3: // el monto correspondiente del arreglo montos

119 case 4:

120 case 5:

121 opcionUsuario = montos[ entrada ]; // guarda la elección del usuario

122 break;

123 case CANCELO: // el usuario eligió cancelar

124 opcionUsuario = CANCELO; // guarda la elección del usuario

125 break;

126 default: // el usuario no introdujo un valor del 1 al 6

127 pantalla.mostrarLineaMensaje(

128 “\nSeleccion invalida. Intente de nuevo.” );

129 } // fin de switch

130 } // fin de while

131

132 return opcionUsuario; // devuelve el monto de retiro o CANCELO

133 } // fin del método mostrarMenuDeMontos

134 } // fin de la clase Retiro

Fig. 13.22 � La clase Retiro representa una transacción de retiro en el ATM (parte 3 de 3).

13.4 Implementación del caso de estudio del ATM 543

vuelve un monto de retiro int, o la constante int CANCELO para indicar que el usuario optó por cancelar la transacción.

El método mostrarMenuDeMontos (líneas 89 a 133) declara primero la variable local opcion-Usuario (que en un principio vale 0) para almacenar el valor que devolverá el método (línea 91). La línea 93 obtiene una referencia a la pantalla mediante una llamada al método obtenerPantalla heredado de la superclase Transaccion. La línea 96 declara un arreglo entero de montos de retiro que corresponden a los montos mostrados en el menú de retiro. Ignoramos el primer elemento en el arreglo (índice 0), debido a que el menú no tiene opción 0. La instrucción while en las líneas 99 a 130 se repite hasta que opcionUsuario recibe un valor distinto de 0. En breve veremos que esto ocurre cuando el usuario realiza una selección válida del menú. Las líneas 102 a 109 muestran el menú de retiro en la pantalla y piden al usuario que introduzca una opción. La línea 111 obtiene el entero entrada por medio del teclado. La instrucción switch en las líneas 114 a 129 determina cómo pro-ceder con base en la entrada del usuario. Si éste selecciona un número entre 1 y 5, la línea 121 esta-blece opcionUsuario en el valor del elemento en montos, en el índice entrada. Por ejemplo, si el usuario introduce 3 para retirar $60, la línea 121 establece opcionUsuario en el valor de montos[3] (es decir, 60). La línea 122 termina la instrucción switch. La variable opcionUsuario ya no es igual a 0, por lo que la instrucción while en las líneas 99 a 130 termina y la línea 132 devuelve opcion-Usuario. Si el usuario selecciona la opción del menú para cancelar, se ejecutan las líneas 124 y 125, las cuales establecen opcionUsuario en CANCELO y hacen que el método devuelva este valor. Si el usuario no introduce una selección válida del menú, las líneas 127 y 128 muestran un mensaje de error y el usuario regresa al menú retiro.

La línea 44 en el método ejecutar determina si el usuario seleccionó un monto de retiro o eligió cancelar. Si el usuario cancela, se ejecutan las líneas 80 y 81 para mostrar un mensaje apropiado al usuario, antes de devolver el control al método que hizo la llamada (el método realizarTransac-ciones de ATM). Si el usuario selecciona un monto de retiro, las líneas 47 y 48 obtienen el saldo disponible de la Cuenta del usuario actual y lo almacenan en la variable saldoDisponible. Luego, la línea 51 determina si el monto seleccionado es menor o igual que el saldo disponible del usuario. Si no es así, las líneas 73 a 75 muestran un mensaje de error apropiado. Después el control continúa hasta el final del ciclo do…while, y éste se repite debido a que efectivoDispensado sigue siendo false. Si el saldo del usuario es suficientemente alto, la instrucción if en la línea 54 determina si el dispensador de efectivo tiene dinero suficiente para cumplir con la solicitud de retiro, invocando al método haySuficienteEfectivoDisponible de dispensadorEfectivo. Si este método devuelve false, las líneas 67 a 69 muestran un mensaje de error apropiado y se repite el ciclo do…while. Si hay suficiente efectivo disponible, entonces se cumplen los requisitos para el retiro y la línea 57 carga monto a la cuenta del usuario en la base de datos. Después, las líneas 59 y 60 instruyen al dis-pensador de efectivo para que dispense el efectivo al usuario y establecen efectivoDispensado en true. Por último, las líneas 63 y 64 muestran un mensaje al usuario para indicarle que se dispensó el efectivo. Puesto que ahora efectivoDispensado es true, el control continúa después del ciclo do…while. No aparecen instrucciones adicionales debajo del ciclo, por lo que el método regresa.

13.4.11 La clase Deposito La clase Deposito (figura 13.23) extiende a Transaccion y representa una transacción de depó-sito. En la figura 13.10 vimos que la clase Deposito tiene un atributo llamado monto, el cual se implementa en la línea 6 como un campo int. Las líneas 7 y 8 crean los atributos de referencia teclado y ranuraDeposito, que implementan las asociaciones entre la clase Deposito y las cla-ses Teclado y RanuraDeposito modeladas en la figura 13.9. La línea 9 declara una constante CANCELO, la cual corresponde al valor que introduce un usuario para cancelar. Pronto veremos cómo es que la clase usa esta constante.

544 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

Fig. 13.23 � La clase Deposito representa una transacción de depósito en el ATM (parte 1 de 2).

1 // Deposito.java 2 // Representa una transacción de depósito en el ATM 3 4 public class Deposito extends Transaccion 5 { 6 private double monto; // monto a depositar 7 private Teclado teclado; // referencia al teclado 8 private RanuraDeposito ranuraDeposito; // referencia a la ranura de depósito 9 private final static int CANCELO = 0; // constante para la opción de cancelar10 11 // constructor de Deposito12 public Deposito( int numeroCuentaUsuario, Pantalla pantallaATM,13 BaseDatosBanco baseDatosBancoATM, Teclado tecladoATM,14 RanuraDeposito ranuraDepositoATM )15 {16 // inicializa las variables de la superclase17 super( numeroCuentaUsuario, pantallaATM, baseDatosBancoATM );18 19 // inicializa las referencias al teclado y la ranura de depósito20 teclado = tecladoATM;21 ranuraDeposito = ranuraDepositoATM;22 } // fin del constructor de Deposito23 24 // realiza la transacción25 @Override26 public void ejecutar()27 {28 BaseDatosBanco baseDatosBanco = obtenerBaseDatosBanco(); // obtiene la

referencia

29 Pantalla pantalla = obtenerPantalla(); // obtiene la referencia30 31 monto = pedirMontoADepositar(); // obtiene el monto a depositar del usuario32 33 // comprueba si el usuario introdujo un monto a depositar o canceló34 if ( monto != CANCELO )35 {36 // solicita un sobre de depósito que contenga el monto especificado37 pantalla.mostrarMensaje(38 “\nInserte un sobre que contenga ” );39 pantalla.mostrarMontoDolares( monto );40 pantalla.mostrarLineaMensaje( “.” );41 42 // recibe el sobre de depósito43 boolean seRecibioSobre = ranuraDeposito.seRecibioSobre();44 45 // comprueba si se recibió el sobre de depósito46 if ( seRecibioSobre )47 {48 pantalla.mostrarLineaMensaje( “\nSe recibio su sobre de ” +49 “deposito.\nNOTA: El dinero que acaba de depositar no ” +50 “estara disponible sino hasta que verifiquemos el monto del ” +51 “efectivo y cualquier cheque incluido.” );52

13.4 Implementación del caso de estudio del ATM 545

Al igual que Retiro, la clase Deposito contiene un constructor (líneas 12 a 22) que pasa tres parámetros al constructor de la superclase Transaccion. El constructor también tiene los parámetros tecladoATM y ranuraDepositoATM, que asigna a los atributos correspondientes (líneas 20 y 21).

El método ejecutar (líneas 25 a 66) sobrescribe la versión abstract en la superclase Transaccion con una implementación completa que realiza los pasos requeridos en una transacción de depósito. Las líneas 28 y 29 obtienen referencias a la base de datos y la pantalla. La línea 31 pide al usuario que introduzca un monto de depósito, para lo cual invoca al método private utilitario pedirMontoA-Depositar (declarado en las líneas 69 a 85) y establece el atributo monto con el valor devuelto. El método pedirMontoADepositar pide al usuario que introduzca un monto de depósito como un número en-tero de centavos (debido a que el teclado del ATM no contiene un punto decimal; esto es consistente con muchos ATM reales) y devuelve el valor double que representa el monto en dólares a depositar.

La línea 71 en el método pedirMontoADepositar obtiene una referencia a la pantalla del ATM. Las líneas 74 y 75 muestran un mensaje que pide al usuario introducir un monto de depósito como un número de centavos, o “0” para cancelar la transacción. La línea 76 obtiene la entrada del usuario desde el teclado. Las líneas 79 a 84 determinan si el usuario introdujo un monto de depósito real o eligió cancelar. Si optó por esto último, la línea 80 devuelve la constante CANCELO. De lo contrario, la línea 83

53 // hace un abono a la cuenta para reflejar el depósito54 baseDatosBanco.abonar( obtenerNumeroCuenta(), monto );55 } // fin de if56 else // no se recibió el sobre de depósito57 {58 pantalla.mostrarLineaMensaje( "\nNo inserto un sobre de " +59 “deposito, por lo que el ATM ha cancelado su transaccion.” );60 } // fin de else61 } // fin de if62 else // el usuario canceló en vez de introducir el monto63 {64 pantalla.mostrarLineaMensaje( “\nCancelando transaccion...” );65 } // fin de else66 } // fin del método ejecutar67 68 // pide al usuario que introduzca un monto a depositar en centavos69 private double pedirMontoADepositar()70 {71 Pantalla pantalla = obtenerPantalla(); // obtiene referencia a la pantalla72 73 // muestra el indicador74 pantalla.mostrarMensaje( “\nIntroduzca un monto a depositar en ” +75 “CENTAVOS (o 0 para cancelar): ” );76 int entrada = teclado.obtenerEntrada(); // recibe la entrada del monto de

depósito77 78 // comprueba si el usuario canceló o introdujo un monto válido79 if ( entrada == CANCELO )80 return CANCELO;81 else82 {83 return ( double ) entrada / 100; // devuelve el monto en dólares84 } // fin de else85 } // fin del método pedirMontoADepositar86 } // fin de la clase Deposito

Fig. 13.23 � La clase Deposito representa una transacción de depósito en el ATM (parte 2 de 2).

546 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

devuelve el monto de depósito después de convertirlo del número de centavos a un monto en dólares, mediante la conversión de entrada a double, y después lo divide entre 100. Por ejemplo, si un usuario introduce 125 como el número de centavos, la línea 83 devuelve el resultado de dividir 125.0 entre 100, o 1.25; 125 centavos son $1.25.

Las líneas 34 a 65 en el método ejecutar determinan si el usuario eligió cancelar la transacción en vez de introducir un monto de depósito. Si el usuario cancela, la línea 64 muestra un mensaje apro-piado y el método regresa. Si el usuario introduce un monto de depósito, las líneas 37 a 40 instruyen al usuario para que inserte un sobre de depósito con el monto correcto. Recuerde que el método mostrar-MontoDolares de Pantalla imprime un valor double con formato de monto en dólares.

La línea 43 establece una variable boolean local en el valor devuelto por el método seRecibioSobre de ranuraDeposito, para indicar si se recibió un sobre de depósito. Recuerde que codificamos el método seRecibioSobre (líneas 8 a 11 de la figura 13.17) para que siempre devuelva true, debido a que esta-mos simulando la funcionalidad de la ranura de depósito y suponemos que el usuario siempre inserta un sobre. Sin embargo, codificamos el método ejecutar de la clase Deposito para que evalúe la posibilidad de que el usuario no inserte un sobre; la buena ingeniería de software exige que los programas tomen en cuenta todos los posibles valores de retorno. Por ende, la clase Deposito está preparada para futuras versiones de seRecibioSobre que pudieran devolver false. Las líneas 48 a 54 se ejecutan si la ranura de depósito recibe un sobre. Las líneas 48 a 51 muestran un mensaje apropiado al usuario. Después, la línea 54 abona el monto de depósito a la cuenta del usuario en la base de datos. Las líneas 58 y 59 se eje-cutarán si la ranura de depósito no recibe un sobre de depósito. En este caso, mostramos un mensaje al usuario para indicarle que el ATM canceló la transacción. A continuación, el método regresa sin mo-dificar la cuenta del usuario.

13.4.12 La clase CasoEstudioATM La clase CasoEstudioATM (figura 13.24) es una clase simple que nos permite iniciar, o “encender”, el ATM y evaluar la implementación de nuestro modelo del sistema ATM. El método main de la clase CasoEs-tudioATM (líneas 7 a 11) no hace nada más que instanciar un nuevo objeto ATM llamado elATM (línea 9), e invoca a su método run (línea 10) para iniciar el ATM.

13.5 ConclusiónEn este capítulo usó la herencia para optimizar el diseño del sistema de software del ATM, e imple-mentó por completo el ATM en Java. ¡Felicidades por completar todo el caso de estudio del ATM!

Fig. 13.24 � CasoEstudioATM.java inicia el ATM.

1 // CasoEstudioATM.java

2 // Programa controlador para el ejemplo práctico del ATM

3

4 public class CasoEstudioATM

5 {

6 // el método main crea y ejecuta el ATM

7 public static void main( String[] args )

8 {

9 ATM elATM = new ATM();

10 elATM.run();

11 } // fin de main

12 } // fin de la clase CasoEstudioATM

Respuestas a los ejercicios de autoevaluación 547

Esperamos que haya encontrado esta experiencia valiosa y que refuerce muchos de los conceptos de la programación orientada a objetos que aprendió. En el siguiente capítulo veremos las interfaces grá-ficas de usuario (GUI) con más detalle.

Respuestas a los ejercicios de autoevaluación13.1 Verdadero. El signo menos (–) indica visibilidad privada.

13.2 b.

13.3 El diseño para la clase Teclado produce el código de la figura 13.25. Recuerde que la clase Teclado no tiene atri-butos en estos momentos, pero pueden volverse aparentes a medida que continuemos con la implementación. Además, si fuéramos a diseñar un ATM real, el método obtenerEntrada tendría que interactuar con el hardware del teclado del ATM. En realidad recibiremos la entrada del teclado de una computadora personal, cuando escribamos el código de Java completo en la sección 13.4.

13.4 b.

13.5 Falso. UML requiere que se escriban los nombres de las clases abstractas y de los métodos abstractos en cursiva.

13.6 El diseño para la clase Transaccion produce el código de la figura 13.26. Los cuerpos del constructor de la clase y los métodos se completarán en la sección 13.4. Cuando estén implementados por completo, los métodos obtenerPantalla y obtenerBaseDatosBanco devolverán los atributos de referencias private de la superclase Transaccion, llamados pantalla y baseDatosBanco, respectivamente. Estos métodos permiten que las subclases de Transaccion accedan a la pantalla del ATM e interactúen con la base de datos del banco.

1 // la clase Teclado representa el teclado de un ATM

2 public class Teclado

3 {

4 // no se han especificado atributos todavía

5

6 // constructor sin argumentos

7 public Teclado()

8 {

9 } // fin del constructor de Teclado sin argumentos

10

11 // operaciones

12 public int obtenerEntrada()

13 {

14 } // fin del método obtenerEntrada

15 } // fin de la clase Teclado

Fig. 13.25 � Código de Java para la clase Teclado, con base en las figuras 13.1 y 13.2.

1 // La clase abstracta Transaccion representa una transacción con el ATM

2 public abstract class Transaccion

3 {

4 // atributos

5 private int numeroCuenta; // indica la cuenta involucrada

6 private Pantalla pantalla; // la pantalla del ATM

7 private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas

Fig. 13.26 � Código de Java para la clase Transaccion, con base en las figuras 13.9 y 13.10 (parte 1 de 2).

548 Capítulo 13 Caso de estudio del ATM, Parte 2: Implementación de un diseño orientado a objetos

8

9 // constructor sin argumentos invocado por las subclases, usando super()

10 public Transaccion()

11 {

12 } // fin del constructor de Transaccion sin argumentos

13

14 // devuelve el número de cuenta

15 public int obtenerNumeroCuenta()

16 {

17 } // fin del método obtenerNumeroCuenta

18

19 // devuelve referencia a la pantalla

20 public Pantalla obtenerPantalla()

21 {

22 } // fin del método obtenerPantalla

23

24 // devuelve referencia a la base de datos del banco

25 public BaseDatosBanco obtenerBaseDatosBanco()

26 {

27 } // fin del método obtenerBaseDatosBanco

28

29 // método abstracto sobrescrito por las subclases

30 public abstract void ejecutar();

31 } // fin de la clase Transaccion

Fig. 13.26 � Código de Java para la clase Transaccion, con base en las figuras 13.9 y 13.10 (parte 2 de 2).

Componentes de la GUI: Parte 1 14¿Crees que puedo escuchar todo el día esas cosas?—Lewis Carroll

Inclusive, hasta un evento menor en la vida de un niño es un evento del mundo de ese niño y, por ende, es un evento del mundo.—Gaston Bachelard

Tú pagas, por lo tanto, tú decides.—Punch

O b j e t i v o sEn este capítulo aprenderá a:

■ Usar la elegante apariencia visual multiplataforma de Java: Nimbus.

■ Crear interfaces gráficas de usuario y manejar los eventos generados por las interacciones de los usuarios con las GUI.

■ Comprender los paquetes que contienen componentes relacionados con las GUI, clases e interfaces manejadoras de eventos.

■ Crear y manipular botones, etiquetas, listas, campos de texto y paneles.

■ Entender el manejo de los eventos de ratón y los eventos de teclado.

■ Utilizar los administradores de esquemas para ordenar los componentes de las GUI.

550 Capítulo 14 Componentes de la GUI: Parte 1

14.1 Introducción

14.2 Nueva apariencia visual Nimbus de Java

14.3 Entrada/salida simple basada en GUI con JOptionPane

14.4 Generalidades de los componentes de Swing

14.5 Mostrar texto e imágenes en una ventana

14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas

14.7 Tipos de eventos comunes de la GUI e interfaces de escucha

14.8 Cómo funciona el manejo de eventos

14.9 JButton

14.10 Botones que mantienen el estado14.10.1 JCheckBox

14.10.2 JRadioButton

14.11 JComboBox: uso de una clase interna anónima para el manejo de eventos

14.12 JList

14.13 Listas de selección múltiple

14.14 Manejo de eventos de ratón

14.15 Clases adaptadoras

14.16 Subclase de JPanel para dibujar con el ratón

14.17 Manejo de eventos de teclas

14.18 Introducción a los administradores de esquemas

14.18.1 FlowLayout

14.18.2 BorderLayout14.18.2 GridLayout

14.19 Uso de paneles para administrar esquemas más complejos

14.20 JTextArea

14.21 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios | Marcar la diferencia

14.1 IntroducciónUna interfaz gráfica de usuario (GUI) presenta un mecanismo amigable al usuario para interactuar con una aplicación. Una GUI proporciona a una aplicación una “apariencia visual” única. Las GUI se construyen a partir de los componentes de GUI. Con frecuencia a éstos se les denomina controles o widgets: abreviación para gadgets de ventana. Un componente de GUI es un objeto con el que el usua-rio interactúa a través del ratón, del teclado u otra forma de entrada, como el reconocimiento de voz. En este capítulo y en el capítulo 25, Componentes de GUI: parte 2, aprenderá acerca de muchos de los llamados componentes de GUI de Swing del paquete javax.swing. Cubriremos otros componentes de GUI a medida que se requieran a lo largo del libro.

Observación de apariencia visual 14.1Al proporcionar distintas aplicaciones en las que los componentes de la interfaz de usuario sean consistentes e intuitivos, los usuarios pueden familiarizarse en cierto modo con una nueva aplicación, de manera que pueden aprender a utilizarla en menor tiempo y con mayor productividad.

Soporte de IDE para el diseño de GUIMuchos IDE cuentan con herramientas de diseño de GUI, con las que podemos especificar el tamaño exacto y la ubicación de un componente en forma visual, mediante el ratón. El IDE genera el código de GUI por usted. Aunque esto simplifica en gran medida la creación de GUI, cada IDE genera este código de manera distinta. Por esta razón, escribimos el código de GUI a mano.

14.2 Nueva apariencia visual Nimbus de Java 551

GUI de ejemplo: la aplicación de demostración SwingSet3Como ejemplo de una GUI, en la figura 14.1 se muestra la aplicación SwingSet3, que está disponible en download.java.net/javadesktop/swingset3/SwingSet3.jnlp. Esta aplicación es una manera conveniente de que usted explore los diversos componentes de GUI que ofrecen las API de la GUI de Swing de Java. Sólo haga clic en el nombre de un componente (como JFrame, JTabbedPane, etc.) en el área GUI Components de la parte izquierda de la pantalla y verá una demostración del componente de GUI en la sección derecha de la ventana. El código fuente de cada demostración se muestra en el área de texto de la parte inferior de la ventana. Hemos etiquetado algunos de los componentes de GUI en la aplicación. En la parte superior de la ventana hay una barra de título, la cual contiene el título de la ventana. Debajo de la barra de título hay una barra de menús que contiene menús (File y View). En la región superior derecha de la ventana hay un conjunto de botones; por lo general, los usuarios oprimen botones para realizar tareas. En el área GUI Components de la ventana, hay un cuadro combinado; el usuario puede hacer clic en la flecha hacia abajo del lado derecho del cuadro para seleccionar de entre una lista de elementos. Los menús, botones y el cuadro combinado son parte de la GUI de la aplica-ción y nos permiten interactuar con ella.

menú barra de título barra de menús cuadro combinado área de texto botón barra de desplazamiento

14.2 Nueva apariencia visual Nimbus de JavaEn la actualización 10 de Java SE 6, se introdujo la elegante apariencia multiplataforma de Java conoci-da como Nimbus. Para las capturas de pantalla de GUI como la figura 14.1, configuramos nuestros sistemas para usar Nimbus como la apariencia visual predeterminada. Hay tres formas en que podemos usar Nimbus:

Fig. 14.1 � La aplicación SwingSet3 demuestra muchos de los componentes de GUI Swing de Java.

552 Capítulo 14 Componentes de la GUI: Parte 1

1. Establecerla como predeterminada para todas las aplicaciones Java que se ejecuten en la computadora.

2. Establecerla como la apariencia visual al momento de iniciar una aplicación, pasando un argumento de línea de comandos al comando java.

3. Establecerla como la apariencia visual mediante programación en nuestra aplicación (vea la sección 25.6).

Para establecer a Nimbus como predeterminada para todas las aplicaciones Java, es necesario crear un archivo de texto llamado swing.properties en la carpeta lib de su carpeta de instalación del JDK y del JRE. Coloque la siguiente línea de código en el archivo:

swing.defaultlaf=com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel

Para obtener más información sobre cómo localizar estas carpetas de instalación (en inglés), visite

bit.ly/JavaInstallationInstructions

Además del JRE independiente, hay un JRE anidado en su carpeta de instalación del JDK. Si utiliza un IDE que dependa del JDK, tal vez también necesite colocar el archivo swing.properties en la carpeta lib de la carpeta jre anidada.

Si prefiere seleccionar Nimbus en cada aplicación individual, coloque el siguiente argumento de línea de comandos después del comando java y antes del nombre de la aplicación al momento de ejecutarla:

-Dswing.defaultlaf=com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel

14.3 Entrada/salida simple basada en GUI con JOptionPane Las aplicaciones en los capítulos 2 a 10 muestran texto en la ventana de comandos y obtienen la entrada de la ventana de comandos. La mayoría de las aplicaciones que usamos a diario utilizan ventanas o cua-dros de diálogo (también conocidos como diálogos) para interactuar con el usuario. Por ejemplo, los programas de correo electrónico le permiten escribir y leer mensajes en una ventana que proporciona el programa. Los cuadros de diálogo son ventanas en las cuales los programas muestran mensajes impor-tantes al usuario, u obtienen información de éste. La clase JOptionPane de Java (paquete javax.swing) proporciona cuadros de diálogo prefabricados para entrada y salida. Estos diálogos se muestran median-te la invocación de los métodos static de JOptionPane. La figura 14.2 presenta una aplicación simple de suma, que utiliza dos diálogos de entrada para obtener enteros del usuario, y un diálogo de mensaje para mostrar la suma de los enteros que introduce el usuario.

1 // Fig. 14.2: Suma.java

2 // Programa de suma que utiliza a JOptionPane para entrada y salida.

3 import javax.swing.JOptionPane; // el programa usa JOptionPane

4

5 public class Suma

6 {

7 public static void main( String[] args )

8 {

9 // obtiene la entrada del usuario de los diálogos de entrada de JOptionPane

10 String primerNumero =

11 JOptionPane.showInputDialog( “Introduzca el primer entero” );

12 String segundoNumero =

13 JOptionPane.showInputDialog( “Introduzca el segundo entero” );

Fig. 14.2 � Programa de suma que utiliza a JOptionPane para la entrada y la salida (parte 1 de 2).

14.3 Entrada/salida simple basada en GUI con JoptionPane 553

Fig. 14.2 � Programa de suma que utiliza a JOptionPane para la entrada y la salida (parte 2 de 2).

14

15 // convierte las entradas String en valores int para usarlos en un cálculo

16 int numero1 = Integer.parseInt( primerNumero );

17 int numero2 = Integer.parseInt( segundoNumero );

18

19 int suma = numero1 + numero2; // suma números

20

21 // muestra los resultados en un diálogo de mensajes de JOptionPane

22 JOptionPane.showMessageDialog( null, “La suma es ” + suma,

23 “Suma de dos enteros”, JOptionPane.PLAIN_MESSAGE );

24 } // fin del método main

25 } // fin de la clase Suma

Cuando el usuario hace clic en Aceptar, showInputDialog

devuelve al programa el 100 que escribió el usuario como un objeto

String. El programa debe convertir el String en un int

(a) Diálogo de entrada mostrado por las líneas 10 y 11Indicador para el usuario

(b) Diálogo de entrada mostrado por las líneas 12 y 13

Campo de texto en el que el usuario escribe un valor

(c) Diálogo de mensaje mostrado por las líneas 22 y 23

Cuando el usuario hace clic en Aceptar, el diálogo de mensaje se cierra (se quita de la pantalla)

Diálogos de entradaLa línea 3 importa la clase JOptionPane. Las líneas 10 y 11 declaran la variable String primerNumero, y le asignan el resultado de la llamada al método static showInputDialog de JOptionPane. Este método muestra un diálogo de entrada (vea la primera captura de pantalla en la figura 14.2), usando el argumen-to String (“Introduzca el primer entero”) como indicador.

Observación de apariencia visual 14.2El indicador en un diálogo de entrada utiliza comúnmente la capitalización estilo oración: un estilo que capitaliza sólo la primera letra de la primera palabra en el texto, a menos que la palabra sea un nombre propio (por ejemplo, Jones).

El usuario escribe caracteres en el campo de texto, y después hace clic en el botón Aceptar u oprime la tecla Intro para enviar el objeto String al programa. Al hacer clic en Aceptar también se cierra (oculta) el diálogo. [Nota: si escribe en el campo de texto y no aparece nada, actívelo haciendo clic sobre él con el ratón.] A diferencia de Scanner, que puede utilizarse para que el usuario introduzca valores de varios tipos mediante el teclado, un diálogo de entrada sólo puede introducir objetos String. Esto es común en la

554 Capítulo 14 Componentes de la GUI: Parte 1

mayoría de los componentes de la GUI. El usuario puede escribir cualquier carácter en el campo de texto del diálogo de entrada. Nuestro programa asume que el usuario introduce un valor entero válido. Si el usuario hace clic en el botón Cancelar, showInputDialog devuelve null. Si el usuario escribe un valor no entero o si hace clic en el botón Cancelar en el diálogo de entrada, ocurrirá una excepción y el programa no operará en forma correcta. En el capítulo 11 vimos cómo manejar dichos errores. Las líneas 12 y 13 muestran otro diálogo de entrada que pide al usuario que introduzca el segundo entero. Cada diálogo JOptionPane que usted muestre en pantalla se conoce como diálogo modal: mientras que el diálogo esté en la pantalla, el usuario no podrá interactuar con el resto de la aplicación.

Observación de apariencia visual 14.3No haga uso excesivo de los diálogos modales, ya que pueden reducir la capacidad de uso de sus aplicaciones. Use un diálogo modal sólo cuando sea necesario evitar que los usuarios interactúen con el resto de una aplicación hasta que cierren el diálogo.

Convertir objetos String en valores intPara realizar el cálculo, debemos convertir los objetos String que el usuario introdujo, en valores int. Recuerde que el método static parseInt de la clase Integer convierte su argumento String en un valor int. Las líneas 16 y 17 asignan los valores convertidos a las variables locales numero1 y numero2. Después, la línea 19 suma estos valores.

Diálogos de mensajeLas líneas 22 y 23 usan el método static showMessageDialog de JOptionPane para mostrar un diálogo de mensaje (la última captura de pantalla de la figura 14.2) que contiene la suma. El primer argumento ayuda a la aplicación de Java a determinar en dónde debe colocar el cuadro de diálogo. Por lo general, un diálogo se muestra desde una aplicación GUI con su propia ventana. El primer argumento se refiere a esa ventana (la cual se denomina ventana padre) y hace que el diálogo aparezca centrado sobre el padre (como veremos en la sección 14.9). Si el primer argumento es null, el cuadro de diálogo se muestra en la parte central de la pantalla. El segundo argumento es el mensaje a mostrar; en este caso, el resultado de conca-tenar el objeto String “La suma es ” y el valor de suma. El tercer argumento (“Suma de dos enteros”) representa el objeto String que debe aparecer en la barra de título del diálogo, en la parte superior. El cuarto argumento (JOptionPane.PLAIN_MESSAGE) es el tipo de diálogo de mensaje a mostrar. Un diálogo PLAIN_MESSAGE no muestra un icono a la izquierda del mensaje. La clase JOptionPane proporciona varias versiones sobrecargadas de los métodos showInputDialog y showMessageDialog, así como métodos que muestran otros tipos de diálogos. Para obtener información completa acerca de la clase JOptionPane, visite el sitio download.oracle.com/javase/6/docs/api/javax/swing/JOptionPane.html.

Observación de apariencia visual 14.4Por lo general, la barra de título de una ventana utiliza capitalización de título de libro: un estilo que capitaliza la primera letra de cada palabra significativa en el texto, y no termina con ningún signo de puntuación (por ejemplo, Capitalización en el Título de un Libro).

Constantes de diálogos de mensajes de JOptionPaneLas constantes que representan los tipos de diálogos de mensajes se muestran en la figura 14.3. Todos los tipos de diálogos de mensaje, excepto PLAIN_MESSAGE, muestran un icono a la izquierda del mensaje. Estos iconos proporcionan una indicación visual de la importancia del mensaje para el usuario. Observe que un icono QUESTION_MESSAGE es el icono predeterminado para un cuadro de diálogo de entrada (vea la figura 14.2).

14.4 Generalidades de los componentes de Swing 555

14.4 Generalidades de los componentes de SwingAunque es posible realizar operaciones de entrada y salida utilizando los diálogos de JOptionPane, la mayoría de las aplicaciones de GUI requieren interfaces de usuario más elaboradas. El resto de este capítulo habla acerca de muchos componentes de la GUI que permiten a los desarrolladores de aplica-ciones crear GUI robustas. La figura 14.4 lista varios componentes básicos de la GUI de Swing que vamos a analizar.

Tipo de diálogo de mensaje Icono Descripción

ERROR_MESSAGE Indica un error.

INFORMATION_MESSAGE Indica un mensaje informativo.

WARNING_MESSAGE Advierte al usuario sobre un problema potencial.

QUESTION_MESSAGE Hace una pregunta al usuario. Por lo general, este diálogo requiere una respuesta, como hacer clic en un botón Sí o No.

PLAIN_MESSAGE sin icono

Un diálogo que contiene un mensaje, pero no un icono.

Fig. 14.3 � Constantes static de JOptionPane para diálogos de mensaje.

Componente Descripción

JLabel Muestra texto y/o iconos que no pueden editarse.

JTextField Por lo general recibe entrada del usuario.

JButton Activa un evento cuando se oprime mediante el ratón.

JCheckBox Especifi ca una opción que puede seleccionarse o no seleccionarse.

JComboBox Una lista desplegable de elementos, a partir de los cuales el usuario puede realizar una selección.

JList Una lista de elementos a partir de los cuales el usuario puede realizar una selección, haciendo clic en cualquiera de ellos. Pueden seleccionarse varios elementos.

JPanel Un área en la que pueden colocarse y organizarse los componentes.

Fig. 14.4 � Algunos componentes básicos de GUI.

Comparación entre Swing y AWTEn realidad hay dos conjuntos de componentes de GUI en Java. En los primeros días de Java, las GUI se creaban a partir de componentes del Abstract Window Toolkit (AWT) en el paquete java.awt. Éstos se ven como los componentes de GUI nativos de la plataforma en la que se ejecuta un programa de Java. Por ejemplo, objeto de tipo Button que se muestra en un programa de Java ejecutándose en Microsoft Windows tendrá la misma apariencia que los botones en las demás aplicaciones Windows. En el sistema operativo Apple Mac OS X, el objeto Button tendrá la misma apariencia visual que los botones en las demás aplicaciones Mac. Algunas veces, incluso la forma en la que un usuario puede interactuar con un componente específico del AWT difiere entre una plataforma y otra. A la apariencia y la forma en la que interactúa el usuario con la aplicación se conoce como su apariencia visual.

556 Capítulo 14 Componentes de la GUI: Parte 1

Observación de apariencia visual 14.5Los componentes de la GUI de Swing nos permiten especificar una apariencia visual uniforme para nuestras aplicaciones en todas las plataformas, o usar la apariencia vi-sual personalizada de cada plataforma. Una aplicación puede incluso cambiar la apa-riencia visual durante la ejecución, para permitir a los usuarios elegir su propia apariencia visual preferida.

Comparación entre componentes de GUI ligeros y pesadosLa mayoría de los componentes de Swing son componentes ligeros: se escriben, manipulan y visua-lizan por completo en Java. Los componentes de AWT son componentes pesados, ya que dependen del sistema de ventanas de la plataforma local para determinar su funcionalidad y su apariencia visual. Varios componentes de Swing son pesados.

Superclases de los componentes de GUI ligeros de SwingEl diagrama de clases de UML de la figura 14.5 muestra una jerarquía de herencia de clases, a partir de las cuales los componentes ligeros de Swing heredan sus atributos y comportamientos comunes.

Fig. 14.5 � Superclases comunes de los componentes ligeros de Swing.

Object

Component

Container

JComponent

La clase Component (paquete java.awt) es una superclase que declara las características comunes de los componentes de GUI en los paquetes java.awt y javax.swing. Cualquier objeto que sea un Container (paquete java.awt) se puede utilizar para organizar a otros objetos Component, adjuntando esos objetos Component al objeto Container. Los objetos Container se pueden colocar en otros objetos Container para organizar una GUI.

La clase JComponent (paquete javax.swing) es una subclase de Container. JComponent es la super-clase de todos los componentes ligeros de Swing, y declara los atributos y comportamientos comunes. Debido a que JComponent es una subclase de Container, todos los componentes ligeros de Swing son también objetos Container. Algunas de las características comunes que soporta JComponent son:

1. Una apariencia visual adaptable, la cual puede utilizarse para personalizar la apariencia de los componentes (por ejemplo, para usarlos en plataformas específicas). En la sección 25.6 veremos un ejemplo de esto.

2. Teclas de método abreviado (llamadas nemónicos) para un acceso directo a los componentes de la GUI por medio del teclado. En la sección 25.4 veremos un ejemplo de esto.

3. Breves descripciones del propósito de un componente de la GUI (lo que se conoce como cuadros de información sobre herramientas o tool tips) que se muestran cuando el cursor del ratón se coloca sobre el componente durante un breve periodo. En la siguiente sección veremos un ejemplo de esto.

4. Soporte para tecnologías de ayuda, como lectores de pantalla Braille para las personas con im-pedimentos visuales.

5. Soporte para la localización de la interfaz de usuario; es decir, personalizar la interfaz de usuario para mostrarla en distintos lenguajes y utilizar las convenciones de la cultura local.

14.5 Mostrar texto e imágenes en una ventana 557

14.5 Mostrar texto e imágenes en una ventanaNuestro siguiente ejemplo introduce un marco de trabajo para crear aplicaciones de GUI. Este marco de trabajo utiliza varios conceptos que aparecerán en muchas de nuestras aplicaciones de GUI. Éste es nuestro primer ejemplo en el que la aplicación aparece en su propia ventana. La mayoría de las ventanas que creará y que pueden contener componentes de GUI de Swing son una instancia de la clase JFrame o una subclase de JFrame. JFrame es una subclase indirecta de la clase java.awt.Window que proporcio-na los atributos y comportamientos básicos de una ventana: una barra de título en la parte superior, y botones para minimizar, maximizar y cerrar la ventana. Como la GUI de una aplicación por lo general es específica para esa aplicación, la mayoría de nuestros ejemplos consistirán en dos clases: una subclase de JFrame que nos ayuda a demostrar los nuevos conceptos de la GUI y una clase de aplicación, en la que main crea y muestra la ventana principal de la aplicación.

Etiquetado de componentes de la GUIUna GUI típica consiste en muchos componentes. Con frecuencia, los diseñadores de GUI proporcio-nan texto que indique el propósito de cada componente. Dicho texto se conoce como etiqueta y se crea con la clase JLabel: una subclase de JComponent. Un objeto JLabel muestra texto de sólo lectura, una imagen, o texto y una imagen. Raras veces las aplicaciones modifican el contenido de una etiqueta, des-pués de crearla.

Observación de apariencia visual 14.6Por lo general, el texto en un objeto JLabel utiliza la capitalización estilo oración.

La aplicación de las figuras 14.6 y 14.7 demuestra varias características de JLabel y presenta el marco de trabajo que utilizamos en la mayoría de nuestros ejemplos de GUI. No resaltamos el código en este ejemplo, ya que casi todo es nuevo. [Nota: hay muchas más características para cada componente de GUI de las que podemos cubrir en nuestros ejemplos. Para conocer todos los detalles acerca de cada componente de la GUI, visite su página en la documentación en línea. Para la clase JLabel, visite download.oracle.com/javase/6/docs/api/javax/swing/JLabel.html.]

Fig. 14.6 � Objetos JLabel con texto e iconos (parte 1 de 2).

1 // Fig. 14.6: LabelFrame.java

2 // Demostración de la clase JLabel.

3 import java.awt.FlowLayout; // especifica cómo se van a ordenar los componentes

4 import javax.swing.JFrame; // proporciona las características básicas de una ventana

5 import javax.swing.JLabel; // muestra texto e imágenes

6 import javax.swing.SwingConstants; // constantes comunes utilizadas con Swing

7 import javax.swing.Icon; // interfaz utilizada para manipular imágenes

8 import javax.swing.ImageIcon; // carga las imágenes

9

10 public class LabelFrame extends JFrame

11 {

12 private JLabel etiqueta1; // JLabel sólo con texto

13 private JLabel etiqueta2; // JLabel construida con texto y un icono

14 private JLabel etiqueta3; // JLabel con texto adicional e icono

15

16 // El constructor de LabelFrame agrega objetos JLabel a JFrame

17 public LabelFrame()

18 {

558 Capítulo 14 Componentes de la GUI: Parte 1

Fig. 14.6 � Objetos JLabel con texto e iconos (parte 2 de 2).

Fig. 14.7 � Clase de prueba para LabelFrame.

19 super( “Prueba de JLabel” );

20 setLayout( new FlowLayout() ); // establece el esquema del marco

21

22 // Constructor de JLabel con un argumento String

23 etiqueta1 = new JLabel( “Etiqueta con texto” );

24 etiqueta1.setToolTipText( “Esta es etiqueta1” );

25 add( etiqueta1 ); // agrega etiqueta1 a JFrame

26

27 // Constructor de JLabel con argumentos de cadena, Icono y alineación

28 Icon insecto = new ImageIcon( getClass().getResource( “insecto1.png” ) );

29 etiqueta2 = new JLabel( “Etiqueta con texto e icono”, insecto,

30 SwingConstants.LEFT );

31 etiqueta2.setToolTipText( “Esta es etiqueta2” );

32 add( etiqueta2 ); // agrega etiqueta2 a JFrame

33

34 etiqueta3 = new JLabel(); // Constructor de JLabel sin argumentos

35 etiqueta3.setText( “Etiqueta con icono y texto en la parte inferior” );

36 etiqueta3.setIcon( insecto ); // agrega icono a JLabel

37 etiqueta3.setHorizontalTextPosition( SwingConstants.CENTER );

38 etiqueta3.setVerticalTextPosition( SwingConstants.BOTTOM );

39 etiqueta3.setToolTipText( “Esta es etiqueta3” );

40 add( etiqueta3 ); // agrega etiqueta3 a JFrame

41 } // fin del constructor de LabelFrame

42 } // fin de la clase LabelFrame

1 // Fig. 14.7: PruebaLabel.java

2 // Prueba de LabelFrame.

3 import javax.swing.JFrame;

4

5 public class PruebaLabel

6 {

7 public static void main( String[] args )

8 {

9 LabelFrame marcoEtiqueta = new LabelFrame(); // crea objeto LabelFrame

10 marcoEtiqueta.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoEtiqueta.setSize( 275, 180 ); // establece el tamaño del marco

12 marcoEtiqueta.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase PruebaLabel

14.5 Mostrar texto e imágenes en una ventana 559

La clase LabelFrame (figura 14.6) es una subclase de JFrame. Utilizaremos una instancia de la clase LabelFrame para mostrar una ventana que contiene tres objetos JLabel. Las líneas 3 a 8 importan las clases utilizadas en la clase LabelFrame. La clase extiende a JFrame para heredar las características de una ventana. Las líneas 12 a 14 declaran las tres variables de instancia JLabel, cada una de las cuales se instan-cia en el constructor de JLabelFrame (líneas 17 a 41). Por lo general, el constructor de la subclase de JFrame crea la GUI que se muestra en la ventana, cuando se ejecuta la aplicación. La línea 19 invoca al constructor de la superclase JFrame con el argumento “Prueba de JLabel”. El constructor de JFrame utiliza este objeto String como el texto en la barra de título de la ventana.

Especificación del esquemaAl crear una GUI, cada componente de ésta debe adjuntarse a un contenedor, como una ventana creada con un objeto JFrame. Además, por lo general debemos decidir en dónde colocar cada componente de la GUI; esto se conoce como especificar el esquema. Como aprenderá al final de este capítulo y en el capí-tulo 25, Java cuenta con varios administradores de esquemas que pueden ayudarle a colocar los com-ponentes.

Muchos entornos de desarrollo integrados (IDE) proporcionan herramientas de diseño de GUI, en las cuales podemos especificar el tamaño y la ubicación exactos de un componente en forma visual utili-zando el ratón; después el IDE genera el código de la GUI por nosotros. Dichos IDE pueden simplificar en forma considerable la creación de GUI.

Para asegurar que nuestras GUI puedan utilizarse con cualquier IDE, no utilizamos un IDE para crear el código de GUI. Usamos administradores de esquemas de Java para ajustar el tamaño de los com-ponentes y posicionarlos. En el administrador de esquemas FlowLayout, los componentes de la GUI se colocan en un contenedor de izquierda a derecha, en el orden en el que el programa los une al contenedor. Cuando no hay más espacio para acomodar los componentes en la línea actual, se siguen mostrando de izquierda a derecha en la siguiente línea. Si se cambia el tamaño del contenedor, un esquema FlowLayout reordena los componentes, posiblemente con menos o más filas con base en la nueva anchura del conte-nedor. Cada contenedor tiene un esquema predeterminado, el cual vamos a cambiar para LabelFrame a FlowLayout (línea 20). El método setLayout se hereda en la clase LabelFrame, indirectamente de la clase Container. El argumento para el método debe ser un objeto de una clase que implemente la interfaz LayoutManager (es decir, FlowLayout). La línea 20 crea un nuevo objeto FlowLayout y pasa su referencia como argumento para setLayout.

Cómo crear y adjuntar etiqueta1Ahora que hemos especificado el esquema de la ventana, podemos empezar a crear y adjuntar componen-tes de la GUI en la ventana. La línea 23 crea un objeto JLabel y pasa “Etiqueta con texto” al constructor. El objeto JLabel muestra este texto en la pantalla como parte de la GUI de la aplicación. La línea 24 utiliza el método setToolTipText (heredado por JLabel de JComponent) para especificar la información sobre herramientas que se muestra cuando el usuario coloca el cursor del ratón sobre el objeto JLabel en la GUI. En la segunda captura de pantalla de la figura 14.7 puede ver un cuadro de información sobre herramientas de ejemplo. Cuando ejecute esta aplicación, trate de colocar el ratón sobre cada objeto JLabel para ver su información sobre herramientas. La línea 25 adjunta etiqueta1 al objeto LabelFrame, para lo cual pasa etiqueta1 al método add, que se hereda indirectamente de la clase Container.

Error común de programación 14.1Si no agrega explícitamente un componente de GUI a un contenedor, el componente no se mostrará cuando aparezca el contenedor en la pantalla.

Observación de apariencia visual 14.7Use cuadros de información sobre herramientas para agregar texto descriptivo a sus componentes de GUI. Este texto ayuda al usuario a determinar el propósito del componente de GUI en la interfaz de usuario.

560 Capítulo 14 Componentes de la GUI: Parte 1

La interfaz Icon y la clase ImageIconLos iconos son una forma popular de mejorar la apariencia visual de una aplicación, y también se utilizan comúnmente para indicar funcionalidad. Por ejemplo, se utiliza el mismo icono para reproducir la ma-yoría de los medios de la actualidad en dispositivos como reproductores de DVD y MP3. Varios compo-nentes de Swing pueden mostrar imágenes. Por lo general, un icono se especifica con un argumento Icon para un constructor o para el método setIcon del componente. Un Icon es un objeto de cualquier clase que implemente a la interfaz Icon (paquete javax.swing). La clase ImageIcon soporta varios formatos de imágenes, incluyendo: Formato de intercambio de gráficos (GIF), Gráficos portables de red (PNG) y Grupo de expertos unidos en fotografía (JPEG).

La línea 28 declara un objeto ImageIcon. El archivo insecto1.png contiene la imagen a cargar y almacenar en el objeto ImageIcon. Esta imagen se incluye en el directorio para este ejemplo. El objeto ImageIcon se asigna a la referencia Icon llamada insecto.

Cómo cargar un recurso de imagenEn la línea 28, la expresión getClass().getResource(“insecto1.png”) invoca al método getClass (heredado de la clase Object) para obtener una referencia al objeto Class que representa la declaración de la clase LabelFrame. Después, esa referencia se utiliza para invocar al método getResource de Class, el cual devuelve la ubicación de la imagen como un URL. El constructor de ImageIcon utiliza el URL para localizar la imagen, y después la carga en la memoria. Como vimos en el capítulo 1, la JVM carga las declaraciones de las clases en la memoria, usando un cargador de clases. El cargador de clases sabe en dónde se encuentra localizada en el disco cada clase que carga. El método getResource utiliza el cargador de clases del objeto Class para determinar la ubicación de un recurso, como un archivo de imagen. En este ejemplo, el archivo de imagen se almacena en la misma ubicación que el archivo LabelFrame.class. Las técnicas aquí descritas permiten que una aplicación cargue archivos de imagen de ubicaciones que son relativas a la ubicación del archivo de clase.

Cómo crear y adjuntar etiqueta2Las líneas 29 y 30 utilizan otro constructor de JLabel para crear un objeto JLabel que muestre el texto “Etiqueta con texto e icono” y el objeto Icon llamado insecto que se creó en la línea 28. El último argumento del constructor indica que el contenido de la etiqueta está justificado a la izquierda, o alinea-do a la izquierda (es decir, el icono y el texto se encuentran en el lado izquierdo del área de la etiqueta en la pantalla). La interfaz SwingConstants (paquete javax.swing) declara un conjunto de constantes enteras comunes (como SwingConstants.LEFT) que se utilizan con muchos componentes de Swing. De manera predeterminada, el texto aparece a la derecha de una imagen cuando una etiqueta contiene tanto texto como una imagen. Las alineaciones horizontal y vertical de un objeto JLabel se pueden es-tablecer mediante los métodos setHorizontalAlignment y setVerticalAlignment, respectivamente. La línea 31 especifica el texto de información sobre herramientas para etiqueta2, y la línea 32 agrega etiqueta2 al objeto JFrame.

Cómo crear y adjuntar etiqueta3La clase JLabel cuenta con métodos para modificar la apariencia de una etiqueta, una vez que se crea una instancia de ésta. La línea 34 crea un objeto JLabel vacío mediante el constructor sin argumentos. La línea 35 utiliza el método setText de JLabel para establecer el texto mostrado en la etiqueta. El método correspondiente getText se puede usar para obtener el texto actual mostrado en una etiqueta. La línea 36 utiliza el método setIcon de JLabel para especificar el objeto Icon a mostrar en la etiqueta. El método getIcon se puede usar para obtener el objeto Icon actual mostrado en una etiqueta. Las líneas 37 y 38 utilizan los métodos setHorizontalTextPosition y setVerticalTextPosition de JLabel para especificar la siguiente posición del texto en la etiqueta. En este caso, el texto se centrará en forma horizontal y aparecerá en la parte inferior de la etiqueta. Por ende, el objeto Icon aparecerá por encima

14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas 561

del texto. Las constantes de posición horizontal en SwingConstants son LEFT, CENTER y RIGHT (figura 14.8). Las constantes de posición vertical en SwingConstants son TOP, CENTER y BOTTOM (figura 14.8). La línea 39 establece el texto de información sobre herramientas para etiqueta3. La línea 40 agrega etiqueta3 al objeto JFrame.

Cómo crear y mostrar una ventana LabelFrameLa clase PruebaLabel (figura 14.7) crea un objeto de la clase LabelFrame (línea 9) y después especi-fica la operación de cierre predeterminada para la ventana. De manera predeterminada, al cerrar una ventana ésta simplemente se oculta. Sin embargo, cuando el usuario cierre la ventana LabelFrame, nos gustaría que la aplicación terminara. La línea 10 invoca al método setDefaultCloseOperation de LabelFrame (heredado de la clase JFrame) con la constante JFrame.EXIT_ON_CLOSE como el argu-mento para indicar que el programa debe terminar cuando el usuario cierre la ventana. Esta línea es importante. Sin ella, la aplicación no terminará cuando el usuario cierre la ventana. A continuación, la línea 11 invoca el método setSize de LabelFrame para especificar la anchura y la altura de la ven-tana en píxeles. Por último, la línea 12 invoca al método setVisible de LabelFrame con el argumen-to true, para mostrar la ventana en la pantalla. Pruebe a cambiar el tamaño de la ventana, para ver cómo el esquema FlowLayout cambia las posiciones de los objetos JLabel, a medida que cambia la anchura de la ventana.

14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas

Por lo general, un usuario interactúa con la GUI de una aplicación para indicar las tareas que ésta debe realizar. Por ejemplo, cuando usted escribe un mensaje en una aplicación de correo electrónico, al hacer clic en el botón Enviar le indica a la aplicación que envíe el correo electrónico a las direcciones especi-ficadas. Las GUI son controladas por eventos. Cuando el usuario interactúa con un componente de la GUI, la interacción (conocida como un evento) controla el programa para que realice una tarea. Algunas interacciones comunes del usuario que podrían hacer que una aplicación realizara una tarea incluyen el hacer clic en un botón, escribir en un campo de texto, seleccionar un elemento de un menú, cerrar una ventana y mover el ratón. El código que realiza una tarea en respuesta a un evento se llama manejador de eventos y al proceso en general de responder a los eventos se le conoce como manejo de eventos.

Vamos a considerar otros dos componentes de GUI que pueden generar eventos: JTextField y JPasswordField (paquete javax.swing).La clase JTextField extiende a la clase JTextComponent (paquete javax.swing.text), que proporciona muchas características comunes para los componen-tes de Swing basados en texto. La clase JPasswordField extiende a JTextField y agrega varios méto-dos específicos para el procesamiento de contraseñas. Cada uno de estos componentes es un área de una sola línea, en la cual el usuario puede introducir texto mediante el teclado. Las aplicaciones también pueden mostrar texto en un objeto JTextField (vea la salida de la figura 14.10). Un objeto JPasswordField muestra que se están escribiendo caracteres a medida que el usuario los introduce,

Constante Descripción Constante Descripción

Constantes de posición horizontal Constantes de posición vertical

LEFT Coloca el texto a la izquierda TOP Coloca el texto en la parte superior.

CENTER Coloca el texto en el centro. CENTER Coloca el texto en el centro.

RIGHT Coloca el texto a la derecha. BOTTOM Coloca el texto en la parte inferior.

Fig. 14.8 � Constantes de posicionamiento (miembros static de la interfaz SwingConstants).

562 Capítulo 14 Componentes de la GUI: Parte 1

pero oculta los caracteres reales con un carácter de eco, asumiendo que representan una contraseña que sólo el usuario debe conocer.

Cuando el usuario escribe datos en un objeto JTextField o JPasswordField y después oprime Intro, ocurre un evento. Nuestro siguiente ejemplo demuestra cómo un programa puede realizar una tarea en respuesta a ese evento. Las técnicas que se muestran aquí se pueden aplicar a todos los compo-nentes de GUI que generen eventos.

La aplicación de las figuras 14.9 y 14.10 utiliza las clases JTextField y JPasswordField para crear y manipular cuatro campos de texto. Cuando el usuario escribe en uno de los campos de texto y después oprime Intro, la aplicación muestra un cuadro de diálogo de mensaje que contiene el texto que escribió el usuario. Sólo podemos escribir en el campo de texto que esté “enfocado”. Un componente recibe el enfoque cuando el usuario hace clic en ese componente. Esto es importante, ya que el campo de texto con el enfoque es el que genera un evento cuando el usuario oprime Intro. En este ejemplo, cuando el usuario oprime Intro en el objeto JPasswordField, se revela la contraseña. Empezaremos por hablar sobre la preparación de la GUI, y después sobre el código para manejar eventos.

Las líneas 3 a 9 importan las clases e interfaces que utilizamos en este ejemplo. La clase CampoTex-toMarco extiende a JFrame y declara tres variables JTextField y una variable JPasswordField (líneas 13 a 16). Cada uno de los correspondientes campos de texto se instancia y se adjunta al objeto Campo-TextoMarco en el constructor (líneas 19 a 47).

Fig. 14.9 � Objetos JTextField y JPasswordField (parte 1 de 2).

1 // Fig. 14.9: CampoTextoMarco.java

2 // Demostración de la clase JTextField.

3 import java.awt.FlowLayout;

4 import java.awt.event.ActionListener;

5 import java.awt.event.ActionEvent;

6 import javax.swing.JFrame;

7 import javax.swing.JTextField;

8 import javax.swing.JPasswordField;

9 import javax.swing.JOptionPane;

10

11 public class CampoTextoMarco extends JFrame

12 {

13 private JTextField campoTexto1; // campo de texto con tamaño fijo

14 private JTextField campoTexto2; // campo de texto construido con texto

15 private JTextField campoTexto3; // campo de texto con texto y tamaño

16 private JPasswordField campoContrasenia; // campo de contraseña con texto

17

18 // El constructor de CampoTextoMarco agrega objetos JTextField a JFrame

19 public CampoTextoMarco()

20 {

21 super( “Prueba de JTextField y JPasswordField” );

22 setLayout( new FlowLayout() ); // establece el esquema del marco

23

24 // construye campo de texto con 10 columnas

25 campoTexto1 = new JTextField( 10 );

26 add( campoTexto1 ); // agrega campoTexto1 a JFrame

27

28 // construye campo de texto con texto predeterminado

29 campoTexto2 = new JTextField( “Escriba el texto aqui” );

30 add( campoTexto2 ); // agrega campoTexto2 a JFrame

14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas 563

Fig. 14.9 � Objetos JTextField y JPasswordField (parte 2 de 2).

31 32 // construye campo de texto con texto predeterminado y 21 columnas

33 campoTexto3 = new JTextField( “Campo de texto no editable”, 21 );

34 campoTexto3.setEditable( false ); // deshabilita la edición

35 add( campoTexto3 ); // agrega campoTexto3 a JFrame

36

37 // construye campo de contraseña con texto predeterminado

38 campoContrasenia = new JPasswordField( “Texto oculto” );

39 add( campoContrasenia ); // agrega campoContrasenia a JFrame

40

41 // registra los manejadores de eventos

42 ManejadorCampoTexto manejador = new ManejadorCampoTexto();

43 campoTexto1.addActionListener( manejador );

44 campoTexto2.addActionListener( manejador );

45 campoTexto3.addActionListener( manejador );

46 campoContrasenia.addActionListener( manejador );

47 } // fin del constructor de CampoTextoMarco

48

49 // clase interna privada para el manejo de eventos

50 private class ManejadorCampoTexto implements ActionListener

51 {

52 // procesa los eventos de campo de texto

53 public void actionPerformed( ActionEvent evento )

54 {

55 String cadena = “”; // declara la cadena a mostrar

56

57 // el usuario oprimió Intro en el objeto JTextField campoTexto1

58 if ( evento.getSource() == campoTexto1 )

59 cadena = String.format( “campoTexto1: %s”,

60 evento.getActionCommand() );

61

62 // el usuario oprimió Intro en el objeto JTextField campoTexto2

63 else if ( evento.getSource() == campoTexto2 )

64 cadena = String.format( “campoTexto2: %s”,

65 evento.getActionCommand() );

66

67 // el usuario oprimió Intro en el objeto JTextField campoTexto3

68 else if ( evento.getSource() == campoTexto3 )

69 cadena = String.format( “campoTexto3: %s”,

70 evento.getActionCommand() );

71

72 // el usuario oprimió Intro en el objeto JTextField campoContrasenia

73 else if ( evento.getSource() == campoContrasenia )

74 cadena = String.format( “campoContrasenia: %s”,

75 evento.getActionCommand() );

76

77 // muestra el contenido del objeto JTextField

78 JOptionPane.showMessageDialog( null, cadena );

79 } // fin del método actionPerformed

80 } // fin de la clase interna privada ManejadorCampoTexto81 } // fin de la clase CampoTextoMarco

564 Capítulo 14 Componentes de la GUI: Parte 1

Creación de la GUILa línea 22 establece el esquema del objeto CampoTextoMarco a FlowLayout. La línea 25 crea el objeto campoTexto1 con 10 columnas de texto. La anchura en píxeles de una columna de texto se determina en base a la anchura promedio de un carácter en el tipo de letra actual del campo de texto. Cuando se mues-tra texto en un campo de texto, y el texto es más ancho que el campo de texto en sí, no está visible una parte del texto del lado derecho. Si usted escribe en un campo de texto y el cursor llega al extremo derecho del campo, el texto en el extremo izquierdo se empuja hacia el lado izquierdo del campo de texto y ya no estará visible. Los usuarios pueden usar las flechas de dirección izquierda y derecha para recorrer el texto completo. La línea 26 agrega el objeto campoTexto1 al objeto JFrame.

La línea 29 crea el objeto campoTexto2 con el texto inicial “Escriba el texto aqui” para mostrar-lo en el campo de texto. La anchura del campo se determina en base a la anchura del texto predetermina-do especificado en el constructor. La línea 30 agrega el objeto campoTexto2 al objeto JFrame.

La línea 33 crea el objeto campoTexto3 y llama al constructor de JTextField con dos argumentos: el texto predeterminado “Campo de texto no editable” para mostrarlo y la anchura del campo de texto en columnas (21). La línea 34 utiliza el método setEditable (heredado por JTextField de la clase JTextComponent) para hacer el campo de texto no editable; es decir, el usuario no puede modificar el texto en el campo. La línea 35 agrega el objeto campoTexto3 al objeto JFrame.

La línea 38 crea campoContrasenia con el texto “Texto oculto” a mostrar en el campo de texto. La anchura de este campo de texto se determina en base a la anchura del texto predeterminado. Al ejecutar la aplicación, observe que el texto se muestra como una cadena de asteriscos. La línea 39 agrega campo-Contrasenia al objeto JFrame.

Pasos requeridos para establecer el manejo de eventos para un componente de GUIEste ejemplo debe mostrar un diálogo de mensaje que contenga el texto de un campo de texto, cuando el usuario oprime Intro en ese campo de texto. Antes de que una aplicación pueda responder a un evento para un componente de GUI específico, debemos realizar varios pasos de codificación:

1. Crear una clase que represente al manejador de eventos e implementar una interfaz apropiada, conocida como interfaz de escucha de eventos.

2. Indicar que se debe notificar a un objeto de la clase del paso 1 cuando ocurra el evento. A esto se le conoce como registrar el manejador de eventos.

Uso de una clase anidada para implementar un manejador de eventosTodas las clases que hemos visto hasta ahora se conocen como clases de nivel superior; es decir, las clases no se declararon dentro de otra clase. Java nos permite declarar clases dentro de otras clases; a éstas se les conoce como clases anidadas. Las clases anidadas pueden ser static o no static. Las clases anidadas no static se llaman clases internas, y se utilizan con frecuencia para implementar maneja-dores de eventos.

Un objeto de una clase interna debe crearse mediante un objeto de la clase de nivel superior que contenga a la clase interna. Cada objeto de la clase interna tiene implícitamente una referencia a un obje-to de su clase de nivel superior. El objeto de la clase interna puede usar esta referencia implícita para acce-der en forma directa a todas las variables y métodos de la clase de nivel superior. Una clase interna que es static no requiere un objeto de su clase de nivel superior, y no tiene implícitamente una referencia a un objeto de la clase de nivel superior. Como veremos en el capítulo 15, Gráficos y Java 2D, la API de gráfi-cos 2D de Java utiliza mucho las clases anidadas static.

La clase interna ManejadorCampoTextoEl manejo de eventos en este ejemplo se realiza mediante un objeto de la clase interna private Maneja-dorCampoTexto (líneas 50 a 80). Esta clase es private debido a que se utilizará sólo para crear maneja-dores de eventos para los campos de texto en la clase de nivel superior CampoTextoMarco. Al igual que

14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas 565

con los otros miembros de una clase, las clases internas pueden declararse como public, protected o private. Puesto que los manejadores de eventos tienden a ser específicos para la aplicación en la que están definidos, a menudo se implementan como clases internas private o como clases internas anónimas (sección 14.11).

Los componentes de GUI pueden generar una variedad de eventos en respuesta a las interacciones del usuario. Cada evento se representa mediante una clase, y sólo puede procesarse mediante el tipo apro-piado de manejador de eventos. En la mayoría de los casos, los eventos que soporta un componente se describen en la documentación de la API de Java para la clase de ese componente y sus superclases. Cuan-do el usuario oprime Intro en un objeto JTextField o JPasswordField, ocurre un evento ActionEvent (paquete java.awt.event). Dicho evento se procesa mediante un objeto que implementa la interfaz ActionListener (paquete java.awt.event). La información aquí descrita está disponible en la docu-mentación de la API de Java para las clases JTextField y ActionEvent. Como JPasswordField es una subclase de JTextField, JPasswordField soporta los mismos eventos.

Para prepararnos para manejar los eventos en este ejemplo, la clase interna ManejadorCampoTexto implementa la interfaz ActionListener y declara el único método en esa interfaz: actionPerformed (líneas 53 a 79). Este método especifica las tareas a realizar cuando ocurre un evento ActionEvent. Por lo tanto, la clase ManejadorCampoTexto cumple con el paso 1 que se listó anteriormente en esta sección. En breve hablaremos sobre los detalles del método actionPerformed.

Registro del manejador de evento para cada campo de textoEn el constructor de CampoTextoMarco, la línea 42 crea un objeto ManejadorCampoTexto y lo asigna a la variable manejador. El método actionPerformed de este objeto se llamará en forma automática cuando el usuario oprima Intro en cualquiera de los campos de texto de la GUI. Sin embargo, antes de que pueda ocurrir esto, el programa debe registrar este objeto como el manejador de eventos para cada campo de texto. Las líneas 43 a 46 son las instrucciones de registro de eventos que especifican a manejador como el manejador de eventos para los tres objetos JTextField y el objeto JPassword-Field. La aplicación llama al método addActionListener de JTextField para registrar el maneja-dor de eventos para cada componente. Este método recibe como argumento un objeto ActionLis-tener, el cual puede ser un objeto de cualquier clase que implemente a ActionListener. El objeto manejador es un ActionListener, ya que la clase ManejadorCampoTexto implementa a ActionLis-tener. Una vez que se ejecutan las líneas 43 a 46, el objeto manejador escucha los eventos. Ahora, cuando el usuario oprime Intro en cualquiera de estos cuatro campos de texto, se hace una llamada al método actionPerformed (líneas 53 a 79) en la clase ManejadorCampoTexto para que maneje el evento. Si no está registrado un manejador de eventos para un campo de texto específico, el evento que ocurre cuando el usuario oprime Intro en ese campo se consume (es decir, la aplicación simple-mente lo ignora).

Observación de ingeniería de software 14.1El componente de escucha de eventos para cierto evento debe implementar a la interfaz de escucha de eventos apropiada.

Error común de programación 14.2Olvidar registrar un objeto manejador de eventos para un tipo de evento específico de un componente de la GUI hace que los eventos de ese tipo se ignoren.

Detalles del método actionPerformed de la clase ManejadorCampoTextoEn este ejemplo estamos usando el método actionPerformed de un objeto manejador de eventos (líneas 53 a 79) para manejar los eventos generados por cuatro campos de texto. Como nos gustaría imprimir en pantalla el nombre de la variable de instancia de cada campo de texto para fines demos-

566 Capítulo 14 Componentes de la GUI: Parte 1

trativos, debemos determinar cuál campo de texto generó el evento cada vez que se hace una llamada a actionPerformed. El componente de GUI con el que interactúa el usuario es el origen del evento. Cuando el usuario oprime Intro mientras uno de los campos de texto o el campo contraseña tiene el enfoque, el sistema crea un objeto ActionEvent único que contiene información acerca del evento que acaba de ocurrir, como el origen del evento y el texto en el campo de texto. Después, el sistema pasa este objeto ActionEvent en una llamada al método actionPerformed del componente de escucha de eventos. La línea 55 declara el objeto String que se va a mostrar. La variable se inicializa con la cadena vacía; un objeto String que no contiene caracteres. El compilador requiere que se inicialice la variable en caso de que no se ejecute ninguna de las bifurcaciones de la instrucción if anidada en las líneas 58 a 75.

El método getSource de ActionEvent (que se llama en las líneas 58, 63, 68 y 73) devuelve una referencia al origen del evento. La condición en la línea 58 pregunta, “¿Es campoTexto1 el origen del evento?” Esta condición compara las referencias en ambos lados del operador == para determinar si se refieren al mismo objeto. Si ambos se refieren a campoTexto1, el usuario oprimió Intro en campoTexto1. En este caso, las líneas 59 y 60 crean un objeto String que contiene el mensaje que la línea 78 mostrará en un diálogo de mensaje. La línea 60 utiliza el método getActionCommand de ActionEvent para obtener el texto que escribió el usuario en el campo de texto que generó el evento.

En este ejemplo, mostramos el texto de la contraseña en el objeto JPasswordField cuando el usuario oprime Intro en ese campo. Algunas veces es necesario procesar mediante programación los caracteres en una contraseña. El método getPassword de JPasswordField devuelve los caracteres de la contraseña como un arreglo de tipo char.

La clase PruebaCampoTextoLa clase PruebaCampoTexto (figura 14.10) contiene el método main que ejecuta esta aplicación y mues-tra un objeto de la clase CampoTextoMarco. Al ejecutar la aplicación, hasta el campo JTextField (cam-poTexto3), que no se puede editar, puede generar un evento ActionEvent. Para probar esto, haga clic en el campo de texto para darle el enfoque y después oprima Intro. Además, el texto actual de la contra-seña se muestra al oprimir Intro en el campo JPasswordField. ¡Desde luego que por lo general no se debe mostrar la contraseña!

Esta aplicación usó un solo objeto de la clase ManejadorCampoTexto como el componente de escu-cha de eventos para cuatro campos de texto. Empezando en la sección 14.10, verá que es posible decla-rar varios objetos de escucha de eventos del mismo tipo, y registrar cada objeto para cada evento de un componente de la GUI por separado. Esta técnica nos permite eliminar la lógica if…else utilizada en el manejador de eventos de este ejemplo, al proporcionar manejadores de eventos separados para los eventos de cada componente.

Fig. 14.10 � Clase de prueba para CampoTextoMarco (parte 1 de 2).

1 // Fig. 14.10: PruebaCampoTexto.java

2 // Prueba de CampoTextoMarco.

3 import javax.swing.JFrame;

4

5 public class PruebaCampoTexto

6 {

7 public static void main( String[] args )

8 {

9 CampoTextoMarco campoTextoMarco = new CampoTextoMarco();

10 campoTextoMarco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 campoTextoMarco.setSize( 350, 100 ); // establece el tamaño del marco

14.7 Tipos de eventos comunes de la GUI e interfaces de escucha 567

Fig. 14.10 � Clase de prueba para CampoTextoMarco (parte 2 de 2).

12 campoTextoMarco.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase PruebaCampoTexto

14.7 Tipos de eventos comunes de la GUI e interfaces de escucha

En la sección 14.6 aprendió que la información acerca del evento que ocurre cuando el usuario oprime Intro en un campo de texto se almacena en un objeto ActionEvent. Pueden ocurrir muchos tipos distin-tos de eventos cuando el usuario interactúa con una GUI. La información acerca de cualquier evento se almacena en un objeto de una clase que extiende a AWTEvent (del paquete java.awt). La figura 14.11 ilustra una jerarquía que contiene muchas clases de eventos del paquete java.awt.event. Algunas de éstas se describen en este capítulo y en el capítulo 25. Estos tipos de eventos se utilizan tanto con compo-nentes de AWT como de Swing. Los tipos de eventos adicionales que son específicos para los componen-tes de GUI de Swing se declaran en el paquete javax.swing.event.

Resumiremos las tres partes requeridas para el mecanismo de manejo de eventos que vimos en la sección 14.6: el origen del evento, el objeto del evento y el componente de escucha del evento. El origen del evento es el componente específico de la GUI con el que interactúa el usuario. El objeto del evento en-

568 Capítulo 14 Componentes de la GUI: Parte 1

capsula información acerca del evento que ocurrió, como una referencia al origen del evento, y cualquier información específica del evento que pueda requerir el componente de escucha del evento, para que pueda manejarlo. El componente de escucha del evento es un objeto que recibe una notificación del origen del evento cuando éste ocurre; en efecto, “escucha” un evento, y uno de sus métodos se ejecuta en respuesta al evento. Un método del componente de escucha del evento recibe un objeto evento cuando se notifica al componente de escucha acerca del evento. Después, el componente de escucha del evento utiliza el objeto evento para responder al evento. A este modelo de manejo de eventos se le conoce como modelo de eventos por delegación: el procesamiento de un evento se delega a un objeto específico (el componente de escucha de eventos) en la aplicación.

Para cada tipo de objeto evento, hay por lo general una interfaz de escucha de eventos que le corres-ponde. Un componente de escucha de eventos para un evento de GUI es un objeto de una clase que implementa a una o más de las interfaces de escucha de eventos de los paquetes java.awt.event y javax.swing.event. Muchos de los tipos de componentes de escucha de eventos son comunes para los compo-nentes de Swing y de AWT. Dichos tipos se declaran en el paquete java.awt.event, y algunos de ellos se muestran en la figura 14.12. Los tipos de escucha de eventos adicionales específicos para los componen-tes de Swing se declaran en el paquete javax.swing.event.

Cada interfaz de escucha de eventos especifica uno o más métodos manejadores de eventos que deben declararse en la clase que implementa a la interfaz. En la sección 10.7 vimos que cualquier clase que implementa a una interfaz debe declarar a todos los métodos abstract de esa interfaz; en caso con-trario, la clase es abstract y no puede utilizarse para crear objetos.

Fig. 14.11 � Algunas clases de eventos del paquete java.awt.event.

Object

EventObject

AWTEvent

ContainerEvent

FocusEvent

PaintEvent

WindowEvent

InputEvent

ActionEvent

AdjustmentEvent

ItemEvent

TextEvent

ComponentEvent

MouseEventKeyEvent

MouseWheelEvent

14.8 Cómo funciona el manejo de eventos 569

Cuando ocurre un evento, el componente de la GUI con el que el usuario interactuó notifica a sus componentes de escucha registrados, llamando al método de manejo de eventos apropiado de cada compo-nente de escucha. Por ejemplo, cuando el usuario oprime la tecla Intro en un objeto JTextField, se hace una llamada al método actionPerformed del componente de escucha registrado. ¿Cómo se registró el manejador de eventos? ¿Cómo sabe el componente de la GUI que debe llamar a actionPerformed, en vez de llamar a otro método manejador de eventos? En la siguiente sección responderemos a estas pre-guntas y haremos un diagrama de la interacción.

14.8 Cómo funciona el manejo de eventosVamos a ilustrar cómo funciona el mecanismo de manejo de eventos, utilizando a campoTexto1 del ejem-plo de la figura 14.9. Tenemos dos preguntas sin contestar de la sección 14.7:

1. ¿Cómo se registró el manejador de eventos?

2. ¿Cómo sabe el componente de la GUI que debe llamar a actionPerformed en vez de llamar a algún otro método manejador de eventos?

La primera pregunta se responde mediante el registro de eventos que se lleva a cabo en las líneas 43 a 46 de la figura 14.9. En la figura 14.13 se muestra un diagrama de la variable JTextField llamada campo-Texto1, la variable ManejadorCampoTexto llamada manejador y los objetos a los que hacen referencia.

Registro de eventosTodo JComponent tiene una variable de instancia llamada listenerList, que hace referencia a un objeto de la clase EventListenerList (paquete javax.swing.event). Cada objeto de una subclase de JCompo-nent mantiene referencias a todos sus componentes de escucha registrados en listenerList. Por cues-tión de simpleza, hemos colocado a listenerList en el diagrama como un arreglo, abajo del objeto JTextField en la figura 14.13.

Cuando se ejecuta la línea 43 de la figura 14.9

campoTexto1.addActionListener( manejador );

Fig. 14.12 � Algunas interfaces comunes de componentes de escucha de eventos del paquete java.awt.event.

«interfaz»ActionListener

«interfaz»ComponentListener

«interfaz»ContainerListener

«interfaz»FocusListener

«interfaz»ItemListener

«interfaz»KeyListener

«interfaz»MouseListener

«interfaz»MouseMotionListener

«interfaz»TextListener

«interfaz»WindowListener

«interfaz»java.util.EventListener

«interfaz»AdjustmentListener

570 Capítulo 14 Componentes de la GUI: Parte 1

se coloca en el objeto listenerList de campoTexto1 una nueva entrada que contiene una referencia al objeto ManejadorCampoTexto. Aunque no se muestra en el diagrama, esta nueva entrada también inclu-ye el tipo del componente de escucha (en este caso, ActionListener). Mediante el uso de este mecanis-mo, cada componente ligero de GUI de Swing mantiene su propia lista de componentes de escucha que se registraron para manejar los eventos del componente.

Llamada al manejador de eventosEl tipo de componente de escucha de eventos es importante para responder a la segunda pregunta: ¿Cómo sabe el componente de la GUI que debe llamar a actionPerformed en vez de llamar a otro método? Todo componente de la GUI soporta varios tipos de eventos, incluyendo eventos de ratón, eventos de tecla y otros más. Cuando ocurre un evento, éste se despacha solamente a los componentes de escucha de eventos del tipo apropiado. El despachamiento (dispatching) es simplemente el proceso por el cual el componente de la GUI llama a un método manejador de eventos en cada uno de sus componentes de escucha registrados para el tipo de evento que ocurrió.

Cada tipo de evento tiene una o más interfaces de escucha de eventos correspondientes. Por ejem-plo, los eventos tipo ActionEvent son manejados por objetos ActionListener, los eventos tipo Mou-seEvent son manejados por objetos MouseListener y MouseMotionListener, y los eventos tipo Key-Event son manejados por objetos KeyListener. Cuando ocurre un evento, el componente de la GUI recibe (de la JVM) un ID de evento único, el cual especifica el tipo de evento. El componente de la GUI utiliza el ID de evento para decidir a cuál tipo de componente de escucha debe despacharse el evento, y para decidir cuál método llamar en cada objeto de escucha. Para un ActionEvent, el evento se despacha al método actionPerformed de todos los objetos ActionListener registrados (el único método en la interfaz ActionListener). En el caso de un MouseEvent, el evento se despacha a todos los objetos MouseListener o MouseMotionListener registrados, dependiendo del evento de ratón que ocurra. El ID de evento del objeto MouseListener determina cuáles de los varios métodos manejadores de eventos de ratón son llamados. Todas estas decisiones las administran los componentes de la GUI por usted. Todo lo que usted necesita hacer es registrar un manejador de eventos para el tipo de evento específico que requiere su aplicación, y el componente de GUI asegurará que se llame al método apro-piado del manejador de eventos, cuando ocurra el evento. Hablaremos sobre otros tipos de eventos e

Fig. 14.13 � Registro de eventos para el objeto JTextField campoTexto1.

Esta referencia se crea mediante la instrucción campoTexto1.addActionListener( manejador );

public void actionPerformed(

ActionEvent evento )

{

// aquí se maneja el evento

}

listenerList

objeto ManejadorCampoTextoobjeto JTextField

campoTexto1 manejador

...

14.9 JButton 571

interfaces de escucha de eventos a medida que se vayan necesitando, con cada nuevo componente que vayamos viendo.

14.9 JButton Un botón es un componente en el que el usuario hace clic para desencadenar cierta acción. Una apli-cación de Java puede utilizar varios tipos de botones, incluyendo botones de comando, casillas de verificación, botones interruptores y botones de opción. En la figura 14.14 se muestra la jerarquía de herencia de los botones de Swing que veremos en este capítulo. Como puede ver en el diagrama, to-dos los tipos de botones son subclases de AbstractButton (paquete javax.swing), la cual declara las características comunes para los botones de Swing. En esta sección nos concentraremos en los botones que se utilizan comúnmente para iniciar un comando.

Fig. 14.14 � Jerarquía de botones de Swing.

JComponent

AbstractButton

JButton JToggleButton

JCheckBox JRadioButton

Un botón de comando (vea la salida de la figura 14.16) genera un evento ActionEvent cuando el usuario hace clic en él. Los botones de comando se crean con la clase JButton. El texto en la cara de un objeto JButton se llama etiqueta del botón. Una GUI puede tener muchos objetos JButton, pero cada etiqueta de botón debe generalmente ser única en las partes de la GUI en que se muestre.

Observación de apariencia visual 14.8Por lo general, el texto en los botones utiliza la capitalización estilo título de libro.

Observación de apariencia visual 14.9Tener más de un objeto JButton con la misma etiqueta hace que los objetos JButton sean ambiguos para el usuario. Debe proporcionar una etiqueta única para cada botón.

La aplicación de las figuras 14.15 y 14.16 crea dos objetos JButton y demuestra que estos objetos tienen soporte para mostrar objetos Icon. El manejo de eventos para los botones se lleva a cabo mediante una sola instancia de la clase interna ManejadorBoton (líneas 39 a 47).

En las líneas 14 y 15 se declaran las variables botonJButtonSimple y botonJButtonElegante de la clase JButton. Los correspondientes objetos se instancian en el constructor. En la línea 23 se crea botonJButtonSimple con la etiqueta “Boton simple”. En la línea 24 se agrega el botón al objeto JFrame.

Un objeto JButton puede mostrar un objeto Icon. Para proveer al usuario un nivel adicional de interacción visual con la GUI, un objeto JButton puede tener también un objeto Icon de sustitución:

572 Capítulo 14 Componentes de la GUI: Parte 1

un Icon que se muestre cuando el usuario coloque el ratón encima del JButton. El icono en el botón cambia a medida que el ratón se mueve hacia dentro y fuera del área del botón en la pantalla. En las líneas 26 y 27 (figura 14.15) se crean dos objetos ImageIcon que representan al objeto Icon predeterminado

Fig. 14.15 � Botones de comando y eventos de acción.

1 // Fig. 14.15: MarcoBoton.java 2 // Creación de objetos JButton. 3 import java.awt.FlowLayout; 4 import java.awt.event.ActionListener; 5 import java.awt.event.ActionEvent; 6 import javax.swing.JFrame; 7 import javax.swing.JButton; 8 import javax.swing.Icon; 9 import javax.swing.ImageIcon;10 import javax.swing.JOptionPane;1112 public class MarcoBoton extends JFrame 13 {14 private JButton botonJButtonSimple; // botón con texto solamente15 private JButton botonJButtonElegante; // botón con iconos1617 // MarcoBoton agrega objetos JButton a JFrame18 public MarcoBoton()19 {20 super( “Prueba de botones” );21 setLayout( new FlowLayout() ); // establece el esquema del marco2223 botonJButtonSimple = new JButton( “Boton simple” ); // botón con texto24 add( botonJButtonSimple ); // agrega botonJButtonSimple a JFrame2526 Icon insecto1 = new ImageIcon( getClass().getResource( “insecto1.gif” ) );27 Icon insecto2 = new ImageIcon( getClass().getResource( “insecto2.gif” ) );

28 botonJButtonElegante = new JButton( “Boton elegante”, insecto1 ); // establece la imagen

29 botonJButtonElegante.setRolloverIcon( insecto2 ); // establece la imagen de sustitución

30 add( botonJButtonElegante ); // agrega botonJButtonElegante a JFrame3132 // crea nuevo ManejadorBoton para manejar los eventos de botón 33 ManejadorBoton manejador = new ManejadorBoton();34 botonJButtonElegante.addActionListener( manejador );35 botonJButtonSimple.addActionListener( manejador );36 } // fin del constructor de MarcoBoton3738 // clase interna para manejar eventos de botón39 private class ManejadorBoton implements ActionListener 40 {41 // maneja evento de botón42 public void actionPerformed( ActionEvent evento )43 {44 JOptionPane.showMessageDialog( MarcoBoton.this, String.format(45 “Usted oprimio: %s”, evento.getActionCommand() ) );46 } // fin del método actionPerformed47 } // fin de la clase interna privada ManejadorBoton48 } // fin de la clase MarcoBoton

14.9 JButton 573

y el objeto Icon de sustitución para el objeto JButton creado en la línea 28. Ambas instrucciones supo-nen que los archivos de imagen están guardados en el mismo directorio que la aplicación. Por lo general, las imágenes se colocan en el mismo directorio que la aplicación o en un subdirectorio como images). Estos archivos de imágenes se incluyen en el ejemplo.

En la línea 28 se crea botonJButtonElegante con el texto “Boton elegante” y el icono insecto1. De manera predeterminada, el texto se muestra a la derecha del icono. En la línea 29 se utiliza el mé-todo setRolloverIcon (heredado de la clase AbstractButton) para especificar la imagen a mostrar en el objeto JButton cuando el usuario coloque el ratón sobre el botón. En la línea 30 se agrega el JButton al objeto JFrame.

Fig. 14.16 � Clase de prueba para MarcoBoton.

1 // Fig. 14.16: PruebaBoton.java

2 // Prueba de MarcoBoton.

3 import javax.swing.JFrame;

4

5 public class PruebaBoton

6 {

7 public static void main( String[] args )

8 {

9 MarcoBoton marcoBoton = new MarcoBoton(); // crea MarcoBoton

10 marcoBoton.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoBoton.setSize( 300, 110 ); // establece el tamaño del marco

12 marcoBoton.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase PruebaBoton

574 Capítulo 14 Componentes de la GUI: Parte 1

Observación de apariencia visual 14.10Como la clase AbstractButton soporta el mostrar texto e imágenes en un botón, todas las subclases de AbstractButton soportan también el mostrar texto e imágenes.

Observación de apariencia visual 14.11Al usar iconos de sustitución para los objetos JButton, los usuarios reciben una retroali-mentación visual que les indica que, al hacer clic en el ratón mientras el cursor está colocado encima del botón, ocurrirá una acción.

Los objetos JButton, al igual que los objetos JTextField, generan eventos ActionEvent que pue-den ser procesados por cualquier objeto ActionListener. En las líneas 33 a 35 se crea un objeto de la clase interna private ManejadorBoton y se usa addActionListener para registrarlo como el manejador de eventos para cada objeto JButton. La clase ManejadorBoton (líneas 39 a 47) declara a actionPerfor-med para mostrar un cuadro de diálogo de mensaje que contiene la etiqueta del botón que el usuario oprimió. Para un evento de JButton, el método getActionCommand de ActionEvent devuelve la eti-queta del objeto JButton.

Cómo acceder a la referencia this en un objeto de una clase de nivel superior desde una clase internaCuando ejecute esta aplicación y haga clic en uno de sus botones, observe que el diálogo de mensaje que aparece está centrado sobre la ventana de la aplicación. Esto ocurre debido a que la llamada al método showMessageDialog de JOptionPane (líneas 44 y 45 de la figura 14.15) utiliza a MarcoBoton.this, en vez de null como el primer argumento. Cuando este argumento no es null, representa lo que se denomi-na el componente de GUI padre del diálogo de mensaje (en este caso, la ventana de aplicación es el compo-nente padre) y permite centrar el diálogo sobre ese componente, cuando se muestra el diálogo. Marco-Boton.this representa a la referencia this del objeto de la clase MarcoBoton de nivel superior.

Observación de ingeniería de software 14.2Cuando se utiliza en una clase interna, la palabra clave this se refiere al objeto actual de la clase interna que se está manipulando. Un método de la clase interna puede utilizar la referencia this del objeto de su clase externa, si antepone a this el nombre de la clase externa y un punto, como en MarcoBoton.this.

14.10 Botones que mantienen el estadoLos componentes de la GUI de Swing contienen tres tipos de botones de estado: JToggleButton, JCheckBox y JRadioButton, los cuales tienen valores encendido/apagado o verdadero/falso. Las clases JCheckBox y JRadioButton son subclases de JToggleButton (figura 14.14). Un objeto JRadioButton es distinto de un objeto JCheckBox en cuanto a que por lo general hay varios objetos JRadioButton que se agrupan, y son mutuamente exclusivos; sólo uno de los objetos JRadioButton en el grupo pue-de estar seleccionado en un momento dado, de igual forma que los botones en la radio de un auto. Primero veremos la clase JCheckBox.

14.10.1 JCheckBox La aplicación de las figuras 14.17 y 14.18 utilizan dos objetos JCheckBox para seleccionar el estilo de-seado de tipo de letra para el texto a mostrar en un objeto JTextField. Cuando se selecciona, uno aplica un estilo en negrita y el otro aplica un estilo en cursivas. Si ambos se seleccionan, el estilo del tipo de letra es negrita y cursiva. Cuando la aplicación se ejecuta por primera vez, ninguno de los objetos JCheckBox está activado (es decir, ambos son false), por lo que el tipo de letra es simple. La clase PruebaCheckBox (figura 14.18) contiene el método main que ejecuta esta aplicación.

14.10 Botones que mantienen el estado 575

Fig. 14.17 � Botones JCheckBox y eventos de los elementos (parte 1 de 2).

1 // Fig. 14.17: MarcoCasillaVerificacion.java 2 // Creación de botones JCheckBox. 3 import java.awt.FlowLayout; 4 import java.awt.Font; 5 import java.awt.event.ItemListener; 6 import java.awt.event.ItemEvent; 7 import javax.swing.JFrame; 8 import javax.swing.JTextField; 9 import javax.swing.JCheckBox;1011 public class MarcoCasillaVerificacion extends JFrame 12 {13 private JTextField campoTexto; // muestra el texto en tipos de letra cambiantes14 private JCheckBox negritaJCheckBox; // para seleccionar/deseleccionar negrita15 private JCheckBox cursivaJCheckBox; // para seleccionar/deseleccionar cursiva1617 // El constructor de MarcoCasillaVerificacion agrega objetos JCheckBox a JFrame18 public MarcoCasillaVerificacion()19 {20 super( “Prueba de JCheckBox” );21 setLayout( new FlowLayout() ); // establece el esquema del marco2223 // establece JTextField y su tipo de letra

24 campoTexto = new JTextField( “Observe como cambia el estilo de tipo de letra”, 27 );

25 campoTexto.setFont( new Font( “Serif”, Font.PLAIN, 14 ) );26 add( campoTexto ); // agrega campoTexto a JFrame27

28 negritaJCheckBox = new JCheckBox( “Negrita” ); // crea casilla de verificación “negrita”

29 cursivaJCheckBox = new JCheckBox( “Cursiva” ); // crea casilla de verificación “cursiva”

30 add( negritaJCheckBox ); // agrega casilla de verificación “negrita” a JFrame31 add( cursivaJCheckBox ); // agrega casilla de verificación “cursiva” a JFrame3233 // registra componentes de escucha para objetos JCheckBox34 ManejadorCheckBox manejador = new ManejadorCheckBox();35 negritaJCheckBox.addItemListener( manejador );36 cursivaJCheckBox.addItemListener( manejador );37 } // fin del constructor de MarcoCasillaVerificacion3839 // clase interna privada para el manejo de eventos ItemListener40 private class ManejadorCheckBox implements ItemListener 41 {42 // responde a los eventos de casilla de verificación43 public void itemStateChanged( ItemEvent evento )44 {45 Font tipoletra = null; // almacena el nuevo objeto Font46

47 // determina cuales objetos CheckBox están seleccionados y crea el objeto Font

48 if ( negritaJCheckBox.isSelected() && cursivaJCheckBox.isSelected() )49 tipoletra = new Font( “Serif”, Font.BOLD + Font.ITALIC, 14 );50 else if ( negritaJCheckBox.isSelected() )51 tipoletra = new Font( “Serif”, Font.BOLD, 14 );52 else if ( cursivaJCheckBox.isSelected() )53 tipoletra = new Font( “Serif”, Font.ITALIC, 14 );

576 Capítulo 14 Componentes de la GUI: Parte 1

Una vez creado e inicializado el objeto JTextField (figura 14.17, línea 24), en la línea 25 se utiliza el método setFont (heredado por JTextField indirectamente de la clase Component) para establecer el tipo de letra del objeto JTextField con un nuevo objeto de la clase Font (paquete java.awt). El nuevo objeto Font se inicializa con “Serif” (un nombre de tipo de letra genérico que representa un tipo de letra como Times, y se soporta en todas las plataformas de Java), estilo Font.PLAIN y tamaño de 14 puntos. A continuación, en las líneas 28 y 29 se crean dos objetos JCheckBox. El objeto String que se pasa al constructor de JCheckBox es la etiqueta de la casilla de verificación que aparece a la derecha del objeto JCheckBox de manera predeterminada.

Cuando el usuario hace clic en un objeto JCheckBox, ocurre un evento ItemEvent. Este evento puede manejarse mediante un objeto ItemListener, que debe implementar al método itemStateChan-ged. En este ejemplo, el manejo de eventos se lleva a cabo mediante una instancia de la clase interna private ManejadorCheckBox (líneas 40 a 59). En las líneas 34 a 36 se crea una instancia de la clase ManejadorCheckBox y se registra con el método addItemListener como componente de escucha para ambos objetos JCheckBox.

El método itemStateChanged (líneas 43 a 58) es llamado cuando el usuario hace clic en el objeto negritaJCheckBox o cursivaJCheckBox. En este ejemplo, no necesitamos saber en cuál de los dos

54 else

55 tipoletra = new Font( “Serif”, Font.PLAIN, 14 );

56

57 campoTexto.setFont( tipoletra ); // establece el tipo de letra del campo de texto

58 } // fin del método itemStateChanged

59 } // fin de la clase interna privada ManejadorCheckBox

60 } // fin de la clase MarcoCasillaVerificacion

Fig. 14.17 � Botones JCheckBox y eventos de los elementos (parte 2 de 2).

Fig. 14.18 � Clase de prueba para MarcoCasillaVerificacion.

1 // Fig. 14.18: PruebaCasillaVerificacion.java 2 // Prueba de MarcoCasillaVerificacion. 3 import javax.swing.JFrame; 4 5 public class PruebaCasillaVerificacion 6 { 7 public static void main( String[] args ) 8 { 9 MarcoCasillaVerificacion marcoCasillaVerificacion = new MarcoCasillaVerificacion(); 10 marcoCasillaVerificacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoCasillaVerificacion.setSize( 350, 100 ); // establece el tamaño del marco12 marcoCasillaVerificacion.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase PruebaCasillaVerificacion

14.10 Botones que mantienen el estado 577

objetos JCheckBox se hizo clic, sólo si están o no seleccionados. En la línea 48 se utiliza el método is-Selected de JCheckBox para determinar si ambos objetos JCheckBox están seleccionados. Si es así, la línea 49 crea un tipo de letra en negrita y cursiva, sumando las constantes Font.BOLD y Font.ITALIC para el argumento de estilo de tipo de letra del constructor de Font. La línea 50 determina si la casilla negritaJCheckBox está seleccionada, y de ser así en la línea 53 se crea un tipo de letra en cursiva. Si ninguna de las condiciones anteriores es verdadera, en la línea 55 se crea un tipo de letra simple usando la constante Font.PLAIN de Font. Por último, en la línea 57 se establece el nuevo tipo de letra de cam-poTexto, el cual cambia el tipo de letra en el objeto JTextField en pantalla.

Relación entre una clase interna y su clase de nivel superiorLa clase ManejadorCheckBox utilizó las variables negritaJCheckBox (figura 14.17, líneas 48 y 50), cursivaJCheckBox (líneas 48 y 52) y campoTexto (línea 57), aun cuando estas variables no se declaran en la clase interna. Recuerde que una clase interna tiene una relación especial con su clase de nivel su-perior; se le permite acceder a todas las variables y métodos de la clase de nivel superior. El método itemStateChanged (líneas 43 a 58) de la clase ManejadorCheckBox utiliza esta relación para determinar cuáles objetos JCheckBox están seleccionados y para establecer el tipo de letra en el objeto JTextField. Observe que ninguna parte del código en la clase interna ManejadorCheckBox requiere una referencia explícita al objeto de la clase de nivel superior.

14.10.2 JRadioButton Los botones de opción (que se declaran con la clase JRadioButton) son similares a las casillas de verifi-cación, en cuanto a que tienen dos estados: seleccionado y no seleccionado (al que también se le conoce como deseleccionado). Sin embargo, los botones de opción por lo general aparecen como un grupo, en el cual sólo un botón de opción puede estar seleccionado en un momento dado (vea la salida de la figura 14.20). Al seleccionar un botón de opción distinto se obliga a que todos los demás botones de opción del grupo se deseleccionen. Los botones de opción se utilizan para representar opciones mutuamente ex-clusivas (es decir, no pueden seleccionarse varias opciones en el grupo al mismo tiempo). La relación ló-gica entre los botones de opción se mantiene mediante un objeto ButtonGroup (paquete javax.swing), el cual en sí no es un componente de la GUI. Un objeto ButtonGroup organiza un grupo de botones y no se muestra a sí mismo en una interfaz de usuario. En vez de ello, se muestra en la GUI cada uno de los objetos JRadioButton del grupo.

Error común de programación 14.3Si se agrega un objeto ButtonGroup (o un objeto de cualquier otra clase que no se derive de Component) a un contenedor, se produce un error de compilación.

La aplicación de las figuras 14.19 y 14.20 es similar a la de las figuras 14.17 y 14.18. El usuario pue-de alterar el estilo del tipo de letra del texto de un objeto JTextField. La aplicación utiliza botones de opción que permiten que se seleccione solamente un estilo de tipo de letra en el grupo a la vez. La clase PruebaBotonOpcion (figura 14.20) contiene el método main que ejecuta esta aplicación.

Fig. 14.19 � Objetos JRadioButton y ButtonGroup (parte 1 de 3).

1 // Fig. 14.19: MarcoBotonOpcion.java

2 // Creación de botones de opción, usando ButtonGroup y JRadioButton.

3 import java.awt.FlowLayout;

4 import java.awt.Font;

578 Capítulo 14 Componentes de la GUI: Parte 1

5 import java.awt.event.ItemListener; 6 import java.awt.event.ItemEvent; 7 import javax.swing.JFrame; 8 import javax.swing.JTextField; 9 import javax.swing.JRadioButton;10 import javax.swing.ButtonGroup;1112 public class MarcoBotonOpcion extends JFrame 13 {

14 private JTextField campoTexto; // se utiliza para mostrar los cambios en el tipo de letra

15 private Font tipoLetraSimple; // tipo de letra para texto simple16 private Font tipoLetraNegrita; // tipo de letra para texto en negrita17 private Font tipoLetraCursiva; // tipo de letra para texto en cursiva

18 private Font tipoLetraNegritaCursiva; // tipo de letra para texto en negrita y cursiva

19 private JRadioButton simpleJRadioButton; // selecciona texto simple20 private JRadioButton negritaJRadioButton; // selecciona texto en negrita21 private JRadioButton cursivaJRadioButton; // selecciona texto en cursiva22 private JRadioButton negritaCursivaJRadioButton; // negrita y cursiva

23 private ButtonGroup grupoOpciones; // grupo de botones que contiene los botones de opción

2425 // El constructor de MarcoBotonOpcion agrega los objetos JRadioButton a JFrame26 public MarcoBotonOpcion()27 {28 super( “Prueba de RadioButton” );29 setLayout( new FlowLayout() ); // establece el esquema del marco30

31 campoTexto = new JTextField( “Observe el cambio en el estilo del tipo de letra”, 28 );

32 add( campoTexto ); // agrega campoTexto a JFrame33 34 // crea los botones de opción35 simpleJRadioButton = new JRadioButton( “Simple”, true );36 negritaJRadioButton = new JRadioButton( “Negrita”, false );37 cursivaJRadioButton = new JRadioButton( “Cursiva”, false );38 negritaCursivaJRadioButton = new JRadioButton( “Negrita/Cursiva”, false );39 add( simpleJRadioButton ); // agrega botón simple a JFrame40 add( negritaJRadioButton ); // agrega botón negrita a JFrame41 add( cursivaJRadioButton ); // agrega botón cursiva a JFrame42 add( negritaCursivaJRadioButton ); // agrega botón negrita y cursiva43 44 // crea una relación lógica entre los objetos JRadioButton45 grupoOpciones = new ButtonGroup(); // crea ButtonGroup46 grupoOpciones.add( simpleJRadioButton ); // agrega simple al grupo47 grupoOpciones.add( negritaJRadioButton ); // agrega negrita al grupo48 grupoOpciones.add( cursivaJRadioButton ); // agrega cursiva al grupo49 grupoOpciones.add( negritaCursivaJRadioButton ); // agrega negrita y cursiva5051 // crea objetos tipo de letra52 tipoLetraSimple = new Font( “Serif”, Font.PLAIN, 14 );53 tipoLetraNegrita = new Font( “Serif”, Font.BOLD, 14 );54 tipoLetraCursiva = new Font( “Serif”, Font.ITALIC, 14 );55 tipoLetraNegritaCursiva = new Font( “Serif”, Font.BOLD + Font.ITALIC, 14 );56 campoTexto.setFont( tipoLetraSimple ); // establece tipo letra inicial a simple57

Fig. 14.19 � Objetos JRadioButton y ButtonGroup (parte 2 de 3).

14.10 Botones que mantienen el estado 579

58 // registra eventos para los objetos JRadioButton59 simpleJRadioButton.addItemListener( 60 new ManejadorBotonOpcion( tipoLetraSimple ) );61 negritaJRadioButton.addItemListener(62 new ManejadorBotonOpcion( tipoLetraNegrita ) );63 cursivaJRadioButton.addItemListener( 64 new ManejadorBotonOpcion( tipoLetraCursiva ) );65 negritaCursivaJRadioButton.addItemListener( 66 new ManejadorBotonOpcion( tipoLetraNegritaCursiva ) );67 } // fin del constructor de MarcoBotonOpcion6869 // clase interna privada para manejar eventos de botones de opción70 private class ManejadorBotonOpcion implements ItemListener 71 {72 private Font tipoLetra; // tipo de letra asociado con este componente de escucha73 74 public ManejadorBotonOpcion( Font f )75 {76 tipoLetra = f; // establece el tipo de letra de este componente de escucha77 } // fin del constructor ManejadorBotonOpcion7879 // maneja los eventos de botones de opción80 public void itemStateChanged( ItemEvent evento )81 {82 campoTexto.setFont( tipoLetra ); // establece el tipo de letra de campoTexto83 } // fin del método itemStateChanged84 } // fin de la clase interna privada ManejadorBotonOpcion85 } // fin de la clase MarcoBotonOpcion

Fig. 14.19 � Objetos JRadioButton y ButtonGroup (parte 3 de 3).

Fig. 14.20 � Clase de prueba para MarcoBotonOpcion (parte 1 de 2).

1 // Fig. 14.20: PruebaBotonOpcion.java

2 // Prueba de MarcoBotonOpcion.

3 import javax.swing.JFrame;

4

5 public class PruebaBotonOpcion

6 {

7 public static void main( String[] args )

8 {

9 MarcoBotonOpcion marcoBotonOpcion = new MarcoBotonOpcion();

10 marcoBotonOpcion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoBotonOpcion.setSize( 350, 100 ); // establece el tamaño del marco

12 marcoBotonOpcion.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase PruebaBotonOpcion

580 Capítulo 14 Componentes de la GUI: Parte 1

En las líneas 35 a 42 del constructor (figura 14.19) se crean cuatro objetos JRadioButton y se agre-gan al objeto JFrame. Cada objeto JRadioButton se crea con una llamada al constructor como la de la línea 35. Este constructor especifica la etiqueta que aparece a la derecha del objeto JRadioButton de manera predeterminada, junto con su estado inicial. Un segundo argumento true indica que el ob-jeto JRadioButton debe aparecer seleccionado al mostrarlo en pantalla.

En la línea 45 se instancia un objeto ButtonGroup llamado grupoOpciones. Este objeto es el “pe-gamento” que forma la relación lógica entre los cuatro objetos JRadioButton y permite que se selec-cione solamente uno de los cuatro en un momento dado. Es posible que no se seleccione ningún JRadioButton en un ButtonGroup, pero esto sólo puede ocurrir si no se agregan objetos JRadioButton preseleccionados al objeto ButtonGroup, y si el usuario no ha seleccionado todavía un objeto JRadio-Button. En las líneas 46 a 49 se utiliza el método add de ButtonGroup para asociar cada uno de los objetos JRadioButton con grupoOpciones. Si se agrega al grupo más de un objeto JRadioButton se-leccionado, el primer objeto JRadioButton seleccionado que se agregue será el que quede seleccionado cuando se muestre la GUI en pantalla.

Los objetos JRadioButton, al igual que los objetos JCheckbox, generan eventos tipo ItemEvent cuando se hace clic sobre ellos. En las líneas 59 a 66 se crean cuatro instancias de la clase interna Maneja-dorBotonOpcion (declarada en las líneas 70 a 84). En este ejemplo, cada objeto componente de escucha de eventos se registra para manejar el evento ItemEvent que se genera cuando el usuario hace clic en cualquiera de los objetos JRadioButton. Observe que cada objeto ManejadorBotonOpcion se inicializa con un objeto Font específico (creado en las líneas 52 a 55).

La clase ManejadorBotonOpcion (línea 70 a 84) implementa la interfaz ItemListener para poder manejar los eventos ItemEvent generados por los objetos JRadioButton. El constructor almacena el objeto Font que recibe como un argumento en la variable de instancia tipoLetra (declarada en la línea 72). Cuando el usuario hace clic en un objeto JRadioButton, grupoOpciones desactiva el objeto JRa-dioButton previamente seleccionado y el método itemStateChanged (líneas 80 a 83) establece el tipo de letra en el objeto JTextField al tipo de letra almacenado en el objeto componente de escucha de eventos correspondiente al objeto JRadioButton. Observe que la línea 82 de la clase interna Maneja-dorBotonOpcion utiliza la variable de instancia campoTexto de la clase de nivel superior para establecer el tipo de letra.

14.11 JComboBox: uso de una clase interna anónima para el manejo de eventos

Un cuadro combinado (algunas veces conocido como lista desplegable) permite al usuario seleccionar un elemento de una lista (figura 14.22). Los cuadros combinados se implementan con la clase JComboBox, la cual extiende a la clase JComponent. Los objetos JComboBox generan eventos ItemEvent, al igual que los objetos JCheckBox y JRadioButton. Este ejemplo también demuestra una forma especial de clase interna, que se utiliza con frecuencia en el manejo de eventos. La aplicación (figuras 14.21 y 14.22) utiliza un objeto JComboBox para proporcionar una lista de cuatro nombres de archivos de imágenes, de los cuales el usuario puede seleccionar una imagen para mostrarla en pantalla. Cuando el usuario selecciona un nombre, la aplicación muestra la imagen correspondiente como un objeto Icon en un objeto JLabel. La clase PruebaCuadroCombinado (figura 14.22) contiene el método main que ejecuta esta aplicación. Las

Fig. 14.20 � Clase de prueba para MarcoBotonOpcion (parte 2 de 2).

14.11 JComboBox: uso de una clase interna anónima para el manejo de eventos 581

capturas de pantalla para esta aplicación muestran la lista JComboBox después de hacer una selección, para ilustrar cuál nombre de archivo de imagen fue seleccionado.

En las líneas 19 a 23 (figura 14.21) se declara e inicializa el arreglo iconos con cuatro nuevos obje-tos ImageIcon. El arreglo String llamado nombres (líneas 17 y 18) contiene los nombres de los cuatro archivos de imagen que se guardan en el mismo directorio que la aplicación.

Fig. 14.21 � Objeto JComboBox que muestra una lista de nombres de imágenes (parte 1 de 2).

1 // Fig. 14.21: MarcoCuadroCombinado.java 2 // Objeto JComboBox que muestra una lista de nombres de imágenes. 3 import java.awt.FlowLayout; 4 import java.awt.event.ItemListener; 5 import java.awt.event.ItemEvent; 6 import javax.swing.JFrame; 7 import javax.swing.JLabel; 8 import javax.swing.JComboBox; 9 import javax.swing.Icon;10 import javax.swing.ImageIcon;1112 public class MarcoCuadroCombinado extends JFrame 13 {

14 private JComboBox imagenesJComboBox; // cuadro combinado con los nombres de los iconos

15 private JLabel etiqueta; // etiqueta para mostrar el icono seleccionado1617 private static final String nombres[] = 18 { “insecto1.gif”, “insecto2.gif”, “insectviaje.gif”, “insectanim.gif” };19 private Icon iconos[] = { 20 new ImageIcon( getClass().getResource( nombres[ 0 ] ) ),21 new ImageIcon( getClass().getResource( nombres[ 1 ] ) ), 22 new ImageIcon( getClass().getResource( nombres[ 2 ] ) ),23 new ImageIcon( getClass().getResource( nombres[ 3 ] ) ) };2425 // El constructor de MarcoCuadroCombinado agrega un objeto JComboBox a JFrame26 public MarcoCuadroCombinado()27 {28 super( “Prueba de JComboBox” );29 setLayout( new FlowLayout() ); // establece el esquema del marco 3031 imagenesJComboBox = new JComboBox( nombres ); // establece JComboBox32 imagenesJComboBox.setMaximumRowCount( 3 ); // muestra tres filas3334 imagenesJComboBox.addItemListener(35 new ItemListener() // clase interna anónima36 {37 // maneja evento de JComboBox38 public void itemStateChanged( ItemEvent evento )39 {40 // determina si está seleccionada la casilla de verificación41 if ( evento.getStateChange() == ItemEvent.SELECTED )42 etiqueta.setIcon( iconos[ 43 imagenesJComboBox.getSelectedIndex() ] );44 } // fin del método itemStateChanged45 } // fin de la clase interna anónima46 ); // fin de la llamada a addItemListener

582 Capítulo 14 Componentes de la GUI: Parte 1

En la línea 31, el constructor inicializa un objeto JComboBox utilizando los objetos String en el arreglo nombres como los elementos en la lista. Cada elemento de la lista tiene un índice. El primer elemento se agrega en el índice 0; el siguiente elemento se agrega en el índice 1, y así sucesivamente. El primer elemento que se agrega a un objeto JComboBox aparece como el elemento actualmente selec-cionado al mostrar el objeto JComboBox. Los otros elementos se seleccionan haciendo clic en el objeto JComboBox, y después seleccionando un elemento de la lista que aparece.

47

48 add( imagenesJComboBox ); // agrega cuadro combinado a JFrame

49 etiqueta = new JLabel( iconos[ 0 ] ); // muestra el primer icono

50 add( etiqueta ); // agrega etiqueta a JFrame

51 } // fin del constructor de MarcoCuadroCombinado

52 } // fin de la clase MarcoCuadroCombinado

Fig. 14.21 � Objeto JComboBox que muestra una lista de nombres de imágenes (parte 2 de 2).

Fig. 14.22 � Prueba de MarcoCuadroCombinado.

1 // Fig. 14.22: PruebaCuadroCombinado.java

2 // Prueba de MarcoCuadroCombinado.

3 import javax.swing.JFrame;

4

5 public class PruebaCuadroCombinado

6 {

7 public static void main( String[] args )

8 {

9 MarcoCuadroCombinado marcoCuadroCombinado = new MarcoCuadroCombinado();

10 marcoCuadroCombinado.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoCuadroCombinado.setSize( 350, 150 ); // establece el tamaño del marco

12 marcoCuadroCombinado.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase PruebaCuadroCombinado

Barra de desplazamiento que permite al usuario desplazarse a través de todos los elementos en la lista

Cuadro de desplazamiento Flechas de desplazamiento

14.11 JComboBox: uso de una clase interna anónima para el manejo de eventos 583

En la línea 32 se utiliza el método setMaximumRowCount de JComboBox para establecer el máximo número de elementos a mostrar cuando el usuario haga clic en el objeto JComboBox. Si hay elementos adicionales, el objeto JComboBox proporciona una barra de desplazamiento (vea la primera captura de pantalla) que permite al usuario desplazarse por todos los elementos en la lista. El usuario puede hacer clic en las flechas de desplazamiento que están en las partes superior e inferior de la barra de desplazamien-to para avanzar hacia arriba y hacia abajo de la lista, un elemento a la vez, o puede arrastrar hacia arriba y hacia abajo el cuadro de desplazamiento que está en medio de la barra de desplazamiento. Para arrastrar el cuadro de desplazamiento, posicione el ratón sobre éste, mantenga presionado el botón izquierdo del ratón y mueva el ratón. En este ejemplo, la lista desplegable es demasiado corta como para poder arrastrar el cuadro de desplazamiento, por lo que puede hacer clic en las flechas arriba y abajo o usar la rueda de su ratón para desplazarse por los cuatro elementos en la lista.

Observación de apariencia visual 14.12Establezca el número máximo de filas en un objeto JComboBox a un valor que evite que la lista se expanda fuera de los límites de la ventana en la que se utilice.

La línea 48 adjunta el objeto JComboBox al esquema FlowLayout de MarcoCuadroCombinado (que se establece en la línea 29). La línea 49 crea el objeto JLabel que muestra objetos ImageIcon y lo inicializa con el primer objeto ImageIcon en el arreglo iconos. La línea 50 adjunta el objeto JLabel al esquema FlowLayout de MarcoCuadroCombinado.

Uso de una clase interna anónima para el manejo de eventosLas líneas 34 a 46 son una instrucción que declara la clase del componente de escucha de eventos, crea un objeto de esa clase y registra el objeto como el componente de escucha para los eventos ItemEvent de ima-genesJComboBox. Este objeto componente de escucha de eventos es una instancia de una clase interna anónima; una clase interna que se declara sin un nombre y por lo general aparece dentro de la declaración de un método. Al igual que las demás clases internas, una clase interna anónima puede acceder a los miembros de su clase de nivel superior. Sin embargo, una clase interna anónima tiene acceso limitado a las variables locales del método en el que está declarada. Como una clase interna anónima no tiene nombre, un objeto de la clase interna anónima debe crearse en el punto en el que se declara la clase (empezando en la línea 35).

Observación de ingeniería de software 14.3Una clase interna anónima declarada en un método puede acceder a las variables de instancia y los métodos del objeto de la clase de nivel superior que la declaró, así como a las variables locales final del método, pero no puede acceder a las variables locales no final del método.

Las líneas 34 a 46 son una llamada al método addItemListener de imagenesJComboBox. El argu-mento para este método debe ser un objeto que sea un ItemListener (es decir, cualquier objeto de una clase que implemente a ItemListener). Las líneas 35 a 45 son una expresión de creación de instancias de clase que declara una clase interna anónima y crea un objeto de esa clase. Después se pasa una refe-rencia a ese objeto como argumento para addItemListener. La sintaxis ItemListener() después de new empieza la declaración de una clase interna anónima que implementa a la interfaz ItemListener. Esto es similar a empezar una declaración con

public class MiManejador implements ItemListener

La llave izquierda de apertura en la línea 36 y la llave derecha de cierre en la línea 45 delimitan el cuerpo de la clase interna anónima. Las líneas 38 a 44 declaran el método itemStateChanged de ItemListener. Cuando el usuario hace una selección de imagenesJComboBox, este método establece el objeto Icon de eti-queta. El objeto Icon se selecciona del arreglo iconos, determinando el índice del elemento seleccionado en el objeto JComboBox con el método getSelectedIndex en la línea 43. Para cada elemento seleccionado de un JComboBox, primero se deselecciona otro elemento; por lo tanto, ocurren dos eventos tipo ItemEvent

584 Capítulo 14 Componentes de la GUI: Parte 1

cuando se selecciona un elemento. Deseamos mostrar sólo el icono para el elemento que el usuario acaba de seleccionar. Por esta razón, la línea 41 determina si el método getStateChange de ItemEvent devuelve ItemEvent.SELECTED. De ser así, las líneas 42 y 43 establecen el icono de etiqueta.

Observación de ingeniería de software 14.4Al igual que cualquier otra clase, cuando una clase interna anónima implementa a una interfaz, la clase debe implementar todos los métodos en la interfaz.

La sintaxis que se muestra en las líneas 35 a 45 para crear un manejador de eventos con una clase interna anónima es similar al código que genera un entorno de desarrollo integrado (IDE) de Java. Por lo general, un IDE permite al programador diseñar una GUI en forma visual, y después el IDE genera código que implementa a la GUI. El programador sólo inserta instrucciones en los métodos maneja-dores de eventos que declaran cómo manejar cada evento.

14.12 JList Una lista muestra una serie de elementos, de la cual el usuario puede seleccionar uno o más elementos (vea la salida de la figura 14.24). Las listas se crean con la clase JList, que extiende directamente a la clase JComponent. La clase JList soporta listas de selección simple (listas que permiten seleccionar solamente un elemento a la vez) y listas de selección múltiple (listas que permiten seleccionar cual-quier número de elementos a la vez). En esta sección hablaremos sobre las listas de selección simple.

La aplicación de las figuras 14.23 y 14.24 crea un objeto JList que contiene los nombres de 13 co-lores. Al hacer clic en el nombre de un color en el objeto JList, ocurre un evento ListSelectionEvent y la aplicación cambia el color de fondo de la ventana de aplicación al color seleccionado. La clase PruebaLista (figura 14.24) contiene el método main que ejecuta esta aplicación.

Fig. 14.23 � Objeto JList que muestra una lista de colores (parte 1 de 2).

1 // Fig. 14.23: MarcoLista.java

2 // Objeto JList que muestra una lista de colores.

3 import java.awt.FlowLayout;

4 import java.awt.Color;

5 import javax.swing.JFrame;

6 import javax.swing.JList;

7 import javax.swing.JScrollPane;

8 import javax.swing.event.ListSelectionListener;

9 import javax.swing.event.ListSelectionEvent;

10 import javax.swing.ListSelectionModel;

11

12 public class MarcoLista extends JFrame

13 {

14 private JList listaJListColores; // lista para mostrar colores

15 private static final String[] nombresColores = { “Negro”, “Azul”, “Cyan”,

16 “Gris oscuro”, “Gris”, “Verde”, “Gris claro”, “Magenta”,

17 “Naranja”, “Rosa”, “Rojo”, “Blanco”, “Amarillo” };

18 private static final Color[] colores = { Color.BLACK, Color.BLUE,

19 Color.CYAN, Color.DARK_GRAY, Color.GRAY, Color.GREEN,

20 Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK,

21 Color.RED, Color.WHITE, Color.YELLOW };

22

14.12 JList 585

Fig. 14.24 � Clase de prueba para MarcoLista.

23 // El constructor de MarcoLista agrega a JFrame el JScrollPane que contiene a JList24 public MarcoLista()25 {26 super( “Prueba de JList” );27 setLayout( new FlowLayout() ); // establece el esquema del marco2829 listaJListColores = new JList( nombresColores ); // crea con nombresColores30 listaJListColores.setVisibleRowCount( 5 ); // muestra cinco filas a la vez3132 // no permite selecciones múltiples33 listaJListColores.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );3435 // agrega al marco un objeto JScrollPane que contiene a JList36 add( new JScrollPane( listaJListColores ) );3738 listaJListColores.addListSelectionListener(39 new ListSelectionListener() // clase interna anónima40 { 41 // maneja los eventos de selección de la lista42 public void valueChanged( ListSelectionEvent evento )43 {44 getContentPane().setBackground( 45 colores[ listaJListColores.getSelectedIndex() ] );46 } // fin del método valueChanged47 } // fin de la clase interna anónima48 ); // fin de la llamada a addListSelectionListener49 } // fin del constructor de MarcoLista50 } // fin de la clase MarcoLista

1 // Fig. 14.24: PruebaLista.java 2 // Selección de colores de un objeto JList. 3 import javax.swing.JFrame; 4 5 public class PruebaLista 6 { 7 public static void main( String[] args ) 8 { 9 MarcoLista marcoLista = new MarcoLista(); // crea objeto MarcoLista10 marcoLista.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoLista.setSize( 350, 150 ); // establece el tamaño del marco12 marcoLista.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase PruebaLista

Fig. 14.23 � Objeto JList que muestra una lista de colores (parte 2 de 2).

586 Capítulo 14 Componentes de la GUI: Parte 1

La línea 29 (figura 14.23) crea el objeto listaJListColores llamado JList. El argumento para el constructor de JList es el arreglo de objetos Object (en este caso, objetos String) a mostrar en la lista. La línea 30 utiliza el método setVisibleRowCount de JList para determinar el número de elementos visibles en la lista.

La línea 33 utiliza el método setSelectionMode de JList para especificar el modo de selección de la lista. La clase ListSelectionModel (del paquete javax.swing) declara tres constantes que especifican el modo de selección de un objeto JList: SINGLE_SELECTION (que sólo permite seleccionar un elemento a la vez), SINGLE_INTERVAL_SELECTION (para una lista de selección múltiple que permite seleccionar varios elementos contiguos) y MULTIPLE_INTERVAL_SELECTION (para una lista de selección múltiple que no restringe los elementos que se pueden seleccionar).

A diferencia de un objeto JComboBox, un objeto JList no proporciona una barra de desplazamiento si hay más elementos en la lista que el número de filas visibles. En este caso se utiliza un objeto JScroll-Pane para proporcionar la capacidad de desplazamiento. En la línea 36 se agrega una nueva instancia de la clase JScrollPane al objeto JFrame. El constructor de JScrollPane recibe como argumento el objeto JComponent que necesita funcionalidad de desplazamiento (en este caso, listaJListColores). Observe en las capturas de pantalla que aparece una barra de desplazamiento creada por el objeto JScrollPane en el lado derecho del objeto JList. De manera predeterminada, la barra de desplazamiento sólo aparece cuando el número de elementos en el objeto JList excede al número de elementos visibles.

Las líneas 38 a 48 usan el método addListSelectionListener de JList para registrar un objeto que implementa a ListSelectionListener (paquete javax.swing.event) como el componente de escucha para los eventos de selección de JList. Una vez más, utilizamos una instancia de una clase in-terna anónima (líneas 39 a 47) como el componente de escucha. En este ejemplo, cuando el usuario realiza una selección de listaJListColores, el método valueChanged (línea 42 a 46) debería cambiar el color de fondo del objeto MarcoLista al color seleccionado. Esto se logra en las líneas 44 y 45. Ob-serve el uso del método getContentPane de JFrame en la línea 44. Cada objeto JFrame en realidad consiste de tres niveles: el fondo, el panel de contenido y el panel de vidrio. El panel de contenido apa-rece en frente del fondo, y es en donde se muestran los componentes de la GUI en el objeto JFrame. El panel de vidrio se utiliza para mostrar cuadros de información sobre herramientas y otros elementos que deben aparecer enfrente de los componentes de la GUI en la pantalla. El panel de contenido oculta por completo el fondo del objeto JFrame; por ende, para cambiar el color de fondo detrás de los componen-tes de la GUI, debe cambiar el color de fondo del panel de contenido. El método getContentPane de-vuelve una referencia al panel de contenido del objeto JFrame (un objeto de la clase Container). En la línea 44, después usamos esa referencia para llamar al método setBackground, el cual establece el color de fondo del panel de contenido a un elemento en el arreglo colores. El color se selecciona del arreglo mediante el uso del índice del elemento seleccionado. El método getSelectedIndex de JList devuelve el índice del elemento seleccionado. Al igual que con los arreglos y los objetos JComboBox, los índices en los objetos JList están basados en cero.

14.13 Listas de selección múltipleUna lista de selección múltiple permite al usuario seleccionar varios elementos de un objeto JList (vea la salida de la figura 14.26). Una lista SINGLE_INTERVAL_SELECTION permite la selección de un rango con-tiguo de elementos. Para ello, haga clic en el primer elemento y después oprima (y mantenga oprimida) la tecla Mayús mientras hace clic en el último elemento a seleccionar en el rango. Una lista MULTIPLE_IN-TERVAL_SELECTION (la opción predeterminada) permite una selección de rango continuo, como se des-cribe para una lista SINGLE_INTERVAL_SELECTION. Dicha lista también permite que se seleccionen diver-sos elementos, oprimiendo y manteniendo oprimida la tecla Ctrl mientras hace clic en cada elemento a seleccionar. Para deseleccionar un elemento, oprima y mantenga oprimida la tecla Ctrl mientras hace clic en el elemento por segunda vez.

La aplicación de las figuras 14.25 y 14.26 utiliza listas de selección múltiple para copiar elemen-tos de un objeto JList a otro. Una lista es de tipo MULTIPLE_INTERVAL_SELECTION y la otra es de tipo

14.13 Listas de selección múltiple 587

SINGLE_INTERVAL_SELECTION. Cuando ejecute la aplicación, trate de usar las técnicas de selección descritas anteriormente para seleccionar elementos en ambas listas.

Fig. 14.25 � Objeto JList que permite selecciones múltiples (parte 1 de 2).

1 // Fig. 14.25: MarcoSeleccionMultiple.java 2 // Copiar elementos de un objeto List a otro. 3 import java.awt.FlowLayout; 4 import java.awt.event.ActionListener; 5 import java.awt.event.ActionEvent; 6 import javax.swing.JFrame; 7 import javax.swing.JList; 8 import javax.swing.JButton; 9 import javax.swing.JScrollPane;10 import javax.swing.ListSelectionModel;1112 public class MarcoSeleccionMultiple extends JFrame 13 {14 private JList listaJListColores; // lista para guardar los nombres de los colores

15 private JList listaJListCopia; // lista en la que se van a copiar los nombres de los colores

16 private JButton botonJButtonCopiar; // botón para copiar los nombres seleccionados17 private final String[] nombresColores = { “Negro”, “Azul”, “Cyan”, 18 “Gris oscuro”, “Gris”, “Verde”, “Gris claro”, “Magenta”, “Naranja”, 19 “Rosa”, “Rojo”, “Blanco”, “Amarillo” };2021 // Constructor de MarcoSeleccionMultiple22 public MarcoSeleccionMultiple()23 {24 super( “Listas de seleccion multiple” );25 setLayout( new FlowLayout() ); // establece el esquema del marco26

27 listaJListColores = new JList( nombresColores ); // contiene nombres de todos los colores

28 listaJListColores.setVisibleRowCount( 5 ); // muestra cinco filas29 listaJListColores.setSelectionMode( 30 ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );

31 add( new JScrollPane( listaJListColores ) ); // agrega lista con panel de desplazamiento

3233 botonJButtonCopiar = new JButton( “Copiar >>>” ); // crea botón para copiar34 botonJButtonCopiar.addActionListener(3536 new ActionListener() // clase interna anónima 37 { 38 // maneja evento de botón39 public void actionPerformed( ActionEvent evento )40 {41 // coloca los valores seleccionados en listaJListCopia42 listaJListCopia.setListData( listaJListColores.getSelectedValues() );43 } // fin del método actionPerformed44 } // fin de la clase interna anónima45 ); // fin de la llamada a addActionListener4647 add( botonJButtonCopiar ); // agrega el botón copiar a JFrame48

49 listaJListCopia = new JList(); // crea lista para guardar nombres de colores copiados

588 Capítulo 14 Componentes de la GUI: Parte 1

En la línea 27 de la figura 14.25 se crea el objeto JList llamado listaJListColores y se iniciali-za con las cadenas en el arreglo nombresColores. En la línea 28 se establece el número de filas visibles en listaJListColores a 5. En las líneas 29 y 30 se especifica que listaJListColores es una lista de tipo MULTIPLE_INTERVAL_SELECTION. En la línea 31 se agrega un nuevo objeto JScrollPane, que contiene listaJListColores, al panel JFrame. En las líneas 49 a 55 se realizan tareas similares para listaJListCopia, la cual se declara como una lista tipo SINGLE_INTERVAL_SELECTION. Si un objeto JList no contiene elementos, no se mostrará en un esquema FlowLayout. Por esta razón, en las líneas 51 y 52 se utilizan los métodos setFixedCellWidth y setFixedCellHeigth de JList para establecer la anchura de listaJListCopia en 100 píxeles y la altura de cada elemento en el objeto JList a 15 píxeles, respectivamente.

Por lo general, un evento generado por otro componente de la GUI (lo que se conoce como un even-to externo) especifica cuándo deben procesarse las selecciones múltiples en un objeto JList. En este ejemplo, el usuario hace clic en el objeto JButton llamado botonJButtonCopiar para desencadenar el evento que copia los elementos seleccionados en listaJListColores a listaJListCopia.

50 listaJListCopia.setVisibleRowCount( 5 ); // muestra 5 filas51 listaJListCopia.setFixedCellWidth( 100 ); // establece la anchura52 listaJListCopia.setFixedCellHeight( 15 ); // establece la altura53 listaJListCopia.setSelectionMode( 54 ListSelectionModel.SINGLE_INTERVAL_SELECTION );

55 add( new JScrollPane( listaJListCopia ) ); // agrega lista con panel de desplazamiento

56 } // fin del constructor de MarcoSeleccionMultiple57 } // fin de la clase MarcoSeleccionMultiple

Fig. 14.25 � Objeto JList que permite selecciones múltiples (parte 2 de 2).

Fig. 14.26 � Clase de prueba para MarcoSeleccionMultiple.

1 // Fig. 14.26: PruebaSeleccionMultiple.java

2 // Prueba de MarcoSeleccionMultiple.

3 import javax.swing.JFrame;

4

5 public class PruebaSeleccionMultiple

6 {

7 public static void main( String[] args )

8 {

9 MarcoSeleccionMultiple marcoSeleccionMultiple =

10 new MarcoSeleccionMultiple();

11 marcoSeleccionMultiple.setDefaultCloseOperation(

12 JFrame.EXIT_ON_CLOSE );

13 marcoSeleccionMultiple.setSize( 350, 140 ); // establece el tamaño del marco

14 marcoSeleccionMultiple.setVisible( true ); // muestra el marco

15 } // fin de main

16 } // fin de la clase PruebaSeleccionMultiple

14.14 Manejo de eventos de ratón 589

Las líneas 34 a 45 declaran, crean y registran un objeto ActionListener para el objeto boton-JButtonCopiar. Cuando el usuario hace clic en botonJButtonCopiar, el método actionPerformed (líneas 39 a 43) utiliza el método setListData de JList para establecer los elementos mostrados en listaJListCopia. En la línea 42 se hace una llamada al método getSelectedValues de listaJListCo-lores, el cual devuelve un arreglo de objetos Object que representan los elementos seleccionados en listaJListColores. En este ejemplo, el arreglo devuelto se pasa como argumento al método setList-Data de listaJListCopia.

Tal vez se pregunte por qué puede usarse listaJListCopia en la línea 42, aun cuando la aplicación no crea el objeto al cual hace referencia sino hasta la línea 49. Recuerde que el método actionPer-formed (líneas 39 a 43) no se ejecuta sino hasta que el usuario oprime el botón botonJButtonCopiar, lo cual no puede ocurrir sino hasta que el constructor termine su ejecución y la aplicación muestre la GUI. En ese punto en la ejecución de la aplicación, listaJListCopia ya se ha inicializado con un nuevo objeto JList.

14.14 Manejo de eventos de ratónEn esta sección presentaremos las interfaces de escucha de eventos MouseListener y MouseMotionLis-tener para manejar eventos de ratón. Estos eventos pueden procesarse para cualquier componente de la GUI que se derive de java.awt.Component. Los métodos de las interfaces MouseListener y Mouse-MotionListener se sintetizan en la figura 14.27. El paquete javax.swing.event contiene la interfaz MouseInputListener, la cual extiende a las interfaces MouseListener y MouseMotionListener para crear una sola interfaz que contiene todos los métodos de MouseListener y MouseMotionListener. Estos métodos se llaman cuando el ratón interactúa con un objeto Component, si se registran objetos componentes de escucha de eventos para ese objeto Component.

Cada uno de los métodos manejadores de eventos de ratón recibe un objeto MouseEvent como su argumento, el cual contiene información acerca del evento de ratón que ocurrió, incluyendo las coor-denadas x y y de su ubicación. Estas coordenadas se miden desde la esquina superior izquierda del com-ponente de la GUI en el que ocurrió el evento. Las coordenadas x empiezan en 0 y se incrementan de izquierda a derecha. Las coordenadas y empiezan en 0 y se incrementan de arriba hacia abajo. Además, los métodos y constantes de la clase InputEvent (superclase de MouseEvent) permiten a una aplicación determinar cuál fue el botón del ratón que oprimió el usuario.

Métodos de las interfaces MouseListener y MouseMotionListener

Métodos de la interfaz MouseListener

public void mousePressed( MouseEvent evento )

Se llama cuando se oprime un botón del ratón, mientras el cursor del ratón está sobre un componente.

public void mouseClicked( MouseEvent evento )

Se llama cuando se oprime y suelta un botón del ratón, mientras el cursor del ratón permanece estacionario sobre un componente. Este evento siempre va precedido por una llamada a mousePressed.

public void mouseReleased( MouseEvent evento )

Se llama cuando se suelta un botón de ratón después de ser oprimido. Este evento siempre va precedido por una llamada a mousePressed y por una o más llamadas a mouseDragged.

Fig. 14.27 � Métodos de las interfaces MouseListener y MouseMotionListener (parte 1 de 2).

590 Capítulo 14 Componentes de la GUI: Parte 1

Métodos de las interfaces MouseListener y MouseMotionListener

public void mouseEntered( MouseEvent evento )

Se llama cuando el cursor del ratón entra a los límites de un componente.

public void mouseExited( MouseEvent evento )

Se llama cuando el cursor del ratón sale de los límites de un componente.

Métodos de la interfaz MouseMotionListener

public void mouseDragged( MouseEvent evento )

Se llama cuando el botón del ratón se oprime mientras el cursor del ratón se encuentra sobre un componente y el ratón se mueve mientras el botón sigue oprimido. Este evento siempre va precedido por una llamada a mousePressed. Todos los eventos de arrastre del ratón se envían al componente en el cual empezó la acción de arrastre.

public void mouseMoved( MouseEvent evento )

Se llama al moverse el ratón (sin oprimir los botones del ratón) cuando su cursor se encuentra sobre un componente. Todos los eventos de movimiento se envían al componente sobre el cual se encuentra el ratón posicionado en ese momento.

Observación de ingeniería de software 14.5Las llamadas a los métodos mouseDragged se envían al objeto MouseMotionListener para el objeto Component en el que empezó la operación de arrastre. De manera similar, la llamada al método mouseReleased al final de una operación de arrastre se envía al objeto MouseListener para el objeto Component en el que empezó la operación de arrastre.

Java también cuenta con la interfaz MouseWheelListener para permitir a las aplicaciones responder a la rotación del disco en un ratón. Esta interfaz declara el método mouseWheelMoved, el cual recibe un evento MouseWheelEvent como argumento. La clase MouseWheelEvent (una subclase de MouseEvent) contiene métodos que permiten al manejador de eventos obtener información acerca de la cantidad de rotación del disco.

Cómo rastrear eventos de ratón en un objeto JPanelLa aplicación RastreadorRaton (figuras 14.28 y 14.29) demuestra el uso de los métodos de las interfaces MouseListener y MouseMotionListener. La clase de manejador de evento (líneas 36 a 90) implementa ambas interfaces. Usted debe declarar los siete métodos de estas dos interfaces cuando una clase imple-menta ambas interfaces. Cada evento de ratón en este ejemplo muestra una cadena en el objeto JLabel llamado barraEstado, en la parte inferior de la ventana.

Fig. 14.28 � Manejo de eventos de ratón (parte 1 de 3).

1 // Fig. 14.28: MarcoRastreadorRaton.java

2 // Demostración de los eventos de ratón.

3 import java.awt.Color;

4 import java.awt.BorderLayout;

5 import java.awt.event.MouseListener;

6 import java.awt.event.MouseMotionListener;

7 import java.awt.event.MouseEvent;

Fig. 14.27 � Métodos de las interfaces MouseListener y MouseMotionListener (parte 2 de 2).

14.14 Manejo de eventos de ratón 591

Fig. 14.28 � Manejo de eventos de ratón (parte 2 de 3).

8 import javax.swing.JFrame; 9 import javax.swing.JLabel;10 import javax.swing.JPanel;1112 public class MarcoRastreadorRaton extends JFrame13 {14 private JPanel panelRaton; // panel en el que ocurrirán los eventos de ratón15 private JLabel barraEstado; // etiqueta que muestra información de los eventos1617 // El constructor de MarcoRastreadorRaton establece la GUI y 18 // registra los manejadores de eventos de ratón19 public MarcoRastreadorRaton()20 {21 super( “Demostracion de los eventos de raton” );2223 panelRaton = new JPanel(); // crea el panel24 panelRaton.setBackground( Color.WHITE ); // establece el color de fondo25 add( panelRaton, BorderLayout.CENTER ); // agrega el panel a JFrame2627 barraEstado = new JLabel( “Raton fuera de JPanel” ); 28 add( barraEstado, BorderLayout.SOUTH ); // agrega etiqueta a JFrame29

30 // crea y registra un componente de escucha para los eventos de ratón y de su movimiento

31 ManejadorRaton manejador = new ManejadorRaton(); 32 panelRaton.addMouseListener( manejador ); 33 panelRaton.addMouseMotionListener( manejador ); 34 } // fin del constructor de MarcoRastreadorRaton3536 private class ManejadorRaton implements MouseListener, 37 MouseMotionListener 38 {39 // Los manejadores de eventos de MouseListener40 // manejan el evento cuando se suelta el ratón justo después de oprimir el botón41 public void mouseClicked( MouseEvent evento )42 {43 barraEstado.setText( String.format( “Se hizo clic en [%d, %d]”, 44 evento.getX(), evento.getY() ) );45 } // fin del método mouseClicked4647 // maneja evento cuando se oprime el ratón48 public void mousePressed( MouseEvent evento )49 {50 barraEstado.setText( String.format( “Se oprimio en [%d, %d]”, 51 evento.getX(), evento.getY() ) );52 } // fin del método mousePressed5354 // maneja evento cuando se suelta el botón del ratón después de arrastrarlo55 public void mouseReleased( MouseEvent evento )56 {57 barraEstado.setText( String.format( “Se solto en [%d, %d]”, 58 evento.getX(), evento.getY() ) );59 } // fin del método mouseReleased60

592 Capítulo 14 Componentes de la GUI: Parte 1

La línea 23 en la figura 14.28 crea el objeto JPanel llamado panelRaton. Los eventos de ratón de este objeto JPanel serán rastreados por la aplicación. En la línea 24 se establece el color de fondo de panelRaton a blanco. Cuando el usuario mueva el ratón hacia el panelRaton, la aplicación cambiará el color de fondo de panelRaton a verde. Cuando el usuario mueva el ratón hacia fuera del panelRaton, la aplicación cambiará el color de fondo de vuelta a blanco. En la línea 25 se adjunta el objeto panel-Raton al objeto JFrame. Como vimos en la sección 14.5, por lo general debemos especificar el esquema de los componentes de GUI en un objeto JFrame. En esa sección presentamos el administrador de esquemas FlowLayout. Aquí utilizamos el esquema predeterminado del panel de contenido de un objeto JFrame: BorderLayout. Este administrador de esquemas ordena los componentes en cinco re-giones: NORTH, SOUTH, EAST, WEST y CENTER. NORTH corresponde a la parte superior del contenedor. Este ejemplo utiliza las regiones CENTER y SOUTH. En la línea 25 se utiliza una versión con dos argumentos del método add para colocar a panelRaton en la región CENTER. El esquema BorderLayout ajusta automáticamente el tamaño del componente en la región CENTER para utilizar todo el espacio en el objeto JFrame que no esté ocupado por los componentes de otras regiones. En la sección 14.18.2 hablaremos sobre BorderLayout con más detalle.

En las líneas 27 y 28 del constructor se declara el objeto JLabel llamado barraEstado y se adjunta a la región SOUTH del objeto JFrame. Este objeto JLabel ocupa la anchura del objeto JFrame. La altura de la región se determina en base al objeto JLabel.

Fig. 14.28 � Manejo de eventos de ratón (parte 3 de 3).

61 // maneja evento cuando el ratón entra al área

62 public void mouseEntered( MouseEvent evento )

63 {

64 barraEstado.setText( String.format( “Raton entro en [%d, %d]”,

65 evento.getX(), evento.getY() ) );

66 panelRaton.setBackground( Color.GREEN );

67 } // fin del método mouseEntered

68

69 // maneja evento cuando el ratón sale del área

70 public void mouseExited( MouseEvent evento )

71 {

72 barraEstado.setText( “Raton fuera de JPanel” );

73 panelRaton.setBackground( Color.WHITE );

74 } // fin del método mouseExited

75

76 // Los manejadores de eventos de MouseMotionListener manejan

77 // el evento cuando el usuario arrastra el ratón con el botón oprimido

78 public void mouseDragged( MouseEvent evento )

79 {

80 barraEstado.setText( String.format( “Se arrastro en [%d, %d]”,

81 evento.getX(), evento.getY() ) );

82 } // fin del método mouseDragged

83

84 // maneja evento cuando el usuario mueve el ratón

85 public void mouseMoved( MouseEvent evento )

86 {

87 barraEstado.setText( String.format( “Se movio en [%d, %d]”,

88 evento.getX(), evento.getY() ) );

89 } // fin del método mouseMoved

90 } // fin de la clase interna ManejadorRaton

91 } // fin de la clase MarcoRastreadorRaton

14.14 Manejo de eventos de ratón 593

En la línea 31 se crea una instancia de la clase interna ManejadorRaton (líneas 36 a 90) llamada manejador, la cual responde a los eventos de ratón. En las líneas 32 y 33 se registra manejador como el componente de escucha para los eventos de ratón de panelRaton. Los métodos addMouseListener y addMouseMotionListener se heredan indirectamente de la clase Component, y pueden utilizarse para registrar objetos MouseListener y MouseMotionListener, respectivamente. Un objeto Manejador-Raton es un MouseListener y es un MouseMotionListener, ya que la clase implementa ambas inter-faces. Optamos por implementar ambas interfaces aquí para demostrar una clase que implementa más de una interfaz, pero también pudimos haber implementado la interfaz MouseInputListener en su defecto.

Cuando el ratón entra y sale del área de panelRaton, se hacen llamadas a los métodos mouseEnte-red (líneas 62 a 67) y mouseExited (líneas 70 a 74), respectivamente. El método mouseEntered mues-tra un mensaje en el objeto barraEstado, indicando que el ratón entró al objeto JPanel y cambia el color de fondo a verde. El método mouseExited muestra un mensaje en el objeto barraEstado, indi-cando que el ratón está fuera del objeto JPanel (vea la primera ventana de resultados de ejemplo) y cambia el color de fondo a blanco.

Los otros cinco eventos muestran una cadena en el objeto barraEstado, la cual contiene el evento y las coordenadas en las que ocurrió. Los métodos getX y getY de MouseEvent devuelven las coordena-das x y y, respectivamente, del ratón en el momento en el que ocurrió el evento.

Fig. 14.29 � Clase de prueba para MarcoRastreadorBoton.

1 // Fig. 14.29: MarcoRastreadorRaton.java 2 // Prueba de MarcoRastreadorRaton. 3 import javax.swing.JFrame; 4 5 public class RastreadorRaton 6 { 7 public static void main( String[] args ) 8 { 9 MarcoRastreadorRaton marcoRastreadorRaton = new MarcoRastreadorRaton(); 10 marcoRastreadorRaton.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoRastreadorRaton.setSize( 300, 100 ); // establece el tamaño del marco12 marcoRastreadorRaton.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase RastreadorRaton

594 Capítulo 14 Componentes de la GUI: Parte 1

14.15 Clases adaptadorasMuchas de las interfaces de escucha de eventos, como MouseListener y MouseMotionListener, con-tienen varios métodos. No siempre es deseable declarar todos los métodos en una interfaz de escucha de eventos. Por ejemplo, una aplicación podría necesitar solamente el manejador mouseClicked de la interfaz MouseListener, o el manejador mouseDragged de la interfaz MouseMotionListener. La interfaz WindowListener especifica siete métodos manejadores de eventos de ventana. Para muchas de las interfaces de escucha de eventos que contienen varios métodos, los paquetes java.awt.event y javax.swing.event proporcionan clases adaptadoras de escucha de eventos. Una clase adaptadora implementa a una interfaz y proporciona una implementación predeterminada (con un cuerpo vacío para los métodos) de todos los métodos en la interfaz. En la figura 14.30 se muestran varias clases adaptadoras de java.awt.event, junto con las interfaces que implementan. Usted puede extender una clase adaptadora para heredar la implementación predeterminada de cada método, y en conse-cuencia sobrescribir sólo el(los) método(s) que necesite para manejar eventos.

Observación de ingeniería de software 14.6Cuando una clase implementa a una interfaz, la clase tiene una relación del tipo “es un” con esa interfaz. Todas las subclases directas e indirectas de esa clase heredan esta interfaz. Por lo tanto, un objeto de una clase que extiende a una clase adaptadora de eventos es un objeto del tipo de escucha de eventos correspondiente (por ejemplo, un objeto de una subclase de MouseAdapter es un MouseListener).

Clase adaptadora de eventos en java.awt.event Implementa a la interfaz

ComponentAdapter ComponentListener

ContainerAdapter ContainerListener

FocusAdapter FocusListener

KeyAdapter KeyListener

MouseAdapter MouseListener

MouseMotionAdapter MouseMotionListener

WindowAdapter WindowListener

Extensión de MouseAdapterLa aplicación de las figuras 14.31 y 14.32 demuestra cómo determinar el número de clics del ratón (es decir, la cuenta de clics) y cómo diferenciar los distintos botones del ratón. El componente de es-cucha de eventos en esta aplicación es un objeto de la clase interna ManejadorClicRaton (líneas 25 a 45) que extiende a MouseAdapter, por lo que podemos declarar sólo el método mouseClicked que nece-sitamos en este ejemplo.

Fig. 14.30 � Las clases adaptadoras de eventos y las interfaces que implementan en el paquete java.awt.event.

Fig. 14.31 � Clics de los botones izquierdo, central y derecho del ratón (parte 1 de 2).

1 // Fig. 14.31: MarcoDetallesRaton.java

2 // Demostración de los clics del ratón y cómo diferenciar los botones del mismo.

3 import java.awt.BorderLayout;

4 import java.awt.event.MouseAdapter;

14.15 Clases adaptadoras 595

Fig. 14.31 � Clics de los botones izquierdo, central y derecho del ratón (parte 2 de 2).

Fig. 14.32 � Clase de prueba para MarcoDetallesRaton (parte 1 de 2).

5 import java.awt.event.MouseEvent; 6 import javax.swing.JFrame; 7 import javax.swing.JLabel; 8 9 public class MarcoDetallesRaton extends JFrame 10 {11 private String detalles; // String que representa al objeto JLabel12 private JLabel barraEstado; // JLabel que aparece en la parte inferior de la ventana13

14 // constructor establece String de la barra de título y registra componente de escucha del ratón

15 public MarcoDetallesRaton()16 {17 super( “Clics y botones del raton” );1819 barraEstado = new JLabel( “Haga clic en el raton” );20 add( barraEstado, BorderLayout.SOUTH );21 addMouseListener( new ManejadorClicRaton() ); // agrega el manejador22 } // fin del constructor de MarcoDetallesRaton2324 // clase interna para manejar los eventos del ratón25 private class ManejadorClicRaton extends MouseAdapter 26 {27 // maneja evento de clic del ratón y determina cuál botón se oprimió28 public void mouseClicked( MouseEvent evento )29 {30 int xPos = evento.getX(); // obtiene posición x del ratón31 int yPos = evento.getY(); // obtiene posición y del ratón3233 detalles = String.format( “Se hizo clic %d vez(veces)”,34 evento.getClickCount() );3536 if ( evento.isMetaDown() ) // botón derecho del ratón37 detalles += “ con el boton derecho del raton”;38 else if ( evento.isAltDown() ) // botón central del ratón39 detalles += “ con el boton central del raton”;40 else // botón izquierdo del ratón41 detalles += “ con el boton izquierdo del raton”;4243 barraEstado.setText( detalles ); // muestra mensaje en barraEstado44 } // fin del método mouseClicked45 } // fin de la clase interna privada ManejadorClicRaton46 } // fin de la clase MarcoDetallesRaton

1 // Fig. 14.32: DetallesRaton.java

2 // Prueba de MarcoDetallesRaton.

3 import javax.swing.JFrame;

4

5 public class DetallesRaton

6 {

596 Capítulo 14 Componentes de la GUI: Parte 1

Error común de programación 14.4Si extiende una clase adaptadora y escribe de manera incorrecta el nombre del método que está sobrescribiendo, su método simplemente se vuelve otro método en la clase. Éste es un error lógico difícil de detectar, ya que el programa llamará a la versión vacía del método heredado de la clase adaptadora.

Un usuario de una aplicación en Java puede estar en un sistema con un ratón de uno, dos o tres botones. Java cuenta con un mecanismo para diferenciar cada uno de los botones del ratón. La clase MouseEvent hereda varios métodos de la clase InputEvent que pueden diferenciar los botones del ratón en un ratón con varios botones, o pueden imitar un ratón de varios botones con una combina-ción de teclas y un clic del botón del ratón. La figura 14.33 muestra los métodos de InputEvent que se utilizan para diferenciar los clics de los botones del ratón. Java asume que cada ratón contiene un botón izquierdo del ratón. Por ende, es fácil probar un clic del botón izquierdo del ratón. Sin embar-go, los usuarios con un ratón de uno o dos botones deben usar una combinación de teclas y clics con el botón del ratón al mismo tiempo, para simular los botones que éste no tenga. En el caso de un ratón con uno o dos botones, una aplicación de Java asume que se hizo clic en el botón central del ratón, si el usuario mantiene oprimida la tecla Alt y hace clic en el botón izquierdo en un ratón con dos boto-nes, o en el único botón en un ratón con un botón. En el caso de un ratón con un botón, una aplica-ción de Java asume que se hizo clic en el botón derecho si el usuario mantiene oprimida la tecla Meta (algunas veces conocida como la tecla de Comando, o la tecla de la “Manzana” en la Mac) y hace clic en el botón del ratón.

7 public static void main( String[] args )

8 {

9 MarcoDetallesRaton marcoDetallesRaton = new MarcoDetallesRaton();

10 marcoDetallesRaton.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoDetallesRaton.setSize( 400, 150 ); // establece el tamaño del marco

12 marcoDetallesRaton.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase DetallesRaton

Fig. 14.32 � Clase de prueba para MarcoDetallesRaton (parte 2 de 2).

14.16 Subclase de JPanel para dibujar con el ratón 597

Método InputEvent Descripción

isMetaDown() Devuelve true cuando el usuario hace clic en el botón derecho del ratón, en un ratón con dos o tres botones. Para simular un clic con el botón derecho del ratón en un ratón con un botón, el usuario puede mantener oprimida la tecla Meta en el teclado y hacer clic con el botón del ratón.

isAltDown() Devuelve true cuando el usuario hace clic con el botón central del ratón, en un ratón con tres botones. Para simular un clic con el botón central del ratón en un ratón con uno o dos botones, el usuario puede oprimir la tecla Alt en el teclado y hacer clic en el único botón o en el botón izquierdo del ratón, respectivamente.

La línea 21 de la figura 14.31 registra un objeto MouseListener para el MarcoDetallesRaton. El componente de escucha de eventos es un objeto de la clase ManejadorClicRaton, el cual extiende a MouseAdapter. Esto nos permite declarar sólo el método mouseClicked (líneas 28 a 44). Este método primero captura las coordenadas en donde ocurrió el evento y las almacena en las variables locales xPos y yPos (líneas 30 y 31). Las líneas 33 y 34 crean un objeto String llamado detalles que contiene el número de clics del ratón, el cual se devuelve mediante el método getClickCount de MouseEvent en la línea 34. Las líneas 36 a 41 utilizan los métodos isMetaDown e isAltDown para determinar cuál botón del ratón oprimió el usuario, y adjuntan un objeto String apropiado a detalles en cada caso. El objeto String resultante se muestra en la barraEstado. La clase DetallesRaton (figura 14.32) contiene el método main que ejecuta la aplicación. Pruebe haciendo clic con cada uno de los botones de su ratón repetidas veces, para ver el incremento en la cuenta de clics.

14.16 Subclase de JPanel para dibujar con el ratónLa sección 14.14 mostró cómo rastrear los eventos del ratón en un objeto JPanel. En esta sección usaremos un objeto JPanel como un área dedicada de dibujo, en la cual el usuario puede dibujar arras-trando el ratón. Además, esta sección demuestra un componente de escucha de eventos que extiende a una clase adaptadora.

Método paintComponentLos componentes ligeros de Swing que extienden a la clase JComponent (como JPanel) contienen el método paintComponent, el cual se llama cuando se muestra un componente ligero de Swing. Al sobrescribir este método, puede especificar cómo dibujar figuras usando las herramientas de gráficos de Java. Al personalizar un objeto JPanel para usarlo como un área dedicada de dibujo, la subclase debe sobrescribir el método paintComponent y llamar a la versión de paintComponent de la superclase como la primera instrucción en el cuerpo del método sobrescrito, para asegurar que el componente se muestre en forma correcta. La razón de ello es que las subclases de JComponent soportan la transparencia. Para mostrar un componente en forma correcta, el programa debe de-terminar si el componente es transparente. El código que determina esto en la implementación del método paintComponent de la superclase JComponent. Cuando un componente es transparente, paintComponent no borra su fondo cuando el programa muestra el componente. Cuando un com-ponente es opaco, paintComponent borra el fondo del componente antes de mostrarlo. La transpa-rencia de un componente ligero de Swing puede establecerse con el método setOpaque (un argumen-to false indica que el componente es transparente).

Fig. 14.33 � Métodos de InputEvent que ayudan a determinar si se hizo clic con el botón izquierdo, central o derecho del ratón.

598 Capítulo 14 Componentes de la GUI: Parte 1

Tip para prevenir errores 14.1En el método paintComponent de una subclase de JComponent, la primera instrucción siempre debe ser una llamada al método paintComponent de la superclase, para asegurar que un objeto de la subclase se muestre en forma correcta.

Error común de programación 14.5Si un método paintComponent sobrescrito no llama a la versión de la superclase, el componente de la subclase tal vez no se muestre en forma apropiada. Si un método paintComponent sobrescrito llama a la versión de la superclase después de realizar otro dibujo, éste se borra.

Definición del área de dibujo personalizadaLa aplicación Pintor de las figuras 14.34 y 14.35 demuestra una subclase personalizada de JPanel que se utiliza para crear un área dedicada de dibujo. La aplicación utiliza el manejador de eventos mouse-Dragged para crear una aplicación simple de dibujo. El usuario puede dibujar imágenes arrastrando el ratón en el objeto JPanel. Este ejemplo no utiliza el método mouseMoved, por lo que nuestra clase de escucha de eventos (la clase interna anónima en las líneas 22 a 34) extiende a MouseMotionAdapter. Como esta clase ya declara tanto a mouseMoved como mouseDragged, simplemente podemos sobrescribir a mouseDragged para proporcionar el manejo de eventos que requiere esta aplicación.

Fig. 14.34 � Clase adaptadora utilizada para implementar los manejadores de eventos (parte 1 de 2).

1 // Fig. 14.34: PanelDibujo.java

2 // Uso de la clase MouseMotionAdapter.

3 import java.awt.Point;

4 import java.awt.Graphics;

5 import java.awt.event.MouseEvent;

6 import java.awt.event.MouseMotionAdapter;

7 import javax.swing.JPanel;

8

9 public class PanelDibujo extends JPanel

10 {

11 private int cuentaPuntos = 0; // cuenta el número de puntos

12

13 // arreglo de 10000 referencias a java.awt.Point

14 private Point[] puntos = new Point[ 10000 ];

15

16 // establece la GUI y registra el manejador de eventos del ratón

17 public PanelDibujo()

18 {

19 // maneja evento de movimiento del ratón en el marco

20 addMouseMotionListener(

21

22 new MouseMotionAdapter() // clase interna anónima

23 {

24 // almacena las coordenadas de arrastre y vuelve a dibujar

25 public void mouseDragged( MouseEvent evento )

26 {

27 if ( cuentaPuntos < puntos.length )

28 {

14.16 Subclase de JPanel para dibujar con el ratón 599

La clase PanelDibujo (figura 14.34) extiende a JPanel para crear el área dedicada de dibujo. Las líneas 3 a 7 importan las clases que se utilizan en la clase PanelDibujo. La clase Point (paquete java.awt) representa una coordenada x-y. Utilizamos objetos de esta clase para almacenar las coordenadas de cada evento de arrastre del ratón. La clase Graphics se utiliza para dibujar.

En este ejemplo, utilizamos un arreglo de 10,000 objetos Point (línea 14) para almacenar la ubi-cación en la cual ocurre cada evento de arrastre del ratón. Como veremos más adelante, el método paintComponent utiliza estos objetos Point para dibujar. La variable de instancia cuentaPuntos (línea 11) mantiene el número total de objetos Point capturados de los eventos de arrastre del ratón hasta cierto punto.

Las líneas 20 a 35 registran un objeto MouseMotionListener para que escuche los eventos de mo-vimiento del ratón de PaintPanel. Las líneas 22 a 34 crean un objeto de una clase interna anónima que extiende a la clase adaptadora MouseMotionAdapter. Recuerde que MouseMotionAdapter imple-menta a MouseMotionListener, por lo que el objeto de la clase interna anónima es un MouseMotion-Listener. La clase interna anónima hereda una implementación predeterminada de los métodos mou-seMoved y mouseDragged, por lo que de antemano implementa a todos los métodos de la interfaz. Sin embargo, los métodos predeterminados no hacen nada cuando se les llama. Por lo tanto, sobrescribi-mos el método mouseDragged en las líneas 25 a 33 para capturar las coordenadas de un evento de arrastre del ratón y las almacenamos como un objeto Point. La línea 27 asegura que se almacenen las coordenadas del evento, sólo si aún hay elementos vacíos en el arreglo. De ser así, en la línea 29 se in-voca el método getPoint de MouseEvent para obtener el objeto Point en donde ocurrió el evento, y lo almacena en el arreglo, en el índice cuentaPuntos. La línea 30 incrementa la cuentaPuntos, y la línea 31 llama al método repaint (heredado directamente de la clase Component) para indicar que el objeto PanelDibujo debe actualizarse en la pantalla lo más pronto posible, con una llamada al método paintComponent de PaintPanel.

El método paintComponent (líneas 39 a 46), que recibe un parámetro Graphics, se llama de ma-nera automática cada vez que el objeto PaintPanel necesita mostrarse en la pantalla (como cuando se muestra por primera vez la GUI) o actualizarse en la pantalla (como cuando se hace una llamada al método repaint, o cuando otra ventana en la pantalla oculta el componente de la GUI y después se vuelve otra vez visible).

Fig. 14.34 � Clase adaptadora utilizada para implementar los manejadores de eventos (parte 2 de 2).

29 puntos[ cuentaPuntos ] = evento.getPoint(); // busca el punto

30 cuentaPuntos++; // incrementa el número de puntos en el arreglo

31 repaint(); // vuelve a dibujar JFrame

32 } // fin de if

33 } // fin del método mouseDragged

34 } // fin de la clase interna anónima

35 ); // fin de la llamada a addMouseMotionListener

36 } // fin del constructor de PanelDibujo

37

38 // dibuja un óvalo en un cuadro delimitador de 4 x 4, en la ubicación especificada en la ventana

39 public void paintComponent( Graphics g )

40 {

41 super.paintComponent( g ); // borra el área de dibujo

42

43 // dibuja todos los puntos en el arreglo

44 for ( int i = 0; i < cuentaPuntos; i++ )

45 g.fillOval( puntos[ i ].x, puntos[ i ].y, 4, 4 );

46 } // fin del método paintComponent

47 } // fin de la clase PanelDibujo

600 Capítulo 14 Componentes de la GUI: Parte 1

Observación de apariencia visual 14.13Una llamada a repaint para un componente de GUI de Swing indica que el compo-nente debe actualizarse en la pantalla lo más pronto que sea posible. El fondo del componen-te de GUI se borra sólo si el componente es opaco. El método setOpaque de JComponent puede recibir un argumento boolean, el cual indica si el componente es opaco (true) o transparente (false).

La línea 41 invoca a la versión de paintComponent de la superclase para borrar el fondo de PanelDi-bujo (los objetos JPanel son opacos de manera predeterminada). Las líneas 44 y 45 dibujan un óvalo en la ubicación especificada por cada objeto Point en el arreglo (hasta la cuentaPuntos). El método fillO-val de Graphics dibuja un óvalo relleno. Los cuatro parámetros del método representan un área rectan-gular (que se conoce como cuadro delimitador) en la cual se muestra el óvalo. Los primeros dos paráme-tros son la coordenada x superior izquierda y la coordenada y superior izquierda del área rectangular. Las últimas dos coordenadas representan la anchura y la altura del área rectangular. El método fillOval di-buja el óvalo de manera que esté en contacto con la parte media de cada lado del área rectangular. En la línea 45, los primeros dos argumentos se especifican mediante el uso de las dos variables de instancia public de la clase Point: x y y. El ciclo termina cuando se haya mostrado una cantidad de puntos equi-valente al valor de cuentaPuntos. En el capítulo 15 aprenderá sobre más características de Graphics.

Observación de apariencia visual 14.14La acción de dibujar en cualquier componente de GUI se lleva a cabo con coordenadas que se miden a partir de la esquina superior izquierda (0,0) de ese componente de la GUI, no de la esquina superior izquierda de la pantalla.

Uso del objeto JPanel personalizado en una aplicaciónLa clase Pintor (figura 14.35) contiene el método main que ejecuta esta aplicación. En la línea 14 se crea un objeto PanelDibujo, en el cual el usuario puede arrastrar el ratón para dibujar. En la línea 15 se adjunta el objeto PanelDibujo al objeto JFrame.

Fig. 14.35 � Clase de prueba para PanelDibujo (parte 1 de 2).

1 // Fig. 14.35: Pintor.java 2 // Prueba de PanelDibujo. 3 import java.awt.BorderLayout; 4 import javax.swing.JFrame; 5 import javax.swing.JLabel; 6 7 public class Pintor 8 { 9 public static void main( String[] args )10 { 11 // crea objeto JFrame12 JFrame aplicacion = new JFrame( “Un programa simple de dibujo” );1314 PanelDibujo panelDibujo = new PanelDibujo(); // crea panel de dibujo15 aplicacion.add( panelDibujo, BorderLayout.CENTER ); // en el centro1617 // crea una etiqueta y la coloca en la región SOUTH de BorderLayout18 aplicacion.add( new JLabel( “Arrastre el raton para dibujar” ), 19 BorderLayout.SOUTH );2021 aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

14.17 Manejo de eventos de teclas 601

14.17 Manejo de eventos de teclasEn esta sección presentamos la interfaz KeyListener para manejar eventos de teclas. Estos eventos se generan cuando se oprimen y sueltan las teclas en el teclado. Una clase que implementa a KeyListener debe proporcionar declaraciones para los métodos keyPressed, keyReleased y keyTyped, cada uno de los cuales recibe un objeto KeyEvent como argumento. La clase KeyEvent es una subclase de InputEvent. El método keyPressed es llamado en respuesta a la acción de oprimir cualquier tecla. El método keyTyped es llamado en respuesta a la acción de oprimir una tecla que no sea una tecla de acción. (Las teclas de acción son cualquier tecla de dirección, Inicio, Fin, Re Pág, Av Pág, cual-quier tecla de función, etc.) El método keyReleased es llamado cuando la tecla se suelta después de un evento keyPressed o keyTyped.

La aplicación de las figuras 14.36 y 14.37 demuestra el uso de los métodos de KeyListener. La clase MarcoDemoTeclas implementa la interfaz KeyListener, por lo que los tres métodos se declaran en la aplicación. El constructor (figuras 14.36, líneas 17 a 28) registra a la aplicación para manejar sus propios eventos de teclas, utilizando el método addKeyListener en la línea 27. Este método se declara en la clase Component, por lo que todas las subclases de Component pueden notificar a objetos KeyListener acerca de los eventos para ese objeto Component.

Fig. 14.35 � Clase de prueba para PanelDibujo (parte 2 de 2).

Fig. 14.36 � Manejo de eventos de teclas (parte 1 de 2).

1 // Fig. 14.36: MarcoDemoTeclas.java

2 // Demostración de los eventos de pulsación de teclas.

3 import java.awt.Color;

4 import java.awt.event.KeyListener;

5 import java.awt.event.KeyEvent;

6 import javax.swing.JFrame;

7 import javax.swing.JTextArea;

8

9 public class MarcoDemoTeclas extends JFrame implements KeyListener

10 {

11 private String linea1 = “”; // primera línea del área de texto

12 private String linea2 = “”; // segunda línea del área de texto

13 private String linea3 = “”; // tercera línea del área de texto

14 private JTextArea areaTexto; // área de texto para mostrar la salida

15

22 aplicacion.setSize( 400, 200 ); // establece el tamaño del marco23 aplicacion.setVisible( true ); // muestra el marco24 } // fin de main25 } // fin de la clase Pintor

602 Capítulo 14 Componentes de la GUI: Parte 1

Fig. 14.36 � Manejo de eventos de teclas (parte 2 de 2).

16 // constructor de MarcoDemoTeclas17 public MarcoDemoTeclas()

18 {

19 super( “Demostración de los eventos de pulsacion de teclas” );

2021 areaTexto = new JTextArea( 10, 15 ); // establece el objeto JTextArea

22 areaTexto.setText( “Oprima cualquier tecla en el teclado...” );

23 areaTexto.setEnabled( false ); // deshabilita el área de texto

24 areaTexto.setDisabledTextColor( Color.BLACK ); // establece el color del texto

25 add( areaTexto ); // agrega areaTexto a JFrame

2627 addKeyListener( this ); // permite al marco procesar los eventos de teclas

28 } // fin del constructor de MarcoDemoTeclas

2930 // maneja el evento de oprimir cualquier tecla

31 public void keyPressed( KeyEvent evento )

32 {

33 linea1 = String.format( “Tecla oprimida: %s”,

34 evento.getKeyText( evento.getKeyCode() ) ); // imprime la tecla oprimida

35 establecerLineas2y3( evento ); // establece las líneas de salida dos y tres

36 } // fin del método keyPressed

3738 // maneja el evento de liberar cualquier tecla

39 public void keyReleased( KeyEvent evento )

40 {

41 linea1 = String.format( “Tecla liberada: %s”,

42 evento.getKeyText( evento.getKeyCode() ) ); // imprime la tecla liberada

43 establecerLineas2y3( evento ); // establece las líneas de salida dos y tres

44 } // fin del método keyReleased

4546 // maneja el evento de oprimir una tecla de acción

47 public void keyTyped( KeyEvent evento )

48 {

49 linea1 = String.format( “Tecla oprimida: %s”, evento.getKeyChar() );

50 establecerLineas2y3( evento ); // establece las líneas de salida dos y tres

51 } // fin del método keyTyped

5253 // establece las líneas de salida dos y tres

54 private void establecerLineas2y3( KeyEvent evento )

55 {

56 linea2 = String.format( “Esta tecla %s es una tecla de accion”,

57 ( evento.isActionKey() ? “” : “no ” ) );

5859 String temp = evento.getKeyModifiersText( evento.getModifiers() );

6061 linea3 = String.format( “Teclas modificadoras oprimidas: %s”,

62 ( temp.equals( “” ) ? “ninguna” : temp ) ); // imprime modificadoras

6364 areaTexto.setText( String.format( “%s\n%s\n%s\n”,

65 linea1, linea2, linea3 ) ); // imprime tres líneas de texto

66 } // fin del método establecerLineas2y367 } // fin de la clase MarcoDemoTeclas

14.17 Manejo de eventos de teclas 603

En la línea 25, el constructor agrega el objeto JTextArea llamado areaTexto (en donde se muestra la salida de la aplicación) al objeto JFrame. Un objeto JTextArea es un área multilínea en la que pode-mos mostrar texto (hablaremos sobre los objetos JTextArea con más detalle en la sección 14.20). Observe en las capturas de pantalla que el objeto areaTexto ocupa toda la ventana. Esto se debe al es-quema predeterminado BorderLayout del objeto JFrame (que describiremos en la sección 14.18.2 y demostraremos en la figura 14.41). Cuando se agrega un objeto Component individual a un objeto BorderLayout, el objeto Component ocupa todo el objeto Container completo. La línea 23 desactiva el objeto JTextArea de modo que el usuario no pueda escribir en él. Esto hace que el texto en el objeto JTextArea se vuelva de color gris. En la línea 24 se utiliza el método setDisabledTextColor para cambiar el color del texto en el área de texto JTextArea a negro para mejorar la legibilidad.

Los métodos keyPressed (líneas 31 a 36) y keyReleased (líneas 39 a 44) utilizan el método getKeyCode de KeyEvent para obtener el código de tecla virtual de la tecla oprimida. La clase KeyEvent mantiene un conjunto de constantes que representa a todas las teclas en el teclado. Estas

Fig. 14.37 � Clase de prueba para MarcoDemoTeclas.

1 // Fig. 14.37: DemoTeclas.java 2 // Prueba de MarcoDemoTeclas. 3 import javax.swing.JFrame; 4 5 public class DemoTeclas 6 { 7 public static void main( String[] args ) 8 { 9 MarcoDemoTeclas marcoDemoTeclas = new MarcoDemoTeclas(); 10 marcoDemoTeclas.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoDemoTeclas.setSize( 350, 100 ); // establece el tamaño del marco12 marcoDemoTeclas.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase DemoTeclas

604 Capítulo 14 Componentes de la GUI: Parte 1

constantes pueden compararse con el valor de retorno de getKeyCode para probar teclas individuales en el teclado. El valor devuelto por getKeyCode se pasa al método getKeyText de KeyEvent, el cual devuelve una cadena que contiene el nombre de la tecla que se oprimió. Para obtener una lista com-pleta de las constantes de teclas virtuales, vea la documentación en línea para la clase KeyEvent (paque-te java.awt.event). El método keyTyped (líneas 47 a 51) utiliza el método getKeyChar de KeyEvent (el cual devuelve un valor char) para obtener el valor Unicode del carácter escrito.

Los tres métodos manejadores de eventos terminan llamando al método establecerLineas2y3 (lí-neas 54 a 66) y le pasan el objeto KeyEvent. Este método utiliza el método isActionKey de KeyEvent (línea 57) para determinar si la tecla en el evento fue una tecla de acción. Además, se hace una llamada al método getModifiers de InputEvent (línea 59) para determinar si se oprimió alguna tecla modificado-ra (como Mayús, Alt y Ctrl ) cuando ocurrió el evento de tecla. El resultado de este método se pasa al método getKeyModifiersText de KeyEvent, el cual produce una cadena que contiene los nombres de las teclas modificadoras que se oprimieron.

[Nota: si necesita probar una tecla específica en el teclado, la clase KeyEvent proporciona una cons-tante de tecla para cada tecla del teclado. Estas constantes pueden utilizarse desde los manejadores de eventos de teclas para determinar si se oprimió una tecla específica. Además, para determinar si las teclas Alt, Ctrl, Meta y Mayús se oprimen individualmente, cada uno de los métodos isAltDown, isControl-Down, isMetaDown e isShiftDown devuelven un valor boolean, indicando si se oprimió dicha tecla du-rante el evento de tecla].

14.18 Introducción a los administradores de esquemasLos administradores de esquemas ordenan los componentes de la GUI en un contenedor, para fines de presentación. Los programadores pueden usar los administradores de esquemas como herramien-tas básicas de distribución visual, en vez de determinar la posición y tamaño exactos de cada compo-nente de la GUI. Esta funcionalidad permite al programador concentrarse en la vista y sentido bási-cos, y deja que el administrador de esquemas procese la mayoría de los detalles de la distribución visual. Todos los administradores de esquemas implementan la interfaz LayoutManager (en el paque-te java.awt). El método setLayout de la clase Container toma un objeto que implementa a la inter-faz LayoutManager como argumento. Básicamente, existen tres formas para poder ordenar los com-ponentes en una GUI:

1. Posicionamiento absoluto: esto proporciona el mayor nivel de control sobre la apariencia de una GUI. Al establecer el esquema de un objeto Container en null, podemos especificar la posición absoluta de cada componente de la GUI con respecto a la esquina superior izquierda del objeto Container, usando los métodos setSize y setLocation o setBounds de Component. Si hacemos esto, también debemos especificar el tamaño de cada componente de la GUI. La programación de una GUI con posicionamiento absoluto puede ser un proceso tedioso, a menos que se cuen-te con un entorno de desarrollo integrado (IDE), que pueda generar el código por nosotros.

2. Administradores de esquemas: el uso de administradores de esquemas para posicionar elementos puede ser un proceso más simple y rápido que la creación de una GUI con posicionamiento absoluto, pero se pierde cierto control sobre el tamaño y el posicionamiento preciso de los com-ponentes de la GUI.

3. Programación visual en un IDE: los IDE proporcionan herramientas que facilitan la creación de GUI. Por lo general, cada IDE proporciona una herramienta de diseño de GUI que nos per-mite arrastrar y soltar componentes de GUI desde un cuadro de herramientas, hacia un área de diseño. Después podemos posicionar, ajustar el tamaño de los componentes de la GUI y alinear-los según lo deseado. El IDE genera el código de Java que crea la GUI. Además, podemos por lo general agregar código manejador de eventos para un componente específico, haciendo doble clic en el componente. Algunas herramientas de diseño también nos permiten utilizar los admi-nistradores de esquemas descritos en este capítulo y en el capítulo 25.

14.18 Introducción a los administradores de esquemas 605

Observación de apariencia visual 14.15La mayoría de los IDE de Java proporcionan herramientas de diseño de GUI para di-señar una GUI en forma visual; después, las herramientas escriben código en Java para crear la GUI. Dichas herramientas proporcionan con frecuencia un mayor control sobre el tamaño, la posición y la alineación de los componentes de la GUI, en comparación con los administradores de esquemas integrados.

Observación de apariencia visual 14.16Es posible establecer el esquema de un objeto Container en null, lo cual indica que no debe utilizarse ningún administrador de esquemas. En un objeto Container sin un adminis-trador de esquemas, el programador debe posicionar y cambiar el tamaño de los componentes en el contenedor dado, y cuidar que, en los eventos de ajuste de tamaño, todos los componen-tes se reposicionen según sea necesario. Los eventos de ajuste de tamaño de un componente pueden procesarse mediante un objeto ComponentListener.

En la figura 14.38 se sintetizan los administradores de esquemas presentados en este capítulo. En el capítulo 25 hablaremos sobre otros administradores de esquemas; también veremos el poderoso admi-nistrador de esquemas GroupLayout en el apéndice I.

Administrador de esquemas Descripción

FlowLayout Es el predeterminado para javax.swing.JPanel. Coloca los componentes secuencial-mente (de izquierda a derecha) en el orden en que se agregaron. También es posible especifi car el orden de los componentes utilizando el método add de Container, el cual toma un objeto Component y una posición de índice entero como argumentos.

BorderLayout Es el predeterminado para los objetos JFrame (y otras ventanas). Ordena los componentes en cinco áreas: NORTH, SOUTH, EAST, WEST y CENTER.

GridLayout Ordena los componentes en fi las y columnas

14.18.1 FlowLayout Éste es el administrador de esquemas más simple. Los componentes de la GUI se colocan en un contene-dor, de izquierda a derecha, en el orden en el que se agregaron al contenedor. Cuando se llega al borde del contenedor, los componentes siguen mostrándose en la siguiente línea. La clase FlowLayout permite a los componentes de la GUI alinearse a la izquierda, al centro (el valor predeterminado) y a la derecha.

La aplicación de las figuras 14.39 y 14.40 crea tres objetos JButton y los agrega a la aplicación, utilizando un administrador de esquemas FlowLayout. Los componentes se alinean hacia el centro de manera predeterminada. Cuando el usuario hace clic en Izquierda, la alineación del administrador de esquemas cambia a un FlowLayout alineado a la izquierda. Cuando el usuario hace clic en Derecha, la alineación del administrador de esquemas cambia a un FlowLayout alineado a la derecha. Cuando el usuario hace clic en Centro, la alineación del administrador de esquemas cambia a un FlowLayout alineado hacia el centro. Cada botón tiene su propio manejador de eventos que se declara con una clase interna anónima, la cual implementa a ActionListener. Las ventanas de salida de ejemplo mues-tran cada una de las alineaciones de FlowLayout. Además, la última ventana de salida de ejemplo muestra la alineación centrada después de ajustar el tamaño de la ventana a una anchura menor. Observe que el botón Derecha fluye hacia una nueva línea.

Como se vio antes, el esquema de un contenedor se establece mediante el método setLayout de la clase Container. En la línea 25 se establece el administrador de esquemas en FlowLayout, el cual se declara en la línea 23. Por lo general, el esquema se establece antes de agregar cualquier componente de la GUI a un contenedor.

Fig. 14.38 � Administradores de esquemas.

606 Capítulo 14 Componentes de la GUI: Parte 1

Observación de apariencia visual 14.17Cada contenedor individual puede tener solamente un administrador de esquemas, pero varios contenedores en la misma aplicación pueden tener distintos administradores de esquemas.

Fig. 14.39 � FlowLayout permite a los componentes fluir a través de varias líneas (parte 1 de 2).

1 // Fig. 14.39: MarcoFlowLayout.java 2 // Demostración de las alineaciones de FlowLayout. 3 import java.awt.FlowLayout; 4 import java.awt.Container; 5 import java.awt.event.ActionListener; 6 import java.awt.event.ActionEvent; 7 import javax.swing.JFrame; 8 import javax.swing.JButton; 910 public class MarcoFlowLayout extends JFrame 11 {

12 private JButton botonJButtonIzquierda; // botón para establecer la alineación a la izquierda

13 private JButton botonJButtonCentro; // botón para establecer la alineación al centro

14 private JButton botonJButtonDerecha; // botón para establecer la alineación a la derecha

15 private FlowLayout esquema; // objeto esquema16 private Container contenedor; // contenedor para establecer el esquema1718 // establece la GUI y registra los componentes de escucha de botones19 public MarcoFlowLayout()20 {21 super( “Demostracion de FlowLayout” );2223 esquema = new FlowLayout(); // crea objeto FlowLayout24 contenedor = getContentPane(); // obtiene contenedor para esquema25 setLayout( esquema ); // establece el esquema del marco2627 // establece botonJButtonIzquierda y registra componente de escucha28 botonJButtonIzquierda = new JButton( “Izquierda” ); // crea botón Izquierda29 add( botonJButtonIzquierda ); // agrega botón Izquierda al marco30 botonJButtonIzquierda.addActionListener(3132 new ActionListener() // clase interna anónima33 { 34 // procesa evento de botonJButtonIzquierda 35 public void actionPerformed( ActionEvent evento )36 {37 esquema.setAlignment( FlowLayout.LEFT );3839 // realinea los componentes adjuntos40 esquema.layoutContainer( contenedor );41 } // fin del método actionPerformed42 } // fin de la clase interna anónima43 ); // fin de la llamada a addActionListener4445 // establece botonJButtonCentro y registra componente de escucha46 botonJButtonCentro = new JButton( “Centro” ); // crea botón Centro47 add( botonJButtonCentro ); // agrega botón Centro al marco

14.18 Introducción a los administradores de esquemas 607

Fig. 14.39 � FlowLayout permite a los componentes fluir a través de varias líneas (parte 2 de 2).

Fig. 14.40 � Clase de prueba para MarcoFlowLayout (parte 1 de 2).

48 botonJButtonCentro.addActionListener(

49

50 new ActionListener() // clase interna anónima

51 {

52 // procesa evento de botonJButtonCentro

53 public void actionPerformed( ActionEvent evento )

54 {

55 esquema.setAlignment( FlowLayout.CENTER );

56

57 // realinea los componentes adjuntos

58 esquema.layoutContainer( contenedor );

59 } // fin del método actionPerformed

60 } // fin de la clase interna anónima

61 ); // fin de la llamada a addActionListener

62

63 // establece botonJButtonDerecha y registra componente de escucha

64 botonJButtonDerecha = new JButton( “Derecha” ); // crea botón Derecha

65 add( botonJButtonDerecha ); // agrega botón Derecha al marco

66 botonJButtonDerecha.addActionListener(

67

68 new ActionListener() // clase interna anónima

69 {

70 // procesa evento de botonJButtonDerecha

71 public void actionPerformed( ActionEvent evento )

72 {

73 esquema.setAlignment( FlowLayout.RIGHT );

74

75 // realinea los componentes adjuntos

76 esquema.layoutContainer( contenedor );

77 } // fin del método actionPerformed

78 } // fin de la clase interna anónima

79 ); // fin de la llamada a addActionListener

80 } // fin del constructor de MarcoFlowLayout

81 } // fin de la clase MarcoFlowLayout

1 // Fig. 14.40: DemoFlowLayout.java

2 // Prueba MarcoFlowLayout.

3 import javax.swing.JFrame;

4

5 public class DemoFlowLayout

6 {

7 public static void main( String[] args )

8 {

9 MarcoFlowLayout marcoFlowLayout = new MarcoFlowLayout();

10 marcoFlowLayout.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoFlowLayout.setSize( 350, 75 ); // establece el tamaño del marco

12 marcoFlowLayout.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase DemoFlowLayout

608 Capítulo 14 Componentes de la GUI: Parte 1

El manejador de eventos de cada botón se especifica con un objeto de una clase interna anónima separada (figura 14.39, líneas 30 a 43, 48 a 61 y 66 a 71, respectivamente) y el método action-Performed en cada caso ejecuta dos instrucciones. Por ejemplo, la línea 37 en el manejador de even-tos para el botón botonJButtonIzquierda utiliza el método setAlignment de FlowLayout para cambiar la alineación del objeto FlowLayout a la izquierda (FlowLayout.LEFT). En la línea 40 se utiliza el método layoutContainer de la interfaz LayoutManager (que todos los administradores de esquemas heredan) para especificar que el objeto JFrame debe reordenarse, con base en el esquema ajustado. Dependiendo del botón oprimido, el método actionPerformed para cada botón establece la alineación del objeto FlowLayout a FlowLayout.LEFT (línea 37), FlowLayout.CENTER (línea 55) o FlowLayout.RIGHT (línea 73).

14.18.2 BorderLayout El administrador de esquemas BorderLayout (el predeterminado para un objeto JFrame) ordena los componentes en cinco regiones: NORTH, SOUTH, EAST, WEST y CENTER. NORTH corresponde a la parte superior del contenedor. La clase BorderLayout extiende a Object e implementa a la interfaz Layout-Manager2 (una subinterfaz de LayoutManager, que agrega varios métodos para un mejor procesa-miento de los esquemas).

Un BorderLayout limita a un objeto Container para que contenga cuando mucho cinco com-ponentes; uno en cada región. El componente que se coloca en cada región puede ser un contenedor, al cual se pueden adjuntar otros componentes. Los componentes que se colocan en las regiones NORTH y SOUTH se extienden horizontalmente hacia los lados del contenedor, y tienen la misma altura que los componentes que se colocan en esas regiones. Las regiones EAST y WEST se expanden verti-calmente entre las regiones NORTH y SOUTH, y tienen la misma anchura que los componentes que se coloquen dentro de ellas. El componente que se coloca en la región CENTER se expande para rellenar todo el espacio restante en el esquema (esto explica por qué el objeto JTextArea de la figura 14.37 ocupa toda la ventana). Si las cinco regiones están ocupadas, todo el espacio del contenedor se cubre con los componentes de la GUI. Si las regiones NORTH o SOUTH no están ocupadas, los componentes de la GUI en las regiones EAST, CENTER y WEST se expanden verticalmente para rellenar el espacio restante. Si las regiones EAST o WEST no están ocupadas, el componente de la GUI en la región CENTER se expande horizontalmente para rellenar el espacio restante. Si la región CENTER no está ocupada, el área se deja vacía; los demás componentes de la GUI no se expanden para rellenar el espacio restante.

Fig. 14.40 � Clase de prueba para MarcoFlowLayout (parte 2 de 2).

14.18 Introducción a los administradores de esquemas 609

La aplicación de las figuras 14.41 y 14.42 demuestra el administrador de esquemas BorderLayout, utilizando cinco objetos JButton.

Fig. 14.41 � BorderLayout que contiene cinco botones (parte 1 de 2).

1 // Fig. 14.41: MarcoBorderLayout.java 2 // Demostración de BorderLayout. 3 import java.awt.BorderLayout; 4 import java.awt.event.ActionListener; 5 import java.awt.event.ActionEvent; 6 import javax.swing.JFrame; 7 import javax.swing.JButton; 8 9 public class MarcoBorderLayout extends JFrame implements ActionListener 10 {11 private JButton botones[]; // arreglo de botones para ocultar porciones12 private final String nombres[] = { "Ocultar Norte", "Ocultar Sur", 13 "Ocultar Este", "Ocultar Oeste", "Ocultar Centro" };14 private BorderLayout esquema; // objeto BorderLayout1516 // establece la GUI y el manejo de eventos17 public MarcoBorderLayout()18 {19 super( "Demostracion de BorderLayout" );2021 esquema = new BorderLayout( 5, 5 ); // espacios de 5 píxeles22 setLayout( esquema ); // establece el esquema del marco23 botones = new JButton[ nombres.length ]; // establece el tamaño del arreglo2425 // crea objetos JButton y registra componentes de escucha para ellos26 for ( int cuenta = 0; cuenta < nombres.length; cuenta++ ) 27 {28 botones[ cuenta ] = new JButton( nombres[ cuenta ] );29 botones[ cuenta ].addActionListener( this );30 } // fin de for3132 add( botones[ 0 ], BorderLayout.NORTH ); // agrega botón al norte33 add( botones[ 1 ], BorderLayout.SOUTH ); // agrega botón al sur34 add( botones[ 2 ], BorderLayout.EAST ); // agrega botón al este35 add( botones[ 3 ], BorderLayout.WEST ); // agrega botón al oeste36 add( botones[ 4 ], BorderLayout.CENTER ); // agrega botón al centro37 } // fin del constructor de MarcoBorderLayout3839 // maneja los eventos de botón40 public void actionPerformed( ActionEvent evento )41 {

42 // comprueba el origen del evento y distribuye el panel de contenido de manera acorde

43 for ( JButton boton : botones )44 {45 if ( evento.getSource() == boton )46 boton.setVisible( false ); // oculta el botón oprimido47 else48 boton.setVisible( true ); // muestra los demás botones49 } // fin de for

610 Capítulo 14 Componentes de la GUI: Parte 1

50

51 esquema.layoutContainer( getContentPane() ); // distribuye el panel de contenido

52 } // fin del método actionPerformed

53 } // fin de la clase MarcoBorderLayout

Fig. 14.41 � BorderLayout que contiene cinco botones (parte 2 de 2).

Fig. 14.42 � Clase de prueba para MarcoDemoTeclas.

1 // Fig. 14.42: DemoBorderLayout.java 2 // Prueba de MarcoBorderLayout. 3 import javax.swing.JFrame; 4 5 public class DemoBorderLayout 6 { 7 public static void main( String[] args ) 8 { 9 MarcoBorderLayout marcoBorderLayout = new MarcoBorderLayout(); 10 marcoBorderLayout.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoBorderLayout.setSize( 300, 200 ); // establece el tamaño del marco12 marcoBorderLayout.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase DemoBorderLayout

espacio horizontal

espacio vertical

14.18 Introducción a los administradores de esquemas 611

En la línea 21 de la figura 14.41 se crea un objeto BorderLayout. Los argumentos del constructor especifican el número de píxeles entre los componentes que se ordenan en forma horizontal (espacio libre horizontal) y entre los componentes que se ordenan en forma vertical (espacio libre vertical), respecti-vamente. El valor predeterminado es un píxel de espacio libre horizontal y vertical. En la línea 22 se uti-liza el método setLayout para establecer el esquema del panel de contenido en esquema.

Agregamos objetos Component a un objeto BorderLayout con otra versión del método add de Con-tainer que toma dos argumentos: el objeto Component que se va a agregar y la región en la que debe aparecer este objeto. Por ejemplo, en la línea 32 se especifica que botones[ 0 ] debe aparecer en la región NORTH. Los componentes pueden agregarse en cualquier orden, pero sólo debe agregarse un componente a cada región.

Observación de apariencia visual 14.18Si no se especifica una región al agregar un objeto Component a un objeto BorderLayout, el administrador de esquemas asume que el objeto Component debe agregarse a la región BorderLayout.CENTER.

Error común de programación 14.6Cuando se agrega más de un componente a una región en un objeto BorderLayout, sólo se mostrará el último componente agregado a esa región. No hay un error que indique este problema.

La clase MarcoBorderLayout implementa directamente a ActionListener en este ejemplo, por lo que el objeto MarcoBorderLayout manejará los eventos de los objetos JButton. Por esta razón, en la línea 29 se pasa la referencia this al método addActionListener de cada objeto JButton. Cuando el usuario hace clic en un objeto JButton específico en el esquema, se ejecuta el método actionPerformed (líneas 40 a 52). La instrucción for mejorada en las líneas 43 a 49 utiliza una instrucción if…else para ocultar el objeto JButton específico que generó el evento. El método setVisible (que JButton hereda de la clase Component) se llama con un argumento false (línea 46) para ocultar el objeto JButton. Si el objeto JButton actual en el arreglo no es el que generó el evento, se hace una llamada al método setVisible con un argumento true (línea 48) para asegurar que el objeto JButton se muestre en la pantalla. En la línea 51 se utiliza el método layoutContainer de LayoutManager para recalcular la distribución visual del panel de contenido. Observe en las capturas de pantalla de la figura 14.42 que ciertas regiones en el objeto BorderLayout cambian de forma a medida que se ocultan objetos JButton y se muestran en otras regiones. Pruebe a cambiar el tamaño de la ventana de la aplicación para ver cómo las diversas regiones ajustan su tamaño, con base en la anchura y la altura de la ventana. Para esquemas más complejos, agrupe los componentes en objetos JPanel, cada uno con un administrador de esquemas separado. Coloque los objetos JPanel en el objeto JFrame, usando el esquema BorderLayout predeterminado o cualquier otro esquema.

14.18.3 GridLayout El administrador de esquemas GridLayout divide el contenedor en una cuadrícula, de manera que los componentes puedan colocarse en filas y columnas. La clase GridLayout hereda directamente de la clase Object e implementa a la interfaz LayoutManager. Todo objeto Component en un objeto GridLayout tiene la misma anchura y altura. Los componentes se agregan a un objeto GridLayout empezando en la celda superior izquierda de la cuadrícula, y procediendo de izquierda a derecha hasta que la fila esté llena. Después el proceso continúa de izquierda a derecha en la siguiente fila de la cuadrícula, y así sucesivamen-te. La aplicación de las figuras 14.43 y 14.44 demuestra el administrador de esquemas GridLayout, uti-lizando seis objetos JButton.

En las líneas 24 y 25 se crean dos objetos GridLayout. El constructor de GridLayout que se utiliza en la línea 24 especifica un objeto GridLayout con 2 filas, 3 columnas, 5 píxeles de espacio libre horizontal entre objetos Component en la cuadrícula y 5 píxeles de espacio libre vertical entre objetos

612 Capítulo 14 Componentes de la GUI: Parte 1

Component en la cuadrícula. El constructor de GridLayout que se utiliza en la línea 25 especifica un objeto GridLayout con 3 filas y 2 columnas que utiliza el espacio libre predeterminado (1 píxel).

Fig. 14.43 � GridLayout que contiene seis botones.

1 // Fig. 14.43: MarcoGridLayout.java 2 // Demostración de GridLayout. 3 import java.awt.GridLayout; 4 import java.awt.Container; 5 import java.awt.event.ActionListener; 6 import java.awt.event.ActionEvent; 7 import javax.swing.JFrame; 8 import javax.swing.JButton; 910 public class MarcoGridLayout extends JFrame implements ActionListener 11 {12 private JButton[] botones; // arreglo de botones13 private final String[] nombres = 14 { “uno”, “dos”, “tres”, “cuatro”, “cinco”, “seis” };15 private boolean alternar = true; // alterna entre dos esquemas16 private Container contenedor; // contenedor del marco17 private GridLayout cuadricula1; // primer objeto GridLayout18 private GridLayout cuadricula2; // segundo objeto GridLayout1920 // constructor sin argumentos21 public MarcoGridLayout()22 {23 super( “Demostracion de GridLayout” );24 cuadricula1 = new GridLayout( 2, 3, 5, 5 ); // 2 por 3; espacios de 525 cuadricula2 = new GridLayout( 3, 2 ); // 3 por 2; sin espacios26 contenedor = getContentPane(); // obtiene el panel de contenido27 setLayout( cuadricula1 ); // establece esquema de objeto JFrame28 botones = new JButton[ nombres.length ]; // crea arreglo de objetos JButton2930 for ( int cuenta = 0; cuenta < nombres.length; cuenta++ )31 {32 botones[ cuenta ] = new JButton( nombres[ cuenta ] );33 botones[ cuenta ].addActionListener( this ); // registra componente de escucha34 add( botones[ cuenta ] ); // agrega boton a objeto JFrame35 } // fin de for36 } // fin del constructor de MarcoGridLayout3738 // maneja eventos de boton, alternando entre los esquemas39 public void actionPerformed( ActionEvent evento )40 { 41 if ( alternar )42 contenedor.setLayout( cuadricula2 ); // establece esquema al primero43 else44 contenedor.setLayout( cuadricula1 ); // establece esquema al segundo4546 alternar = !alternar; // establece alternar a su valor opuesto47 contenedor.validate(); // redistribuye el contenedor48 } // fin del método actionPerformed49 } // fin de la clase MarcoGridLayout

14.19 Uso de paneles para administrar esquemas más complejos 613

Los objetos JButton en este ejemplo se ordenan en un principio utilizando cuadricula1 (que se establece para el panel de contenido en la línea 27, mediante el método setLayout). El primer compo-nente se agrega a la primera columna de la primera fila. El siguiente componente se agrega a la segunda columna de la primera fila, y así en lo sucesivo. Cuando se oprime un objeto JButton, se hace una llama-da al método actionPerformed (líneas 39 a 48). Todas las llamadas a actionPerformed alternan el es-quema entre cuadricula2 y cuadricula1, utilizando la variable boolean llamada alternar para deter-minar el siguiente esquema a establecer.

En la línea 47 se muestra otra manera para cambiar el formato a un contenedor para el cual haya cambiado el esquema. El método validate de Container recalcula el esquema del contenedor, con base en el administrador de esquemas actual para ese objeto Container y el conjunto actual de componentes de la GUI que se muestran en pantalla.

14.19 Uso de paneles para administrar esquemas más complejosLas GUI complejas (como la de la figura 14.1) requieren que cada componente se coloque en una ubicación exacta. A menudo consisten de varios paneles, en donde los componentes de cada panel se ordenan en un esquema específico. La clase JPanel extiende a JComponent, y JComponent extiende a la clase Container, por lo que todo JPanel es un Container. Por lo tanto, todo objeto JPanel puede tener componentes, in-cluyendo otros paneles, los cuales se adjuntan mediante el método add de Container. La aplicación de las figuras 14.45 y 14.46 demuestra cómo puede usarse un objeto JPanel para crear un esquema más comple-jo, en el cual se coloquen varios objetos JButton en la región SOUTH de un esquema BorderLayout.

Una vez que el objeto JPanel llamado panelBotones se declara (línea 11) y se crea (línea 19), en la línea 20 se establece el esquema de panelBotones a un GridLayout con una fila y cinco columnas (hay cinco objetos JButton en el arreglo botones). En las líneas 23 a 27 se agregan los cinco objetos JButton del arreglo botones al objeto JPanel. En la línea 26 se agregan los botones directamente al objeto JPanel (la clase JPanel no tiene un panel de contenido, a diferencia de JFrame). En la línea 29 se utiliza el obje-to BorderLayout predeterminado de JFrame para agregar panelBotones a la región SOUTH. Observe que esta región tiene la misma altura que los botones en panelBotones. Un objeto JPanel ajusta su tamaño

1 // Fig. 14.44: DemoGridLayout.java 2 // Prueba de MarcoGridLayout. 3 import javax.swing.JFrame; 4 5 public class DemoGridLayout 6 { 7 public static void main( String[] args ) 8 { 9 MarcoGridLayout marcoGridLayout = new MarcoGridLayout(); 10 marcoGridLayout.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );11 marcoGridLayout.setSize( 300, 200 ); // establece el tamaño del marco12 marcoGridLayout.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase DemoGridLayout

Fig. 14.44 � Clase de prueba para MarcoGridLayout.

614 Capítulo 14 Componentes de la GUI: Parte 1

de acuerdo con los componentes que contiene. A medida que se agregan más componentes, el objeto JPanel crece (de acuerdo con las restricciones de su administrador de esquemas) para dar cabida a esos nuevos componentes. Ajuste el tamaño de la ventana para que vea cómo el administrador de esquemas afecta al tamaño de los objetos JButton.

Fig. 14.45 � Jpanel con cinco objetos JButton, en un esquema GridLayout adjunto a la región SOUTH de un esquema BorderLayout.

1 // Fig. 14.45: MarcoPanel.java

2 // Uso de un objeto JPanel para ayudar a distribuir los componentes.

3 import java.awt.GridLayout;

4 import java.awt.BorderLayout;

5 import javax.swing.JFrame;

6 import javax.swing.JPanel;

7 import javax.swing.JButton;

8

9 public class MarcoPanel extends JFrame

10 {

11 private JPanel panelBotones; // panel que contiene los botones

12 private JButton[] botones; // arreglo de botones

13

14 // constructor sin argumentos

15 public MarcoPanel()

16 {

17 super( “Demostracion de Panel” );

18 botones = new JButton[ 5 ]; // crea el arreglo botones

19 panelBotones = new JPanel(); // establece el panel

20 panelBotones.setLayout( new GridLayout( 1, botones.length ) );

21

22 // crea y agrega los botones

23 for ( int cuenta = 0; cuenta < botones.length; cuenta++ )

24 {

25 botones[ cuenta ] = new JButton( "Boton " + ( cuenta + 1 ) );

26 panelBotones.add( botones[ cuenta ] ); // agrega el botón al panel

27 } // fin de for

28

29 add( panelBotones, BorderLayout.SOUTH ); // agrega el panel a JFrame

30 } // fin del constructor de MarcoPanel

31 } // fin de la clase MarcoPanel

Fig. 14.46 � Clase de prueba para MarcoPanel (parte 1 de 2).

1 // Fig. 14.46: DemoPanel.java

2 // Prueba de MarcoPanel.

3 import javax.swing.JFrame;

4

5 public class DemoPanel extends JFrame

6 {

7 public static void main( String[] args )

8 {

9 MarcoPanel marcoPanel = new MarcoPanel();

10 marcoPanel.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoPanel.setSize( 450, 200 ); // establece el tamaño del marco

14.20 JTextArea 615

14.20 JTextArea Un objeto JTextArea proporciona un área para manipular varias líneas de texto. Al igual que la clase JTextField, JTextArea es una subclase de JTextComponent, el cual declara métodos comunes para objetos JTextField, JTextArea y varios otros componentes de GUI basados en texto.

La aplicación en las figuras 14.47 y 14.48 demuestra el uso de los objetos JTextArea. Un objeto JTextArea muestra texto que el usuario puede seleccionar. El otro no puede editarse, y se utiliza para mostrar el texto que seleccionó el usuario en el primer objeto JTextArea. A diferencia de los objetos JTextField, los objetos JTextArea no tienen eventos de acción: al oprimir Intro mientras escribe en un objeto JTextArea, el cursor simplemente avanza a la siguiente línea. Al igual que con los objetos JList de selección múltiple (sección 14.13), un evento externo de otro componente de GUI indica cuándo se debe procesar el texto en un objeto JTextArea. Por ejemplo, al escribir un mensaje de correo electrónico, por lo general hacemos clic en un botón Enviar para enviar el texto del mensaje al recipiente. De manera similar, al editar un documento en un procesador de palabras, por lo general guardamos el archivo se-leccionando un elemento de menú llamado Guardar o Guardar como…. En este programa, el botón Copiar>>> genera el evento externo que copia el texto seleccionado en el objeto JTextArea de la iz-quierda, y lo muestra en el objeto JTextArea de la derecha.

Fig. 14.46 � Clase de prueba para MarcoPanel (parte 2 de 2).

Fig. 14.47 � Copiado de texto seleccionado, de un objeto JTextArea a otro (parte 1 de 2).

1 // Fig. 14.47: MarcoAreaTexto.java

2 // Copia el texto seleccionado de un área de texto a otra.

3 import java.awt.event.ActionListener;

4 import java.awt.event.ActionEvent;

5 import javax.swing.Box;

6 import javax.swing.JFrame;

7 import javax.swing.JTextArea;

8 import javax.swing.JButton;

9 import javax.swing.JScrollPane;

10

11 public class MarcoAreaTexto extends JFrame

12 {

13 private JTextArea areaTexto1; // muestra cadena de demostración

14 private JTextArea areaTexto2; // el texto resaltado se copia aquí

15 private JButton botonCopiar; // inicia el copiado de texto

16

12 marcoPanel.setVisible( true ); // muestra el marco13 } // fin de main14 } // fin de la clase DemoPanel

616 Capítulo 14 Componentes de la GUI: Parte 1

Fig. 14.47 � Copiado de texto seleccionado, de un objeto JTextArea a otro (parte 2 de 2).

Fig. 14.48 � Clase de prueba para MarcoAreaTexto (parte 1 de 2).

17 // constructor sin argumentos

18 public MarcoAreaTexto()

19 {

20 super( "Demostracion de JTextArea" );

21 Box cuadro = Box.createHorizontalBox(); // crea un cuadro

22 String demo = "Esta es una cadena de\ndemostracion para\n" +

23 "ilustrar como copiar texto\nde un area de texto a \n" +

24 "otra, usando un\nevento externo\n";

25

26 areaTexto1 = new JTextArea( demo, 10, 15 ); // crea área de texto 1

27 cuadro.add( new JScrollPane( areaTexto1 ) ); // agrega panel de desplazamiento

28

29 botonCopiar = new JButton( "Copiar >>>" ); // crea botón para copiar

30 cuadro.add( botonCopiar ); // agrega botón de copia al cuadro

31 botonCopiar.addActionListener(

32

33 new ActionListener() // clase interna anónima

34 {

35 // establece el texto en areaTexto2 con el texto seleccionado de areaTexto1

36 public void actionPerformed( ActionEvent evento )

37 {

38 areaTexto2.setText( areaTexto1.getSelectedText() );

39 } // fin del método actionPerformed

40 } // fin de la clase interna anónima

41 ); // fin de la llamada a addActionListener

42

43 areaTexto2 = new JTextArea( 10, 15 ); // crea segunda área de texto

44 areaTexto2.setEditable( false ); // deshabilita edición

45 cuadro.add( new JScrollPane( areaTexto2 ) ); // agrega panel de desplazamiento

46

47 add( cuadro ); // agrega cuadro al marco

48 } // fin del constructor de MarcoAreaTexto

49 } // fin de la clase MarcoAreaTexto

1 // Fig. 14.48: DemoAreaTexto.java

2 // Copia el texto seleccionado de un área de texto a otra.

3 import javax.swing.JFrame;

4

5 public class DemoAreaTexto

6 {

7 public static void main( String[] args )

8 {

9 MarcoAreaTexto marcoAreaTexto = new MarcoAreaTexto();

10 marcoAreaTexto.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 marcoAreaTexto.setSize( 425, 200 ); // establece el tamaño del marco

12 marcoAreaTexto.setVisible( true ); // muestra el marco

13 } // fin de main

14 } // fin de la clase DemoAreaTexto

14.20 JTextArea 617

En el constructor (líneas 18 a 48), la línea 21 crea un contenedor Box (paquete javax.swing) para organizar los componentes de la GUI. Box es una subclase de Container que utiliza un administrador de esquemas BoxLayout (que veremos con detalle en la sección 25.9) para ordenar los componentes de la GUI, ya sea en forma horizontal o vertical. El método static createHorizontalBox de Box crea un objeto Box que ordena los componentes de izquierda a derecha, en el orden en el que se adjuntan.

En las líneas 26 y 43 se crean los objetos JTextArea llamados areaTexto1 y areaTexto2. La línea 26 utiliza el constructor con tres argumentos de JTextArea, el cual recibe un objeto String que representa el texto inicial y dos valores int que especifican que el objeto JTextArea tiene 10 filas y 15 columnas. En la línea 43 se utiliza el constructor con dos argumentos de JTextArea, el cual especifica que el objeto JText-Area tiene 10 filas y 15 columnas. En la línea 26 se especifica que demo debe mostrarse como el contenido predeterminado del objeto JTextArea. Un objeto JTextArea no proporciona barras de desplazamiento si no puede mostrar su contenido completo. Por lo tanto, en la línea 27 se crea un objeto JScrollPane, se inicializa con areaTexto1 y se adjunta al contenedor cuadro. En un objeto JScrollPane aparecen de ma-nera predeterminada las barras de desplazamiento horizontal y vertical, según sea necesario.

En las líneas 29 a 41 se crea el objeto JButton llamado botonCopiar con la etiqueta “Copiar >>>”, se agrega botonCopiar al contenedor cuadro y se registra el manejador de eventos para el evento Actio-nEvent de botonCopiar. Este botón proporciona el evento externo que determina cuándo debe copiar el programa el texto seleccionado en areaTexto1 a areaTexto2. Cuando el usuario hace clic en botonCo-piar, la línea 38 en actionPerformed indica que el método getSelectedText (que hereda JTextArea de JTextComponent) debe devolver el texto seleccionado de areaTexto1. Para seleccionar el texto, el usuario arrastra el ratón sobre el texto deseado para resaltarlo. El método setText cambia el texto en areaTexto2 por la cadena que devuelve getSelectedText.

En las líneas 43 a 45 se crea areaTexto2, se establece su propiedad editable a false y se agrega al contenedor box. En la línea 47 se agrega cuadro al objeto JFrame. En la sección 14.18 vimos que el esquema predeterminado de un objeto JFrame es BorderLayout, y que el método add adjunta de ma-nera predeterminada su argumento a la región CENTER de este esquema.

Cuando el texto llega al lado derecho de un objeto JTextArea, puede recorrerse a la siguiente línea. A esto se le conoce como envoltura de línea. La clase JTextArea no envuelve líneas de manera predeterminada.

Observación de apariencia visual 14.19Para proporcionar la funcionalidad de envoltura de líneas para un objeto JTextArea, invoque el método setLineWrap de JTextArea con un argumento true.

Políticas de las barras de desplazamiento de JScrollPaneEn este ejemplo se utiliza un objeto JScrollPane para proporcionar la capacidad de desplazamiento a un objeto JTextArea. De manera predeterminada, JScrollPane muestra las barras de desplazamiento sólo si se requieren. Puede establecer las políticas de las barras de desplazamiento horizontal y vertical de un objeto JScrollPane al momento de crearlo. Si un programa tiene una referencia a un objeto JScroll-

Fig. 14.48 � Clase de prueba para MarcoAreaTexto (parte 2 de 2).

618 Capítulo 14 Componentes de la GUI: Parte 1

Pane, puede usar los métodos setHorizontalScrollBarPolicy y setVerticalScrollBarPolicy de JScrollPane para modificar las políticas de las barras de desplazamiento en cualquier momento. La clase JScrollPane declara las constantes

JScrollPane.VERTICAL_SCROLLBAR_ALWAYSJScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS

para indicar que siempre debe aparecer una barra de desplazamiento, las constantes

JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDEDJScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED

para indicar que debe aparecer una barra de desplazamiento sólo si es necesario (los valores predeterminados), y las constantes

JScrollPane.VERTICAL_SCROLLBAR_NEVERJScrollPane.HORIZONTAL_SCROLLBAR_NEVER

para indicar que nunca debe aparecer una barra de desplazamiento. Si la política de la barra de desplaza-miento horizontal se establece en JScrollPane.HORIZONTAL_SCROLLBAR_NEVER, un objeto JTextArea adjunto al objeto JScrollPane envolverá las líneas de manera automática.

14.21 ConclusiónEn este capítulo aprendió acerca de muchos componentes de la GUI, y cómo implementar el manejo de eventos. También aprendió acerca de las clases anidadas, las clases internas y las clases internas anónimas. Vio la relación especial entre un objeto de la clase interna y un objeto de su clase de nivel superior. Apren-dió a utilizar diálogos JOptionPane para obtener datos de entrada de texto del usuario, y cómo mostrar mensajes a éste. También aprendió a crear aplicaciones que se ejecuten en sus propias ventanas. Hablamos sobre la clase JFrame y los componentes que permiten a un usuario interactuar con una aplicación. Tam-bién le enseñamos cómo mostrar texto e imágenes al usuario. Vimos cómo personalizar los objetos JPanel para crear áreas de dibujo personalizadas, las cuales utilizará ampliamente en el siguiente capítulo. Vio cómo organizar los componentes en una ventana mediante el uso de los administradores de esquemas, y cómo crear GUI más complejas mediante el uso de objetos JPanel para organizar los componentes. Por último, aprendió acerca del componente JTextArea, en el cual un usuario puede introducir texto y una aplicación puede mostrarlo. En el capítulo 25 aprenderá acerca de los componentes de GUI más avanzados, como los botones deslizables, los menús y los administradores de esquemas más compli-cados. En el siguiente capítulo aprenderá a agregar gráficos a su aplicación de GUI. Los gráficos nos permiten dibujar figuras y texto con colores y estilos.

ResumenSección 14.1 Introducción• Una interfaz gráfi ca de usuario (GUI; pág. 550) presenta un mecanismo amigable al usuario para interactuar con una

aplicación. Una GUI proporciona a una aplicación una “apariencia visual” única (pág. 555).

• Al proporcionar distintas aplicaciones en las que los componentes de la interfaz de usuario sean consistentes e intui-tivos, los usuarios pueden familiarizarse en cierto modo con una nueva aplicación, de manera que pueden aprender a utilizarla con mayor rapidez.

• Las GUI se crean a partir de componentes de GUI (pág. 550); a éstos se les conoce algunas veces como controles o “widgets”.

Sección 14.2 Nueva apariencia visual Nimbus de Java• A partir de la actualización 10 de Java SE 6, se incluye una nueva apariencia visual elegante multiplataforma, conocida

como Nimbus (pág. 551).

• Para establecer Nimbus como predeterminada para todas las aplicaciones Java, cree un archivo de texto swing.proper-ties en la carpeta lib de sus carpetas de instalación JDK y JRE. Coloque la siguiente línea de código en el archivo:

swing.defaultlaf=com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel

• Para seleccionar Nimbus en cada aplicación por separado, coloque el siguiente argumento de línea de comandos después del comando java y antes del nombre de la aplicación, al momento de ejecutarla:

-Dswing.defaultlaf=com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel

Sección 14.3 Entrada/salida simple basada en GUI con JOptionPane• La mayoría de las aplicaciones utilizan ventanas o cuadros de diálogo (pág. 552) para interactuar con el usuario.

• La clase JOptionPane de Java (pág. 552) del paquete javax.swing (pág. 550) proporciona cuadros de diálogo pre-fabricados para entrada y salida. El método static showInputDialog (pág. 553) de JOptionPane muestra un diálogo de entrada (pág. 552).

• Por lo general, un indicador utiliza la capitalización estilo oración: un estilo que capitaliza sólo la primera letra de la primera palabra en el texto, a menos que la palabra sea un nombre propio.

• Un diálogo de entrada sólo puede introducir objetos String. Esto es común en la mayoría de los componentes de la GUI.

• El método static showMessageDialog (pág. 554) de JOptionPane muestra un diálogo de mensaje (pág. 552).

Sección 14.4 Generalidades de los componentes de Swing• La mayoría de los componentes de GUI de Swing (pág. 550) se encuentran en el paquete javax.swing.

• En conjunto, a la apariencia y la forma en la que interactúa el usuario con la aplicación se les denomina la apariencia visual de esa aplicación. Los componentes de GUI de Swing nos permiten especifi car una apariencia visual uniforme para una aplicación a través de todas las plataformas, o para usar la apariencia visual personalizada de cada plataforma.

• Los componentes ligeros de Swing no están enlazados a los componentes actuales de GUI que soporta la plataforma subyacente en la que se ejecuta una aplicación.

• Varios componentes de Swing son componentes pesados (pág. 556), que requieren una interacción directa con el sis-tema de ventanas local (pág. 556), lo cual puede restringir su apariencia y funcionalidad.

• La clase Component (pág. 556; paquete java.awt) declara muchos de los atributos y comportamientos comunes para los componentes de GUI en los paquetes java.awt (pág. 555) y javax.swing.

• La clase Container (pág. 556; paquete java.awt) es una subclase de Component. Los objetos Component se adjuntan a los objetos Container, de manera que puedan organizarse y mostrarse en la pantalla.

• La clase JComponent (pág. 556) del paquete javax.swing es una subclase de Container. JComponent es la superclase de todos los componentes ligeros de Swing, y declara los atributos y comportamientos comunes.

• Algunas de las características comunes de JComponent son: una apariencia visual adaptable (pág. 556), teclas de método abreviado llamadas nemónicos (pág. 556), cuadros de información sobre herramientas (pág. 556), soporte para tec-nologías de ayuda y soporte para la localización de la interfaz de usuario (pág. 556).

Sección 14.5 Mostrar texto e imágenes en una ventana• La clase JFrame proporciona los atributos y comportamientos básicos de una ventana.

• Un objeto JLabel (pág. 557) muestra una sola línea de texto de sólo lectura, una imagen, o texto y una imagen. Por lo general, el texto en un objeto JLabel usa la capitalización estilo oración.

• Cada componente de una GUI debe adjuntarse a un contenedor, como una ventana creada con un objeto JFrame (pág. 559).

• Muchos IDE proporcionan herramientas de diseño de GUI (pág. 604), en las cuales podemos especifi car el tamaño y la ubicación exactos de un componente mediante el uso del ratón; después el IDE genera el código de la GUI por nosotros.

• El método setToolTipText de JComponent (pág. 559) especifi ca la información sobre herramientas que se muestra cuando el usuario coloca el cursor del ratón sobre un componente ligero (pág. 556).

Resumen 619

• El método add de Container adjunta un componente de GUI a un objeto Container.

• La clase ImageIcon (pág. 560) soporta varios formatos de imagen, incluyendo GIF, PNG y JPEG.

• El método getClass de la clase Object (pág. 560) obtiene una referencia al objeto Class que representa la declaración de la clase para el objeto en el que se hace la llamada al método.

• El método getResource de Class (pág. 560) devuelve la ubicación de su argumento en forma de URL. El método getResource usa el cargador de clases del objeto Class para determinar la ubicación del recurso.

• Las alineaciones horizontal y vertical de un objeto JLabel se pueden establecer mediante los métodos setHorizontal-Alignment (pág. 560) y setVerticalAlignment (pág. 560), respectivamente.

• Los métodos setText (pág. 560) y getText (pág. 560) de JLabel establecen y obtienen el texto a mostrar en una etiqueta.

• Los métodos setIcon (pág. 560) y getIcon (pág. 560) de JLabel establecen y obtienen el objeto Icon (pág. 560) en una etiqueta.

• Los métodos setHorizontalTextPosition (pág. 560) y setVerticalTextPosition (pág. 560) de JLabel especifi can la posición del texto en la etiqueta.

• El método setDefaultCloseOperation (pág. 561) de JFrame, con la constante Frame.EXIT_ON_CLOSE como argu-mento, indica que el programa debe terminar cuando el usuario cierre la ventana.

• El método setSize de Component (pág. 561) especifi ca la anchura y la altura de un componente.

• El método setVisible (pág. 561) de Component con el argumento true muestra un objeto JFrame en la pantalla.

Sección 14.6 Campos de texto y una introducción al manejo de eventos con clases anidadas• Las GUI se controlan por eventos; cuando el usuario interactúa con un componente de GUI, los eventos (pág. 561)

controlan al programa para realizar las tareas.

• Un manejador de eventos (pág. 561) realiza una tarea en respuesta a un evento.

• La clase JTextField (pág. 561) extiende a la clase JTextComponent (pág. 561) del paquete javax.swing.text, que pro-porciona muchas características comunes para los componentes de Swing basados en texto. La clase JPasswordField (pág. 561) extiende a JTextField y agrega varios métodos específi cos para el procesamiento de contraseñas.

• Un objeto JPasswordField (pág. 561) muestra que se están escribiendo caracteres a medida que el usuario los intro-duce, pero oculta los caracteres reales con caracteres de eco (pág. 561).

• Un componente recibe el enfoque (pág. 561) cuando el usuario hace clic sobre él.

• El método setEditable de JTextComponent (pág. 564) puede usarse para hacer que un campo de texto no pueda editarse.

• Para responder a un evento para un componente específi co de la GUI, debemos crear una clase que represente al manejador de eventos e implementar una interfaz de escucha de eventos apropiada (pág. 564), después registrar un objeto de la clase manejadora de eventos como el manejador de eventos (pág. 564).

• Las clases anidadas no static se llaman clases internas, y se utilizan con frecuencia para el manejo de eventos.

• Un objeto de una clase no static interna (pág. 564) debe crearse mediante un objeto de la clase de nivel superior (pág. 564), que contenga a la clase interna.

• Un objeto de la clase interna puede acceder directamente a todas las variables y métodos de su clase de nivel superior.

• Una clase anidada que sea static no requiere un objeto de su clase de nivel superior, y no tiene de manera implícita una referencia a un objeto de la clase de nivel superior.

• Cuando el usuario oprime Intro en un objeto JTextField (pág. 561) o JPasswordField, el componente de la GUI genera un evento ActionEvent (pág. 565) del paquete java.awt.event (pág. 567), el cual puede ser manejado por un objeto ActionListener (pág. 565; paquete java.awt.event).

• El método addActionListener de JTextField (pág. 565) registra el manejador de eventos para un campo de texto de ActionEvent.

• El componente de GUI con el que interactúa el usuario es el origen del evento (pág. 566).

620 Capítulo 14 Componentes de la GUI: Parte 1

• Un objeto ActionEvent contiene información acerca del evento que acaba de ocurrir, como el origen del evento y el texto en el campo de texto.

• El método getSource de ActionEvent devuelve una referencia al origen del evento. El método getActionCommand (pág. 566) de ActionEvent devuelve el texto que escribió el usuario en un campo de texto o en la etiqueta de un objeto JButton.

• El método getPassword de JPasswordField (pág. 566) devuelve la contraseña que escribió el usuario.

Sección 14.7 Tipos de eventos comunes de la GUI e interfaces de escucha• Para cada tipo de objeto evento, hay por lo general una interfaz de escucha de eventos que le corresponde y que espe-

cifi ca uno o más métodos manejadores de eventos, que deben declararse en la clase que implementa a la interfaz.

Sección 14.8 Cómo funciona el manejo de eventos• Cuando ocurre un evento, el componente de la GUI con el que el usuario interactuó notifi ca a sus componentes de

escucha registrados, llamando al método de manejo de eventos apropiado de cada componente de escucha.

• Todo componente de la GUI soporta varios tipos de eventos. Cuando ocurre un evento, éste se despacha (pág. 570) sólo a los componentes de escucha de eventos del tipo apropiado.

Sección 14.9 JButton• Un botón es un componente en el que el usuario hace clic para desencadenar cierta acción. Todos los tipos de botones

son subclases de AbstractButton (pág. 571; paquete javax.swing). Por lo general, las etiquetas de los botones (pág. 571) usan la capitalización tipo título de libro (pág. 554).

• Los botones de comandos (pág. 571) se crean con la clase JButton.

• Un objeto JButton puede mostrar un objeto Icon. Un objeto JButton también puede tener un icono de sustitución (pág. 571) —un objeto Icon que se muestra cuando el usuario coloca el ratón sobre el botón.

• El método setRolloverIcon (pág. 573) de la clase AbstractButton especifi ca la imagen a mostrar en un botón, cuando el usuario coloca el ratón sobre él.

Sección 14.10 Botones que mantienen el estado• Hay tres tipos de botones de estado de Swing: JToggleButton (pág. 574), JCheckBox (pág. 574) y JRadioButton

(pág. 574).

• Las clases JCheckBox y JRadioButton son subclases de JToggleButton.

• El método setFont (de la clase Component) (pág. 576) establece el tipo de letra de un componente a un nuevo objeto de la clase Font (pág. 576; paquete java.awt).

• Al hacer clic en un objeto JCheckBox, ocurre un evento ItemEvent (pág. 576), que puede manejarse mediante un objeto ItemListener (pág. 576), que defi ne al método itemStateChanged (pág. 576). El método addItemListener registra el componente de escucha para un objeto JCheckBox o JRadioButton.

• El método isSelected de JCheckBox determina si un objeto JCheckBox está seleccionado.

• Los objetos JRadioButton tienen dos estados: seleccionado y no seleccionado. Por lo general, los botones de opción (pág. 571) aparecen como un grupo (pág. 577), en el cual sólo puede seleccionarse un botón a la vez.

• Los objetos JRadioButton se utilizan para representar opciones mutuamente exclusivas (pág. 577).

• La relación lógica entre los objetos JRadioButton se mantiene mediante un objeto ButtonGroup (pág. 577).

• El método add de ButtonGroup (pág. 580) asocia a cada objeto JRadioButton con un objeto ButtonGroup. Si se agrega más de un objeto JRadioButton seleccionado a un grupo, el primer objeto JRadioButton seleccionado que se agregue será el que quede seleccionado cuando se muestre la GUI en pantalla.

• Los objetos JRadioButton generan eventos ItemEvent cuando se hace clic sobre ellos.

Sección 14.11 JComboBox: uso de una clase interna anónima para el manejo de eventos• Un objeto JComboBox (pág. 580) proporciona una lista de elementos, de los cuales el usuario puede seleccionar uno.

Los objetos JComboBox generan eventos ItemEvent.

Resumen 621

• Cada elemento en un objeto JComboBox tiene un índice (pág. 582). El primer elemento que se agrega a un objeto JComboBox aparece como el elemento actualmente seleccionado cuando se muestra el objeto JComboBox.

• El método setMaximumRowCount de JComboBox (pág. 583) establece el máximo número de elementos a mostrar cuando el usuario haga clic en el objeto JComboBox.

• Una clase interna anónima (pág. 583) es una clase interna sin nombre y por lo general aparece dentro de la declara-ción de un método. Un objeto de la clase interna anónima debe crearse en el punto en el que se declara la clase.

• El método getSelectedIndex de JcomboBox (pág. 583) devuelve el índice del elemento seleccionado.

Sección 14.12 JList• Un objeto JList muestra una serie de elementos, de los cuales el usuario puede seleccionar uno o más. La clase JList

soporta las listas de selección simple (pág. 584) y de selección múltiple.

• Cuando el usuario hace clic en un elemento de un objeto JList, ocurre un evento ListSelectionEvent (pág. 584). El método addListSelectionListener de JList (pág. 586) registra un objeto ListSelectionListener (pág. 586) para los eventos de selección de un objeto JList. Un objeto ListSelectionListener del paquete javax.swing.event (pág. 567) debe implementar el método valueChanged.

• El método setVisibleRowCount de JList (pág. 586) especifi ca el número de elementos visibles en la lista.

• El método setSelectionMode de JList (pág. 586) especifi ca el modo de selección de una lista (pág. 586).

• Un objeto JList se puede adjuntar a un JScrollPane (pág. 586) para proveer una barra de desplazamiento (pág. 583) para ese objeto JList.

• El método getContentPane de JFrame (pág. 586) devuelve una referencia al panel de contenido de JFrame, en donde se muestran los componentes de la GUI.

• El método getSelectedIndex de JList (pág. 586) devuelve el índice del elemento seleccionado.

Sección 14.13 Listas de selección múltiple• Una lista de selección múltiple (pág. 584) permite al usuario seleccionar muchos elementos de un objeto JList.

• El método setFixedCellWidth de JList (pág. 588) establece la anchura de un objeto JList. El método setFixed-CellHeight (pág. 588) establece la altura de cada elemento en un objeto JList.

• Por lo general, un evento externo (pág. 588) generado por otro componente de la GUI (como un JButton) especifi ca cuándo deben procesarse las selecciones múltiples en un objeto JList.

• El método setListData de JList (pág. 589) establece los elementos a mostrar en un objeto JList. El método get-SelectedValues de JList (pág. 589) devuelve un arreglo de objetos Object que representan los elementos seleccionados en un objeto JList.

Sección 14.14 Manejo de eventos de ratón• Las interfaces de escucha de eventos MouseListener (pág. 570) y MouseMotionListener (pág. 589) se utilizan para

manejar los eventos del ratón (pág. 570). Estos eventos se pueden atrapar para cualquier componente de la GUI que extienda a Component.

• La interfaz MouseInputListener (pág. 589) del paquete javax.swing.event extiende a las interfaces MouseListener y MouseMotionListener para crear una sola interfaz que contenga a todos sus métodos.

• Cada uno de los métodos manejadores de eventos del ratón recibe un objeto MouseEvent (pág. 570), el cual con-tiene información acerca del evento, incluyendo las coordenadas x y y de la ubicación en donde ocurrió el evento. Estas coordenadas se miden empezando desde la esquina superior izquierda del componente de la GUI en donde ocurrió el evento.

• Los métodos y constantes de la clase InputEvent (pág. 589; superclase de MouseEvent) permiten a una aplicación de-terminar cuál botón oprimió el usuario.

• La interfaz MouseWheelListener (pág. 590) permite a las aplicaciones responder a la rotación de la rueda de un ratón.

622 Capítulo 14 Componentes de la GUI: Parte 1

Sección 14.15 Clases adaptadoras• Una clase adaptadora (pág. 594) implementa a una interfaz y proporciona implementaciones predeterminadas de sus

métodos. Al extender una clase adaptadora, podemos sobrescribir sólo el (los) método(s) que necesitamos.

• El método getClickCount de MouseEvent (pág. 597) devuelve el número de clics consecutivos de los botones del ratón. Los métodos isMetaDown (pág. 604) e isAltDown (pág. 597) determinan cuál botón del ratón oprimió el usuario.

Sección 14.16 Subclase de JPanel para dibujar con el ratón• El método paintComponent de JComponent (pág. 597) se llama cuando se muestra un componente ligero de Swing.

Al sobrescribir este método, puede especifi car cómo dibujar fi guras usando las herramientas de gráfi cos de Java.

• Al sobrescribir el método paintComponent, hay que llamar a la versión de la superclase como la primera instrucción en el cuerpo.

• Las subclases de JComponent soportan la transparencia. Cuando un componente es opaco (pág. 597), paintComponent borra el fondo del componente antes de mostrarlo en pantalla.

• La transparencia de un componente ligero de Swing puede establecerse con el método setOpaque (pág. 597; un argumento false indica que el componente es transparente).

• La clase Point (pág. 599) del paquete java.awt representa una coordenada x-y.

• La clase Graphics (pág. 599) se utiliza para dibujar.

• El método getPoint de MouseEvent (pág. 599) obtiene el objeto Point en donde ocurrió un evento de ratón.

• El método repaint (pág. 599), heredado directamente de la clase Component, indica que un componente debe actua-lizarse en la pantalla lo más pronto posible.

• El método paintComponent recibe un parámetro Graphics, y se llama de manera automática cada vez que un com-ponente ligero necesita mostrarse en la pantalla.

• El método fillOval de Graphics (pág. 600) dibuja un óvalo relleno. Los primeros dos argumentos son la coorde-nada x superior izquierda y la coordenada y superior izquierda del área rectangular, y las últimas dos coordenadas representan la anchura y la altura del área rectangular.

Sección 14.17 Manejo de eventos de teclas• La interfaz KeyListener (pág. 570) se utiliza para manejar eventos de teclas (pág. 570) que se generan cuando se opri-

men y sueltan las teclas en el teclado. El método addKeyListener de la clase Component (pág. 601) registra un objeto KeyListener.

• El método getKeyCode (pág. 603) de KeyEvent (pág. 570) obtiene el código de tecla virtual (pág. 603) de la tecla oprimida. La clase KeyEvent mantiene un conjunto de constantes de código de tecla virtual que representa a todas las teclas en el teclado.

• El método getKeyText (pág. 604) de KeyEvent devuelve una cadena que contiene el nombre de la tecla que se oprimió.

• El método getKeyChar (pág. 604) de KeyEvent obtiene el valor Unicode del carácter escrito.

• El método isActionKey (pág. 604) de KeyEvent determina si la tecla en un evento fue una tecla de acción (pág. 601).

• El método getModifiers (pág. 604) de InputEvent determina si se oprimió alguna tecla modifi cadora (como Mayús, Alt y Ctrl ) cuando ocurrió el evento de tecla.

• El método getKeyModifiersText (pág. 604) de KeyEvent produce una cadena que contiene los nombres de las teclas modifi cadoras que se oprimieron.

Sección 14.18 Introducción a los administradores de esquemas• Los administradores de esquemas (pág. 559) ordenan los componentes de la GUI en un contenedor, para fi nes de

presentación.

• Todos los administradores de esquemas implementan la interfaz LayoutManager (pág. 604) del paquete java.awt.

• El método setLayout (pág. 599) de la clase Container especifi ca el esquema de un contenedor.

Resumen 623

• FlowLayout (pág. 559) coloca componentes de izquierda a derecha, en el orden en el que se agregaron al contenedor. Cuando se llega al borde del contenedor, los componentes siguen mostrándose en la siguiente línea. FlowLayout permi-te a los componentes de la GUI alinearse a la izquierda, al centro (el valor predeterminado) y a la derecha.

• El método setAlignment (pág. 608) de FlowLayout cambia la alineación para un objeto FlowLayout.

• BorderLayout (pág. 592) (el predeterminado para un objeto JFrame) ordena los componentes en cinco regiones: NORTH, SOUTH, EAST, WEST y CENTER. NORTH corresponde a la parte superior del contenedor.

• Un BorderLayout limita a un objeto Container para que contenga cuando mucho cinco componentes; uno en cada región.

• GridLayout (pág. 611) divide un contenedor en una cuadrícula de fi las y columnas.

• El método validate (pág. 613) de Container recalcula el esquema del contenedor, con base en el administrador de esquemas actual para ese objeto Container y el conjunto actual de componentes de la GUI que se muestran en pantalla.

Sección 14.19 Uso de paneles para administrar esquemas más complejos• Las GUI complejas consisten a menudo de varios paneles con distintos esquemas. Cada JPanel puede tener compo-

nentes, incluyendo otros paneles, los cuales se adjuntan mediante el método add de Container.

Sección 14.20 JTextArea• Un objeto JTextArea (pág. 615) puede contener varias líneas de texto. JTextArea es una subclase de JTextComponent.

• La clase Box (pág. 617) es una subclase de Container que utiliza un administrador de esquemas BoxLayout (pág. 617) para ordenar los componentes de la GUI, ya sea en forma horizontal o vertical.

• El método static createHorizontalBox (pág. 617) de Box crea un objeto Box que ordena los componentes de iz-quierda a derecha, en el orden en el que se adjuntan.

• El método getSelectedText (pág. 617) devuelve el texto seleccionado de un objeto JTextArea.

• Podemos establecer las políticas de las barras de desplazamiento horizontal y vertical (pág. 617) de un objeto JScroll-Pane al momento de crearlo. Los métodos setHorizontalScrollBarPolicy (pág. 618) y setVerticalScrollBarPolicy (pág. 618) de JScrollPane pueden usarse para modifi car las políticas de las barras de desplazamiento en cualquier momento.

Ejercicios de autoevaluación14.1 Complete las siguientes oraciones:

a) El método es llamado cuando el ratón se mueve sin oprimir los botones y un componente de es-cucha de eventos está registrado para manejar el evento.

b) El texto que no puede ser modificado por el usuario se llama texto .c) Un ordena los componentes de la GUI en un objeto Container.d) El método add para adjuntar componentes de la GUI es un método de la clase .e) GUI es un acrónimo para .f ) El método se utiliza para especificar el administrador de esquemas para un contenedor.g) Una llamada al método mouseDragged va precedida por una llamada al método y va seguida de

una llamada al método .h) La clase contiene métodos que muestran diálogos de mensaje y diálogos de entrada.i) Un diálogo de entrada capaz de recibir entrada del usuario se muestra con el método de la clase

.j) Un diálogo capaz de mostrar un mensaje al usuario se muestra con el método de la clase

.k) JTextField y JTextArea extienden directamente a la clase .

14.2 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.a) BorderLayout es el administrador de esquemas predeterminado para un panel de contenido de JFrame.b) Cuando el cursor del ratón se mueve hacia los límites de un componente de la GUI, se hace una llamada al

método mouseOver.c) Un objeto JPanel no puede agregarse a otro JPanel.

624 Capítulo 14 Componentes de la GUI: Parte 1

d) En un esquema BorderLayout, dos botones que se agreguen a la región NORTH se mostrarán uno al lado del otro.e) Se puede agregar un máximo de cinco componentes un BorderLayout.f ) Las clases internas no pueden acceder a los miembros de la clase que las encierra.g) El texto de un objeto JTextArea siempre es de sólo lectura.h) La clase JTextArea es una subclase directa de la clase Component.

14.3 Encuentre el (los) error(es) en cada una de las siguientes instrucciones y explique cómo corregirlo(s). a) nombreBoton = JButton( “Leyenda” );

b) JLabel unaEtiqueta, JLabel; // crear referencias c) campoTexto = new JTextField( 50, “Texto predeterminado” );

d) setLayout( new BorderLayout() );boton1 = new JButton( “Estrella del norte” );

boton2 = new JButton( “Polo sur” );

contenedor.add( boton1 );

contenedor.add( boton2 );

Respuestas a los ejercicios de autoevaluación14.1 a) mouseMoved. b) no editable (de sólo lectura). c) administrador de esquemas. d) Container. e) interfaz grá-fica de usuario. f ) setLayout. g) mousePressed, mouseReleased. h) JOptionPane. i) showInputDialog, JOptionPane. j) showMessageDialog, JOptionPane. k) JTextComponent.

14.2 a) Verdadero.b) Falso. Se hace una llamada al método mouseEntered.c) Falso. Un JPanel puede agregarse a otro JPanel, ya que JPanel es una subclase indirecta de Component. Por lo

tanto, un JPanel es un Component. Cualquier Component puede agregarse a un Container.d) Falso. Sólo se mostrará el último botón que se agregue. Recuerde que sólo debe agregarse un componente a

cada región en un esquema BorderLayout.e) Verdadero. [Nota: se pueden agregar paneles que contienen varios componentes en cada región].f ) Falso. Las clases internas tienen acceso a todos los miembros de la declaración de la clase que las encierra.g) Falso. Los objetos JTextArea pueden editarse de manera predeterminada.h) Falso. JTextArea se deriva de la clase JTextComponent.

14.3 a) Se necesita new para crear un objeto.b) JLabel es el nombre de una clase y no puede utilizarse como nombre de variable.c) Los argumentos que se pasan al constructor están invertidos. El objeto String debe pasarse primero.d) Se ha establecido BorderLayout y los componentes se agregarán sin especificar la región, por lo que ambos se

agregarán a la región central. Las instrucciones add apropiadas serían:contenedor.add( boton1, BorderLayout.NORTH );

contenedor.add( boton2, BorderLayout.SOUTH );

Ejercicios14.4 Complete las siguientes oraciones:

a) La clase JTextField extiende directamente a la clase .b) El método de Container adjunta un componente de la GUI a un contenedor.c) El método es llamado cuando se suelta uno de los botones del ratón (sin mover el ratón).d) La clase se utiliza para crear un grupo de objetos JRadioButton.

14.5 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.a) Sólo puede usarse un administrador de esquemas por cada objeto Container.b) Los componentes de la GUI pueden agregarse a un objeto Container en cualquier orden, en un esquema

BorderLayout.c) Los objetos JRadioButton proporcionan una serie de opciones mutuamente exclusivas (es decir, sólo uno

puede ser true en un momento dado).

Ejercicios 625

d) El método setFont de Graphics se utiliza para establecer el tipo de letra para los campos de texto.e) Un objeto JList muestra una barra de desplazamiento si hay más elementos en la lista de los que puedan

mostrarse en pantalla.f ) Un objeto Mouse tiene un método llamado mouseDragged.

14.6 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.a) Un objeto JPanel es un objeto JComponent.b) Un objeto JPanel es un objeto Component.c) Un objeto JLabel es un objeto Container.d) Un objeto JList es un objeto JPanel.e) Un objeto AbstractButton es un objeto JButton.f ) Un objeto JTextField es un objeto Object.g) ButtonGroup es una subclase de JComponent.

14.7 Encuentre los errores en cada una de las siguientes líneas de código y explique cómo corregirlos. a) import javax.swing.JFrame

b) objetoPanel.GridLayout( 8, 8 ); // establecer esquema GridLayout c) contenedor.setLayout( new FlowLayout( FlowLayout.DEFAULT ) );

d) contenedor.add( botonEste, EAST ); // BorderLayout

14.8 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

14.9 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

14.10 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

14.11 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

626 Capítulo 14 Componentes de la GUI: Parte 1

14.12 (Conversión de temperatura) Escriba una aplicación de conversión de temperatura, que convierta de grados Fahrenheit a Centígrados. La temperatura en grados Fahrenheit deberá introducirse desde el teclado (mediante un objeto JTextField). Debe usarse un objeto JLabel para mostrar la temperatura convertida. Use la siguiente fórmula para la conversión:

Centígrados = 5–9 � ( Fahrenheit – 32 )

14.13 (Modificación a la conversión de temperatura) Mejore la aplicación de conversión de temperatura del ejercicio 14.12, agregando la escala de temperatura Kelvin. Además, la aplicación debe permitir al usuario realizar conversiones entre dos escalas cualesquiera. Use la siguiente fórmula para la conversión entre Kelvin y Centígrados (además de la fórmu-la del ejercicio 14.12):

Kelvin = Centígrados + 273.15

14.14 (Juego: adivine el número) Escriba una aplicación que juegue a “adivinar el número” de la siguiente manera: su aplicación debe elegir el número a adivinar, seleccionando un entero al azar en el rango de 1 a 1000. La aplicación entonces deberá mostrar lo siguiente en una etiqueta:

Tengo un numero entre 1 y 1000. Puede usted adivinarlo?Por favor escriba su primer intento.

Debe usarse un objeto JTextField para introducir el intento. A medida que se introduzca cada intento, el color de fondo deberá cambiar ya sea a rojo o azul. Rojo indica que el usuario se está “acercando” y azul indica que el usuario se está “alejando”. Un objeto JLabel deberá mostrar el mensaje “Demasiado alto” o “Demasiado bajo” para ayudar al usuario a tratar de adivinar correctamente el número. Cuando el usuario adivine correctamente, deberá mostrarse el mensaje “Correcto!”, y el objeto JTextField utilizado para la entrada deberá cambiar para que no pueda editarse. Debe proporcionarse un objeto JButton para permitir al usuario jugar de nuevo. Cuando se haga clic en el objeto JButton, deberá generarse un nuevo número aleatorio y el objeto JTextField de entrada deberá cambiar para poder editarse otra vez.

14.15 (Mostrar eventos) A menudo es conveniente mostrar los eventos que ocurren durante la ejecución de una aplicación. Esto puede ayudarle a comprender cuándo ocurren los eventos y cómo se generan. Escriba una aplicación que permita al usuario generar y procesar cada uno de los eventos descritos en este capítulo. La aplicación deberá proporcionar métodos de las interfaces ActionListener, ItemListener, ListSelectionListener, MouseListener, MouseMotionListener y KeyListener, para mostrar mensajes cuando ocurran los eventos. Use el método toString para convertir los objetos evento que se reciban en cada manejador de eventos, en un objeto String que pueda mos-trarse en pantalla. El método toString crea un objeto String que contiene toda la información del objeto evento.

14.16 (Juego de craps basado en GUI) Modifique la aplicación de la sección 6.10 para proporcionar una GUI que permita al usuario hacer clic en un objeto JButton para tirar los dados. La aplicación debe también mostrar cuatro objetos JLabel y cuatro objetos JTextField, con un objeto JLabel para cada objeto JTextField. Los objetos JTextField deben usarse para mostrar los valores de cada dado, y la suma de los dados después de cada tiro. El punto debe mostrarse en el cuarto objeto JTextField cuando el usuario no gane o pierda en el primer tiro, y debe seguir mostrándose hasta que el usuario pierda el juego.

(Opcional) Ejercicio del caso de estudio de GUI y gráficos: expansión de la interfaz14.17 (Aplicación de dibujo interactiva) En este ejercicio, implementará una aplicación de GUI que utiliza la jerar-quía MiFigura del ejercicio 10.2 del caso de estudio de GUI y gráficos, para crear una aplicación de dibujo interactiva. Debe crear dos clases para la GUI y proporcionar una clase de prueba para iniciar la aplicación. Las clases de la jerarquía MiFigura no requieren modificaciones adicionales.

La primera clase a crear es una subclase de JPanel llamada PanelDibujo, la cual representa el área en la cual el usuario dibuja las figuras. La clase PanelDibujo debe tener las siguientes variables de instancia:

a) Un arreglo llamado figuras de tipo MiFigura, que almacene todas las figuras que dibuje el usuario.b) Una variable entera llamada cuentaFiguras, que cuente el número de figuras en el arreglo.c) Una variable entera llamada tipoFigura, que determine el tipo de la figura a dibujar.d) Un objeto MiFigura llamado figuraActual, que represente la figura actual que está dibujando el usuario.

Ejercicios 627

e) Un objeto Color llamado colorActual, que represente el color del dibujo actual.f ) Una variable boolean llamada figuraRellena, que determine si se va a dibujar una figura rellena.g) Un objeto JLabel llamado etiquetaEstado, que represente a la barra de estado. Esta barra deberá mostrar las

coordenadas de la posición actual del ratón.

La clase PanelDibujo también debe declarar los siguientes métodos:a) El método sobrescrito paintComponent, que dibuja las figuras en el arreglo. Use la variable de instancia

cuentaFiguras para determinar cuántas figuras hay que dibujar. El método paintComponent también debe llamar al método draw de figuraActual, siempre y cuando figuraActual no sea null.

b) Métodos establecer para tipoFigura, colorActual y figuraRellena.c) El método borrarUltimaFigura debe borrar la última figura dibujada, decrementando la variable de ins-

tancia cuentaFiguras. Asegúrese de que cuentaFiguras nunca sea menor que cero.d) El método borrarDibujo debe eliminar todas las figuras en el dibujo actual, estableciendo cuentaFiguras

en cero.

Los métodos borrarUltimaFigura y borrarDibujo deben llamar al método repaint (heredado de Jpanel) para actualizar el dibujo en el objeto PanelDibujo, indicando que el sistema nunca debe llamar al método paintComponent.

La clase PanelDibujo también debe proporcionar el manejo de eventos, para permitir al usuario dibujar con el ratón. Cree una clase interna individual que extienda a MouseAdapter e implemente a MouseMotionListener para manejar todos los eventos de ratón en una clase.

En la clase interna, sobrescriba el método mousePressed de manera que asigne a figuraActual una nueva figura del tipo especificado por tipoFigura, y que inicialice ambos puntos con la posición del ratón. A continuación, sobrescriba el método mouseReleased para terminar de dibujar la figura actual y colocarla en el arreglo. Establezca el segundo punto de figuraActual con la posición actual del ratón y agregue figuraActual al arreglo. La variable de instancia cuentaFiguras determina el índice de inserción. Establezca figuraActual a null y llame al método repaint para actualizar el dibujo con la nueva figura.

Sobrescriba el método mouseMoved para establecer el texto de etiquetaEstado, de manera que muestre las coor-denadas del ratón; esto actualizará la etiqueta con las coordenadas cada vez que el usuario mueva (pero no arrastre) el ratón dentro del objeto PanelDibujo. A continuación, sobrescriba el método mouseDragged de manera que establezca el segundo punto de figuraActual con la posición actual del ratón y llame al método repaint. Esto permitirá al usua-rio ver la figura mientras arrastra el ratón. Además, actualice el objeto JLabel en mouseDragged con la posición actual del ratón.

Cree un constructor para PanelDibujo que tenga un solo parámetro JLabel. En el constructor, inicialice etique-taEstado con el valor que se pasa al parámetro. Además, inicialice el arreglo figuras con 100 entradas, cuentaFiguras con 0, tipoFigura con el valor que represente a una línea, figuraActual con null y colorActual con Color.BLACK. El constructor deberá entonces establecer el color de fondo del objeto PanelDibujo a Color.WHITE y registrar a MouseListener y MouseMotionListener, de manera que el objeto JPanel maneje los eventos de ratón en forma apropiada.

A continuación, cree una subclase de JFrame llamada MarcoDibujo, que proporcione una GUI que permita al usuario controlar varios aspectos del dibujo. Para el esquema del objeto MarcoDibujo, recomendamos BorderLayout, con los com-ponentes en la región NORTH, el panel de dibujo principal en la región CENTER y una barra de estado en la región SOUTH, como en la figura 14.49. En el panel superior, cree los componentes que se listan a continuación. El manejador de eventos de cada componente deberá llamar al método apropiado en la clase PanelDibujo.

a) Un botón para deshacer la última figura que se haya dibujado.b) Un botón para borrar todas las figuras del dibujo.c) Un cuadro combinado para seleccionar el color de los 13 colores predefinidos.d) Un cuadro combinado para seleccionar la figura a dibujar.e) Una casilla de verificación que especifique si una figura debe estar rellena o sin relleno.

Declare y cree los componentes de la interfaz en el constructor de MarcoDibujo. Necesitará crear la barra de estado JLabel antes de crear el objeto PanelDibujo, de manera que pueda pasar el objeto JLabel como argumento para el cons-tructor de PanelDibujo. Por último, cree una clase de prueba para inicializar y mostrar el objeto MarcoDibujo para ejecutar la aplicación.

628 Capítulo 14 Componentes de la GUI: Parte 1

14.18 (Versión del caso de estudio del ATM basada en GUI) Vuelva a implementar el caso de estudio del ATM de los capítulos 12 y 13 como una aplicación basada en GUI. Use componentes de GUI para crear un diseño aproximado de la interfaz que se muestra en la figura 12.1 Para el dispensador de efectivo y la ranura de depósito, use objetos JButton etique-tados como Recoger efectivo e Insertar sobre. Esto permitirá a la aplicación recibir eventos que indiquen cuando el usua-rio toma el efectivo e inserta un sobre de depósito, respectivamente.

Marcar la diferencia14.19 (Ecofont) Ecofont (www.ecofont.eu/ecofont_en.html) —desarrollada por SPRANQ (una compañía con sede en los Países Bajos)— es un tipo de letra de computadora gratuito de código fuente abierto, diseñado para reducir hasta en un 20% la cantidad de tinta utilizada para imprimir, con lo cual se reduce también el número de cartuchos de tinta utilizados y el impacto ambiental de los procesos de manufactura y envío (se usa menos energía, menos combus-tible para el envío, etcétera). El tipo de letra, basado en Verdana sans-serif, tiene pequeños “orificios” circulares en las letras que no son visibles en tamaños más pequeños —como el tipo de 9 o 10 puntos de uso frecuente. Descargue Ecofont, después instale el archivo de tipo de letra Spranq_eco_sans_regular.ttf usando las instrucciones del sitio Web de Ecofont. A continuación, desarrolle un programa basado en GUI que le permita escribir una cadena de texto para visualizarse en Ecofont. Cree botones Aumentar tamaño de letra y Reducir tamaño de letra que le permitan esca-lar hacia arriba o hacia abajo, un punto a la vez. Empiece con un tamaño de tipo de letra predeterminado de 9 puntos. A medida que vaya escalando hacia arriba, podrá ver los orificios en las letras con más claridad. A medida que vaya escalando hacia abajo, los orificios serán menos aparentes. ¿Cuál es el menor tamaño de tipo de letra en el que empezó a notar los orificios?

14.20 (Tutor de mecanografía: optimizar una habilidad crucial en la era de las computadoras) Escribir con rapidez y en forma correcta es una habilidad esencial para trabajar de manera efectiva con las computadoras e Internet. En este ejercicio, creará una aplicación de GUI que pueda ayudar a los usuarios a aprender a “escribir al tacto” (es decir, escribir correctamente sin ver el teclado). La aplicación deberá mostrar un teclado virtual (figura 14.50) y deberá permitir al usuario ver lo que escribe en la pantalla, sin tener que ver el teclado real. Use objetos JButton para representar las teclas. A medida que el usuario oprima cada tecla, la aplicación deberá resaltar el objeto JButton correspondiente en la GUI, y

Marcar la diferencia 629

Fig. 14.49 � Interfaz para dibujar figuras.

agregará el carácter a un objeto JTextArea que mostrará lo que ha escrito el usuario en un momento dado. [Sugerencia: para resaltar un JButton, use su método setBackground para cambiar su color de fondo. Cuando se libere la tecla, resta-blezca su color de fondo original. Puede obtener el color de fondo original del objeto JButton mediante el método getBackground antes de cambia su color].

630 Capítulo 14 Componentes de la GUI: Parte 1

Fig. 14.50 � Tutor de mecanografía.

Para probar su programa, escriba un pangrama: una frase que contiene todas las letras del alfabeto por lo menos una vez, como “El veloz murciélago hindú comía feliz cardillo y kiwi. La cigüeña tocaba el saxofón detrás del palenque de paja”. Encontrará más pangramas en Web.

Para que el programa sea más interesante, podría monitorear la precisión del usuario. Podría hacer que el usuario escribiera frases específicas que tenga pregrabadas en su programa, y que muestre en la pantalla por encima del teclado virtual. Podría llevar la cuenta de cuántas pulsaciones de teclas ha escrito correctamente el usuario, y cuántas escribió en forma incorrecta. Podría también llevar la cuenta de cuáles son las teclas que se le dificultan al usuario, y mostrar un infor-me en donde aparezcan esas teclas.

Gráfi cos y Java 2D 15Una imagen vale más que mil palabras.—Proverbio chino

Hay que tratar a la naturaleza en términos del cilindro, de la esfera, del cono, todo en perspectiva.—Paul Cézanne

Los colores, al igual que las características, siguen los cambios de las emociones.—Pablo Picasso

Nada se vuelve real sino hasta que se experimenta; incluso un proverbio no será proverbio para usted, sino hasta que su vida lo haya ilustrado.—John Keats

O b j e t i v o sEn este capítulo aprenderá a:

■ Comprender los contextos y los objetos de gráficos.

■ Entender y manipular los colores y los tipos de letra.

■ Usar métodos de la clase Graphics para dibujar varias figuras.

■ Utilizar métodos de la clase Graphics2D de la API Java 2D para dibujar diversas figuras.

■ Especificar las características Paint y Stroke de las figuras mostradas con Graphics2D.

632 Capítulo 15 Gráfi cos y Java 2D

15.1 Introducción

15.2 Contextos y objetos de gráfi cos

15.3 Control de colores

15.4 Manipulación de tipos de letra

15.5 Dibujo de líneas, rectángulos y óvalos

15.6 Dibujo de arcos

15.7 Dibujo de polígonos y polilíneas

15.8 La API Java 2D

15.9 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios | Caso de estudio opcional de GUI y gráfi cos: agregar Java 2D | Marcar la diferencia

15.1 IntroducciónEn este capítulo veremos varias de las herramientas de Java para dibujar figuras bidimensionales, con-trolar colores y fuentes. Uno de los principales atractivos de Java era su soporte para gráficos, el cual permitía a los programadores mejorar la apariencia visual de sus aplicaciones. Ahora, Java contiene muchas más herramientas sofisticadas de dibujo como parte de la API Java 2D. Comenzaremos este capítulo con una introducción a muchas de las herramientas de dibujo originales de Java. Después presentaremos varias de las más poderosas herramientas de Java 2D, como el control del estilo de líneas utilizadas para dibujar figuras y el control del relleno de las figuras con colores y patrones. Las clases que eran parte de las herramientas de gráficos originales de Java ahora se consideran parte de la API Java 2D.

En la figura 15.1 se muestra una parte de la jerarquía de clases de Java que incluye varias de las clases de gráficos básicas y las clases e interfaces de la API Java 2D que cubriremos en este capítulo. La clase Color contiene métodos y constantes para manipular los colores. La clase JComponent con-tiene el método paintComponent, que se utiliza para dibujar gráficos en un componente. La clase Font contiene métodos y constantes para manejar los tipos de letras. La clase FontMetrics contiene méto-dos y constantes para manipular tipos de letras. La clase Graphics contiene métodos para dibujar cadenas, líneas, rectángulos y demás figuras. La clase Graphics2D, que extiende a la clase Graphics, se utiliza para dibujar con la API Java 2D. La clase Polygon contiene métodos para crear polígonos. La mitad inferior de la figura muestra varias clases e interfaces de la API Java 2D. La clase BasicStroke ayuda a especificar las características de dibujo de las líneas. Las clases GradientPaint y Texture-Paint ayudan a especificar las características para rellenar figuras con colores o patrones. Las clases GeneralPath, Line2D, Arc2D, Ellipse2D, Rectangle2D y RoundRectangle2D representan varias fi-guras de Java 2D.

Para empezar a dibujar en Java, primero debemos entender su sistema de coordenadas (figura 15.2), el cual es un esquema para identificar a cada uno de los posibles puntos en la pantalla. De ma-nera predeterminada, la esquina superior izquierda de un componente de la GUI (como una ventana) tiene las coordenadas (0,0). Un par de coordenadas está compuesto por una coordenada x (la coorde-nada horizontal) y una coordenada y (la coordenada vertical). La coordenada x es la distancia hori-zontal que se desplaza hacia la derecha, desde la parte izquierda de la pantalla. La coordenada y es la distancia vertical que se desplaza hacia abajo, desde la parte superior de la pantalla. El eje x describe cada una de las coordenadas horizontales, y el eje y describe cada una de las coordenadas verticales. Las coordenadas se utilizan para indicar en dónde deben mostrarse los gráficos en una pantalla. Las unida-des de las coordenadas se miden en píxeles (lo que se conoce como “elementos de imagen”). Un píxel es la unidad más pequeña de resolución de un monitor de computadora.

15.1 Introducción 633

Tip de portabilidad 15.1Existen distintos tipos de monitores de computadora con distintas resoluciones (es decir, la densidad de los píxeles varía). Esto puede hacer que los gráficos aparezcan de distintos tamaños en distintos monitores, o en el mismo monitor con distintas configuraciones.

Fig. 15.1 � Clases e interfaces utilizadas en este capítulo, provenientes de las herramientas de gráficos originales de Java y de la API Java2D.

java.awt.Color

java.lang.Object

java.awt.Component

java.awt.Font

java.awt.FontMetrics

java.awt.Graphics

java.awt.Polygon

java.awt.geom.Arc2D

java.awt.geom.Ellipse2D

java.awt.geom.Rectangle2D

java.awt.geom.RoundRectangle2D

java.awt.Graphics2D

java.awt.Container javax.swing.JComponent

«interfaz»java.awt.Paint

«interfaz»java.awt.Shape

«interfaz»java.awt.Stroke

java.awt.BasicStroke

java.awt.GradientPaint

java.awt.TexturePaint

java.awt.geom.GeneralPath

java.awt.geom.Line2D

java.awt.geom.RectangularShape

634 Capítulo 15 Gráfi cos y Java 2D

15.2 Contextos y objetos de gráficosUn contexto de gráficos permite dibujar en la pantalla. Un objeto Graphics administra un contexto de gráficos y dibuja píxeles en la pantalla que representan texto y otros objetos gráficos (como líneas, elipses, rectángulos y otros polígonos). Los objetos Graphics contienen métodos para dibujar, mani-pular tipos de letra, manipular colores y varias cosas más.

La clase Graphics es una clase abstract (es decir, no pueden instanciarse objetos Graphics). Esto contribuye a la portabilidad de Java. Como el dibujo se lleva a cabo de manera distinta en cada platafor-ma que soporta a Java, no puede haber sólo una implementación de las herramientas de dibujo en todos los sistemas. Por ejemplo, las herramientas de gráficos que permiten a una PC con Microsoft Windows dibujar un rectángulo, son distintas de las herramientas de gráficos que permiten a una estación de trabajo Linux dibujar un rectángulo; y ambas son distintas de las herramientas de gráficos que permiten a una Macintosh dibujar un rectángulo. Cuando Java se implementa en cada plataforma, se crea una subclase de Graphics que implementa las herramientas de dibujo. Esta implementación está oculta para nosotros por medio de la clase Graphics, la cual proporciona la interfaz que nos permite utilizar gráficos de una manera independiente de la plataforma.

En el capítulo 14 vimos que la clase Component es la superclase para muchas de las clases en el pa-quete java.awt. La clase JComponent (paquete javax.swing), que hereda de manera indirecta de la clase Component, contiene un método llamado paintComponent, que puede utilizarse para dibujar gráficos. El método paintComponent toma un objeto Graphics como argumento. El sistema pasa este objeto al método paintComponent cuando se requiere volver a pintar un componente ligero de Swing. El encabezado del método paintComponent es:

public void paintComponent( Graphics g )

El parámetro g recibe una referencia a una instancia de la subclase específica del sistema que Graphics extiende. Tal vez a usted le parezca conocido el encabezado del método anterior; es el mismo que utili-zamos en algunas de las aplicaciones del capítulo 14. En realidad, la clase JComponent es una superclase de JPanel. Muchas herramientas de la clase JPanel son heredadas de la clase JComponent.

El método paintComponent raras veces es llamado directamente por el programador, ya que el dibujo de gráficos es un proceso controlado por eventos. Como vimos en el capítulo 11, Java usa un modelo multihilos de ejecución del programa. Cada hilo es una actividad paralela. Cada programa puede tener muchos hilos. Al crear una aplicación de GUI, uno de esos hilos es el hilo de despacha-miento de eventos (EDT): es el que se utiliza para procesar todos los eventos de GUI. Todas las opera-ciones de dibujo y manipulación de los componentes de GUI se deben realizar en ese hilo. Cuando se ejecuta una aplicación de GUI, el contenedor de la aplicación llama al método paintComponent (en

Fig. 15.2 � Sistema de coordenadas de Java. Las unidades se miden en píxeles.

(0, 0)

(x, y)

+y

+x

eje y

eje x

15.3 Control de colores 635

el hilo de despachamiento de eventos) para cada componente ligero, a medida que se muestra la GUI en pantalla. Para que paintComponent sea llamado de nuevo, debe ocurrir un evento (como cubrir y descubrir el componente con otra ventana).

Si el programador necesita hacer que se ejecute paintComponent (es decir, si desea actualizar los gráficos dibujados en el componente de Swing), se hace una llamada al método repaint, que to-dos los objetos JComponent heredan indirectamente de la clase Component (paquete java.awt). El en-cabezado para repaint es:

public void repaint()

15.3 Control de coloresLa clase Color declara los métodos y las constantes para manipular los colores en un programa de Java. Las constantes de colores previamente declaradas se sintetizan en la figura 15.3, y varios métodos y cons-tructores para los colores se sintetizan en la figura 15.4. Dos de los métodos de la figura 15.4 son métodos de Graphics que son específicos para los colores.

Constante de Color Valor RGB

public final static Color RED 255, 0, 0

public final static Color GREEN 0, 255, 0

public final static Color BLUE 0, 0, 255

public final static Color ORANGE 255, 200, 0

public final static Color PINK 255, 175, 175

public final static Color CYAN 0, 255, 255

public final static Color MAGENTA 255, 0, 255

public final static Color YELLOW 255, 255, 0

public final static Color BLACK 0, 0, 0

public final static Color WHITE 255, 255, 255

public final static Color GRAY 128, 128, 128

public final static Color LIGHT_GRAY 192, 192, 192

public final static Color DARK_GRAY 64, 64, 64

Fig. 15.3 � Constantes de Color y sus valores RGB.

Fig. 15.4 � Los métodos de Color y los métodos de Graphics relacionados con los colores (parte 1 de 2).

Método Descripción

Constructores y métodos de Color

public Color( int r, int g, int b )

Crea un color basado en los componentes rojo, verde y azul, expresados como enteros de 0 a 255.

public Color( float r, float g, float b )

Crea un color basado en los componentes rojo, verde y azul, expresados como valores de punto fl otante de 0.0 a 1.0.

636 Capítulo 15 Gráfi cos y Java 2D

Todo color se crea a partir de un componente rojo, uno verde y otro azul. En conjunto, a estos componentes se les llama valores RGB. Los tres componentes RGB pueden ser enteros en el rango de 0 a 255, o pueden ser valores de punto flotante en el rango de 0.0 a 1.0. El primer componente RGB especifica la cantidad de rojo, el segundo, de verde y el tercero, de azul. Entre mayor sea el valor RGB, mayor será la cantidad de ese color en particular. Java permite al programador seleccio-nar de entre 256 � 256 � 256 (o aproximadamente 16.7 millones de) colores. No todas las compu-tadoras son capaces de mostrar todos estos colores. La computadora mostrará el color más cercano que pueda.

En la figura 15.4 se muestran dos de los constructores de la clase Color (uno que toma tres argu-mentos int y otro que toma tres argumentos float, en donde cada argumento especifica la cantidad de rojo, verde y azul). Los valores int deben estar en el rango de 0 a 255 y los valores float deben estar en el rango de 0.0 a 1.0. El nuevo objeto Color tendrá las cantidades de rojo, verde y azul que se especifiquen. Los métodos getRed, getGreen y getBlue de Color devuelven valores enteros de 0 a 255, los cuales re-presentan la cantidad de rojo, verde y azul, respectivamente. El método getColor de Graphics devuelve un objeto Color que representa el color actual de dibujo. El método setColor de Graphics establece el color actual de dibujo.

Dibujar en distintos coloresLas figuras 15.5 y 15.6 demuestran varios métodos de la figura 15.4, al dibujar rectángulos rellenos y objetos String en varios colores distintos. Cuando la aplicación empieza a ejecutarse, se hace una llamada al método paintComponent de la clase JPanelColor (líneas 10 a 37 de la figura 15.5) para pintar la ventana. En la línea 17 se utiliza el método setColor de Graphics para establecer el color actual de dibujo. El método setColor recibe un objeto Color. La expresión new Color( 255, 0, 0 ) crea un nuevo objeto Color que representa rojo (valor 255 para rojo y 0 para los valores verde y azul). En la línea 18 se utiliza el método fillRect de Graphics para dibujar un rectángulo relleno con el color actual. El método fillRect dibuja un rectángulo con base en sus cuatro argumentos. Los pri-meros dos valores enteros representan la coordenada x superior izquierda y la coordenada y superior izquierda, en donde el objeto Graphics empieza a dibujar el rectángulo. Los argumentos tercero y cuarto son enteros no negativos que representan la anchura y la altura del rectángulo en píxeles, res-

Fig. 15.4 � Los métodos de Color y los métodos de Graphics relacionados con los colores (parte 2 de 2).

Método Descripción

public int getRed()

Devuelve un valor entre 0 y 255, el cual representa el contenido rojo.

public int getGreen()

Devuelve un valor entre 0 y 255, el cual representa el contenido verde.

public int getBlue()

Devuelve un valor entre 0 y 255, el cual representa el contenido azul.

Métodos de Graphics para manipular objetos Color

public Color getColor()

Devuelve un objeto Color que representa el color actual para el contexto de gráfi cos.

public void setColor( Color c )

Establece el color actual para dibujar con el contexto de gráfi cos.

15.3 Control de colores 637

pectivamente. Un rectángulo que se dibuja usando el método fillRect se rellena con el color actual del objeto Graphics.

Fig. 15.5 � Cambio de Color para dibujar.

1 // Fig. 15.5: JPanelColor.java

2 // Demostración de objetos Color.

3 import java.awt.Graphics;

4 import java.awt.Color;

5 import javax.swing.JPanel;

6

7 public class JPanelColor extends JPanel

8 {

9 // dibuja rectángulos y objetos String en distintos colores

10 public void paintComponent( Graphics g )

11 {

12 super.paintComponent( g ); // llama al método paintComponent de la superclase

13

14 this.setBackground( Color.WHITE );

15

16 // establece nuevo color de dibujo, usando valores enteros

17 g.setColor( new Color( 255, 0, 0 ) );

18 g.fillRect( 15, 25, 100, 20 );

19 g.drawString( “RGB actual: ” + g.getColor(), 130, 40 );

20

21 // establece nuevo color de dibujo, usando valores de punto flotante

22 g.setColor( new Color( 0.50f, 0.75f, 0.0f ) );

23 g.fillRect( 15, 50, 100, 20 );

24 g.drawString( “RGB actual: ” + g.getColor(), 130, 65 );

25

26 // establece nuevo color de dibujo, usando objetos Color static

27 g.setColor( Color.BLUE );

28 g.fillRect( 15, 75, 100, 20 );

29 g.drawString( “RGB actual: ” + g.getColor(), 130, 90 );

30

31 // muestra los valores RGB individuales

32 Color color = Color.MAGENTA;

33 g.setColor( color );

34 g.fillRect( 15, 100, 100, 20 );

35 g.drawString( “Valores RGB: ” + color.getRed() + “, ” +

36 color.getGreen() + “, ” + color.getBlue(), 130, 115 );

37 } // fin del método paintComponent

38 } // fin de la clase JPanelColor

Fig. 15.6 � Creación de un objeto JFrame para mostrar colores en un objeto JPanel (parte 1 de 2).

1 // Fig. 15.6: MostrarColores.java

2 // Demostración de objetos Color.

3 import javax.swing.JFrame;

4

5 public class MostrarColores

6 {

638 Capítulo 15 Gráfi cos y Java 2D

En la línea 19 (figura 15.5) se utiliza el método drawString de Graphics para dibujar un objeto String en el color actual. La expresión g.getColor() recupera el color actual del objeto Graphics. Des-pués concatenamos el objeto Color devuelto con la cadena “RGB actual:”, lo que produce una llamada implícita al método toString de la clase Color. La representación String de un objeto Color contiene el nombre de la clase y el paquete (java.awt.Color), además de los valores rojo, verde y azul.

Observación de apariencia visual 15.1Todos percibimos los colores de una forma distinta. Elija sus colores con cuidado, para asegurarse que su aplicación sea legible, tanto para las personas que pueden percibir el color, como para aquellas que no pueden ver ciertos colores. Trate de evitar usar muchos colores distintos, muy cerca unos de otros.

En las líneas 22 a 24 y 27 a 29 se llevan a cabo las mismas tareas de nuevo. En la línea 22 se utiliza el constructor de Color con tres argumentos float para crear un color verde oscuro (0.50f para rojo, 0.75f para verde y 0.0f para azul). Observe la sintaxis de los valores. La letra f anexada a una literal de punto flotante indica que la literal debe tratarse como de tipo float. Recuerde que, de manera prede-terminada, las literales de punto flotante se tratan como de tipo double.

En la línea 27 se establece el color actual de dibujo a una de las constantes de Color declaradas con anterioridad (Color.BLUE). Las constantes de Color son static, por lo que se crean cuando la clase Color se carga en memoria, en tiempo de ejecución.

La instrucción de las líneas 35 y 36 hace llamadas a los métodos getRed, getGreen y getBlue de Color en la constante Color.MAGENTA antes declarada. El método main de la clase MostrarColores (líneas 8 a 18 de la figura 15.6) crea el objeto JFrame que contendrá un objeto ColorJPanel, en donde se mostrarán los colores.

Fig. 15.6 � Creación de un objeto JFrame para mostrar colores en un objeto JPanel (parte 2 de 2).

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 // crea marco para objeto JPanelColor

11 JFrame frame = new JFrame( “Uso de colores” );

12 frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

13

14 JPanelColor jPanelColor = new JPanelColor(); // create JPanelColor

15 frame.add( jPanelColor ); // agrega jPanelColor a marco

16 frame.setSize( 400, 180 ); // establece el tamaño del marco

17 frame.setVisible( true ); // muestra el marco

18 } // fin de main

19 } // fin de la clase MostrarColores

15.3 Control de colores 639

Observación de ingeniería de software 15.1Para cambiar el color, debe crear un nuevo objeto Color (o utilizar una de las constantes de Color previamente declaradas). Al igual que los objetos String, los objetos Color son inmutables (no pueden modificarse).

El paquete javax.swing proporciona el componente de la GUI JColorChooser, que permite a los usuarios de aplicaciones seleccionar colores. La aplicación de las figuras 15.7 y 15.8 demuestra un cuadro de diálogo JColorChooser. Al hacer clic en el botón Cambiar color, aparece un cuadro de diálogo JColorChooser. Al seleccionar un color y oprimir el botón Aceptar del diálogo, el color de fondo de la ventana de la aplicación cambia.

Fig. 15.7 � Cuadro de diálogo JColorChooser (parte 1 de 2).

1 // Fig. 15.7: MostrarColores2JFrame.java 2 // Selección de colores con JColorChooser. 3 import java.awt.BorderLayout; 4 import java.awt.Color; 5 import java.awt.event.ActionEvent; 6 import java.awt.event.ActionListener; 7 import javax.swing.JButton; 8 import javax.swing.JFrame; 9 import javax.swing.JColorChooser;10 import javax.swing.JPanel;1112 public class MostrarColores2JFrame extends JFrame 13 {14 private JButton cambiarColorJButton;15 private Color color = Color.LIGHT_GRAY;16 private JPanel coloresJPanel;1718 // establece la GUI19 public MostrarColores2JFrame()20 {21 super( “Uso de JColorChooser” );2223 // crea objeto JPanel para mostrar color24 coloresJPanel = new JPanel();25 coloresJPanel.setBackground( color );2627 // establece cambiarColorJButton y registra su manejador de eventos28 cambiarColorJButton = new JButton( “Cambiar color” );29 cambiarColorJButton.addActionListener(3031 new ActionListener() // clase interna anónima32 {33 // muestra JColorChooser cuando el usuario hace clic con el botón34 public void actionPerformed( ActionEvent evento )35 {36 color = JColorChooser.showDialog( 37 MostrarColores2JFrame.this, “Seleccione un color”, color );3839 // establece el color predeterminado, si no se devuelve un color40 if ( color == null )41 color = Color.LIGHT_GRAY;

640 Capítulo 15 Gráfi cos y Java 2D

42

43 // cambia el color de fondo del panel de contenido

44 coloresJPanel.setBackground( color );

45 } // fin del método actionPerformed

46 } // fin de la clase interna anónima

47 ); // fin de la llamada a addActionListener

48

49 add( coloresJPanel, BorderLayout.CENTER ); // agrega coloresJPanel

50 add( cambiarColorJButton, BorderLayout.SOUTH ); // agrega botón

51

52 setSize( 400, 130 ); // establece el tamaño del marco

53 setVisible( true ); // muestra el marco

54 } // fin del constructor de MostrarColores2JFrame

55 } // fin de la clase MostrarColores2JFrame

Fig. 15.7 � Cuadro de diálogo JColorChooser (parte 2 de 2).

Fig. 15.8 � Selección de colores con JColorChooser.

1 // Fig. 15.8: MostrarColores2.java

2 // Selección de colores con JColorChooser.

3 import javax.swing.JFrame;

4

5 public class MostrarColores2

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 MostrarColores2JFrame aplicacion = new MostrarColores2JFrame();

11 aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

12 } // fin de main

13 } // fin de la clase MostrarColores2

(a) Ventana inicial de la aplicación

(c) Ventana de la aplicación después de cambiar el color de fondo del objeto JPanel

Seleccione un color de una de las

muestras de colores.

(b) Ventana de JColorChooser

15.3 Control de colores 641

La clase JColorChooser proporciona el método estático showDialog, el cual crea un objeto JColor-Chooser, lo adjunta a un cuadro de diálogo y lo muestra en pantalla. Las líneas 36 y 37 de la figura 15.7 invocan a este método para mostrar el cuadro de diálogo del selector de colores. El método showDialog devuelve el objeto Color seleccionado, o null si el usuario oprime Cancelar o cierra el cuadro de diálogo sin oprimir Aceptar. Este método recibe tres argumentos: una referencia a su objeto Component padre, un objeto String a mostrar en la barra de título del cuadro de diálogo y el Color inicial seleccionado para el cuadro de diálogo. El componente padre es una referencia a la ventana desde la que se muestra el diálogo (en este caso el objeto JFrame, con el nombre de referencia marco). Este diálogo estará centrado en el componente padre. Si el padre es null, entonces el cuadro de diálogo se centra en la pantalla. Mientras el diálogo para seleccionar colores se encuentre en la pantalla, el usuario no podrá interactuar con el componente padre sino hasta cerrar el diálogo. A este tipo de cuadro de diálogo se le conoce como cuadro de diálogo modal.

Una vez que el usuario selecciona un color, en las líneas 40 y 41 se determina si color es null, y de ser así color se establece en el valor predeterminado Color.LIGHT_GRAY. En la línea 44 se utiliza el mé-todo setBackground para cambiar el color de fondo del objeto JPanel. El método setBackground es uno de los muchos métodos de la clase Component que pueden utilizarse en la mayoría de los componentes de la GUI. El usuario puede seguir utilizando el botón Cambiar color para cambiar el color de fondo de la aplicación. La figura 15.8 contiene el método main, que ejecuta el programa.

La figura 15.8(b) muestra el cuadro de diálogo JColorChooser predeterminado, que permite al usuario seleccionar un color de una variedad de muestras de colores. Hay tres fichas en la parte su-perior del cuadro de diálogo: Muestras, HSB y RGB. Estas fichas representan tres distintas formas de seleccionar un color. La ficha HSB le permite seleccionar un color con base en matiz (hue), saturación (saturation) y brillo (brightness): valores que se utilizan para definir la cantidad de luz en un color. No hablaremos sobre los valores HSB. Para obtener más información sobre ellos, visite http://es.wikipedia.org/wiki/Modelo_de_color_HSV. La ficha RGB le permite seleccionar un color mediante el uso de controles deslizables para seleccionar los componentes rojo, verde y azul del color. Las fichas HSB y RGB se muestran en la figura 15.9.

Fig. 15.9 � Las fichas HSB y RGB del cuadro de diálogo JColorChooser (parte 1 de 2).

642 Capítulo 15 Gráfi cos y Java 2D

15.4 Manipulación de tipos de letraEn esta sección presentaremos los métodos y constantes para controlar los tipos de letras. La mayoría de los métodos y constantes de tipos de letra son parte de la clase Font. Algunos métodos de la clase Font y la clase Graphics se sintetizan en la figura 15.10.

Fig. 15.10 � Métodos y constantes relacionados con Font (parte 1 de 2).

Fig. 15.9 � Las fichas HSB y RGB del cuadro de diálogo JColorChooser (parte 2 de 2).

Controles deslizables para seleccionar los componentes rojo,

verde y azul

Método o constante Descripción

Métodos de Graphics para manipular objetos Font

public final static int PLAIN Constante que representa un estilo de tipo de letra simple.

public final static int BOLD Constante que representa un estilo de tipo de letra en negritas.

public final static int ITALIC Constante que representa un estilo de tipo de letra en cursivas.

public Font( String nombre,

int estilo, int tamaño ) Crea un objeto Font con el nombre de tipo de letra, estilo y tamaño especifi cados.

public int getStyle() Devuelve un int que indica el estilo actual de tipo de letra.

public int getSize() Devuelve un int que indica el tamaño actual del tipo de letra.

public String getName() Devuelve el nombre actual del tipo de letra, como una cadena.

public String getFamily() Devuelve el nombre de la familia del tipo de letra, como una cadena.

public boolean isPlain() Devuelve true si el tipo de letra es simple; false en caso contrario.

public boolean isBold() Devuelve true si el tipo de letra está en negritas; false en caso contrario.

public boolean isItalic() Devuelve true si el tipo de letra está en cursivas; false en caso contrario.

15.4 Manipulación de tipos de letra 643

El constructor de la clase Font recibe tres argumentos: el nombre del tipo de letra, su estilo y su tamaño. El nombre del tipo de letra es cualquier tipo de letra soportado por el sistema en el que se esté ejecutando el programa, como los tipos de letra estándar de Java Monospaced, SansSerif y Serif. El estilo de tipo de letra es Font.PLAIN (simple), Font.ITALIC (cursivas) o Font.BOLD (negritas); cada uno es un campo static de la clase Font. Los estilos de los tipos de letra pueden usarse combinados (por ejemplo, Font.ITALIC + Font.BOLD). El tamaño del tipo de letra se mide en puntos. Un punto es 1/72 de una pulgada. El método setFont de Graphics establece el tipo de letra a dibujar en ese momen-to (el tipo de letra en el cual se mostrará el texto) en base a su argumento Font.

Tip de portabilidad 15.2El número de tipos de letra varía enormemente entre sistemas. Java proporciona cinco nombres de tipos de letras (Serif, Monospaced, SansSerif, Dialog y DialogInput) que pueden usarse en todas las plataformas de Java. El entorno en tiempo de ejecución de Java (JRE) en cada plataforma asigna estos nombres de tipos de letras lógicos a los tipos de letras que están realmente instalados en la plataforma. Los tipos de letras reales que se utilicen pueden variar de una plataforma a otra.

La aplicación de las figuras 15.11 y 15.12 muestra texto en cuatro tipos de letra distintos, con cada tipo de letra en diferente tamaño. La figura 15.11 utiliza el constructor de Font para inicializar objetos Font (en las líneas 16, 20, 24 y 29) que se pasan al método setFont de Graphics para cambiar el tipo de letra para dibujar. Cada llamada al constructor de Font pasa un nombre de tipo de letra (Serif, Monospaced, o SansSerif) como una cadena, un estilo de tipo de letra (Font.PLAIN, Font.ITALIC o Font.BOLD) y un tamaño de tipo de letra. Una vez que se invoca el método setFont de Graphics, todo el texto que se muestre después de la llamada aparecerá en el nuevo tipo de letra hasta que éste se modifique. La información de cada tipo de letra se muestra en las líneas 17, 21, 25, 30 y 31, usando el método drawString. Las coordenadas que se pasan a drawString corresponden a la esquina inferior izquierda de la línea base del tipo de letra. En la línea 28 se cambia el color de dibujo a rojo, por lo que la siguiente cadena que se muestra aparece en color rojo. En las líneas 30 y 31 se muestra informa-ción acerca del objeto Font final. El método getFont de la clase Graphics devuelve un objeto Font que representa el tipo de letra actual. El método getName devuelve el nombre del tipo de letra actual como una cadena. El método getSize devuelve el tamaño del tipo de letra, en puntos.

Observación de ingeniería de software 15.2Para cambiar el tipo de letra, debe crear un nuevo objeto Font. Los objetos Font son inmutables; la clase Font no tiene métodos establecer para modificar las características del tipo de letra actual.

La figura 15.12 contiene el método main, que crea un objeto JFrame para mostrar un objeto FontJPanel. Agregamos un objeto FontJPanel a este objeto JFrame (línea 15), el cual muestra los grá-ficos creados en la figura 15.11.

Fig. 15.10 � Métodos y constantes relacionados con Font (parte 2 de 2).

Método o constante Descripción

Métodos de Graphics para manipular objetos Font

public Font getFont() Devuelve la referencia a un objeto Font que representa el tipo de letra actual.

public void setFont( Font f ) Establece el tipo de letra actual al tipo de letra, estilo y tamaño especifi cados por la referencia f al objeto Font.

644 Capítulo 15 Gráfi cos y Java 2D

Fig. 15.11 � El método setFont de Graphics cambia el tipo de letra para dibujar.

1 // Fig. 15.11: FontJPanel.java 2 // Muestra cadenas en distintos tipos de letra y colores. 3 import java.awt.Font; 4 import java.awt.Color; 5 import java.awt.Graphics; 6 import javax.swing.JPanel; 7 8 public class FontJPanel extends JPanel 9 {10 // muestra objetos String en distintos tipos de letra y colores11 public void paintComponent( Graphics g )12 {13 super.paintComponent( g ); // llama al método paintComponent de la superclase14

15 // establece el tipo de letra a Serif (Times), negrita, 12 puntos y dibuja una cadena

16 g.setFont( new Font( “Serif”, Font.BOLD, 12 ) );17 g.drawString( “Serif 12 puntos, negrita.”, 20, 50 );18

19 // establece el tipo de letra a Monospaced (Courier), cursiva, 24 puntos y dibuja una cadena

20 g.setFont( new Font( “Monospaced”, Font.ITALIC, 24 ) );21 g.drawString( “Monospaced 24 puntos, cursiva.”, 20, 70 );22

23 // establece el tipo de letra a SansSerif (Helvetica), simple, 14 puntos y dibuja una cadena

24 g.setFont( new Font( “SansSerif”, Font.PLAIN, 14 ) );25 g.drawString( “SansSerif 14 puntos, simple.”, 20, 90 );26

27 // establece el tipo de letra a Serif (Times), negrita/cursiva, 18 puntos y dibuja una cadena

28 g.setColor( Color.RED );29 g.setFont( new Font( “Serif”, Font.BOLD + Font.ITALIC, 18 ) );30 g.drawString( g.getFont().getName() + “ ” + g.getFont().getSize() +31 “ puntos, negrita cursiva.”, 20, 110 );32 } // fin del método paintComponent33 } // fin de la clase FontJPanel

Fig. 15.12 � Creación de un objeto JFrame para mostrar tipos de letra (parte 1 de 2).

1 // Fig. 15.12: TiposDeLetra.java 2 // Uso de tipos de letra. 3 import javax.swing.JFrame; 4 5 public class TiposDeLetra 6 { 7 // ejecuta la aplicación 8 public static void main( String[] args ) 9 {10 // crea marco para FontJPanel11 JFrame marco = new JFrame( “Uso de tipos de letra” );12 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );1314 FontJPanel fontJPanel = new FontJPanel(); // crea objeto FontJPanel15 marco.add( fontJPanel ); // agrega objeto fontJPanel al marco

15.4 Manipulación de tipos de letra 645

Métrica de los tipos de letraEn ocasiones es necesario obtener información acerca del tipo de letra actual para dibujar, como el nom-bre, el estilo y el tamaño del tipo de letra. En la figura 15.10 se sintetizan varios métodos de Font que se utilizan para obtener información sobre el tipo de letra. El método getStyle devuelve un valor entero que representa el estilo actual. El valor entero devuelto puede ser Font.PLAIN, Font.ITALIC, Font.BOLD o la combinación de Font.ITALIC y Font.BOLD. El método getFamily devuelve el nombre de la familia a la que pertenece el tipo de letra actual. El nombre de la familia del tipo de letra es específico de la pla-taforma. También hay métodos de Font disponibles para probar el estilo del tipo de letra actual, los cuales se sintetizan también en la figura 15.10. Los métodos isPlain, isBold e isItalic devuelven true si el estilo del tipo de letra actual es simple, negrita o cursiva, respectivamente.

En la figura 15.13 se muestran algunos elementos comunes de la métrica de los tipos de letras, que proporcionan información precisa acerca de un tipo de letra, como la altura, el descendente (la dis-tancia entre la base de la línea y el punto inferior del tipo de letra), el ascendente (la cantidad que se eleva un carácter por encima de la base de la línea) y el interlineado (la diferencia entre el descen-dente de una línea de texto y el ascendente de la línea de texto que está debajo; es decir, el espacia-miento entre líneas).

Fig. 15.12 � Creación de un objeto JFrame para mostrar tipos de letra (parte 2 de 2).

16 marco.setSize( 475, 170 ); // establece el tamaño del marco

17 marco.setVisible( true ); // muestra el marco

18 } // fin de main

19 } // fin de la clase TiposDeLetra

Fig. 15.13 � Métrica de los tipos de letra.

ascendentealtura

interlineado

descendentelínea base

La clase FontMetrics declara varios métodos para obtener información métrica de los tipos de letra. En la figura 15.14 se sintetizan estos métodos, junto con el método getFontMetrics de la clase Graphics. La aplicación de las figuras 15.15 y 15.16 utiliza los métodos de la figura 15.14 para obte-ner la información métrica de dos tipos de letra.

646 Capítulo 15 Gráfi cos y Java 2D

Método Descripción

Métodos de FontMetrics

public int getAscent() Devuelve un valor que representa el ascendente de un tipo de letra, en puntos.

public int getDescent() Devuelve un valor que representa el descendente de un tipo de letra, en puntos.

public int getLeading() Devuelve un valor que representa el interlineado de un tipo de letra, en puntos.

public int getHeight() Devuelve un valor que representa la altura de un tipo de letra, en puntos.

Métodos de Graphics para obtener la métrica de un tipo de letra

public FontMetrics getFontMetrics()

Devuelve el objeto FontMetrics para el objeto Font actual para dibujar.

public FontMetrics getFontMetrics( Font f )

Devuelve el objeto FontMetrics para el argumento Font especifi cado.

Fig. 15.14 � Métodos de FontMetrics y Graphics para obtener la métrica de los tipos de letra.

Fig. 15.15 � Métrica de los tipos de letra.

1 // Fig. 15.15: MetricaJPanel.java

2 // Métodos de FontMetrics y Graphics útiles para obtener la métrica de los tipos de letra.

3 import java.awt.Font;

4 import java.awt.FontMetrics;

5 import java.awt.Graphics;

6 import javax.swing.JPanel;

7

8 public class MetricaJPanel extends JPanel

9 {

10 // muestra la métrica de los tipos de letra

11 public void paintComponent( Graphics g )

12 {

13 super.paintComponent( g ); // llama al método paintComponent de la superclase

14

15 g.setFont( new Font( “SansSerif”, Font.BOLD, 12 ) );

16 FontMetrics metrica = g.getFontMetrics();

17 g.drawString( “Tipo de letra actual: ” + g.getFont(), 10, 40 );

18 g.drawString( “Ascendente: ” + metrica.getAscent(), 10, 55 );

19 g.drawString( “Descendente: ” + metrica.getDescent(), 10, 70 );

20 g.drawString( “Altura: ” + metrica.getHeight(), 10, 85 );

21 g.drawString( “Interlineado: ” + metrica.getLeading(), 10, 100 );

22

23 Font tipoLetra = new Font( “Serif”, Font.ITALIC, 14 );

24 metrica = g.getFontMetrics( tipoLetra );

25 g.setFont( tipoLetra );

26 g.drawString( “Tipo de letra actual: ” + tipoLetra, 10, 130 );

27 g.drawString( “Ascendente: ” + metrica.getAscent(), 10, 145 );

28 g.drawString( “Descendente: ” + metrica.getDescent(), 10, 160 );

29 g.drawString( “Altura: ” + metrica.getHeight(), 10, 175 );

30 g.drawString( “Interlineado: ” + metrica.getLeading(), 10, 190 );

31 } // fin del método paintComponent

32 } // fin de la clase MetricaJPanel

15.5 Dibujo de líneas, rectángulos y óvalos 647

En la línea 15 de la figura 15.15 se crea y se establece el tipo de letra actual para dibujar en SansSerif, negrita, 12 puntos. En la línea 16 se utiliza el método getFontMetrics de Graphics para obtener el objeto FontMetrics del tipo de letra actual. En la línea 17 se imprime la representación String del ob-jeto Font devuelto por g.getFont(). En las líneas 18 a 21 se utilizan los métodos de FontMetrics para obtener el ascendente, descendente, altura e interlineado del tipo de letra.

En la línea 23 se crea un nuevo tipo de letra Serif, cursiva, 14 puntos. En la línea 24 se utiliza una segunda versión del método getFontMetrics de Graphics, la cual recibe un argumento Font y devuel-ve su correspondiente objeto FontMetrics. En las líneas 27 a 30 se obtiene el ascendente, descendente, altura e interlineado de ese tipo de letra. Observe que la métrica es ligeramente distinta para cada uno de los tipos de letra.

15.5 Dibujo de líneas, rectángulos y óvalosEn esta sección presentaremos varios métodos de Graphics para dibujar líneas, rectángulos y óvalos. Los métodos y sus parámetros se sintetizan en la figura 15.17. Para cada método de dibujo que requiere un parámetro anchura y otro parámetro altura, sus valores deben ser números no negativos. De lo contrario, no se mostrará la figura.

Fig. 15.16 � Creación de un objeto JFrame para mostrar información sobre la métrica de los tipos de letra.

1 // Fig. 15.16: Metrica.java

2 // Muestra la métrica de los tipos de letra.

3 import javax.swing.JFrame;

4

5 public class Metrica

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 // crea marco para objeto MetricaJPanel

11 JFrame marco = new JFrame( “Demostracion de FontMetrics” );

12 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

13

14 MetricaJPanel metricaJPanel = new MetricaJPanel();

15 marco.add( metricaJPanel ); // agrega metricaJPanel al marco

16 marco.setSize( 530, 250 ); // establece el tamaño del marco

17 marco.setVisible( true ); // muestra el marco

18 } // fin de main

19 } // fin de la clase Metrica

648 Capítulo 15 Gráfi cos y Java 2D

Método Descripción

public void drawLine( int x1, int y1, int x2, int y2 )

Dibuja una línea entre el punto (x1, y1) y el punto (x2, y2).

public void drawRect( int x, int y, int anchura, int altura )

Dibuja un rectángulo con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y). Sólo el contorno del rectángulo se dibuja usando el color del objeto Graphics; el cuerpo del rectángulo no se rellena con este color.

public void fillRect( int x, int y, int anchura, int altura )

Dibuja un rectángulo relleno con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y).

public void clearRect( int x, int y, int anchura, int altura )

Dibuja un rectángulo relleno con la anchura y altura especifi cadas, en el color de fondo actual. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y). Este método es útil si el programador desea eliminar una porción de una imagen.

public void drawRoundRect( int x, int y, int anchura, int altura, int anchuraArco,

int alturaArco )

Dibuja un rectángulo con esquinas redondeadas, en el color actual y con la anchura y altura especifi cadas. Los valores de anchuraArco y alturaArco determinan el grado de redondez de las esquinas (vea la fi gura 15.20). Sólo se dibuja el contorno de la fi gura.

public void fillRoundRect( int x, int y, int anchura, int altura, int anchuraArco,

int alturaArco )

Dibuja un rectángulo relleno con esquinas redondeadas, en el color actual y con la anchura y altura especifi cadas. Los valores de anchuraArco y alturaArco determinan el grado de redondez de las esquinas (vea la fi gura 15.20).

public void draw3DRect( int x, int y, int anchura, int altura, boolean b )

Dibuja un rectángulo tridimensional en el color actual, con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y). El rectángulo aparece con relieve cuando b es true y sin relieve cuando b es false. Sólo se dibuja el contorno de la fi gura.

public void fill3DRect( int x, int y, int anchura, int altura, boolean b )

Dibuja un rectángulo tridimensional relleno en el color actual, con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y). El rectángulo aparece con relieve cuando b es true y sin relieve cuando b es false.

public void drawOval( int x, int y, int anchura, int altura )

Dibuja un óvalo en el color actual, con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo imaginario que lo rodea tiene las coordenadas (x, y). El óvalo toca los cuatro lados del rectángulo imaginario en el centro de cada uno de los lados (vea la fi gura 15.21). Sólo se dibuja el contorno de la fi gura.

public void fillOval( int x, int y, int anchura, int altura )

Dibuja un óvalo relleno en el color actual, con la anchura y altura especifi cadas. La esquina superior izquierda del rectángulo imaginario que lo rodea tiene las coordenadas (x, y). El óvalo toca los cuatro lados del rectángulo imaginario en el centro de cada uno de los lados (vea la fi gura 15.21).

Fig. 15.17 � Métodos de Graphics para dibujar líneas, rectángulos y óvalos.

15.5 Dibujo de líneas, rectángulos y óvalos 649

La aplicación de las figuras 15.18 y 15.19 demuestra cómo dibujar una variedad de líneas, rectángu-los, rectángulos tridimensionales, rectángulos con esquinas redondeadas y óvalos. En la figura 15.18, en la línea 17 se dibuja una línea roja, en la línea 20 se dibuja un rectángulo vacío de color azul y en la línea 21 se dibuja un rectángulo relleno de color azul. Los métodos fillRoundRect (línea 24) y drawRoundRect (línea 25) dibujan rectángulos con esquinas redondeadas. Sus primeros dos argumentos especifican las coordenadas de la esquina superior izquierda del rectángulo delimitador (el área en la que se dibujará el rectángulo redondeado). Las coordenadas de la esquina superior izquierda no son el borde del rectángulo redondeado, sino las coordenadas en donde se encontraría el borde si el rectángulo tuviera esquinas cua-dradas. Los argumentos tercero y cuarto especifican la anchura y altura del rectángulo. Sus últimos dos argumentos determinan los diámetros horizontal y vertical del arco (es decir, la anchura y la altura del arco) que se utiliza para representar las esquinas.

En la figura 15.20 se muestran la anchura y altura del arco, junto con la anchura y la altura de un rectángulo redondeado. Si se utiliza el mismo valor para la anchura y la altura del arco, se produce

Fig. 15.18 � Dibujo de líneas, rectángulos y óvalos.

1 // Fig. 15.18:LineasRectsOvalosJPanel.java

2 // Dibujo de líneas, rectángulos y óvalos.

3 import java.awt.Color;

4 import java.awt.Graphics;

5 import javax.swing.JPanel;

6

7 public class LineasRectsOvalosJPanel extends JPanel

8 {

9 // muestra varias líneas, rectángulos y óvalos

10 public void paintComponent( Graphics g )

11 {

12 super.paintComponent( g ); // llama al método paintComponent de la superclase

13

14 this.setBackground( Color.WHITE );

15

16 g.setColor( Color.RED );

17 g.drawLine( 5, 30, 380, 30 );

18

19 g.setColor( Color.BLUE );

20 g.drawRect( 5, 40, 90, 55 );

21 g.fillRect( 100, 40, 90, 55 );

22

23 g.setColor( Color.CYAN );

24 g.fillRoundRect( 195, 40, 90, 55, 50, 50 );

25 g.drawRoundRect( 290, 40, 90, 55, 20, 20 );

26

27 g.setColor( Color.GREEN );

28 g.draw3DRect( 5, 100, 90, 55, true );

29 g.fill3DRect( 100, 100, 90, 55, false );

30

31 g.setColor( Color.MAGENTA );

32 g.drawOval( 195, 100, 90, 55 );

33 g.fillOval( 290, 100, 90, 55 );

34 } // fin del método paintComponent

35 } // fin de la clase LineasRectsOvalosJPanel

650 Capítulo 15 Gráfi cos y Java 2D

Fig. 15.19 � Creación de JFrame para mostrar líneas, rectángulos y óvalos.

1 // Fig. 15.19: LineasRectsOvalos.java

2 // Dibujo de líneas, rectángulos y óvalos.

3 import java.awt.Color;

4 import javax.swing.JFrame;

5

6 public class LineasRectsOvalos

7 {

8 // ejecuta la aplicación

9 public static void main( String[] args )

10 {

11 // crea marco para LineasRectsOvalosJPanel

12 JFrame marco =

13 new JFrame( “Dibujo de lineas, rectangulos y ovalos” );

14 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

15

16 LineasRectsOvalosJPanel lineasRectsOvalosJPanel =

17 new LineasRectsOvalosJPanel();

18 lineasRectsOvalosJPanel.setBackground( Color.WHITE );

19 marco.add( lineasRectsOvalosJPanel ); // agrega el panel al marco

20 marco.setSize( 400, 210 ); // establece el tamaño del marco

21 marco.setVisible( true ); // muestra el marco

22 } // fin de main

23 } // fin de la clase LineasRectsOvalos

Fig. 15.20 � Anchura y altura del arco para los rectángulos redondeados.

anchura

(x, y)

altura del arco

anchura del arcoaltura

drawLine

drawRect

fillRect

draw3DRect

fill3DRect

fillRoundRect

drawRoundRect

drawOval

fillOval

15.6 Dibujo de arcos 651

un cuarto de círculo en cada esquina. Cuando la anchura y la altura del arco, la anchura y la altura del rectángulo tienen los mismos valores, el resultado es un círculo. Si los valores para anchura y altura son los mismos, y los valores de anchuraArco y alturaArco son 0, el resultado es un cuadrado.

Los métodos draw3DRect (línea 28) y fill3DRect (línea 29) reciben los mismos argumentos. Los primeros dos argumentos especifican la esquina superior izquierda del rectángulo. Los siguien-tes dos argumentos especifican la anchura y altura del rectángulo, respectivamente. El último argu-mento determina si el rectángulo está con relieve (true) o sin relieve (false). El efecto tridimen-sional de draw3DRect aparece como dos bordes del rectángulo en el color original y dos bordes en un color ligeramente más oscuro. El efecto tridimensional de fill3DRect aparece como dos bordes del rectángulo en el color del dibujo original y los otros dos bordes y el relleno en un color ligera-mente más oscuro. Los rectángulos con relieve tienen los bordes de color original del dibujo en las partes superior e izquierda del rectángulo. Los rectángulos sin relieve tienen los bordes de color original del dibujo en las partes inferior y derecha del rectángulo. El efecto tridimensional es difícil de ver en ciertos colores.

Los métodos drawOval y fillOval (figura 15.18, líneas 32 y 33) reciben los mismos cuatro argu-mentos. Los primeros dos argumentos especifican la coordenada superior izquierda del rectángulo delimitador que contiene el óvalo. Los últimos dos argumentos especifican la anchura y la altura del rectángulo delimitador, respectivamente. En la figura 15.21 se muestra un óvalo delimitado por un rectángulo. Observe que el óvalo toca el centro de los cuatro lados del rectángulo delimitador. (El rectángulo delimitador no se muestra en la pantalla.)

Fig. 15.21 � Óvalo delimitado por un rectángulo.

(x,y)

anchura

altura

15.6 Dibujo de arcosUn arco se dibuja como una porción de un óvalo. Los ángulos de los arcos se miden en grados. Los arcos se extienden (es decir, se mueven a lo largo de una curva) desde un ángulo inicial, en base al número de grados especificados por el ángulo del arco. El ángulo inicial indica, en grados, en dónde empieza el arco. El ángulo del arco especifica el número total de grados hasta los que se va a extender el arco. En la figura 15.22 se muestran dos arcos. El conjunto izquierdo de ejes mues-tra a un arco extendiéndose desde cero hasta aproximadamente 110 grados. Los arcos que se ex-tienden en dirección en contra de las manecillas del reloj se miden en grados positivos. El conjunto derecho de ejes muestra a un arco extendiéndose desde cero hasta aproximadamente �110 grados. Los arcos que se extienden en dirección a favor de las manecillas del reloj se miden en grados ne-gativos. Observe los cuadros punteados alrededor de los arcos en la figura 15.22. Cuando dibujamos un arco, debemos especificar un rectángulo delimitador para un óvalo. El arco se extenderá a lo largo de una parte del óvalo. Los métodos drawArc y fillArc de Graphics para dibujar arcos se sinteti-zan en la figura 15.23.

652 Capítulo 15 Gráfi cos y Java 2D

Las figuras 15.24 y 15.25 demuestran el uso de los métodos para arcos de la figura 15.23. La apli-cación dibuja seis arcos (tres sin rellenar y tres rellenos). Para ilustrar el rectángulo delimitador que ayuda a determinar en dónde aparece el arco, los primeros tres arcos se muestran dentro de un rectán-gulo rojo que tiene los mismos argumentos x, y, anchura y altura que los arcos.

Fig. 15.22 � Ángulos positivos y negativos de un arco.

90º

270º

Ángulos positivos

180º 0º

90º

270º

Ángulos negativos

180º 0º

Método Descripción

public void drawArc( int x, int y, int anchura, int altura, int anguloInicial,

int anguloArco )

Dibuja un arco relativo a las coordenadas (x, y) de la esquina superior izquierda del rectángulo delimitador, con la anchura y altura especifi cadas. El segmento del arco se dibuja empezando en anguloInicial y se extiende hasta los grados especifi cados por anguloArco.

public void fillArc( int x, int y, int anchura, int altura, int anguloInicial,

int anguloArco )

Dibuja un arco relleno (es decir, un sector) relativo a las coordenadas (x, y) de la esquina superior izquierda del rectángulo delimitador, con la anchura y altura especifi cadas. El segmento del arco se dibuja empezando en anguloInicial y se extiende hasta los grados especifi cados por anguloArco.

Fig. 15.23 � Métodos de Graphics para dibujar arcos.

Fig. 15.24 � Arcos mostrados con drawArc y fillArc (parte 1 de 2).

1 // Fig. 15.24: ArcosJPanel.java

2 // Dibujo de arcos.

3 import java.awt.Color;

4 import java.awt.Graphics;

5 import javax.swing.JPanel;

6

7 public class ArcosJPanel extends JPanel

8 {

9 // dibuja rectángulos y arcos

10 public void paintComponent( Graphics g )

11 {

12 super.paintComponent( g ); // llama al método paintComponent de la superclase

13

15.6 Dibujo de arcos 653

Fig. 15.24 � Arcos mostrados con drawArc y fillArc (parte 2 de 2).

Fig. 15.25 � Creación de un objeto JFrame para mostrar arcos (parte 1 de 2).

14 // empieza en 0 y se extiende hasta 360 grados

15 g.setColor( Color.RED );

16 g.drawRect( 15, 35, 80, 80 );

17 g.setColor( Color.BLACK );

18 g.drawArc( 15, 35, 80, 80, 0, 360 );

19

20 // empieza en 0 y se extiende hasta 110

21 g.setColor( Color.RED );

22 g.drawRect( 100, 35, 80, 80 );

23 g.setColor( Color.BLACK );

24 g.drawArc( 100, 35, 80, 80, 0, 110 );

25

26 // empieza en 0 y se extiende hasta -270 grados

27 g.setColor( Color.RED );

28 g.drawRect( 185, 35, 80, 80 );

29 g.setColor( Color.BLACK );

30 g.drawArc( 185, 35, 80, 80, 0, -270 );

31

32 // empieza en 0 y se extiende hasta 360 grados

33 g.fillArc( 15, 120, 80, 40, 0, 360 );

34

35 // empieza en 270 y se extiende hasta -90 grados

36 g.fillArc( 100, 120, 80, 40, 270, -90 );

37

38 // empieza en 0 y se extiende hasta -270 grados

39 g.fillArc( 185, 120, 80, 40, 0, -270 );

40 } // fin del método paintComponent

41 } // fin de la clase ArcosJPanel

1 // Fig. 15.25: DibujarArcos.java

2 // Dibujo de arcos.

3 import javax.swing.JFrame;

4

5 public class DibujarArcos

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 // crea marco para ArcosJPanel

11 JFrame marco = new JFrame( “Dibujo de arcos” );

12 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

13

14 ArcosJPanel arcosJPanel = new ArcosJPanel(); // crea objeto ArcosJPanel

15 marco.add( arcosJPanel ); // agrega arcosJPanel al marco

16 marco.setSize( 300, 210 ); // establece el tamaño del marco

17 marco.setVisible( true ); // muestra el marco

18 } // fin de main

19 } // fin de la clase DibujarArcos

654 Capítulo 15 Gráfi cos y Java 2D

15.7 Dibujo de polígonos y polilíneasLos polígonos son figuras cerradas de varios lados, compuestas por segmentos de línea recta. Las poli-líneas son una secuencia de puntos conectados. En la figura 15.26 describimos los métodos para dibujar polígonos y polilíneas. Algunos métodos requieren un objeto Polygon (paquete java.awt). Los construc-tores de la clase Polygon se describen también en la figura 15.26. La aplicación de las figuras 15.27 y 15.28 dibuja polígonos y polilíneas.

Fig. 15.25 � Creación de un objeto JFrame para mostrar arcos (parte 2 de 2).

Método Descripción

Métodos de Graphics para dibujar polígonos

public void drawPolygon( int[] puntosX, int[] puntosY, int puntos )

Dibuja un polígono. La coordenada x de cada punto se especifi ca en el arreglo puntosX y la coordenada y de cada punto se especifi ca en el arreglo puntosY. El último argumento especifi ca el número de puntos. Este método dibuja un polígono cerrado. Si el último punto es distinto del primero, el polígono se cierra mediante una línea que conecte el último punto con el primero.

public void drawPolyline( int[] puntosX, int[] puntosY, int puntos )

Dibuja una secuencia de líneas conectadas. La coordenada x de cada punto se especifi ca en el arreglo puntosX y la coordenada y de cada punto se especifi ca en el arreglo puntosY. El último argumento especifi ca el número de puntos. Si el último punto es distinto del primero, la polilínea no se cierra.

public void drawPolygon( Polygon p )

Dibuja el polígono especifi cado.

public void fillPolygon( int[] puntosX, int[] puntosY, int puntos )

Dibuja un polígono relleno. La coordenada x de cada punto se especifi ca en el arreglo puntosX y la coordenada y de cada punto se especifi ca en el arreglo puntosY. El último argumento especifi ca el número de puntos. Este método dibuja un polígono cerrado. Si el último punto es distinto del primero, el polígono se cierra mediante una línea que conecte el último punto con el primero.

public void fillPolygon( Polygon p )

Dibuja el polígono relleno especifi cado. El polígono es cerrado.

Fig. 15.26 � Métodos de Graphics para polígonos y métodos de la clase Polygon (parte 1 de 2).

15.7 Dibujo de polígonos y polilíneas 655

Fig. 15.27 � Polígonos mostrados con drawPolygon y fillPolygon (parte 1 de 2).

1 // Fig. 15.27: PoligonosJPanel.java

2 // Dibujo de polígonos.

3 import java.awt.Graphics;

4 import java.awt.Polygon;

5 import javax.swing.JPanel;

6

7 public class PoligonosJPanel extends JPanel

8 {

9 // dibuja polígonos y polilíneas

10 public void paintComponent( Graphics g )

11 {

12 super.paintComponent( g ); // llama al método paintComponent de la superclase

13

14 // dibuja polígono con objeto Polygon

15 int[] valoresX = { 20, 40, 50, 30, 20, 15 };

16 int[] valoresY = { 50, 50, 60, 80, 80, 60 };

17 Polygon poligono1 = new Polygon( valoresX, valoresY, 6 );

18 g.drawPolygon( poligono1 );

19

20 // dibuja polilíneas con dos arreglos

21 int[] valoresX2 = { 70, 90, 100, 80, 70, 65, 60 };

22 int[] valoresY2 = { 100, 100, 110, 110, 130, 110, 90 };

23 g.drawPolyline( valoresX2, valoresY2, 7 );

24

25 // rellena polígono con dos arreglos

26 int[] valoresX3 = { 120, 140, 150, 190 };

27 int[] valoresY3 = { 40, 70, 80, 60 };

28 g.fillPolygon( valoresX3, valoresY3, 4 );

29

30 // dibuja polígono relleno con objeto Polygon

31 Polygon poligono2= new Polygon();

32 poligono2.addPoint( 165, 135 );

33 poligono2.addPoint( 175, 150 );

34 poligono2.addPoint( 270, 200 );

Método Descripción

Constructores y métodos de Polygon

public Polygon()

Crea un nuevo objeto polígono. Este objeto no contiene ningún punto.

public Polygon( int[] valoresX, int[] valoresY, int numeroDePuntos )

Crea un nuevo objeto polígono. Este objeto tiene numeroDePuntos lados, en donde cada punto consiste de una coordenada x desde valoresX, y una coordenada y desde valoresY.

public void addPoint( int x, int y )

Agrega pares de coordenadas x y y al objeto Polygon.

Fig. 15.26 � Métodos de Graphics para polígonos y métodos de la clase Polygon (parte 2 de 2).

656 Capítulo 15 Gráfi cos y Java 2D

35 poligono2.addPoint( 200, 220 );

36 poligono2.addPoint( 130, 180 );

37 g.fillPolygon( poligono2);

38 } // fin del método paintComponent

39 } // fin de la clase PoligonosJPanel

Fig. 15.27 � Polígonos mostrados con drawPolygon y fillPolygon (parte 2 de 2).

Fig. 15.28 � Creación de un objeto JFrame para mostrar polígonos.

1 // Fig. 15.28: DibujarPoligonos.java

2 // Dibujo de polígonos.

3 import javax.swing.JFrame;

4

5 public class DibujarPoligonos

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 // crea marco para objeto PoligonosJPanel

11 JFrame marco = new JFrame( “Dibujo de poligonos” );

12 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

13

14 PoligonosJPanel poligonosJPanel = new PoligonosJPanel();

15 marco.add( poligonosJPanel ); // agrega poligonosJPanel al marco

16 marco.setSize( 280, 270 ); // establece el tamaño del marco

17 marco.setVisible( true ); // muestra el marco

18 } // fin de main

19 } // fin de la clase DibujarPoligonos

Resultado de la línea 18

Resultado de la línea 28

Resultado de la línea 23 Resultado de la línea 37

En las líneas 15 y 16 de la figura 15.27 se crean dos arreglos int y se utilizan para especificar los puntos del objeto Polygon llamado poligono1. La llamada al constructor de Polygon en la línea 17 reci-be el arreglo valoresX, el cual contiene la coordenada x de cada punto; el arreglo valoresY, que contiene la coordenada y de cada punto y el número 6 (el número de puntos en el polígono). En la línea 18 se muestra poligono1 al pasarlo como argumento para el método drawPolygon de Graphics.

En las líneas 21 y 22 se crean dos arreglos int y se utilizan para especificar los puntos de una serie de líneas conectadas. El arreglo valoresX2 contiene la coordenada x de cada punto y el arreglo valoresY2 contiene la coordenada y de cada punto. En la línea 23 se utiliza el método drawPolyline de Graphics

15.8 La API Java 2D 657

para mostrar la serie de líneas conectadas que se especifican mediante los argumentos valoresX2, valoresY2 y 7 (el número de puntos).

En las líneas 26 y 27 se crean dos arreglos int y se utilizan para especificar los puntos de un polí-gono. El arreglo valoresX3 contiene la coordenada x de cada punto y el arreglo valoresY3 contiene la coordenada y de cada punto. En la línea 28 se muestra un polígono, al pasar al método fillPolygon de Graphics los dos arreglos (valoresX3 y valoresY3) y el número de puntos a dibujar (4).

Error común de programación 15.1Se lanzará una excepción ArrayIndexOutOfBoundsException si el número de puntos es-pecificados en el tercer argumento del método drawPolygon o del método fillPolygon es mayor que el número de elementos en los arreglos de las coordenadas que especifican el polígono a mostrar.

En la línea 31 se crea el objeto Polygon llamado poligono2, sin puntos. En las líneas 32 a 36 se uti-liza el método addPoint de Polygon para agregar pares de coordenadas x y y al objeto Polygon. En la línea 37 se muestra el objeto Polygon llamado poligono2, al pasarlo al método fillPolygon de Graphics.

15.8 La API Java 2DLa API Java 2D proporciona herramientas avanzadas para gráficos bidimensionales, para los programa-dores que requieren manipulaciones gráficas detalladas y complejas. La API incluye características para procesar arte lineal, texto e imágenes en los paquetes java.awt, java.awt.image, java.awt.color, java.awt.font, java.awt.geom, java.awt.print y java.awt.image.renderable. Las herramientas de la API son muy extensas como para cubrirlas todas en este libro. Para ver las generalidades acerca de estas herramientas, consulte la demostración de Java 2D (que veremos en el capítulo 23, Applets and Java Web Start) o visite la página Web download.oracle.com/javase/6/docs/technotes/guides/2d. En esta sección veremos las generalidades de varias herramientas de Java 2D.

El dibujo con la API Java 2D se logra mediante el uso de una referencia Graphics2D (paquete java.awt), que es una subclase abstracta de la clase Graphics, por lo que tiene todas las herramientas para gráficos que se demostraron anteriormente en este capítulo. De hecho, el objeto en sí utilizado para dibujar en todos los métodos paintComponent es una instancia de una subclase de Graphics2D que se pasa al método paintComponent y se utiliza mediante la superclase Graphics. Para acceder a las herra-mientas de Graphics2D, debemos convertir la referencia Graphics (g) que se pasa a paintComponent en una referencia Graphics2D, mediante una instrucción como:

Graphics2D g2d = ( Graphics2D ) g;

Los siguientes dos ejemplos utilizan esta técnica.

Líneas, rectángulos, rectángulos redondeados, arcos y elipsesEn el siguiente ejemplo se muestran varias figuras de Java 2D del paquete java.awt.geom, incluyendo a Line2D.Double, Rectangle2D.Double, RoundRectangle2D.Double, Arc2D.Double y Ellipse2D.Double. Observe la sintaxis de cada uno de los nombres de las clases. Cada una de estas clases representa una figura con las dimensiones especificadas como valores double. Hay una versión separada de cada figura, represen-tada con valores float (como Ellipse2D.Float). En cada caso, Double es una clase public static ani-dada de la clase que se especifica a la izquierda del punto (por ejemplo, Ellipse2D). Para utilizar la clase static anidada, simplemente debemos calificar su nombre con el nombre de la clase externa.

En las figuras 15.29 y15.30, dibujamos figuras de Java 2D y modificamos sus características de dibujo, como cambiar el grosor de línea, rellenar figuras con patrones y dibujar líneas punteadas. Éstas son sólo algunas de las muchas herramientas que proporciona Java 2D.

658 Capítulo 15 Gráfi cos y Java 2D

En la línea 25 de la figura 15.29 se convierte la referencia Graphics recibida por paintComponent a una referencia Graphics2D, y se asigna a g2d para permitir el acceso a las características de Java2D.

Fig. 15.29 � Figuras de Java 2D (parte 1 de 2).

1 // Fig. 15.29: FigurasJPanel.java 2 // Demostración de algunas figuras de Java 2D. 3 import java.awt.Color; 4 import java.awt.Graphics; 5 import java.awt.BasicStroke; 6 import java.awt.GradientPaint; 7 import java.awt.TexturePaint; 8 import java.awt.Rectangle; 9 import java.awt.Graphics2D;10 import java.awt.geom.Ellipse2D;11 import java.awt.geom.Rectangle2D;12 import java.awt.geom.RoundRectangle2D;13 import java.awt.geom.Arc2D;14 import java.awt.geom.Line2D;15 import java.awt.image.BufferedImage;16 import javax.swing.JPanel;1718 public class FigurasJPanel extends JPanel 19 {20 // dibuja figuras con la API Java 2D21 public void paintComponent( Graphics g )22 {23 super.paintComponent( g ); // llama al método paintComponent de la superclase2425 Graphics2D g2d = ( Graphics2D ) g; // convierte a g en objeto Graphics2D2627 // dibuja un elipse en 2D, relleno con un gradiente color azul-amarillo28 g2d.setPaint( new GradientPaint( 5, 30, Color.BLUE, 35, 100, 29 Color.YELLOW, true ) ); 30 g2d.fill( new Ellipse2D.Double( 5, 30, 65, 100 ) );3132 // dibuja rectángulo en 2D de color rojo33 g2d.setPaint( Color.RED ); 34 g2d.setStroke( new BasicStroke( 10.0f ) ); 35 g2d.draw( new Rectangle2D.Double( 80, 30, 65, 100 ) );3637 // dibuja rectángulo delimitador en 2D, con un fondo con búfer38 BufferedImage imagenBuf = new BufferedImage( 10, 10, 39 BufferedImage.TYPE_INT_RGB );4041 // obtiene objeto Graphics2D de imagenBuf y dibuja en él42 Graphics2D gg = imagenBuf.createGraphics(); 43 gg.setColor( Color.YELLOW ); // dibuja en color amarillo44 gg.fillRect( 0, 0, 10, 10 ); // dibuja un rectángulo relleno45 gg.setColor( Color.BLACK ); // dibuja en color negro46 gg.drawRect( 1, 1, 6, 6 ); // dibuja un rectángulo47 gg.setColor( Color.BLUE ); // dibuja en color azul48 gg.fillRect( 1, 1, 3, 3 ); // dibuja un rectángulo relleno49 gg.setColor( Color.RED ); // dibuja en color rojo

15.8 La API Java 2D 659

Fig. 15.29 � Figuras de Java 2D (parte 2 de 2).

Fig. 15.30 � Creación de un objeto JFrame para mostrar figuras (parte 1 de 2).

50 gg.fillRect( 4, 4, 3, 3 ); // dibuja un rectángulo relleno

51

52 // pinta a imagenBuf en el objeto JFrame

53 g2d.setPaint( new TexturePaint( imagenBuf,

54 new Rectangle( 10, 10 ) ) );

55 g2d.fill(

56 new RoundRectangle2D.Double( 155, 30, 75, 100, 50, 50 ) );

57

58 // dibuja arco en forma de pastel en 2D, de color blanco

59 g2d.setPaint( Color.WHITE );

60 g2d.setStroke( new BasicStroke( 6.0f ) );

61 g2d.draw(

62 new Arc2D.Double( 240, 30, 75, 100, 0, 270, Arc2D.PIE ) );

63

64 // dibuja líneas 2D en verde y amarillo

65 g2d.setPaint( Color.GREEN );

66 g2d.draw( new Line2D.Double( 395, 30, 320, 150 ) );

67

68 // dibuja línea 2D usando el trazo

69 float[] guiones = { 10 }; // especifica el patrón de guiones

70 g2d.setPaint( Color.YELLOW );

71 g2d.setStroke( new BasicStroke( 4, BasicStroke.CAP_ROUND,

72 BasicStroke.JOIN_ROUND, 10, guiones, 0 ) );

73 g2d.draw( new Line2D.Double( 320, 30, 395, 150 ) );

74 } // fin del método paintComponent

75 } // fin de la clase FigurasJPanel

1 // Fig. 15.30: Figuras.java

2 // Demostración de algunas figuras de Java 2D.

3 import javax.swing.JFrame;

4

5 public class Figuras

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 // crea marco para objeto FigurasJPanel

11 JFrame marco = new JFrame( "Dibujo de figuras en 2D" );

12 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

13

14 // crea objeto FigurasJPanel

15 FigurasJPanel figurasJPanel = new FigurasJPanel();

16

17 marco.add( figurasJPanel ); // agrega figurasJPanel para marco

18 marco.setSize( 425, 200 ); // establece el tamaño del marco

19 marco.setVisible( true ); // muestra el marco

20 } // fin de main

21 } // fin de la clase Figuras

660 Capítulo 15 Gráfi cos y Java 2D

Óvalos, rellenos con degradado y objetos PaintLa primera figura que dibujamos es un óvalo relleno con colores que cambian gradualmente. En las lí-neas 28 y 29 se invoca el método setPaint de Graphics2D para establecer el objeto Paint que determina el color para la figura a mostrar. Un objeto Paint implementa a la interfaz java.awt.Paint. Puede ser algo tan simple como uno de los objetos Color que presentamos antes en la sección 15.3 (la clase Color implementa a Paint), o puede ser una instancia de las clases GradientPaint, SystemColor, Texture-Paint, LinearGradientPain o RadientGradientPaint de la API Java 2D. En este caso, utilizamos un objeto GradientPaint.

La clase GradientPaint ayuda a dibujar una figura en colores que cambian gradualmente (lo cual se conoce como degradado). El constructor de GradientPaint que se utiliza aquí requiere siete argu-mentos. Los primeros dos especifican la coordenada inicial del degradado. El tercer argumento especi-fica el Color inicial del degradado. Los argumentos cuarto y quinto especifican la coordenada final del degradado. El sexto especifica el Color final del degradado. El último argumento especifica si el de-gradado es cíclico (true) o acíclico (false). Los dos conjuntos de coordenadas determinan la direc-ción del degradado. Como la segunda coordenada (35, 100) se encuentra hacia abajo y a la derecha de la primera coordenada (5, 30), el degradado va hacia abajo y a la derecha con cierto ángulo. Como este degradado es cíclico (true), el color empieza con azul, se convierte gradualmente en amarillo y luego regresa gradualmente a azul. Si el degradado es acíclico, el color cambia del primer color especifi-cado (por ejemplo, azul) al segundo color (por ejemplo, amarillo).

En la línea 30 se utiliza el método fill de Graphics2D para dibujar un objeto Shape relleno (un objeto que implementa a la interfaz Shape del paquete java.awt). En este caso mostramos un objeto Ellipse2D.Double. El constructor de Ellipse2D.Double recibe cuatro argumentos que especifican el rectángulo delimitador para mostrar la elipse.

Rectángulos, trazos (objetos Stroke)A continuación dibujamos un rectángulo rojo con un borde grueso. En la línea 33 se utiliza setPaint para establecer el objeto Paint en Color.RED. En la línea 34 se utiliza el método setStroke de Graphic-s2D para establecer las características del borde del rectángulo (o las líneas para cualquier otra figura). El método setStroke requiere como argumento un objeto que implemente a la interfaz Stroke (paquete java.awt). En este caso, utilizamos una instancia de la clase BasicStroke. Esta clase proporciona varios constructores para especificar la anchura de la línea, la manera en que ésta termina (lo cual se le conoce como cofias), la manera en que las líneas se unen entre sí (lo cual se le conoce como uniones de línea) y los atributos de los guiones de la línea (si es una línea punteada). El constructor aquí especifica que la línea debe tener una anchura de 10 píxeles.

En la línea 35 se utiliza el método draw de Graphics2D para dibujar un objeto Shape; en este caso, una instancia de la clase Rectangle2D.Double. El constructor de Rectangle2D.Double recibe argumentos que especifican las coordenadas x y y de la esquina superior izquierda, la anchura y la al-tura del rectángulo.

Fig. 15.30 � Creación de un objeto JFrame para mostrar figuras (parte 2 de 2).

15.8 La API Java 2D 661

Rectángulos redondeados, objetos BufferedImage y TexturePaintA continuación dibujamos un rectángulo redondeado, relleno con un patrón creado en un ob-jeto BufferedImage (paquete java.awt.image). En las líneas 38 y 39 se crea el objeto Buffered-Image. La clase BufferedImage puede usarse para producir imágenes en color y escala de grises. Este objeto BufferedImage en particular tiene una anchura y una altura de 10 píxeles (según lo especi-ficado por los primeros dos argumentos del constructor). El tercer argumento del constructor, BufferedImage.TYPE_INT_RGB, indica que la imagen se almacena en color, utilizando el esquema de colores RGB.

Para crear el patrón de relleno para el rectángulo redondeado, debemos primero dibujar en el objeto BufferedImage. En la línea 42 se crea un objeto Graphics2D (con una llamada al método createGraphics de BufferedImage) que puede usarse para dibujar en el objeto BufferedImage. En las líneas 43 a 50 se utilizan los métodos setColor, fillRect y drawRect para crear el patrón.

En las líneas 53 y 54 se establece el objeto Paint en un nuevo objeto TexturePaint (paquete java.awt). Un objeto TexturePaint utiliza la imagen almacenada en su objeto BufferedImage aso-ciado (el primer argumento del constructor) como la textura para rellenar una figura. El segundo argu-mento especifica el área Rectangle del objeto BufferedImage que se repetirá en toda la textura. En este caso, el objeto Rectangle es del mismo tamaño que el objeto BufferedImage. Sin embargo, puede utilizarse una porción más pequeña del objeto BufferedImage.

En las líneas 55 y 56 se utiliza el método fill de Graphics2D para dibujar un objeto Shape re-lleno; en este caso, una instancia de la clase RoundRectangle2D.Double. El constructor de la clase RoundRectangle2D.Double recibe seis argumentos que especifican las dimensiones del rectángulo, la anchura y la altura del arco utilizado para redondear las esquinas.

ArcosA continuación dibujamos un arco en forma de pastel, con una línea blanca gruesa. En la línea 59 se establece el objeto Paint en Color.WHITE. En la línea 60 se establece el objeto Stroke en un nuevo objeto BasicStroke para una línea con 6 píxeles de anchura. En las líneas 61 y 62 se utiliza el méto-do draw de Graphics2D para dibujar un objeto Shape; en este caso, un Arc2D.Double. Los primeros cuatro argumentos del constructor de Arc2D.Double especifican las coordenadas x y y de la esquina superior izquierda, la anchura y la altura del rectángulo delimitador para el arco. El quinto argu-mento especifica el ángulo inicial. El sexto especifica el ángulo del arco. El último argumento especifica cómo se cierra el arco. La constante Arc2D.PIE indica que el arco se cierra dibujando dos líneas: una línea va desde el punto inicial del arco hasta el centro del rectángulo delimitador, y otra va desde el centro del rectángulo delimitador hasta el punto final. La clase Arc2D proporciona otras dos cons-tantes estáticas para especificar cómo se cierra el arco. La constante Arc2D.CHORD dibuja una línea que va desde el punto inicial hasta el punto final. La constante Arc2D.OPEN especifica que el arco no debe cerrarse.

LíneasPor último, dibujamos dos líneas utilizando objetos Line2D: una sólida y una punteada. En la línea 65 se establece el objeto Paint en Color.GREEN. En la línea 66 se utiliza el método draw de Graphics2D para dibujar un objeto Shape; en este caso, una instancia de la clase Line2D.Double. Los argumentos del cons-tructor de Line2D.Double especifican las coordenadas inicial y final de la línea.

En la línea 69 se declara un arreglo float de un elemento, el cual contiene el valor 10. Este arreglo describe los guiones en la línea punteada. En este caso, cada guión será de 10 píxeles de largo. Para crear guiones de diferentes longitudes en un patrón, simplemente debe proporcionar la longitud de cada guión como un elemento en el arreglo. En la línea 70 se establece el objeto Paint en Color.YELLOW. En las líneas 71 y 72 se establece el objeto Stroke en un nuevo objeto BasicStroke. La línea tendrá una anchura de 4 píxeles y extremos redondeados (BasicStroke.CAP_ROUND). Si las líneas se unen

662 Capítulo 15 Gráfi cos y Java 2D

entre sí (como en un rectángulo en las esquinas), la unión de las líneas será redondeada (BasicStroke.JOIN_ROUND). El argumento guiones especifica las longitudes de los guiones de la línea. El último argumento indica el índice inicial en el arreglo guiones para el primer guión en el patrón. En la línea 73 se dibuja una línea con el objeto Stroke actual.

Creación de sus propias figuras mediante las rutas generalesA continuación presentamos una ruta general: una figura compuesta de líneas rectas y curvas com-plejas. Una ruta general se representa con un objeto de la clase GeneralPath (paquete java.awt.geom). La aplicación de las figuras 15.31 y 15.32 demuestra cómo dibujar una ruta general, en forma de una estrella de cinco puntas.

Fig. 15.31 � Rutas generales de Java 2D (parte 1 de 2).

1 // Fig. 15.31: Figuras2JPanel.java

2 // Demostración de una ruta general.

3 import java.awt.Color;

4 import java.awt.Graphics;

5 import java.awt.Graphics2D;

6 import java.awt.geom.GeneralPath;

7 import java.util.Random;

8 import javax.swing.JPanel;

9

10 public class Figuras2JPanel extends JPanel

11 {

12 // dibuja rutas generales

13 public void paintComponent( Graphics g )

14 {

15 super.paintComponent( g ); // llama al método paintComponent de la superclase

16 Random aleatorio = new Random(); // obtiene el generador de números aleatorios

17

18 int[] puntosX = { 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 };

19 int[] puntosY = { 0, 36, 36, 54, 96, 72, 96, 54, 36, 36 };

20

21 Graphics2D g2d = ( Graphics2D ) g;

22 GeneralPath estrella = new GeneralPath(); // crea objeto GeneralPath

23

24 // establece la coordenada inicial de la ruta general

25 estrella.moveTo( puntosX[ 0 ], puntosY[ 0 ] );

26

27 // crea la estrella; esto no la dibuja

28 for ( int cuenta = 1; cuenta < puntosX.length; cuenta++ )

29 estrella.lineTo( puntosX[ cuenta ], puntosY[ cuenta ] );

30

31 estrella.closePath(); // cierra la figura

32

33 g2d.translate( 200, 200 ); // traslada el origen a (200, 200)

34

35 // gira alrededor del origen y dibuja estrellas en colores aleatorios

36 for ( int cuenta = 1; cuenta <= 20; cuenta++ )

37 {

38 g2d.rotate( Math.PI / 10.0 ); // gira el sistema de coordenadas

39

15.8 La API Java 2D 663

Fig. 15.31 � Rutas generales de Java 2D (parte 2 de 2).

40 // establece el color de dibujo al azar

41 g2d.setColor( new Color( aleatorio.nextInt( 256 ),

42 aleatorio.nextInt( 256 ), aleatorio.nextInt( 256 ) ) );

43

44 g2d.fill( estrella ); // dibuja estrella rellena

45 } // fin de for

46 } // fin del método paintComponent

47 } // fin de la clase Figuras2JPanel

Fig. 15.32 � Creación de un objeto JFrame para mostrar estrellas.

1 // Fig. 15.32: Figuras2.java

2 // Demostración de una ruta general.

3 import java.awt.Color;

4 import javax.swing.JFrame;

5

6 public class Figuras2

7 {

8 // ejecuta la aplicación

9 public static void main( String[] args )

10 {

11 // crea marco para Figuras2JPanel

12 JFrame marco = new JFrame( “Dibujo de figuras en 2D” );

13 marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

14

15 Figuras2JPanel figuras2JPanel = new Figuras2JPanel();

16 marco.add( figuras2JPanel ); // agrega figuras2JPanel al marco

17 marco.setBackground( Color.WHITE ); // establece color de fondo del marco

18 marco.setSize( 400, 400 ); // establece el tamaño del marco

19 marco.setVisible( true ); // muestra el marco

20 } // fin de main

21 } // fin de la clase Figuras2

En las líneas 18 y 19 se declaran dos arreglos int que representan las coordenadas x y y de los pun-tos en la estrella. En la línea 22 se crea el objeto GeneralPath llamado estrella. En la línea 25 se utiliza el método moveTo de GeneralPath para especificar el primer punto en la estrella. La instrucción for de las líneas 28 y 29 utiliza el método lineTo de GeneralPath para dibujar una línea al siguiente punto en

664 Capítulo 15 Gráfi cos y Java 2D

la estrella. Cada nueva llamada a lineTo dibuja una línea del punto anterior al punto actual. En la línea 31 se utiliza el método closePath de GeneralPath para dibujar una línea del último punto hasta el punto especificado en la última llamada a moveTo. Esto completa la ruta general.

En la línea 33 se utiliza el método translate de Graphics2D para desplazar el origen del dibujo hasta la ubicación (200, 200). Todas las operaciones de dibujo utilizarán ahora la ubicación (200, 200) como si fuera (0, 0).

La instrucción for de las líneas 36 a 45 dibujan la estrella 20 veces, girándola alrededor del nuevo punto del origen. En la línea 38 se utiliza el método rotate de Graphics2D para girar la siguiente figura a mostrar. El argumento especifica el ángulo de giro en radianes (con 360° � 2� radianes). En la línea 44 se utiliza el método fill de Graphics2D para dibujar una versión rellena de la estrella.

15.9 ConclusiónEn este capítulo aprendió a utilizar las herramientas de gráficos de Java para producir dibujos a colores. Aprendió a especificar la ubicación de un objeto, usando el sistema de coordenadas de Java, y a dibujar en una ventana usando el método paintComponent. Vio una introducción a la clase Color y aprendió a utilizar esta clase para especificar distintos colores, usando sus componentes RGB. Utilizó el cuadro de diálogo JColorChooser para permitir a los usuarios seleccionar colores en un programa. Después aprendió a trabajar con los tipos de letra al dibujar texto en una ventana. Aprendió a crear un objeto Font a partir de un nombre, estilo y tamaño de tipo de letra, así como acceder a la métrica de un tipo de letra. De ahí, aprendió a dibujar varias figuras en una ventana, como rectángulos (regulares, redon-deados y en 3D), óvalos y polígonos, así como líneas y arcos. Después utilizó la API Java 2D para crear figuras más complejas y rellenarlas con degradados o patrones. El capítulo concluyó con una discusión sobre las rutas generales, que se utilizan para construir figuras a partir de líneas rectas y curvas complejas. En el siguiente capítulo analizaremos la clase String y sus métodos. Presentaremos las expresiones re-gulares para asociar patrones en cadenas de texto, y demostraremos cómo validar la entrada del usuario mediante expresiones regulares.

ResumenSección 15.1 Introducción• El sistema de coordenadas de Java (pág. 632) es un esquema para identifi car todos los posibles puntos (pág. 643) en

la pantalla.

• Un par de coordenadas (pág. 632) está compuesto de una coordenada x (horizontal) y una coordenada y (vertical).

• Las coordenadas se utilizan para indicar en dónde deben mostrarse los gráfi cos en una pantalla.

• Las unidades de las coordenadas se miden en píxeles (pág. 632). Un píxel es la unidad más pequeña de resolución de un monitor de computadora.

Sección 15.2 Contextos y objetos de gráfi cos• Un contexto de gráfi cos en Java (pág. 634) permite dibujar en la pantalla.

• La clase Graphics (pág. 632) contiene métodos para dibujar cadenas, líneas, rectángulos y otras fi guras. También se incluyen métodos para manipular tipos de letra y colores.

• Un objeto Graphics administra un contexto de gráfi cos y dibuja píxeles en la pantalla, los cuales representan a otros objetos gráfi cos, como líneas, elipses, rectángulos y otros polígonos (pág. 654).

• La clase Graphics es una clase abstract. Cada implementación de Java tiene una subclase de Graphics, la cual proporciona herramientas de dibujo. Esta implementación se oculta de nosotros mediante la clase Graphics, la cual proporciona la interfaz que nos permite utilizar los gráfi cos en forma independiente de la plataforma.

• El método paintComponent puede usarse para dibujar gráfi cos en cualquier componente JComponent.

• El método paintComponent recibe como argumento un objeto Graphics que el sistema pasa al método cuando un componente ligero de Swing necesita volver a pintarse.

• Cuando se ejecuta una aplicación, el contenedor de ésta llama al método paintComponent. Para que paintComponent se llame otra vez, debe ocurrir un evento.

• Cuando se muestra un objeto JComponent, se hace una llamada a su método paintComponent.

• Al llamar al método repaint (pág. 635) en un componente, se actualizan los gráfi cos que se dibujan en ese componente.

Sección 15.3 Control de colores• La clase Color (pág. 632) declara métodos y constantes para manipular los colores en un programa de Java.

• Todo color se crea a partir de un componente rojo, uno verde y uno azul. En conjunto estos componentes se llaman valores RGB (pág. 636). Los componentes RGB especifi can la cantidad de rojo, verde y azul en un color, en forma respectiva. Entre mayor sea el valor RGB, mayor será la cantidad de ese color específi co.

• Los métodos getRed, getGreen y getBlue de Color (pág. 636) devuelven valores enteros de 0 a 255, los cuales representan la cantidad de rojo, verde y azul, en forma respectiva.

• El método getColor de Graphics (pág. 636) devuelve un objeto Color que representa el color actual para dibujar.

• El método setColor de Graphics (pág. 636) establece el color actual para dibujar.

• El método fillRect de Graphics (pág. 636) dibuja un rectángulo relleno por el color actual del objeto Graphics.

• El método drawString de Graphics (pág. 638) dibuja un objeto String en el color actual.

• El componente de GUI JColorChooser (pág. 639) permite a los usuarios de una aplicación seleccionar colores.

• El método static showDialog de JColorChooser (pág. 641) muestra un cuadro de diálogo modal JColorChooser.

Sección 15.4 Manipulación de tipos de letra• La clase Font (pág. 632) contiene métodos y constantes para manipular tipos de letra.

• El constructor de la clase Font recibe tres argumentos: el nombre (pág. 634), estilo y tamaño del tipo de letra.

• El estilo de tipo de letra de un objeto Font puede ser Font.PLAIN, Font.ITALIC o Font.BOLD (cada uno es un campo static de la clase Font). Los estilos de tipos de letra pueden usarse combinados (por ejemplo, Font.ITALIC + Font.BOLD).

• El tamaño de un tipo de letra se mide en puntos. Un punto es 1/72 de una pulgada.

• El método setFont de Graphics (pág. 643) establece el tipo de letra para dibujar el texto que se va a mostrar.

• El método getStyle de Font (pág. 645) devuelve un valor entero que representa el estilo actual del objeto Font.

• El método getSize de Font (pág. 643) devuelve el tamaño del tipo de letra, en puntos.

• El método getName de Font (pág. 643) devuelve el nombre del tipo de letra actual, como una cadena.

• El método getFamily de Font (pág. 645) devuelve el nombre de la familia a la que pertenece el tipo de letra actual. El nombre de la familia del tipo de letra es específi co de cada plataforma.

• La clase FontMetrics (pág. 645) contiene métodos para obtener información sobre los tipos de letra.

• La métrica de tipos de letra (pág. 645) incluye la altura, el descendente y el ascendente.

Sección 15.5 Dibujo de líneas, rectángulos y óvalos• Los métodos fillRoundRect y drawRoundRect de Graphics (pág. 649) dibujan rectángulos con esquinas redondeadas.

• Los métodos draw3DRect (pág. 651) y fill3DRect (pág. 651) de Graphics dibujan rectángulos tridimensionales.

• Los métodos drawOval (pág. 651) y fillOval (pág. 651) de Graphics dibujan óvalos.

Sección 15.6 Dibujo de arcos• Un arco (pág. 651) se dibuja como una porción de un óvalo.

Resumen 665

• Los arcos se extienden desde un ángulo inicial, según el número de grados especifi cados por el ángulo del arco (pág. 651).

• Los métodos drawArc (pág. 651) y fillArc (pág. 651) de Graphics se utilizan para dibujar arcos.

Sección 15.7 Dibujo de polígonos y polilíneas• La clase Polygon contiene métodos para crear polígonos.

• Los polígonos son fi guras con varios lados, compuestas de segmentos de línea recta.

• Las polilíneas (pág. 654) son una secuencia de puntos conectados.

• El método drawPolyline de Graphics (pág. 656) muestra una serie de líneas conectadas.

• Los métodos drawPolygon (pág. 656) y fillPolygon (pág. 657) de Graphics se utilizan para dibujar polígonos.

• El método addPoint (pág. 657) de la clase Polygon agrega pares de coordenadas x y y a un objeto Polygon.

Sección 15.8 La API Java 2D• La API Java 2D (pág. 657) proporciona herramientas de gráfi cos bidimensionales avanzadas.

• La clase Graphics2D (pág. 632), una subclase de Graphics, se utiliza para dibujar con la API Java 2D.

• La API Java 2D contiene varias clases para dibujar fi guras, incluyendo Line2D.Double, Rectangle2D.Double, RoundRectangle2D.Double, Arc2D.Double y Ellipse2D.Double (pág. 657).

• La clase GradientPaint (pág. 632) ayuda a dibujar una fi gura en colores que cambian en forma gradual; a esto se le conoce como degradado (pág. 660).

• El método fill de Graphics2D (pág. 660) dibuja un objeto relleno de cualquier tipo que implemente a la interfaz Shape (pág. 660).

• La clase BasicStroke (pág. 632) ayuda a especifi car las características de dibujo de líneas.

• El método draw de Graphics2D (pág. 660) se utiliza para dibujar un objeto Shape.

• Las clases GradientPaint y TexturePaint (pág. 632) ayudan a especifi car las características para rellenar fi guras con colores o patrones.

• Una ruta general (pág. 662) es una fi gura construida a partir de líneas rectas y curvas complejas; se representa mediante un objeto de la clase GeneralPath (pág. 662).

• El método moveTo de GeneralPath (pág. 663) especifi ca el primer punto en una ruta general.

• El método lineTo de GeneralPath (pág. 663) dibuja una línea hasta el siguiente punto en la ruta. Cada nueva llamada a lineTo dibuja una línea desde el punto anterior hasta el punto actual.

• El método closePath de GeneralPath (pág. 664) dibuja una línea desde el último punto hasta el punto especifi cado en la última llamada a moveTo. Esto completa la ruta general.

• El método translate de Graphics2D (pág. 664) se utiliza para mover el origen de dibujo hasta una nueva ubicación.

• El método rotate de Graphics2D (pág. 664) se utiliza para girar la siguiente fi gura a mostrar.

Ejercicios de autoevaluación15.1 Complete las siguientes oraciones:

a) En Java 2D, el método de la clase establece las características de una línea utilizada para dibujar una figura.

b) La clase ayuda a especificar el relleno para una figura, de tal forma que el relleno cambie gradual-mente de un color a otro.

c) El método de la clase Graphics dibuja una línea entre dos puntos.d) RGB son las iniciales de , y .e) Los tamaños de los tipos de letra se miden en unidades llamadas .f ) La clase ayuda a especificar el relleno para una figura, utilizando un patrón dibujado en un objeto

BufferedImage.

666 Capítulo 15 Gráfi cos y Java 2D

15.2 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.a) Los primeros dos argumentos del método drawOval de Graphics especifican la coordenada central del óvalo.b) En el sistema de coordenadas de Java, las coordenadas x se incrementan de izquierda a derecha y las coorde-

nadas y de arriba hacia abajo.c) El método fillPolygon de Graphics dibuja un polígono sólido en el color actual.d) El método drawArc de Graphics permite ángulos negativos.e) El método getSize de Graphics devuelve el tamaño del tipo de letra actual, en centímetros.f ) La coordenada de píxel (0, 0) se encuentra exactamente en el centro del monitor.

15.3 Encuentre el(los) error(es) en cada una de las siguientes instrucciones, y explique cómo corregirlos. Suponga que g es un objeto Graphics.

a) g.setFont( “SansSerif” );

b) g.erase( x, y, w, h ); // borrar rectángulo en (x, y) c) Font f = new Font( “Serif”, Font.BOLDITALIC, 12 );

d) g.setColor( 255, 255, 0 ); // cambiar color a amarillo

Respuestas a los ejercicios de autoevaluación15.1 a) setStroke, Graphics2D. b) GradientPaint. c) drawLine. d) rojo, verde, azul. e) puntos. f ) TexturePaint.

15.2 a) Falso. Los primeros dos argumentos especifican la esquina superior izquierda del rectángulo delimitador.b) Verdadero.c) Verdadero.d) Verdadero.e) Falso. Los tamaños de los tipos de letra se miden en puntos.f ) Falso. La coordenada (0,0) corresponde a la esquina superior izquierda de un componente de la GUI, en el

cual ocurre el dibujo.

15.3 a) El método setFont toma un objeto Font como argumento, no un String.b) La clase Graphics no tiene un método erase. Debe utilizarse el método clearRect.c) Font.BOLDITALIC no es un estilo de tipo de letra válido. Para obtener un tipo de letra en cursiva y negrita,

use Font.BOLD + Font.ITALIC.d) El método setColor toma un objeto Color como argumento, no tres enteros.

Ejercicios15.4 Complete las siguientes oraciones:

a) La clase de la API Java 2D se utiliza para dibujar óvalos. b) Los métodos draw y fill de la clase Graphics2D requieren un objeto de tipo como su argumento.c) Las tres constantes que especifican el estilo de los tipos de letra son , y .d) El método de Graphics2D establece el color para pintar en figuras de Java 2D.

15.5 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.a) El método drawPolygon de Graphics conecta en forma automática los puntos de los extremos del polígono.b) El método drawLine de Graphics dibuja una línea entre dos puntos.c) El método fillArc de Graphics utiliza grados para especificar el ángulo.d) En el sistema de coordenadas de Java, los valores del eje y se incrementan de izquierda a derecha.e) La clase Graphics hereda directamente de la clase Object.f ) Graphics es una clase abstract.g) La clase Font hereda en forma directa de la clase Graphics.

Ejercicios 667

15.6 (Círculos concéntricos mediante el uso del método drawArc) Escriba una aplicación que dibuje una serie de ocho círculos concéntricos. Los círculos deberán estar separados por 10 píxeles. Use el método drawArc de la clase Graphics.

15.7 (Círculos concéntricos mediante el uso de la clase Ellipse2D.Double) Modifique su solución al ejercicio 15.6, para dibujar los óvalos mediante el uso de instancias de la clase Ellipse2D.Double y el método draw de la clase Graphics2D.

15.8 (Líneas aleatorias mediante el uso de la clase Line2D.Double) Modifique su solución al ejercicio 15.7 para dibujar líneas aleatorias en colores aleatorios y grosores de línea aleatorios. Use la clase Line2D.Double y el método draw de la clase Graphics2D para dibujar las líneas.

15.9 (Triángulos aleatorios) Escriba una aplicación que muestre triángulos generados al azar en distintos colores. Cada triángulo deberá rellenarse con un color distinto. Use la clase GeneralPath y el método fill de la clase Graphics2D para dibujar los triángulos.

15.10 (Caracteres aleatorios) Escriba una aplicación que dibuje caracteres al azar, en distintos tamaños y colores de di-versos tipos de letra.

15.11 (Cuadrícula mediante el uso del método drawLine) Escriba una aplicación que dibuje una cuadrícula de 8 por 8. Use el método drawLine de Graphics.

15.12 (Cuadrícula mediante el uso de la clase Line2D.Double) Modifique su solución al ejercicio 15.11 para dibujar la cuadrícula utilizando instancias de la clase Line2D.Double y el método draw de la clase Graphics2D.

15.13 (Cuadrícula mediante el uso del método drawRect) Escriba una aplicación que dibuje una cuadrícula de 10 por 10. Use el método drawRect de Graphics.

15.14 (Cuadrícula mediante el uso de la clase Rectangle2D.Double) Modifique su solución al ejercicio 15.13 para dibu-jar la cuadrícula utilizando la clase Rectangle2D.Double y el método draw de la clase Graphics2D.

15.15 (Dibujo de tetraedros) Escriba una aplicación que dibuje un tetraedro (una figura tridimensional con cuatro caras triangulares). Use la clase GeneralPath y el método draw de la clase Graphics2D.

15.16 (Dibujo de cubos) Escriba una aplicación que dibuje un cubo. Use la clase GeneralPath y el método draw de la clase Graphics2D.

15.17 (Círculos mediante el uso de la clase Ellipse2D.Double) Escriba una aplicación que pida al usuario introducir el radio de un círculo como número de punto flotante y que dibuje el círculo, así como los valores del diámetro, la circunfe-rencia y el área del círculo. Use el valor 3.14159 para �. [Nota: también puede usar la constante predefinida Math.PI para el valor de �. Esta constante es más precisa que el valor 3.14159. La clase Math se declara en el paquete java.lang, por lo que no necesita importarla. Use las siguientes fórmulas (r es el radio):

diámetro = 2rcircunferencia = 2�rárea = �r 2

El usuario debe introducir también un conjunto de coordenadas además del radio. Después dibuje el círculo y muestre su diámetro, circunferencia y área, mediante el uso de un objeto Ellipse2D.Double para representar el círculo y el método draw de la clase Graphics2D para mostrarlo en pantalla.

15.18 (Protector de pantalla) Escriba una aplicación que simule un protector de pantalla. La aplicación deberá di-bujar líneas al azar, utilizando el método drawLine de la clase Graphics. Después de dibujar 100 líneas, la aplicación deberá borrarse a sí misma y empezar a dibujar líneas nuevamente. Para permitir al programa dibujar en forma con-tinua, coloque una llamada a repaint como la última línea en el método paintComponent. ¿Observó algún problema con esto en su sistema?

15.19 (Protector de pantalla mediante el uso de Timer) El paquete javax.swing contiene una clase llamada Timer, la cual es capaz de llamar al método actionPerformed de la interfaz ActionListener durante un intervalo de tiempo fijo (especi-ficado en milisegundos). Modifique su solución al ejercicio 15.18 para eliminar la llamada a repaint desde el método paintComponent. Declare su clase de manera que implemente a ActionListener. (El método actionPerformed deberá

668 Capítulo 15 Gráfi cos y Java 2D

simplemente llamar a repaint). Declare una variable de instancia de tipo Timer, llamada temporizador, en su clase. En el constructor para su clase, escriba las siguientes instrucciones:

temporizador = new Timer( 1000, this );temporizador.start();

Esto crea una instancia de la clase Timer que llamará al método actionPerformed del objeto this cada 1000 milisegundos (es decir, cada segundo).

15.20 (Protector de pantalla para un número aleatorio de líneas) Modifique su solución al ejercicio 15.19 para permitir al usuario introducir el número de líneas aleatorias que deben dibujarse antes de que la aplicación se borre a sí misma y empiece a dibujar líneas otra vez. Use un objeto JTextField para obtener el valor. El usuario deberá ser capaz de escribir un nuevo número en el objeto JTextField en cualquier momento durante la ejecución del programa. Use una clase interna para realizar el manejo de eventos para el objeto JTextField.

15.21 (Protector de pantalla con figuras) Modifique su solución al ejercicio 15.19, de tal forma que utilice la generación de números aleatorios para seleccionar diferentes figuras a mostrar. Use métodos de la clase Graphics.

15.22 (Protector de pantalla mediante el uso de la API Java 2D) Modifique su solución al ejercicio 15.21 para utilizar clases y herramientas de dibujo de la API Java 2D. Para las figuras como rectángulos y elipses, dibújelas con degradados generados al azar. Use la clase GradientPaint para generar el degradado.

15.23 (Gráficos de tortuga) Modifique su solución al ejercicio 7.21 (Gráficos de tortuga) para agregar una interfaz grá-fica de usuario, mediante el uso de objetos JTextField y JButton. Dibuje líneas en vez de asteriscos (*). Cuando el pro-grama de gráficos de tortuga especifique un movimiento, traduzca el número de posiciones en un número de píxeles en la pantalla, multiplicando el número de posiciones por 10 (o cualquier valor que usted elija). Implemente el dibujo con características de la API Java 2D.

15.24 (Paseo del caballo) Produzca una versión gráfica del problema del Paseo del caballo (ejercicios 7.22, 7.23 y 7.26). A medida que se realice cada movimiento, la celda apropiada del tablero de ajedrez deberá actualizarse con el número de movimiento apropiado. Si el resultado del programa es un paseo completo o un paseo cerrado, el programa deberá mostrar un mensaje apropiado. Si lo desea, puede utilizar la clase Timer (vea el ejercicio 15.19) para que le ayude con la animación del Paseo del caballo.

15.25 (La tortuga y la liebre) Produzca una versión gráfica de la simulación La tortuga y la liebre (ejercicio 7.28). Simule la montaña dibujando un arco que se extienda desde la esquina inferior izquierda de la ventana, hasta la esquina superior derecha. La tortuga y la liebre deberán correr hacia arriba de la montaña. Implemente la salida gráfica de manera que la tortuga y la liebre se impriman en el arco, en cada movimiento. [Sugerencia: extienda la longitud de la carrera de 70 a 300, para que cuente con un área de gráficos más grande].

15.26 (Dibujo de espirales) Escriba una aplicación que utilice el método drawPolyline de Graphics para dibujar una espiral similar a la que se muestra en la figura 15.33.

Ejercicios 669

Fig. 15.33 � Dibujo de una espiral mediante el uso del método drawPolyline.

15.27 (Gráfico de pastel) Escriba un programa que reciba como entrada cuatro números y que los grafique en forma de gráfico de pastel. Use la clase Arc2D.Double y el método fill de la clase Graphics2D para realizar el dibujo. Dibuje cada pieza del pastel en un color distinto.

15.28 (Selección de figuras) Escriba una aplicación que permita al usuario seleccionar una figura de un objeto JComboBox y que la dibuje 20 veces, con ubicaciones y medidas aleatorias en el método paintComponent. El primer ele-mento en el objeto JComboBox debe ser la figura predeterminada a mostrar la primera vez que se hace una llamada a paintComponent.

15.29 (Colores aleatorios) Modifique el ejercicio 15.28 para dibujar cada una de las 20 figuras con tamaños aleatorios en un color seleccionado al azar. Use los 13 objetos Color predefinidos en un arreglo de objetos Color.

15.30 (Cuadro de diálogo JColorChooser) Modifique el ejercicio 15.28 para permitir al usuario seleccionar de un cuadro de diálogo JColorChooser el color en el que deben dibujarse las figuras.

(Opcional) Caso de estudio de GUI y Gráfi cos: agregar Java 2D15.31 Java 2D presenta muchas nuevas herramientas para crear gráficos únicos e impresionantes. Agregaremos un pequeño subconjunto de estas características a la aplicación de dibujo que creó en el ejercicio 14.17. En esta versión de la aplicación de dibujo, permitirá al usuario especificar degradados para rellenar figuras y modificar las características de trazo para dibujar líneas y los contornos de las figuras. El usuario podrá elegir cuáles colores van a formar el degradado, y también podrá establecer la anchura y longitud de guión del trazo.

Primero debe actualizar la jerarquía MiFigura para que soporte la funcionalidad de Java 2D. Haga las siguientes mo-dificaciones en la clase MiFigura:

a) Cambie el tipo del parámetro del método abstract draw, de Graphics a Graphics2D.b) Cambie todas las variables de tipo Color al tipo Paint, para habilitar el soporte para los degradados.

[Nota: recuerde que la clase Color implementa a la interfaz Paint].c) Agregue una variable de instancia de tipo Stroke en la clase MiFigura y un parámetro Stroke en el cons-

tructor, para inicializar la nueva variable de instancia. El trazo predeterminado deberá ser una instancia de la clase BasicStroke.

Cada una de las clases MiLinea, MiFiguraDelimitada, MiOvalo y MiRect debe agregar un parámetro Stroke a sus constructores. En los métodos draw, cada fi gura debe establecer los objetos Paint y Stroke antes de dibujar o rellenar una fi gura. Como Graphics2D es una subclase de Graphics, podemos seguir utilizando los métodos drawLine, drawOval, fillOval, y otros métodos más de Graphics, para dibujar las fi guras. Al llamar a estos métodos, dibujarán la fi gura apropiada usando las opciones especifi cadas para los objetos Paint y Stroke.

Después, actualice el objeto PanelDibujo para manejar las herramientas de Java 2D. Cambie todas las variables Color a variables Paint. Declare una variable de instancia llamada trazoActual de tipo Stroke, y proporcione un método establecer para esta variable. Actualice las llamadas a los constructores de cada fi gura para incluir los argumentos Paint y Stroke. En el método paintComponent, convierta la referencia Graphics al tipo Graphics2D y use la referencia Graphics2D en cada llamada al método draw de MiFigura.

A continuación, haga que se pueda tener acceso a las nuevas características de Java 2D mediante la GUI. Cree un objeto JPanel de componentes de GUI para establecer las opciones de Java2D. Agregue esos componentes a la parte superior del objeto MarcoDibujo, debajo del panel que actualmente contiene los controles de las fi guras estándar (vea la fi gura 15.34). Estos componentes de GUI deben incluir lo siguiente:

a) Una casilla de verifi cación para especifi car si se va a pintar usando un degradado.b) Dos objetos JButton, cada uno de los cuales debe mostrar un cuadro de diálogo JColorChooser, para per-

mitir al usuario elegir los colores primero y segundo en el degradado. (Éstos sustituirán al objeto JComboBox que se utiliza para extender el color en el ejercicio 14.17).

c) Un campo de texto para introducir la anchura del objeto Stroke.d) Un campo de texto para introducir la longitud de guión del objeto Stroke.e) Una casilla de verifi cación para seleccionar si se va a dibujar una línea punteada o sólida.

Si el usuario opta por dibujar con un degradado, establezca el objeto Paint en el PanelDibujo de forma que sea un degradado de los dos colores seleccionados por el usuario. La expresión

670 Capítulo 15 Gráfi cos y Java 2D

new GradientPaint( 0, 0, color1, 50, 50, color2, true ) )

crea un objeto GradientPath que avanza diagonalmente en círculos, desde la esquina superior izquierda hasta la esquina inferior derecha, cada 50 píxeles. Las variables color1 y color2 representan los colores elegidos por el usuario. Si éste no elije usar un degradado, entonces simplemente establezca el objeto Paint en el PanelDibujo de manera que sea el primer Color elegido por el usuario.

Para los trazos, si el usuario elije una línea sólida, entonces cree el objeto Stroke con la expresión

new BasicStroke( anchura, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND )

en donde la variable anchura es la anchura especifi cada por el usuario en el campo de texto de anchura de línea. Si el usuario selecciona una línea punteada, entonces cree el objeto Stroke con la expresión

new BasicStroke( anchura, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10, guiones, 0 )

en donde anchura es de nuevo la anchura en el campo de anchura de texto, y guiones es un arreglo con un elemento, cuyo valor es la longitud especifi cada en el campo de longitud de guión. Los objetos Panel y Stroke deben pasarse al constructor del objeto fi gura, cuando se cree la fi gura en el objeto PanelDibujo.

Marcar la diferencia15.32 (Pantallas con tipos de letra grandes para personas con poca visión) La accesibilidad de las computadoras e Internet para todas las personas, sin importar las discapacidades, se está volviendo cada vez más importante a medida que estas herramientas desempeñan más roles en nuestras vidas personales y de negocios. De acuerdo con una estimación reciente de la Organización Mundial de la Salud (www.who.int/mediacentre/factsheets/fs282/es), 124 millones de personas en todo el mundo sufren de baja visión. Para aprender más sobre la baja visión, de un vistazo a la simulación de baja visión basada en GUI en www.webaim.org/simulations/lowvision.php. Las personas con baja visión podrían preferir un tipo de letra y/o un tamaño de tipo de letra de mayor tamaño al leer documentos electrónicos y páginas Web. Java cuenta con cinco tipos de letra “lógicos” integrados, que se garantiza estarán disponibles en cualquier implementa-ción de Java, incluyendo Serif, Sans-serif y Monospaced. Escriba una aplicación con GUI que proporcione un objeto JTextArea en donde el usuario pueda escribir texto. Permita al usuario seleccionar Serif, Sans-serif o Monospaced de un JComboBox. Proporcione un objeto JCheckBox Negrita, que cuando se seleccione el texto se ponga en negrita. Incluya los objetos JButton Aumentar tamaño de letra y Reducir tamaño de letra, que permitan al usuario escalar el tamaño del tipo de letra para aumentarlo o reducirlo, en forma respectiva, un punto a la vez. Empiece con un tamaño de tipo de letra de 18 puntos. Para los fines de este ejercicio, establezca el tamaño del tipo de letra en los objetos JComboBox, JButton y JCheckBox en 20 puntos, de modo que una persona con baja visión pueda leer el texto en ellos.

Marcar la diferencia 671

Fig. 15.34 � Dibujo con Java2D.

Cadenas, caracteres y expresiones regulares16

El principal defecto del rey Enrique era masticar pequeños pedazos de hilo.—Hilaire Belloc

La escritura vigorosa es concisa. Una oración no debe contener palabras innecesarias, un párrafo no debe contener oraciones innecesarias.—William Strunk, Jr.

He extendido esta carta más de lo usual, ya que carezco de tiempo para hacerla breve.—Blaise Pascal

O b j e t i v o sEn este capítulo aprenderá a:

■ Crear y manipular objetos de cadena de caracteres inmutables de la clase String.

■ Crear y manipular objetos de cadena de caracteres mutables de la clase StringBuilder.

■ Crear y manipular objetos de la clase Character.

■ Dividir un objeto String en tokens mediante el método split de String.

■ Usar expresiones regulares para validar los datos String introducidos en una aplicación.

16.1 Introducción 673

16.1 Introducción

16.2 Fundamentos de los caracteres y las cadenas

16.3 La clase String 16.3.1 Constructores de String16.3.2 Métodos length, charAt y getChars

de String16.3.3 Comparación entre cadenas16.3.4 Localización de caracteres y subcadenas

en las cadenas16.3.5 Extracción de subcadenas de las cadenas16.3.6 Concatenación de cadenas16.3.7 Métodos varios de String16.3.8 Método valueOf de String

16.4 La clase StringBuilder 16.4.1 Constructores de StringBuilder

16.4.2 Métodos length, capacity, setLength y ensureCapacity de StringBuilder

16.4.3 Métodos charAt, setCharAt, getChars y reverse de StringBuilder

16.4.4 Métodos append de StringBuilder16.4.5 Métodos de inserción y eliminación de

StringBuilder

16.5 La clase Character

16.6 División de objetos String en tokens

16.7 Expresiones regulares, la clase Pattern y la clase Matcher

16.8 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios | Sección especial: ejercicios de manipulación avanzada de cadenas |

Sección especial: proyectos desafi antes de manipulación de cadenas | Marcar la diferencia

16.1 IntroducciónEn este capítulo presentamos las herramientas para el procesamiento de cadenas y caracteres en Java. Las técnicas que se describen aquí son apropiadas para validar la entrada en los programas, para mos-trar información a los usuarios y otras manipulaciones basadas en texto. Estas técnicas son también apropiadas para desarrollar editores de texto, procesadores de palabras, software para el diseño de páginas, sistemas de composición computarizados y demás tipos de software para el procesamiento de texto. Ya hemos presentado varias herramientas para el procesamiento de texto en capítulos anteriores. En este capítulo describiremos con detalle las herramientas de las clases String, StringBuilder y Character del paquete java.lang. Estas clases proporcionan la base para la manipulación de cadenas y caracteres en Java.

En este capítulo también hablaremos sobre las expresiones regulares que proporcionan a las aplica-ciones la capacidad de validar los datos de entrada. Esta funcionalidad se encuentra en la clase String, junto con las clases Matcher y Pattern ubicadas en el paquete java.util.regex.

16.2 Fundamentos de los caracteres y las cadenasLos caracteres son los bloques de construcción básicos de los programas fuente de Java. Todo programa está compuesto de una secuencia de caracteres que, cuando se agrupan en forma significativa, son in-terpretados por la computadora como una serie de instrucciones utilizadas para realizar una tarea. Un programa puede contener literales de carácter. Una literal de carácter es un valor entero represen-tado como un carácter entre comillas simples. Por ejemplo, ‘z’ representa el valor entero de z, y ‘\n’ representa el valor entero de una nueva línea. El valor de una literal de carácter es el valor entero del carácter en el conjunto de caracteres Unicode. En el apéndice B se presentan los equivalentes enteros de los caracteres en el conjunto de caracteres ASCII, el cual es un subconjunto de Unicode (que veremos en el apéndice L).

Recuerde que en la sección 2.2 vimos que una cadena es una secuencia de caracteres que se trata como una sola unidad. Una cadena puede incluir letras, dígitos y varios caracteres especiales, tales como +, -, *,

674 Capítulo 16 Cadenas, caracteres y expresiones regulares

/ y $. Una cadena es un objeto de la clase String. Las literales de cadena (que se almacenan en memoria como objetos String) se escriben como una secuencia de caracteres entre comillas dobles, como en:

“Juan E. Pérez” (un nombre)“Calle Principal 9999” (una dirección)“Monterrey, Nuevo León” (una ciudad y un estado)“(201) 555-1212” (un número telefónico)

Una cadena puede asignarse a una referencia String. La declaración

String color = “azul”;

inicializa la variable String de nombre color para que haga referencia a un objeto String que contiene la cadena “azul”.

Tip de rendimiento 16.1Para conservar memoria, Java trata a todas las literales de cadena con el mismo contenido como un solo objeto String que tiene muchas referencias.

16.3 La clase StringLa clase String se utiliza para representar cadenas en Java. En las siguientes subsecciones cubriremos muchas de las herramientas de la clase String.

16.3.1 Constructores de StringLa clase String proporciona constructores para inicializar objetos String de varias formas. Cuatro de los constructores se muestran en el método main de la figura 16.1.

En la línea 12 se crea una instancia de un nuevo objeto String, utilizando el constructor sin argu-mentos de la clase String, y se le asigna su referencia a s1. El nuevo objeto String no contiene caracteres

1 // Fig. 16.1: ConstructoresString.java 2 // Constructores de la clase String. 3 4 public class ConstructoresString 5 { 6 public static void main( String[] args ) 7 { 8 char arregloChar[] = { ‘c’, ‘u’, ‘m’, ‘p’, ‘l’, ‘e’, ‘ ’, ‘a’, ‘n’, ‘i’, ‘o’, ‘s’ }; 9 String s = new String( “hola” );1011 // usa los constructores de String12 String s1 = new String();13 String s2 = new String( s );14 String s3 = new String( arregloChar );15 String s4 = new String( arregloChar, 7, 5 );1617 System.out.printf(18 “s1 = %s\ns2 = %s\ns3 = %s\ns4 = %s\n”, 19 s1, s2, s3, s4 ); // muestra las cadenas20 } // fin de main21 } // fin de la clase ConstructoresString

Fig. 16.1 � Constructores de la clase String (parte 1 de 2).

16.3 La clase String 675

(la cadena vacía, que se puede representar también como “”) y tiene una longitud de 0. En la línea 13 se crea una instancia de un nuevo objeto String, utilizando el constructor de la clase String que toma un objeto String como argumento, y se le asigna su referencia a s2. El nuevo objeto String contiene la misma secuencia de caracteres que el objeto String de nombre s, el cual se pasa como argu-mento para el constructor.

Observación de ingeniería de software 16.1No es necesario copiar un objeto String existente. Los objetos String son inmutables: su contenido de caracteres no puede modificarse una vez que son creados, ya que la clase String no proporciona métodos que permitan modificar el contenido de un ob-jeto String.

En la línea 14 se crea la instancia de un nuevo objeto String y se le asigna su referencia a s3, utilizan-do el constructor de la clase String que toma un arreglo de caracteres como argumento. El nuevo objeto String contiene una copia de los caracteres en el arreglo.

En la línea 15 se crea la instancia de un nuevo objeto String y se le asigna su referencia a s4, utili-zando el constructor de la clase String que toma un arreglo char y dos enteros como argumentos. El segundo argumento especifica la posición inicial (el desplazamiento) a partir del cual se accede a los caracteres en el arreglo. Recuerde que el primer carácter se encuentra en la posición 0. El tercer argu-mento especifica el número de caracteres (la cuenta) que se van a utilizar del arreglo. El nuevo objeto String se forma a partir de los caracteres utilizados. Si el desplazamiento o la cuenta especificados como argumentos ocasionan que se acceda a un elemento fuera de los límites del arreglo de caracteres, se lanza una excepción StringIndexOutOfBoundsException.

Error común de programación 16.1Intentar acceder a un carácter que se encuentra fuera de los límites de un objeto String (es decir, un índice menor que 0 o un índice mayor o igual a la longitud del objeto String), se produce una excepción StringIndexOutOfBoundsException.

16.3.2 Métodos length, charAt y getChars de StringLos métodos length, charAt, y getChars de String devuelven la longitud de un objeto String, obtie-nen el carácter que se encuentra en una ubicación específica de un objeto String y recuperan un conjun-to de caracteres en un objeto String como un arreglo char, respectivamente. La figura 16.2 demuestra cada uno de estos métodos.

s1 =s2 = holas3 = cumple anioss4 = anios

1 // Fig. 16.2: VariosString.java

2 // Esta aplicación muestra los métodos length, charAt y getChars

3 // de la clase String.

4

5 public class VariosString

6 {

Fig. 16.2 � Los métodos length, charAt y getChars de String (parte 1 de 2).

Fig. 16.1 � Constructores de la clase String (parte 2 de 2).

676 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la línea 15 se utiliza el método length de String para determinar el número de caracteres en la cadena s1. Al igual que los arreglos, las cadenas conocen su propia longitud. Sin embargo, a diferencia de los arreglos, no podemos acceder a la longitud de un objeto String mediante un campo length; en vez de ello, debemos llamar al método length de ese objeto.

En las líneas 20 y 21 se imprimen los caracteres de la cadena s1 en orden inverso (y separados por espacios). El método charAt de String (línea 21) devuelve el carácter ubicado en una posición específica en el objeto String. El método charAt recibe un argumento entero que se utiliza como el índice, y de-vuelve el carácter en esa posición. Al igual que los arreglos, se considera que el primer elemento de un objeto String está en la posición 0.

En la línea 24 se utiliza el método getChars de String para copiar los caracteres de un objeto String en un arreglo de caracteres. El primer argumento es el índice inicial en la cadena, a partir del cual se van a copiar los caracteres. El segundo argumento es el índice que está una posición más adelante del último carácter que se va a copiar del objeto String. El tercer argumento es el arreglo de caracteres en el que se van a copiar los caracteres. El último argumento es el índice inicial en donde se van a colocar los caracte-res copiados en el arreglo de caracteres de destino. A continuación, en las líneas 27 y 28 se imprime el contenido del arreglo char, un carácter a la vez.

16.3.3 Comparación entre cadenasEn el capítulo 19 hablaremos sobre el ordenamiento y la búsqueda en los arreglos. Con frecuencia, la información que se va a ordenar o buscar consiste en cadenas que deben compararse para determinar

7 public static void main( String[] args ) 8 { 9 String s1 = “hola a todos”;10 char[] arregloChar = new char[ 4 ];1112 System.out.printf( “s1: %s”, s1 );1314 // prueba el método length15 System.out.printf( “\nLongitud de s1: %d”, s1.length() );1617 // itera a través de los caracteres en s1 con charAt y muestra la cadena invertida18 System.out.print( “\nLa cadena invertida es: ” );1920 for ( int cuenta = s1.length() - 1; cuenta >= 0; cuenta-- )21 System.out.printf( “%s ”, s1.charAt( cuenta) );2223 // copia los caracteres de la cadena a arregloChar24 s1.getChars( 0, 4, arregloChar, 0 );25 System.out.print( “\nEl arreglo de caracteres es: ” );2627 for ( char caracter : arregloChar )28 System.out.print( caracter );2930 System.out.println();31 } // fin de main32 } // fin de la clase VariosString

s1: hola a todosLongitud de s1: 12La cadena invertida es: s o d o t a a l o hEl arreglo de caracteres es: hola

Fig. 16.2 � Los métodos length, charAt y getChars de String (parte 2 de 2).

16.3 La clase String 677

el orden o para determinar si una cadena aparece en un arreglo (u otra colección). La clase String pro-porciona varios métodos para comparar cadenas, como se demuestra en los siguientes dos ejemplos.

Para comprender lo que significa que una cadena sea mayor o menor que otra, considere el proceso de alfabetizar una serie de apellidos. Sin duda usted colocaría a “Jones” antes que “Smith”, ya que en el alfabeto la primera letra de “Jones” viene antes que la primera letra de “Smith”. Pero el alfabeto es algo más que una lista de 26 letras; es un conjunto ordenado de caracteres. Cada letra ocupa una posi-ción específica dentro del conjunto. Z es más que una letra del alfabeto; es en específico la letra número veintiséis del alfabeto.

¿Cómo sabe la computadora que una letra “va antes” que otra? Todos los caracteres se representan en la computadora como códigos numéricos (vea el apéndice B). Cuando la computadora compara objetos String, en realidad compara los códigos numéricos de los caracteres en estos objetos.

En la figura 16.3 se muestran los métodos equals, equalsIgnoreCase, compareTo y regionMatches de String, y se muestra el uso del operador de igualdad == para comparar objetos String.

1 // Fig. 16.3: CompararCadenas.java

2 // Los métodos equals, equalsIgnoreCase, compareTo y regionMatches de String.

3

4 public class CompararCadenas

5 {

6 public static void main( String[] args )

7 {

8 String s1 = new String( “hola” ); // s1 es una copia de “hola”

9 String s2 = “adios”;

10 String s3 = “Feliz cumpleanios”;

11 String s4 = “feliz cumpleanios”;

12

13 System.out.printf(

14 “s1 = %s\ns2 = %s\ns3 = %s\ns4 = %s\n\n”, s1, s2, s3, s4 );

15

16 // prueba la igualdad

17 if ( s1.equals( “hola” ) ) // true

18 System.out.println( “s1 es igual a \“hola\”” );

19 else

20 System.out.println( “s1 no es igual a \“hola\”” );

21

22 // prueba la igualdad con ==

23 if ( s1 == “hola” ) // false; no son el mismo objeto

24 System.out.println( “s1 es el mismo objeto que \“hola\”” );

25 else

26 System.out.println( “s1 no es el mismo objeto que \“hola\”” );

27

28 // prueba la igualdad (ignora el uso de mayúsculas/minúsculas)

29 if ( s3.equalsIgnoreCase( s4 ) ) // true

30 System.out.printf( “%s es igual a %s si se ignora el uso de mayusculas/minusculas\n”, s3, s4 );

31 else

32 System.out.println( “s3 no es igual a s4” );

33

Fig. 16.3 � Los métodos equals, equalsIgnoreCase, compareTo y regionMatches de String (parte 1 de 2).

678 Capítulo 16 Cadenas, caracteres y expresiones regulares

Método equals de String La condición en la línea 17 utiliza el método equals para comparar la igualdad entre el objeto String s1 y la literal String “hola”. El método equals (un método de la clase Object, sobrescrito en String) prueba la igualdad entre dos objetos (es decir, que las cadenas contenidas en los dos objetos

34 // prueba con compareTo

35 System.out.printf(

36 “\ns1.compareTo( s2 ) es %d”, s1.compareTo( s2 ) );

37 System.out.printf(

38 “\ns2.compareTo( s1 ) es %d”, s2.compareTo( s1 ) );

39 System.out.printf(

40 “\ns1.compareTo( s1 ) es %d”, s1.compareTo( s1 ) );

41 System.out.printf(

42 “\ns3.compareTo( s4 ) es %d”, s3.compareTo( s4 ) );

43 System.out.printf(

44 “\ns4.compareTo( s3 ) es %d\n\n”, s4.compareTo( s3 ) );

45

46 // prueba con regionMatches (sensible a mayúsculas/minúsculas)

47 if ( s3.regionMatches( 0, s4, 0, 5 ) )

48 System.out.println( “Los primeros 5 caracteres de s3 y s4 coinciden” );

49 else

50 System.out.println(

51 “Los primeros 5 caracteres de s3 y s4 no coinciden” );

52

53 // prueba con regionMatches (ignora el uso de mayúsculas/minúsculas)

54 if ( s3.regionMatches( true, 0, s4, 0, 5 ) )

55 System.out.println(

56 “Los primeros 5 caracteres de s3 y s4 coinciden” );

57 else

58 System.out.println(

59 “Los primeros 5 caracteres de s3 y s4 no coinciden” );

60 } // fin de main

61 } // fin de la clase CompararCadenas

s1 = holas2 = adioss3 = Feliz cumpleanioss4 = feliz cumpleanios

s1 es igual a “hola”s1 no es el mismo objeto que “hola”Feliz cumpleanios es igual a feliz cumpleanios si se ignora el uso de mayusculas/minusculas

s1.compareTo( s2 ) es 7s2.compareTo( s1 ) es -7s1.compareTo( s1 ) es 0s3.compareTo( s4 ) es -32s4.compareTo( s3 ) es 32

Los primeros 5 caracteres de s3 y s4 no coincidenLos primeros 5 caracteres de s3 y s4 coinciden

Fig. 16.3 � Los métodos equals, equalsIgnoreCase, compareTo y regionMatches de String (parte 2 de 2).

16.3 La clase String 679

sean idénticas). El método devuelve true si el contenido de los objetos es igual y false en caso con-trario. La condición anterior es true, ya que el objeto String s1 se inicializó con la literal de cadena “hola”. El método equals utiliza una comparación lexicográfica; se comparan los valores enteros Unicode (vea el apéndice L para obtener más información) que representan a cada uno de los carac-teres en cada objeto String. Por lo tanto, si la cadena “hola” se compara con la cadena “HOLA”, el resultado es false ya que la representación entera de una letra minúscula es distinta de la letra ma-yúscula correspondiente.

Comparación de objetos String con el operador ==La condición en la línea 23 utiliza el operador de igualdad == para comparar la igualdad entre el objeto String s1 y la literal String “hola”. Cuando se comparan valores de tipos primitivos con ==, el resul-tado es true si ambos valores son idénticos. Cuando se comparan referencias con ==, el resultado es true si ambas referencias se refieren al mismo objeto en memoria. Para comparar la igualdad del contenido en sí (o información sobre el estado) de los objetos, hay que invocar un método. En el caso de los objetos String, ese método es equals. La condición anterior se evalúa como false en la línea 23, ya que la refe-rencia s1 se inicializó con la instrucción

s1 = new String( “hola” );

con lo cual se crea un nuevo objeto String con una copia de la literal de cadena “hola”, y se asigna el nuevo objeto a la variable s1. Si s1 se hubiera inicializado con la instrucción

s1 = “hola”;

con lo cual se asigna directamente la literal de cadena “hola” a la variable s1, la condición hubiera sido true. Recuerde que Java trata a todos los objetos de literal de cadena con el mismo contenido como un objeto String, al cual puede haber muchas referencias. Por lo tanto, en las líneas 8, 17 y 23 se hace refe-rencia al mismo objeto String “hola” en memoria.

Error común de programación 16.2Al comparar referencias con == se pueden producir errores lógicos, ya que == compara las referencias para determinar si se refieren al mismo objeto, no si dos objetos tienen el mismo contenido. Si se comparan dos objetos idénticos (pero separados) con ==, el resultado será false. Si va a comparar objetos para determinar si tienen el mismo contenido, utilice el método equals.

Método equalsIgnoreCase de String Si va a ordenar objetos String, puede comparar si son iguales con el método equalsIgnoreCase, el cual ignora si las letras en cada objeto String son mayúsculas o minúsculas al realizar la comparación. Por lo tanto, la cadena “hola” y la cadena “HOLA” se consideran iguales. En la línea 29 se utiliza el método equalsIgnoreCase de String para comparar si el objeto String s3 (Feliz Cumpleanios) es igual a la cadena s4 (feliz cumpleanios). El resultado de esta comparación es true, ya que la comparación igno-ra el uso de mayúsculas y minúsculas.

Método compareTo de String En las líneas 35 a 44 se utiliza el método compareTo de String para comparar objetos String. El mé-todo compareTo se declara en la interfaz Comparable y se implementa en la clase String. En la línea 36 se compara el objeto String s1 con el objeto String s2. El método compareTo devuelve 0 si los obje-tos String son iguales, un número negativo si el objeto String que invoca a compareTo es menor que el objeto String que se pasa como argumento, y un número positivo si el objeto String que invoca a compareTo es mayor que la cadena que se pasa como argumento. El método compareTo utiliza una

680 Capítulo 16 Cadenas, caracteres y expresiones regulares

comparación lexicográfica; compara los valores numéricos de los caracteres correspondientes en cada objeto String.

Método regionMatches de String La condición en la línea 47 utiliza el método regionMatches de String para comparar si ciertas porcio-nes de dos objetos String son iguales. El primer argumento es el índice inicial en el objeto String que invoca al método. El segundo argumento es un objeto String de comparación. El tercer argumento es el índice inicial en el objeto String de comparación. El último argumento es el número de caracteres a comparar entre los dos objetos String. El método devuelve true solamente si el número especificado de caracteres son lexicográficamente iguales.

Por último, la condición en la línea 54 utiliza una versión con cinco argumentos del método region-Matches de String para comparar si ciertas porciones de dos objetos String son iguales. Cuando el primer argumento es true, el método ignora el uso de mayúsculas y minúsculas en los caracteres que se van a comparar. Los argumentos restantes son idénticos a los que se describieron para el método regionMatches con cuatro argumentos.

Métodos startsWith y endsWith de StringEn el siguiente ejemplo (figura 16.4) vamos a mostrar los métodos startsWith y endsWith de la clase String. El método main crea el arreglo cadenas que contiene las cadenas “empezo”, “empezando”, “termino” y “terminando”. El resto del método main consiste en tres instrucciones for que prue-ban los elementos del arreglo para determinar si empiezan o terminan con un conjunto específico de caracteres.

1 // Fig. 16.4: CadenaInicioFin.java

2 // Los métodos startsWith y endsWith de String.

3

4 public class CadenaInicioFin

5 {

6 public static void main( String[] args )

7 {

8 String[] cadenas = { “empezo”, “empezando”, “termino”, “terminando” };

9

10 // prueba el método startsWith

11 for ( String cadena: cadenas )

12 {

13 if ( cadena.startsWith( “em” ) )

14 System.out.printf( “\“%s\” empieza con \“em\“\n”, cadena);

15 } // fin de for

16

17 System.out.println();

18

19 // prueba el método startsWith empezando desde la posición 2 de la cadena

20 for ( String cadena: cadenas )

21 {

22 if ( cadena.startsWith( “pez”, 2 ) )

23 System.out.printf(

24 “\“%s\” empieza con \“pez\” en la posicion 2\n”, cadena);

25 } // fin de for

26

27 System.out.println();

Fig. 16.4 � Métodos startsWith y endsWith de la clase String (parte 1 de 2).

16.3 La clase String 681

En las líneas 11 a 15 se utiliza la versión del método startsWith que recibe un argumento String. La condición en la instrucción if (línea 13) determina si cada objeto String en el arreglo empieza con los caracteres “em”. De ser así, el método devuelve true y la aplicación imprime ese objeto String. En caso contrario, el método devuelve false y no ocurre nada.

En las líneas 20 a 25 se utiliza el método startsWith que recibe un objeto String y un entero como argumentos. El argumento entero especifica el índice en el que debe empezar la comparación en el objeto String. La condición en la instrucción if (línea 22) determina si cada objeto String en el arre-glo tiene los caracteres “pez”, empezando con el tercer carácter en cada objeto String. De ser así, el método devuelve true y la aplicación imprime el objeto String.

La tercera instrucción for (líneas 30 a 34) utiliza el método endsWith, que recibe un argumento String. La condición en la línea 32 determina si cada objeto String en el arreglo termina con los carac-teres “do”. De ser así, el método devuelve true y la aplicación imprime el objeto String.

16.3.4 Localización de caracteres y subcadenas en las cadenasA menudo es útil buscar un carácter o conjunto de caracteres en una cadena. Por ejemplo, si usted va a crear su propio procesador de palabras, tal vez quiera proporcionar una herramienta para buscar a través de los documentos. En la figura 16.5 se muestran las diversas versiones de los métodos indexOf y lastIndexOf de String, que buscan un carácter o subcadena especificados en un objeto String.

1 // Fig. 16.5: MetodosIndexString.java

2 // Métodos indexOf y lastIndexOf para buscar en cadenas.

3

4 public class MetodosIndexString

5 {

6 public static void main( String[] args )

7 {

8 String letras = “abcdefghijklmabcdefghijklm”;

28

29 // prueba el método endsWith

30 for ( String cadena: cadenas )

31 {

32 if ( cadena.endsWith( “do” ) )

33 System.out.printf( “\“%s\” termina con \“do\”\n”, cadena);

34 } // fin de for

35 } // fin de main

36 } // fin de la clase CadenaInicioFin

“empezo” empieza con “em”“empezando” empieza con “em”

“empezo” empieza con “pez” en la posicion 2“empezando” empieza con “pez” en la posicion 2

“empezando” termina con “do”“terminando” termina con “do”

Fig. 16.5 � Métodos de búsqueda indexOf y lastIndexOf de la clase String (parte 1 de 2).

Fig. 16.4 � Métodos startsWith y endsWith de la clase String (parte 2 de 2).

682 Capítulo 16 Cadenas, caracteres y expresiones regulares

Todas las búsquedas en este ejemplo se realizan en el objeto String letras (inicializado con “abc-defghijklmabcdefghijklm”). En las líneas 11 a 16 se utiliza el método indexOf para localizar la prime-

910 // prueba indexOf para localizar un carácter en una cadena11 System.out.printf( 12 “‘c’ se encuentra en el indice %d\n”, letras.indexOf( ‘c’ ) );13 System.out.printf(14 “‘a' se encuentra en el indice %d\n”, letras.indexOf( ‘a’, 1 ) );15 System.out.printf(16 “‘$’ se encuentra en el indice %d\n\n”, letras.indexOf( ‘$’ ) );1718 // prueba lastIndexOf para buscar un carácter en una cadena19 System.out.printf( “La ultima ‘c’ se encuentra en el indice %d\n”,20 letras.lastIndexOf( ‘c’ ) );21 System.out.printf( “La ultima ‘a’ se encuentra en el indice %d\n”,22 letras.lastIndexOf( ‘a’, 25 ) );23 System.out.printf( “La ultima ‘$’ se encuentra en el indice %d\n\n”,24 letras.lastIndexOf( ‘$’ ) );2526 // prueba indexOf para localizar una subcadena en una cadena27 System.out.printf( “\“def\” se encuentra en el indice %d\n”, 28 letras.indexOf( “def” ) );29 System.out.printf( “\“def\” se encuentra en el indice %d\n”,30 letras.indexOf( “def”, 7 ) );31 System.out.printf( “\“hola\” se encuentra en el indice %d\n\n”,32 letras.indexOf( “hola” ) );3334 // prueba lastIndexOf para buscar una subcadena en una cadena

35 System.out.printf( “La ultima ocurrencia de \“def\” se encuentra en el indice %d\n”,

36 letras.lastIndexOf( “def” ) );

37 System.out.printf( “La ultima ocurrencia de \“def\” se encuentra en el indice %d\n”,

38 letras.lastIndexOf( “def”, 25 ) );

39 System.out.printf( “La ultima ocurrencia de \“hola\” se encuentra en el indice %d\n”,

40 letras.lastIndexOf( “hola” ) );41 } // fin de main42 } // fin de la clase MetodosIndexString

‘c’ se encuentra en el indice 2‘a’ se encuentra en el indice 13‘$’ se encuentra en el indice -1

La ultima ‘c’ se encuentra en el indice 15La ultima ‘a’ se encuentra en el indice 13La ultima ‘$’ se encuentra en el indice -1

“def” se encuentra en el indice 3“def” se encuentra en el indice 16“hola” se encuentra en el indice -1

La ultima ocurrencia de “def” se encuentra en el indice 16La ultima ocurrencia de “def” se encuentra en el indice 16La ultima ocurrencia de “hola” se encuentra en el indice -1

Fig. 16.5 � Métodos de búsqueda indexOf y lastIndexOf de la clase String (parte 2 de 2).

16.3 La clase String 683

ra ocurrencia de un carácter en un objeto String. Si el método encuentra el carácter, devuelve el índice de ese carácter en la cadena; en caso contrario devuelve –1. Hay dos versiones de indexOf que buscan caracteres en un objeto String. La expresión en la línea 12 utiliza la versión de indexOf que recibe una representación entera del carácter que se va a buscar. La expresión en la línea 14 utiliza otra versión del método indexOf, que recibe dos argumentos enteros: el carácter y el índice inicial en el que debe empezar la búsqueda en el objeto String.

En las líneas 19 a 24 se utiliza el método lastIndexOf para localizar la última ocurrencia de un ca-rácter en un objeto String. El método realiza la búsqueda desde el final del objeto String hacia el inicio del mismo. Si encuentra el carácter, devuelve el índice de ese carácter en el objeto String; en caso contra-rio, devuelve –1. Hay dos versiones de lastIndexOf que buscan caracteres en un objeto String. La ex-presión en la línea 20 utiliza la versión que recibe la representación entera del carácter. La expresión en la línea 22 utiliza la versión que recibe dos argumentos enteros: la representación entera del carácter y el índice a partir del cual debe iniciarse la búsqueda inversa de ese carácter.

En las líneas 27 a 40 se demuestran las versiones de los métodos indexOf y lastIndexOf que reciben, cada una de ellas, un objeto String como el primer argumento. Estas versiones de los métodos se ejecutan en forma idéntica a las descritas anteriormente, excepto que buscan secuencias de caracteres (o subcade-nas) que se especifican mediante sus argumentos String. Si se encuentra la subcadena, estos métodos devuelven el índice en el objeto String del primer carácter en la subcadena.

16.3.5 Extracción de subcadenas de las cadenasLa clase String proporciona dos métodos substring para permitir la creación de un nuevo objeto String al copiar parte de un objeto String existente. Cada método devuelve un nuevo objeto String. Ambos métodos se muestran en la figura 16.6.

La expresión letras.substring( 20 ) en la línea 12 utiliza el método substring que recibe un ar-gumento entero. Este argumento especifica el índice inicial en el objeto String original letras, a partir

1 // Fig. 16.6: SubString.java

2 // Métodos substring de la clase String.

3

4 public class SubString

5 {

6 public static void main( String[] args )

7 {

8 String letras = “abcdefghijklmabcdefghijklm”;

9

10 // prueba los métodos substring

11 System.out.printf( “La subcadena desde el índice 20 hasta el final es \“%s\”\n”,

12 letras.substring( 20 ) );

13 System.out.printf( “%s \“%s\”\n”,

14 “La subcadena desde el índice 3 hasta, pero sin incluir al 6 es”,

15 letras.substring( 3, 6 ) );

16 } // fin de main

17 } // fin de la clase SubString

La subcadena desde el índice 20 hasta el final es “hijklm”La subcadena desde el índice 3 hasta, pero sin incluir al 6 es “def”

Fig. 16.6 � Métodos substring de la clase String.

684 Capítulo 16 Cadenas, caracteres y expresiones regulares

del cual se van a copiar caracteres. La subcadena devuelta contiene una copia de los caracteres, desde el índice inicial hasta el final del objeto String. Si el índice especificado como argumento se encuentra fuera de los límites del objeto String, se genera una excepción StringIndexOutOfBoundsException.

En la línea 15 se utiliza el método substring que recibe dos argumentos enteros: el índice inicial a partir del cual se van a copiar caracteres en el objeto String original y el índice que está una posición más allá del último carácter a copiar (es decir, copiar hasta, pero sin incluir a, ese índice en el objeto String). La subcadena devuelta contiene copias de los caracteres especificados del objeto String original. Un ín-dice que se encuentre fuera de los límites de la cadena genera una excepción StringIndexOutOfBounds-Exception.

16.3.6 Concatenación de cadenasEl método concat (figura 16.7) de String concatena dos objetos String y devuelve un nuevo objeto String, el cual contiene los caracteres de ambos objetos String originales. La expresión s1.concat( s2 ) de la línea 13 forma un objeto String, anexando los caracteres de la cadena s2 a los caracteres de la cadena s1. Los objetos String originales a los que se refieren s1 y s2 no se modifican.

16.3.7 Métodos varios de StringLa clase String proporciona varios métodos que devuelven copias modificadas de objetos String, o que devuelven arreglos de caracteres. Estos métodos se muestran en la aplicación de la figura 16.8.

En la línea 16 se utiliza el método replace de String para devolver un nuevo objeto String, en el que cada ocurrencia del carácter ‘l’ en la cadena s1 se reemplaza con el carácter ‘L’. El método replace no modifica el objeto String original. Si no hay ocurrencias del primer argumento en el objeto String, el método replace devuelve el objeto String original. Una versión sobrecargada del método replace nos permite reemplazar subcadenas en vez de caracteres individuales.

Fig. 16.7 � Método concat de String.

1 // Fig. 16.7: ConcatenacionString.java

2 // Método concat de String.

3

4 public class ConcatenacionString

5 {

6 public static void main( String[] args )

7 {

8 String s1 = new String( “Feliz ” );

9 String s2 = new String( “Cumpleanios” );

10

11 System.out.printf( “s1 = %s\ns2 = %s\n\n”,s1, s2 );

12 System.out.printf(

13 “Resultado de s1.concat( s2 ) = %s\n”, s1.concat( s2 ) );

14 System.out.printf( “s1 despues de la concatenacion= %s\n”, s1 );

15 } // fin de main

16 } // fin de la clase ConcatenacionString

s1 = Felizs2 = Cumpleanios

Resultado de s1.concat( s2 ) = Feliz Cumpleanioss1 despues de la concatenacion= Feliz

16.3 La clase String 685

En la línea 19 se utiliza el método toUpperCase de String para generar un nuevo objeto String con letras mayúsculas, cuyas letras minúsculas correspondientes existen en s1. El método devuelve un nuevo objeto String que contiene el objeto String convertido y deja el objeto String original sin cambios. Si no hay caracteres qué convertir, el método toUpperCase devuelve el objeto String original.

Fig. 16.8 � Métodos replace, toLowerCase, toUpperCase, trim y toCharArray de String.

1 // Fig. 16.8: VariosString2.java

2 // Métodos replace, toLowerCase, toUpperCase, trim y toCharArray de String.

3

4 public class VariosString2

5 {

6 public static void main( String[] args )

7 {

8 String s1 = new String( “hola” );

9 String s2 = new String( “ADIOS” );

10 String s3 = new String( “ espacios ” );

11

12 System.out.printf( “s1 = %s\ns2 = %s\ns3 = %s\n\n”, s1, s2, s3 );

13

14 // prueba del método replace

15 System.out.printf(

16 “Remplazar ‘l’ con ‘L’ en s1: %s\n\n”, s1.replace( ‘l’, ‘L’ ) );

17

18 // prueba de toLowerCase y toUpperCase

19 System.out.printf( “s1.toUpperCase() = %s\n”, s1.toUpperCase() );

20 System.out.printf( “s2.toLowerCase() = %s\n\n”, s2.toLowerCase() );

21

22 // prueba del método trim

23 System.out.printf( “s3 despues de trim = \“%s\”\n\n”, s3.trim() );

24

25 // prueba del método toCharArray

26 char arregloChar[] = s1.toCharArray();

27 System.out.print( “s1 como arreglo de caracteres = ” );

28

29 for ( char caracter : arregloChar )

30 System.out.print( caracter );

31

32 System.out.println();

33 } // fin de main

34 } // fin de la clase VariosString2

s1 = holas2 = ADIOSs3 = espacios

Remplazar ‘l’ con ‘L’ en s1: hoLa

s1.toUpperCase() = HOLAs2.toLowerCase() = adios

s3 despues de trim = “espacios”

s1 como arreglo de caracteres = hola

686 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la línea 20 se utiliza el método toLowerCase de String para devolver un nuevo objeto String con letras minúsculas, cuyas letras mayúsculas correspondientes existen en s2. El objeto String original permanece sin cambios. Si no hay caracteres qué convertir en el objeto String original, toLowerCase devuelve el objeto String original.

En la línea 23 se utiliza el método trim de String para generar un nuevo objeto String que eli-mine todos los caracteres de espacio en blanco que aparecen al principio o al final del objeto String en el que opera trim. El método devuelve un nuevo objeto String que contiene el objeto String sin espa-cios en blanco a la izquierda o a la derecha. El objeto String original permanece sin cambios. Si no hay caracteres de espacios en blanco al principio y/o al final, trim devuelve el objeto String original.

En la línea 26 se utiliza el método toCharArray de String para crear un nuevo arreglo de carac-teres, el cual contiene una copia de los caracteres en la cadena s1. En las líneas 29 y 30 se imprime cada char en el arreglo.

16.3.8 Método valueOf de StringComo hemos visto, todo objeto en Java tiene un método toString que permite a un programa ob-tener la representación de cadena del objeto. Por desgracia, esta técnica no puede utilizarse con tipos primitivos, ya que éstos no tienen métodos. La clase String proporciona métodos static que reciben un argumento de cualquier tipo y lo convierten en un objeto String. En la figura 16.9 se demuestra el uso de los métodos valueOf de la clase String.

La expresión String.valueOf( arregloChar ) en la línea 18 utiliza el arreglo de caracteres arregloChar para crear un nuevo objeto String. La expresión String.valueOf( arregloChar, 3, 3 ) de la línea 20 utiliza una porción del arreglo de caracteres arregloChar para crear un nuevo objeto String. El segundo argumento especifica el índice inicial a partir del cual se van a utilizar caracteres. El tercer argumento especifica el número de caracteres a usar.

Fig. 16.9 � Métodos valueOf de la clase String (parte 1 de 2).

1 // Fig. 16.9: StringValueOf.java

2 // Métodos valueOf de String.

3

4 public class StringValueOf

5 {

6 public static void main( String[] args )

7 {

8 char[] arregloChar = { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’ };

9 boolean valorBoolean = true;

10 char valorCaracter = ‘Z’;

11 int valorEntero = 7;

12 long valorLong = 10000000000L; // el sufijo L indica long

13 float valorFloat = 2.5f; // f indica que 2.5 es un float

14 double valorDouble = 33.333; // no hay sufijo, double es el predeterminado

15 Object refObjeto = “hola”; // asigna la cadena a una referencia Object

16

17 System.out.printf(

18 “arreglo de valores char = %s\n”, String.valueOf( arregloChar ) );

19 System.out.printf( “parte del arreglo char = %s\n”,

20 String.valueOf( arregloChar, 3, 3 ) );

21 System.out.printf(

22 “boolean = %s\n”, String.valueOf( valorBoolean ) );

23 System.out.printf(

24 “char = %s\n”, String.valueOf( valorCaracter ) );

16.4 La clase StringBuilder 687

Existen otras siete versiones del método valueOf, las cuales toman argumentos de tipo boolean, char, int, long, float, double y Object, respectivamente. Estas versiones se muestran en las líneas 21 a 30. La versión de valueOf que recibe un objeto Object como argumento puede hacerlo debido a que todos los objetos Object pueden convertirse en objetos String mediante el método toString.

[Nota: en las líneas 12 y 13 se utilizan los valores literales 10000000000L y 2.5f como valores ini-ciales de la variable valorLong tipo long y de la variable valorFloat tipo float, respectivamente. De manera predeterminada, Java trata a las literales enteras como tipo int y a las literales de punto flotante como tipo double. Si se anexa la letra L a la literal 10000000000 y se anexa la letra f a la literal 2.5, se indica al compilador que 10000000000 debe tratarse como long y que 2.5 debe tratarse como float. Se puede usar una L mayúscula o una l minúscula para denotar una variable de tipo long, y una F mayúscula o una f minúscula para denotar una variable de tipo float].

16.4 La clase StringBuilder Ahora hablaremos sobre las características de la clase StringBuilder para crear y manipular infor-mación de cadenas dinámicas; es decir, cadenas que pueden modificarse. Cada objeto StringBuilder es capaz de almacenar varios caracteres especificados por su capacidad. Si se excede la capacidad de un objeto StringBuilder, ésta se expande de manera automática para dar cabida a los caracteres adicionales.

Tip de rendimiento 16.2Java puede realizar ciertas optimizaciones en las que se involucran objetos String (como hacer referencia a un objeto String desde múltiples variables), ya que sabe que estos objetos no cambiarán. Si los datos no van a cambiar, debemos usar objetos String (y no StringBuilder).

Tip de rendimiento 16.3En los programas que realizan la concatenación de cadenas con frecuencia, o en otras modificaciones de cadenas, a menudo es más eficiente implementar las modificaciones con la clase StringBuilder.

25 System.out.printf( “int = %s\n”, String.valueOf( valorEntero ) );

26 System.out.printf( “long = %s\n”, String.valueOf( valorLong ) );

27 System.out.printf( “float = %s\n”, String.valueOf( valorFloat ) );

28 System.out.printf(

29 “double = %s\n”, String.valueOf( valorDouble ) );

30 System.out.printf( “Object = %s\n”, String.valueOf( refObjeto ) );

31 } // fin de main

32 } // fin de la clase StringValueOf

arreglo de valores char = abcdefparte del arreglo char = defboolean = truechar = Zint = 7long = 10000000000float = 2.5double = 33.333Object = hola

Fig. 16.9 � Métodos valueOf de la clase String (parte 2 de 2).

688 Capítulo 16 Cadenas, caracteres y expresiones regulares

Observación de ingeniería de software 16.2Los objetos StringBuilder no son seguros para usarse en hilos. Si varios hilos requieren acceso a la misma información de una cadena dinámica, use la clase StringBuffer en su código. Las clases StringBuilder y StringBuffer son idénticas, pero la clase StringBuffer es segura para los hilos.

16.4.1 Constructores de StringBuilderLa clase StringBuilder proporciona cuatro constructores. En la figura 16.10 mostramos tres de ellos. En la línea 8 se utiliza el constructor de StringBuilder sin argumentos para crear un objeto StringBuil-der que no contiene caracteres, y tiene una capacidad inicial de 16 caracteres (el valor predeterminado para un objeto StringBuilder). En la línea 9 se utiliza el constructor de StringBuilder que recibe un argumento entero para crear un objeto StringBuilder que no contiene caracteres, y su capacidad inicial se especifica mediante el argumento entero (es decir, 10). En la línea 10 se utiliza el constructor de StringBuilder que recibe un argumento String para crear un objeto StringBuilder que contiene los caracteres en el argumento String. La capacidad inicial es el número de caracteres en el argumento String, más 16.

Las instrucciones en las líneas 12 a 14 utilizan el método toString de la clase StringBuilder para imprimir los objetos StringBuilder con el método printf. En la sección 16.4.4, hablaremos acerca de cómo Java usa los objetos StringBuilder para implementar los operadores + y += para la concatenación de cadenas.

16.4.2 Métodos length, capacity, setLength y ensureCapacity de StringBuilder

La clase StringBuilder proporciona los métodos length y capacity para devolver el número actual de caracteres en un objeto StringBuilder y el número de caracteres que pueden almacenarse en un objeto StringBuilder sin necesidad de asignar más memoria, respectivamente. El método ensureCapa-city permite al programador garantizar que un StringBuilder tenga, cuando menos, la capacidad

Fig. 16.10 � Constructores de la clase StringBuilder.

1 // Fig. 16.10: ConstructoresStringBuilder.java

2 // Constructores de StringBuilder.

3

4 public class ConstructoresStringBuilder

5 {

6 public static void main( String[] args )

7 {

8 StringBuilder bufer1 = new StringBuilder();

9 StringBuilder bufer2 = new StringBuilder( 10 );

10 StringBuilder bufer3 = new StringBuilder( “hola” );

11

12 System.out.printf( “bufer1 = \“%s\”\n”, bufer1.toString() );

13 System.out.printf( “bufer2 = \“%s\”\n”, bufer2.toString() );

14 System.out.printf( “bufer3 = \“%s\”\n”, bufer3.toString() );

15 } // fin de main

16 } // fin de la clase ConstructoresStringBuilder

bufer1 = “”bufer2 = “”bufer3 = “hola”

16.4 La clase StringBuilder 689

especificada. El método setLength incrementa o decrementa la longitud de un objeto StringBuilder. En la figura 16.11 se muestra el uso de estos métodos.

1 // Fig. 16.11: StringBuilderCapLen.java

2 // Métodos length, setLength, capacity y ensureCapacity de StringBuilder.

3

4 public class StringBuilderCapLen

5 {

6 public static void main( String[] args )

7 {

8 StringBuilder bufer = new StringBuilder( “Hola, como estas?” );

9

10 System.out.printf( “bufer = %s\nlongitud = %d\ncapacidad = %d\n\n”,

11 bufer.toString(), bufer.length(), bufer.capacity() );

12

13 bufer.ensureCapacity( 75 );

14 System.out.printf( “Nueva capacidad = %d\n\n”, bufer.capacity() );

15

16 bufer.setLength( 10 );

17 System.out.printf( “Nueva longitud = %d\nbufer = %s\n”,

18 bufer.length(), bufer.toString() );

19 } // fin de main

20 } // fin de la clase StringBuilderCapLen

bufer = Hola, como estas?longitud = 17capacidad = 33

Nueva capacidad = 75

Nueva longitud = 10bufer = Hola, como

La aplicación contiene un objeto StringBuilder llamado bufer. En la línea 8 se utiliza el cons-tructor de StringBuilder que toma un argumento String para inicializar el objeto StringBuilder con la cadena “Hola, como estas?”. En las líneas 10 y 11 se imprime el contenido, la longitud y la capaci-dad del objeto StringBuilder. Observe en la ventana de salida que la capacidad inicial del obje-to StringBuilder es de 35. Recuerde que el constructor de StringBuilder que recibe un argumento String inicializa la capacidad con la longitud de la cadena que se le pasa como argumento, más 16.

En la línea 13 se utiliza el método ensureCapacity para expandir la capacidad del objeto String-Builder a un mínimo de 75 caracteres. En realidad, si la capacidad original es menor que el argumento, este método asegura una capacidad que sea el valor mayor entre el número especificado como argumen-to, y el doble de la capacidad original más 2. La capacidad del objeto StringBuilder permanece sin cambios si es mayor que la capacidad especificada.

Tip de rendimiento 16.4El proceso de incrementar en forma dinámica la capacidad de un objeto StringBuilder puede requerir una cantidad considerable de tiempo. La ejecución de un gran número de estas operaciones puede degradar el rendimiento de una aplicación. Si un objeto StringBuilder va a aumentar su tamaño en forma considerable, tal vez varias veces, al establecer su capacidad a un nivel alto en un principio se incrementará el rendimiento.

Fig. 16.11 � Métodos length, setLength, capacity y ensureCapacity de StringBuilder.

690 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la línea 16 se utiliza el método setLength para establecer la longitud del objeto StringBuilder en 10. Si la longitud especificada es menor que el número actual de caracteres en el objeto StringBuilder, el búfer se trunca a la longitud especificada (es decir, los caracteres en el objeto StringBuilder después de la longitud especificada se descartan). Si la longitud especificada es mayor que el número de caracteres actualmente en el objeto StringBuilder, se anexan caracteres null (caracteres con la representación numérica de 0) hasta que el número total de caracteres en el objeto StringBuilder sea igual a la longitud especificada.

16.4.3 Métodos charAt, setCharAt, getChars y reverse de StringBuilder

La clase StringBuilder proporciona los métodos charAt, setCharAt, getChars y reverse para mani-pular los caracteres en un objeto StringBuilder (figura 16.12). El método charAt (línea 12) recibe un argumento entero y devuelve el carácter que se encuentre en el objeto StringBuilder en ese índice. El método getChars (línea 15) copia los caracteres de un objeto StringBuilder al arreglo de caracteres que recibe como argumento. Este método recibe cuatro argumentos: el índice inicial a partir del cual deben copiarse caracteres en el objeto StringBuilder, el índice una posición más allá del último carácter a co-piar del objeto StringBuilder, el arreglo de caracteres en el que se van a copiar los caracteres y la posición inicial en el arreglo de caracteres en donde debe colocarse el primer carácter. El método setCharAt (líneas 21 y 22) recibe un entero y un carácter como argumentos y asigna el carácter en la posición especificada en el objeto StringBuilder al carácter que recibe como argumento. El método reverse (línea 25) in-vierte el contenido del objeto StringBuilder.

Error común de programación 16.3Al tratar de acceder a un carácter que se encuentra fuera de los límites de un objeto StringBuilder (es decir, con un índice menor que 0, o mayor o igual que la longitud del objeto StringBuilder) se produce una excepción StringIndexOutOfBoundsException.

Fig. 16.12 � Métodos charAt, setCharAt, getChars y reverse de StringBuilder (parte 1 de 2).

1 // Fig. 16.12: StringBuilderChars.java

2 // Métodos charAt, setCharAt, getChars y reverse de StringBuilder.

3

4 public class StringBuilderChars

5 {

6 public static void main( String[] args )

7 {

8 StringBuilder bufer = new StringBuilder( “hola a todos” );

9

10 System.out.printf( "bufer = %s\n", bufer.toString() );

11 System.out.printf( "Caracter en 0: %s\nCaracter en 3: %s\n\n",

12 bufer.charAt( 0 ), bufer.charAt( 3 ) );

13

14 char[] arregloChars = new char[ bufer.length() ];

15 bufer.getChars( 0, bufer.length(), arregloChars, 0 );

16 System.out.print( “Los caracteres son: ” );

17

18 for ( char character : arregloChars )

19 System.out.print( character );

16.4 La clase StringBuilder 691

16.4.4 Métodos append de StringBuilderLa clase StringBuilder proporciona métodos append sobrecargados (figura 16.13) para permitir que se agreguen valores de diversos tipos al final de un objeto StringBuilder. Se proporcionan versiones para cada uno de los tipos primitivos y para arreglos de caracteres, objetos String, Object y demás (recuerde que el método toString produce una representación de cadena de cualquier objeto Object). Cada mé-todo recibe su argumento, lo convierte en una cadena y lo anexa al objeto StringBuilder.

Fig. 16.13 � Métodos append de la clase StringBuilder (parte 1 de 2).

Fig. 16.12 � Métodos charAt, setCharAt, getChars y reverse de StringBuilder (parte 2 de 2).

1 // Fig. 16.13: StringBuilderAppend.java

2 // Métodos append de StringBuilder.

3

4 public class StringBuilderAppend

5 {

6 public static void main( String[] args )

7 {

8 Object refObjeto = “hola”;

9 String cadena = “adios”;

10 char[] arregloChar = { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’ };

11 boolean valorBoolean = true;

12 char valorChar = ‘Z’;

13 int valorInt = 7;

14 long valorLong = 10000000000L;

15 float valorFloat = 2.5f; //

16 double valorDouble = 33.333;

17

18 StringBuilder ultimoBufer = new StringBuilder( “ultimo bufer” );

19 StringBuilder bufer = new StringBuilder();

20

21 bufer.append( refObjeto );

20

21 bufer.setCharAt( 0, ‘H’ );

22 bufer.setCharAt( 6, ‘T’ );

23 System.out.printf( “\n\nbufer = %s”, bufer.toString() );

24

25 bufer.reverse();

26 System.out.printf( “\n\nbufer = %s\n”, bufer.toString() );

27 } // fin de main

28 } // fin de la clase StringBuilderChars

bufer = hola a todosCaracter en 0: hCaracter en 3: a

Los caracteres son: hola a todos

bufer = Hola a Todos

bufer = sodoT a aloH

692 Capítulo 16 Cadenas, caracteres y expresiones regulares

En realidad, el compilador utiliza los objetos StringBuilder (o StringBuffer) y los métodos ap-pend para implementar los operadores + y += para concatenar objetos String. Por ejemplo, suponga que se realizan las siguientes declaraciones:

String cadena1 = “hola”;String cadena2 = “BC”;int valor = 22;

la instrucción

String s = cadena1 + cadena2 + valor;

concatena a “hola”, “BC” y 22. La concatenación se puede realizar de la siguiente manera:

String s = new StringBuilder().append( “hola” ).append( “BC” ). append( 22 ).toString();

Fig. 16.13 � Métodos append de la clase StringBuilder (parte 2 de 2).

22 bufer.append( “\n” );

23 bufer.append( cadena );

24 bufer.append( “\n” );

25 bufer.append( arregloChar );

26 bufer.append( “\n” );

27 bufer.append( arregloChar, 0, 3 );

28 bufer.append( “\n” );

29 bufer.append( valorBoolean );

30 bufer.append( “\n” );

31 bufer.append( valorChar );

32 bufer.append( “\n” );

33 bufer.append( valorInt );

34 bufer.append( “\n” );

35 bufer.append( valorLong );

36 bufer.append( “\n” );

37 bufer.append( valorFloat );

38 bufer.append( “\n” );

39 bufer.append( valorDouble );

40 bufer.append( “\n” );

41 bufer.append( ultimoBufer );

42

43 System.out.printf( “bufer contiene %s\n”, bufer.toString() );

44 } // fin de main

45 } // fin de StringBuilderAppend

bufer contiene holaadiosabcdefabctrueZ7100000000002.533.333ultimo bufer

16.4 La clase StringBuilder 693

Primero, la instrucción anterior crea un objeto StringBuilder vacío y después le anexa las cadenas “hola”, “BC” y el entero 22. A continuación, el método toString de StringBuilder convierte el objeto StringBuilder en un objeto String que se asigna al objeto String s. La instrucción

s += “!”;

se ejecuta de la siguiente manera (esto puede variar de un compilador a otro):

s = new StringBuilder().append( s ).append( “!” ).toString();

Primero se crea un objeto StringBuilder vacío y después se le anexa el contenido actual de s, segui-do por “!”. A continuación, el método toString de StringBuilder (que debe llamarse de manera ex-plícita aquí) devuelve el contenido del objeto StringBuilder como un objeto String, y el resultado se asigna a s.

16.4.5 Métodos de inserción y eliminación de StringBuilderLa clase StringBuilder proporciona métodos insert sobrecargados para permitir que se inserten va-lores de diversos tipos en cualquier posición de un objeto StringBuilder. Se proporcionan versiones para cada uno de los tipos primitivos, y para arreglos de caracteres, objetos String, Object y CharSe-quence. Cada uno de los métodos toma su segundo argumento y lo inserta en el índice especificado por el primer argumento. Si el primer argumento es menor que 0 o mayor que la longitud del objeto StringBuilder, ocurre una excepción StringIndexOutOfBoundsException. La clase StringBuilder también proporciona métodos delete y deleteCharAt para eliminar caracteres en cualquier posición de un objeto StringBuilder. El método delete recibe dos argumentos: el índice inicial y el índice que se encuentra una posición más allá del último de los caracteres que se van a eliminar. Se eliminan todos los caracteres que empiezan en el índice inicial hasta, pero sin incluir al índice final. El método delete-CharAt recibe un argumento: el índice del carácter a eliminar. El uso de índices inválidos hace que am-bos métodos lancen una excepción StringIndexOutOfBoundsException. En la figura 16.14 se mues-tran los métodos insert, delete y deleteCharAt.

Fig. 16.14 � Métodos insert, delete y deleteCharAt de StringBuilder (parte 1 de 2).

1 // Fig. 16.14: StringBuilderInsertDelete.java 2 // Métodos insert, delete y deleteCharAt de StringBuilder 3 4 public class StringBuilderInsertDelete 5 { 6 public static void main( String[] args[] ) 7 { 8 Object refObjeto = "hola"; 9 String cadena = "adios"; 10 char[] arregloChars = { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’ };11 boolean valorBoolean = true;12 char valorChar = ‘K’;13 int valorInt = 7;14 long valorLong = 10000000;15 float valorFloat = 2.5f; // el sufijo f indica que 2.5 es un float16 double valorDouble = 33.333;1718 StringBuilder bufer = new StringBuilder();1920 bufer.insert( 0, refObjeto );21 bufer.insert( 0, “ ” ); // cada uno de estos contiene dos espacios22 bufer.insert( 0, cadena );

694 Capítulo 16 Cadenas, caracteres y expresiones regulares

16.5 La clase CharacterJava proporciona ocho clases de envoltura de tipos: Boolean, Character, Double, Float, Byte, Short, Integer y Long, las cuales permiten que los valores de tipo primitivo se traten como objetos. En esta sección presentaremos la clase Character: la clase de envoltura de tipos para el tipo primitivo char.

La mayoría de los métodos de la clase Character son static, diseñados para facilitar el procesa-miento de valores char individuales. Estos métodos reciben cuando menos un argumento tipo carácter y realizan una prueba o manipulación del carácter. Esta clase también contiene un constructor que recibe un argumento char para inicializar un objeto Character. En los siguientes tres ejemplos presen-taremos la mayor parte de los métodos de la clase Character. Para obtener más información sobre esta clase (y las demás clases de envoltura de tipos), consulte el paquete java.lang en la documentación de la API de Java.

En la figura 16.15 se muestran algunos métodos static que prueban caracteres para determinar si son de un tipo de carácter específico y los métodos static que realizan conversiones de caracteres de minúscula a mayúscula, y viceversa. Puede introducir cualquier carácter y aplicar estos métodos a ese carácter.

Fig. 16.14 � Métodos insert, delete y deleteCharAt de StringBuilder (parte 2 de 2).

23 bufer.insert( 0, “ ” );

24 bufer.insert( 0, arregloChars );

25 bufer.insert( 0, “ ” );

26 bufer.insert( 0, arregloChars, 3, 3 );

27 bufer.insert( 0, “ ” );

28 bufer.insert( 0, valorBoolean );

29 bufer.insert( 0, “ ” );

30 bufer.insert( 0, valorChar );

31 bufer.insert( 0, “ ” );

32 bufer.insert( 0, valorInt );

33 bufer.insert( 0, “ ” );

34 bufer.insert( 0, valorLong );

35 bufer.insert( 0, “ ” );

36 bufer.insert( 0, valorFloat );

37 bufer.insert( 0, “ ” );

38 bufer.insert( 0, valorDouble );

39

40 System.out.printf(

41 “bufer despues de insertar:\n%s\n\n”, bufer.toString() );

42

43 bufer.deleteCharAt( 10 ); // elimina el 5 en 2.5

44 bufer.delete( 2, 6 ); // elimina el .333 en 33.333

45

46 System.out.printf(

47 “bufer despues de eliminar:\n%s\n”, bufer.toString() );

48 } // fin de main

49 } // fin de la clase StringBuilderInsertDelete

bufer despues de insertar:33.333 2.5 10000000 7 K true def abcdef adios hola

bufer despues de eliminar:33 2. 10000000 7 K true def abcdef adios hola

16.5 La clase Character 695

En la línea 15 se utiliza el método isDefined de Character para determinar si el carácter c está definido en el conjunto de caracteres Unicode. De ser así, el método devuelve true; en caso con-trario, devuelve false. En la línea 16 se utiliza el método isDigit de Character para determinar si el carácter c es un dígito definido en Unicode. De ser así el método devuelve true y, en caso contrario devuelve false.

Fig. 16.15 � Métodos static de Character para probar caracteres y convertir de mayúsculas a minúsculas, y viceversa (parte 1 de 2).

1 // Fig. 16.15: MetodosStaticChar.java

2 // Los métodos static de Character para probar caracteres y conversión de mayúsculas/minúsculas.

3 import java.util.Scanner; 4 5 public class MetodosStaticChar 6 { 7 public static void main( String[] args ) 8 { 9 Scanner scanner = new Scanner( System.in ); // crea objeto scanner10 System.out.println( “Escriba un caracter y oprima Intro” );11 String entrada = scanner.next(); 12 char c = entrada.charAt( 0 ); // obtiene el caracter de entrada1314 // muestra información sobre los caracteres15 System.out.printf( “esta definido: %b\n”, Character.isDefined( c ) );16 System.out.printf( “es digito: %b\n”, Character.isDigit( c ) );17 System.out.printf( “es el primer caracter en un identificador de Java: %b\n”,18 Character.isJavaIdentifierStart( c ) );19 System.out.printf( “es parte de un identificador de Java: %b\n”,20 Character.isJavaIdentifierPart( c ) );21 System.out.printf( “es letra: %b\n”, Character.isLetter( c ) );22 System.out.printf( 23 “es letra o digito: %b\n”, Character.isLetterOrDigit( c ) );24 System.out.printf(25 “es minuscula: %b\n”, Character.isLowerCase( c ) );26 System.out.printf( 27 “es mayuscula: %b\n”, Character.isUpperCase( c ) );28 System.out.printf( 29 “a mayuscula: %s\n”, Character.toUpperCase( c ) );30 System.out.printf( 31 “a minuscula: %s\n”, Character.toLowerCase( c ) );32 } // fin de main33 } // fin de la clase MetodosStaticChar

Escriba un caracter y oprima IntroAesta definido: truees digito: falsees el primer caracter en un identificador de Java: truees parte de un identificador de Java: truees letra: truees letra o digito: truees minuscula: falsees mayuscula: truea mayuscula: Aa minuscula: a

696 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la línea 18 se utiliza el método isJavaIdentifierStart de Character para determinar si c es un carácter que puede ser el primer carácter de un identificador en Java; es decir, una letra, un guión bajo (_) o un signo de dólares ($). De ser así, el método devuelve true; en caso contrario devuelve false. En la línea 20 se utiliza el método isJavaIdentifierPart de Character para determinar si el carácter c puede utilizarse en un identificador en Java; es decir, un dígito, una letra, un guión bajo (_) o un signo de dóla-res ($). De ser así, el método devuelve true; en caso contrario devuelve false.

En la línea 21 se utiliza el método isLetter de Character para determinar si el carácter c es una letra. Si es así, el método devuelve true; en caso contrario devuelve false. En la línea 23 se utiliza el método isLetterOrDigit de Character para determinar si el carácter c es una letra o un dígito. Si es así, el método devuelve true; en caso contrario devuelve false.

En la línea 25 se utiliza el método isLowerCase de Character para determinar si el carácter c es una letra minúscula. Si es así, el método devuelve true; en caso contrario devuelve false. En la línea 27 se utiliza el método isUpperCase de Character para determinar si el carácter c es una letra mayúscula. Si es así, el método devuelve true; en caso contrario devuelve false.

En la línea 29 se utiliza el método toUpperCase de Character para convertir el carácter c en su letra mayúscula equivalente. El método devuelve el carácter convertido si éste tiene un equivalente en mayúscula; en caso contrario, el método devuelve su argumento original. En la línea 31 se utiliza el método toLowerCase de Character para convertir el carácter c en su letra minúscula equivalente. El método devuelve el carácter convertido si éste tiene un equivalente en minúscula; en caso contrario, el método devuelve su argumento original.

En la figura 16.16 se muestran los métodos static digit y forDigit de Character, los cuales convierten caracteres a dígitos y dígitos a caracteres, respectivamente, en distintos sistemas numéricos.

Fig. 16.15 � Métodos static de Character para probar caracteres y convertir de mayúsculas a minúsculas, y viceversa (parte 2 de 2).

Escriba un caracter y oprima Intro8esta definido: truees digito: truees el primer caracter en un identificador de Java: falsees parte de un identificador de Java: truees letra: falsees letra o digito: truees minuscula: falsees mayuscula: falsea mayuscula: 8a minuscula: 8

Escriba un caracter y oprima Intro$esta definido: truees digito: falsees el primer caracter en un identificador de Java: truees parte de un identificador de Java: truees letra: falsees letra o digito: falsees minuscula: falsees mayuscula: falsea mayuscula: $a minúscula: $

16.5 La clase Character 697

Los sistemas numéricos comunes son el decimal (base 10), octal (base 8), hexadecimal (base 16) y binario (base 2). La base de un número se conoce también como su raíz. Para obtener más información sobre las conversiones entre sistemas numéricos, vea el apéndice H.

En la línea 28 se utiliza el método forDigit para convertir el entero digito en un carácter del sistema numérico especificado por el entero raiz (la base del número). Por ejemplo, el entero decimal 13 en base 16 (la raiz) tiene el valor de carácter ‘d’. Las letras en minúsculas y mayúsculas representan el mismo valor en los sistemas numéricos. En la línea 35 se utiliza el método digit para convertir la variable caracter en un entero del sistema numérico especificado por el entero raiz (la base del número). Por ejemplo, el carácter ‘A’ es la representación en base 16 (la raiz) del valor 10 en base 10. La raíz debe estar entre 2 y 36, inclusive.

Fig. 16.16 � Métodos de conversión static de la clase Character (parte 1 de 2).

1 // Fig. 16.16: MetodosStaticChar2.java

2 // Métodos de conversión estáticos de la clase Character.

3 import java.util.Scanner;

4

5 public class MetodosStaticChar2

6 {

7 // ejecuta la aplicación

8 public static void main( String[] args )

9 {

10 Scanner scanner = new Scanner( System.in );

11

12 // obtiene la raíz

13 System.out.println( “Escriba una raiz:” );

14 int raiz = scanner.nextInt();

15

16 // obtiene la selección del usuario

17 System.out.printf( “Seleccione una opcion:\n1 -- %s\n2 -- %s\n”,

18 “Convertir digito a caracter”, “Convertir caracter a digito” );

19 int opcion = scanner.nextInt();

20

21 // procesa la petición

22 switch ( opcion )

23 {

24 case 1: // convierte dígito a carácter

25 System.out.println( “Escriba un digito:” );

26 int digito = scanner.nextInt();

27 System.out.printf( “Convertir digito a caracter: %s\n”,

28 Character.forDigit( digito, raiz ) );

29 break;

30

31 case 2: // convierte carácter a dígito

32 System.out.println( “Escriba un caracter:” );

33 char caracter = scanner.next().charAt( 0 );

34 System.out.printf( “Convertir caracter a digito: %s\n”,

35 Character.digit( caracter, raiz ) );

36 break;

37 } // fin de switch

38 } // fin de main

39 } // fin de la clase MetodosStaticChar2

698 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la figura 16.17 se muestra el constructor y varios métodos no static de la clase Character: charValue, toString y equals. En las líneas 7 y 8 se instancian dos objetos Character al asignar las constantes de caracteres 'A' y 'a', respectivamente, a las variables Character. Java convierte de manera automática estas literales char en objetos Character: un proceso conocido como autoboxing, que veremos con más detalle en la sección 20.4. En la línea 11 se utiliza el método charValue de Character para devolver el valor char almacenado en el objeto Character llamado c1. En la línea 11 se devuelve la re-presentación de cadena del objeto Character llamado c2, utilizando el método toString. La condición en la línea 13 utiliza el método equals para determinar si el objeto c1 tiene el mismo contenido que el objeto c2 (es decir, si los caracteres dentro de cada objeto son iguales).

Fig. 16.16 � Métodos de conversión static de la clase Character (parte 2 de 2).

Escriba una raiz:16Seleccione una opcion:1 -- Convertir digito a caracter2 -- Convertir caracter a digito2Escriba un caracter:AConvertir caracter a digito: 10

Escriba una raiz:16Seleccione una opcion:1 -- Convertir digito a caracter2 -- Convertir caracter a digito1Escriba un digito:13Convertir digito a caracter: d

Fig. 16.17 � Métodos no static de la clase Character (parte 1 de 2).

1 // Fig. 16.17: OtrosMetodosChar.java

2 // Métodos no static de la clase Character.

3 public class OtrosMetodosChar

4 {

5 public static void main( String[] args )

6 {

7 Character c1 = ‘A’;

8 Character c2 = ‘a’;

9

10 System.out.printf(

11 “c1 = %s\nc2 = %s\n\n”, c1.charValue(), c2.toString() );

12

13 if ( c1.equals( c2 ) )

14 System.out.println( “c1 y c2 son iguales\n” );

15 else

16 System.out.println( “c1 y c2 no son iguales\n” );

17 } // fin de main

18 } // fin de la clase OtrosMetodosChar

16.6 División de objetos String en tokens 699

16.6 División de objetos String en tokensCuando usted lee una oración, su mente la divide en tokens (palabras individuales y signos de pun-tuación, cada uno de los cuales transfiere a usted su significado). Los compiladores también dividen objetos en tokens. Descomponen instrucciones en piezas individuales tales como palabras clave, iden-tificadores, operadores y demás elementos de un lenguaje de programación. Ahora estudiaremos el método split de la clase String, que descompone un objeto String en los tokens que lo componen. Los tokens se separan unos de otros mediante delimitadores, que por lo general son caracteres de es-pacio en blanco tales como los espacios, tabuladores, nuevas líneas y retornos de carro. También pueden utilizarse otros caracteres como delimitadores para separar tokens. La aplicación de la figura 16.18 muestra el uso del método split de String.

Cuando el usuario oprime Intro, el enunciado de entrada se almacena en la variable enunciado. La línea 17 invoca el método split de String con el argumento “ ”, que devuelve un arreglo de obje-tos String. El carácter de espacio en el argumento String es el delimitador que el método split utiliza para localizar los tokens en el objeto String. Como veremos en la siguiente sección, el argumento para el método split puede ser una expresión regular para realizar descomposiciones en tokens más com-plejas. En la línea 19 se muestra en pantalla la longitud del arreglo tokens; es decir, el número de tokens en enunciado. En las líneas 21 y 22 se imprime cada token en una línea separada.

Fig. 16.17 � Métodos no static de la clase Character (parte 2 de 2).

c1 = Ac2 = a

c1 y c2 no son iguales

Fig. 16.18 � Objeto StringTokenizer utilizado para descomponer cadenas en tokens (parte 1 de 2).

1 // Fig. 16.18: PruebaToken.java

2 // Uso de un objeto StringTokenizer para descomponer objetos String en tokens.

3 import java.util.Scanner;

4 import java.util.StringTokenizer;

5

6 public class PruebaToken

7 {

8 // ejecuta la aplicación

9 public static void main( String[] args )

10 {

11 // obtiene el enunciado

12 Scanner scanner = new Scanner( System.in );

13 System.out.println( “Escriba un enunciado y oprima Intro” );

14 String enunciado = scanner.nextLine();

15

16 // procesa el enunciado del usuario

17 String[] tokens = enunciado.split( “ ” );

18 System.out.printf( “Numero de elementos: %d\nLos tokens son:\n”,

19 tokens.length );

20

21 for ( String token : tokens )

22 System.out.println( token );

23 } // fin de main

24 } // fin de la clase PruebaToken

700 Capítulo 16 Cadenas, caracteres y expresiones regulares

16.7 Expresiones regulares, la clase Pattern y la clase Matcher

Una expresión regular es un objeto String que describe un patrón de búsqueda para relacionar ca-racteres en otros objetos String. Dichas expresiones son útiles para validar la entrada y asegurar que los datos estén en un formato específico. Por ejemplo, un código postal debe consistir de cinco dí-gitos, y un apellido sólo debe contener letras, espacios, apóstrofes y guiones cortos. Una aplicación de las expresiones regulares es facilitar la construcción de un compilador. A menudo se utiliza una expresión regular larga y compleja para validar la sintaxis de un programa. Si el código del pro-grama no coincide con la expresión regular, el compilador sabe que hay un error de sintaxis dentro del código.

La clase String proporciona varios métodos para realizar operaciones con expresiones regulares, siendo la más simple la operación de concordancia. El método matches de la clase String recibe un objeto String que especifica la expresión regular, e iguala el contenido del objeto String que lo llama con la expresión regular. Este método devuelve un valor de tipo boolean, indicando si hubo concor-dancia o no.

Una expresión regular consiste de caracteres literales y símbolos especiales. La figura 16.19 es-pecifica algunas clases predefinidas de caracteres que pueden usarse con las expresiones regulares. Una clase de carácter es una secuencia de escape que representa a un grupo de caracteres. Un dígito es cualquier carácter numérico. Un carácter de palabra es cualquier letra (mayúscula o minúscula), cualquier dígito o el carácter de guión bajo. Un carácter de espacio en blanco es un espacio, tabulador, retorno de carro, nueva línea o avance de página. Cada clase de carácter se iguala con un solo carácter en el objeto String que intentamos hacer concordar con la expresión regular.

Las expresiones regulares no están limitadas a esas clases predefinidas de caracteres. Las expresiones utilizan varios operadores y otras formas de notación para igualar patrones complejos. Analizaremos va-

Fig. 16.18 � Objeto StringTokenizer utilizado para descomponer cadenas en tokens (parte 2 de 2).

Escriba un enunciado y oprima IntroEste es un enunciado con siete tokensNumero de elementos: 7Los tokens son:Esteesunenunciadoconsietetokens

Carácter Concuerda con Carácter Concuerda con

\d cualquier dígito \D cualquier carácter que no sea dígito

\w cualquier carácter de palabra \W cualquier carácter que no sea de palabra

\s cualquier espacio en blanco \S cualquier carácter que no sea de espacio en blanco

Fig. 16.19 � Clases predefinidas de caracteres.

16.7 Expresiones regulares, la clase Pattern y la clase Matcher 701

rias de estas técnicas en la aplicación de las figuras 16.20 y 16.21, la cual valida la entrada del usuario mediante expresiones regulares. [Nota: esta aplicación no está diseñada para igualar todos los posibles datos de entrada del usuario].

Fig. 16.20 � Validación de la información del usuario mediante expresiones regulares.

1 // Fig. 16.20: ValidacionEntrada.java

2 // Valida la información del usuario mediante expresiones regulares.

3 4 public class ValidacionEntrada

5 {

6 // valida el primer nombre

7 public static boolean validarPrimerNombre( String primerNombre )

8 {

9 return primerNombre.matches( “[A-Z][a-zA-Z]*” );

10 } // fin del método validarPrimerNombre

1112 // valida el apellido

13 public static boolean validarApellidoPaterno( String apellidoPaterno )

14 {

15 return apellidoPaterno.matches( “[a-zA-z]+([ '-][a-zA-Z]+)*” );

16 } // fin del método validarApellidoPaterno

1718 // valida la dirección

19 public static boolean validarDireccion( String direccion )

20 {

21 return direccion.matches(

22 “\\d+\\s+([a-zA-Z]+|[a-zA-Z]+\\s[a-zA-Z]+)” );

23 } // fin del método validarDireccion

2425 // valida la ciudad

26 public static boolean validarCiudad( String ciudad )

27 {

28 return ciudad.matches( “([a-zA-Z]+|[a-zA-Z]+\\s[a-zA-Z]+)” );

29 } // fin del método validarCiudad

3031 // valida el estado

32 public static boolean validarEstado( String estado )

33 {

34 return estado.matches( “([a-zA-Z]+|[a-zA-Z]+\\s[a-zA-Z]+)” ) ;

35 } // fin del método validarEstado

3637 // valida el código postal

38 public static boolean validarCP( String cp )

39 {

40 return cp.matches( “\\d{5}” );

41 } // fin del método validarCP

4243 // valida el teléfono

44 public static boolean validarTelefono( String telefono )

45 {

46 return telefono.matches( “[1-9]\\d{2}-[1-9]\\d{2}-\\d{4}” );

47 } // fin del método validarTelefono

48 } // fin de la clase ValidacionEntrada

702 Capítulo 16 Cadenas, caracteres y expresiones regulares

Fig. 16.21 � Recibe datos del usuario y los valida mediante la clase ValidacionEntrada (parte 1 de 2).

1 // Fig. 16.21: Validacion.java

2 // Valida la información del usuario mediante expresiones regulares.

3 import java.util.Scanner;

4 5 public class Validacion

6 {

7 public static void main( String[] args )

8 {

9 // obtiene la entrada del usuario

10 Scanner scanner = new Scanner( System.in );

11 System.out.println( “Escriba el primer nombre:” );

12 String primerNombre = scanner.nextLine();

13 System.out.println( “Escriba el apellido paterno:” );

14 String apellidoPaterno = scanner.nextLine();

15 System.out.println( “Escriba la direccion:” );

16 String direccion = scanner.nextLine();

17 System.out.println( “Escriba la ciudad:” );

18 String ciudad = scanner.nextLine();

19 System.out.println( “Escriba el estado:” );

20 String estado = scanner.nextLine();

21 System.out.println( “Escriba el codigo postal:” );

22 String cp = scanner.nextLine();

23 System.out.println( “Escriba el telefono:” );

24 String telefono = scanner.nextLine();

2526 // valida la entrada del usuario y muestra mensaje de error

27 System.out.println( “\nValidar resultado:” );

2829 if ( !ValidacionEntrada.validarPrimerNombre( primerNombre ) )

30 System.out.println( “Primer nombre invalido” );

31 else if ( !ValidacionEntrada.validarApellidoPaterno( apellidoPaterno ) )

32 System.out.println( “Apellido paterno invalido” );

33 else if ( !ValidacionEntrada.validarDireccion( direccion ) )

34 System.out.println( “Direccion invalida” );

35 else if ( !ValidacionEntrada.validarCiudad( ciudad ) )

36 System.out.println( “Ciudad invalida” );

37 else if ( !ValidacionEntrada.validarEstado( estado ) )

38 System.out.println( “Estado invalido” );

39 else if ( !ValidacionEntrada.validarCP( cp ) )

40 System.out.println( “Codigo postal invalido” );

41 else if ( !ValidacionEntrada.validarTelefono( telefono ) )

42 System.out.println( “Numero telefonico invalido” );

43 else

44 System.out.println( “La entrada es valida. Gracias.” );

45 } // fin de main

46 } // fin de la clase Validacion

Escriba el primer nombre:JaneEscriba el apellido paterno:DoeEscriba la direccion:123 Cierta Calle

16.7 Expresiones regulares, la clase Pattern y la clase Matcher 703

En la figura 16.20 se valida la entrada del usuario. En la línea 9 se valida el nombre. Para hacer que concuerde un conjunto de caracteres que no tiene una clase predefinida de carácter, utilice los corche-tes ([ ]). Por ejemplo, el patrón “[aeiou]” puede utilizarse para concordar con una sola vocal. Los ran-gos de caracteres pueden representarse colocando un guión corto (-) entre dos caracteres. En el ejemplo, “[A-Z]” concuerda con una sola letra mayúscula. Si el primer carácter entre corchetes es “^”, la expresión acepta cualquier carácter distinto a los que se indiquen. Sin embargo, es importante observar que “[^Z]” no es lo mismo que “[A-Y]”, la cual concuerda con las letras mayúsculas A-Y; “[^Z]” concuerda con cualquier carácter distinto de la letra Z mayúscula, incluyendo las letras minúsculas y los caracteres que no son letras, como el carácter de nueva línea. Los rangos en las clases de caracteres se determinan mediante los valores enteros de las letras. En este ejemplo, “[A-Za-z]” concuerda con todas las letras mayúsculas y minúsculas. El rango “[A-z]” concuerda con todas las letras y también concuerda con los caracteres (como [ y \) que tengan un valor entero entre la letra Z mayúscula y la letra a minúscula (para obtener más información acerca de los valores enteros de los caracteres, consulte el apéndice B). Al igual que las clases predefinidas de caracteres, las clases de caracteres delimitadas entre corchetes concuerdan con un solo carácter en el objeto de búsqueda.

En la línea 9, el asterisco después de la segunda clase de carácter indica que puede concordar cual-quier número de letras. En general, cuando aparece el operador de expresión regular “*” en una expresión regular, la aplicación intenta hacer que concuerden cero o más ocurrencias de la subexpresión que va inmediatamente después de “*”. El operador “+” intenta hacer que concuerden una o más ocurrencias

Fig. 16.21 � Recibe datos del usuario y los valida mediante la clase ValidacionEntrada (parte 2 de 2).

Escriba la ciudad:Una ciudadEscriba el estado:SSEscriba el codigo postal:123Escriba el telefono:123-456-7890

Validar resultado:Codigo postal invalido

Escriba el primer nombre:JaneEscriba el apellido paterno:DoeEscriba la direccion:123 Una calleEscriba la ciudad:Una ciudadEscriba el estado:SSEscriba el codigo postal:12345Escriba el telefono:123-456-7890

Validar resultado:La entrada es valida. Gracias.

704 Capítulo 16 Cadenas, caracteres y expresiones regulares

de la subexpresión que va inmediatamente después de “+”. Por lo tanto, “A*” y “A+” concordarán con “AAA” o “A”, pero sólo “A*” concordará con una cadena vacía.

Si el método validarPrimerNombre devuelve true (línea 29 de la figura 16.21), la aplicación trata de validar el apellido paterno (línea 31) llamando a validarApellidoPaterno (líneas 13 a 16 de la figu-ra 16.20). La expresión regular para validar el apellido concuerda con cualquier número de letras dividi-das por espacios, apóstrofes o guiones cortos.

En la línea 33 de la figura 16.21 se hace una llamada al método validarDireccion (líneas 19 a 23 de la figura 16.20) para valida la dirección. La primera clase de carácter concuerda con cualquier dí-gito una o más veces (\\d+). Se utilizan dos caracteres \, ya que \ generalmente inicia una secuencia de escape en una cadena. Por lo tanto, \\d en un objeto String representa al patrón de expresión re-gular \d. Después concordamos uno o más caracteres de espacio en blanco (\\s+). El carácter “|” concuerda con la expresión a su izquierda o a su derecha. Por ejemplo, “Hola (Juan | Juana)” con-cuerda tanto con “Hola Juan” como con “Hola Juana”. Los paréntesis se utilizan para agrupar par-tes de la expresión regular. En este ejemplo, el lado izquierdo de | concuerda con una sola palabra y el lado derecho concuerda con dos palabras separadas por cualquier cantidad de espacios en blanco. Por lo tanto, la dirección debe contener un número seguido de una o dos palabras. Por lo tanto, “10 Broadway” y “10 Main Street” son ambas direcciones válidas en este ejemplo. Los métodos ciudad (líneas 26 a 29 de la figura 16.20) y estado (líneas 32 a 35 de la figura 16.20) también concuer-dan con cualquier palabra que tenga al menos un carácter o, de manera alternativa, con dos palabras cualesquiera con al menos un carácter, si éstas van separadas por un solo espacio. Esto significa que tanto Waltham como West Newton concordarían.

CuantificadoresEl asterisco (*) y el signo de suma (+) se conocen de manera formal como cuantificadores. En la figura 16.22 se presentan todos los cuantificadores. Ya hemos visto cómo funcionan el asterisco (*) y el signo de suma (+). Todos los cuantificadores afectan solamente a la subexpresión que va inmediatamente an-tes del cuantificador. El cuantificador signo de interrogación (?) concuerda con cero o una ocurrencia de la expresión que cuantifica. Un conjunto de llaves que contienen un número ({n}) concuerda exactamente con n ocurrencias de la expresión que cuantifica. En la figura 16.20 mostramos este cuan-tificador para validar el código postal, en la línea 40. Si se incluye una coma después del número ence-rrado entre llaves, el cuantificador concordará al menos con n ocurrencias de la expresión cuantificada. El conjunto de llaves que contienen dos números ({n,m}) concuerda entre n y m ocurrencias de la expresión que califica. Los cuantificadores pueden aplicarse a patrones encerrados entre paréntesis para crear expresiones regulares más complejas.

Fig. 16.22 � Cuantificadores utilizados en expresiones regulares.

Cuantifi cador Concuerda con

* Concuerda con cero o más ocurrencias del patrón.

+ Concuerda con una o más ocurrencias del patrón.

? Concuerda con cero o una ocurrencia del patrón.

{n} Concuerda con exactamente n ocurrencias.

{n,} Concuerda con al menos n ocurrencias.

{n,m} Concuerda con entre n y m (inclusive) ocurrencias.

Todos los cuantificadores son avaros. Esto significa que concordarán con todas las ocurrencias que puedan, siempre y cuando haya concordancia. No obstante, si alguno de estos cuantificadores va

16.7 Expresiones regulares, la clase Pattern y la clase Matcher 705

seguido por un signo de interrogación (?), el cuantificador se vuelve reacio (o, en algunas ocasiones, flojo). De esta forma, concordará con la menor cantidad de ocurrencias posibles, siempre y cuando haya concordancia.

El código postal (línea 40 en la figura 16.20) concuerda con un dígito cinco veces. Esta expresión regular utiliza la clase de carácter de dígito y un cuantificador con el dígito 5 entre llaves. El número tele-fónico (línea 46 en la figura 16.20) concuerda con tres dígitos (el primero no puede ser cero) seguidos de un guión corto, seguido de tres dígitos más (de nuevo, el primero no puede ser cero), seguidos de cuatro dígitos más.

El método matches de String verifica si una cadena completa se conforma a una expresión regular. Por ejemplo, queremos aceptar “Smith” como apellido, pero no “9@Smith#”. Si sólo una subcadena concuerda con la expresión regular, el método matches devuelve false.

Reemplazo de subcadenas y división de cadenasEn ocasiones es conveniente reemplazar partes de una cadena, o dividir una cadena en varias piezas. Para este fin, la clase String proporciona los métodos replaceAll, replaceFirst y split. Estos mé-todos se muestran en la figura 16.23.

Fig. 16.23 � Métodos replaceFirst, replaceAll y split de String (parte 1 de 2).

1 // Fig. 16.23: SustitucionRegex.java

2 // Uso de los métodos replaceFirst, replaceAll y split.

3 import java.util.Arrays;

4 5 public class SustitucionRegex

6 {

7 public static void main( String[] args )

8 {

9 String primeraCadena = “Este enunciado termina con 5 estrellas *****”;

10 String segundaCadena = “1, 2, 3, 4, 5, 6, 7, 8”;

1112 System.out.printf( “Cadena 1 original: %s\n”, primeraCadena );

1314 // sustituye ‘*’ con ‘^’

15 primeraCadena = primeraCadena.replaceAll( “\\*”, “^” );

1617 System.out.printf( “^ sustituyen a *: %s\n”, primeraCadena );

1819 // sustituye ‘estrellas’ con ‘intercaladores’

20 primeraCadena = primeraCadena.replaceAll( “estrellas”, “intercaladores” );

2122 System.out.printf(

23 “\“intercaladores\” sustituye a \“estrellas\”: %s\n”, primeraCadena );

2425 // sustituye las palabras con ‘palabra’

26 System.out.printf( “Cada palabra se sustituye por \7palabra\”: %s\n\n”,

27 primeraCadena.replaceAll( “\\w+”, “palabra” ) );

2829 System.out.printf( “Cadena 2 original: %s\n”, segundaCadena );

3031 // sustituye los primeros tres dígitos con ‘digito’

32 for ( int i = 0; i < 3; i++ )

33 segundaCadena = segundaCadena.replaceFirst( “\\d”, “digito” );

34

706 Capítulo 16 Cadenas, caracteres y expresiones regulares

El método replaceAll reemplaza el texto en un objeto String con nuevo texto (el segundo argu-mento) en cualquier parte en donde el objeto String original concuerde con una expresión regular (el primer argumento). En la línea 15 se reemplaza cada instancia de “*” en primeraCadena con “^”. La expresión regular (“\\*”) coloca dos barras diagonales inversas (\) antes del carácter *. Por lo general, * es un cuantificador que indica que una expresión regular debe concordar con cualquier número de ocurrencias del patrón que se coloca antes de este carácter. Sin embargo, en la línea 15 queremos encontrar todas las ocurrencias del carácter literal *; para ello, debemos escapar el carácter * con el carácter \. Al escapar un carácter especial de expresión regular con una \, indicamos al motor de concordancia de expresiones regulares que busque el carácter en sí. Como la expresión está almacenada en un objeto String de Java y \ es un carácter especial en los objetos String de Java, debemos incluir un \ adicional. Por lo tanto, la cadena de Java “\\*” representa el patrón de expresión regular \*, que concuerda con un solo carácter * en la cadena de búsqueda. En la línea 20, todas las coincidencias con la expresión re-gular “estrellas” en primeraCadena se reemplazan con “intercaladores”. En la línea 27 se utiliza replaceAll para reemplazar todas las palabras en la cadena con “palabra”.

El método replaceFirst (línea 33) reemplaza la primera ocurrencia de la concordancia de un patrón. En Java los objetos String son inmutables; por lo tanto, el método replaceFirst devuel-ve un nuevo objeto String en el que se han reemplazado los caracteres apropiados. Esta línea toma el objeto String original y lo reemplaza con el objeto String devuelto por replaceFirst. Al iterar tres veces, reemplazamos las primeras tres instancias de un dígito (\d) en segundaCadena con el texto “digito”.

El método split divide un objeto String en varias subcadenas. La cadena original se divide en cualquier posición que concuerde con una expresión regular especificada. El método split devuelve un arreglo de objetos String que contiene las subcadenas que resultan de cada concordancia con la expre-sión regular. En la línea 39 utilizamos el método split para descomponer en tokens un objeto String de enteros separados por comas. El argumento es la expresión regular que localiza el delimitador. En este caso, utilizamos la expresión regular “,\\s*” para separar las subcadenas siempre que haya una coma. Al concordar con cualquier carácter de espacio en blanco, eliminamos los espacios adicionales de las subcadenas resultantes. Las comas y los espacios en blanco no se devuelven como parte de las subcade-nas. De nuevo, el objeto String de Java “,\\s*” representa la expresión regular ,\s*. En la línea 40 se utiliza el método toString de Arrays para mostrar el contenido del arreglo resultados entre corchetes y separado por comas.

Fig. 16.23 � Métodos replaceFirst, replaceAll y split de String (parte 2 de 2).

35 System.out.printf(

36 “Los primeros 3 digitos se sustituyeron por \“digito\” : %s\n”, segundaCadena );

37

38 System.out.print( “Cadena dividida en comas: ” );

39 String[] resultados = segundaCadena.split( “,\\s*” ); // se divide en las comas

40 System.out.println( Arrays.toString( resultados ) );

41 } // fin de main

42 } // fin de la clase SustitucionRegex

Cadena 1 original: Este enunciado termina con 5 estrellas *****^ sustituyen a *: Este enunciado termina con 5 estrellas ^^^^^“intercaladores” sustituye a “estrellas”: Este enunciado termina con 5 intercaladores ^^^^^Cada palabra se sustituye por “palabra”: palabra palabra palabra palabra palabra palabra ^^^^^

Cadena 2 original: 1, 2, 3, 4, 5, 6, 7, 8Los primeros 3 digitos se sustituyeron por “digito” : digito, digito, digito, 4, 5, 6, 7, 8Cadena dividida en comas: [“digito”, “digito”, “digito”, “4”, “5”, “6”, “7”, “8”]

16.7 Expresiones regulares, la clase Pattern y la clase Matcher 707

Las clases Pattern y MatcherAdemás de las herramientas para el uso de expresiones regulares de la clase String, Java proporciona otras clases en el paquete java.util.regex que ayudan a los desarrolladores a manipular expresiones regulares. La clase Pattern representa una expresión regular. La clase Matcher contiene tanto un patrón de expre-sión regular como un objeto CharSequence en el que se va a buscar ese patrón.

CharSequence (paquete java.lang) es una interfaz que permite el acceso de lectura a una secuen-cia de caracteres. Esta interfaz requiere que se declaren los métodos charAt, length, subSequence y toString. Tanto String como StringBuilder implementan la interfaz CharSequence, por lo que pue-de usarse una instancia de cualquiera de estas clases con la clase Matcher.

Error común de programación 16.4Una expresión regular puede compararse con un objeto de cualquier clase que imple-mente a la interfaz CharSequence, pero la expresión regular debe ser un objeto String. Si se intenta crear una expresión regular como un objeto StringBuilder se produce un error.

Si se va a utilizar una expresión regular sólo una vez, puede usarse el método static matches de la clase Pattern. Este método toma una cadena que especifica la expresión regular y un objeto CharSequence en el que se va a realizar la prueba de concordancia. Este método devuelve un valor de tipo boolean, el cual indica si el objeto de búsqueda (el segundo argumento) concuerda con la expre-sión regular.

Si se va a utilizar una expresión regular más de una vez (en un ciclo, por ejemplo), es más eficien-te usar el método static compile de la clase Pattern para crear un objeto Pattern específico de esa expresión regular. Este método recibe un objeto String que representa el patrón y devuelve un nuevo objeto Pattern, el cual puede utilizarse para llamar al método matcher. Este método recibe un objeto CharSequence en el que se va a realizar la búsqueda, y devuelve un objeto Matcher.

La clase Matcher cuenta con el método matches, el cual realiza la misma tarea que el método matches de Pattern, pero no recibe argumentos; el patrón y el objeto de búsqueda están encapsula-dos en el objeto Matcher. La clase Matcher proporciona otros métodos, incluyendo find, lookingAt, replaceFirst y replaceAll.

En la figura 16.24 presentamos un ejemplo sencillo en el que se utilizan expresiones regulares. Este programa compara las fechas de cumpleaños con una expresión regular. La expresión concuer-da sólo con los cumpleaños que no ocurran en abril y que pertenezcan a personas cuyos nombres empiecen con “J”.

En las líneas 11 y 12 se crea un objeto Pattern mediante la invocación al método estático compile de la clase Pattern. El carácter de punto “.” en la expresión regular (línea 12) concuerda con cualquier carácter individual, excepto un carácter de nueva línea.

Fig. 16.24 � Las clases Pattern y Matcher (parte 1 de 2).

1 // Fig. 16.24: ConcordanciasRegex.java

2 // Clases Pattern y Matcher.

3 import java.util.regex.Matcher;

4 import java.util.regex.Pattern;

5

6 public class ConcordanciasRegex

7 {

8 public static void main( String[] args )

9 {

10 // crea la expresión regular

11 Pattern expresion =

12 Pattern.compile( “J.*\\d[0-35-9]-\\d\\d-\\d\\d” );

708 Capítulo 16 Cadenas, caracteres y expresiones regulares

En la línea 20 se crea el objeto Matcher para la expresión regular compilada y la secuencia de con-cordancia (cadena1). En las líneas 22 y 23 se utiliza un ciclo while para iterar a través de la cadena. En la línea 22 se utiliza el método find de la clase Matcher para tratar de hacer que concuerde una pieza del objeto de búsqueda con el patrón de búsqueda. Cada una de las llamadas a este método empieza en el punto en el que terminó la última llamada, por lo que pueden encontrarse varias con-cordancias. El método lookingAt de la clase Matcher funciona de manera similar, sólo que siempre comienza desde el principio del objeto de búsqueda, y siempre encontrará la primera concordancia, si es que hay una.

Error común de programación 16.5El método matches (de las clases String, Pattern o Matcher) devuelve true sólo si todo el objeto de búsqueda concuerda con la expresión regular. Los métodos find y lookingAt (de la clase Matcher) devuelven true si una parte del objeto de búsqueda concuerda con la expresión regular.

En la línea 23 se utiliza el método group de la clase Matcher, el cual devuelve el objeto String del objeto de búsqueda que concuerda con el patrón de búsqueda. El objeto String devuelto es el que haya concordado la última vez en una llamada a find o lookingAt. La salida en la figura 16.24 muestra las dos concordancias que se encontraron en cadena1.

Para obtener más información sobre las expresiones regulares, visite nuestro Centro de recursos Web sobre expresiones regulares en www.deitel.com/regularexpressions/.

16.8 ConclusiónEn este capítulo aprendió acerca de más métodos de String para seleccionar porciones de objetos String y manipularlos. También aprendió acerca de la clase Character y sobre algunos de los métodos que de-clara para manejar valores char. En este capítulo también hablamos sobre las herramientas de la clase StringBuilder para crear objetos String. En la parte final del capítulo hablamos sobre las expresiones regulares, las cuales proporcionan una poderosa herramienta para buscar y relacionar porciones de obje-tos String que coincidan con un patrón específico. En el siguiente capítulo aprenderá acerca del proce-samiento de archivos, incluyendo la forma en que se almacenan y recuperan los datos persistentes.

Fig. 16.24 � Las clases Pattern y Matcher (parte 2 de 2).

13

14 String cadena1 = “Jane nacio el 05-12-75\n” +

15 “Dave nacio el 11-04-68\n” +

16 “John nacio el 04-28-73\n” +

17 “Joe nacio el 12-17-77”;

18

19 // compara la expresión regular con la cadena e imprime las concordancias

20 Matcher matcher = expresion.matcher( cadena1 );

21

22 while ( matcher.find() )

23 System.out.println( matcher.group() );

24 } // fin de main

25 } // fin de la clase ConcordanciasRegex

Jane nacio el 05-12-75Joe nacio el 12-17-77

Resumen 709

ResumenSección 16.2 Fundamentos de los caracteres y las cadenas• El valor de una literal de carácter (pág. 673) es el valor entero del carácter en el conjunto de caracteres Unicode (pág.

673). Los objetos String pueden incluir letras, dígitos y varios caracteres especiales, tales como +, -, *, / y $. Una cadena en Java es un objeto de la clase String. Las literales de cadena (pág. 674) se conocen por lo regular como objetos String, y se escriben entre comillas dobles en un programa.

Sección 16.3 La clase String• Los objetos String son inmutables (pág. 675): los caracteres que contienen no se pueden modifi car una vez que se crean.

• El método length de String (pág. 675) devuelve el número de caracteres en un objeto String.

• El método charAt de String (pág. 675) devuelve el carácter en una posición específi ca.

• El método equals de String compara la igualdad entre dos objetos. Este método devuelve true si el contenido de los objetos String es igual, y false en caso contrario. El método equals utiliza una comparación lexicográfi ca (pág. 679) para los objetos String.

• Cuando se comparan valores de tipo primitivo con ==, el resultado es true si ambos valores son idénticos. Cuando las referencias se comparan con ==, el resultado es true si ambas referencias son al mismo objeto en memoria.

• Java trata a todas las literales de cadena con el mismo contenido como un solo objeto String.

• El método equalsIgnoreCase de String realiza una comparación de cadenas insensible al uso de mayúsculas y minúsculas.

• El método compareTo de String usa una comparación lexicográfi ca y devuelve 0 si los objetos String que está comparando son iguales, un número negativo si la cadena con la que se invoca a compareTo es menor que el argumento String y un número positivo si la cadena con la que se invoca a compareTo es mayor que el argumento String.

• El método regionMatches de String (pág. 677) compara la igualdad entre porciones de dos cadenas.

• Los métodos startsWith y endsWith de String (pág. 680) determinan si una cadena empieza o termina con los caracteres especifi cados, respectivamente.

• El método indexOf de String (pág. 681) localiza la primera ocurrencia de un carácter, o de una subcadena en una cadena. El método lastIndexOf de String (pág. 681) localiza la última ocurrencia de un carácter, o de una subcadena en una cadena.

• El método substring de String copia y devuelve parte de un objeto cadena existente.

• El método concat de String concatena dos objetos cadena y devuelve un nuevo objeto cadena.

• El método replace de String devuelve un nuevo objeto cadena que reemplaza cada ocurrencia en un objeto String de su primer argumento carácter, con su segundo argumento carácter.

• El método toUpperCase de String (pág. 685) devuelve una nueva cadena con letras mayúsculas, en las posiciones en donde la cadena original tenía letras minúsculas. El método toLowerCase de String (pág. 686) devuelve una nueva cadena con letras minúsculas en las posiciones en donde la cadena original tenía letras mayúsculas.

• El método trim de String (pág. 686) devuelve un nuevo objeto cadena, en el que todos los caracteres de espacio en blanco (espacios, nuevas líneas y tabuladores) se eliminan de la parte inicial y la parte fi nal de una cadena.

• El método toCharArray de String (pág. 686) devuelve un arreglo char que contiene una copia de los caracteres de una cadena.

• El método static valueOf de String devuelve su argumento convertido en una cadena.

Sección 16.4 La clase StringBuilder• La clase StringBuilder proporciona constructores que permiten inicializar objetos StringBuilder sin caracteres, y

con una capacidad inicial de 16 caracteres, sin caracteres y con una capacidad inicial especifi cada en el argumento entero, o con una copia de los caracteres del argumento String y una capacidad inicial equivalente al número de caracteres en el argumento String, más 16.

• El método length de StringBuilder (pág. 688) devuelve el número de caracteres actualmente almacenados en un objeto StringBuilder. El método capacity de StringBuilder (pág. 688) devuelve el número de caracteres que se pueden almacenar en un objeto StringBuilder sin necesidad de asignar más memoria.

• El método ensureCapacity de StringBuilder (pág. 688) asegura que un objeto StringBuilder tenga por lo menos la capacidad especifi cada. El método setLength de StringBuilder incrementa o decrementa la longitud de un objeto StringBuilder.

• El método charAt de StringBuilder (pág. 690) devuelve el carácter que se encuentra en el índice especifi cado. El método setCharAt (pág. 690) establece el carácter en la posición especifi cada. El método getChars de StringBuilder (pág. 690) copia los caracteres que están en el objeto StringBuilder y los coloca en el arreglo de caracteres que se pasa como argumento.

• Los métodos append (pág. 691) sobrecargados de la clase StringBuilder agregan valores de tipo primitivo, arreglos de caracteres, String, Object y CharSequence (pág. 707) al fi nal de un objeto StringBuilder.

• Los métodos insert sobrecargados de la clase StringBuilder insertan valores de tipo primitivo, arreglos de caracteres, String, Object y CharSequence en cualquier posición en un objeto StringBuilder.

Sección 16.5 La clase Character• El método isDefined de Character (pág. 695) determina si un carácter está defi nido en el conjunto de caracteres

Unicode.

• El método isDigit de Character (pág. 695) determina si un carácter es un dígito defi nido en Unicode.

• El método isJavaIdentifierStart de Character (pág. 696) determina si un carácter se puede utilizar como el primer carácter de un identifi cador en Java El método isJavaIdentifierPart de Character (pág. 696) determina si se puede utilizar un carácter en un identifi cador.

• El método isLetter de Character (pág. 696) determina si un carácter es una letra. El método isLetterOrDigit de Character (pág. 696) determina si un carácter es una letra o un dígito.

• El método isLowerCase de Character (pág. 696) determina si un carácter es una letra minúscula. El método isUpperCase de Character (pág. 696) determina si un carácter es una letra mayúscula.

• El método toUpperCase de Character (pág. 696) convierte un carácter en su equivalente en mayúscula. El método toLowerCase de Character (pág. 696) convierte un carácter en su equivalente en minúscula.

• El método digit de Character (pág. 696) convierte su argumento carácter en un entero en el sistema numérico especifi cado por su argumento entero raíz (pág. 697). El método forDigit de Character (pág. 696) convierte su argumento entero digito en un carácter en el sistema numérico especifi cado por su argumento entero raiz.

• El método charValue de Character (pág. 698) devuelve el valor char almacenado en un objeto Character. El método toString de Character devuelve una representación String de un objeto Character.

Sección 16.6 División de objetos String en tokens• El método split de String (pág. 705) descompone un objeto String en tokens, con base en el delimitador (pág. 699)

especifi cado como argumento, y devuelve un arreglo de objetos String que contiene los tokens (pág. 699).

Sección 16.7 Expresiones regulares, la clase Pattern y la clase Matcher• Las expresiones regulares (pág. 700) son secuencias de caracteres y símbolos que defi nen un conjunto de cadenas. Son

útiles para validar la entrada y asegurar que lo datos se encuentren en un formato específi co.

• El método matches de String (pág. 700) recibe una cadena que especifi ca una expresión regular y relaciona el contenido del objeto String en el que se llama con la expresión regular. El método devuelve un valor de tipo boolean, el cual indica si hubo concordancia o no.

• Una clase de carácter es una secuencia de escape que representa a un grupo de caracteres. Cada clase de carácter concuerda con un solo carácter en la cadena que estamos tratando de igualar con la expresión regular.

• Un carácter de palabra (\w; pág. 700) es cualquier letra (mayúscula o minúscula), dígito o el carácter de guión bajo.

• Un carácter de espacio en blanco (\s) es un espacio, un tabulador, un retorno de carro, un carácter de nueva línea o un avance de página.

710 Capítulo 16 Cadenas, caracteres y expresiones regulares

• Un dígito (\d) es cualquier carácter numérico.

• Para relacionar un conjunto de caracteres que no tienen una clase de carácter predefi nida (pág. 700) , use corchetes ([]). Para representar los rangos, coloque un guión corto (-) entre dos caracteres. Si el primer carácter en los corchetes es “^”, la expresión acepta a cualquier carácter distinto de los que se indican.

• Cuando aparece el operador “*” en una expresión regular, el programa trata de relacionar cero o más ocurrencias de la subexpresión que está justo antes del “*”.

• El operador “+” trata de relacionar una o más ocurrencias de la subexpresión que está antes de éste.

• El carácter “|” permite una concordancia de la expresión a su izquierda o a su derecha.

• Los paréntesis () se utilizan para agrupar partes de la expresión regular.

• El asterisco (*) y el signo positivo (+) se conocen formalmente como cuantifi cadores (pág. 704) .

• Un cuantifi cador afecta sólo a la subexpresión que va justo antes de él.

• El cuantifi cador signo de interrogación (?) concuerda con cero o una ocurrencias de la expresión que cuantifi ca.

• Un conjunto de llaves que contienen un número ({n}) concuerda exactamente con n ocurrencias de la expresión que cuantifi ca. Si se incluye una coma después del número encerrado entre llaves, concuerda con al menos n ocurrencias.

• Un conjunto de llaves que contienen dos números ({n,m}) concuerda con entre n y m ocurrencias de la expresión que califi ca.

• Todos los cuantifi cadores son avaros (pág. 704) , lo cual signifi ca que concordarán con tantas ocurrencias como puedan, mientras que haya concordancia. Si un cuantifi cador va seguido de un signo de interrogación (?), el cuantifi cador se vuelve renuente (pág. 705) y concuerda con el menor número posible de ocurrencias, mientras que haya concordancia.

• El método replaceAll de String (pág. 705) reemplaza texto en una cadena con nuevo texto (el segundo argumento), en cualquier parte en donde la cadena original concuerde con una expresión regular (el primer argumento).

• Al escapar un carácter de expresión regular especial con una \, indicamos al motor de concordancia de expresiones regulares que encuentre el carácter actual, en contraste a lo que representa en una expresión regular.

• El método replaceFirst de String (pág. 705) reemplaza la primera ocurrencia de la concordancia de un patrón y devuelve una nueva cadena en la que se han reemplazado los caracteres apropiados.

• El método split de String divide una cadena en varias subcadenas, en cualquier ubicación que concuerde con una expresión regular especifi cada, y devuelve un arreglo de las subcadenas.

• La clase Pattern (pág. 707) representa a una expresión regular.

• La clase Matcher (pág. 707) contiene tanto un patrón de expresión regular como un objeto CharSequence, en el cual puede buscar el patrón.

• CharSequence es una interfaz (pág. 707) que permite el acceso de lectura a una secuencia de caracteres. Tanto String como StringBuilder implementan a esta interfaz, por lo que se pueden utilizar con la clase Matcher.

• Si una expresión regular se va a utilizar sólo una vez, el método estático matches de Pattern (pág. 707) recibe una cadena que especifi ca la expresión regular y un objeto CharSequence en el que se va a realizar la concordancia. Este método devuelve un valor de tipo boolean que indica si el objeto de búsqueda concuerda o no con la expresión regular.

• Si una expresión regular se va a utilizar más de una vez, es más efi ciente usar el método estático compile de Pattern (pág. 707) para crear un objeto Pattern específi co para esa expresión regular. Este método recibe una cadena que representa el patrón y devuelve un nuevo objeto Pattern.

• El método matcher de Pattern (pág. 707) recibe un objeto CharSequence para realizar la búsqueda y devuelve un objeto Matcher. El método matches de Matcher (pág. 707) realiza la misma tarea que el método matches de Pattern, pero no recibe argumentos.

• El método find de Matcher (pág. 707) trata de relacionar una pieza del objeto de la búsqueda con el patrón de búsqueda. Cada llamada a este método empieza en el punto en el que terminó la última llamada, por lo que se pueden encontrar varias concordancias.

Resumen 711

• El método lookingAt de Matcher (pág. 707) realiza lo mismo que find, excepto que siempre empieza desde el inicio del objeto de búsqueda, y siempre encuentra la primera concordancia, si hay una.

• El método group de Matcher (pág. 708) devuelve la cadena del objeto de búsqueda que concuerda con el patrón de búsqueda. La cadena que se devuelve es la última que concordó mediante una llamada a find o a lookingAt.

Ejercicios de autoevaluación16.1 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.

a) Cuando los objetos String se comparan utilizando ==, el resultado es true si los objetos String contiene los mismos valores.

b) Un objeto String puede modificarse una vez creado.

16.2 Para cada uno de los siguientes enunciados, escriba una instrucción que realice la tarea indicada: a) Comparar la cadena en s1 con la cadena en s2 para ver si su contenido es igual.b) Anexar la cadena s2 a la cadena s1, utilizando +=.c) Determinar la longitud de la cadena en s1.

Respuestas a los ejercicios de autoevaluación16.1 a) Falso. Los objetos String se comparan con el operador == para determinar si son el mismo objeto en la

memoria.b) Falso. Los objetos String son inmutables y no pueden modificarse una vez creados. Los objetos

StringBuilder sí pueden modificarse una vez creados.

16.2 a) s1.equals( s2 )

b) s1 += s2; c) s1.length()

Ejercicios16.3 (Comparación de objetos String) Escriba una aplicación que utilice el método compareTo de la clase String para comparar dos cadenas introducidas por el usuario. Muestre si la primera cadena es menor, igual o mayor que la segunda.

16.4 (Comparar porciones de objetos String) Escriba una aplicación que utilice el método regionMatches de la clase String para comparar dos cadenas introducidas por el usuario. La aplicación deberá recibir como entrada el número de caracteres a comparar y el índice inicial de la comparación. La aplicación deberá indicar si las cadenas son iguales. Ignore si los caracteres están en mayúsculas o en minúsculas al momento de realizar la comparación.

16.5 (Enunciados aleatorios) Escriba una aplicación que utilice la generación de números aleatorios para crear enun-ciados. Use cuatro arreglos de cadenas llamados articulo, sustantivo, verbo y preposicion. Cree una oración selec-cionando una palabra al azar de cada uno de los arreglos, en el siguiente orden: articulo, sustantivo, verbo, preposicion, articulo y sustantivo. A medida que se elija cada palabra, concaténela con las palabras anteriores en el enunciado. Las palabras deberán separarse mediante espacios. Cuando se muestre el enunciado final, deberá empezar con una letra mayúscula y terminar con un punto. El programa deberá generar y mostrar 20 enunciados en pantalla.

El arreglo de artículos debe contener los artículos “el”, “un”, “algún” y “ningún”; el arreglo de sustantivos deberá contener los sustantivos “ninio”, “ninia”, “perro”, “ciudad” y “auto”; el arreglo de verbos deberá contener los verbos “manejo”, “salto”, “corrio”, “camino” y “omitio”; el arreglo de preposiciones deberá contener las preposiciones “a”, “desde”, “encima de”, “debajo de” y “sobre”.

16.6 (Proyecto: quintillas) Una quintilla es un verso humorístico de cinco líneas en el cual la primera y segunda línea riman con la quinta, y la tercera línea rima con la cuarta. Utilizando técnicas similares a las desarrolladas en el ejercicio 16.5, escriba una aplicación en Java que produzca quintillas al azar. Mejorar el programa para producir buenas quintillas es un gran desafío, ¡pero el resultado valdrá la pena!

712 Capítulo 16 Cadenas, caracteres y expresiones regulares

16.7 (Latín cerdo) Escriba una aplicación que codifique frases en español a frases en latín cerdo. El latín cerdo es una forma de lenguaje codificado. Existen muchas variaciones en los métodos utilizados para formar frases en latín cerdo. Por cuestiones de simpleza, utilice el siguiente algoritmo:

Para formar una frase en latín cerdo a partir de una frase en español, divida la frase en palabras con el método split de String. Para traducir cada palabra en español a una palabra en latín cerdo, coloque la primera letra de la palabra en español al fi nal de la palabra, y agregue las letras “ae”. De esta forma, la palabra “salta” se convierte a “altasae”, la pa-labra “el” se convierte en “leae” y la palabra “computadora” se convierte en “omputadoracae”. Los espacios en blanco entre las palabras permanecen como espacios en blanco. Suponga que la frase en español consiste en palabras separadas por espacios en blanco, que no hay signos de puntuación y que todas las palabras tienen dos o más letras. El método imprimirPalabraEnLatin deberá mostrar cada palabra. Cada token devuelto se pasará al método imprimirPalabraEnLatin para imprimir la palabra en latín cerdo. Permita al usuario introducir el enunciado. Use un área de texto para ir mostrando cada uno de los enunciados convertidos.

16.8 (Descomponer números telefónicos en tokens) Escriba una aplicación que reciba como entrada un número telefó-nico como una cadena de la forma (555)555-5555. La aplicación deberá utilizar el método split de String para extraer el código de área como un token, los primeros tres dígitos del número telefónico como otro token y los últimos cuatro dígitos del número telefónico como otro token. Los siete dígitos del número telefónico deberán concatenarse en una cadena. Deberán imprimirse tanto el código de área como el número telefónico. Recuerde que tendrá que modificar los caracteres delimitadores al dividir la cadena en tokens.

16.9 (Mostrar un enunciado con sus palabras invertidas) Escriba una aplicación que reciba como entrada una línea de texto, que divida la línea en tokens mediante el método split de String y que muestre los tokens en orden inverso. Use caracteres de espacio como delimitadores.

16.10 (Mostrar objetos String en mayúsculas y minúsculas) Escriba una aplicación que reciba como entrada una línea de texto y que la imprima dos veces; una vez en letras mayúsculas y otra en letras minúsculas.

16.11 (Búsqueda en objetos String) Escriba una aplicación que reciba como entrada una línea de texto y un carácter de búsqueda, y que utilice el método indexOf de la clase String para determinar el número de ocurrencias de ese carác-ter en el texto.

16.12 (Búsqueda en objetos String) Escriba una aplicación con base en el programa del ejercicio 16.11, que reciba como entrada una línea de texto y utilice el método indexOf de la clase String para determinar el número total de ocurrencias de cada letra del alfabeto en ese texto. Las letras mayúsculas y minúsculas deben contarse como una sola. Almacene los totales para cada letra en un arreglo, e imprima los valores en formato tabular después de que se hayan determinado los totales.

16.13 (Dividir objetos String en tokens y compararlos) Escriba una aplicación que lea una línea de texto, que divida la línea en tokens utilizando caracteres de espacio como delimitadores, y que imprima sólo aquellas palabras que comien-cen con la letra “b”.

16.14 (Dividir objetos String en tokens y compararlos) Escriba una aplicación que lea una línea de texto, que divida la línea en tokens utilizando caracteres de espacio como delimitadores, y que imprima sólo aquellas palabras que co-miencen con las letras “ED”.

16.15 (Convertir valores int a caracteres) Escriba una aplicación que reciba como entrada un código entero para un carácter y que muestre el carácter correspondiente. Modifique esta aplicación de manera que genere todos los posibles códigos de tres dígitos en el rango de 000 a 255, y que intente imprimir los caracteres correspondientes.

16.16 (Defina sus propios métodos de String) Escriba sus propias versiones de los métodos de búsqueda indexOf y lastIndexOf de la clase String.

16.17 (Crear objetos String de tres letras a partir de una palabra de cinco letras) Escriba una aplicación que lea una palabra de cinco letras proveniente del usuario, y que produzca todas las posibles cadenas de tres letras que puedan deri-varse de las letras de la palabra con cinco letras. Por ejemplo, las palabras de tres letras producidas a partir de la palabra “trigo” son “rio”, “tio” y “oir”.

Ejercicios 713

Sección especial: ejercicios de manipulación avanzada de cadenasLos siguientes ejercicios son clave para el libro y están diseñados para evaluar la comprensión del lector sobre los con-ceptos fundamentales de la manipulación de cadenas. Esta sección incluye una colección de ejercicios intermedios y avanzados de manipulación de cadenas. El lector encontrará estos ejercicios desafi antes, pero divertidos. Los problemas varían considerablemente en difi cultad. Algunos requieren una hora o dos para escribir e implementar la aplicación. Otros son útiles como tareas de laboratorio que pudieran requerir dos o tres semanas de estudio e implementación. Algunos son proyectos de fi n de curso desafi antes.

16.18 (Análisis de textos) La disponibilidad de computadoras con capacidades de manipulación de cadenas ha dado como resultado algunos métodos interesantes para analizar los escritos de grandes autores. Se ha dado mucha importancia para saber si realmente vivió William Shakespeare. Algunos estudiosos creen que existe una gran evidencia que indica que en realidad fue Cristopher Marlowe quien escribió las obras maestras que se atribuyen a Shakespeare. Los investigadores han utilizado computadoras para buscar similitudes en los escritos de estos dos autores. En este ejercicio se examinan tres métodos para analizar textos mediante una computadora.

a) Escriba una aplicación que lea una línea de texto desde el teclado e imprima una tabla que indique el nú-mero de ocurrencias de cada letra del alfabeto en el texto. Por ejemplo, la frase:

Ser o no ser: ese es el dilema:

contiene una “a”, ninguna “b”, ninguna “c”, etcétera.b) Escriba una aplicación que lea una línea de texto e imprima una tabla que indique el número de palabras

de una letra, de dos letras, de tres letras, etcétera, que aparezcan en el texto. Por ejemplo, en la fi gura 16.25 se muestra la cuenta para la frase:

¿Qué es más noble para el espíritu?

714 Capítulo 16 Cadenas, caracteres y expresiones regulares

c) Escriba una aplicación que lea una línea de texto e imprima una tabla que indique el número de ocurrencias de cada palabra distinta en el texto. La aplicación debe incluir las palabras en la tabla, en el mismo orden en el cual aparecen en el texto. Por ejemplo, las líneas:

Ser o no ser: ese es el dilema: ¿Qué es más noble para el espíritu?

contiene la palabra “ser” dos veces, La palabra “o” una vez, la palabra “ese” una vez, etcétera.

16.19 (Impresión de fechas en varios formatos) Las fechas se imprimen en varios formatos comunes. Dos de los formatos más utilizados son:

04/25/1955 y Abril 25, 1955

Escriba una aplicación que lea una fecha en el primer formato e imprima dicha fecha en el segundo formato.

Longitud de palabra Ocurrencias

1 0

2 2

3 2

4 1

5 1

6 0

7 0

8 1

Fig. 16.25 � La cuenta de longitudes de palabras para la cadena “¿Qué es más noble para el espíritu?”.

16.20 (Protección de cheques) Las computadoras se utilizan con frecuencia en los sistemas de escritura de cheques, tales como aplicaciones para nóminas y para cuentas por pagar. Existen muchas historias extrañas acerca de cheques de nómina que se imprimen (por error) con montos que se exceden de $1 millón. Los sistemas de emisión de cheques computarizados imprimen cantidades incorrectas debido al error humano o a una falla de la máquina. Los diseñadores de sistemas constru-yen controles en sus sistemas para evitar la emisión de dichos cheques erróneos.

Otro problema grave es la alteración intencional del monto de un cheque por alguien que planee cobrar un cheque de manera fraudulenta. Para evitar la alteración de un monto, la mayoría de los sistemas computarizados que emiten cheques emplean una técnica llamada protección de cheques. Los cheques diseñados para impresión por computadora contienen un número fi jo de espacios en los cuales la computadora puede imprimir un monto. Suponga que un che-que contiene ocho espacios en blanco en los cuales la computadora puede imprimir el monto de un cheque de nómina semanal. Si el monto es grande, entonces se llenarán los ocho espacios. Por ejemplo:

1,230.60 (monto del cheque)--------12345678 (números de posición)

Por otra parte, si el monto es menor de $1,000, entonces varios espacios quedarían vacíos. Por ejemplo:

99.87--------12345678

contiene tres espacios en blanco. Si se imprime un cheque con espacios en blanco, es más fácil para alguien alterar el monto del cheque. Para evitar que se altere el cheque, muchos sistemas de escritura de cheques insertan asteriscos al principio para proteger la cantidad, como se muestra a continuación:

***99.87--------12345678

Escriba una aplicación que reciba como entrada un monto a imprimir sobre un cheque y que lo escriba mediante el formato de protección de cheques, con asteriscos al principio si es necesario. Suponga que existen nueve espacios disponibles para imprimir el monto.

16.21 (Escritura en letras del código de un cheque) Para continuar con la discusión del ejercicio 16.20, reiteramos la importancia de diseñar sistemas de escritura de cheques para evitar la alteración de los montos de los cheques. Un método común de seguridad requiere que el monto del cheque se escriba tanto en números como en letras. Aun cuando alguien pueda alterar el monto numérico del cheque, es extremadamente difícil modificar el monto en letras. Escriba una aplicación que reciba como entrada un monto numérico para el cheque que sea menor a $1000, y que escriba el equivalente del mon-to en letras. Por ejemplo, el monto 112.43 debe escribirse como

CIENTO DOCE CON 43/100

16.22 (Clave Morse) Quizá el más famoso de todos los esquemas de codificación es el código Morse, desarrollado por Samuel Morse en 1832 para usarlo con el sistema telegráfico. El código Morse asigna una serie de puntos y guiones a cada letra del alfabeto, cada dígito y algunos caracteres especiales (tales como el punto, la coma, los dos puntos y el punto y coma). En los sistemas orientados a sonidos, el punto representa un sonido corto y el guión representa un sonido largo. Otras representaciones de puntos y guiones se utilizan en los sistemas orientados a luces y sistemas de señalización con ban-deras. La separación entre palabras se indica mediante un espacio o, simplemente, con la ausencia de un punto o un guión. En un sistema orientado a sonidos, un espacio se indica por un tiempo breve durante el cual no se transmite sonido al-guno. La versión internacional del código Morse aparece en la figura 16.26.

Escriba una aplicación que lea una frase en español y que codifi que la frase en clave Morse. Además, escriba una aplicación que lea una frase en código Morse y que la convierta en su equivalente en español. Use un espacio en blanco entre cada letra en clave Morse, y tres espacios en blanco entre cada palabra en clave Morse.

Sección especial: ejercicios de manipulación avanzada de cadenas 715

16.23 (Conversiones al sistema métrico) Escriba una aplicación que ayude al usuario a realizar conversiones métricas. Su aplicación debe permitir al usuario especificar los nombres de las unidades como cadenas (es decir, centímetros, litros, gramos, etcétera, para el sistema métrico, y pulgadas, cuartos, libras, etcétera, para el sistema inglés) y debe responder a preguntas simples tales como:

“¿Cuántas pulgadas hay en 2 metros?”“¿Cuántos litros hay en 10 cuartos?”

Su programa debe reconocer conversiones inválidas. Por ejemplo, la pregunta:

“¿Cuántos pies hay en 5 kilogramos?”

no es correcta, debido a que los “pies” son unidades de longitud, mientras que los “kilogramos” son unidades de masa.

Sección especial: proyectos desafi antes de manipulación de cadenas16.24 (Proyecto: un corrector ortográfico) Muchos paquetes populares de software de procesamiento de palabras cuen-tan con correctores ortográficos integrados. En este proyecto usted debe desarrollar su propia herramienta de corrección ortográfica. Le haremos unas sugerencias para ayudarlo a empezar. Sería conveniente que después le agregara más carac-terísticas. Use un diccionario computarizado (si tiene acceso a uno) como fuente de palabras.

¿Por qué escribimos tantas palabras en forma incorrecta? En algunos casos es porque simplemente no conocemos la manera correcta de escribirlas, por lo que tratamos de adivinar lo mejor que podemos. En otros casos, es porque trans-ponemos dos letras (por ejemplo, “perdeterminado” en lugar de “predeterminado”). Algunas veces escribimos una letra doble por accidente (por ejemplo, “úttil” en vez de “útil”). Otras veces escribimos una tecla que está cerca de la que pretendíamos escribir (por ejemplo, “cunpleaños” en vez de “cumpleaños”), etcétera.

Diseñe e implemente una aplicación de corrección ortográfi ca en Java. Su aplicación debe mantener un arreglo de cadenas llamado listaDePalabras. Permita al usuario introducir estas cadenas. [Nota: en el capítulo 17 presentaremos el procesamiento de archivos. Con esta capacidad, puede obtener las palabras para el corrector ortográfi co de un diccio-nario computarizado almacenado en un archivo].

Su aplicación debe pedir al usuario que introduzca una palabra. La aplicación debe entonces buscar esa pala-bra en el arreglo listaDePalabras. Si la palabra se encuentra en el arreglo, su aplicación deberá imprimir “La palabra está escrita correctamente”. Si la palabra no se encuentra en el arreglo, su aplicación debe imprimir “La palabra no

716 Capítulo 16 Cadenas, caracteres y expresiones regulares

Carácter Código Carácter Código Carácter Código

A .- N -. Dígitos

B -... O --- 1 .----

C -.-. P .--. 2 ..---

D -.. Q --.- 3 ...--

E . R .-. 4 ....-

F ..-. S ... 5 .....

G --. T - 6 -....

H .... U ..- 7 --...

I .. V ...- 8 ---..

J .--- W .-- 9 ----.

K -.- X -..- 0 -----

L .-.. Y -.--

M -- Z --..

Fig. 16.26 � Las letras del alfabeto expresadas en código Morse internacional.

está escrita correctamente”. Después su aplicación debe tratar de localizar otras palabras en la listaDePalabras que puedan ser la palabra que el usuario trataba de escribir. Por ejemplo, puede probar con todas las transposiciones simples posibles de letras adyacentes para descubrir que la palabra “predeterminado” concuerda directamente con una palabra en listaDePalabras. Desde luego que esto implica que su programa comprobará todas las otras transposiciones posibles, como “rpedeterminado”, “perdeterminado”, “predetreminado”, “predetemrinado” y “pre-determniado”. Cuando encuentre una nueva palabra que concuerde con una en la listaDePalabras, imprima esa palabra en un mensaje como

“¿Quiso decir “predeterminado”?”

Implemente otras pruebas, como reemplazar cada letra doble con una sola letra y cualquier otra prueba que pueda desarrollar para aumentar el valor de su corrector ortográfi co.

16.25 (Proyecto: un generador de crucigramas) La mayoría de las personas han resuelto crucigramas, pero pocos han intentado generar uno. Aquí lo sugerimos como un proyecto de manipulación de cadenas que requiere una cantidad considerable de sofisticación y esfuerzo.

Hay muchas cuestiones que el programador tiene que resolver para hacer que funcione incluso hasta la aplicación generador de crucigramas más simple. Por ejemplo, ¿cómo representaría la cuadrícula de un crucigrama dentro de la computadora? ¿Debería utilizar una serie de cadenas o arreglos bidimensionales?

El programador necesita una fuente de palabras (es decir, un diccionario computarizado) a la que la aplicación pueda hacer referencia de manera directa. ¿De qué manera deben almacenarse estas palabras para facilitar las manipulaciones complejas que requiere la aplicación?

Si usted es realmente ambicioso, querrá generar la porción de “claves” del crucigrama, en la que se imprimen pistas breves para cada palabra “horizontal” y cada palabra “vertical”. La sola impresión de la versión del crucigrama en blanco no es una tarea fácil.

Marcar la diferencia16.26 (Cocinar con ingredientes más saludables) La obesidad en Estados Unidos está aumentando a un ritmo alarmante. De un vistazo al mapa de los Centros para el control y la prevención de enfermedades (CDC) en http://www.cdc.gov/obesity/data/trends.html, que muestra las tendencias de obesidad en Estados Unidos du-rante los últimos 20 años. A medida que aumenta la obesidad, también se incrementan las ocurrencias de los pro-blemas relacionados (enfermedades cardiacas, presión alta, colesterol alto, diabetes tipo 2). Escriba un programa que ayude a los usuarios a elegir ingredientes más saludables al cocinar, y que ayude a los que padecen alergias a ciertos alimentos (como nueces, gluten, etc.) a encontrar sustitutos. El programa debe leer una receta de un objeto JTextArea y sugerir sustitutos más saludables para algunos de los ingredientes. Por cuestión de simpleza, su programa debe asumir que la receta no tiene abreviaciones para las medidas como cucharaditas, tazas y cucharadas, y que usa dígitos numéricos para las cantidades (por ejemplo, 1 huevo, 2 tazas) en vez de deletrearlos (un huevo, dos tazas). Algunos sustitutos comunes se muestran en la figura 16.27. Su programa debe mostrar una advertencia tal como, “Consulte siempre a su médico antes de realizar cambios considerables en su dieta”.

Su programa debe tener en cuenta que los sustitutos no son siempre en la misma cantidad. Por ejemplo, si la receta de un pastel dice que se usan tres huevos, una sustitución razonable podría ser seis claras de huevo. Es posible obtener datos de conversión para las medidas y sustitutos en sitios Web tales como:

chinesefood.about.com/od/recipeconversionfaqs/f/usmetricrecipes.htmwww.pioneerthinking.com/eggsub.htmlwww.gourmetsleuth.com/conversions.htm

Su programa debe considerar los problemas de salud del usuario, como el colesterol alto, la presión alta, la pérdida de peso, la alergia al gluten, etcétera. Para el colesterol alto, el programa debe sugerir sustitutos para huevos y pro-ductos lácteos; si el usuario desea perder peso, deberían sugerirse sustitutos de bajas calorías para los ingredientes como el azúcar.

16.27 (Explorador de spam) El spam (o correo electrónico basura) cuesta a las organizaciones estadounidenses miles de millones de dólares al año en software para prevención de spam, equipo, recursos de red, ancho de banda y pérdida de productividad. Investigue en línea algunos de los mensajes y palabras de correo electrónico de spam más comunes, y revise su propia carpeta de correo electrónico basura. Cree una lista de 30 palabras y frases que se encuentren comúnmente en los mensajes de spam. Escriba una aplicación en donde el usuario introduzca un mensaje de texto en un objeto JTextArea.

Marcar la diferencia 717

Después, explore el mensaje en busca de cada una de las 30 palabras clave o frases. Para cada ocurrencia de una de éstas dentro del mensaje, agregue un punto a la “puntuación del spam” del mensaje. Después califique la probabilidad de que el mensaje sea spam, con base en el número de puntos que haya recibido.

16.28 (Lenguaje SMS) El Servicio de mensaje cortos (SMS) es un servicio de comunicaciones que permite enviar mensajes de texto de 160 o menos caracteres entre teléfonos móviles. Con la proliferación del uso de teléfonos móviles en todo el mundo, SMS se utiliza en muchas naciones en desarrollo para fines políticos (por ejemplo, manifestar opiniones y oposición), informar noticias sobre desastres naturales, etcétera. Por ejemplo, visite comunica.org/radio2.0/archives/87. Como la longitud de los mensajes SMS es limitada, a menudo se utiliza el lenguaje SMS: abreviaciones de palabras y frases comunes en mensajes de texto móviles, correos electrónicos, mensajes instantáneos, etc. Por ejemplo, “ntc” es “no te creas” en lenguaje SMS. Investigue el lenguaje SMS en línea. Escriba una aplicación de GUI en donde el usuario pueda introducir un mensaje mediante lenguaje SMS, y que después haga clic en un botón para traducirlo en español (o en su propio lenguaje). Proporcione además un mecanismo para traducir texto escrito en español (o su propio lenguaje) a lenguaje SMS. Un posible problema es que una abreviación en SMS podría expandirse en una variedad de frases. Por ejemplo, “ntc” (como se indicó antes) podría también representar “no te compliques”, “nadie tiene control”, o cual-quier otra cosa.

718 Capítulo 16 Cadenas, caracteres y expresiones regulares

Ingrediente Sustituto

1 taza de crema agria 1 taza de yogur

1 taza de leche 1/2 taza de leche evaporada y 1/2 taza de agua

1 cucharadita de jugo de limón 1/2 cucharadita de vinagre

1 taza de azúcar 1/2 taza de miel, 1 taza de melaza o 1/4 taza de néctar de agave

1 taza de mantequilla 1 taza de margarina o yogur

1 taza de harina 1 taza de harina de centeno o de arroz

1 taza de mayonesa 1 taza de queso cottage o 1/8 taza de mayonesa y 7/8 taza de yogur

1 huevo 2 cucharadas de almidón, harina de arrurruz o almidón de papa, o 2 claras de huevo, o 1/2 plátano grande (machacado)

1 taza de leche 1 taza de leche de soya

1/4 taza de aceite 1/4 taza de puré de manzana

pan blanco pan de grano entero

Fig. 16.27 � Ingredientes y sustitutos.

Archivos, fl ujos y serialización de objetos 17

Sólo puedo suponer que un documento “No archivar” se archiva en un archivo “No archivar”.—Senador Frank ChurchAudiencia del subcomité de inteligencia del Senado, 1975

La conciencia … no aparece a sí misma cortada en pequeños pedazos. … Un “río” o un “flujo” son las metáforas por las cuales se describe con más naturalidad.—William James

O b j e t i v o sEn este capítulo aprenderá a:

■ Crear, leer, escribir y actualizar archivos.

■ Obtener información acerca de los archivos y directorios.

■ Comprender la jerarquía de clases de flujos de entrada/salida en Java.

■ Conocer las diferencias entre los archivos de texto y los archivos binarios.

■ Utilizar las clases Scanner y Formatter para procesar archivos de texto.

■ Utilizar las clases FileInputStream y FileOutputStream para leer de, y escribir en, archivos.

■ Utilizar las clases ObjectInputStream y ObjectOutputStream para leer objetos de, y escribir objetos en, archivos.

■ Utilizar un cuadro de diálogo JFileChooser.

720 Capítulo 17 Archivos, fl ujos y serialización de objetos

17.1 Introducción

17.2 Archivos y fl ujos

17.3 La clase File

17.4 Archivos de texto de acceso secuencial 17.4.1 Creación de un archivo de texto de acceso

secuencial17.4.2 Cómo leer datos de un archivo de texto de

acceso secuencial17.4.3 Caso de estudio: un programa de solicitud

de crédito17.4.4 Actualización de archivos de acceso secuencial

17.5 Serialización de objetos

17.5.1 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos

17.5.2 Lectura y deserialización de datos de un archivo de acceso secuencial

17.6 Clases adicionales de java.io 17.6.1 Interfaces y clases para entrada y salida basada

en bytes17.6.2 Interfaces y clases para entrada y salida basada

en caracteres

17.7 Abrir archivos con JFileChooser

17.8 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios | Marcar la diferencia

17.1 Introducción1

El almacenamiento de datos en variables y arreglos es temporal; los datos se pierden cuando una variable local queda fuera de alcance, o cuando el programa termina. Las computadoras utilizan archivos para la retención a largo plazo de datos, incluso hasta después de que terminan los programas que crean esos datos. Usted utiliza archivos a diario, para tareas como escribir un documento o crear una hoja de cálcu-lo. Las computadoras almacenan archivos en dispositivos de almacenamiento secundario como discos duros, discos ópticos y cintas magnéticas. Nos referimos a los datos que se mantienen en archivos como datos persistentes, ya que existen más allá de la duración de la ejecución del programa. En este capítulo explicaremos cómo los programas en Java crean, actualizan y procesan archivos.

Empezaremos con un análisis sobre la arquitectura de Java para manejar archivos mediante programa-ción. Luego explicaremos que los datos pueden almacenarse en archivos de texto y archivos binarios, y cubriremos las diferencias entre ellos. Demostraremos cómo obtener información sobre archivos y direc-torios mediante el uso de la clase File, y después dedicaremos varias secciones a los distintos mecanismos para escribir datos en, y leer datos de, archivos. Le mostraremos cómo crear y manipular archivos de texto de acceso secuencial. Al trabajar con archivos de texto, el lector puede empezar a manipular archivos con rapidez y facilidad. Sin embargo, como veremos más adelante, es difícil leer datos de los archivos de texto y devolverlos al formato de los objetos. Por fortuna, muchos lenguajes orientados a objetos (incluyen-do Java) ofrecen distintas formas de escribir objetos en (y leer objetos de) archivos (lo que se conoce como serialización y deserialización de objetos). Para demostrar esto, recreamos algunos de los programas de acceso secuencial que utilizaban archivos de texto, esta vez almacenando objetos en archivos binarios.

17.2 Archivos y flujosJava considera a cada archivo como un flujo de bytes secuencial (figura 17.1). Cada sistema operativo proporciona un mecanismo para determinar el fin de un archivo, como el marcador de fin de archivo o la cuenta de bytes totales en el archivo que se registra en una estructura de datos administrativa, mantenida por el sistema. Un programa de Java que procesa un flujo de bytes simplemente recibe una indicación del sistema operativo cuando el programa llega al fin del flujo; el programa no necesita sa-ber cómo representa la plataforma subyacente a los archivos o flujos. En algunos casos, la indicación de

1 Las técnicas que se muestran en este capítulo se basan en Java SE 6. Java SE 7 introduce nuevas API en el sistema de ar-chivos para interactuar con los archivos y directorios. En el sitio Web complementario del libro (al que puede acceder en www.pearsonhighered.com/deitel) publicamos una versión de este capítulo, en el que implementamos el uso de estas API de Java SE 7.

17.2 Archivos y fl ujos 721

fin de archivo ocurre como una excepción. En otros casos, la indicación es un valor de retorno de un método invocado en un objeto procesador de flujos.

Fig. 17.1 � La manera en que Java ve a un archivo de n bytes.

0 1 2 3 4 5 6 7 8 9 ...

...

n-1

marcador de fin de archivo

Flujos basados en bytes y basados en caracteresLos flujos de archivos se pueden utilizar para la entrada y salida de datos, ya sea como bytes o carac-teres. Los flujos basados en bytes reciben y envían datos en su formato binario. Los flujos basados en caracteres reciben y envían datos como una secuencia de caracteres. Por ejemplo, si se almacenara el valor 5 usando un flujo basado en bytes, sería en el formato binario del valor numérico 5, o 101. Si se almacenara el valor 5 usando un flujo basado en caracteres, sería en el formato binario del ca-rácter 5, o 00000000 00110101 (ésta es la representación binaria para el valor numérico 53, el cual indica el carácter 5 en el conjunto de caracteres Unicode®). La diferencia entre las dos formas es que el valor numérico se puede utilizar como un entero en los cálculos, mientras que el carácter 5 es simplemente un carácter que puede utilizarse en una cadena de texto, como en “Sarah Miller tiene 15 años de edad”. Los archivos que se crean usando flujos basados en bytes se conocen como archivos binarios, mientras que los archivos que se crean usando flujos basados en caracteres se conocen como archivos de texto. Los archivos de texto se pueden leer con editores de texto, mien-tras que los archivos binarios se leen mediante programas que comprenden el contenido específico del archivo y su orden.

Flujos estándar de entrada salida y error estándarUn programa de Java abre un archivo creando un objeto y asociándole un flujo de bytes o de carac-teres. El constructor del objeto interactúa con el sistema operativo para abrir el archivo. Java tam-bién puede asociar flujos con distintos dispositivos. De hecho, Java crea tres objetos flujo que se asocian con dispositivos cuando un programa de Java empieza a ejecutarse: System.in, System.out y System.err. Por lo general, System.in (el objeto flujo de entrada estándar) permite a un pro-grama recibir bytes desde el teclado; el objeto System.out (el objeto flujo estándar de salida) gene-ralmente permite a un programa mostrar datos en la pantalla; y el objeto System.err (el objeto flujo estándar de error) normalmente permite a un programa mostrar en la pantalla mensajes de error basados en caracteres. Cada uno de estos flujos puede redirigirse. Para System.in, esta capaci-dad permite al programa leer bytes desde un origen distinto. Para System.out y System.err, esta capacidad permite que la salida se envíe a una ubicación distinta, como un archivo en disco. La clase System proporciona los métodos setIn, setOut y setErr para redirigir los flujos estándar de entrada, salida y error, respectivamente.

El paquete java.ioLos programas de Java realizan el procesamiento de archivos utilizando clases del paquete java.io. Este paquete incluye definiciones para las clases de flujo como FileInputStream (para la entrada basada en bytes desde un archivo), FileOutputStream (para la salida basada en bytes hacia un ar-chivo), FileReader (para la entrada basada en caracteres desde un archivo) y FileWriter (para la salida basada en caracteres hacia un archivo), que heredan de las clases InputStream, OutputStream, Reader y Writer, respectivamente. Por lo tanto, los métodos de estas clases de flujos pueden aplicarse a los flujos de archivos también.

722 Capítulo 17 Archivos, fl ujos y serialización de objetos

Java contiene clases que permiten al programador realizar operaciones de entrada y salida con ob-jetos o variables de tipos de datos primitivos. Los datos se siguen almacenando como bytes o caracteres tras bambalinas, lo cual permite al programador leer o escribir datos en forma de valores int, String u otros tipos de datos, sin tener que preocuparse por los detalles acerca de convertir dichos valores al formato de bytes. Para realizar dichas operaciones de entrada y salida, pueden usarse objetos de las clases ObjectInputStream y ObjectOutputStream junto con las clases de flujos de archivos basadas en bytes FileInputStream y FileOutputStream (en breve hablaremos con más detalle sobre estas clases). Es posible consultar la jerarquía completa de tipos del paquete java.io en la documentación en línea, en la página:

download.oracle.com/javase/6/docs/api/java/io/package-tree.html

Como puede ver en la jerarquía, Java ofrece muchas clases para realizar operaciones de entrada/salida. En este capítulo usaremos varias de estas clases para implementar programas de procesamiento de ar-chivos, que crean y manipulan archivos de acceso secuencial. En el capítulo 27 utilizaremos las clases de flujos en forma extensa, para implementar aplicaciones de red.

Además de las clases de java.io, las operaciones de entrada y salida basadas en caracteres se pue-den llevar a cabo con las clases Scanner y Formatter. La clase Scanner se utiliza con mucha frecuen-cia para recibir datos del teclado; también puede leer datos desde un archivo. La clase Formatter permite mostrar datos con formato a cualquier flujo basado en texto, en forma similar al método System.out.printf. En el apéndice G, se presentan los detalles acerca de la salida con formato me-diante printf. Todas estas características se pueden utilizar también para dar formato a los archivos de texto.

17.3 La clase File En esta sección presentamos la clase File, que es especialmente útil para recuperar información acerca de un archivo o directorio de un disco. Los objetos de la clase File no abren archivos ni proporcionan herramientas para procesarlos. No obstante, los objetos File se utilizan frecuentemente con objetos de otras clases de java.io para especificar los archivos o directorios que van a manipularse.

Creación de objetos FileLa clase File proporciona cuatro constructores. El constructor con un argumento String especifica el nombre de un archivo o directorio que se asociará con el objeto File. El nombre puede conte-ner información sobre la ruta, así como el nombre de un archivo o directorio. La ruta de un archivo o directorio especifica su ubicación en el disco. La ruta incluye algunos o todos los directorios que conducen a ese archivo o directorio. Una ruta absoluta contiene todos los directorios, empezando con el directorio raíz, que conducen a un archivo o directorio específico. Cada archivo o directorio en un disco duro específico tiene el mismo directorio raíz en su ruta. Por lo general, una ruta relativa empieza desde el directorio en el que la aplicación empezó a ejecutarse, y es por lo tanto una ruta “relativa” al directorio actual. El constructor con dos argumentos String especifica una ruta abso-luta o relativa como el primer argumento, y el archivo o directorio a asociar con el objeto File como el segundo argumento. El constructor con argumentos File y String usa un objeto File existente, que especifica el directorio padre del archivo o directorio especificado por el argumento String. El cuarto constructor usa un objeto URI para localizar el archivo. Un Identificador uniforme de recursos (URI) es una forma más general de un Localizador uniforme de recursos (URL), el cual se utiliza para localizar sitios Web. Por ejemplo, http://www.deitel.com/ es el URL para el sitio Web de Deitel & Associates. Los URI para localizar archivos varían entre los distintos sistemas operativos. En plataformas Windows, el URI:

file://C:/datos.txt

17.3 La clase File 723

identifica al archivo datos.txt, almacenado en el directorio raíz de la unidad C:. En plataformas UNIX/Linux, el URI

file:/home/estudiante/datos.txt

identifica el archivo datos.txt almacenado en el directorio home del usuario estudiante.La figura 17.2 muestra una lista de los métodos más comunes de File. En download.oracle.com/

javase/6/docs/api/java/io/File.html encontrará la lista completa.

Fig. 17.2 � Métodos de File.

Método Descripción

boolean canRead() Devuelve true si la aplicación actual puede leer un archivo; false en caso contrario.

boolean canWrite() Devuelve true si la aplicación actual puede escribir en un archivo; false en caso contrario.

boolean exists() Devuelve true si el archivo o directorio representado por el objeto File existe; false en caso contrario.

boolean isFile() Devuelve true si el nombre especifi cado como argumento para el constructor de File es un archivo; false en caso contrario.

boolean isDirectory() Devuelve true si el nombre especifi cado como argumento para el constructor de File es un directorio; false en caso contrario.

boolean isAbsolute() Devuelve true si los argumentos especifi cados para el constructor de File indican una ruta absoluta a un archivo o directorio; false en caso contrario.

String getAbsolutePath() Devuelve un objeto String con la ruta absoluta del archivo o directorio.

String getName() Devuelve un objeto String con el nombre del archivo o directorio.

String getPath() Devuelve un objeto String con la ruta del archivo o directorio.

String getParent() Devuelve un objeto String con el directorio padre del archivo o directorio (es decir, el directorio en el que puede encontrarse ese archivo o directorio).

long length() Devuelve la longitud del archivo, en bytes. Si el objeto File representa a un directorio, se devuelve un valor no especifi cado.

long lastModified() Devuelve una representación dependiente de la plataforma de la hora en la que se hizo la última modifi cación en el archivo o directorio. El valor devuelto es útil sólo para compararlo con otros valores devueltos por este método.

String[] list() Devuelve un arreglo de objetos String, los cuales representan el contenido de un directorio. Devuelve null si el objeto File no representa a un directorio.

Demostración de la clase FileLa figura 17.3 pide al usuario que introduzca el nombre de un archivo o directorio, y después usa la clase File para imprimir información en pantalla acerca del nombre de archivo o directorio intro-ducido.

El programa empieza pidiendo al usuario un archivo o directorio (línea 12). En la línea 13 se introduce el nombre del archivo o directorio y se pasa al método analizarRuta (líneas 17 a 50). El método crea un nuevo objeto File (línea 20) y asigna su referencia a nombre. En la línea 22 se

724 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.3 � Uso de la clase File para obtener información sobre archivos y directorios (parte 1 de 2).

1 // Fig. 17.3: DemostracionFile.java

2 // Clase File utilizada para obtener información sobre archivos y directorios.

3 import java.io.File;

4 import java.util.Scanner;

5

6 public class DemostracionFile

7 {

8 public static void main( String[] args )

9 {

10 Scanner entrada = new Scanner( System.in );

11

12 System.out.print( “Escriba aqui el nombre del archivo o directorio: ” );

13 analizarRuta( entrada.nextLine() );

14 } // fin de main

15

16 // muestra información acerca del archivo especificado por el usuario

17 public static void analizarRuta( String ruta )

18 {

19 // crea un objeto File con base en la entrada del usuario

20 File nombre = new File( ruta );

21

22 if ( nombre.exists() ) // si existe el nombre, muestra información sobre él

23 {

24 // muestra información del archivo (o directorio)

25 System.out.printf(

26 “%s%s\n%s\n%s\n%s\n%s%s\n%s%s\n%s%s\n%s%s\n%s%s”,

27 nombre.getName(), “ existe”,

28 ( nombre.isFile() ? “es un archivo” : “no es un archivo” ),

29 ( nombre.isDirectory() ? “es un directorio” :

30 “no es un directorio” ),

31 ( nombre.isAbsolute() ? “es ruta absoluta” :

32 “no es ruta absoluta” ), “Ultima modificacion: ”,

33 nombre.lastModified(), “Tamanio: ”, nombre.length(),

34 “Ruta: ”, nombre.getPath(), “Ruta absoluta: ”,

35 nombre.getAbsolutePath(), “Padre: ”, nombre.getParent() );

36

37 if ( nombre.isDirectory() ) // muestra el listado del directorio

38 {

39 String[] directorio = nombre.list();

40 System.out.println( “\n\nContenido del directorio:\n” );

41

42 for ( String nombreDirectorio : directorio )

43 System.out.printf( “%s\n”, nombreDirectorio );

44 } // fin de if

45 } // fin de if exterior

46 else // no es archivo o directorio, muestra mensaje de error

47 {

48 System.out.printf( “%s %s”, ruta, “no existe.” );

49 } // fin de else

50 } // fin del método analizarRuta

51 } // fin de la clase DemostracionFile

17.3 La clase File 725

invoca el método exists de File para determinar si el nombre introducido por el usuario existe (ya sea como archivo o directorio) en el disco. Si el nombre no existe, el control procede a las líneas 46 a 49 y muestra un mensaje en la pantalla, que contiene el nombre que escribió el usuario, seguido de “no existe”. En caso contrario, se ejecuta el cuerpo de la instrucción if (líneas 22 a 45). El programa imprime el nombre del archivo o directorio (línea 27), seguido de los resultados de probar el objeto File con isFile (línea 28), isDirectory (línea 29) e isAbsolute (línea 31). A continuación, el pro-grama muestra los valores devueltos por lastModified (línea 33), length (línea 33), getPath (línea 34), getAbsolutePath (línea 35) y getParent (línea 35). Si el objeto File representa un directorio (línea 37), el programa obtiene una lista del contenido del directorio como un arreglo de objetos String, usando el método list de File (línea 39), y muestra la lista en la pantalla.

El primer resultado de este programa demuestra un objeto File asociado con el directorio jfc del JDK. El segundo resultado demuestra un objeto File asociado con el archivo README.txt del

Fig. 17.3 � Uso de la clase File para obtener información sobre archivos y directorios (parte 2 de 2).

Escriba aqui el nombre del archivo o directorio: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfcjfc existeno es un archivoes un directorioes ruta absolutaUltima modificacion: 1228404395024Tamanio: 4096Ruta: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfcRuta absoluta: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfcPadre: E:\Archivos de programa\Java\jdk1.6.0_11\demo

Contenido del directorio:

CodePointIMFileChooserDemoFont2DTestJava2DLaffyMetalworksNotepadSampleTreeStylepadSwingAppletSwingSet2SwingSet3

Escriba aqui el nombre del archivo o directorio: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfc\Java2D\README.txtreadme.txt existees un archivono es un directorioes ruta absolutaUltima modificacion: 1228404384270Tamanio: 7518Ruta: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfc\Java2D\README.txtRuta absoluta: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfc\Java2D\README.txtPadre: E:\Archivos de programa\Java\jdk1.6.0_11\demo\jfc\Java2D

726 Capítulo 17 Archivos, fl ujos y serialización de objetos

ejemplo de Java 2D que viene con el JDK. En ambos casos, especificamos una ruta absoluta en nues-tra computadora personal.

Un carácter separador se utiliza para separar directorios y archivos en la ruta. En un equipo Windows, el carácter separador es la barra diagonal inversa (\). En un sistema UNIX, el carácter separa-dor es la barra diagonal (/). Java procesa ambos caracteres en forma idéntica en el nombre de una ruta. Por ejemplo, si deseamos utilizar la ruta

c:\Archivos de programa\Java\jdk1.6.0_11\demo/jfc

que emplea uno de cada uno de los caracteres separadores antes mencionados, Java de todas formas pro-cesa la ruta en forma apropiada. Al construir objetos String que representen la información de una ruta, use File.separator para obtener el carácter separador apropiado del equipo local, en vez de utilizar / o \ de manera explícita. Esta constante devuelve un objeto String que consiste de un carácter: el separador apropiado para el sistema.

Error común de programación 17.1Usar \ como separador de directorios en vez de \\ en una literal de cadena es un error lógico. Una sola \ indica que la \ y el siguiente carácter representan una secuencia de escape. Para insertar una \ en una literal de cadena, debe usar \\.

17.4 Archivos de texto de acceso secuencialA continuación crearemos y manipularemos archivos de acceso secuencial, en donde se guardan los re-gistros en orden, en base al campo clave de registro. Empezaremos con los archivos de texto, que permi-ten al lector crear y editar rápidamente archivos que puedan ser leídos por los humanos. Hablaremos sobre crear, escribir datos en, leer datos de y actualizar los archivos de texto de acceso secuencial. También incluiremos un programa de consulta de crédito para obtener datos específicos de un archivo.

17.4.1 Creación de un archivo de texto de acceso secuencialJava no impone una estructura en un archivo; las nociones tales como los registros no existen como parte del lenguaje Java. Por lo tanto, el programador debe estructurar los archivos de manera que cumplan con los requerimientos de sus aplicaciones. En el siguiente ejemplo veremos cómo imponer una estructura de registros con claves en un archivo.

El programa de las figuras 17.4, 17.5 y 17.8 crea un archivo simple de acceso secuencial, que podría utilizarse en un sistema de cuentas por cobrar para ayudar a administrar el dinero que deben a una com-pañía los clientes a crédito. Por cada cliente, el programa obtiene un número de cuenta, el nombre del cliente y su saldo (es decir, el monto que el cliente aún debe a la compañía por los bienes y servicios reci-bidos). Los datos obtenidos para cada cliente constituyen un “registro” para ese cliente. El número de cuenta se utiliza como la clave de registro en esta aplicación; el archivo se creará y mantendrá en orden basado en el número de cuenta. El programa supone que el usuario introduce los registros en orden de número de cuenta. En un sistema completo de cuentas por cobrar (basado en archivos de acceso secuen-cial), se proporcionaría una herramienta para ordenar datos, de manera que el usuario pudiera introducir los registros en cualquier orden. Después, los registros se ordenarían y se escribirían en el archivo.

La clase RegistroCuentaLa clase RegistroCuenta (figura 17.4) encapsula la información de registro del cliente utilizada por los ejemplos en este capítulo. La clase RegistroCuenta se declara en el paquete com.deitel.cap17 (línea 3), de forma que se pueda importar en varios ejemplos de este capítulo para reutilizarla (en la sección 8.14 encontrará información sobre cómo compilar y usar sus propios paquetes). La clase RegistroCuenta contiene las variables de instancia private llamadas cuenta, primerNombre, apellidoPaterno y saldo (líneas 7 a 10), además de los métodos establecer y obtener para acceder a

17.4 Archivos de texto de acceso secuencial 727

estos campos. Aunque los métodos establecer no validan los datos en este ejemplo, deberían hacerlo en un sistema de “nivel industrial”.

Fig. 17.4 � La clase RegistroCuenta mantiene la información para una cuenta (parte 1 de 2).

1 // Fig. 17.4: RegistroCuenta.java 2 // La clase RegistroCuenta mantiene la información de una cuenta. 3 package com.deitel.cap17; // se empaqueta para reutilizarla 4 5 public class RegistroCuenta 6 { 7 private int cuenta; 8 private String primerNombre; 9 private String apellidoPaterno;10 private double saldo;1112 // el constructor sin argumentos llama a otro constructor con valores predeterminados13 public RegistroCuenta() 14 {15 this( 0, “”, “”, 0.0 ); // llama al constructor con cuatro argumentos16 } // fin del constructor de RegistroCuenta sin argumentos1718 // inicializa un registro19 public RegistroCuenta( int cta, String nombre, String apellido, double sal )20 {21 establecerCuenta( cta );22 establecerPrimerNombre( nombre );23 establecerApellidoPaterno( apellido );24 establecerSaldo( sal );25 } // fin del constructor de RegistroCuenta con cuatro argumentos2627 // establece el número de cuenta 28 public void establecerCuenta( int cta )29 {30 cuenta = cta;31 } // fin del método establecerCuenta3233 // obtiene el número de cuenta 34 public int obtenerCuenta() 35 { 36 return cuenta; 37 } // fin del método obtenerCuenta3839 // establece el primer nombre 40 public void establecerPrimerNombre( String nombre )41 {42 primerNombre = nombre;43 } // fin del método establecerPrimerNombre4445 // obtiene el primer nombre 46 public String obtenerPrimerNombre() 47 { 48 return primerNombre; 49 } // fin del método obtenerPrimerNombre

728 Capítulo 17 Archivos, fl ujos y serialización de objetos

Para compilar la clase RegistroCuenta, abra una ventana de símbolo del sistema, cambie al direc-torio fig17_05 de este capítulo (que contiene el archivo RegistroCuenta.java) y escriba lo siguiente:

javac -d .. RegistroCuenta.java

Esto coloca a RegistroCuenta.class en la estructura de directorios de su paquete, y coloca el paquete en la carpeta cap17 que contiene todos los ejemplos para este capítulo. Cuando compile la clase RegistroCuen-ta (o cualquier otra clase que se reutilice en este capítulo), debe colocarla en un directorio común. Cuando compile o ejecute clases que utilicen a RegistroCuenta (por ejemplo, CrearArchivoTexto en la figura 17.5), debe especificar el argumento de línea de comandos –classpath para javac y java, como en

javac -classpath .;c:\ejemplos\cap17 CrearArchivoTexto.javajava -classpath .;c:\ejemplos\cap17 CrearArchivoTexto

El directorio actual (que se especifica con .) se incluye en la ruta de clases para asegurar que el compila-dor pueda localizar otras clases en el mismo directorio que el de la clase que se está compilando. El se-parador de ruta que se utiliza en los comandos anteriores debe ser el apropiado para su plataforma: un punto y coma (;) en Windows y dos puntos (:) en UNIX/Linux/Mac OS X. Los anteriores comandos asumen que el paquete que contiene a RegistroCuenta se encuentra en el directorio C:\ejemplos\cap17 en un equipo Windows.

La clase CrearArchivoTextoAhora examinemos la clase CrearArchivoTexto (figura 17.5). La línea 14 declara la variable Formatter llamada salida. Como vimos en la sección 17.2, un objeto Formatter muestra en pantalla objetos

Fig. 17.4 � La clase RegistroCuenta mantiene la información para una cuenta (parte 2 de 2).

50

51 // establece el apellido paterno

52 public void establecerApellidoPaterno( String apellido )

53 {

54 apellidoPaterno = apellido;

55 } // fin del método establecerApellidoPaterno

56

57 // obtiene el apellido paterno

58 public String obtenerApellidoPaterno()

59 {

60 return apellidoPaterno;

61 } // fin del método obtenerApellidoPaterno

62

63 // establece el saldo

64 public void establecerSaldo( double sal )

65 {

66 saldo = sal;

67 } // fin del método establecerSaldo

68

69 // obtiene el saldo

70 public double obtenerSaldo()

71 {

72 return saldo;

73 } // fin del método obtenerSaldo

74 } // fin de la clase RegistroCuenta

17.4 Archivos de texto de acceso secuencial 729

String con formato, usando las mismas herramientas de formato que el método System.out.printf. Un objeto Formatter puede enviar datos a varias ubicaciones, como la pantalla o a un archivo, como lo hacemos aquí. El objeto Formatter se instancia en la línea 21, en el método abrirArchivo (líneas 17 a 34). El constructor que se utiliza en la línea 21 recibe un argumento: un objeto String que contiene el nombre del archivo, incluyendo su ruta. Si no se especifica una ruta, como se da aquí el caso, la JVM asume que los archivos están en el directorio desde el cual se ejecutó el programa. Para los archivos de texto, utilizamos la extensión .txt. Si el archivo no existe, se creará. Si se abre un archivo existente, su contenido se trunca; todos los datos en el archivo se descartan. En este punto, el archivo se abre para escritura y el objeto Formatter resultante se puede utilizar para escribir datos en el archivo.

Fig. 17.5 � Escribir datos en un archivo de texto secuencial mediante la clase Formatter (parte 1 de 3).

1 // Fig. 17.5: CrearArchivoTexto.java

2 // Escribir datos en un archivo de texto secuencial mediante la clase Formatter.

3 import java.io.FileNotFoundException;

4 import java.lang.SecurityException;

5 import java.util.Formatter;

6 import java.util.FormatterClosedException;

7 import java.util.NoSuchElementException;

8 import java.util.Scanner;

9

10 import com.deitel.cap17.RegistroCuenta;

11

12 public class CrearArchivoTexto

13 {

14 private Formatter salida; // objeto usado para enviar texto al archivo

15

16 // permite al usuario abrir el archivo

17 public void abrirArchivo()

18 {

19 try

20 {

21 salida = new Formatter( “clientes.txt” ); // abre el archivo

22 } // fin de try

23 catch ( SecurityException securityException )

24 {

25 System.err.println(

26 “No tiene acceso de escritura a este archivo.” );

27 System.exit( 1 ); // termina el programa

28 } // fin de catch

29 catch ( FileNotFoundException fileNotFoundException )

30 {

31 System.err.println( “Error al abrir o crear el archivo.” );

32 System.exit( 1 ); // termina el programa

33 } // fin de catch

34 } // fin del método abrirArchivo

35

36 // agrega registros al archivo

37 public void agregarRegistros()

38 {

39 // objeto que se va a escribir en el archivo

40 RegistroCuenta registro = new RegistroCuenta();

730 Capítulo 17 Archivos, fl ujos y serialización de objetos

4142 Scanner entrada = new Scanner( System.in );4344 System.out.printf( “%s\n%s\n%s\n%s\n\n”,45 “Para terminar la entrada, escriba el indicador de fin de archivo ”,46 “cuando se le pida que escriba los datos de entrada.”,47 “En UNIX/Linux/Mac OS X escriba <ctrl> d y oprima Intro”,48 “En Windows escriba <ctrl> z y oprima Intro” );4950 System.out.printf( “%s\n%s”, 51 “Escriba el numero de cuenta (> 0), primer nombre, apellido paterno y saldo.”,52 “? ” );53

54 while ( entrada.hasNext() ) // itera hasta encontrar el indicador de fin de archivo

55 {56 try // envía valores al archivo57 {58 // obtiene los datos que se van a enviar59 registro.establecerCuenta( entrada.nextInt() ); // lee el número de cuenta60 registro.establecerPrimerNombre( entrada.next() ); // lee el primer nombre

61 registro.establecerApellidoPaterno( entrada.next() ); // lee el apellido paterno

62 registro.establecerSaldo( entrada.nextDouble() ); // lee el saldo6364 if ( registro.obtenerCuenta() > 0 )65 {66 // escribe el nuevo registro67 salida.format( “%d %s %s %.2f\n”, registro.obtenerCuenta(), 68 registro.obtenerPrimerNombre(), registro.obtenerApellidoPaterno(),69 registro.obtenerSaldo() );70 } // fin de if71 else72 {73 System.out.println(74 “El numero de cuenta debe ser mayor que 0.” );75 } // fin de else76 } // fin de try77 catch ( FormatterClosedException formatterClosedException )78 {79 System.err.println( “Error al escribir en el archivo.” );80 return;81 } // fin de catch82 catch ( NoSuchElementException elementException )83 {84 System.err.println( “Entrada invalida. Intente de nuevo.” );

85 entrada.nextLine(); // descarta la entrada para que el usuario intente de nuevo

86 } // fin de catch8788 System.out.printf( “%s %s\n%s”, “Escriba el numero de cuenta (> 0),”,89 “primer nombre, apellido paterno y saldo.”, “? ” );90 } // fin de while91 } // fin del método agregarRegistros92

Fig. 17.5 � Escribir datos en un archivo de texto secuencial mediante la clase Formatter (parte 2 de 3).

17.4 Archivos de texto de acceso secuencial 731

En las líneas 23 a 28 se maneja la excepción tipo SecurityException, que ocurre si el usuario no tiene permiso para escribir datos en el archivo. En las líneas 29 a 33 se maneja la excepción tipo FileNotFoundException, que ocurre si el archivo no existe y no se puede crear uno nuevo. Esta excep-ción también puede ocurrir si hay un error al abrir el archivo. En ambos manejadores de excepciones podemos llamar al método static System.exit, y pasarle el valor 1. Este método termina la aplicación. Un argumento de 0 para el método exit indica la terminación exitosa del programa. Un valor distinto de cero, como el 1 en este ejemplo, por lo general indica que ocurrió un error. Este valor se pasa a la ven-tana de comandos en la que se ejecutó el programa. El argumento es útil si el programa se ejecuta desde un archivo de procesamiento por lotes en los sistemas Windows, o una secuencia de comandos de shell en sistemas UNIX/Linux/Mac OS X. Los archivos de procesamiento por lotes y las secuen-cias de comandos de shell ofrecen una manera conveniente de ejecutar varios programas en secuencia. Cuando termina el primer programa, el siguiente programa empieza su ejecución. Es posible utilizar el argumento para el método exit en un archivo de procesamiento por lotes o secuencia de comandos de shell, para determinar si deben ejecutarse otros programas. Para obtener más información acerca de los archivos de procesamiento por lotes o las secuencias de comandos de shell, consulte la documenta-ción de su sistema operativo.

El método agregarRegistros (líneas 37 a 91) pide al usuario que introduzca los diversos cam-pos para cada registro, o la secuencia de teclas de fin de archivo cuando termine de introducir los datos. La figura 17.6 enlista las combinaciones de teclas para introducir el fin de archivo en varios sis-temas computacionales.

93 // cierra el file94 public void cerrarArchivo()95 {96 if ( salida != null )97 salida.close();98 } // fin del método cerrarArchivo99 } // fin de la clase CrearArchivoTexto

Fig. 17.5 � Escribir datos en un archivo de texto secuencial mediante la clase Formatter (parte 3 de 3).

Fig. 17.6 � Combinaciones de teclas de fin de archivo.

Sistema operativo Combinación de teclas

UNIX/Linux/Mac OS X <Enter> <Ctrl> d

Windows <Ctrl> z

En la línea 40 se crea un objeto RegistroCuenta, el cual se utilizará para almacenar los valores del registro actual introducido por el usuario. En la línea 42 se crea un objeto Scanner para leer la entra-da del usuario mediante el teclado. En las líneas 44 a 48 y 50 a 52 se pide al usuario que introduzca los datos.

En la línea 54 se utiliza el método hasNext de Scanner para determinar si se ha introducido la combinación de teclas de fin de archivo. El ciclo se ejecuta hasta que hasNext encuentra los indica-dores de fin de archivo.

En las líneas 59 a 62 se leen datos del usuario y se almacena la información del registro en el ob-jeto RegistroCuenta. Cada instrucción lanza una excepción tipo NoSuchElementException (que se maneja en las líneas 82 a 86) si los datos se encuentran en el formato incorrecto (por ejemplo, un ob-jeto String cuando se espera un valor int), o si no hay más datos que introducir. Si el número de cuenta es mayor que 0 (línea 64), la información del registro se escribe en clientes.txt (líneas 67 a 69) me-diante el método format, que puede efectuar un formato idéntico al del método System.out.printf, que se utilizó en muchos de los ejemplos de capítulos anteriores. El método format envía un objeto

732 Capítulo 17 Archivos, fl ujos y serialización de objetos

String con formato al destino de salida del objeto Formatter, en este caso el archivo clientes.txt. La cadena de formato “%d &s &s &.2f\n” indica que el registro actual se almacenará como un entero (el número de cuenta) seguido de un objeto String (el primer nombre), otra String (el apellido paterno) y un valor de punto flotante (el saldo). Cada pieza de información se separa de la siguiente mediante un espacio, y el valor tipo double (el saldo) se imprime en pantalla con dos dígitos a la derecha del punto decimal (como lo indica el .2 en %.2f). Los datos en el archivo de texto se pueden ver con un editor, o posteriormente mediante un programa diseñado para leer el archivo (sección 17.4.2).

Cuando se ejecutan las líneas 67 a 69, si se cierra el objeto Formatter se lanza una excepción tipo FormatterClosedException. Esta excepción se maneja en las líneas 77 a 81. [Nota: también puede enviar datos a un archivo de texto mediante la clase java.io.PrintWriter, la cual también cuenta con los mé-todos format y printf para imprimir datos con formato].

En las líneas 94 a 98 se declara el método cerrarArchivo, el cual cierra el objeto Formatter y el archivo de salida subyacente. En la línea 97 se cierra el objeto, mediante una llamada simple al método close. Si el método close no se llama en forma explícita, el sistema operativo comúnmente cierra el archivo cuando el programa termina de ejecutarse; éste es un ejemplo de las “tareas de mantenimiento” del sistema operativo. Sin embargo, siempre debemos cerrar un archivo en forma explícita cuando ya no lo necesitemos.

Caracteres separadores de línea específicos de la plataformaEn las líneas 67 a 69 se imprime una línea de texto seguida de una nueva línea (\n). Si usa un editor de texto para abrir el archivo clientes.txt que se produce, tal vez no todos los registros se muestre en una línea separada. Por ejemplo, en el Bloc de notas (Microsoft Windows) los usuarios verán una línea continua de texto. Esto ocurre debido a que las distintas plataformas usan distintos caracteres separadores de líneas. En UNIX/Linux/Mac OS X, el separador de líneas es una nueva línea (\n). En Windows, es una combinación de retorno de carro y avance de línea, lo cual se representa como \r\n. Puede usar el especificador de formato %n en una cadena de control de formato para imprimir un se-parador de línea específico de la plataforma, con lo cual asegura que sea posible abrir y ver el archivo de texto correctamente en un editor de texto para la plataforma en la que se creó el archivo. El método System.out.println imprime un separador de líneas específico de la plataforma después de su argu-mento. Además, sin importar el separador de línea que se utilice en un archivo de texto, un programa de Java puede aún reconocer las líneas de texto y leerlas.

La clase PruebaCrearArchivoTextoLa figura 17.7 ejecuta el programa. En la línea 8 se crea un objeto CrearArchivoTexto, el cual se utiliza posteriormente para abrir, agregar registros y cerrar el archivo (líneas 10 a 12). Los datos de ejemplo para esta aplicación se muestran en la figura 17.8. En la ejecución de ejemplo para este pro-grama, el usuario introduce información para cinco cuentas, y después introduce el fin de archivo para indicar que ha terminado de introducir datos. La ejecución de ejemplo no muestra cómo apare-cen realmente los registros de datos en el archivo. En la siguiente sección, para verificar que el archivo se haya creado sin problemas, presentamos un programa que lee el archivo e imprime su contenido. Como es un archivo de texto, también puede verificar la información con sólo abrir el archivo en un editor de texto.

Fig. 17.7 � Prueba de la clase CrearArchivoTexto (parte 1 de 2).

1 // Fig. 17.7: PruebaCrearArchivoTexto.java

2 // Prueba de la clase CrearArchivoTexto.

3

4 public class PruebaCrearArchivoTexto

5 {

17.4 Archivos de texto de acceso secuencial 733

17.4.2 Cómo leer datos de un archivo de texto de acceso secuencialLos datos se almacenan en archivos, para poder procesarlos según sea necesario. En la sección 17.4.1 demostramos cómo crear un archivo para acceso secuencial. Esta sección muestra cómo leer los datos en forma secuencial desde un archivo de texto. Demostraremos cómo puede utilizarse la clase Scanner para recibir datos de un archivo, en vez del teclado.

La aplicación de las figuras 17.9 y 17.10 lee registros del archivo “clientes.txt” creado por la aplicación de la sección 17.4.1 y muestra el contenido de los registros. En la línea 13 de la figura 17.9 se declara un objeto Scanner, que se utilizará para obtener los datos de entrada del archivo.

Fig. 17.7 � Prueba de la clase CrearArchivoTexto (parte 2 de 2).

6 public static void main( String[] args )

7 {

8 CrearArchivoTexto aplicacion = new CrearArchivoTexto();

9

10 aplicacion.abrirArchivo();

11 aplicacion.agregarRegistros();

12 aplicacion.cerrarArchivo();

13 } // fin de main

14 } // fin de la clase PruebaCrearArchivoTexto

Para terminar la entrada, escriba el indicador de fin de archivo

cuando se le pida que escriba los datos de entrada.

En UNIX/Linux/Mac OS X escriba <ctrl> d y oprima Intro

En Windows escriba <ctrl> z y oprima Intro

Escriba el numero de cuenta (> 0), primer nombre, apellido paterno y saldo.

? 100 Bob Jones 24.98

Escriba el numero de cuenta (>0), primer nombre, apellido paterno y saldo.

? 200 Steve Doe -345.67

Escriba el numero de cuenta (>0), primer nombre, apellido paterno y saldo.

? 300 Pam White 0.00

Escriba el numero de cuenta (>0), primer nombre, apellido paterno y saldo.

? 400 Sam Stone -42.16

Escriba el numero de cuenta (>0), primer nombre, apellido paterno y saldo.

? 500 Sue Rich 224.62

Escriba el numero de cuenta (>0), primer nombre, apellido paterno y saldo.

? ^Z

Fig. 17.8 � Datos de ejemplo para el programa de las figuras 17.5 a 17.7.

Datos de ejemplo

100 Bob Jones 24.98

200 Steve Doe -345.67

300 Pam White 0.00

400 Sam Stone -42.16

500 Sue Rich 224.62

734 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.9 � Lectura de un archivo secuencial mediante un objeto Scanner (parte 1 de 2).

1 // Fig. 17.9: LeerArchivoTexto.java 2 // Este programa lee un archivo de texto y muestra cada registro. 3 import java.io.File; 4 import java.io.FileNotFoundException; 5 import java.lang.IllegalStateException; 6 import java.util.NoSuchElementException; 7 import java.util.Scanner; 8 9 import com.deitel.cap17.RegistroCuenta;1011 public class LeerArchivoTexto12 {13 private Scanner entrada;1415 // permite al usuario abrir el archivo16 public void abrirArchivo()17 {18 try19 {20 entrada = new Scanner( new File( “clientes.txt” ) );21 } // fin de try22 catch ( FileNotFoundException fileNotFoundException )23 {24 System.err.println( “Error al abrir el archivo.” );25 System.exit( 1 );26 } // fin de catch27 } // fin del método abrirArchivo2829 // lee registro del archivo30 public void leerRegistros()31 {32 // objeto que se va a escribir en la pantalla33 RegistroCuenta registro = new RegistroCuenta();3435 System.out.printf( “%-9s%-15s%-18s%10s\n”, “Cuenta”,36 “Primer nombre”, “Apellido paterno”, “Saldo” );3738 try // lee registros del archivo, usando el objeto Scanner39 {40 while ( entrada.hasNext() )41 {42 registro.establecerCuenta( entrada.nextInt() ); // lee el número de cuenta43 registro.establecerPrimerNombre( entrada.next() ); // lee el primer nombre

44 registro.establecerApellidoPaterno( entrada.next() ); // lee el apellido paterno

45 registro.establecerSaldo( entrada.nextDouble() ); // lee el saldo4647 // muestra el contenido del registro48 System.out.printf( “<%-9d%-15s%-18s%10.2f\n”,49 registro.obtenerCuenta(), registro.obtenerPrimerNombre(),50 registro.obtenerApellidoPaterno(), registro.obtenerSaldo() );51 } // fin de while52 } // fin de try

17.4 Archivos de texto de acceso secuencial 735

El método abrirArchivo (líneas 16 a 27) abre el archivo en modo de lectura, creando una instan-cia de un objeto Scanner en la línea 20. Pasamos un objeto File al constructor, el cual especifica que el objeto Scanner leerá datos del archivo “clientes.txt” ubicado en el directorio desde el que se ejecu-ta la aplicación. Si no puede encontrarse el archivo, ocurre una excepción tipo FileNotFoundException. La excepción se maneja en las líneas 22 a 26.

Fig. 17.9 � Lectura de un archivo secuencial mediante un objeto Scanner (parte 2 de 2).

53 catch ( NoSuchElementException elementException )54 {55 System.err.println( “El archivo no esta bien formado.” );56 entrada.close();57 System.exit( 1 );58 } // fin de catch59 catch ( IllegalStateException stateException )60 {61 System.err.println( “Error al leer del archivo.” );62 System.exit( 1 );63 } // fin de catch64 } // fin del método leerRegistros6566 // cierra el archivo y termina la aplicación67 public void cerrarArchivo()68 {69 if ( entrada != null )70 entrada.close(); // cierra el archivo71 } // fin del método cerrarArchivo72 } // fin de la clase LeerArchivoTexto

Fig. 17.10 � Prueba de la clase LeerArchivoTexto.

1 // Fig. 17.10: PruebaLeerArchivoTexto.java

2 // Este programa prueba la clase LeerArchivoTexto.

3

4 public class PruebaLeerArchivoTexto

5 {

6 public static void main( String[] args )

7 {

8 LeerArchivoTexto aplicacion = new LeerArchivoTexto();

9

10 aplicacion.abrirArchivo();

11 aplicacion.leerRegistros();

12 aplicacion.cerrarArchivo();

13 } // fin de main

14 } // fin de la clase PruebaLeerArchivoTexto

Cuenta Primer nombre Apellido paterno Saldo

100 Bob Jones 24.98

200 Steve Doe -345.67

300 Pam White 0.00

400 Sam Stone -42.16

500 Sue Rich 224.62

736 Capítulo 17 Archivos, fl ujos y serialización de objetos

El método leerRegistros (líneas 30 a 64) lee y muestra registros del archivo. En la línea 33 se crea el objeto RegistroCuenta llamado registro, para almacenar la información del registro actual. En las líneas 35 y 36 se muestran encabezados para las columnas, en los resultados de la aplicación. En las líneas 40 a 51 se leen datos del archivo hasta llegar al marcador de fin de archivo (en cuyo caso, el método has-Next devolverá false en la línea 40). En las líneas 42 a 45 se utilizan los métodos nextInt, next y next-Double de Scanner para recibir un int (el número de cuenta), dos objetos String (el primer nombre y el apellido paterno) y un valor double (el saldo). Cada registro es una línea de datos en el archivo. Los valo-res se almacenan en el objeto registro. Si la información en el archivo no está bien formada (por ejemplo, que haya un apellido paterno en donde debe haber un saldo), se produce una excepción tipo NoSuchEle-mentException al momento de introducir el registro. Esta excepción se maneja en las líneas 53 a 58. Si el objeto Scanner se cerró antes de introducir los datos, se produce una excepción tipo IllegalStateEx-ception (que se maneja en las líneas 59 a 63). Si no ocurren excepciones, la información del registro se muestra en pantalla (líneas 48 a 50). Observe en la cadena de formato de la línea 48 que el número de cuenta, primer nombre y apellido paterno están justificados a la izquierda, mientras que el saldo está justificado a la derecha y se imprime con dos dígitos de precisión. Cada iteración del ciclo introduce una línea de texto del archivo de texto, la cual representa un registro.

En las líneas 67 a 71 se define el método cerrarArchivo, el cual cierra el objeto Scanner. El método main se define en la figura 17.10, en las líneas 6 a 13. En la línea 8 se crea un objeto LeerArchivoTexto, el cual se utiliza entonces para abrir, agregar registros y cerrar el archivo (líneas 10 a 12).

17.4.3 Caso de estudio: un programa de solicitud de créditoPara obtener datos secuencialmente de un archivo, por lo general los programas empiezan desde el principio del archivo y leen todos los datos en forma consecutiva, hasta encontrar la información desea-da. Podría ser necesario procesar el archivo secuencialmente varias veces (desde el principio del archivo) durante la ejecución de un programa. La clase Scanner no proporciona la habilidad de reposicionarse hasta el principio del archivo. Si es necesario leer el archivo de nuevo, el programa debe cerrar el archivo y volver a abrirlo.

El programa de las figuras 17.11 a 17.13 permite a un gerente de créditos obtener listas de clien-tes con saldos de cero (es decir, los clientes que no deben dinero), saldos con crédito (es decir, los clientes a quienes la compañía les debe dinero) y saldos con débito (es decir, los clientes que deben dinero a la compañía por los bienes y servicios recibidos en el pasado). Un saldo con crédito es un monto negativo, y un saldo con débito es un monto positivo.

La enumeración OpcionMenuEmpezamos por crear un tipo enum (figura 17.11) para definir las distintas opciones del menú que tendrá el usuario. Las opciones y sus valores se enlistan en las líneas 7 a 10. El método obtenerValor (líneas 19 a 22) obtiene el valor de una constante enum específica.

Fig. 17.11 � Enumeración para las opciones del menú del programa de consulta de crédito (parte 1 de 2).

1 // Fig. 17.11: OpcionMenu.java

2 // Enumeración para las opciones del programa de consulta de crédito.

3

4 public enum OpcionMenu

5 {

6 // declara el contenido del tipo enum

7 SALDO_CERO( 1 ),

8 SALDO_CREDITO( 2 ),

9 SALDO_DEBITO( 3 ),

10 FIN( 4 );

17.4 Archivos de texto de acceso secuencial 737

La clase ConsultaCreditoLa figura 17.12 contiene la funcionalidad para el programa de consulta de crédito, y la figura 17.13 contiene el método main que ejecuta el programa. Este programa muestra un menú de texto y permite al gerente de créditos introducir una de tres opciones para obtener información sobre un crédito. La opción 1 (SALDO_CERO) muestra las cuentas con saldos de cero. La opción 2 (SALDO_CREDITO) muestra las cuentas con saldos con crédito. La opción 3 (SALDO_DEBITO) muestra las cuentas con saldos con débito. La opción 4 (FIN) termina la ejecución del programa.

Fig. 17.11 � Enumeración para las opciones del menú del programa de consulta de crédito (parte 2 de 2).

Fig. 17.12 � Programa de consulta de crédito (parte 1 de 4).

1 // Fig. 17.12: ConsultaCredito.java

2 // Este programa lee un archivo secuencialmente y muestra su

3 // contenido con base en el tipo de cuenta que solicita el usuario

4 // (saldo con crédito, saldo con débito o saldo de cero).

5 import java.io.File;

6 import java.io.FileNotFoundException;

7 import java.lang.IllegalStateException;

8 import java.util.NoSuchElementException;

9 import java.util.Scanner;

10

11 import com.deitel.cap17.RegistroCuenta;

12

13 public class ConsultaCredito

14 {

15 private OpcionMenu tipoCuenta;

16 private Scanner entrada;

17 private final static OpcionMenu[] opciones = { OpcionMenu.SALDO_CERO,

18 OpcionMenu.SALDO_CREDITO, OpcionMenu.SALDO_DEBITO,

19 OpcionMenu.FIN };

20

21 // lee los registros del archivo y muestra sólo los registros del tipo apropiado

22 private void leerRegistros()

23 {

24 // objeto que se va a escribir en el archivo

25 RegistroCuenta registro = new RegistroCuenta();

11

12 private final int valor; // opción actual del menú

13

14 // constructor

15 OpcionMenu( int valorOpcion )

16 {

17 valor = valorOpcion;

18 } // fin del constructor del tipo enum OpcionMenu

19

20 // devuelve el valor de una constante

21 public int obtenerValor()

22 {

23 return valor;

24 } // fin del método obtenerValor

25 } // fin del tipo enum OpcionMenu

738 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.12 � Programa de consulta de crédito (parte 2 de 4).

2627 try // lee registros28 { 29 // abre el archivo para leer desde el principio30 entrada = new Scanner( new File( “clientes.txt” ) );3132 while ( entrada.hasNext() ) // recibe los valores del archivo33 {34 registro.establecerCuenta( entrada.nextInt() ); // lee número de cuenta35 registro.establecerPrimerNombre( entrada.next() ); // lee primer nombre

36 registro.establecerApellidoPaterno( entrada.next() ); // lee apellido paterno

37 registro.establecerSaldo( entrada.nextDouble() ); // lee saldo3839 // si el tipo de cuenta es apropiado, muestra el registro40 if ( debeMostrar( registro.obtenerSaldo() ) )41 System.out.printf( “%-10d%-12s%-12s%10.2f\n”,42 registro.obtenerCuenta(), registro.obtenerPrimerNombre(),43 registro.obtenerApellidoPaterno(), registro.obtenerSaldo() );44 } // fin de while45 } // fin de try46 catch ( NoSuchElementException elementException )47 {48 System.err.println( “El archivo no esta bien formado.” );49 entrada.close();50 System.exit( 1 );51 } // fin de catch52 catch ( IllegalStateException stateException )53 {54 System.err.println( “Error al leer del archivo.” );55 System.exit( 1 );56 } // fin de catch57 catch ( FileNotFoundException fileNotFoundException )58 {59 System.err.println( “No se puede encontrar el archivo.” );60 System.exit( 1 );61 } // fin de catch62 finally63 {64 if ( entrada != null )65 entrada.close(); // cierra el objeto Scanner y el archivo66 } // fin de finally67 } // fin del método leerRegistros6869 // usa el tipo de registro para determinar si el registro debe mostrarse70 private boolean debeMostrar( double saldo )71 {72 if ( ( tipoCuenta == OpcionMenu.SALDO_CREDITO )73 && ( saldo < 0 ) )74 return true;7576 else if ( ( tipoCuenta == OpcionMenu.SALDO_DEBITO )77 && ( saldo > 0 ) )78 return true;

17.4 Archivos de texto de acceso secuencial 739

Fig. 17.12 � Programa de consulta de crédito (parte 3 de 4).

7980 else if ( ( tipoCuenta == OpcionMenu.SALDO_CERO )

81 && ( saldo == 0 ) )

82 return true;

8384 return false;

85 } // fin del método debeMostrar

8687 // obtiene solicitud del usuario

88 private OpcionMenu obtenerSolicitud()

89 {

90 Scanner textoEnt = new Scanner( System.in );

91 int solicitud = 1;

9293 // muestra opciones de solicitud

94 System.out.printf( “\n%s\n%s\n%s\n%s\n%s\n”,

95 “Escriba solicitud”, “ 1 - Lista de cuentas con saldos de cero”,

96 “ 2 - Lista de cuentas con saldos con credito”,

97 “ 3 - Lista de cuentas con saldos con debito”, “ 4 - Finalizar ejecucion” );

9899 try // trata de recibir la opción del menú

100 {

101 do // recibe solicitud del usuario

102 {

103 System.out.print( “\n? ” );

104 solicitud = textoEnt.nextInt();

105 } while ( ( solicitud < 1 ) || ( solicitud > 4 ) );

106 } // fin de try

107 catch ( NoSuchElementException elementException )

108 {

109 System.err.println( “Entrada invalida.” );

110 System.exit( 1 );

111 } // fin de catch

112113 return opciones[ solicitud - 1 ]; // devuelve valor de enum para la opción

114 } // fin del método obtenerSolicitud

115116 public void procesarSolicitudes()

117 {

118 // obtiene la solicitud del usuario (saldo de cero, con crédito o con débito)

119 tipoCuenta = obtenerSolicitud();

120121 while ( tipoCuenta != OpcionMenu.FIN )

122 {

123 switch ( tipoCuenta )

124 {

125 case SALDO_CERO:

126 System.out.println( “\nCuentas con saldos de cero:\n” );

127 break;

128 case SALDO_CREDITO:

129 System.out.println( “\nCuentas con saldos con credito:\n” );

130 break;

740 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.12 � Programa de consulta de crédito (parte 4 de 4).

131 case SALDO_DEBITO:

132 System.out.println( “\nCuentas con saldos con debito:\n” );

133 break;

134 } // fin de switch

135

136 leerRegistros();

137 tipoCuenta = obtenerSolicitud();

138 } // fin de while

139 } // fin del método procesarSolicitudes

140 } // fin de la clase ConsultaCredito

Fig. 17.13 � Prueba de la clase ConsultaCredito.

Fig. 17.14 � Salida de ejemplo del programa de consulta de crédito de la figura 17.13 (parte 1 de 2).

1 // Fig. 17.13: PruebaConsultaCredito.java

2 // Este programa prueba la clase ConsultaCredito.

3

4 public class PruebaConsultaCredito

5 {

6 public static void main( String[] args )

7 {

8 ConsultaCredito aplicacion = new ConsultaCredito();

9 aplicacion.procesarSolicitudes();

10 } // fin de main

11 } // fin de la clase PruebaConsultaCredito

Escriba solicitud 1 - Lista de cuentas con saldos de cero 2 - Lista de cuentas con saldos con credito 3 - Lista de cuentas con saldos con debito 4 - Finalizar ejecucion

? 1

Cuentas con saldos de cero:

300 Pam White 0.00

Escriba solicitud 1 - Lista de cuentas con saldos de cero 2 - Lista de cuentas con saldos con credito 3 - Lista de cuentas con saldos con debito 4 - Finalizar ejecucion

? 2

Cuentas con saldos con credito:

200 Steve Doe -345.67400 Sam Stone -42.16

17.4 Archivos de texto de acceso secuencial 741

Para recolectar la información de los registros, se lee todo el archivo completo y se determina si cada uno de los registro cumple o no con los criterios para el tipo de cuenta seleccionado . El método procesarSolicitudes (líneas 116 a 139 de la figura 17.12) llama al método obtenerSolicitud para mostrar las opciones del menú (línea 119), traduce el número introducido por el usuario en un objeto OpcionMenu y almacena el resultado en la variable OpcionMenu llamada tipoCuenta. En las líneas 121 a 138 se itera hasta que el usuario especifique que el programa debe terminar. En las líneas 123 a 134 se muestra un encabezado para imprimir el conjunto actual de registros en la pantalla. En la lí-nea 136 se hace una llamada al método leerRegistros (líneas 22 a 67), el cual itera a través del ar-chivo y lee todos los registros.

La línea 30 del método leerRegistros abre el archivo en modo de lectura con un objeto Scanner. El archivo se abrirá en modo de lectura con un nuevo objeto Scanner cada vez que se haga una llamada a este método, para que podamos leer de nuevo desde el principio del archivo. En las líneas 34 a 37 se lee un registro. En la línea 40 se hace una llamada al método debeMostrar (líneas 70 a 85), para determi-nar si el registro actual cumple con el tipo de cuenta solicitado. Si debeMostrar devuelve true, el pro-grama muestra la información de la cuenta. Al llegar al marcador de fin de archivo, el ciclo termina y en la línea 65 se hace una llamada al método close de Scanner para cerrar el objeto Scanner y el archivo. Observe que esto ocurre en un bloque finally, el cual se ejecutará sin importar que se haya leído o no el archivo con éxito. Una vez que se hayan leído todos los registros, el control regresa al método procesarSolicitudes y se hace una llamada otra vez al método obtenerSolicitud (línea 137) para obtener la siguiente opción de menú del usuario. La figura 17.13 contiene el método main, y llama al método procesarSolicitudes en la línea 9.

17.4.4 Actualización de archivos de acceso secuencialEn muchos archivos secuenciales, los datos no se pueden modificar sin el riesgo de destruir otros datos en el archivo. Por ejemplo, si el nombre “White” tuviera que cambiarse a “Worthington”, el nombre anterior no podría simplemente sobrescribirse, debido a que el nuevo nombre requiere más espacio. El registro para White se escribió en el archivo como

300 Pam White 0.00

Si el registro se sobrescribe empezando en la misma ubicación en el archivo que utiliza el nuevo nombre, el registro será

300 Pam Worthington 0.00

El nuevo registro es más extenso (tiene más caracteres) que el registro original. Los caracteres más allá de la segunda “o” en “Worthington” sobrescribirán el principio del siguiente registro secuencial

Fig. 17.14 � Salida de ejemplo del programa de consulta de crédito de la figura 17.13 (parte 2 de 2).

Escriba solicitud 1 - Lista de cuentas con saldos de cero 2 - Lista de cuentas con saldos con credito 3 - Lista de cuentas con saldos con debito 4 - Finalizar ejecucion

? 3

Cuentas con saldos con debito:

100 Bob Jones 24.98500 Sue Rich 224.62

? 4

742 Capítulo 17 Archivos, fl ujos y serialización de objetos

en el archivo. El problema aquí es que los campos en un archivo de texto (y por ende, los registros) pueden variar en tamaño. Por ejemplo, 7, 14, –117, 2074 y 27383 son todos valores int almacenados en el mismo número de bytes (4) internamente, pero son campos con distintos tamaños cuando se muestran en la pantalla, o se escriben en un archivo como texto. Por lo tanto, los registros en un archi-vo de acceso secuencial comúnmente no se actualizan por partes. En vez de ello, por lo general se sobrescribe todo el archivo. Para realizar el cambio anterior, los registros antes de 300 Pam White 0.00 se copian a un nuevo archivo, se escribe el nuevo registro (que puede tener un tamaño distinto al que está sustituyendo) y se copian los registros después de 300 Pam White 0.00 al nuevo archivo. Es incon-veniente actualizar sólo un registro, pero razonable si una porción substancial de los registros nece-sitan actualización.

17.5 Serialización de objetosEn la sección 17.4 demostramos cómo escribir los campos individuales de un objeto RegistroCuen-ta en un archivo como texto, y cómo leer esos campos de un archivo y colocar sus valores en un ob-jeto RegistroCuenta en la memoria. En los ejemplos, se usó RegistroCuenta para agregar la infor-mación de un registro. Cuando las variables de instancia de un objeto RegistroCuenta se enviaban a un archivo en disco, se perdía cierta información, como el tipo de cada valor. Por ejemplo, si se lee el valor “3” de un archivo, no hay forma de saber si el valor proviene de un int, un String o un double. En un disco sólo tenemos los datos, no la información sobre los tipos. Si el programa que va a leer estos datos “sabe” a qué tipo de objeto corresponden, entonces simplemente se leen y se colocan en objetos de ese tipo. Por ejemplo, en la sección 17.4.2 sabemos que introduciremos un int (el número de cuenta), seguido de dos objetos String (el primer nombre y el apellido paterno) y un double (el saldo). También sabemos que estos valores se separan mediante espacios, y sólo se coloca un registro en cada línea. Algunas veces no sabremos con exactitud cómo se almacenan los datos en un archivo. En tales casos, sería conveniente poder escribir o leer un objeto completo de un archivo. Java cuenta con dicho mecanismo, el cual se conoce como serialización de objetos. Un objeto serializado es un objeto que se representa como una secuencia de bytes, la cual incluye los datos del objeto, así como información acerca del tipo del objeto y los tipos de los datos almacenados en el mismo. Una vez que se escribe un objeto serializado en un archivo, se puede leer de ese archivo y deserializarse; es decir, la información del tipo y los bytes que representan al objeto y sus datos se puede utilizar para recrear el objeto en memoria.

Observación de ingeniería de software 17.1El mecanismo de serialización realiza copias exactas de los objetos. Ésta es una forma sim-ple de clonar objetos sin tener que sobrescribir el método clone de Object.

Las clases ObjectInputStream y ObjectOutputStreamLas clases ObjectInputStream y ObjectOutputStream, que implementan en forma respectiva a las interfaces ObjectInput y ObjectOutput, permiten leer/escribir objetos completos de/en un flujo (posiblemente un archivo). Para utilizar la serialización con los archivos, inicializamos los objetos ObjectInputStream y ObjectOutputStream con objetos flujo que pueden leer y escribir informa-ción desde/hacia los archivos; objetos de las clases FileInputStream y FileOutputStream, en for-ma respectiva. La acción de inicializar objetos flujo con otros objetos flujo de esta forma se conoce algunas veces como envoltura: el nuevo objeto flujo que se va a crear envuelve al objeto flujo especificado como un argumento del constructor. Por ejemplo, para envolver un objeto File-InputStream en un objeto ObjectInputStream, pasamos el objeto FileInputStream al constructor de ObjectInputStream.

17.5 Serialización de objetos 743

Las interfaces ObjectOutput y ObjectInputLa interfaz ObjectOutput contiene el método writeObject, el cual toma un objeto Object como un argumento y escribe su información a un objeto OutputStream. Una clase que implementa a la interfaz ObjectOutput (como ObjectOutputStream) declara este método y se asegura de que el objeto que se va a producir implemente la interfaz Serializable (que veremos en breve). De manera correspondiente, la interfaz ObjectInput contiene el método readObject, el cual lee y devuelve una referencia a un objeto Object de un objeto InputStream. Una vez que se lee un objeto, su referencia puede convertirse en el tipo actual del objeto. Como veremos en el capítulo 27, las aplicaciones que se comunican a través de una red (como Internet) también pueden transmitir objetos completos a través de la red.

17.5.1 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos

En esta sección y en la sección 17.5.2 vamos a crear y manipular archivos de acceso secuencial, usando la serialización de objetos. La serialización de objetos que mostraremos aquí se realiza mediante flujos basa-dos en bytes, de manera que los archivos secuenciales que se creen y manipulen serán archivos binarios. Recuerde que, por lo general, los archivos binarios no se pueden ver en los editores de texto estándar. Por esta razón, escribimos una aplicación separada que sabe cómo leer y mostrar objetos serializados. Empe-zaremos por crear y escribir objetos serializados a un archivo de acceso secuencial. El ejemplo es similar al de la sección 17.4, por lo que sólo nos enfocaremos en las nuevas características.

Definición de la clase RegistroCuentaSerializablePara empezar, modificaremos nuestra clase RegistroCuenta de manera que los objetos de esta clase puedan serializarse. La clase RegistroCuentaSerializable (figura 17.15) implementa a la interfaz Serializable (línea 7), la cual permite serializar y deserializar los objetos de la clase Registro-CuentaSerializable con objetos ObjectOutputStream y ObjectInputStream, respectivamente. La interfaz Serializable es una interfaz de marcado. Dicha interfaz no contiene métodos. Una clase que implementa a Serializable se marca como objeto Serializable. Esto es importante, ya que un objeto ObjectOutputStream no enviará un objeto como salida a menos que sea un objeto Seria-lizable, lo cual es el caso para cualquier objeto de una clase que implemente a Serializable.

Fig. 17.15 � La clase RegistroCuentaSerializable para los objetos serializables (parte 1 de 3).

1 // Fig. 17.15: RegistroCuentaSerializable.java

2 // La clase RegistroCuentaSerializable para objetos serializables.

3 package com.deitel.cap17; // empaquetada para reutilizarla

4

5 import java.io.Serializable;

6

7 public class RegistroCuentaSerializable implements Serializable

8 {

9 private int cuenta;

10 private String primerNombre;

11 private String apellidoPaterno;

12 private double saldo;

13

14 // el constructor sin argumentos llama al otro constructor con valores predeterminados

15 public RegistroCuentaSerializable()

16 {

17 this( 0, “”, “”, 0.0 );

18 } // fin del constructor de RegistroCuentaSerializable sin argumentos

744 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.15 � La clase RegistroCuentaSerializable para los objetos serializables (parte 2 de 3).

1920 // el constructor con cuatro argumentos inicializa un registro21 public RegistroCuentaSerializable(22 int cta, String nombre, String apellido, double sal )23 {24 establecerCuenta( cta );25 establecerPrimerNombre( nombre );26 establecerApellidoPaterno( apellido );27 establecerSaldo( sal );28 } // fin del constructor de RegistroCuentaSerializable con cuatro argumentos2930 // establece el número de cuenta31 public void establecerCuenta( int cta )32 {33 cuenta = cta;34 } // fin del método establecerCuenta3536 // obtiene el número de cuenta37 public int obtenerCuenta() 38 { 39 return cuenta; 40 } // fin del método obtenerCuenta4142 // establece el primer nombre 43 public void establecerPrimerNombre( String nombre )44 {45 primerNombre = nombre;46 } // fin del método establecerPrimerNombre4748 // obtiene el primer nombre 49 public String obtenerPrimerNombre() 50 { 51 return primerNombre; 52 } // fin del método obtenerPrimerNombre5354 // establece el apellido paterno 55 public void establecerApellidoPaterno( String apellido )56 {57 apellidoPaterno = apellido;58 } // fin del método establecerApellidoPaterno5960 // obtiene el apellido paterno61 public String obtenerApellidoPaterno() 62 {63 return apellidoPaterno; 64 } // fin del método obtenerApellidoPaterno6566 // establece el saldo 67 public void establecerSaldo( double sal )68 {69 saldo = sal;70 } // fin del método establecerSaldo71

17.5 Serialización de objetos 745

En una clase Serializable, cada variable de instancia debe ser Serializable. Cualquier variable de instancia que no sea serializable debe declararse como transient, para indicar que debe ignorarse durante el proceso de serialización. De manera predeterminada, todas las variables de tipos primitivos son serializables. Para las variables de tipos de referencias, hay que verificar la documentación de la clase (y posiblemente de sus superclases) para asegurar que el tipo sea Serializable. Por ejemplo, los objetos String son Serializable. De manera predeterminada, los arreglos son serializables; no obstante, en un arreglo de tipo por referencia, los objetos referenciados tal vez no lo sean. La clase RegistroCuentaSerializable contiene los miembros de datos private llamados cuenta, primer-Nombre, apellidoPaterno y saldo; todos ellos son Serializable. Esta clase también proporciona métodos public establecer y obtener para acceder a los campos private.

Escritura de objetos serializados en un archivo de acceso secuencialAhora hablaremos sobre el código que crea el archivo de acceso secuencial (figuras 17.16 y 17.17). Aquí nos concentraremos sólo en los nuevos conceptos. Como dijimos en la sección 17.2, un programa puede abrir un archivo creando un objeto de las clases de flujo FileInputStream o FileOuptutStream. En este ejemplo, el archivo se abrirá en modo de salida, por lo que el programa crea un objeto Fi-leOutputStream (línea 21 de la figura 17.16). El argumento String que se pasa al constructor de FileOutputStream representa el nombre y la ruta del archivo que se va a abrir. Los archivos existentes que se abren en modo de salida de esta forma se truncan. Elegimos la extensión de archivo .ser para los archivos binarios que contienen objetos serializados, pero esto no es obligatorio.

Error común de programación 17.2Es un error lógico abrir un archivo existente en modo de salida cuando, de hecho, el usua-rio desea preservar ese archivo. La clase FileOutputStream cuenta con un constructor sobrecargado que nos permite abrir un archivo y adjuntar datos al final del mismo. Esto preserva el contenido del archivo.

Fig. 17.15 � La clase RegistroCuentaSerializable para los objetos serializables (parte 3 de 3).

Fig. 17.16 � Archivo secuencial creado mediante ObjectOutputStream (parte 1 de 3).

72 // obtiene el saldo

73 public double obtenerSaldo()

74 {

75 return saldo;

76 } // fin del método obtenerSaldo

77 } // fin de la clase RegistroCuentaSerializable

1 // Fig. 17.16: CrearArchivoSecuencial.java

2 // Escritura de objetos en forma secuencial a un archivo, con la clase ObjectOutputStream.

3 import java.io.FileOutputStream;

4 import java.io.IOException;

5 import java.io.ObjectOutputStream;

6 import java.util.NoSuchElementException;

7 import java.util.Scanner;

8

9 import com.deitel.cap17.RegistroCuentaSerializable;

10

11 public class CrearArchivoSecuencial

12 {

13 private ObjectOutputStream salida; // envía los datos a un archivo

746 Capítulo 17 Archivos, fl ujos y serialización de objetos

1415 // permite al usuario especificar el nombre del archivo

16 public void abrirArchivo()

17 {

18 try // abre el archivo

19 {

20 salida = new ObjectOutputStream(

21 new FileOutputStream( “clientes.ser” ) );

22 } // fin de try

23 catch ( IOException ioException )

24 {

25 System.err.println( “Error al abrir el archivo.” );

26 } // fin de catch

27 } // fin del método abrirArchivo

2829 // agrega registros al archivo

30 public void agregarRegistros()

31 {

32 RegistroCuentaSerializable registro; // objeto que se va a escribir al archivo

33 int numeroCuenta = 0; // número de cuenta para el objeto registro

34 String primerNombre; // primer nombre para el objeto registro

35 String apellidoPaterno; // apellido paterno para el objeto registro

36 double saldo; // saldo para el objeto registro

3738 Scanner entrada = new Scanner( System.in );

3940 System.out.printf( “%s\n%s\n%s\n%s\n\n”,

41 “Para terminar de introducir datos, escriba el indicador de fin de archivo”,

42 “Cuando se le pida que introduzca los datos.”,

43 “En UNIX/Linux/Mac OS X escriba <ctrl> d y oprima Intro”,

44 “En Windows escriba <ctrl> z y oprima Intro” );

4546 System.out.printf( "%s\n%s",

47 “Escriba el numero de cuenta (> 0), primer nombre, apellido y saldo.”,

48 “? ” );

4950 while ( entrada.hasNext() ) // itera hasta el indicador de fin de archivo

51 {

52 try // envía los valores al archivo

53 {

54 numeroCuenta = entrada.nextInt(); // lee el número de cuenta

55 primerNombre = entrada.next(); // lee el primer nombre

56 apellidoPaterno = entrada.next(); // lee el apellido paterno

57 saldo = entrada.nextDouble(); // lee el saldo

5859 if ( numeroCuenta > 0 )

60 {

61 // crea un registro nuevo

62 registro = new RegistroCuentaSerializable( numeroCuenta,

63 primerNombre, apellidoPaterno, saldo );

64 salida.writeObject( registro ); // envía el registro como salida65 } // fin de if

Fig. 17.16 � Archivo secuencial creado mediante ObjectOutputStream (parte 2 de 3).

17.5 Serialización de objetos 747

Fig. 17.16 � Archivo secuencial creado mediante ObjectOutputStream (parte 3 de 3).

66 else67 {68 System.out.println(69 “El numero de cuenta debe ser mayor de 0.” );70 } // fin de else71 } // fin de try72 catch ( IOException ioException )73 {74 System.err.println( “Error al escribir en el archivo.” );75 return;76 } // fin de catch77 catch ( NoSuchElementException elementException )78 {79 System.err.println( “Entrada invalida. Intente de nuevo.” );

80 entrada.nextLine(); // descarta la entrada para que el usuario intente de nuevo

81 } // fin de catch8283 System.out.printf( “%s %s\n%s”, “Escriba el numero de cuenta (>0),”,84 “primer nombre, apellido y saldo.”, “? ” );85 } // fin de while86 } // fin del método agregarRegistros8788 // cierra el archivo y termina la aplicación89 public void cerrarArchivo() 90 {91 try // cierra el archivo92 {93 if ( salida != null )94 salida.close();95 } // fin de try96 catch ( IOException ioException )97 {98 System.err.println( “Error al cerrar el archivo.” );99 System.exit( 1 );100 } // fin de catch101 } // fin del método cerrarArchivo102 } // fin de la clase CrearArchivoSecuencial

Fig. 17.17 � Prueba de la clase CrearArchivoSecuencial (parte 1 de 2).

1 // Fig. 17.17: PruebaCrearArchivoSecuencial.java

2 // Prueba de la clase CrearArchivoSecuencial.

3

4 public class PruebaCrearArchivoSecuencial

5 {

6 public static void main( String[] args )

7 {

8 CrearArchivoSecuencial aplicacion = new CrearArchivoSecuencial();

9

10 aplicacion.abrirArchivo();

11 aplicacion.agregarRegistros();

748 Capítulo 17 Archivos, fl ujos y serialización de objetos

La clase FileOutputStream cuenta con métodos para escribir arreglos tipo byte y objetos byte in-dividuales en un archivo, pero nosotros queremos escribir objetos en un archivo. Por esta razón, envolve-mos un objeto FileOutputStream en un objeto ObjectOutputStream, pasando el nuevo objeto Fi-leOutputStream al constructor de ObjectOutputStream (líneas 20 y 21). El objeto ObjectOutputStream utiliza al objeto FileOutputStream para escribir objetos en el archivo. En las líneas 20 y 21 se podría lanzar una excepción tipo IOException si ocurre un problema al abrir el archivo (por ejemplo, cuando se abre un archivo para escribir en una unidad de disco con espacio insuficiente, o cuando se abre un archi-vo de sólo lectura para escribir datos). Si es así, el programa muestra un mensaje de error (líneas 23 a 26). Si no ocurre una excepción, el archivo se abre y se puede utilizar la variable salida para escribir objetos en el archivo.

Este programa asume que los datos se introducen de manera correcta y en el orden de número de registro apropiado. El método agregarRegistros (líneas 30 a 86) realiza la operación de escritura. En las líneas 62 y 63 se crea un objeto RegistroCuentaSerializable a partir de los datos introducidos por el usuario. En la línea 64 se hace una llamada al método writeObject de ObjectOutputStream para escribir el objeto registro en el archivo de salida. Observe que sólo se requiere una instrucción para escribir todo el objeto.

El método cerrarArchivo (líneas 89 a 101) llama al método close de ObjectOutputStream en sa-lida para cerrar el objeto ObjectOutputStream y su objeto FileOutputStream subyacente (línea 94). La llamada al método close está dentro de un bloque try. El método close lanza una excepción IOExcep-tion si el archivo no se puede cerrar en forma apropiada. En este caso, es importante notificar al usuario que la información en el archivo podría estar corrupta. Al utilizar flujos envueltos, si se cierra el flujo ex-terior también se cierra el archivo subyacente.

En la ejecución de ejemplo para el programa de la figura 17.17, introdujimos información para cinco cuentas; la misma información que se muestra en la figura 17.8. El programa no muestra cómo es que aparecen en realidad los registros en el archivo. Recuerde que ahora estamos usando archivos bi-narios, que no pueden ser leídos por los humanos. Para verificar que el archivo se haya creado con éxito, la siguiente sección presenta un programa para leer el contenido del mismo.

Fig. 17.17 � Prueba de la clase CrearArchivoSecuencial (parte 2 de 2).

12 aplicacion.cerrarArchivo();

13 } // fin de main

14 } // fin de la clase PruebaCrearArchivoSecuencial

Para terminar de introducir datos, escriba el indicador de fin de archivo

Cuando se le pida que introduzca los datos.

En UNIX/Linux/Mac OS X escriba <ctrl> d y oprima Intro

En Windows escriba <ctrl> z y oprima Intro

Escriba el numero de cuenta (> 0), primer nombre, apellido y saldo.

? 100 Bob Jones 24.98

Escriba el numero de cuenta (>0), primer nombre, apellido y saldo.

? 200 Steve Doe -345.67

Escriba el numero de cuenta (>0), primer nombre, apellido y saldo.

? 300 Pam White 0.00

Escriba el numero de cuenta (>0), primer nombre, apellido y saldo.

? 400 Sam Stone -42.16

Escriba el numero de cuenta (>0), primer nombre, apellido y saldo.

? 500 Sue Rich 224.62

Escriba el numero de cuenta (>0), primer nombre, apellido y saldo.

? ^Z

17.5 Serialización de objetos 749

17.5.2 Lectura y deserialización de datos de un archivo de acceso secuencialEn la sección anterior mostramos cómo crear un archivo para acceso secuencial, usando la serialización de objetos. En esta sección, veremos cómo leer datos serializados de un archivo, en forma secuencial.

El programa de las figuras 17.18 y 17.19 lee registros de un archivo creado por el programa de la sección 17.5.1 y muestra el contenido. El programa abre el archivo en modo de entrada, creando un objeto FileInputStream (línea 21). El nombre del archivo a abrir se especifica como un argumento para el constructor de FileInputStream. En la figura 17.16 escribimos objetos al archivo, usando un objeto ObjectOutputStream. Los datos se deben leer del archivo en el mismo formato en el que se es-cribió. Por lo tanto, utilizamos un objeto ObjectInputStream envuelto alrededor de un objeto FileInputStream en este programa (líneas 20 y 21). Si no ocurren excepciones al abrir el archivo, podemos usar la variable entrada para leer objetos del archivo.

Fig. 17.18 � Leer un archivo de objetos en forma secuencial mediante ObjectInputStream y mostrar cada registro en pantalla (parte 1 de 2).

1 // Fig. 17.18: LeerArchivoSecuencial.java

2 // Leer un archivo de objetos en forma secuencial con ObjectInputStream

3 // y mostrar cada registro en pantalla.

4 import java.io.EOFException;

5 import java.io.FileInputStream;

6 import java.io.IOException;

7 import java.io.ObjectInputStream;

8

9 import com.deitel.cap17.RegistroCuentaSerializable;

10

11 public class LeerArchivoSecuencial

12 {

13 private ObjectInputStream entrada;

14

15 // permite al usuario seleccionar el archivo a abrir

16 public void abrirArchivo()

17 {

18 try // abre el archivo

19 {

20 entrada = new ObjectInputStream(

21 new FileInputStream( “clientes.ser” ) );

22 } // fin de try

23 catch ( IOException ioException )

24 {

25 System.err.println( “Error al abrir el archivo.” );

26 } // fin de catch

27 } // fin del método abrirArchivo

28

29 // lee el registro del archivo

30 public void leerRegistros()

31 {

32 RegistroCuentaSerializable registro;

33 System.out.printf( “%-10s%-15s%-15s%10s\n”, “Cuenta”,

34 “Primer nombre”, “Apellido paterno”, “Saldo” );

35

36 try // recibe los valores del archivo

37 {

750 Capítulo 17 Archivos, fl ujos y serialización de objetos

El programa lee registros del archivo en el método leerRegistros (líneas 30 a 60). En la línea 40 se hace una llamada al método readObject de ObjectInputStream para leer un objeto Object del archivo. Para utilizar los métodos específicos de RegistroCuentaSerializable, realizamos una conversión des-cendente en el objeto Object devuelto, al tipo RegistroCuentaSerializable. El método readObject lanza una excepción tipo EOFException (que se procesa en las líneas 48 a 51) si se hace un intento por leer más allá del fin del archivo. El método readObject lanza una excepción ClassNotFoundException si no se puede localizar la clase para el objeto que se está leyendo. Esto podría ocurrir si se accede al archivo en una computadora que no tenga esa clase. La figura 17.19 contiene el método main (líneas 6 a 13), el cual abre el archivo, llama al método leerRegistros y cierra el archivo.

Fig. 17.18 � Leer un archivo de objetos en forma secuencial mediante ObjectInputStream y mostrar cada registro en pantalla (parte 2 de 2).

38 while ( true )

39 {

40 registro = ( RegistroCuentaSerializable ) entrada.readObject();

4142 // muestra el contenido del registro

43 System.out.printf( “%-10d%-15s%-15s%11.2f\n”,

44 registro.obtenerCuenta(), registro.obtenerPrimerNombre(),

45 registro.obtenerApellidoPaterno(), registro.obtenerSaldo() );

46 } // fin de while

47 } // fin de try

48 catch ( EOFException endOfFileException )

49 {

50 return; // se llegó al fin del archivo

51 } // fin de catch

52 catch ( ClassNotFoundException classNotFoundException )

53 {

54 System.err.println( “No se pudo crear el objeto.” );

55 } // fin de catch

56 catch ( IOException ioException )

57 {

58 System.err.println( “Error al leer del archivo.” );

59 } // fin de catch

60 } // fin del método leerRegistros

6162 // cierra el archivo y termina la aplicación

63 public void cerrarArchivo()

64 {

65 try // cierra el archivo y sale

66 {

67 if ( entrada != null )

68 entrada.close();

69 System.exit( 0 );

70 } // fin de try

71 catch ( IOException ioException )

72 {

73 System.err.println( “Error al cerrar el archivo.” );

74 System.exit( 1 );

75 } // fin de catch

76 } // fin del método cerrarArchivo

77 } // fin de la clase LeerArchivoSecuencial

17.6 Clases adicionales de java.io 751

17.6 Clases adicionales de java.ioEn esta sección veremos las generalidades acerca de las interfaces y clases adicionales (del paquete java.io) para los flujos de entrada y salida basados en bytes, y los flujos de entrada y salida basados en caracteres.

17.6.1 Interfaces y clases para entrada y salida basada en bytesInputStream y OutputStream son clases abstract que declaran métodos para realizar operaciones ba-sadas en bytes de entrada y salida, respectivamente. En este capítulo utilizamos varias subclases concretas FileInputStream InputStream y OuputStream para manipular archivos.

Flujos de canalizacionesLas canalizaciones son canales de comunicación sincronizados entre hilos. Hablaremos sobre los hilos en el capítulo 26. Java proporciona las clases PipedOutputStream (una subclase de OutputStream) y PipedInputStream (una subclase de InputStream) para establecer canalizaciones entre dos hilos en un programa. Un hilo envía datos a otro, escribiendo a un objeto PipedOutputStream. El hilo de destino lee la información de la canalización mediante un objeto PipedInputStream.

Flujos de filtrosUn objeto FilterInputStream filtra a un objeto InputStream, y un objeto FilterOutputStream filtra a un objeto OutputStream. Filtrar significa simplemente que el flujo que actúa como filtro pro-porciona una funcionalidad adicional, como la agregación de bytes de datos en unidades de tipo primi-tivo significativas. Por lo general, FilterInputStream y FilterOutputStream se extienden, de manera que sus subclases proporcionan sus capacidades de filtrado.

Un objeto PrintStream (una subclase de FilterOutputStream) envía texto como salida hacia el flujo especificado. En realidad, hemos estado utilizando la salida mediante PrintStream a lo largo de este texto, hasta este punto; System.out y System.err son objetos PrintStream.

Fig. 17.19 � Prueba de la clase LeerArchivoSecuencial.

1 // Fig. 17.19: PruebaLeerArchivoSecuencial.java

2 // Prueba de la clase LeerArchivoSecuencial.

3

4 public class PruebaLeerArchivoSecuencial

5 {

6 public static void main( String[] args )

7 {

8 LeerArchivoSecuencial aplicacion = new LeerArchivoSecuencial();

9

10 aplicacion.abrirArchivo();

11 aplicacion.leerRegistros();

12 aplicacion.cerrarArchivo();

13 } // fin de main

14 } // fin de la clase PruebaLeerArchivoSecuencial

Cuenta Primer nombre Apellido paterno Saldo

100 Bob Jones 24.98

200 Steve Doe -345.67

300 Pam White 0.00

400 Sam Stone -42.16

500 Sue Rich 224.62

752 Capítulo 17 Archivos, fl ujos y serialización de objetos

Flujos de datosLeer datos en forma de bytes sin ningún formato es un proceso rápido, pero crudo. Por lo general, los programas leen datos como agregados de bytes que forman valores int, float, double y así, en lo suce-sivo. Los programas de Java pueden utilizar varias clases para recibir datos de entrada y enviar datos de salida en forma de agregación.

La interfaz DataInput describe métodos para leer tipos primitivos desde un flujo de entrada. Las clases DataInputStream y RandomAccessFile implementan a esta interfaz para leer conjuntos de bytes y verlos como valores de tipo primitivo. La interfaz DataInput incluye métodos tales como readBoolean, readByte, readChar, readDouble, readFloat, readFully (para arreglos tipo byte), readInt, readLong, readShort, readUnsignedByte, readUnsignedShort, readUTF (para leer caracteres Unicode codificados por Java; hablaremos sobre la codificación UTF en el apéndice L) y skipBytes.

La interfaz DataOutput describe un conjunto de métodos para escribir tipos primitivos hacia un flujo de salida. Las clases DataOutputStream (una subclase de FilterOutputStream) y RandomAccessFi-le implementan a esta interfaz para escribir valores de tipos primitivos como bytes. La interfaz Da-taOutput incluye versiones sobrecargadas del método write (para un byte o para un arreglo byte) y los métodos writeBoolean, writeByte, writeBytes, writeChar, writeChars (para objetos String Unico-de), writeDouble, writeFloat, writeInt, writeLong, writeShort y writeUTF (para enviar texto modi-ficado para Unicode).

Flujos con búferEl uso de un búfer es una técnica para mejorar el rendimiento de las operaciones de E/S. Con un objeto BufferedOutputStream (una subclase de la clase FilterOutputStream), cada instrucción de salida no produce necesariamente una transferencia física real de datos hacia el dispositivo de salida (una operación lenta, en comparación con las velocidades del procesador y de la memoria principal). En vez de ello, cada operación de salida se dirige hacia una región en memoria conocida como búfer, que es lo bastante gran-de como para almacenar los datos de muchas operaciones de salida. Después, la transferencia real hacia el dispositivo de salida se realiza en una sola operación física de salida extensa cada vez que se llena el búfer. Las operaciones de salida dirigidas hacia el búfer de salida en memoria se conocen a menudo como ope-raciones lógicas de salida. Con un objeto BufferedOutputStream, se puede forzar a un búfer parcial-mente lleno para que envíe su contenido al dispositivo en cualquier momento, mediante la invocación del método flush del objeto flujo.

El uso de búfer puede aumentar de manera considerable la eficiencia de una aplicación. Las opera-ciones comunes de E/S son en extremo lentas, si se les compara con la velocidad de acceso a los datos de la memoria de la computadora. El uso de búfer reduce el número de operaciones de E/S, al combinar primero las operaciones de salida más pequeñas en la memoria. El número de operaciones físicas de E/S reales es pequeño, en comparación con el número de solicitudes de E/S emitidas por el programa. Por ende, el programa que usa un búfer es más eficiente.

Tip de rendimiento 17.1La E/S con búfer puede producir mejoras considerables en el rendimiento, en comparación con la E/S sin búfer.

Con un objeto BufferedInputStream (una subclase de la clase FilterInputStream), muchos tro-zos “lógicos” de datos de un archivo se leen como una sola operación física de entrada extensa y se envían a un búfer de memoria. A medida que un programa solicita cada nuevo trozo de datos, éste se toma del búfer. (A este procedimiento se le conoce algunas veces como operación lógica de entrada). Cuando el búfer está vacío, se lleva a cabo la siguiente operación física de entrada real desde el dispo-sitivo de entrada, para leer el siguiente grupo de trozos “lógicos” de datos. Por lo tanto, el número de operaciones físicas de entrada reales es pequeño, en comparación con el número de solicitudes de lec-tura emitidas por el programa.

17.6 Clases adicionales de java.io 753

Flujos de arreglos byte basados en memoriaLa E/S de flujos en Java incluye herramientas para recibir datos de entrada de arreglos byte en memoria, y enviar datos de salida a arreglos byte en memoria. Un objeto ByteArrayInputStream (una subclase de InputStream) lee de un arreglo byte en memoria. Un objeto ByteArrayOutputStream (una subcla-se de OutputStream) escribe en un arreglo byte en memoria. Una aplicación de la E/S con arreglos byte es la validación de datos. Un programa puede recibir como entrada una línea completa a la vez desde el flujo de entrada, para colocarla en un arreglo byte. Después puede usarse una rutina de validación para analizar detalladamente el contenido del arreglo byte y corregir los datos, si es necesario. Por último, el programa puede recibir los datos de entrada del arreglo byte, “sabiendo” que los datos de entrada se encuentran en el formato adecuado. Enviar datos de salida a un arreglo byte es una excelente manera de aprovechar las poderosas herramientas de formato para los datos de salida que proporcionan los flujos en Java. Por ejemplo, los datos pueden almacenarse en un arreglo byte, utilizando el mismo formato que se mostrará posteriormente, y luego el arreglo byte se puede enviar hacia un archivo en disco para preservar la imagen en pantalla.

Secuenciamiento de la entrada desde varios flujosUn objeto SequenceInputStream (una subclase de InputStream) permite la concatenación lógica de varios objetos InputStream: el programa ve al grupo como un flujo InputStream continuo. Cuan-do el programa llega al final de un flujo de entrada, ese flujo se cierra y se abre el siguiente flujo en la secuencia.

17.6.2 Interfaces y clases para entrada y salida basada en caracteresAdemás de los flujos basados en bytes, Java proporciona las clases abstract Reader y Writer, que son flujos basados en caracteres Unicode de dos bytes. La mayoría de los flujos basados en bytes tienen sus correspondientes clases Reader o Writer concretas, basadas en caracteres.

Objetos Reader y Writer con búfer basados en caracteresLas clases BufferedReader (una subclase de la clase abstract Reader) y BufferedWriter (una sub-clase de la clase abstract Writer) permiten el uso del búfer para los flujos basados en caracteres. Recuer-de que los flujos basados en caracteres utilizan caracteres Unicode; dichos flujos pueden procesar datos en cualquier lenguaje que sea representado por el conjunto de caracteres Unicode.

Objetos Reader y Writer para arreglos char basados en memoriaLas clases CharArrayReader y CharArrayWriter leen y escriben, respectivamente, un flujo de caracteres en un arreglo char. Un objeto LineNumberReader (una subclase de BufferedReader) es un flujo de caracteres con búfer que lleva el registro de los números de línea leídos; los caracteres de nueva línea, retorno o las combinaciones de retorno de carro y avance de línea incrementan la cuenta de líneas. Puede ser útil llevar la cuenta de los números de línea si el programa necesita informar al lector sobre un error en una línea específica.

Objetos Reader y Writer para archivos, canalizaciones y cadenas basadas en caracteresUn objeto InputStream puede convertirse en un objeto Reader por medio de la clase InputStream-Reader. De manera similar, un objeto OutputStream puede convertirse en un objeto Writer por medio de la clase OutputStreamWriter. Las clases FileReader (una subclase de InputStreamReader) y FileWriter (una subclase de OutputStreamWriter) leen caracteres de, y escriben caracteres en, un archivo, respectivamente. Las clases PipedReader y PipedWriter implementan flujos de caracteres canalizados, que pueden utilizarse para transferir datos entre hilos. Las clases StringReader y StringWriter leen y escriben caracteres, respectivamente, en objetos String. Un objeto PrintWriter escribe caracteres en un flujo.

754 Capítulo 17 Archivos, fl ujos y serialización de objetos

17.7 Abrir archivos con JFileChooser La clase JFileChooser muestra un cuadro de diálogo (conocido como cuadro de diálogo JFileChooser) que permite al usuario seleccionar archivos o directorios con facilidad. Para demostrar este cuadro de diálogo, mejoramos el ejemplo de la sección 17.3, como se muestra en las figuras 17.20 y 17.21. El ejemplo ahora contiene una interfaz gráfica de usuario, pero sigue mostrando los mismos datos. El constructor llama al método analizarRuta en la línea 34. Después, este método llama al método obtenerArchivo en la línea 68 para obtener el objeto File.

Fig. 17.20 � Demostración de JFileChooser (parte 1 de 3).

1 // Fig. 17.20: DemostracionFile.java 2 // Demostración de la clase JFileChooser. 3 import java.awt.BorderLayout; 4 import java.awt.event.ActionEvent; 5 import java.awt.event.ActionListener; 6 import java.io.File; 7 import javax.swing.JFileChooser; 8 import javax.swing.JFrame; 9 import javax.swing.JOptionPane;10 import javax.swing.JScrollPane;11 import javax.swing.JTextArea;12 import javax.swing.JTextField;1314 public class DemostracionFile extends JFrame15 {16 private JTextArea areaSalida; // se utiliza para salida17 private JScrollPane panelDespl; // se utiliza para que la salida pueda desplazarse1819 // establece la GUI20 public DemostracionFile()21 {22 super( “Prueba de la clase File” );2324 areaSalida = new JTextArea();2526 // agrega areaSalida a panelDespl27 panelDespl = new JScrollPane( areaSalida ); 2829 add( panelDespl, BorderLayout.CENTER ); // agrega panelDespl a la GUI3031 setSize( 400, 400 ); // establece el tamaño de la GUI32 setVisible( true ); // muestra la GUI3334 analizarRuta(); // crea y analiza un objeto File35 } // fin del constructor de DemostracionFile3637 // permite al usuario especificar el nombre del archivo o directorio38 private File obtenerArchivoODirectorio()39 {

40 // muestra el cuadro de diálogo de archivos, para que el usuario pueda elegir el archivo a abrir

41 JFileChooser selectorArchivos = new JFileChooser();42 selectorArchivos.setFileSelectionMode(43 JFileChooser.FILES_AND_DIRECTORIES );

17.7 Abrir archivos con JFileChooser 755

Fig. 17.20 � Demostración de JFileChooser (parte 2 de 3).

4445 int resultado = selectorArchivos.showOpenDialog( this );4647 // si el usuario hizo clic en el botón Cancelar en el cuadro de diálogo, regresa48 if ( resultado == JFileChooser.CANCEL_OPTION )49 System.exit( 1 );50

51 File nombreArchivo = selectorArchivos.getSelectedFile(); // obtiene el archivo seleccionado

5253 // muestra error si es inválido54 if ( ( nombreArchivo == null ) || ( nombreArchivo.getName().equals( “” ) ) )55 {56 JOptionPane.showMessageDialog( this, “Nombre de archivo inválido”,57 “Nombre de archivo inválido”, JOptionPane.ERROR_MESSAGE );58 System.exit( 1 );59 } // fin de if6061 return nombreArchivo;62 } // fin del método obtenerArchivoODirectorio6364 // muestra información acerca del archivo que especifica el usuario65 public void analizarRuta()66 {67 // crea un objeto File basado en la entrada del usuario68 File nombre = obtenerArchivoODirectorio();6970 if ( nombre.exists() ) // si el nombre existe, muestra información sobre él71 {72 // muestra la información sobre el archivo (o directorio)73 areaSalida.setText( String.format(74 “%s%s\n%s\n%s\n%s\n%s%s\n%s%s\n%s%s\n%s%s\n%s%s”,75 nombre.getName(), “ existe”,76 ( nombre.isFile() ? “es un archivo” : “no es un archivo” ),77 ( nombre.isDirectory() ? “es un directorio” : 78 “no es un directorio” ),79 ( nombre.isAbsolute() ? “es una ruta absoluta” : 80 “no es una ruta absoluta” ), “Ultima modificacion: ”,81 nombre.lastModified(), “Tamanio: ”, nombre.length(), 82 “Ruta: ”, nombre.getPath(), “Ruta absoluta: ”,83 nombre.getAbsolutePath(), “Padre: ”, nombre.getParent() ) );8485 if ( nombre.isDirectory() ) // imprime el listado del directorio86 {87 String[] directorio = nombre.list();88 areaSalida.append( “\n\nContenido del directorio:\n” );8990 for ( String nombreDirectorio : directorio )91 areaSalida.append( nombreDirectorio + “\n” );92 } // fin de else93 } // fin de if exterior94 else // no es archivo ni directorio, imprime mensaje de error95 {

756 Capítulo 17 Archivos, fl ujos y serialización de objetos

96 JOptionPane.showMessageDialog( this, nombre +

97 “ no existe.”, “ERROR”, JOptionPane.ERROR_MESSAGE );

98 } // fin de else

99 } // fin del método analizarRuta

100 } // fin de la clase DemostracionFile

Fig. 17.20 � Demostración de JFileChooser (parte 3 de 3).

Fig. 17.21 � Prueba de la clase DemostracionFile.

1 // Fig. 17.21: PruebaDemostracionFile.java

2 // Prueba de la clase DemostracionFile.

3 import javax.swing.JFrame;

4

5 public class PruebaDemostracionFile

6 {

7 public static void main( String[] args )

8 {

9 DemostracionFile aplicacion = new DemostracionFile();

10 aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

11 } // fin de main

12 } // fin de la clase PruebaDemostracionFile

Seleccione aquí la ubicación del archivo o directorio

Aquí se muestran los archivos y directorios

Haga clic en Abrir para enviar el nombre del archivo o directorio al programa.

17.8 Conclusión 757

El método getFile se define en las líneas 38 a 62 de la figura 17.20. En la línea 41 se crea un objeto JFileChooser y se asigna su referencia a selectorArchivos. En las líneas 42 y 43 se hace una llamada al método setFileSelectionMode para especificar lo que el usuario puede seleccionar del objeto selector-Archivos. Para este programa, utilizamos la constante static FILES_AND_DIRECTORIES de JFileChooser para indicar que pueden seleccionarse archivos y directorios. Otras constantes static son FILES_ONLY (sólo archivos, el valor predeterminado) y DIRECTORIES_ONLY (sólo directorios).

En la línea 45 se hace una llamada al método showOpenDialog para mostrar el cuadro de diálogo JFileChooser llamado Abrir. El argumento this especifica la ventana padre del cuadro de diálo-go JFileChooser, la cual determina la posición del cuadro de diálogo en la pantalla. Si se pasa null, el cuadro de diálogo se muestra en el centro de la pantalla; en caso contrario, el cuadro de diálogo se centra sobre la ventana de la aplicación (lo cual se especifica mediante el argumento this). Un cuadro de diálogo JFileChooser es un cuadro de diálogo modal que no permite al usuario interactuar con cualquier otra ventana en el programa, sino hasta que el usuario cierre el objeto JFileChooser, haciendo clic en el botón Abrir o Cancelar. El usuario selecciona la unidad, el nombre del directorio o archivo, y después hace clic en Abrir. El método showOpenDialog devuelve un entero, el cual especifica qué botón (Abrir o Cancelar) oprimió el usuario para cerrar el cuadro de diálogo. En la línea 48 se evalúa si el usuario hizo clic en Cancelar, para lo cual se compara el resultado con la constante static CANCEL_OPTION. Si son iguales, el programa termina. En la línea 51 se obtiene el archivo que seleccionó el usuario, lla-mando al método getSelectedFile de JFileChooser. Después, el programa muestra información acerca del archivo o directorio seleccionado.

17.8 ConclusiónEn este capítulo aprendió a manipular datos persistentes. Comparamos los flujos basados en caracteres y los flujos basados en bytes, y presentamos varias clases para procesamiento de archivos que proporciona el paquete java.io. Utilizó la clase File para obtener información acerca de un archivo o directorio. Utilizó el procesamiento de archivos de acceso secuencial para manipular registros que se almacenan en orden, en base al campo clave del registro. Conoció las diferencias entre el procesamiento de archivos de texto y la serialización de objetos, y utilizó la serialización para almacenar y obtener objetos completos. El capítulo concluyó con un pequeño ejemplo acerca del uso de un cuadro de diálogo JFileChooser para permitir a los usuarios seleccionar archivos de una GUI con facilidad. En el siguiente capítulo veremos el concepto de la recursividad: métodos que se llaman a sí mismos.

ResumenSección 17.1 Introducción• Las computadoras utilizan archivos para la retención a largo plazo de grandes cantidades de datos persistentes (pág. 720),

incluso después de que los programas que crearon los datos terminan de ejecutarse.

• Las computadoras almacenan los archivos en dispositivos de almacenamiento secundario (pág. 720), como los discos duros.

Sección 17.2 Archivos y fl ujos• Java ve a cada archivo como un fl ujo secuencial de bytes (pág. 720).

• Cada sistema operativo cuenta con un mecanismo para determinar el fi n de un archivo, como un marcador de fi n de archivo (pág. 720) o la cuenta de los bytes totales en el archivo.

• Los fl ujos basados en bytes (pág. 721) representan datos en formato binario.

• Los fl ujos basados en caracteres (pág. 721) representan datos como secuencias de caracteres.

• Los archivos que se crean usando fl ujos basados en bytes son archivos binarios (pág. 721). Los archivos que se crean usando fl ujos basados en caracteres son archivos de texto (pág. 721). Los archivos de texto se pueden leer mediante editores de texto, mientras que los archivos binarios se leen mediante un programa que convierte esos datos en un formato legible para los humanos.

• Java también puede asociar los fl ujos con distintos dispositivos. Tres objetos fl ujo se asocian con dispositivos cuando un programa de Java empieza a ejecutarse: System.in, System.out y System.err.

Sección 17.3 La clase File• La clase File (pág. 722) se utiliza para obtener información acerca de los archivos y directorios.

• Las operaciones de entrada y salida basadas en caracteres se pueden llevar a cabo con las clases Scanner y Formatter.

• La clase Formatter (pág. 722) permite mostrar datos con formato en la pantalla, o enviarlos a un archivo, de una manera similar a System.out.printf.

• La ruta de un archivo o directorio (pág. 722) especifi ca su ubicación en el disco.

• Una ruta absoluta (pág. 722) contiene todos los directorios, empezando con el directorio raíz (pág. 722), que conducen hacia un archivo o directorio específi co. Cada archivo o directorio en una unidad de disco tiene el mismo directorio raíz en su ruta.

• Una ruta relativa (pág. 722) empieza desde el directorio en el que se empezó a ejecutar la aplicación.

• Un carácter separador (pág. 726) se utiliza para separar directorios y archivos en la ruta.

Sección 17.4 Archivos de texto de acceso secuencial• Java no impone una estructura en un archivo. El programador debe estructurar los archivos para satisfacer los

requerimientos de una aplicación.

• Para obtener datos de un archivo en forma secuencial, los programas comúnmente empiezan desde el principio del archivo y leen todos los datos en forma consecutiva, hasta encontrar la información deseada.

• Los datos en muchos archivos secuenciales no se pueden modifi car sin el riesgo de destruir otros datos en el archivo. Por lo general, los registros en un archivo de acceso secuencial se actualizan volviendo a escribir el archivo completo.

Sección 17.5 Serialización de objetos• Java cuenta con un mecanismo llamado serialización de objetos (pág. 742), el cual permite escribir o leer objetos

completos mediante un fl ujo.

• Un objeto serializado (pág. 742) se representa como una secuencia de bytes, e incluye los datos del objeto, así como información acerca del tipo del objeto y los tipos de datos que almacena.

• Una vez que se escribe un objeto serializado en un archivo, se puede leer del archivo y deserializarse (pág. 742) para recrearlo en la memoria.

• Las clases ObjectInputStream (pág. 722) y ObjectOutputStream (pág. 722) permiten leer o escribir objetos completos de/a un fl ujo (posiblemente un archivo).

• Sólo las clases que implementan a la interfaz Serializable (pág. 743) pueden serializarse y deserializarse.

• La interfaz ObjectOutput (pág. 742) contiene el método writeObject (pág. 743), el cual recibe un Object como ar-gumento y escribe su información en un OutputStream (pág. 751). Una clase que implementa a esta inferfaz, como ObjectOutputStream, debe asegurarse que el objeto Object sea Serializable.

• La interfaz ObjectInput (pág. 742) contiene el método readObject (pág. 743), el cual lee y devuelve una referencia a un objeto Object a partir de un InputStream. Una vez que se lee un objeto, su referencia puede convertirse al tipo actual del objeto.

Sección 17.6 Clases adicionales de java.io• InputStream y OutputStream son clases abstract para realizar operaciones de E/S basadas en bytes.

• Las canalizaciones (pág. 751) son canales de comunicación sincronizados entre hilos. Un hilo envía datos mediante un PipedOutputStream (pág. 751). El hilo de destino lee información de la canalización mediante un PipedInputStream (pág. 751).

• Un fl ujo de fi ltro (pág. 751) proporciona una funcionalidad adicional, como la agregación de bytes de datos en unidades de tipo primitivo signifi cativas. Por lo general, FilterInputStream (pág. 751) y FilterOutputStream se extienden, de manera que sus subclases proporcionan algunas de sus capacidades de fi ltrado.

758 Capítulo 17 Archivos, fl ujos y serialización de objetos

• Un objeto PrintStream (pág. 751) envía texto como salida. System.out y System.err son objetos PrintStream.

• La interfaz DataInput describe métodos para leer tipos primitivos desde un fl ujo de entrada. Las clases DataInputStream (pág. 752) y RandomAccessFile implementan a esta interfaz.

• La interfaz DataOutput describe métodos para escribir tipos primitivos hacia un fl ujo de salida. Las clases DataOutput-Stream (pág. 752) y RandomAccessFile implementan a esta interfaz.

• El uso de búfer es una técnica para mejorar el rendimiento de E/S. Reduce el número de operaciones de E/S al com-binar salidas más pequeñas y juntarlas en la memoria. El número de operaciones físicas de E/S es mucho menor que el número de peticiones de E/S emitidas por el programa.

• Con un objeto BufferedOutputStream (pág. 752), cada operación de salida se dirige hacia un búfer (pág. 752) lo bastante grande como para contener los datos de muchas operaciones de salida. La transferencia al dispositivo de sa-lida se realiza entonces en una sola operación de salida física extensa (pág. 752), cada vez que se llena el búfer. Es posible forzar el envío de datos de un búfer parcialmente lleno hacia el dispositivo en cualquier momento, invocando al mé-todo flush del objeto fl ujo (pág. 752).

• Con un objeto BufferedInputStream (pág. 752), muchos trozos “lógicos” de datos de un archivo se leen como una sola operación de entrada física extensa (pág. 752) y se colocan en un búfer de memoria. A medida que un programa solicita datos, se obtienen del búfer. Cuando el búfer está vacío, se lleva a cabo la siguiente operación de entrada física real.

• Un objeto ByteArrayInputStream lee de un arreglo byte en memoria. Un objeto ByteArrayOutputStream escribe en un arreglo byte en memoria.

• Un objeto SequenceInputStream concatena varios objetos InputStream. Cuando el programa llega al fi nal de un fl ujo de entrada, ese fl ujo se cierra y se abre el siguiente fl ujo en la secuencia.

• Las clases abstract Reader (pág. 753) y Writer (pág. 753) son fl ujos Unicode basados en caracteres. La mayoría de los fl ujos basados en caracteres tienen sus correspondientes clases Reader o Writer concretas, basadas en caracteres.

• Las clases BufferedReader (pág. 753) y BufferedWriter (pág. 753) usan búfer con los fl ujos basados en caracteres.

• Las clases CharArrayReader (pág. 753) y CharArrayWriter (pág. 753) manipulan los arreglos char.

• Un objeto LineNumberReader (pág. 753) es un fl ujo de caracteres con búfer que lleva el registro de los números de línea leídos.

• Las clases FileReader (pág. 753) y FileWriter (pág. 753) realizan operaciones de E/S de archivos basadas en caracteres.

• Las clases PipedReader (pág. 753) y PipedWriter (pág. 753) implementan fl ujos de caracteres canalizados, que pue-den utilizarse para transferir datos entre hilos.

• Las clases StringReader (pág. 753) y StringWriter (pág. 753) leen y escriben caracteres, respectivamente, en objetos String. Un objeto PrintWriter (pág. 752) escribe caracteres en un fl ujo.

Sección 17.7 Abrir archivos con JFileChooser• La clase JFileChooser (pág. 754) se utiliza para mostrar un cuadro de diálogo, que permite a los usuarios de un pro-

grama seleccionar archivos o directorios con facilidad, mediante una GUI.

Ejercicios de autoevaluación17.1 Determine cuál de las siguientes proposiciones es verdadera o falsa. En caso de ser falsa, explique por qué.

a) Usted debe crear en forma explícita los objetos flujo System.in, System.out y System.err.b) Al leer datos de un archivo mediante la clase Scanner, si desea leer datos en el archivo varias veces, el archivo

debe cerrarse y volver a abrirse para leer desde el principio del archivo. c) El método exists de la clase File devuelve true si el nombre que se especifica como argumento para el cons-

tructor de File es un archivo o directorio en la ruta especificada.d) Los archivos binarios pueden ser leídos por los humanos.e) Una ruta absoluta contiene todos los directorios, empezando con el directorio raíz, que conducen hacia un

archivo o directorio específico.f ) La clase Formatter contiene el método printf, que permite imprimir datos con formato en la pantalla, o

enviarlos a un archivo.

Ejercicios de autoevaluación 759

17.2 Complete las siguientes tareas; suponga que cada una se aplica al mismo programa:a) Escriba una instrucción que abra el archivo “antmaest.txt” en modo de entrada; use la variable Scanner

llamada entAntMaestro.b) Escriba una instrucción que abra el archivo “trans.txt” en modo de entrada; use la variable Scanner llamada

entTransaccion.c) Escriba una instrucción para abrir el archivo “nuevomaest.txt” en modo de salida (y creación); use la varia-

ble Formatter llamada salNuevoMaest.d) Escriba las instrucciones necesarias para leer un registro del archivo “antmaest.txt”. Los datos leídos deben

usarse para crear un objeto de la clase RegistroCuenta; use la variable Scanner llamada entAntMaest. Suponga que la clase RegistroCuenta es la misma que la de la figura 17.4.

e) Escriba las instrucciones necesarias para leer un registro del archivo “trans.txt”. El registro es un objeto de la clase RegistroTransaccion; use la variable Scanner llamada entTransaccion. Suponga que la clase Registro-Transaccion contiene el método establecerCuenta (que recibe un int) para establecer el número de cuenta, y el método establecerMonto (que recibe un double) para establecer el monto de la transacción.

f ) Escriba una instrucción que escriba un registro en el archivo “nuevomaest.txt”. El registro es un objeto de tipo RegistroCuenta; use la variable Formatter llamada salNuevoMaest.

17.3 Complete las siguientes tareas, suponiendo que cada una se aplica al mismo programa:a) Escriba una instrucción que abra el archivo “antmaest.ser” en modo de entrada; use la variable Object-

InputStream llamada entAntMaest para envolver un objeto FileInputStream.b) Escriba una instrucción que abra el archivo “trans.ser” en modo de entrada; use la variable ObjectInput-

Stream llamada entTransaccion para envolver un objeto FileInputStream.c) Escriba una instrucción para abrir el archivo “nuevomaest.ser” en modo de salida (y creación); use la varia-

ble ObjectOutputStream llamada salNuevoMaest para envolver un objeto FileOutputStream.d) Escriba una instrucción que lea un registro del archivo “antmaest.ser”. El registro es un objeto de la clase

RegistroCuentaSerializable; use la variable ObjectInputStream llamada entAntMaestro. Suponga que la clase RegistroCuentaSerializable es igual que la de la figura 17.15.

e) Escriba una instrucción que lea un registro del archivo “trans.ser”. El registro es un objeto de la clase RegistroTransaccion; use la variable ObjectInputStream llamada entTransaccion.

f ) Escriba una instrucción que escriba un registro de tipo RegistroCuentaSerializable en el archivo “nuevomaest.ser”; use la variable ObjectOutputStream llamada salNuevoMaest.

17.4 Encuentre el error en cada uno de los siguientes bloques de código y muestre cómo corregirlo. a) Suponga que se declaran cuenta, compania y monto.

ObjectOutputStream flujoSalida;flujoSalida.writeInt( cuenta );flujoSalida.writeChars( compania );flujoSalida.writeDouble( monto );

b) Las siguientes instrucciones deben leer un registro del archivo “porpagar.txt”. Se debe utilizar la variable entPorPagar de Scanner para hacer referencia a este archivo.

Scanner entPorPagar = new Scanner( new File( “porpagar.txt”) );RegistroPorPagar registro = ( RegistroPorPagar ) entPorPagar.readObject();

Respuestas a los ejercicios de autoevaluación17.1 a) Falso. Estos tres flujos se crean para el programador cuando se empieza a ejecutar una aplicación de Java.

b) Verdadero.c) Verdadero.d) Falso. Los archivos de texto pueden ser leídos por los humanos en un editor de texto. Los archivos binarios

podrían ser leídos por los humanos, pero sólo si los bytes en el archivo representan caracteres ASCII.

760 Capítulo 17 Archivos, fl ujos y serialización de objetos

e) Verdadero.f ) Falso. La clase Formatter contiene el método format, el cual permite imprimir datos con formato en la

pantalla, o enviarlos a un archivo.

17.2 a) Scanner entAntMaest = new Scanner( new File( “antmaest.txt” ) );

b) Scanner entTransaccion = new Scanner( new File( “trans.txt” ) );c) Formatter salNuevoMaest = new Formatter( “nuevomaest.txt” );

d) RegistroCuenta cuenta = new RegistroCuenta();cuenta.establecerCuenta( entAntMaest.nextInt() );

cuenta.establecerPrimerNombre( entAntMaest.next() );

cuenta.establecerApellidoPaterno( entAntMaest.next() );

cuenta.establecerSaldo( entAntMaest.nextDouble() );

e) RegistroTransaccion transaccion = new Transacción();

transaccion.establecerCuenta( entTransaccion.nextInt() );

transaccion.establecerMonto( entTransaccion.nextDouble() );

f ) salNuevMaest.format( “%d %s %s &.2f\n”,

cuenta.obtenerCuenta(), cuenta.obtenerPrimerNombre(),

cuenta.obtenerApellidoPaterno(), cuenta.obtenerSaldo() );

17.3 a) ObjectInputStream entAntMaest = new ObjectInputStream(

new FileInputStream( “antmaest.ser” ) );

b) ObjectInputStream entTransaccion = new ObjectInputStream( new FileInputStream( “trans.ser” ) );

c) ObjectOutputStream salNuevMaest = new ObjectOutputStream(

new FileOutputStream( “nuevmaest.ser” ) );

d) registroCuenta = ( RegistroCuentaSerializable ) entAntMaest.readObject(); e) registroTransaccion = ( RegistroTransaccion ) entTransaccion.readObject();

f ) salNuevMaest.writeObject( nuevoRegistroCuenta );

17.4 a) Error: el archivo no se abrió antes de tratar de enviar datos al flujo. Corrección: Abrir un archivo en modo de salida, creando un nuevo objeto ObjectOutputStream que en-

vuelva a un objeto FileOutputStream.b) Error: este ejemplo utiliza archivos de texto con un objeto Scanner; no hay serialización de objetos. Como

resultado, el método readObject no puede usarse para leer esos datos del archivo. Cada pieza de datos debe leerse por separado y después utilizarse para crear un objeto RegistroPorPagar.

Corrección: utilice los métodos de entPorPagar para leer cada pieza del objeto RegistroPorPagar.

Ejercicios17.5 (Asociación de archivos) El ejercicio de autoevaluación 17.2 pide al lector que escriba una serie de instrucciones individuales. En realidad, estas instrucciones forman el núcleo de un tipo importante de programa para procesar archivos: un programa para asociar archivos. En el procesamiento de datos comercial, es común tener varios archivos en cada sistema de aplicaciones. Por ejemplo, en un sistema de cuentas por cobrar hay por lo general un archivo maestro, el cual contiene información detallada acerca de cada cliente, como su nombre, dirección, número telefónico, saldo deudor, límite de cré-dito, términos de descuento, acuerdos contractuales y posiblemente un historial condensado de compras recientes y pagos en efectivo.

A medida que ocurren las transacciones (es decir, a medida que se generan las ventas y llegan los pagos en el correo), la información acerca de ellas se introduce en un archivo. Al fi nal de cada periodo de negocios (un mes para algunas compañías, una semana para otras y un día en algunos casos), el archivo de transacciones (llamado “trans.txt”) se aplica al archivo maestro (llamado “antmaest.txt”) para actualizar el registro de compras y pagos de cada cuenta. Durante una actualización, el archivo maestro se rescribe como el archivo “nuevomaest.txt”, el cual se utiliza al fi nal del siguiente periodo de negocios para empezar de nuevo el proceso de actualización.

Ejercicios 761

Los programas para asociar archivos deben tratar con ciertos problemas que no existen en programas de un solo archivo. Por ejemplo, no siempre ocurre una asociación. Si un cliente en el archivo maestro no ha realizado compras ni pagos en efectivo en el periodo actual de negocios, no aparecerá ningún registro para este cliente en el archivo de transacciones. De manera similar, un cliente que haya realizado compras o pagos en efectivo podría haberse mudado recientemente a esta comunidad, y tal vez la compañía no haya tenido la oportunidad de crear un registro maestro para este cliente.

Escriba un programa completo para asociar archivos de cuentas por cobrar. Utilice el número de cuenta en cada archivo como la clave de registro para fi nes de asociar los archivos. Suponga que cada archivo es un archivo de texto se-cuencial con registros almacenados en orden ascendente, por número de cuenta.

a) Defi na la clase RegistroTransaccion. Los objetos de esta clase contienen un número de cuenta y un monto para la transacción. Proporcione métodos para modifi car y obtener estos valores.

b) Modifi que la clase RegistroCuenta de la fi gura 17.4 para incluir el método combinar, el cual recibe un ob-jeto RegistroTransaccion y combina el saldo del objeto RegistroCuenta con el valor del monto del objeto RegistroTransaccion.

c) Escriba un programa para crear datos de prueba para el programa. Use los datos de la cuenta de ejemplo de las fi guras 17.22 y 17.23. Ejecute el programa para crear los archivos trans.txt y antmaest.txt, para que los utilice su programa de asociación de archivos.

762 Capítulo 17 Archivos, fl ujos y serialización de objetos

Fig. 17.22 � Datos de ejemplo para el archivo maestro.

Archivo maestroNúmero de cuenta Nombre Saldo

100 Alan Jones 348.17

300 Mary Smith 27.19

500 Sam Sharp 0.00

700 Suzy Green –14.22

Fig. 17.23 � Datos de ejemplo para el archivo de transacciones.

Archivo de transaccionesNúmero de cuenta Monto de la transacción

100 27.14

300 62.11

400 100.56

900 82.17

d) Cree la clase AsociarArchivos para llevar a cabo la funcionalidad de asociación de archivos. La clase debe contener métodos para leer antmaest.txt y trans.txt. Cuando ocurra una coincidencia (es decir, que aparezcan registros con el mismo número de cuenta en el archivo maestro y en el archivo de transacciones), sume el monto en dólares del registro de transacciones al saldo actual en el registro maestro, y escriba el re-gistro “nuevomaest.txt”. (Suponga que las compras se indican mediante montos positivos en el archivo de transacciones, y los pagos mediante montos negativos.) Cuando haya un registro maestro para una cuenta específi ca, pero no haya un registro de transacciones correspondiente, simplemente escriba el regis-tro maestro en "nuevomaest.txt". Cuando haya un registro de transacciones pero no un registro maestro correspondiente, imprima en un archivo de registro el mensaje "Hay un registro de transacciones no aso-ciado para ese numero de cliente ..." (utilice el número de cuenta del registro de transacciones). El ar-chivo de registro debe ser un archivo de texto llamado “registro.txt”.

17.6 (Asociación de archivos con varias transacciones) Es posible (y muy común) tener varios registros de trans-acciones con la misma clave de registro. Por ejemplo, esta situación ocurre cuando un cliente hace varias compras y pagos en efectivo durante un periodo de negocios. Rescriba su programa para asociar archivos de cuentas por cobrar del ejercicio 17.5, para proporcionar la posibilidad de manejar varios registros de transacciones con la misma clave de registro. Modifique los datos de prueba de CrearDatos.java para incluir los registros de transacciones adicionales de la figura 17.24.

Ejercicios 763

Fig. 17.24 � Registros de transacciones adicionales.

Número de cuenta Monto en dólares

300 83.89

700 80.78

700 1.53

Fig. 17.25 � Dígitos y letras de los teclados telefónicos.

Dígito Letras Dígito Letras Dígito Letras

2 A B C 5 J K L 8 T U V

3 D E F 6 M N O 9 W X Y

4 G H I 7 P R S

17.7 (Asociación de archivos con serialización de objetos) Vuelva a crear su solución para el ejercicio 17.6, usando la serialización de objetos. Use las instrucciones del ejercicio 17.3 como base para este programa. Tal vez sea conveniente crear aplicaciones para que lean los datos almacenados en los archivos .ser; puede modificar el código de la sección 17.5.2 para este fin.

17.8 (Generador de palabras de números telefónicos) Los teclados telefónicos estándar contienen los dígitos del cero al nueve. Cada uno de los números del dos al nueve tiene tres letras asociadas (figura 17.25). A muchas personas se les dificulta memorizar números telefónicos, por lo que utilizan la correspondencia entre los dígitos y las letras para desarrollar palabras de siete letras que corresponden a sus números telefónicos. Por ejemplo, una persona cuyo número telefónico sea 686-3767 podría utilizar la correspondencia indicada en la figura 17.25 para desarrollar la palabra de siete letras “NUMEROS”. Cada palabra de siete letras corresponde exactamente a un número telefónico de siete dígitos. El restaurante que desea incrementar su negocio de comidas para llevar podría lograrlo utilizando el número 266-4327 (es decir, “COMIDAS”).

Cada número telefónico de siete letras corresponde a muchas palabras de siete letras distintas, pero la mayoría de estas palabras representan yuxtaposiciones irreconocibles de letras. Sin embargo, es posible que el dueño de una carpintería se complazca en saber que el número telefónico de su taller, 683-2537, corresponde a “MUEBLES”. El propietario de una tienda de licores estaría, sin duda, feliz de averiguar que el número telefónico 232-4327 corresponde a “BEBIDAS”. Un veterinario con el número telefónico 627-2682 se complacería en saber que ese número corresponde a las letras “MASCOTA”. El propietario de una tienda de música estaría complacido en saber que su número telefónico 687-4225 corresponde a “MUSICAL”.

Escriba un programa que, dado un número de siete dígitos, utilice un objeto PrintStream para escribir en un archivo todas las combinaciones posibles de palabras de siete letras que corresponden a ese número. Hay 2,187 (37) combinaciones posibles. Evite los números telefónicos con los dígitos 0 y 1.

17.9 (Encuesta estudiantil) La figura 7.8 contiene un arreglo de respuestas a una encuesta, el cual está codificado directamente en el programa. Suponga que deseamos procesar resultados de encuestas que se guarden en un archivo.

Este ejercicio requiere de dos programas separados. Primero, cree una aplicación que pida al usuario las respuestas de la encuesta y que escriba cada respuesta en un archivo. Utilice un objeto Formatter para crear un archivo llamado numeros.txt. Cada entero debe escribirse utilizando el método format. Después modifique el programa de la figura 7.8 para leer las respuestas de la encuesta del archivo numeros.txt. Las respuestas deben leerse del archivo mediante el uso de un objeto Scanner. Use el método nextInt para introducir un entero del archivo a la vez. El programa deberá seguir leyendo respues-tas hasta que llegue al fin del archivo. Los resultados deberán escribirse en el archivo de texto “salida.txt”.

17.10 (Agregar serialización de objetos a la aplicación de dibujo MiFigura) Modifique el ejercicio 14.17 para permi-tir que el usuario guarde un dibujo en un archivo, o cargue un dibujo anterior de un archivo, usando la serialización de objetos. Agregue los botones Cargar (para leer objetos de un archivo), Guardar (para escribir objetos en un archivo). Use un objeto ObjectOutputStream para escribir en el archivo y un objeto ObjectInputStream para leer del archivo. Escriba el arreglo de objetos MiFigura usando el método writeObject (clase ObjectOutputStream) y lea el arreglo usando el método readObject (ObjectInputStream). El mecanismo de serialización de objetos puede leer o escribir arreglos completos; no es necesario manipular cada elemento del arreglo de objetos MiFigura por separado. Simplemente se requiere que todas las figuras sean Serializable. Para los botones Cargar y Guardar, use un objeto JFileChooser para permitir que el usuario seleccione el archivo en el que se almacenarán las figuras, o del que se leerán. Cuando el usuario ejecute el programa por primera vez, no se mostrarán figuras en la pantalla. El usuario puede mostrar figuras abriendo un archivo de figuras previa-mente guardado, o puede dibujar nuevas figuras. Una vez que haya figuras en la pantalla, los usuarios podrán guardarlas en un archivo, usando el botón Guardar.

Marcar la diferencia17.11 (Explorador de phishing) El phishing es una forma de robo de identidad, en la que mediante un correo, el emisor se hace pasar por una fuente confiable, e intenta adquirir información privada, como nombres de usuario, contraseñas, números de tarjetas de crédito y número de seguro social. Los correos electrónicos de phishing que afirman provenir de bancos, compañías de tarjetas de crédito, sitios de subastas, redes sociales y sistemas de pago en línea populares pueden tener una apariencia bastante legítima. A menudo, estos mensajes fraudulentos proveen vínculos a sitios Web falsificados, en donde se pide al usuario que introduzca información confidencial.

Visite McAfee® (www.mcafee.com/us/threat_center/anti_phishing/phishing_top10.html), Security Extra (www.securityextra.com/) y otros sitios Web en donde encontrará listas de las principales estafas de phishing. Visite también el sitio Web del Grupo de trabajo antiphishing (www.antiphishing.org/), y el sitio Web de Investigaciones cibernéticas del FBI (www.fbi.gov/cyberinvest/cyberhome.htm), en donde encontrará información sobre las estafas más recientes y cómo protegerse a sí mismo.

Cree una lista de 30 palabras, frases y nombres de compañías que se encuentren con frecuencia en los mensajes de phishing. Asigne el valor de un punto a cada una, con base en una estimación de la probabilidad de que aparezca en un mensaje de phishing (por ejemplo, un punto si es poco probable, dos puntos si es algo probable, o tres puntos si es muy probable). Escriba una aplicación que explore un archivo de texto en busca de estos términos y frases. Para cada ocurrencia de una palabra clave o frase dentro del archivo de texto, agregue el valor del punto asignado al total de puntos para esa palabra o frase. Para cada palabra clave o frase encontrada, imprima en pantalla una línea con la palabra o frase, el número de ocurrencias y el total de puntos. Después muestre el total de puntos para todo el mensaje. ¿Asigna su programa un total de puntos alto a ciertos mensajes reales de correo electrónico de phishing que haya recibido? ¿Asigna un total de puntos alto a ciertos correos electrónicos legítimos que haya recibido?

764 Capítulo 17 Archivos, fl ujos y serialización de objetos

Recursividad 18Debemos aprender a explorar todas las opciones y posibilidades a las que nos enfrentamos en un mundo complejo, que evoluciona rápidamente.—James William Fulbright

Oh, maldita iteración, que eres capaz de corromper hasta a un santo.—William Shakespeare

Es un pobre orden de memoria, que sólo funciona al revés.—Lewis Carroll

La vida sólo puede comprenderse al revés; pero debe vivirse hacia delante.—Soren Kierkegaard

O b j e t i v o sEn este capítulo aprenderá a:

■ Comprender el concepto de recursividad.

■ Escribir y utilizar métodos recursivos.

■ Determinar el caso base y el paso de recursividad en un algoritmo recursivo.

■ Conocer cómo el sistema maneja las llamadas a métodos recursivos.

■ Conocer las diferencias entre recursividad e iteración, y cuándo es apropiado utilizar cada una.

■ Conocer las figuras geométricas llamadas fractales, y cómo se dibujan mediante la recursividad.

■ Conocer el concepto de “vuelta atrás” recursiva (backtracking), y por qué es una técnica efectiva para solucionar problemas.

766 Capítulo 18 Recursividad

18.1 Introducción

18.2 Conceptos de recursividad

18.3 Ejemplo de uso de recursividad: factoriales

18.4 Ejemplo de uso de recursividad: serie de Fibonacci

18.5 La recursividad y la pila de llamadas a métodos

18.6 Comparación entre recursividad e iteración

18.7 Las torres de Hanoi

18.8 Fractales

18.9 “Vuelta atrás” recursiva (backtracking)

18.10 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

18.1 IntroducciónLos programas que hemos visto hasta ahora están estructurados en general como métodos que se lla-man entre sí, de una manera jerárquica. Para algunos problemas, es conveniente hacer que un método se llame a sí mismo. Dicho método se conoce como método recursivo; este método se puede llamar en forma directa o indirecta a través de otro método. La recursividad es un tema importante, que puede tratarse de manera extensa en los cursos de ciencias computacionales de nivel superior. En este capí-tulo consideraremos la recursividad en forma conceptual, y después presentaremos varios programas que contienen métodos recursivos. En la figura 18.1 se sintetizan los ejemplos y ejercicios de recursi-vidad que se incluyen en este libro.

Capítulo Ejemplos y ejercicios de recursividad en este libro

18 Método factorial (fi guras 18.3 y 18.4)Método Fibonacci (fi gura 18.5)Torres de Hanoi (fi guras 18.11)Fractales (fi guras 18.18 y 18.19)¿Qué hace este código? (ejercicios 18.7, 18.12 y 18.13)Encuentre el error en el siguiente código (ejercicio 18.8)Elevar un entero a una potencia entera (ejercicio 18.9)Visualización de la recursividad (ejercicio 18.10)Máximo común divisor (ejercicio 18.11)Determinar si una cadena es un palíndromo (ejercicio 18.14)Ocho reinas (ejercicio 18.15)Imprimir un arreglo (ejercicio 18.16)Imprimir un arreglo en forma inversa (ejercicio 18.17)Mínimo valor en un arreglo (ejercicio 18.18)Fractal de estrella (ejercicio 18.19)Recorrido de laberinto usando la “vuelta atrás” recursiva (ejercicio 18.20)Generación de laberintos al azar (ejercicio 18.21)Laberintos de cualquier tamaño (ejercicio 18.22)Tiempo necesario para calcular un número de Fibonacci (ejercicio 18.23)

19 Ordenamiento por combinación (fi guras 19.10 y 19.11)Búsqueda lineal (ejercicio 19.8)Búsqueda binaria (ejercicio 19.9)Quicksort (ejercicio 19.10)

Fig. 18.1 � Resumen de los ejemplos y ejercicios de recursividad en este libro (parte 1 de 2).

18.2 Conceptos de recursividad 767

18.2 Conceptos de recursividadLos métodos para solucionar problemas recursivos tienen varios elementos en común. Cuando se hace una llamada a un método recursivo para resolver un problema, el método en realidad es capaz de resolver sólo el (los) caso(s) más simple(s), o caso(s) base. Si se hace la llamada al método con un caso base, el método devuelve un resultado. Si se hace la llamada al método con un problema más complejo, por lo general divide el problema en dos piezas conceptuales: una pieza que el método sabe cómo resolver y otra pieza que no sabe cómo resolver. Para que la recursividad sea factible, esta última pieza debe ser similar al problema original, pero una versión ligeramente más sencilla o simple del mismo. Debido a que este nuevo problema se parece al problema original, el método llama a una nueva copia de sí mismo para trabajar en el problema más pequeño; a esto se le conoce como llamada recursiva, y también como paso recursivo. Por lo general, el paso recursivo incluye una instrucción return, ya que su resultado se com-bina con la parte del problema que el método supo cómo resolver, para formar un resultado que se pasará de vuelta al método original que hizo la llamada. Este concepto de separar el problema en dos porciones más pequeñas es una forma del método “divide y vencerás” que presentamos en el capítulo 6.

El paso recursivo se ejecuta mientras siga activa la llamada original al método (es decir, que no haya terminado su ejecución). Se pueden producir muchas llamadas recursivas más, a medida que el método divide cada nuevo subproblema en dos piezas conceptuales. Para que la recursividad termine en un mo-mento dado, cada vez que el método se llama a sí mismo con una versión más simple del problema origi-nal, la secuencia de problemas cada vez más pequeños debe converger en un caso base. Cuando el método reconoce el caso base, devuelve un resultado a la copia anterior del método. Después se origina una se-cuencia de retornos, hasta que la llamada al método original devuelve el resultado final al método que lo llamó.

Un método recursivo puede llamar a otro método, que a su vez puede hacer una llamada de vuelta al método recursivo. A dicho proceso se le conoce como llamada recursiva indirecta o recursividad indirecta. Por ejemplo, el método A llama al método B, que hace una llamada de vuelta al método A. Esto se sigue considerando como recursividad, debido a que la segunda llamada al método A se realiza mientras la primera sigue activa; es decir, la primera llamada al método A no ha terminado todavía de ejecutarse (debido a que está esperando que el método B le devuelva un resultado) y no ha regresado al método original que llamó al método A.

Para comprender mejor el concepto de recursividad, veamos un ejemplo que es bastante común para los usuarios de computadora: la definición recursiva de un directorio en una computadora. Por lo general, una computadora almacena los archivos relacionados en un directorio. Este directorio puede estar vacío, puede contener archivos y/o puede contener otros directorios (que por lo general se conocen como subdirectorios). A su vez, cada uno de estos directorios puede contener también archivos y directorios. Si queremos enlistar cada archivo en un directorio (incluyendo todos los ar-chivos en los subdirectorios de ese directorio), necesitamos crear un método que lea primero los archi-vos del directorio inicial y que después haga llamadas recursivas para enlistar los archivos en cada uno de los subdirectorios de ese directorio. El caso base ocurre cuando se llega a un directorio que no con-

Capítulo Ejemplos y ejercicios de recursividad en este libro

22(en inglés)

Inserción en árbol binario (fi gura 22.17)Recorrido preorden de un árbol binario (22.17)Recorrido inorden de un árbol binario (22.17)Recorrido postorden de un árbol binario (22.17)Imprimir una lista enlazada en forma inversa (ejercicio 22.20)Buscar en una lista enlazada (ejercicio 22.21)

Fig. 18.1 � Resumen de los ejemplos y ejercicios de recursividad en este libro (parte 2 de 2).

768 Capítulo 18 Recursividad

tenga subdirectorios. En este punto, se han enlistado todos los archivos en el directorio original y no se necesita más la recursividad.

18.3 Ejemplo de uso de recursividad: factorialesVamos a escribir un programa recursivo, para realizar un popular cálculo matemático. Considere el factorial de un entero positivo n, escrito como n! (y se pronuncia como “factorial de n”), que viene siendo el producto

n · (n – 1) · (n – 2) · … · 1

en donde 1! es igual a 1 y 0! se define como 1. Por ejemplo, 5! es el producto 5 · 4 · 3 · 2 · 1, que es igual a 120.

El factorial del entero numero (en donde numero ≥ 0) puede calcularse de manera iterativa (sin recursividad), usando una instrucción for de la siguiente manera:

factorial = 1;

for ( int contador = número; contador >= 1; contador-- ) factorial *= contador;

Podemos llegar a una declaración recursiva del método del factorial, si observamos la siguiente relación:

n! = n · (n – 1)!

Por ejemplo, 5! es sin duda igual a 5 · 4!, como se muestra en las siguientes ecuaciones:

5! = 5 · 4 · 3 · 2 · 1 5! = 5 · (4 · 3 · 2 · 1) 5! = 5 · (4!)

La evaluación de 5! procedería como se muestra en la figura 18.2. La figura 18.2(a) muestra cómo procede la sucesión de llamadas recursivas hasta que 1! (el caso base) se evalúa como 1, lo cual termina

Fig. 18.2 � Evaluación recursiva de 5!.

(a) Secuencia de llamadas recursivas

5 * 4!

4 * 3!

3 * 2!

2 * 1!

5!

1

(b) Valores devueltos de cada llamada recursiva

Valor final = 120

se devuelve 5! = 5 * 24 = 120

se devuelve 4! = 4 * 6 = 24

se devuelve 3! = 3 * 2 = 6

se devuelve 2! = 2 * 1 = 2

se devuelve 1

5 * 4!

4 * 3!

3 * 2!

2 * 1!

5!

1

18.3 Ejemplo de uso de recursividad: factoriales 769

la recursividad. La figura 18.2(b) muestra los valores devueltos de cada llamada recursiva al método que hizo la llamada, hasta que se calcula y devuelve el valor final.

En la figura 18.3 se utiliza la recursividad para calcular e imprimir los factoriales de los enteros del 0 al 21. El método recursivo factorial (líneas 7 a 13) realiza primero una evaluación para determi-nar si una condición de terminación (línea 9) es true. Si numero es menor o igual que 1 (el caso base), factorial devuelve 1, ya no es necesaria más recursividad y el método regresa. Si numero es mayor que 1, en la línea 12 se expresa el problema como el producto de numero y una llamada recursiva a factorial en la que se evalúa el factorial de numero – 1, el cual es un problema un poco más pequeño que el cálculo original, factorial ( numero ).

1 // Fig. 18.3: CalculadoraFactorial.java 2 // Método factorial recursivo. 3 4 public class CalculadoraFactorial 5 { 6 // método factorial recursivo (asume que su parámetro es >= 0) 7 public static long factorial( long numero ) 8 { 9 if ( numero <= 1 ) // evalúa el caso base10 return 1; // casos base: 0! = 1 y 1! = 111 else // paso recursivo12 return numero * factorial( numero - 1 );13 } // fin del método factorial1415 // imprime factoriales para los valores del 0 al 2116 public static void main( String[] args )17 {18 // calcula los factoriales del 0 al 2119 for ( int contador = 0; contador <= 21; contador++ )20 System.out.printf( “%d! = %d\n”, contador, factorial( contador ) );21 } // fin de main22 } // fin de la clase CalculadoraFactorial

0! = 11! = 12! = 23! = 64! = 245! = 120...12! = 479001600 ----- 12! Produce desbordamiento para variables int...20! = 243290200817664000021! = -4249290049419214848 ----- 21! Produce desbordamiento para variables long

Fig. 18.3 � Cálculos de factoriales con un método recursivo.

Error común de programación 18.1Si omitimos el caso base o escribimos el paso recursivo en forma incorrecta, de manera que no converja en el caso base, se puede producir un error lógico conocido como recursividad infinita, en donde se realizan llamadas recursivas en forma continua, hasta que se agota la memoria. Este error es análogo al problema de un ciclo infinito en una solución iterativa (sin recursividad).

770 Capítulo 18 Recursividad

El método main (líneas 16 a 21) muestra los factoriales del 0 al 21. La llamada al método factorial ocurre en la línea 20. Este método recibe un parámetro de tipo long y devuelve un resultado de tipo long. Los resultados del programa muestran que los valores de los factoriales crecen con rapidez. Utilizamos el tipo long (que puede representar enteros relativamente grandes) para que el programa pueda calcular factoriales mayores que 12!. Por desgracia, el método factorial produce valores grandes con tanta rapidez que exceden al valor long máximo cuando tratamos de calcular 21!, como puede ver en la úl-tima línea de salida del programa.

Debido a las limitaciones de los tipos integrales, tal vez se necesiten variables float o double para calcular factoriales o números más grandes. Esto apunta a una debilidad en algunos lenguajes de pro-gramación: a saber, que los lenguajes no se extienden fácilmente para manejar los requerimientos únicos de una aplicación. Como vimos en el capítulo 9, Java es un lenguaje extensible que nos permite crear números arbitrariamente grandes, si lo deseamos. De hecho, el paquete java.math cuenta con las clases BigInteger y BigDecimal, que son específicas para los cálculos de precisión arbitraria, que no pueden llevarse a cabo con los tipos primitivos. Aprenderá más sobre estas clases en

download.oracle.com/javase/6/docs/api/java/math/BigInteger.html download.oracle.com/javase/6/docs/api/java/math/BigDecimal.html

Reimplementación de la clase CalculadoraFactorial mediante la clase BigInteger En la figura 18.4 se reimplementa la clase CalculadoraFactorial mediante el uso de variables BigInteger. Para demostrar valores más grandes de los que pueden guardar las variables long, calcu-lamos los factoriales de los números 0 al 50. En la línea 3 se importa la clase BigInteger del paquete java.math. El nuevo método factorial (líneas 8 a 15) recibe un objeto BigInteger como argumento, y devuelve un BigInteger.

1 // Fig. 18.4: CalculadoraFactorial.java

2 // Método factorial recursivo.

3 import java.math.BigInteger;

4

5 public class CalculadoraFactorial

6 {

7 // método factorial recursivo (asume que su parámetro es >= 0)

8 public static BigInteger factorial( BigInteger numero )

9 {

10 if ( numero.compareTo( BigInteger.ONE) <= 0 ) // evalúa el caso base

11 return BigInteger.ONE; // casos base: 0! = 1 y 1! = 1

12 else // paso recursivo

13 return numero.multiply(

14 factorial( numero.subtract( BigInteger.ONE) ) );

15 } // fin del método factorial

16

17 // imprime factoriales para los valores del 0 al 50

18 public static void main( String[] args )

19 {

20 // calcula los factoriales del 0 al 50

21 for ( int contador = 0; contador <= 50; contador++ )

22 System.out.printf( “%d! = %d\n”, contador,

23 factorial( BigInteger.valueOf( contador ) ) );

24 } // fin de main

25 } // fin de la clase CalculadoraFactorial

Fig. 18.4 � Cálculos del factorial con un método recursivo (parte 1 de 2).

18.4 Ejemplo de uso de recursividad: serie de Fibonacci 771

Puesto que BigInteger no es un tipo primitivo, no podemos usar los operadores aritméticos, re-lacionales y de igualdad con objetos BigInteger; en su defecto debemos usar métodos BigInteger para realizar estas tareas. La línea 10 evalúa el caso base usando el método compareTo de BigInteger. Este método compara el objeto BigInteger que llama el método con el argumento BigInteger del método. El método devuelve -1 si el objeto BigInteger que llamó al método es menor que el argumento, 0 si son iguales o 1 si el objeto BigInteger que llamó al método es mayor que el argumento. La línea 10 compara el objeto BigInteger llamado numero con la constante ONE de BigInteger, la cual representa el valor entero 1. Si compareTo devuelve -1 o 0, entonces numero es menor o igual a 1 (el caso base) y el método devuelve la constante BigInteger.ONE. De lo contrario, las líneas 13 y 14 realizan el paso de recursividad usando los métodos multiply y subtract de BigInteger para implementar los cálculos requeridos para multiplicar numero por el factorial de numero - 1). La salida del programa muestra que BigInteger maneja con facilidad los valores grandes producidos por el cálculo del factorial.

18.4 Ejemplo de uso de recursividad: serie de FibonacciLa serie de Fibonacci,

0, 1, 1, 2, 3, 5, 8, 13, 21, …

empieza con 0 y 1, y tiene la propiedad de que cada número subsiguiente de Fibonacci es la suma de los dos números anteriores. Esta serie ocurre en la naturaleza y describe una forma de espiral. La proporción de números de Fibonacci sucesivos converge en un valor constante de 1.618…, un número denominado proporción dorada, o media dorada. Los humanos tienden a descubrir que la media dorada es estética-mente placentera. A menudo, los arquitectos diseñan ventanas, cuartos y edificios con una proporción de longitud-anchura en la que se utiliza la media dorada. A menudo, las tarjetas postales se diseñan con una proporción de longitud-anchura de media dorada.

La serie de Fibonacci se puede definir de manera recursiva como:

fi bonacci(0) = 0fi bonacci(1) = 1fi bonacci(n) = fi bonacci(n – 1) + fi bonacci(n – 2)

Hay dos casos base para el cálculo de Fibonacci: fibonacci(0) se define como 0, y fibonacci(1) se define como 1. El programa de la figura 18.5 calcula el i-ésimo número de Fibonacci en forma recur-siva, usando el método fibonacci (líneas 10 a 18). El método main (líneas 21 a 26) prueba a fibonacci, mostrando los valores de Fibonacci del 0 al 40. La variable contador creada en el encabezado de la

0! = 11! = 12! = 23! = 6… 21! = 51090942171709440000 --- 21! y valores mayores ya no provocan desbordamiento22! = 1124000727777607680000… 47! = 25862324151116818064296435515361197996919763238912000000000048! = 1241391559253607267086228904737337503852148635467776000000000049! = 60828186403426756087225216332129537688755283137921024000000000050! = 30414093201713378043612608166064768844377641568960512000000000000

Fig. 18.4 � Cálculos del factorial con un método recursivo (parte 2 de 2).

772 Capítulo 18 Recursividad

instrucción for (línea 23) indica cuál número de Fibonacci se debe calcular para cada iteración del ciclo. Los números de Fibonacci tienden a aumentar con rapidez (aunque no tanto como los factoria-les). Por lo tanto, utilizamos el tipo BigInteger como el tipo del parámetro y el tipo de valor de re-torno del método fibonacci.

1 // Fig. 18.5: CalculadoraFibonacci.java 2 // Método fibonacci recursivo. 3 import java.math.BigInteger; 4 5 public class CalculadoraFibonacci 6 { 7 private static BigInteger DOS = BigInteger.valueOf( 2 ); 8 9 // declaración recursiva del método fibonacci10 public static BigInteger fibonacci( BigInteger numero )11 {12 if ( numero.equals( BigInteger.ZERO ) || 13 numero.equals( BigInteger.ONE ) ) // casos base14 return numero;15 else // paso recursivo16 return fibonacci( numero.subtract( BigInteger.ONE ) ).add(17 fibonacci( numero.subtract( DOS ) ) );18 } // fin del método fibonacci1920 // muestra los valores de fibonacci del 0 al 4021 public static void main( String[] args)22 {23 for ( int contador = 0; contador <= 40; contador++ )24 System.out.printf( “Fibonacci de %d es: %d\n”, contador,25 fibonacci( BigInteger.valueOf( contador ) ) );26 } // fin de main27 } // fin de la clase CalculadoraFibonacci

Fibonacci de 0 es: 0Fibonacci de 1 es: 1Fibonacci de 2 es: 1Fibonacci de 3 es: 2Fibonacci de 4 es: 3Fibonacci de 5 es: 5Fibonacci de 6 es: 8Fibonacci de 7 es: 13Fibonacci de 8 es: 21Fibonacci de 9 es: 34Fibonacci de 10 es: 55… Fibonacci de 37 es: 24157817Fibonacci de 38 es: 39088169Fibonacci de 39 es: 63245986Fibonacci de 40 es: 102334155

Fig. 18.5 � Números de Fibonacci generados mediante un método recursivo.

La llamada al método fibonacci (línea 25 de la figura 18.5) desde main no es una llamada recur-siva, pero todas las llamadas subsiguientes a fibonacci que se llevan a cabo desde las líneas 16 y 17 de la

18.4 Ejemplo de uso de recursividad: serie de Fibonacci 773

figura 18.5 son recursivas, ya que en ese punto es el mismo método fibonacci el que inicia las llama-das. Cada vez que se hace una llamada a fibonacci, se evalúan inmediatamente los dos casos base: numero igual a 0 o numero igual a 1 (figura 18.5, líneas 12 y 13). Utilizamos las constantes ZERO y ONE de BigInteger para representar los valores 0 y 1, respectivamente. Si la condición en las líneas 12 y 13 es true, fibonacci sólo devuelve numero, ya que fibonacci(0) es 0 y fibonacci(1) es 1. Lo intere-sante es que, si numero es mayor que 1, el paso recursivo genera dos llamadas recursivas (líneas 16 y 17), cada una de ellas para un problema un poco más pequeño que el de la llamada original a fibonacci. Las líneas 16 y 17 utilizan los métodos add y subtract de BigInteger para ayudar a implementar el paso recursivo. También usamos una constante de tipo BigInteger llamada DOS, que definimos en la línea 7.

Análisis de las llamadas al método Fibonacci La figura 18.6 muestra cómo el método fibonacci evalúa fibonacci(3). En la parte inferior de la figura, nos quedamos con los valores 1, 0 y 1; los resultados de evaluar los casos base. Los primeros dos valores de retorno (de izquierda a derecha), 1 y 0, se devuelven como los valores para las llamadas fibonacci(1) y fibonacci(0). La suma 1 más 0 se devuelve como el valor de fibonacci(2). Esto se suma al resultado (1) de la llamada a fibonacci(1), para producir el valor 2. Después, este valor final se devuelve como el valor de fibonacci(3).

Fig. 18.6 � Conjunto de llamadas recursivas para fibonacci(3).

+

fibonacci( 3 )

fibonacci( 2 ) fibonacci( 1 )return +

fibonacci( 1 ) fibonacci( 0 ) return 1

return 0return 1

return

La figura 18.6 genera ciertas preguntas interesantes, en cuanto al orden en el que los compiladores de Java evalúan los operandos de los operadores. Este orden es distinto del orden en el que se aplican los operadores a sus operandos; a saber, el orden que dictan las reglas de la precedencia de operadores. De la figura 18.6, parece ser que mientras se evalúa fibonacci(3), se harán dos llamadas recursivas: fibonacci(2) y fibonacci(1). Pero ¿en qué orden se harán estas llamadas? El lenguaje Java especifica que el orden de evaluación de los operandos es de izquierda a derecha. Por ende, la llamada a fibonacci(2) se realiza primero, y después la llamada a fibonacci(1).

Hay que tener cuidado con los programas recursivos como el que utilizamos aquí para generar números de Fibonacci. Cada invocación del método fibonacci que no coincide con uno de los casos base (0 o 1) produce dos llamadas recursivas más al método fibonacci. Por lo tanto, este conjunto de llamadas recursivas se sale rápidamente de control. Para calcular el valor 20 de Fibonacci con el programa

774 Capítulo 18 Recursividad

de la figura 18.5, se requieren 21,891 llamadas al método fibonacci; ¡para calcular el valor 30 de Fibonacci se requieren 2,692,537 llamadas! A medida que trate de calcular valores más grandes de Fibo-nacci, observará que cada número de Fibonacci consecutivo que calcule con la aplicación requiere un aumento considerable en tiempo de cálculo y en el número de llamadas al método fibonacci. Por ejemplo, el valor 31 de Fibonacci requiere 4,356,617 llamadas, y ¡el valor 32 de Fibonacci requiere 7,049,155 llamadas! Como puede ver, el número de llamadas al método fibonacci se incrementa con rapidez; 1,664,080 llamadas adicionales entre los valores 30 y 31 de Fibonacci, y ¡2,692,538 llamadas adicionales entre los valores 31 y 32 de Fibonacci! La diferencia en el número de llamadas realizadas entre los valores 31 y 32 de Fibonacci es de más de 1.5 veces la diferencia en el número de llamadas para los valores entre 30 y 31 de Fibonacci. Los problemas de esta naturaleza pueden humillar incluso hasta a las computadoras más poderosas del mundo. [Nota: en el campo de la teoría de la complejidad, los científicos de computadoras estudian qué tanto tienen que trabajar los algoritmos para completar sus tareas. Las cuestiones relacionadas con la complejidad se discuten con detalle en un curso del plan de estudios de ciencias computacionales de nivel superior, al que por lo general se le llama “Algoritmos”. En el capítulo 19, Búsqueda, ordenamiento y Big O, presentamos varias cuestiones acerca de la com-plejidad.] En los ejercicios le pediremos que mejore el programa de Fibonacci de la figura 18.5, de tal forma que calcule el monto aproximado de tiempo requerido para realizar el cálculo. Para este fin, invocará al método static de System llamado currentTimeMillis, el cual no recibe argumentos y devuelve el tiempo actual de la computadora en milisegundos.

Tip de rendimiento 18.1Evite los programas recursivos al estilo de Fibonacci, ya que producen una “explosión” exponencial de llamadas a métodos.

18.5 La recursividad y la pila de llamadas a métodosEn el capítulo 6 se presentó la estructura de datos tipo pila, para comprender cómo Java realiza las llamadas a los métodos. Hablamos sobre la pila de llamadas a métodos (también conocida como la pila de ejecución del programa) y los registros de activación. En esta sección, utilizaremos estos concep-tos para demostrar la forma en que la pila de ejecución del programa maneja las llamadas a los métodos recursivos.

Para empezar, regresemos al ejemplo de Fibonacci; en específico, la llamada al método fibonacci con el valor 3, como en la figura 18.6. Para mostrar el orden en el que se colocan los registros de activa-ción de las llamadas a los métodos en la pila, hemos clasificado las llamadas a los métodos con letras en la figura 18.7.

Cuando se hace la primera llamada al método (A), un registro de activación se mete en la pila de ejecución del programa, que contiene el valor de la variable local numero (3 en este caso). Esta pila, que incluye el registro de activación para la llamada A al método, se ilustra en la parte (a) de la figura 18.8. [Nota: aquí utilizamos una pila simplificada. Una pila de ejecución del programa real y sus registros de activación serían más complejos que en la figura 18.8, ya que contienen información como la ubica-ción a la que va a regresar la llamada al método cuando haya terminado de ejecutarse].

Dentro de la llamada al método A se realizan las llamadas B y E. La llamada original al método no se ha completado, por lo que su registro de activación permanece en la pila. La primera llamada al método en realizarse desde el interior de A es la llamada B al método, por lo que se mete el registro de activación para la llamada B al método en la pila, encima del registro de activación para la llamada A al método. La llamada B al método debe ejecutarse y completarse antes de realizar la llamada E. Dentro de la llamada B al método, se harán las llamadas C y D al método. La llamada C se realiza primero, y su registro de activación se mete en la pila [parte (b) de la figura 18.8]. La llamada B al método todavía no ha terminado, y su registro de activación sigue en la pila de llamadas a métodos. Cuando se ejecuta la llamada C, no realiza ninguna otra llamada al método, sino que simplemente devuelve el valor 1.

18.5 La recursividad y la pila de llamadas a métodos 775

Cuando este método regresa, su registro de activación se saca de la parte superior de la pila. La llama-da al método en la parte superior de la pila es ahora B, que continúa ejecutándose y realiza la llamada D al método. El registro de activación para la llamada D se mete en la pila [parte (c) de la figura 18.8]. La llamada D al método se completa sin realizar ninguna otra llamada, y devuelve el valor 0. Después, se saca el registro de activación para esta llamada de la pila. Ahora, ambas llamadas al método que se realizaron desde el interior de la llamada B al método han regresado. La llamada B continúa ejecután-dose, y devuelve el valor 1. La llamada B al método se completa y su registro de activación se saca de la pila. En este punto, el registro de activación para la llamada A al método se encuentra en la parte su-perior de la pila, y el método continúa su ejecución. Este método realiza la llamada E al método, cuyo registro de activación se mete ahora en la pila [parte (d) de la figura 18.8]. La llamada E al método se completa y devuelve el valor 1. El registro de activación para esta llamada al método se saca de la pila, y una vez más la llamada A al método continúa su ejecución. En este punto, la llamada A no realizará ninguna otra llamada al método y puede terminar su ejecución, para lo cual devuelve el valor 2 al mé-todo que llamó a A (fibonacci(3) = 2). El registro de activación de A se saca de la pila. El método en ejecución es siempre el que tiene su registro de activación en la parte superior de la pila, y el registro de activación para ese método contiene los valores de sus variables locales.

Fig. 18.7 � Llamadas al método realizadas dentro de la llamada fibonacci( 3 ).

fibonacci( 3 )A

fibonacci( 2 )B fibonacci( 1 )E

fibonacci( 1 )C fibonacci( 0 )D

return 0return 1

return 1

Fig. 18.8 � Llamadas al método en la pila de ejecución del programa.

Parte superior de la pila

(d)

Llamada al método: E numero = 1

Llamada al método: A numero = 3

Parte superior de la pila

(c)

Llamada al método: D numero = 0

Llamada al método: B numero = 2

Llamada al método: A numero = 3

Parte superior de la pila

(b)

Llamada al método: C numero = 1

Llamada al método: B numero = 2

Llamada al método: A numero = 3

Parte superior de la pila

(a)

Llamada al método: Anumero = 3

776 Capítulo 18 Recursividad

18.6 Comparación entre recursividad e iteraciónEn las secciones anteriores estudiamos los métodos factorial y fibonacci, que pueden implemen-tarse fácilmente, ya sea en forma recursiva o iterativa. En esta sección compararemos los dos métodos, y veremos por qué le convendría al programador elegir un método en vez del otro, en una situación específica.

Tanto la iteración como la recursividad se basan en una instrucción de control: la iteración utiliza una instrucción de repetición (for, while o do…while), mientras que la recursividad utiliza una ins-trucción de selección (if, if…else o switch). Tanto la iteración como la recursividad implican la repetición: la iteración utiliza de manera explícita una instrucción de repetición, mientras que la recursividad logra la repetición a través de llamadas repetidas al método. La iteración y la recursividad implican una prueba de terminación: la iteración termina cuando falla la condición de continuación de ciclo, mientras que la recursividad termina cuando se llega a un caso base. La iteración con repe-tición controlada por contador y la recursividad llegan en forma gradual a la terminación: la itera-ción sigue modificando un contador, hasta que éste asume un valor que hace que falle la condición de continuación de ciclo, mientras que la recursividad sigue produciendo versiones cada vez más pequeñas del problema original, hasta que se llega a un caso base. Tanto la iteración como la recursi-vidad pueden ocurrir infinitamente: un ciclo infinito ocurre con la iteración si la prueba de conti-nuación de ciclo nunca se vuelve falsa, mientas que la recursividad infinita ocurre si el paso recursivo no reduce el problema cada vez, de forma tal que llegue a converger en el caso base, o si el caso base no se evalúa.

Para ilustrar las diferencias entre la iteración y la recursividad, examinaremos una solución itera-tiva para el problema del factorial (figura 18.9). Se utiliza una instrucción de repetición (líneas 12 y 13) en vez de la instrucción de selección de la solución recursiva (líneas 9 a 12 de la figura 18.3). Ambas soluciones usan una prueba de terminación. En la solución recursiva, en la línea 9 se evalúa el caso base. En la solución iterativa, en la línea 12 se evalúa la condición de continuación de ciclo; si la prueba falla, el ciclo termina. Por último, en vez de producir versiones cada vez más pequeñas del problema original, la solución iterativa utiliza un contador que se modifica hasta que la condición de continua-ción de ciclo se vuelve falsa.

Fig. 18.9 � Solución de factorial iterativa (parte 1 de 2).

1 // Fig. 18.9: CalculadoraFactorial.java

2 // Método factorial iterativo.

3

4 public class CalculadoraFactorial

5 {

6 // declaración recursiva del método factorial

7 public static long factorial( long numero )

8 {

9 long resultado = 1;

10

11 // declaración iterativa del método factorial

12 for ( long i = numero; i >= 1; i-- )

13 resultado *= i;

14

15 return resultado;

16 } // fin del método factorial

17

18 // muestra los factoriales para los valores del 0 al 10

19 public static void main( String[] args )

20 {

18.7 Las torres de Hanoi 777

La recursividad tiene muchas desventajas. Invoca al mecanismo en forma repetida, y en consecuencia se produce una sobrecarga de las llamadas al método. Esta repetición puede ser perjudicial, en términos de tiempo del procesador y espacio de la memoria. Cada llamada recursiva crea otra copia del método (en realidad, sólo las variables del mismo, que se almacenan en el registro de activación); este conjunto de copias puede consumir una cantidad considerable de espacio en memoria. Como la iteración ocurre dentro de un método, se evitan las llamadas repetidas al método y la asignación adicional de memoria.

Observación de ingeniería de software 18.1Cualquier problema que se pueda resolver mediante la recursividad, se puede resolver también mediante la iteración (sin recursividad). Por lo general se prefiere un método recursivo a uno iterativo cuando el primero refleja con más naturalidad el problema, y se produce un programa más fácil de entender y de depurar. A menudo, puede implemen-tarse un método recursivo con menos líneas de código. Otra razón por la que es preferible elegir un método recursivo es que uno iterativo podría no ser aparente.

Tip de rendimiento 18.2Evite usar la recursividad en situaciones en las que se requiera un alto rendimiento. Las llamadas recursivas requieren tiempo y consumen memoria adicional.

Error común de programación 18.2Hacer que un método no recursivo se llame a sí mismo por accidente, ya sea en forma di-recta o indirecta a través de otro método, puede provocar recursividad infinita.

18.7 Las torres de HanoiEn las secciones anteriores de este capítulo, estudiamos métodos que pueden implementarse con facilidad, tanto en forma recursiva como iterativa. En esta sección presentamos un problema cuya solución recursiva demuestra la elegancia de la recursividad, y cuya solución iterativa tal vez no sea tan aparente.

Las Torres de Hanoi son uno de los problemas clásicos con los que todo científico computacional en ciernes tiene que lidiar. Cuenta la leyenda que en un templo del Lejano Oriente, los sacerdotes inten-

21 // calcula los factoriales del 0 al 10

22 for ( int contador = 0; contador <= 10; contador++ )

23 System.out.printf( “%d! = %d\n”, contador, factorial( contador ) );

24 } // fin de main

25 } // fin de la clase CalculadoraFactorial

0! = 11! = 12! = 23! = 64! = 245! = 1206! = 7207! = 50408! = 403209! = 36288010! = 3628800

Fig. 18.9 � Solución de factorial iterativa (parte 2 de 2).

778 Capítulo 18 Recursividad

tan mover una pila de discos dorados, de una aguja de diamante a otra (figura 18.10). La pila inicial tiene 64 discos insertados en una aguja y se ordenan de abajo hacia arriba, de mayor a menor tamaño. Los sa-cerdotes intentan mover la pila de una aguja a otra, con las restricciones de que sólo se puede mover un disco a la vez, y en ningún momento se puede colocar un disco más grande encima de uno más pequeño. Se cuenta con tres agujas, una de las cuales se utiliza para almacenar discos en forma temporal. Se supone que el mundo acabará cuando los sacerdotes completen su tarea, por lo que hay pocos incentivos para que nosotros podamos facilitar sus esfuerzos.

Fig. 18.10 � Las Torres de Hanoi para el caso con cuatro discos.

aguja 1 aguja 2 aguja 3

Supongamos que los sacerdotes intentan mover los discos de la aguja 1 a la aguja 2. Deseamos desa-rrollar un algoritmo que imprima la secuencia precisa de transferencias de los discos de una aguja a otra.

Si tratamos de encontrar una solución iterativa, es probable que terminemos “atados” manejando los discos sin esperanza. En vez de ello, si atacamos este problema mediante la recursividad podemos produ-cir rápidamente una solución. La acción de mover n discos puede verse en términos de mover sólo n – 1 discos (de ahí la recursividad) de la siguiente forma:

1. Mover n – 1 discos de la aguja 1 a la aguja 2, usando la aguja 3 como un área de almacena-miento temporal.

2. Mover el último disco (el más grande) de la aguja 1 a la aguja 3.

3. Mover n – 1 discos de la aguja 2 a la aguja 3, usando la aguja 1 como área de almacenamiento temporal.

El proceso termina cuando la última tarea implica mover n = 1 disco (es decir, el caso base). Esta tarea se logra con sólo mover el disco, sin necesidad de un área de almacenamiento temporal.

En la figura 18.11, el método resolverTorres (líneas 6 a 25) resuelve el acertijo de las Torres de Hanoi, dado el número total de discos (en este caso 3), la aguja inicial, la aguja final y la aguja de alma-cenamiento temporal como parámetros. El caso base (líneas 10 a 14) ocurre cuando sólo se necesita mover un disco de la aguja inicial a la aguja final. En el paso recursivo (líneas 18 a 24), la línea 18 mueve discos – 1 discos de la primera aguja (agujaOrigen) a la aguja de almacenamiento temporal (agujaTemp). Cuando se han movido todos los discos a la aguja temporal excepto uno, en la línea 21 se mueve el disco más grande de la aguja inicial a la aguja de destino. En la línea 24 se termina el resto de los movimientos, llamando al método resolverTorres para mover discos – 1 discos de manera recursiva, de la aguja temporal (agujaTemp) a la aguja de destino (agujaDestino), esta vez usando la primera aguja (agujaOrigen) como aguja temporal. La línea 35 en main llama al método recursivo resolverTorres, el cual imprime los pasos en el símbolo del sistema.

18.8 Fractales 779

18.8 FractalesUn fractal es una figura geométrica que puede generarse a partir de un patrón que se repite en forma recursiva (figura 18.12). Para modificar la figura, se aplica el patrón a cada segmento de la figura origi-nal. En esta sección analizaremos unas cuantas aproximaciones. [Nota: nos referiremos a nuestras figu-ras geométricas como fractales, aun cuando son aproximaciones.] Aunque estas figuras se han estudiado

1 // Fig. 18.11: TorresDeHanoi.java 2 // Solución de las torres de Hanoi con un método recursivo. 3 public class TorresDeHanoi 4 { 5 // mueve discos de una torre a otra, de manera recursiva 6 public static void resolverTorres( int discos, int agujaOrigen, 7 int agujaDestino, int agujaTemp ) 8 { 9 // caso base -- sólo hay que mover un disco10 if ( discos == 1 )11 {12 System.out.printf( “\n%d --> %d”, agujaOrigen, agujaDestino );13 return;14 } // fin de if1516 // paso recursivo -- mueve (disco - 1) discos de agujaOrigen17 // a agujaTemp usando agujaDestino18 resolverTorres( discos - 1, agujaOrigen, agujaTemp, agujaDestino );1920 // mueve el último disco de agujaOrigen a agujaDestino21 System.out.printf( “\n%d --> %d”, agujaOrigen, agujaDestino );2223 // mueve ( discos - 1 ) discos de agujaTemp a agujaDestino24 resolverTorres( discos - 1, agujaTemp, agujaDestino, agujaOrigen );25 } // fin del método resolverTorres2627 public static void main( String[] args )28 {

29 int agujaInicial = 1; // se usa el valor 1 para indicar agujaInicial en la salida

30 int agujaFinal = 3; // se usa el valor 3 para indicar agujaFinal en la salida31 int agujaTemp = 2; // se usa el valor 2 para indicar agujaTemp en la salida32 int totalDiscos = 3; // número de discos3334 // llamada no recursiva inicial: mueve todos los discos.35 resolverTorres( totalDiscos, agujaInicial, agujaFinal, agujaTemp );36 } // fin de main37 } // fin de la clase TorresDeHanoi

1 --> 31 --> 23 --> 21 --> 32 --> 12 --> 31 --> 3

Fig. 18.11 � Solución de las Torres de Hanoi, con un método recursivo.

780 Capítulo 18 Recursividad

desde antes del siglo 20, fue el matemático polaco Benoit Mandelbrot quien introdujo el término “fractal” en la década de 1970, junto con los detalles específicos acerca de cómo se crea un fractal, y la aplicación práctica de los fractales. La geometría fractal de Mandelbrot proporciona modelos matemáticos para muchas formas complejas que se encuentran en la naturaleza, como las montañas, nubes y litorales. Los fractales tienen muchos usos en las matemáticas y la ciencia. Pueden utilizarse para comprender mejor los sistemas o patrones que aparecen en la naturaleza (por ejemplo, los ecosistemas), en el cuerpo humano (por ejemplo, en los pliegues del cerebro) o en el universo (por ejemplo, los grupos de galaxias). No todos los fractales se asemejan a los objetos en la naturaleza. El dibujo de fractales se ha convertido en una forma de arte popular. Los fractales tienen una propiedad autosimilar: cuando se subdividen en partes, cada una se asemeja a una copia del todo, en un tamaño reducido. Muchos fractales produ-cen una copia exacta del original cuando se amplía una porción de la imagen original; se dice que dicho fractal es estrictamente autosimilar. En nuestro Centro de recursos de recursividad (www.deitel.com/recursion) encontrará sitios Web con demostraciones sobre los fractales.

Como ejemplo, veamos el fractal estrictamente auto-similar, conocido como la Curva de Koch (figura 18.12). Para formar este fractal, se elimina la tercera parte media de cada línea en el dibujo, y se sustituye con dos líneas que forman un punto, de tal forma que si permaneciera la tercera parte media de la línea original, se formaría un triángulo equilátero. A menudo, las fórmulas para crear fractales im-plican eliminar toda, o parte de, la imagen del fractal anterior. Este patrón ya se ha determinado para este fractal; en esta sección nos enfocaremos en cómo utilizar esas fórmulas en una solución recursiva.

Fig. 18.12 � Fractal de la Curva de Koch.

Empezamos con una línea recta [figura 18.12(a)] y aplicamos el patrón, creando un triángulo a partir de la tercera parte media [figura 18.12(b)]. Después aplicamos el patrón de nuevo a cada línea recta, lo cual produce la figura 18.12(c). Cada vez que se aplica el patrón, decimos que el fractal está en un nuevo nivel, o profundidad (algunas veces se utiliza también el término orden). Los frac-tales pueden mostrarse en muchos niveles; por ejemplo, a un fractal de nivel 3 se le han aplicado tres iteraciones del patrón [figura 18.12(d)]. Después de sólo unas cuantas iteraciones, este fractal empieza

(a) Nivel 0

(c) Nivel 2

(e) Nivel 4

(b) Nivel 1

(d) Nivel 3

(f) Nivel 5

18.8 Fractales 781

a verse como una porción de un copo de nieve [figura 18.12(e y f )]. Como éste es un fractal estrictamen-te autosimilar, cada porción del mismo contiene una copia exacta del fractal. Por ejemplo, en la figura 18.12(f ), hemos resaltado una porción del fractal con un cuadro punteado. Si se aumentara el tamaño de la imagen en este cuadro, se vería exactamente igual que el fractal completo de la parte (f ).

Hay un fractal similar, llamado Copo de nieve de Koch, que es similar a la Curva de Koch, pero empieza con un triángulo en vez de una línea. Se aplica el mismo patrón a cada lado del triángulo, lo cual produce una imagen que se asemeja a un copo de nieve encerrado. Hemos optado por enfocarnos en la Curva de Koch por cuestión de simpleza. Para aprender más acerca de la Curva de Koch y del Copo de nieve de Koch, vea los vínculos en nuestro Centro de recursos de recursividad (www.deitel.com/recursion/).

El “Fractal Lo”Ahora demostraremos el uso de la recursividad para dibujar fractales, escribiendo un programa para crear un fractal estrictamente auto-similar. A este fractal lo llamaremos “fractal Lo”, en honor de Sin Han Lo, un colega de Deitel & Associates que lo creó. En un momento dado, el fractal se asemejará a la mitad de una pluma (vea los resultados en la figura 18.19). El caso base, o nivel 0 del fractal, empieza como una línea entre dos puntos, A y B (figura 18.13). Para crear el siguiente nivel superior, buscamos el punto medio (C) de la línea. Para calcular la ubicación del punto C, utilice la siguiente fórmula:

xC = (xA + xB) / 2;yC = (yA + yB) / 2;

[Nota: la x y la y a la izquierda de cada letra se refieren a las coordenadas x y y de ese punto, respectiva-mente. Por ejemplo, xA se refiere a la coordenada x del punto A, mientras que yC se refiere a la coorde-nada y del punto C. En nuestros diagramas denotamos el punto por su letra, seguida de dos números que representan las coordenadas x y y].

Para crear este fractal, también debemos buscar un punto D que se encuentre a la izquierda del seg-mento AC y que cree un triángulo recto isósceles ADC. Para calcular la ubicación del punto D, utilice las siguientes fórmulas:

xD = xA + (xC - xA) / 2 - (yC - yA) / 2;yD = yA + (yC - yA) / 2 + (xC - xA) / 2;

Fig. 18.13 � El “fractal Lo” en el nivel 0.

Origen (0, 0)

A (6, 5) B (30, 5)

Ahora nos movemos del nivel 0 al nivel 1 de la siguiente manera: primero, se suman los puntos C y D (como en la figura 18.14). Después se elimina la línea original y se agregan los segmentos DA, DC y DB.

782 Capítulo 18 Recursividad

El resto de las líneas se curvearán en un ángulo, haciendo que nuestro fractal se vea como una pluma. Para el siguiente nivel del fractal, este algoritmo se repite en cada una de las tres líneas en el nivel 1. Para cada línea, se aplican las fórmulas anteriores, en donde el punto anterior D se considera ahora como el punto A, mientras que el otro extremo de cada línea se considera como el punto B. La figura 18.15 contiene la línea del nivel 0 (ahora una línea punteada) y las tres líneas que se agregaron del nivel 1. Hemos cambiado el punto D para que sea el punto A, y los puntos originales A, C y B son B1, B2 y B3, respectivamente. Las fórmulas anteriores se utilizaron para buscar los nuevos puntos C y D en cada línea. Estos puntos también se enumeran del 1 al 3 para llevar la cuenta de cuál punto está asociado con cada línea. Por ejemplo, los puntos C1 y D1 representan a los puntos C y D asociados con la línea que se forma de los puntos A a B1. Para llegar al nivel 2, se eliminan las tres líneas de la figura 18.15 y se sus-tituyen con nuevas líneas de los puntos C y D que se acaban de agregar. La figura 18.16 muestra las nuevas líneas (las líneas del nivel 2 se muestran como líneas punteadas, para conveniencia del lector). La figura 18.17 muestra el nivel 2 sin las líneas punteadas del nivel 1. Una vez que se ha repetido este proceso varias veces, el fractal creado empezará a parecerse a la mitad de una pluma, como se muestra en los resultados de la figura 18.19. En breve presentaremos el código para esta aplicación.

Fig. 18.14 � Determinación de los puntos C y D para el nivel 1 del “fractal Lo”.

Origen (0, 0)

A (6, 5)

D (12, 11)

C (18, 5) B (30, 5)

Fig. 18.15 � El “fractal Lo” en el nivel 1, y se determinan los puntos C y D para el nivel 2. [Nota: se incluye el fractal en el nivel 0 como una línea punteada, para recordar en dónde se encontraba la línea en relación con el fractal actual].

D3 (18, 14)

D2 (15, 11)

C2 (15, 8)

Origen (0, 0)

C3 (21, 8)C1 (9, 8)

A (12, 11)

D1 (12, 8)

B2 (18, 5) B3 (30, 5)B1 (6, 5)

18.8 Fractales 783

La aplicación de la figura 18.18 define la interfaz de usuario para dibujar este fractal (que se muestra al final de la figura 18.19). La interfaz consiste de tres botones: uno para que el usuario modifique el co-lor del fractal, otro para incrementar el nivel de recursividad y uno para reducir el nivel de recursividad. Un objeto JLabel lleva la cuenta del nivel actual de recursividad, que se modifica mediante una llamada al método establecerNivel, que veremos en breve. En las líneas 15 y 16 se especifican las constantes ANCHURA y ALTURA como 400 y 480 respectivamente, para indicar el tamaño del objeto JFrame. El usuario activa un evento ActionEvent haciendo clic en el botón Color. El manejador de eventos para este bo-tón se registra en las líneas 37 a 53. El método actionPerformed muestra un cuadro de diálogo JColor-Chooser. Este cuadro de diálogo devuelve el objeto Color seleccionado, o azul (si el usuario oprime Cancelar o cierra el cuadro de diálogo sin oprimir Aceptar). En la línea 50 se hace una llamada al método establecerColor en la clase FractalJPanel para actualizar el color.

Fig. 18.16 � El “fractal Lo” en el nivel 2, y se proporcionan las líneas punteadas del nivel 1.

Origen (0, 0)

Fig. 18.17 � El “fractal Lo” en el nivel 2.

Origen (0, 0)

Fig. 18.18 � Interfaz de usuario para dibujar un fractal (parte 1 de 4).

1 // Fig. 18.18: Fractal.java 2 // Interfaz de usuario para dibujar un fractal. 3 import java.awt.Color; 4 import java.awt.FlowLayout; 5 import java.awt.event.ActionEvent;

784 Capítulo 18 Recursividad

Fig. 18.18 � Interfaz de usuario para dibujar un fractal (parte 2 de 4).

6 import java.awt.event.ActionListener;

7 import javax.swing.JFrame;

8 import javax.swing.JButton;

9 import javax.swing.JLabel;

10 import javax.swing.JPanel;

11 import javax.swing.JColorChooser;

1213 public class Fractal extends JFrame

14 {

15 private static final int ANCHURA = 400; // define la anchura de la GUI

16 private static final int ALTURA = 480; // define la altura de la GUI

17 private static final int NIVEL_MIN = 0, NIVEL_MAX = 15;

1819 private JButton cambiarColorJButton, aumentarNivelJButton,

20 reducirNivelJButton;

21 private JLabel nivelJLabel;

22 private FractalJPanel espacioDibujo;

23 private JPanel principalJPanel, controlJPanel;

2425 // establece la GUI

26 public Fractal()

27 {

28 super( “Fractal” );

2930 // establece el panel de control

31 controlJPanel = new JPanel();

32 controlJPanel.setLayout( new FlowLayout() );

3334 // establece el botón de color y registra el componente de escucha

35 cambiarColorJButton = new JButton( “Color” );

36 controlJPanel.add( cambiarColorJButton );

37 cambiarColorJButton.addActionListener(

38 new ActionListener() // clase interna anónima

39 {

40 // procesa el evento de cambiarColorJButton

41 public void actionPerformed( ActionEvent evento )

42 {

43 Color color = JColorChooser.showDialog(

44 Fractal.this, “Elija un color”, Color.BLUE );

4546 // establece el color predeterminado, si no se devuelve un color

47 if ( color == null )

48 color = Color.BLUE;

4950 espacioDibujo.establecerColor( color );

51 } // fin del método actionPerformed

52 } // fin de la clase interna anónima

53 ); // fin de addActionListener

5455 // establece botón para reducir nivel, para agregarlo al panel de control y

56 // registra el componente de escucha57 reducirNivelJButton = new JButton( “Reducir nivel” );

18.8 Fractales 785

Fig. 18.18 � Interfaz de usuario para dibujar un fractal (parte 3 de 4).

58 controlJPanel.add( reducirNivelJButton );59 reducirNivelJButton.addActionListener(60 new ActionListener() // clase interna anónima61 {62 // procesa el evento de reducirNivelJButton63 public void actionPerformed( ActionEvent evento )64 {65 int nivel = espacioDibujo.obtenerNivel();66 --nivel; // reduce el nivel en uno6768 // modifica el nivel si es posible69 if ( ( nivel >= NIVEL_MIN ) && 70 ( nivel <= NIVEL_MAX ) )71 {72 nivelJLabel.setText( “Nivel: ” + nivel );73 espacioDibujo.establecerNivel( nivel );74 repaint(); 75 } // fin de if76 } // fin del método actionPerformed77 } // fin de la clase interna anónima78 ); // fin de addActionListener7980 // establece el botón para aumentar nivel, para agregarlo al panel de control81 // y registra el componente de escucha82 aumentarNivelJButton = new JButton( “Aumentar nivel” );83 controlJPanel.add( aumentarNivelJButton );84 aumentarNivelJButton.addActionListener(85 new ActionListener() // clase interna anónima86 {87 // procesa el evento de aumentarNivelJButton88 public void actionPerformed( ActionEvent evento )89 {90 int nivel = espacioDibujo.obtenerNivel();91 ++nivel; // aumenta el nivel en uno9293 // modifica el nivel si es posible94 if ( ( nivel >= NIVEL_MIN ) && 95 ( nivel <= NIVEL_MAX ) )96 {97 nivelJLabel.setText( “Nivel: ” + nivel );98 espacioDibujo.establecerNivel( nivel );99 repaint();100 } // fin de if101 } // fin del método actionPerformed102 } // fin de la clase interna anónima103 ); // fin de addActionListener104105 // establece nivelJLabel para agregarlo a controlJPanel106 nivelJLabel = new JLabel( “Nivel: 0” );107 controlJPanel.add( nivelJLabel );108109 espacioDibujo = new FractalJPanel( 0 );110

786 Capítulo 18 Recursividad

El manejador de eventos para el botón Reducir nivel se registra en las líneas 59 a 78. En el método actionPerformed obtiene el nivel actual de recursividad y lo reduce en 1 (líneas 65 y 66). En las líneas 69 y 70 se realiza una verificación para asegurar que el nivel sea mayor o igual que NIVEL_MIN y menor o igual que NIVEL_MAX; el fractal no está definido para niveles de recursividad menores que NIVEL_MIN, y no es posible ver el detalle adicional por encima de NIVEL_MAX. El programa permite al usuario avanzar hacia cualquier nivel deseado, pero cerca del nivel 10 y de niveles superiores el despliegue del fractal se vuelve cada vez más lento, ya que hay muchos detalles que dibujar. En las líneas 72 a 74 se restablece la etiqueta del nivel para reflejar el cambio; se establece el nuevo nivel y se hace una llamada al método repaint para actualizar la imagen y mostrar el fractal correspondiente al nuevo nivel.

El objeto JButton Aumentar nivel funciona de la misma forma que el objeto JButton Reducir nivel, excepto que el nivel se incrementa en vez de reducirse para mostrar más detalles del fractal (líneas 90 y 91). Cuando se ejecuta la aplicación por primera vez, el nivel se establece en 0, en donde se muestra una línea azul entre dos puntos especificados en la clase FractalJPanel.

La clase FractalJPanel (figura 18.19) especifica las medidas del objeto JPanel del dibujo como 400 por 400 (líneas 13 y 14). El constructor de FractalJPanel (líneas 18 a 24) recibe el nivel actual como parámetro y lo asigna a su variable de instancia nivel. La variable de instancia color se establece en el color azul predeterminado. En las líneas 22 y 23 se cambia el color de fondo del objeto JPanel a blanco (para que el fractal se pueda ver con facilidad), y se establecen las medidas del objeto Fractal-JPanel de dibujo.

Fig. 18.18 � Interfaz de usuario para dibujar un fractal (parte 4 de 4).

111 // crea principalJPanel para que contenga a controlJPanel y espacioDibujo112 principalJPanel = new JPanel();113 principalJPanel.add( controlJPanel );114 principalJPanel.add( espacioDibujo );115116 add( principalJPanel ); // agrega JPanel a JFrame117118 setSize( ANCHURA, ALTURA ); // establece el tamaño de JFrame119 setVisible( true ); // muestra JFrame120 } // fin del constructor de Fractal121122 public static void main( String[] args )123 {124 Fractal demo = new Fractal();125 demo.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );126 } // fin de main127 } // fin de la clase Fractal

Fig. 18.19 � Dibujo del “fractal Lo” mediante recursividad (parte 1 de 4).

1 // Fig. 18.19: FractalJPanel.java

2 // Cómo dibujar el “fractal Lo” mediante recursividad.

3 import java.awt.Graphics;

4 import java.awt.Color;

5 import java.awt.Dimension;

6 import javax.swing.JPanel;

7

8 public class FractalJPanel extends JPanel

9 {

10 private Color color; // almacena el color utilizado para dibujar el fractal

11 private int nivel; // almacena el nivel actual del fractal

18.8 Fractales 787

Fig. 18.19 � Dibujo del “fractal Lo” mediante recursividad (parte 2 de 4).

1213 private final int ANCHURA = 400; // define la anchura de JPanel14 private final int ALTURA = 400; // define la altura de JPanel1516 // establece el nivel inicial del fractal al valor especificado17 // y establece las especificaciones del JPanel18 public FractalJPanel( int nivelActual )19 {20 color = Color.BLUE; // inicializa el color de dibujo en azul21 nivel = nivelActual; // establece el nivel inicial del fractal22 setBackground( Color.WHITE );23 setPreferredSize( new Dimension( ANCHURA, ALTURA ) );24 } // fin del constructor de FractalJPanel2526 // dibuja el fractal en forma recursiva27 public void dibujarFractal( int nivel, int xA, int yA, int xB, 28 int yB, Graphics g )29 {30 // caso base: dibuja una línea que conecta dos puntos dados31 if ( nivel == 0 ) 32 g.drawLine( xA, yA, xB, yB );33 else // paso recursivo: determina los nuevos puntos, dibuja el siguiente nivel34 {35 // calcula punto medio entre (xA, yA) y (xB, yB)36 int xC = ( xA + xB ) / 2;37 int yC = ( yA + yB ) / 2;3839 // calcula el cuarto punto (xD, yD) que forma un 40 // triángulo recto isósceles entre (xA, yA) y (xC, yC) 41 // en donde el ángulo recto está en (xD, yD)42 int xD = xA + ( xC - xA ) / 2 - ( yC - yA ) / 2;43 int yD = yA + ( yC - yA ) / 2 + ( xC - xA ) / 2;4445 // dibuja el Fractal en forma recursiva46 dibujarFractal( nivel - 1, xD, yD, xA, yA, g );47 dibujarFractal( nivel - 1, xD, yD, xC, yC, g );48 dibujarFractal( nivel - 1, xD, yD, xB, yB, g ); 49 } // fin de else50 } // fin del método dibujarFractal5152 // inicia el dibujo del fractal53 public void paintComponent( Graphics g )54 {55 super.paintComponent( g );5657 // dibuja el patrón del fractal58 g.setColor( color );59 dibujarFractal( nivel, 100, 90, 290, 200, g ); 60 } // fin del método paintComponent6162 // establece el color de dibujo a c63 public void establecerColor( Color c )64 {

788 Capítulo 18 Recursividad

Fig. 18.19 � Dibujo del “fractal Lo” mediante recursividad (parte 3 de 4).

65 color = c;

66 } // fin del método establecerColor

67

68 // establece el nuevo nivel de recursividad

69 public void establecerNivel( int nivelActual )

70 {

71 nivel = nivelActual;

72 } // fin del método establecerNivel

73

74 // devuelve el nivel de recursividad

75 public int obtenerNivel()

76 {

77 return nivel;

78 } // fin del método obtenerNivel

79 }; // fin de la clase FractalJPanel

18.8 Fractales 789

En las líneas 27 a 50 se define el método recursivo que crea el fractal. Este método recibe seis parámetros: el nivel, cuatro enteros que especifican las coordenadas x y y de dos puntos, y el objeto g de Graphics. El caso base para este método (línea 31) ocurre cuando nivel es igual a 0, en cuyo momen-to se dibujará una línea entre los dos puntos que se proporcionan como parámetros. En las líneas 36 a 43 se calcula (xC, yC), el punto medio entre (xA, yA) y (xB, yB), y (xD, yD), el punto que crea un triángulo isósceles recto con (xA, yA) y (xC, yC). En las líneas 46 a 48 se realizan tres llamadas recursivas en tres conjuntos distintos de puntos.

En el método paintComponent, en la línea 59 se realiza la primera llamada al método dibujar-Fractal para empezar el dibujo. Esta llamada al método no es recursiva, pero todas las llamadas subsi-guientes a dibujarFractal que se realicen desde el cuerpo de dibujarFractal sí lo son. Como las líneas no se dibujarán sino hasta que se llegue al caso base, la distancia entre dos puntos se reduce en cada llamada recursiva. A medida que aumenta el nivel de recursividad, el fractal se vuelve más uniforme y detallado. La figura de este fractal se estabiliza a medida que el nivel se acerca a 11. Los fractales se esta-bilizarán en distintos niveles, con base en la figura y el tamaño del fractal.

Fig. 18.19 � Dibujo del “fractal Lo” mediante recursividad (parte 4 de 4).

790 Capítulo 18 Recursividad

En la figura 18.19 se muestra el desarrollo del fractal, de los niveles 0 al 6. La última imagen mues-tra la figura que define el fractal en el nivel 11. Si nos enfocamos en uno de los brazos de este fractal, será idéntico a la imagen completa. Esta propiedad define al fractal como estrictamente autosimilar. En nuestro Centro de recursos de recursividad en www.deitel.com/recursion/ podrá consultar más recursos sobre los fractales.

18.9 “Vuelta atrás” recursiva (backtracking)Todos nuestros métodos recursivos tienen una arquitectura similar; si se llega al caso base, devuelven un resultado; si no, hacen una o más llamadas recursivas. En esta sección exploraremos una técnica recursiva más completa, que busca una ruta a través de un laberinto, y devuelve verdadero si hay una posible solución al laberinto. La solución implica recorrer el laberinto un paso a la vez, en donde los movimientos pueden ser hacia abajo, a la derecha, hacia arriba o a la izquierda (no se permiten movi-mientos diagonales). De la posición actual en el laberinto (empezando con el punto de entrada), se realizan los siguientes pasos: para cada posible dirección, se realiza el movimiento en esa dirección y se hace una llamada recursiva para resolver el resto del laberinto desde la nueva ubicación. Cuando se llega a un punto sin salida (es decir, no podemos avanzar más pasos sin pegar en la pared), retroce-demos a la ubicación anterior y tratamos de avanzar en otra dirección. Si no puede elegirse otra direc-ción, retrocedemos de nuevo. Este proceso continúa hasta que encontramos un punto en el laberinto en donde puede realizarse un movimiento en otra dirección. Una vez que se encuentra dicha ubica-ción, avanzamos en la nueva dirección y continuamos con otra llamada recursiva para resolver el resto del laberinto.

Para retroceder a la ubicación anterior en el laberinto, nuestro método recursivo simplemente de-vuelve falso, avanzando hacia arriba en la cadena de llamadas a métodos, hasta la llamada recursiva ante-rior (que hace referencia a la ubicación anterior en el laberinto). A este proceso de utilizar la recursividad para regresar a un punto de decisión anterior se le conoce como “vuelta atrás” recursiva. Si un conjunto de llamadas recursivas no resulta en una solución para el problema, el programa retrocede hasta el pun-to de decisión anterior y toma una decisión distinta, lo que a menudo produce otro conjunto de llamadas recursivas. En este ejemplo, el punto de decisión anterior es la ubicación anterior en el laberinto, y la decisión a realizar es la dirección que debe tomar el siguiente movimiento. Una dirección ha conducido a un punto sin salida, por lo que la búsqueda continúa con una dirección diferente. La solución de “vuel-ta atrás” recursiva para el problema del laberinto utiliza la recursividad para regresar sólo una parte a través de la cadena de llamadas a métodos, y después probar una dirección diferente. Si la vuelta atrás llega a la ubicación de entrada del laberinto y se han recorrido todas las direcciones, entonces el laberinto no tiene solución.

En los ejercicios del capítulo le pediremos que implemente soluciones de “vuelta atrás” recursivas para el problema del laberinto (ejercicios 18.20 a 18.22) y para el problema de las Ocho Reinas (ejercicio 18.15), el cual trata de encontrar la manera de colocar ocho reinas en un tablero de ajedrez vacío, de forma que ninguna reina esté “atacando” a otra (es decir, que no haya dos reinas en la misma fila, en la misma columna o a lo largo de la misma diagonal).

18.10 ConclusiónEn este capítulo aprendió a crear métodos recursivos; es decir, métodos que se llaman a sí mismos. Aprendió que los métodos recursivos por lo general dividen a un problema en dos piezas conceptuales: una pieza que el método sabe cómo resolver (el caso base) y una pieza que el método no sabe cómo resolver (el paso recursivo). El paso recursivo es una versión un poco más pequeña del problema ori-ginal, y se lleva a cabo mediante una llamada a un método recursivo. Usted ya vio algunos ejemplos populares de recursividad, incluyendo el cálculo de factoriales y la producción de valores en la serie

Resumen 791

de Fibonacci. Después aprendió cómo funciona la recursividad “detrás de las cámaras”, incluyendo el orden en el que se meten o se sacan las llamadas a métodos recursivos de la pila de ejecución del programa. Después comparó los métodos recursivo e iterativo (no recursivo). Aprendió a resolver problemas más complejos mediante la recursividad: las torres de Hanoi y mostrar fractales. El capí-tulo concluyó con una introducción a la “vuelta atrás” recursiva, una técnica para resolver problemas que implica retroceder a través de llamadas recursivas para probar distintas soluciones posibles. En el siguiente capítulo, aprenderá diversas técnicas para ordenar listas de datos y buscar un elemento en una lista de datos, y explorará las circunstancias bajo las que debe utilizarse cada técnica de bús-queda y ordenamiento.

ResumenSección 18.1 Introducción• Un método recursivo (pág. 766) se llama a sí mismo en forma directa o indirecta a través de otro método.

Sección 18.2 Conceptos de recursividad• Cuando se llama a un método recursivo (pág. 767) para resolver un problema, es capaz de resolver sólo el (los) caso(s)

más simple(s), o caso(s) base. Si se llama con un caso base (pág. 767), el método devuelve un resultado.

• Si se llama a un método recursivo con un problema más complejo que el caso base, por lo general divide el problema en dos piezas conceptuales: una pieza que el método sabe cómo resolver y otra pieza que no sabe cómo resolver.

• Para que la recursividad sea factible, la pieza que el método no sabe cómo resolver debe asemejarse al problema original, pero debe ser una versión ligeramente más simple o pequeña del mismo. Como este nuevo problema se parece al problema original, el método llama a una nueva copia de sí mismo para trabajar en el problema más pequeño; a esto se le conoce como paso recursivo (pág. 767).

• Para que la recursividad termine en un momento dado, cada vez que un método se llame a sí mismo con una versión más simple del problema original, la secuencia de problemas cada vez más pequeños debe converger en un caso base. Cuando el método reconoce el caso base, devuelve un resultado a la copia anterior del método.

• Una llamada recursiva puede ser una llamada a otro método, que a su vez realiza una llamada de vuelta al método original. Dicho proceso sigue provocando una llamada recursiva al método original. A esto se le conoce como llamada recursiva indirecta, o recursividad indirecta (pág. 767).

Sección 18.3 Ejemplo de uso de recursividad: factoriales• La acción de omitir el caso base, o escribir el paso recursivo de manera incorrecta para que no converja en el caso base,

puede ocasionar una recursividad infi nita (pág. 769), con lo cual se agota la memoria en cierto punto. Este error es análogo al problema de un ciclo infi nito en una solución iterativa (no recursiva).

Sección 18.4 Ejemplo de uso de recursividad: serie de Fibonacci• La serie de Fibonacci (pág. 771) empieza con 0 y 1, y tiene la propiedad de que cada número subsiguiente de Fibonacci

es la suma de los dos anteriores.

• La proporción de números de Fibonacci sucesivos converge en un valor constante de 1.618…, un número al que se le denomina la proporción dorada, o media dorada (pág. 771).

• Algunas soluciones recursivas, como la de Fibonacci, producen una “explosión” de llamadas a métodos.

Sección 18.5 La recursividad y la pila de llamadas a métodos• El método en ejecución es siempre el que tiene su registro de activación en la parte superior de la pila, y el registro de

activación para ese método contiene los valores de sus variables locales.

Sección 18.6 Comparación entre recursividad e iteración• Tanto la iteración como la recursividad se basan en una instrucción de control: la iteración utiliza una instrucción de

repetición, la recursividad una instrucción de selección.

• Tanto la iteración como la recursividad implican la repetición: la iteración utiliza de manera explícita una instrucción de repetición, mientras que la recursividad logra la repetición a través de llamadas repetidas a un método.

• La iteración y la recursividad implican una prueba de terminación: la iteración termina cuando falla la condición de continuación de ciclo, la recursividad cuando se reconoce un caso base.

• La iteración con repetición controlada por contador y la recursividad se acercan en forma gradual a la terminación: la iteración sigue modifi cando un contador, hasta que éste asume un valor que hace que falle la condición de continua-ción de ciclo, mientras que la recursividad sigue produciendo versiones cada vez más simples del problema original, hasta llegar al caso base.

• Tanto la iteración como la recursividad pueden ocurrir en forma infi nita. Un ciclo infi nito ocurre con la iteración si la prueba de continuación de ciclo nunca se vuelve falsa, mientras que la recursividad infi nita ocurre si el paso recursivo no reduce el problema cada vez más, de una forma que converja en el caso base.

• La recursividad invoca el mecanismo en forma repetida, y en consecuencia a la sobrecarga producida por las llamadas al método.

• Cualquier problema que pueda resolverse en forma recursiva, se puede resolver también en forma iterativa.

• Por lo general se prefi ere un método recursivo en vez de uno iterativo cuando el primero refl eja el problema con más naturalidad, y produce un programa más fácil de comprender y de depurar.

• A menudo se puede implementar un método recursivo con pocas líneas de código, pero el método iterativo correspon-diente podría requerir una gran cantidad de código. Otra razón por la que es más conveniente elegir una solución recursiva es que una solución iterativa podría no ser aparente.

Sección 18.8 Fractales• Un fractal (pág. 779) es una fi gura geométrica que se genera a partir de un patrón que se repite en forma recursiva, un

número infi nito de veces.

• Los fractales tienen una propiedad de autosimilitud (pág. 780): las subpartes son copias de tamaño reducido de toda la pieza.

Sección 18.9 “Vuelta atrás” recursiva (backtracking)• En la “vuelta atrás” recursiva (pág. 790), si un conjunto de llamadas recursivas no produce como resultado una solu-

ción al problema, el programa retrocede hasta el punto de decisión anterior y toma una decisión distinta, lo cual a menudo produce otro conjunto de llamadas recursivas.

Ejercicios de autoevaluación18.1 Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué.

a) Un método que se llama a sí mismo en forma indirecta no es un ejemplo de recursividad.b) La recursividad puede ser eficiente en la computación, debido a la reducción en el uso del espacio en memoria.c) Cuando se llama a un método recursivo para resolver un problema, en realidad es capaz de resolver sólo el (los)

caso(s) más simple(s), o caso(s) base.d) Para que la recursividad sea factible, el paso recursivo en una solución recursiva debe asemejarse al problema

original, pero debe ser una versión ligeramente más grande del mismo.

18.2 Para terminar la recursividad, se requiere un(a) .a) paso recursivob) instrucción breakc) tipo de valor de retorno voidd) caso base

18.3 La primera llamada para invocar a un método recursivo es .a) no recursiva

792 Capítulo 18 Recursividad

b) recursivac) el paso recursivod) ninguna de las anteriores

18.4 Cada vez que se aplica el patrón de un fractal, se dice que el fractal está en un(a) nuevo(a) .a) anchurab) alturac) niveld) volumen

18.5 La iteración y la recursividad implican un(a) .a) instrucción de repeticiónb) prueba de terminaciónc) variable contadord) ninguna de las anteriores

18.6 Complete los siguientes enunciados:a) La proporción de números de Fibonacci sucesivos converge en un valor constante de 1.618…, un número

al que se le conoce como o .b) Por lo general, la iteración utiliza una instrucción de repetición, mientras que la recursividad comúnmente

utiliza una instrucción .c) Los fractales tienen una propiedad llamada ; cuando se subdividen en partes, cada una de ellas es

una copia de tamaño reducido de la pieza completa.

Respuestas a los ejercicios de autoevaluación18.1 a) Falso. Un método que se llama a sí mismo de esta forma es un ejemplo de recursividad indirecta. b) Falso. La recursividad puede ser ineficiente en la computación debido a las múltiples llamadas a un método y el uso del espacio de memoria. c) Verdadero. d) Falso. Para que la recursividad sea factible, el paso recursivo en una solución recursiva debe asemejarse al problema original, pero debe ser una versión un poco más pequeña del mismo.

18.2 d

18.3 a

18.4 c

18.5 b

18.6 a) proporción dorada, media dorada. b) selección. c) autosimilitud.

Ejercicios18.7 ¿Qué hace el siguiente código?

Ejercicios 793

1 public int misterio( int a, int b )

2 {

3 if ( b == 1 )

4 return a;

5 else

6 return a + misterio( a, b – 1 );

7 } // fin del método misterio

18.8 Busque el(los) error(es) en el siguiente método recursivo, y explique cómo corregirlo(s). Este método debe encon-trar la suma de los valores de 0 a n.

18.9 (Método potencia recursivo) Escriba un método recursivo llamado potencia( base, exponente ) que, cuando sea llamado, devuelva

base exponente

Por ejemplo, potencia( 3,4 ) = 3*3*3*3. Suponga que exponente es un entero mayor o igual que 1. [Sugerencia: el paso re-cursivo debe utilizar la relación

base exponente = base · base exponente – 1

y la condición de terminación ocurre cuando exponente es igual a 1, ya quebase1 = base

Incorpore este método en un programa que permita al usuario introducir la base y el exponente.]

18.10 (Visualización de la recursividad) Es interesante observar la recursividad “en acción”. Modifique el método factorial de la figura 18.3 para imprimir su variable local y su parámetro de llamada recursiva. Para cada llamada recursiva, muestre los resultados en una línea separada y agregue un nivel de sangría. Haga su máximo esfuerzo por hacer que los re-sultados sean claros, interesantes y significativos. Su meta aquí es diseñar e implementar un formato de salida que facilite la comprensión de la recursividad. Tal vez desee agregar ciertas capacidades de visualización a otros ejemplos y ejercicios recursivos a lo largo de este libro.

18.11 (Máximo común divisor) El máximo común divisor de los enteros x y y es el entero más grande que se puede dividir entre x y y de manera uniforme. Escriba un método recursivo llamado mcd, que devuelva el máximo común divisor de x y y. El mcd de x y y se define, mediante la recursividad, de la siguiente manera: si y es igual a 0, entonces mcd( x, y ) es x; en caso contrario, mcd( x, y ) es mcd( y, x % y ), en donde % es el operador residuo. Use este método para sustituir el que escribió en la aplicación del ejercicio 6.27.

18.12 ¿Qué hace el siguiente programa?

794 Capítulo 18 Recursividad

1 public int suma( int n )

2 {

3 if ( n == 0 )

4 return 0;

5 else

6 return n + suma( n );

7 } // fin del método suma

1 // Ejercicio 18.12 Solución: ClaseMisteriosa.java

2 public class ClaseMisteriosa

3 {

4 public static int misterio( int[] arreglo2, int tamanio )

5 {

6 if ( tamanio == 1 )

7 return arreglo2[ 0 ];

8 else

9 return arreglo2[ tamanio – 1 ] + misterio( arreglo2, tamanio – 1 );

10 } // fin del método misterio

11

12 public static void main( String[] args )

13 {

14 int arreglo[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

15

16 int resultado = misterio( arreglo, arreglo.length );

17 System.out.printf( “El resultado es: %d\n”, resultado );

18 } // fin del método main

19 } // fin de la clase ClaseMisteriosa

18.13 ¿Qué hace el siguiente programa?

Ejercicios 795

1 // Ejercicio 18.13 Solución: UnaClase.java

2 public class UnaClase

3 {

4 public static String unMetodo( int[] arreglo2, int x )

5 {

6 if ( x < arreglo2.length )

7 return String.format(

8 “%s%d ”, unMetodo( arreglo2, x + 1 ), arreglo2[ x ] );

9 else

10 return “”;

11 } // fin del método unMetodo

12

13 public static void main( String[] args )

14 {

15 int[] arreglo = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

16 String resultados = unMetodo( arreglo, 0 );

17 System.out.println( resultados );

18 } // fin de main

19 } // fin de la clase UnaClase

Fig. 18.20 � Eliminación de posiciones al colocar una reina en la esquina superior izquierda de un tablero de ajedrez.

* *****

* *

* *

* *

* *

* *

*

*

*

*

*

*

18.14 (Palíndromos) Un palíndromo es una cadena que se escribe de la misma forma tanto al derecho como al revés. Algunos ejemplos de palíndromos son “radar”, “reconocer” y (si se ignoran los espacios) “anita lava la tina”. Escriba un método recursivo llamado probarPalindromo, que devuelva el valor boolean true si la cadena almacenada en el arreglo es un palíndromo, y false en caso contrario. El método debe ignorar espacios y puntuación en la cadena.

18.15 (Ocho reinas) Un buen acertijo para los fanáticos del ajedrez es el problema de las Ocho reinas, que se describe a continuación: ¿es posible colocar ocho reinas en un tablero de ajedrez vacío, de forma que ninguna reina “ataque” a otra (es decir, que no haya dos reinas en la misma fila, en la misma columna o a lo largo de la misma diagonal)? Por ejemplo, si se coloca una reina en la esquina superior izquierda del tablero, no pueden colocarse otras reinas en ninguna de las posiciones marcadas que se muestran en la figura 18.20. Resuelva el problema mediante el uso de recursividad. [Sugerencia: su solución debe empezar con la primera columna y buscar una ubicación en esa columna, en donde pueda colocarse una reina; al principio, coloque la reina en la primera fila. Después, la solución debe buscar en forma recursiva el resto de las columnas. En las primeras columnas, habrá varias ubicaciones en donde pueda colocarse una reina. Tome la primera posición disponible. Si se llega a una columna sin que haya una posible ubicación para una reina, el programa deberá regresar a la columna anterior y desplazar la reina que está en esa columna hacia una nueva fila. Este proceso continuo de retroceder y probar nuevas alternativas es un ejemplo de la “vuelta atrás” recursiva].

18.16 (Imprimir un arreglo) Escriba un método recursivo llamado imprimirArreglo, que muestre todos los elementos en un arreglo de enteros, separados por espacios.

18.17 (Imprimir un arreglo al revés) Escriba un método recursivo llamado cadenaInversa, que reciba un arreglo de caracteres que contenga una cadena como argumento, y que la imprima al revés. [Sugerencia: use el método String llamado toCharArray, el cual no recibe argumentos, para obtener un arreglo char que contenga los caracteres en el ob-jeto String.]

18.18 (Buscar el valor mínimo en un arreglo) Escriba un método recursivo llamado minimoRecursivo, que determine el elemento más pequeño en un arreglo de enteros. Este método deberá regresar cuando reciba un arreglo de un elemento.

18.19 (Fractales) Repita el patrón del fractal de la sección 18.8 para formar una estrella. Empiece con cinco líneas (figura 18.21) en vez de una, en donde cada línea es un pico distinto de la estrella. Aplique el patrón del “fractal Lo” a cada pico de la estrella.

796 Capítulo 18 Recursividad

Fig. 18.22 � Representación de un laberinto mediante un arreglo bidimensional.

18.20 (Recorrido de un laberinto mediante el uso de la “vuelta atrás” recursiva) La cuadrícula de caracteres # y puntos (.) en la figura 18.22 es una representación de un laberinto mediante un arreglo bidimensional. Los caracteres # representan las paredes del laberinto, y los puntos representan las ubicaciones en las posibles rutas a través del laberinto. Sólo pueden realizarse movimientos hacia una ubicación en el arreglo que contenga un punto.

Fig. 18.21 � Resultados de ejemplo para el ejercicio 18.19.

Escriba un método recursivo (recorridoLaberinto) para avanzar a través de laberintos como el de la fi gura 18.22. El método debe recibir como argumentos un arreglo de caracteres de 12 por 12 que representa el laberinto, y la posi-ción actual en el laberinto (la primera vez que se llama a este método, la posición actual debe ser el punto de entrada del laberinto). A medida que recorridoLaberinto trate de localizar la salida, debe colocar el carácter x en cada posición en la ruta. Hay un algoritmo simple para avanzar a través de un laberinto, que garantiza encontrar la salida (suponiendo que haya una). Si no hay salida, llegaremos a la posición inicial de nuevo. El algoritmo es el siguiente: partiendo de la posición actual en el laberinto, trate de avanzar un espacio en cualquiera de las posibles direcciones (abajo, derecha, arriba o izquier-da). Si es posible avanzar por lo menos en una dirección, llame a recorridoLaberinto en forma recursiva, pasándole la nueva posición en el laberinto como la posición actual. Si no es posible avanzar en ninguna dirección, “retroceda” a una posición anterior en el laberinto y pruebe una nueva dirección para esa posición (éste es un ejemplo de vuelta atrás recur-siva). Programe el método para que muestre el laberinto después de cada movimiento, de manera que el usuario pueda observar a la hora de que se resuelva el laberinto. La salida fi nal del laberinto deberá mostrar sólo la ruta necesaria para resolverlo; si al ir en una dirección específi ca se llega a un punto sin salida, no se deben mostrar las x que avancen en esa dirección. [Sugerencia: para mostrar sólo la ruta fi nal, tal vez sea útil marcar las posiciones que resulten en un punto sin salida con otro carácter (como ‘0’)].

18.21 (Generación de laberintos al azar) Escriba un método llamado generadorLaberintos, que reciba como argu-mento un arreglo bidimensional de 12 por 12 caracteres, y que produzca un laberinto al azar. Este método también de-berá proporcionar las posiciones inicial y final del laberinto. Pruebe su método recorridoLaberinto del ejercicio 18.20, usando varios laberintos generados al azar.

18.22 (Laberintos de cualquier tamaño) Generalice los métodos recorridoLaberinto y generadorLaberintos de los ejercicios 18.20 y 18.21 para procesar laberintos de cualquier anchura y altura.

18.23 (Tiempo para calcular números de Fibonacci) Mejore el programa de Fibonacci de la figura 18.5, de manera que calcule el monto de tiempo aproximado requerido para realizar el cálculo, y el número de llamadas realizadas al método recursivo. Para este fin, llame al método static de System llamado currentTimeMillis, el cual no recibe argu-mento y devuelve el tiempo actual de la computadora en milisegundos. Llame a este método dos veces; una antes y la otra después de la llamada a fibonacci. Guarde cada valor y calcule la diferencia en los tiempos, para determinar cuántos milisegundos se requirieron para realizar el cálculo. Después, agregue una variable a la clase CalculadoraFibonacci, y utilice esta variable para determinar el número de llamadas realizadas al método fibonacci. Muestre sus resultados.

Ejercicios 797

Búsqueda, ordenamiento y Big O19

Con sollozos y lágrimas él sorteó los de mayor tamaño…—Lewis Carroll

Intenta el final, y nunca dejes lugar a dudas; no hay nada tan difícil que no pueda averiguarse mediante la búsqueda.—Robert Eric

Está bloqueado en mi memoria, y tú deberás guardar la llave.—William Shakespeare

Una ley inmutable en los negocios es que las palabras son palabras, las explicaciones son explicaciones, las promesas son promesas; pero sólo el desempeño es la realidad.—Harold S. Green

O b j e t i v o sEn este capítulo aprenderá a:

■ Buscar un valor dado en un arreglo, usando la búsqueda lineal y la búsqueda binaria.

■ Ordenar arreglos, usando los algoritmos iterativos de ordenamiento por selección y por inserción.

■ Ordenar arreglos, usando el algoritmo recursivo de ordenamiento por combinación.

■ Determinar la eficiencia de los algoritmos de búsqueda y ordenamiento.

19.1 Introducción 799

19.1 Introducción

19.2 Algoritmos de búsqueda 19.2.1 Búsqueda lineal19.2.2 Búsqueda binaria

19.3 Algoritmos de ordenamiento 19.3.1 Ordenamiento por selección19.3.2 Ordenamiento por inserción19.3.3 Ordenamiento por combinación

19.4 Conclusión

Resumen | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

19.1 IntroducciónLa búsqueda de datos implica el determinar si un valor (conocido como la clave de búsqueda) está presente en los datos y, de ser así, hay que encontrar su ubicación. Dos algoritmos populares de bús-queda son la búsqueda lineal simple y la búsqueda binaria, que es más rápida pero a la vez más com-pleja. El ordenamiento coloca los datos en orden ascendente o descendente, con base en una o más claves de ordenamiento. Una lista de nombres se podría ordenar en forma alfabética, las cuentas bancarias podrían ordenarse por número de cuenta, los registros de nóminas de empleados podrían ordenarse por número de seguro social, etcétera. En este capítulo se presentan dos algoritmos de or-denamiento simples, el ordenamiento por selección y el ordenamiento por inserción, junto con el ordenamiento por combinación, que es más eficiente pero también más complejo. La API Collections de Java (capítulo 20, en inglés) cuenta con varias herramientas integradas de búsqueda y ordena-miento. En la figura 19.1 se sintetizan los algoritmos de búsqueda y ordenamiento que veremos en los ejemplos y ejercicios de este libro.

Fig. 19.1 � Los algoritmos de búsqueda y ordenamiento que se cubren en este libro.

Capítulo Algoritmo Ubicación

Algoritmos de búsqueda:

19 Búsqueda linealBúsqueda binariaBúsqueda lineal recursivaBúsqueda binaria recursiva

Sección 19.2.1Sección 19.2.2Ejercicio 19.8Ejercicio 19.9

20 Método binarySearch de la clase Collections Figura 20.12

22 Búsqueda lineal en un objeto ListBúsqueda con árboles binarios

Ejercicio 22.21Ejercicio 22.23

Algoritmos de ordenamiento:

19 Ordenamiento por selecciónOrdenamiento por inserciónOrdenamiento por combinaciónOrdenamiento de burbujaOrdenamiento de cubetaOrdenamiento rápido (quicksort) recursivo

Sección 19.3.1Sección 19.3.2Sección 19.3.3Ejercicios 19.5 y 19.6Ejercicio 19.7Ejercicio 19.10

20 método sort de la clase CollectionsSortedSet collection

Figuras 20.6 a 20.9Figura 20.17

22 Ordenamiento con árboles binarios Sección 22.7

800 Capítulo 19 Búsqueda, ordenamiento y Big O

19.2 Algoritmos de búsquedaBuscar un número telefónico, buscar un sitio Web a través de un motor de búsqueda y comprobar la definición de una palabra en un diccionario son acciones que implican buscar entre grandes canti-dades de datos. En las siguientes dos secciones hablaremos sobre dos algoritmos de búsqueda comunes: uno que es fácil de programar, pero relativamente ineficiente, y uno que es relativamente eficiente pero más complejo y difícil de programar.

19.2.1 Búsqueda linealEl algoritmo de búsqueda lineal busca por cada elemento de un arreglo en forma secuencial. Si la clave de búsqueda no coincide con un elemento en el arreglo, el algoritmo evalúa cada elemento y, cuando se llega al final del arreglo, informa al usuario que la clave de búsqueda no está presente. Si la clave de búsqueda se encuentra en el arreglo, el algoritmo evalúa cada elemento hasta encontrar uno que coincida con la clave de búsqueda y devuelve el índice de ese elemento.

Como ejemplo, considere un arreglo que contiene los siguientes valores:

34 56 2 10 77 51 93 30 5 52

y un programa que busca el número 51. Usando el algoritmo de búsqueda lineal, el programa pri-mero comprueba si el 34 coincide con la clave de búsqueda. Si no es así, el algoritmo comprueba si 56 coincide con la clave de búsqueda. El programa continúa recorriendo el arreglo en forma secuencial, y evalúa el 2, luego el 10, después el 77. Cuando el programa evalúa el número 51, que coincide con la clave de búsqueda, devuelve el índice 5, que está en la posición del 51 en el arreglo. Si después de comprobar cada elemento del arreglo, el programa determina que la clave de búsqueda no coincide con ningún elemento del arreglo, el programa devuelve un valor centinela (por ejemplo, -1).

En la figura 19.2 se declara la clase ArregloLineal. Esta clase tiene dos variables de instancia private: un arreglo de valores int llamado datos, y un objeto static Random para llenar el arreglo con valores int generados al azar. Cuando se crea una instancia de un objeto de la clase ArregloLineal, el constructor (líneas 13 a 20) crea e inicializa el arreglo datos con valores int aleatorios en el rango de 10 a 99. Si hay valores duplicados en el arreglo, la búsqueda lineal devuelve el índice del primer ele-mento en el arreglo que coincide con la clave de búsqueda.

1 // Fig. 19.2: ArregloLineal.java

2 // Clase que contiene un arreglo de enteros aleatorios y un método

3 // que busca en ese arreglo, en forma secuencial.

4 import java.util.Random;

5 import java.util.Arrays;

6

7 public class ArregloLineal

8 {

9 private int[] datos; // arreglo de valores

10 private static final Random generador = new Random();

11

12 // crea un arreglo de un tamaño dado, y lo rellena con enteros aleatorios

13 public ArregloLineal( int tamanio )

14 {

15 datos = new int[ tamanio ]; // crea un espacio para el arreglo

16

Fig. 19.2 � Clase que contiene un arreglo de enteros aleatorios y un método que busca en ese arreglo, en forma secuencial (parte 1 de 2).

19.2 Algoritmos de búsqueda 801

En las líneas 23 a 31 se realiza la búsqueda lineal. La clave de búsqueda se pasa al parámetro clave-Busqueda. En las líneas 26 a 28 se itera a través de los elementos en el arreglo. En la línea 27 se compara cada elemento en el arreglo con claveBusqueda. Si los valores son iguales, en la línea 28 se devuelve el índice del elemento. Si el ciclo termina sin encontrar el valor, en la línea 30 se devuelve -1. En las líneas 34 a 37 se declara el método toString, que devuelve una representación String del arreglo.

En la figura 19.3 se crea un objeto ArregloLineal, el cual contiene un arreglo de 10 valores int (línea 16) y permite al usuario buscar elementos específicos en el arreglo. En las líneas 20 a 22 se pide al usuario la clave de búsqueda y se almacena. El arreglo contiene valores int de 10 a 99 (línea 19 de la figura 19.2). En la línea 28 se llama al método busquedaLineal para determinar si enteroBusqueda está en el arreglo. Si no lo está, busquedaLineal devuelve -1 y el programa notifica al usuario (líneas 31 y 32). Si enteroBusqueda está en el arreglo, busquedaLineal devuelve la posición del elemento, que el pro-grama muestra en las líneas 34 y 35. En las líneas 38 a 40 se obtiene el siguiente entero del usuario.

17 // llena el arreglo con valores int aleatorios, en el rango de 10 a 99

18 for ( int i = 0; i < tamanio; i++ )

19 datos[ i ] = 10 + generador.nextInt( 90 );

20 } // fin del constructor de ArregloLineal

21

22 // realiza una búsqueda lineal en los datos

23 public int busquedaLineal( int claveBusqueda )

24 {

25 // itera a través del arreglo en forma secuencial

26 for ( int indice = 0; indice < datos.length; indice++ )

27 if ( datos[ indice ] == claveBusqueda )

28 return indice; // devuelve el índice del entero

29

30 return -1; // no se encontró el entero

31 } // fin del método busquedaLineal

32

33 // método para imprimir los valores del arreglo

34 public String toString()

35 {

36 return Arrays.toString ( datos );

37 } // fin del método toString

38 } // fin de la clase ArregloLineal

1 // Fig. 19.3: PruebaBusquedaLineal.java

2 // Busca un elemento en el arreglo, en forma secuencial.

3 import java.util.Scanner;

4

5 public class PruebaBusquedaLineal

6 {

7 public static void main( String[] args )

8 {

9 // crea objeto Scanner para los datos de entrada

10 Scanner entrada = new Scanner( System.in );

Fig. 19.2 � Clase que contiene un arreglo de enteros aleatorios y un método que busca en ese arreglo, en forma secuencial (parte 2 de 2).

Fig. 19.3 � Busca un elemento en un arreglo, en forma secuencial (parte 1 de 2).

802 Capítulo 19 Búsqueda, ordenamiento y Big O

Eficiencia de la búsqueda linealTodos los algoritmos de búsqueda logran el mismo objetivo: encontrar un elemento que coincida con una clave de búsqueda dada, si es que existe dicho elemento. Sin embargo, hay varias cosas que diferen-cian a un algoritmo de otro. La principal diferencia es la cantidad de esfuerzo que requieren para completar la búsqueda. Una forma de describir este esfuerzo es mediante la notación Big O, la cual indica el tiempo

11

12 int enteroBusqueda; // clave de búsqueda

13 int posicion; // ubicación de la clave de búsqueda en el arreglo

14

15 // crea el arreglo y lo muestra en pantalla

16 ArregloLineal arregloBusqueda = new ArregloLineal( 10 );

17 System.out.println( arregloBusqueda + “/n” ); // imprime el arreglo

18

19 // obtiene la entrada del usuario

20 System.out.print(

21 “Escriba un valor entero (-1 para terminar): ” );

22 enteroBusqueda = entrada.nextInt(); // lee el primer entero del usuario

23

24 // recibe en forma repetida un entero como entrada; -1 termina el programa

25 while ( enteroBusqueda != -1 )

26 {

27 // realiza una búsqueda lineal

28 posicion = arregloBusqueda.busquedaLineal( enteroBusqueda );

29

30 if ( posicion == -1 ) // no se encontró el entero

31 System.out.println( “El entero ” + enteroBusqueda +

32 “ no se encontro.\n” );

33 else // se encontró el entero

34 System.out.println( “El entero ” + enteroBusqueda +

35 “ se encontro en la posicion ” + posicion + “.\n” );

36

37 // obtiene la entrada del usuario

38 System.out.print(

39 “Escriba un valor entero (-1 para terminar): ” );

40 enteroBusqueda = entrada.nextInt(); // lee el siguiente entero del usuario

41 } // fin de while

42 } // fin de main

43 } // fin de la clase PruebaBusquedaLineal

[59, 97, 34, 90, 79, 56, 24, 51, 30, 69]

Escriba un valor entero (-1 para terminar): 79El entero 79 se encontro en la posicion 4.

Escriba un valor entero (-1 para terminar): 61El entero 61 no se encontro.

Escriba un valor entero (-1 para terminar): 51El entero 51 se encontro en la posicion 7.

Escriba un valor entero (-1 para terminar): -1

Fig. 19.3 � Busca un elemento en un arreglo, en forma secuencial (parte 2 de 2).

19.2 Algoritmos de búsqueda 803

de ejecución para el peor caso de un algoritmo; es decir, qué tan duro tendrá que trabajar un algoritmo para resolver un problema. En los algoritmos de búsqueda y ordenamiento, esto depende específica-mente de cuántos elementos de datos haya.

Algoritmos O(1)Suponga que un algoritmo está diseñado para evaluar si el primer elemento de un arreglo es igual al se-gundo elemento. Si el arreglo tiene 10 elementos, este algoritmo requiere una comparación. Si el arreglo tiene 1000 elementos, sigue requiriendo una comparación. De hecho, el algoritmo es completamente independiente del número de elementos en el arreglo. Se dice que este algoritmo tiene un tiempo de ejecución constante, el cual se representa en la notación Big O como O(1). Un algoritmo que es O(1) no necesariamente requiere sólo de una comparación. O(1) sólo significa que el número de com-paraciones es constante; no crece a medida que aumenta el tamaño del arreglo. Un algoritmo que eva-lúa si el primer elemento de un arreglo es igual a los siguientes tres elementos sigue siendo O(1), aun cuando requiera tres comparaciones.

Algoritmos O(n)Un algoritmo que evalúa si el primer elemento de un arreglo es igual a cualquiera de los demás elemen-tos del arreglo requerirá cuando menos de n – 1 comparaciones, en donde n es el número de elementos en el arreglo. Si el arreglo tiene 10 elementos, este algoritmo requiere hasta nueve comparaciones. Si el arreglo tiene 1000 elementos, requiere hasta 999 comparaciones. A medida que n aumenta en tamaño, la parte de la expresión correspondiente a la n “domina”, y si le restamos uno no hay conse-cuencias. Big O está diseñado para resaltar estos términos dominantes, e ignorar los términos que pierden importancia a medida que n crece. Por esta razón, se dice que un algoritmo que requiere un total de n – 1 comparaciones (como el que describimos antes) es O(n). Se considera que un algoritmo O(n) tiene un tiempo de ejecución lineal. A menudo, O(n) significa “en el orden de n”, o dicho en forma más simple, “orden n”.

Algoritmos O(n2)Ahora, suponga que tiene un algoritmo que evalúa si cualquier elemento de un arreglo se duplica en cualquier otra parte del mismo. El primer elemento debe compararse con todos los demás elementos del arreglo. El segundo elemento debe compararse con todos los demás elementos, excepto con el pri-mero (ya se comparó con éste). El tercer elemento debe compararse con todos los elementos, excepto los primeros dos. Al final, este algoritmo terminará realizando (n – 1) + (n – 2) + … + 2 + 1, o n2/2 – n/2 comparaciones. A medida que n aumenta, el término n2 domina y el término n se vuelve inconsecuente. De nuevo, la notación Big O resalta el término n2, dejando a n2/2. Pero como veremos pronto, los fac-tores constantes se omiten en la notación Big O.

Big O se enfoca en la forma en que aumenta el tiempo de ejecución de un algoritmo, en relación con el número de elementos procesados. Suponga que un algoritmo requiere n2 comparaciones. Con cuatro elementos, el algoritmo requiere 16 comparaciones; con ocho elementos, 64 comparaciones. Con este algoritmo, al duplicar el número de elementos se cuadruplica el número de comparacio-nes. Considere un algoritmo similar que requiere n2/2 comparaciones. Con cuatro elementos, el algorit-mo requiere ocho comparaciones; con ocho elementos, 32 comparaciones. De nuevo, al duplicar el número de elementos se cuadruplica el número de comparaciones. Ambos algoritmos aumentan como el cuadrado de n, por lo que Big O ignora la constante y ambos algoritmos se consideran como O(n2), lo cual se conoce como tiempo de ejecución cuadrático y se pronuncia como “en el orden de n al cuadrado”, o dicho en forma más simple, “orden n al cuadrado”.

Cuando n es pequeña, los algoritmos O(n2) (que se ejecutan en las computadoras personales de la actualidad) no afectan el rendimiento en forma considerable. Pero a medida que n aumenta, se em-pieza a notar la reducción en el rendimiento. Un algoritmo O(n2) que se ejecute en un arreglo de un millón de elementos requeriría un billón de “operaciones” (en donde cada una requeriría en realidad

804 Capítulo 19 Búsqueda, ordenamiento y Big O

varias instrucciones de máquina para ejecutarse). Esto podría tardar varias horas. Un arreglo de mil millones de elementos requeriría un trillón de operaciones, ¡un número tan grande que el algoritmo tardaría décadas! Por desgracia, los algoritmos O(n2) son fáciles de escribir, como veremos en este capítulo. También veremos algoritmos con medidas de Big O más favorables. Estos algoritmos eficien-tes comúnmente requieren un poco más de astucia y trabajo para crearlos, pero su rendimiento superior bien vale la pena el esfuerzo adicional, en especial a medida que n aumenta y los algoritmos se combi-nan en programas más grandes.

Big O de la búsqueda linealEl algoritmo de búsqueda lineal se ejecuta en un tiempo O(n). El peor caso en este algoritmo es que se debe comprobar cada elemento para determinar si el elemento que se busca existe en el arreglo. Si el tamaño del arreglo se duplica, el número de comparaciones que el algoritmo debe realizar también se duplica. La búsqueda lineal puede proporcionar un rendimiento sorprendente, si el elemento que coin-cide con la clave de búsqueda se encuentra en (o cerca de) la parte frontal del arreglo. Pero buscamos algoritmos que tengan un buen desempeño, en promedio, en todas las búsquedas, incluyendo aquellas en las que el elemento que coincide con la clave de búsqueda se encuentra cerca del final del arreglo.

La búsqueda lineal es fácil de programar, pero puede ser lenta si se le compara con otros algorit-mos de búsqueda. Si un programa necesita realizar muchas búsquedas en arreglos grandes, puede ser mejor implementar un algoritmo más eficiente, como la búsqueda binaria, el cual presentaremos en la siguiente sección.

Tip de rendimiento 19.1Algunas veces los algoritmos más simples tienen un desempeño pobre. Su virtud es que son fáciles de programar, probar y depurar. En ocasiones se requieren algoritmos más com-plejos para obtener el máximo rendimiento.

19.2.2 Búsqueda binariaEl algoritmo de búsqueda binaria es más eficiente que el algoritmo de búsqueda lineal, pero requiere que el arreglo se ordene. La primera iteración de este algoritmo evalúa el elemento medio del arreglo. Si éste coincide con la clave de búsqueda, el algoritmo termina. Suponiendo que el arreglo se ordene en forma ascendente, entonces si la clave de búsqueda es menor que el elemento de en medio, no puede coincidir con ningún elemento en la segunda mitad del arreglo, y el algoritmo continúa sólo con la pri-mera mitad (es decir, el primer elemento hasta, pero sin incluir, el elemento de en medio). Si la clave de búsqueda es mayor que el elemento de en medio, no puede coincidir con ninguno de los elementos de la primera mitad del arreglo, y el algoritmo continúa sólo con la segunda mitad del arreglo (es decir, desde el elemento después del elemento de en medio, hasta el último elemento). Cada iteración evalúa el valor medio de la porción restante del arreglo. Si la clave de búsqueda no coincide con el elemento, el algoritmo elimina la mitad de los elementos restantes. Para terminar, el algoritmo encuentra un elemento que coin-cide con la clave de búsqueda o reduce el subarreglo hasta un tamaño de cero.

Como ejemplo, considere el siguiente arreglo ordenado de 15 elementos:

2 3 5 10 27 30 34 51 56 65 77 81 82 93 99

y una clave de búsqueda de 65. Un programa que implemente el algoritmo de búsqueda binaria pri-mero comprobaría si el 51 es la clave de búsqueda (ya que 51 es el elemento de en medio del arreglo). La clave de búsqueda (65) es mayor que 51, por lo que este número se descarta junto con la primera mitad del arreglo (todos los elementos menores que 51). Con esto obtenemos lo siguiente:

56 65 77 81 82 93 99

19.2 Algoritmos de búsqueda 805

A continuación, el algoritmo comprueba si 81 (el elemento de en medio del resto del arreglo) coincide con la clave de búsqueda. La clave de búsqueda (65) es menor que 81, por lo que se descarta este número junto con los elementos mayores de 81. Después de sólo dos pruebas, el algoritmo ha reducido el número de valores a comprobar a tres (56, 65 y 77). Después el algoritmo comprueba el 65 (que sin duda coincide con la clave de búsqueda), y devuelve el índice del elemento del arreglo que contiene el 65. Este algoritmo sólo requirió tres comparaciones para determinar si la clave de bús-queda coincidió con un elemento del arreglo. Un algoritmo de búsqueda lineal hubiera requerido 10 comparaciones. [Nota: en este ejemplo optamos por usar un arreglo con 15 elementos, para que siem-pre haya un elemento obvio en medio del arreglo. Con un número par de elementos, la parte media del arreglo se encuentra entre dos elementos. Implementamos el algoritmo para elegir el menor de esos dos elementos].

Implementación de la búsqueda binariaLa figura 19.4 declara la clase ArregloBinario. Esta clase es similar a ArregloLineal: tiene dos varia-bles de instancia private, un constructor, un método de búsqueda (busquedaBinaria), un método elementosRestantes y un método toString. En las líneas 13 a 22 se declara el constructor. Una vez que se inicializa el arreglo con valores int aleatorios de 10 a 99 (líneas 18 y 19), en la línea 21 se hace una llamada al método Arrays.sort en el arreglo datos. El método sort es un método static de la clase Arrays, que ordena los elementos en un arreglo en orden ascendente de manera predeterminada; una versión sobrecargada de este método nos permite cambiar la forma de ordenar los datos. Recuerde que el algoritmo de búsqueda binaria sólo funciona en un arreglo ordenado.

1 // Fig. 19.4: ArregloBinario.java

2 // Clase que contiene un arreglo de enteros aleatorios y un método

3 // que utiliza la búsqueda binaria para encontrar un entero.

4 import java.util.Random;

5 import java.util.Arrays;

6

7 public class ArregloBinario

8 {

9 private int[] datos; // arreglo de valores

10 private static final Random generador = new Random();

11

12 // crea un arreglo de un tamaño dado y lo llena con enteros aleatorios

13 public ArregloBinario( int tamanio )

14 {

15 datos = new int[ tamanio ]; // crea espacio para el arreglo

16

17 // llena el arreglo con enteros aleatorios en el rango de 10 a 99

18 for ( int i = 0; i < tamanio; i++ )

19 datos[ i ] = 10 + generador.nextInt( 90 );

20

21 Arrays.sort( datos );

22 } // fin del constructor de ArregloBinario

23

24 // realiza una búsqueda binaria en los datos

25 public int busquedaBinaria( int elementoBusqueda )

26 {

Fig. 19.4 � Clase que contiene un arreglo de enteros aleatorios y un método que utiliza la búsqueda binaria para encontrar un entero (parte 1 de 3).

806 Capítulo 19 Búsqueda, ordenamiento y Big O

27 int inferior = 0; // extremo inferior del área de búsqueda28 int superior = datos.length - 1; // extremo superior del área de búsqueda

29 int medio = ( inferior + superior + 1 ) / 2; // elemento medio

30 int ubicacion = -1; // devuelve el valor; -1 si no lo encontró

3132 do // ciclo para buscar un elemento

33 {

34 // imprime el resto de los elementos del arreglo

35 System.out.print( elementosRestantes( inferior, superior ) );

3637 // imprime espacios para alineación

38 for ( int i = 0; i < medio; i++ )

39 System.out.print( “ ” );

40 System.out.println( “ * ” ); // indica el elemento medio actual

4142 // si el elemento se encuentra en medio

43 if ( elementoBusqueda == datos[ medio ] )

44 ubicacion = medio; // la ubicación es el elemento medio actual

4546 // el elemento medio es demasiado alto

47 else if ( elementoBusqueda < datos[ medio ] )

48 superior = medio - 1; // elimina la mitad superior

49 else // el elemento medio es demasiado bajo

50 inferior = medio + 1; // elimina la mitad inferior

5152 medio = ( inferior + superior + 1 ) / 2; // recalcula el elemento medio

53 } while ( ( inferior <= superior ) && ( ubicacion == -1 ) );

5455 return ubicacion; // devuelve la ubicación de la clave de búsqueda

56 } // fin del método busquedaBinaria

5758 // método para imprimir ciertos valores en el arreglo

59 public String elementosRestantes( int inferior, int superior )

60 {

61 StringBuilder temporal = new StringBuilder();

6263 // imprime espacios para alineación

64 for ( int i = 0; i < inferior; i++ )

65 temporal.append( “ ” );

6667 // imprime los elementos que quedan en el arreglo

68 for ( int i = inferior; i <= superior; i++ )

69 temporal.append( datos[ i ] + “ ” );

7071 temporal.append( “\n” );

72 return temporal.toString();

73 } // fin del método elementosRestantes

7475 // método para imprimir los valores en el arreglo

76 public String toString()77 {

Fig. 19.4 � Clase que contiene un arreglo de enteros aleatorios y un método que utiliza la búsqueda binaria para encontrar un entero (parte 2 de 3).

19.2 Algoritmos de búsqueda 807

En las líneas 25 a 56 se declara el método busquedaBinaria. La clave de búsqueda se pasa al paráme-tro elementoBusqueda (línea 25). En las líneas 27 a 29 se calcula el índice del extremo inferior, el índi-ce del extremo superior y el índice medio de la porción del arreglo en la que el programa está buscando en ese momento. Al principio del método, el extremo inferior es 0, el extremo superior es la longitud del arreglo menos 1, y medio es el promedio de estos dos valores. En la línea 30 se inicializa la ubicacion del elemento en -1; el valor que se devolverá si no se encuentra el elemento. En las líneas 32 a 53 se itera hasta que inferior sea mayor que superior (esto ocurre cuando no se encuentra el elemento), o cuando ubicacion no sea igual a -1 (lo cual indica que se encontró la clave de búsqueda). En la línea 43 se evalúa si el valor en el elemento medio es igual a elementoBusqueda. Si esto es true, en la línea 44 se asigna me-dio a ubicacion. Después el ciclo termina y ubicacion se devuelve al método que hizo la llamada. Cada iteración del ciclo evalúa un solo valor (línea 43) y elimina la mitad del resto de los valores en el arreglo (línea 48 o 50).

En la figura 19.5 se realiza una búsqueda binaria, usando la clave que el usuario introduzca para determinar si coincide con un elemento en el arreglo. La primera línea de salida de este programa es el arreglo de valores int, en orden ascendente. Cuando el usuario indica al programa que busque el número 23, el programa primero evalúa el elemento medio, que es 42 (según lo indicado por el sím-bolo *). La clave de búsqueda es menor que 42, por lo que el programa elimina la segunda mitad del arreglo y evalúa el elemento medio de la primera mitad. La clave de búsqueda es menor que 34, por lo que el programa elimina la segunda mitad del arreglo, dejando sólo tres elementos. Por último, el pro-grama comprueba el 23 (que coincide con la clave de búsqueda) y devuelve el índice 1.

78 return elementosRestantes( 0, datos.length - 1 );

79 } // fin del método toString

80 } // fin de la clase ArregloBinario

1 // Fig. 19.5: PruebaBusquedaBinaria.java

2 // Usa la búsqueda binaria para localizar un elemento en un arreglo.

3 import java.util.Scanner;

4

5 public class PruebaBusquedaBinaria

6 {

7 public static void main( String[] args )

8 {

9 // crea un objeto Scanner para recibir datos de entrada

10 Scanner entrada = new Scanner( System.in );

11

12 int enteroABuscar; // clave de búsqueda

13 int posicion; // ubicación de la clave de búsqueda en el arreglo

14

15 // crea un arreglo y lo muestra en pantalla

16 ArregloBinario arregloBusqueda = new ArregloBinario( 15 );

17 System.out.println( arregloBusqueda );

18

Fig. 19.4 � Clase que contiene un arreglo de enteros aleatorios y un método que utiliza la búsqueda binaria para encontrar un entero (parte 3 de 3).

Fig. 19.5 � Uso de la búsqueda binaria para localizar un elemento en un arreglo (el símbolo * en la salida marca el elemento medio) (parte 1 de 3).

808 Capítulo 19 Búsqueda, ordenamiento y Big O

19 // obtiene la entrada del usuario

20 System.out.print(

21 “Escriba un valor entero (-1 para salir): ” );

22 enteroABuscar = entrada.nextInt(); // lee un entero del usuario

23 System.out.println();

24

25 // recibe un entero como entrada en forma repetida; -1 termina el programa

26 while ( enteroABuscar != -1 )

27 {

28 // usa la búsqueda binaria para tratar de encontrar el entero

29 posicion = arregloBusqueda.busquedaBinaria( enteroABuscar );

30

31 // el valor de retorno -1 indica que no se encontró el entero

32 if ( posicion == -1 )

33 System.out.println( “El entero ” + enteroABuscar +

34 “ no se encontro.\n” );

35 else

36 System.out.println( “El entero ” + enteroABuscar +

37 “ se encontro en la posicion ” + posicion + “.\n” );

38

39 // obtiene entrada del usuario

40 System.out.print(

41 “Escriba un valor entero (-1 para salir): ” );

42 enteroABuscar = entrada.nextInt(); // lee un entero del usuario

43 System.out.println();

44 } // fin de while

45 } // fin de main

46 } // fin de la clase PruebaBusquedaBinaria

13 23 24 34 35 36 38 42 47 51 68 74 75 85 97

Escriba un valor entero (-1 para salir): 23

13 23 24 34 35 36 38 42 47 51 68 74 75 85 97 *13 23 24 34 35 36 38 *13 23 24 *El entero 23 se encontro en la posicion 1.

Escriba un valor entero (-1 para salir): 75

13 23 24 34 35 36 38 42 47 51 68 74 75 85 97 * 47 51 68 74 75 85 97 * 75 85 97 * 75 *El entero 75 se encontro en la posicion 12.

Fig. 19.5 � Uso de la búsqueda binaria para localizar un elemento en un arreglo (el símbolo * en la salida marca el elemento medio) (parte 2 de 3).

19.3 Algoritmos de ordenamiento 809

Eficiencia de la búsqueda binariaEn el peor de los casos, el proceso de buscar en un arreglo ordenado de 1023 elementos sólo requiere 10 comparaciones cuando se utiliza una búsqueda binaria. Al dividir 1023 entre 2 en forma repetida (ya que después de cada comparación podemos eliminar la mitad del arreglo) y redondear (porque también eli-minamos el elemento medio), se producen los valores 511, 255, 127, 63, 31, 15, 7, 3, 1 y 0. El número 1023 (210 – 1) se divide entre 2 sólo 10 veces para obtener el valor 0, que indica que no hay más elementos para probar. La división entre 2 equivale a una comparación en el algoritmo de búsqueda binaria. Por ende, un arreglo de 1,048,575 (220 – 1) elementos requiere un máximo de 20 comparaciones para encon-trar la clave, y un arreglo de más de mil millones de elementos requiere un máximo de 30 comparaciones para encontrar la clave. Ésta es una enorme mejora en el rendimiento, en comparación con la búsqueda lineal. Para un arreglo de mil millones de elementos, ésta es una diferencia entre un promedio de 500 millones de comparaciones para la búsqueda lineal, ¡y un máximo de sólo 30 comparaciones para la bús-queda binaria! El número máximo de comparaciones necesarias para la búsqueda binaria de cualquier arreglo ordenado es el exponente de la primera potencia de 2 mayor que el número de elementos en el arreglo, que se representa como log

2 n. Todos los logaritmos crecen aproximadamente a la misma pro-

porción, por lo que en notación Big O se puede omitir la base. Esto produce un valor Big O de O(log n) para una búsqueda binaria, que también se conoce como tiempo de ejecución logarítmico.

19.3 Algoritmos de ordenamientoEl ordenamiento de datos (es decir, colocar los datos en cierto orden específico, como ascendente o descendente) es una de las aplicaciones computacionales más importantes. Un banco ordena todos los cheques por número de cuenta, de manera que pueda preparar instrucciones bancarias indivi-duales al final de cada mes. Las compañías telefónicas ordenan sus listas de cuentas por apellido paterno y luego por primer nombre, para facilitar el proceso de buscar números telefónicos. Casi cualquier organización debe ordenar datos, y a menudo cantidades masivas de ellos. El ordenamiento de datos es un problema intrigante, que requiere un uso intensivo de la computadora, y ha atraído un enorme esfuerzo de investigación.

Un punto importante a comprender acerca del ordenamiento es que el resultado final (los datos ordenados) será el mismo, sin importar qué algoritmo se utilice para ordenar los datos. La elección del algoritmo sólo afecta al tiempo de ejecución y el uso que haga el programa de la memoria. En el resto del capítulo se introducen tres algoritmos de ordenamiento comunes. Los primeros dos (ordenamiento por selección y ordenamiento por inserción) son simples de programar, pero ineficientes. El último algorit-mo (ordenamiento por combinación) es más rápido que el ordenamiento por selección y el ordena-

Escriba un valor entero (-1 para salir): 52

13 23 24 34 35 36 38 42 47 51 68 74 75 85 97 * 47 51 68 74 75 85 97 * 47 51 68 * 68 *El entero 52 no se encontro.

Escriba un valor entero (-1 para salir): -1

Fig. 19.5 � Uso de la búsqueda binaria para localizar un elemento en un arreglo (el símbolo * en la salida marca el elemento medio) (parte 3 de 3).

810 Capítulo 19 Búsqueda, ordenamiento y Big O

miento por inserción, pero más difícil de programar. Nos enfocaremos en ordenar arreglos de datos de tipos primitivos, a saber, valores int. Es posible ordenar arreglos de objetos de clases también. En la sección 20.7.1 hablaremos sobre esto.

19.3.1 Ordenamiento por selecciónEl ordenamiento por selección es un algoritmo de ordenamiento simple, pero ineficiente. En la pri-mera iteración del algoritmo se selecciona el elemento más pequeño en el arreglo, y se intercambia con el primer elemento. En la segunda iteración se selecciona el segundo elemento más pequeño (que viene siendo el elemento más pequeño de los elementos restantes) y se intercambia con el segundo elemento. El algoritmo continúa hasta que en la última iteración se selecciona el segundo elemento más grande y se intercambia con el índice del segundo al último, dejando el elemento más grande en el último índice. Después de la i-ésima iteración, los i elementos más pequeños del arreglo se ordenarán en forma ascen-dente, en los primeros i elementos del arreglo.

Como ejemplo, considere el siguiente arreglo:

34 56 4 10 77 51 93 30 5 52

Un programa que implemente el ordenamiento por selección primero determinará el elemento más pequeño (4) de este arreglo, que está contenido en el índice 2. El programa intercambia 4 con 34, dando el siguiente resultado:

4 56 34 10 77 51 93 30 5 52

Después el programa determina el valor más pequeño del resto de los elementos (todos los elemen-tos excepto el 4), que es 5 y está contenido en el índice 8. El programa intercambia el 5 con el 56, dando el siguiente resultado:

4 5 34 10 77 51 93 30 56 52

En la tercera iteración, el programa determina el siguiente valor más pequeño (10) y lo intercambia con el 34.

4 5 10 34 77 51 93 30 56 52

El proceso continúa hasta que el arreglo quede completamente ordenado.

4 5 10 30 34 51 52 56 77 93

Después de la primera iteración, el elemento más pequeño está en la primera posición. Después de la segun-da iteración los dos elementos más pequeños están en orden, en las primeras dos posiciones. Después de la tercera iteración los tres elementos más pequeños están en orden, en las primeras tres posiciones.

En la figura 19.6 se declara la clase OrdenamientoSeleccion. Esta clase tiene dos variables de ins-tancia private: un arreglo de valores int llamado datos, y un objeto static Random para generar enteros aleatorios y llenar el arreglo. Cuando se crea una instancia de un objeto de la clase Ordenamien-toSeleccion, el constructor (líneas 13 a 20) crea e inicializa el arreglo datos con valores int aleatorios, en el rango de 10 a 99.

1 // Fig. 19.6: OrdenamientoSeleccion.java 2 // Clase que crea un arreglo lleno con enteros aleatorios. 3 // Proporciona un método para ordenar el arreglo mediante el ordenamiento por selección. 4 import java.util.Arrays; 5 import java.util.Random; 6

Fig. 19.6 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por selección (parte 1 de 3).

19.3 Algoritmos de ordenamiento 811

7 public class OrdenamientoSeleccion 8 { 9 private int[] datos; // arreglo de valores10 private static final Random generador = new Random();1112 // crea un arreglo de un tamaño dado y lo llena con enteros aleatorios13 public OrdenamientoSeleccion( int tamanio )14 {15 datos = new int[ tamanio ]; // crea espacio para el arreglo1617 // llena el arreglo con enteros aleatorios en el rango de 10 a 9918 for ( int i = 0; i < tamanio; i++ )19 datos[ i ] = 10 + generador.nextInt( 90 );20 } // fin del constructor de OrdenamientoSeleccion2122 // ordena el arreglo usando el ordenamiento por selección23 public void ordenar()24 {25 int masPequenio; // índice del elemento más pequeño2627 // itera a través de datos.length - 1 elementos28 for ( int i = 0; i < datos.length - 1; i++ )29 {30 masPequenio = i; // primer índice del resto del arreglo3132 // itera para buscar el índice del elemento más pequeño33 for ( int indice = i + 1; indice < datos.length; indice++ )34 if ( datos[ indice ] < datos[ masPequenio ] )35 masPequenio = indice;36

37 intercambiar( i, masPequenio ); // intercambia el elemento más pequeño en la posición

38 imprimirPasada( i + 1, masPequenio ); // imprime la pasada del algoritmo39 } // fin de for exterior40 } // fin del método ordenar4142 // método ayudante para intercambiar los valores de dos elementos43 public void intercambiar( int primero, int segundo )44 {45 int temporal = datos[ primero ]; // almacena primero en temporal46 datos[ primero ] = datos[ segundo ]; // sustituye primero con segundo47 datos[ segundo ] = temporal; // coloca temporal en segundo48 } // fin del método intercambiar4950 // imprime una pasada del algoritmo51 public void imprimirPasada( int pasada, int indice )52 {53 System.out.print( String.format( “despues de pasada %2d: ”, pasada ) );5455 // imprime elementos hasta el elemento seleccionado56 for ( int i = 0; i < indice; i++ )57 System.out.print( datos[ i ] + “ ” );

Fig. 19.6 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por selección (parte 2 de 3).

812 Capítulo 19 Búsqueda, ordenamiento y Big O

En las líneas 23 a 40 se declara el método ordenar. En la línea 24 se declara la variable masPequenio, que almacenará el índice del elemento más pequeño en el resto del arreglo. En las líneas 28 a 39 se itera datos.length – 1 veces. En la línea 30 se inicializa el índice del elemento más pequeño con el elemen-to actual. En las líneas 33 a 35 se itera a través del resto de los elementos en el arreglo. Para cada uno de estos elementos, en la línea 34 se compara su valor con el valor del elemento más pequeño. Si el elemento actual es menor que el elemento más pequeño, en la línea 35 se asigna el índice del elemento actual a masPequenio. Cuando termine este ciclo, masPequenio contendrá el índice del elemento más pequeño en el resto del arreglo. En la línea 36 se hace una llamada al método intercambiar (líneas 43 a 48) para colocar el elemento restante más pequeño en la siguiente posición ordenada en el arreglo.

En la línea 9 de la figura 19.7 se crea un objeto OrdenamientoSeleccion con 10 elementos. En la línea 12 se imprime el objeto desordenado en pantalla. En la línea 14 se hace una llamada al método ordenar (líneas 22 a 39 de la figura 19.6), el cual ordena los elementos mediante el ordenamiento por selección. Después, en las líneas 16 y 17 se imprime el objeto ordenado en pantalla. En la salida de este programa se utilizan guiones cortos (líneas 67 y 68) para indicar la porción del arreglo que se or-denó después de cada pasada. Se coloca un asterisco enseguida de la posición del elemento que se inter-cambió con el elemento más pequeño en esa pasada. En cada pasada, el elemento enseguida del asterisco (especificado en la línea 58) y el elemento por encima del conjunto de guiones cortos de más a la derecha fueron los dos valores que se intercambiaron.

58

59 System.out.print( datos[ indice ] + “* ” ); // indica intercambio

60

61 // termina de imprimir el arreglo en pantalla

62 for ( int i = indice + 1; i < datos.length; i++ )

63 System.out.print( datos[ i ] + “ ” );

64

65 System.out.print( “\n ” ); // para alineación

66

67 // indica la cantidad del arreglo que está almacenada

68 for( int j = 0; j < pasada; j++ )

69 System.out.print( “-- ” );

70 System.out.println( “\n” ); // agrega fin de línea

71 } // fin del método imprimirPasada

72

73 // método para imprimir los valores del arreglo

74 public String toString()

75 {

76 return Arrays.toString( datos );

77 } // fin del método toString

78 } // fin de la clase OrdenamientoSeleccion

1 // Fig. 19.7: PruebaOrdenamientoSeleccion.java

2 // Prueba de la clase de ordenamiento por selección.

3

4 public class PruebaOrdenamientoSeleccion

5 {

Fig. 19.6 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por selección (parte 3 de 3).

Fig. 19.7 � Prueba de la clase de ordenamiento por selección (parte 1 de 2).

19.3 Algoritmos de ordenamiento 813

Eficiencia del ordenamiento por selecciónEl algoritmo de ordenamiento por selección se ejecuta en un tiempo igual a O(n2). El método ordenar en las líneas 23 a 40 de la figura 19.6, que implementa el algoritmo de ordenamiento por selección, con-tiene dos ciclos for. El ciclo for exterior (líneas 28 a 39) itera a través de los primeros n – 1 elementos en el arreglo, intercambiando el elemento más pequeño restante a su posición ordenada. El ciclo for interior (líneas 33 a 35) itera a través de cada elemento en el arreglo restante, buscando el elemento más pequeño.

6 public static void main( String[] args ) 7 { 8 // crea objeto para realizar el ordenamiento por selección 9 OrdenamientoSeleccion arregloOrden = new OrdenamientoSeleccion( 10 );1011 System.out.println( “Arreglo desordenado:” );12 System.out.println( arregloOrden ); // imprime arreglo desordenado1314 arregloOrden.ordenar(); // ordena el arreglo1516 System.out.println( “Arreglo ordenado:” );17 System.out.println( arregloOrden ); // imprime el arreglo ordenado18 } // fin de main19 } // fin de la clase PruebaOrdenamientoSeleccion

Arreglo desordenado:[72, 10, 69, 62, 37, 59, 51, 86, 47, 15]

despues de pasada 1: 10 72* 69 62 37 59 51 86 47 15 --

despues de pasada 2: 10 15 69 62 37 59 51 86 47 72* -- --

despues de pasada 3: 10 15 37 62 69* 59 51 86 47 72 -- -- --

despues de pasada 4: 10 15 37 47 69 59 51 86 62* 72 -- -- -- --

despues de pasada 5: 10 15 37 47 51 59 69* 86 62 72 -- -- -- -- --

despues de pasada 6: 10 15 37 47 51 59* 69 86 62 72 -- -- -- -- -- --

despues de pasada 7: 10 15 37 47 51 59 62 86 69* 72 -- -- -- -- -- -- --

despues de pasada 8: 10 15 37 47 51 59 62 69 86* 72 -- -- -- -- -- -- -- --

despues de pasada 9: 10 15 37 47 51 59 62 69 72 86* -- -- -- -- -- -- -- -- --

Arreglo ordenado:[10, 15, 37, 47, 51, 59, 62, 69, 72, 86]

Fig. 19.7 � Prueba de la clase de ordenamiento por selección (parte 2 de 2).

814 Capítulo 19 Búsqueda, ordenamiento y Big O

Este ciclo se ejecuta n – 1 veces durante la primera iteración del ciclo exterior, n – 2 veces durante la se-gunda iteración, después n – 3, …, 3, 2, 1. Este ciclo interior iterará un total de n(n – 1)/2 o (n2 – n)/2. En notación Big O, los términos más pequeños se eliminan y las constantes se ignoran, lo cual nos deja un valor Big O final de O(n2).

19.3.2 Ordenamiento por inserciónEl ordenamiento por inserción es otro algoritmo de ordenamiento simple, pero ineficiente. En la prime-ra iteración de este algoritmo se toma el segundo elemento en el arreglo y, si es menor que el primero, se intercambian. En la segunda iteración se analiza el tercer elemento y se inserta en la posición correcta, con respecto a los primeros dos elementos, de manera que los tres elementos estén ordenados. En la i-ésima iteración de este algoritmo, los primeros i elementos en el arreglo original estarán ordenados.

Considere como ejemplo el siguiente arreglo. [Nota: este arreglo es idéntico al que se utiliza en las discusiones sobre el ordenamiento por selección y el ordenamiento por combinación].

34 56 4 10 77 51 93 30 5 52

Un programa que implemente el algoritmo de ordenamiento por inserción primero analizará los prime-ros dos elementos del arreglo, 34 y 56. Éstos ya se encuentran ordenados, por lo que el programa continúa (si estuvieran desordenados, el programa los intercambiaría).

En la siguiente iteración, el programa analiza el tercer valor, 4. Este valor es menor que 56, por lo que el programa almacena el 4 en una variable temporal y mueve el 56 un elemento a la derecha. Después, el programa comprueba y determina que 4 es menor que 34, por lo que mueve el 34 un elemento a la dere-cha. Ahora el programa ha llegado al principio del arreglo, por lo que coloca el 4 en el elemento cero. Entonces, el arreglo es ahora

4 34 56 10 77 51 93 30 5 52

En la siguiente iteración, el programa almacena el valor 10 en una variable temporal. Después el progra-ma compara el 10 con el 56, y mueve el 56 un elemento a la derecha, ya que es mayor que 10. Luego, el programa compara 10 y 34, y mueve el 34 un elemento a la derecha. Cuando el programa compara el 10 con el 4, observa que el primero es mayor que el segundo, por lo cual coloca el 10 en el elemento 1. Aho-ra el arreglo es

4 10 34 56 77 51 93 30 5 52

Utilizando este algoritmo, en la i-ésima iteración, los primeros i elementos del arreglo original están or-denados, pero tal vez no se encuentren en sus posiciones finales; puede haber valores más pequeños en posiciones más adelante en el arreglo.

En la figura 19.8 se declara la clase OrdenamientoInsercion. En las líneas 23 a 47 se declara el mé-todo ordenar. En la línea 25 se declara la variable insercion, la cual contiene el elemento que insertare-mos mientras movemos los demás elementos. En las líneas 28 a 46 se itera a través de datos.length – 1 elementos en el arreglo. En cada iteración, en la línea 31 se almacena en inserciones el valor del elemento que se insertará en la parte ordenada del arreglo. En la línea 34 se declara e inicializa la variable mover-Elemento, que lleva la cuenta de la posición en la que se insertará el elemento. En las líneas 37 a 42 se itera para localizar la posición correcta en la que debe insertarse el elemento. El ciclo terminará, ya sea cuando el programa llegue a la parte frontal del arreglo, o cuando llegue a un elemento que sea menor que el valor a insertar. En la línea 40 se mueve un elemento a la derecha, y en la línea 41 se decrementa la posición en la que se insertará el siguiente elemento. Una vez que termina el ciclo, en la línea 44 se inserta el elemento en su posición. La figura 19.9 es igual que la figura 19.7, sólo que crea y utiliza un objeto OrdenamientoInsercion. En la salida de este programa se utilizan guiones cortos para indicar la parte del arreglo que se ordena después de cada pasada. Se coloca un asterisco enseguida del elemento que se insertó en su posición en esa pasada.

19.3 Algoritmos de ordenamiento 815

1 // Fig. 19.8: OrdenamientoInsercion.java 2 // Clase que crea un arreglo lleno de enteros aleatorios.

3 // Proporciona un método para ordenar el arreglo mediante el ordenamiento por inserción.

4 import java.util.Arrays;

5 import java.util.Random;

6 7 public class OrdenamientoInsercion

8 {

9 private int[] datos; // arreglo de valores

10 private static final Random generador = new Random();

1112 // crea un arreglo de un tamaño dado y lo llena con enteros aleatorios

13 public OrdenamientoInsercion( int tamanio )

14 {

15 datos = new int[ tamanio ]; // crea espacio para el arreglo

1617 // llena el arreglo con enteros aleatorios en el rango de 10 a 99

18 for ( int i = 0; i < tamanio; i++ )

19 datos[ i ] = 10 + generador.nextInt( 90 );

20 } // fin del constructor de OrdenamientoInsercion

2122 // ordena el arreglo usando el ordenamiento por inserción

23 public void sort()

24 {

25 int insercion; // variable temporal para contener el elemento a insertar

2627 // itera a través de datos.length - 1 elementos

28 for ( int siguiente = 1; siguiente < datos.length; siguiente++ )

29 {

30 // almacena el valor en el elemento actual

31 insercion = datos[ siguiente ];

3233 // inicializa ubicación para colocar el elemento

34 int moverElemento = siguiente;

3536 // busca un lugar para colocar el elemento actual

37 while ( moverElemento > 0 && datos[ moverElemento - 1 ] > insercion )

38 {

39 // desplaza el elemento una posición a la derecha

40 datos[ moverElemento ] = datos[ moverElemento - 1 ];

41 moverElemento--;

42 } // fin de while

4344 datos[ moverElemento ] = insercion; // coloca el elemento insertado

45 imprimirPasada( siguiente, moverElemento ); // imprime la pasada del algoritmo

46 } // fin de for

47 } // fin del método ordenar

4849 // imprime una pasada del algoritmo

50 public void imprimirPasada( int pasada, int indice )51 {

Fig. 19.8 � Clase que crea un arreglo lleno de enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por inserción (parte 1 de 2).

816 Capítulo 19 Búsqueda, ordenamiento y Big O

52 System.out.print( String.format( “despues de pasada %2d: ”, pasada ) );

53

54 // imprime los elementos hasta el elemento intercambiado

55 for ( int i = 0; i < indice; i++ )

56 System.out.print( datos[ i ] + “ ” );

57

58 System.out.print( datos[ indice ] + “* ” ); // indica intercambio

59

60 // termina de imprimir el arreglo en pantalla

61 for ( int i = indice + 1; i < datos.length; i++ )

62 System.out.print( datos[ i ] + “ ” );

63

64 System.out.print( “\n ” ); // para alineación

65

66 // indica la cantidad del arreglo que está ordenado

67 for( int i = 0; i <= pasada; i++ )

68 System.out.print( “-- ” );

69 System.out.println( “\n” ); // agrega fin de línea

70 } // fin del método imprimirPasada

71

72 // método para mostrar los valores del arreglo en pantalla

73 public String toString()

74 {

75 return Arrays.toString( datos );

76 } // fin del método toString

77 } // fin de la clase OrdenamientoInsercion

1 // Fig. 19.9: PruebaOrdenamientoInsercion.java

2 // Prueba la clase de ordenamiento por inserción.

3

4 public class PruebaOrdenamientoInsercion

5 {

6 public static void main( String[] args )

7 {

8 // crea objeto para realizar el ordenamiento por inserción

9 OrdenamientoInsercion arregloOrden = new OrdenamientoInsercion( 10 );

10

11 System.out.println( “Arreglo desordenado:” );

12 System.out.println( arregloOrden + “/n”); // imprime el arreglo desordenado

13

14 arregloOrden.sort(); // ordena el arreglo

15

16 System.out.println( “Arreglo ordenado:” );

17 System.out.println( arregloOrden ); // imprime el arreglo ordenado

18 } // fin de main

19 } // fin de la clase PruebaOrdenamientoInsercion

Fig. 19.8 � Clase que crea un arreglo lleno de enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por inserción (parte 2 de 2).

Fig. 19.9 � Prueba de la clase de ordenamiento por inserción (parte 1 de 2).

19.3 Algoritmos de ordenamiento 817

Eficiencia del ordenamiento por inserciónEl algoritmo de ordenamiento por inserción también se ejecuta en un tiempo igual a O(n2). Al igual que el ordenamiento por selección, la implementación del ordenamiento por inserción (líneas 23 a 47 de la figura 19.8) contiene dos ciclos. El ciclo for (líneas 28 a 46) itera datos.length – 1 veces, inser-tando un elemento en la posición apropiada en los elementos ordenados hasta ahora. Para los fines de esta aplicación, datos.length – 1 es equivalente a n – 1 (ya que datos.length es el tamaño del arreglo). El ciclo while (líneas 37 a 42) itera a través de los anteriores elementos en el arreglo. En el peor de los casos, el ciclo while requerirá n – 1 comparaciones. Cada ciclo individual se ejecuta en un tiempo O(n). En notación Big O, los ciclos anidados indican que debemos multiplicar el número de compara-ciones. Para cada iteración de un ciclo exterior, habrá cierto número de iteraciones en el ciclo interior. En este algoritmo, para cada O(n) iteraciones del ciclo exterior, habrá O(n) iteraciones del ciclo inte-rior. Al multiplicar estos valores se produce un valor Big O de O(n2).

19.3.3 Ordenamiento por combinaciónEl ordenamiento por combinación es un algoritmo de ordenamiento eficiente, pero en concepto es más complejo que los ordenamientos de selección y de inserción. Para ordenar un arreglo, el algoritmo de ordenamiento por combinación lo divide en dos subarreglos de igual tamaño, ordena cada sub-arreglo y después los combina en un arreglo más grande. Con un número impar de elementos, el algo-ritmo crea los dos subarreglos de tal forma que uno tenga más elementos que el otro.

Arreglo desordenado:[61, 96, 59, 33, 58, 22, 68, 86, 34, 85]despues de pasada 1: 61 96* 59 33 58 22 68 86 34 85 -- --

despues de pasada 2: 59* 61 96 33 58 22 68 86 34 85 -- -- --

despues de pasada 3: 33* 59 61 96 58 22 68 86 34 85 -- -- -- --

despues de pasada 4: 33 58* 59 61 96 22 68 86 34 85 -- -- -- -- --

despues de pasada 5: 22* 33 58 59 61 96 68 86 34 85 -- -- -- -- -- --

despues de pasada 6: 22 33 58 59 61 68* 96 86 34 85 -- -- -- -- -- -- --

despues de pasada 7: 22 33 58 59 61 68 86* 96 34 85 -- -- -- -- -- -- -- --

despues de pasada 8: 22 33 34* 58 59 61 68 86 96 85 -- -- -- -- -- -- -- -- --

despues de pasada 9: 22 33 34 58 59 61 68 85* 86 96 -- -- -- -- -- -- -- -- -- --

Arreglo ordenado:[22, 33, 34, 58, 59, 61, 68, 85, 86, 96]

Fig. 19.9 � Prueba de la clase de ordenamiento por inserción (parte 2 de 2).

818 Capítulo 19 Búsqueda, ordenamiento y Big O

La implementación del ordenamiento por combinación en este ejemplo es recursiva. El caso base es un arreglo con un elemento que, desde luego, está ordenado, por lo que el ordenamiento por com-binación regresa de inmediato en este caso. El paso recursivo divide el arreglo en dos piezas de un tamaño aproximadamente igual, las ordena en forma recursiva y después combina los dos arreglos ordenados en un arreglo ordenado de mayor tamaño.

Suponga que el algoritmo ya ha combinado arreglos más pequeños para crear los arreglos orde-nados A:

4 10 34 56 77

y B:

5 30 51 52 93

El ordenamiento por combinación combina estos dos arreglos en un arreglo ordenado de mayor tamaño. El elemento más pequeño en A es 4 (que se encuentra en el índice cero de A). El elemento más pequeño en B es 5 (que se encuentra en el índice cero de B). Para poder determinar el elemento más pequeño en el arreglo más grande, el algoritmo compara 4 y 5. El valor de A es más pequeño, por lo que el 4 se convier-te en el primer elemento del arreglo combinado. El algoritmo continúa, para lo cual compara 10 (el se-gundo elemento en A) con 5 (el primer elemento en B). El valor de B es más pequeño, por lo que 5 se convierte en el segundo elemento del arreglo más grande. El algoritmo continúa comparando 10 con 30, en donde 10 se convierte en el tercer elemento del arreglo, y así en lo sucesivo.

En las líneas 22 a 25 de la figura 19.10 se declara el método ordenar. En la línea 24 se hace una lla-mada al método ordenarArreglo con 0 y datos.length – 1 como los argumentos (que corresponden a los índices inicial y final, respectivamente, del arreglo que se ordenará). Estos valores indican al método ordenarArreglo que debe operar en todo el arreglo completo.

El método ordenarArreglo se declara en las líneas 28 a 49. En la línea 31 se evalúa el caso base. Si el tamaño del arreglo es 1, ya está ordenado, por lo que el método regresa de inmediato. Si el tamaño del arreglo es mayor que 1, el método divide el arreglo en dos, llama en forma recursiva al método ordenarArreglo para ordenar los dos subarreglos y después los combina. En la línea 43 se hace una llamada recursiva al método ordenarArreglo en la primera mitad del arreglo, y en la línea 44 se hace una llamada recursiva al método ordenarArreglo en la segunda mitad del mismo. Cuando regresan estas dos llamadas al método, cada mitad del arreglo se ha ordenado. En la línea 47 se hace una llamada al método combinar (líneas 52 a 91) con las dos mitades del arreglo, para combinar los dos arreglos or-denados en un arreglo ordenado más grande.

1 // Fig. 19.10: OrdenamientoCombinacion.java

2 // Clase que crea un arreglo lleno con enteros aleatorios.

3 // Proporciona un método para ordenar el arreglo mediante el ordenamiento por combinación.

4 import java.util.Random;

5

6 public class OrdenamientoCombinacion

7 {

8 private int[] datos; // arreglo de valores

9 private static final Random generador = new Random();

10

11 // crea un arreglo de un tamaño dado y lo llena con enteros aleatorios

12 public OrdenamientoCombinacion( int tamanio )

13 {

14 datos = new int[ tamanio ]; // crea espacio para el arreglo

15

Fig. 19.10 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por combinación (parte 1 de 3).

19.3 Algoritmos de ordenamiento 819

16 // llena el arreglo con enteros aleatorios en el rango de 10 a 9917 for ( int i = 0; i < tamanio; i++ )18 datos[ i ] = 10 + generador.nextInt( 90 );19 } // fin del constructor de OrdenamientoCombinacion20

21 // llama al método de división recursiva para comenzar el ordenamiento por combinación

22 public void ordenar()23 {24 ordenarArreglo( 0, datos.length - 1 ); // divide todo el arreglo25 } // fin del método ordenar2627 // divide el arreglo, ordena los subarreglos y los combina en un arreglo ordenado28 private void ordenarArreglo( int inferior, int superior ) 29 {30 // evalúa el caso base; el tamaño del arreglo es igual a 131 if ( ( superior - inferior ) >= 1 ) // si no es el caso base32 {

33 int medio1 = ( inferior + superior ) / 2; // calcula el elemento medio del arreglo

34 int medio2 = medio1 + 1; // calcula el siguiente elemento arriba3536 // imprime en pantalla el paso de división37 System.out.println( “division: ” + subarreglo( inferior, superior ) );38 System.out.println( “ ” + subarreglo( inferior, medio1 ) );39 System.out.println( “ ” + subarreglo( medio2, superior ) );40 System.out.println();4142 // divide el arreglo a la mitad; ordena cada mitad (llamadas recursivas)43 ordenarArreglo( inferior, medio1 ); // primera mitad del arreglo44 ordenarArreglo( medio2, superior ); // segunda mitad del arreglo45

46 // combina dos arreglos ordenados después de que regresan las llamadas de división

47 combinar ( inferior, medio1, medio2, superior );48 } // fin de if49 } // fin del método ordenarArreglo5051 // combina dos subarreglos ordenados en un subarreglo ordenado52 private void combinar( int izquierdo, int medio1, int medio2, int derecho ) 53 {54 int indiceIzq = izquierdo; // índice en subarreglo izquierdo55 int indiceDer = medio2; // índice en subarreglo derecho56 int indiceCombinado = izquierdo; // índice en arreglo de trabajo temporal57 int[] combinado = new int[ datos.length ]; // arreglo de trabajo5859 // imprime en pantalla los dos subarreglos antes de combinarlos60 System.out.println( “combinacion:” + subarreglo( izquierdo, medio1 ) );61 System.out.println( “ ” + subarreglo( medio2, derecho ) );6263 // combina los arreglos hasta llegar al final de uno de ellos64 while ( indiceIzq <= medio1 && indiceDer <= derecho )65 {66 // coloca el menor de dos elementos actuales en el resultado67 // y lo mueve al siguiente espacio en los arreglos

Fig. 19.10 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por combinación (parte 2 de 3).

820 Capítulo 19 Búsqueda, ordenamiento y Big O

En las líneas 64 a 72 en el método combinar se itera hasta que el programa llega al final de cualquie-ra de los subarreglos. En la línea 68 se evalúa cuál elemento al principio de los arreglos es más pequeño. Si el elemento en el arreglo izquierdo es más pequeño, en la línea 69 se coloca el elemento en su posición

68 if ( datos[ indiceIzq ] <= datos[ indiceDer ] )69 combinado[ indiceCombinado++ ] = datos[ indiceIzq++ ]; 70 else 71 combinado[ indiceCombinado++ ] = datos[ indiceDer++ ];72 } // fin de while7374 // si el arreglo izquierdo está vacío75 if ( indiceIzq == medio2 )76 // copia el resto del arreglo derecho77 while ( indiceDer <= derecho )78 combinado[ indiceCombinado++ ] = datos[ indiceDer++ ];79 else // el arreglo derecho está vacío80 // copia el resto del arreglo izquierdo81 while ( indiceIzq <= medio1 ) 82 combinado[ indiceCombinado++ ] = datos[ indiceIzq++ ]; 8384 // copia los valores de vuelta al arreglo original85 for ( int i = izquierdo; i <= derecho; i++ )86 datos[ i ] = combinado[ i ];8788 // imprime en pantalla el arreglo combinado89 System.out.println( “ ” + subarreglo( izquierdo, derecho ) );90 System.out.println();91 } // fin del método combinar9293 // método para imprimir en pantalla ciertos valores en el arreglo94 public String subarreglo( int inferior, int superior )95 {96 StringBuilder temporal = new StringBuilder();9798 // imprime en pantalla espacios para la alineación99 for ( int i = 0; i < inferior; i++ )100 temporal.append( “ ” );101102 // imprime en pantalla el resto de los elementos en el arreglo103 for ( int i = inferior; i <= superior; i++ )104 temporal.append( “ ” + datos[ i ] );105106 return temporal.toString();107 } // fin del método subarreglo108109 // método para imprimir los valores en el arreglo110 public String toString()111 {112 return subarreglo( 0, datos.length - 1 );113 } // fin del método toString 114 } // fin de la clase OrdenamientoCombinacion

Fig. 19.10 � Clase que crea un arreglo lleno con enteros aleatorios. Proporciona un método para ordenar el arreglo mediante el ordenamiento por combinación (parte 3 de 3).

19.3 Algoritmos de ordenamiento 821

en el arreglo combinado. Si el elemento en el arreglo derecho es más pequeño, en la línea 71 se coloca en su posición en el arreglo combinado. Cuando el ciclo while termina (línea 72), un subarreglo com-pleto se coloca en el arreglo combinado, pero el otro subarreglo aún contiene datos. En la línea 75 se evalúa si el arreglo izquierdo ha llegado al final. De ser así, en las líneas 77 y 78 se llena el arreglo com-binado con los elementos del arreglo derecho. Si el arreglo izquierdo no ha llegado al final, entonces el arreglo derecho debe haber llegado, por lo que en las líneas 81 y 82 se llena el arreglo combinado con los elementos del arreglo izquierdo. Por último, en las líneas 85 y 86 se copia el arreglo combinado en el arreglo original. En la figura 19.11 se crea y se utiliza un objeto OrdenamientoCombinado. Los resultados de este programa muestran las divisiones y combinaciones que realiza el ordenamiento por combina-ción, mostrando también el progreso del ordenamiento en cada paso del algoritmo. Bien vale la pena el tiempo que usted invierta al recorrer estos resultados paso a paso, para comprender por completo este elegante algoritmo de ordenamiento.

1 // Fig. 19.11: PruebaOrdenamientoCombinacion.java

2 // Prueba de la clase de ordenamiento por combinación.

3

4 public class PruebaOrdenamientoCombinacion

5 {

6 public static void main( String[] args )

7 {

8 // crea un objeto para realizar el ordenamiento por combinación

9 OrdenamientoCombinacion arregloOrden = new OrdenamientoCombinacion( 10 );

10

11 // imprime el arreglo desordenado

12 System.out.println( “Desordenado:” + arregloOrden + “\n” );

13

14 arregloOrden.ordenar(); // ordena el arreglo

15

16 // imprime el arreglo ordenado

17 System.out.println( “Ordenado: ” + arregloOrden );

18 } // fin de main

19 } // fin de la clase PruebaOrdenamientoCombinacion

Desordenado: 47 68 34 68 35 57 14 92 71 24

division: 47 68 34 68 35 57 14 92 71 24 47 68 34 68 35 57 14 92 71 24

division: 47 68 34 68 35 47 68 34 68 35

division: 47 68 34 47 68 34

division: 47 68 47 68

combinacion: 47 68 47 68

Fig. 19.11 � Prueba de la clase de ordenamiento por combinación (parte 1 de 2).

822 Capítulo 19 Búsqueda, ordenamiento y Big O

Eficiencia del ordenamiento por combinaciónEl ordenamiento por combinación es un algoritmo mucho más eficiente que el de inserción o el de selección. Considere la primera llamada (no recursiva) al método ordenarArreglo. Esto produce dos llamadas recursivas al método ordenarArreglo con subarreglos, cada uno de los cuales tiene un tamaño

combinacion: 47 68 34 34 47 68

division: 68 35 68 35

combinacion: 68 35 35 68

combinacion: 34 47 68 35 68 34 35 47 68 68

division: 57 14 92 71 24 57 14 92 71 24

division: 57 14 92 57 14 92

division: 57 14 57 14

combinacion: 57 14 14 57

combinacion: 14 57 92 14 57 92

division: 71 24 71 24

combinacion: 71 24 24 71

combinacion: 14 57 92 24 71 14 24 57 71 92

combinacion: 34 35 47 68 68 14 24 57 71 92 14 24 34 35 47 57 68 68 71 92

Ordenado: 14 24 34 35 47 57 68 68 71 92

Fig. 19.11 � Prueba de la clase de ordenamiento por combinación (parte 2 de 2).

19.3 Algoritmos de ordenamiento 823

aproximado de la mitad del arreglo original, y una sola llamada al método combinar. Esta llamada a combinar requiere, a lo más, n – 1 comparaciones para llenar el arreglo original, que es O(n). (Recuerde que se puede elegir cada elemento en el arreglo mediante la comparación de un elemento de cada uno de los subarreglos.) Las dos llamadas al método ordenarArreglo producen cuatro llamadas recursivas más al método ordenarArreglo, cada una con un subarreglo de un tamaño aproximado a una cuarta parte del tamaño del arreglo original, junto con dos llamadas al método combinar. Estas dos llamadas al método combinar requieren, a lo más, n/2 – 1 comparaciones para un número total de O(n) compa-raciones. Este proceso continúa, y cada llamada a ordenarArreglo genera dos llamadas adicionales a ordenarArreglo y una llamada a combinar, hasta que el algoritmo divide el arreglo en subarreglos de un elemento. En cada nivel, se requieren O(n) comparaciones para combinar los subarreglos. Cada nivel divide el tamaño de los arreglos a la mitad, por lo que al duplicar el tamaño del arreglo se requiere un nivel más. Si se cuadruplica el tamaño del arreglo, se requieren dos niveles más. Este patrón es logarít-mico, y produce log

2 n niveles. Esto resulta en una eficiencia total de O(n log n).

En la figura 19.12 se sintetizan muchos de los algoritmos de búsqueda y ordenamiento que cu-brimos en este libro, junto con el valor de Big O para cada uno de ellos. En la figura 19.13 se enlistan los valores de Big O que hemos cubierto en este capítulo, junto con cierto número de valores para n, de modo que se resalten las diferencias en las proporciones de crecimiento.

Fig. 19.12 � Algoritmos de búsqueda y ordenamiento con valores de Big O.

Algoritmo Ubicación Big O

Algoritmos de búsqueda:

Búsqueda lineal Sección 19.2.1 O(n)

Búsqueda binaria Sección 19.2.2 O(log n)

Búsqueda lineal recursiva Ejercicio 19.8 O(n)

Búsqueda binaria recursiva Ejercicio 19.9 O(log n)

Algoritmos de ordenamiento:

Ordenamiento por selección Sección 19.3.1 O(n2)

Ordenamiento por inserción Sección 19.3.2 O(n2)

Ordenamiento por combinación Ejercicio 19.3.3 O(n log n)

Ordenamiento de burbuja Ejercicios 19.5 y 19.6 O(n2)

Fig. 19.13 � Número de comparaciones para las notaciones comunes de Big O.

n = O(log n) O(n) O(n log n) O(n2)

1 0 1 0 1

2 1 2 2 4

3 1 3 3 9

4 1 4 4 16

5 1 5 5 25

10 1 10 10 100

100 2 100 200 10,000

1000 3 1000 3000 106

1,000,000 6 1,000,000 6,000,000 1012

1,000,000,000 9 1,000,000,000 9,000,000,000 1018

824 Capítulo 19 Búsqueda, ordenamiento y Big O

19.4 ConclusiónEn este capítulo se presentaron las técnicas de ordenamiento y búsqueda. Hablamos sobre dos algorit-mos de búsqueda (búsqueda lineal y búsqueda binaria) y tres algoritmos de ordenamiento (ordenamien-to por selección, por inserción y por combinación). Presentamos la notación Big O, la cual nos ayuda a analizar la eficiencia de un algoritmo. En los siguientes tres capítulos continuaremos nuestro análisis sobre las estructuras de datos dinámicas, que pueden aumentar o reducir su tamaño en tiempo de ejecu-ción. En el capítulo 20 presentaremos los algoritmos integrados de la API de Java (como los de búsqueda y ordenamiento) y las colecciones de genéricos. En el capítulo 21 demostraremos cómo usar las herra-mientas de genéricos de Java para implementar métodos genéricos y clases genéricas. En el capítulo 22 veremos los detalles sobre la implementación de estructuras de datos genéricas.

ResumenSección 19.1 Introducción• La búsqueda (pág. 799) implica determinar si una clave de búsqueda está presente en los datos y, de ser así, encontrar

su ubicación.

• El ordenamiento (pág. 799) implica poner los datos en orden.

Sección 19.2 Algoritmos de búsqueda• El algoritmo de búsqueda lineal (pág. 800) busca cada elemento en el arreglo en forma secuencial, hasta que encuen-

tra el elemento correcto, o hasta llegar al fi nal del mismo sin encontrar ese elemento.

• Una de las principales diferencias entre los algoritmos de búsqueda es la cantidad de esfuerzo que requieren para poder devolver un resultado.

• La notación Big O (pág. 802) describe la efi ciencia de un algoritmo en términos del trabajo requerido para resolver un problema. Para los algoritmos de búsqueda y ordenamiento, por lo general depende del número de elementos en los datos.

• Un algoritmo que es O(1) no necesariamente requiere sólo una comparación (pág. 803). Sólo signifi ca que el número de comparaciones no aumenta a medida que se incrementa el tamaño del arreglo.

• Se considera que un algoritmo O(n) tiene un tiempo de ejecución lineal (pág. 803).

• La notación Big O está diseñada para resaltar los factores dominantes, e ignorar los términos que pierden importancia con valores altos de n.

• La notación Big O se enfoca en la proporción de crecimiento de los tiempos de ejecución de los algoritmos, por lo que se ignoran las constantes.

• El algoritmo de búsqueda lineal se ejecuta en un tiempo O(n).

• El peor caso en la búsqueda lineal es que se debe comprobar cada elemento para determinar si el elemento de búsqueda existe. Esto ocurre si la clave de búsqueda (pág. 799) es el último elemento en el arreglo, o si no está presente.

• El algoritmo de búsqueda binaria (pág. 804) es más efi ciente que el algoritmo de búsqueda lineal, pero requiere que el arreglo esté ordenado.

• La primera iteración de la búsqueda binaria evalúa el elemento medio del arreglo. Si es igual a la clave de búsqueda, el algoritmo devuelve su ubicación. Si la clave de búsqueda es menor que el elemento medio, la búsqueda continúa con la primera mitad del arreglo. Si la clave de búsqueda es mayor que el elemento medio, la búsqueda binaria continúa con la segunda mitad del arreglo. En cada iteración se evalúa el valor medio del resto del arreglo y, si no se encuentra el elemento, se elimina la mitad de los elementos restantes.

• La búsqueda binaria es un algoritmo de búsqueda más efi ciente que la búsqueda lineal, ya que cada comparación eli-mina la mitad de los elementos del arreglo a considerar.

• La búsqueda binaria se ejecuta en un tiempo O(log n), ya que cada paso elimina la mitad de los elementos restantes; esto también se conoce como tiempo de ejecución logarítmico (pág. 809).

• Si el tamaño del arreglo se duplica, la búsqueda binaria sólo requiere una comparación adicional.

Sección 19.3 Algoritmos de ordenamiento• El ordenamiento por selección (pág. 810) es un algoritmo de ordenamiento simple, pero inefi ciente.

• El algoritmo por selección empieza seleccionando el elemento más pequeño en el arreglo y lo intercambia con el pri-mer elemento. En la segunda iteración se selecciona el segundo elemento más pequeño (que viene siendo el elemento restante más pequeño) y se intercambia con el segundo elemento. El ordenamiento por selección continúa hasta que en la última iteración se selecciona el segundo elemento más grande, y se intercambia con el antepenúltimo elemen-to, dejando el elemento más grande en el último índice. En la i-ésima iteración del ordenamiento por selección, los i elementos más pequeños de todo el arreglo se ordenan en los primeros i índices.

• El algoritmo de ordenamiento por selección se ejecuta en un tiempo O(n2) (pág. 803).

• En la primera iteración del ordenamiento por inserción (pág. 814), se toma el segundo elemento en el arreglo y, si es menor que el primer elemento, se intercambian. En la segunda iteración se analiza el tercer elemento y se inserta en la posición correcta, con respecto a los primeros dos elementos. Después de la i-ésima iteración del ordenamiento por inserción, quedan ordenados los primeros i elementos del arreglo original.

• El algoritmo de ordenamiento por inserción se ejecuta en un tiempo O(n2).

• El ordenamiento por combinación (pág. 817) es un algoritmo de ordenamiento que es más rápido, pero más complejo de implementar, que el ordenamiento por selección y el ordenamiento por inserción. Para ordenar un arreglo, el algoritmo de ordenamiento por combinación lo divide en dos subarreglos de igual tamaño, ordena cada subarreglo en forma recursiva y combina los subarreglos en un arreglo más grande.

• El caso base del ordenamiento por combinación es un arreglo con un elemento. Un arreglo de un elemento ya está ordenado, por lo que el ordenamiento por combinación regresa de inmediato, cuando se llama con un arreglo de un elemento. La parte de este algoritmo que corresponde al proceso de combinar recibe dos arreglos ordenados y los combina en un arreglo ordenado más grande.

• Para realizar la combinación, el ordenamiento por combinación analiza el primer elemento en cada arreglo, que también es el elemento más pequeño en el arreglo. El ordenamiento por combinación recibe el más pequeño de estos elementos y lo coloca en el primer elemento del arreglo más grande. Si aún hay elementos en el subarreglo, el ordenamiento por combinación analiza el segundo elemento en el subarreglo (que ahora es el elemento más pequeño restante) y lo compara con el primer elemento en el otro subarreglo. El ordenamiento por combinación continúa con este proceso, hasta que se llena el arreglo más grande.

• En el peor caso, la primera llamada al ordenamiento por combinación tiene que realizar O(n) comparaciones para llenar las n posiciones en el arreglo fi nal.

• La porción del algoritmo de ordenamiento por combinación que corresponde al proceso de combinar se realiza en dos subarreglos, cada uno de un tamaño aproximado a n/2. Para crear cada uno de estos subarreglos, se requieren n/2 – 1 comparaciones para cada subarreglo, o un total de O(n) comparaciones. Este patrón continúa a medida que cada nivel trabaja hasta en el doble de esa cantidad de arreglos, pero cada uno equivale a la mitad del tamaño del arreglo anterior.

• De manera similar a la búsqueda binaria, esta acción de partir los subarreglos a la mitad produce un total de log n niveles, para una efi ciencia total de O(n log n) (pág. 823).

Ejercicios de autoevaluación19.1 Complete los siguientes enunciados:

a) Una aplicación de ordenamiento por selección debe requerir un tiempo aproximado de veces más para ejecutarse en un arreglo de 128 elementos, en comparación con un arreglo de 32 elementos.

b) La eficiencia del ordenamiento por combinación es de .

19.2 ¿Qué aspecto clave de la búsqueda binaria y del ordenamiento por combinación es responsable de la parte loga-rítmica de sus respectivos valores Big O?

19.3 ¿En qué sentido es superior el ordenamiento por inserción al ordenamiento por combinación? ¿En qué sentido es superior el ordenamiento por combinación al ordenamiento por inserción?

19.4 En el texto decimos que, una vez que el ordenamiento por combinación divide el arreglo en dos subarreglos, después ordena estos dos subarreglos y los combina. ¿Por qué alguien podría quedar desconcertado al decir nosotros que “después ordena estos dos subarreglos”?

Ejercicios de autoevaluación 825

Respuestas a los ejercicios de autoevaluación19.1 a) 16, ya que un algoritmo O(n2) requiere 16 veces más de tiempo para ordenar hasta cuatro veces más infor-mación. b) O(n log n).

19.2 Ambos algoritmos incorporan la acción de “dividir a la mitad” (reducir algo de cierta forma a la mitad). La bús-queda binaria elimina del proceso una mitad del arreglo después de cada comparación. El ordenamiento por combina-ción divide el arreglo a la mitad, cada vez que se llama.

19.3 El ordenamiento por inserción es más fácil de comprender y de programar que el ordenamiento por com-binación. El ordenamiento por combinación es mucho más eficiente [O(n log n)] que el ordenamiento por inserción [O(n2)].

19.4 En cierto sentido, en realidad no ordena estos dos subarreglos. Simplemente sigue dividiendo el arreglo original a la mitad, hasta que obtiene un subarreglo de un elemento, que desde luego, está ordenado. Después construye los dos subarreglos originales al combinar estos arreglos de un elemento para formar subarreglos más grandes, los cuales se mez-clan, y así en lo sucesivo.

Ejercicios19.5 (Ordenamiento de burbuja) Implemente el ordenamiento de burbuja, otra técnica de ordenamiento simple, pero ineficiente. Se le llama ordenamiento de burbuja o de hundimiento, debido a que los valores más pequeños van “subiendo como burbujas” en forma gradual, hasta llegar a la parte superior del arreglo (es decir, hacia el primer elemento) como las burbujas de aire que se elevan en el agua, mientras que los valores más grandes se hunden en el fondo (final) del arreglo. Esta técnica utiliza ciclos anidados para realizar varias pasadas a través del arreglo. Cada pasada compara pares sucesivos de elementos. Si un par se encuentra en orden ascendente (o los valores son iguales), el ordenamiento de burbuja deja los va-lores como están. Si un par se encuentra en orden descendente, el ordenamiento de burbuja intercambia sus valores en el arreglo. En la primera pasada se comparan los primeros dos elementos del arreglo, y se intercambian sus valores si es nece-sario. Después se comparan los elementos segundo y tercero en el arreglo. Al final de esta pasada, se comparan los últimos dos elementos en el arreglo y se intercambian, en caso de ser necesario. Después de una pasada, el elemento más grande estará en el último índice. Después de dos pasadas, los dos elementos más grandes se encontrarán en los últimos dos índices. Explique por qué el ordenamiento de burbuja es un algoritmo O(n2).

19.6 (Ordenamiento de burbuja mejorado) Realice las siguientes modificaciones simples para mejorar el rendimien-to del ordenamiento de burbuja que desarrolló en el ejercicio 16.5:

a) Después de la primera pasada, se garantiza que el número más grande estará en el elemento con la numera-ción más alta del arreglo; después de la segunda pasada, los dos números más altos estarán “acomodados”; y así en lo sucesivo. En vez de realizar nueve comparaciones en cada pasada para un elemento con diez arreglos, modifique el ordenamiento de burbuja para que realice ocho comparaciones en la segunda pasada, siete en la tercera, y así en lo sucesivo.

b) Los datos en el arreglo tal vez se encuentren ya en el orden apropiado, o casi apropiado, así que ¿para qué realizar nueve pasadas, si basta con menos? Modifique el ordenamiento para comprobar al final de cada pa-sada si se han realizado intercambios. Si no se ha realizado ninguno, los datos ya deben estar en el orden apropiado, por lo que el programa debe terminar. Si se han realizado intercambios, por lo menos se necesita una pasada más.

19.7 (Ordenamiento de cubeta) Un ordenamiento de cubeta comienza con un arreglo unidimensional de enteros positivos que se deben ordenar, y un arreglo bidimensional de enteros, en el que las filas están indexadas de 0 a 9 y las columnas de 0 a n – 1, en donde n es el número de valores a ordenar. Cada fila del arreglo bidimensional se conoce como una cubeta. Escriba una clase llamada OrdenamientoCubeta, que contenga un método llamado ordenar y que opere de la siguiente manera:

a) Coloque cada valor del arreglo unidimensional en una fila del arreglo de cubeta, con base en el dígito de las unidades (el de más a la derecha) del valor. Por ejemplo, el número 97 se coloca en la fila 7, el 3 se coloca en la fila 3 y el 100 se coloca en la fila 0. A este procedimiento se le llama pasada de distribución.

b) Itere a través del arreglo de cubeta fila por fila, y copie los valores de vuelta al arreglo original. A este pro-cedimiento se le llama pasada de recopilación. El nuevo orden de los valores anteriores en el arreglo unidimen-sional es 100, 3 y 97.

826 Capítulo 19 Búsqueda, ordenamiento y Big O

c) Repita este proceso para cada posición de dígito subsiguiente (decenas, centenas, miles, etcétera). En la se-gunda pasada (el dígito de las decenas) se coloca el 100 en la fila 0, el 3 en la fila 0 (ya que 3 no tiene dígito de decenas) y el 97 en la fila 9. Después de la pasada de recopilación, el orden de los valores en el arreglo unidimensional es 100, 3 y 97. En la tercera pasada (dígito de las centenas), el 100 se coloca en la fila 1, el 3 en la fila 0 y el 97 en la fila 0 (después del 3). Después de esta última pasada de recopilación, el arreglo ori-ginal se encuentra en orden.

El arreglo bidimensional de cubetas es 10 veces la longitud del arreglo entero que se está ordenando. Esta técnica de ordenamiento proporciona un mejor rendimiento que el ordenamiento de burbuja, pero requiere mucha más memoria; el ordenamiento de burbuja requiere espacio sólo para un elemento adicional de datos. Esta comparación es un ejemplo de la concesión entre espacio y tiempo: el ordenamiento de cubeta utiliza más memoria que el ordenamiento de burbuja, pero su rendimiento es mejor. Esta versión del ordenamiento de cubeta requiere copiar todos los datos de vuelta al arreglo original en cada pasada. Otra posibilidad es crear un segundo arreglo de cubeta bidimensional, e intercambiar en forma repetida los datos entre los dos arreglos de cubeta.

19.8 (Búsqueda lineal recursiva) Modifique la figura 19.2 de modo que se utilice el método recursivo busqueda-LinealRecursiva para realizar una búsqueda lineal en el arreglo. El método debe recibir la clave de búsqueda y el índice inicial como argumentos. Si se encuentra la clave de búsqueda, se devuelve su índice en el arreglo; en caso contrario, se devuelve -1. Cada llamada al método recursivo debe comprobar un índice en el arreglo.

19.9 (Búsqueda binaria recursiva) Modifique la figura 19.4 de modo que se utilice el método recursivo busqueda-BinariaRecursiva para realizar una búsqueda binaria en el arreglo. El método debe recibir la clave de búsqueda, el ín-dice inicial y el índice final como argumentos. Si se encuentra la clave de búsqueda, se devuelve su índice en el arreglo. Si no se encuentra, se devuelve -1.

19.10 (Quicksort) La técnica de ordenamiento recursiva llamada “quicksort” utiliza el siguiente algoritmo básico para un arreglo unidimensional de valores:

a) Paso de particionamiento: tomar el primer elemento del arreglo desordenado y determinar su ubicación final en el arreglo ordenado (es decir, todos los valores a la izquierda del elemento en el arreglo son menores que el elemento, y todos los valores a la derecha del elemento en el arreglo son mayores; a continuación le mostrare-mos cómo hacer esto). Ahora tenemos un elemento en su ubicación apropiada y dos subarreglos desordenados.

b) Paso recursivo: llevar a cabo el paso 1 en cada subarreglo desordenado. Cada vez que se realiza el paso 1 en un subarreglo, se coloca otro elemento en su ubicación final en el arreglo ordenado, y se crean dos subarreglos desordenados. Cuando un subarreglo consiste en un elemento, ese elemento se encuentra en su ubicación final (debido a que un arreglo de un elemento ya está ordenado).

El algoritmo básico parece bastante simple, pero ¿cómo determinamos la posición fi nal del primer elemento de cada subarreglo? Como ejemplo, considere el siguiente conjunto de valores (el elemento en negritas es el elemento de particionamiento; se colocará en su ubicación fi nal en el arreglo ordenado):

37 2 6 4 89 8 10 12 68 45

Empezando desde el elemento de más a la derecha del arreglo, se compara cada elemento con 37 hasta que se encuentra un elemento menor que 37; después se intercambian el 37 y ese elemento. El primer elemento menor que 37 es 12, por lo que se intercambian el 37 y el 12. El nuevo arreglo es

12 2 6 4 89 8 10 37 68 45

El elemento 12 está en cursivas, para indicar que se acaba de intercambiar con el 37. Empezando desde la parte izquierda del arreglo, pero con el elemento que está después de 12, com-

pare cada elemento con 37 hasta encontrar un elemento mayor que 37; después intercambie el 37 y ese elemento. El primer elemento mayor que 37 es 89, por lo que se intercambian el 37 y el 89. El nuevo arreglo es

12 2 6 4 37 8 10 89 68 45

Ejercicios 827

Empezando desde la derecha, pero con el elemento antes del 89, compare cada elemento con 37 hasta encontrar un elemento menor que 37; después se intercambian el 37 y ese elemento. El primer elemento menor que 37 es 10, por lo que se intercambian 37 y 10. El nuevo arreglo es

12 2 6 4 10 8 37 89 68 45

Empezando desde la izquierda, pero con el elemento que está después de 10, compare cada elemento con 37 hasta encontrar un elemento mayor que 37; después intercambie el 37 y ese elemento. No hay más elementos mayores que 37, por lo que al comparar el 37 consigo mismo, sabemos que se ha colocado en su ubicación fi nal en el arreglo ordenado. Cada valor a la izquierda de 37 es más pequeño, y cada valor a la derecha de 37 es más grande.

Una vez que se ha aplicado la partición en el arreglo anterior, hay dos subarreglos desordenados. El subarreglo con valores menores que 37 contiene 12, 2, 6, 4, 10 y 8. El subarreglo con valores mayores que 37 contiene 89, 68 y 45. El ordenamiento continúa en forma recursiva, y ambos subarreglos se parti-cionan de la misma forma que el arreglo original.

Con base en la discusión anterior, escriba el método recursivo ayudanteQuicksort para ordenar un arre-glo entero unidimensional. El método debe recibir como argumentos un índice inicial y un índice fi nal en el arreglo original que se está ordenando.

828 Capítulo 19 Búsqueda, ordenamiento y Big O